Spaces:
Running
Running
Trust Gemini timestamps over unreliable ffprobe duration
Browse filesThe container duration probe under-reported a ~5min video as 209s
(bad WhatsApp/edited MP4 metadata), causing a valid 230-329s clip to be
skipped. Stop using the probe to skip/clamp or to constrain Gemini's
prompt; trust Gemini's timestamps (it watched the real file) and let
-c copy fail gracefully on genuinely out-of-range segments. Probe is now
diagnostic-only. Raise clip cap to 300s so long steps aren't truncated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main.py
CHANGED
|
@@ -183,8 +183,9 @@ def _storage_signed_url(path: str, minutes: int = 120) -> str:
|
|
| 183 |
return ""
|
| 184 |
|
| 185 |
|
| 186 |
-
# Maximum length (seconds) of a tutorial clip we will cut.
|
| 187 |
-
|
|
|
|
| 188 |
# Padding added to the end of a clip so the demonstrated step is not cut off abruptly.
|
| 189 |
CLIP_TAIL_PAD = 2
|
| 190 |
|
|
@@ -1242,29 +1243,6 @@ def tutorial_ingest():
|
|
| 1242 |
logger.info("[tutorial] STEP 1/5: received '%s' (%.2f MB, mime=%s)",
|
| 1243 |
video_title, len(video_bytes) / 1024 / 1024, mime_type)
|
| 1244 |
|
| 1245 |
-
# Probe the real duration up front so we can constrain Gemini's timestamps to it —
|
| 1246 |
-
# otherwise the model sometimes invents segments past the end of the video.
|
| 1247 |
-
vid_dur_hint = 0.0
|
| 1248 |
-
try:
|
| 1249 |
-
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as _pt:
|
| 1250 |
-
_pt.write(video_bytes)
|
| 1251 |
-
_probe_path = _pt.name
|
| 1252 |
-
vid_dur_hint = _video_duration(_probe_path)
|
| 1253 |
-
logger.info("[tutorial] probed duration %.1fs, codec %s",
|
| 1254 |
-
vid_dur_hint, _video_codec(_probe_path))
|
| 1255 |
-
os.remove(_probe_path)
|
| 1256 |
-
except Exception as e:
|
| 1257 |
-
logger.warning("[tutorial] pre-probe failed: %s", e)
|
| 1258 |
-
|
| 1259 |
-
extraction_prompt = TUTORIAL_VIDEO_PROMPT
|
| 1260 |
-
if vid_dur_hint and vid_dur_hint > 0:
|
| 1261 |
-
extraction_prompt += (
|
| 1262 |
-
f"\n\nIMPORTANT: This video is exactly {int(vid_dur_hint)} seconds long. "
|
| 1263 |
-
f"Every timestamp_start and timestamp_end MUST be between 0 and {int(vid_dur_hint)}. "
|
| 1264 |
-
f"Do not output any timestamp greater than {int(vid_dur_hint)}, and only describe "
|
| 1265 |
-
f"steps that actually occur within the video's length."
|
| 1266 |
-
)
|
| 1267 |
-
|
| 1268 |
# Upload to Gemini Files API and wait for processing
|
| 1269 |
logger.info("[tutorial] STEP 2/5: uploading to Gemini Files API + waiting for ACTIVE...")
|
| 1270 |
gemini_file = _upload_video_to_gemini(video_bytes, mime_type, display_name=video_title)
|
|
@@ -1280,7 +1258,7 @@ def tutorial_ingest():
|
|
| 1280 |
t_extract = time.time()
|
| 1281 |
resp = _gemini_client.models.generate_content(
|
| 1282 |
model=GEMINI_MODEL,
|
| 1283 |
-
contents=[gemini_file,
|
| 1284 |
config=genai_types.GenerateContentConfig(
|
| 1285 |
response_mime_type="application/json",
|
| 1286 |
http_options=genai_types.HttpOptions(timeout=GEMINI_VIDEO_TIMEOUT_MS),
|
|
@@ -1338,9 +1316,12 @@ def tutorial_ingest():
|
|
| 1338 |
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as src_tmp:
|
| 1339 |
src_tmp.write(video_bytes)
|
| 1340 |
src_path = src_tmp.name
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
|
|
|
|
|
|
|
|
|
| 1344 |
for idx, a in enumerate(articles, 1):
|
| 1345 |
start = a.get("timestamp_start", 0)
|
| 1346 |
end = a.get("timestamp_end", 0)
|
|
@@ -1351,7 +1332,7 @@ def tutorial_ingest():
|
|
| 1351 |
out_path = os.path.join(tempfile.gettempdir(), f"clip_{uuid.uuid4().hex}.mp4")
|
| 1352 |
clip_path = f"kb_clips/{uuid.uuid4().hex}.mp4"
|
| 1353 |
logger.info("[tutorial] clip %d/%d: cropping %ss–%ss...", idx, len(articles), start, end)
|
| 1354 |
-
if _crop_video_segment(src_path, start, end, out_path
|
| 1355 |
try:
|
| 1356 |
clip_bytes = os.path.getsize(out_path)
|
| 1357 |
logger.info("[tutorial] clip %d/%d: uploading %d bytes to %s...",
|
|
|
|
| 183 |
return ""
|
| 184 |
|
| 185 |
|
| 186 |
+
# Maximum length (seconds) of a tutorial clip we will cut. Generous so full steps
|
| 187 |
+
# aren't truncated; stream-copied clips stay well under Twilio's ~16MB media limit.
|
| 188 |
+
MAX_CLIP_SECONDS = 300
|
| 189 |
# Padding added to the end of a clip so the demonstrated step is not cut off abruptly.
|
| 190 |
CLIP_TAIL_PAD = 2
|
| 191 |
|
|
|
|
| 1243 |
logger.info("[tutorial] STEP 1/5: received '%s' (%.2f MB, mime=%s)",
|
| 1244 |
video_title, len(video_bytes) / 1024 / 1024, mime_type)
|
| 1245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1246 |
# Upload to Gemini Files API and wait for processing
|
| 1247 |
logger.info("[tutorial] STEP 2/5: uploading to Gemini Files API + waiting for ACTIVE...")
|
| 1248 |
gemini_file = _upload_video_to_gemini(video_bytes, mime_type, display_name=video_title)
|
|
|
|
| 1258 |
t_extract = time.time()
|
| 1259 |
resp = _gemini_client.models.generate_content(
|
| 1260 |
model=GEMINI_MODEL,
|
| 1261 |
+
contents=[gemini_file, TUTORIAL_VIDEO_PROMPT],
|
| 1262 |
config=genai_types.GenerateContentConfig(
|
| 1263 |
response_mime_type="application/json",
|
| 1264 |
http_options=genai_types.HttpOptions(timeout=GEMINI_VIDEO_TIMEOUT_MS),
|
|
|
|
| 1316 |
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as src_tmp:
|
| 1317 |
src_tmp.write(video_bytes)
|
| 1318 |
src_path = src_tmp.name
|
| 1319 |
+
# Probe is logged for diagnostics only — container metadata is sometimes
|
| 1320 |
+
# wrong (esp. WhatsApp/edited MP4s), so we do NOT use it to skip segments.
|
| 1321 |
+
# We trust Gemini's timestamps (it watched the real file) and let ffmpeg
|
| 1322 |
+
# copy fail gracefully if a segment is genuinely past EOF.
|
| 1323 |
+
logger.info("[tutorial] wrote source video to temp: %s (reported duration %.1fs, codec %s)",
|
| 1324 |
+
src_path, _video_duration(src_path), _video_codec(src_path))
|
| 1325 |
for idx, a in enumerate(articles, 1):
|
| 1326 |
start = a.get("timestamp_start", 0)
|
| 1327 |
end = a.get("timestamp_end", 0)
|
|
|
|
| 1332 |
out_path = os.path.join(tempfile.gettempdir(), f"clip_{uuid.uuid4().hex}.mp4")
|
| 1333 |
clip_path = f"kb_clips/{uuid.uuid4().hex}.mp4"
|
| 1334 |
logger.info("[tutorial] clip %d/%d: cropping %ss–%ss...", idx, len(articles), start, end)
|
| 1335 |
+
if _crop_video_segment(src_path, start, end, out_path):
|
| 1336 |
try:
|
| 1337 |
clip_bytes = os.path.getsize(out_path)
|
| 1338 |
logger.info("[tutorial] clip %d/%d: uploading %d bytes to %s...",
|