CB commited on
Commit
15568fb
·
verified ·
1 Parent(s): 7e44dbd

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +126 -455
streamlit_app.py CHANGED
@@ -1,9 +1,11 @@
1
  # streamlit_app.py
 
2
  import os
3
  import time
4
  import string
5
  import hashlib
6
  import traceback
 
7
  from glob import glob
8
  from pathlib import Path
9
 
@@ -28,13 +30,9 @@ except Exception:
28
  # google.generativeai SDK (guarded)
29
  try:
30
  import google.generativeai as genai
31
- # older SDK exposed upload_file/get_file at top-level; try to bind them if present
32
- upload_file = getattr(genai, "upload_file", None)
33
- get_file = getattr(genai, "get_file", None)
34
  HAS_GENAI = True
35
  except Exception:
36
  genai = None
37
- upload_file = get_file = None
38
  HAS_GENAI = False
39
 
40
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
@@ -147,84 +145,109 @@ def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) ->
147
  final = convert_video_to_mp4(chosen)
148
  return final
149
 
150
- def file_name_or_id(file_obj):
151
- if file_obj is None:
152
- return None
153
- if isinstance(file_obj, dict):
154
- return file_obj.get("name") or file_obj.get("id") or file_obj.get("fileId")
155
- # SDK objects might expose .name, .id, .fileId, or nested fields
156
- for attr in ("name", "id", "fileId", "file_id"):
157
- v = getattr(file_obj, attr, None)
158
- if v:
159
- return v
160
- # sometimes SDK returns {'file': {...}}
161
  try:
162
- if hasattr(file_obj, "to_dict"):
163
- d = file_obj.to_dict()
164
- if isinstance(d, dict):
165
- return d.get("name") or d.get("id") or d.get("fileId")
166
  except Exception:
167
- pass
168
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- def get_effective_api_key():
171
- return st.session_state.get("api_key") or os.getenv("GOOGLE_API_KEY")
 
172
 
173
- def configure_genai_if_needed():
174
- key = get_effective_api_key()
175
- if not key:
176
- return False
177
  try:
178
- genai.configure(api_key=key)
 
 
 
179
  except Exception:
180
- pass
181
- return True
182
-
183
- # ---- Agent management (reuse) ----
184
- _agent = None
185
- def maybe_create_agent(model_id: str):
186
- global _agent
187
- key = get_effective_api_key()
188
- if not (HAS_PHI and HAS_GENAI and key):
189
- _agent = None
190
- return None
191
- if _agent and st.session_state.get("last_model") == model_id:
192
- return _agent
 
 
 
 
 
193
  try:
194
- genai.configure(api_key=key)
195
- _agent = Agent(name="Video AI summarizer", model=Gemini(id=model_id), tools=[DuckDuckGo()], markdown=True)
196
- st.session_state["last_model"] = model_id
 
 
 
197
  except Exception:
198
- _agent = None
199
- return _agent
200
-
201
- def clear_all_video_state():
202
- st.session_state.pop("uploaded_file", None)
203
- st.session_state.pop("processed_file", None)
204
- st.session_state["videos"] = ""
205
- st.session_state["last_local_path"] = ""
206
- st.session_state["analysis_out"] = ""
207
- st.session_state["last_error"] = ""
208
- st.session_state["file_hash"] = None
209
- for f in glob(str(DATA_DIR / "*")):
210
- try:
211
- os.remove(f)
212
- except Exception:
213
- pass
214
 
215
  # Reset when URL changes (compare against last_url only)
216
  current_url = st.session_state.get("url", "")
217
  if current_url != st.session_state.get("last_url"):
218
  if st.session_state.get("last_url"):
219
- clear_all_video_state()
 
 
 
 
 
 
 
 
 
 
 
 
220
  st.session_state["last_url"] = current_url
221
 
222
- # ---- Sidebar UI ----
223
  st.sidebar.header("Video Input")
224
  st.sidebar.text_input("Video URL", key="url", placeholder="https://")
225
 
226
  settings_exp = st.sidebar.expander("Settings", expanded=False)
227
- model_choice = settings_exp.selectbox("Select model", options=MODEL_OPTIONS, index=MODEL_OPTIONS.index(DEFAULT_MODEL) if DEFAULT_MODEL in MODEL_OPTIONS else 0)
 
228
  if model_choice == "custom":
229
  model_input = settings_exp.text_input("Custom model id", value=DEFAULT_MODEL, key="model_input")
230
  model_selected = model_input.strip() or DEFAULT_MODEL
@@ -254,191 +277,13 @@ settings_exp.number_input(
254
 
255
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
256
  settings_exp.caption(f"Using API key from: {key_source}")
257
- if not get_effective_api_key():
258
- settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
259
-
260
- safety_settings = [
261
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
262
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
263
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
264
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
265
- ]
266
-
267
- # ---- Upload & processing helpers ----
268
- def _upload_with_kwargs(fn, filepath):
269
- """
270
- Call an upload function `fn` with the required ragStoreName argument.
271
- Most recent google.generativeai SDK versions expect:
272
- upload_file(path, ragStoreName="default")
273
- Older versions accept just the path, so we try both signatures.
274
- """
275
- try:
276
- # Preferred modern signature
277
- return fn(filepath, ragStoreName="default")
278
- except TypeError:
279
- # Fallback to older signature (no ragStoreName)
280
- return fn(filepath)
281
- except Exception as e:
282
- # Propagate any other error for higher‑level handling
283
- raise e
284
-
285
-
286
- def upload_video_sdk(filepath: str):
287
- """Upload a video file using whichever upload function the SDK provides."""
288
- key = get_effective_api_key()
289
- if not key:
290
- raise RuntimeError("No API key provided")
291
- if not HAS_GENAI or genai is None:
292
- raise RuntimeError("google.generativeai SDK not available; cannot upload")
293
-
294
- genai.configure(api_key=key)
295
-
296
- # Build a list of possible upload callables (newest first)
297
- candidate_calls = []
298
-
299
- # Newer SDK exposes upload_file directly
300
- if upload_file:
301
- candidate_calls.append(("upload_file", upload_file))
302
-
303
- # Some installations expose it under ragstore or files namespaces
304
- candidate_calls.append(
305
- ("genai.ragstore.upload_file",
306
- getattr(genai, "ragstore", None) and getattr(genai.ragstore, "upload_file", None))
307
- )
308
- candidate_calls.append(
309
- ("genai.files.upload",
310
- getattr(genai, "files", None) and getattr(genai.files, "upload", None))
311
- )
312
- candidate_calls.append(
313
- ("genai.upload",
314
- getattr(genai, "upload", None))
315
- )
316
-
317
- # Filter out any None entries
318
- candidate_calls = [(name, fn) for name, fn in candidate_calls if fn]
319
-
320
- if not candidate_calls:
321
- raise RuntimeError("No upload function available in google.generativeai SDK")
322
-
323
- last_exc = None
324
- for name, fn in candidate_calls:
325
- try:
326
- # Use the wrapper that injects ragStoreName when needed
327
- res = _upload_with_kwargs(fn, filepath)
328
- # Normalise the return value so the rest of the code works
329
- return _normalize_uploaded_obj(res)
330
- except Exception as e:
331
- last_exc = e
332
- # Log the attempted method for debugging (optional)
333
- st.session_state["last_error"] = f"Upload attempt '{name}' failed: {e}"
334
- continue
335
-
336
- # If we get here every method failed
337
- raise RuntimeError(f"All upload methods failed. Last error: {last_exc}")
338
-
339
- def wait_for_processed(file_obj, timeout: int = None):
340
- if timeout is None:
341
- timeout = st.session_state.get("processing_timeout", 900)
342
- if not HAS_GENAI or genai is None:
343
- return file_obj
344
- start = time.time()
345
- name = file_name_or_id(file_obj)
346
- # If no determinable name/id, just return file_obj
347
- if not name:
348
- return file_obj
349
- genai.configure(api_key=get_effective_api_key())
350
- backoff = 1.0
351
- last_exc = None
352
- # prefer get_file if present, otherwise try genai.ragstore.get or genai.files.get
353
- candidate_getters = []
354
- if get_file:
355
- candidate_getters.append(("get_file", get_file))
356
- candidate_getters.append(("genai.ragstore.get", getattr(genai, "ragstore", None) and getattr(genai.ragstore, "get", None)))
357
- candidate_getters.append(("genai.files.get", getattr(genai, "files", None) and getattr(genai.files, "get", None)))
358
- candidate_getters = [(n, fn) for n, fn in candidate_getters if fn]
359
- # If none, return file_obj immediately
360
- if not candidate_getters:
361
- return file_obj
362
-
363
- while True:
364
- for name_label, getter in candidate_getters:
365
- try:
366
- obj = getter(name)
367
- # normalize possible SDK object/dict
368
- # check for state attribute or dict field
369
- state = None
370
- if isinstance(obj, dict):
371
- state = obj.get("state") or (obj.get("status") and {"name": obj.get("status")})
372
- else:
373
- state = getattr(obj, "state", None) or getattr(obj, "status", None)
374
- if not state:
375
- return obj
376
- # state might be dict or object with .name
377
- state_name = state.get("name") if isinstance(state, dict) else getattr(state, "name", None)
378
- if state_name and state_name.upper() == "PROCESSING":
379
- # still processing; continue polling
380
- last_obj = obj
381
- break
382
- return obj
383
- except Exception as e:
384
- last_exc = e
385
- # transient errors: backoff and retry until timeout
386
- continue
387
- if time.time() - start > timeout:
388
- raise TimeoutError(f"File processing timed out. Last error: {last_exc}")
389
- time.sleep(backoff)
390
- backoff = min(backoff * 2, 8.0)
391
-
392
- def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
393
- if not prompt or not text:
394
- return text
395
- a = " ".join(prompt.strip().lower().split())
396
- b_full = text.strip()
397
- b = " ".join(b_full[:check_len].lower().split())
398
- try:
399
- from difflib import SequenceMatcher
400
- ratio = SequenceMatcher(None, a, b).ratio()
401
- except Exception:
402
- ratio = 0.0
403
- if ratio >= ratio_threshold:
404
- cut = min(len(b_full), max(int(len(prompt) * 0.9), len(a)))
405
- new_text = b_full[cut:].lstrip(" \n:-")
406
- if len(new_text) >= 3:
407
- return new_text
408
- placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
409
- low = b_full.strip().lower()
410
- for ph in placeholders:
411
- if low.startswith(ph):
412
- return b_full[len(ph):].lstrip(" \n:-")
413
- return text
414
-
415
- def compress_video_if_large(local_path: str, threshold_mb: int = 200):
416
- try:
417
- file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
418
- except Exception:
419
- st.session_state["last_error"] = "Failed to stat file before compression"
420
- return local_path, False
421
-
422
- if file_size_mb <= threshold_mb:
423
- return local_path, False
424
-
425
- p = Path(local_path)
426
- compressed_name = f"{p.stem}_compressed.mp4"
427
- compressed_path = str(p.with_name(compressed_name))
428
-
429
- try:
430
- result = compress_video(local_path, compressed_path, crf=28, preset="fast")
431
- if result and os.path.exists(result) and os.path.getsize(result) > 0:
432
- return result, True
433
- return local_path, False
434
- except Exception:
435
- st.session_state["last_error"] = "Video compression failed"
436
- return local_path, False
437
 
438
- # ---- Simple layout ----
439
  col1, col2 = st.columns([1, 3])
440
  with col1:
441
- generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
442
  with col2:
443
  if not st.session_state.get("videos"):
444
  st.info("Load a video first (sidebar) to enable generation.", icon="ℹ️")
@@ -453,10 +298,7 @@ if st.sidebar.button("Load Video", use_container_width=True):
453
  st.session_state["last_local_path"] = path
454
  st.session_state.pop("uploaded_file", None)
455
  st.session_state.pop("processed_file", None)
456
- try:
457
- st.session_state["file_hash"] = file_sha256(path)
458
- except Exception:
459
- st.session_state["file_hash"] = None
460
  except Exception as e:
461
  st.sidebar.error(f"Failed to load video: {e}")
462
 
@@ -475,11 +317,25 @@ if st.session_state["videos"]:
475
  st.session_state["loop_video"] = loop_checkbox
476
 
477
  if st.button("Clear Video(s)"):
478
- clear_all_video_state()
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  try:
481
  with open(st.session_state["videos"], "rb") as vf:
482
- st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["videos"]), mime="video/mp4", use_container_width=True)
 
 
483
  except Exception:
484
  st.sidebar.error("Failed to prepare download")
485
 
@@ -488,226 +344,41 @@ if st.session_state["videos"]:
488
  file_size_mb = os.path.getsize(st.session_state["videos"]) / (1024 * 1024)
489
  st.sidebar.caption(f"File size: {file_size_mb:.1f} MB")
490
  if file_size_mb > st.session_state.get("compress_threshold_mb", 200):
491
- st.sidebar.warning(f"Large file detected — it will be compressed automatically before upload (>{st.session_state.get('compress_threshold_mb')} MB).", icon="⚠️")
 
 
 
492
  except Exception:
493
  pass
494
 
495
- # ---- Generation flow (fixed and robust) ----
496
  if generate_now and not st.session_state.get("busy"):
497
  if not st.session_state.get("videos"):
498
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
499
  else:
500
- key_to_use = get_effective_api_key()
501
- if not key_to_use:
502
  st.error("Google API key not set.")
503
  else:
 
504
  try:
505
- st.session_state["busy"] = True
506
- try:
507
- if HAS_GENAI and genai is not None:
508
- genai.configure(api_key=key_to_use)
509
- except Exception:
510
- pass
511
-
512
  model_id = (st.session_state.get("model_input") or model_selected or DEFAULT_MODEL).strip()
513
- if st.session_state.get("last_model") != model_id:
514
- st.session_state["last_model"] = ""
515
- maybe_create_agent(model_id)
516
-
517
- processed = st.session_state.get("processed_file")
518
- current_path = st.session_state.get("videos")
519
- try:
520
- current_hash = file_sha256(current_path) if current_path and os.path.exists(current_path) else None
521
- except Exception:
522
- current_hash = None
523
-
524
- reupload_needed = True
525
- uploaded_file = st.session_state.get("uploaded_file")
526
- uploaded_name = file_name_or_id(uploaded_file)
527
- if processed and st.session_state.get("last_local_path") == current_path and st.session_state.get("file_hash") == current_hash and uploaded_name:
528
- reupload_needed = False
529
-
530
- if reupload_needed:
531
- if not HAS_GENAI:
532
- raise RuntimeError("google.generativeai SDK not available; install it.")
533
- local_path = current_path
534
- upload_path, compressed = compress_video_if_large(local_path, threshold_mb=st.session_state.get("compress_threshold_mb", 200))
535
-
536
- with st.spinner(f"Uploading video{' (compressed)' if compressed else ''}..."):
537
- try:
538
- uploaded = upload_video_sdk(upload_path)
539
- except Exception as e:
540
- st.session_state["last_error"] = f"Upload failed for {upload_path}: {e}\n{traceback.format_exc()}"
541
- st.error(f"Upload failed: {e}. Check the error log for more details.")
542
- st.session_state["busy"] = False
543
- raise
544
-
545
- try:
546
- processing_placeholder = st.empty()
547
- processing_bar = processing_placeholder.progress(0)
548
- start_time = time.time()
549
- processed = wait_for_processed(uploaded, timeout=st.session_state.get("processing_timeout", 900))
550
- elapsed = time.time() - start_time
551
- timeout = st.session_state.get("processing_timeout", 900)
552
- pct = min(100, int((elapsed / timeout) * 100)) if timeout > 0 else 0
553
- processing_bar.progress(pct)
554
- processing_placeholder.success("Processing complete")
555
- except Exception as e:
556
- st.session_state["last_error"] = f"Processing failed/wait timeout: {e}"
557
- st.error("Video processing failed or timed out. See Last Error.")
558
- st.session_state["busy"] = False
559
- raise
560
-
561
- st.session_state["uploaded_file"] = uploaded
562
- st.session_state["processed_file"] = processed
563
- st.session_state["last_local_path"] = current_path
564
- st.session_state["file_hash"] = current_hash
565
-
566
  prompt_text = (analysis_prompt.strip() or DEFAULT_PROMPT).strip()
