prithivMLmods commited on
Commit
8b09592
·
verified ·
1 Parent(s): 9c7c8a3

update app

Browse files
Files changed (1) hide show
  1. app.py +1281 -417
app.py CHANGED
@@ -1,19 +1,16 @@
 
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 typing import Iterable
9
 
10
  import gradio as gr
11
  import spaces
12
  import torch
13
- import numpy as np
14
  from PIL import Image
15
- import cv2
16
- import requests
17
 
18
  from transformers import (
19
  Qwen2VLForConditionalGeneration,
@@ -23,126 +20,17 @@ from transformers import (
23
  AutoModel,
24
  AutoTokenizer,
25
  )
26
- from transformers.image_utils import load_image
27
- from gradio.themes import Soft
28
- from gradio.themes.utils import colors, fonts, sizes
29
-
30
- colors.steel_blue = colors.Color(
31
- name="steel_blue",
32
- c50="#EBF3F8",
33
- c100="#D3E5F0",
34
- c200="#A8CCE1",
35
- c300="#7DB3D2",
36
- c400="#529AC3",
37
- c500="#4682B4",
38
- c600="#3E72A0",
39
- c700="#36638C",
40
- c800="#2E5378",
41
- c900="#264364",
42
- c950="#1E3450",
43
- )
44
-
45
- class SteelBlueTheme(Soft):
46
- def __init__(
47
- self,
48
- *,
49
- primary_hue: colors.Color | str = colors.gray,
50
- secondary_hue: colors.Color | str = colors.steel_blue,
51
- neutral_hue: colors.Color | str = colors.slate,
52
- text_size: sizes.Size | str = sizes.text_lg,
53
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
54
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
55
- ),
56
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
57
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
58
- ),
59
- ):
60
- super().__init__(
61
- primary_hue=primary_hue,
62
- secondary_hue=secondary_hue,
63
- neutral_hue=neutral_hue,
64
- text_size=text_size,
65
- font=font,
66
- font_mono=font_mono,
67
- )
68
- super().set(
69
- background_fill_primary="*primary_50",
70
- background_fill_primary_dark="*primary_900",
71
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
72
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
73
- button_primary_text_color="white",
74
- button_primary_text_color_hover="white",
75
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
76
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
77
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
78
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
79
- button_secondary_text_color="black",
80
- button_secondary_text_color_hover="white",
81
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
82
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
83
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
84
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
85
- slider_color="*secondary_500",
86
- slider_color_dark="*secondary_600",
87
- block_title_text_weight="600",
88
- block_border_width="3px",
89
- block_shadow="*shadow_drop_lg",
90
- button_primary_shadow="*shadow_drop_lg",
91
- button_large_padding="11px",
92
- color_accent_soft="*primary_100",
93
- block_label_background_fill="*primary_200",
94
- )
95
-
96
- steel_blue_theme = SteelBlueTheme()
97
-
98
- css = """
99
- #main-title h1 {
100
- font-size: 2.3em !important;
101
- }
102
- #output-title h2 {
103
- font-size: 2.1em !important;
104
- }
105
-
106
- /* RadioAnimated Styles */
107
- .ra-wrap{ width: fit-content; }
108
- .ra-inner{
109
- position: relative; display: inline-flex; align-items: center; gap: 0; padding: 6px;
110
- background: var(--neutral-200); border-radius: 9999px; overflow: hidden;
111
- }
112
- .ra-input{ display: none; }
113
- .ra-label{
114
- position: relative; z-index: 2; padding: 8px 16px;
115
- font-family: inherit; font-size: 14px; font-weight: 600;
116
- color: var(--neutral-500); cursor: pointer; transition: color 0.2s; white-space: nowrap;
117
- }
118
- .ra-highlight{
119
- position: absolute; z-index: 1; top: 6px; left: 6px;
120
- height: calc(100% - 12px); border-radius: 9999px;
121
- background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
122
- transition: transform 0.2s, width 0.2s;
123
- }
124
- .ra-input:checked + .ra-label{ color: black; }
125
-
126
- /* Dark mode adjustments for Radio */
127
- .dark .ra-inner { background: var(--neutral-800); }
128
- .dark .ra-label { color: var(--neutral-400); }
129
- .dark .ra-highlight { background: var(--neutral-600); }
130
- .dark .ra-input:checked + .ra-label { color: white; }
131
-
132
- #gpu-duration-container {
133
- padding: 10px;
134
- border-radius: 8px;
135
- background: var(--background-fill-secondary);
136
- border: 1px solid var(--border-color-primary);
137
- margin-top: 10px;
138
- }
139
- """
140
 
 
 
 
141
  MAX_MAX_NEW_TOKENS = 4096
142
  DEFAULT_MAX_NEW_TOKENS = 1024
143
  MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "4096"))
144
 
145
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
 
146
 
147
  print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
148
  print("torch.__version__ =", torch.__version__)
@@ -152,357 +40,1333 @@ print("cuda device count:", torch.cuda.device_count())
152
  if torch.cuda.is_available():
153
  print("current device:", torch.cuda.current_device())
154
  print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
155
-
156
  print("Using device:", device)
157
 
158
- # --- RadioAnimated Component ---
159
- class RadioAnimated(gr.HTML):
160
- def __init__(self, choices, value=None, **kwargs):
161
- if not choices or len(choices) < 2:
162
- raise ValueError("RadioAnimated requires at least 2 choices.")
163
- if value is None:
164
- value = choices[0]
165
-
166
- uid = uuid.uuid4().hex[:8]
167
- group_name = f"ra-{uid}"
168
-
169
- inputs_html = "\n".join(
170
- f"""
171
- <input class="ra-input" type="radio" name="{group_name}" id="{group_name}-{i}" value="{c}">
172
- <label class="ra-label" for="{group_name}-{i}">{c}</label>
173
- """
174
- for i, c in enumerate(choices)
175
- )
176
-
177
- html_template = f"""
178
- <div class="ra-wrap" data-ra="{uid}">
179
- <div class="ra-inner">
180
- <div class="ra-highlight"></div>
181
- {inputs_html}
182
- </div>
183
- </div>
184
- """
185
-
186
- js_on_load = r"""
187
- (() => {
188
- const wrap = element.querySelector('.ra-wrap');
189
- const inner = element.querySelector('.ra-inner');
190
- const highlight = element.querySelector('.ra-highlight');
191
- const inputs = Array.from(element.querySelectorAll('.ra-input'));
192
-
193
- if (!inputs.length) return;
194
-
195
- const choices = inputs.map(i => i.value);
196
-
197
- function setHighlightByIndex(idx) {
198
- const n = choices.length;
199
- const pct = 100 / n;
200
- highlight.style.width = `calc(${pct}% - 6px)`;
201
- highlight.style.transform = `translateX(${idx * 100}%)`;
202
- }
203
-
204
- function setCheckedByValue(val, shouldTrigger=false) {
205
- const idx = Math.max(0, choices.indexOf(val));
206
- inputs.forEach((inp, i) => { inp.checked = (i === idx); });
207
- setHighlightByIndex(idx);
208
-
209
- props.value = choices[idx];
210
- if (shouldTrigger) trigger('change', props.value);
211
- }
212
-
213
- setCheckedByValue(props.value ?? choices[0], false);
214
-
215
- inputs.forEach((inp) => {
216
- inp.addEventListener('change', () => {
217
- setCheckedByValue(inp.value, true);
218
- });
219
- });
220
- })();
221
- """
222
-
223
- super().__init__(
224
- value=value,
225
- html_template=html_template,
226
- js_on_load=js_on_load,
227
- **kwargs
228
- )
229
-
230
- def apply_gpu_duration(val: str):
231
- return int(val)
232
-
233
  MODEL_ID_X = "Senqiao/VisionThink-Efficient"
234
  processor_x = AutoProcessor.from_pretrained(MODEL_ID_X, trust_remote_code=True, use_fast=False)
235
  model_x = Qwen2_5_VLForConditionalGeneration.from_pretrained(
236
  MODEL_ID_X,
237
- attn_implementation="kernels-community/flash-attn2",
238
  trust_remote_code=True,
239
- torch_dtype=torch.float16
240
  ).to(device).eval()
241
 
242
  MODEL_ID_T = "scb10x/typhoon-ocr-3b"
243
  processor_t = AutoProcessor.from_pretrained(MODEL_ID_T, trust_remote_code=True, use_fast=False)
244
  model_t = Qwen2_5_VLForConditionalGeneration.from_pretrained(
245
  MODEL_ID_T,
246
- attn_implementation="kernels-community/flash-attn2",
247
  trust_remote_code=True,
248
- torch_dtype=torch.float16
249
  ).to(device).eval()
250
 
251
  MODEL_ID_O = "allenai/olmOCR-7B-0225-preview"
252
  processor_o = AutoProcessor.from_pretrained(MODEL_ID_O, trust_remote_code=True, use_fast=False)
253
  model_o = Qwen2VLForConditionalGeneration.from_pretrained(
254
  MODEL_ID_O,
255
- attn_implementation="kernels-community/flash-attn2",
256
  trust_remote_code=True,
257
- torch_dtype=torch.float16
258
  ).to(device).eval()
259
 
260
  MODEL_ID_J = "prithivMLmods/Lumian-VLR-7B-Thinking"
261
  SUBFOLDER = "think-preview"
262
- processor_j = AutoProcessor.from_pretrained(MODEL_ID_J, trust_remote_code=True, subfolder=SUBFOLDER, use_fast=False)
 
 
263
  model_j = Qwen2_5_VLForConditionalGeneration.from_pretrained(
264
  MODEL_ID_J,
265
- attn_implementation="kernels-community/flash-attn2",
266
  trust_remote_code=True,
267
  subfolder=SUBFOLDER,
268
- torch_dtype=torch.float16
269
  ).to(device).eval()
270
 
