CB commited on
Commit
ef36655
·
verified ·
1 Parent(s): eb3dff7

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +342 -234
streamlit_app.py CHANGED
@@ -1,53 +1,58 @@
1
  # streamlit_app.py
2
  import os
3
  import time
4
- import hashlib
 
5
  from glob import glob
6
  from pathlib import Path
7
- from tempfile import NamedTemporaryFile
 
8
 
9
- import ffmpeg
10
  import yt_dlp
 
11
  import streamlit as st
12
  from dotenv import load_dotenv
13
 
14
  load_dotenv()
15
 
16
- # Require google.generativeai SDK
17
  try:
18
- import google.generativeai as genai # type: ignore
19
- except Exception as e:
20
- st.error("Missing required dependency: google.generativeai. Install it and restart.")
21
- raise
22
-
23
- # ensure SDK helpers exist
24
- upload_file = getattr(genai, "upload_file", None)
25
- get_file = getattr(genai, "get_file", None)
26
- delete_file = getattr(genai, "delete_file", None)
27
- if upload_file is None:
28
- st.error("google.generativeai SDK installed but upload_file is not available in this version.")
29
- raise RuntimeError("upload_file missing")
30
-
31
- st.set_page_config(page_title="Video → Story (GenAI)", layout="wide")
32
- DATA_DIR = Path("data")
33
- DATA_DIR.mkdir(exist_ok=True)
34
-
35
- # session defaults
36
- for k, v in {
37
- "video_path": "",
38
- "uploaded_file": None,
39
- "processed_file": None,
40
- "busy": False,
41
- "file_hash": None,
42
- "fast_mode": False,
43
- "use_compression": True,
44
- }.items():
45
- st.session_state.setdefault(k, v)
46
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def sanitize_filename(path_str: str):
49
- return Path(path_str).name.lower().replace(" ", "_")
50
-
51
 
52
  def file_sha256(path: str, block_size: int = 65536) -> str:
53
  h = hashlib.sha256()
@@ -56,82 +61,159 @@ def file_sha256(path: str, block_size: int = 65536) -> str:
56
  h.update(chunk)
57
  return h.hexdigest()
58
 
59
-
60
- def safe_ffmpeg_run(cmd):
61
- try:
62
- cmd.run(overwrite_output=True, capture_stdout=True, capture_stderr=True)
63
- return True, ""
64
- except ffmpeg.Error as e:
65
- try:
66
- return False, e.stderr.decode(errors="ignore")
67
- except Exception:
68
- return False, str(e)
69
-
70
-
71
  def convert_video_to_mp4(video_path: str) -> str:
72
- target = Path(video_path).with_suffix(".mp4")
73
- if target.exists():
74
- return str(target)
75
- tmp = NamedTemporaryFile(prefix=target.stem + "_", suffix=".mp4", delete=False, dir=target.parent)
76
- tmp.close()
77
- ok, err = safe_ffmpeg_run(ffmpeg.input(video_path).output(str(tmp.name)))
78
- if not ok:
79
- raise RuntimeError(f"ffmpeg conversion failed: {err}")
80
- os.replace(tmp.name, str(target))
81
- if Path(video_path).suffix.lower() != ".mp4":
82
- try:
83
- Path(video_path).unlink(missing_ok=True)
84
- except Exception:
85
- pass
86
- return str(target)
87
-
88
 
89
  def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
90
- tmp = NamedTemporaryFile(prefix=Path(target_path).stem + "_", suffix=".mp4", delete=False, dir=Path(target_path).parent)
91
- tmp.close()
92
- ok, err = safe_ffmpeg_run(ffmpeg.input(input_path).output(str(tmp.name), vcodec="libx264", crf=crf, preset=preset))
93
- if not ok:
94
  return input_path
95
- os.replace(tmp.name, target_path)
96
- return target_path
97
-
98
 
99
  def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
100
  if not url:
101
  raise ValueError("No URL provided")
