Skip to content

Commit 22945c2

Browse files
committed
refactor: streamline evaluator and validator logic to require only GEMINI_API_KEY; remove OpenAI fallback
feat: enhance evaluation process by integrating full video input for Gemini test: update tests to reflect changes in evaluation logic and ensure compatibility with new video input chore: update dependencies in uv.lock
1 parent 54030f9 commit 22945c2

6 files changed

Lines changed: 173 additions & 200 deletions

File tree

env.example

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
# Centralized API URL (validators use for GET /tasks/latest, GET /weights, POST evaluations)
99
API_URL=https://api.leoma.ai
1010

11-
# Validator evaluator LLM keys. At least one must be set.
12-
# - GEMINI_API_KEY (preferred, cheaper): gemini-3.1-flash-lite-preview
13-
# - OPENAI_API_KEY (fallback): gpt-4o
14-
# If both are set, Gemini is used first and GPT-4o is the per-request fallback
15-
# when Gemini fails.
11+
# Validator evaluator LLM key. REQUIRED for validators.
12+
# - GEMINI_API_KEY: gemini-3.1-flash-lite-preview. The evaluator sends the full
13+
# generated video to Gemini (no GPT-4o fallback).
1614
# Gemini key: https://aistudio.google.com/app/apikey
17-
# OpenAI key: https://platform.openai.com/api-keys
1815
GEMINI_API_KEY=
16+
# OPENAI_API_KEY: NOT used by validators. Required only by the owner-sampler
17+
# (gpt-4o benchmark prompt/description generation). See OWNER-SAMPLER section.
1918
OPENAI_API_KEY=
2019

2120
# ═══════════════════════════════════════════════════════════════════════════════

leoma/app/evaluator/main.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@
1818
from typing import Set
1919

2020
from google import genai
21-
from openai import AsyncOpenAI
2221

2322
from leoma.bootstrap import (
2423
GEMINI_API_KEY,
2524
OBJECT_STORAGE_BACKEND,
26-
OPENAI_API_KEY,
2725
SAMPLES_BUCKET,
2826
)
2927
from leoma.bootstrap import emit_log as log, emit_header as log_header, log_exception
@@ -32,14 +30,12 @@
3230
list_evaluated_task_ids,
3331
download_task_artifacts,
3432
)
35-
from leoma.infra.video_utils import extract_frames, frames_to_base64
33+
from leoma.infra.video_utils import frames_to_base64
3634
from leoma.infra.judge import evaluate_generated_video_async
3735

3836
EVALUATOR_POLL_INTERVAL = int(os.environ.get("EVALUATOR_POLL_INTERVAL", "60"))
3937
EVALUATED_LIST_MAX = 100
4038
API_URL = os.environ.get("API_URL", "https://api.leoma.ai")
41-
EVALUATION_MAX_FRAMES = int(os.environ.get("EVALUATION_MAX_FRAMES", "12"))
42-
EVALUATION_FRAME_FPS = float(os.environ.get("EVALUATION_FRAME_FPS", "3"))
4339
EVALUATION_PASS_THRESHOLD = int(os.environ.get("EVALUATION_PASS_THRESHOLD", "75"))
4440
EVALUATION_CRITICAL_THRESHOLD = int(os.environ.get("EVALUATION_CRITICAL_THRESHOLD", "50"))
4541

@@ -72,18 +68,11 @@ async def run_evaluator_loop() -> None:
7268
from leoma.bootstrap import WALLET_NAME, HOTKEY_NAME
7369

7470
gemini_key = GEMINI_API_KEY or os.environ.get("GEMINI_API_KEY")
75-
openai_key = OPENAI_API_KEY or os.environ.get("OPENAI_API_KEY")
76-
if not gemini_key and not openai_key:
77-
log("Evaluator requires GEMINI_API_KEY and/or OPENAI_API_KEY (fallback)", "error")
71+
if not gemini_key:
72+
log("Evaluator requires GEMINI_API_KEY", "error")
7873
return
79-
gemini_client = genai.Client(api_key=gemini_key) if gemini_key else None
80-
openai_client = AsyncOpenAI(api_key=openai_key) if openai_key else None
81-
if gemini_client and openai_client:
82-
log("Evaluator LLM: Gemini primary, GPT-4o fallback", "info")
83-
elif gemini_client:
84-
log("Evaluator LLM: Gemini only (no GPT-4o fallback configured)", "info")
85-
else:
86-
log("Evaluator LLM: GPT-4o only (GEMINI_API_KEY not set)", "info")
74+
gemini_client = genai.Client(api_key=gemini_key)
75+
log("Evaluator LLM: Gemini only (full-video evaluation)", "info")
8776
try:
8877
s3_client = create_samples_read_client()
8978
except ValueError as e:
@@ -182,22 +171,13 @@ async def run_evaluator_loop() -> None:
182171
samples_payload: list = []
183172
eval_entries: list = []
184173
for miner_hotkey, gen_video_path in generated_videos.items():
185-
gen_frames_dir = os.path.join(dest_dir, f"gen_frames_{miner_hotkey[:8]}")
186174
try:
187175
latency_ms = miner_latencies_ms.get(miner_hotkey)
188-
gen_frames = await extract_frames(
189-
gen_video_path,
190-
gen_frames_dir,
191-
max_frames=EVALUATION_MAX_FRAMES,
192-
fps=EVALUATION_FRAME_FPS,
193-
)
194-
gen_frames_b64 = frames_to_base64(gen_frames)
195176
comparison = await evaluate_generated_video_async(
196177
first_frame_b64,
197-
gen_frames_b64,
178+
gen_video_path,
198179
description,
199180
gemini_client=gemini_client,
200-
openai_client=openai_client,
201181
pass_threshold=EVALUATION_PASS_THRESHOLD,
202182
critical_threshold=EVALUATION_CRITICAL_THRESHOLD,
203183
)
@@ -248,9 +228,6 @@ async def run_evaluator_loop() -> None:
248228
log(f"Miner {miner_hotkey[:12]}...: {'PASSED' if passed else 'FAILED'}", "info")
249229
except Exception as e:
250230
log(f"Miner {miner_hotkey[:12]}... evaluation failed: {e}", "error")
251-
finally:
252-
if os.path.exists(gen_frames_dir):
253-
_remove_directory(gen_frames_dir)
254231