271
- MODEL_ID_V4 = 'openbmb/MiniCPM-V-4'
272
  model_v4 = AutoModel.from_pretrained(
273
  MODEL_ID_V4,
274
- attn_implementation="kernels-community/flash-attn2",
275
  trust_remote_code=True,
276
- torch_dtype=torch.bfloat16,
277
  ).eval().to(device)
278
  tokenizer_v4 = AutoTokenizer.from_pretrained(MODEL_ID_V4, trust_remote_code=True, use_fast=False)
279
 
280
  MODELS = {
 
281
  "VisionThink-Efficient": (processor_x, model_x),
282
  "Typhoon-OCR-3B": (processor_t, model_t),
283
  "olmOCR-7B-0225-preview": (processor_o, model_o),
284
- "Lumian-VLR-7B-Thinking": (processor_j, model_j),
285
  }
286
 
287
- def downsample_video(video_path):
288
- vidcap = cv2.VideoCapture(video_path)
289
- total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
290
- fps = vidcap.get(cv2.CAP_PROP_FPS)
291
- frames = []
292
- frame_indices = np.linspace(0, total_frames - 1, min(total_frames, 10), dtype=int)
293
- for i in frame_indices:
294
- vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
295
- success, image = vidcap.read()
296
- if success:
297
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
298
- pil_image = Image.fromarray(image)
299
- timestamp = round(i / fps, 2)
300
- frames.append((pil_image, timestamp))
301
- vidcap.release()
302
- return frames
303
-
304
- # --- GPU Timeout Calculation Functions ---
305
- def calc_timeout_image(model_name: str, text: str, image: Image.Image,
306
- max_new_tokens: int, temperature: float, top_p: float,
307
- top_k: int, repetition_penalty: float, gpu_timeout: int):
308
- """Calculate GPU timeout duration for image inference."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  try:
310
- return int(gpu_timeout)
311
- except:
312
- return 60
 
 
 
313
 
314
- def calc_timeout_video(model_name: str, text: str, video_path: str,
315
- max_new_tokens: int, temperature: float, top_p: float,
316
- top_k: int, repetition_penalty: float, gpu_timeout: int):
317
- """Calculate GPU timeout duration for video inference."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  try:
319
  return int(gpu_timeout)
320
- except:
321
  return 60
322
 
 
 
 
323
  @spaces.GPU(duration=calc_timeout_image)
