CB commited on
Commit
a1e5710
·
verified ·
1 Parent(s): 3c37e6c

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +36 -108
streamlit_app.py CHANGED
@@ -33,17 +33,14 @@ except Exception:
33
 
34
  load_dotenv()
35
 
36
- # Logging (minimal)
37
  logging.basicConfig(level=logging.INFO)
38
  logger = logging.getLogger("video_ai")
39
  logger.propagate = False
40
 
41
- # App config
42
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
43
  DATA_DIR = Path("./data")
44
  DATA_DIR.mkdir(exist_ok=True)
45
 
46
- # Session defaults
47
  st.session_state.setdefault("videos", "")
48
  st.session_state.setdefault("loop_video", False)
49
  st.session_state.setdefault("uploaded_file", None)
@@ -69,7 +66,6 @@ MODEL_OPTIONS = [
69
  "custom",
70
  ]
71
 
72
- # Utilities
73
  def sanitize_filename(path_str: str):
74
  name = Path(path_str).name
75
  return name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
@@ -145,7 +141,6 @@ def configure_genai_if_needed():
145
  logger.exception("Failed to configure genai")
146
  return True
147
 
148
- # Upload & processing
149
  def upload_video_sdk(filepath: str):
150
  key = get_effective_api_key()
151
  if not key:
@@ -201,7 +196,6 @@ def wait_for_processed(file_obj, timeout: int = None, progress_callback=None):
201
  time.sleep(backoff)
202
  backoff = min(backoff * 2, 8.0)
203
 
204
- # Normalize responses into text
205
  def _normalize_genai_response(response):
206
  if response is None:
207
  return ""
@@ -266,42 +260,6 @@ def _normalize_genai_response(response):
266
  seen.add(t)
267
  return "\n\n".join(filtered).strip()
268
 
269
- # REST fallback to GenAI Responses API
270
- def rest_responses_api(prompt_text: str, file_path: str, model: str, max_tokens: int = 1024, timeout: int = 300, progress_callback=None):
271
- key = get_effective_api_key()
272
- if not key:
273
- raise RuntimeError("No API key provided")
274
- url = "https://generativelanguage.googleapis.com/v1beta2/responses:generate"
275
- headers = {"Authorization": f"Bearer {key}"}
276
- # Build a simple request that attaches the file as a "file" in multipart/form-data.
277
- # Use a minimal JSON payload referencing the file by name in the input.
278
- fname = Path(file_path).name
279
- input_json = {
280
- "model": model,
281
- "input": [
282
- {
283
- "text": prompt_text,
284
- "mimeType": mimetypes.guess_type(file_path)[0] or "application/octet-stream",
285
- "attachments": [{"contentType": mimetypes.guess_type(file_path)[0] or "application/octet-stream", "name": fname}],
286
- }
287
- ],
288
- "maxOutputTokens": max_tokens,
289
- }
290
- # Multipart: one part "request" with JSON, another with the file binary.
291
- try:
292
- with open(file_path, "rb") as f:
293
- files = {
294
- "request": ("request", json.dumps(input_json), "application/json"),
295
- "file": (fname, f, mimetypes.guess_type(file_path)[0] or "application/octet-stream"),
296
- }
297
- resp = requests.post(url, headers=headers, files=files, timeout=timeout)
298
- resp.raise_for_status()
299
- data = resp.json()
300
- return _normalize_genai_response(data)
301
- except Exception as e:
302
- raise RuntimeError(f"REST Responses API failed: {e}")
303
-
304
- # Generation (supports various SDK shapes + REST fallback)
305
  def generate_via_responses_api(prompt_text: str, processed, model_used: str, max_tokens: int = 1024, timeout: int = 300, progress_callback=None):
306
  key = get_effective_api_key()
307
  if not key:
@@ -313,7 +271,6 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
313
  pass
314
  fname = file_name_or_id(processed) or None
315
 
316
- # Prepare simple system+user structure
317
  system_msg = {"role": "system", "content": prompt_text}
318
  user_msg = {"role": "user", "content": f"Please summarize the attached video: {fname or '[uploaded file]'}."}
319
 
@@ -327,33 +284,11 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
327
  txt = str(e_text).lower()
328
  return any(k in txt for k in ("internal", "unavailable", "deadlineexceeded", "deadline exceeded", "timeout", "rate limit", "503", "502", "500"))
329
 
330
- # Quick pre-check: if processed is a local path (dictless), prefer REST fallback to ensure attachment works
331
- local_file_path = None
332
- if isinstance(processed, str) and os.path.exists(processed):
333
- local_file_path = processed
334
- elif isinstance(processed, dict):
335
- # if SDK provided a dict with local path info (rare), try to detect
336
- for k in ("path", "name", "filename", "uri"):
337
- v = processed.get(k)
338
- if isinstance(v, str) and os.path.exists(v):
339
- local_file_path = v
340
- break
341
-
342
  start = time.time()
343
  last_exc = None
344
  backoff = 1.0
345
  attempts = 0
346
 
347
- # If we have a local file path, try REST fallback first for reliable file attachment.
348
- if local_file_path:
349
- try:
350
- if progress_callback:
351
- progress_callback("rest-fallback", 0, {"file": local_file_path, "model": model_used})
352
- return rest_responses_api(prompt_text, local_file_path, model_used, max_tokens=max_tokens, timeout=timeout, progress_callback=progress_callback)
353
- except Exception as e:
354
- last_exc = e
355
- logger.warning("REST fallback failed; will try SDK: %s", e)
356
-
357
  while True:
358
  for method_name, payload in call_variants:
359
  attempts += 1
@@ -361,20 +296,16 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
361
  if progress_callback:
362
  progress_callback("starting", int(time.time() - start), {"model": model_used, "attempt": attempts, "method": method_name})
363
 
364
- # Preferred modern: genai.responses.generate or genai_responses.generate
365
  if genai_responses is not None and hasattr(genai_responses, "generate"):
366
- # Remove None entries from payload
367
  payload = {k: v for k, v in payload.items() if v is not None}
368
  resp = genai_responses.generate(**payload)
369
  text = _normalize_genai_response(resp)
370
  if progress_callback:
371
  progress_callback("done", int(time.time() - start), {"method": method_name})
372
- # If the model returns a request-for-file style message, try REST fallback
373
  if text and ("please provide the video" in text.lower() or "upload the video" in text.lower()):
374
  raise RuntimeError("Model indicates it didn't receive the file")
375
  return text
376
 
377
- # Older path: genai.Responses.create
378
  if hasattr(genai, "Responses") and hasattr(genai.Responses, "create"):
379
  payload = {k: v for k, v in payload.items() if v is not None}
380
  resp = genai.Responses.create(**payload) # type: ignore
@@ -385,7 +316,6 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
385
  raise RuntimeError("Model indicates it didn't receive the file")
386
  return text
387
 
388
- # Fallback: GenerativeModel API (ChatSession). This SDK's ChatSession.send_message may not accept timeout kw.
389
  if hasattr(genai, "GenerativeModel"):
390
  try:
391
  model_obj = genai.GenerativeModel(model_name=model_used)
@@ -395,9 +325,9 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
395
  if send is None:
396
  raise RuntimeError("ChatSession has no send_message")
397
  try:
398
- resp = send(prompt_text, timeout=timeout) # try with timeout
399
  except TypeError:
400
- resp = send(prompt_text) # fallback without timeout
401
  text = getattr(resp, "text", None) or str(resp)
402
  text = text if text else _normalize_genai_response(resp)
403
  if progress_callback:
@@ -415,10 +345,9 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
415
  logger.warning("Generation error (model=%s attempt=%s method=%s): %s", model_used, attempts, method_name, msg)
416
  if not is_transient_error(msg):
417
  if "No supported response generation method" in msg or "has no attribute" in msg or "didn't receive the file" in msg:
418
- # If it's a file-attachment issue or incompatible SDK, offer a clear upgrade message (but don't spam UI)
419
  raise RuntimeError(
420
  "Installed google-generativeai package may not expose a compatible Responses API or the SDK didn't attach the file correctly. "
421
- "Try upgrading the SDK: pip install --upgrade google-generativeai, or use the app's REST fallback."
422
  ) from e
423
  raise
424
  if time.time() - start > timeout:
@@ -426,7 +355,6 @@ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max
426
  time.sleep(backoff)
427
  backoff = min(backoff * 2, 8.0)
428
 
429
- # Trim prompt echoes
430
  def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
431
  if not prompt or not text:
432
  return text
@@ -446,7 +374,7 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
446
  return b_full[len(ph):].lstrip(" \n:-")
447
  return text
448
 
449
- # UI: reset per new URL value
450
  current_url = st.session_state.get("url", "")
451
  if current_url != st.session_state.get("last_url_value"):
452
  st.session_state["videos"] = ""
@@ -464,56 +392,63 @@ if current_url != st.session_state.get("last_url_value"):
464
  st.session_state["last_url_value"] = current_url
465
 
466
  st.sidebar.header("Video Input")
467
- st.sidebar.text_input("Video URL", key="url", placeholder="https://")
468
 
469
  settings_exp = st.sidebar.expander("Settings", expanded=False)
470
- chosen = settings_exp.selectbox("Gemini model", MODEL_OPTIONS, index=MODEL_OPTIONS.index(st.session_state.get("preferred_model", "gemini-2.0-flash-lite")))
471
  custom_model = ""
472
- if chosen == "custom":
473
- custom_model = settings_exp.text_input("Custom model name", value=st.session_state.get("preferred_model", "gemini-2.0-flash-lite"))
474
- model_input_value = (custom_model.strip() if chosen == "custom" else chosen).strip()
475
 
476
- settings_exp.text_input("Google API Key", key="api_key", value=os.getenv("GOOGLE_API_KEY", ""), type="password")
 
477
 
478
  default_prompt = (
479
  "Watch the video and provide a detailed behavioral report focusing on human actions, interactions, posture, movement, and apparent intent. Keep language professional. Include a list of observations for notable events."
480
  )
481
- analysis_prompt = settings_exp.text_area("Enter analysis prompt", value=default_prompt, height=140)
482
- settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
 
 
483
 
484
  settings_exp.number_input(
485
  "Processing timeout (s)", min_value=60, max_value=3600,
486
  value=st.session_state.get("processing_timeout", 900), step=30,
487
- key="processing_timeout",
488
  )
 
 
489
  settings_exp.number_input(
490
  "Generation timeout (s)", min_value=30, max_value=1800,
491
  value=st.session_state.get("generation_timeout", 300), step=10,
492
- key="generation_timeout",
493
  )
 
494
 
495
  settings_exp.number_input(
496
  "Compression threshold (MB)", min_value=10, max_value=2000,
497
  value=st.session_state.get("compression_threshold_mb", 200), step=10,
498
- key="compression_threshold_mb",
499
  )
500
- settings_exp.caption("Files threshold are uploaded unchanged. Files > threshold are compressed before upload (tunable).")
501
 
502
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
503
- settings_exp.caption(f"Using API key from: **{key_source}**")
 
504
  if not get_effective_api_key():
505
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
506
 
507
  col1, col2 = st.columns([1, 3])
508
  with col1:
509
- generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
510
  with col2:
511
  pass
512
 
513
- if st.sidebar.button("Load Video", use_container_width=True):
514
  try:
515
- vpw = st.session_state.get("video-password", "")
516
- path = download_video_ytdlp(st.session_state.get("url", ""), str(DATA_DIR), vpw)
517
  st.session_state["videos"] = path
518
  st.session_state["last_loaded_path"] = path
519
  st.session_state.pop("uploaded_file", None)
@@ -532,10 +467,10 @@ if st.session_state["videos"]:
532
  st.sidebar.write("Couldn't preview video")
533
 
534
  with st.sidebar.expander("Options", expanded=False):
535
- loop_checkbox = st.checkbox("Enable Loop", value=st.session_state.get("loop_video", False))
536
- st.session_state["loop_video"] = loop_checkbox
537
 
538
- if st.button("Clear Video(s)"):
539
  st.session_state["videos"] = ""
540
  st.session_state["last_loaded_path"] = ""
541
  st.session_state["uploaded_file"] = None
@@ -551,7 +486,7 @@ if st.session_state["videos"]:
551
 
552
  try:
553
  with open(st.session_state["videos"], "rb") as vf:
554
- st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["videos"]), mime="video/mp4", use_container_width=True)
555
  except Exception:
556
  st.sidebar.error("Failed to prepare download")
557
 
@@ -599,8 +534,8 @@ if generate_now and not st.session_state.get("busy"):
599
  reupload_needed = False
600
 
601
  if reupload_needed:
602
- if not HAS_GENAI and not get_effective_api_key():
603
- raise RuntimeError("google.generativeai SDK not available and no API key; cannot upload")
604
  local_path = current_path
605
 
606
  try:
@@ -620,12 +555,7 @@ if generate_now and not st.session_state.get("busy"):
620
 
621
  with st.spinner(f"Uploading video{' (compressed)' if compressed else ''}..."):
622
  try:
623
- # Prefer SDK upload if available, else keep local path for REST fallback
624
- if HAS_GENAI and upload_file is not None:
625
- uploaded = upload_video_sdk(upload_path)
626
- else:
627
- # No SDK upload; retain local path (REST fallback will attach file directly)
628
- uploaded = upload_path
629
  except Exception as e:
630
  st.session_state["last_error"] = f"Upload failed: {e}\n\nTraceback:\n{traceback.format_exc()}"
631
  st.error("Upload failed. See Last Error for details.")
@@ -653,7 +583,7 @@ if generate_now and not st.session_state.get("busy"):
653
  st.session_state["last_loaded_path"] = current_path
654
  st.session_state["file_hash"] = current_hash
655
 
656
- prompt_text = (analysis_prompt.strip() or default_prompt).strip()
657
  out = ""
658
  model_used = model_id
659
  max_tokens = 2048 if "2.5" in model_used else 1024
@@ -702,14 +632,12 @@ if generate_now and not st.session_state.get("busy"):
702
  finally:
703
  st.session_state["busy"] = False
704
 
705
- # Display existing analysis
706
  if st.session_state.get("analysis_out"):
707
  just_loaded_same = (st.session_state.get("last_loaded_path") == st.session_state.get("videos"))
708
  if not just_loaded_same:
709
  st.subheader("Analysis Result")
710
  st.markdown(st.session_state.get("analysis_out"))
711
 
712
- # Last error expander
713
  if st.session_state.get("last_error"):
714
  with st.expander("Last Error", expanded=False):
715
  st.write(st.session_state.get("last_error"))
 
33
 
34
  load_dotenv()
35
 
 
36
  logging.basicConfig(level=logging.INFO)
37
  logger = logging.getLogger("video_ai")
38
  logger.propagate = False
39
 
 
40
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
41
  DATA_DIR = Path("./data")
42
  DATA_DIR.mkdir(exist_ok=True)
43
 
 
44
  st.session_state.setdefault("videos", "")
45
  st.session_state.setdefault("loop_video", False)
46
  st.session_state.setdefault("uploaded_file", None)
 
66
  "custom",
67
  ]
68
 
 
69
  def sanitize_filename(path_str: str):
70
  name = Path(path_str).name
71
  return name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
 
141
  logger.exception("Failed to configure genai")
142
  return True
143
 
 
144
  def upload_video_sdk(filepath: str):
145
  key = get_effective_api_key()
146
  if not key:
 
196
  time.sleep(backoff)
197
  backoff = min(backoff * 2, 8.0)
198
 
 
199
  def _normalize_genai_response(response):
200
  if response is None:
201
  return ""
 
260
  seen.add(t)
261
  return "\n\n".join(filtered).strip()
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  def generate_via_responses_api(prompt_text: str, processed, model_used: str, max_tokens: int = 1024, timeout: int = 300, progress_callback=None):
264
  key = get_effective_api_key()
265
  if not key:
 
271
  pass
272
  fname = file_name_or_id(processed) or None
273
 
 
274
  system_msg = {"role": "system", "content": prompt_text}
275
  user_msg = {"role": "user", "content": f"Please summarize the attached video: {fname or '[uploaded file]'}."}
276
 
 
284
  txt = str(e_text).lower()
285
  return any(k in txt for k in ("internal", "unavailable", "deadlineexceeded", "deadline exceeded", "timeout", "rate limit", "503", "502", "500"))
286
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  start = time.time()
288
  last_exc = None
289
  backoff = 1.0
290
  attempts = 0
291
 
 
 
 
 
 
 
 
 
 
 
292
  while True:
293
  for method_name, payload in call_variants:
294
  attempts += 1
 
296
  if progress_callback:
297
  progress_callback("starting", int(time.time() - start), {"model": model_used, "attempt": attempts, "method": method_name})
298
 
 
299
  if genai_responses is not None and hasattr(genai_responses, "generate"):
 
300
  payload = {k: v for k, v in payload.items() if v is not None}
301
  resp = genai_responses.generate(**payload)
302
  text = _normalize_genai_response(resp)
303
  if progress_callback:
304
  progress_callback("done", int(time.time() - start), {"method": method_name})
 
305
  if text and ("please provide the video" in text.lower() or "upload the video" in text.lower()):
306
  raise RuntimeError("Model indicates it didn't receive the file")
307
  return text
308
 
 
309
  if hasattr(genai, "Responses") and hasattr(genai.Responses, "create"):
310
  payload = {k: v for k, v in payload.items() if v is not None}
311
  resp = genai.Responses.create(**payload) # type: ignore
 
316
  raise RuntimeError("Model indicates it didn't receive the file")
317
  return text
318
 
 
319
  if hasattr(genai, "GenerativeModel"):
320
  try:
321
  model_obj = genai.GenerativeModel(model_name=model_used)
 
325
  if send is None:
326
  raise RuntimeError("ChatSession has no send_message")
327
  try:
328
+ resp = send(prompt_text, timeout=timeout)
329
  except TypeError:
330
+ resp = send(prompt_text)
331
  text = getattr(resp, "text", None) or str(resp)
332
  text = text if text else _normalize_genai_response(resp)
333
  if progress_callback:
 
345
  logger.warning("Generation error (model=%s attempt=%s method=%s): %s", model_used, attempts, method_name, msg)
346
  if not is_transient_error(msg):
347
  if "No supported response generation method" in msg or "has no attribute" in msg or "didn't receive the file" in msg:
 
348
  raise RuntimeError(
349
  "Installed google-generativeai package may not expose a compatible Responses API or the SDK didn't attach the file correctly. "
350
+ "Try upgrading the SDK: pip install --upgrade google-generativeai."
351
  ) from e
352
  raise
353
  if time.time() - start > timeout:
 
355
  time.sleep(backoff)
356
  backoff = min(backoff * 2, 8.0)
357
 
 
358
  def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
359
  if not prompt or not text:
360
  return text
 
374
  return b_full[len(ph):].lstrip(" \n:-")
375
  return text
376
 
377
+ # UI reset on URL change
378
  current_url = st.session_state.get("url", "")
379
  if current_url != st.session_state.get("last_url_value"):
380
  st.session_state["videos"] = ""
 
392
  st.session_state["last_url_value"] = current_url
393
 
394
  st.sidebar.header("Video Input")
395
+ st.sidebar.text_input("Video URL", key="url_input", placeholder="https://", value=st.session_state.get("url", ""))
396
 
397
  settings_exp = st.sidebar.expander("Settings", expanded=False)
398
+ chosen = settings_exp.selectbox("Gemini model", MODEL_OPTIONS, index=MODEL_OPTIONS.index(st.session_state.get("preferred_model", "gemini-2.0-flash-lite")), key="model_select")
399
  custom_model = ""
400
+ if settings_exp.session_state.get("model_select") == "custom":
401
+ custom_model = settings_exp.text_input("Custom model name", value=st.session_state.get("preferred_model", "gemini-2.0-flash-lite"), key="custom_model")
402
+ model_input_value = (custom_model.strip() if custom_model else settings_exp.session_state.get("model_select")).strip()
403
 
404
+ settings_exp.text_input("Google API Key", key="api_key_input", value=st.session_state.get("api_key", ""), type="password")
405
+ st.session_state["api_key"] = settings_exp.session_state.get("api_key_input", st.session_state.get("api_key", ""))
406
 
407
  default_prompt = (
408
  "Watch the video and provide a detailed behavioral report focusing on human actions, interactions, posture, movement, and apparent intent. Keep language professional. Include a list of observations for notable events."
409
  )
410
+ analysis_prompt = settings_exp.text_area("Enter analysis prompt", value=st.session_state.get("analysis_prompt", default_prompt), height=140, key="analysis_prompt")
411
+ st.session_state["analysis_prompt"] = settings_exp.session_state.get("analysis_prompt", default_prompt)
412
+
413
+ settings_exp.text_input("Video Password (if needed)", key="video_password_input", placeholder="password", type="password")
414
 
415
  settings_exp.number_input(
416
  "Processing timeout (s)", min_value=60, max_value=3600,
417
  value=st.session_state.get("processing_timeout", 900), step=30,
418
+ key="processing_timeout_input",
419
  )
420
+ st.session_state["processing_timeout"] = settings_exp.session_state.get("processing_timeout_input", st.session_state.get("processing_timeout", 900))
421
+
422
  settings_exp.number_input(
423
  "Generation timeout (s)", min_value=30, max_value=1800,
424
  value=st.session_state.get("generation_timeout", 300), step=10,
425
+ key="generation_timeout_input",
426
  )
427
+ st.session_state["generation_timeout"] = settings_exp.session_state.get("generation_timeout_input", st.session_state.get("generation_timeout", 300))
428
 
429
  settings_exp.number_input(
430
  "Compression threshold (MB)", min_value=10, max_value=2000,
431
  value=st.session_state.get("compression_threshold_mb", 200), step=10,
432
+ key="compression_threshold_input",
433
  )
434
+ st.session_state["compression_threshold_mb"] = settings_exp.session_state.get("compression_threshold_input", st.session_state.get("compression_threshold_mb", 200))
435
 
436
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
437
+ settings_exp.caption(f"Using API key from: {key_source}")
438
+
439
  if not get_effective_api_key():
440
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
441
 
442
  col1, col2 = st.columns([1, 3])
443
  with col1:
444
+ generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()), key="gen_button")
445
  with col2:
446
  pass
447
 
448
+ if st.sidebar.button("Load Video", use_container_width=True, key="load_video_btn"):
449
  try:
450
+ vpw = settings_exp.session_state.get("video_password_input", "")
451
+ path = download_video_ytdlp(st.session_state.get("url", settings_exp.session_state.get("url_input", "")), str(DATA_DIR), vpw)
452
  st.session_state["videos"] = path
453
  st.session_state["last_loaded_path"] = path
454
  st.session_state.pop("uploaded_file", None)
 
467
  st.sidebar.write("Couldn't preview video")
468
 
469
  with st.sidebar.expander("Options", expanded=False):
470
+ loop_checkbox = st.checkbox("Enable Loop", value=st.session_state.get("loop_video", False), key="loop_checkbox")
471
+ st.session_state["loop_video"] = settings_exp.session_state.get("loop_checkbox", st.session_state.get("loop_video", False))
472
 
473
+ if st.button("Clear Video(s)", key="clear_videos_btn"):
474
  st.session_state["videos"] = ""
475
  st.session_state["last_loaded_path"] = ""
476
  st.session_state["uploaded_file"] = None
 
486
 
487
  try:
488
  with open(st.session_state["videos"], "rb") as vf:
489
+ st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["videos"]), mime="video/mp4", use_container_width=True, key="download_video_btn")
490
  except Exception:
491
  st.sidebar.error("Failed to prepare download")
492
 
 
534
  reupload_needed = False
535
 
536
  if reupload_needed:
537
+ if not HAS_GENAI or upload_file is None:
538
+ raise RuntimeError("google.generativeai SDK or upload support unavailable; cannot upload video. Use SDK with upload_file support.")
539
  local_path = current_path
540
 
541
  try:
 
555
 
556
  with st.spinner(f"Uploading video{' (compressed)' if compressed else ''}..."):
557
  try:
558
+ uploaded = upload_video_sdk(upload_path)
 
 
 
 
 
559
  except Exception as e:
560
  st.session_state["last_error"] = f"Upload failed: {e}\n\nTraceback:\n{traceback.format_exc()}"
561
  st.error("Upload failed. See Last Error for details.")
 
583
  st.session_state["last_loaded_path"] = current_path
584
  st.session_state["file_hash"] = current_hash
585
 
586
+ prompt_text = (st.session_state.get("analysis_prompt", "") or default_prompt).strip()
587
  out = ""
588
  model_used = model_id
589
  max_tokens = 2048 if "2.5" in model_used else 1024
 
632
  finally:
633
  st.session_state["busy"] = False
634
 
 
635
  if st.session_state.get("analysis_out"):
636
  just_loaded_same = (st.session_state.get("last_loaded_path") == st.session_state.get("videos"))
637
  if not just_loaded_same:
638
  st.subheader("Analysis Result")
639
  st.markdown(st.session_state.get("analysis_out"))
640
 
 
641
  if st.session_state.get("last_error"):
642
  with st.expander("Last Error", expanded=False):
643
  st.write(st.session_state.get("last_error"))