rehan953 commited on
Commit
7beae18
·
verified ·
1 Parent(s): 5625297

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -250
app.py CHANGED
@@ -1,46 +1,17 @@
1
  #!/usr/bin/env python3
2
 
3
- import asyncio
4
  import logging
5
  import os
6
- import re
7
  import tempfile
8
  import uuid
9
  from typing import List, Tuple
10
 
11
- import yaml
12
-
13
- try:
14
- _orig_close = asyncio.BaseEventLoop.close
15
-
16
- def _safe_close(self):
17
- try:
18
- _orig_close(self)
19
- except (ValueError, OSError):
20
- pass
21
-
22
- asyncio.BaseEventLoop.close = _safe_close
23
- except Exception:
24
- pass
25
-
26
- try:
27
- import glmocr
28
-
29
- GLMOCR_BASE = os.path.dirname(glmocr.__file__)
30
- CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
31
- except ImportError:
32
- glmocr = None
33
- GLMOCR_BASE = ""
34
- CONFIG_PATH = ""
35
-
36
  log = logging.getLogger("glmocr_simple_app")
37
  logging.basicConfig(level=logging.INFO)
38
 
39
- GLMOCR_API_KEY = "cee1d52dd91a4ab591b3f6e105f8ad89.LgbQTECuzX0zrito"
40
- if not GLMOCR_API_KEY:
41
- log.warning(
42
- "No ZHIPU_API_KEY or GLMOCR_API_KEY in environment; GlmOcr() will fail until you set one."
43
- )
44
 
45
  RENDER_SCALE = 3.0
46
  PAD_LEFT_FRAC = 0.035
@@ -55,17 +26,36 @@ UNSHARP_RADIUS = 0.78
55
  UNSHARP_PERCENT = 76
56
  UNSHARP_THRESHOLD = 1
57
 
58
- DEFAULT_ZONE_FRAC = 0.12
59
- PDF_HEADER_BAND_FRAC = 0.10
60
- ENABLE_FOOTER_OCR = True
61
- PDF_FOOTER_BAND_FRAC = 0.88
62
-
63
- MIN_CROP_HEIGHT = 112
64
- MIN_CROP_PIXELS = 112 * 112
65
  PAGE_PNG_COMPRESS_LEVEL = 3
66
- ZONE_JPEG_QUALITY = 95
 
67
 
68
- _parser = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
 
71
  def _enhance_raster_for_ocr(img):
@@ -84,129 +74,67 @@ def _enhance_raster_for_ocr(img):
84
  return img
85
 
86
 
87
- def get_parser():
88
- global _parser
89
- if glmocr is None:
90
- raise RuntimeError("glmocr is not installed.")
91
- if _parser is None:
92
- from glmocr import GlmOcr
93
 
94
- kw = {"mode": "maas"}
95
- if GLMOCR_API_KEY:
96
- kw["api_key"] = GLMOCR_API_KEY
97
- _parser = GlmOcr(**kw)
98
- return _parser
 
 
99
 
100
 
101
- if CONFIG_PATH:
102
- try:
103
- with open(CONFIG_PATH, "r", encoding="utf-8") as f:
104
- config = yaml.safe_load(f)
105
- config.setdefault("pipeline", {}).setdefault("maas", {})
106
- config["pipeline"]["maas"]["enabled"] = True
107
- config["pipeline"]["maas"]["api_key"] = GLMOCR_API_KEY
108
- with open(CONFIG_PATH, "w", encoding="utf-8") as f:
109
- yaml.dump(config, f, default_flow_style=False, sort_keys=False)
110
- except Exception:
111
- pass
112
-
113
-
114
- def get_header_footer_zones(regions, norm_height=1000):
115
- if not regions:
116
- return None, None
117
- y_tops, y_bottoms = [], []
118
- for r in regions:
119
- bbox = r.get("bbox_2d") if isinstance(r, dict) else getattr(r, "bbox_2d", None)
120
- if bbox and len(bbox) >= 4:
121
- y_tops.append(bbox[1])
122
- y_bottoms.append(bbox[3])
123
- if not y_tops:
124
- return None, None
125
- return min(y_tops) / norm_height, max(y_bottoms) / norm_height
126
-
127
-
128
- def extract_zone_text_pdf(pdf_path, page_num, y_start_frac, y_end_frac):
129
- try:
130
- import pymupdf as fitz
131
 
