prithivMLmods commited on
Commit
fd88998
·
verified ·
1 Parent(s): c9f6ff4

update app

Browse files
Files changed (1) hide show
  1. app.py +1506 -476
app.py CHANGED
@@ -1,13 +1,11 @@
1
  import os
2
- import random
3
- import uuid
4
  import json
5
  import time
6
- import asyncio
7
- from threading import Thread
8
- from pathlib import Path
9
  from io import BytesIO
10
- from typing import Optional, Tuple, Dict, Any, Iterable
11
 
12
  import gradio as gr
13
  import spaces
@@ -15,7 +13,6 @@ import torch
15
  import numpy as np
16
  from PIL import Image
17
  import cv2
18
- import requests
19
  import fitz
20
 
21
  from transformers import (
@@ -24,202 +21,12 @@ from transformers import (
24
  AutoProcessor,
25
  TextIteratorStreamer,
26
  )
27
- from transformers.image_utils import load_image
28
-
29
- from gradio.themes import Soft
30
- from gradio.themes.utils import colors, fonts, sizes
31
-
32
- colors.orange_red = colors.Color(
33
- name="orange_red",
34
- c50="#FFF0E5",
35
- c100="#FFE0CC",
36
- c200="#FFC299",
37
- c300="#FFA366",
38
- c400="#FF8533",
39
- c500="#FF4500",
40
- c600="#E63E00",
41
- c700="#CC3700",
42
- c800="#B33000",
43
- c900="#992900",
44
- c950="#802200",
45
- )
46
-
47
- class OrangeRedTheme(Soft):
48
- def __init__(
49
- self,
50
- *,
51
- primary_hue: colors.Color | str = colors.gray,
52
- secondary_hue: colors.Color | str = colors.orange_red,
53
- neutral_hue: colors.Color | str = colors.slate,
54
- text_size: sizes.Size | str = sizes.text_lg,
55
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
56
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
57
- ),
58
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
59
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
60
- ),
61
- ):
62
- super().__init__(
63
- primary_hue=primary_hue,
64
- secondary_hue=secondary_hue,
65
- neutral_hue=neutral_hue,
66
- text_size=text_size,
67
- font=font,
68
- font_mono=font_mono,
69
- )
70
- super().set(
71
- background_fill_primary="*primary_50",
72
- background_fill_primary_dark="*primary_900",
73
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
74
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
75
- button_primary_text_color="white",
76
- button_primary_text_color_hover="white",
77
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
78
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
79
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
80
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
81
- button_secondary_text_color="black",
82
- button_secondary_text_color_hover="white",
83
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
84
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
85
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
86
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
87
- slider_color="*secondary_500",
88
- slider_color_dark="*secondary_600",
89
- block_title_text_weight="600",
90
- block_border_width="3px",
91
- block_shadow="*shadow_drop_lg",
92
- button_primary_shadow="*shadow_drop_lg",
93
- button_large_padding="11px",
94
- color_accent_soft="*primary_100",
95
- block_label_background_fill="*primary_200",
96
- )
97
-
98
- orange_red_theme = OrangeRedTheme()
99
-
100
- css = """
101
- #main-title h1 {
102
- font-size: 2.3em !important;
103
- }
104
- #output-title h2 {
105
- font-size: 2.2em !important;
106
- }
107
-
108
- /* RadioAnimated Styles */
109
- .ra-wrap{ width: fit-content; }
110
- .ra-inner{
111
- position: relative; display: inline-flex; align-items: center; gap: 0; padding: 6px;
112
- background: var(--neutral-200); border-radius: 9999px; overflow: hidden;
113
- }
114
- .ra-input{ display: none; }
115
- .ra-label{
116
- position: relative; z-index: 2; padding: 8px 16px;
117
- font-family: inherit; font-size: 14px; font-weight: 600;
118
- color: var(--neutral-500); cursor: pointer; transition: color 0.2s; white-space: nowrap;
119
- }
120
- .ra-highlight{
121
- position: absolute; z-index: 1; top: 6px; left: 6px;
122
- height: calc(100% - 12px); border-radius: 9999px;
123
- background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
124
- transition: transform 0.2s, width 0.2s;
125
- }
126
- .ra-input:checked + .ra-label{ color: black; }
127
-
128
- /* Dark mode adjustments for Radio */
129
- .dark .ra-inner { background: var(--neutral-800); }
130
- .dark .ra-label { color: var(--neutral-400); }
131
- .dark .ra-highlight { background: var(--neutral-600); }
132
- .dark .ra-input:checked + .ra-label { color: white; }
133
-
134
- #gpu-duration-container {
135
- padding: 10px;
136
- border-radius: 8px;
137
- background: var(--background-fill-secondary);
138
- border: 1px solid var(--border-color-primary);
139
- margin-top: 10px;
140
- }
141
- """
142
 
143
  MAX_MAX_NEW_TOKENS = 4096
144
  DEFAULT_MAX_NEW_TOKENS = 1024
145
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
146
-
147
  print("Using device:", device)
148
 
149
- class RadioAnimated(gr.HTML):
150
- def __init__(self, choices, value=None, **kwargs):
151
- if not choices or len(choices) < 2:
152
- raise ValueError("RadioAnimated requires at least 2 choices.")
153
- if value is None:
154
- value = choices[0]
155
-
156
- uid = uuid.uuid4().hex[:8]
157
- group_name = f"ra-{uid}"
158
-
159
- inputs_html = "\n".join(
160
- f"""
161
- <input class="ra-input" type="radio" name="{group_name}" id="{group_name}-{i}" value="{c}">
162
- <label class="ra-label" for="{group_name}-{i}">{c}</label>
163
- """
164
- for i, c in enumerate(choices)
165
- )
166
-
167
- html_template = f"""
168
- <div class="ra-wrap" data-ra="{uid}">
169
- <div class="ra-inner">
170
- <div class="ra-highlight"></div>
171
- {inputs_html}
172
- </div>
173
- </div>
174
- """
175
-
176
- js_on_load = r"""
177
- (() => {
178
- const wrap = element.querySelector('.ra-wrap');
179
- const inner = element.querySelector('.ra-inner');
180
- const highlight = element.querySelector('.ra-highlight');
181
- const inputs = Array.from(element.querySelectorAll('.ra-input'));
182
-
183
- if (!inputs.length) return;
184
-
185
- const choices = inputs.map(i => i.value);
186
-
187
- function setHighlightByIndex(idx) {
188
- const n = choices.length;
189
- const pct = 100 / n;
190
- highlight.style.width = `calc(${pct}% - 6px)`;
191
- highlight.style.transform = `translateX(${idx * 100}%)`;
192
- }
193
-
194
- function setCheckedByValue(val, shouldTrigger=false) {
195
- const idx = Math.max(0, choices.indexOf(val));
196
- inputs.forEach((inp, i) => { inp.checked = (i === idx); });
197
- setHighlightByIndex(idx);
198
-
199
- props.value = choices[idx];
200
- if (shouldTrigger) trigger('change', props.value);
201
- }
202
-
203
- setCheckedByValue(props.value ?? choices[0], false);
204
-
205
- inputs.forEach((inp) => {
206
- inp.addEventListener('change', () => {
207
- setCheckedByValue(inp.value, true);
208
- });
209
- });
210
- })();
211
- """
212
-
213
- super().__init__(
214
- value=value,
215
- html_template=html_template,
216
- js_on_load=js_on_load,
217
- **kwargs
218
- )
219
-
220
- def apply_gpu_duration(val: str):
221
- return int(val)
222
-
223
  MODEL_ID_Q4B = "Qwen/Qwen3-VL-4B-Instruct"
224
  processor_q4b = AutoProcessor.from_pretrained(MODEL_ID_Q4B, trust_remote_code=True)
225
  model_q4b = Qwen3VLForConditionalGeneration.from_pretrained(
@@ -265,20 +72,71 @@ model_x3b = Qwen2_5_VLForConditionalGeneration.from_pretrained(
265
  torch_dtype=torch.float16
266
  ).to(device).eval()
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  def select_model(model_name: str):
270
- if model_name == "Qwen3-VL-4B-Instruct":
271
- return processor_q4b, model_q4b
272
- elif model_name == "Qwen3-VL-8B-Instruct":
273
- return processor_q8b, model_q8b
274
- elif model_name == "Qwen3-VL-2B-Instruct":
275
- return processor_q2b, model_q2b
276
- elif model_name == "Qwen2.5-VL-7B-Instruct":
277
- return processor_m7b, model_m7b
278
- elif model_name == "Qwen2.5-VL-3B-Instruct":
279
- return processor_x3b, model_x3b
280
- else:
281
  raise ValueError("Invalid model selected.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  def extract_gif_frames(gif_path: str):
284
  if not gif_path:
@@ -288,26 +146,12 @@ def extract_gif_frames(gif_path: str):
288
  frame_indices = np.linspace(0, total_frames - 1, min(total_frames, 10), dtype=int)
289
  frames = []
290
  for i in frame_indices:
291
- gif.seek(i)
292
  frames.append(gif.convert("RGB").copy())
293
  return frames
294
 
295
- def downsample_video(video_path):
296
- vidcap = cv2.VideoCapture(video_path)
297
- total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
298
- frames = []
299
- frame_indices = np.linspace(0, total_frames - 1, min(total_frames, 10), dtype=int)
300
- for i in frame_indices:
301
- vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
302
- success, image = vidcap.read()
303
- if success:
304
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
305
- pil_image = Image.fromarray(image)
306
- frames.append(pil_image)
307
- vidcap.release()
308
- return frames
309
 
310
- def convert_pdf_to_images(file_path: str, dpi: int = 200):
311
  if not file_path:
312
  return []
313
  images = []
@@ -318,14 +162,135 @@ def convert_pdf_to_images(file_path: str, dpi: int = 200):
318
  page = pdf_document.load_page(page_num)
319
  pix = page.get_pixmap(matrix=mat)
320
  img_data = pix.tobytes("png")
321
- images.append(Image.open(BytesIO(img_data)))
322
  pdf_document.close()
323
  return images
324
 
325
- def get_initial_pdf_state() -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  return {"pages": [], "total_pages": 0, "current_page_index": 0}
327
 
328
- def load_and_preview_pdf(file_path: Optional[str]) -> Tuple[Optional[Image.Image], Dict[str, Any], str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  state = get_initial_pdf_state()
330
  if not file_path:
331
  return None, state, '<div style="text-align:center;">No file loaded</div>'
@@ -340,7 +305,8 @@ def load_and_preview_pdf(file_path: Optional[str]) -> Tuple[Optional[Image.Image
340
  except Exception as e:
341
  return None, state, f'<div style="text-align:center;">Failed to load preview: {e}</div>'
342
 
343
- def navigate_pdf_page(direction: str, state: Dict[str, Any]):
 
344
  if not state or not state["pages"]:
345
  return None, state, '<div style="text-align:center;">No file loaded</div>'
346
  current_index = state["current_page_index"]
@@ -356,154 +322,96 @@ def navigate_pdf_page(direction: str, state: Dict[str, Any]):
356
  page_info_html = f'<div style="text-align:center;">Page {new_index + 1} / {total_pages}</div>'
357
  return image_preview, state, page_info_html
358
 
359
- def calc_timeout_image(model_name: str, text: str, image: Image.Image,
360
- max_new_tokens: int, temperature: float, top_p: float,
361
- top_k: int, repetition_penalty: float, gpu_timeout: int):
362
- try:
363
- return int(gpu_timeout)
364
- except:
365
- return 60
366
 
367
- def calc_timeout_video(model_name: str, text: str, video_path: str,
368
- max_new_tokens: int, temperature: float, top_p: float,
369
- top_k: int, repetition_penalty: float, gpu_timeout: int):
370
  try:
371
- return int(gpu_timeout)
372
- except:
373
  return 60
374
 
375
- def calc_timeout_pdf(model_name: str, text: str, state: Dict[str, Any],
376
- max_new_tokens: int, temperature: float, top_p: float,
377
- top_k: int, repetition_penalty: float, gpu_timeout: int):
378
- try:
379
- return int(gpu_timeout)
380
- except:
381
- return 60
382
 
383
- def calc_timeout_caption(model_name: str, image: Image.Image,
384
- max_new_tokens: int, temperature: float, top_p: float,
385
- top_k: int, repetition_penalty: float, gpu_timeout: int):
386
- try:
387
- return int(gpu_timeout)
388
- except:
389
- return 60
390
-
391
- def calc_timeout_gif(model_name: str, text: str, gif_path: str,
392
- max_new_tokens: int, temperature: float, top_p: float,
393
- top_k: int, repetition_penalty: float, gpu_timeout: int):
394
- try:
395
- return int(gpu_timeout)
396
- except:
397
- return 60
398
-
399
- @spaces.GPU(duration=calc_timeout_image)
400
- def generate_image(model_name: str, text: str, image: Image.Image,
401
- max_new_tokens: int = 1024, temperature: float = 0.6,
402
- top_p: float = 0.9, top_k: int = 50,
403
  repetition_penalty: float = 1.2, gpu_timeout: int = 60):
404
  if image is None:
405
- yield "Please upload an image.", "Please upload an image."
406
- return
407
- try:
408
- processor, model = select_model(model_name)
409
- except ValueError as e:
410
- yield str(e), str(e)
411
- return
412
-
413
  messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}]
414
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
415
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
416
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
417
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens}
 
 
 
 
 
 
 
 
 
418
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
419
  thread.start()
420
  buffer = ""
421
  for new_text in streamer:
422
  buffer += new_text
423
  time.sleep(0.01)
424
- yield buffer, buffer
425
-
426
- @spaces.GPU(duration=calc_timeout_video)
427
- def generate_video(model_name: str, text: str, video_path: str,
428
- max_new_tokens: int = 1024, temperature: float = 0.6,
429
- top_p: float = 0.9, top_k: int = 50,
430
- repetition_penalty: float = 1.2, gpu_timeout: int = 90):
431
- if video_path is None:
432
- yield "Please upload a video.", "Please upload a video."
433
- return
434
- try:
435
- processor, model = select_model(model_name)
436
- except ValueError as e:
437
- yield str(e), str(e)
438
- return
439
 
440
- frames = downsample_video(video_path)
441
- if not frames:
442
- yield "Could not process video.", "Could not process video."
443
- return
444
-
445
- messages = [{"role": "user", "content": [{"type": "text", "text": text}]}]
446
- for frame in frames:
447
- messages[0]["content"].insert(0, {"type": "image"})
448
- prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
449
- inputs = processor(text=[prompt_full], images=frames, return_tensors="pt", padding=True).to(device)
450
- streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
451
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens, "do_sample": True, "temperature": temperature, "top_p": top_p, "top_k": top_k, "repetition_penalty": repetition_penalty}
452
- thread = Thread(target=model.generate, kwargs=generation_kwargs)
453
- thread.start()
454
- buffer = ""
455
- for new_text in streamer:
456
- buffer += new_text
457
- time.sleep(0.01)
458
- yield buffer, buffer
459
 
460
- @spaces.GPU(duration=calc_timeout_pdf)
461
- def generate_pdf(model_name: str, text: str, state: Dict[str, Any],
462
- max_new_tokens: int = 2048, temperature: float = 0.6,
463
- top_p: float = 0.9, top_k: int = 50,
464
  repetition_penalty: float = 1.2, gpu_timeout: int = 120):
465
  if not state or not state["pages"]:
466
- yield "Please upload a PDF file first.", "Please upload a PDF file first."
467
- return
468
- try:
469
- processor, model = select_model(model_name)
470
- except ValueError as e:
471
- yield str(e), str(e)
472
- return
473
-
474
  page_images = state["pages"]
475
  full_response = ""
476
  for i, image in enumerate(page_images):
477
  page_header = f"--- Page {i+1}/{len(page_images)} ---\n"
478
- yield full_response + page_header, full_response + page_header
479
  messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}]
