CB commited on
Commit
d7c5962
·
verified ·
1 Parent(s): a37fc26

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +130 -205
streamlit_app.py CHANGED
@@ -1,4 +1,3 @@
1
- # streamlit_app.py
2
  import os
3
  import time
4
  import string
@@ -6,18 +5,15 @@ import hashlib
6
  import traceback
7
  from glob import glob
8
  from pathlib import Path
9
- from difflib import SequenceMatcher
10
- import json
11
- import logging
12
 
13
  import yt_dlp
14
- import ffmpeg
15
  import streamlit as st
16
  from dotenv import load_dotenv
17
 
18
  load_dotenv()
19
 
20
- # Optional PHI integration
21
  try:
22
  from phi.agent import Agent
23
  from phi.model.google import Gemini
@@ -28,7 +24,7 @@ except Exception:
28
  Agent = Gemini = DuckDuckGo = None
29
  HAS_PHI = False
30
 
31
- # google.generativeai SDK
32
  try:
33
  import google.generativeai as genai
34
  from google.generativeai import upload_file, get_file
@@ -39,8 +35,6 @@ except Exception:
39
  upload_file = get_file = None
40
  HAS_GENAI = False
41
 
42
- logging.basicConfig(level=logging.INFO)
43
-
44
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
45
  DATA_DIR = Path("./data")
46
  DATA_DIR.mkdir(exist_ok=True)
@@ -72,8 +66,6 @@ st.session_state.setdefault("last_error", "")
72
  st.session_state.setdefault("file_hash", None)
73
  st.session_state.setdefault("api_key", os.getenv("GOOGLE_API_KEY", ""))
74
  st.session_state.setdefault("last_model", "")
75
- st.session_state.setdefault("upload_progress", {"uploaded": 0, "total": 0})
76
- st.session_state.setdefault("last_url_value", "")
77
  st.session_state.setdefault("processing_timeout", 900)
78
  st.session_state.setdefault("generation_timeout", 300)
79
  st.session_state.setdefault("compress_threshold_mb", 200)
@@ -99,17 +91,16 @@ def convert_video_to_mp4(video_path: str) -> str:
99
  return target_path
100
  try:
101
  ffmpeg.input(video_path).output(target_path).run(overwrite_output=True, quiet=True)
102
- except Exception as e:
103
- logging.exception("ffmpeg conversion failed")
104
- # If conversion fails, do not delete original; re-raise for caller to handle if needed
105
  raise
106
- # Only remove source if target exists and is non-empty
107
  if os.path.exists(target_path) and os.path.getsize(target_path) > 0:
108
  try:
109
  if str(Path(video_path).resolve()) != str(Path(target_path).resolve()):
110
  os.remove(video_path)
111
  except Exception:
112
- logging.exception("Failed to remove original video after conversion")
113
  return target_path
114
 
115
  def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
@@ -119,10 +110,8 @@ def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str
119
  ).run(overwrite_output=True, quiet=True)
120
  if os.path.exists(target_path) and os.path.getsize(target_path) > 0:
121
  return target_path
122
- logging.warning("Compression completed but target missing or empty; returning input path")
123
  return input_path
124
  except Exception:
125
- logging.exception("Video compression failed")
126
  return input_path
127
 
128
  def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
@@ -131,37 +120,29 @@ def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) ->
131
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
132
  ydl_opts = {"outtmpl": outtmpl, "format": "best"}
133
  if video_password:
134
- # yt-dlp accepts 'videopassword' in options for password-protected videos
135
  ydl_opts["videopassword"] = video_password
136
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
137
  info = ydl.extract_info(url, download=True)
138
- # info may be a dict for single video or playlist; prefer single entry if present
139
- video_candidates = []
140
  if isinstance(info, dict):
141
- # playlist -> entries list
142
  entries = info.get("entries")
143
  if entries:
144
- # get last-downloaded entry (entries may be nested); map to filesystem files by ids
145
  for e in entries:
146
  if isinstance(e, dict) and e.get("id"):
147
- video_candidates.append(str(Path(save_dir) / f"{e['id']}.mp4"))
148
  else:
149
  vid = info.get("id")
150
  ext = info.get("ext") or "mp4"
151
  if vid:
152
- video_candidates.append(str(Path(save_dir) / f"{vid}.{ext}"))
153
- # fallback: pick most recent file in dir
154
- if not video_candidates:
155
  all_files = glob(os.path.join(save_dir, "*"))
