CB commited on
Commit
59ad1a6
·
verified ·
1 Parent(s): 534bb58

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +195 -40
streamlit_app.py CHANGED
@@ -5,6 +5,8 @@ import string
5
  import hashlib
6
  import traceback
7
  import inspect
 
 
8
  from glob import glob
9
  from pathlib import Path
10
  from difflib import SequenceMatcher
@@ -57,6 +59,8 @@ st.session_state.setdefault("api_key", os.getenv("GOOGLE_API_KEY", ""))
57
  st.session_state.setdefault("last_model", "")
58
  st.session_state.setdefault("last_url_value", "")
59
 
 
 
60
  def sanitize_filename(path_str: str):
61
  return Path(path_str).name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
62
 
@@ -142,16 +146,24 @@ def clear_all_video_state():
142
  except Exception:
143
  pass
144
 
145
- # URL expand + extraction helpers
146
  def expand_url(short_url, timeout=10):
 
 
 
 
147
  try:
148
- r = requests.get(short_url, allow_redirects=True, timeout=timeout, headers={"User-Agent":"Mozilla/5.0"})
149
- final_url = r.url
150
- return final_url, r.text
 
151
  except Exception as e:
152
  return None, f"error: {e}"
153
 
154
  def extract_video_from_html(html, base_url=None):
 
 
 
155
  soup = BeautifulSoup(html, "html.parser")
156
  og = soup.find("meta", property="og:video")
157
  if og and og.get("content"):
@@ -166,7 +178,6 @@ def extract_video_from_html(html, base_url=None):
166
  return source.get("src")
167
  for script in soup.find_all("script", type="application/ld+json"):
168
  try:
169
- import json
170
  data = json.loads(script.string or "{}")
171
  if isinstance(data, dict):
172
  video = data.get("video") or data.get("videoObject") or data.get("mainEntity")
@@ -188,6 +199,117 @@ def extract_video_from_html(html, base_url=None):
188
  return href
189
  return None
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  def upload_video_sdk(filepath: str):
192
  key = get_effective_api_key()
193
  if not key:
@@ -240,40 +362,34 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
240
  return b_full[len(ph):].lstrip(" \n:-")
241
  return text
242
 
243
- # Helper: try to call GenerativeModel with compatible signature
244
  def generative_model_call_flexible(model_name, messages, files=None, max_output_tokens=1024):
245
  """
246
  Try different call patterns for genai.GenerativeModel depending on its constructor/signature.
247
- Returns the response object or raises if none work.
248
  """
249
  if not HAS_GENAI or genai is None:
250
  raise RuntimeError("genai not available")
251
 
252
- # Inspect GenerativeModel if present
253
  GM = getattr(genai, "GenerativeModel", None)
254
  if GM is None:
255
  raise RuntimeError("GenerativeModel not available")
256
 
257
- # Inspect constructor signature
258
  try:
259
  sig = inspect.signature(GM)
260
  params = sig.parameters
261
- # prefer 'model' if available
262
  if "model" in params:
263
  gm = GM(model=model_name)
264
  elif "model_name" in params:
265
  gm = GM(model_name=model_name)
266
  else:
267
- # fallback to no-arg constructor
268
  gm = GM()
269
- # attempt to set attribute if accepted
270
  try:
271
  if hasattr(gm, "model"):
272
  setattr(gm, "model", model_name)
273
  except Exception:
274
  pass
275
  except Exception:
276
- # if signature inspection fails, try common constructors defensively
277
  try:
278
  gm = GM(model=model_name)
279
  except TypeError:
