Hug0endob commited on
Commit
785aaa3
·
verified ·
1 Parent(s): 58d3ae4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -77
app.py CHANGED
@@ -2,14 +2,14 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  from __future__ import annotations
5
- import os, shutil, subprocess, tempfile, base64
6
  from io import BytesIO
7
  from typing import List, Tuple
8
  import requests
9
  from PIL import Image, ImageFile, UnidentifiedImageError
10
  import gradio as gr
11
 
12
- # --- CONFIG (keep or set env MISTRAL_API_KEY)
13
  DEFAULT_KEY = os.getenv("MISTRAL_API_KEY", "")
14
  PIXTRAL_MODEL = "pixtral-12b-2409"
15
  VIDEO_MODEL = "voxtral-mini-latest"
@@ -18,8 +18,12 @@ FFMPEG_BIN = shutil.which("ffmpeg")
18
  IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif")
19
  VIDEO_EXTS = (".mp4", ".mov", ".webm", ".mkv", ".avi", ".flv")
20
 
21
- SYSTEM_INSTRUCTION = ("You are a clinical visual analyst. Only analyze media actually provided. "
22
- "Provide factual descriptions; do not invent sensory info.")
 
 
 
 
23
 
24
  ImageFile.LOAD_TRUNCATED_IMAGES = True
25
  Image.MAX_IMAGE_PIXELS = 10000 * 10000
@@ -32,13 +36,13 @@ except Exception:
32
  def get_client(key: str | None = None):
33
  api_key = (key or "").strip() or DEFAULT_KEY
34
  if Mistral is None:
35
- class Dummy:
36
- def __init__(self,k): self.api_key=k
37
  return Dummy(api_key)
38
  return Mistral(api_key=api_key)
39
 
40
  def is_remote(src: str) -> bool:
41
- return bool(src) and src.startswith(("http://","https://"))
42
 
43
  def ext_from_src(src: str) -> str:
44
  if not src: return ""