255232
if samples_payload:
256233
signature = api_client.sign_evaluation_payload(eval_entries)

leoma/app/validator/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
EPOCH_LEN,
1919
OBJECT_STORAGE_BACKEND,
2020
GEMINI_API_KEY,
21-
OPENAI_API_KEY,
2221
WALLET_NAME,
2322
HOTKEY_NAME,
2423
NETWORK,
@@ -113,8 +112,8 @@ async def main() -> None:
113112
"""Main entry point: run evaluator (background) + weight-setting loop."""
114113
log_header("Leoma Validator Starting (evaluator + weight-setter)")
115114

116-
if not GEMINI_API_KEY and not OPENAI_API_KEY:
117-
log("Evaluator requires GEMINI_API_KEY and/or OPENAI_API_KEY (fallback)", "error")
115+
if not GEMINI_API_KEY:
116+
log("Evaluator requires GEMINI_API_KEY", "error")
118117
return
119118

120119
log(f"Using centralized API: {API_URL}", "info")

leoma/infra/judge.py

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import base64
77
import json
8+
import os
89
from typing import Any, Dict, List
910
from leoma.bootstrap import emit_log as log
1011
from openai import AsyncOpenAI
@@ -15,6 +16,17 @@
1516
GEMINI_EVAL_MAX_ATTEMPTS = 3
1617
GEMINI_EVAL_RETRY_SLEEP_S = 300
1718

