rehan953 commited on
Commit
721c9d9
·
verified ·
1 Parent(s): 177ec96

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -321
app.py CHANGED
@@ -1,361 +1,87 @@
1
  #!/usr/bin/env python3
2
 
 
3
  import logging
4
  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
- )
56
- _model.eval()
57
- log.info("Model loaded.")
58
- return _model, _processor
59
-
60
-
61
- def _enhance_raster_for_ocr(img):
62
- from PIL import ImageEnhance, ImageFilter
63
-
64
- if ENABLE_CONTRAST:
65
- img = ImageEnhance.Contrast(img).enhance(CONTRAST_FACTOR)
66
- if ENABLE_UNSHARP:
67
- img = img.filter(
68
- ImageFilter.UnsharpMask(
69
- radius=UNSHARP_RADIUS,
70
- percent=UNSHARP_PERCENT,
71
- threshold=UNSHARP_THRESHOLD,
72
- )
73
- )
74
- return img
75
-
76
-
77
- def _resize_for_inference(img):
78
- """Resize image preserving aspect ratio so longest side <= MAX_IMAGE_SIDE."""
79
- from PIL import Image
80
-
81
- w, h = img.size
82
- longest = max(w, h)
83
- if longest <= MAX_IMAGE_SIDE:
84
- return img
85
- ratio = MAX_IMAGE_SIDE / longest
86
- new_size = (int(w * ratio), int(h * ratio))
87
- return img.resize(new_size, Image.LANCZOS)
88
-
89
-
90
- def _detect_headers(header_line: str) -> List[str]:
91
- """Extract column headers from a header line by splitting on 2+ spaces."""
92
- parts = re.split(r'\s{2,}', header_line.strip())
93
- headers = [p.strip() for p in parts if p.strip()]
94
- return headers if headers else ["POSTING DATE", "DESCRIPTION", "AMOUNT"]
95
-
96
-
97
- def _plaintext_rows_to_table(text: str) -> str:
98
- """
99
- Convert plain-text transaction rows (no pipe boundaries) into proper
100
- markdown tables. Detects headers dynamically from the document.
101
- Works for any column layout, not just Date/Description/Amount.
102
- """
103
- lines = text.split('\n')
104
- result = []
105
- i = 0
106
- # Pattern to detect a header line (2+ words separated by 2+ spaces)
107
- header_re = re.compile(
108
- r'^[A-Z][A-Z\s]{3,}$',
109
- )
110
- # Pattern for a transaction row starting with MM/DD
111
- txn_re = re.compile(r'^\d{2}/\d{2}\s+\S')
112
- # Pattern to extract amount at end of line
113
- amt_re = re.compile(r'^(.*?)\s+([\d,]+\.\d{2})$')
114
-
115
- while i < len(lines):
116
- line = lines[i]
117
- stripped = line.strip()
118
-
119
- # Already a proper markdown table row — pass through unchanged
120
- if stripped.startswith('|'):
121
- result.append(line)
122
- i += 1
123
- continue
124
-
125
- # Detect start of a plain-text transaction block
126
- if txn_re.match(stripped):
127
- # Look back to find the most recent header line before this block
128
- detected_headers = None
129
- for j in range(len(result) - 1, max(len(result) - 5, -1), -1):
130
- prev = result[j].strip()
131
- if prev and not prev.startswith('|') and not prev.startswith('#'):
132
- candidate_headers = _detect_headers(prev)
133
- if len(candidate_headers) >= 2:
134
- detected_headers = candidate_headers
135
- # Remove the header line from result since we'll put it in table
136
- result.pop(j)
137
- break
138
-
139
- if not detected_headers:
140
- detected_headers = ["POSTING DATE", "DESCRIPTION", "AMOUNT"]
141
-
142
- # Collect all transaction rows in this block
143
- block = []
144
- while i < len(lines):
145
- l = lines[i].strip()
146
- if not l:
147
- break
148
- if l.startswith('|') or l.startswith('#'):
149
- break
150
- if re.match(r'^Subtotal', l, re.IGNORECASE):
151
- break
152
- if txn_re.match(l):
153
- block.append(l)
154
- elif block:
155
- # Continuation line — merge into previous row
156
- block[-1] += ' ' + l
157
- i += 1
158
-
159
- if block:
160
- # Build markdown table with detected headers
161
- num_cols = len(detected_headers)
162
- result.append('| ' + ' | '.join(detected_headers) + ' |')
163
- result.append('| ' + ' | '.join(['---'] * num_cols) + ' |')
164
 
165
- for row in block:
166
- if num_cols == 3:
167
- # Try to split into date / description / amount
168
- m = amt_re.match(row)
169
- if m:
170
- rest = m.group(1)
171
- amount = m.group(2)
172
- date_m = re.match(r'^(\d{2}/\d{2})\s+(.*)', rest)
173
- if date_m:
174
- result.append(
175
- f'| {date_m.group(1)} | {date_m.group(2)} | {amount} |'
176
- )
177
- continue
178
- # No amount found — put full row in description, leave amount blank
179
- date_m = re.match(r'^(\d{2}/\d{2})\s+(.*)', row)
180
- if date_m:
181
- result.append(
182
- f'| {date_m.group(1)} | {date_m.group(2)} | |'
183
- )
184
- else:
185
- result.append(f'| | {row} | |')
186
- else:
187
- # For non-3-column tables just put full row as single cell
188
- result.append(f'| {row} |')
189
- continue
190
-
191
- result.append(line)
192
- i += 1
193
-
194
- return '\n'.join(result)
195
-
196
-
197
- def _infer_image(image_path: str) -> str:
198
- """Run fine-tuned model on a single image file and return markdown string."""
199
- import torch
200
- from PIL import Image
201
-
202
- model, processor = _load_model()
203
-
204
- img = Image.open(image_path).convert("RGB")
205
- img = _resize_for_inference(img)
206
-
207
- fd, resized_path = tempfile.mkstemp(suffix=".png")
208
- os.close(fd)
209
- try:
210
- img.save(resized_path, "PNG")
211
-
212
- messages = [{
213
- "role": "user",
214
- "content": [
215
- {"type": "image", "url": resized_path},
216
- {"type": "text", "text": "Document Parsing:"},
217
- ],
218
- }]
219
-
220
- inputs = processor.apply_chat_template(
221
- messages,
222
- tokenize=True,
223
- add_generation_prompt=True,
224
- return_dict=True,
225
- return_tensors="pt",
226
- ).to(model.device)
227
- inputs.pop("token_type_ids", None)
228
-
229
- torch.cuda.empty_cache()
230
-
231
- with torch.no_grad():
232
- ids = model.generate(
233
- **inputs,
234
- max_new_tokens=MAX_NEW_TOKENS,
235
- do_sample=False,
236
- repetition_penalty=1.1,
237
- )
238
-
239
- result = processor.decode(
240
- ids[0][inputs["input_ids"].shape[1]:],
241
- skip_special_tokens=True,
242
- )
243
-
244
- result = _plaintext_rows_to_table(result)
245
- return result.strip()
246
-
247
- finally:
248
  try:
249
- os.unlink(resized_path)
250
- except Exception:
251
  pass
252
 
 
 
 
253
 
254
- def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
255
- import pymupdf as fitz
256
- from PIL import Image
257
 
258
- doc = fitz.open(pdf_path)
259
- page_images: List[str] = []
260
- page_heights: List[int] = []
261
 
262
- for i in range(len(doc)):
263
- page = doc[i]
264
- pix = page.get_pixmap(
265
- matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False
266
- )
267
 
268
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
269
- img = _enhance_raster_for_ocr(img)
270
 
271
- w, h = img.size
272
- pad_l = int(w * PAD_LEFT_FRAC)
273
- pad_r = int(w * PAD_RIGHT_FRAC)
274
- pad_t = int(h * PAD_TOP_FRAC)
275
- pad_b = int(h * PAD_BOTTOM_FRAC)
276
 
277
- if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
278
- canvas = Image.new(
279
- "RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255)
280
- )
281
- canvas.paste(img, (pad_l, pad_t))
282
- img = canvas
283
 
284
- uniq = uuid.uuid4().hex[:10]
285
- img_path = os.path.join(
286
- tempfile.gettempdir(),
287
- f"glmocr_page_{os.getpid()}_{uniq}_{i}.png",
288
- )
289
- img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
290
- page_images.append(img_path)
291
- page_heights.append(img.height)
292
 
293
- doc.close()
294
- return page_images, page_heights
 
 
 
 
 
 
 
 
 
 
295
 
296
 
297
- def run_ocr(uploaded_file):
298
  if uploaded_file is None:
299
  return "Please upload a file."
300
-
301
- page_images: List[str] = []
302
  try:
303
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
304
- is_pdf = path.lower().endswith(".pdf")
305
-
306
- if is_pdf:
307
- page_images, _ = render_pdf_pages_to_images(path)
308
- else:
309
- page_images = [path]
310
-
311
- all_pages = []
312
- for page_num, img_path in enumerate(page_images):
313
- log.info("Processing page %d / %d ...", page_num + 1, len(page_images))
314
- page_md = _infer_image(img_path)
315
- if page_md:
316
- all_pages.append(page_md)
317
-
318
- merged = (
319
- "\n\n---page-separator---\n\n".join(all_pages)
320
- if all_pages
321
- else "(No content extracted)"
322
- )
323
- return merged
324
-
325
  except Exception as e:
