Hug0endob commited on
Commit
e14aeda
·
verified ·
1 Parent(s): da12238

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -66
app.py CHANGED
@@ -9,8 +9,6 @@ from typing import List, Tuple, Optional
9
  import requests
10
  from PIL import Image, ImageFile, UnidentifiedImageError
11
  import gradio as gr
12
- # import asyncio # Not used
13
- # import threading # Not directly used for core logic, implicit in Gradio's max_threads
14
  import time
15
  import atexit
16
  from requests.exceptions import RequestException, HTTPError # Import for rate limiting
@@ -19,7 +17,7 @@ from requests.exceptions import RequestException, HTTPError # Import for rate li
19
  DEFAULT_KEY = os.getenv("MISTRAL_API_KEY", "")
20
  PIXTRAL_MODEL = "pixtral-12b-2409"
21
  VIDEO_MODEL = "voxtral-mini-latest"
22
- STREAM_THRESHOLD = 20 * 1024 * 1024
23
  FFMPEG_BIN = shutil.which("ffmpeg")
24
  IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif")
25
  VIDEO_EXTS = (".mp4", ".mov", ".webm", ".mkv", ".avi", ".flv")
@@ -49,10 +47,11 @@ except ImportError:
49
  def __init__(self, message, status_code=None, *args, **kwargs):
50
  super().__init__(message, *args)
51
  self.status_code = status_code
 
 
52
  @property
53
  def message(self):
54
- return self.args[0] if self.args else "An unknown Mistral API error occurred (mistralai library not installed)."
55
-
56
 
57
  # --- Temporary File Cleanup ---
58
  _temp_preview_files_to_delete = []
@@ -75,48 +74,50 @@ def get_client(key: Optional[str] = None):
75
  Returns a Mistral client instance. If the mistralai library is not installed
76
  or the API key is missing, a mock client is returned which raises
77
  MistralAPIException with specific messages.
 
 
78
  """
79
  api_key = (key or "").strip() or DEFAULT_KEY
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  if not api_key:
82
- class Dummy:
83
- def __init__(self, k): self.api_key = k
84
- class Chat:
85
- def complete(self, **kwargs):
86
- raise MistralAPIException(
87
- "Mistral API key is not set. Please provide it in the UI or as MISTRAL_API_KEY environment variable.",
88
- status_code=401
89
- )
90
- chat = Chat()
91
- class Files:
92
- def upload(self, **kwargs):
93
- raise MistralAPIException(
94
- "Mistral API key is not set. Please provide it in the UI or as MISTRAL_API_KEY environment variable.",
95
- status_code=401
96
- )
97
- files = Files()
98
- return Dummy(api_key)
99
 
100
  if Mistral is None:
101
- class Dummy:
102
- def __init__(self, k): self.api_key = k
103
- class Chat:
104
- def complete(self, **kwargs):
105
- raise MistralAPIException(
106
- "Mistral client library is not installed. Please install it with 'pip install mistralai'.",
107
- status_code=500
108
- )
109
- chat = Chat()
110
- class Files:
111
- def upload(self, **kwargs):
112
- raise MistralAPIException(
113
- "Mistral client library is not installed. Please install it with 'pip install mistralai'.",
114
- status_code=500
115
- )
116
- files = Files()
117
- return Dummy(api_key)
118
 
119
- return Mistral(api_key=api_key)
 
 
 
120
 
121
  def is_remote(src: str) -> bool:
122
  return bool(src) and src.startswith(("http://", "https://"))
@@ -264,18 +265,19 @@ def _get_video_info_and_timestamps(media_path: str, sample_count: int) -> Tuple[
264
  timestamps: List[float] = []
265
  if duration > 0 and sample_count > 0:
266
  # Ensure we don't go past the video duration, or request too many samples
267
- actual_sample_count = min(sample_count, int(duration)) # Limit samples to integer seconds
 
268
  if actual_sample_count > 0:
269
  step = duration / (actual_sample_count + 1)
270
  timestamps = [step * (i + 1) for i in range(actual_sample_count)]
271
 
272
  # If no timestamps generated or duration is 0, fall back to fixed times
273
  if not timestamps:
274
- timestamps = [0.5, 1.0, 2.0, 3.0, 4.0][:sample_count]
275
 
276
  return info, timestamps
277
 
278
- def extract_frames_for_model_and_gallery(media_path: str, sample_count: int = 5, timeout_extract: int = 15, gallery_base_h: int = 720, progress=None) -> Tuple[List[bytes], List[str]]:
279
  """
