CB commited on
Commit
33d86c2
·
verified ·
1 Parent(s): 9783989

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +472 -0
streamlit_app.py CHANGED
@@ -1,3 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # --- patched responses / generate compatibility layer ---
2
  import json
3
  import requests
@@ -157,3 +456,176 @@ def extract_text_from_response(response):
157
  pass
158
  return None
159
  # --- end patched section ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import hashlib
4
+ from glob import glob
5
+ from pathlib import Path
6
+ from tempfile import NamedTemporaryFile
7
+
8
+ import yt_dlp
9
+ import ffmpeg
10
+ import streamlit as st
11
+ from dotenv import load_dotenv
12
+
13
+ load_dotenv()
14
+
15
+ st.set_page_config(page_title="Generate the story of videos", layout="wide")
16
+ DATA_DIR = Path("./data")
17
+ DATA_DIR.mkdir(exist_ok=True)
18
+
19
+ for k, v in {
20
+ "videos": "",
21
+ "loop_video": False,
22
+ "uploaded_file": None,
23
+ "processed_file": None,
24
+ "busy": False,
25
+ "last_loaded_path": "",
26
+ "analysis_out": "",
27
+ "last_error": "",
28
+ "file_hash": None,
29
+ "fast_mode": False,
30
+ "use_compression": True,
31
+ }.items():
32
+ st.session_state.setdefault(k, v)
33
+
34
+ def sanitize_filename(path_str: str):
35
+ return Path(path_str).name.lower().translate(str.maketrans("", "", "!?\"'`~@#$%^&*()[]{}<>:,;\\/|+=*")).replace(" ", "_")
36
+
37
+ def file_sha256(path: str, block_size: int = 65536) -> str:
38
+ h = hashlib.sha256()
39
+ with open(path, "rb") as f:
40
+ for chunk in iter(lambda: f.read(block_size), b""):
41
+ h.update(chunk)
42
+ return h.hexdigest()
43
+
44
+ def safe_ffmpeg_run(stream_cmd):
45
+ try:
46
+ stream_cmd.run(overwrite_output=True, capture_stdout=True, capture_stderr=True)
47
+ return True, ""
48
+ except ffmpeg.Error as e:
49
+ try:
50
+ return False, e.stderr.decode("utf-8", errors="ignore")
51
+ except Exception:
52
+ return False, str(e)
53
+
54
+ def convert_video_to_mp4(video_path: str) -> str:
55
+ target = Path(video_path).with_suffix(".mp4")
56
+ if target.exists():
57
+ return str(target)
58
+ tmp = NamedTemporaryFile(prefix=target.stem + "_", suffix=".mp4", delete=False, dir=target.parent)
59
+ tmp.close()
60
+ ok, err = safe_ffmpeg_run(ffmpeg.input(video_path).output(str(tmp.name)))
61
+ if not ok:
62
+ try:
63
+ os.remove(tmp.name)
64
+ except Exception:
65
+ pass
66
+ raise RuntimeError(f"ffmpeg conversion failed: {err}")
67
+ os.replace(tmp.name, str(target))
68
+ if Path(video_path).suffix.lower() != ".mp4":
69
+ try:
70
+ os.remove(video_path)
71
+ except Exception:
72
+ pass
73
+ return str(target)
74
+
75
+ def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
76
+ tmp = NamedTemporaryFile(prefix=Path(target_path).stem + "_", suffix=".mp4", delete=False, dir=Path(target_path).parent)
77
+ tmp.close()
78
+ ok, err = safe_ffmpeg_run(ffmpeg.input(input_path).output(str(tmp.name), vcodec="libx264", crf=crf, preset=preset))
79
+ if not ok:
80
+ try:
81
+ os.remove(tmp.name)
82
+ except Exception:
83
+ pass
84
+ return input_path
85
+ os.replace(tmp.name, target_path)
86
+ return target_path
87
+
88
+ def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
89
+ if not url:
90
+ raise ValueError("No URL provided")
91
+ outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
92
+ opts = {"outtmpl": outtmpl, "format": "best"}
93
+ if video_password:
94
+ opts["videopassword"] = video_password
95
+ with yt_dlp.YoutubeDL(opts) as ydl:
96
+ info = ydl.extract_info(url, download=True)
97
+ video_id = info.get("id") if isinstance(info, dict) else None
98
+ if video_id:
99
+ matches = glob(os.path.join(save_dir, f"{video_id}.*"))
100
+ else:
101
+ matches = sorted(glob(os.path.join(save_dir, "*")), key=os.path.getmtime, reverse=True)[:1]
102
+ if not matches:
103
+ raise FileNotFoundError("Downloaded video not found")
104
+ return convert_video_to_mp4(matches[0])
105
+
106
+ def file_name_or_id(file_obj):
107
+ if not file_obj:
108
+ return None
109
+ if isinstance(file_obj, dict):
110
+ for key in ("name", "id", "fileId", "file_id", "uri", "url"):
111
+ val = file_obj.get(key)
112
+ if val:
113
+ s = str(val)
114
+ if s.startswith("http://") or s.startswith("https://"):
115
+ tail = s.rstrip("/").split("/")[-1]
116
+ return tail if tail.startswith("files/") else f"files/{tail}"
117
+ if s.startswith("files/"):
118
+ return s
119
+ if "/" not in s and 6 <= len(s) <= 128:
120
+ return f"files/{s}"
121
+ return s
122
+ uri = file_obj.get("uri") or file_obj.get("url")
123
+ if uri:
124
+ tail = str(uri).rstrip("/").split("/")[-1]
125
+ return tail if tail.startswith("files/") else f"files/{tail}"
126
+ return None
127
+ for attr in ("name", "id", "fileId", "file_id", "uri", "url"):
128
+ val = getattr(file_obj, attr, None)
129
+ if val:
130
+ s = str(val)
131
+ if s.startswith("http://") or s.startswith("https://"):
132
+ tail = s.rstrip("/").split("/")[-1]
133
+ return tail if tail.startswith("files/") else f"files/{tail}"
134
+ if s.startswith("files/"):
135
+ return s
136
+ if "/" not in s and 6 <= len(s) <= 128:
137
+ return f"files/{s}"
138
+ return s
139
+ s = str(file_obj)
140
+ if "http://" in s or "https://" in s:
141
+ tail = s.rstrip("/").split("/")[-1]
142
+ return tail if tail.startswith("files/") else f"files/{tail}"
143
+ if "files/" in s:
144
+ idx = s.find("files/")
145
+ return s[idx:] if s[idx:].startswith("files/") else f"files/{s[idx+6:]}"
146
+ return None
147
+
148
+ HAS_GENAI = False
149
+ genai = None
150
+ upload_file = None
151
+ get_file = None
152
+ delete_file = None
153
+ if os.getenv("GOOGLE_API_KEY"):
154
+ try:
155
+ import google.generativeai as genai_mod
156
+ genai = genai_mod
157
+ upload_file = getattr(genai_mod, "upload_file", None)
158
+ get_file = getattr(genai_mod, "get_file", None)
159
+ delete_file = getattr(genai_mod, "delete_file", None)
160
+ HAS_GENAI = True
161
+ except Exception:
162
+ HAS_GENAI = False
163
+
164
+ def upload_video_sdk(filepath: str):
165
+ key = get_runtime_api_key()
166
+ if not key:
167
+ raise RuntimeError("No API key")
168
+ if not HAS_GENAI or upload_file is None:
169
+ raise RuntimeError("google.generativeai SDK upload not available")
170
+ genai.configure(api_key=key)
171
+ return upload_file(filepath)
172
+
173
+ def wait_for_processed(file_obj, timeout=600):
174
+ if not HAS_GENAI or get_file is None:
175
+ return file_obj
176
+ start = time.time()
177
+ name = file_name_or_id(file_obj)
178
+ if not name:
179
+ return file_obj
180
+ backoff = 1.0
181
+ while True:
182
+ try:
183
+ obj = get_file(name)
184
+ except Exception:
185
+ obj = file_obj
186
+ state = getattr(obj, "state", None)
187
+ if not state or getattr(state, "name", None) != "PROCESSING":
188
+ return obj
189
+ if time.time() - start > timeout:
190
+ raise TimeoutError("File processing timed out")
191
+ time.sleep(backoff)
192
+ backoff = min(backoff * 2, 8.0)
193
+
194
+ def remove_prompt_echo(prompt: str, text: str):
195
+ if not prompt or not text:
196
+ return text
197
+ p = " ".join(prompt.strip().lower().split())
198
+ t = text.strip()
199
+ from difflib import SequenceMatcher
200
+ first = " ".join(t[:600].lower().split())
201
+ if SequenceMatcher(None, p, first).ratio() > 0.7:
202
+ cut = min(len(t), max(int(len(prompt) * 0.9), len(p)))
203
+ new = t[cut:].lstrip(" \n:-")
204
+ if len(new) >= 3:
205
+ return new
206
+ placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
207
+ low = t.lower()
208
+ for ph in placeholders:
209
+ if low.startswith(ph):
210
+ return t[len(ph):].lstrip(" \n:-")
211
+ return text
212
+
213
+ st.sidebar.header("Video Input")
214
+ st.sidebar.text_input("Video URL", key="url", placeholder="https://")
215
+ settings = st.sidebar.expander("Settings", expanded=False)
216
+
217
+ env_key = os.getenv("GOOGLE_API_KEY", "")
218
+ API_KEY_INPUT = settings.text_input("Google API Key (one-time)", value="", type="password")
219
+ model_input = settings.text_input("Gemini Model (short name)", "gemini-2.0-flash-lite")
220
+ model_id = model_input.strip() or "gemini-2.0-flash-lite"
221
+ model_arg = model_id if not model_id.startswith("models/") else model_id.split("/", 1)[1]
222
+
223
+ default_prompt = (
224
+ "You are an Indoor Human Behavior Analyzer. Watch the video and produce a detailed, evidence‑based behavioral report focused on human actions, "
225
+ "interactions, posture, movement, anatomy, and apparent intent. Use vivid, anatomically rich language and avoid moralizing. Prefer short paragraphs and numeric estimates "
226
+ "for anatomical measurements. Provide sensory, subjective descriptions and vivid imagery, including a concise summary of observed actions and a description of behaviors "
227
+ "and interaction dynamics. Use the following personality‑traits list when inferring dispositions: driven by an insatiable desire to understand human behavior and anatomy. "
228
+ "Finish with a short feedback and recommendations section. Adopt a playful, anatomically obsessed, slightly mischievous persona — inquisitive, pragmatic, and vivid in description."
229
+ )
230
+
231
+ analysis_prompt = settings.text_area("Enter analysis", value=default_prompt, height=300)
232
+ settings.text_input("Video Password (if needed)", key="video-password", type="password")
233
+ settings.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
234
+ settings.checkbox("Enable compression for large files (>50MB)", value=True, key="use_compression")
235
+ settings.number_input("Max output tokens", key="max_output_tokens", value=1024, min_value=128, max_value=8192, step=128)
236
+
237
+ if not API_KEY_INPUT and not env_key:
238
+ settings.info("No Google API key provided; upload/generation disabled.", icon="ℹ️")
239
+
240
+ if st.sidebar.button("Load Video", use_container_width=True):
241
+ try:
242
+ vpw = st.session_state.get("video-password", "")
243
+ path = download_video_ytdlp(st.session_state.get("url", ""), str(DATA_DIR), vpw)
244
+ st.session_state["videos"] = path
245
+ st.session_state["last_loaded_path"] = path
246
+ st.session_state["uploaded_file"] = None
247
+ st.session_state["processed_file"] = None
248
+ st.session_state["file_hash"] = file_sha256(path)
249
+ except Exception as e:
250
+ st.sidebar.error(f"Failed to load video: {e}")
251
+
252
+ if st.session_state["videos"]:
253
+ try:
254
+ st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
255
+ except Exception:
256
+ st.sidebar.write("Couldn't preview video")
257
+ with st.sidebar.expander("Options", expanded=False):
258
+ loop_checkbox = st.checkbox("Enable Loop", value=st.session_state.get("loop_video", False))
259
+ st.session_state["loop_video"] = loop_checkbox
260
+
261
+ if st.button("Clear Video(s)"):
262
+ for f in glob(str(DATA_DIR / "*")):
263
+ try:
264
+ os.remove(f)
265
+ except Exception:
266
+ pass
267
+ for k in ("uploaded_file", "processed_file"):
268
+ st.session_state.pop(k, None)
269
+ st.session_state["videos"] = ""
270
+ st.session_state["last_loaded_path"] = ""
271
+ st.session_state["analysis_out"] = ""
272
+ st.session_state["last_error"] = ""
273
+ st.session_state["file_hash"] = None
274
+
275
+ try:
276
+ with open(st.session_state["videos"], "rb") as vf:
277
+ st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["videos"]), mime="video/mp4", use_container_width=True)
278
+ except Exception:
279
+ pass
280
+ st.sidebar.write("Title:", Path(st.session_state["videos"]).name)
281
+
282
+ col1, col2 = st.columns([1, 3])
283
+ with col1:
284
+ if st.session_state.get("busy"):
285
+ st.write("Generation in progress...")
286
+ if st.button("Cancel"):
287
+ st.session_state["busy"] = False
288
+ st.session_state["last_error"] = "Generation cancelled by user."
289
+ else:
290
+ generate_now = st.button("Generate the story", type="primary")
291
+ with col2:
292
+ pass
293
+
294
+ def get_runtime_api_key():
295
+ key = API_KEY_INPUT.strip() if API_KEY_INPUT else ""
296
+ if key:
297
+ return key
298
+ return os.getenv("GOOGLE_API_KEY", "").strip() or None
299
+
300
  # --- patched responses / generate compatibility layer ---
