CB commited on
Commit
5947544
·
verified ·
1 Parent(s): 598a32e

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +224 -137
streamlit_app.py CHANGED
@@ -8,6 +8,9 @@ from glob import glob
8
  from pathlib import Path
9
  from difflib import SequenceMatcher
10
 
 
 
 
11
  import yt_dlp
12
  import ffmpeg
13
  import streamlit as st
@@ -15,30 +18,25 @@ from dotenv import load_dotenv
15
 
16
  load_dotenv()
17
 
18
- # phi agent disabled to avoid phi IndexError
19
- try:
20
- from phi.agent import Agent # noqa: F401
21
- from phi.model.google import Gemini # noqa: F401
22
- from phi.tools.duckduckgo import DuckDuckGo # noqa: F401
23
- HAS_PHI = True
24
- except Exception:
25
- HAS_PHI = False
26
-
27
  HAS_PHI = False
28
 
 
29
  try:
30
- import google.generativeai as genai
31
  from google.generativeai import upload_file, get_file # type: ignore
32
  HAS_GENAI = True
33
  except Exception:
34
  genai = None
35
- upload_file = get_file = None
 
36
  HAS_GENAI = False
37
 
38
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
39
  DATA_DIR = Path("./data")
40
  DATA_DIR.mkdir(exist_ok=True)
41
 
 
42
  st.session_state.setdefault("videos", "")
43
  st.session_state.setdefault("loop_video", False)
44
  st.session_state.setdefault("uploaded_file", None)
@@ -119,6 +117,7 @@ def maybe_configure_genai(key):
119
  if not key or not HAS_GENAI:
120
  return False
121
  try:
 
122
  genai.configure(api_key=key)
123
  return True
124
  except Exception:
@@ -138,43 +137,67 @@ def clear_all_video_state():
138
  except Exception:
139
  pass
140
 
141
- current_url = st.session_state.get("url", "")
142
- if current_url != st.session_state.get("last_url_value"):
143
- clear_all_video_state()
144
- st.session_state["last_url_value"] = current_url
145
-
146
- st.sidebar.header("Video Input")
147
- st.sidebar.text_input("Video URL", key="url", placeholder="https://")
148
-
149
- settings_exp = st.sidebar.expander("Settings", expanded=False)
150
- settings_exp.text_input("Gemini Model (short name)", "gemini-2.5-flash-lite", key="model_input")
151
- settings_exp.text_input("Google API Key", key="api_key", value=os.getenv("GOOGLE_API_KEY", ""), type="password")
152
- default_prompt = (
153
- "Watch the video and provide a detailed behavioral report focusing on human actions, interactions, posture, movement, and apparent intent. Keep language professional. Include a list of observations for notable events."
154
- )
155
- settings_exp.text_area("Enter analysis", value=default_prompt, height=140, key="analysis_prompt")
156
- settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
157
- settings_exp.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
158
-
159
- key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
160
- settings_exp.caption(f"Using API key from: **{key_source}**")
161
-
162
- if not get_effective_api_key():
163
- settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
164
-
165
- safety_settings = [
166
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
167
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
168
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
169
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
170
- ]
171
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  def upload_video_sdk(filepath: str):
173
  key = get_effective_api_key()
174
  if not key:
175
  raise RuntimeError("No API key provided")
176
  if not HAS_GENAI or upload_file is None:
177
- raise RuntimeError("google.generativeai SDK not available; cannot upload")
178
  genai.configure(api_key=key)
179
  return upload_file(filepath)
180
 
@@ -221,16 +244,66 @@ def remove_prompt_echo(prompt: str, text: str, check_len: int = 600, ratio_thres
221
  return b_full[len(ph):].lstrip(" \n:-")
222
  return text
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  col1, col2 = st.columns([1, 3])
225
  with col1:
226
  generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
227
  with col2:
228
  pass
229
 
 
230
  if st.sidebar.button("Load Video", use_container_width=True):
231
  try:
232
  vpw = st.session_state.get("video-password", "")
233
- path = download_video_ytdlp(st.session_state.get("url", ""), str(DATA_DIR), vpw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  st.session_state["videos"] = path
235
  st.session_state["last_loaded_path"] = path
236
  st.session_state.pop("uploaded_file", None)
@@ -270,6 +343,7 @@ if st.session_state["videos"]:
270
  except Exception:
271
  pass
272
 
 
273
  if generate_now and not st.session_state.get("busy"):
274
  if not st.session_state.get("videos"):
275
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
@@ -336,7 +410,12 @@ if generate_now and not st.session_state.get("busy"):
336
  try:
337
  if not HAS_GENAI or genai is None:
338
  raise RuntimeError("Responses API not available; install google.generativeai SDK.")
339
- genai.configure(api_key=key_to_use)
 
 
 
 
 
340
  fname = file_name_or_id(processed)
341
  if not fname:
342
  raise RuntimeError("Uploaded file missing name/id")
@@ -345,10 +424,12 @@ if generate_now and not st.session_state.get("busy"):
345
  user_msg = {"role": "user", "content": "Please summarize the attached video."}
346
 
347
  response = None
 
348
 
349
- # try modern responses.generate if available
350
  try:
351
  if hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
 
352
  response = genai.responses.generate(
353
  model=model_used,
354
  messages=[system_msg, user_msg],
@@ -356,129 +437,135 @@ if generate_now and not st.session_state.get("busy"):
356
  safety_settings=safety_settings,
357
  max_output_tokens=max_tokens,
358
  )
359
- except Exception:
 
360
  response = None
361
 
362
- # try GenerativeModel (0.8.x)
363
  if response is None:
364
  try:
365
  if hasattr(genai, "GenerativeModel"):
 
366
  gm = genai.GenerativeModel(model=model_used)
367
  if hasattr(gm, "generate_content"):
368
  response = gm.generate_content([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
369
  elif hasattr(gm, "generate"):
370
  response = gm.generate([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
371
- except Exception:
 
372
  response = None
373
 
374
- # try top-level legacy helpers
375
  if response is None:
376
  try:
377
  if hasattr(genai, "generate"):
 
378
  response = genai.generate(model=model_used, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=max_tokens)
379
  elif hasattr(genai, "create"):
 
380
  response = genai.create(model=model_used, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=max_tokens)
381
- except Exception:
 
382
  response = None
383
 
384
- # if still None, fall back to best-effort: some environments may have the module but missing callables
385
  if response is None:
386
- # attempt to call GenerativeModel regardless if present (wrapped defensively)
387
  try:
388
  if hasattr(genai, "GenerativeModel"):
 
389
  gm = genai.GenerativeModel(model=model_used)
390
- # try generate_content/generate without raising
391
  try:
392
  response = gm.generate_content([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
393
  except Exception:
394
- try:
395
- response = gm.generate([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
396
- except Exception:
397
- response = None
398
- except Exception:
399
  response = None
400
 
401
  if response is None:
402
- # don't raise here; provide helpful diagnostics instead and return gracefully
403
- raise RuntimeError("No supported generate method found on google.generativeai in this runtime. See Environment diagnostics below.")
404
-
405
- # normalize outputs
406
- outputs = []
407
- try:
408
- if isinstance(response, dict):
409
- for key in ("output", "candidates", "items", "responses"):
410
- val = response.get(key)
411
- if isinstance(val, (list, tuple)) and val:
412
- outputs = list(val)
413
- break
414
- if not outputs:
415
- for v in response.values():
416
- if isinstance(v, (list, tuple)) and v:
417
- outputs = list(v)
418
- break
419
- else:
420
- for attr in ("output", "candidates", "items", "responses"):
421
- val = getattr(response, attr, None)
422
- if isinstance(val, (list, tuple)) and val:
423
- try:
424
- outputs = list(val)
425
- except Exception:
426
- outputs = val
427
- break
428
- except Exception:
429
  outputs = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
- if not outputs:
432
- candidate_text = None
433
- if isinstance(response, dict):
434
- candidate_text = response.get("text") or response.get("message")
435
- else:
436
- candidate_text = getattr(response, "text", None) or getattr(response, "message", None)
437
- if candidate_text:
438
- outputs = [{"text": candidate_text}]
439
-
440
- text_pieces = []
441
- for item in outputs:
442
- if not item:
443
- continue
444
- if isinstance(item, dict):
445
- for k in ("content", "text", "message", "output_text", "output"):
446
- v = item.get(k)
447
- if v:
448
- if isinstance(v, str):
449
- text_pieces.append(v.strip())
450
- elif isinstance(v, (list, tuple)):
451
- for e in v:
452
- if isinstance(e, str):
453
- text_pieces.append(e.strip())
454
- elif isinstance(e, dict):
455
- t = e.get("text") or e.get("content")
456
- if t:
457
- text_pieces.append(str(t).strip())
458
- break
459
- else:
460
- for k in ("content", "text", "message", "output", "output_text"):
461
- v = getattr(item, k, None)
462
- if v:
463
- if isinstance(v, str):
464
- text_pieces.append(v.strip())
465
- elif isinstance(v, (list, tuple)):
466
- for e in v:
467
- if isinstance(e, str):
468
- text_pieces.append(e.strip())
469
- else:
470
- t = getattr(e, "text", None) or getattr(e, "content", None)
471
- if t:
472
- text_pieces.append(str(t).strip())
473
- break
474
-
475
- seen = set()
476
- filtered = []
477
- for t in text_pieces:
478
- if t and t not in seen:
479
- filtered.append(t)
480
- seen.add(t)
481
- out = "\n\n".join(filtered)
482
 
483
  except Exception as e:
484
  tb = traceback.format_exc()
 
8
  from pathlib import Path
9
  from difflib import SequenceMatcher
10
 
11
+ import requests
12
+ from bs4 import BeautifulSoup
13
+
14
  import yt_dlp
15
  import ffmpeg
16
  import streamlit as st
 
18
 
19
  load_dotenv()
20
 
21
+ # phi agent removed to avoid fragile imports in varied environments
 
 
 
 
 
 
 
 
22
  HAS_PHI = False
23
 
24
+ # google generative ai SDK (may be absent or partial in some runtimes)
25
  try:
26
+ import google.generativeai as genai # type: ignore
27
  from google.generativeai import upload_file, get_file # type: ignore
28
  HAS_GENAI = True
29
  except Exception:
30
  genai = None
31
+ upload_file = None
32
+ get_file = None
33
  HAS_GENAI = False
34
 
35
  st.set_page_config(page_title="Generate the story of videos", layout="wide")
36
  DATA_DIR = Path("./data")
37
  DATA_DIR.mkdir(exist_ok=True)
38
 
39
+ # Session defaults
40
  st.session_state.setdefault("videos", "")
41
  st.session_state.setdefault("loop_video", False)
42
  st.session_state.setdefault("uploaded_file", None)
 
117
  if not key or not HAS_GENAI:
118
  return False
119
  try:
120
+ # defensive configuration (some envs require configure)
121
  genai.configure(api_key=key)
122
  return True
123
  except Exception:
 
137
  except Exception:
138
  pass
139
 
140
+ # URL expand + extraction helpers (integrated into Load Video)
141
+ def expand_url(short_url, timeout=10):
142
+ try:
143
+ r = requests.get(short_url, allow_redirects=True, timeout=timeout, headers={"User-Agent":"Mozilla/5.0"})
144
+ final_url = r.url
145
+ return final_url, r.text
146
+ except Exception as e:
147
+ return None, f"error: {e}"
148
+
149
+ def extract_video_from_html(html, base_url=None):
150
+ soup = BeautifulSoup(html, "html.parser")
151
+ # 1) Open Graph video
152
+ og = soup.find("meta", property="og:video")
153
+ if og and og.get("content"):
154
+ return og.get("content")
155
+ # 2) Look for video tags
156
+ vtag = soup.find("video")
157
+ if vtag:
158
+ src = vtag.get("src")
159
+ if src:
160
+ return src
161
+ # source children
162
+ source = vtag.find("source")
163
+ if source and source.get("src"):
164
+ return source.get("src")
165
+ # 3) JSON-LD or structured data with video
166
+ for script in soup.find_all("script", type="application/ld+json"):
167
+ try:
168
+ import json
169
+ data = json.loads(script.string or "{}")
170
+ # common pattern
171
+ if isinstance(data, dict):
172
+ video = data.get("video") or data.get("videoObject") or data.get("mainEntity")
173
+ if isinstance(video, dict):
174
+ for k in ("contentUrl", "url"):
175
+ if video.get(k):
176
+ return video.get(k)
177
+ # top-level contentUrl
178
+ if data.get("contentUrl"):
179
+ return data.get("contentUrl")
180
+ except Exception:
181
+ continue
182
+ # 4) look for meta property site-specific fallbacks
183
+ for meta_name in ("twitter:player:stream", "twitter:player"):
184
+ m = soup.find("meta", attrs={"name": meta_name})
185
+ if m and m.get("content"):
186
+ return m.get("content")
187
+ # fallback: search for direct links to common video hosts (youtube, vimeo) in anchor tags
188
+ for a in soup.find_all("a", href=True):
189
+ href = a["href"]
190
+ if any(domain in href for domain in ("youtube.com", "youtu.be", "vimeo.com")):
191
+ return href
192
+ return None
193
+
194
+ # When SDK has upload_file/get_file, use them; else raise when needed
195
  def upload_video_sdk(filepath: str):
196
  key = get_effective_api_key()
197
  if not key:
198
  raise RuntimeError("No API key provided")
199
  if not HAS_GENAI or upload_file is None:
200
+ raise RuntimeError("google.generativeai SDK upload not available; cannot upload")
201
  genai.configure(api_key=key)
202
  return upload_file(filepath)
203
 
 
244
  return b_full[len(ph):].lstrip(" \n:-")
245
  return text
246
 
247
+ # UI layout
248
+ current_url = st.session_state.get("url", "")
249
+ if current_url != st.session_state.get("last_url_value"):
250
+ clear_all_video_state()
251
+ st.session_state["last_url_value"] = current_url
252
+
253
+ st.sidebar.header("Video Input")
254
+ st.sidebar.text_input("Video URL", key="url", placeholder="https://")
255
+
256
+ settings_exp = st.sidebar.expander("Settings", expanded=False)
257
+ settings_exp.text_input("Gemini Model (short name)", "gemini-2.5-flash-lite", key="model_input")
258
+ settings_exp.text_input("Google API Key", key="api_key", value=os.getenv("GOOGLE_API_KEY", ""), type="password")
259
+ default_prompt = (
260
+ "Watch the video and provide a detailed behavioral report focusing on human actions, interactions, posture, movement, and apparent intent. Keep language professional. Include a list of observations for notable events."
261
+ )
262
+ settings_exp.text_area("Enter analysis", value=default_prompt, height=140, key="analysis_prompt")
263
+ settings_exp.text_input("Video Password (if needed)", key="video-password", placeholder="password", type="password")
264
+ settings_exp.checkbox("Fast mode (skip compression, smaller model, fewer tokens)", key="fast_mode")
265
+
266
+ key_source = "session" if st.session_state.get("api_key") else ".env" if os.getenv("GOOGLE_API_KEY") else "none"
267
+ settings_exp.caption(f"Using API key from: **{key_source}**")
268
+
269
+ if not get_effective_api_key():
270
+ settings_exp.warning("No Google API key provided; upload/generation disabled.", icon="⚠️")
271
+
272
+ safety_settings = [
273
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
274
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
275
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
276
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
277
+ ]
278
+
279
  col1, col2 = st.columns([1, 3])
280
  with col1:
281
  generate_now = st.button("Generate the story", type="primary", disabled=not bool(get_effective_api_key()))
282
  with col2:
283
  pass
284
 
285
+ # Load Video flow: expand short URLs and try to extract direct video links from HTML before yt-dlp
286
  if st.sidebar.button("Load Video", use_container_width=True):
287
  try:
288
  vpw = st.session_state.get("video-password", "")
289
+ url_val = st.session_state.get("url", "").strip()
290
+ # If URL present, try to expand and extract video from HTML first
291
+ final_url = url_val
292
+ html_text = None
293
+ if url_val:
294
+ expanded, html_or_err = expand_url(url_val)
295
+ if expanded:
296
+ final_url = expanded
297
+ html_text = html_or_err
298
+ else:
299
+ # expansion failed but html_or_err contains error message; ignore
300
+ html_text = None
301
+ # If we have HTML, try to find direct video link
302
+ extracted = None
303
+ if html_text:
304
+ extracted = extract_video_from_html(html_text, base_url=final_url)
305
+ target_url_for_ytdlp = extracted or final_url
306
+ path = download_video_ytdlp(target_url_for_ytdlp, str(DATA_DIR), vpw)
307
  st.session_state["videos"] = path
308
  st.session_state["last_loaded_path"] = path
309
  st.session_state.pop("uploaded_file", None)
 
343
  except Exception:
344
  pass
345
 
346
+ # Generation flow (robust handling of google.generativeai variants)
347
  if generate_now and not st.session_state.get("busy"):
348
  if not st.session_state.get("videos"):
349
  st.error("No video loaded. Use 'Load Video' in the sidebar.")
 
410
  try:
411
  if not HAS_GENAI or genai is None:
412
  raise RuntimeError("Responses API not available; install google.generativeai SDK.")
413
+ # ensure configured
414
+ try:
415
+ genai.configure(api_key=key_to_use)
416
+ except Exception:
417
+ pass
418
+
419
  fname = file_name_or_id(processed)
420
  if not fname:
421
  raise RuntimeError("Uploaded file missing name/id")
 
424
  user_msg = {"role": "user", "content": "Please summarize the attached video."}
425
 
426
  response = None
427
+ diagnostics = {"attempts": []}
428
 
429
+ # Attempt #1: genai.responses.generate (modern)
430
  try:
431
  if hasattr(genai, "responses") and hasattr(genai.responses, "generate"):
432
+ diagnostics["attempts"].append("responses.generate")
433
  response = genai.responses.generate(
434
  model=model_used,
435
  messages=[system_msg, user_msg],
 
437
  safety_settings=safety_settings,
438
  max_output_tokens=max_tokens,
439
  )
440
+ except Exception as e:
441
+ diagnostics["responses.generate_error"] = str(e)
442
  response = None
443
 
444
+ # Attempt #2: GenerativeModel variants (0.8.x+)
445
  if response is None:
446
  try:
447
  if hasattr(genai, "GenerativeModel"):
448
+ diagnostics["attempts"].append("GenerativeModel")
449
  gm = genai.GenerativeModel(model=model_used)
450
  if hasattr(gm, "generate_content"):
451
  response = gm.generate_content([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
452
  elif hasattr(gm, "generate"):
453
  response = gm.generate([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
454
+ except Exception as e:
455
+ diagnostics["GenerativeModel_error"] = str(e)
456
  response = None
457
 
458
+ # Attempt #3: top-level legacy helpers
459
  if response is None:
460
  try:
461
  if hasattr(genai, "generate"):
462
+ diagnostics["attempts"].append("top.generate")
463
  response = genai.generate(model=model_used, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=max_tokens)
464
  elif hasattr(genai, "create"):
465
+ diagnostics["attempts"].append("top.create")
466
  response = genai.create(model=model_used, input=[{"text": prompt_text, "files": [{"name": fname}]}], max_output_tokens=max_tokens)
467
+ except Exception as e:
468
+ diagnostics["top_level_error"] = str(e)
469
  response = None
470
 
471
+ # Final defensive tries for known objects
472
  if response is None:
 
473
  try:
474
  if hasattr(genai, "GenerativeModel"):
475
+ diagnostics["attempts"].append("GenerativeModel_last")
476
  gm = genai.GenerativeModel(model=model_used)
 
477
  try:
478
  response = gm.generate_content([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
479
  except Exception:
480
+ response = gm.generate([system_msg, user_msg], files=[{"name": fname}], max_output_tokens=max_tokens)
481
+ except Exception as e:
482
+ diagnostics["GenerativeModel_last_error"] = str(e)
 
 
483
  response = None
484
 
485
  if response is None:
486
+ # Instead of raising the runtime error seen previously, attach diagnostics to last_error and return gracefully
487
+ diag_text = f"No supported generate method found on google.generativeai in this runtime. Diagnostics: {diagnostics}"
488
+ st.session_state["last_error"] = diag_text
489
+ st.error("Responses API not supported in this runtime. See Last Error for details.")
490
+ out = ""
491
+ else:
492
+ # Normalize outputs into text pieces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  outputs = []
494
+ try:
495
+ if isinstance(response, dict):
496
+ for key in ("output", "candidates", "items", "responses"):
497
+ val = response.get(key)
498
+ if isinstance(val, (list, tuple)) and val:
499
+ outputs = list(val)
500
+ break
501
+ if not outputs:
502
+ for v in response.values():
503
+ if isinstance(v, (list, tuple)) and v:
504
+ outputs = list(v)
505
+ break
506
+ else:
507
+ for attr in ("output", "candidates", "items", "responses"):
508
+ val = getattr(response, attr, None)
509
+ if isinstance(val, (list, tuple)) and val:
510
+ try:
511
+ outputs = list(val)
512
+ except Exception:
513
+ outputs = val
514
+ break
515
+ except Exception:
516
+ outputs = []
517
+
518
+ if not outputs:
519
+ candidate_text = None
520
+ if isinstance(response, dict):
521
+ candidate_text = response.get("text") or response.get("message")
522
+ else:
523
+ candidate_text = getattr(response, "text", None) or getattr(response, "message", None)
524
+ if candidate_text:
525
+ outputs = [{"text": candidate_text}]
526
+
527
+ text_pieces = []
528
+ for item in outputs:
529
+ if not item:
530
+ continue
531
+ if isinstance(item, dict):
532
+ for k in ("content", "text", "message", "output_text", "output"):
533
+ v = item.get(k)
534
+ if v:
535
+ if isinstance(v, str):
536
+ text_pieces.append(v.strip())
537
+ elif isinstance(v, (list, tuple)):
538
+ for e in v:
539
+ if isinstance(e, str):
540
+ text_pieces.append(e.strip())
541
+ elif isinstance(e, dict):
542
+ t = e.get("text") or e.get("content")
543
+ if t:
544
+ text_pieces.append(str(t).strip())
545
+ break
546
+ else:
547
+ for k in ("content", "text", "message", "output", "output_text"):
548
+ v = getattr(item, k, None)
549
+ if v:
550
+ if isinstance(v, str):
551
+ text_pieces.append(v.strip())
552
+ elif isinstance(v, (list, tuple)):
553
+ for e in v:
554
+ if isinstance(e, str):
555
+ text_pieces.append(e.strip())
556
+ else:
557
+ t = getattr(e, "text", None) or getattr(e, "content", None)
558
+ if t:
559
+ text_pieces.append(str(t).strip())
560
+ break
561
 
562
+ seen = set()
563
+ filtered = []
564
+ for t in text_pieces:
565
+ if t and t not in seen:
566
+ filtered.append(t)
567
+ seen.add(t)
568
+ out = "\n\n".join(filtered)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  except Exception as e:
571
  tb = traceback.format_exc()