prithivMLmods commited on
Commit
bc01f64
·
verified ·
1 Parent(s): f0054a6

update app

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