156
  if not all_files:
157
  raise FileNotFoundError("Downloaded video not found")
158
- matches = sorted(all_files, key=os.path.getmtime, reverse=True)
159
- chosen = matches[0]
160
  else:
161
- # prefer existing files among candidates; pick first that exists, else fall back to newest
162
- existing = [p for p in video_candidates if os.path.exists(p)]
163
- chosen = existing[0] if existing else (sorted(glob(os.path.join(save_dir, "*")), key=os.path.getmtime, reverse=True)[0])
164
- # Ensure mp4 target
165
  final = convert_video_to_mp4(chosen)
166
  return final
167
 
@@ -170,7 +151,6 @@ def file_name_or_id(file_obj):
170
  return None
171
  if isinstance(file_obj, dict):
172
  return file_obj.get("name") or file_obj.get("id")
173
- # common SDK wrappers may expose 'name', 'id', 'fileId'
174
  return getattr(file_obj, "name", None) or getattr(file_obj, "id", None) or getattr(file_obj, "fileId", None)
175
 
176
  def get_effective_api_key():
@@ -183,7 +163,7 @@ def configure_genai_if_needed():
183
  try:
184
  genai.configure(api_key=key)
185
  except Exception:
186
- logging.exception("genai.configure failed")
187
  return True
188
 
189
  # ---- Agent management (reuse) ----
@@ -201,7 +181,6 @@ def maybe_create_agent(model_id: str):
201
  _agent = Agent(name="Video AI summarizer", model=Gemini(id=model_id), tools=[DuckDuckGo()], markdown=True)
202
  st.session_state["last_model"] = model_id
203
  except Exception:
204
- logging.exception("Failed to create PHI Agent")
205
  _agent = None
206
  return _agent
207
 
@@ -217,13 +196,13 @@ def clear_all_video_state():
217
  try:
218
  os.remove(f)
219
  except Exception:
220
- logging.exception("Failed to remove data file during clear_all_video_state")
221
 
222
  # Reset when URL changes
223
  current_url = st.session_state.get("url", "")
224
- if current_url != st.session_state.get("last_url_value"):
225
  clear_all_video_state()
226
- st.session_state["last_url_value"] = current_url
227
 
228
  # ---- Sidebar UI ----
229
  st.sidebar.header("Video Input")
@@ -235,7 +214,6 @@ if model_choice == "custom":
235
  model_input = settings_exp.text_input("Custom model id", value=DEFAULT_MODEL, key="model_input")
236
  model_selected = model_input.strip() or DEFAULT_MODEL
237
  else:
238
- # keep model_input in session_state for later reads
239
  st.session_state["model_input"] = model_choice
240
  model_selected = model_choice
241
 
@@ -260,7 +238,7 @@ settings_exp.number_input(
260
  )
261
 
262
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
263
- settings_exp.caption(f"Using API key from: **{key_source}**")
264
  if not get_effective_api_key():
265
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
266
 
@@ -279,7 +257,6 @@ def upload_video_sdk(filepath: str):
279
  if not HAS_GENAI or upload_file is None:
280
  raise RuntimeError("google.generativeai SDK not available; cannot upload")
281
  genai.configure(api_key=key)
282
- # upload_file may return object with id or name, keep as-is
283
  return upload_file(filepath)
284
 
285
  def wait_for_processed(file_obj, timeout: int = None):
@@ -295,9 +272,9 @@ def wait_for_processed(file_obj, timeout: int = None):
295
  while True:
296
  try:
297
  obj = get_file(name)
298
- except Exception as e:
299
  if time.time() - start > timeout:
300
- raise TimeoutError(f"Failed to fetch file status before timeout: {e}")
301
  time.sleep(backoff)
302
  backoff = min(backoff * 2, 8.0)
303
  continue
@@ -307,7 +284,7 @@ def wait_for_processed(file_obj, timeout: int = None):
307
  return obj
308
 
309
  if time.time() - start > timeout:
310
- raise TimeoutError(f"File processing timed out after {int(time.time() - start)}s")
311
  time.sleep(backoff)
312
  backoff = min(backoff * 2, 8.0)
313
 
@@ -317,7 +294,11 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
317
  a = " ".join(prompt.strip().lower().split())
318
  b_full = text.strip()
319
  b = " ".join(b_full[:check_len].lower().split())