102
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
103
- opts = {"outtmpl": outtmpl, "format": "best"}
104
  if video_password:
105
- opts["videopassword"] = video_password
106
- with yt_dlp.YoutubeDL(opts) as ydl:
107
  info = ydl.extract_info(url, download=True)
108
  video_id = info.get("id") if isinstance(info, dict) else None
109
  if video_id:
110
- matches = glob(str(Path(save_dir) / f"{video_id}.*"))
111
  else:
112
- matches = sorted(glob(str(Path(save_dir) / "*")), key=os.path.getmtime, reverse=True)[:1]
113
  if not matches:
114
  raise FileNotFoundError("Downloaded video not found")
115
  return convert_video_to_mp4(matches[0])
116
 
117
-
118
  def file_name_or_id(file_obj):
119
- if not file_obj:
120
  return None
121
  if isinstance(file_obj, dict):
122
- for key in ("name", "id", "fileId", "file_id", "uri", "url"):
123
- v = file_obj.get(key)
124
- if v:
125
- return str(v)
126
- for attr in ("name", "id", "fileId", "file_id", "uri", "url"):
127
- v = getattr(file_obj, attr, None)
128
- if v:
129
- return str(v)
130
- return str(file_obj)
131
-
132
-
133
- def wait_for_processed(file_obj, timeout=600):
134
- if get_file is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  return file_obj
136
  start = time.time()
137
  name = file_name_or_id(file_obj)
@@ -139,10 +221,7 @@ def wait_for_processed(file_obj, timeout=600):
139
  return file_obj
140
  backoff = 1.0
141
  while True:
142
- try:
143
- obj = get_file(name)
144
- except Exception:
145
- obj = file_obj
146
  state = getattr(obj, "state", None)
147
  if not state or getattr(state, "name", None) != "PROCESSING":
148
  return obj
@@ -151,168 +230,197 @@ def wait_for_processed(file_obj, timeout=600):
151
  time.sleep(backoff)
152
  backoff = min(backoff * 2, 8.0)
153
 
154
-
155
- # UI
156
- st.sidebar.header("Input")
157
- st.sidebar.text_input("Video URL", key="url", placeholder="https://")
158
- settings = st.sidebar.expander("Settings", expanded=True)
159
- API_KEY_INPUT = settings.text_input("Google API Key (one-time)", value=os.getenv("GOOGLE_API_KEY", ""), type="password")
160
- MODEL = settings.text_input("Model", value="text-bison@001")
161
- settings.checkbox("Fast mode (skip compress)", key="fast_mode")
162
- settings.checkbox("Compress >50MB", value=True, key="use_compression")
163
- settings.number_input("Max output tokens", key="max_output_tokens", value=1024, min_value=128, max_value=8192, step=128)
164
-
165
- if st.sidebar.button("Load Video"):
166
- try:
167
- vpw = st.session_state.get("video-password", "")
168
- p = download_video_ytdlp(st.session_state.get("url", ""), str(DATA_DIR), vpw)
169
- st.session_state["video_path"] = p
170
- st.session_state["file_hash"] = file_sha256(p)
171
- st.session_state["uploaded_file"] = None
172
- st.session_state["processed_file"] = None
173
- except Exception as e:
174
- st.sidebar.error(f"Failed to load video: {e}")
175
-
176
- if st.session_state["video_path"]:
177
- try:
178
- st.sidebar.video(st.session_state["video_path"])
179
- except Exception:
180
- st.sidebar.write("Can't preview")
181
- with st.sidebar.expander("Actions"):
182
- if st.button("Clear"):
183
- for f in glob(str(DATA_DIR / "*")):
184
- try:
185
- Path(f).unlink(missing_ok=True)
186
- except Exception:
187
- pass
188
- st.session_state["video_path"] = ""
189
- st.session_state["uploaded_file"] = None
190
- st.session_state["processed_file"] = None
191
- st.session_state["file_hash"] = None
192
- try:
193
- with open(st.session_state["video_path"], "rb") as vf:
194
- st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["video_path"]))
195
- except Exception:
196
- pass
197
-
198
  col1, col2 = st.columns([1, 3])
199
  with col1:
200
- if st.session_state["busy"]:
201
- st.write("Working...")
202
- if st.button("Cancel"):
203
- st.session_state["busy"] = False
204
  else:
205
- gen_btn = st.button("Generate the story", type="primary")
206
-
207
  with col2:
208
- prompt_text = st.text_area("Analysis prompt", value="Summarize the video's main events vividly, 2-4 paragraphs.", height=200)
209
-
210
- def configure_sdk(api_key: str):
211
- genai.configure(api_key=api_key)
212
-
213
- def responses_generate_via_sdk(model, prompt, file_name, max_tokens):
214
- # SDK responses.generate: model + messages or input; include file via files param if available
215
- messages = [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt}]
216
- kwargs = {"model": model, "messages": messages, "max_output_tokens": int(max_tokens)}
217
- if file_name:
218
- kwargs["files"] = [{"name": file_name}]
219
- return genai.responses.generate(**kwargs)
220
 
221
- def extract_text(resp):
222
- if resp is None:
223
- return None
224
- # SDK object: resp.output is list
225
- out = []
226
- out_items = getattr(resp, "output", None) or []
227
- for it in out_items:
228
- cont = getattr(it, "content", None) or []
229
- if isinstance(cont, list):
230
- for c in cont:
231
- if isinstance(c, dict) and "text" in c:
232
- out.append(c["text"])
233
- elif isinstance(c, str):
234
- out.append(c)
235
- elif isinstance(cont, str):
236
- out.append(cont)
237
- txt = getattr(it, "text", None)
238
- if isinstance(txt, str):
239
- out.append(txt)
240
- if out:
241
- return "\n\n".join(out)
242
- # fallback attributes
243
- return getattr(resp, "output_text", None) or getattr(resp, "text", None)
244
-
245
- if 'gen_btn' in locals() and gen_btn:
246
- if not st.session_state["video_path"]:
247
- st.error("No video loaded")
248
  else:
249
- key = API_KEY_INPUT.strip() or None
250
- if not key:
251
- st.error("Set GOOGLE_API_KEY in .env or paste in Settings")
252
  else:
253
  try:
254
  st.session_state["busy"] = True
255
- configure_sdk(key)
256
-
257
- # decide whether to upload
258
- path = st.session_state["video_path"]
259
- current_hash = file_sha256(path) if Path(path).exists() else None
260
- need_upload = True
261
- if st.session_state["processed_file"] and st.session_state.get("file_hash") == current_hash:
262
- need_upload = False
263
-
264
- upload_path = path
265
- compressed = None
266
- if need_upload:
267
- size_mb = Path(path).stat().st_size / (1024 * 1024)
268
- if st.session_state.get("use_compression") and not st.session_state.get("fast_mode") and size_mb > 50:
269
- compressed = str(Path(path).with_name(Path(path).stem + "_compressed.mp4"))
270
- upload_path = compress_video(path, compressed, crf=28, preset="fast")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  with st.spinner("Uploading video..."):
272
- uploaded = upload_file(upload_path)
273
- processed = wait_for_processed(uploaded, timeout=600)
274
- st.session_state["uploaded_file"] = uploaded
275
- st.session_state["processed_file"] = processed
276
- st.session_state["file_hash"] = current_hash
 
 
 
 
 
 
 
 
 
277
  else:
278
- uploaded = st.session_state["uploaded_file"]
279
- processed = st.session_state["processed_file"]
280
 