@@ -66,12 +70,13 @@ def fetch_bytes(src: str, stream_threshold: int = STREAM_THRESHOLD, timeout: int
66
  if cl and int(cl) > stream_threshold:
67
  with requests.get(src, timeout=timeout, stream=True) as r:
68
  r.raise_for_status()
69
- fd, p = tempfile.mkstemp(); os.close(fd)
 
70
  try:
71
- with open(p,"wb") as fh:
72
  for chunk in r.iter_content(8192):
73
  if chunk: fh.write(chunk)
74
- with open(p,"rb") as fh: return fh.read()
75
  finally:
76
  try: os.remove(p)
77
  except Exception: pass
@@ -80,98 +85,233 @@ def fetch_bytes(src: str, stream_threshold: int = STREAM_THRESHOLD, timeout: int
80
  r = safe_get(src, timeout=timeout)
81
  return r.content
82
  else:
83
- with open(src,"rb") as f: return f.read()
84
 
85
- def save_bytes_to_temp(b: bytes, suffix: str) -> str:
86
- fd, path = tempfile.mkstemp(suffix=suffix); os.close(fd)
87
- with open(path,"wb") as f: f.write(b)
88
- return path
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- def convert_to_jpeg_bytes(img_bytes: bytes, base_h: int = 480) -> bytes:
91
- img = Image.open(BytesIO(img_bytes))
92
  try:
93
- if getattr(img,"is_animated",False): img.seek(0)
94
- except Exception: pass
95
- if img.mode != "RGB": img = img.convert("RGB")
96
- h = base_h; w = max(1, int(img.width * (h / img.height)))
97
- img = img.resize((w,h), Image.LANCZOS)
98
- buf = BytesIO(); img.save(buf, format="JPEG", quality=85)
99
- return buf.getvalue()
100
-
101
- def b64_bytes(b: bytes, mime: str = "image/jpeg") -> str:
102
- return f"data:{mime};base64," + base64.b64encode(b).decode("utf-8")
103
-
104
- # --- model wrappers (chat_complete, upload_file_to_mistral, analyze_image_structured, analyze_video_cohesive)
105
- # Keep your existing implementations here unchanged (omitted in this snippet for brevity).
106
- # Insert the exact helper implementations from your prior file for chat_complete, upload_file_to_mistral,
107
- # analyze_image_structured, analyze_video_cohesive, extract_best_frames_bytes, determine_media_type, process_media.
108
- # (To run, paste the helper functions you already have above this UI block.)
109
-
110
- # --- UI ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  css = ".preview_media img, .preview_media video { max-width: 100%; height: auto; border-radius:6px; }"
112
 
113
  def _btn_label_for_status(status: str) -> str:
114
- return {"idle":"Submit","busy":"Processing…","done":"Submit","error":"Retry"}.get(status or "idle","Submit")
115
 
116
  def create_demo():
117
  with gr.Blocks(title="Flux Multimodal", css=css) as demo:
118
- with gr.Column():
119
- with gr.Row():
120
  preview_image = gr.Image(label="Preview Image", type="pil", elem_classes="preview_media", visible=False)
121
  preview_video = gr.Video(label="Preview Video", elem_classes="preview_media", visible=False)
122
- url_input = gr.Textbox(label="Image / Video URL or local path", placeholder="https://... or /path/to/file", lines=1)
123
- with gr.Accordion("Prompt (optional)", open=False):
124
- custom_prompt = gr.Textbox(label="Prompt", lines=4, value="")
125
- with gr.Accordion("Mistral API Key (optional)", open=False):
126
- api_key = gr.Textbox(label="API Key", type="password", max_lines=1)
127
-
128
- # buttons on same row
129
- with gr.Row():
130
- submit_btn = gr.Button(_btn_label_for_status("idle"))
131
- clear_btn = gr.Button("Clear")
132
- output_md = gr.Markdown("")
133
- status_state = gr.State("idle")
134
 
135
  def load_preview(url: str):
136
  empty_img = gr.update(value=None, visible=False)
137
  empty_vid = gr.update(value=None, visible=False)
138
- if not url:
139
- return empty_img, empty_vid
140
- # local files
141
  if not is_remote(url) and os.path.exists(url):
142
  ext = ext_from_src(url)
143
- if ext in VIDEO_EXTS:
144
- return empty_img, gr.update(value=os.path.abspath(url), visible=True)
145
  if ext in IMAGE_EXTS:
146
  try:
147
  img = Image.open(url)
148
- if getattr(img,"is_animated",False): img.seek(0)
149
  return gr.update(value=img.convert("RGB"), visible=True), empty_vid
150
- except Exception:
151
- return empty_img, empty_vid
152
- # remote: header-based check
153
  head = safe_head(url)
154
  if head:
155
  ctype = (head.headers.get("content-type") or "").lower()
156
  if ctype.startswith("video/") or any(url.lower().endswith(ext) for ext in VIDEO_EXTS):
157
  return empty_img, gr.update(value=url, visible=True)
158
- if ctype.startswith("image/") or any(url.lower().endswith(ext) for ext in IMAGE_EXTS):
159
- try:
160
- r = safe_get(url, timeout=15)
161
- img = Image.open(BytesIO(r.content))
162
- if getattr(img,"is_animated",False): img.seek(0)
163
- return gr.update(value=img.convert("RGB"), visible=True), empty_vid
164
- except Exception:
165
- return empty_img, empty_vid
166
- # fallback: try GET as image, otherwise treat as video URL
167
  try:
168
  r = safe_get(url, timeout=15)
169
  img = Image.open(BytesIO(r.content))
170
- if getattr(img,"is_animated",False): img.seek(0)
171
  return gr.update(value=img.convert("RGB"), visible=True), empty_vid
172
  except Exception:
173
- # pass URL to video if it looks like a video ext or header indicated video earlier failed
174
- return empty_img, gr.update(value=url, visible=True)
175
 
176
  url_input.change(fn=load_preview, inputs=[url_input], outputs=[preview_image, preview_video])
177
 
@@ -179,16 +319,13 @@ def create_demo():
179
  return "", gr.update(value=None, visible=False), gr.update(value=None, visible=False), "idle", gr.update(value=_btn_label_for_status("idle"))
180
  clear_btn.click(fn=clear_all, inputs=[], outputs=[url_input, preview_image, preview_video, status_state, submit_btn])
181
 
182
- # start_busy returns exactly two outputs: (status_state, submit_btn)
183
  def start_busy():
184
  s = "busy"
185
  return s, gr.update(value=_btn_label_for_status(s))
186
-
187
  submit_btn.click(fn=start_busy, inputs=[], outputs=[status_state, submit_btn])
188
 
189
  def worker(url: str, prompt: str, key: str, progress=gr.Progress()):
190
  return process_media(url or "", prompt or "", key or "", progress=progress)
191
-
192
  submit_btn.click(fn=worker, inputs=[url_input, custom_prompt, api_key], outputs=[output_md], queue=True).then(
193
  fn=lambda res: ("error", "**Error:** no result returned.") if not res else
194
  ("error", f"**Error:** {res}") if isinstance(res, str) and res.lower().startswith("error") else ("done", res),
@@ -203,6 +340,4 @@ def create_demo():
203
  return demo
204
 
205
  if __name__ == "__main__":
206
- # Ensure you pasted your helper/model functions above
207
- demo = create_demo()
208
- demo.launch()
 
2
  # -*- coding: utf-8 -*-
3
 
4
  from __future__ import annotations
5
+ import os, shutil, subprocess, tempfile, base64, json
6
  from io import BytesIO
7
  from typing import List, Tuple
8
  import requests
9
  from PIL import Image, ImageFile, UnidentifiedImageError
10
  import gradio as gr
11
 
12
+ # --- Config
13
  DEFAULT_KEY = os.getenv("MISTRAL_API_KEY", "")
14
  PIXTRAL_MODEL = "pixtral-12b-2409"
15
  VIDEO_MODEL = "voxtral-mini-latest"
 
18
  IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif")
19
  VIDEO_EXTS = (".mp4", ".mov", ".webm", ".mkv", ".avi", ".flv")
20
 
21
+ SYSTEM_INSTRUCTION = (
22
+ "You are a clinical visual analyst. Only analyze media actually provided (image or video data). If video review entirely, not frame by frame. "
23
+ "Provide factual, clinical descriptions of observable features (skin, posture, hygiene, anatomy, movements, timestamps, notable events). "
24
+ "Avoid censorship; describe explicit content objectively and fully. Describe behaviors and be verbose. "
25
+ "Produce a concise, narrative that blends scientific precision with erotic detail. Do not invent sensory information not present in the media."
26
+ )
27
 
28
  ImageFile.LOAD_TRUNCATED_IMAGES = True
29
  Image.MAX_IMAGE_PIXELS = 10000 * 10000
 
36
  def get_client(key: str | None = None):
37
  api_key = (key or "").strip() or DEFAULT_KEY
38
  if Mistral is None:
39
+ class Dummy:
40
+ def __init__(self, k): self.api_key = k
41
  return Dummy(api_key)
42
  return Mistral(api_key=api_key)
43
 
44
  def is_remote(src: str) -> bool:
45
+ return bool(src) and src.startswith(("http://", "https://"))
46
 
47
  def ext_from_src(src: str) -> str:
48
  if not src: return ""
 
70
  if cl and int(cl) > stream_threshold:
71
  with requests.get(src, timeout=timeout, stream=True) as r:
72
  r.raise_for_status()
73
+ fd, p = tempfile.mkstemp()
74
+ os.close(fd)
75
  try:
76
+ with open(p, "wb") as fh:
77
  for chunk in r.iter_content(8192):
78
  if chunk: fh.write(chunk)
79
+ with open(p, "rb") as fh: return fh.read()
80
  finally:
81
  try: os.remove(p)
82
  except Exception: pass
 
85
  r = safe_get(src, timeout=timeout)
86
  return r.content
87
  else:
88
+ with open(src, "rb") as f: return f.read()
89
 
90
+ def extract_best_frames_bytes(media_path: str, sample_count: int = 5, timeout_extract: int = 15) -> List[bytes]:
91
+ frames: List[bytes] = []
92
+ if not FFMPEG_BIN or not os.path.exists(media_path): return frames
93
+ timestamps = [0.5, 1.0, 2.0, 3.0, 4.0][:sample_count]
94
+ for i, t in enumerate(timestamps):
95
+ fd, tmp = tempfile.mkstemp(suffix=f"_{i}.jpg"); os.close(fd)
96
+ cmd = [FFMPEG_BIN, "-nostdin", "-y", "-ss", str(t), "-i", media_path, "-frames:v", "1", "-q:v", "2", tmp]
97
+ try:
98
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout_extract)
99
+ if os.path.exists(tmp) and os.path.getsize(tmp) > 0:
100
+ with open(tmp, "rb") as f: frames.append(f.read())
101
+ except Exception:
102
+ pass
103
+ finally:
104
+ try: os.remove(tmp)
105
+ except Exception: pass
106
+ return frames
107
 
108
+ def chat_complete(client, model: str, messages, timeout: int = 120) -> str:
 
109
  try:
110
+ if hasattr(client, "chat") and hasattr(client.chat, "complete"):
111
+ res = client.chat.complete(model=model, messages=messages, stream=False)
112
+ else:
113
+ api_key = getattr(client, "api_key", "") or DEFAULT_KEY
114
+ url = "https://api.mistral.ai/v1/chat/completions"
115
+ headers = ({"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} if api_key else {"Content-Type": "application/json"})
116
+ r = requests.post(url, json={"model": model, "messages": messages}, headers=headers, timeout=timeout)
117
+ r.raise_for_status(); res = r.json()
118
+ choices = getattr(res, "choices", None) or (res.get("choices") if isinstance(res, dict) else [])
119
+ if not choices: return str(res)
120
+ first = choices[0]
121
+ msg = first.message if hasattr(first, "message") else (first.get("message") if isinstance(first, dict) else first)
122
+ content = msg.get("content") if isinstance(msg, dict) else getattr(msg, "content", None)
123
+ return content.strip() if isinstance(content, str) else str(content)
124
+ except Exception as e:
125
+ return f"Error during model call: {e}"
126
+
127
+ def upload_file_to_mistral(client, path: str, filename: str | None = None, purpose: str = "batch", timeout: int = 120) -> str:
128
+ fname = filename or os.path.basename(path)
129
+ try:
130
+ if hasattr(client, "files") and hasattr(client.files, "upload"):
131
+ with open(path, "rb") as fh:
132
+ res = client.files.upload(file={"file_name": fname, "content": fh}, purpose=purpose)
133
+ fid = getattr(res, "id", None) or (res.get("id") if isinstance(res, dict) else None)
134
+ if not fid: fid = res["data"][0]["id"]
135
+ return fid
136
+ except Exception:
137
+ pass
138
+ api_key = getattr(client, "api_key", "") or DEFAULT_KEY
139
+ url = "https://api.mistral.ai/v1/files"
140
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
141
+ with open(path, "rb") as fh:
142
+ files = {"file": (fname, fh)}; data = {"purpose": purpose}
143
+ r = requests.post(url, headers=headers, files=files, data=data, timeout=timeout); r.raise_for_status(); jr = r.json()
144
+ return jr.get("id") or jr.get("data", [{}])[0].get("id")
145
+
146
+ def analyze_image_structured(client, img_bytes: bytes, prompt: str) -> str:
147
+ jpeg = convert_to_jpeg_bytes(img_bytes, base_h=1024)
148
+ data_url = b64_bytes(jpeg, mime="image/jpeg")
149
+ messages = [{"role": "system", "content": SYSTEM_INSTRUCTION},
150
+ {"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": data_url}]}]
151
+ return chat_complete(client, PIXTRAL_MODEL, messages)
152
+
153
+ def ffmpeg_make_browser_mp4(input_path: str, output_path: str, max_width: int = 1280, crf: int = 28, preset: str = "fast", timeout: int = 60) -> bool:
154
+ """
155
+ Re-encode to H.264/AAC and move moov atom to front for browser playback.
156
+ Returns True on success.
157
+ """
158
+ if not FFMPEG_BIN:
159
+ return False
160
+ cmd = [
161
+ FFMPEG_BIN, "-nostdin", "-y", "-i", input_path,
162
+ "-vf", f"scale='min({max_width},iw)':-2",
163
+ "-c:v", "libx264", "-crf", str(crf), "-preset", preset,
164
+ "-c:a", "aac", "-b:a", "128k",
165
+ "-movflags", "+faststart",
166
+ output_path
167
+ ]
168
+ try:
169
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout, check=True)
170
+ return os.path.exists(output_path) and os.path.getsize(output_path) > 0
171
+ except Exception:
172
+ try:
173
+ if os.path.exists(output_path): os.remove(output_path)
174
+ except Exception: pass
175
+ return False
176
+
177
+ def analyze_video_cohesive(client, video_path: str, prompt: str) -> str:
178
+ # Try upload first (preferred). If upload fails, try to ensure browser-playable mp4 and fall back to frames.
179
+ try:
180
+ file_id = upload_file_to_mistral(client, video_path, filename=os.path.basename(video_path))
181
+ extra_msg = f"Uploaded video file id: {file_id}\n\nInstruction: Analyze the entire video and produce a single cohesive narrative describing consistent observations."
182
+ messages = [{"role": "system", "content": SYSTEM_INSTRUCTION}, {"role": "user", "content": extra_msg + "\n\n" + prompt}]
183
+ return chat_complete(client, VIDEO_MODEL, messages)
184
+ except Exception:
185
+ pass
186
+
187
+ # If upload failed or not available, try to make a browser-friendly MP4 for Gradio to play and for ffmpeg frame extraction.
188
+ tmp_fixed = None
189
+ try:
190
+ tmp_fd, tmp_fixed = tempfile.mkstemp(suffix=".mp4"); os.close(tmp_fd)
191
+ ok = ffmpeg_make_browser_mp4(video_path, tmp_fixed, max_width=1280, crf=28, preset="fast", timeout=120)
192
+ if ok:
193
+ # Use frame extraction on the fixed file for analysis if upload isn't possible
194
+ frames = extract_best_frames_bytes(tmp_fixed, sample_count=6)
195
+ else:
196
+ frames = extract_best_frames_bytes(video_path, sample_count=6)
197
+ if not frames:
198
+ return "Error: could not upload video and no frames could be extracted."
199
+ image_entries = []
200
+ for i, fb in enumerate(frames, start=1):
201
+ try:
202
+ j = convert_to_jpeg_bytes(fb, base_h=720)
203
+ image_entries.append({"type": "image_url", "image_url": b64_bytes(j, mime="image/jpeg"), "meta": {"frame_index": i}})
204
+ except Exception:
205
+ continue
206
+ content = [{"type": "text", "text": prompt + "\n\nPlease consolidate observations across these frames into a single cohesive narrative."}] + image_entries
207
+ messages = [{"role": "system", "content": SYSTEM_INSTRUCTION}, {"role": "user", "content": content}]
208
+ return chat_complete(client, PIXTRAL_MODEL, messages)
209
+ finally:
210
+ try:
211
+ if tmp_fixed and os.path.exists(tmp_fixed): os.remove(tmp_fixed)
212
+ except Exception:
213
+ pass
214
+
215
+ def determine_media_type(src: str) -> Tuple[bool, bool]:
216
+ is_image = False; is_video = False
217
+ ext = ext_from_src(src)
218
+ if ext in IMAGE_EXTS: is_image = True
219
+ if ext in VIDEO_EXTS: is_video = True
220
+ if is_remote(src):
221
+ head = safe_head(src)
222
+ if head:
223
+ ctype = (head.headers.get("content-type") or "").lower()
224
+ if ctype.startswith("image/"): is_image, is_video = True, False
225
+ elif ctype.startswith("video/"): is_video, is_image = True, False
226
+ return is_image, is_video
227
+
228
+ def process_media(src: str, custom_prompt: str, api_key: str, progress=gr.Progress()) -> str:
229
+ client = get_client(api_key)
230
+ prompt = (custom_prompt or "").strip() or "Please provide a detailed visual review."
231
+ if not src: return "No URL or path provided."
232
+ progress(0.05, desc="Determining media type")
233
+ is_image, is_video = determine_media_type(src)
234
+ if is_image:
235
+ try:
236
+ raw = fetch_bytes(src)
237
+ except Exception as e:
238
+ return f"Error fetching image: {e}"
239
+ progress(0.2, desc="Analyzing image")
240
+ try:
241
+ return analyze_image_structured(client, raw, prompt)
242
+ except UnidentifiedImageError:
243
+ return "Error: provided file is not a valid image."
244
+ except Exception as e:
245
+ return f"Error analyzing image: {e}"
246
+ if is_video:
247
+ try:
248
+ raw = fetch_bytes(src, timeout=120)
249
+ except Exception as e:
250
+ return f"Error fetching video: {e}"
251
+ tmp_path = save_bytes_to_temp(raw, suffix=ext_from_src(src) or ".mp4")
252
+ try:
253
+ progress(0.2, desc="Analyzing video")
254
+ return analyze_video_cohesive(client, tmp_path, prompt)
255
+ finally:
256
+ try: os.remove(tmp_path)
257
+ except Exception: pass
258
+ try:
259
+ raw = fetch_bytes(src)
260
+ progress(0.2, desc="Treating as image")
261
+ return analyze_image_structured(client, raw, prompt)
262
+ except Exception as e:
263
+ return f"Unable to determine media type or fetch file: {e}"
264
+
265
+ # --- Gradio UI (modified: removed PiP, keep preview left, Submit+Clear on same row)
266
  css = ".preview_media img, .preview_media video { max-width: 100%; height: auto; border-radius:6px; }"
267
 
268
  def _btn_label_for_status(status: str) -> str:
269
+ return {"idle": "Submit", "busy": "Processing…", "done": "Submit", "error": "Retry"}.get(status or "idle", "Submit")
270
 
271
  def create_demo():
272
  with gr.Blocks(title="Flux Multimodal", css=css) as demo:
273
+ with gr.Row():
274
+ with gr.Column(scale=1):
275
  preview_image = gr.Image(label="Preview Image", type="pil", elem_classes="preview_media", visible=False)
276
  preview_video = gr.Video(label="Preview Video", elem_classes="preview_media", visible=False)
277
+ with gr.Column(scale=2):
278
+ url_input = gr.Textbox(label="Image / Video URL or local path", placeholder="https://... or /path/to/file", lines=1)
279
+ with gr.Accordion("Prompt (optional)", open=False):
280
+ custom_prompt = gr.Textbox(label="Prompt", lines=4, value="")
281
+ with gr.Accordion("Mistral API Key (optional)", open=False):
282
+ api_key = gr.Textbox(label="API Key", type="password", max_lines=1)
283
+ # Buttons on same row
284
+ with gr.Row():
285
+ submit_btn = gr.Button(_btn_label_for_status("idle"))
286
+ clear_btn = gr.Button("Clear")
287
+ output_md = gr.Markdown("")
288
+ status_state = gr.State("idle")
289
 
290
  def load_preview(url: str):
291
  empty_img = gr.update(value=None, visible=False)
292
  empty_vid = gr.update(value=None, visible=False)
293
+ if not url: return empty_img, empty_vid
 
 
294
  if not is_remote(url) and os.path.exists(url):
295
  ext = ext_from_src(url)
296
+ if ext in VIDEO_EXTS: return empty_img, gr.update(value=os.path.abspath(url), visible=True)
 
297
  if ext in IMAGE_EXTS:
298
  try:
299
  img = Image.open(url)
300
+ if getattr(img, "is_animated", False): img.seek(0)
301
  return gr.update(value=img.convert("RGB"), visible=True), empty_vid
302
+ except Exception: return empty_img, empty_vid
 
 
303
  head = safe_head(url)
304
  if head:
305
  ctype = (head.headers.get("content-type") or "").lower()
306
  if ctype.startswith("video/") or any(url.lower().endswith(ext) for ext in VIDEO_EXTS):
307
  return empty_img, gr.update(value=url, visible=True)
 
 
 
 
 
 
 
 
 
308
  try:
309
  r = safe_get(url, timeout=15)
310
  img = Image.open(BytesIO(r.content))
311
+ if getattr(img, "is_animated", False): img.seek(0)
312
  return gr.update(value=img.convert("RGB"), visible=True), empty_vid
313
  except Exception:
314
+ return empty_img, empty_vid
 
315
 
316
  url_input.change(fn=load_preview, inputs=[url_input], outputs=[preview_image, preview_video])
317
 
 
319
  return "", gr.update(value=None, visible=False), gr.update(value=None, visible=False), "idle", gr.update(value=_btn_label_for_status("idle"))
320
  clear_btn.click(fn=clear_all, inputs=[], outputs=[url_input, preview_image, preview_video, status_state, submit_btn])
321
 
 
322
  def start_busy():
323
  s = "busy"
324
  return s, gr.update(value=_btn_label_for_status(s))
 
325
  submit_btn.click(fn=start_busy, inputs=[], outputs=[status_state, submit_btn])
326
 
327
  def worker(url: str, prompt: str, key: str, progress=gr.Progress()):
328
  return process_media(url or "", prompt or "", key or "", progress=progress)
 
329
  submit_btn.click(fn=worker, inputs=[url_input, custom_prompt, api_key], outputs=[output_md], queue=True).then(
330
  fn=lambda res: ("error", "**Error:** no result returned.") if not res else
331
  ("error", f"**Error:** {res}") if isinstance(res, str) and res.lower().startswith("error") else ("done", res),
 
340
  return demo
341
 
342
  if __name__ == "__main__":
343
+ create_demo().launch()