CB commited on
Commit
1866c11
·
verified ·
1 Parent(s): 8ce448b

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +139 -93
streamlit_app.py CHANGED
@@ -10,6 +10,7 @@ import re
10
  from glob import glob
11
  from pathlib import Path
12
  from difflib import SequenceMatcher
 
13
 
14
  import requests
15
  from bs4 import BeautifulSoup
@@ -21,8 +22,11 @@ from dotenv import load_dotenv
21
 
22
  load_dotenv()
23
 
24
- # Feature flags
25
- HAS_PHI = False
 
 
 
26
  try:
27
  import google.generativeai as genai # type: ignore
28
  try:
@@ -42,27 +46,26 @@ DATA_DIR = Path("./data")
42
  DATA_DIR.mkdir(exist_ok=True)
43
 
44
  # Session defaults
45
- for k, v in {
46
- "videos": "",
47
- "loop_video": False,
48
- "uploaded_file": None,
49
- "processed_file": None,
50
- "busy": False,
51
- "last_loaded_path": "",
52
- "analysis_out": "",
53
- "last_error": "",
54
- "file_hash": None,
55
- "fast_mode": False,
56
- "api_key": os.getenv("GOOGLE_API_KEY", ""),
57
- "last_model": "",
58
- "last_url_value": "",
59
- }.items():
60
- st.session_state.setdefault(k, v)
61
 
62
  HEADERS = {"User-Agent": "Mozilla/5.0 (compatible)"}
63
 
64
- # Utilities --------------------------------------------------------------------
65
- def sanitize_filename(path_str: str):
66
  return Path(path_str).name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
67
 
68
  def file_sha256(path: str, block_size: int = 65536) -> str:
@@ -83,15 +86,15 @@ def convert_video_to_mp4(video_path: str) -> str:
83
  pass
84
  return target_path
85
 
86
- def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast"):
87
  try:
88
  ffmpeg.input(input_path).output(target_path, vcodec="libx264", crf=crf, preset=preset).run(overwrite_output=True, quiet=True)
89
  return target_path
90
  except Exception:
91
  return input_path
92
 
93
- # Downloader / extractor ------------------------------------------------------
94
- def expand_url(short_url, timeout=10):
95
  try:
96
  r = requests.get(short_url, allow_redirects=True, timeout=timeout, headers=HEADERS)
97
  r.raise_for_status()
@@ -99,7 +102,7 @@ def expand_url(short_url, timeout=10):
99
  except Exception as e:
100
  return None, f"error: {e}"
101
 
102
- def extract_video_from_html(html, base_url=None):
103
  soup = BeautifulSoup(html, "html.parser")
104
  og = soup.find("meta", property="og:video")
105
  if og and og.get("content"):
@@ -115,7 +118,6 @@ def extract_video_from_html(html, base_url=None):
115
  for script in soup.find_all("script", type="application/ld+json"):
116
  try:
117
  data = json.loads(script.string or "{}")
118
- video = None
119
  if isinstance(data, dict):
120
  video = data.get("video") or data.get("videoObject") or data.get("mainEntity")
121
  if isinstance(video, dict):
@@ -127,16 +129,16 @@ def extract_video_from_html(html, base_url=None):
127
  except Exception:
128
  continue
129
  for mname in ("twitter:player:stream", "twitter:player"):
130
- meta = soup.find("meta", attrs={"name": mname})
131
- if meta and meta.get("content"):
132
- return meta.get("content")
133
  for a in soup.find_all("a", href=True):
134
  href = a["href"]
135
- if any(d in href for d in ("youtube.com", "youtu.be", "vimeo.com")):
136
  return href
137
  return None
138
 
139
- def extract_video_from_twitter_html(html):
140
  soup = BeautifulSoup(html, "html.parser")
141
  og_video = soup.find("meta", property="og:video")
142
  if og_video and og_video.get("content"):
@@ -185,10 +187,10 @@ def extract_video_from_twitter_html(html):
185
  return url
186
  return None
187
 
188
- def extract_direct_twitter_video(url):
189
  final, html_or_err = expand_url(url)
190
  if final is None:
191
- return None, html_or_err
192
  variants = [
193
  final,
194
  final.replace("://twitter.com/", "://mobile.twitter.com/"),
@@ -218,7 +220,7 @@ def extract_direct_twitter_video(url):
218
  pass
219
  return None, "not found"
220
 
221
- def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) -> str:
222
  if not url:
223
  raise ValueError("No URL provided")
224
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
@@ -237,11 +239,11 @@ def download_video_ytdlp(url: str, save_dir: str, video_password: str = None) ->
237
  raise FileNotFoundError("Downloaded video not found")
238
  return convert_video_to_mp4(matches[0])
239
 
240
- # Generative AI helpers -------------------------------------------------------
241
- def get_effective_api_key():
242
  return st.session_state.get("api_key") or os.getenv("GOOGLE_API_KEY")
243
 
244
- def maybe_configure_genai(key):
245
  if not key or not HAS_GENAI:
246
  return False
247
  try:
@@ -250,7 +252,7 @@ def maybe_configure_genai(key):
250
  except Exception:
251
  return False
252
 
253
- def upload_video_sdk(filepath: str):
254
  key = get_effective_api_key()
255
  if not key:
256
  raise RuntimeError("No API key provided")
@@ -259,18 +261,11 @@ def upload_video_sdk(filepath: str):
259
  genai.configure(api_key=key)
260
  return upload_file(filepath)
261
 
262
- def wait_for_processed(file_obj, timeout=180):
263
  if not HAS_GENAI or get_file is None:
264
  return file_obj
265
  start = time.time()
266
- name = None
267
- if isinstance(file_obj, dict):
268
- name = file_obj.get("name") or file_obj.get("id")
269
- else:
270
- for attr in ("name", "id", "fileId", "file_id"):
271
- if hasattr(file_obj, attr):
272
- name = getattr(file_obj, attr)
273
- break
274
  if not name:
275
  return file_obj
276
  backoff = 1.0
@@ -287,7 +282,19 @@ def wait_for_processed(file_obj, timeout=180):
287
  time.sleep(backoff)
288
  backoff = min(backoff * 2, 8.0)
289
 
290
- def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68):
 
 
 
 
 
 
 
 
 
 
 
 
291
  if not prompt or not text:
292
  return text
293
  a = " ".join(prompt.strip().lower().split())
@@ -309,13 +316,19 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
309
  return b_full[len(ph):].lstrip(" \n:-")
310
  return text
311
 
312
- def generative_model_call_flexible(model_name, messages, files=None, max_output_tokens=1024):
 
 
 
 
313
  if not HAS_GENAI or genai is None:
314
  raise RuntimeError("genai not available")
315
  GM = getattr(genai, "GenerativeModel", None)
316
  if GM is None:
317
  raise RuntimeError("GenerativeModel not available")
318
- # robust constructor
 
 
319
  try:
320
  sig = inspect.signature(GM)
321
  params = sig.parameters