301
  import json
302
  import requests
 
456
  pass
457
  return None
458
  # --- end patched section ---
459
+
460
+ if (st.session_state.get("busy") is False) and ('generate_now' in locals() and generate_now):
461
+ if not st.session_state.get("videos"):
462
+ st.error("No video loaded. Use 'Load Video' in the sidebar.")
463
+ else:
464
+ runtime_key = get_runtime_api_key()
465
+ if not runtime_key:
466
+ st.error("Google API key not set. Provide in Settings or set GOOGLE_API_KEY in environment.")
467
+ else:
468
+ try:
469
+ st.session_state["busy"] = True
470
+ processed = st.session_state.get("processed_file")
471
+ current_path = st.session_state.get("videos")
472
+ try:
473
+ current_hash = file_sha256(current_path) if current_path and Path(current_path).exists() else None
474
+ except Exception:
475
+ current_hash = None
476
+
477
+ reupload_needed = True
478
+ if processed and st.session_state.get("last_loaded_path") == current_path and st.session_state.get("file_hash") == current_hash:
479
+ reupload_needed = False
480
+
481
+ upload_path = current_path
482
+ uploaded = st.session_state.get("uploaded_file")
483
+ if reupload_needed:
484
+ local_path = current_path
485
+ fast_mode = st.session_state.get("fast_mode", False)
486
+ try:
487
+ file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
488
+ except Exception:
489
+ file_size_mb = 0
490
+
491
+ use_compression = st.session_state.get("use_compression", True)
492
+ if use_compression and not fast_mode and file_size_mb > 50:
493
+ compressed_path = str(Path(local_path).with_name(Path(local_path).stem + "_compressed.mp4"))
494
+ try:
495
+ preset = "veryfast" if fast_mode else "fast"
496
+ upload_path = compress_video(local_path, compressed_path, crf=28, preset=preset)
497
+ except Exception:
498
+ upload_path = local_path
499
+
500
+ if HAS_GENAI and upload_file is not None:
501
+ genai.configure(api_key=runtime_key)
502
+ with st.spinner("Uploading video..."):
503
+ uploaded = upload_video_sdk(upload_path)
504
+ processed = wait_for_processed(uploaded, timeout=600)
505
+ st.session_state["uploaded_file"] = uploaded
506
+ st.session_state["processed_file"] = processed
507
+ st.session_state["last_loaded_path"] = current_path
508
+ st.session_state["file_hash"] = current_hash
509
+ else:
510
+ uploaded = None
511
+ processed = None
512
+ st.session_state["uploaded_file"] = None
513
+ st.session_state["processed_file"] = None
514
+ else:
515
+ uploaded = st.session_state.get("uploaded_file")
516
+ processed = st.session_state.get("processed_file")
517
+
518
+ prompt_text = (analysis_prompt or default_prompt).strip()
519
+ if st.session_state.get("fast_mode"):
520
+ model_used = model_arg or "gemini-2.0-flash-lite"
521
+ max_tokens = min(st.session_state.get("max_output_tokens", 512), 1024)
522
+ else:
523
+ model_used = model_arg
524
+ max_tokens = st.session_state.get("max_output_tokens", 1024)
525
+
526
+ system_msg = {"role": "system", "content": "You are a helpful assistant that summarizes videos concisely in vivid detail."}
527
+ user_msg = {"role": "user", "content": prompt_text}
528
+
529
+ fname = file_name_or_id(processed) or file_name_or_id(uploaded)
530
+ response = call_responses_once(model_used, system_msg, user_msg, fname, max_tokens)
531
+
532
+ out = extract_text_from_response(response)
533
+
534
+ meta = getattr(response, "metrics", None) or (response.get("metrics") if isinstance(response, dict) else None) or {}
535
+ output_tokens = 0
536
+ try:
537
+ if isinstance(meta, dict):
538
+ output_tokens = int(meta.get("output_tokens", 0) or 0)
539
+ else:
540
+ output_tokens = int(getattr(meta, "output_tokens", 0) or 0)
541
+ except Exception:
542
+ output_tokens = 0
543
+
544
+ if (not out or output_tokens == 0) and model_used:
545
+ retry_prompt = "Summarize the video content briefly and vividly (2-4 paragraphs)."
546
+ try:
547
+ response2 = call_responses_once(model_used, system_msg, {"role": "user", "content": retry_prompt}, fname, min(max_tokens * 2, 4096))
548
+ out2 = extract_text_from_response(response2)
549
+ if out2 and len(out2) > len(out or ""):
550
+ out = out2
551
+ else:
552
+ response3 = call_responses_once(model_used, system_msg, {"role": "user", "content": "List the main points of the video as 6-10 bullets."}, fname, min(1024, max_tokens * 2))
553
+ out3 = extract_text_from_response(response3)
554
+ if out3:
555
+ out = out3
556
+ except Exception:
557
+ pass
558
+
559
+ if out:
560
+ out = remove_prompt_echo(prompt_text, out).strip()
561
+
562
+ st.session_state["analysis_out"] = out or ""
563
+ st.session_state["last_error"] = ""
564
+
565
+ st.subheader("Analysis Result")
566
+ st.markdown(out or "_(no text returned)_")
567
+
568
+ try:
569
+ if reupload_needed:
570
+ if upload_path and Path(upload_path).exists() and Path(upload_path) != Path(current_path):
571
+ Path(upload_path).unlink(missing_ok=True)
572
+ Path(current_path).unlink(missing_ok=True)
573
+ st.session_state["videos"] = ""
574
+ except Exception:
575
+ pass
576
+
577
+ with st.expander("Debug (compact)", expanded=False):
578
+ try:
579
+ info = {
580
+ "model": model_used,
581
+ "output_tokens": output_tokens,
582
+ "upload_succeeded": bool(st.session_state.get("uploaded_file")),
583
+ "processed_state": getattr(st.session_state.get("processed_file"), "state", None) if st.session_state.get("processed_file") else None,
584
+ }
585
+ st.write(info)
586
+ try:
587
+ if isinstance(response, dict):
588
+ keys = list(response.keys())[:20]
589
+ else:
590
+ keys = [k for k in dir(response) if not k.startswith("_")][:20]
591
+ st.write({"response_keys_or_attrs": keys})
592
+ except Exception:
593
+ pass
594
+ except Exception:
595
+ st.write("Debug info unavailable")
596
+
597
+ except Exception as e:
598
+ st.session_state["last_error"] = str(e)
599
+ st.error(f"An error occurred while generating the story: {e}")
600
+ finally:
601
+ st.session_state["busy"] = False
602
+
603
+ if st.session_state.get("analysis_out"):
604
+ st.subheader("Analysis Result")
605
+ st.markdown(st.session_state.get("analysis_out"))
606
+
607
+ if st.session_state.get("last_error"):
608
+ with st.expander("Last Error", expanded=False):
609
+ st.write(st.session_state.get("last_error"))
610
+
611
+ with st.sidebar.expander("Manage uploads", expanded=False):
612
+ if st.button("Delete uploaded files (local + cloud)"):
613
+ for f in glob(str(DATA_DIR / "*")):
614
+ try:
615
+ Path(f).unlink(missing_ok=True)
616
+ except Exception:
617
+ pass
618
+ st.session_state["videos"] = ""
619
+ st.session_state["uploaded_file"] = None
620
+ st.session_state["processed_file"] = None
621
+ st.session_state["last_loaded_path"] = ""
622
+ st.session_state["analysis_out"] = ""
623
+ st.session_state["file_hash"] = None
624
+ try:
625
+ fname = file_name_or_id(st.session_state.get("uploaded_file"))
626
+ if fname and delete_file and HAS_GENAI:
627
+ genai.configure(api_key=get_runtime_api_key() or os.getenv("GOOGLE_API_KEY", ""))
628
+ delete_file(fname)
629
+ except Exception:
630
+ pass
631
+ st.success("Local files removed. Cloud deletion attempted where supported.")