132
- doc = fitz.open(pdf_path)
133
- page = doc[page_num]
134
- h, w = page.rect.height, page.rect.width
135
- rect = fitz.Rect(0, h * y_start_frac, w, h * y_end_frac)
136
- text = page.get_text(clip=rect).strip()
137
- doc.close()
138
- return text
139
- except Exception:
140
- return ""
141
 
 
 
142
 
143
- def extract_pdf_text_in_band(pdf_path, page_num, y_start_frac, y_end_frac):
144
- try:
145
- import pymupdf as fitz
146
-
147
- doc = fitz.open(pdf_path)
148
- page = doc[page_num]
149
- h = page.rect.height
150
- y_lo = h * y_start_frac
151
- y_hi = h * y_end_frac
152
- words = page.get_text("words")
153
- doc.close()
154
- parts = []
155
- for w in words:
156
- if len(w) >= 5:
157
- y0, y1 = float(w[1]), float(w[3])
158
- if y0 < y_hi and y1 > y_lo:
159
- parts.append(w[4])
160
- return " ".join(parts).strip()
161
- except Exception:
162
- return ""
163
-
164
-
165
- def ocr_zone(image_path, y_start_frac, y_end_frac):
166
- zone_name = "header" if y_end_frac < 0.5 else "footer"
167
  try:
168
- from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- img = Image.open(image_path).convert("RGB")
171
- w, h = img.size
172
- y0 = max(0, int(h * y_start_frac))
173
- y1 = min(h, int(h * y_end_frac))
174
- if y1 <= y0:
175
- return ""
176
-
177
- crop = img.crop((0, y0, w, y1))
178
- cw, ch = crop.size
179
-
180
- if ch < MIN_CROP_HEIGHT or (cw * ch) < MIN_CROP_PIXELS:
181
- need_h = max(ch, MIN_CROP_HEIGHT)
182
- need_w = max(cw, 1)
183
- if (need_w * need_h) < MIN_CROP_PIXELS:
184
- need_w = max(need_w, (MIN_CROP_PIXELS + need_h - 1) // need_h)
185
- canvas = Image.new("RGB", (need_w, need_h), (255, 255, 255))
186
- if zone_name == "header":
187
- canvas.paste(crop, (0, 0))
188
- else:
189
- canvas.paste(crop, (0, need_h - ch))
190
- crop = canvas
191
-
192
- fd, path = tempfile.mkstemp(suffix=".jpg")
193
- os.close(fd)
194
  try:
195
- crop.save(path, "JPEG", quality=ZONE_JPEG_QUALITY)
196
- parser = get_parser()
197
- out = parser.parse(path)
198
- if not isinstance(out, list):
199
- out = [out]
200
- if out and getattr(out[0], "markdown_result", None):
201
- return (out[0].markdown_result or "").strip()
202
- finally:
203
- try:
204
- os.unlink(path)
205
- except Exception:
206
- pass
207
- except Exception as e:
208
- log.warning("[%s] ocr_zone failed: %s", zone_name, e, exc_info=True)
209
- return ""
210
 
211
 
212
  def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
@@ -219,7 +147,9 @@ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
219
 
220
  for i in range(len(doc)):
221
  page = doc[i]
222
- pix = page.get_pixmap(matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False)
 
 
223
 
224
  img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
225
  img = _enhance_raster_for_ocr(img)
@@ -231,12 +161,17 @@ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
231
  pad_b = int(h * PAD_BOTTOM_FRAC)
232
 
233
  if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
234
- canvas = Image.new("RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255))
 
 
235
  canvas.paste(img, (pad_l, pad_t))
236
  img = canvas
237
 
238
  uniq = uuid.uuid4().hex[:10]
239
- img_path = os.path.join(tempfile.gettempdir(), f"glmocr_page_{os.getpid()}_{uniq}_{i}.png")
 
 
 
240
  img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
241
  page_images.append(img_path)
242
  page_heights.append(img.height)
@@ -245,24 +180,6 @@ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
245
  return page_images, page_heights
246
 
247
 
248
- def get_page_md_and_regions(page_result):
249
- md = ""
250
- if hasattr(page_result, "markdown_result") and page_result.markdown_result:
251
- md = (page_result.markdown_result or "").strip()
252
- regions = []
253
- if hasattr(page_result, "json_result"):
254
- jr = page_result.json_result
255
- if isinstance(jr, dict) and "regions" in jr:
256
- regions = jr.get("regions") or []
257
- elif isinstance(jr, list) and len(jr) > 0:
258
- r = jr[0] if isinstance(jr[0], list) else jr
259
- if isinstance(r, list):
260
- regions = r
261
- elif isinstance(r, dict) and "regions" in r:
262
- regions = r.get("regions") or []
263
- return md, regions
264
-
265
-
266
  def run_ocr(uploaded_file):
267
  if uploaded_file is None:
268
  return "Please upload a file."
@@ -271,92 +188,39 @@ def run_ocr(uploaded_file):
271
  try:
272
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
273
  is_pdf = path.lower().endswith(".pdf")
274
- parser = get_parser()
275
-
276
- page_heights: List[int] = []
277
 
278
  if is_pdf:
279
- page_images, page_heights = render_pdf_pages_to_images(path)
280
- results = parser.parse(page_images)
281
  else:
282
  page_images = [path]
283
- page_heights = [1000]
284
- results = parser.parse(path)
285
-
286
- if not isinstance(results, list):
287
- results = [results]
288
 
289
  all_pages = []
290
- for page_num, page_result in enumerate(results):
291
- page_md, regions = get_page_md_and_regions(page_result)
292
- img_h = page_heights[page_num] if page_num < len(page_heights) else 1000
293
- header_end_frac, footer_start_frac = get_header_footer_zones(regions, img_h)
294
-
295
- he = header_end_frac if header_end_frac is not None else DEFAULT_ZONE_FRAC
296
- fs = footer_start_frac if footer_start_frac is not None else (1.0 - DEFAULT_ZONE_FRAC)
297
-
298
- he = max(0.02, min(0.25, he))
299
- fs = max(0.75, min(0.98, fs))
300
-
301
- parts = []
302
-
303
- hdr = ""
304
- if is_pdf:
305
- hdr = extract_zone_text_pdf(path, page_num, 0, he)
306
- if not (hdr and hdr.strip()):
307
- hdr = extract_pdf_text_in_band(path, page_num, 0, PDF_HEADER_BAND_FRAC)
308
- if not (hdr and hdr.strip()) and page_num < len(page_images):
309
- hdr = ocr_zone(page_images[page_num], 0, he)
310
- if hdr and hdr.strip():
311
- parts.append(hdr.strip())
312
-
313
- if page_md and page_md.strip():
314
- parts.append(page_md.strip())
315
-
316
- if ENABLE_FOOTER_OCR and page_num < len(page_images):
317
- ftr = ""
318
- if is_pdf:
319
- ftr = extract_zone_text_pdf(path, page_num, fs, 1.0)
320
- if not (ftr and ftr.strip()):
321
- ftr = extract_pdf_text_in_band(path, page_num, PDF_FOOTER_BAND_FRAC, 1.0)
322
- if not (ftr and ftr.strip()):
323
- ftr = ocr_zone(page_images[page_num], fs, 1.0)
324
- if ftr and ftr.strip():
325
- ftr_clean = ftr.strip()
326
-
327
- ftr_first_line = next(
328
- (ln.strip().lower() for ln in ftr_clean.splitlines() if ln.strip()),
329
- "",
330
- )
331
- already_present = ftr_first_line and any(
332
- ftr_first_line in part.lower() for part in parts
333
- )
334
-
335
- _footer_date_re = re.compile(r"\b\d{1,2}[-/]\d{2}\b")
336
- _footer_amt_re = re.compile(r"\b\d{1,3}(?:,\d{3})*\.\d{2}\b")
337
- _date_hits = len(_footer_date_re.findall(ftr_clean))
338
- _amt_hits = len(_footer_amt_re.findall(ftr_clean))
339
- is_txn_dump = _date_hits >= 3 and _amt_hits >= 3
340
-
341
- if not already_present and not is_txn_dump:
342
- parts.append(ftr_clean)
343
-
344
- if parts:
345
- all_pages.append("\n\n".join(parts))
346
-
347
- merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
348
  return merged
349
 
350
  except Exception as e:
351
  import traceback
352
-
353
  log.exception("run_ocr failed: %s", e)
354
  return f"Error: {e}\n\n{traceback.format_exc()}"
355
 
356
  finally:
357
  for p in page_images:
358
  try:
359
- if isinstance(p, str) and p.endswith(".png") and "glmocr_page_" in os.path.basename(p):
 
 
 
 
360
  os.unlink(p)
361
  except Exception:
362
  pass
@@ -365,17 +229,17 @@ def run_ocr(uploaded_file):
365
  def _create_gradio_demo():
366
  import gradio as gr
367
 
368
- with gr.Blocks(title="GLM-OCR (simple)") as demo:
369
- gr.Markdown("# GLM-OCR")
370
  file_in = gr.File(
371
  label="Upload PDF or image",
372
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
373
  )
374
  run_btn = gr.Button("Run OCR", variant="primary")
375
- out = gr.Textbox(lines=40, label="Output (markdown / HTML from model)")
376
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
377
  return demo
378
 
379
 
380
  if __name__ == "__main__":
381
- _create_gradio_demo().launch()
 
1
  #!/usr/bin/env python3
2
 
 
3
  import logging
4
  import os
 
5
  import tempfile
6
  import uuid
7
  from typing import List, Tuple
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  log = logging.getLogger("glmocr_simple_app")
10
  logging.basicConfig(level=logging.INFO)
11
 
12
+ # ── Fine-tuned model repo on HuggingFace ─────────────────────────────────────
13
+ # Loads from HF Hub at runtime — no local storage needed in the Space
14
+ MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
 
 
15
 
16
  RENDER_SCALE = 3.0
17
  PAD_LEFT_FRAC = 0.035
 
26
  UNSHARP_PERCENT = 76
27
  UNSHARP_THRESHOLD = 1
28
 
 
 
 
 
 
 
 
29
  PAGE_PNG_COMPRESS_LEVEL = 3
30
+ MAX_NEW_TOKENS = 2048
31
+ MAX_IMAGE_SIDE = 1344 # resize longest side to this before inference
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):
 
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 _infer_image(image_path: str) -> str:
91
+ """Run fine-tuned model on a single image file and return markdown string."""
92
+ import torch
93
+ from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
+ model, processor = _load_model()
 
 
 
 
 
 
 
 
96
 
97
+ img = Image.open(image_path).convert("RGB")
98
+ img = _resize_for_inference(img)
99
 
100
+ fd, resized_path = tempfile.mkstemp(suffix=".png")
101
+ os.close(fd)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
+ img.save(resized_path, "PNG")
104
+
105
+ messages = [{
106
+ "role": "user",
107
+ "content": [
108
+ {"type": "image", "url": resized_path},
109
+ {"type": "text", "text": "Document Parsing:"},
110
+ ],
111
+ }]
112
+
113
+ inputs = processor.apply_chat_template(
114
+ messages,
115
+ tokenize=True,
116
+ add_generation_prompt=True,
117
+ return_dict=True,
118
+ return_tensors="pt",
119
+ ).to(model.device)
120
+ inputs.pop("token_type_ids", None)
121
+
122
+ torch.cuda.empty_cache()
123
+
124
+ with torch.no_grad():
125
+ ids = model.generate(**inputs, max_new_tokens=MAX_NEW_TOKENS)
126
+
127
+ result = processor.decode(
128
+ ids[0][inputs["input_ids"].shape[1]:],
129
+ skip_special_tokens=True,
130
+ )
131
+ return result.strip()
132
 
133
+ finally:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  try:
135
+ os.unlink(resized_path)
136
+ except Exception:
137
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
 
140
  def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
 
147
 
148
  for i in range(len(doc)):
149
  page = doc[i]
150
+ pix = page.get_pixmap(
151
+ matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False
152
+ )
153
 
154
  img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
155
  img = _enhance_raster_for_ocr(img)
 
161
  pad_b = int(h * PAD_BOTTOM_FRAC)
162
 
163
  if any(p > 0 for p in (pad_l, pad_r, pad_t, pad_b)):
164
+ canvas = Image.new(
165
+ "RGB", (w + pad_l + pad_r, h + pad_t + pad_b), (255, 255, 255)
166
+ )
167
  canvas.paste(img, (pad_l, pad_t))
168
  img = canvas
169
 
170
  uniq = uuid.uuid4().hex[:10]
171
+ img_path = os.path.join(
172
+ tempfile.gettempdir(),
173
+ f"glmocr_page_{os.getpid()}_{uniq}_{i}.png",
174
+ )
175
  img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
176
  page_images.append(img_path)
177
  page_heights.append(img.height)
 
180
  return page_images, page_heights
181
 
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def run_ocr(uploaded_file):
184
  if uploaded_file is None:
185
  return "Please upload a file."
 
188
  try:
189
  path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
190
  is_pdf = path.lower().endswith(".pdf")
 
 
 
191
 
192
  if is_pdf:
193
+ page_images, _ = render_pdf_pages_to_images(path)
 
194
  else:
195
  page_images = [path]
 
 
 
 
 
196
 
197
  all_pages = []
198
+ for page_num, img_path in enumerate(page_images):
199
+ log.info("Processing page %d / %d ...", page_num + 1, len(page_images))
200
+ page_md = _infer_image(img_path)
201
+ if page_md:
202
+ all_pages.append(page_md)
203
+
204
+ merged = (
205
+ "\n\n---page-separator---\n\n".join(all_pages)
206
+ if all_pages
207
+ else "(No content extracted)"
208
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  return merged
210
 
211
  except Exception as e:
212
  import traceback
 
213
  log.exception("run_ocr failed: %s", e)
214
  return f"Error: {e}\n\n{traceback.format_exc()}"
215
 
216
  finally:
217
  for p in page_images:
218
  try:
219
+ if (
220
+ isinstance(p, str)
221
+ and p.endswith(".png")
222
+ and "glmocr_page_" in os.path.basename(p)
223
+ ):
224
  os.unlink(p)
225
  except Exception:
226
  pass
 
229
  def _create_gradio_demo():
230
  import gradio as gr
231
 
232
+ with gr.Blocks(title="GLM-OCR Fine-tuned") as demo:
233
+ gr.Markdown("# GLM-OCR (Fine-tuned)")
234
  file_in = gr.File(
235
  label="Upload PDF or image",
236
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
237
  )
238
  run_btn = gr.Button("Run OCR", variant="primary")
239
+ out = gr.Textbox(lines=40, label="Output (markdown)")
240
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
241
  return demo
242
 
243
 
244
  if __name__ == "__main__":
245
+ _create_gradio_demo().launch()