281
- fname = file_name_or_id(processed) or file_name_or_id(uploaded)
282
- resp = responses_generate_via_sdk(MODEL, prompt_text, fname, st.session_state.get("max_output_tokens", 1024))
283
- out = extract_text(resp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  if out:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  out = out.strip()
286
- st.session_state["analysis_out"] = out or ""
 
287
  st.session_state["last_error"] = ""
288
  st.subheader("Analysis Result")
289
- st.markdown(out or "_(no text returned)_")
290
- # cleanup compressed
291
- if compressed:
292
- try:
293
- Path(compressed).unlink(missing_ok=True)
294
- except Exception:
295
- pass
296
  except Exception as e:
297
  st.session_state["last_error"] = str(e)
298
- st.error(f"An error occurred while generating the story: {e}")
299
  finally:
300
  st.session_state["busy"] = False
301
 
 
302
  if st.session_state.get("analysis_out"):
303
- st.subheader("Analysis Result")
304
- st.markdown(st.session_state.get("analysis_out"))
 
 
305
 
306
  if st.session_state.get("last_error"):
307
- with st.expander("Last Error"):
308
- st.write(st.session_state.get("last_error"))
309
-
310
- with st.sidebar.expander("Manage uploads"):
311
- if st.button("Delete local files"):
312
- for f in glob(str(DATA_DIR / "*")):
313
- try:
314
- Path(f).unlink(missing_ok=True)
315
- except Exception:
316
- pass
317
- st.session_state["video_path"] = ""
318
- st.success("Local files removed")
 
1
  # streamlit_app.py
2
  import os
3
  import time
4
+ import json
5
+ import string
6
  from glob import glob
7
  from pathlib import Path
8
+ import hashlib
9
+ from difflib import SequenceMatcher
10
 
 
11
  import yt_dlp
12
+ import ffmpeg
13
  import streamlit as st
14
  from dotenv import load_dotenv
15
 
16
  load_dotenv()
17
 
 
18
  try:
19
+ from phi.agent import Agent
20
+ from phi.model.google import Gemini
21
+ from phi.tools.duckduckgo import DuckDuckGo
22
+ HAS_PHI = True
23
+ except Exception:
24
+ Agent = Gemini = DuckDuckGo = None
25
+ HAS_PHI = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ try:
28
+ import google.generativeai as genai
29
+ from google.generativeai import upload_file, get_file # type: ignore
30
+ HAS_GENAI = True
31
+ except Exception:
32
+ genai = None
33
+ upload_file = get_file = None
34
+ HAS_GENAI = False
35
+
36
+ st.set_page_config(page_title="Generate the story of videos", layout="wide")
37
+ DATA_DIR = Path("./data")
38
+ DATA_DIR.mkdir(exist_ok=True)
39
 
40
+ # Session state defaults
41
+ st.session_state.setdefault("videos", "")
42
+ st.session_state.setdefault("loop_video", False)
43
+ st.session_state.setdefault("uploaded_file", None)
44
+ st.session_state.setdefault("processed_file", None)
45
+ st.session_state.setdefault("busy", False)
46
+ st.session_state.setdefault("last_loaded_path", "")
47
+ st.session_state.setdefault("analysis_out", "")
48
+ st.session_state.setdefault("last_error", "")
49
+ st.session_state.setdefault("file_hash", None)
50
+ st.session_state.setdefault("fast_mode", False)
51
+
52
+ # Helpers
53
  def sanitize_filename(path_str: str):
54
+ name = Path(path_str).name
55
+ return name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
56
 
57
  def file_sha256(path: str, block_size: int = 65536) -> str:
58
  h = hashlib.sha256()
 
61
  h.update(chunk)
62
  return h.hexdigest()
63
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def convert_video_to_mp4(video_path: str) -> str:
65
+ target_path = str(Path(video_path).with_suffix(".mp4"))
66
+ if os.path.exists(target_path):
67
+ return target_path
68
+ ffmpeg.input(video_path).output(target_path).run(overwrite_output=True, quiet=True)
69
+ try:
70
+ os.remove(video_path)
71
+ except Exception:
72
+ pass
73
+ return target_path
 
 
 
 
 
 
 
74
 
75
  def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
76
+ try:
77
+ ffmpeg.input(input_path).output(target_path, vcodec="libx264", crf=crf, preset=preset).run(overwrite_output=True, quiet=True)
78
+ return target_path
79
+ except Exception:
80
  return input_path
 
 
 
81
 
82
  def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
83
  if not url:
84
  raise ValueError("No URL provided")
85
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
86
+ ydl_opts = {"outtmpl": outtmpl, "format": "best"}
87
  if video_password:
88
+ ydl_opts["videopassword"] = video_password
89
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
90
  info = ydl.extract_info(url, download=True)
91
  video_id = info.get("id") if isinstance(info, dict) else None
92
  if video_id:
93
+ matches = glob(os.path.join(save_dir, f"{video_id}.*"))
94
  else:
95
+ matches = sorted(glob(os.path.join(save_dir, "*")), key=os.path.getmtime, reverse=True)[:1]
96
  if not matches:
97
  raise FileNotFoundError("Downloaded video not found")
98
  return convert_video_to_mp4(matches[0])
99
 
 
100
  def file_name_or_id(file_obj):
101
+ if file_obj is None:
102
  return None
103
  if isinstance(file_obj, dict):
104
+ return file_obj.get("name") or file_obj.get("id")
105
+ return getattr(file_obj, "name", None) or getattr(file_obj, "id", None) or getattr(file_obj, "fileId", None)
106
+
107
+ # Configure Google SDK if key present
108
+ if os.getenv("GOOGLE_API_KEY") and HAS_GENAI:
109
+ try:
110
+ genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
111
+ except Exception:
112
+ pass
113
+
114
+ # UI: Sidebar inputs
115
+ st.sidebar.header("Video Input")
116
+ st.sidebar.text_input("Video URL", key="url", placeholder="https://")
117
+
118
+ settings_exp = st.sidebar.expander("Settings", expanded=False)
119
+ env_api_key = os.getenv("GOOGLE_API_KEY", "")
120
+ API_KEY = settings_exp.text_input("Google API Key", value=env_api_key, placeholder="Set GOOGLE_API_KEY in .env or enter here", type="password")
121
+ model_input = settings_exp.text_input("Gemini Model (short name)", "gemini-2.0-flash-lite")
122
+ model_id = model_input.strip() or "gemini-2.0-flash-lite"
123
+ model_arg = model_id if not model_id.startswith("models/") else model_id.split("/", 1)[1]
124
+ analysis_prompt = settings_exp.text_area("Enter analysis", value="watch entire video and describe", height=120)
125
+ settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
126
+ settings_exp.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
127
+
128
+ if not API_KEY and not os.getenv("GOOGLE_API_KEY"):
129
+ settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
130
+
131
+ safety_settings = [
132
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
133
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
134
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
135
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
136
+ ]
137
+
138
+ # Build Agent if available
139
+ _agent = None
140
+ if HAS_PHI and HAS_GENAI and (API_KEY or os.getenv("GOOGLE_API_KEY")):
141
+ try:
142
+ key_to_use = API_KEY if API_KEY else os.getenv("GOOGLE_API_KEY")
143
+ genai.configure(api_key=key_to_use)
144
+ _agent = Agent(name="Video AI summarizer", model=Gemini(id=model_id), tools=[DuckDuckGo()], markdown=True)
145
+ except Exception:
146
+ _agent = None
147
+
148
+ def clear_all_video_state():
149
+ st.session_state.pop("uploaded_file", None)
150
+ st.session_state.pop("processed_file", None)
151
+ st.session_state["videos"] = ""
152
+ st.session_state["last_loaded_path"] = ""
153
+ st.session_state["analysis_out"] = ""
154
+ st.session_state["last_error"] = ""
155
+ st.session_state["file_hash"] = None
156
+ for f in glob(str(DATA_DIR / "*")):
157
+ try:
158
+ os.remove(f)
159
+ except Exception:
160
+ pass
161
+
162
+ # Track URL changes
163
+ if "last_url_value" not in st.session_state:
164
+ st.session_state["last_url_value"] = st.session_state.get("url", "")
165
+ current_url = st.session_state.get("url", "")
166
+ if current_url != st.session_state.get("last_url_value"):
167
+ clear_all_video_state()
168
+ st.session_state["last_url_value"] = current_url
169
+
170
+ # Load video button
171
+ if st.sidebar.button("Load Video", use_container_width=True):
172
+ try:
173
+ vpw = st.session_state.get("video-password", "")
174
+ path = download_video_ytdlp(st.session_state.get("url", ""), str(DATA_DIR), vpw)
175
+ st.session_state["videos"] = path
176
+ st.session_state["last_loaded_path"] = path
177
+ st.session_state.pop("uploaded_file", None)
178
+ st.session_state.pop("processed_file", None)
179
+ st.session_state["file_hash"] = file_sha256(path)
180
+ except Exception as e:
181
+ st.sidebar.error(f"Failed to load video: {e}")
182
+
183
+ # Sidebar preview & options
184
+ if st.session_state["videos"]:
185
+ try:
186
+ st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
187
+ except Exception:
188
+ st.sidebar.write("Couldn't preview video")
189
+
190
+ with st.sidebar.expander("Options", expanded=False):
191
+ loop_checkbox = st.checkbox("Enable Loop", value=st.session_state.get("loop_video", False))
192
+ st.session_state["loop_video"] = loop_checkbox
193
+
194
+ if st.button("Clear Video(s)"):
195
+ clear_all_video_state()
196
+
197
+ try:
198
+ with open(st.session_state["videos"], "rb") as vf:
199
+ st.download_button("Download Video", data=vf, file_name=sanitize_filename(st.session_state["videos"]), mime="video/mp4", use_container_width=True)
200
+ except Exception:
201
+ st.sidebar.error("Failed to prepare download")
202
+
203
+ st.sidebar.write("Title:", Path(st.session_state["videos"]).name)
204
+
205
+ # Upload helpers
206
+ def upload_video_sdk(filepath: str):
207
+ key = API_KEY if API_KEY else os.getenv("GOOGLE_API_KEY")
208
+ if not key:
209
+ raise RuntimeError("No API key provided")
210
+ if not HAS_GENAI or upload_file is None:
211
+ raise RuntimeError("google.generativeai SDK not available; cannot upload")
212
+ genai.configure(api_key=key)
213
+ return upload_file(filepath)
214
+
215
+ def wait_for_processed(file_obj, timeout=180):
216
+ if not HAS_GENAI or get_file is None:
217
  return file_obj
218
  start = time.time()
219
  name = file_name_or_id(file_obj)
 
221
  return file_obj
222
  backoff = 1.0
223
  while True:
224
+ obj = get_file(name)
 
 
 
225
  state = getattr(obj, "state", None)
226
  if not state or getattr(state, "name", None) != "PROCESSING":
227
  return obj
 
230
  time.sleep(backoff)
231
  backoff = min(backoff * 2, 8.0)
232
 
233
+ # Robust prompt-echo removal
234
+ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
235
+ if not prompt or not text:
236
+ return text
237
+ a = " ".join(prompt.strip().lower().split())
238
+ b_full = text.strip()
239
+ b = " ".join(b_full[:check_len].lower().split())
240
+ ratio = SequenceMatcher(None, a, b).ratio()
241
+ if ratio >= ratio_threshold:
242
+ # remove the approximate prefix by length of prompt, but be conservative
243
+ cut = min(len(b_full), max(int(len(prompt) * 0.9), len(a)))
244
+ new_text = b_full[cut:].lstrip(" \n:-")
245
+ # If result is empty or too small, return original to avoid data loss
246
+ if len(new_text) >= 3:
247
+ return new_text
248
+ # also remove common placeholder prefixes
249
+ placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
250
+ low = b_full.strip().lower()
251
+ for ph in placeholders:
252
+ if low.startswith(ph):
253
+ return b_full[len(ph):].lstrip(" \n:-")
254
+ return text
255
+
256
+ # Main UI layout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  col1, col2 = st.columns([1, 3])
258
  with col1:
259
+ if st.session_state.get("busy"):
260
+ st.button("Generate the story", disabled=True)
 
 
261
  else:
262
+ generate_now = st.button("Generate the story", type="primary")
 
263
  with col2:
264
+ pass
 
 
 
 
 
 
 
 
 
 
 
265
 
266
+ # Generation flow
267
+ if (st.session_state.get("busy") is False) and ('generate_now' in locals() and generate_now):
268
+ if not st.session_state.get("videos"):
269
+ st.error("No video loaded. Use 'Load Video' in the sidebar.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  else:
271
+ key_to_use = API_KEY if API_KEY else os.getenv("GOOGLE_API_KEY")
272
+ if not key_to_use:
273
+ st.error("Google API key not set.")
274
  else:
275
  try:
276
  st.session_state["busy"] = True
277
+ processed = st.session_state.get("processed_file")
278
+ # Use file hash to determine if we must re-upload
279
+ current_path = st.session_state.get("videos")
280
+ current_hash = None
281
+ try:
282
+ current_hash = file_sha256(current_path) if current_path and os.path.exists(current_path) else None
283
+ except Exception:
284
+ current_hash = None
285
+
286
+ reupload_needed = True
287
+ if processed and st.session_state.get("last_loaded_path") == current_path and st.session_state.get("file_hash") == current_hash:
288
+ reupload_needed = False
289
+
290
+ if reupload_needed:
291
+ if not HAS_GENAI:
292
+ raise RuntimeError("google.generativeai SDK not available; install it.")
293
+ local_path = current_path
294
+ # Fast mode overrides compression behavior
295
+ fast_mode = st.session_state.get("fast_mode", False)
296
+ upload_path = local_path
297
+ try:
298
+ file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
299
+ except Exception:
300
+ file_size_mb = 0
301
+
302
+ # Only compress if large and not in fast mode
303
+ if not fast_mode and file_size_mb > 50:
304
+ compressed_path = str(Path(local_path).with_name(Path(local_path).stem + "_compressed.mp4"))
305
+ try:
306
+ # Use faster preset when focusing on speed
307
+ preset = "veryfast" if fast_mode else "fast"
308
+ upload_path = compress_video(local_path, compressed_path, crf=28, preset=preset)
309
+ except Exception:
310
+ upload_path = local_path
311
+
312
  with st.spinner("Uploading video..."):
313
+ uploaded = upload_video_sdk(upload_path)
314
+ processed = wait_for_processed(uploaded, timeout=180)
315
+ st.session_state["uploaded_file"] = uploaded
316
+ st.session_state["processed_file"] = processed
317
+ st.session_state["last_loaded_path"] = current_path
318
+ st.session_state["file_hash"] = current_hash
319
+
320
+ prompt_text = (analysis_prompt.strip() or "Describe this video in vivid detail.").strip()
321
+
322
+ out = ""
323
+ # Use lighter model/tokens in fast mode
324
+ if st.session_state.get("fast_mode"):
325
+ model_used = model_arg if model_arg else "gemini-2.0-flash-lite"
326
+ max_tokens = 512
327
  else:
328
+ model_used = model_arg
329
+ max_tokens = 1024
330
 
331
+ if _agent:
332
+ with st.spinner("Generating description via Agent..."):
333
+ response = _agent.run(prompt_text, videos=[processed], safety_settings=safety_settings)
334
+ out = getattr(response, "content", None) or getattr(response, "outputText", None) or str(response)
335
+ else:
336
+ if not HAS_GENAI or genai is None:
337
+ raise RuntimeError("Responses API not available; install google.generativeai SDK.")
338
+ genai.configure(api_key=key_to_use)
339
+ fname = file_name_or_id(processed)
340
+ if not fname:
341
+ raise RuntimeError("Uploaded file missing name/id")
342
+ system_msg = {"role": "system", "content": prompt_text}
343
+ user_msg = {"role": "user", "content": "Please summarize the attached video."}
344
+ try:
345
+ response = genai.responses.generate(
346
+ model=model_used,
347
+ messages=[system_msg, user_msg],
348
+ files=[{"name": fname}],
349
+ safety_settings=safety_settings,
350
+ max_output_tokens=max_tokens,
351
+ )
352
+ except TypeError:
353
+ response = genai.responses.generate(
354
+ model=model_used,
355
+ input=[{"text": prompt_text, "files": [{"name": fname}]}],
356
+ safety_settings=safety_settings,
357
+ max_output_tokens=max_tokens,
358
+ )
359
+
360
+ outputs = getattr(response, "output", None) or (response.get("output") if isinstance(response, dict) else None) or []
361
+ if not outputs and isinstance(response, dict):
362
+ outputs = response.get("output", [])
363
+
364
+ text_pieces = []
365
+ for item in outputs or []:
366
+ contents = getattr(item, "content", None) or (item.get("content") if isinstance(item, dict) else None) or []
367
+ for c in contents:
368
+ ctype = getattr(c, "type", None) or (c.get("type") if isinstance(c, dict) else None)
369
+ if ctype in ("output_text", "text") or ctype is None:
370
+ txt = getattr(c, "text", None) or (c.get("text") if isinstance(c, dict) else None)
371
+ if txt:
372
+ text_pieces.append(txt)
373
+
374
+ if not text_pieces:
375
+ top_text = getattr(response, "text", None) or (response.get("text") if isinstance(response, dict) else None)
376
+ if top_text:
377
+ text_pieces.append(top_text)
378
+
379
+ # dedupe preserving order
380
+ seen = set()
381
+ filtered = []
382
+ for t in text_pieces:
383
+ if t not in seen:
384
+ filtered.append(t)
385
+ seen.add(t)
386
+ out = "\n\n".join(filtered)
387
+
388
+ # Remove prompt echo robustly
389
  if out:
390
+ out = remove_prompt_echo(prompt_text, out)
391
+
392
+ # fallback: trim if startswith prompt exactly (legacy)
393
+ p = prompt_text
394
+ if p and out.strip().lower().startswith(p.lower()):
395
+ out = out.strip()[len(p):].lstrip(" \n:-")
396
+
397
+ # strip placeholders
398
+ placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
399
+ low = out.strip().lower()
400
+ for ph in placeholders:
401
+ if low.startswith(ph):
402
+ out = out.strip()[len(ph):].lstrip(" \n:-")
403
+ break
404
+
405
  out = out.strip()
406
+
407
+ st.session_state["analysis_out"] = out
408
  st.session_state["last_error"] = ""
409
  st.subheader("Analysis Result")
410
+ st.markdown(out)
 
 
 
 
 
 
411
  except Exception as e:
412
  st.session_state["last_error"] = str(e)
413
+ st.error("An error occurred while generating the story. You can try Generate again; the uploaded video will be reused.")
414
  finally:
415
  st.session_state["busy"] = False
416
 
417
+ # Display cached analysis if available (avoid duplicate on same run)
418
  if st.session_state.get("analysis_out"):
419
+ just_loaded_same = (st.session_state.get("last_loaded_path") == st.session_state.get("videos"))
420
+ if not just_loaded_same:
421
+ st.subheader("Analysis Result")
422
+ st.markdown(st.session_state.get("analysis_out"))
423
 
424
  if st.session_state.get("last_error"):
425
+ with st.expander("Last Error", expanded=False):
426
+ st.write(st.session_state.get("last_error"))