480
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
481
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
482
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
483
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens}
 
 
 
 
 
 
 
 
 
484
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
485
  thread.start()
486
  page_buffer = ""
487
  for new_text in streamer:
488
  page_buffer += new_text
489
- yield full_response + page_header + page_buffer, full_response + page_header + page_buffer
490
  time.sleep(0.01)
491
  full_response += page_header + page_buffer + "\n\n"
 
 
 
 
492
 
493
- @spaces.GPU(duration=calc_timeout_caption)
494
- def generate_caption(model_name: str, image: Image.Image,
495
- max_new_tokens: int = 1024, temperature: float = 0.6,
496
- top_p: float = 0.9, top_k: int = 50,
497
  repetition_penalty: float = 1.2, gpu_timeout: int = 60):
498
  if image is None:
499
- yield "Please upload an image to caption.", "Please upload an image to caption."
500
- return
501
- try:
502
- processor, model = select_model(model_name)
503
- except ValueError as e:
504
- yield str(e), str(e)
505
- return
506
-
507
  system_prompt = (
508
  "You are an AI assistant. For the given image, write a precise caption and provide a structured set of "
509
  "attributes describing visual elements like objects, people, actions, colors, and environment."
@@ -512,170 +420,1292 @@ def generate_caption(model_name: str, image: Image.Image,
512
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
513
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
514
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
515
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens}
 
 
 
 
 
 
 
 
 
516
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
517
  thread.start()
518
  buffer = ""
519
  for new_text in streamer:
520
  buffer += new_text
521
  time.sleep(0.01)
522
- yield buffer, buffer
 
 
 
523
 
524
- @spaces.GPU(duration=calc_timeout_gif)
525
- def generate_gif(model_name: str, text: str, gif_path: str,
526
- max_new_tokens: int = 1024, temperature: float = 0.6,
527
- top_p: float = 0.9, top_k: int = 50,
 
528
  repetition_penalty: float = 1.2, gpu_timeout: int = 90):
529
  if gif_path is None:
530
- yield "Please upload a GIF.", "Please upload a GIF."
531
- return
532
- try:
533
- processor, model = select_model(model_name)
534
- except ValueError as e:
535
- yield str(e), str(e)
536
- return
537
-
538
  frames = extract_gif_frames(gif_path)
539
  if not frames:
540
- yield "Could not process GIF.", "Could not process GIF."
541
- return
542
  messages = [{"role": "user", "content": [{"type": "text", "text": text}]}]
543
- for frame in frames:
544
  messages[0]["content"].insert(0, {"type": "image"})
545
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
546
  inputs = processor(text=[prompt_full], images=frames, return_tensors="pt", padding=True).to(device)
547
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
548
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens, "do_sample": True, "temperature": temperature, "top_p": top_p, "top_k": top_k, "repetition_penalty": repetition_penalty}
 
 
 
 
 
 
 
 
 
549
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
550
  thread.start()
551
  buffer = ""
552
  for new_text in streamer:
553
  buffer += new_text
554
  time.sleep(0.01)
555
- yield buffer, buffer
556
-
557
- image_examples = [["Perform OCR on the image...", "examples/images/1.jpg"],
558
- ["Caption the image. Describe the safety measures shown in the image. Conclude whether the situation is (safe or unsafe)...", "examples/images/2.jpg"],
559
- ["Solve the problem...", "examples/images/3.png"]]
560
- video_examples = [["Explain the Ad video in detail.", "examples/videos/1.mp4"],
561
- ["Explain the video in detail.", "examples/videos/2.mp4"]]
562
- pdf_examples = [["Extract the content precisely.", "examples/pdfs/doc1.pdf"],
563
- ["Analyze and provide a short report.", "examples/pdfs/doc2.pdf"]]
564
- gif_examples = [["Describe this GIF.", "examples/gifs/1.gif"],
565
- ["Describe this GIF.", "examples/gifs/2.gif"]]
566
- caption_examples = [["examples/captions/1.JPG"],
567
- ["examples/captions/2.jpeg"], ["examples/captions/3.jpeg"]]
568
 
569
- with gr.Blocks() as demo:
570
- pdf_state = gr.State(value=get_initial_pdf_state())
571
- gr.Markdown("# **Qwen3-VL-Outpost**", elem_id="main-title")
572
- with gr.Row():
573
- with gr.Column(scale=2):
574
- with gr.Tabs():
575
- with gr.TabItem("Image Inference"):
576
- image_query = gr.Textbox(label="Query Input", placeholder="Enter your query here...")
577
- image_upload = gr.Image(type="pil", label="Upload Image", height=290)
578
- image_submit = gr.Button("Submit", variant="primary")
579
- gr.Examples(examples=image_examples, inputs=[image_query, image_upload])
580
-
581
- with gr.TabItem("PDF Inference"):
582
- with gr.Row():
583
- with gr.Column(scale=1):
584
- pdf_query = gr.Textbox(label="Query Input", placeholder="e.g., 'Summarize this document'")
585
- pdf_upload = gr.File(label="Upload PDF", file_types=[".pdf"])
586
- pdf_submit = gr.Button("Submit", variant="primary")
587
- with gr.Column(scale=1):
588
- pdf_preview_img = gr.Image(label="PDF Preview", height=290)
589
- with gr.Row():
590
- prev_page_btn = gr.Button("◀ Previous")
591
- page_info = gr.HTML('<div style="text-align:center;">No file loaded</div>')
592
- next_page_btn = gr.Button("Next ▶")
593
- gr.Examples(examples=pdf_examples, inputs=[pdf_query, pdf_upload])
594
-
595
- with gr.TabItem("Long Caption"):
596
- caption_image_upload = gr.Image(type="pil", label="Image to Caption", height=290)
597
- caption_submit = gr.Button("Generate Caption", variant="primary")
598
- gr.Examples(examples=caption_examples, inputs=[caption_image_upload])
599
-
600
- with gr.TabItem("Video Inference"):
601
- video_query = gr.Textbox(label="Query Input", placeholder="Enter your query here...")
602
- video_upload = gr.Video(label="Upload Video(≤30s)", height=290)
603
- video_submit = gr.Button("Submit", variant="primary")
604
- gr.Examples(examples=video_examples, inputs=[video_query, video_upload])
605
-
606
- with gr.TabItem("Gif Inference"):
607
- gif_query = gr.Textbox(label="Query Input", placeholder="e.g., 'What is happening in this gif?'")
608
- gif_upload = gr.Image(type="filepath", label="Upload GIF", height=290)
609
- gif_submit = gr.Button("Submit", variant="primary")
610
- gr.Examples(examples=gif_examples, inputs=[gif_query, gif_upload])
611
-
612
- with gr.Accordion("Advanced options", open=False):
613
- max_new_tokens = gr.Slider(label="Max new tokens", minimum=1, maximum=MAX_MAX_NEW_TOKENS, step=1, value=DEFAULT_MAX_NEW_TOKENS)
614
- temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=4.0, step=0.1, value=0.6)
615
- top_p = gr.Slider(label="Top-p (nucleus sampling)", minimum=0.05, maximum=1.0, step=0.05, value=0.9)
616
- top_k = gr.Slider(label="Top-k", minimum=1, maximum=1000, step=1, value=50)
617
- repetition_penalty = gr.Slider(label="Repetition penalty", minimum=1.0, maximum=2.0, step=0.05, value=1.2)
618
-
619
- with gr.Column(scale=3):
620
- gr.Markdown("## Output", elem_id="output-title")
621
- output = gr.Textbox(label="Raw Output Stream", interactive=True, lines=12)
622
- with gr.Accordion("(Result.md)", open=False):
623
- markdown_output = gr.Markdown(label="(Result.Md)")
624
-
625
- model_choice = gr.Radio(
626
- choices=[
627
- "Qwen3-VL-4B-Instruct",
628
- "Qwen3-VL-8B-Instruct",
629
- "Qwen3-VL-2B-Instruct",
630
- "Qwen2.5-VL-7B-Instruct",
631
- "Qwen2.5-VL-3B-Instruct"
632
- ],
633
- label="Select Model",
634
- value="Qwen3-VL-4B-Instruct"
635
  )
636
-
637
- with gr.Row(elem_id="gpu-duration-container"):
638
- with gr.Column():
639
- gr.Markdown("**GPU Duration (seconds)**")
640
- radioanimated_gpu_duration = RadioAnimated(
641
- choices=["60", "90", "120", "180", "240", "300"],
642
- value="60",
643
- elem_id="radioanimated_gpu_duration"
644
- )
645
- gpu_duration_state = gr.Number(value=60, visible=False)
646
-
647
- gr.Markdown("*Note: Higher GPU duration allows for longer processing but consumes more GPU quota.*")
648
-
649
- radioanimated_gpu_duration.change(
650
- fn=apply_gpu_duration,
651
- inputs=radioanimated_gpu_duration,
652
- outputs=[gpu_duration_state],
653
- api_visibility="private"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  )
655
 