324
- def generate_image(model_name: str, text: str, image: Image.Image,
325
- max_new_tokens: int = 1024,
326
- temperature: float = 0.6,
327
- top_p: float = 0.9,
328
- top_k: int = 50,
329
- repetition_penalty: float = 1.2,
330
- gpu_timeout: int = 60):
331
- if image is None:
332
- yield "Please upload an image.", "Please upload an image."
333
- return
334
-
335
- if model_name == "openbmb/MiniCPM-V-4":
336
- msgs = [{'role': 'user', 'content': [image, text]}]
337
- try:
338
- answer = model_v4.chat(
339
- image=image.convert('RGB'), msgs=msgs, tokenizer=tokenizer_v4,
340
- max_new_tokens=max_new_tokens, temperature=temperature,
341
- top_p=top_p, repetition_penalty=repetition_penalty,
342
- )
343
- yield answer, answer
344
- except Exception as e:
345
- yield f"Error: {e}", f"Error: {e}"
346
- return
347
-
348
- if model_name not in MODELS:
349
- yield "Invalid model selected.", "Invalid model selected."
350
- return
351
- processor, model = MODELS[model_name]
352
-
353
- messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}]
354
- prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
355
- inputs = processor(
356
- text=[prompt_full], images=[image], return_tensors="pt", padding=True).to(device)
357
- streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
358
- generation_kwargs = {**inputs, "streamer": streamer, "max_new_tokens": max_new_tokens}
359
- thread = Thread(target=model.generate, kwargs=generation_kwargs)
360
- thread.start()
361
- buffer = ""
362
- for new_text in streamer:
363
- buffer += new_text
364
- time.sleep(0.01)
365
- yield buffer, buffer
366
-
367
- @spaces.GPU(duration=calc_timeout_video)
368
- def generate_video(model_name: str, text: str, video_path: str,
369
- max_new_tokens: int = 1024,
370
- temperature: float = 0.6,
371
- top_p: float = 0.9,
372
- top_k: int = 50,
373
- repetition_penalty: float = 1.2,
374
- gpu_timeout: int = 90):
375
- if video_path is None:
376
- yield "Please upload a video.", "Please upload a video."
377
- return
378
-
379
- frames_with_ts = downsample_video(video_path)
380
- if not frames_with_ts:
381
- yield "Could not process video.", "Could not process video."
382
- return
383
-
384
- if model_name == "openbmb/MiniCPM-V-4":
385
- images = [frame for frame, ts in frames_with_ts]
386
- content = [text] + images
387
- msgs = [{'role': 'user', 'content': content}]
388
- try:
389
- answer = model_v4.chat(
390
- image=images[0].convert('RGB'), msgs=msgs, tokenizer=tokenizer_v4,
391
- max_new_tokens=max_new_tokens, temperature=temperature,
392
- top_p=top_p, repetition_penalty=repetition_penalty,
393
- )
394
- yield answer, answer
395
- except Exception as e:
396
- yield f"Error: {e}", f"Error: {e}"
397
- return
398
-
399
- if model_name not in MODELS:
400
- yield "Invalid model selected.", "Invalid model selected."
401
- return
402
- processor, model = MODELS[model_name]
403
-
404
- messages = [{"role": "user", "content": [{"type": "text", "text": text}]}]
405
- images_for_processor = []
406
- for frame, timestamp in frames_with_ts:
407
- messages[0]["content"].insert(0, {"type": "image"})
408
- images_for_processor.append(frame)
409
-
410
- prompt_full = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
411
- inputs = processor(
412
- text=[prompt_full], images=images_for_processor, return_tensors="pt", padding=True).to(device)
413
- streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
414
- generation_kwargs = {
415
- **inputs, "streamer": streamer, "max_new_tokens": max_new_tokens,
416
- "do_sample": True, "temperature": temperature, "top_p": top_p,
417
- "top_k": top_k, "repetition_penalty": repetition_penalty,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
- thread = Thread(target=model.generate, kwargs=generation_kwargs)
420
- thread.start()
421
- buffer = ""
422
- for new_text in streamer:
423
- buffer += new_text
424
- buffer = buffer.replace("<|im_end|>", "")
425
- time.sleep(0.01)
426
- yield buffer, buffer
427
-
428
- image_examples = [
429
- ["Describe the safety measures in the image. Conclude (Safe / Unsafe)..", "images/5.jpg"],
430
- ["Convert this page to doc [markdown] precisely.", "images/3.png"],
431
- ["Convert this page to doc [markdown] precisely.", "images/4.png"],
432
- ["Explain the creativity in the image.", "images/6.jpg"],
433
- ["Convert this page to doc [markdown] precisely.", "images/1.png"],
434
- ["Convert chart to OTSL.", "images/2.png"]
435
- ]
436
 
437
- video_examples = [
438
- ["Explain the video in detail.", "videos/2.mp4"],
439
- ["Explain the ad in detail.", "videos/1.mp4"]
440
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  with gr.Blocks() as demo:
443
- gr.Markdown("# **Multimodal VLM Thinking**", elem_id="main-title")
444
- with gr.Row():
445
- with gr.Column(scale=2):
446
- with gr.Tabs():
447
- with gr.TabItem("Image Inference"):
448
- image_query = gr.Textbox(label="Query Input", placeholder="Enter your query here...")
449
- image_upload = gr.Image(type="pil", label="Image", height=290)
450
- image_submit = gr.Button("Submit", variant="primary")
451
- gr.Examples(examples=image_examples, inputs=[image_query, image_upload])
452
- with gr.TabItem("Video Inference"):
453
- video_query = gr.Textbox(label="Query Input", placeholder="Enter your query here...")
454
- video_upload = gr.Video(label="Video", height=290)
455
- video_submit = gr.Button("Submit", variant="primary")
456
- gr.Examples(examples=video_examples, inputs=[video_query, video_upload])
457
-
458
- with gr.Accordion("Advanced options", open=False):
459
- max_new_tokens = gr.Slider(label="Max new tokens", minimum=1, maximum=MAX_MAX_NEW_TOKENS, step=1, value=DEFAULT_MAX_NEW_TOKENS)
460
- temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=4.0, step=0.1, value=0.6)
461
- top_p = gr.Slider(label="Top-p (nucleus sampling)", minimum=0.05, maximum=1.0, step=0.05, value=0.9)
462
- top_k = gr.Slider(label="Top-k", minimum=1, maximum=1000, step=1, value=50)
463
- repetition_penalty = gr.Slider(label="Repetition penalty", minimum=1.0, maximum=2.0, step=0.05, value=1.2)
464
-
465
- with gr.Column(scale=3):
466
- gr.Markdown("## Output", elem_id="output-title")
467
- output = gr.Textbox(label="Raw Output Stream", lines=11, interactive=True)
468
- with gr.Accordion("(Result.md)", open=False):
469
- markdown_output = gr.Markdown(label="(Result.Md)")
470
-
471
- model_choice = gr.Radio(
472
- choices=["Lumian-VLR-7B-Thinking", "VisionThink-Efficient", "openbmb/MiniCPM-V-4", "Typhoon-OCR-3B", "olmOCR-7B-0225-preview"],
473
- label="Select Model",
474
- value="Lumian-VLR-7B-Thinking"
475
- )
476
-
477
- with gr.Row(elem_id="gpu-duration-container"):
478
- with gr.Column():
479
- gr.Markdown("**GPU Duration (seconds)**")
480
- radioanimated_gpu_duration = RadioAnimated(
481
- choices=["60", "90", "120", "180", "240", "300"],
482
- value="60",
483
- elem_id="radioanimated_gpu_duration"
484
- )
485
- gpu_duration_state = gr.Number(value=60, visible=False)
486
-
487
- gr.Markdown("*Note: Higher GPU duration allows for longer processing but consumes more GPU quota.*")
488
-
489
- radioanimated_gpu_duration.change(
490
- fn=apply_gpu_duration,
491
- inputs=radioanimated_gpu_duration,
492
- outputs=[gpu_duration_state],
493
- api_visibility="private"
494
- )
 
 
 
 
 
 
 
 
 
495
 
496
- image_submit.click(
497
- fn=generate_image,
498
- inputs=[model_choice, image_query, image_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
499
- outputs=[output, markdown_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  )
501
- video_submit.click(
502
- fn=generate_video,
503
- inputs=[model_choice, video_query, video_upload, max_new_tokens, temperature, top_p, top_k, repetition_penalty, gpu_duration_state],
504
- outputs=[output, markdown_output]
 
 
505
  )
506
 
507
  if __name__ == "__main__":
508
- demo.queue(max_size=50).launch(theme=steel_blue_theme, css=css, mcp_server=True, ssr_mode=False, show_error=True)
 
 
 
 
 
 
 
1
+
2
  import os
3
+ import gc
 
4
  import json
5
+ import base64
6
  import time
7
+ from io import BytesIO
8
  from threading import Thread
 
9
 
10
  import gradio as gr
11
  import spaces
12
  import torch
 
13
  from PIL import Image
 
 
14
 
15
  from transformers import (
16
  Qwen2VLForConditionalGeneration,
 
20
  AutoModel,
21
  AutoTokenizer,
22
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # =========================
25
+ # Config
26
+ # =========================
27
  MAX_MAX_NEW_TOKENS = 4096
28
  DEFAULT_MAX_NEW_TOKENS = 1024
29
  MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "4096"))
30
 
31
+ ACCENT = "#00FF00"
32
+
33
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
34
 
35
  print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
36
  print("torch.__version__ =", torch.__version__)
 
40
  if torch.cuda.is_available():
41
  print("current device:", torch.cuda.current_device())
42
  print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
 
43
  print("Using device:", device)
44
 
45
+ # =========================
46
+ # Models
47
+ # =========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  MODEL_ID_X = "Senqiao/VisionThink-Efficient"
49
  processor_x = AutoProcessor.from_pretrained(MODEL_ID_X, trust_remote_code=True, use_fast=False)
50
  model_x = Qwen2_5_VLForConditionalGeneration.from_pretrained(
51
  MODEL_ID_X,
52
+ attn_implementation="kernels-community/flash-attn2" if torch.cuda.is_available() else "eager",
53
  trust_remote_code=True,
54
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
55
  ).to(device).eval()
56
 
57
  MODEL_ID_T = "scb10x/typhoon-ocr-3b"
58
  processor_t = AutoProcessor.from_pretrained(MODEL_ID_T, trust_remote_code=True, use_fast=False)
59
  model_t = Qwen2_5_VLForConditionalGeneration.from_pretrained(
60
  MODEL_ID_T,
61
+ attn_implementation="kernels-community/flash-attn2" if torch.cuda.is_available() else "eager",
62
  trust_remote_code=True,
63
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
64
  ).to(device).eval()
65
 
66
  MODEL_ID_O = "allenai/olmOCR-7B-0225-preview"
67
  processor_o = AutoProcessor.from_pretrained(MODEL_ID_O, trust_remote_code=True, use_fast=False)
68
  model_o = Qwen2VLForConditionalGeneration.from_pretrained(
69
  MODEL_ID_O,
70
+ attn_implementation="kernels-community/flash-attn2" if torch.cuda.is_available() else "eager",
71
  trust_remote_code=True,
72
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
73
  ).to(device).eval()
74
 
75
  MODEL_ID_J = "prithivMLmods/Lumian-VLR-7B-Thinking"
76
  SUBFOLDER = "think-preview"
77
+ processor_j = AutoProcessor.from_pretrained(
78
+ MODEL_ID_J, trust_remote_code=True, subfolder=SUBFOLDER, use_fast=False
79
+ )
80
  model_j = Qwen2_5_VLForConditionalGeneration.from_pretrained(
81
  MODEL_ID_J,
82
+ attn_implementation="kernels-community/flash-attn2" if torch.cuda.is_available() else "eager",
83
  trust_remote_code=True,
84
  subfolder=SUBFOLDER,
85
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
86
  ).to(device).eval()
87
 
88
+ MODEL_ID_V4 = "openbmb/MiniCPM-V-4"
89
  model_v4 = AutoModel.from_pretrained(
90
  MODEL_ID_V4,
91
+ attn_implementation="kernels-community/flash-attn2" if torch.cuda.is_available() else "eager",
92
  trust_remote_code=True,
93
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
94
  ).eval().to(device)
95
  tokenizer_v4 = AutoTokenizer.from_pretrained(MODEL_ID_V4, trust_remote_code=True, use_fast=False)
96
 
97
  MODELS = {
98
+ "Lumian-VLR-7B-Thinking": (processor_j, model_j),
99
  "VisionThink-Efficient": (processor_x, model_x),
100
  "Typhoon-OCR-3B": (processor_t, model_t),
101
  "olmOCR-7B-0225-preview": (processor_o, model_o),
102
+ "openbmb/MiniCPM-V-4": (None, model_v4),
103
  }
104
 
105
+ MODEL_CHOICES = list(MODELS.keys())
106
+
107
+ # =========================
108
+ # Examples
109
+ # =========================
110
+ image_examples = [
111
+ {"query": "Describe the safety measures in the image. Conclude (Safe / Unsafe).", "image": "images/5.jpg", "model": "Lumian-VLR-7B-Thinking"},
112
+ {"query": "Convert this page to doc [markdown] precisely.", "image": "images/3.png", "model": "Typhoon-OCR-3B"},
113
+ {"query": "Convert this page to doc [markdown] precisely.", "image": "images/4.png", "model": "olmOCR-7B-0225-preview"},
114
+ {"query": "Explain the creativity in the image.", "image": "images/6.jpg", "model": "VisionThink-Efficient"},
115
+ {"query": "Convert this page to doc [markdown] precisely.", "image": "images/1.png", "model": "Typhoon-OCR-3B"},
116
+ {"query": "Convert chart to OTSL.", "image": "images/2.png", "model": "openbmb/MiniCPM-V-4"},
117
+ ]
118
+
119
+ # =========================
120
+ # Helpers
121
+ # =========================
122
+ def pil_to_data_url(img: Image.Image, fmt="PNG"):
123
+ buf = BytesIO()
124
+ img.save(buf, format=fmt)
125
+ data = base64.b64encode(buf.getvalue()).decode()
126
+ mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg"
127
+ return f"data:{mime};base64,{data}"
128
+
129
+ def file_to_data_url(path):
130
+ if not os.path.exists(path):
131
+ return ""
132
+ ext = path.rsplit(".", 1)[-1].lower()
133
+ mime = {
134
+ "jpg": "image/jpeg",
135
+ "jpeg": "image/jpeg",
136
+ "png": "image/png",
137
+ "webp": "image/webp",
138
+ }.get(ext, "image/jpeg")
139
+ with open(path, "rb") as f:
140
+ data = base64.b64encode(f.read()).decode()
141
+ return f"data:{mime};base64,{data}"
142
+
143
+ def make_thumb_b64(path, max_dim=240):
144
  try:
145
+ img = Image.open(path).convert("RGB")
146
+ img.thumbnail((max_dim, max_dim))
147
+ return pil_to_data_url(img, "JPEG")
148
+ except Exception as e:
149
+ print("Thumbnail error:", e)
150
+ return ""
151
 
152
+ def b64_to_pil(b64_str):
153
+ if not b64_str:
154
+ return None
155
+ try:
156
+ if b64_str.startswith("data:"):
157
+ _, data = b64_str.split(",", 1)
158
+ else:
159
+ data = b64_str
160
+ image_data = base64.b64decode(data)
161
+ return Image.open(BytesIO(image_data)).convert("RGB")
162
+ except Exception:
163
+ return None
164
+
165
+ def build_example_cards_html():
166
+ cards = ""
167
+ for i, ex in enumerate(image_examples):
168
+ thumb = make_thumb_b64(ex["image"])
169
+ prompt_short = ex["query"][:72] + ("..." if len(ex["query"]) > 72 else "")
170
+ cards += f"""
171
+ <div class="example-card" data-idx="{i}">
172
+ <div class="example-thumb-wrap">
173
+ {"<img src='" + thumb + "' alt=''>" if thumb else "<div class='example-thumb-placeholder'>Preview</div>"}
174
+ </div>
175
+ <div class="example-meta-row">
176
+ <span class="example-badge">{ex["model"]}</span>
177
+ </div>
178
+ <div class="example-prompt-text">{prompt_short}</div>
179
+ </div>
180
+ """
181
+ return cards
182
+
183
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
184
+
185
+ def load_example_data(idx_str):
186
+ try:
187
+ idx = int(str(idx_str).strip())
188
+ except Exception:
189
+ return gr.update(value=json.dumps({"status": "error", "message": "Invalid example index"}))
190
+
191
+ if idx < 0 or idx >= len(image_examples):
192
+ return gr.update(value=json.dumps({"status": "error", "message": "Example index out of range"}))
193
+
194
+ ex = image_examples[idx]
195
+ img_b64 = file_to_data_url(ex["image"])
196
+ if not img_b64:
197
+ return gr.update(value=json.dumps({"status": "error", "message": "Could not load example image"}))
198
+
199
+ return gr.update(value=json.dumps({
200
+ "status": "ok",
201
+ "query": ex["query"],
202
+ "image": img_b64,
203
+ "model": ex["model"],
204
+ "name": os.path.basename(ex["image"]),
205
+ }))
206
+
207
+ def calc_timeout_image(*args, **kwargs):
208
+ gpu_timeout = kwargs.get("gpu_timeout", None)
209
+ if gpu_timeout is None and args:
210
+ gpu_timeout = args[-1]
211
  try:
212
  return int(gpu_timeout)
213
+ except Exception:
214
  return 60
215
 
216
+ # =========================
217
+ # Inference
218
+ # =========================
219
  @spaces.GPU(duration=calc_timeout_image)
220
+ def generate_image(
221
+ model_name,
222
+ text,
223
+ image,
224
+ max_new_tokens,
225
+ temperature,
226
+ top_p,
227
+ top_k,
228
+ repetition_penalty,
229
+ gpu_timeout=60
230
+ ):
231
+ try:
232
+ if not model_name or model_name not in MODELS:
233
+ yield "[ERROR] Please select a valid model."
234
+ return
235
+ if image is None:
236
+ yield "[ERROR] Please upload an image."
237
+ return
238
+ if not text or not str(text).strip():
239
+ yield "[ERROR] Please enter your query."
240
+ return
241
+ if len(str(text)) > MAX_INPUT_TOKEN_LENGTH * 8:
242
+ yield "[ERROR] Query is too long. Please shorten your input."
243
+ return
244
+
245
+ image = image.convert("RGB")
246
+
247
+ if model_name == "openbmb/MiniCPM-V-4":
248
+ try:
249
+ msgs = [{"role": "user", "content": [image, text]}]
250
+ answer = model_v4.chat(
251
+ image=image,
252
+ msgs=msgs,
253
+ tokenizer=tokenizer_v4,
254
+ max_new_tokens=int(max_new_tokens),
255
+ temperature=float(temperature),
256
+ top_p=float(top_p),
257
+ repetition_penalty=float(repetition_penalty),
258
+ )
259
+ yield answer
260
+ except Exception as e:
261
+ yield f"[ERROR] Inference failed: {str(e)}"
262
+ return
263
+
264
+ processor, model = MODELS[model_name]
265
+
266
+ messages = [{
267
+ "role": "user",
268
+ "content": [
269
+ {"type": "image"},
270
+ {"type": "text", "text": text},
271
+ ]
272
+ }]
273
+
274
+ prompt_full = processor.apply_chat_template(
275
+ messages,
276
+ tokenize=False,
277
+ add_generation_prompt=True
278
+ )
279
+
280
+ inputs = processor(
281
+ text=[prompt_full],
282
+ images=[image],
283
+ return_tensors="pt",
284
+ padding=True,
285
+ truncation=True,
286
+ max_length=MAX_INPUT_TOKEN_LENGTH
287
+ ).to(device)
288
+
289
+ streamer = TextIteratorStreamer(
290
+ processor.tokenizer if hasattr(processor, "tokenizer") else processor,
291
+ skip_prompt=True,
292
+ skip_special_tokens=True
293
+ )
294
+
295
+ generation_error = {"error": None}
296
+
297
+ generation_kwargs = {
298
+ **inputs,
299
+ "streamer": streamer,
300
+ "max_new_tokens": int(max_new_tokens),
301
+ "do_sample": True,
302
+ "temperature": float(temperature),
303
+ "top_p": float(top_p),
304
+ "top_k": int(top_k),
305
+ "repetition_penalty": float(repetition_penalty),
306
+ }
307
+
308
+ def _run_generation():
309
+ try:
310
+ model.generate(**generation_kwargs)
311
+ except Exception as e:
312
+ generation_error["error"] = e
313
+ try:
314
+ streamer.end()
315
+ except Exception:
316
+ pass
317
+
318
+ thread = Thread(target=_run_generation, daemon=True)
319
+ thread.start()
320
+
321
+ buffer = ""
322
+ for new_text in streamer:
323
+ buffer += new_text.replace("<|im_end|>", "")
324
+ time.sleep(0.01)
325
+ yield buffer
326
+
327
+ thread.join(timeout=1.0)
328
+
329
+ if generation_error["error"] is not None:
330
+ err_msg = f"[ERROR] Inference failed: {str(generation_error['error'])}"
331
+ if buffer.strip():
332
+ yield buffer + "\n\n" + err_msg
333
+ else:
334
+ yield err_msg
335
+ return
336
+
337
+ if not buffer.strip():
338
+ yield "[ERROR] No output was generated."
339
+
340
+ except Exception as e:
341
+ yield f"[ERROR] {str(e)}"
342
+ finally:
343
+ gc.collect()
344
+ if torch.cuda.is_available():
345
+ torch.cuda.empty_cache()
346
+
347
+ def run_image(model_name, text, image_b64, max_new_tokens_v, temperature_v, top_p_v, top_k_v, repetition_penalty_v, gpu_timeout_v):
348
+ try:
349
+ image = b64_to_pil(image_b64)
350
+ yield from generate_image(
351
+ model_name=model_name,
352
+ text=text,
353
+ image=image,
354
+ max_new_tokens=max_new_tokens_v,
355
+ temperature=temperature_v,
356
+ top_p=top_p_v,
357
+ top_k=top_k_v,
358
+ repetition_penalty=repetition_penalty_v,
359
+ gpu_timeout=gpu_timeout_v,
360
+ )
361
+ except Exception as e:
362
+ yield f"[ERROR] {str(e)}"
363
+
364
+ def noop():
365
+ return None
366
+
367
+ # =========================
368
+ # SVGs
369
+ # =========================
370
+ THUNDER_SVG = f"""
371
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
372
+ <path fill="white" d="M13.2 2L5 13h5l-1.2 9L19 10h-5l-.8-8Z"/>
373
+ </svg>
374
+ """
375
+
376
+ UPLOAD_PREVIEW_SVG = f"""
377
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
378
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="{ACCENT}" stroke-width="2" stroke-dasharray="4 3"/>
379
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(0,255,0,0.14)" stroke="{ACCENT}" stroke-width="1.5"/>
380
+ <circle cx="28" cy="30" r="6" fill="rgba(0,255,0,0.2)" stroke="{ACCENT}" stroke-width="1.5"/>
381
+ </svg>
382
+ """
383
+
384
+ COPY_SVG = f"""<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="{ACCENT}" 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>"""
385
+ SAVE_SVG = f"""<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="{ACCENT}" 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>"""
386
+
387
+ MODEL_TABS_HTML = "".join([
388
+ f'<button class="model-tab{" active" if m == "Lumian-VLR-7B-Thinking" else ""}" data-model="{m}"><span class="model-tab-label">{m}</span></button>'
389
+ for m in MODEL_CHOICES
390
+ ])
391
+
392
+ # =========================
393
+ # CSS
394
+ # =========================
395
+ css = f"""
396
+ @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');
397
+ *{{box-sizing:border-box;margin:0;padding:0}}
398
+ html,body{{height:100%;overflow-x:hidden}}
399
+ body,.gradio-container{{
400
+ background:#0f0f13!important;
401
+ font-family:'Inter',system-ui,-apple-system,sans-serif!important;
402
+ font-size:14px!important;color:#e4e4e7!important;min-height:100vh;overflow-x:hidden;
403
+ }}
404
+ .dark body,.dark .gradio-container{{background:#0f0f13!important;color:#e4e4e7!important}}
405
+ footer{{display:none!important}}
406
+ .hidden-input{{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}}
407
+
408
+ #gradio-run-btn,#example-load-btn{{
409
+ position:absolute!important;left:-9999px!important;top:-9999px!important;
410
+ width:1px!important;height:1px!important;opacity:0.01!important;
411
+ pointer-events:none!important;overflow:hidden!important;
412
+ }}
413
+
414
+ .app-shell{{
415
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
416
+ margin:12px auto;max-width:1400px;overflow:hidden;
417
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
418
+ }}
419
+ .app-header{{
420
+ background:linear-gradient(135deg,#18181b,#132013);border-bottom:1px solid #27272a;
421
+ padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
422
+ }}
423
+ .app-header-left{{display:flex;align-items:center;gap:12px}}
424
+ .app-logo{{
425
+ width:38px;height:38px;background:linear-gradient(135deg,{ACCENT},#52ff52,#9dff9d);
426
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
427
+ box-shadow:0 4px 12px rgba(0,255,0,.30);
428
+ }}
429
+ .app-logo svg{{width:22px;height:22px;fill:#fff;flex-shrink:0}}
430
+
431
+ .app-title{{
432
+ font-size:18px;font-weight:700;background:linear-gradient(135deg,#f5f5f5,#bdbdbd);
433
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
434
+ }}
435
+ .app-badge{{
436
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
437
+ background:rgba(0,255,0,.10);color:#baffba;border:1px solid rgba(0,255,0,.24);letter-spacing:.3px;
438
+ }}
439
+ .app-badge.fast{{background:rgba(0,255,0,.08);color:#95ff95;border:1px solid rgba(0,255,0,.20)}}
440
+
441
+ .model-tabs-bar{{
442
+ background:#18181b;border-bottom:1px solid #27272a;padding:10px 16px;
443
+ display:flex;gap:8px;align-items:center;flex-wrap:wrap;
444
+ }}
445
+ .model-tab{{
446
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
447
+ min-width:32px;height:34px;background:transparent;border:1px solid #27272a;
448
+ border-radius:999px;cursor:pointer;font-size:12px;font-weight:600;padding:0 12px;
449
+ color:#ffffff!important;transition:all .15s ease;
450
+ }}
451
+ .model-tab:hover{{background:rgba(0,255,0,.10);border-color:rgba(0,255,0,.35)}}
452
+ .model-tab.active{{background:rgba(0,255,0,.16);border-color:{ACCENT};color:#fff!important;box-shadow:0 0 0 2px rgba(0,255,0,.10)}}
453
+ .model-tab-label{{font-size:12px;color:#ffffff!important;font-weight:600}}
454
+
455
+ .app-main-row{{display:flex;gap:0;flex:1;overflow:hidden}}
456
+ .app-main-left{{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}}
457
+ .app-main-right{{width:470px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}}
458
+
459
+ #image-drop-zone{{
460
+ position:relative;background:#09090b;height:440px;min-height:440px;max-height:440px;
461
+ overflow:hidden;
462
+ }}
463
+ #image-drop-zone.drag-over{{outline:2px solid {ACCENT};outline-offset:-2px;background:rgba(0,255,0,.04)}}
464
+ .upload-prompt-modern{{
465
+ position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
466
+ padding:20px;z-index:20;overflow:hidden;
467
+ }}
468
+ .upload-click-area{{
469
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
470
+ cursor:pointer;padding:28px 36px;max-width:92%;max-height:92%;
471
+ border:2px dashed #3f3f46;border-radius:16px;
472
+ background:rgba(0,255,0,.03);transition:all .2s ease;gap:8px;text-align:center;
473
+ overflow:hidden;
474
+ }}
475
+ .upload-click-area:hover{{background:rgba(0,255,0,.08);border-color:{ACCENT};transform:scale(1.02)}}
476
+ .upload-click-area:active{{background:rgba(0,255,0,.12);transform:scale(.99)}}
477
+ .upload-click-area svg{{width:86px;height:86px;max-width:100%;flex-shrink:0}}
478
+ .upload-main-text{{color:#a1a1aa;font-size:14px;font-weight:600;margin-top:4px}}
479
+ .upload-sub-text{{color:#71717a;font-size:12px}}
480
+
481
+ .single-preview-wrap{{
482
+ width:100%;height:100%;display:none;align-items:center;justify-content:center;padding:16px;
483
+ overflow:hidden;
484
+ }}
485
+ .single-preview-card{{
486
+ width:100%;height:100%;max-width:100%;max-height:100%;border-radius:14px;
487
+ overflow:hidden;border:1px solid #27272a;background:#111114;
488
+ display:flex;align-items:center;justify-content:center;position:relative;
489
+ }}
490
+ .single-preview-card img{{
491
+ width:100%;height:100%;max-width:100%;max-height:100%;
492
+ object-fit:contain;display:block;
493
+ }}
494
+ .preview-overlay-actions{{
495
+ position:absolute;top:12px;right:12px;display:flex;gap:8px;z-index:5;
496
+ }}
497
+ .preview-action-btn{{
498
+ display:inline-flex;align-items:center;justify-content:center;
499
+ min-width:34px;height:34px;padding:0 12px;background:rgba(0,0,0,.65);
500
+ border:1px solid rgba(255,255,255,.14);border-radius:10px;cursor:pointer;
501
+ color:#fff!important;font-size:12px;font-weight:600;transition:all .15s ease;
502
+ }}
503
+ .preview-action-btn:hover{{background:{ACCENT};border-color:{ACCENT};color:#031103!important}}
504
+
505
+ .hint-bar{{
506
+ background:rgba(0,255,0,.05);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
507
+ padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
508
+ }}
509
+ .hint-bar b{{color:#b8ffb8;font-weight:600}}
510
+ .hint-bar kbd{{
511
+ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
512
+ border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
513
+ }}
514
+
515
+ .examples-section{{border-top:1px solid #27272a;padding:12px 16px}}
516
+ .examples-title{{
517
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
518
+ letter-spacing:.8px;margin-bottom:10px;
519
+ }}
520
+ .examples-scroll{{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}}
521
+ .examples-scroll::-webkit-scrollbar{{height:6px}}
522
+ .examples-scroll::-webkit-scrollbar-track{{background:#09090b;border-radius:3px}}
523
+ .examples-scroll::-webkit-scrollbar-thumb{{background:#27272a;border-radius:3px}}
524
+ .examples-scroll::-webkit-scrollbar-thumb:hover{{background:#3f3f46}}
525
+ .example-card{{
526
+ flex-shrink:0;width:220px;background:#09090b;border:1px solid #27272a;
527
+ border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
528
+ }}
529
+ .example-card:hover{{border-color:{ACCENT};transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,255,0,.14)}}
530
+ .example-card.loading{{opacity:.5;pointer-events:none}}
531
+ .example-thumb-wrap{{height:120px;overflow:hidden;background:#18181b}}
532
+ .example-thumb-wrap img{{width:100%;height:100%;object-fit:cover}}
533
+ .example-thumb-placeholder{{
534
+ width:100%;height:100%;display:flex;align-items:center;justify-content:center;
535
+ background:#18181b;color:#3f3f46;font-size:11px;
536
+ }}
537
+ .example-meta-row{{padding:6px 10px;display:flex;align-items:center;gap:6px}}
538
+ .example-badge{{
539
+ display:inline-flex;padding:2px 7px;background:rgba(0,255,0,.12);border-radius:4px;
540
+ font-size:10px;font-weight:600;color:#b8ffb8;font-family:'JetBrains Mono',monospace;white-space:nowrap;
541
+ }}
542
+ .example-prompt-text{{
543
+ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
544
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
545
+ }}
546
+
547
+ .panel-card{{border-bottom:1px solid #27272a}}
548
+ .panel-card-title{{
549
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
550
+ text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
551
+ }}
552
+ .panel-card-body{{padding:16px 20px;display:flex;flex-direction:column;gap:8px}}
553
+ .modern-label{{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}}
554
+ .modern-textarea{{
555
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
556
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
557
+ resize:none;outline:none;min-height:100px;transition:border-color .2s;
558
+ }}
559
+ .modern-textarea:focus{{border-color:{ACCENT};box-shadow:0 0 0 3px rgba(0,255,0,.14)}}
560
+ .modern-textarea::placeholder{{color:#3f3f46}}
561
+ .modern-textarea.error-flash{{
562
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
563
+ }}
564
+ @keyframes shake{{0%,100%{{transform:translateX(0)}}20%,60%{{transform:translateX(-4px)}}40%,80%{{transform:translateX(4px)}}}}
565
+
566
+ .toast-notification{{
567
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
568
+ z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
569
+ font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
570
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
571
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
572
+ }}
573
+ .toast-notification.visible{{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}}
574
+ .toast-notification.error{{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}}
575
+ .toast-notification.warning{{background:linear-gradient(135deg,#00cc00,#00aa00);color:#fff;border:1px solid rgba(255,255,255,.15)}}
576
+ .toast-notification.info{{background:linear-gradient(135deg,#16d916,{ACCENT});color:#fff;border:1px solid rgba(255,255,255,.15)}}
577
+ .toast-notification .toast-icon{{font-size:16px;line-height:1}}
578
+ .toast-notification .toast-text{{line-height:1.3}}
579
+
580
+ .btn-run{{
581
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
582
+ background:linear-gradient(135deg,{ACCENT},#00d000);border:none;border-radius:10px;
583
+ padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
584
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
585
+ transition:all .2s ease;letter-spacing:-.2px;
586
+ box-shadow:0 4px 16px rgba(0,255,0,.25),inset 0 1px 0 rgba(255,255,255,.18);
587
+ }}
588
+ .btn-run:hover{{
589
+ background:linear-gradient(135deg,#58ff58,{ACCENT});transform:translateY(-1px);
590
+ box-shadow:0 6px 24px rgba(0,255,0,.35),inset 0 1px 0 rgba(255,255,255,.22);
591
+ }}
592
+ .btn-run:active{{transform:translateY(0);box-shadow:0 2px 8px rgba(0,255,0,.25)}}
593
+ #custom-run-btn,#custom-run-btn *,#run-btn-label,.btn-run,.btn-run *{{
594
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
595
+ }}
596
+
597
+ .output-frame{{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}}
598
+ .output-frame .out-title,
599
+ .output-frame .out-title *,
600
+ #output-title-label{{
601
+ color:#ffffff!important;
602
+ -webkit-text-fill-color:#ffffff!important;
603
+ }}
604
+ .output-frame .out-title{{
605
+ padding:10px 20px;font-size:13px;font-weight:700;
606
+ text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
607
+ display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;
608
+ }}
609
+ .out-title-right{{display:flex;gap:8px;align-items:center}}
610
+ .out-action-btn{{
611
+ display:inline-flex;align-items:center;justify-content:center;background:rgba(0,255,0,.10);
612
+ border:1px solid rgba(0,255,0,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
613
+ font-size:11px;font-weight:500;color:#b8ffb8!important;gap:4px;height:24px;transition:all .15s;
614
+ }}
615
+ .out-action-btn:hover{{background:rgba(0,255,0,.2);border-color:rgba(0,255,0,.35);color:#ffffff!important}}
616
+ .out-action-btn svg{{width:12px;height:12px;fill:{ACCENT}}}
617
+ .output-frame .out-body{{
618
+ flex:1;background:#09090b;display:flex;align-items:stretch;justify-content:stretch;
619
+ overflow:hidden;min-height:360px;position:relative;
620
+ }}
621
+ .output-scroll-wrap{{width:100%;height:100%;padding:0;overflow:hidden}}
622
+ .output-textarea{{
623
+ width:100%;height:360px;min-height:360px;max-height:360px;background:#09090b;color:#e4e4e7;
624
+ border:none;outline:none;padding:16px 18px;font-size:13px;line-height:1.6;
625
+ font-family:'JetBrains Mono',monospace;overflow:auto;resize:none;white-space:pre-wrap;
626
+ }}
627
+ .output-textarea::placeholder{{color:#52525b}}
628
+ .output-textarea.error-flash{{box-shadow:inset 0 0 0 2px rgba(239,68,68,.6)}}
629
+
630
+ .modern-loader{{
631
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
632
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
633
+ }}
634
+ .modern-loader.active{{display:flex}}
635
+ .modern-loader .loader-spinner{{
636
+ width:36px;height:36px;border:3px solid #27272a;border-top-color:{ACCENT};
637
+ border-radius:50%;animation:spin .8s linear infinite;
638
+ }}
639
+ @keyframes spin{{to{{transform:rotate(360deg)}}}}
640
+ .modern-loader .loader-text{{font-size:13px;color:#a1a1aa;font-weight:500}}
641
+ .loader-bar-track{{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}}
642
+ .loader-bar-fill{{
643
+ height:100%;background:linear-gradient(90deg,{ACCENT},#7dff7d,{ACCENT});
644
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
645
+ }}
646
+ @keyframes shimmer{{0%{{background-position:200% 0}}100%{{background-position:-200% 0}}}}
647
+
648
+ .settings-group{{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}}
649
+ .settings-group-title{{
650
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
651
+ padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
652
+ }}
653
+ .settings-group-body{{padding:14px 16px;display:flex;flex-direction:column;gap:12px}}
654
+ .slider-row{{display:flex;align-items:center;gap:10px;min-height:28px}}
655
+ .slider-row label{{font-size:13px;font-weight:500;color:#a1a1aa;min-width:118px;flex-shrink:0}}
656
+ .slider-row input[type="range"]{{
657
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
658
+ border-radius:3px;outline:none;min-width:0;
659
+ }}
660
+ .slider-row input[type="range"]::-webkit-slider-thumb{{
661
+ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,{ACCENT},#00d000);
662
+ border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(0,255,0,.35);transition:transform .15s;
663
+ }}
664
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{{transform:scale(1.2)}}
665
+ .slider-row input[type="range"]::-moz-range-thumb{{
666
+ width:16px;height:16px;background:linear-gradient(135deg,{ACCENT},#00d000);
667
+ border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(0,255,0,.35);
668
+ }}
669
+ .slider-row .slider-val{{
670
+ min-width:58px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
671
+ font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
672
+ border-radius:6px;color:#a1a1aa;flex-shrink:0;
673
+ }}
674
+
675
+ .app-statusbar{{
676
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
677
+ display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
678
+ }}
679
+ .app-statusbar .sb-section{{
680
+ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
681
+ font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
682
+ }}
683
+ .app-statusbar .sb-section.sb-fixed{{
684
+ flex:0 0 auto;min-width:110px;text-align:center;justify-content:center;
685
+ padding:3px 12px;background:rgba(0,255,0,.08);border-radius:6px;color:#b8ffb8;font-weight:500;
686
+ }}
687
+
688
+ .exp-note{{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}}
689
+ .exp-note a{{color:#b8ffb8;text-decoration:none}}
690
+ .exp-note a:hover{{text-decoration:underline}}
691
+
692
+ ::-webkit-scrollbar{{width:8px;height:8px}}
693
+ ::-webkit-scrollbar-track{{background:#09090b}}
694
+ ::-webkit-scrollbar-thumb{{background:#27272a;border-radius:4px}}
695
+ ::-webkit-scrollbar-thumb:hover{{background:#3f3f46}}
696
+
697
+ @media(max-width:980px){{
698
+ .app-main-row{{flex-direction:column}}
699
+ .app-main-right{{width:100%}}
700
+ .app-main-left{{border-right:none;border-bottom:1px solid #27272a}}
701
+ }}
702
+ """
703
+
704
+ # =========================
705
+ # JS
706
+ # =========================
707
+ gallery_js = r"""
708
+ () => {
709
+ function init() {
710
+ if (window.__vlmThinkInitDone) return;
711
+
712
+ const dropZone = document.getElementById('image-drop-zone');
713
+ const uploadPrompt = document.getElementById('upload-prompt');
714
+ const uploadClick = document.getElementById('upload-click-area');
715
+ const fileInput = document.getElementById('custom-file-input');
716
+ const previewWrap = document.getElementById('single-preview-wrap');
717
+ const previewImg = document.getElementById('single-preview-img');
718
+ const btnUpload = document.getElementById('preview-upload-btn');
719
+ const btnClear = document.getElementById('preview-clear-btn');
720
+ const promptInput = document.getElementById('custom-query-input');
721
+ const runBtnEl = document.getElementById('custom-run-btn');
722
+ const outputArea = document.getElementById('custom-output-textarea');
723
+ const imgStatus = document.getElementById('sb-image-status');
724
+
725
+ if (!dropZone || !fileInput || !promptInput || !previewWrap || !previewImg) {
726
+ setTimeout(init, 250);
727
+ return;
728
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
+ window.__vlmThinkInitDone = true;
731
+ let imageState = null;
732
+ let toastTimer = null;
733
+ let examplePoller = null;
734
+ let lastSeenExamplePayload = null;
735
+
736
+ function showToast(message, type) {
737
+ let toast = document.getElementById('app-toast');
738
+ if (!toast) {
739
+ toast = document.createElement('div');
740
+ toast.id = 'app-toast';
741
+ toast.className = 'toast-notification';
742
+ toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
743
+ document.body.appendChild(toast);
744
+ }
745
+ const icon = toast.querySelector('.toast-icon');
746
+ const text = toast.querySelector('.toast-text');
747
+ toast.className = 'toast-notification ' + (type || 'error');
748
+ if (type === 'warning') icon.textContent = '\u26A0';
749
+ else if (type === 'info') icon.textContent = '\u2139';
750
+ else icon.textContent = '\u2717';
751
+ text.textContent = message;
752
+ if (toastTimer) clearTimeout(toastTimer);
753
+ void toast.offsetWidth;
754
+ toast.classList.add('visible');
755
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
756
+ }
757
+
758
+ function showLoader() {
759
+ const l = document.getElementById('output-loader');
760
+ if (l) l.classList.add('active');
761
+ const sb = document.getElementById('sb-run-state');
762
+ if (sb) sb.textContent = 'Processing...';
763
+ }
764
+ function hideLoader() {
765
+ const l = document.getElementById('output-loader');
766
+ if (l) l.classList.remove('active');
767
+ const sb = document.getElementById('sb-run-state');
768
+ if (sb) sb.textContent = 'Done';
769
+ }
770
+ function setRunErrorState() {
771
+ const l = document.getElementById('output-loader');
772
+ if (l) l.classList.remove('active');
773
+ const sb = document.getElementById('sb-run-state');
774
+ if (sb) sb.textContent = 'Error';
775
+ }
776
+
777
+ window.__showToast = showToast;
778
+ window.__showLoader = showLoader;
779
+ window.__hideLoader = hideLoader;
780
+ window.__setRunErrorState = setRunErrorState;
781
+
782
+ function flashPromptError() {
783
+ promptInput.classList.add('error-flash');
784
+ promptInput.focus();
785
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
786
+ }
787
+
788
+ function flashOutputError() {
789
+ if (!outputArea) return;
790
+ outputArea.classList.add('error-flash');
791
+ setTimeout(() => outputArea.classList.remove('error-flash'), 800);
792
+ }
793
+
794
+ function getValueFromContainer(containerId) {
795
+ const container = document.getElementById(containerId);
796
+ if (!container) return '';
797
+ const el = container.querySelector('textarea, input');
798
+ return el ? (el.value || '') : '';
799
+ }
800
+
801
+ function setGradioValue(containerId, value) {
802
+ const container = document.getElementById(containerId);
803
+ if (!container) return false;
804
+ const el = container.querySelector('textarea, input');
805
+ if (!el) return false;
806
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
807
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
808
+ if (ns && ns.set) {
809
+ ns.set.call(el, value);
810
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
811
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
812
+ return true;
813
+ }
814
+ return false;
815
+ }
816
+
817
+ function syncImageToGradio() {
818
+ setGradioValue('hidden-image-b64', imageState ? imageState.b64 : '');
819
+ const txt = imageState ? '1 image uploaded' : 'No image uploaded';
820
+ if (imgStatus) imgStatus.textContent = txt;
821
+ }
822
+
823
+ function syncPromptToGradio() {
824
+ setGradioValue('prompt-gradio-input', promptInput.value);
825
+ }
826
+
827
+ function syncModelToGradio(name) {
828
+ setGradioValue('hidden-model-name', name);
829
+ }
830
 
831
+ function setPreview(b64, name) {
832
+ imageState = {b64, name: name || 'image'};
833
+ previewImg.src = b64;
834
+ previewWrap.style.display = 'flex';
835
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
836
+ syncImageToGradio();
837
+ }
838
+ window.__setPreview = setPreview;
839
+
840
+ function clearPreview() {
841
+ imageState = null;
842
+ previewImg.src = '';
843
+ previewWrap.style.display = 'none';
844
+ if (uploadPrompt) uploadPrompt.style.display = 'flex';
845
+ syncImageToGradio();
846
+ }
847
+ window.__clearPreview = clearPreview;
848
+
849
+ function processFile(file) {
850
+ if (!file) return;
851
+ if (!file.type.startsWith('image/')) {
852
+ showToast('Only image files are supported', 'error');
853
+ return;
854
+ }
855
+ const reader = new FileReader();
856
+ reader.onload = (e) => setPreview(e.target.result, file.name);
857
+ reader.readAsDataURL(file);
858
+ }
859
+
860
+ fileInput.addEventListener('change', (e) => {
861
+ const file = e.target.files && e.target.files[0] ? e.target.files[0] : null;
862
+ if (file) processFile(file);
863
+ e.target.value = '';
864
+ });
865
+
866
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
867
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
868
+ if (btnClear) btnClear.addEventListener('click', clearPreview);
869
+
870
+ dropZone.addEventListener('dragover', (e) => {
871
+ e.preventDefault();
872
+ dropZone.classList.add('drag-over');
873
+ });
874
+ dropZone.addEventListener('dragleave', (e) => {
875
+ e.preventDefault();
876
+ dropZone.classList.remove('drag-over');
877
+ });
878
+ dropZone.addEventListener('drop', (e) => {
879
+ e.preventDefault();
880
+ dropZone.classList.remove('drag-over');
881
+ if (e.dataTransfer.files && e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
882
+ });
883
+
884
+ promptInput.addEventListener('input', syncPromptToGradio);
885
+
886
+ function activateModelTab(name) {
887
+ document.querySelectorAll('.model-tab[data-model]').forEach(btn => {
888
+ btn.classList.toggle('active', btn.getAttribute('data-model') === name);
889
+ });
890
+ syncModelToGradio(name);
891
+ }
892
+ window.__activateModelTab = activateModelTab;
893
+
894
+ document.querySelectorAll('.model-tab[data-model]').forEach(btn => {
895
+ btn.addEventListener('click', () => {
896
+ const model = btn.getAttribute('data-model');
897
+ activateModelTab(model);
898
+ });
899
+ });
900
+
901
+ activateModelTab('Lumian-VLR-7B-Thinking');
902
+
903
+ function syncSlider(customId, gradioId) {
904
+ const slider = document.getElementById(customId);
905
+ const valSpan = document.getElementById(customId + '-val');
906
+ if (!slider) return;
907
+ slider.addEventListener('input', () => {
908
+ if (valSpan) valSpan.textContent = slider.value;
909
+ const container = document.getElementById(gradioId);
910
+ if (!container) return;
911
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
912
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
913
+ if (ns && ns.set) {
914
+ ns.set.call(el, slider.value);
915
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
916
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
917
+ }
918
+ });
919
+ });
920
+ }
921
+
922
+ syncSlider('custom-max-new-tokens', 'gradio-max-new-tokens');
923
+ syncSlider('custom-temperature', 'gradio-temperature');
924
+ syncSlider('custom-top-p', 'gradio-top-p');
925
+ syncSlider('custom-top-k', 'gradio-top-k');
926
+ syncSlider('custom-repetition-penalty', 'gradio-repetition-penalty');
927
+ syncSlider('custom-gpu-duration', 'gradio-gpu-duration');
928
+
929
+ function validateBeforeRun() {
930
+ const promptVal = promptInput.value.trim();
931
+ if (!imageState && !promptVal) {
932
+ showToast('Please upload an image and enter your query', 'error');
933
+ flashPromptError();
934
+ return false;
935
+ }
936
+ if (!imageState) {
937
+ showToast('Please upload an image', 'error');
938
+ return false;
939
+ }
940
+ if (!promptVal) {
941
+ showToast('Please enter your query', 'warning');
942
+ flashPromptError();
943
+ return false;
944
+ }
945
+ const currentModel = (document.querySelector('.model-tab.active') || {}).dataset?.model;
946
+ if (!currentModel) {
947
+ showToast('Please select a model', 'error');
948
+ return false;
949
+ }
950
+ return true;
951
+ }
952
+
953
+ window.__clickGradioRunBtn = function() {
954
+ if (!validateBeforeRun()) return;
955
+ syncPromptToGradio();
956
+ syncImageToGradio();
957
+ const active = document.querySelector('.model-tab.active');
958
+ if (active) syncModelToGradio(active.getAttribute('data-model'));
959
+ if (outputArea) outputArea.value = '';
960
+ showLoader();
961
+ setTimeout(() => {
962
+ const gradioBtn = document.getElementById('gradio-run-btn');
963
+ if (!gradioBtn) {
964
+ setRunErrorState();
965
+ if (outputArea) outputArea.value = '[ERROR] Run button not found.';
966
+ showToast('Run button not found', 'error');
967
+ return;
968
+ }
969
+ const btn = gradioBtn.querySelector('button');
970
+ if (btn) btn.click(); else gradioBtn.click();
971
+ }, 180);
972
+ };
973
+
974
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
975
+
976
+ const copyBtn = document.getElementById('copy-output-btn');
977
+ if (copyBtn) {
978
+ copyBtn.addEventListener('click', async () => {
979
+ try {
980
+ const text = outputArea ? outputArea.value : '';
981
+ if (!text.trim()) {
982
+ showToast('No output to copy', 'warning');
983
+ flashOutputError();
984
+ return;
985
+ }
986
+ await navigator.clipboard.writeText(text);
987
+ showToast('Output copied to clipboard', 'info');
988
+ } catch(e) {
989
+ showToast('Copy failed', 'error');
990
+ }
991
+ });
992
+ }
993
+
994
+ const saveBtn = document.getElementById('save-output-btn');
995
+ if (saveBtn) {
996
+ saveBtn.addEventListener('click', () => {
997
+ const text = outputArea ? outputArea.value : '';
998
+ if (!text.trim()) {
999
+ showToast('No output to save', 'warning');
1000
+ flashOutputError();
1001
+ return;
1002
+ }
1003
+ const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
1004
+ const a = document.createElement('a');
1005
+ a.href = URL.createObjectURL(blob);
1006
+ a.download = 'multimodal_vlm_thinking_output.txt';
1007
+ document.body.appendChild(a);
1008
+ a.click();
1009
+ setTimeout(() => {
1010
+ URL.revokeObjectURL(a.href);
1011
+ document.body.removeChild(a);
1012
+ }, 200);
1013
+ showToast('Output saved', 'info');
1014
+ });
1015
+ }
1016
+
1017
+ function applyExamplePayload(raw) {
1018
+ try {
1019
+ const data = JSON.parse(raw);
1020
+ if (data.status === 'ok') {
1021
+ if (data.image) setPreview(data.image, data.name || 'example.jpg');
1022
+ if (data.query) {
1023
+ promptInput.value = data.query;
1024
+ syncPromptToGradio();
1025
+ }
1026
+ if (data.model) activateModelTab(data.model);
1027
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1028
+ showToast('Example loaded', 'info');
1029
+ } else if (data.status === 'error') {
1030
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1031
+ showToast(data.message || 'Failed to load example', 'error');
1032
+ }
1033
+ } catch (e) {
1034
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1035
+ }
1036
+ }
1037
+
1038
+ function startExamplePolling() {
1039
+ if (examplePoller) clearInterval(examplePoller);
1040
+ let attempts = 0;
1041
+ examplePoller = setInterval(() => {
1042
+ attempts += 1;
1043
+ const current = getValueFromContainer('example-result-data');
1044
+ if (current && current !== lastSeenExamplePayload) {
1045
+ lastSeenExamplePayload = current;
1046
+ clearInterval(examplePoller);
1047
+ examplePoller = null;
1048
+ applyExamplePayload(current);
1049
+ return;
1050
+ }
1051
+ if (attempts >= 100) {
1052
+ clearInterval(examplePoller);
1053
+ examplePoller = null;
1054
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1055
+ showToast('Example load timed out', 'error');
1056
+ }
1057
+ }, 120);
1058
+ }
1059
+
1060
+ function triggerExampleLoad(idx) {
1061
+ const btnWrap = document.getElementById('example-load-btn');
1062
+ const btn = btnWrap ? (btnWrap.querySelector('button') || btnWrap) : null;
1063
+ if (!btn) return;
1064
+
1065
+ let attempts = 0;
1066
+
1067
+ function writeIdxAndClick() {
1068
+ attempts += 1;
1069
+ const ok1 = setGradioValue('example-idx-input', String(idx));
1070
+ setGradioValue('example-result-data', '');
1071
+ const currentVal = getValueFromContainer('example-idx-input');
1072
+
1073
+ if (ok1 && currentVal === String(idx)) {
1074
+ btn.click();
1075
+ startExamplePolling();
1076
+ return;
1077
+ }
1078
+
1079
+ if (attempts < 30) {
1080
+ setTimeout(writeIdxAndClick, 100);
1081
+ } else {
1082
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1083
+ showToast('Failed to initialize example loader', 'error');
1084
+ }
1085
+ }
1086
+
1087
+ writeIdxAndClick();
1088
+ }
1089
+
1090
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
1091
+ card.addEventListener('click', () => {
1092
+ const idx = card.getAttribute('data-idx');
1093
+ if (idx === null || idx === undefined || idx === '') return;
1094
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1095
+ card.classList.add('loading');
1096
+ showToast('Loading example...', 'info');
1097
+ triggerExampleLoad(idx);
1098
+ });
1099
+ });
1100
+
1101
+ const observerTarget = document.getElementById('example-result-data');
1102
+ if (observerTarget) {
1103
+ const obs = new MutationObserver(() => {
1104
+ const current = getValueFromContainer('example-result-data');
1105
+ if (!current || current === lastSeenExamplePayload) return;
1106
+ lastSeenExamplePayload = current;
1107
+ if (examplePoller) {
1108
+ clearInterval(examplePoller);
1109
+ examplePoller = null;
1110
+ }
1111
+ applyExamplePayload(current);
1112
+ });
1113
+ obs.observe(observerTarget, {childList:true, subtree:true, characterData:true, attributes:true});
1114
+ }
1115
+
1116
+ if (outputArea) outputArea.value = '';
1117
+ const sb = document.getElementById('sb-run-state');
1118
+ if (sb) sb.textContent = 'Ready';
1119
+ if (imgStatus) imgStatus.textContent = 'No image uploaded';
1120
+ }
1121
+ init();
1122
+ }
1123
+ """
1124
+
1125
+ wire_outputs_js = r"""
1126
+ () => {
1127
+ function watchOutputs() {
1128
+ const resultContainer = document.getElementById('gradio-result');
1129
+ const outArea = document.getElementById('custom-output-textarea');
1130
+ if (!resultContainer || !outArea) { setTimeout(watchOutputs, 500); return; }
1131
+
1132
+ let lastText = '';
1133
+
1134
+ function isErrorText(val) {
1135
+ return typeof val === 'string' && val.trim().startsWith('[ERROR]');
1136
+ }
1137
+
1138
+ function syncOutput() {
1139
+ const el = resultContainer.querySelector('textarea') || resultContainer.querySelector('input');
1140
+ if (!el) return;
1141
+ const val = el.value || '';
1142
+ if (val !== lastText) {
1143
+ lastText = val;
1144
+ outArea.value = val;
1145
+ outArea.scrollTop = outArea.scrollHeight;
1146
+
1147
+ if (val.trim()) {
1148
+ if (isErrorText(val)) {
1149
+ if (window.__setRunErrorState) window.__setRunErrorState();
1150
+ if (window.__showToast) window.__showToast('Inference failed', 'error');
1151
+ } else {
1152
+ if (window.__hideLoader) window.__hideLoader();
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+
1158
+ const observer = new MutationObserver(syncOutput);
1159
+ observer.observe(resultContainer, {childList:true, subtree:true, characterData:true, attributes:true});
1160
+ setInterval(syncOutput, 500);
1161
+ }
1162
+ watchOutputs();
1163
+ }
1164
+ """
1165
+
1166
+ # =========================
1167
+ # App
1168
+ # =========================
1169
  with gr.Blocks() as demo:
1170
+ hidden_image_b64 = gr.Textbox(value="", elem_id="hidden-image-b64", elem_classes="hidden-input", container=False)
1171
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
1172
+ hidden_model_name = gr.Textbox(value="Lumian-VLR-7B-Thinking", elem_id="hidden-model-name", elem_classes="hidden-input", container=False)
1173
+
1174
+ 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)
1175
+ 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)
1176
+ 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)
1177
+ top_k = gr.Slider(minimum=1, maximum=1000, step=1, value=50, elem_id="gradio-top-k", elem_classes="hidden-input", container=False)
1178
+ 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)
1179
+ gpu_duration_state = gr.Number(value=60, elem_id="gradio-gpu-duration", elem_classes="hidden-input", container=False)
1180
+
1181
+ result = gr.Textbox(value="", elem_id="gradio-result", elem_classes="hidden-input", container=False)
1182
+
1183
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1184
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1185
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
1186
+
1187
+ gr.HTML(f"""
1188
+ <div class="app-shell">
1189
+ <div class="app-header">
1190
+ <div class="app-header-left">
1191
+ <div class="app-logo">{THUNDER_SVG}</div>
1192
+ <span class="app-title">Multimodal VLM Thinking</span>
1193
+ <span class="app-badge">vision enabled</span>
1194
+ <span class="app-badge fast">image only</span>
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <div class="model-tabs-bar">
1199
+ {MODEL_TABS_HTML}
1200
+ </div>
1201
+
1202
+ <div class="app-main-row">
1203
+ <div class="app-main-left">
1204
+ <div id="image-drop-zone">
1205
+ <div id="upload-prompt" class="upload-prompt-modern">
1206
+ <div id="upload-click-area" class="upload-click-area">
1207
+ {UPLOAD_PREVIEW_SVG}
1208
+ <span class="upload-main-text">Click or drag an image here</span>
1209
+ <span class="upload-sub-text">Upload one image for multimodal reasoning, OCR, visual understanding, or chart/document interpretation</span>
1210
+ </div>
1211
+ </div>
1212
+
1213
+ <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
1214
+
1215
+ <div id="single-preview-wrap" class="single-preview-wrap">
1216
+ <div class="single-preview-card">
1217
+ <img id="single-preview-img" src="" alt="Preview">
1218
+ <div class="preview-overlay-actions">
1219
+ <button id="preview-upload-btn" class="preview-action-btn" title="Replace">Upload</button>
1220
+ <button id="preview-clear-btn" class="preview-action-btn" title="Clear">Clear</button>
1221
+ </div>
1222
+ </div>
1223
+ </div>
1224
+ </div>
1225
+
1226
+ <div class="hint-bar">
1227
+ <b>Upload:</b> Click or drag to add an image &nbsp;&middot;&nbsp;
1228
+ <b>Model:</b> Switch model tabs from the header &nbsp;&middot;&nbsp;
1229
+ <kbd>Clear</kbd> removes the current image
1230
+ </div>
1231
 
1232
+ <div class="examples-section">
1233
+ <div class="examples-title">Quick Examples</div>
1234
+ <div class="examples-scroll">
1235
+ {EXAMPLE_CARDS_HTML}
1236
+ </div>
1237
+ </div>
1238
+ </div>
1239
+
1240
+ <div class="app-main-right">
1241
+ <div class="panel-card">
1242
+ <div class="panel-card-title">Vision / OCR Instruction</div>
1243
+ <div class="panel-card-body">
1244
+ <label class="modern-label" for="custom-query-input">Query Input</label>
1245
+ <textarea id="custom-query-input" class="modern-textarea" rows="4" placeholder="e.g., explain the image, convert page to markdown, extract chart structure, describe safety issues..."></textarea>
1246
+ </div>
1247
+ </div>
1248
+
1249
+ <div style="padding:12px 20px;">
1250
+ <button id="custom-run-btn" class="btn-run">
1251
+ <span id="run-btn-label">Run Inference</span>
1252
+ </button>
1253
+ </div>
1254
+
1255
+ <div class="output-frame">
1256
+ <div class="out-title">
1257
+ <span id="output-title-label">Raw Output Stream</span>
1258
+ <div class="out-title-right">
1259
+ <button id="copy-output-btn" class="out-action-btn" title="Copy">{COPY_SVG} Copy</button>
1260
+ <button id="save-output-btn" class="out-action-btn" title="Save">{SAVE_SVG} Save File</button>
1261
+ </div>
1262
+ </div>
1263
+ <div class="out-body">
1264
+ <div class="modern-loader" id="output-loader">
1265
+ <div class="loader-spinner"></div>
1266
+ <div class="loader-text">Running multimodal inference...</div>
1267
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1268
+ </div>
1269
+ <div class="output-scroll-wrap">
1270
+ <textarea id="custom-output-textarea" class="output-textarea" placeholder="Raw output will appear here..." readonly></textarea>
1271
+ </div>
1272
+ </div>
1273
+ </div>
1274
+
1275
+ <div class="settings-group">
1276
+ <div class="settings-group-title">Advanced Settings</div>
1277
+ <div class="settings-group-body">
1278
+ <div class="slider-row">
1279
+ <label>Max new tokens</label>
1280
+ <input type="range" id="custom-max-new-tokens" min="1" max="{MAX_MAX_NEW_TOKENS}" step="1" value="{DEFAULT_MAX_NEW_TOKENS}">
1281
+ <span class="slider-val" id="custom-max-new-tokens-val">{DEFAULT_MAX_NEW_TOKENS}</span>
1282
+ </div>
1283
+ <div class="slider-row">
1284
+ <label>Temperature</label>
1285
+ <input type="range" id="custom-temperature" min="0.1" max="4.0" step="0.1" value="0.6">
1286
+ <span class="slider-val" id="custom-temperature-val">0.6</span>
1287
+ </div>
1288
+ <div class="slider-row">
1289
+ <label>Top-p</label>
1290
+ <input type="range" id="custom-top-p" min="0.05" max="1.0" step="0.05" value="0.9">
1291
+ <span class="slider-val" id="custom-top-p-val">0.9</span>
1292
+ </div>
1293
+ <div class="slider-row">
1294
+ <label>Top-k</label>
1295
+ <input type="range" id="custom-top-k" min="1" max="1000" step="1" value="50">
1296
+ <span class="slider-val" id="custom-top-k-val">50</span>
1297
+ </div>
1298
+ <div class="slider-row">
1299
+ <label>Repetition penalty</label>
1300
+ <input type="range" id="custom-repetition-penalty" min="1.0" max="2.0" step="0.05" value="1.2">
1301
+ <span class="slider-val" id="custom-repetition-penalty-val">1.2</span>
1302
+ </div>
1303
+ <div class="slider-row">
1304
+ <label>GPU Duration (seconds)</label>
1305
+ <input type="range" id="custom-gpu-duration" min="60" max="300" step="30" value="60">
1306
+ <span class="slider-val" id="custom-gpu-duration-val">60</span>
1307
+ </div>
1308
+ </div>
1309
+ </div>
1310
+ </div>
1311
+ </div>
1312
+
1313
+ <div class="exp-note">
1314
+ Experimental VLM Suite &middot; Video inference removed as requested
1315
+ </div>
1316
+
1317
+ <div class="app-statusbar">
1318
+ <div class="sb-section" id="sb-image-status">No image uploaded</div>
1319
+ <div class="sb-section sb-fixed" id="sb-run-state">Ready</div>
1320
+ </div>
1321
+ </div>
1322
+ """)
1323
+
1324
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1325
+
1326
+ demo.load(fn=noop, inputs=None, outputs=None, js=gallery_js)
1327
+ demo.load(fn=noop, inputs=None, outputs=None, js=wire_outputs_js)
1328
+
1329
+ run_btn.click(
1330
+ fn=run_image,
1331
+ inputs=[
1332
+ hidden_model_name,
1333
+ prompt,
1334
+ hidden_image_b64,
1335
+ max_new_tokens,
1336
+ temperature,
1337
+ top_p,
1338
+ top_k,
1339
+ repetition_penalty,
1340
+ gpu_duration_state,
1341
+ ],
1342
+ outputs=[result],
1343
+ js=r"""(m, p, img, mnt, t, tp, tk, rp, gd) => {
1344
+ const modelEl = document.querySelector('.model-tab.active');
1345
+ const model = modelEl ? modelEl.getAttribute('data-model') : m;
1346
+ const promptEl = document.getElementById('custom-query-input');
1347
+ const promptVal = promptEl ? promptEl.value : p;
1348
+ const imgContainer = document.getElementById('hidden-image-b64');
1349
+ let imgVal = img;
1350
+ if (imgContainer) {
1351
+ const inner = imgContainer.querySelector('textarea, input');
1352
+ if (inner) imgVal = inner.value;
1353
+ }
1354
+ return [model, promptVal, imgVal, mnt, t, tp, tk, rp, gd];
1355
+ }""",
1356
  )
1357
+
1358
+ example_load_btn.click(
1359
+ fn=load_example_data,
1360
+ inputs=[example_idx],
1361
+ outputs=[example_result],
1362
+ queue=False,
1363
  )
1364
 
1365
  if __name__ == "__main__":
1366
+ demo.queue(max_size=50).launch(
1367
+ css=css,
1368
+ mcp_server=True,
1369
+ ssr_mode=False,
1370
+ show_error=True,
1371
+ allowed_paths=["images"],
1372
+ )