280
  Extracts frames from a video, processes them for both model input (high-res JPEG bytes)
281
  and gallery display (smaller JPEG temp file paths), in a single pass.
@@ -304,6 +306,7 @@ def extract_frames_for_model_and_gallery(media_path: str, sample_count: int = 5,
304
  fd_raw, tmp_png_path = tempfile.mkstemp(suffix=f"_frame_{i}.png")
305
  os.close(fd_raw)
306
 
 
307
  cmd_extract = [
308
  FFMPEG_BIN, "-nostdin", "-y", "-ss", str(t), "-i", media_path,
309
  "-frames:v", "1", "-pix_fmt", "rgb24", tmp_png_path,
@@ -317,14 +320,14 @@ def extract_frames_for_model_and_gallery(media_path: str, sample_count: int = 5,
317
  raw_frame_bytes = f.read()
318
 
319
  # For model: convert to high-res JPEG bytes (model expects this)
320
- jpeg_model_bytes = convert_to_jpeg_bytes(raw_frame_bytes, base_h=720) # Keep higher res for model
321
  if jpeg_model_bytes: # Only append if conversion was successful
322
  frames_for_model.append(jpeg_model_bytes)
323
  else:
324
  print(f"Warning: Failed to convert extracted frame {i+1} to JPEG for model input.")
325
 
326
  # For gallery: convert to smaller JPEG bytes and save as new temp file
327
- jpeg_gallery_bytes = convert_to_jpeg_bytes(raw_frame_bytes, base_h=gallery_base_h) # Higher base_h
328
  if jpeg_gallery_bytes: # Only create temp file if conversion was successful
329
  temp_jpeg_path = _temp_file(jpeg_gallery_bytes, suffix=f"_gallery_{i}.jpg") # _temp_file tracks this for cleanup
330
  if temp_jpeg_path: # Only add to gallery if temp file was successfully created
@@ -353,23 +356,28 @@ def chat_complete(client, model: str, messages, timeout: int = 120, progress=Non
353
  if progress is not None:
354
  progress(0.6 + 0.01 * attempt, desc=f"Sending request to model (attempt {attempt+1}/{max_retries})...")
355
 
356
- # Prefer using the Mistral client if available and functional
357
- if hasattr(client, "chat") and hasattr(client.chat, "complete"):
358
  res = client.chat.complete(model=model, messages=messages, stream=False, timeout_ms=timeout * 1000)
359
- else: # This path should ideally not be reached if get_client returns a dummy with exceptions
360
- api_key = getattr(client, "api_key", "") or DEFAULT_KEY
 
361
  if not api_key:
362
- return "Error: Mistral API key is not set. Cannot make direct requests."
 
 
 
 
363
  url = "https://api.mistral.ai/v1/chat/completions"
364
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
365
  r = requests.post(url, json={"model": model, "messages": messages}, headers=headers, timeout=timeout)
366
  r.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
367
  res = r.json()
 
368
 
369
  if progress is not None:
370
  progress(0.8, desc="Model responded, parsing...")
371
 
372
- choices = getattr(res, "choices", None) or (res.get("choices") if isinstance(res, dict) else [])
373
  if not choices:
374
  return f"Empty response from model: {res}"
375
 
@@ -413,7 +421,8 @@ def upload_file_to_mistral(client, path: str, filename: str | None = None, purpo
413
  if progress is not None:
414
  progress(0.5 + 0.01 * attempt, desc=f"Uploading file to model service (attempt {attempt+1}/{max_retries})...")
415
 
416
- if hasattr(client, "files") and hasattr(client.files, "upload"):
 
417
  with open(path, "rb") as fh:
418
  res = client.files.upload(file={"file_name": fname, "content": fh}, purpose=purpose)
419
  fid = getattr(res, "id", None) or (res.get("id") if isinstance(res, dict) else None)
@@ -424,10 +433,12 @@ def upload_file_to_mistral(client, path: str, filename: str | None = None, purpo
424
  if progress is not None:
425
  progress(0.6, desc="Upload complete")
426
  return fid
427
- else: # This path should ideally not be reached if get_client returns a dummy with exceptions
428
- api_key = getattr(client, "api_key", "") or DEFAULT_KEY
429
  if not api_key:
430
- raise RuntimeError("Mistral API key is not set for file upload.")
 
 
431
  url = "https://api.mistral.ai/v1/files"
432
  headers = {"Authorization": f"Bearer {api_key}"}
433
  with open(path, "rb") as fh:
@@ -491,7 +502,7 @@ def analyze_image_structured(client, img_bytes: bytes, prompt: str, progress=Non
491
  try:
492
  if progress is not None:
493
  progress(0.3, desc="Preparing image for analysis...")
494
- jpeg = convert_to_jpeg_bytes(img_bytes, base_h=1024)
495
  if not jpeg: # Handle case where convert_to_jpeg_bytes failed
496
  return "Error: Could not convert image for analysis."
497
  data_url = b64_bytes(jpeg, mime="image/jpeg")
@@ -528,9 +539,9 @@ def analyze_video_cohesive(client, video_path: str, prompt: str, progress=None)
528
  ]
529
  result = chat_complete(client, VIDEO_MODEL, messages, progress=progress)
530
  # If successful upload, still extract frames for gallery display
531
- # Use the combined function for gallery frames
532
  _, gallery_frame_paths = extract_frames_for_model_and_gallery(
533
- video_path, sample_count=6, gallery_base_h=720, progress=progress # Increased gallery_base_h
534
  )
535
  return result, gallery_frame_paths
536
  except Exception as e:
@@ -538,8 +549,9 @@ def analyze_video_cohesive(client, video_path: str, prompt: str, progress=None)
538
  progress(0.35, desc=f"Upload failed for video ({e}). Extracting frames as fallback...")
539
 
540
  # Use the combined extraction function for both model input and gallery display
 
541
  frames_for_model_bytes, gallery_frame_paths = extract_frames_for_model_and_gallery(
542
- video_path, sample_count=6, gallery_base_h=720, progress=progress # Increased gallery_base_h
543
  )
544
 
545
  if not frames_for_model_bytes:
@@ -576,9 +588,7 @@ def _convert_video_for_preview_if_needed(path: str) -> str:
576
  return path # Cannot convert, return original
577
 
578
  # Check if the video is already likely browser-compatible (MP4 with H.264/H.265)
579
- # This check is a heuristic; direct ffprobe is more reliable but adds overhead.
580
- # For a preview, extension check is usually sufficient.
581
- if path.lower().endswith((".mp4", ".m4v")): # .mov might still need conversion
582
  info = _ffprobe_streams(path)
583
  if info:
584
  video_streams = [s for s in info.get("streams", []) if s.get("codec_type") == "video"]
@@ -761,7 +771,6 @@ def create_demo():
761
  return img_update_clear, video_update_clear, gallery_update_clear, \
762
  gr.update(value="", visible=True), main_path_clear, screenshot_paths_clear, raw_media_path_clear
763
 
764
- raw_bytes_for_analysis = b""
765
  temp_raw_path_for_analysis = ""
766
  try:
767
  # 1. Fetch raw bytes once for both preview and analysis
@@ -789,6 +798,7 @@ def create_demo():
789
 
790
  if not local_playable_path:
791
  # If preview failed, cleanup the temp_raw_path_for_analysis as well
 
792
  if temp_raw_path_for_analysis in _temp_preview_files_to_delete:
793
  _temp_preview_files_to_delete.remove(temp_raw_path_for_analysis)
794
  try: os.remove(temp_raw_path_for_analysis)
@@ -811,6 +821,12 @@ def create_demo():
811
  gr.update(value=[], visible=False), gr.update(value="Video preview loaded.", visible=True), \
812
  local_playable_path, [], temp_raw_path_for_analysis # Update raw_media_path_state
813
  else:
 
 
 
 
 
 
814
  return img_update_clear, video_update_clear, gallery_update_clear, \
815
  gr.update(value="Preview load failed: unknown playable format.", visible=True), \
816
  main_path_clear, screenshot_paths_clear, raw_media_path_clear
@@ -829,7 +845,7 @@ def create_demo():
829
  url_input.change(
830
  fn=load_main_preview_and_clear_old,
831
  inputs=[url_input],
832
- outputs=[preview_image, preview_video, screenshot_gallery, preview_status, main_preview_path_state, screenshot_paths_state, raw_media_path_state]
833
  )
834
 
835
  def worker(url: str, prompt: str, key: str, current_main_preview_path: str, raw_media_path: str, progress=gr.Progress()):
@@ -861,7 +877,8 @@ def create_demo():
861
  Image.open(BytesIO(raw_bytes_for_analysis)).verify()
862
  is_actually_image_for_analysis = True
863
  except UnidentifiedImageError:
864
- is_actually_video_for_analysis = True # Not an identifiable image
 
865
  except Exception as e:
866
  print(f"Warning: PIL error during image verification for raw analysis media ({raw_media_path}): {e}. Falling back to video.")
867
  is_actually_video_for_analysis = True # Other PIL error, treat as video
@@ -870,7 +887,7 @@ def create_demo():
870
 
871
  # --- Video Processing Path ---
872
  if is_actually_video_for_analysis:
873
- progress(0.25, desc="Running fullvideo analysis")
874
  result_text, generated_screenshot_paths = analyze_video_cohesive(client, raw_media_path, prompt, progress=progress)
875
 
876
  # --- Image Processing Path ---
 
9
  import requests
10
  from PIL import Image, ImageFile, UnidentifiedImageError
11
  import gradio as gr
 
 
12
  import time
13
  import atexit
14
  from requests.exceptions import RequestException, HTTPError # Import for rate limiting
 
17
  DEFAULT_KEY = os.getenv("MISTRAL_API_KEY", "")
18
  PIXTRAL_MODEL = "pixtral-12b-2409"
19
  VIDEO_MODEL = "voxtral-mini-latest"
20
+ STREAM_THRESHOLD = 20 * 1024 * 1024 # 20MB
21
  FFMPEG_BIN = shutil.which("ffmpeg")
22
  IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif")
23
  VIDEO_EXTS = (".mp4", ".mov", ".webm", ".mkv", ".avi", ".flv")
 
47
  def __init__(self, message, status_code=None, *args, **kwargs):
48
  super().__init__(message, *args)
49
  self.status_code = status_code
50
+ self._message = message # Store message explicitly for property
51
+
52
  @property
53
  def message(self):
54
+ return self._message # Return the stored message
 
55
 
56
  # --- Temporary File Cleanup ---
57
  _temp_preview_files_to_delete = []
 
74
  Returns a Mistral client instance. If the mistralai library is not installed
75
  or the API key is missing, a mock client is returned which raises
76
  MistralAPIException with specific messages.
77
+ A `is_real_client` flag is added for `chat_complete`/`upload_file_to_mistral`
78
+ to distinguish between a real client and a dummy.
79
  """
80
  api_key = (key or "").strip() or DEFAULT_KEY
81
 
82
+ # Helper class for dummy chat/files service
83
+ class DummyMistralService:
84
+ def __init__(self, error_message: str, status_code: Optional[int] = None):
85
+ self._error_message = error_message
86
+ self._status_code = status_code
87
+
88
+ def complete(self, **kwargs):
89
+ raise MistralAPIException(self._error_message, status_code=self._status_code)
90
+
91
+ def upload(self, **kwargs):
92
+ raise MistralAPIException(self._error_message, status_code=self._status_code)
93
+
94
+ # Dummy client wrapper
95
+ class DummyClient:
96
+ def __init__(self, k, library_missing_msg: Optional[str] = None, api_key_missing_msg: Optional[str] = None):
97
+ self.api_key = k
98
+ self.is_real_client = False
99
+
100
+ # Determine which dummy service to provide
101
+ if not k: # API key missing
102
+ error_msg = api_key_missing_msg or "Mistral API key is not set."
103
+ status_code = 401
104
+ else: # Library missing (and key is present)
105
+ error_msg = library_missing_msg or "Mistral client library is not installed."
106
+ status_code = 500
107
+
108
+ self.chat = DummyMistralService(error_msg, status_code)
109
+ self.files = DummyMistralService(error_msg, status_code)
110
+
111
  if not api_key:
112
+ return DummyClient(api_key, api_key_missing_msg="Mistral API key is not set. Please provide it in the UI or as MISTRAL_API_KEY environment variable.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  if Mistral is None:
115
+ return DummyClient(api_key, library_missing_msg="Mistral client library is not installed. Please install it with 'pip install mistralai'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ # If Mistral is installed and API key is present, return real client
118
+ client = Mistral(api_key=api_key)
119
+ setattr(client, 'is_real_client', True) # Mark as real client
120
+ return client
121
 
122
  def is_remote(src: str) -> bool:
123
  return bool(src) and src.startswith(("http://", "https://"))
 
265
  timestamps: List[float] = []
266
  if duration > 0 and sample_count > 0:
267
  # Ensure we don't go past the video duration, or request too many samples
268
+ # Limit samples to integer seconds to avoid issues with very short videos and excessive samples
269
+ actual_sample_count = min(sample_count, max(1, int(duration)))
270
  if actual_sample_count > 0:
271
  step = duration / (actual_sample_count + 1)
272
  timestamps = [step * (i + 1) for i in range(actual_sample_count)]
273
 
274
  # If no timestamps generated or duration is 0, fall back to fixed times
275
  if not timestamps:
276
+ timestamps = [0.5, 1.0, 2.0, 3.0, 4.0][:sample_count] # Use fixed initial seconds
277
 
278
  return info, timestamps
279
 
280
+ def extract_frames_for_model_and_gallery(media_path: str, sample_count: int = 5, timeout_extract: int = 15, gallery_base_h: int = 1080, model_base_h: int = 1024, progress=None) -> Tuple[List[bytes], List[str]]:
281
  """
282
  Extracts frames from a video, processes them for both model input (high-res JPEG bytes)
283
  and gallery display (smaller JPEG temp file paths), in a single pass.
 
306
  fd_raw, tmp_png_path = tempfile.mkstemp(suffix=f"_frame_{i}.png")
307
  os.close(fd_raw)
308
 
309
+ # ffmpeg command to extract a single frame
310
  cmd_extract = [
311
  FFMPEG_BIN, "-nostdin", "-y", "-ss", str(t), "-i", media_path,
312
  "-frames:v", "1", "-pix_fmt", "rgb24", tmp_png_path,
 
320
  raw_frame_bytes = f.read()
321
 
322
  # For model: convert to high-res JPEG bytes (model expects this)
323
+ jpeg_model_bytes = convert_to_jpeg_bytes(raw_frame_bytes, base_h=model_base_h) # Use model_base_h
324
  if jpeg_model_bytes: # Only append if conversion was successful
325
  frames_for_model.append(jpeg_model_bytes)
326
  else:
327
  print(f"Warning: Failed to convert extracted frame {i+1} to JPEG for model input.")
328
 
329
  # For gallery: convert to smaller JPEG bytes and save as new temp file
330
+ jpeg_gallery_bytes = convert_to_jpeg_bytes(raw_frame_bytes, base_h=gallery_base_h) # Use gallery_base_h
331
  if jpeg_gallery_bytes: # Only create temp file if conversion was successful
332
  temp_jpeg_path = _temp_file(jpeg_gallery_bytes, suffix=f"_gallery_{i}.jpg") # _temp_file tracks this for cleanup
333
  if temp_jpeg_path: # Only add to gallery if temp file was successfully created
 
356
  if progress is not None:
357
  progress(0.6 + 0.01 * attempt, desc=f"Sending request to model (attempt {attempt+1}/{max_retries})...")
358
 
359
+ # Check if it's the real Mistral client or a dummy/fallback to direct requests
360
+ if getattr(client, 'is_real_client', False):
361
  res = client.chat.complete(model=model, messages=messages, stream=False, timeout_ms=timeout * 1000)
362
+ choices = getattr(res, "choices", None)
363
+ else: # Fallback to direct requests if Mistral library not installed or API key missing
364
+ api_key = getattr(client, "api_key", "") # Get API key from the client object (real or dummy)
365
  if not api_key:
366
+ # This case should ideally be caught by get_client returning a DummyClient
367
+ # but provides a safety net if somehow we bypass that.
368
+ raise MistralAPIException(
369
+ "Mistral API key is not set for direct HTTP request.", status_code=401
370
+ )
371
  url = "https://api.mistral.ai/v1/chat/completions"
372
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
373
  r = requests.post(url, json={"model": model, "messages": messages}, headers=headers, timeout=timeout)
374
  r.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
375
  res = r.json()
376
+ choices = res.get("choices") # For dictionary response from direct requests
377
 
378
  if progress is not None:
379
  progress(0.8, desc="Model responded, parsing...")
380
 
 
381
  if not choices:
382
  return f"Empty response from model: {res}"
383
 
 
421
  if progress is not None:
422
  progress(0.5 + 0.01 * attempt, desc=f"Uploading file to model service (attempt {attempt+1}/{max_retries})...")
423
 
424
+ # Check if it's the real Mistral client or a dummy/fallback to direct requests
425
+ if getattr(client, 'is_real_client', False):
426
  with open(path, "rb") as fh:
427
  res = client.files.upload(file={"file_name": fname, "content": fh}, purpose=purpose)
428
  fid = getattr(res, "id", None) or (res.get("id") if isinstance(res, dict) else None)
 
433
  if progress is not None:
434
  progress(0.6, desc="Upload complete")
435
  return fid
436
+ else: # Fallback to direct requests if Mistral library not installed or API key missing
437
+ api_key = getattr(client, "api_key", "")
438
  if not api_key:
439
+ raise MistralAPIException(
440
+ "Mistral API key is not set for file upload.", status_code=401
441
+ )
442
  url = "https://api.mistral.ai/v1/files"
443
  headers = {"Authorization": f"Bearer {api_key}"}
444
  with open(path, "rb") as fh:
 
502
  try:
503
  if progress is not None:
504
  progress(0.3, desc="Preparing image for analysis...")
505
+ jpeg = convert_to_jpeg_bytes(img_bytes, base_h=1024) # High-res for model input
506
  if not jpeg: # Handle case where convert_to_jpeg_bytes failed
507
  return "Error: Could not convert image for analysis."
508
  data_url = b64_bytes(jpeg, mime="image/jpeg")
 
539
  ]
540
  result = chat_complete(client, VIDEO_MODEL, messages, progress=progress)
541
  # If successful upload, still extract frames for gallery display
542
+ # Use the combined function for gallery frames, now with higher resolution
543
  _, gallery_frame_paths = extract_frames_for_model_and_gallery(
544
+ video_path, sample_count=6, gallery_base_h=1080, model_base_h=1024, progress=progress
545
  )
546
  return result, gallery_frame_paths
547
  except Exception as e:
 
549
  progress(0.35, desc=f"Upload failed for video ({e}). Extracting frames as fallback...")
550
 
551
  # Use the combined extraction function for both model input and gallery display
552
+ # Higher resolution for both model frames and gallery frames
553
  frames_for_model_bytes, gallery_frame_paths = extract_frames_for_model_and_gallery(
554
+ video_path, sample_count=6, gallery_base_h=1080, model_base_h=1024, progress=progress
555
  )
556
 
557
  if not frames_for_model_bytes:
 
588
  return path # Cannot convert, return original
589
 
590
  # Check if the video is already likely browser-compatible (MP4 with H.264/H.265)
591
+ if path.lower().endswith((".mp4", ".m4v")):
 
 
592
  info = _ffprobe_streams(path)
593
  if info:
594
  video_streams = [s for s in info.get("streams", []) if s.get("codec_type") == "video"]
 
771
  return img_update_clear, video_update_clear, gallery_update_clear, \
772
  gr.update(value="", visible=True), main_path_clear, screenshot_paths_clear, raw_media_path_clear
773
 
 
774
  temp_raw_path_for_analysis = ""
775
  try:
776
  # 1. Fetch raw bytes once for both preview and analysis
 
798
 
799
  if not local_playable_path:
800
  # If preview failed, cleanup the temp_raw_path_for_analysis as well
801
+ # (though atexit/clear_all_files_and_ui would eventually get it)
802
  if temp_raw_path_for_analysis in _temp_preview_files_to_delete:
803
  _temp_preview_files_to_delete.remove(temp_raw_path_for_analysis)
804
  try: os.remove(temp_raw_path_for_analysis)
 
821
  gr.update(value=[], visible=False), gr.update(value="Video preview loaded.", visible=True), \
822
  local_playable_path, [], temp_raw_path_for_analysis # Update raw_media_path_state
823
  else:
824
+ # If local_playable_path exists but is not image/video, clean it up
825
+ if local_playable_path in _temp_preview_files_to_delete:
826
+ _temp_preview_files_to_delete.remove(local_playable_path)
827
+ try: os.remove(local_playable_path)
828
+ except Exception as e: print(f"Error during cleanup of unplayable temp file {local_playable_path}: {e}")
829
+
830
  return img_update_clear, video_update_clear, gallery_update_clear, \
831
  gr.update(value="Preview load failed: unknown playable format.", visible=True), \
832
  main_path_clear, screenshot_paths_clear, raw_media_path_clear
 
845
  url_input.change(
846
  fn=load_main_preview_and_clear_old,
847
  inputs=[url_input],
848
+ outputs=[preview_image, preview_video, screenshot_gallery, preview_status, main_preview_path_state, raw_media_path_state] # Removed screenshot_paths_state from here as it's cleared anyway.
849
  )
850
 
851
  def worker(url: str, prompt: str, key: str, current_main_preview_path: str, raw_media_path: str, progress=gr.Progress()):
 
877
  Image.open(BytesIO(raw_bytes_for_analysis)).verify()
878
  is_actually_image_for_analysis = True
879
  except UnidentifiedImageError:
880
+ # If it's not an identifiable image, assume it might be a video
881
+ is_actually_video_for_analysis = True
882
  except Exception as e:
883
  print(f"Warning: PIL error during image verification for raw analysis media ({raw_media_path}): {e}. Falling back to video.")
884
  is_actually_video_for_analysis = True # Other PIL error, treat as video
 
887
 
888
  # --- Video Processing Path ---
889
  if is_actually_video_for_analysis:
890
+ progress(0.25, desc="Running full-video analysis")
891
  result_text, generated_screenshot_paths = analyze_video_cohesive(client, raw_media_path, prompt, progress=progress)
892
 
893
  # --- Image Processing Path ---