656
- image_submit.click(fn=generate_image,
657
- inputs=[model_choice, image_query, image_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
658
- outputs=[output, markdown_output])
659
-
660
- video_submit.click(fn=generate_video,
661
- inputs=[model_choice, video_query, video_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
662
- outputs=[output, markdown_output])
663
-
664
- pdf_submit.click(fn=generate_pdf,
665
- inputs=[model_choice, pdf_query, pdf_state, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
666
- outputs=[output, markdown_output])
667
-
668
- gif_submit.click(fn=generate_gif,
669
- inputs=[model_choice, gif_query, gif_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
670
- outputs=[output, markdown_output])
671
-
672
- caption_submit.click(fn=generate_caption,
673
- inputs=[model_choice, caption_image_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
674
- outputs=[output, markdown_output])
675
-
676
- pdf_upload.change(fn=load_and_preview_pdf, inputs=[pdf_upload], outputs=[pdf_preview_img, pdf_state, page_info])
677
- prev_page_btn.click(fn=lambda s: navigate_pdf_page("prev", s), inputs=[pdf_state], outputs=[pdf_preview_img, pdf_state, page_info])
678
- next_page_btn.click(fn=lambda s: navigate_pdf_page("next", s), inputs=[pdf_state], outputs=[pdf_preview_img, pdf_state, page_info])
679
 
680
  if __name__ == "__main__":
681
- demo.queue(max_size=50).launch(theme=orange_red_theme, css=css, mcp_server=True, ssr_mode=False, show_error=True)
 
 
 
 
 
 
 
1
  import os
2
+ import gc
 
3
  import json
4
  import time
5
+ import base64
6
+ import uuid
 
7
  from io import BytesIO
8
+ from threading import Thread
9
 
10
  import gradio as gr
11
  import spaces
 
13
  import numpy as np
14
  from PIL import Image
15
  import cv2
 
16
  import fitz
17
 
18
  from transformers import (
 
21
  AutoProcessor,
22
  TextIteratorStreamer,
23
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  MAX_MAX_NEW_TOKENS = 4096
26
  DEFAULT_MAX_NEW_TOKENS = 1024
27
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
28
  print("Using device:", device)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  MODEL_ID_Q4B = "Qwen/Qwen3-VL-4B-Instruct"
31
  processor_q4b = AutoProcessor.from_pretrained(MODEL_ID_Q4B, trust_remote_code=True)
32
  model_q4b = Qwen3VLForConditionalGeneration.from_pretrained(
 
72
  torch_dtype=torch.float16
73
  ).to(device).eval()
74
 
75
+ MODEL_MAP = {
76
+ "Qwen3-VL-4B-Instruct": (processor_q4b, model_q4b),
77
+ "Qwen3-VL-8B-Instruct": (processor_q8b, model_q8b),
78
+ "Qwen3-VL-2B-Instruct": (processor_q2b, model_q2b),
79
+ "Qwen2.5-VL-7B-Instruct": (processor_m7b, model_m7b),
80
+ "Qwen2.5-VL-3B-Instruct": (processor_x3b, model_x3b),
81
+ }
82
+
83
+ MODEL_CHOICES = list(MODEL_MAP.keys())
84
+
85
+ image_examples = [
86
+ {"query": "Perform OCR on the image...", "media": "examples/images/1.jpg", "model": "Qwen3-VL-4B-Instruct", "kind": "image"},
87
+ {"query": "Caption the image. Describe the safety measures shown in the image. Conclude whether the situation is (safe or unsafe)...", "media": "examples/images/2.jpg", "model": "Qwen3-VL-8B-Instruct", "kind": "image"},
88
+ {"query": "Solve the problem...", "media": "examples/images/3.png", "model": "Qwen3-VL-2B-Instruct", "kind": "image"},
89
+ ]
90
+
91
+ pdf_examples = [
92
+ {"query": "Extract the content precisely.", "media": "examples/pdfs/doc1.pdf", "model": "Qwen2.5-VL-7B-Instruct", "kind": "pdf"},
93
+ {"query": "Analyze and provide a short report.", "media": "examples/pdfs/doc2.pdf", "model": "Qwen2.5-VL-3B-Instruct", "kind": "pdf"},
94
+ ]
95
+
96
+ gif_examples = [
97
+ {"query": "Describe this GIF.", "media": "examples/gifs/1.gif", "model": "Qwen3-VL-4B-Instruct", "kind": "gif"},
98
+ {"query": "Describe this GIF.", "media": "examples/gifs/2.gif", "model": "Qwen3-VL-8B-Instruct", "kind": "gif"},
99
+ ]
100
+
101
+ caption_examples = [
102
+ {"query": "Generate a detailed caption and structured visual attributes.", "media": "examples/captions/1.JPG", "model": "Qwen3-VL-4B-Instruct", "kind": "caption"},
103
+ {"query": "Generate a detailed caption and structured visual attributes.", "media": "examples/captions/2.jpeg", "model": "Qwen2.5-VL-7B-Instruct", "kind": "caption"},
104
+ {"query": "Generate a detailed caption and structured visual attributes.", "media": "examples/captions/3.jpeg", "model": "Qwen2.5-VL-3B-Instruct", "kind": "caption"},
105
+ ]
106
+
107
+ all_examples = image_examples + pdf_examples + gif_examples + caption_examples
108
+
109
 
110
  def select_model(model_name: str):
111
+ if model_name not in MODEL_MAP:
 
 
 
 
 
 
 
 
 
 
112
  raise ValueError("Invalid model selected.")
113
+ return MODEL_MAP[model_name]
114
+
115
+
116
+ def pil_to_data_url(img: Image.Image, fmt="PNG"):
117
+ buf = BytesIO()
118
+ img.save(buf, format=fmt)
119
+ data = base64.b64encode(buf.getvalue()).decode()
120
+ mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg"
121
+ return f"data:{mime};base64,{data}"
122
+
123
+
124
+ def file_to_data_url(path):
125
+ if not os.path.exists(path):
126
+ return ""
127
+ ext = path.rsplit(".", 1)[-1].lower()
128
+ mime = {
129
+ "jpg": "image/jpeg",
130
+ "jpeg": "image/jpeg",
131
+ "png": "image/png",
132
+ "webp": "image/webp",
133
+ "gif": "image/gif",
134
+ "pdf": "application/pdf",
135
+ }.get(ext, "application/octet-stream")
136
+ with open(path, "rb") as f:
137
+ data = base64.b64encode(f.read()).decode()
138
+ return f"data:{mime};base64,{data}"
139
+
140
 
141
  def extract_gif_frames(gif_path: str):
142
  if not gif_path:
 
146
  frame_indices = np.linspace(0, total_frames - 1, min(total_frames, 10), dtype=int)
147
  frames = []
148
  for i in frame_indices:
149
+ gif.seek(int(i))
150
  frames.append(gif.convert("RGB").copy())
151
  return frames
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ def convert_pdf_to_images(file_path: str, dpi: int = 160):
155
  if not file_path:
156
  return []
157
  images = []
 
162
  page = pdf_document.load_page(page_num)
163
  pix = page.get_pixmap(matrix=mat)
164
  img_data = pix.tobytes("png")
165
+ images.append(Image.open(BytesIO(img_data)).convert("RGB"))
166
  pdf_document.close()
167
  return images
168
 
169
+
170
+ def make_thumb_b64(path, kind="image", max_dim=240):
171
+ try:
172
+ if kind == "pdf":
173
+ pages = convert_pdf_to_images(path, dpi=120)
174
+ if not pages:
175
+ return ""
176
+ img = pages[0].convert("RGB")
177
+ elif kind == "gif":
178
+ frames = extract_gif_frames(path)
179
+ if not frames:
180
+ return ""
181
+ img = frames[0].convert("RGB")
182
+ else:
183
+ img = Image.open(path).convert("RGB")
184
+ img.thumbnail((max_dim, max_dim))
185
+ return pil_to_data_url(img, "JPEG")
186
+ except Exception as e:
187
+ print("Thumbnail error:", e)
188
+ return ""
189
+
190
+
191
+ def build_example_cards_html():
192
+ cards = ""
193
+ for i, ex in enumerate(all_examples):
194
+ thumb = make_thumb_b64(ex["media"], ex["kind"])
195
+ media_badge = ex["kind"].upper()
196
+ prompt_short = ex["query"][:72] + ("..." if len(ex["query"]) > 72 else "")
197
+ cards += f"""
198
+ <div class="example-card" data-idx="{i}">
199
+ <div class="example-thumb-wrap">
200
+ {"<img src='" + thumb + "' alt=''>" if thumb else "<div class='example-thumb-placeholder'>Preview</div>"}
201
+ <div class="example-media-chip">{media_badge}</div>
202
+ </div>
203
+ <div class="example-meta-row">
204
+ <span class="example-badge">{ex["model"]}</span>
205
+ </div>
206
+ <div class="example-prompt-text">{prompt_short}</div>
207
+ </div>
208
+ """
209
+ return cards
210
+
211
+
212
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
213
+
214
+
215
+ def get_initial_pdf_state():
216
  return {"pages": [], "total_pages": 0, "current_page_index": 0}
217
 
218
+
219
+ def load_example_data(idx_str):
220
+ try:
221
+ idx = int(float(idx_str))
222
+ except Exception:
223
+ return json.dumps({"status": "error", "message": "Invalid example index"})
224
+ if idx < 0 or idx >= len(all_examples):
225
+ return json.dumps({"status": "error", "message": "Example index out of range"})
226
+ ex = all_examples[idx]
227
+
228
+ payload = {
229
+ "status": "ok",
230
+ "query": ex["query"],
231
+ "model": ex["model"],
232
+ "kind": ex["kind"],
233
+ "name": os.path.basename(ex["media"]),
234
+ }
235
+
236
+ if ex["kind"] == "pdf":
237
+ file_b64 = file_to_data_url(ex["media"])
238
+ pages = convert_pdf_to_images(ex["media"])
239
+ preview_b64 = pil_to_data_url(pages[0], "JPEG") if pages else ""
240
+ payload["file"] = file_b64
241
+ payload["preview"] = preview_b64
242
+ payload["page_info"] = f"Page 1 / {len(pages)}" if pages else "No file loaded"
243
+ else:
244
+ media_b64 = file_to_data_url(ex["media"])
245
+ if not media_b64:
246
+ return json.dumps({"status": "error", "message": f"Could not load example {ex['kind']}"})
247
+ payload["media"] = media_b64
248
+
249
+ return json.dumps(payload)
250
+
251
+
252
+ def b64_to_pil(b64_str):
253
+ if not b64_str:
254
+ return None
255
+ try:
256
+ if b64_str.startswith("data:"):
257
+ _, data = b64_str.split(",", 1)
258
+ else:
259
+ data = b64_str
260
+ image_data = base64.b64decode(data)
261
+ return Image.open(BytesIO(image_data)).convert("RGB")
262
+ except Exception:
263
+ return None
264
+
265
+
266
+ def b64_to_temp_file(b64_str, base_dir="/tmp/qwen3_vl_outpost_media"):
267
+ if not b64_str:
268
+ return None
269
+ try:
270
+ os.makedirs(base_dir, exist_ok=True)
271
+ if b64_str.startswith("data:"):
272
+ header, data = b64_str.split(",", 1)
273
+ mime = header.split(";")[0].replace("data:", "")
274
+ else:
275
+ data = b64_str
276
+ mime = "application/octet-stream"
277
+ ext = {
278
+ "application/pdf": ".pdf",
279
+ "image/gif": ".gif",
280
+ "image/png": ".png",
281
+ "image/jpeg": ".jpg",
282
+ "image/webp": ".webp",
283
+ }.get(mime, ".bin")
284
+ raw = base64.b64decode(data)
285
+ path = os.path.join(base_dir, f"{uuid.uuid4().hex}{ext}")
286
+ with open(path, "wb") as f:
287
+ f.write(raw)
288
+ return path
289
+ except Exception:
290
+ return None
291
+
292
+
293
+ def load_and_preview_pdf(file_path):
294
  state = get_initial_pdf_state()
295
  if not file_path:
296
  return None, state, '<div style="text-align:center;">No file loaded</div>'
 
305
  except Exception as e:
306
  return None, state, f'<div style="text-align:center;">Failed to load preview: {e}</div>'
307
 
308
+
309
+ def navigate_pdf_page(direction: str, state):
310
  if not state or not state["pages"]:
311
  return None, state, '<div style="text-align:center;">No file loaded</div>'
312
  current_index = state["current_page_index"]
 
322
  page_info_html = f'<div style="text-align:center;">Page {new_index + 1} / {total_pages}</div>'
323
  return image_preview, state, page_info_html
324
 
 
 
 
 
 
 
 
325
 
326
+ def calc_timeout_generic(*args):
 
 
327
  try:
328
+ return int(args[-1])
329
+ except Exception:
330
  return 60
331
 
 
 
 
 
 
 
 
332
 
333
+ @spaces.GPU(duration=calc_timeout_generic)
334
+ def generate_image(model_name: str, text: str, image: Image.Image,
335
+ max_new_tokens: int = 1024, temperature: float = 0.6,
336
+ top_p: float = 0.9, top_k: int = 50,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  repetition_penalty: float = 1.2, gpu_timeout: int = 60):
338
  if image is None:
339
+ raise gr.Error("Please upload an image.")
340
+ processor, model = select_model(model_name)
 
 
 
 
 
 
341
  messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}]
342
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
343
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
344
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
345
+ generation_kwargs = {
346
+ **inputs,
347
+ "streamer": streamer,
348
+ "max_new_tokens": int(max_new_tokens),
349
+ "do_sample": True,
350
+ "temperature": float(temperature),
351
+ "top_p": float(top_p),
352
+ "top_k": int(top_k),
353
+ "repetition_penalty": float(repetition_penalty),
354
+ }
355
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
356
  thread.start()
357
  buffer = ""
358
  for new_text in streamer:
359
  buffer += new_text
360
  time.sleep(0.01)
361
+ yield buffer
362
+ gc.collect()
363
+ if torch.cuda.is_available():
364
+ torch.cuda.empty_cache()
 
 
 
 
 
 
 
 
 
 
 
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ @spaces.GPU(duration=calc_timeout_generic)
368
+ def generate_pdf(model_name: str, text: str, state,
369
+ max_new_tokens: int = 2048, temperature: float = 0.6,
370
+ top_p: float = 0.9, top_k: int = 50,
371
  repetition_penalty: float = 1.2, gpu_timeout: int = 120):
372
  if not state or not state["pages"]:
373
+ raise gr.Error("Please upload a PDF file first.")
374
+ processor, model = select_model(model_name)
 
 
 
 
 
 
375
  page_images = state["pages"]
376
  full_response = ""
377
  for i, image in enumerate(page_images):
378
  page_header = f"--- Page {i+1}/{len(page_images)} ---\n"
379
+ yield full_response + page_header
380
  messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}]
381
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
382
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
383
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
384
+ generation_kwargs = {
385
+ **inputs,
386
+ "streamer": streamer,
387
+ "max_new_tokens": int(max_new_tokens),
388
+ "do_sample": True,
389
+ "temperature": float(temperature),
390
+ "top_p": float(top_p),
391
+ "top_k": int(top_k),
392
+ "repetition_penalty": float(repetition_penalty),
393
+ }
394
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
395
  thread.start()
396
  page_buffer = ""
397
  for new_text in streamer:
398
  page_buffer += new_text
399
+ yield full_response + page_header + page_buffer
400
  time.sleep(0.01)
401
  full_response += page_header + page_buffer + "\n\n"
402
+ gc.collect()
403
+ if torch.cuda.is_available():
404
+ torch.cuda.empty_cache()
405
+
406
 
407
+ @spaces.GPU(duration=calc_timeout_generic)
408
+ def generate_caption(model_name: str, image: Image.Image,
409
+ max_new_tokens: int = 1024, temperature: float = 0.6,
410
+ top_p: float = 0.9, top_k: int = 50,
411
  repetition_penalty: float = 1.2, gpu_timeout: int = 60):
412
  if image is None:
413
+ raise gr.Error("Please upload an image to caption.")
414
+ processor, model = select_model(model_name)
 
 
 
 
 
 
415
  system_prompt = (
416
  "You are an AI assistant. For the given image, write a precise caption and provide a structured set of "
417
  "attributes describing visual elements like objects, people, actions, colors, and environment."
 
420
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
421
  inputs = processor(text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
422
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
423
+ generation_kwargs = {
424
+ **inputs,
425
+ "streamer": streamer,
426
+ "max_new_tokens": int(max_new_tokens),
427
+ "do_sample": True,
428
+ "temperature": float(temperature),
429
+ "top_p": float(top_p),
430
+ "top_k": int(top_k),
431
+ "repetition_penalty": float(repetition_penalty),
432
+ }
433
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
434
  thread.start()
435
  buffer = ""
436
  for new_text in streamer:
437
  buffer += new_text
438
  time.sleep(0.01)
439
+ yield buffer
440
+ gc.collect()
441
+ if torch.cuda.is_available():
442
+ torch.cuda.empty_cache()
443
 
444
+
445
+ @spaces.GPU(duration=calc_timeout_generic)
446
+ def generate_gif(model_name: str, text: str, gif_path: str,
447
+ max_new_tokens: int = 1024, temperature: float = 0.6,
448
+ top_p: float = 0.9, top_k: int = 50,
449
  repetition_penalty: float = 1.2, gpu_timeout: int = 90):
450
  if gif_path is None:
451
+ raise gr.Error("Please upload a GIF.")
452
+ processor, model = select_model(model_name)
 
 
 
 
 
 
453
  frames = extract_gif_frames(gif_path)
454
  if not frames:
455
+ raise gr.Error("Could not process GIF.")
 
456
  messages = [{"role": "user", "content": [{"type": "text", "text": text}]}]
457
+ for _ in frames:
458
  messages[0]["content"].insert(0, {"type": "image"})
459
  prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
460
  inputs = processor(text=[prompt_full], images=frames, return_tensors="pt", padding=True).to(device)
461
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
462
+ generation_kwargs = {
463
+ **inputs,
464
+ "streamer": streamer,
465
+ "max_new_tokens": int(max_new_tokens),
466
+ "do_sample": True,
467
+ "temperature": float(temperature),
468
+ "top_p": float(top_p),
469
+ "top_k": int(top_k),
470
+ "repetition_penalty": float(repetition_penalty),
471
+ }
472
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
473
  thread.start()
474
  buffer = ""
475
  for new_text in streamer:
476
  buffer += new_text
477
  time.sleep(0.01)
478
+ yield buffer
479
+ gc.collect()
480
+ if torch.cuda.is_available():
481
+ torch.cuda.empty_cache()
 
 
 
 
 
 
 
 
 
482
 
483
+
484
+ def run_router(tab_kind, model_name, text, image_b64, pdf_b64, gif_b64, pdf_state_json,
485
+ max_new_tokens_v, temperature_v, top_p_v, top_k_v, repetition_penalty_v, gpu_timeout_v):
486
+ if tab_kind == "pdf":
487
+ temp_pdf_path = b64_to_temp_file(pdf_b64)
488
+ if not temp_pdf_path:
489
+ raise gr.Error("Could not decode uploaded PDF.")
490
+ try:
491
+ pages = convert_pdf_to_images(temp_pdf_path)
492
+ state = {"pages": pages, "total_pages": len(pages), "current_page_index": 0}
493
+ yield from generate_pdf(
494
+ model_name=model_name,
495
+ text=text,
496
+ state=state,
497
+ max_new_tokens=max_new_tokens_v,
498
+ temperature=temperature_v,
499
+ top_p=top_p_v,
500
+ top_k=top_k_v,
501
+ repetition_penalty=repetition_penalty_v,
502
+ gpu_timeout=gpu_timeout_v,
503
+ )
504
+ finally:
505
+ try:
506
+ os.remove(temp_pdf_path)
507
+ except Exception:
508
+ pass
509
+ elif tab_kind == "gif":
510
+ temp_gif_path = b64_to_temp_file(gif_b64)
511
+ if not temp_gif_path:
512
+ raise gr.Error("Could not decode uploaded GIF.")
513
+ try:
514
+ yield from generate_gif(
515
+ model_name=model_name,
516
+ text=text,
517
+ gif_path=temp_gif_path,
518
+ max_new_tokens=max_new_tokens_v,
519
+ temperature=temperature_v,
520
+ top_p=top_p_v,
521
+ top_k=top_k_v,
522
+ repetition_penalty=repetition_penalty_v,
523
+ gpu_timeout=gpu_timeout_v,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  )
525
+ finally:
526
+ try:
527
+ os.remove(temp_gif_path)
528
+ except Exception:
529
+ pass
530
+ elif tab_kind == "caption":
531
+ image = b64_to_pil(image_b64)
532
+ yield from generate_caption(
533
+ model_name=model_name,
534
+ image=image,
535
+ max_new_tokens=max_new_tokens_v,
536
+ temperature=temperature_v,
537
+ top_p=top_p_v,
538
+ top_k=top_k_v,
539
+ repetition_penalty=repetition_penalty_v,
540
+ gpu_timeout=gpu_timeout_v,
541
+ )
542
+ else:
543
+ image = b64_to_pil(image_b64)
544
+ yield from generate_image(
545
+ model_name=model_name,
546
+ text=text,
547
+ image=image,
548
+ max_new_tokens=max_new_tokens_v,
549
+ temperature=temperature_v,
550
+ top_p=top_p_v,
551
+ top_k=top_k_v,
552
+ repetition_penalty=repetition_penalty_v,
553
+ gpu_timeout=gpu_timeout_v,
554
+ )
555
+
556
+
557
+ def noop():
558
+ return None
559
+
560
+
561
+ css = r"""
562
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
563
+ *{box-sizing:border-box;margin:0;padding:0}
564
+ html,body{height:100%;overflow-x:hidden}
565
+ body,.gradio-container{
566
+ background:#0f0f13!important;
567
+ font-family:'Inter',system-ui,-apple-system,sans-serif!important;
568
+ font-size:14px!important;color:#e4e4e7!important;min-height:100vh;overflow-x:hidden;
569
+ }
570
+ .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
571
+ footer{display:none!important}
572
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
573
+
574
+ #gradio-run-btn,#example-load-btn{
575
+ position:absolute!important;left:-9999px!important;top:-9999px!important;
576
+ width:1px!important;height:1px!important;opacity:0.01!important;
577
+ pointer-events:none!important;overflow:hidden!important;
578
+ }
579
+
580
+ .app-shell{
581
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
582
+ margin:12px auto;max-width:1450px;overflow:hidden;
583
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
584
+ }
585
+ .app-header{
586
+ background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
587
+ padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
588
+ }
589
+ .app-header-left{display:flex;align-items:center;gap:12px}
590
+ .app-logo{
591
+ width:38px;height:38px;background:linear-gradient(135deg,#0000CD,#2645ff,#5876ff);
592
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
593
+ box-shadow:0 4px 12px rgba(0,0,205,.35);
594
+ }
595
+ .app-logo svg{width:22px;height:22px;fill:#fff;flex-shrink:0}
596
+ .app-title{
597
+ font-size:18px;font-weight:700;background:linear-gradient(135deg,#f5f5f5,#bdbdbd);
598
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
599
+ }
600
+ .app-badge{
601
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
602
+ background:rgba(0,0,205,.12);color:#8da1ff;border:1px solid rgba(0,0,205,.25);letter-spacing:.3px;
603
+ }
604
+ .app-badge.fast{background:rgba(38,69,255,.10);color:#90a3ff;border:1px solid rgba(38,69,255,.22)}
605
+
606
+ .model-tabs-bar,.mode-tabs-bar{
607
+ background:#18181b;border-bottom:1px solid #27272a;padding:10px 16px;
608
+ display:flex;gap:8px;align-items:center;flex-wrap:wrap;
609
+ }
610
+ .mode-tabs-bar{padding-top:8px;padding-bottom:12px}
611
+ .model-tab,.mode-tab{
612
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
613
+ min-width:32px;height:34px;background:transparent;border:1px solid #27272a;
614
+ border-radius:999px;cursor:pointer;font-size:12px;font-weight:600;padding:0 12px;
615
+ color:#ffffff!important;transition:all .15s ease;
616
+ }
617
+ .mode-tab{min-width:115px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
618
+ .model-tab:hover,.mode-tab:hover{background:rgba(0,0,205,.12);border-color:rgba(0,0,205,.35)}
619
+ .model-tab.active,.mode-tab.active{background:rgba(0,0,205,.22);border-color:#0000CD;color:#fff!important;box-shadow:0 0 0 2px rgba(0,0,205,.10)}
620
+ .model-tab-label{font-size:12px;color:#ffffff!important;font-weight:600}
621
+
622
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
623
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
624
+ .app-main-right{width:500px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
625
+
626
+ #media-drop-zone{
627
+ position:relative;background:#09090b;height:440px;min-height:440px;max-height:440px;overflow:hidden;
628
+ }
629
+ #media-drop-zone.drag-over{outline:2px solid #0000CD;outline-offset:-2px;background:rgba(0,0,205,.04)}
630
+ .upload-prompt-modern{
631
+ position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:20px;z-index:20;overflow:hidden;
632
+ }
633
+ .upload-click-area{
634
+ display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;
635
+ padding:28px 36px;max-width:92%;max-height:92%;border:2px dashed #3f3f46;border-radius:16px;
636
+ background:rgba(0,0,205,.03);transition:all .2s ease;gap:8px;text-align:center;overflow:hidden;
637
+ }
638
+ .upload-click-area:hover{background:rgba(0,0,205,.08);border-color:#0000CD;transform:scale(1.02)}
639
+ .upload-click-area:active{background:rgba(0,0,205,.12);transform:scale(.99)}
640
+ .upload-click-area svg{width:86px;height:86px;max-width:100%;flex-shrink:0}
641
+ .upload-main-text{color:#a1a1aa;font-size:14px;font-weight:600;margin-top:4px}
642
+ .upload-sub-text{color:#71717a;font-size:12px}
643
+
644
+ .single-preview-wrap{
645
+ width:100%;height:100%;display:none;align-items:center;justify-content:center;padding:16px;overflow:hidden;
646
+ }
647
+ .single-preview-card{
648
+ width:100%;height:100%;max-width:100%;max-height:100%;border-radius:14px;overflow:hidden;border:1px solid #27272a;background:#111114;
649
+ display:flex;align-items:center;justify-content:center;position:relative;
650
+ }
651
+ .single-preview-card img,.single-preview-card iframe{
652
+ width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;background:#000;border:none;
653
+ }
654
+ .preview-overlay-actions{
655
+ position:absolute;top:12px;right:12px;display:flex;gap:8px;z-index:5;
656
+ }
657
+ .preview-action-btn{
658
+ display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:34px;padding:0 12px;background:rgba(0,0,0,.65);
659
+ border:1px solid rgba(255,255,255,.14);border-radius:10px;cursor:pointer;color:#fff!important;font-size:12px;font-weight:600;transition:all .15s ease;
660
+ }
661
+ .preview-action-btn:hover{background:#0000CD;border-color:#0000CD}
662
+
663
+ .pdf-nav-wrap{
664
+ position:absolute;left:12px;bottom:12px;z-index:6;display:flex;align-items:center;gap:8px;
665
+ background:rgba(0,0,0,.6);border:1px solid rgba(255,255,255,.1);padding:8px 10px;border-radius:10px;
666
+ }
667
+ .pdf-nav-btn{
668
+ display:inline-flex;align-items:center;justify-content:center;height:30px;min-width:30px;padding:0 10px;
669
+ background:#18181b;border:1px solid #27272a;border-radius:8px;color:#fff;cursor:pointer;font-size:12px;font-weight:700;
670
+ }
671
+ .pdf-nav-btn:hover{background:#0000CD;border-color:#0000CD}
672
+ .pdf-page-indicator{font-size:12px;color:#d4d4d8;font-family:'JetBrains Mono',monospace}
673
+
674
+ .hint-bar{
675
+ background:rgba(0,0,205,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
676
+ padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
677
+ }
678
+ .hint-bar b{color:#8da1ff;font-weight:600}
679
+ .hint-bar kbd{
680
+ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;border-radius:4px;
681
+ font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
682
+ }
683
+
684
+ .examples-section{border-top:1px solid #27272a;padding:12px 16px}
685
+ .examples-title{
686
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px;
687
+ }
688
+ .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
689
+ .examples-scroll::-webkit-scrollbar{height:6px}
690
+ .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
691
+ .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
692
+ .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
693
+ .example-card{
694
+ position:relative;flex-shrink:0;width:220px;background:#09090b;border:1px solid #27272a;border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
695
+ }
696
+ .example-card:hover{border-color:#0000CD;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,205,.15)}
697
+ .example-card.loading{opacity:.5;pointer-events:none}
698
+ .example-thumb-wrap{height:120px;overflow:hidden;background:#18181b;position:relative}
699
+ .example-thumb-wrap img{width:100%;height:100%;object-fit:cover}
700
+ .example-media-chip{
701
+ position:absolute;top:8px;left:8px;display:inline-flex;padding:3px 7px;background:rgba(0,0,0,.7);border:1px solid rgba(255,255,255,.12);
702
+ border-radius:999px;font-size:10px;font-weight:700;color:#fff;letter-spacing:.5px;
703
+ }
704
+ .example-thumb-placeholder{
705
+ width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#18181b;color:#3f3f46;font-size:11px;
706
+ }
707
+ .example-meta-row{padding:6px 10px;display:flex;align-items:center;gap:6px}
708
+ .example-badge{
709
+ display:inline-flex;padding:2px 7px;background:rgba(0,0,205,.12);border-radius:4px;font-size:10px;font-weight:600;color:#8da1ff;
710
+ font-family:'JetBrains Mono',monospace;white-space:nowrap;
711
+ }
712
+ .example-prompt-text{
713
+ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
714
+ }
715
+
716
+ .panel-card{border-bottom:1px solid #27272a}
717
+ .panel-card-title{
718
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
719
+ }
720
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
721
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
722
+ .modern-textarea{
723
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
724
+ resize:none;outline:none;min-height:100px;transition:border-color .2s;
725
+ }
726
+ .modern-textarea:focus{border-color:#0000CD;box-shadow:0 0 0 3px rgba(0,0,205,.15)}
727
+ .modern-textarea::placeholder{color:#3f3f46}
728
+ .modern-textarea.error-flash{
729
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
730
+ }
731
+ @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
732
+
733
+ .toast-notification{
734
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);z-index:9999;padding:10px 24px;border-radius:10px;
735
+ font-family:'Inter',sans-serif;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);
736
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
737
+ }
738
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
739
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
740
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
741
+ .toast-notification.info{background:linear-gradient(135deg,#1e40af,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
742
+ .toast-notification .toast-icon{font-size:16px;line-height:1}
743
+ .toast-notification .toast-text{line-height:1.3}
744
+
745
+ .btn-run{
746
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:linear-gradient(135deg,#0000CD,#1638b7);border:none;border-radius:10px;
747
+ padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
748
+ transition:all .2s ease;letter-spacing:-.2px;box-shadow:0 4px 16px rgba(0,0,205,.3),inset 0 1px 0 rgba(255,255,255,.1);
749
+ }
750
+ .btn-run:hover{
751
+ background:linear-gradient(135deg,#2645ff,#0000CD);transform:translateY(-1px);box-shadow:0 6px 24px rgba(0,0,205,.45),inset 0 1px 0 rgba(255,255,255,.15);
752
+ }
753
+ .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(0,0,205,.3)}
754
+ #custom-run-btn,#custom-run-btn *,#run-btn-label,.btn-run,.btn-run *{
755
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
756
+ }
757
+
758
+ .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
759
+ .output-frame .out-title,.output-frame .out-title *,#output-title-label{
760
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
761
+ }
762
+ .output-frame .out-title{
763
+ padding:10px 20px;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
764
+ display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;
765
+ }
766
+ .out-title-right{display:flex;gap:8px;align-items:center}
767
+ .out-action-btn{
768
+ display:inline-flex;align-items:center;justify-content:center;background:rgba(0,0,205,.1);border:1px solid rgba(0,0,205,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
769
+ font-size:11px;font-weight:500;color:#8da1ff!important;gap:4px;height:24px;transition:all .15s;
770
+ }
771
+ .out-action-btn:hover{background:rgba(0,0,205,.2);border-color:rgba(0,0,205,.35);color:#ffffff!important}
772
+ .out-action-btn svg{width:12px;height:12px;fill:#8da1ff}
773
+ .output-frame .out-body{
774
+ flex:1;background:#09090b;display:flex;align-items:stretch;justify-content:stretch;overflow:hidden;min-height:320px;position:relative;
775
+ }
776
+ .output-scroll-wrap{width:100%;height:100%;padding:0;overflow:hidden}
777
+ .output-textarea{
778
+ width:100%;height:320px;min-height:320px;max-height:320px;background:#09090b;color:#e4e4e7;border:none;outline:none;padding:16px 18px;font-size:13px;line-height:1.6;
779
+ font-family:'JetBrains Mono',monospace;overflow:auto;resize:none;white-space:pre-wrap;
780
+ }
781
+ .output-textarea::placeholder{color:#52525b}
782
+ .output-textarea.error-flash{box-shadow:inset 0 0 0 2px rgba(239,68,68,.6)}
783
+ .modern-loader{
784
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
785
+ }
786
+ .modern-loader.active{display:flex}
787
+ .modern-loader .loader-spinner{
788
+ width:36px;height:36px;border:3px solid #27272a;border-top-color:#0000CD;border-radius:50%;animation:spin .8s linear infinite;
789
+ }
790
+ @keyframes spin{to{transform:rotate(360deg)}}
791
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
792
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
793
+ .loader-bar-fill{
794
+ height:100%;background:linear-gradient(90deg,#0000CD,#4d6dff,#0000CD);background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
795
+ }
796
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
797
+
798
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
799
+ .settings-group-title{
800
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
801
+ }
802
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
803
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
804
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:118px;flex-shrink:0}
805
+ .slider-row input[type="range"]{
806
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;border-radius:3px;outline:none;min-width:0;
807
+ }
808
+ .slider-row input[type="range"]::-webkit-slider-thumb{
809
+ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#0000CD,#1638b7);border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,205,.4);transition:transform .15s;
810
+ }
811
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
812
+ .slider-row input[type="range"]::-moz-range-thumb{
813
+ width:16px;height:16px;background:linear-gradient(135deg,#0000CD,#1638b7);border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(0,0,205,.4);
814
+ }
815
+ .slider-row .slider-val{
816
+ min-width:58px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;border-radius:6px;color:#a1a1aa;flex-shrink:0;
817
+ }
818
+
819
+ .app-statusbar{
820
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
821
+ }
822
+ .app-statusbar .sb-section{
823
+ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
824
+ }
825
+ .app-statusbar .sb-section.sb-fixed{
826
+ flex:0 0 auto;min-width:110px;text-align:center;justify-content:center;padding:3px 12px;background:rgba(0,0,205,.08);border-radius:6px;color:#8da1ff;font-weight:500;
827
+ }
828
+
829
+ .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}
830
+ .exp-note a{color:#8da1ff;text-decoration:none}
831
+ .exp-note a:hover{text-decoration:underline}
832
+
833
+ ::-webkit-scrollbar{width:8px;height:8px}
834
+ ::-webkit-scrollbar-track{background:#09090b}
835
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
836
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
837
+
838
+ @media(max-width:980px){
839
+ .app-main-row{flex-direction:column}
840
+ .app-main-right{width:100%}
841
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
842
+ }
843
+ """
844
+
845
+ gallery_js = r"""
846
+ () => {
847
+ function init() {
848
+ if (window.__outpostInitDone) return;
849
+
850
+ const dropZone = document.getElementById('media-drop-zone');
851
+ const uploadPrompt = document.getElementById('upload-prompt');
852
+ const uploadClick = document.getElementById('upload-click-area');
853
+ const fileInput = document.getElementById('custom-file-input');
854
+ const previewWrap = document.getElementById('single-preview-wrap');
855
+ const previewImg = document.getElementById('single-preview-img');
856
+ const previewPdf = document.getElementById('single-preview-pdf');
857
+ const btnUpload = document.getElementById('preview-upload-btn');
858
+ const btnClear = document.getElementById('preview-clear-btn');
859
+ const promptInput = document.getElementById('custom-query-input');
860
+ const runBtnEl = document.getElementById('custom-run-btn');
861
+ const outputArea = document.getElementById('custom-output-textarea');
862
+ const mediaStatus = document.getElementById('sb-media-status');
863
+ const exampleResultContainer = document.getElementById('example-result-data');
864
+ const pdfPrevBtn = document.getElementById('pdf-prev-btn');
865
+ const pdfNextBtn = document.getElementById('pdf-next-btn');
866
+ const pdfPageInfo = document.getElementById('pdf-page-info');
867
+
868
+ if (!dropZone || !fileInput || !promptInput || !previewWrap || !previewImg || !previewPdf) {
869
+ setTimeout(init, 250);
870
+ return;
871
+ }
872
+
873
+ window.__outpostInitDone = true;
874
+ let mediaState = null;
875
+ let currentMode = 'image';
876
+ let toastTimer = null;
877
+ let examplePoller = null;
878
+ let lastSeenExamplePayload = null;
879
+
880
+ function showToast(message, type) {
881
+ let toast = document.getElementById('app-toast');
882
+ if (!toast) {
883
+ toast = document.createElement('div');
884
+ toast.id = 'app-toast';
885
+ toast.className = 'toast-notification';
886
+ toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
887
+ document.body.appendChild(toast);
888
+ }
889
+ const icon = toast.querySelector('.toast-icon');
890
+ const text = toast.querySelector('.toast-text');
891
+ toast.className = 'toast-notification ' + (type || 'error');
892
+ if (type === 'warning') icon.textContent = '\u26A0';
893
+ else if (type === 'info') icon.textContent = '\u2139';
894
+ else icon.textContent = '\u2717';
895
+ text.textContent = message;
896
+ if (toastTimer) clearTimeout(toastTimer);
897
+ void toast.offsetWidth;
898
+ toast.classList.add('visible');
899
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
900
+ }
901
+
902
+ function showLoader() {
903
+ const l = document.getElementById('output-loader');
904
+ if (l) l.classList.add('active');
905
+ const sb = document.getElementById('sb-run-state');
906
+ if (sb) sb.textContent = 'Processing...';
907
+ }
908
+ function hideLoader() {
909
+ const l = document.getElementById('output-loader');
910
+ if (l) l.classList.remove('active');
911
+ const sb = document.getElementById('sb-run-state');
912
+ if (sb) sb.textContent = 'Done';
913
+ }
914
+ window.__hideLoader = hideLoader;
915
+
916
+ function flashPromptError() {
917
+ promptInput.classList.add('error-flash');
918
+ promptInput.focus();
919
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
920
+ }
921
+
922
+ function flashOutputError() {
923
+ if (!outputArea) return;
924
+ outputArea.classList.add('error-flash');
925
+ setTimeout(() => outputArea.classList.remove('error-flash'), 800);
926
+ }
927
+
928
+ function getValueFromContainer(containerId) {
929
+ const container = document.getElementById(containerId);
930
+ if (!container) return '';
931
+ const el = container.querySelector('textarea, input');
932
+ return el ? (el.value || '') : '';
933
+ }
934
+
935
+ function setGradioValue(containerId, value) {
936
+ const container = document.getElementById(containerId);
937
+ if (!container) return;
938
+ container.querySelectorAll('input, textarea').forEach(el => {
939
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
940
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
941
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
942
+ if (ns && ns.set) {
943
+ ns.set.call(el, value);
944
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
945
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
946
+ }
947
+ });
948
+ }
949
+
950
+ function syncMediaToGradio() {
951
+ setGradioValue('hidden-image-b64', mediaState && (mediaState.mode === 'image' || mediaState.mode === 'caption') ? mediaState.b64 : '');
952
+ setGradioValue('hidden-pdf-b64', mediaState && mediaState.mode === 'pdf' ? mediaState.b64 : '');
953
+ setGradioValue('hidden-gif-b64', mediaState && mediaState.mode === 'gif' ? mediaState.b64 : '');
954
+ if (mediaStatus) mediaStatus.textContent = mediaState ? (`1 ${mediaState.mode} uploaded`) : `No ${currentMode} uploaded`;
955
+ }
956
+
957
+ function syncPromptToGradio() {
958
+ setGradioValue('prompt-gradio-input', promptInput.value);
959
+ }
960
+
961
+ function syncModelToGradio(name) {
962
+ setGradioValue('hidden-model-name', name);
963
+ }
964
+
965
+ function syncModeToGradio(mode) {
966
+ setGradioValue('hidden-mode-name', mode);
967
+ }
968
+
969
+ function resetPdfState() {
970
+ window.__pdfPages = [];
971
+ window.__pdfPageIndex = 0;
972
+ if (pdfPageInfo) pdfPageInfo.textContent = 'No file loaded';
973
+ }
974
+
975
+ function renderPdfPage() {
976
+ if (!window.__pdfPages || !window.__pdfPages.length) {
977
+ previewImg.src = '';
978
+ previewImg.style.display = 'none';
979
+ previewPdf.style.display = 'none';
980
+ if (pdfPageInfo) pdfPageInfo.textContent = 'No file loaded';
981
+ return;
982
+ }
983
+ const idx = Math.max(0, Math.min(window.__pdfPageIndex || 0, window.__pdfPages.length - 1));
984
+ window.__pdfPageIndex = idx;
985
+ previewPdf.style.display = 'none';
986
+ previewImg.src = window.__pdfPages[idx];
987
+ previewImg.style.display = 'block';
988
+ if (pdfPageInfo) pdfPageInfo.textContent = `Page ${idx + 1} / ${window.__pdfPages.length}`;
989
+ }
990
+
991
+ function renderPreview() {
992
+ if (!mediaState) {
993
+ previewImg.src = '';
994
+ previewPdf.removeAttribute('src');
995
+ previewImg.style.display = 'none';
996
+ previewPdf.style.display = 'none';
997
+ previewWrap.style.display = 'none';
998
+ resetPdfState();
999
+ if (uploadPrompt) uploadPrompt.style.display = 'flex';
1000
+ syncMediaToGradio();
1001
+ return;
1002
+ }
1003
+
1004
+ previewWrap.style.display = 'flex';
1005
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
1006
+
1007
+ if (mediaState.mode === 'pdf') {
1008
+ previewImg.style.display = 'none';
1009
+ previewPdf.style.display = 'none';
1010
+ renderPdfPage();
1011
+ } else {
1012
+ resetPdfState();
1013
+ previewPdf.removeAttribute('src');
1014
+ previewPdf.style.display = 'none';
1015
+ previewImg.src = mediaState.preview || mediaState.b64;
1016
+ previewImg.style.display = 'block';
1017
+ }
1018
+
1019
+ syncMediaToGradio();
1020
+ }
1021
+
1022
+ function setPreviewFromFileReader(b64, name, mode) {
1023
+ mediaState = {b64, name: name || 'file', mode: mode || currentMode};
1024
+ renderPreview();
1025
+ }
1026
+
1027
+ function clearPreview() {
1028
+ mediaState = null;
1029
+ renderPreview();
1030
+ }
1031
+ window.__clearPreview = clearPreview;
1032
+
1033
+ function processFile(file) {
1034
+ if (!file) return;
1035
+ const mode = currentMode;
1036
+ if (mode === 'image' || mode === 'caption') {
1037
+ if (!file.type.startsWith('image/')) {
1038
+ showToast('Only image files are supported in this mode', 'error');
1039
+ return;
1040
+ }
1041
+ } else if (mode === 'gif') {
1042
+ const ok = file.type === 'image/gif' || file.name.toLowerCase().endsWith('.gif');
1043
+ if (!ok) {
1044
+ showToast('Only GIF files are supported in GIF mode', 'error');
1045
+ return;
1046
+ }
1047
+ } else if (mode === 'pdf') {
1048
+ const ok = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
1049
+ if (!ok) {
1050
+ showToast('Only PDF files are supported in PDF mode', 'error');
1051
+ return;
1052
+ }
1053
+ }
1054
+
1055
+ const reader = new FileReader();
1056
+ reader.onload = (e) => {
1057
+ const b64 = e.target.result;
1058
+ if (mode === 'pdf') {
1059
+ mediaState = {b64, name: file.name, mode: 'pdf'};
1060
+ resetPdfState();
1061
+ renderPreview();
1062
+ } else if (mode === 'gif') {
1063
+ setPreviewFromFileReader(b64, file.name, 'gif');
1064
+ } else if (mode === 'caption') {
1065
+ setPreviewFromFileReader(b64, file.name, 'caption');
1066
+ } else {
1067
+ setPreviewFromFileReader(b64, file.name, 'image');
1068
+ }
1069
+ };
1070
+ reader.readAsDataURL(file);
1071
+ }
1072
+
1073
+ function updateAccept() {
1074
+ if (currentMode === 'pdf') fileInput.accept = '.pdf,application/pdf';
1075
+ else if (currentMode === 'gif') fileInput.accept = '.gif,image/gif';
1076
+ else fileInput.accept = 'image/*';
1077
+
1078
+ const main = document.getElementById('upload-main-text');
1079
+ const sub = document.getElementById('upload-sub-text');
1080
+
1081
+ if (currentMode === 'pdf') {
1082
+ if (main) main.textContent = 'Click or drag a PDF here';
1083
+ if (sub) sub.textContent = 'Upload one PDF document for page-wise multimodal analysis';
1084
+ } else if (currentMode === 'gif') {
1085
+ if (main) main.textContent = 'Click or drag a GIF here';
1086
+ if (sub) sub.textContent = 'Upload one animated GIF for multimodal motion understanding';
1087
+ } else if (currentMode === 'caption') {
1088
+ if (main) main.textContent = 'Click or drag an image here';
1089
+ if (sub) sub.textContent = 'Upload one image for long caption and visual attribute generation';
1090
+ } else {
1091
+ if (main) main.textContent = 'Click or drag an image here';
1092
+ if (sub) sub.textContent = 'Upload one document, page, chart, screenshot, or scene image for vision tasks';
1093
+ }
1094
+
1095
+ if (!mediaState && mediaStatus) mediaStatus.textContent = `No ${currentMode} uploaded`;
1096
+ }
1097
+
1098
+ function activateModelTab(name) {
1099
+ document.querySelectorAll('.model-tab[data-model]').forEach(btn => {
1100
+ btn.classList.toggle('active', btn.getAttribute('data-model') === name);
1101
+ });
1102
+ syncModelToGradio(name);
1103
+ }
1104
+
1105
+ function activateModeTab(mode) {
1106
+ currentMode = mode;
1107
+ document.querySelectorAll('.mode-tab[data-mode]').forEach(btn => {
1108
+ btn.classList.toggle('active', btn.getAttribute('data-mode') === mode);
1109
+ });
1110
+ syncModeToGradio(mode);
1111
+ updateAccept();
1112
+
1113
+ const title = document.getElementById('instruction-title');
1114
+ const promptLabel = document.getElementById('query-label');
1115
+ const runLabel = document.getElementById('run-btn-label');
1116
+ const textarea = document.getElementById('custom-query-input');
1117
+
1118
+ if (mode === 'caption') {
1119
+ if (title) title.textContent = 'Caption Instruction';
1120
+ if (promptLabel) promptLabel.textContent = 'Caption Prompt';
1121
+ if (runLabel) runLabel.textContent = 'Generate Caption';
1122
+ if (textarea) textarea.placeholder = 'e.g., generate a detailed caption with structured attributes...';
1123
+ } else if (mode === 'pdf') {
1124
+ if (title) title.textContent = 'Document Instruction';
1125
+ if (promptLabel) promptLabel.textContent = 'PDF Query';
1126
+ if (runLabel) runLabel.textContent = 'Run PDF Inference';
1127
+ if (textarea) textarea.placeholder = 'e.g., summarize this PDF, extract content precisely, analyze the report...';
1128
+ } else if (mode === 'gif') {
1129
+ if (title) title.textContent = 'GIF Instruction';
1130
+ if (promptLabel) promptLabel.textContent = 'GIF Query';
1131
+ if (runLabel) runLabel.textContent = 'Run GIF Inference';
1132
+ if (textarea) textarea.placeholder = 'e.g., what is happening in this gif? describe the motion and scene...';
1133
+ } else {
1134
+ if (title) title.textContent = 'Vision Instruction';
1135
+ if (promptLabel) promptLabel.textContent = 'Query Input';
1136
+ if (runLabel) runLabel.textContent = 'Run Inference';
1137
+ if (textarea) textarea.placeholder = 'e.g., perform OCR, solve the problem, describe the image, extract visible text...';
1138
+ }
1139
+
1140
+ if (mediaState && mediaState.mode !== mode) clearPreview();
1141
+ }
1142
+
1143
+ window.__activateModeTab = activateModeTab;
1144
+ window.__activateModelTab = activateModelTab;
1145
+
1146
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
1147
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
1148
+ if (btnClear) btnClear.addEventListener('click', clearPreview);
1149
+
1150
+ fileInput.addEventListener('change', (e) => {
1151
+ const file = e.target.files && e.target.files[0] ? e.target.files[0] : null;
1152
+ if (file) processFile(file);
1153
+ e.target.value = '';
1154
+ });
1155
+
1156
+ dropZone.addEventListener('dragover', (e) => {
1157
+ e.preventDefault();
1158
+ dropZone.classList.add('drag-over');
1159
+ });
1160
+ dropZone.addEventListener('dragleave', (e) => {
1161
+ e.preventDefault();
1162
+ dropZone.classList.remove('drag-over');
1163
+ });
1164
+ dropZone.addEventListener('drop', (e) => {
1165
+ e.preventDefault();
1166
+ dropZone.classList.remove('drag-over');
1167
+ if (e.dataTransfer.files && e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
1168
+ });
1169
+
1170
+ promptInput.addEventListener('input', syncPromptToGradio);
1171
+
1172
+ document.querySelectorAll('.model-tab[data-model]').forEach(btn => {
1173
+ btn.addEventListener('click', () => activateModelTab(btn.getAttribute('data-model')));
1174
+ });
1175
+ document.querySelectorAll('.mode-tab[data-mode]').forEach(btn => {
1176
+ btn.addEventListener('click', () => activateModeTab(btn.getAttribute('data-mode')));
1177
+ });
1178
+
1179
+ if (pdfPrevBtn) {
1180
+ pdfPrevBtn.addEventListener('click', () => {
1181
+ if (!window.__pdfPages || !window.__pdfPages.length) return;
1182
+ window.__pdfPageIndex = Math.max(0, (window.__pdfPageIndex || 0) - 1);
1183
+ renderPdfPage();
1184
+ });
1185
+ }
1186
+ if (pdfNextBtn) {
1187
+ pdfNextBtn.addEventListener('click', () => {
1188
+ if (!window.__pdfPages || !window.__pdfPages.length) return;
1189
+ window.__pdfPageIndex = Math.min(window.__pdfPages.length - 1, (window.__pdfPageIndex || 0) + 1);
1190
+ renderPdfPage();
1191
+ });
1192
+ }
1193
+
1194
+ activateModelTab('Qwen3-VL-4B-Instruct');
1195
+ activateModeTab('image');
1196
+
1197
+ function syncSlider(customId, gradioId) {
1198
+ const slider = document.getElementById(customId);
1199
+ const valSpan = document.getElementById(customId + '-val');
1200
+ if (!slider) return;
1201
+ slider.addEventListener('input', () => {
1202
+ if (valSpan) valSpan.textContent = slider.value;
1203
+ const container = document.getElementById(gradioId);
1204
+ if (!container) return;
1205
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
1206
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
1207
+ if (ns && ns.set) {
1208
+ ns.set.call(el, slider.value);
1209
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
1210
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
1211
+ }
1212
+ });
1213
+ });
1214
+ }
1215
+
1216
+ syncSlider('custom-max-new-tokens', 'gradio-max-new-tokens');
1217
+ syncSlider('custom-temperature', 'gradio-temperature');
1218
+ syncSlider('custom-top-p', 'gradio-top-p');
1219
+ syncSlider('custom-top-k', 'gradio-top-k');
1220
+ syncSlider('custom-repetition-penalty', 'gradio-repetition-penalty');
1221
+ syncSlider('custom-gpu-duration', 'gradio-gpu-duration');
1222
+
1223
+ function validateBeforeRun() {
1224
+ const promptVal = promptInput.value.trim();
1225
+ if (currentMode !== 'caption' && !promptVal) {
1226
+ showToast('Please enter your instruction', 'warning');
1227
+ flashPromptError();
1228
+ return false;
1229
+ }
1230
+ if (!mediaState) {
1231
+ showToast(`Please upload a ${currentMode}`, 'error');
1232
+ return false;
1233
+ }
1234
+ if (mediaState.mode !== currentMode) {
1235
+ showToast(`Uploaded media does not match ${currentMode} mode`, 'error');
1236
+ return false;
1237
+ }
1238
+ const currentModel = (document.querySelector('.model-tab.active') || {}).dataset?.model;
1239
+ if (!currentModel) {
1240
+ showToast('Please select a model', 'error');
1241
+ return false;
1242
+ }
1243
+ return true;
1244
+ }
1245
+
1246
+ window.__clickGradioRunBtn = function() {
1247
+ if (!validateBeforeRun()) return;
1248
+ syncPromptToGradio();
1249
+ syncMediaToGradio();
1250
+ const activeModel = document.querySelector('.model-tab.active');
1251
+ const activeMode = document.querySelector('.mode-tab.active');
1252
+ if (activeModel) syncModelToGradio(activeModel.getAttribute('data-model'));
1253
+ if (activeMode) syncModeToGradio(activeMode.getAttribute('data-mode'));
1254
+ if (outputArea) outputArea.value = '';
1255
+ showLoader();
1256
+ setTimeout(() => {
1257
+ const gradioBtn = document.getElementById('gradio-run-btn');
1258
+ if (!gradioBtn) return;
1259
+ const btn = gradioBtn.querySelector('button');
1260
+ if (btn) btn.click(); else gradioBtn.click();
1261
+ }, 180);
1262
+ };
1263
+
1264
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
1265
+
1266
+ const copyBtn = document.getElementById('copy-output-btn');
1267
+ if (copyBtn) {
1268
+ copyBtn.addEventListener('click', async () => {
1269
+ try {
1270
+ const text = outputArea ? outputArea.value : '';
1271
+ if (!text.trim()) {
1272
+ showToast('No output to copy', 'warning');
1273
+ flashOutputError();
1274
+ return;
1275
+ }
1276
+ await navigator.clipboard.writeText(text);
1277
+ showToast('Output copied to clipboard', 'info');
1278
+ } catch(e) {
1279
+ showToast('Copy failed', 'error');
1280
+ }
1281
+ });
1282
+ }
1283
+
1284
+ const saveBtn = document.getElementById('save-output-btn');
1285
+ if (saveBtn) {
1286
+ saveBtn.addEventListener('click', () => {
1287
+ const text = outputArea ? outputArea.value : '';
1288
+ if (!text.trim()) {
1289
+ showToast('No output to save', 'warning');
1290
+ flashOutputError();
1291
+ return;
1292
+ }
1293
+ const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
1294
+ const a = document.createElement('a');
1295
+ a.href = URL.createObjectURL(blob);
1296
+ a.download = 'qwen3_vl_outpost_output.txt';
1297
+ document.body.appendChild(a);
1298
+ a.click();
1299
+ setTimeout(() => {
1300
+ URL.revokeObjectURL(a.href);
1301
+ document.body.removeChild(a);
1302
+ }, 200);
1303
+ showToast('Output saved', 'info');
1304
+ });
1305
+ }
1306
+
1307
+ function applyExamplePayload(raw) {
1308
+ try {
1309
+ const data = JSON.parse(raw);
1310
+ if (data.status !== 'ok') {
1311
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1312
+ showToast(data.message || 'Failed to load example', 'error');
1313
+ return;
1314
+ }
1315
+
1316
+ if (data.kind) activateModeTab(data.kind);
1317
+ if (data.model) activateModelTab(data.model);
1318
+
1319
+ if (data.query) {
1320
+ promptInput.value = data.query;
1321
+ syncPromptToGradio();
1322
+ }
1323
+
1324
+ if (data.kind === 'pdf') {
1325
+ mediaState = {b64: data.file || '', name: data.name || 'example.pdf', mode: 'pdf'};
1326
+ window.__pdfPages = data.preview ? [data.preview] : [];
1327
+ window.__pdfPageIndex = 0;
1328
+ if (pdfPageInfo) pdfPageInfo.textContent = data.page_info || 'Page 1 / 1';
1329
+ renderPreview();
1330
+ } else {
1331
+ mediaState = {
1332
+ b64: data.media || '',
1333
+ preview: data.media || '',
1334
+ name: data.name || 'example_file',
1335
+ mode: data.kind === 'caption' ? 'caption' : data.kind
1336
+ };
1337
+ renderPreview();
1338
+ }
1339
+
1340
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1341
+ showToast('Example loaded', 'info');
1342
+ } catch (e) {
1343
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1344
+ showToast('Failed to parse example data', 'error');
1345
+ }
1346
+ }
1347
+
1348
+ function startExamplePolling() {
1349
+ if (examplePoller) clearInterval(examplePoller);
1350
+ let attempts = 0;
1351
+ examplePoller = setInterval(() => {
1352
+ attempts += 1;
1353
+ const current = getValueFromContainer('example-result-data');
1354
+ if (current && current !== lastSeenExamplePayload) {
1355
+ lastSeenExamplePayload = current;
1356
+ clearInterval(examplePoller);
1357
+ examplePoller = null;
1358
+ applyExamplePayload(current);
1359
+ return;
1360
+ }
1361
+ if (attempts >= 80) {
1362
+ clearInterval(examplePoller);
1363
+ examplePoller = null;
1364
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1365
+ showToast('Example load timed out', 'error');
1366
+ }
1367
+ }, 150);
1368
+ }
1369
+
1370
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
1371
+ card.addEventListener('click', () => {
1372
+ const idx = card.getAttribute('data-idx');
1373
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1374
+ card.classList.add('loading');
1375
+ showToast('Loading example...', 'info');
1376
+
1377
+ setGradioValue('example-result-data', '');
1378
+ setGradioValue('example-idx-input', idx);
1379
+
1380
+ setTimeout(() => {
1381
+ const btn = document.getElementById('example-load-btn');
1382
+ if (btn) {
1383
+ const b = btn.querySelector('button');
1384
+ if (b) b.click(); else btn.click();
1385
+ }
1386
+ startExamplePolling();
1387
+ }, 220);
1388
+ });
1389
+ });
1390
+
1391
+ const observerTarget = document.getElementById('example-result-data');
1392
+ if (observerTarget) {
1393
+ const obs = new MutationObserver(() => {
1394
+ const current = getValueFromContainer('example-result-data');
1395
+ if (current && current !== lastSeenExamplePayload) {
1396
+ lastSeenExamplePayload = current;
1397
+ if (examplePoller) {
1398
+ clearInterval(examplePoller);
1399
+ examplePoller = null;
1400
+ }
1401
+ applyExamplePayload(current);
1402
+ }
1403
+ });
1404
+ obs.observe(observerTarget, {childList:true, subtree:true, characterData:true, attributes:true});
1405
+ }
1406
+
1407
+ if (outputArea) outputArea.value = '';
1408
+ const sb = document.getElementById('sb-run-state');
1409
+ if (sb) sb.textContent = 'Ready';
1410
+ if (mediaStatus) mediaStatus.textContent = 'No image uploaded';
1411
+ }
1412
+ init();
1413
+ }
1414
+ """
1415
+
1416
+ wire_outputs_js = r"""
1417
+ () => {
1418
+ function watchOutputs() {
1419
+ const resultContainer = document.getElementById('gradio-result');
1420
+ const outArea = document.getElementById('custom-output-textarea');
1421
+ if (!resultContainer || !outArea) { setTimeout(watchOutputs, 500); return; }
1422
+
1423
+ let lastText = '';
1424
+
1425
+ function syncOutput() {
1426
+ const el = resultContainer.querySelector('textarea') || resultContainer.querySelector('input');
1427
+ if (!el) return;
1428
+ const val = el.value || '';
1429
+ if (val !== lastText) {
1430
+ lastText = val;
1431
+ outArea.value = val;
1432
+ outArea.scrollTop = outArea.scrollHeight;
1433
+ if (window.__hideLoader && val.trim()) window.__hideLoader();
1434
+ }
1435
+ }
1436
+
1437
+ const observer = new MutationObserver(syncOutput);
1438
+ observer.observe(resultContainer, {childList:true, subtree:true, characterData:true, attributes:true});
1439
+ setInterval(syncOutput, 500);
1440
+ }
1441
+ watchOutputs();
1442
+ }
1443
+ """
1444
+
1445
+ OUTPOST_LOGO_SVG = """
1446
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1447
+ <path d="M12 2l8 4v6c0 5-3.5 8.8-8 10-4.5-1.2-8-5-8-10V6l8-4Zm0 3.2L7 7.7v4.2c0 3.4 2.2 6 5 7 2.8-1 5-3.6 5-7V7.7l-5-2.5Z" fill="white"/>
1448
+ <circle cx="12" cy="12" r="2.2" fill="white"/>
1449
+ </svg>
1450
+ """
1451
+
1452
+ UPLOAD_PREVIEW_SVG = """
1453
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1454
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#0000CD" stroke-width="2" stroke-dasharray="4 3"/>
1455
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(0,0,205,0.15)" stroke="#0000CD" stroke-width="1.5"/>
1456
+ <circle cx="28" cy="30" r="6" fill="rgba(0,0,205,0.2)" stroke="#0000CD" stroke-width="1.5"/>
1457
+ </svg>
1458
+ """
1459
+
1460
+ COPY_SVG = """<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4C2.9 1 2 1.9 2 3v12h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>"""
1461
+ SAVE_SVG = """<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7l-4-4zM7 5h8v4H7V5zm12 14H5v-6h14v6z"/></svg>"""
1462
+
1463
+ MODEL_TABS_HTML = "".join([
1464
+ f'<button class="model-tab{" active" if m == "Qwen3-VL-4B-Instruct" else ""}" data-model="{m}"><span class="model-tab-label">{m}</span></button>'
1465
+ for m in MODEL_CHOICES
1466
+ ])
1467
+
1468
+ MODE_TABS_HTML = """
1469
+ <button class="mode-tab active" data-mode="image">Image Inference</button>
1470
+ <button class="mode-tab" data-mode="pdf">PDF Inference</button>
1471
+ <button class="mode-tab" data-mode="caption">Long Caption</button>
1472
+ <button class="mode-tab" data-mode="gif">GIF Inference</button>
1473
+ """
1474
+
1475
+ with gr.Blocks() as demo:
1476
+ hidden_mode_name = gr.Textbox(value="image", elem_id="hidden-mode-name", elem_classes="hidden-input", container=False)
1477
+ hidden_image_b64 = gr.Textbox(value="", elem_id="hidden-image-b64", elem_classes="hidden-input", container=False)
1478
+ hidden_pdf_b64 = gr.Textbox(value="", elem_id="hidden-pdf-b64", elem_classes="hidden-input", container=False)
1479
+ hidden_gif_b64 = gr.Textbox(value="", elem_id="hidden-gif-b64", elem_classes="hidden-input", container=False)
1480
+ hidden_pdf_state = gr.Textbox(value="", elem_id="hidden-pdf-state", elem_classes="hidden-input", container=False)
1481
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
1482
+ hidden_model_name = gr.Textbox(value="Qwen3-VL-4B-Instruct", elem_id="hidden-model-name", elem_classes="hidden-input", container=False)
1483
+
1484
+ max_new_tokens = gr.Slider(minimum=1, maximum=MAX_MAX_NEW_TOKENS, step=1, value=DEFAULT_MAX_NEW_TOKENS, elem_id="gradio-max-new-tokens", elem_classes="hidden-input", container=False)
1485
+ temperature = gr.Slider(minimum=0.1, maximum=4.0, step=0.1, value=0.6, elem_id="gradio-temperature", elem_classes="hidden-input", container=False)
1486
+ top_p = gr.Slider(minimum=0.05, maximum=1.0, step=0.05, value=0.9, elem_id="gradio-top-p", elem_classes="hidden-input", container=False)
1487
+ top_k = gr.Slider(minimum=1, maximum=1000, step=1, value=50, elem_id="gradio-top-k", elem_classes="hidden-input", container=False)
1488
+ repetition_penalty = gr.Slider(minimum=1.0, maximum=2.0, step=0.05, value=1.2, elem_id="gradio-repetition-penalty", elem_classes="hidden-input", container=False)
1489
+ gpu_duration_state = gr.Number(value=60, elem_id="gradio-gpu-duration", elem_classes="hidden-input", container=False)
1490
+
1491
+ result = gr.Textbox(value="", elem_id="gradio-result", elem_classes="hidden-input", container=False)
1492
+
1493
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1494
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1495
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
1496
+
1497
+ gr.HTML(f"""
1498
+ <div class="app-shell">
1499
+ <div class="app-header">
1500
+ <div class="app-header-left">
1501
+ <div class="app-logo">{OUTPOST_LOGO_SVG}</div>
1502
+ <span class="app-title">Qwen3-VL-Outpost</span>
1503
+ <span class="app-badge">multimodal lab</span>
1504
+ <span class="app-badge fast">Image + PDF + GIF</span>
1505
+ </div>
1506
+ </div>
1507
+
1508
+ <div class="model-tabs-bar">
1509
+ {MODEL_TABS_HTML}
1510
+ </div>
1511
+
1512
+ <div class="mode-tabs-bar">
1513
+ {MODE_TABS_HTML}
1514
+ </div>
1515
+
1516
+ <div class="app-main-row">
1517
+ <div class="app-main-left">
1518
+ <div id="media-drop-zone">
1519
+ <div id="upload-prompt" class="upload-prompt-modern">
1520
+ <div id="upload-click-area" class="upload-click-area">
1521
+ {UPLOAD_PREVIEW_SVG}
1522
+ <span id="upload-main-text" class="upload-main-text">Click or drag an image here</span>
1523
+ <span id="upload-sub-text" class="upload-sub-text">Upload one image, PDF, or GIF for multimodal inference</span>
1524
+ </div>
1525
+ </div>
1526
+
1527
+ <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
1528
+
1529
+ <div id="single-preview-wrap" class="single-preview-wrap">
1530
+ <div class="single-preview-card">
1531
+ <img id="single-preview-img" src="" alt="Preview" style="display:none;">
1532
+ <iframe id="single-preview-pdf" style="display:none;"></iframe>
1533
+ <div id="pdf-nav" class="pdf-nav-wrap">
1534
+ <button id="pdf-prev-btn" class="pdf-nav-btn">◀</button>
1535
+ <span id="pdf-page-info" class="pdf-page-indicator">No file loaded</span>
1536
+ <button id="pdf-next-btn" class="pdf-nav-btn">▶</button>
1537
+ </div>
1538
+ <div class="preview-overlay-actions">
1539
+ <button id="preview-upload-btn" class="preview-action-btn" title="Replace">Upload</button>
1540
+ <button id="preview-clear-btn" class="preview-action-btn" title="Clear">Clear</button>
1541
+ </div>
1542
+ </div>
1543
+ </div>
1544
+ </div>
1545
+
1546
+ <div class="hint-bar">
1547
+ <b>Modes:</b> Image, PDF, Long Caption, GIF &nbsp;&middot;&nbsp;
1548
+ <b>Model:</b> Switch between Qwen VL variants &nbsp;&middot;&nbsp;
1549
+ <kbd>Clear</kbd> removes the current media
1550
+ </div>
1551
+
1552
+ <div class="examples-section">
1553
+ <div class="examples-title">Quick Examples</div>
1554
+ <div class="examples-scroll">
1555
+ {EXAMPLE_CARDS_HTML}
1556
+ </div>
1557
+ </div>
1558
+ </div>
1559
+
1560
+ <div class="app-main-right">
1561
+ <div class="panel-card">
1562
+ <div id="instruction-title" class="panel-card-title">Vision Instruction</div>
1563
+ <div class="panel-card-body">
1564
+ <label id="query-label" class="modern-label" for="custom-query-input">Query Input</label>
1565
+ <textarea id="custom-query-input" class="modern-textarea" rows="4" placeholder="e.g., perform OCR, summarize the PDF, describe the GIF, generate a long caption..."></textarea>
1566
+ </div>
1567
+ </div>
1568
+
1569
+ <div style="padding:12px 20px;">
1570
+ <button id="custom-run-btn" class="btn-run">
1571
+ <span id="run-btn-label">Run Inference</span>
1572
+ </button>
1573
+ </div>
1574
+
1575
+ <div class="output-frame">
1576
+ <div class="out-title">
1577
+ <span id="output-title-label">Raw Output Stream</span>
1578
+ <div class="out-title-right">
1579
+ <button id="copy-output-btn" class="out-action-btn" title="Copy">{COPY_SVG} Copy</button>
1580
+ <button id="save-output-btn" class="out-action-btn" title="Save">{SAVE_SVG} Save File</button>
1581
+ </div>
1582
+ </div>
1583
+ <div class="out-body">
1584
+ <div class="modern-loader" id="output-loader">
1585
+ <div class="loader-spinner"></div>
1586
+ <div class="loader-text">Running inference...</div>
1587
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1588
+ </div>
1589
+ <div class="output-scroll-wrap">
1590
+ <textarea id="custom-output-textarea" class="output-textarea" placeholder="Raw output will appear here..." readonly></textarea>
1591
+ </div>
1592
+ </div>
1593
+ </div>
1594
+
1595
+ <div class="settings-group">
1596
+ <div class="settings-group-title">Advanced Settings</div>
1597
+ <div class="settings-group-body">
1598
+ <div class="slider-row">
1599
+ <label>Max new tokens</label>
1600
+ <input type="range" id="custom-max-new-tokens" min="1" max="{MAX_MAX_NEW_TOKENS}" step="1" value="{DEFAULT_MAX_NEW_TOKENS}">
1601
+ <span class="slider-val" id="custom-max-new-tokens-val">{DEFAULT_MAX_NEW_TOKENS}</span>
1602
+ </div>
1603
+ <div class="slider-row">
1604
+ <label>Temperature</label>
1605
+ <input type="range" id="custom-temperature" min="0.1" max="4.0" step="0.1" value="0.6">
1606
+ <span class="slider-val" id="custom-temperature-val">0.6</span>
1607
+ </div>
1608
+ <div class="slider-row">
1609
+ <label>Top-p</label>
1610
+ <input type="range" id="custom-top-p" min="0.05" max="1.0" step="0.05" value="0.9">
1611
+ <span class="slider-val" id="custom-top-p-val">0.9</span>
1612
+ </div>
1613
+ <div class="slider-row">
1614
+ <label>Top-k</label>
1615
+ <input type="range" id="custom-top-k" min="1" max="1000" step="1" value="50">
1616
+ <span class="slider-val" id="custom-top-k-val">50</span>
1617
+ </div>
1618
+ <div class="slider-row">
1619
+ <label>Repetition penalty</label>
1620
+ <input type="range" id="custom-repetition-penalty" min="1.0" max="2.0" step="0.05" value="1.2">
1621
+ <span class="slider-val" id="custom-repetition-penalty-val">1.2</span>
1622
+ </div>
1623
+ <div class="slider-row">
1624
+ <label>GPU Duration (seconds)</label>
1625
+ <input type="range" id="custom-gpu-duration" min="60" max="300" step="30" value="60">
1626
+ <span class="slider-val" id="custom-gpu-duration-val">60</span>
1627
+ </div>
1628
+ </div>
1629
+ </div>
1630
+ </div>
1631
+ </div>
1632
+
1633
+ <div class="exp-note">
1634
+ Experimental Qwen VL workspace
1635
+ </div>
1636
+
1637
+ <div class="app-statusbar">
1638
+ <div class="sb-section" id="sb-media-status">No image uploaded</div>
1639
+ <div class="sb-section sb-fixed" id="sb-run-state">Ready</div>
1640
+ </div>
1641
+ </div>
1642
+ """)
1643
+
1644
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1645
+
1646
+ demo.load(fn=noop, inputs=None, outputs=None, js=gallery_js)
1647
+ demo.load(fn=noop, inputs=None, outputs=None, js=wire_outputs_js)
1648
+
1649
+ run_btn.click(
1650
+ fn=run_router,
1651
+ inputs=[
1652
+ hidden_mode_name,
1653
+ hidden_model_name,
1654
+ prompt,
1655
+ hidden_image_b64,
1656
+ hidden_pdf_b64,
1657
+ hidden_gif_b64,
1658
+ hidden_pdf_state,
1659
+ max_new_tokens,
1660
+ temperature,
1661
+ top_p,
1662
+ top_k,
1663
+ repetition_penalty,
1664
+ gpu_duration_state,
1665
+ ],
1666
+ outputs=[result],
1667
+ js=r"""(mode, model, p, img, pdf, gif, pdfs, mnt, t, tp, tk, rp, gd) => {
1668
+ const modelEl = document.querySelector('.model-tab.active');
1669
+ const modeEl = document.querySelector('.mode-tab.active');
1670
+ const modelVal = modelEl ? modelEl.getAttribute('data-model') : model;
1671
+ const modeVal = modeEl ? modeEl.getAttribute('data-mode') : mode;
1672
+ const promptEl = document.getElementById('custom-query-input');
1673
+ const promptVal = promptEl ? promptEl.value : p;
1674
+
1675
+ let imgVal = img, pdfVal = pdf, gifVal = gif;
1676
+ const imgContainer = document.getElementById('hidden-image-b64');
1677
+ const pdfContainer = document.getElementById('hidden-pdf-b64');
1678
+ const gifContainer = document.getElementById('hidden-gif-b64');
1679
+
1680
+ if (imgContainer) {
1681
+ const inner = imgContainer.querySelector('textarea, input');
1682
+ if (inner) imgVal = inner.value;
1683
+ }
1684
+ if (pdfContainer) {
1685
+ const inner = pdfContainer.querySelector('textarea, input');
1686
+ if (inner) pdfVal = inner.value;
1687
+ }
1688
+ if (gifContainer) {
1689
+ const inner = gifContainer.querySelector('textarea, input');
1690
+ if (inner) gifVal = inner.value;
1691
+ }
1692
+
1693
+ return [modeVal, modelVal, promptVal, imgVal, pdfVal, gifVal, pdfs, mnt, t, tp, tk, rp, gd];
1694
+ }""",
1695
  )
1696
 
1697
+ example_load_btn.click(
1698
+ fn=load_example_data,
1699
+ inputs=[example_idx],
1700
+ outputs=[example_result],
1701
+ queue=False,
1702
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1703
 
1704
  if __name__ == "__main__":
1705
+ demo.queue(max_size=50).launch(
1706
+ css=css,
1707
+ mcp_server=True,
1708
+ ssr_mode=False,
1709
+ show_error=True,
1710
+ allowed_paths=["examples"],
1711
+ )