326
  import traceback
327
- log.exception("run_ocr failed: %s", e)
328
- return f"Error: {e}\n\n{traceback.format_exc()}"
329
 
330
- finally:
331
- for p in page_images:
332
- try:
333
- if (
334
- isinstance(p, str)
335
- and p.endswith(".png")
336
- and "glmocr_page_" in os.path.basename(p)
337
- ):
338
- os.unlink(p)
339
- except Exception:
340
- pass
341
 
342
 
343
  def _create_gradio_demo():
344
  import gradio as gr
345
 
346
- with gr.Blocks(title="GLM-OCR Fine-tuned") as demo:
347
- gr.Markdown("# GLM-OCR (Fine-tuned)")
348
- gr.Markdown("Upload a PDF or image to extract structured markdown content.")
349
  file_in = gr.File(
350
  label="Upload PDF or image",
351
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
352
  )
353
  run_btn = gr.Button("Run OCR", variant="primary")
354
- out = gr.Textbox(lines=40, label="Output (markdown)")
355
- run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
356
  return demo
357
 
358
 
359
  if __name__ == "__main__":
360
- _load_model()
361
- _create_gradio_demo().launch()
 
1
  #!/usr/bin/env python3
2
 
3
+ import asyncio
4
  import logging
5
  import os
6
+ from typing import Any
 
 
 
7
 
8
+ try:
9
+ _orig_close = asyncio.BaseEventLoop.close
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ def _safe_close(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
13
+ _orig_close(self)
14
+ except (ValueError, OSError):
15
  pass
16
 
17
+ asyncio.BaseEventLoop.close = _safe_close
18
+ except Exception:
19
+ pass
20
 
21
+ log = logging.getLogger("glmocr_raw_app")
22
+ logging.basicConfig(level=logging.INFO)
 
23
 
24
+ GLMOCR_API_KEY = "cee1d52dd91a4ab591b3f6e105f8ad89.LgbQTECuzX0zrito"
 
 
25
 
26
+ _parser = None
 
 
 
 
27
 
 
 
28
 
29
+ def get_parser():
30
+ global _parser
31
+ if _parser is None:
32
+ from glmocr import GlmOcr
 
33
 
34
+ kwargs = {"mode": "maas"}
35
+ if GLMOCR_API_KEY:
36
+ kwargs["api_key"] = GLMOCR_API_KEY
37
+ _parser = GlmOcr(**kwargs)
38
+ return _parser
 
39
 
 
 
 
 
 
 
 
 
40
 
41
+ def _extract_markdown(result: Any) -> str:
42
+ if result is None:
43
+ return ""
44
+ if isinstance(result, list):
45
+ parts = []
46
+ for item in result:
47
+ md = getattr(item, "markdown_result", "")
48
+ if md:
49
+ parts.append(str(md).strip())
50
+ return "\n\n---page-separator---\n\n".join(p for p in parts if p).strip()
51
+ md = getattr(result, "markdown_result", "")
52
+ return str(md).strip() if md else ""
53
 
54
 
55
+ def run_ocr_raw(uploaded_file):
56
  if uploaded_file is None:
57
  return "Please upload a file."
 
 
58
  try:
59
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
60
+ parser = get_parser()
61
+ result = parser.parse(path)
62
+ markdown = _extract_markdown(result)
63
+ return markdown or "(No content)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  except Exception as e:
65
  import traceback
 
 
66
 
67
+ log.exception("run_ocr_raw failed: %s", e)
68
+ return f"Error: {e}\n\n{traceback.format_exc()}"
 
 
 
 
 
 
 
 
 
69
 
70
 
71
  def _create_gradio_demo():
72
  import gradio as gr
73
 
74
+ with gr.Blocks(title="GLM-OCR Raw (no fixes)") as demo:
75
+ gr.Markdown("# GLM-OCR Raw (No Fixes)")
 
76
  file_in = gr.File(
77
  label="Upload PDF or image",
78
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
79
  )
80
  run_btn = gr.Button("Run OCR", variant="primary")
81
+ out = gr.Textbox(lines=40, label="Raw markdown_result")
82
+ run_btn.click(fn=run_ocr_raw, inputs=file_in, outputs=out)
83
  return demo
84
 
85
 
86
  if __name__ == "__main__":
87
+ _create_gradio_demo().launch()