19+
GEMINI_EVAL_VIDEO_FPS = float(os.environ.get("EVALUATION_VIDEO_FPS", "16"))
20+
GEMINI_EVAL_MEDIA_RESOLUTION = os.environ.get(
21+
"EVALUATION_MEDIA_RESOLUTION", "high"
22+
).strip().lower()
23+
24+
_MEDIA_RESOLUTION_MAP = {
25+
"low": genai_types.MediaResolution.MEDIA_RESOLUTION_LOW,
26+
"medium": genai_types.MediaResolution.MEDIA_RESOLUTION_MEDIUM,
27+
"high": genai_types.MediaResolution.MEDIA_RESOLUTION_HIGH,
28+
}
29+
1830
DESCRIPTION_PROMPT = """You are writing a benchmark prompt for first-frame-conditioned video generation.
1931
2032
Use the provided sequential frames from a 5-second one-shot clip.
@@ -162,7 +174,7 @@ def _build_eval_instructions(prompt: str) -> str:
162174
Given:
163175
1) A conditioning first frame
164176
2) The benchmark generation prompt
165-
3) Sequential frames from a generated video
177+
3) The full generated video
166178
167179
Benchmark prompt:
168180
"{prompt}"
@@ -211,19 +223,29 @@ def _build_eval_instructions(prompt: str) -> str:
211223
)
212224

213225

226+
def _video_to_gemini_part(video_path: str) -> genai_types.Part:
227+
"""Inline the full generated video, with explicit fps so Gemini samples it
228+
densely instead of its 1 fps default."""
229+
with open(video_path, "rb") as f:
230+
data = f.read()
231+
return genai_types.Part(
232+
inline_data=genai_types.Blob(data=data, mime_type="video/mp4"),
233+
video_metadata=genai_types.VideoMetadata(fps=GEMINI_EVAL_VIDEO_FPS),
234+
)
235+
236+
214237
async def _evaluate_via_gemini(
215238
gemini_client: genai.Client,
216239
first_frame: List[Dict[str, Any]],
217-
generated_frames: List[Dict[str, Any]],
240+
generated_video_path: str,
218241
prompt: str,
219242
) -> str:
220243
first_frame_parts = [_frame_dict_to_gemini_part(f) for f in first_frame]
221-
gen_frame_parts = [_frame_dict_to_gemini_part(f) for f in generated_frames]
222244

223245
contents: List[Any] = [_build_eval_instructions(prompt), "CONDITIONING FIRST FRAME:"]
224246
contents.extend(first_frame_parts)
225-
contents.append("GENERATED VIDEO FRAMES (chronological):")
226-
contents.extend(gen_frame_parts)
247+
contents.append("GENERATED VIDEO:")
248+
contents.append(_video_to_gemini_part(generated_video_path))
227249

228250
response = await gemini_client.aio.models.generate_content(
229251
model=GEMINI_EVAL_MODEL,
@@ -233,79 +255,48 @@ async def _evaluate_via_gemini(
233255
temperature=0.1,
234256
max_output_tokens=450,
235257
response_mime_type="application/json",
258+
media_resolution=_MEDIA_RESOLUTION_MAP.get(
259+
GEMINI_EVAL_MEDIA_RESOLUTION,
260+
genai_types.MediaResolution.MEDIA_RESOLUTION_HIGH,
261+
),
236262
),
237263
)
238264
return response.text or ""
239265

240266

241-
async def _evaluate_via_openai(
242-
openai_client: AsyncOpenAI,
243-
first_frame: List[Dict[str, Any]],
244-
generated_frames: List[Dict[str, Any]],
245-
prompt: str,
246-
) -> str:
247-
content = [
248-
{"type": "text", "text": _build_eval_instructions(prompt)},
249-
{"type": "text", "text": "CONDITIONING FIRST FRAME:"},
250-
] + first_frame + [
251-
{"type": "text", "text": "GENERATED VIDEO FRAMES (chronological):"},
252-
] + generated_frames
253-
254-
response = await openai_client.chat.completions.create(
255-
model="gpt-4o",
256-
messages=[
257-
{"role": "system", "content": EVAL_SYSTEM_MSG},
258-
{"role": "user", "content": content},
259-
],
260-
max_tokens=450,
261-
)
262-
return response.choices[0].message.content or ""
263-
264-
265267
async def evaluate_generated_video_async(
266268
first_frame: List[Dict[str, Any]],
267-
generated_frames: List[Dict[str, Any]],
269+
generated_video_path: str,
268270
prompt: str,
269271
*,
270272
gemini_client: genai.Client | None = None,
271-
openai_client: AsyncOpenAI | None = None,
272273
pass_threshold: int = 70,
273274
critical_threshold: int = 50,
274275
) -> Dict[str, Any]:
275-
if gemini_client is None and openai_client is None:
276-
raise ValueError("evaluate_generated_video_async requires gemini_client or openai_client")
276+
if gemini_client is None:
277+
raise ValueError("evaluate_generated_video_async requires gemini_client")
277278

278279
raw: str | None = None
279-
if gemini_client is not None:
280-
last_error: Exception | None = None
281-
for attempt in range(1, GEMINI_EVAL_MAX_ATTEMPTS + 1):
282-
try:
283-
raw = await _evaluate_via_gemini(
284-
gemini_client, first_frame, generated_frames, prompt
285-
)
286-
break
287-
except Exception as e:
288-
last_error = e
289-
log(
290-
f"Gemini evaluation attempt {attempt}/{GEMINI_EVAL_MAX_ATTEMPTS} "
291-
f"failed: {e}",
292-
"warn",
293-
)
294-
if attempt < GEMINI_EVAL_MAX_ATTEMPTS:
295-
await asyncio.sleep(GEMINI_EVAL_RETRY_SLEEP_S)
296-
297-
if raw is None:
298-
if openai_client is None:
299-
assert last_error is not None
300-
raise last_error
280+
last_error: Exception | None = None
281+
for attempt in range(1, GEMINI_EVAL_MAX_ATTEMPTS + 1):
282+
try:
283+
raw = await _evaluate_via_gemini(
284+
gemini_client, first_frame, generated_video_path, prompt
285+
)
286+
break
287+
except Exception as e:
288+
last_error = e
301289
log(
302-
f"Gemini evaluation failed after {GEMINI_EVAL_MAX_ATTEMPTS} attempts; "
303-
"falling back to GPT-4o",
290+
f"Gemini evaluation attempt {attempt}/{GEMINI_EVAL_MAX_ATTEMPTS} "
291+
f"failed: {e}",
304292
"warn",
305293
)
294+
if attempt < GEMINI_EVAL_MAX_ATTEMPTS:
295+
await asyncio.sleep(GEMINI_EVAL_RETRY_SLEEP_S)
306296

307297
if raw is None:
308-
raw = await _evaluate_via_openai(openai_client, first_frame, generated_frames, prompt)
298+
assert last_error is not None
299+
raise last_error
309300

310301
text = _strip_json_fence(raw)
311302
try:

0 commit comments

Comments
 (0)