rehan953 commited on
Commit
d2c3dbb
·
verified ·
1 Parent(s): 194f537

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -172
app.py CHANGED
@@ -2,7 +2,6 @@
2
 
3
  import logging
4
  import os
5
- import re
6
  import tempfile
7
  import uuid
8
  from typing import List, Tuple
@@ -10,7 +9,7 @@ from typing import List, Tuple
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
@@ -19,10 +18,6 @@ PAD_RIGHT_FRAC = 0.10
19
  PAD_TOP_FRAC = 0.018
20
  PAD_BOTTOM_FRAC = 0.018
21
 
22
- # Slightly stronger retry profile for right-side amount clipping
23
- RETRY_RENDER_SCALE = 2.6
24
- RETRY_PAD_RIGHT_FRAC = 0.18
25
-
26
  ENABLE_CONTRAST = True
27
  CONTRAST_FACTOR = 1.18
28
  ENABLE_UNSHARP = True
@@ -34,7 +29,7 @@ PAGE_PNG_COMPRESS_LEVEL = 3
34
  MAX_IMAGE_SIDE = 1568
35
  MAX_NEW_TOKENS = 3000
36
 
37
- # Model singleton
38
  _model = None
39
  _processor = None
40
 
@@ -45,16 +40,15 @@ def _load_model():
45
  return _model, _processor
46
 
47
  import torch
48
- from transformers import AutoModelForImageTextToText, AutoProcessor
49
 
50
  log.info("Loading fine-tuned model from %s ...", MERGED_MODEL_DIR)
51
  _processor = AutoProcessor.from_pretrained(
52
- MERGED_MODEL_DIR,
53
- trust_remote_code=True,
54
  )
55
  _model = AutoModelForImageTextToText.from_pretrained(
56
  MERGED_MODEL_DIR,
57
- dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
58
  device_map="auto",
59
  trust_remote_code=True,
60
  )
@@ -92,69 +86,8 @@ def _resize_for_inference(img):
92
  return img.resize(new_size, Image.LANCZOS)
93
 
94
 
95
- def _strip_outer_code_fences(text: str) -> str:
96
- t = text.strip()
97
- if t.startswith("```") and t.endswith("```"):
98
- t = re.sub(r"^```[a-zA-Z0-9_-]*\n?", "", t)
99
- t = re.sub(r"\n?```$", "", t)
100
- return t.strip()
101
-
102
-
103
- def _build_prompt(strict: bool = False) -> str:
104
- base = (
105
- "Document Parsing to markdown.\n"
106
- "Rules:\n"
107
- "1) Preserve content exactly in reading order.\n"
108
- "2) Do not summarize.\n"
109
- "3) Do not invent rows, values, subtotals, or examples.\n"
110
- "4) Output markdown only; do not output HTML tags.\n"
111
- )
112
- if strict:
113
- base += (
114
- "5) For statement activity tables, preserve date/description/amount rows exactly.\n"
115
- "6) If uncertain, keep raw line text instead of fabricating table rows.\n"
116
- )
117
- return base
118
-
119
-
120
- def _is_hallucinated(text: str) -> bool:
121
- low = text.lower()
122
- if "<table" in low or "<thead" in low or "<tbody" in low:
123
- return True
124
- if re.search(r"<!--\s*row\s+\d+\s*-->", low):
125
- return True
126
- # Signature from your bad sample
127
- if (
128
- len(re.findall(r"\b100\.00\b", text)) >= 15
129
- and len(re.findall(r"\bcredit card\b|\bbank transfer\b", low)) >= 8
130
- ):
131
- return True
132
- return False
133
-
134
-
135
- def _count_amounts(text: str) -> int:
136
- return len(re.findall(r"\b\d{1,3}(?:,\d{3})*\.\d{2}\b", text))
137
-
138
-
139
- def _looks_amount_missing(text: str) -> bool:
140
- low = text.lower()
141
- if not any(
142
- key in low
143
- for key in (
144
- "electronic payments",
145
- "electronic deposits",
146
- "daily account activity",
147
- "checks paid",
148
- "other withdrawals",
149
- )
150
- ):
151
- return False
152
- dated_rows = len(re.findall(r"(?m)^\s*\d{2}/\d{2}\b", text))
153
- return dated_rows >= 10 and _count_amounts(text) <= max(2, dated_rows // 12)
154
-
155
-
156
- def _infer_image(image_path: str, strict: bool = False) -> str:
157
- """Run model on a single image and return markdown string."""
158
  import torch
159
  from PIL import Image
160
 
@@ -168,15 +101,13 @@ def _infer_image(image_path: str, strict: bool = False) -> str:
168
  try:
169
  img.save(resized_path, "PNG")
170
 
171
- messages = [
172
- {
173
- "role": "user",
174
- "content": [
175
- {"type": "image", "url": resized_path},
176
- {"type": "text", "text": _build_prompt(strict)},
177
- ],
178
- }
179
- ]
180
 
181
  inputs = processor.apply_chat_template(
182
  messages,
@@ -187,8 +118,7 @@ def _infer_image(image_path: str, strict: bool = False) -> str:
187
  ).to(model.device)
188
  inputs.pop("token_type_ids", None)
189
 
190
- if torch.cuda.is_available():
191
- torch.cuda.empty_cache()
192
 
193
  with torch.no_grad():
194
  ids = model.generate(
@@ -199,10 +129,10 @@ def _infer_image(image_path: str, strict: bool = False) -> str:
199
  )
200
 
201
  result = processor.decode(
202
- ids[0][inputs["input_ids"].shape[1] :],
203
  skip_special_tokens=True,
204
  )
205
- return _strip_outer_code_fences(result)
206
 
207
  finally:
208
  try:
@@ -211,55 +141,46 @@ def _infer_image(image_path: str, strict: bool = False) -> str:
211
  pass
212
 
213
 
214
- def _render_page(pdf_path: str, page_idx: int, scale: float, right_pad_frac: float) -> str:
215
  import pymupdf as fitz
216
  from PIL import Image
217
 
218
  doc = fitz.open(pdf_path)
219
- try:
220
- page = doc[page_idx]
221
- pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale), alpha=False)
222
- finally:
223
- doc.close()
224
-
225
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
226
- img = _enhance_raster_for_ocr(img)
227
-
228
- w, h = img.size
229
- pad_l = int(w * PAD_LEFT_FRAC)
230
- pad_r = int(w * right_pad_frac)
231
- pad_t = int(h * PAD_TOP_FRAC)
232
- pad_b = int(h * PAD_BOTTOM_FRAC)
233
-
234
- if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
235
- canvas = Image.new("RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255))
236
- canvas.paste(img, (pad_l, pad_t))
237
- img = canvas
238
 
239
- uniq = uuid.uuid4().hex[:10]
240
- img_path = os.path.join(tempfile.gettempdir(), f"glmocr_page_{os.getpid()}_{uniq}_{page_idx}.png")
241
- img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
242
- return img_path
 
243
 
 
 
244
 
245
- def _pdf_page_count(pdf_path: str) -> int:
246
- import pymupdf as fitz
 
 
 
247
 
248
- doc = fitz.open(pdf_path)
249
- try:
250
- return len(doc)
251
- finally:
252
- doc.close()
 
253
 
 
 
 
 
 
 
 
 
254
 
255
- def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
256
- page_images: List[str] = []
257
- page_heights: List[int] = []
258
- total = _pdf_page_count(pdf_path)
259
- for i in range(total):
260
- path = _render_page(pdf_path, i, RENDER_SCALE, PAD_RIGHT_FRAC)
261
- page_images.append(path)
262
- page_heights.append(0)
263
  return page_images, page_heights
264
 
265
 
@@ -273,54 +194,26 @@ def run_ocr(uploaded_file):
273
  is_pdf = path.lower().endswith(".pdf")
274
 
275
  if is_pdf:
276
- page_total = _pdf_page_count(path)
277
- all_pages: List[str] = []
278
-
279
- for page_num in range(page_total):
280
- log.info("Processing page %d / %d ...", page_num + 1, page_total)
281
- img_path = _render_page(path, page_num, RENDER_SCALE, PAD_RIGHT_FRAC)
282
- page_images.append(img_path)
283
-
284
- page_md = _infer_image(img_path, strict=False)
285
-
286
- # Retry when output looks clearly wrong.
287
- if _is_hallucinated(page_md) or _looks_amount_missing(page_md):
288
- retry_img = _render_page(path, page_num, RETRY_RENDER_SCALE, RETRY_PAD_RIGHT_FRAC)
289
- page_images.append(retry_img)
290
- retry_md = _infer_image(retry_img, strict=True)
291
-
292
- # Prefer non-hallucinated output; otherwise keep original.
293
- if not _is_hallucinated(retry_md):
294
- page_md = retry_md
295
-
296
- # Hard guard: if still hallucinated, drop page instead of poisoning full markdown.
297
- if _is_hallucinated(page_md):
298
- log.warning("Skipping hallucinated output on page %d", page_num + 1)
299
- continue
300
-
301
- if page_md:
302
- all_pages.append(page_md)
303
-
304
- merged = (
305
- "\n\n---page-separator---\n\n".join(all_pages)
306
- if all_pages
307
- else "(No content extracted)"
308
- )
309
- return merged
310
-
311
- page_md = _infer_image(path, strict=True)
312
- if _is_hallucinated(page_md):
313
- # Last fallback for single image
314
- page_md_retry = _infer_image(path, strict=True)
315
- if not _is_hallucinated(page_md_retry):
316
- page_md = page_md_retry
317
- if _is_hallucinated(page_md):
318
- return "(Output rejected: detected hallucinated HTML/sample table content)"
319
- return page_md
320
 
321
  except Exception as e:
322
  import traceback
323
-
324
  log.exception("run_ocr failed: %s", e)
325
  return f"Error: {e}\n\n{traceback.format_exc()}"
326
 
@@ -354,4 +247,4 @@ def _create_gradio_demo():
354
 
355
  if __name__ == "__main__":
356
  _load_model()
357
- _create_gradio_demo().launch()
 
2
 
3
  import logging
4
  import os
 
5
  import tempfile
6
  import uuid
7
  from typing import List, Tuple
 
9
  log = logging.getLogger("glmocr_simple_app")
10
  logging.basicConfig(level=logging.INFO)
11
 
12
+ # ── Fine-tuned model repo on HuggingFace ─────────────────────────────────────
13
  MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
14
 
15
  RENDER_SCALE = 2.0
 
18
  PAD_TOP_FRAC = 0.018
19
  PAD_BOTTOM_FRAC = 0.018
20
 
 
 
 
 
21
  ENABLE_CONTRAST = True
22
  CONTRAST_FACTOR = 1.18
23
  ENABLE_UNSHARP = True
 
29
  MAX_IMAGE_SIDE = 1568
30
  MAX_NEW_TOKENS = 3000
31
 
32
+ # ── Model singleton ───────────────────────────────────────────────────────────
33
  _model = None
34
  _processor = None
35
 
 
40
  return _model, _processor
41
 
42
  import torch
43
+ from transformers import AutoProcessor, AutoModelForImageTextToText
44
 
45
  log.info("Loading fine-tuned model from %s ...", MERGED_MODEL_DIR)
46
  _processor = AutoProcessor.from_pretrained(
47
+ MERGED_MODEL_DIR, trust_remote_code=True
 
48
  )
49
  _model = AutoModelForImageTextToText.from_pretrained(
50
  MERGED_MODEL_DIR,
51
+ dtype=torch.bfloat16,
52
  device_map="auto",
53
  trust_remote_code=True,
54
  )
 
86
  return img.resize(new_size, Image.LANCZOS)
87
 
88
 
89
+ def _infer_image(image_path: str) -> str:
90
+ """Run fine-tuned model on a single image file and return markdown string."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  import torch
92
  from PIL import Image
93
 
 
101
  try:
102
  img.save(resized_path, "PNG")
103
 
104
+ messages = [{
105
+ "role": "user",
106
+ "content": [
107
+ {"type": "image", "url": resized_path},
108
+ {"type": "text", "text": "Document Parsing:"},
109
+ ],
110
+ }]
 
 
111
 
112
  inputs = processor.apply_chat_template(
113
  messages,
 
118
  ).to(model.device)
119
  inputs.pop("token_type_ids", None)
120
 
121
+ torch.cuda.empty_cache()
 
122
 
123
  with torch.no_grad():
124
  ids = model.generate(
 
129
  )
130
 
131
  result = processor.decode(
132
+ ids[0][inputs["input_ids"].shape[1]:],
133
  skip_special_tokens=True,
134
  )
135
+ return result.strip()
136
 
137
  finally:
138
  try:
 
141
  pass
142
 
143
 
144
+ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
145
  import pymupdf as fitz
146
  from PIL import Image
147
 
148
  doc = fitz.open(pdf_path)
149
+ page_images: List[str] = []
150
+ page_heights: List[int] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ for i in range(len(doc)):
153
+ page = doc[i]
154
+ pix = page.get_pixmap(
155
+ matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False
156
+ )
157
 
158
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
159
+ img = _enhance_raster_for_ocr(img)
160
 
161
+ w, h = img.size
162
+ pad_l = int(w * PAD_LEFT_FRAC)
163
+ pad_r = int(w * PAD_RIGHT_FRAC)
164
+ pad_t = int(h * PAD_TOP_FRAC)
165
+ pad_b = int(h * PAD_BOTTOM_FRAC)
166
 
167
+ if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
168
+ canvas = Image.new(
169
+ "RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255)
170
+ )
171
+ canvas.paste(img, (pad_l, pad_t))
172
+ img = canvas
173
 
174
+ uniq = uuid.uuid4().hex[:10]
175
+ img_path = os.path.join(
176
+ tempfile.gettempdir(),
177
+ f"glmocr_page_{os.getpid()}_{uniq}_{i}.png",
178
+ )
179
+ img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
180
+ page_images.append(img_path)
181
+ page_heights.append(img.height)
182
 
183
+ doc.close()
 
 
 
 
 
 
 
184
  return page_images, page_heights
185
 
186
 
 
194
  is_pdf = path.lower().endswith(".pdf")
195
 
196
  if is_pdf:
197
+ page_images, _ = render_pdf_pages_to_images(path)
198
+ else:
199
+ page_images = [path]
200
+
201
+ all_pages = []
202
+ for page_num, img_path in enumerate(page_images):
203
+ log.info("Processing page %d / %d ...", page_num + 1, len(page_images))
204
+ page_md = _infer_image(img_path)
205
+ if page_md:
206
+ all_pages.append(page_md)
207
+
208
+ merged = (
209
+ "\n\n---page-separator---\n\n".join(all_pages)
210
+ if all_pages
211
+ else "(No content extracted)"
212
+ )
213
+ return merged
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  except Exception as e:
216
  import traceback
 
217
  log.exception("run_ocr failed: %s", e)
218
  return f"Error: {e}\n\n{traceback.format_exc()}"
219
 
 
247
 
248
  if __name__ == "__main__":
249
  _load_model()
250
+ _create_gradio_demo().launch()