55import asyncio
66import base64
77import json
8+ import os
89from typing import Any , Dict , List
910from leoma .bootstrap import emit_log as log
1011from openai import AsyncOpenAI
1516GEMINI_EVAL_MAX_ATTEMPTS = 3
1617GEMINI_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+
1830DESCRIPTION_PROMPT = """You are writing a benchmark prompt for first-frame-conditioned video generation.
1931
2032Use the provided sequential frames from a 5-second one-shot clip.
@@ -162,7 +174,7 @@ def _build_eval_instructions(prompt: str) -> str:
162174Given:
1631751) A conditioning first frame
1641762) The benchmark generation prompt
165- 3) Sequential frames from a generated video
177+ 3) The full generated video
166178
167179Benchmark 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+
214237async 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-
265267async 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