320
- ratio = SequenceMatcher(None, a, b).ratio()
 
 
 
 
321
  if ratio >= ratio_threshold:
322
  cut = min(len(b_full), max(int(len(prompt) * 0.9), len(a)))
323
  new_text = b_full[cut:].lstrip(" \n:-")
@@ -333,14 +314,13 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
333
  def compress_video_if_large(local_path: str, threshold_mb: int = 200):
334
  try:
335
  file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
336
- except Exception as e:
337
- st.session_state["last_error"] = f"Failed to stat file before compression: {e}"
338
  return local_path, False
339
 
340
  if file_size_mb <= threshold_mb:
341
  return local_path, False
342
 
343
- # build compressed path reliably
344
  p = Path(local_path)
345
  compressed_name = f"{p.stem}_compressed.mp4"
346
  compressed_path = str(p.with_name(compressed_name))
@@ -350,136 +330,11 @@ def compress_video_if_large(local_path: str, threshold_mb: int = 200):
350
  if result and os.path.exists(result) and os.path.getsize(result) > 0:
351
  return result, True
352
  return local_path, False
353
- except Exception as e:
354
- st.session_state["last_error"] = f"Video compression failed: {e}\n{traceback.format_exc()}"
355
  return local_path, False
356
 
357
- # ---- Responses API integration ----
358
- def generate_via_responses_api(prompt_text: str, processed, model_used: str, max_tokens: int = 1024, timeout: int = 300):
359
- key = get_effective_api_key()
360
- if not key:
361
- raise RuntimeError("No API key provided")
362
- if not HAS_GENAI or genai is None:
363
- raise RuntimeError("Responses API not available; install google.generativeai SDK.")
364
- genai.configure(api_key=key)
365
- fname = file_name_or_id(processed)
366
- if not fname:
367
- raise RuntimeError("Uploaded file missing name/id")
368
-
369
- system_msg = {"role": "system", "content": prompt_text}
370
- user_msg = {"role": "user", "content": "Please summarize the attached video."}
371
-
372
- call_variants = [
373
- {"messages": [system_msg, user_msg], "files": [{"name": fname}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
374
- {"input": [{"text": prompt_text, "files": [{"name": fname}]}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
375
- ]
376
-
377
- last_exc = None
378
- start = time.time()
379
- backoff = 1.0
380
- while True:
381
- for payload in call_variants:
382
- try:
383
- response = genai.responses.generate(model=model_used, **payload)
384
- return _normalize_genai_response(response)
385
- except Exception as e:
386
- last_exc = e
387
- msg = str(e).lower()
388
- # retry for transient/server errors
389
- if any(k in msg for k in ("internal", "unavailable", "deadlineexceeded", "deadline exceeded", "timeout", "rate limit")):
390
- logging.warning("Transient error from Responses API, will retry: %s", e)
391
- continue
392
- logging.exception("Non-retryable Responses API error")
393
- raise
394
- if time.time() - start > timeout:
395
- raise TimeoutError(f"Responses.generate timed out after {timeout}s: last error: {last_exc}")
396
- time.sleep(backoff)
397
- backoff = min(backoff * 2, 8.0)
398
-
399
- def _normalize_genai_response(response):
400
- outputs = []
401
- if response is None:
402
- return ""
403
-
404
- if not isinstance(response, dict):
405
- try:
406
- response = json.loads(str(response))
407
- except Exception:
408
- pass
409
-
410
- candidate_lists = []
411
- if isinstance(response, dict):
412
- for key in ("output", "candidates", "items", "responses", "choices"):
413
- val = response.get(key)
414
- if isinstance(val, list) and val:
415
- candidate_lists.append(val)
416
- if not candidate_lists and isinstance(response, dict):
417
- for v in response.values():
418
- if isinstance(v, list) and v:
419
- candidate_lists.append(v)
420
- break
421
-
422
- text_pieces = []
423
- for lst in candidate_lists:
424
- for item in lst:
425
- if not item:
426
- continue
427
- if isinstance(item, dict):
428
- for k in ("content", "text", "message", "output_text", "output"):
429
- t = item.get(k)
430
- if t:
431
- text_pieces.append(str(t).strip())
432
- break
433
- else:
434
- if "content" in item and isinstance(item["content"], list):
435
- for part in item["content"]:
436
- if isinstance(part, dict):
437
- t = part.get("text") or part.get("content")
438
- if t:
439
- text_pieces.append(str(t).strip())
440
- elif isinstance(part, str):
441
- text_pieces.append(part.strip())
442
- elif isinstance(item, str):
443
- text_pieces.append(item.strip())
444
- else:
445
- try:
446
- t = getattr(item, "text", None) or getattr(item, "content", None)
447
- if t:
448
- text_pieces.append(str(t).strip())
449
- except Exception:
450
- pass
451
-
452
- if not text_pieces and isinstance(response, dict):
453
- for k in ("text", "message", "output_text"):
454
- v = response.get(k)
455
- if v:
456
- text_pieces.append(str(v).strip())
457
- break
458
-
459
- seen = set()
460
- filtered = []
461
- for t in text_pieces:
462
- if not isinstance(t, str):
463
- continue
464
- if t and t not in seen:
465
- filtered.append(t)
466
- seen.add(t)
467
- return "\n\n".join(filtered).strip()
468
-
469
- # ---- small helpers for safer tracebacks ----
470
- def safe_traceback(max_chars=2000):
471
- tb = traceback.format_exc()
472
- return tb if len(tb) <= max_chars else tb[:max_chars] + "\n...[truncated]"
473
-
474
- def scrub_api_keys(s: str) -> str:
475
- if not s:
476
- return s
477
- key = get_effective_api_key()
478
- if key and key in s:
479
- return s.replace(key, "[REDACTED_API_KEY]")
480
- return s
481
-
482
- # ---- Layout ----
483
  col1, col2 = st.columns([1, 3])
484
  with col1:
485
  generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
@@ -499,12 +354,16 @@ if st.sidebar.button("Load Video", use_container_width=True):
499
  except Exception:
500
  st.session_state["file_hash"] = None
501
  except Exception as e:
502
- logging.exception("Failed to load video")
503
  st.sidebar.error(f"Failed to load video: {e}")
504
 
505
  if st.session_state["videos"]:
 
 
506
  try:
507
- st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
 
 
 
508
  except Exception:
509
  st.sidebar.write("Couldn't preview video")
510
 
@@ -530,7 +389,7 @@ if st.session_state["videos"]:
530
  except Exception:
531
  pass
532
 
533
- # ---- Main generation flow ----
534
  if generate_now and not st.session_state.get("busy"):
535
  if not st.session_state.get("videos"):
536
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
@@ -545,7 +404,7 @@ if generate_now and not st.session_state.get("busy"):
545
  if HAS_GENAI and genai is not None:
546
  genai.configure(api_key=key_to_use)
547
  except Exception:
548
- logging.exception("genai.configure failed at start")
549
 
550
  model_id = (st.session_state.get("model_input") or model_selected or DEFAULT_MODEL).strip()
551
  if st.session_state.get("last_model") != model_id:
@@ -559,7 +418,6 @@ if generate_now and not st.session_state.get("busy"):
559
  except Exception:
560
  current_hash = None
561
 
562
- # determine if reupload is needed: same local path + same hash + we have uploaded/processed file id
563
  reupload_needed = True
564
  uploaded_file = st.session_state.get("uploaded_file")
565
  uploaded_name = file_name_or_id(uploaded_file)
@@ -576,8 +434,7 @@ if generate_now and not st.session_state.get("busy"):
576
  try:
577
  uploaded = upload_video_sdk(upload_path)
578
  except Exception as e:
579
- err = scrub_api_keys(f"Upload failed: {e}\n\nTraceback:\n{safe_traceback()}")
580
- st.session_state["last_error"] = err
581
  st.error("Upload failed. See Last Error for details.")
582
  raise
583
 
@@ -586,15 +443,13 @@ if generate_now and not st.session_state.get("busy"):
586
  processing_bar = processing_placeholder.progress(0)
587
  start_time = time.time()
588
  processed = wait_for_processed(uploaded, timeout=st.session_state.get("processing_timeout", 900))
589
- # update progress once after wait (full incremental requires moving polling here)
590
  elapsed = time.time() - start_time
591
  timeout = st.session_state.get("processing_timeout", 900)
592
  pct = min(100, int((elapsed / timeout) * 100)) if timeout > 0 else 0
593
  processing_bar.progress(pct)
594
  processing_placeholder.success("Processing complete")
595
  except Exception as e:
596
- err = scrub_api_keys(f"Processing failed/wait timeout: {e}\n\nTraceback:\n{safe_traceback()}")
597
- st.session_state["last_error"] = err
598
  st.error("Video processing failed or timed out. See Last Error.")
599
  raise
600
 
@@ -621,14 +476,11 @@ if generate_now and not st.session_state.get("busy"):
621
  agent_response = agent.run(prompt_text, videos=[processed], safety_settings=safety_settings)
622
  agent_text = getattr(agent_response, "content", None) or getattr(agent_response, "outputText", None) or None
623
  if not agent_text:
624
- try:
625
- if isinstance(agent_response, dict):
626
- for k in ("content", "outputText", "text", "message"):
627
- if k in agent_response and agent_response[k]:
628
- agent_text = agent_response[k]
629
- break
630
- except Exception:
631
- pass
632
  if agent_text and str(agent_text).strip():
633
  out = str(agent_text).strip()
634
  debug_info["agent_ok"] = True
@@ -637,15 +489,92 @@ if generate_now and not st.session_state.get("busy"):
637
  debug_info["agent_ok"] = False
638
  except Exception as ae:
639
  debug_info["agent_error"] = f"{ae}"
640
- debug_info["agent_traceback"] = traceback.format_exc()
641
 
642
  if not out:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  try:
644
  with st.spinner("Generating description via Responses API..."):
645
  out = generate_via_responses_api(prompt_text, processed, model_used, max_tokens=max_tokens, timeout=st.session_state.get("generation_timeout", 300))
646
  except Exception as e:
647
- tb = traceback.format_exc()
648
- st.session_state["last_error"] = scrub_api_keys(f"Responses API error: {e}\n\nDebug: {debug_info}\n\nTraceback:\n{safe_traceback()}")
649
  st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
650
  out = ""
651
 
@@ -669,18 +598,14 @@ if generate_now and not st.session_state.get("busy"):
669
  st.caption(f"Est. max tokens: {est_tokens}")
670
 
671
  except Exception as e:
672
- tb = traceback.format_exc()
673
- err = scrub_api_keys(f"{e}\n\nDebug: {locals().get('debug_info', {})}\n\nTraceback:\n{safe_traceback()}")
674
- st.session_state["last_error"] = err
675
  st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
676
  finally:
677
  st.session_state["busy"] = False
678
 
679
  if st.session_state.get("analysis_out"):
680
- just_loaded_same = (st.session_state.get("last_loaded_path") == st.session_state.get("videos"))
681
- if not just_loaded_same:
682
- st.subheader("Analysis Result")
683
- st.markdown(st.session_state.get("analysis_out"))
684
 
685
  if st.session_state.get("last_error"):
686
  with st.expander("Last Error", expanded=False):
 
 
1
  import os
2
  import time
3
  import string
 
5
  import traceback
6
  from glob import glob
7
  from pathlib import Path
 
 
 
8
 
9
  import yt_dlp
10
+ import ffmpeg # ffmpeg-python
11
  import streamlit as st
12
  from dotenv import load_dotenv
13
 
14
  load_dotenv()
15
 
16
+ # Optional PHI integration (kept guarded)
17
  try:
18
  from phi.agent import Agent
19
  from phi.model.google import Gemini
 
24
  Agent = Gemini = DuckDuckGo = None
25
  HAS_PHI = False
26
 
27
+ # google.generativeai SDK (guarded)
28
  try:
29
  import google.generativeai as genai
30
  from google.generativeai import upload_file, get_file
 
35
  upload_file = get_file = None
36
  HAS_GENAI = False
37
 
 
 
38
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
39
  DATA_DIR = Path("./data")
40
  DATA_DIR.mkdir(exist_ok=True)
 
66
  st.session_state.setdefault("file_hash", None)
67
  st.session_state.setdefault("api_key", os.getenv("GOOGLE_API_KEY", ""))
68
  st.session_state.setdefault("last_model", "")
 
 
69
  st.session_state.setdefault("processing_timeout", 900)
70
  st.session_state.setdefault("generation_timeout", 300)
71
  st.session_state.setdefault("compress_threshold_mb", 200)
 
91
  return target_path
92
  try:
93
  ffmpeg.input(video_path).output(target_path).run(overwrite_output=True, quiet=True)
94
+ except Exception:
95
+ # re-raise so caller can handle
 
96
  raise
97
+ # remove source only if different and successful
98
  if os.path.exists(target_path) and os.path.getsize(target_path) > 0:
99
  try:
100
  if str(Path(video_path).resolve()) != str(Path(target_path).resolve()):
101
  os.remove(video_path)
102
  except Exception:
103
+ pass
104
  return target_path
105
 
106
  def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
 
110
  ).run(overwrite_output=True, quiet=True)
111
  if os.path.exists(target_path) and os.path.getsize(target_path) > 0:
112
  return target_path
 
113
  return input_path
114
  except Exception:
 
115
  return input_path
116
 
117
  def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
 
120
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
121
  ydl_opts = {"outtmpl": outtmpl, "format": "best"}
122
  if video_password:
 
123
  ydl_opts["videopassword"] = video_password
124
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
125
  info = ydl.extract_info(url, download=True)
126
+ candidates = []
 
127
  if isinstance(info, dict):
 
128
  entries = info.get("entries")
129
  if entries:
 
130
  for e in entries:
131
  if isinstance(e, dict) and e.get("id"):
132
+ candidates.append(str(Path(save_dir) / f"{e['id']}.mp4"))
133
  else:
134
  vid = info.get("id")
135
  ext = info.get("ext") or "mp4"
136
  if vid:
137
+ candidates.append(str(Path(save_dir) / f"{vid}.{ext}"))
138
+ if not candidates:
 
139
  all_files = glob(os.path.join(save_dir, "*"))
140
  if not all_files:
141
  raise FileNotFoundError("Downloaded video not found")
142
+ chosen = sorted(all_files, key=os.path.getmtime, reverse=True)[0]
 
143
  else:
144
+ existing = [p for p in candidates if os.path.exists(p)]
145
+ chosen = existing[0] if existing else sorted(glob(os.path.join(save_dir, "*")), key=os.path.getmtime, reverse=True)[0]
 
 
146
  final = convert_video_to_mp4(chosen)
147
  return final
148
 
 
151
  return None
152
  if isinstance(file_obj, dict):
153
  return file_obj.get("name") or file_obj.get("id")
 
154
  return getattr(file_obj, "name", None) or getattr(file_obj, "id", None) or getattr(file_obj, "fileId", None)
155
 
156
  def get_effective_api_key():
 
163
  try:
164
  genai.configure(api_key=key)
165
  except Exception:
166
+ pass
167
  return True
168
 
169
  # ---- Agent management (reuse) ----
 
181
  _agent = Agent(name="Video AI summarizer", model=Gemini(id=model_id), tools=[DuckDuckGo()], markdown=True)
182
  st.session_state["last_model"] = model_id
183
  except Exception:
 
184
  _agent = None
185
  return _agent
186
 
 
196
  try:
197
  os.remove(f)
198
  except Exception:
199
+ pass
200
 
201
  # Reset when URL changes
202
  current_url = st.session_state.get("url", "")
203
+ if current_url != st.session_state.get("last_loaded_path"):
204
  clear_all_video_state()
205
+ st.session_state["last_loaded_path"] = current_url
206
 
207
  # ---- Sidebar UI ----
208
  st.sidebar.header("Video Input")
 
214
  model_input = settings_exp.text_input("Custom model id", value=DEFAULT_MODEL, key="model_input")
215
  model_selected = model_input.strip() or DEFAULT_MODEL
216
  else:
 
217
  st.session_state["model_input"] = model_choice
218
  model_selected = model_choice
219
 
 
238
  )
239
 
240
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
241
+ settings_exp.caption(f"Using API key from: {key_source}")
242
  if not get_effective_api_key():
243
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
244
 
 
257
  if not HAS_GENAI or upload_file is None:
258
  raise RuntimeError("google.generativeai SDK not available; cannot upload")
259
  genai.configure(api_key=key)
 
260
  return upload_file(filepath)
261
 
262
  def wait_for_processed(file_obj, timeout: int = None):
 
272
  while True:
273
  try:
274
  obj = get_file(name)
275
+ except Exception:
276
  if time.time() - start > timeout:
277
+ raise TimeoutError("Failed to fetch file status before timeout")
278
  time.sleep(backoff)
279
  backoff = min(backoff * 2, 8.0)
280
  continue
 
284
  return obj
285
 
286
  if time.time() - start > timeout:
287
+ raise TimeoutError("File processing timed out")
288
  time.sleep(backoff)
289
  backoff = min(backoff * 2, 8.0)
290
 
 
294
  a = " ".join(prompt.strip().lower().split())
295
  b_full = text.strip()
296
  b = " ".join(b_full[:check_len].lower().split())
297
+ try:
298
+ from difflib import SequenceMatcher
299
+ ratio = SequenceMatcher(None, a, b).ratio()
300
+ except Exception:
301
+ ratio = 0.0
302
  if ratio >= ratio_threshold:
303
  cut = min(len(b_full), max(int(len(prompt) * 0.9), len(a)))
304
  new_text = b_full[cut:].lstrip(" \n:-")
 
314
  def compress_video_if_large(local_path: str, threshold_mb: int = 200):
315
  try:
316
  file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
317
+ except Exception:
318
+ st.session_state["last_error"] = "Failed to stat file before compression"
319
  return local_path, False
320
 
321
  if file_size_mb <= threshold_mb:
322
  return local_path, False
323
 
 
324
  p = Path(local_path)
325
  compressed_name = f"{p.stem}_compressed.mp4"
326
  compressed_path = str(p.with_name(compressed_name))
 
330
  if result and os.path.exists(result) and os.path.getsize(result) > 0:
331
  return result, True
332
  return local_path, False
333
+ except Exception:
334
+ st.session_state["last_error"] = "Video compression failed"
335
  return local_path, False
336
 
337
+ # ---- Simple layout ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  col1, col2 = st.columns([1, 3])
339
  with col1:
340
  generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
 
354
  except Exception:
355
  st.session_state["file_hash"] = None
356
  except Exception as e:
 
357
  st.sidebar.error(f"Failed to load video: {e}")
358
 
359
  if st.session_state["videos"]:
360
+ path = st.session_state["videos"]
361
+ # ensure mp4 for preview and read bytes for reliable preview
362
  try:
363
+ mp4_path = convert_video_to_mp4(path)
364
+ with open(mp4_path, "rb") as vf:
365
+ video_bytes = vf.read()
366
+ st.sidebar.video(video_bytes, format="video/mp4", start_time=0)
367
  except Exception:
368
  st.sidebar.write("Couldn't preview video")
369
 
 
389
  except Exception:
390
  pass
391
 
392
+ # ---- Generation flow (minimal) ----
393
  if generate_now and not st.session_state.get("busy"):
394
  if not st.session_state.get("videos"):
395
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
 
404
  if HAS_GENAI and genai is not None:
405
  genai.configure(api_key=key_to_use)
406
  except Exception:
407
+ pass
408
 
409
  model_id = (st.session_state.get("model_input") or model_selected or DEFAULT_MODEL).strip()
410
  if st.session_state.get("last_model") != model_id:
 
418
  except Exception:
419
  current_hash = None
420
 
 
421
  reupload_needed = True
422
  uploaded_file = st.session_state.get("uploaded_file")
423
  uploaded_name = file_name_or_id(uploaded_file)
 
434
  try:
435
  uploaded = upload_video_sdk(upload_path)
436
  except Exception as e:
437
+ st.session_state["last_error"] = f"Upload failed: {e}"
 
438
  st.error("Upload failed. See Last Error for details.")
439
  raise
440
 
 
443
  processing_bar = processing_placeholder.progress(0)
444
  start_time = time.time()
445
  processed = wait_for_processed(uploaded, timeout=st.session_state.get("processing_timeout", 900))
 
446
  elapsed = time.time() - start_time
447
  timeout = st.session_state.get("processing_timeout", 900)
448
  pct = min(100, int((elapsed / timeout) * 100)) if timeout > 0 else 0
449
  processing_bar.progress(pct)
450
  processing_placeholder.success("Processing complete")
451
  except Exception as e:
452
+ st.session_state["last_error"] = f"Processing failed/wait timeout: {e}"
 
453
  st.error("Video processing failed or timed out. See Last Error.")
454
  raise
455
 
 
476
  agent_response = agent.run(prompt_text, videos=[processed], safety_settings=safety_settings)
477
  agent_text = getattr(agent_response, "content", None) or getattr(agent_response, "outputText", None) or None
478
  if not agent_text:
479
+ if isinstance(agent_response, dict):
480
+ for k in ("content", "outputText", "text", "message"):
481
+ if k in agent_response and agent_response[k]:
482
+ agent_text = agent_response[k]
483
+ break
 
 
 
484
  if agent_text and str(agent_text).strip():
485
  out = str(agent_text).strip()
486
  debug_info["agent_ok"] = True
 
489
  debug_info["agent_ok"] = False
490
  except Exception as ae:
491
  debug_info["agent_error"] = f"{ae}"
 
492
 
493
  if not out:
494
+ # Use Responses API directly
495
+ def generate_via_responses_api(prompt_text: str, processed, model_used: str, max_tokens: int = 1024, timeout: int = 300):
496
+ key = get_effective_api_key()
497
+ if not key:
498
+ raise RuntimeError("No API key provided")
499
+ if not HAS_GENAI or genai is None:
500
+ raise RuntimeError("Responses API not available")
501
+ genai.configure(api_key=key)
502
+ fname = file_name_or_id(processed)
503
+ if not fname:
504
+ raise RuntimeError("Uploaded file missing name/id")
505
+
506
+ system_msg = {"role": "system", "content": prompt_text}
507
+ user_msg = {"role": "user", "content": "Please summarize the attached video."}
508
+
509
+ call_variants = [
510
+ {"messages": [system_msg, user_msg], "files": [{"name": fname}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
511
+ {"input": [{"text": prompt_text, "files": [{"name": fname}]}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
512
+ ]
513
+
514
+ last_exc = None
515
+ start = time.time()
516
+ backoff = 1.0
517
+ while True:
518
+ for payload in call_variants:
519
+ try:
520
+ response = genai.responses.generate(model=model_used, **payload)
521
+ return _normalize_genai_response(response)
522
+ except Exception as e:
523
+ last_exc = e
524
+ msg = str(e).lower()
525
+ if any(k in msg for k in ("internal", "unavailable", "deadlineexceeded", "deadline exceeded", "timeout", "rate limit")):
526
+ continue
527
+ raise
528
+ if time.time() - start > timeout:
529
+ raise TimeoutError("Responses.generate timed out")
530
+ time.sleep(backoff)
531
+ backoff = min(backoff * 2, 8.0)
532
+
533
+ def _normalize_genai_response(response):
534
+ outputs = []
535
+ if response is None:
536
+ return ""
537
+ # response may be SDK object or dict-like; coerce to string chunks
538
+ text_pieces = []
539
+ try:
540
+ if isinstance(response, dict):
541
+ for key in ("output", "candidates", "items", "responses", "choices"):
542
+ val = response.get(key)
543
+ if isinstance(val, list) and val:
544
+ for item in val:
545
+ if isinstance(item, dict):
546
+ for k in ("content", "text", "message", "output_text", "output"):
547
+ t = item.get(k)
548
+ if t:
549
+ text_pieces.append(str(t).strip())
550
+ break
551
+ elif isinstance(item, str):
552
+ text_pieces.append(item.strip())
553
+ if not text_pieces:
554
+ for k in ("text", "message", "output_text"):
555
+ v = response.get(k)
556
+ if v:
557
+ text_pieces.append(str(v).strip())
558
+ break
559
+ else:
560
+ text_pieces.append(str(response))
561
+ except Exception:
562
+ text_pieces.append(str(response))
563
+ seen = set()
564
+ filtered = []
565
+ for t in text_pieces:
566
+ if not isinstance(t, str):
567
+ continue
568
+ if t and t not in seen:
569
+ filtered.append(t)
570
+ seen.add(t)
571
+ return "\n\n".join(filtered).strip()
572
+
573
  try:
574
  with st.spinner("Generating description via Responses API..."):
575
  out = generate_via_responses_api(prompt_text, processed, model_used, max_tokens=max_tokens, timeout=st.session_state.get("generation_timeout", 300))
576
  except Exception as e:
577
+ st.session_state["last_error"] = f"Responses API error: {e}\nDebug: {debug_info}"
 
578
  st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
579
  out = ""
580
 
 
598
  st.caption(f"Est. max tokens: {est_tokens}")
599
 
600
  except Exception as e:
601
+ st.session_state["last_error"] = f"{e}"
 
 
602
  st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
603
  finally:
604
  st.session_state["busy"] = False
605
 
606
  if st.session_state.get("analysis_out"):
607
+ st.subheader("Analysis Result")
608
+ st.markdown(st.session_state.get("analysis_out"))
 
 
609
 
610
  if st.session_state.get("last_error"):
611
  with st.expander("Last Error", expanded=False):