@@ -282,28 +398,40 @@ def generative_model_call_flexible(model_name, messages, files=None, max_output_
282
  except TypeError:
283
  gm = GM()
284
 
285
- # Now try available methods
 
286
  if hasattr(gm, "generate_content"):
287
- return gm.generate_content(messages, files=files, max_output_tokens=max_output_tokens)
288
- if hasattr(gm, "generate"):
289
- # some versions use generate(messages,...)
290
  try:
291
- return gm.generate(messages, files=files, max_output_tokens=max_output_tokens)
292
  except TypeError:
293
- # try positional
 
 
 
 
 
 
 
 
 
294
  return gm.generate(messages, max_output_tokens=max_output_tokens)
 
 
 
 
 
 
295
  raise RuntimeError("No usable generate method on GenerativeModel instance")
296
 
297
- # Fallback HTTP call using the REST Responses endpoint if the SDK is present but broken.
298
- # This requires an API key and uses the public Responses API endpoint.
299
  def responses_http_call(api_key, model, messages, file_name=None, max_output_tokens=1024, safety_settings=None):
300
  """
301
- Minimal fallback: POST to the Responses API /v1/responses.
302
- This constructs a small payload; note: some runtimes may block direct HTTP to Google or expect different endpoints.
303
  """
304
- # Basic endpoint; adjust if your environment needs a different base URL
305
- url = "https://generativeai.googleapis.com/v1beta2/models/{model}:generateMessage".format(model=model)
306
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
 
307
  payload = {
308
  "messages": [{"role": m.get("role", "user"), "content": [{"type": "text", "text": m.get("content", "")}]} for m in messages],
309
  "maxOutputTokens": max_output_tokens,
@@ -311,7 +439,7 @@ def responses_http_call(api_key, model, messages, file_name=None, max_output_tok
311
  if safety_settings:
312
  payload["safetySettings"] = safety_settings
313
  if file_name:
314
- # Attach file ref format used by some SDKs
315
  payload["files"] = [{"name": file_name}]
316
  try:
317
  r = requests.post(url, json=payload, headers=headers, timeout=60)
@@ -365,15 +493,27 @@ if st.sidebar.button("Load Video", use_container_width=True):
365
  url_val = st.session_state.get("url", "").strip()
366
  final_url = url_val
367
  html_text = None
 
368
  if url_val:
369
- expanded, html_or_err = expand_url(url_val)
370
- if expanded:
371
- final_url = expanded
372
- html_text = html_or_err
 
 
 
 
 
 
 
 
373
  else:
374
- html_text = None
375
- extracted = None
376
- if html_text:
 
 
 
377
  extracted = extract_video_from_html(html_text, base_url=final_url)
378
  target_url_for_ytdlp = extracted or final_url
379
  path = download_video_ytdlp(target_url_for_ytdlp, str(DATA_DIR), vpw)
@@ -388,6 +528,7 @@ if st.sidebar.button("Load Video", use_container_width=True):
388
  except Exception as e:
389
  st.sidebar.error(f"Failed to load video: {e}")
390
 
 
391
  if st.session_state["videos"]:
392
  try:
393
  st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
@@ -499,7 +640,7 @@ if generate_now and not st.session_state.get("busy"):
499
  response = None
500
  diagnostics = {"attempts": []}
501
 
502
- # Attempt #1: genai.responses.generate (modern)
503
  try:
504
  if hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
505
  diagnostics["attempts"].append("responses.generate")
@@ -519,6 +660,7 @@ if generate_now and not st.session_state.get("busy"):
519
  try:
520
  if hasattr(genai, "GenerativeModel"):
521
  diagnostics["attempts"].append("GenerativeModel")
 
522
  response = generative_model_call_flexible(model_used, [system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
523
  except Exception as e:
524
  diagnostics["GenerativeModel_error"] = str(e)
@@ -537,7 +679,7 @@ if generate_now and not st.session_state.get("busy"):
537
  diagnostics["top_level_error"] = str(e)
538
  response = None
539
 
540
- # Attempt #4: fallback HTTP Responses call
541
  if response is None:
542
  try:
543
  diagnostics["attempts"].append("http_fallback")
@@ -562,10 +704,22 @@ if generate_now and not st.session_state.get("busy"):
562
  outputs = list(val)
563
  break
564
  if not outputs:
565
- for v in response.values():
566
- if isinstance(v, (list, tuple)) and v:
567
- outputs = list(v)
568
- break
 
 
 
 
 
 
 
 
 
 
 
 
569
  else:
570
  for attr in ("output", "candidates", "items", "responses"):
571
  val = getattr(response, attr, None)
@@ -581,7 +735,7 @@ if generate_now and not st.session_state.get("busy"):
581
  if not outputs:
582
  candidate_text = None
583
  if isinstance(response, dict):
584
- candidate_text = response.get("text") or response.get("message")
585
  else:
586
  candidate_text = getattr(response, "text", None) or getattr(response, "message", None)
587
  if candidate_text:
@@ -592,6 +746,7 @@ if generate_now and not st.session_state.get("busy"):
592
  if not item:
593
  continue
594
  if isinstance(item, dict):
 
595
  for k in ("content", "text", "message", "output_text", "output"):
596
  v = item.get(k)
597
  if v:
 
5
  import hashlib
6
  import traceback
7
  import inspect
8
+ import re
9
+ import json
10
  from glob import glob
11
  from pathlib import Path
12
  from difflib import SequenceMatcher
 
59
  st.session_state.setdefault("last_model", "")
60
  st.session_state.setdefault("last_url_value", "")
61
 
62
+ HEADERS = {"User-Agent": "Mozilla/5.0 (compatible)"}
63
+
64
  def sanitize_filename(path_str: str):
65
  return Path(path_str).name.lower().translate(str.maketrans("", "", string.punctuation)).replace(" ", "_")
66
 
 
146
  except Exception:
147
  pass
148
 
149
+ # --- Twitter (t.co / X) helpers integrated into expand/extract flow ---
150
  def expand_url(short_url, timeout=10):
151
+ """
152
+ General URL expander. For t.co/twitter shortlinks we try multiple variants
153
+ and return final URL and HTML if available.
154
+ """
155
  try:
156
+ r = requests.get(short_url, allow_redirects=True, timeout=timeout, headers=HEADERS)
157
+ r.raise_for_status()
158
+ final = r.url
159
+ return final, r.text
160
  except Exception as e:
161
  return None, f"error: {e}"
162
 
163
  def extract_video_from_html(html, base_url=None):
164
+ """
165
+ Generic extractor tries og:video, <video>, LD+JSON, twitter tags, and links to common hosts.
166
+ """
167
  soup = BeautifulSoup(html, "html.parser")
168
  og = soup.find("meta", property="og:video")
169
  if og and og.get("content"):
 
178
  return source.get("src")
179
  for script in soup.find_all("script", type="application/ld+json"):
180
  try:
 
181
  data = json.loads(script.string or "{}")
182
  if isinstance(data, dict):
183
  video = data.get("video") or data.get("videoObject") or data.get("mainEntity")
 
199
  return href
200
  return None
201
 
202
+ def extract_video_from_twitter_html(html):
203
+ """
204
+ Attempt to pull direct MP4 URL from Twitter/X HTML by searching JSON blobs and OG tags.
205
+ This is a best-effort extractor and may fail if Twitter/X obfuscates content.
206
+ """
207
+ soup = BeautifulSoup(html, "html.parser")
208
+
209
+ # 1) Open Graph video tag
210
+ og_video = soup.find("meta", property="og:video")
211
+ if og_video and og_video.get("content"):
212
+ return og_video["content"]
213
+
214
+ # 2) Look for JSON blobs in <script> tags and search for variants/urls
215
+ scripts = soup.find_all("script")
216
+ for s in scripts:
217
+ txt = s.string
218
+ if not txt:
219
+ continue
220
+ # crude detect for embedded JSON-ish blobs that include "video_info" or "variants"
221
+ if "video_info" in txt or "variants" in txt or "playbackUrl" in txt or "media" in txt:
222
+ # try to extract a JSON object within the script text
223
+ m = re.search(r"(?s)(\{.+\})", txt)
224
+ if not m:
225
+ continue
226
+ try:
227
+ blob = json.loads(m.group(1))
228
+ except Exception:
229
+ # sometimes it's not strict JSON; skip
230
+ continue
231
+
232
+ # deep search for urls and variants
233
+ def find_media_urls(obj):
234
+ if isinstance(obj, dict):
235
+ for k, v in obj.items():
236
+ if isinstance(v, str):
237
+ if v.startswith("https://") and v.endswith(".mp4"):
238
+ yield v
239
+ else:
240
+ yield from find_media_urls(v)
241
+ elif isinstance(obj, list):
242
+ for it in obj:
243
+ yield from find_media_urls(it)
244
+
245
+ for url in find_media_urls(blob):
246
+ return url
247
+
248
+ # also look for variant lists
249
+ def find_variants(obj):
250
+ if isinstance(obj, dict):
251
+ for k, v in obj.items():
252
+ if k == "variants" and isinstance(v, list):
253
+ for vi in v:
254
+ if isinstance(vi, dict):
255
+ url = vi.get("url") or vi.get("playbackUrl")
256
+ ct = vi.get("content_type", "") or vi.get("contentType", "")
257
+ if url and url.startswith("http") and ("mp4" in url or "video" in ct or "video" in url):
258
+ yield url
259
+ else:
260
+ yield from find_variants(v)
261
+ elif isinstance(obj, list):
262
+ for it in obj:
263
+ yield from find_variants(it)
264
+
265
+ for url in find_variants(blob):
266
+ return url
267
+
268
+ return None
269
+
270
+ def extract_direct_twitter_video(url):
271
+ """
272
+ Expand t.co and try several page variants (mobile, amp, x.com) and oEmbed.
273
+ Returns (direct_video_url or None, info_string)
274
+ """
275
+ final, html_or_err = expand_url(url)
276
+ if final is None:
277
+ return None, html_or_err
278
+
279
+ # Try several variants (mobile, x.com, with query params)
280
+ variants = [
281
+ final,
282
+ final.replace("://twitter.com/", "://mobile.twitter.com/"),
283
+ final.replace("://twitter.com/", "://x.com/"),
284
+ final + "?s=20",
285
+ final + "?ref_src=twsrc%5Etfw",
286
+ ]
287
+ for u in variants:
288
+ try:
289
+ r = requests.get(u, allow_redirects=True, headers=HEADERS, timeout=10)
290
+ r.raise_for_status()
291
+ direct = extract_video_from_twitter_html(r.text)
292
+ if direct:
293
+ return direct, u
294
+ except Exception:
295
+ continue
296
+
297
+ # Try oEmbed as last resort
298
+ try:
299
+ oembed = requests.get("https://publish.twitter.com/oembed?url=" + final, headers=HEADERS, timeout=6)
300
+ if oembed.ok:
301
+ j = oembed.json()
302
+ html = j.get("html", "")
303
+ soup = BeautifulSoup(html, "html.parser")
304
+ video = soup.find("video")
305
+ if video and video.get("src"):
306
+ return video["src"], final
307
+ except Exception:
308
+ pass
309
+
310
+ return None, "not found"
311
+
312
+ # --- Upload helpers for Generative AI SDK + HTTP fallback (fixed endpoint/use patterns) ---
313
  def upload_video_sdk(filepath: str):
314
  key = get_effective_api_key()
315
  if not key:
 
362
  return b_full[len(ph):].lstrip(" \n:-")
363
  return text
364
 
 
365
  def generative_model_call_flexible(model_name, messages, files=None, max_output_tokens=1024):
366
  """
367
  Try different call patterns for genai.GenerativeModel depending on its constructor/signature.
368
+ Do NOT pass unsupported keywords called 'files' into generate_content() if the SDK rejects them.
369
  """
370
  if not HAS_GENAI or genai is None:
371
  raise RuntimeError("genai not available")
372
 
 
373
  GM = getattr(genai, "GenerativeModel", None)
374
  if GM is None:
375
  raise RuntimeError("GenerativeModel not available")
376
 
377
+ # Construct instance robustly
378
  try:
379
  sig = inspect.signature(GM)
380
  params = sig.parameters
 
381
  if "model" in params:
382
  gm = GM(model=model_name)
383
  elif "model_name" in params:
384
  gm = GM(model_name=model_name)
385
  else:
 
386
  gm = GM()
 
387
  try:
388
  if hasattr(gm, "model"):
389
  setattr(gm, "model", model_name)
390
  except Exception:
391
  pass
392
  except Exception:
 
393
  try:
394
  gm = GM(model=model_name)
395
  except TypeError:
 
398
  except TypeError:
399
  gm = GM()
400
 
401
+ # Now attempt supported generate methods but avoid unsupported kwargs
402
+ # 1) generate_content(messages...) may accept just messages and options (no files)
403
  if hasattr(gm, "generate_content"):
 
 
 
404
  try:
405
+ return gm.generate_content(messages, max_output_tokens=max_output_tokens)
406
  except TypeError:
407
+ # generate_content signature doesn't accept our args; try positional single string fallback
408
+ try:
409
+ # some versions expect a string prompt
410
+ prompt = messages[-1].get("content") if isinstance(messages, (list, tuple)) and messages else str(messages)
411
+ return gm.generate_content(prompt)
412
+ except Exception as e:
413
+ raise RuntimeError(f"GenerativeModel.generate_content unusable: {e}")
414
+ # 2) generate(...) variants
415
+ if hasattr(gm, "generate"):
416
+ try:
417
  return gm.generate(messages, max_output_tokens=max_output_tokens)
418
+ except TypeError:
419
+ try:
420
+ return gm.generate(messages)
421
+ except Exception as e:
422
+ raise RuntimeError(f"GenerativeModel.generate unusable: {e}")
423
+
424
  raise RuntimeError("No usable generate method on GenerativeModel instance")
425
 
 
 
426
  def responses_http_call(api_key, model, messages, file_name=None, max_output_tokens=1024, safety_settings=None):
427
  """
428
+ Fallback to the public Responses API v1 endpoint (modern). Construct a minimal request body.
429
+ Note: endpoint and schema may change; this uses a simple v1-compatible payload.
430
  """
431
+ # Use the modern Responses v1 endpoint format
432
+ url = "https://api.generativeai.googleapis.com/v1/models/{model}:generateMessage".format(model=model)
433
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
434
+ # Build minimal 'messages' style payload expected by many GenAI endpoints
435
  payload = {
436
  "messages": [{"role": m.get("role", "user"), "content": [{"type": "text", "text": m.get("content", "")}]} for m in messages],
437
  "maxOutputTokens": max_output_tokens,
 
439
  if safety_settings:
440
  payload["safetySettings"] = safety_settings
441
  if file_name:
442
+ # Some endpoints accept files as references
443
  payload["files"] = [{"name": file_name}]
444
  try:
445
  r = requests.post(url, json=payload, headers=headers, timeout=60)
 
493
  url_val = st.session_state.get("url", "").strip()
494
  final_url = url_val
495
  html_text = None
496
+ extracted = None
497
  if url_val:
498
+ # Special handling for t.co / twitter shortlinks
499
+ if "t.co/" in url_val or ("twitter.com" in url_val or "x.com" in url_val):
500
+ extracted, src_info = extract_direct_twitter_video(url_val)
501
+ if extracted:
502
+ final_url = extracted
503
+ html_text = None
504
+ else:
505
+ # fallback to expand_url to get final page HTML
506
+ expanded, html_or_err = expand_url(url_val)
507
+ if expanded:
508
+ final_url = expanded
509
+ html_text = html_or_err
510
  else:
511
+ expanded, html_or_err = expand_url(url_val)
512
+ if expanded:
513
+ final_url = expanded
514
+ html_text = html_or_err
515
+
516
+ if html_text and not extracted:
517
  extracted = extract_video_from_html(html_text, base_url=final_url)
518
  target_url_for_ytdlp = extracted or final_url
519
  path = download_video_ytdlp(target_url_for_ytdlp, str(DATA_DIR), vpw)
 
528
  except Exception as e:
529
  st.sidebar.error(f"Failed to load video: {e}")
530
 
531
+ # Player / sidebar controls
532
  if st.session_state["videos"]:
533
  try:
534
  st.sidebar.video(st.session_state["videos"], loop=st.session_state.get("loop_video", False))
 
640
  response = None
641
  diagnostics = {"attempts": []}
642
 
643
+ # Attempt #1: genai.responses.generate (modern public SDK)
644
  try:
645
  if hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
646
  diagnostics["attempts"].append("responses.generate")
 
660
  try:
661
  if hasattr(genai, "GenerativeModel"):
662
  diagnostics["attempts"].append("GenerativeModel")
663
+ # generative_model_call_flexible avoids passing unsupported 'files' kwarg
664
  response = generative_model_call_flexible(model_used, [system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
665
  except Exception as e:
666
  diagnostics["GenerativeModel_error"] = str(e)
 
679
  diagnostics["top_level_error"] = str(e)
680
  response = None
681
 
682
+ # Attempt #4: fallback HTTP Responses call (modern endpoint)
683
  if response is None:
684
  try:
685
  diagnostics["attempts"].append("http_fallback")
 
704
  outputs = list(val)
705
  break
706
  if not outputs:
707
+ # some Responses v1 return {'message': {...}}
708
+ msg = response.get("message") or response.get("response") or response.get("output")
709
+ if isinstance(msg, dict):
710
+ # try to extract text from structured message
711
+ c = msg.get("content")
712
+ if isinstance(c, list):
713
+ for part in c:
714
+ if isinstance(part, dict) and part.get("type") == "output_text":
715
+ outputs.append({"text": part.get("text")})
716
+ elif isinstance(part, dict) and part.get("type") == "text":
717
+ outputs.append({"text": part.get("text")})
718
+ else:
719
+ # fallback: join string values
720
+ for v in response.values():
721
+ if isinstance(v, str) and v.strip():
722
+ outputs.append({"text": v.strip()})
723
  else:
724
  for attr in ("output", "candidates", "items", "responses"):
725
  val = getattr(response, attr, None)
 
735
  if not outputs:
736
  candidate_text = None
737
  if isinstance(response, dict):
738
+ candidate_text = response.get("text") or response.get("message") or response.get("output_text")
739
  else:
740
  candidate_text = getattr(response, "text", None) or getattr(response, "message", None)
741
  if candidate_text:
 
746
  if not item:
747
  continue
748
  if isinstance(item, dict):
749
+ # common dict shapes
750
  for k in ("content", "text", "message", "output_text", "output"):
751
  v = item.get(k)
752
  if v: