Files changed (1) hide show
  1. app.py +489 -1022
app.py CHANGED
@@ -1,20 +1,27 @@
1
- import os
2
  import gc
3
- import time
4
- import json
5
- import base64
6
  import tempfile
 
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, ImageOps
14
 
 
 
 
 
 
15
  from transformers import (
16
- AutoProcessor,
17
  AutoModelForImageTextToText,
 
18
  TextIteratorStreamer,
19
  )
20
 
@@ -22,16 +29,117 @@ MAX_MAX_NEW_TOKENS = 8192
22
  DEFAULT_MAX_NEW_TOKENS = 4096
23
 
24
  MODEL_PATH = "zai-org/GLM-OCR"
 
 
 
 
 
 
 
 
 
 
25
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
  print("Using device:", device)
27
 
28
- processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=True)
29
- model = AutoModelForImageTextToText.from_pretrained(
30
- pretrained_model_name_or_path=MODEL_PATH,
31
- torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
32
- device_map="auto",
33
- trust_remote_code=True,
34
- ).eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  TASK_PROMPTS = {
37
  "Text": "Text Recognition:",
@@ -41,104 +149,193 @@ TASK_PROMPTS = {
41
 
42
  TASK_CHOICES = list(TASK_PROMPTS.keys())
43
 
44
- image_examples = [
45
- {"media": "examples/1.jpg", "task": "Text"},
46
- {"media": "examples/4.jpg", "task": "Text"},
47
- {"media": "examples/5.webp", "task": "Formula"},
48
- {"media": "examples/2.jpg", "task": "Table"},
49
- {"media": "examples/3.jpg", "task": "Text"},
50
- ]
51
-
52
-
53
- def pil_to_data_url(img: Image.Image, fmt="PNG"):
54
- buf = BytesIO()
55
- img.save(buf, format=fmt)
56
- data = base64.b64encode(buf.getvalue()).decode()
57
- mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg"
58
- return f"data:{mime};base64,{data}"
59
-
60
-
61
- def file_to_data_url(path):
62
- if not os.path.exists(path):
63
- return ""
64
- ext = path.rsplit(".", 1)[-1].lower()
65
- mime = {
66
- "jpg": "image/jpeg",
67
- "jpeg": "image/jpeg",
68
- "png": "image/png",
69
- "webp": "image/webp",
70
- }.get(ext, "image/jpeg")
71
- with open(path, "rb") as f:
72
- data = base64.b64encode(f.read()).decode()
73
- return f"data:{mime};base64,{data}"
74
-
75
-
76
- def make_thumb_b64(path, max_dim=240):
77
- try:
78
- img = Image.open(path).convert("RGB")
79
- img.thumbnail((max_dim, max_dim))
80
- return pil_to_data_url(img, "JPEG")
81
- except Exception as e:
82
- print("Thumbnail error:", e)
83
- return ""
84
-
85
-
86
- def build_example_cards_html():
87
- cards = ""
88
- for i, ex in enumerate(image_examples):
89
- thumb = make_thumb_b64(ex["media"])
90
- cards += f"""
91
- <div class="example-card" data-idx="{i}">
92
- <div class="example-thumb-wrap">
93
- {"<img src='" + thumb + "' alt=''>" if thumb else "<div class='example-thumb-placeholder'>Preview</div>"}
94
- <div class="example-media-chip">IMAGE</div>
95
- </div>
96
- <div class="example-meta-row">
97
- <span class="example-badge">{ex["task"]}</span>
98
- </div>
99
- <div class="example-prompt-text">GLM-OCR example · {os.path.basename(ex["media"])}</div>
100
- </div>
101
- """
102
- return cards
103
-
104
-
105
- EXAMPLE_CARDS_HTML = build_example_cards_html()
106
-
107
-
108
- def load_example_data(idx_str):
109
- try:
110
- idx = int(str(idx_str).strip())
111
- except Exception:
112
- return gr.update(value="")
113
 
114
- if idx < 0 or idx >= len(image_examples):
115
- return gr.update(value="")
 
116
 
117
- ex = image_examples[idx]
118
- media_b64 = file_to_data_url(ex["media"])
119
- if not media_b64:
120
- return gr.update(value=json.dumps({"status": "error", "message": "Could not load example image"}))
121
 
122
- return gr.update(value=json.dumps({
123
- "status": "ok",
124
- "media": media_b64,
125
- "task": ex["task"],
126
- "name": os.path.basename(ex["media"]),
127
- }))
128
 
 
 
 
 
 
129
 
130
- def b64_to_pil(b64_str):
131
- if not b64_str:
132
- return None
133
- try:
134
- if b64_str.startswith("data:"):
135
- _, data = b64_str.split(",", 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  else:
137
- data = b64_str
138
- image_data = base64.b64decode(data)
139
- return Image.open(BytesIO(image_data)).convert("RGB")
140
- except Exception:
141
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
 
144
  def calc_timeout_generic(*args, **kwargs):
@@ -151,8 +348,8 @@ def calc_timeout_generic(*args, **kwargs):
151
  return 60
152
 
153
 
154
- @spaces.GPU(duration=calc_timeout_generic)
155
  def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu_timeout=60):
 
156
  tmp_path = None
157
  try:
158
  if image is None:
@@ -163,17 +360,15 @@ def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu
163
  yield "[ERROR] Invalid OCR task selected."
164
  return
165
 
166
- if image.mode in ("RGBA", "LA", "P"):
167
- image = image.convert("RGB")
168
- image = ImageOps.exif_transpose(image)
169
 
170
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
171
  image.save(tmp.name, "PNG")
172
  tmp_path = tmp.name
173
  tmp.close()
174
 
175
- prompt = TASK_PROMPTS.get(task, "Text Recognition:")
176
-
177
  messages = [
178
  {
179
  "role": "user",
@@ -184,7 +379,7 @@ def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu
184
  }
185
  ]
186
 
187
- inputs = processor.apply_chat_template(
188
  messages,
189
  tokenize=True,
190
  add_generation_prompt=True,
@@ -193,33 +388,32 @@ def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu
193
  )
194
 
195
  inputs.pop("token_type_ids", None)
196
- inputs = {k: v.to(model.device) if hasattr(v, "to") else v for k, v in inputs.items()}
197
 
198
  streamer = TextIteratorStreamer(
199
- processor.tokenizer if hasattr(processor, "tokenizer") else processor,
200
  skip_prompt=True,
201
  skip_special_tokens=True,
202
  )
203
 
204
  generation_error = {"error": None}
205
-
206
  generation_kwargs = {
207
  **inputs,
208
  "streamer": streamer,
209
  "max_new_tokens": int(max_new_tokens),
210
  }
211
 
212
- def _run_generation():
213
  try:
214
- model.generate(**generation_kwargs)
215
- except Exception as e:
216
- generation_error["error"] = e
217
  try:
218
  streamer.end()
219
  except Exception:
220
  pass
221
 
222
- thread = Thread(target=_run_generation, daemon=True)
223
  thread.start()
224
 
225
  buffer = ""
@@ -231,22 +425,21 @@ def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu
231
  thread.join(timeout=1.0)
232
 
233
  if generation_error["error"] is not None:
234
- err_msg = f"[ERROR] Inference failed: {str(generation_error['error'])}"
235
  if buffer.strip():
236
- yield buffer.strip() + "\n\n" + err_msg
237
  else:
238
- yield err_msg
239
  return
240
 
241
  if not buffer.strip():
242
  yield "[ERROR] No output was generated."
243
-
244
- except Exception as e:
245
- yield f"[ERROR] {str(e)}"
246
  finally:
247
- if tmp_path and os.path.exists(tmp_path):
248
  try:
249
- os.unlink(tmp_path)
250
  except Exception:
251
  pass
252
  gc.collect()
@@ -254,928 +447,202 @@ def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu
254
  torch.cuda.empty_cache()
255
 
256
 
257
- def run_router(task, image_b64, max_new_tokens_v, gpu_timeout_v):
258
- try:
259
- image = b64_to_pil(image_b64)
260
- yield from process_image_stream(
261
- image=image,
262
- task=task,
263
- max_new_tokens=max_new_tokens_v,
264
- gpu_timeout=gpu_timeout_v,
 
 
 
 
265
  )
266
- except Exception as e:
267
- yield f"[ERROR] {str(e)}"
268
-
269
-
270
- def noop():
271
- return None
272
-
273
-
274
- css = r"""
275
- @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');
276
- *{box-sizing:border-box;margin:0;padding:0}
277
- html,body{height:100%;overflow-x:hidden}
278
- body,.gradio-container{
279
- background:#0f0f13!important;
280
- font-family:'Inter',system-ui,-apple-system,sans-serif!important;
281
- font-size:14px!important;color:#e4e4e7!important;min-height:100vh;overflow-x:hidden;
282
- }
283
- .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
284
- footer{display:none!important}
285
- .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
286
-
287
- #gradio-run-btn,#example-load-btn{
288
- position:absolute!important;left:-9999px!important;top:-9999px!important;
289
- width:1px!important;height:1px!important;opacity:0.01!important;
290
- pointer-events:none!important;overflow:hidden!important;
291
- }
292
-
293
- .app-shell{
294
- background:#18181b;border:1px solid #27272a;border-radius:16px;
295
- margin:12px auto;max-width:1450px;overflow:hidden;
296
- box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
297
- }
298
- .app-header{
299
- background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
300
- padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
301
- }
302
- .app-header-left{display:flex;align-items:center;gap:12px}
303
- .app-logo{
304
- width:38px;height:38px;background:linear-gradient(135deg,#FF1493,#ff3cad,#ff70c6);
305
- border-radius:10px;display:flex;align-items:center;justify-content:center;
306
- box-shadow:0 4px 12px rgba(255,20,147,.35);
307
- }
308
- .app-logo svg{width:22px;height:22px;fill:#fff;flex-shrink:0}
309
- .app-title{
310
- font-size:18px;font-weight:700;background:linear-gradient(135deg,#f5f5f5,#bdbdbd);
311
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
312
- }
313
- .app-badge{
314
- font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
315
- background:rgba(255,20,147,.12);color:#ff8fcf;border:1px solid rgba(255,20,147,.25);letter-spacing:.3px;
316
- }
317
- .app-badge.fast{background:rgba(255,60,173,.10);color:#ff9ad5;border:1px solid rgba(255,60,173,.22)}
318
-
319
- .model-tabs-bar{
320
- background:#18181b;border-bottom:1px solid #27272a;padding:10px 16px;
321
- display:flex;gap:8px;align-items:center;flex-wrap:wrap;
322
- }
323
- .model-tab{
324
- display:inline-flex;align-items:center;justify-content:center;gap:6px;
325
- min-width:32px;height:34px;background:transparent;border:1px solid #27272a;
326
- border-radius:999px;cursor:pointer;font-size:12px;font-weight:600;padding:0 12px;
327
- color:#ffffff!important;transition:all .15s ease;
328
- }
329
- .model-tab:hover{background:rgba(255,20,147,.12);border-color:rgba(255,20,147,.35)}
330
- .model-tab.active{background:rgba(255,20,147,.22);border-color:#FF1493;color:#fff!important;box-shadow:0 0 0 2px rgba(255,20,147,.10)}
331
- .model-tab-label{font-size:12px;color:#ffffff!important;font-weight:600}
332
-
333
- .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
334
- .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
335
- .app-main-right{width:500px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
336
 
337
- #media-drop-zone{
338
- position:relative;background:#09090b;height:440px;min-height:440px;max-height:440px;overflow:hidden;
339
- }
340
- #media-drop-zone.drag-over{outline:2px solid #FF1493;outline-offset:-2px;background:rgba(255,20,147,.04)}
341
- .upload-prompt-modern{
342
- position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:20px;z-index:20;overflow:hidden;
343
- }
344
- .upload-click-area{
345
- display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;
346
- padding:28px 36px;max-width:92%;max-height:92%;border:2px dashed #3f3f46;border-radius:16px;
347
- background:rgba(255,20,147,.03);transition:all .2s ease;gap:8px;text-align:center;overflow:hidden;
348
- }
349
- .upload-click-area:hover{background:rgba(255,20,147,.08);border-color:#FF1493;transform:scale(1.02)}
350
- .upload-click-area:active{background:rgba(255,20,147,.12);transform:scale(.99)}
351
- .upload-click-area svg{width:86px;height:86px;max-width:100%;flex-shrink:0}
352
- .upload-main-text{color:#a1a1aa;font-size:14px;font-weight:600;margin-top:4px}
353
- .upload-sub-text{color:#71717a;font-size:12px}
354
-
355
- .single-preview-wrap{
356
- width:100%;height:100%;display:none;align-items:center;justify-content:center;padding:16px;overflow:hidden;
357
- }
358
- .single-preview-card{
359
- width:100%;height:100%;max-width:100%;max-height:100%;border-radius:14px;overflow:hidden;border:1px solid #27272a;background:#111114;
360
- display:flex;align-items:center;justify-content:center;position:relative;
361
- }
362
- .single-preview-card img{
363
- width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;background:#000;border:none;
364
- }
365
- .preview-overlay-actions{
366
- position:absolute;top:12px;right:12px;display:flex;gap:8px;z-index:5;
367
- }
368
- .preview-action-btn{
369
- display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:34px;padding:0 12px;background:rgba(0,0,0,.65);
370
- border:1px solid rgba(255,255,255,.14);border-radius:10px;cursor:pointer;color:#fff!important;font-size:12px;font-weight:600;transition:all .15s ease;
371
- }
372
- .preview-action-btn:hover{background:#FF1493;border-color:#FF1493}
373
-
374
- .hint-bar{
375
- background:rgba(255,20,147,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
376
- padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
377
- }
378
- .hint-bar b{color:#ff8fcf;font-weight:600}
379
- .hint-bar kbd{
380
- display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;border-radius:4px;
381
- font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
382
- }
383
-
384
- .examples-section{border-top:1px solid #27272a;padding:12px 16px}
385
- .examples-title{
386
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px;
387
- }
388
- .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
389
- .examples-scroll::-webkit-scrollbar{height:6px}
390
- .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
391
- .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
392
- .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
393
- .example-card{
394
- position:relative;flex-shrink:0;width:220px;background:#09090b;border:1px solid #27272a;border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
395
- }
396
- .example-card:hover{border-color:#FF1493;transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,20,147,.15)}
397
- .example-card.loading{opacity:.5;pointer-events:none}
398
- .example-thumb-wrap{height:120px;overflow:hidden;background:#18181b;position:relative}
399
- .example-thumb-wrap img{width:100%;height:100%;object-fit:cover}
400
- .example-media-chip{
401
- position:absolute;top:8px;left:8px;display:inline-flex;padding:3px 7px;background:rgba(0,0,0,.7);border:1px solid rgba(255,255,255,.12);
402
- border-radius:999px;font-size:10px;font-weight:700;color:#fff;letter-spacing:.5px;
403
- }
404
- .example-thumb-placeholder{
405
- width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#18181b;color:#3f3f46;font-size:11px;
406
- }
407
- .example-meta-row{padding:6px 10px;display:flex;align-items:center;gap:6px}
408
- .example-badge{
409
- display:inline-flex;padding:2px 7px;background:rgba(255,20,147,.12);border-radius:4px;font-size:10px;font-weight:600;color:#ff8fcf;
410
- font-family:'JetBrains Mono',monospace;white-space:nowrap;
411
- }
412
- .example-prompt-text{
413
- padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
414
- }
415
-
416
- .panel-card{border-bottom:1px solid #27272a}
417
- .panel-card-title{
418
- padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
419
- }
420
- .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
421
- .info-markdown{
422
- background:#09090b;border:1px solid #27272a;border-radius:8px;padding:12px 14px;color:#e4e4e7;
423
- }
424
- .info-markdown p{margin:0;color:#d4d4d8;line-height:1.6}
425
- .info-markdown strong{color:#ffffff}
426
-
427
- .toast-notification{
428
- position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);z-index:9999;padding:10px 24px;border-radius:10px;
429
- font-family:'Inter',sans-serif;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);
430
- transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
431
- }
432
- .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
433
- .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
434
- .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
435
- .toast-notification.info{background:linear-gradient(135deg,#c2187a,#FF1493);color:#fff;border:1px solid rgba(255,255,255,.15)}
436
- .toast-notification .toast-icon{font-size:16px;line-height:1}
437
- .toast-notification .toast-text{line-height:1.3}
438
-
439
- .btn-run{
440
- display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:linear-gradient(135deg,#FF1493,#c2187a);border:none;border-radius:10px;
441
- padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
442
- transition:all .2s ease;letter-spacing:-.2px;box-shadow:0 4px 16px rgba(255,20,147,.3),inset 0 1px 0 rgba(255,255,255,.1);
443
- }
444
- .btn-run:hover{
445
- background:linear-gradient(135deg,#ff3cad,#FF1493);transform:translateY(-1px);box-shadow:0 6px 24px rgba(255,20,147,.45),inset 0 1px 0 rgba(255,255,255,.15);
446
- }
447
- .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(255,20,147,.3)}
448
- #custom-run-btn,#custom-run-btn *,#run-btn-label,.btn-run,.btn-run *{
449
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
450
- }
451
-
452
- .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
453
- .output-frame .out-title,.output-frame .out-title *,#output-title-label{
454
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
455
- }
456
- .output-frame .out-title{
457
- padding:10px 20px;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
458
- display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;
459
- }
460
- .out-title-right{display:flex;gap:8px;align-items:center}
461
- .out-action-btn{
462
- display:inline-flex;align-items:center;justify-content:center;background:rgba(255,20,147,.1);border:1px solid rgba(255,20,147,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
463
- font-size:11px;font-weight:500;color:#ff8fcf!important;gap:4px;height:24px;transition:all .15s;
464
- }
465
- .out-action-btn:hover{background:rgba(255,20,147,.2);border-color:rgba(255,20,147,.35);color:#ffffff!important}
466
- .out-action-btn svg{width:12px;height:12px;fill:#ff8fcf}
467
- .output-frame .out-body{
468
- flex:1;background:#09090b;display:flex;align-items:stretch;justify-content:stretch;overflow:hidden;min-height:320px;position:relative;
469
- }
470
- .output-scroll-wrap{width:100%;height:100%;padding:0;overflow:hidden}
471
- .output-textarea{
472
- width:100%;height:320px;min-height:320px;max-height:320px;background:#09090b;color:#e4e4e7;border:none;outline:none;padding:16px 18px;font-size:13px;line-height:1.6;
473
- font-family:'JetBrains Mono',monospace;overflow:auto;resize:none;white-space:pre-wrap;
474
- }
475
- .output-textarea::placeholder{color:#52525b}
476
- .output-textarea.error-flash{box-shadow:inset 0 0 0 2px rgba(239,68,68,.6)}
477
- .modern-loader{
478
- display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
479
- }
480
- .modern-loader.active{display:flex}
481
- .modern-loader .loader-spinner{
482
- width:36px;height:36px;border:3px solid #27272a;border-top-color:#FF1493;border-radius:50%;animation:spin .8s linear infinite;
483
- }
484
- @keyframes spin{to{transform:rotate(360deg)}}
485
- .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
486
- .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
487
- .loader-bar-fill{
488
- height:100%;background:linear-gradient(90deg,#FF1493,#ff70c6,#FF1493);background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
489
- }
490
- @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
491
-
492
- .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
493
- .settings-group-title{
494
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
495
- }
496
- .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
497
- .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
498
- .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:118px;flex-shrink:0}
499
- .slider-row input[type="range"]{
500
- flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;border-radius:3px;outline:none;min-width:0;
501
- }
502
- .slider-row input[type="range"]::-webkit-slider-thumb{
503
- -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#FF1493,#c2187a);border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,20,147,.4);transition:transform .15s;
504
- }
505
- .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
506
- .slider-row input[type="range"]::-moz-range-thumb{
507
- width:16px;height:16px;background:linear-gradient(135deg,#FF1493,#c2187a);border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,20,147,.4);
508
- }
509
- .slider-row .slider-val{
510
- min-width:58px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;border-radius:6px;color:#a1a1aa;flex-shrink:0;
511
- }
512
-
513
- .app-statusbar{
514
- background:#18181b;border-top:1px solid #27272a;padding:6px 20px;display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
515
- }
516
- .app-statusbar .sb-section{
517
- padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
518
- }
519
- .app-statusbar .sb-section.sb-fixed{
520
- flex:0 0 auto;min-width:110px;text-align:center;justify-content:center;padding:3px 12px;background:rgba(255,20,147,.08);border-radius:6px;color:#ff8fcf;font-weight:500;
521
- }
522
-
523
- .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}
524
- .exp-note a{color:#ff8fcf;text-decoration:none}
525
- .exp-note a:hover{text-decoration:underline}
526
-
527
- ::-webkit-scrollbar{width:8px;height:8px}
528
- ::-webkit-scrollbar-track{background:#09090b}
529
- ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
530
- ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
531
-
532
- @media(max-width:980px){
533
- .app-main-row{flex-direction:column}
534
- .app-main-right{width:100%}
535
- .app-main-left{border-right:none;border-bottom:1px solid #27272a}
536
- }
537
- """
538
-
539
- gallery_js = r"""
540
- () => {
541
- function init() {
542
- if (window.__glmOutpostInitDone) return;
543
-
544
- const dropZone = document.getElementById('media-drop-zone');
545
- const uploadPrompt = document.getElementById('upload-prompt');
546
- const uploadClick = document.getElementById('upload-click-area');
547
- const fileInput = document.getElementById('custom-file-input');
548
- const previewWrap = document.getElementById('single-preview-wrap');
549
- const previewImg = document.getElementById('single-preview-img');
550
- const btnUpload = document.getElementById('preview-upload-btn');
551
- const btnClear = document.getElementById('preview-clear-btn');
552
- const runBtnEl = document.getElementById('custom-run-btn');
553
- const outputArea = document.getElementById('custom-output-textarea');
554
- const mediaStatus = document.getElementById('sb-media-status');
555
-
556
- if (!dropZone || !fileInput || !previewWrap || !previewImg) {
557
- setTimeout(init, 250);
558
- return;
559
- }
560
-
561
- window.__glmOutpostInitDone = true;
562
- let mediaState = null;
563
- let toastTimer = null;
564
- let examplePoller = null;
565
- let lastSeenExamplePayload = null;
566
-
567
- function showToast(message, type) {
568
- let toast = document.getElementById('app-toast');
569
- if (!toast) {
570
- toast = document.createElement('div');
571
- toast.id = 'app-toast';
572
- toast.className = 'toast-notification';
573
- toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
574
- document.body.appendChild(toast);
575
- }
576
- const icon = toast.querySelector('.toast-icon');
577
- const text = toast.querySelector('.toast-text');
578
- toast.className = 'toast-notification ' + (type || 'error');
579
- if (type === 'warning') icon.textContent = '\u26A0';
580
- else if (type === 'info') icon.textContent = '\u2139';
581
- else icon.textContent = '\u2717';
582
- text.textContent = message;
583
- if (toastTimer) clearTimeout(toastTimer);
584
- void toast.offsetWidth;
585
- toast.classList.add('visible');
586
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
587
- }
588
-
589
- function showLoader() {
590
- const l = document.getElementById('output-loader');
591
- if (l) l.classList.add('active');
592
- const sb = document.getElementById('sb-run-state');
593
- if (sb) sb.textContent = 'Processing...';
594
- }
595
- function hideLoader() {
596
- const l = document.getElementById('output-loader');
597
- if (l) l.classList.remove('active');
598
- const sb = document.getElementById('sb-run-state');
599
- if (sb) sb.textContent = 'Done';
600
- }
601
- function setRunErrorState() {
602
- const l = document.getElementById('output-loader');
603
- if (l) l.classList.remove('active');
604
- const sb = document.getElementById('sb-run-state');
605
- if (sb) sb.textContent = 'Error';
606
- }
607
-
608
- window.__hideLoader = hideLoader;
609
- window.__setRunErrorState = setRunErrorState;
610
- window.__showToast = showToast;
611
-
612
- function flashOutputError() {
613
- if (!outputArea) return;
614
- outputArea.classList.add('error-flash');
615
- setTimeout(() => outputArea.classList.remove('error-flash'), 800);
616
- }
617
-
618
- function getValueFromContainer(containerId) {
619
- const container = document.getElementById(containerId);
620
- if (!container) return '';
621
- const el = container.querySelector('textarea, input');
622
- return el ? (el.value || '') : '';
623
- }
624
-
625
- function setGradioValue(containerId, value) {
626
- const container = document.getElementById(containerId);
627
- if (!container) return false;
628
- const el = container.querySelector('textarea, input');
629
- if (!el) return false;
630
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
631
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
632
- if (ns && ns.set) {
633
- ns.set.call(el, value);
634
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
635
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
636
- return true;
637
- }
638
- return false;
639
- }
640
-
641
- function syncImageToGradio() {
642
- setGradioValue('hidden-image-b64', mediaState ? mediaState.b64 : '');
643
- if (mediaStatus) mediaStatus.textContent = mediaState ? '1 image uploaded' : 'No image uploaded';
644
- }
645
-
646
- function syncTaskToGradio(name) {
647
- setGradioValue('hidden-task-name', name);
648
- }
649
-
650
- function renderPreview() {
651
- if (!mediaState) {
652
- previewImg.src = '';
653
- previewImg.style.display = 'none';
654
- previewWrap.style.display = 'none';
655
- if (uploadPrompt) uploadPrompt.style.display = 'flex';
656
- syncImageToGradio();
657
- return;
658
- }
659
-
660
- previewWrap.style.display = 'flex';
661
- if (uploadPrompt) uploadPrompt.style.display = 'none';
662
- previewImg.src = mediaState.preview || mediaState.b64;
663
- previewImg.style.display = 'block';
664
- syncImageToGradio();
665
- }
666
-
667
- function setPreviewFromFileReader(b64, name) {
668
- mediaState = {b64, name: name || 'file', mode: 'image'};
669
- renderPreview();
670
- }
671
-
672
- function clearPreview() {
673
- mediaState = null;
674
- renderPreview();
675
- }
676
- window.__clearPreview = clearPreview;
677
-
678
- function processFile(file) {
679
- if (!file) return;
680
- if (!file.type.startsWith('image/')) {
681
- showToast('Only image files are supported', 'error');
682
- return;
683
- }
684
- const reader = new FileReader();
685
- reader.onload = (e) => setPreviewFromFileReader(e.target.result, file.name);
686
- reader.readAsDataURL(file);
687
- }
688
-
689
- if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
690
- if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
691
- if (btnClear) btnClear.addEventListener('click', clearPreview);
692
-
693
- fileInput.addEventListener('change', (e) => {
694
- const file = e.target.files && e.target.files[0] ? e.target.files[0] : null;
695
- if (file) processFile(file);
696
- e.target.value = '';
697
- });
698
-
699
- dropZone.addEventListener('dragover', (e) => {
700
- e.preventDefault();
701
- dropZone.classList.add('drag-over');
702
- });
703
- dropZone.addEventListener('dragleave', (e) => {
704
- e.preventDefault();
705
- dropZone.classList.remove('drag-over');
706
- });
707
- dropZone.addEventListener('drop', (e) => {
708
- e.preventDefault();
709
- dropZone.classList.remove('drag-over');
710
- if (e.dataTransfer.files && e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
711
- });
712
-
713
- function activateTaskTab(name) {
714
- document.querySelectorAll('.model-tab[data-task]').forEach(btn => {
715
- btn.classList.toggle('active', btn.getAttribute('data-task') === name);
716
- });
717
- syncTaskToGradio(name);
718
- }
719
-
720
- window.__activateTaskTab = activateTaskTab;
721
-
722
- document.querySelectorAll('.model-tab[data-task]').forEach(btn => {
723
- btn.addEventListener('click', () => activateTaskTab(btn.getAttribute('data-task')));
724
- });
725
-
726
- activateTaskTab('Text');
727
-
728
- function syncSlider(customId, gradioId) {
729
- const slider = document.getElementById(customId);
730
- const valSpan = document.getElementById(customId + '-val');
731
- if (!slider) return;
732
- slider.addEventListener('input', () => {
733
- if (valSpan) valSpan.textContent = slider.value;
734
- const container = document.getElementById(gradioId);
735
- if (!container) return;
736
- container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
737
- const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
738
- if (ns && ns.set) {
739
- ns.set.call(el, slider.value);
740
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
741
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
742
- }
743
- });
744
- });
745
- }
746
-
747
- syncSlider('custom-max-new-tokens', 'gradio-max-new-tokens');
748
- syncSlider('custom-gpu-duration', 'gradio-gpu-duration');
749
-
750
- function validateBeforeRun() {
751
- if (!mediaState) {
752
- showToast('Please upload an image', 'error');
753
- return false;
754
- }
755
- const currentTask = (document.querySelector('.model-tab.active') || {}).dataset?.task;
756
- if (!currentTask) {
757
- showToast('Please select a task', 'error');
758
- return false;
759
- }
760
- return true;
761
- }
762
-
763
- window.__clickGradioRunBtn = function() {
764
- if (!validateBeforeRun()) return;
765
- syncImageToGradio();
766
- const activeTask = document.querySelector('.model-tab.active');
767
- if (activeTask) syncTaskToGradio(activeTask.getAttribute('data-task'));
768
- if (outputArea) outputArea.value = '';
769
- showLoader();
770
- setTimeout(() => {
771
- const gradioBtn = document.getElementById('gradio-run-btn');
772
- if (!gradioBtn) {
773
- setRunErrorState();
774
- if (outputArea) outputArea.value = '[ERROR] Run button not found.';
775
- showToast('Run button not found', 'error');
776
- return;
777
- }
778
- const btn = gradioBtn.querySelector('button');
779
- if (btn) btn.click(); else gradioBtn.click();
780
- }, 180);
781
- };
782
-
783
- if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
784
-
785
- const copyBtn = document.getElementById('copy-output-btn');
786
- if (copyBtn) {
787
- copyBtn.addEventListener('click', async () => {
788
- try {
789
- const text = outputArea ? outputArea.value : '';
790
- if (!text.trim()) {
791
- showToast('No output to copy', 'warning');
792
- flashOutputError();
793
- return;
794
- }
795
- await navigator.clipboard.writeText(text);
796
- showToast('Output copied to clipboard', 'info');
797
- } catch(e) {
798
- showToast('Copy failed', 'error');
799
- }
800
- });
801
- }
802
-
803
- const saveBtn = document.getElementById('save-output-btn');
804
- if (saveBtn) {
805
- saveBtn.addEventListener('click', () => {
806
- const text = outputArea ? outputArea.value : '';
807
- if (!text.trim()) {
808
- showToast('No output to save', 'warning');
809
- flashOutputError();
810
- return;
811
- }
812
- const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
813
- const a = document.createElement('a');
814
- a.href = URL.createObjectURL(blob);
815
- a.download = 'glm_ocr_output.txt';
816
- document.body.appendChild(a);
817
- a.click();
818
- setTimeout(() => {
819
- URL.revokeObjectURL(a.href);
820
- document.body.removeChild(a);
821
- }, 200);
822
- showToast('Output saved', 'info');
823
- });
824
- }
825
 
826
- function applyExamplePayload(raw) {
827
- try {
828
- const data = JSON.parse(raw);
829
- if (data.status !== 'ok') return;
830
-
831
- if (data.task) activateTaskTab(data.task);
832
-
833
- mediaState = {
834
- b64: data.media || '',
835
- preview: data.media || '',
836
- name: data.name || 'example_file',
837
- mode: 'image'
838
- };
839
- renderPreview();
840
-
841
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
842
- showToast('Example loaded', 'info');
843
- } catch (e) {
844
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
845
- }
846
- }
847
 
848
- function startExamplePolling() {
849
- if (examplePoller) clearInterval(examplePoller);
850
- let attempts = 0;
851
- examplePoller = setInterval(() => {
852
- attempts += 1;
853
- const current = getValueFromContainer('example-result-data');
854
- if (current && current !== lastSeenExamplePayload) {
855
- lastSeenExamplePayload = current;
856
- clearInterval(examplePoller);
857
- examplePoller = null;
858
- applyExamplePayload(current);
859
- return;
860
- }
861
- if (attempts >= 100) {
862
- clearInterval(examplePoller);
863
- examplePoller = null;
864
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
865
- showToast('Example load timed out', 'error');
866
- }
867
- }, 120);
868
- }
869
 
870
- function triggerExampleLoad(idx) {
871
- const btnWrap = document.getElementById('example-load-btn');
872
- const btn = btnWrap ? (btnWrap.querySelector('button') || btnWrap) : null;
873
- if (!btn) return;
 
 
 
 
 
 
 
874
 
875
- let attempts = 0;
 
 
 
876
 
877
- function writeIdxAndClick() {
878
- attempts += 1;
 
 
 
 
 
 
879
 
880
- const ok1 = setGradioValue('example-idx-input', String(idx));
881
- setGradioValue('example-result-data', '');
882
- const currentVal = getValueFromContainer('example-idx-input');
883
 
884
- if (ok1 && currentVal === String(idx)) {
885
- btn.click();
886
- startExamplePolling();
887
- return;
888
- }
889
 
890
- if (attempts < 30) {
891
- setTimeout(writeIdxAndClick, 100);
892
- } else {
893
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
894
- showToast('Failed to initialize example loader', 'error');
895
- }
896
- }
897
 
898
- writeIdxAndClick();
899
- }
 
 
 
 
 
 
 
 
 
900
 
901
- document.querySelectorAll('.example-card[data-idx]').forEach(card => {
902
- card.addEventListener('click', () => {
903
- const idx = card.getAttribute('data-idx');
904
- if (idx === null || idx === undefined || idx === '') return;
905
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
906
- card.classList.add('loading');
907
- showToast('Loading example...', 'info');
908
- triggerExampleLoad(idx);
909
- });
910
- });
911
-
912
- const observerTarget = document.getElementById('example-result-data');
913
- if (observerTarget) {
914
- const obs = new MutationObserver(() => {
915
- const current = getValueFromContainer('example-result-data');
916
- if (!current || current === lastSeenExamplePayload) return;
917
- lastSeenExamplePayload = current;
918
- if (examplePoller) {
919
- clearInterval(examplePoller);
920
- examplePoller = null;
921
- }
922
- applyExamplePayload(current);
923
- });
924
- obs.observe(observerTarget, {childList:true, subtree:true, characterData:true, attributes:true});
925
- }
926
 
927
- if (outputArea) outputArea.value = '';
928
- const sb = document.getElementById('sb-run-state');
929
- if (sb) sb.textContent = 'Ready';
930
- if (mediaStatus) mediaStatus.textContent = 'No image uploaded';
931
- }
932
- init();
933
- }
934
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
935
 
936
- wire_outputs_js = r"""
937
- () => {
938
- function watchOutputs() {
939
- const resultContainer = document.getElementById('gradio-result');
940
- const outArea = document.getElementById('custom-output-textarea');
941
- if (!resultContainer || !outArea) { setTimeout(watchOutputs, 500); return; }
942
 
943
- let lastText = '';
 
 
 
 
 
944
 
945
- function isErrorText(val) {
946
- return typeof val === 'string' && val.trim().startsWith('[ERROR]');
947
- }
 
 
 
 
 
 
 
 
 
 
 
948
 
949
- function syncOutput() {
950
- const el = resultContainer.querySelector('textarea') || resultContainer.querySelector('input');
951
- if (!el) return;
952
- const val = el.value || '';
953
- if (val !== lastText) {
954
- lastText = val;
955
- outArea.value = val;
956
- outArea.scrollTop = outArea.scrollHeight;
957
-
958
- if (val.trim()) {
959
- if (isErrorText(val)) {
960
- if (window.__setRunErrorState) window.__setRunErrorState();
961
- if (window.__showToast) window.__showToast('Inference failed', 'error');
962
- } else {
963
- if (window.__hideLoader) window.__hideLoader();
964
- }
965
- }
966
- }
967
- }
968
 
969
- const observer = new MutationObserver(syncOutput);
970
- observer.observe(resultContainer, {childList:true, subtree:true, characterData:true, attributes:true});
971
- setInterval(syncOutput, 500);
972
- }
973
- watchOutputs();
974
- }
975
- """
976
-
977
- THUNDER_LOGO_SVG = """
978
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
979
- <path d="M13 2L5 13h5l-1 9 8-11h-5l1-9z" fill="white"/>
980
- </svg>
981
- """
982
-
983
- UPLOAD_PREVIEW_SVG = """
984
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
985
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#FF1493" stroke-width="2" stroke-dasharray="4 3"/>
986
- <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,20,147,0.15)" stroke="#FF1493" stroke-width="1.5"/>
987
- <circle cx="28" cy="30" r="6" fill="rgba(255,20,147,0.2)" stroke="#FF1493" stroke-width="1.5"/>
988
- </svg>
989
- """
990
-
991
- 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>"""
992
- 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>"""
993
-
994
- TASK_TABS_HTML = "".join([
995
- f'<button class="model-tab{" active" if t == "Text" else ""}" data-task="{t}"><span class="model-tab-label">{t}</span></button>'
996
- for t in TASK_CHOICES
997
- ])
998
-
999
- with gr.Blocks() as demo:
1000
- hidden_image_b64 = gr.Textbox(value="", elem_id="hidden-image-b64", elem_classes="hidden-input", container=False)
1001
- hidden_task_name = gr.Textbox(value="Text", elem_id="hidden-task-name", elem_classes="hidden-input", container=False)
1002
-
1003
- max_new_tokens = gr.Slider(
1004
- minimum=1,
1005
- maximum=MAX_MAX_NEW_TOKENS,
1006
- step=1,
1007
- value=DEFAULT_MAX_NEW_TOKENS,
1008
- elem_id="gradio-max-new-tokens",
1009
- elem_classes="hidden-input",
1010
- container=False,
1011
  )
1012
- gpu_duration_state = gr.Number(value=60, elem_id="gradio-gpu-duration", elem_classes="hidden-input", container=False)
1013
-
1014
- result = gr.Textbox(value="", elem_id="gradio-result", elem_classes="hidden-input", container=False)
1015
-
1016
- example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1017
- example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1018
- example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
1019
-
1020
- gr.HTML(f"""
1021
- <div class="app-shell">
1022
- <div class="app-header">
1023
- <div class="app-header-left">
1024
- <div class="app-logo">{THUNDER_LOGO_SVG}</div>
1025
- <span class="app-title">GLM-OCR</span>
1026
- <span class="app-badge">vision enabled</span>
1027
- <span class="app-badge fast">Image Inference</span>
1028
- </div>
1029
- </div>
1030
-
1031
- <div class="model-tabs-bar">
1032
- {TASK_TABS_HTML}
1033
- </div>
1034
-
1035
- <div class="app-main-row">
1036
- <div class="app-main-left">
1037
- <div id="media-drop-zone">
1038
- <div id="upload-prompt" class="upload-prompt-modern">
1039
- <div id="upload-click-area" class="upload-click-area">
1040
- {UPLOAD_PREVIEW_SVG}
1041
- <span id="upload-main-text" class="upload-main-text">Click or drag an image here</span>
1042
- <span id="upload-sub-text" class="upload-sub-text">Upload one image for OCR inference</span>
1043
- </div>
1044
- </div>
1045
-
1046
- <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
1047
-
1048
- <div id="single-preview-wrap" class="single-preview-wrap">
1049
- <div class="single-preview-card">
1050
- <img id="single-preview-img" src="" alt="Preview" style="display:none;">
1051
- <div class="preview-overlay-actions">
1052
- <button id="preview-upload-btn" class="preview-action-btn" title="Replace">Upload</button>
1053
- <button id="preview-clear-btn" class="preview-action-btn" title="Clear">Clear</button>
1054
- </div>
1055
- </div>
1056
- </div>
1057
- </div>
1058
-
1059
- <div class="hint-bar">
1060
- <b>Mode:</b> OCR image inference only &nbsp;&middot;&nbsp;
1061
- <b>Task:</b> Switch between Text, Formula, and Table &nbsp;&middot;&nbsp;
1062
- <kbd>Clear</kbd> removes the current image
1063
- </div>
1064
-
1065
- <div class="examples-section">
1066
- <div class="examples-title">Quick Examples</div>
1067
- <div class="examples-scroll">
1068
- {EXAMPLE_CARDS_HTML}
1069
- </div>
1070
- </div>
1071
- </div>
1072
-
1073
- <div class="app-main-right">
1074
- <div class="panel-card">
1075
- <div id="instruction-title" class="panel-card-title">OCR Task</div>
1076
- <div class="panel-card-body">
1077
- <div class="info-markdown">
1078
- <p><strong>Use the task tabs above</strong> to run <strong>Text Recognition</strong>, <strong>Formula Recognition</strong>, or <strong>Table Recognition</strong> on the uploaded image.</p>
1079
- </div>
1080
- </div>
1081
- </div>
1082
-
1083
- <div style="padding:12px 20px;">
1084
- <button id="custom-run-btn" class="btn-run">
1085
- <span id="run-btn-label">Run Inference</span>
1086
- </button>
1087
- </div>
1088
-
1089
- <div class="output-frame">
1090
- <div class="out-title">
1091
- <span id="output-title-label">Raw Output Stream</span>
1092
- <div class="out-title-right">
1093
- <button id="copy-output-btn" class="out-action-btn" title="Copy">{COPY_SVG} Copy</button>
1094
- <button id="save-output-btn" class="out-action-btn" title="Save">{SAVE_SVG} Save File</button>
1095
- </div>
1096
- </div>
1097
- <div class="out-body">
1098
- <div class="modern-loader" id="output-loader">
1099
- <div class="loader-spinner"></div>
1100
- <div class="loader-text">Running inference...</div>
1101
- <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1102
- </div>
1103
- <div class="output-scroll-wrap">
1104
- <textarea id="custom-output-textarea" class="output-textarea" placeholder="Raw output will appear here..." readonly></textarea>
1105
- </div>
1106
- </div>
1107
- </div>
1108
-
1109
- <div class="settings-group">
1110
- <div class="settings-group-title">Advanced Settings</div>
1111
- <div class="settings-group-body">
1112
- <div class="slider-row">
1113
- <label>Max new tokens</label>
1114
- <input type="range" id="custom-max-new-tokens" min="1" max="{MAX_MAX_NEW_TOKENS}" step="1" value="{DEFAULT_MAX_NEW_TOKENS}">
1115
- <span class="slider-val" id="custom-max-new-tokens-val">{DEFAULT_MAX_NEW_TOKENS}</span>
1116
- </div>
1117
- <div class="slider-row">
1118
- <label>GPU Duration (seconds)</label>
1119
- <input type="range" id="custom-gpu-duration" min="60" max="300" step="30" value="60">
1120
- <span class="slider-val" id="custom-gpu-duration-val">60</span>
1121
- </div>
1122
- </div>
1123
- </div>
1124
- </div>
1125
- </div>
1126
-
1127
- <div class="exp-note">
1128
- Experimental GLM-OCR workspace
1129
- </div>
1130
-
1131
- <div class="app-statusbar">
1132
- <div class="sb-section" id="sb-media-status">No image uploaded</div>
1133
- <div class="sb-section sb-fixed" id="sb-run-state">Ready</div>
1134
- </div>
1135
- </div>
1136
- """)
1137
-
1138
- run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1139
-
1140
- demo.load(fn=noop, inputs=None, outputs=None, js=gallery_js)
1141
- demo.load(fn=noop, inputs=None, outputs=None, js=wire_outputs_js)
1142
 
1143
  run_btn.click(
1144
  fn=run_router,
1145
  inputs=[
1146
- hidden_task_name,
1147
- hidden_image_b64,
 
 
 
1148
  max_new_tokens,
1149
  gpu_duration_state,
1150
  ],
1151
  outputs=[result],
1152
- js=r"""(task, img, mnt, gd) => {
1153
- const taskEl = document.querySelector('.model-tab.active');
1154
- const taskVal = taskEl ? taskEl.getAttribute('data-task') : task;
1155
-
1156
- let imgVal = img;
1157
- const imgContainer = document.getElementById('hidden-image-b64');
1158
- if (imgContainer) {
1159
- const inner = imgContainer.querySelector('textarea, input');
1160
- if (inner) imgVal = inner.value;
1161
- }
1162
-
1163
- return [taskVal, imgVal, mnt, gd];
1164
- }""",
1165
  )
1166
 
1167
- example_load_btn.click(
1168
- fn=load_example_data,
1169
- inputs=[example_idx],
1170
- outputs=[example_result],
1171
- queue=False,
1172
- )
1173
 
1174
  if __name__ == "__main__":
1175
- demo.queue(max_size=50).launch(
1176
- css=css,
1177
- mcp_server=True,
1178
- ssr_mode=False,
1179
- show_error=True,
1180
- allowed_paths=["examples"],
1181
- )
 
 
 
 
 
 
 
1
  import gc
2
+ import importlib.util
3
+ import inspect
4
+ import re
5
  import tempfile
6
+ import time
7
  from io import BytesIO
8
+ from pathlib import Path
9
+ from threading import Lock, Thread
10
 
11
  import gradio as gr
12
  import spaces
13
  import torch
14
+ import transformers
15
  from PIL import Image, ImageOps
16
 
17
+ try:
18
+ import fitz
19
+ except ImportError:
20
+ fitz = None
21
+
22
  from transformers import (
 
23
  AutoModelForImageTextToText,
24
+ AutoProcessor,
25
  TextIteratorStreamer,
26
  )
27
 
 
29
  DEFAULT_MAX_NEW_TOKENS = 4096
30
 
31
  MODEL_PATH = "zai-org/GLM-OCR"
32
+ MIN_TRANSFORMERS_VERSION = "5.0.0"
33
+ UPGRADE_TRANSFORMERS_CMD = 'pip install -U "transformers>=5.0.0"'
34
+ MODEL_CARD_TRANSFORMERS_CMD = "pip install git+https://github.com/huggingface/transformers.git"
35
+
36
+ BASE_DIR = Path(__file__).resolve().parent
37
+ DATA_DIR = BASE_DIR / "data"
38
+
39
+ INPUT_MODES = ["Upload PDF", "PDF from data", "All PDFs in data"]
40
+ DEFAULT_INPUT_MODE = "Upload PDF"
41
+
42
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
43
  print("Using device:", device)
44
 
45
+ processor = None
46
+ model = None
47
+ model_load_error = None
48
+ model_init_lock = Lock()
49
+
50
+
51
+ def parse_version_triplet(version_text):
52
+ parts = [int(part) for part in re.findall(r"\d+", str(version_text))[:3]]
53
+ while len(parts) < 3:
54
+ parts.append(0)
55
+ return tuple(parts)
56
+
57
+
58
+ def build_glm_ocr_dependency_error():
59
+ transformers_version = getattr(transformers, "__version__", "unknown")
60
+ try:
61
+ import torchvision # noqa: F401
62
+ except ImportError:
63
+ torch_version = getattr(torch, "__version__", "unknown")
64
+ return (
65
+ "GLM-OCR requires torchvision, but it is not installed in the active environment. "
66
+ f"Install a torchvision build compatible with torch {torch_version} "
67
+ "(for example, `pip install torchvision`) and restart the app."
68
+ )
69
+ except Exception as exc:
70
+ return (
71
+ "GLM-OCR could not import torchvision from the active environment. "
72
+ f"Resolve the torchvision installation issue ({exc}) and restart the app."
73
+ )
74
+
75
+ has_glm_ocr_support = importlib.util.find_spec("transformers.models.glm_ocr") is not None
76
+ if has_glm_ocr_support:
77
+ return None
78
+
79
+ version_triplet = parse_version_triplet(transformers_version)
80
+ minimum_triplet = parse_version_triplet(MIN_TRANSFORMERS_VERSION)
81
+ if version_triplet and version_triplet < minimum_triplet:
82
+ return (
83
+ f"GLM-OCR support is not available in transformers {transformers_version}. "
84
+ f"Upgrade to {MIN_TRANSFORMERS_VERSION}+ with `{UPGRADE_TRANSFORMERS_CMD}` or use the "
85
+ f"model card recommendation `{MODEL_CARD_TRANSFORMERS_CMD}`, then restart the app."
86
+ )
87
+
88
+ return (
89
+ f"GLM-OCR support is not available in the installed transformers build ({transformers_version}). "
90
+ f"Upgrade transformers with `{UPGRADE_TRANSFORMERS_CMD}` or use the model card recommendation "
91
+ f"`{MODEL_CARD_TRANSFORMERS_CMD}`, then restart the app."
92
+ )
93
+
94
+
95
+ def load_glm_ocr_components():
96
+ global processor, model, model_load_error
97
+
98
+ if model_load_error is not None:
99
+ raise RuntimeError(model_load_error)
100
+
101
+ if processor is not None and model is not None:
102
+ return processor, model
103
+
104
+ with model_init_lock:
105
+ if model_load_error is not None:
106
+ raise RuntimeError(model_load_error)
107
+
108
+ if processor is not None and model is not None:
109
+ return processor, model
110
+
111
+ dependency_error = build_glm_ocr_dependency_error()
112
+ if dependency_error is not None:
113
+ model_load_error = dependency_error
114
+ print(model_load_error)
115
+ raise RuntimeError(model_load_error)
116
+
117
+ try:
118
+ processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=True)
119
+
120
+ model_kwargs = {
121
+ "pretrained_model_name_or_path": MODEL_PATH,
122
+ "torch_dtype": torch.bfloat16 if torch.cuda.is_available() else torch.float32,
123
+ "trust_remote_code": True,
124
+ }
125
+ if torch.cuda.is_available():
126
+ model_kwargs["device_map"] = "auto"
127
+
128
+ model = AutoModelForImageTextToText.from_pretrained(**model_kwargs).eval()
129
+ if not torch.cuda.is_available():
130
+ model = model.to(device)
131
+ except Exception as exc:
132
+ model_load_error = (
133
+ f"Failed to load {MODEL_PATH}: {exc}. "
134
+ "If the error mentions an unrecognized processor or model type, upgrade transformers with "
135
+ f"`{UPGRADE_TRANSFORMERS_CMD}` or follow the model card recommendation "
136
+ f"`{MODEL_CARD_TRANSFORMERS_CMD}`. Restart the app after upgrading."
137
+ )
138
+ print(model_load_error)
139
+ raise RuntimeError(model_load_error) from exc
140
+
141
+ return processor, model
142
+
143
 
144
  TASK_PROMPTS = {
145
  "Text": "Text Recognition:",
 
149
 
150
  TASK_CHOICES = list(TASK_PROMPTS.keys())
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ def list_data_pdfs():
154
+ if not DATA_DIR.exists():
155
+ return []
156
 
157
+ return sorted(
158
+ [path for path in DATA_DIR.iterdir() if path.is_file() and path.suffix.lower() == ".pdf"],
159
+ key=lambda path: path.name.lower(),
160
+ )
161
 
 
 
 
 
 
 
162
 
163
+ def build_data_folder_note():
164
+ pdf_paths = list_data_pdfs()
165
+ folder_line = f"Data folder: `{DATA_DIR}`"
166
+ if not pdf_paths:
167
+ return folder_line + "\n\nNo PDF files were found."
168
 
169
+ lines = "\n".join(f"- `{path.name}`" for path in pdf_paths)
170
+ return folder_line + "\n\nAvailable PDFs:\n" + lines
171
+
172
+
173
+ def refresh_pdf_dropdown():
174
+ pdf_names = [path.name for path in list_data_pdfs()]
175
+ value = pdf_names[0] if pdf_names else None
176
+ return gr.update(choices=pdf_names, value=value), build_data_folder_note()
177
+
178
+
179
+ def resolve_data_pdf_path(pdf_name):
180
+ if not pdf_name:
181
+ raise ValueError("Please choose a PDF from the data folder.")
182
+
183
+ for path in list_data_pdfs():
184
+ if path.name == pdf_name:
185
+ return path
186
+
187
+ raise ValueError(f"Could not find `{pdf_name}` in `{DATA_DIR}`.")
188
+
189
+
190
+ def resolve_uploaded_pdf_path(uploaded_pdf):
191
+ if not uploaded_pdf:
192
+ raise ValueError("Please upload a PDF first.")
193
+
194
+ pdf_path = Path(str(uploaded_pdf))
195
+ if not pdf_path.exists():
196
+ raise ValueError("The uploaded PDF could not be read. Please upload it again.")
197
+
198
+ if pdf_path.suffix.lower() != ".pdf":
199
+ raise ValueError("Please upload a PDF file.")
200
+
201
+ return pdf_path
202
+
203
+
204
+ def cache_uploaded_pdf(uploaded_pdf):
205
+ if not uploaded_pdf:
206
+ return None, "No PDF uploaded yet."
207
+
208
+ pdf_path = resolve_uploaded_pdf_path(uploaded_pdf)
209
+ return str(pdf_path), f"Selected upload: `{pdf_path.name}`"
210
+
211
+
212
+ def normalize_image(image: Image.Image):
213
+ if image.mode in ("RGBA", "LA", "P"):
214
+ image = image.convert("RGB")
215
+ return ImageOps.exif_transpose(image)
216
+
217
+
218
+ def pdf_page_to_image(pdf_path, page_number):
219
+ if fitz is None:
220
+ raise RuntimeError("PDF support requires PyMuPDF (`fitz`) to be installed.")
221
+
222
+ page_number = int(page_number)
223
+ if page_number < 1:
224
+ raise ValueError("Page number must be 1 or higher.")
225
+
226
+ with fitz.open(pdf_path) as document:
227
+ if page_number > document.page_count:
228
+ raise ValueError(
229
+ f"Page {page_number} is outside the page count for {Path(pdf_path).name} "
230
+ f"({document.page_count} pages)."
231
+ )
232
+ page = document.load_page(page_number - 1)
233
+ pixmap = page.get_pixmap(matrix=fitz.Matrix(2, 2), alpha=False)
234
+ image = Image.open(BytesIO(pixmap.tobytes("png"))).convert("RGB")
235
+ return ImageOps.exif_transpose(image)
236
+
237
+
238
+ def get_pdf_page_count(pdf_path):
239
+ if fitz is None:
240
+ raise RuntimeError("PDF support requires PyMuPDF (`fitz`) to be installed.")
241
+
242
+ with fitz.open(pdf_path) as document:
243
+ return document.page_count
244
+
245
+
246
+ def parse_page_selection(page_selection, page_count):
247
+ selection = str(page_selection or "1").strip().lower()
248
+ if not selection:
249
+ selection = "1"
250
+ if selection == "all":
251
+ return list(range(1, page_count + 1))
252
+
253
+ pages = []
254
+ for raw_part in selection.split(","):
255
+ part = raw_part.strip()
256
+ if not part:
257
+ continue
258
+ if "-" in part:
259
+ start_str, end_str = [token.strip() for token in part.split("-", 1)]
260
+ start_page = int(start_str)
261
+ end_page = int(end_str)
262
+ if start_page > end_page:
263
+ raise ValueError(f"Invalid page range: {part}")
264
+ pages.extend(range(start_page, end_page + 1))
265
  else:
266
+ pages.append(int(part))
267
+
268
+ if not pages:
269
+ raise ValueError("Please provide page numbers such as `1`, `1,3`, `2-5`, or `all`.")
270
+
271
+ deduped_pages = []
272
+ seen = set()
273
+ for page in pages:
274
+ if page < 1 or page > page_count:
275
+ raise ValueError(f"Page {page} is outside the PDF page count ({page_count}).")
276
+ if page not in seen:
277
+ deduped_pages.append(page)
278
+ seen.add(page)
279
+ return deduped_pages
280
+
281
+
282
+ def join_output_blocks(blocks):
283
+ return "\n\n".join(block.strip() for block in blocks if str(block).strip()).strip()
284
+
285
+
286
+ def stream_pdf_text(pdf_path, task, page_selection, max_new_tokens):
287
+ page_count = get_pdf_page_count(pdf_path)
288
+ selected_pages = parse_page_selection(page_selection, page_count)
289
+ include_page_headers = len(selected_pages) > 1
290
+ combined_sections = []
291
+
292
+ for page_number in selected_pages:
293
+ page_title = f"Page {page_number}"
294
+ try:
295
+ image = pdf_page_to_image(pdf_path, page_number)
296
+ page_text = ""
297
+ for chunk in process_image_stream(
298
+ image=image,
299
+ task=task,
300
+ max_new_tokens=max_new_tokens,
301
+ ):
302
+ page_text = chunk.strip()
303
+ current_block = f"{page_title}\n{page_text}" if include_page_headers else page_text
304
+ yield join_output_blocks(combined_sections + [current_block])
305
+
306
+ if not page_text.strip():
307
+ page_text = "[ERROR] No output was generated."
308
+ except Exception as exc:
309
+ page_text = f"[ERROR] {str(exc)}"
310
+
311
+ final_block = f"{page_title}\n{page_text}" if include_page_headers else page_text
312
+ combined_sections.append(final_block)
313
+ yield join_output_blocks(combined_sections)
314
+
315
+
316
+ def stream_data_folder_pdfs(task, page_selection, max_new_tokens):
317
+ pdf_paths = list_data_pdfs()
318
+ if not pdf_paths:
319
+ yield f"[ERROR] No PDF files were found in `{DATA_DIR}`."
320
+ return
321
+
322
+ combined_files = []
323
+ for path in pdf_paths:
324
+ pdf_text = ""
325
+ try:
326
+ for partial in stream_pdf_text(path, task, page_selection, max_new_tokens):
327
+ pdf_text = partial
328
+ yield join_output_blocks(combined_files + [f"File: {path.name}\n{pdf_text}"])
329
+
330
+ if not pdf_text.strip():
331
+ pdf_text = "[ERROR] No output was generated."
332
+ file_block = f"File: {path.name}\n{pdf_text}"
333
+ except Exception as exc:
334
+ file_block = f"File: {path.name}\n[ERROR] {str(exc)}"
335
+ yield join_output_blocks(combined_files + [file_block])
336
+
337
+ combined_files.append(file_block)
338
+ yield join_output_blocks(combined_files)
339
 
340
 
341
  def calc_timeout_generic(*args, **kwargs):
 
348
  return 60
349
 
350
 
 
351
  def process_image_stream(image, task, max_new_tokens=DEFAULT_MAX_NEW_TOKENS, gpu_timeout=60):
352
+ del gpu_timeout
353
  tmp_path = None
354
  try:
355
  if image is None:
 
360
  yield "[ERROR] Invalid OCR task selected."
361
  return
362
 
363
+ processor_obj, model_obj = load_glm_ocr_components()
364
+ image = normalize_image(image)
 
365
 
366
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
367
  image.save(tmp.name, "PNG")
368
  tmp_path = tmp.name
369
  tmp.close()
370
 
371
+ prompt = TASK_PROMPTS[task]
 
372
  messages = [
373
  {
374
  "role": "user",
 
379
  }
380
  ]
381
 
382
+ inputs = processor_obj.apply_chat_template(
383
  messages,
384
  tokenize=True,
385
  add_generation_prompt=True,
 
388
  )
389
 
390
  inputs.pop("token_type_ids", None)
391
+ inputs = {key: value.to(model_obj.device) if hasattr(value, "to") else value for key, value in inputs.items()}
392
 
393
  streamer = TextIteratorStreamer(
394
+ processor_obj.tokenizer if hasattr(processor_obj, "tokenizer") else processor_obj,
395
  skip_prompt=True,
396
  skip_special_tokens=True,
397
  )
398
 
399
  generation_error = {"error": None}
 
400
  generation_kwargs = {
401
  **inputs,
402
  "streamer": streamer,
403
  "max_new_tokens": int(max_new_tokens),
404
  }
405
 
406
+ def run_generation():
407
  try:
408
+ model_obj.generate(**generation_kwargs)
409
+ except Exception as exc:
410
+ generation_error["error"] = exc
411
  try:
412
  streamer.end()
413
  except Exception:
414
  pass
415
 
416
+ thread = Thread(target=run_generation, daemon=True)
417
  thread.start()
418
 
419
  buffer = ""
 
425
  thread.join(timeout=1.0)
426
 
427
  if generation_error["error"] is not None:
428
+ error_message = f"[ERROR] Inference failed: {generation_error['error']}"
429
  if buffer.strip():
430
+ yield buffer.strip() + "\n\n" + error_message
431
  else:
432
+ yield error_message
433
  return
434
 
435
  if not buffer.strip():
436
  yield "[ERROR] No output was generated."
437
+ except Exception as exc:
438
+ yield f"[ERROR] {str(exc)}"
 
439
  finally:
440
+ if tmp_path and Path(tmp_path).exists():
441
  try:
442
+ Path(tmp_path).unlink()
443
  except Exception:
444
  pass
445
  gc.collect()
 
447
  torch.cuda.empty_cache()
448
 
449
 
450
+ def toggle_input_mode(mode):
451
+ mode = str(mode or DEFAULT_INPUT_MODE)
452
+ if mode == "Upload PDF":
453
+ help_text = "Click the upload button to choose a PDF, then enter pages like `1`, `1,3`, `2-5`, or `all`."
454
+ return (
455
+ gr.update(visible=True),
456
+ gr.update(visible=True),
457
+ gr.update(visible=False),
458
+ gr.update(visible=True),
459
+ gr.update(visible=False),
460
+ gr.update(visible=False),
461
+ gr.update(value=help_text),
462
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
+ if mode == "All PDFs in data":
465
+ help_text = (
466
+ "Run OCR on every PDF in the data folder. Page selections such as `1`, `1,3`, `2-5`, "
467
+ "or `all` are applied to each file."
468
+ )
469
+ return (
470
+ gr.update(visible=False),
471
+ gr.update(visible=False),
472
+ gr.update(visible=False),
473
+ gr.update(visible=True),
474
+ gr.update(visible=True),
475
+ gr.update(visible=True),
476
+ gr.update(value=help_text),
477
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ help_text = (
480
+ "Choose one PDF from the data folder and enter pages like `1`, `1,3`, `2-5`, or `all`."
481
+ )
482
+ return (
483
+ gr.update(visible=False),
484
+ gr.update(visible=False),
485
+ gr.update(visible=True),
486
+ gr.update(visible=True),
487
+ gr.update(visible=True),
488
+ gr.update(visible=True),
489
+ gr.update(value=help_text),
490
+ )
 
 
 
 
 
 
 
 
 
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
+ @spaces.GPU(duration=calc_timeout_generic)
494
+ def run_router(input_mode, task, uploaded_pdf, pdf_name, page_selection, max_new_tokens_v, gpu_timeout_v):
495
+ try:
496
+ mode = str(input_mode or DEFAULT_INPUT_MODE)
497
+ if mode == "All PDFs in data":
498
+ yield from stream_data_folder_pdfs(
499
+ task=task,
500
+ page_selection=page_selection,
501
+ max_new_tokens=max_new_tokens_v,
502
+ )
503
+ return
504
 
505
+ if mode == "Upload PDF":
506
+ pdf_path = resolve_uploaded_pdf_path(uploaded_pdf)
507
+ else:
508
+ pdf_path = resolve_data_pdf_path(pdf_name)
509
 
510
+ yield from stream_pdf_text(
511
+ pdf_path=pdf_path,
512
+ task=task,
513
+ page_selection=page_selection,
514
+ max_new_tokens=max_new_tokens_v,
515
+ )
516
+ except Exception as exc:
517
+ yield f"[ERROR] {str(exc)}"
518
 
 
 
 
519
 
520
+ available_pdfs = [path.name for path in list_data_pdfs()]
521
+ default_pdf = available_pdfs[0] if available_pdfs else None
 
 
 
522
 
523
+ with gr.Blocks(title="GLM-OCR") as demo:
524
+ gr.Markdown("# GLM-OCR")
525
+ gr.Markdown("Upload a PDF with the button below or run OCR on PDFs stored in `glmocr/data`.")
 
 
 
 
526
 
527
+ with gr.Row():
528
+ input_mode = gr.Radio(
529
+ choices=INPUT_MODES,
530
+ value=DEFAULT_INPUT_MODE,
531
+ label="Input mode",
532
+ )
533
+ task = gr.Radio(
534
+ choices=TASK_CHOICES,
535
+ value="Text",
536
+ label="OCR task",
537
+ )
538
 
539
+ input_help = gr.Markdown(
540
+ "Click the upload button to choose a PDF, then enter pages like `1`, `1,3`, `2-5`, or `all`."
541
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
543
+ with gr.Column():
544
+ uploaded_pdf_state = gr.State(None)
545
+ upload_pdf_btn = gr.UploadButton(
546
+ "Upload PDF",
547
+ file_types=[".pdf"],
548
+ file_count="single",
549
+ type="filepath",
550
+ visible=True,
551
+ )
552
+ uploaded_pdf_status = gr.Markdown("No PDF uploaded yet.", visible=True)
553
+ pdf_dropdown = gr.Dropdown(
554
+ choices=available_pdfs,
555
+ value=default_pdf,
556
+ label="PDF from data folder",
557
+ visible=False,
558
+ )
559
+ page_selection = gr.Textbox(
560
+ value="1",
561
+ label="Pages",
562
+ placeholder="Examples: 1, 1,3, 2-5, all",
563
+ visible=True,
564
+ )
565
+ data_folder_note = gr.Markdown(build_data_folder_note(), visible=False)
566
+ refresh_pdfs_btn = gr.Button("Refresh PDF list", visible=False)
567
+
568
+ with gr.Row():
569
+ max_new_tokens = gr.Slider(
570
+ minimum=1,
571
+ maximum=MAX_MAX_NEW_TOKENS,
572
+ step=1,
573
+ value=DEFAULT_MAX_NEW_TOKENS,
574
+ label="Max new tokens",
575
+ )
576
+ gpu_duration_state = gr.Number(
577
+ value=60,
578
+ precision=0,
579
+ label="GPU duration (seconds)",
580
+ )
581
 
582
+ run_btn = gr.Button("Run OCR", variant="primary")
583
+ result = gr.Textbox(label="OCR output", lines=24, max_lines=40)
 
 
 
 
584
 
585
+ demo.load(
586
+ fn=refresh_pdf_dropdown,
587
+ inputs=None,
588
+ outputs=[pdf_dropdown, data_folder_note],
589
+ queue=False,
590
+ )
591
 
592
+ input_mode.change(
593
+ fn=toggle_input_mode,
594
+ inputs=[input_mode],
595
+ outputs=[
596
+ upload_pdf_btn,
597
+ uploaded_pdf_status,
598
+ pdf_dropdown,
599
+ page_selection,
600
+ data_folder_note,
601
+ refresh_pdfs_btn,
602
+ input_help,
603
+ ],
604
+ queue=False,
605
+ )
606
 
607
+ upload_pdf_btn.upload(
608
+ fn=cache_uploaded_pdf,
609
+ inputs=[upload_pdf_btn],
610
+ outputs=[uploaded_pdf_state, uploaded_pdf_status],
611
+ queue=False,
612
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
+ refresh_pdfs_btn.click(
615
+ fn=refresh_pdf_dropdown,
616
+ inputs=None,
617
+ outputs=[pdf_dropdown, data_folder_note],
618
+ queue=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
 
621
  run_btn.click(
622
  fn=run_router,
623
  inputs=[
624
+ input_mode,
625
+ task,
626
+ uploaded_pdf_state,
627
+ pdf_dropdown,
628
+ page_selection,
629
  max_new_tokens,
630
  gpu_duration_state,
631
  ],
632
  outputs=[result],
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  )
634
 
 
 
 
 
 
 
635
 
636
  if __name__ == "__main__":
637
+ launch_signature = inspect.signature(demo.launch)
638
+ launch_kwargs = {
639
+ "show_error": True,
640
+ }
641
+
642
+ if "ssr_mode" in launch_signature.parameters:
643
+ launch_kwargs["ssr_mode"] = False
644
+
645
+ if "mcp_server" in launch_signature.parameters:
646
+ launch_kwargs["mcp_server"] = True
647
+
648
+ demo.queue(max_size=50).launch(**launch_kwargs)