567
- out = ""
568
- model_used = model_id
569
- max_tokens = 2048 if "2.5" in model_used else 1024
570
- est_tokens = max_tokens
571
-
572
- # Try Agent first, fallback to Responses API
573
- agent = maybe_create_agent(model_used)
574
- debug_info = {"agent_attempted": False, "agent_ok": False, "agent_error": None, "agent_response_has_text": False}
575
- if agent:
576
- debug_info["agent_attempted"] = True
577
- try:
578
- with st.spinner("Generating description via Agent..."):
579
- if not processed:
580
- raise RuntimeError("Processed file missing for agent generation")
581
- agent_response = agent.run(prompt_text, videos=[processed], safety_settings=safety_settings)
582
- agent_text = getattr(agent_response, "content", None) or getattr(agent_response, "outputText", None) or None
583
- if not agent_text:
584
- if isinstance(agent_response, dict):
585
- for k in ("content", "outputText", "text", "message"):
586
- if k in agent_response and agent_response[k]:
587
- agent_text = agent_response[k]
588
- break
589
- if agent_text and str(agent_text).strip():
590
- out = str(agent_text).strip()
591
- debug_info["agent_ok"] = True
592
- debug_info["agent_response_has_text"] = True
593
- else:
594
- debug_info["agent_ok"] = False
595
- except Exception as ae:
596
- debug_info["agent_error"] = f"{ae}"
597
-
598
- if not out:
599
- def generate_via_responses_api(prompt_text: str, processed, model_used: str, max_tokens: int = 1024, timeout: int = 300):
600
- key = get_effective_api_key()
601
- if not key:
602
- raise RuntimeError("No API key provided")
603
- if not HAS_GENAI or genai is None:
604
- raise RuntimeError("Responses API not available")
605
- genai.configure(api_key=key)
606
- fname = file_name_or_id(processed)
607
- if not fname:
608
- raise RuntimeError("Uploaded file missing name/id")
609
-
610
- system_msg = {"role": "system", "content": prompt_text}
611
- user_msg = {"role": "user", "content": "Please summarize the attached video."}
612
-
613
- call_variants = [
614
- {"messages": [system_msg, user_msg], "files": [{"name": fname}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
615
- {"input": [{"text": prompt_text, "files": [{"name": fname}]}], "safety_settings": safety_settings, "max_output_tokens": max_tokens},
616
- ]
617
-
618
- last_exc = None
619
- start = time.time()
620
- backoff = 1.0
621
- while True:
622
- for payload in call_variants:
623
- try:
624
- response = genai.responses.generate(model=model_used, **payload)
625
- return _normalize_genai_response(response)
626
- except Exception as e:
627
- last_exc = e
628
- msg = str(e).lower()
629
- if any(k in msg for k in ("internal", "unavailable", "deadlineexceeded", "deadline exceeded", "timeout", "rate limit")):
630
- continue
631
- raise
632
- if time.time() - start > timeout:
633
- raise TimeoutError("Responses.generate timed out")
634
- time.sleep(backoff)
635
- backoff = min(backoff * 2, 8.0)
636
-
637
- def _normalize_genai_response(response):
638
- outputs = []
639
- if response is None:
640
- return ""
641
- text_pieces = []
642
- try:
643
- if isinstance(response, dict):
644
- for key in ("output", "candidates", "items", "responses", "choices"):
645
- val = response.get(key)
646
- if isinstance(val, list) and val:
647
- for item in val:
648
- if isinstance(item, dict):
649
- for k in ("content", "text", "message", "output_text", "output"):
650
- t = item.get(k)
651
- if t:
652
- text_pieces.append(str(t).strip())
653
- break
654
- elif isinstance(item, str):
655
- text_pieces.append(item.strip())
656
- if not text_pieces:
657
- for k in ("text", "message", "output_text"):
658
- v = response.get(k)
659
- if v:
660
- text_pieces.append(str(v).strip())
661
- break
662
- else:
663
- # SDK object: try attributes
664
- for attr in ("output", "candidates", "text", "message", "content"):
665
- v = getattr(response, attr, None)
666
- if v:
667
- text_pieces.append(str(v).strip())
668
- if not text_pieces:
669
- text_pieces.append(str(response))
670
- except Exception:
671
- text_pieces.append(str(response))
672
- seen = set()
673
- filtered = []
674
- for t in text_pieces:
675
- if not isinstance(t, str):
676
- continue
677
- if t and t not in seen:
678
- filtered.append(t)
679
- seen.add(t)
680
- return "\n\n".join(filtered).strip()
681
-
682
- try:
683
- with st.spinner("Generating description via Responses API..."):
684
- out = generate_via_responses_api(prompt_text, processed, model_used, max_tokens=max_tokens, timeout=st.session_state.get("generation_timeout", 300))
685
- except Exception as e:
686
- st.session_state["last_error"] = f"Responses API error: {e}\nDebug: {debug_info}"
687
- st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
688
- out = ""
689
-
690
- if out:
691
- out = remove_prompt_echo(prompt_text, out)
692
- p = prompt_text
693
- if p and out.strip().lower().startswith(p.lower()):
694
- out = out.strip()[len(p):].lstrip(" \n:-")
695
- placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
696
- low = out.strip().lower()
697
- for ph in placeholders:
698
- if low.startswith(ph):
699
- out = out.strip()[len(ph):].lstrip(" \n:-")
700
- break
701
- out = out.strip()
702
 
 
 
 
 
 
 
 
703
  st.session_state["analysis_out"] = out
704
  st.session_state["last_error"] = ""
705
  st.subheader("Analysis Result")
706
- st.markdown(out if out else "No analysis returned.")
707
- st.caption(f"Est. max tokens: {est_tokens}")
708
-
709
  except Exception as e:
710
- st.session_state["last_error"] = f"{e}\n{traceback.format_exc()}"
711
- st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
712
  finally:
713
  st.session_state["busy"] = False
 
1
  # streamlit_app.py
2
+
3
  import os
4
  import time
5
  import string
6
  import hashlib
7
  import traceback
8
+ import base64
9
  from glob import glob
10
  from pathlib import Path
11
 
 
30
  # google.generativeai SDK (guarded)
31
  try:
32
  import google.generativeai as genai
 
 
 
33
  HAS_GENAI = True
34
  except Exception:
35
  genai = None
 
36
  HAS_GENAI = False
37
 
38
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
 
145
  final = convert_video_to_mp4(chosen)
146
  return final
147
 
148
+ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
149
+ if not prompt or not text:
150
+ return text
151
+ a = " ".join(prompt.strip().lower().split())
152
+ b_full = text.strip()
153
+ b = " ".join(b_full[:check_len].lower().split())
 
 
 
 
 
154
  try:
155
+ from difflib import SequenceMatcher
156
+ ratio = SequenceMatcher(None, a, b).ratio()
 
 
157
  except Exception:
158
+ ratio = 0.0
159
+ if ratio >= ratio_threshold:
160
+ cut = min(len(b_full), max(int(len(prompt) * 0.9), len(a)))
161
+ new_text = b_full[cut:].lstrip(" \n:-")
162
+ if len(new_text) >= 3:
163
+ return new_text
164
+ placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
165
+ low = b_full.strip().lower()
166
+ for ph in placeholders:
167
+ if low.startswith(ph):
168
+ return b_full[len(ph):].lstrip(" \n:-")
169
+ return text
170
+
171
+ def compress_video_if_large(local_path: str, threshold_mb: int = 200):
172
+ try:
173
+ file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
174
+ except Exception:
175
+ st.session_state["last_error"] = "Failed to stat file before compression"
176
+ return local_path, False
177
+
178
+ if file_size_mb <= threshold_mb:
179
+ return local_path, False
180
 
181
+ p = Path(local_path)
182
+ compressed_name = f"{p.stem}_compressed.mp4"
183
+ compressed_path = str(p.with_name(compressed_name))
184
 
 
 
 
 
185
  try:
186
+ result = compress_video(local_path, compressed_path, crf=28, preset="fast")
187
+ if result and os.path.exists(result) and os.path.getsize(result) > 0:
188
+ return result, True
189
+ return local_path, False
190
  except Exception:
191
+ st.session_state["last_error"] = "Video compression failed"
192
+ return local_path, False
193
+
194
+ # ---- Inline-video generation via base64 (bypass upload) ----
195
+ def generate_with_inline_video(local_path: str, prompt: str, model_used: str, timeout: int = 300):
196
+ # Read the video bytes
197
+ with open(local_path, "rb") as f:
198
+ video_bytes = f.read()
199
+ b64 = base64.b64encode(video_bytes).decode("utf-8")
200
+ video_part = {
201
+ "inline_data": {
202
+ "mime_type": "video/mp4",
203
+ "data": b64
204
+ }
205
+ }
206
+ contents = [prompt, video_part]
207
+
208
+ # Use new client API if present
209
  try:
210
+ client = genai.Client()
211
+ resp = client.models.generate_content(
212
+ model=model_used,
213
+ contents=contents,
214
+ generation_config={"max_output_tokens": 1024}
215
+ )
216
  except Exception:
217
+ # Fallback older style
218
+ resp = genai.GenerativeModel(model_used).generate_content(contents)
219
+
220
+ text = getattr(resp, "text", None) or getattr(resp, "output_text", None) or str(resp)
221
+ return text
222
+
223
+ # ---- Main UI & logic ----
 
 
 
 
 
 
 
 
 
224
 
225
  # Reset when URL changes (compare against last_url only)
226
  current_url = st.session_state.get("url", "")
227
  if current_url != st.session_state.get("last_url"):
228
  if st.session_state.get("last_url"):
229
+ # clear old video state
230
+ st.session_state.pop("uploaded_file", None)
231
+ st.session_state.pop("processed_file", None)
232
+ st.session_state["videos"] = ""
233
+ st.session_state["last_local_path"] = ""
234
+ st.session_state["analysis_out"] = ""
235
+ st.session_state["last_error"] = ""
236
+ st.session_state["file_hash"] = None
237
+ for f in glob(str(DATA_DIR / "*")):
238
+ try:
239
+ os.remove(f)
240
+ except Exception:
241
+ pass
242
  st.session_state["last_url"] = current_url
243
 
244
+ # Sidebar UI
245
  st.sidebar.header("Video Input")
246
  st.sidebar.text_input("Video URL", key="url", placeholder="https://")
247
 
248
  settings_exp = st.sidebar.expander("Settings", expanded=False)
249
+ model_choice = settings_exp.selectbox("Select model", options=MODEL_OPTIONS,
250
+ index=MODEL_OPTIONS.index(DEFAULT_MODEL) if DEFAULT_MODEL in MODEL_OPTIONS else 0)
251
  if model_choice == "custom":
252
  model_input = settings_exp.text_input("Custom model id", value=DEFAULT_MODEL, key="model_input")
253
  model_selected = model_input.strip() or DEFAULT_MODEL
 
277
 
278
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
279
  settings_exp.caption(f"Using API key from: {key_source}")
280
+ if not st.session_state.get("api_key") and not os.getenv("GOOGLE_API_KEY"):
281
+ settings_exp.warning("No Google API key provided; generation disabled.", icon="⚠️")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ # Buttons & layout
284
  col1, col2 = st.columns([1, 3])
285
  with col1:
286
+ generate_now = st.button("Generate the story", type="primary")
287
  with col2:
288
  if not st.session_state.get("videos"):
289
  st.info("Load a video first (sidebar) to enable generation.", icon="ℹ️")
 
298
  st.session_state["last_local_path"] = path
299
  st.session_state.pop("uploaded_file", None)
300
  st.session_state.pop("processed_file", None)
301
+ st.session_state["file_hash"] = file_sha256(path)
 
 
 
302
  except Exception as e:
303
  st.sidebar.error(f"Failed to load video: {e}")
304
 
 
317
  st.session_state["loop_video"] = loop_checkbox
318
 
319
  if st.button("Clear Video(s)"):
320
+ # Clear video state
321
+ st.session_state.pop("uploaded_file", None)
322
+ st.session_state.pop("processed_file", None)
323
+ st.session_state["videos"] = ""
324
+ st.session_state["last_local_path"] = ""
325
+ st.session_state["analysis_out"] = ""
326
+ st.session_state["last_error"] = ""
327
+ st.session_state["file_hash"] = None
328
+ for f in glob(str(DATA_DIR / "*")):
329
+ try:
330
+ os.remove(f)
331
+ except Exception:
332
+ pass
333
 
334
  try:
335
  with open(st.session_state["videos"], "rb") as vf:
336
+ st.download_button("Download Video", data=vf,
337
+ file_name=sanitize_filename(st.session_state["videos"]),
338
+ mime="video/mp4", use_container_width=True)
339
  except Exception:
340
  st.sidebar.error("Failed to prepare download")
341
 
 
344
  file_size_mb = os.path.getsize(st.session_state["videos"]) / (1024 * 1024)
345
  st.sidebar.caption(f"File size: {file_size_mb:.1f} MB")
346
  if file_size_mb > st.session_state.get("compress_threshold_mb", 200):
347
+ st.sidebar.warning(
348
+ f"Large file detected — it may exceed inline size limits (>{st.session_state.get('compress_threshold_mb')} MB).",
349
+ icon="⚠️"
350
+ )
351
  except Exception:
352
  pass
353
 
354
+ # Generation / analysis
355
  if generate_now and not st.session_state.get("busy"):
356
  if not st.session_state.get("videos"):
357
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
358
  else:
359
+ key = st.session_state.get("api_key") or os.getenv("GOOGLE_API_KEY")
360
+ if not key:
361
  st.error("Google API key not set.")
362
  else:
363
+ st.session_state["busy"] = True
364
  try:
365
+ genai.configure(api_key=key)
 
 
 
 
 
 
366
  model_id = (st.session_state.get("model_input") or model_selected or DEFAULT_MODEL).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  prompt_text = (analysis_prompt.strip() or DEFAULT_PROMPT).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
+ with st.spinner("Generating analysis (inline video)…"):
370
+ out = generate_with_inline_video(
371
+ st.session_state["videos"], prompt_text, model_id,
372
+ timeout=st.session_state.get("generation_timeout", 300)
373
+ )
374
+
375
+ out = remove_prompt_echo(prompt_text, out)
376
  st.session_state["analysis_out"] = out
377
  st.session_state["last_error"] = ""
378
  st.subheader("Analysis Result")
379
+ st.markdown(out or "No analysis returned.")
 
 
380
  except Exception as e:
381
+ st.session_state["last_error"] = f"Inline generation error: {e}\n{traceback.format_exc()}"
382
+ st.error("An error occurred while generating the story using inline video. Check the error log.")
383
  finally:
384
  st.session_state["busy"] = False