@@ -325,34 +338,58 @@ def generative_model_call_flexible(model_name, messages, files=None, max_output_
325
  gm = GM(model_name=model_name)
326
  else:
327
  gm = GM()
328
- if hasattr(gm, "model"):
329
- try:
330
  setattr(gm, "model", model_name)
331
- except Exception:
332
- pass
333
  except Exception:
334
  try:
335
  gm = GM(model=model_name)
336
  except Exception:
337
- gm = GM()
338
- # try generate methods but avoid unsupported kwargs
 
 
 
 
 
 
 
339
  if hasattr(gm, "generate_content"):
 
340
  try:
341
- # many versions accept 'messages' and 'files'
 
 
342
  try:
343
- return gm.generate_content(messages=messages, files=files, max_output_tokens=max_output_tokens)
344
- except TypeError:
345
  return gm.generate_content(messages, max_output_tokens)
 
 
 
 
 
 
 
346
  except Exception as e:
347
  raise RuntimeError(f"generate_content failed: {e}")
 
 
348
  if hasattr(gm, "generate"):
349
  try:
350
  return gm.generate(messages=messages, files=files, max_output_tokens=max_output_tokens)
351
  except TypeError:
352
- return gm.generate(messages, max_output_tokens=max_output_tokens)
 
 
 
 
353
  raise RuntimeError("No usable generate method on GenerativeModel instance")
354
 
355
- def responses_http_call(api_key, model, messages, file_name=None, max_output_tokens=1024, safety_settings=None):
 
 
 
356
  url = f"https://api.generativeai.googleapis.com/v1/models/{model}:generateMessage"
357
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
358
  payload = {
@@ -363,25 +400,36 @@ def responses_http_call(api_key, model, messages, file_name=None, max_output_tok
363
  payload["safetySettings"] = safety_settings
364
  if file_name:
365
  payload["files"] = [{"name": file_name}]
366
- r = requests.post(url, json=payload, headers=headers, timeout=60)
367
- r.raise_for_status()
368
- return r.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- def normalize_response_to_text(response) -> str:
371
  """Extract text from SDK or HTTP responses into a single string."""
372
  if not response:
373
  return ""
374
- # dict-like responses (HTTP fallback or genai.responses)
375
  if isinstance(response, dict):
376
- # modern Responses v1 may include 'output' or 'message'
377
- # search keys for lists of candidates/items/responses
378
- for list_key in ("output", "candidates", "items", "responses"):
379
- val = response.get(list_key)
380
  if isinstance(val, (list, tuple)) and val:
381
  pieces = []
382
  for el in val:
383
  if isinstance(el, dict):
384
- # content field may be list of {type, text}
385
  c = el.get("content") or el.get("message") or el.get("text")
386
  if isinstance(c, list):
387
  for part in c:
@@ -397,7 +445,7 @@ def normalize_response_to_text(response) -> str:
397
  pieces.append(el)
398
  if pieces:
399
  return "\n\n".join(pieces)
400
- # message/content path
401
  msg = response.get("message") or response.get("response") or response.get("output")
402
  if isinstance(msg, dict):
403
  c = msg.get("content")
@@ -430,14 +478,12 @@ def normalize_response_to_text(response) -> str:
430
  else:
431
  pieces.append(str(el))
432
  return "\n\n".join([p for p in pieces if p])
433
- # fallback
434
  text = getattr(response, "text", None) or getattr(response, "message", None)
435
  return text or ""
436
 
437
- # UI --------------------------------------------------------------------------
438
  current_url = st.session_state.get("url", "")
439
  if current_url != st.session_state.get("last_url_value"):
440
- # clear when user changes URL
441
  st.session_state.update({"videos": "", "uploaded_file": None, "processed_file": None, "last_loaded_path": "", "analysis_out": "", "last_error": "", "file_hash": None})
442
  st.session_state["last_url_value"] = current_url
443
 
@@ -453,9 +499,11 @@ default_prompt = (
453
  settings_exp.text_area("Enter analysis", value=default_prompt, height=140, key="analysis_prompt")
454
  settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
455
  settings_exp.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
 
456
 
457
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
458
  settings_exp.caption(f"Using API key from: **{key_source}**")
 
459
  if not get_effective_api_key():
460
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
461
 
@@ -472,7 +520,7 @@ with col1:
472
  with col2:
473
  pass
474
 
475
- # Load Video button
476
  if st.sidebar.button("Load Video", use_container_width=True):
477
  try:
478
  vpw = st.session_state.get("video-password", "")
@@ -507,7 +555,7 @@ if st.sidebar.button("Load Video", use_container_width=True):
507
  except Exception as e:
508
  st.sidebar.error(f"Failed to load video: {e}")
509
 
510
- # Sidebar preview + controls
511
  if st.session_state["videos"]:
512
  try:
513
  st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
@@ -537,7 +585,7 @@ if st.session_state["videos"]:
537
  except Exception:
538
  pass
539
 
540
- # Generation flow --------------------------------------------------------------
541
  if generate_now and not st.session_state.get("busy"):
542
  if not st.session_state.get("videos"):
543
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
@@ -560,7 +608,6 @@ if generate_now and not st.session_state.get("busy"):
560
 
561
  upload_path = current_path
562
  if reupload_needed:
563
- # compress if large and not fast
564
  fast_mode = st.session_state.get("fast_mode", False)
565
  try:
566
  file_size_mb = os.path.getsize(current_path) / (1024 * 1024)
@@ -580,20 +627,13 @@ if generate_now and not st.session_state.get("busy"):
580
  prompt_text = (st.session_state.get("analysis_prompt", "") or default_prompt).strip()
581
  system_msg = {"role": "system", "content": prompt_text}
582
  user_msg = {"role": "user", "content": "Please summarize the attached video."}
583
- fname = None
584
- if processed:
585
- if isinstance(processed, dict):
586
- fname = processed.get("name") or processed.get("id")
587
- else:
588
- for attr in ("name", "id", "fileId", "file_id"):
589
- if hasattr(processed, attr):
590
- fname = getattr(processed, attr)
591
- break
592
- # prefer SDK methods that support 'files' / file references
593
  response = None
594
  diagnostics = {"attempts": []}
595
 
596
- # 1) genai.responses.generate (if available) - supports files param
597
  if response is None and HAS_GENAI and genai is not None and hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
598
  try:
599
  diagnostics["attempts"].append("responses.generate")
@@ -622,15 +662,22 @@ if generate_now and not st.session_state.get("busy"):
622
  try:
623
  if hasattr(genai, "generate"):
624
  diagnostics["attempts"].append("top.generate")
625
- response = genai.generate(model=model_id, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=(256 if st.session_state.get("fast_mode") else 1024))
 
 
 
 
626
  elif hasattr(genai, "create"):
627
  diagnostics["attempts"].append("top.create")
628
- response = genai.create(model=model_id, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=(256 if st.session_state.get("fast_mode") else 1024))
 
 
 
629
  except Exception as e:
630
  diagnostics["top_level_error"] = str(e)
631
  response = None
632
 
633
- # 4) HTTP fallback to Responses endpoint (supports file references)
634
  if response is None:
635
  try:
636
  diagnostics["attempts"].append("http_fallback")
@@ -646,7 +693,6 @@ if generate_now and not st.session_state.get("busy"):
646
  else:
647
  out = normalize_response_to_text(response)
648
  out = remove_prompt_echo(prompt_text, out).strip()
649
- # additional cleanup of obvious echoes/placeholders
650
  placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
651
  low = out.strip().lower()
652
  for ph in placeholders:
 
10
  from glob import glob
11
  from pathlib import Path
12
  from difflib import SequenceMatcher
13
+ from typing import Optional, Tuple, Any
14
 
15
  import requests
16
  from bs4 import BeautifulSoup
 
22
 
23
  load_dotenv()
24
 
25
+ # Try import google.generativeai (optional)
26
+ HAS_GENAI = False
27
+ genai = None
28
+ upload_file = None
29
+ get_file = None
30
  try:
31
  import google.generativeai as genai # type: ignore
32
  try:
 
46
  DATA_DIR.mkdir(exist_ok=True)
47
 
48
  # Session defaults
49
+ st.session_state.setdefault("videos", "")
50
+ st.session_state.setdefault("loop_video", False)
51
+ st.session_state.setdefault("uploaded_file", None)
52
+ st.session_state.setdefault("processed_file", None)
53
+ st.session_state.setdefault("busy", False)
54
+ st.session_state.setdefault("last_loaded_path", "")
55
+ st.session_state.setdefault("analysis_out", "")
56
+ st.session_state.setdefault("last_error", "")
57
+ st.session_state.setdefault("file_hash", None)
58
+ st.session_state.setdefault("fast_mode", False)
59
+ st.session_state.setdefault("api_key", os.getenv("GOOGLE_API_KEY", ""))
60
+ st.session_state.setdefault("last_model", "")
61
+ st.session_state.setdefault("last_url_value", "")
62
+ # allow disabling SSL verify for HTTP fallback (not recommended)
63
+ st.session_state.setdefault("http_skip_ssl_verify", False)
 
64
 
65
  HEADERS = {"User-Agent": "Mozilla/5.0 (compatible)"}
66
 
67
+ # ----------------- Utilities -----------------
68
+ def sanitize_filename(path_str: str) -> str:
69
  return Path(path_str).name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
70
 
71
  def file_sha256(path: str, block_size: int = 65536) -> str:
 
86
  pass
87
  return target_path
88
 
89
+ def compress_video(input_path: str, target_path: str, crf: int = 28, preset: str = "fast") -> str:
90
  try:
91
  ffmpeg.input(input_path).output(target_path, vcodec="libx264", crf=crf, preset=preset).run(overwrite_output=True, quiet=True)
92
  return target_path
93
  except Exception:
94
  return input_path
95
 
96
+ # ----------------- Download / Extract -----------------
97
+ def expand_url(short_url: str, timeout: int = 10) -> Tuple[Optional[str], Optional[str]]:
98
  try:
99
  r = requests.get(short_url, allow_redirects=True, timeout=timeout, headers=HEADERS)
100
  r.raise_for_status()
 
102
  except Exception as e:
103
  return None, f"error: {e}"
104
 
105
+ def extract_video_from_html(html: str, base_url: Optional[str] = None) -> Optional[str]:
106
  soup = BeautifulSoup(html, "html.parser")
107
  og = soup.find("meta", property="og:video")
108
  if og and og.get("content"):
 
118
  for script in soup.find_all("script", type="application/ld+json"):
119
  try:
120
  data = json.loads(script.string or "{}")
 
121
  if isinstance(data, dict):
122
  video = data.get("video") or data.get("videoObject") or data.get("mainEntity")
123
  if isinstance(video, dict):
 
129
  except Exception:
130
  continue
131
  for mname in ("twitter:player:stream", "twitter:player"):
132
+ m = soup.find("meta", attrs={"name": mname})
133
+ if m and m.get("content"):
134
+ return m.get("content")
135
  for a in soup.find_all("a", href=True):
136
  href = a["href"]
137
+ if any(domain in href for domain in ("youtube.com", "youtu.be", "vimeo.com")):
138
  return href
139
  return None
140
 
141
+ def extract_video_from_twitter_html(html: str) -> Optional[str]:
142
  soup = BeautifulSoup(html, "html.parser")
143
  og_video = soup.find("meta", property="og:video")
144
  if og_video and og_video.get("content"):
 
187
  return url
188
  return None
189
 
190
+ def extract_direct_twitter_video(url: str) -> Tuple[Optional[str], str]:
191
  final, html_or_err = expand_url(url)
192
  if final is None:
193
+ return None, html_or_err or "expand failed"
194
  variants = [
195
  final,
196
  final.replace("://twitter.com/", "://mobile.twitter.com/"),
 
220
  pass
221
  return None, "not found"
222
 
223
+ def download_video_ytdlp(url: str, save_dir: str, video_password: Optional[str] = None) -> str:
224
  if not url:
225
  raise ValueError("No URL provided")
226
  outtmpl = str(Path(save_dir) / "%(id)s.%(ext)s")
 
239
  raise FileNotFoundError("Downloaded video not found")
240
  return convert_video_to_mp4(matches[0])
241
 
242
+ # ----------------- Generative AI helpers -----------------
243
+ def get_effective_api_key() -> Optional[str]:
244
  return st.session_state.get("api_key") or os.getenv("GOOGLE_API_KEY")
245
 
246
+ def maybe_configure_genai(key: str) -> bool:
247
  if not key or not HAS_GENAI:
248
  return False
249
  try:
 
252
  except Exception:
253
  return False
254
 
255
+ def upload_video_sdk(filepath: str) -> Any:
256
  key = get_effective_api_key()
257
  if not key:
258
  raise RuntimeError("No API key provided")
 
261
  genai.configure(api_key=key)
262
  return upload_file(filepath)
263
 
264
+ def wait_for_processed(file_obj: Any, timeout: int = 180) -> Any:
265
  if not HAS_GENAI or get_file is None:
266
  return file_obj
267
  start = time.time()
268
+ name = file_name_or_id(file_obj)
 
 
 
 
 
 
 
269
  if not name:
270
  return file_obj
271
  backoff = 1.0
 
282
  time.sleep(backoff)
283
  backoff = min(backoff * 2, 8.0)
284
 
285
+ def file_name_or_id(file_obj: Any) -> Optional[str]:
286
+ if file_obj is None:
287
+ return None
288
+ if isinstance(file_obj, dict):
289
+ return file_obj.get("name") or file_obj.get("id")
290
+ for attr in ("name", "id", "fileId", "file_id"):
291
+ if hasattr(file_obj, attr):
292
+ val = getattr(file_obj, attr)
293
+ if val:
294
+ return val
295
+ return str(file_obj)
296
+
297
+ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_threshold: float = 0.68) -> str:
298
  if not prompt or not text:
299
  return text
300
  a = " ".join(prompt.strip().lower().split())
 
316
  return b_full[len(ph):].lstrip(" \n:-")
317
  return text
318
 
319
+ def generative_model_call_flexible(model_name: str, messages: list, files: Optional[list] = None, max_output_tokens: int = 1024) -> Any:
320
+ """
321
+ Robustly call GenerativeModel with different signatures.
322
+ This function anticipates generate_content taking different arg shapes.
323
+ """
324
  if not HAS_GENAI or genai is None:
325
  raise RuntimeError("genai not available")
326
  GM = getattr(genai, "GenerativeModel", None)
327
  if GM is None:
328
  raise RuntimeError("GenerativeModel not available")
329
+
330
+ # Construct instance robustly
331
+ gm = None
332
  try:
333
  sig = inspect.signature(GM)
334
  params = sig.parameters
 
338
  gm = GM(model_name=model_name)
339
  else:
340
  gm = GM()
341
+ try:
342
+ if hasattr(gm, "model"):
343
  setattr(gm, "model", model_name)
344
+ except Exception:
345
+ pass
346
  except Exception:
347
  try:
348
  gm = GM(model=model_name)
349
  except Exception:
350
+ try:
351
+ gm = GM(model_name=model_name)
352
+ except Exception:
353
+ gm = GM()
354
+
355
+ # Try generate_content with multiple call shapes:
356
+ # - generate_content(messages=..., files=..., max_output_tokens=...)
357
+ # - generate_content(messages, files, max_output_tokens)
358
+ # - generate_content(messages)
359
  if hasattr(gm, "generate_content"):
360
+ # try keyword style first
361
  try:
362
+ return gm.generate_content(messages=messages, files=files, max_output_tokens=max_output_tokens)
363
+ except TypeError:
364
+ # try positional: some versions expect (messages, max_output_tokens)
365
  try:
 
 
366
  return gm.generate_content(messages, max_output_tokens)
367
+ except TypeError:
368
+ # maybe (prompt_str,) shape
369
+ try:
370
+ prompt = messages[-1].get("content") if isinstance(messages, (list, tuple)) and messages else str(messages)
371
+ return gm.generate_content(prompt)
372
+ except Exception as e:
373
+ raise RuntimeError(f"GenerativeModel.generate_content unusable: {e}")
374
  except Exception as e:
375
  raise RuntimeError(f"generate_content failed: {e}")
376
+
377
+ # Try generate with files kw / positional
378
  if hasattr(gm, "generate"):
379
  try:
380
  return gm.generate(messages=messages, files=files, max_output_tokens=max_output_tokens)
381
  except TypeError:
382
+ try:
383
+ return gm.generate(messages, max_output_tokens)
384
+ except Exception as e:
385
+ raise RuntimeError(f"GenerativeModel.generate unusable: {e}")
386
+
387
  raise RuntimeError("No usable generate method on GenerativeModel instance")
388
 
389
+ def responses_http_call(api_key: str, model: str, messages: list, file_name: Optional[str] = None, max_output_tokens: int = 1024, safety_settings: Optional[list] = None) -> dict:
390
+ """
391
+ HTTP fallback to Responses v1 endpoint. Attempts retries and allows optional SSL skip.
392
+ """
393
  url = f"https://api.generativeai.googleapis.com/v1/models/{model}:generateMessage"
394
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
395
  payload = {
 
400
  payload["safetySettings"] = safety_settings
401
  if file_name:
402
  payload["files"] = [{"name": file_name}]
403
+ # allow skipping SSL verify in special environments (not recommended)
404
+ verify = not bool(st.session_state.get("http_skip_ssl_verify", False))
405
+ last_err = None
406
+ for attempt in range(1, 4):
407
+ try:
408
+ r = requests.post(url, json=payload, headers=headers, timeout=30, verify=verify)
409
+ r.raise_for_status()
410
+ return r.json()
411
+ except requests.exceptions.SSLError as e:
412
+ last_err = e
413
+ # If SSL hostname mismatch or cert issues, surface helpful message once
414
+ raise RuntimeError(f"SSL error when calling Responses HTTP endpoint: {e}. If you are behind a proxy intercepting TLS, set 'Skip HTTP SSL verify' in Settings (not recommended) or fix your CA bundle.")
415
+ except Exception as e:
416
+ last_err = e
417
+ time.sleep(0.8 * attempt)
418
+ continue
419
+ raise RuntimeError(f"HTTP responses fallback failed after retries: {last_err}")
420
 
421
+ def normalize_response_to_text(response: Any) -> str:
422
  """Extract text from SDK or HTTP responses into a single string."""
423
  if not response:
424
  return ""
425
+ # dict-like
426
  if isinstance(response, dict):
427
+ for key in ("output", "candidates", "items", "responses"):
428
+ val = response.get(key)
 
 
429
  if isinstance(val, (list, tuple)) and val:
430
  pieces = []
431
  for el in val:
432
  if isinstance(el, dict):
 
433
  c = el.get("content") or el.get("message") or el.get("text")
434
  if isinstance(c, list):
435
  for part in c:
 
445
  pieces.append(el)
446
  if pieces:
447
  return "\n\n".join(pieces)
448
+ # message path
449
  msg = response.get("message") or response.get("response") or response.get("output")
450
  if isinstance(msg, dict):
451
  c = msg.get("content")
 
478
  else:
479
  pieces.append(str(el))
480
  return "\n\n".join([p for p in pieces if p])
 
481
  text = getattr(response, "text", None) or getattr(response, "message", None)
482
  return text or ""
483
 
484
+ # ----------------- UI -----------------
485
  current_url = st.session_state.get("url", "")
486
  if current_url != st.session_state.get("last_url_value"):
 
487
  st.session_state.update({"videos": "", "uploaded_file": None, "processed_file": None, "last_loaded_path": "", "analysis_out": "", "last_error": "", "file_hash": None})
488
  st.session_state["last_url_value"] = current_url
489
 
 
499
  settings_exp.text_area("Enter analysis", value=default_prompt, height=140, key="analysis_prompt")
500
  settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
501
  settings_exp.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
502
+ settings_exp.checkbox("Skip HTTP SSL verify (only if you trust the network)", key="http_skip_ssl_verify")
503
 
504
  key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
505
  settings_exp.caption(f"Using API key from: **{key_source}**")
506
+
507
  if not get_effective_api_key():
508
  settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
509
 
 
520
  with col2:
521
  pass
522
 
523
+ # Load Video
524
  if st.sidebar.button("Load Video", use_container_width=True):
525
  try:
526
  vpw = st.session_state.get("video-password", "")
 
555
  except Exception as e:
556
  st.sidebar.error(f"Failed to load video: {e}")
557
 
558
+ # Sidebar preview
559
  if st.session_state["videos"]:
560
  try:
561
  st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
 
585
  except Exception:
586
  pass
587
 
588
+ # Generation flow
589
  if generate_now and not st.session_state.get("busy"):
590
  if not st.session_state.get("videos"):
591
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
 
608
 
609
  upload_path = current_path
610
  if reupload_needed:
 
611
  fast_mode = st.session_state.get("fast_mode", False)
612
  try:
613
  file_size_mb = os.path.getsize(current_path) / (1024 * 1024)
 
627
  prompt_text = (st.session_state.get("analysis_prompt", "") or default_prompt).strip()
628
  system_msg = {"role": "system", "content": prompt_text}
629
  user_msg = {"role": "user", "content": "Please summarize the attached video."}
630
+
631
+ fname = file_name_or_id(processed)
632
+
 
 
 
 
 
 
 
633
  response = None
634
  diagnostics = {"attempts": []}
635
 
636
+ # 1) genai.responses.generate (supports files)
637
  if response is None and HAS_GENAI and genai is not None and hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
638
  try:
639
  diagnostics["attempts"].append("responses.generate")
 
662
  try:
663
  if hasattr(genai, "generate"):
664
  diagnostics["attempts"].append("top.generate")
665
+ # don't assume exact param shapes; try best-effort
666
+ try:
667
+ response = genai.generate(model=model_id, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=(256 if st.session_state.get("fast_mode") else 1024))
668
+ except TypeError:
669
+ response = genai.generate(model=model_id, input=prompt_text)
670
  elif hasattr(genai, "create"):
671
  diagnostics["attempts"].append("top.create")
672
+ try:
673
+ response = genai.create(model=model_id, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=(256 if st.session_state.get("fast_mode") else 1024))
674
+ except TypeError:
675
+ response = genai.create(model=model_id, input=prompt_text)
676
  except Exception as e:
677
  diagnostics["top_level_error"] = str(e)
678
  response = None
679
 
680
+ # 4) HTTP fallback
681
  if response is None:
682
  try:
683
  diagnostics["attempts"].append("http_fallback")
 
693
  else:
694
  out = normalize_response_to_text(response)
695
  out = remove_prompt_echo(prompt_text, out).strip()
 
696
  placeholders = ["enter analysis", "enter your analysis", "enter analysis here", "please enter analysis"]
697
  low = out.strip().lower()
698
  for ph in placeholders: