rehan953 commited on
Commit
39680f0
·
verified ·
1 Parent(s): 03fcc38

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +231 -127
app.py CHANGED
@@ -5,51 +5,89 @@ import os
5
  import re
6
  import tempfile
7
  import uuid
8
- from typing import List, Tuple
 
9
 
10
- log = logging.getLogger("glmocr_simple_app")
11
  logging.basicConfig(level=logging.INFO)
12
 
13
- # ── Fine-tuned model repo on HuggingFace ─────────────────────────────────────
14
  MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
15
 
16
- RENDER_SCALE = 2.0
17
- PAD_LEFT_FRAC = 0.035
18
- PAD_RIGHT_FRAC = 0.10
19
- PAD_TOP_FRAC = 0.018
20
- PAD_BOTTOM_FRAC = 0.018
 
 
 
 
 
21
 
22
  ENABLE_CONTRAST = True
23
- CONTRAST_FACTOR = 1.18
24
  ENABLE_UNSHARP = True
25
  UNSHARP_RADIUS = 0.78
26
- UNSHARP_PERCENT = 76
27
  UNSHARP_THRESHOLD = 1
28
 
29
  PAGE_PNG_COMPRESS_LEVEL = 3
30
- MAX_IMAGE_SIDE = 1568
31
- MAX_NEW_TOKENS = 3000
32
 
33
- # ── Model singleton ───────────────────────────────────────────────────────────
34
  _model = None
35
  _processor = None
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def _load_model():
39
  global _model, _processor
40
  if _model is not None:
41
  return _model, _processor
42
 
43
  import torch
44
- from transformers import AutoProcessor, AutoModelForImageTextToText
45
 
46
  log.info("Loading fine-tuned model from %s ...", MERGED_MODEL_DIR)
47
  _processor = AutoProcessor.from_pretrained(
48
- MERGED_MODEL_DIR, trust_remote_code=True
 
49
  )
 
 
 
 
 
 
50
  _model = AutoModelForImageTextToText.from_pretrained(
51
  MERGED_MODEL_DIR,
52
- dtype=torch.bfloat16,
53
  device_map="auto",
54
  trust_remote_code=True,
55
  )
@@ -89,98 +127,138 @@ def _resize_for_inference(img):
89
 
90
  def _normalize_tables(text: str) -> str:
91
  """
92
- Convert plain-text table rows (no pipe boundaries) into proper
93
- markdown table rows with | boundaries.
94
-
95
- Handles rows like:
96
- 10/01 CCD DEBIT, SOME MERCHANT 654.00
97
- 10/01 DEBIT 2,500.00
98
- and header rows like:
99
- POSTING DATE DESCRIPTION AMOUNT
100
  """
101
- lines = text.split('\n')
102
- result = []
103
- i = 0
104
-
105
- # Amount pattern: number like 1,234.56 or 123.45
106
- amt_re = re.compile(r'[\d,]+\.\d{2}$')
107
- # Date pattern at start of line: MM/DD
108
- date_re = re.compile(r'^\d{2}/\d{2}\s')
109
- # Header pattern
110
  header_re = re.compile(
111
- r'^(POSTING\s+DATE|DATE)\s+(DESCRIPTION|SERIAL\s+NO\.?|NO\.?\s+CHECKS)',
112
- re.IGNORECASE
113
  )
 
114
 
115
  in_plain_table = False
116
 
117
- while i < len(lines):
118
- line = lines[i]
119
  stripped = line.strip()
120
 
121
- # Skip empty lines — reset table tracking
122
  if not stripped:
123
  in_plain_table = False
124
  result.append(line)
125
- i += 1
126
  continue
127
 
128
- # Already a proper markdown table row — pass through
129
- if stripped.startswith('|'):
130
  in_plain_table = False
131
  result.append(line)
132
- i += 1
133
  continue
134
 
135
- # Detect plain-text table header row
136
  if header_re.match(stripped):
137
- # Split by 2+ spaces
138
- parts = re.split(r'\s{2,}', stripped)
139
- parts = [p.strip() for p in parts if p.strip()]
140
  if len(parts) >= 2:
141
- result.append('| ' + ' | '.join(parts) + ' |')
142
- result.append('| ' + ' | '.join(['---'] * len(parts)) + ' |')
143
  in_plain_table = True
144
- i += 1
145
  continue
146
 
147
- # Detect plain-text data row starting with date
148
  if date_re.match(stripped):
149
- # Split by 2+ spaces to separate columns
150
- parts = re.split(r'\s{2,}', stripped)
151
- parts = [p.strip() for p in parts if p.strip()]
152
 
153
- # If only one part (description runs together), try splitting off amount
154
  if len(parts) == 1 and amt_re.search(stripped):
155
- # Split last amount token off
156
- m = re.search(r'^(.*?)\s+([\d,]+\.\d{2})$', stripped)
157
  if m:
158
  parts = [m.group(1).strip(), m.group(2).strip()]
159
 
160
  if len(parts) >= 2:
161
- result.append('| ' + ' | '.join(parts) + ' |')
162
  in_plain_table = True
163
- i += 1
164
  continue
165
 
166
- # Detect subtotal lines inside a plain table
167
- if in_plain_table and re.match(r'^Subtotal:', stripped, re.IGNORECASE):
168
- m = re.match(r'^(Subtotal:)\s+([\d,]+\.\d{2})', stripped, re.IGNORECASE)
169
  if m:
170
- result.append(f'| | **{m.group(1)}** | **{m.group(2)}** |')
171
- i += 1
172
  continue
173
 
174
- # Not a table row
175
  in_plain_table = False
176
  result.append(line)
177
- i += 1
178
 
179
- return '\n'.join(result)
 
 
 
 
 
 
 
 
 
180
 
181
 
182
- def _infer_image(image_path: str) -> str:
183
- """Run fine-tuned model on a single image file and return markdown string."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  import torch
185
  from PIL import Image
186
 
@@ -194,13 +272,15 @@ def _infer_image(image_path: str) -> str:
194
  try:
195
  img.save(resized_path, "PNG")
196
 
197
- messages = [{
198
- "role": "user",
199
- "content": [
200
- {"type": "image", "url": resized_path},
201
- {"type": "text", "text": "Document Parsing:"},
202
- ],
203
- }]
 
 
204
 
205
  inputs = processor.apply_chat_template(
206
  messages,
@@ -211,25 +291,22 @@ def _infer_image(image_path: str) -> str:
211
  ).to(model.device)
212
  inputs.pop("token_type_ids", None)
213
 
214
- torch.cuda.empty_cache()
 
215
 
216
  with torch.no_grad():
217
  ids = model.generate(
218
  **inputs,
219
  max_new_tokens=MAX_NEW_TOKENS,
220
  do_sample=False,
221
- repetition_penalty=1.1,
222
  )
223
 
224
  result = processor.decode(
225
- ids[0][inputs["input_ids"].shape[1]:],
226
  skip_special_tokens=True,
227
  )
228
-
229
- result = result.strip()
230
-
231
- return result
232
-
233
  finally:
234
  try:
235
  os.unlink(resized_path)
@@ -237,69 +314,95 @@ def _infer_image(image_path: str) -> str:
237
  pass
238
 
239
 
240
- def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
241
  import pymupdf as fitz
242
  from PIL import Image
243
 
244
  doc = fitz.open(pdf_path)
245
- page_images: List[str] = []
246
- page_heights: List[int] = []
 
 
 
247
 
248
- for i in range(len(doc)):
249
- page = doc[i]
250
- pix = page.get_pixmap(
251
- matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False
252
- )
253
 
254
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
255
- img = _enhance_raster_for_ocr(img)
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- w, h = img.size
258
- pad_l = int(w * PAD_LEFT_FRAC)
259
- pad_r = int(w * PAD_RIGHT_FRAC)
260
- pad_t = int(h * PAD_TOP_FRAC)
261
- pad_b = int(h * PAD_BOTTOM_FRAC)
 
 
262
 
263
- if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
264
- canvas = Image.new(
265
- "RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255)
266
- )
267
- canvas.paste(img, (pad_l, pad_t))
268
- img = canvas
269
 
270
- uniq = uuid.uuid4().hex[:10]
271
- img_path = os.path.join(
272
- tempfile.gettempdir(),
273
- f"glmocr_page_{os.getpid()}_{uniq}_{i}.png",
274
- )
275
- img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
276
- page_images.append(img_path)
277
- page_heights.append(img.height)
278
 
279
- doc.close()
280
- return page_images, page_heights
 
 
 
281
 
282
 
283
  def run_ocr(uploaded_file):
284
  if uploaded_file is None:
285
  return "Please upload a file."
286
 
287
- page_images: List[str] = []
288
  try:
289
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
290
  is_pdf = path.lower().endswith(".pdf")
291
 
292
- if is_pdf:
293
- page_images, _ = render_pdf_pages_to_images(path)
294
- else:
295
- page_images = [path]
296
 
297
- all_pages = []
298
- for page_num, img_path in enumerate(page_images):
299
- log.info("Processing page %d / %d ...", page_num + 1, len(page_images))
300
- page_md = _infer_image(img_path)
301
  if page_md:
302
  all_pages.append(page_md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  merged = (
305
  "\n\n---page-separator---\n\n".join(all_pages)
@@ -310,11 +413,12 @@ def run_ocr(uploaded_file):
310
 
311
  except Exception as e:
312
  import traceback
 
313
  log.exception("run_ocr failed: %s", e)
314
  return f"Error: {e}\n\n{traceback.format_exc()}"
315
 
316
  finally:
317
- for p in page_images:
318
  try:
319
  if (
320
  isinstance(p, str)
@@ -329,8 +433,8 @@ def run_ocr(uploaded_file):
329
  def _create_gradio_demo():
330
  import gradio as gr
331
 
332
- with gr.Blocks(title="GLM-OCR Fine-tuned") as demo:
333
- gr.Markdown("# GLM-OCR (Fine-tuned)")
334
  file_in = gr.File(
335
  label="Upload PDF or image",
336
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
@@ -343,4 +447,4 @@ def _create_gradio_demo():
343
 
344
  if __name__ == "__main__":
345
  _load_model()
346
- _create_gradio_demo().launch()
 
5
  import re
6
  import tempfile
7
  import uuid
8
+ from dataclasses import dataclass
9
+ from typing import List, Optional, Tuple
10
 
11
+ log = logging.getLogger("glmocr_improved_app")
12
  logging.basicConfig(level=logging.INFO)
13
 
14
+ # Fine-tuned model repo on HuggingFace
15
  MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
16
 
17
+ # Primary render profile (fast)
18
+ RENDER_SCALE = 2.6
19
+ PAD_LEFT_FRAC = 0.04
20
+ PAD_RIGHT_FRAC = 0.16 # right-side amounts are often clipped
21
+ PAD_TOP_FRAC = 0.02
22
+ PAD_BOTTOM_FRAC = 0.02
23
+
24
+ # Retry render profile (quality)
25
+ RETRY_RENDER_SCALE = 3.1
26
+ RETRY_PAD_RIGHT_FRAC = 0.24
27
 
28
  ENABLE_CONTRAST = True
29
+ CONTRAST_FACTOR = 1.20
30
  ENABLE_UNSHARP = True
31
  UNSHARP_RADIUS = 0.78
32
+ UNSHARP_PERCENT = 82
33
  UNSHARP_THRESHOLD = 1
34
 
35
  PAGE_PNG_COMPRESS_LEVEL = 3
36
+ MAX_IMAGE_SIDE = 1792
37
+ MAX_NEW_TOKENS = 3800
38
 
39
+ # Model singleton
40
  _model = None
41
  _processor = None
42
 
43
 
44
+ @dataclass
45
+ class RenderProfile:
46
+ scale: float
47
+ pad_left: float
48
+ pad_right: float
49
+ pad_top: float
50
+ pad_bottom: float
51
+
52
+
53
+ PRIMARY_PROFILE = RenderProfile(
54
+ scale=RENDER_SCALE,
55
+ pad_left=PAD_LEFT_FRAC,
56
+ pad_right=PAD_RIGHT_FRAC,
57
+ pad_top=PAD_TOP_FRAC,
58
+ pad_bottom=PAD_BOTTOM_FRAC,
59
+ )
60
+ RETRY_PROFILE = RenderProfile(
61
+ scale=RETRY_RENDER_SCALE,
62
+ pad_left=PAD_LEFT_FRAC,
63
+ pad_right=RETRY_PAD_RIGHT_FRAC,
64
+ pad_top=PAD_TOP_FRAC,
65
+ pad_bottom=PAD_BOTTOM_FRAC,
66
+ )
67
+
68
+
69
  def _load_model():
70
  global _model, _processor
71
  if _model is not None:
72
  return _model, _processor
73
 
74
  import torch
75
+ from transformers import AutoModelForImageTextToText, AutoProcessor
76
 
77
  log.info("Loading fine-tuned model from %s ...", MERGED_MODEL_DIR)
78
  _processor = AutoProcessor.from_pretrained(
79
+ MERGED_MODEL_DIR,
80
+ trust_remote_code=True,
81
  )
82
+
83
+ dtype = torch.bfloat16
84
+ if not torch.cuda.is_available():
85
+ # CPU path is safer with fp32
86
+ dtype = torch.float32
87
+
88
  _model = AutoModelForImageTextToText.from_pretrained(
89
  MERGED_MODEL_DIR,
90
+ dtype=dtype,
91
  device_map="auto",
92
  trust_remote_code=True,
93
  )
 
127
 
128
  def _normalize_tables(text: str) -> str:
129
  """
130
+ Normalize plain-text table rows into markdown table rows.
131
+ This is critical for downstream parsers that expect markdown-table structure.
 
 
 
 
 
 
132
  """
133
+ lines = text.split("\n")
134
+ result: List[str] = []
135
+
136
+ amt_re = re.compile(r"[\d,]+\.\d{2}$")
137
+ date_re = re.compile(r"^\d{2}/\d{2}\s")
 
 
 
 
138
  header_re = re.compile(
139
+ r"^(POSTING\s+DATE|DATE)\s+(DESCRIPTION|SERIAL\s+NO\.?|NO\.?\s+CHECKS)",
140
+ re.IGNORECASE,
141
  )
142
+ subtotal_re = re.compile(r"^(Subtotal:)\s+([\d,]+\.\d{2})", re.IGNORECASE)
143
 
144
  in_plain_table = False
145
 
146
+ for raw_line in lines:
147
+ line = raw_line.rstrip()
148
  stripped = line.strip()
149
 
 
150
  if not stripped:
151
  in_plain_table = False
152
  result.append(line)
 
153
  continue
154
 
155
+ if stripped.startswith("|"):
 
156
  in_plain_table = False
157
  result.append(line)
 
158
  continue
159
 
 
160
  if header_re.match(stripped):
161
+ parts = [p.strip() for p in re.split(r"\s{2,}", stripped) if p.strip()]
 
 
162
  if len(parts) >= 2:
163
+ result.append("| " + " | ".join(parts) + " |")
164
+ result.append("| " + " | ".join(["---"] * len(parts)) + " |")
165
  in_plain_table = True
 
166
  continue
167
 
 
168
  if date_re.match(stripped):
169
+ parts = [p.strip() for p in re.split(r"\s{2,}", stripped) if p.strip()]
 
 
170
 
171
+ # Recover lines where spacing collapsed and only amount is clearly separable.
172
  if len(parts) == 1 and amt_re.search(stripped):
173
+ m = re.search(r"^(.*?)\s+([\d,]+\.\d{2})$", stripped)
 
174
  if m:
175
  parts = [m.group(1).strip(), m.group(2).strip()]
176
 
177
  if len(parts) >= 2:
178
+ result.append("| " + " | ".join(parts) + " |")
179
  in_plain_table = True
 
180
  continue
181
 
182
+ if in_plain_table:
183
+ m = subtotal_re.match(stripped)
 
184
  if m:
185
+ result.append(f"| | **{m.group(1)}** | **{m.group(2)}** |")
 
186
  continue
187
 
 
188
  in_plain_table = False
189
  result.append(line)
 
190
 
191
+ return "\n".join(result)
192
+
193
+
194
+ def _strip_code_fences(text: str) -> str:
195
+ # Some generations wrap markdown in code fences; strip only outer wrappers.
196
+ t = text.strip()
197
+ if t.startswith("```") and t.endswith("```"):
198
+ t = re.sub(r"^```[a-zA-Z0-9_-]*\n?", "", t)
199
+ t = re.sub(r"\n?```$", "", t)
200
+ return t.strip()
201
 
202
 
203
+ def _postprocess_markdown(text: str) -> str:
204
+ text = _strip_code_fences(text)
205
+ text = _normalize_tables(text)
206
+ return text.strip()
207
+
208
+
209
+ def _build_prompt(strict_table_mode: bool = False) -> str:
210
+ base = (
211
+ "Document Parsing to markdown.\n"
212
+ "Rules:\n"
213
+ "1) Preserve every row and amount exactly as seen.\n"
214
+ "2) Keep transaction/payment/check sections in markdown table format.\n"
215
+ "3) Never drop right-most numeric columns (amount/balance).\n"
216
+ "4) Keep page content order exactly.\n"
217
+ "5) Do not summarize.\n"
218
+ )
219
+ if strict_table_mode:
220
+ base += (
221
+ "6) If a row starts with a date (MM/DD), output it as a table row and include trailing amount.\n"
222
+ "7) Prefer explicit table rows over plain text for statement activity.\n"
223
+ )
224
+ return base
225
+
226
+
227
+ def _count_amounts(text: str) -> int:
228
+ return len(re.findall(r"\b\d{1,3}(?:,\d{3})*\.\d{2}\b", text))
229
+
230
+
231
+ def _count_dated_rows(text: str) -> int:
232
+ return len(re.findall(r"(?m)^\s*\d{2}/\d{2}\b", text))
233
+
234
+
235
+ def _looks_amount_missing(text: str) -> bool:
236
+ """
237
+ Heuristic: many dated activity rows but very low amount density.
238
+ This catches pages where payment rows were parsed but right-side amounts vanished.
239
+ """
240
+ low = text.lower()
241
+ likely_activity = any(
242
+ key in low
243
+ for key in (
244
+ "daily account activity",
245
+ "electronic payments",
246
+ "electronic deposits",
247
+ "checks paid",
248
+ "other withdrawals",
249
+ "service charges",
250
+ )
251
+ )
252
+ if not likely_activity:
253
+ return False
254
+
255
+ dated = _count_dated_rows(text)
256
+ amts = _count_amounts(text)
257
+ return dated >= 12 and amts <= max(3, dated // 10)
258
+
259
+
260
+ def _infer_image(image_path: str, strict_table_mode: bool = False) -> str:
261
+ """Run fine-tuned model on a single image file and return markdown."""
262
  import torch
263
  from PIL import Image
264
 
 
272
  try:
273
  img.save(resized_path, "PNG")
274
 
275
+ messages = [
276
+ {
277
+ "role": "user",
278
+ "content": [
279
+ {"type": "image", "url": resized_path},
280
+ {"type": "text", "text": _build_prompt(strict_table_mode)},
281
+ ],
282
+ }
283
+ ]
284
 
285
  inputs = processor.apply_chat_template(
286
  messages,
 
291
  ).to(model.device)
292
  inputs.pop("token_type_ids", None)
293
 
294
+ if torch.cuda.is_available():
295
+ torch.cuda.empty_cache()
296
 
297
  with torch.no_grad():
298
  ids = model.generate(
299
  **inputs,
300
  max_new_tokens=MAX_NEW_TOKENS,
301
  do_sample=False,
302
+ repetition_penalty=1.08,
303
  )
304
 
305
  result = processor.decode(
306
+ ids[0][inputs["input_ids"].shape[1] :],
307
  skip_special_tokens=True,
308
  )
309
+ return _postprocess_markdown(result)
 
 
 
 
310
  finally:
311
  try:
312
  os.unlink(resized_path)
 
314
  pass
315
 
316
 
317
+ def _render_pdf_page_to_image(pdf_path: str, page_index: int, profile: RenderProfile) -> str:
318
  import pymupdf as fitz
319
  from PIL import Image
320
 
321
  doc = fitz.open(pdf_path)
322
+ try:
323
+ page = doc[page_index]
324
+ pix = page.get_pixmap(matrix=fitz.Matrix(profile.scale, profile.scale), alpha=False)
325
+ finally:
326
+ doc.close()
327
 
328
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
329
+ img = _enhance_raster_for_ocr(img)
 
 
 
330
 
331
+ w, h = img.size
332
+ pad_l = int(w * profile.pad_left)
333
+ pad_r = int(w * profile.pad_right)
334
+ pad_t = int(h * profile.pad_top)
335
+ pad_b = int(h * profile.pad_bottom)
336
+
337
+ if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
338
+ canvas = Image.new(
339
+ "RGB",
340
+ (w + pad_l + pad_r, h + pad_t + pad_b),
341
+ (255, 255, 255),
342
+ )
343
+ canvas.paste(img, (pad_l, pad_t))
344
+ img = canvas
345
 
346
+ uniq = uuid.uuid4().hex[:10]
347
+ out_path = os.path.join(
348
+ tempfile.gettempdir(),
349
+ f"glmocr_page_{os.getpid()}_{uniq}_{page_index}.png",
350
+ )
351
+ img.save(out_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
352
+ return out_path
353
 
 
 
 
 
 
 
354
 
355
+ def _pdf_page_count(pdf_path: str) -> int:
356
+ import pymupdf as fitz
 
 
 
 
 
 
357
 
358
+ doc = fitz.open(pdf_path)
359
+ try:
360
+ return len(doc)
361
+ finally:
362
+ doc.close()
363
 
364
 
365
  def run_ocr(uploaded_file):
366
  if uploaded_file is None:
367
  return "Please upload a file."
368
 
369
+ temp_paths: List[str] = []
370
  try:
371
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
372
  is_pdf = path.lower().endswith(".pdf")
373
 
374
+ all_pages: List[str] = []
 
 
 
375
 
376
+ if not is_pdf:
377
+ page_md = _infer_image(path, strict_table_mode=True)
 
 
378
  if page_md:
379
  all_pages.append(page_md)
380
+ else:
381
+ page_total = _pdf_page_count(path)
382
+ for page_idx in range(page_total):
383
+ log.info("Processing page %d / %d ...", page_idx + 1, page_total)
384
+
385
+ # Pass 1: primary profile
386
+ img_path = _render_pdf_page_to_image(path, page_idx, PRIMARY_PROFILE)
387
+ temp_paths.append(img_path)
388
+ page_md = _infer_image(img_path, strict_table_mode=False)
389
+
390
+ # Pass 2: retry for amount-missing pages
391
+ if _looks_amount_missing(page_md):
392
+ log.info(
393
+ "Page %d flagged as amount-missing; retrying high-quality render.",
394
+ page_idx + 1,
395
+ )
396
+ retry_img = _render_pdf_page_to_image(path, page_idx, RETRY_PROFILE)
397
+ temp_paths.append(retry_img)
398
+ retry_md = _infer_image(retry_img, strict_table_mode=True)
399
+
400
+ # Keep better output by amount density.
401
+ if _count_amounts(retry_md) > _count_amounts(page_md):
402
+ page_md = retry_md
403
+
404
+ if page_md:
405
+ all_pages.append(page_md)
406
 
407
  merged = (
408
  "\n\n---page-separator---\n\n".join(all_pages)
 
413
 
414
  except Exception as e:
415
  import traceback
416
+
417
  log.exception("run_ocr failed: %s", e)
418
  return f"Error: {e}\n\n{traceback.format_exc()}"
419
 
420
  finally:
421
+ for p in temp_paths:
422
  try:
423
  if (
424
  isinstance(p, str)
 
433
  def _create_gradio_demo():
434
  import gradio as gr
435
 
436
+ with gr.Blocks(title="GLM-OCR Fine-tuned (Improved)") as demo:
437
+ gr.Markdown("# GLM-OCR (Fine-tuned, Improved)")
438
  file_in = gr.File(
439
  label="Upload PDF or image",
440
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
 
447
 
448
  if __name__ == "__main__":
449
  _load_model()
450
+ _create_gradio_demo().launch()