rehan953 commited on
Commit
68c9814
Β·
verified Β·
1 Parent(s): f8367fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -73
app.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
- GLM-OCR Hugging Face Space β€” MaaS mode with header extraction.
3
- Header is extracted from PDF text layer using bbox gap detection.
4
- Footer is NOT extracted separately β€” API already captures it as text regions.
5
  """
 
6
  import asyncio
7
  try:
8
  _orig_close = asyncio.BaseEventLoop.close
@@ -21,44 +21,79 @@ import re
21
  import tempfile
22
  import yaml
23
  import gradio as gr
 
24
  import glmocr
25
 
26
  log = logging.getLogger("glmocr_app")
27
 
28
- GLMOCR_BASE = os.path.dirname(glmocr.__file__)
29
- CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
30
  FORMATTER_PATH = os.path.join(GLMOCR_BASE, "postprocess", "result_formatter.py")
31
 
32
- DEFAULT_HEADER_FRAC = float(os.environ.get("GLMOCR_DEFAULT_HEADER_FRAC", "0.12"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- # ── STEP 1: Fix config ────────────────────────────────────────
 
 
35
  try:
36
  with open(CONFIG_PATH, "r") as f:
37
  config = yaml.safe_load(f)
38
-
39
  config["pipeline"]["maas"]["enabled"] = True
40
- config["pipeline"]["maas"]["api_key"] = os.environ.get(
41
- "GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"
42
- )
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- to_include = {"header", "footer"}
45
  formatter_section = config.get("pipeline", {}).get("result_formatter", {})
46
- if "abandon" in formatter_section and isinstance(formatter_section["abandon"], list):
47
- formatter_section["abandon"] = [
48
- x for x in formatter_section["abandon"] if x not in to_include
49
- ]
 
 
 
 
 
 
50
 
51
  with open(CONFIG_PATH, "w") as f:
52
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
53
  except Exception:
54
  pass
55
 
56
- # ── STEP 2: Fix result_formatter.py ──────────────────────────
 
 
57
  try:
58
  with open(FORMATTER_PATH, "r") as f:
59
  source = f.read()
60
- for label in ('"header"', "'header'", '"footer"', "'footer'",
61
- '"doc_header"', "'doc_header'", '"doc_footer"', "'doc_footer'"):
62
  source = re.sub(r",\s*" + re.escape(label), "", source)
63
  source = re.sub(re.escape(label) + r"\s*,", "", source)
64
  source = re.sub(re.escape(label), "", source)
@@ -67,25 +102,14 @@ try:
67
  except Exception:
68
  pass
69
 
70
- # ── Single shared parser ─────────────────────────────���────────
71
- _parser = None
72
 
73
- def get_parser():
74
- global _parser
75
- if _parser is None:
76
- from glmocr import GlmOcr
77
- _parser = GlmOcr(
78
- api_key=os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"),
79
- mode="maas",
80
- )
81
- return _parser
82
-
83
- # ── STEP 3: Header extraction helpers ────────────────────────
84
  def get_top_gap_frac(regions, img_height):
85
  """
86
- Find the fraction of image height that the API missed at the top.
87
- Uses bbox_2d of the first (topmost) region.
88
- Returns a fraction (0–1) or None if no gap detected.
 
 
89
  """
90
  y_tops = []
91
  for r in regions:
@@ -93,96 +117,166 @@ def get_top_gap_frac(regions, img_height):
93
  if bbox and len(bbox) >= 4:
94
  y_tops.append(bbox[1])
95
  if not y_tops:
96
- return DEFAULT_HEADER_FRAC # no bbox info β€” use default band
97
  first_y = min(y_tops)
98
- # Only treat as missed header if gap > 8% of image height
99
  return (first_y / img_height) if first_y > img_height * 0.08 else None
100
 
101
 
102
- def extract_header_text(pdf_path, page_num, y_end_frac):
103
- """Extract text from the top zone of a PDF page using PyMuPDF."""
104
  try:
105
  import pymupdf as fitz
106
- doc = fitz.open(pdf_path)
107
  page = doc[page_num]
108
  h, w = page.rect.height, page.rect.width
109
- text = page.get_text(clip=fitz.Rect(0, 0, w, h * y_end_frac)).strip()
 
110
  doc.close()
111
  return text
112
  except Exception:
113
  return ""
114
 
115
 
116
- def get_page_data(page_result):
117
- """Extract markdown and regions from one PipelineResult."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  md = ""
119
  if hasattr(page_result, "markdown_result") and page_result.markdown_result:
120
  md = (page_result.markdown_result or "").strip()
121
  regions = []
122
  if hasattr(page_result, "json_result"):
123
  jr = page_result.json_result
124
- if isinstance(jr, list) and len(jr) > 0:
 
 
125
  r = jr[0] if isinstance(jr[0], list) else jr
126
  if isinstance(r, list):
127
  regions = r
 
 
128
  return md, regions
129
 
130
 
131
- # ── STEP 4: Main OCR function ─────────────────────────────────
132
  def run_ocr(uploaded_file):
133
  if uploaded_file is None:
134
  return "Please upload a file."
135
  try:
136
  import pymupdf as fitz
137
 
138
- path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
139
  is_pdf = path.lower().endswith(".pdf")
140
  parser = get_parser()
141
 
142
  if is_pdf:
143
- doc = fitz.open(path)
144
- page_images = []
145
  page_heights = []
146
  for i in range(len(doc)):
147
- pix = doc[i].get_pixmap(matrix=fitz.Matrix(1.5, 1.5), alpha=False)
148
  img_path = os.path.join(tempfile.gettempdir(), f"maas_page_{i}.png")
149
  pix.save(img_path)
150
  page_images.append(img_path)
151
- # Image height at 1.5x matches bbox_2d coordinate space
152
  page_heights.append(doc[i].rect.height * 1.5)
153
  doc.close()
154
  results = parser.parse(page_images)
155
  else:
156
- page_images = [path]
157
  page_heights = []
158
- results = parser.parse(path)
159
 
160
  if not isinstance(results, list):
161
  results = [results]
162
 
163
  all_pages = []
164
-
165
  for page_num, page_result in enumerate(results):
166
- page_md, regions = get_page_data(page_result)
167
  parts = []
168
 
169
  # ── HEADER at TOP ─────────────────────────────────
170
- # Only for PDFs β€” extract top zone the API missed.
171
- # We do NOT extract footer β€” the API already captures
172
- # footer text (copyright, address) as regular text regions.
173
- if is_pdf and page_num < len(page_heights):
174
- top_gap = get_top_gap_frac(regions, page_heights[page_num])
175
- if top_gap is not None:
176
- hdr = extract_header_text(path, page_num, top_gap)
177
- if hdr:
178
- parts.append(hdr)
 
 
 
 
 
 
 
179
 
180
  # ── BODY in MIDDLE ────────────────────────────────
181
  if page_md:
182
  parts.append(page_md)
183
 
184
- # No footer extraction β€” avoids duplicating body content.
185
- # The API captures real footer text as text regions in page_md.
 
 
 
186
 
187
  if parts:
188
  all_pages.append("\n\n".join(parts))
@@ -195,15 +289,11 @@ def run_ocr(uploaded_file):
195
  return f"Error: {e}\n\n{traceback.format_exc()}"
196
 
197
 
198
- # ── STEP 5: Gradio UI ─────────────────────────────────────────
199
  with gr.Blocks(title="GLM-OCR") as demo:
200
- gr.Markdown("# πŸ” GLM-OCR\nUpload a PDF or image. Headers included in correct position.")
201
- file_in = gr.File(
202
- label="Upload PDF or image",
203
- file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"]
204
- )
205
- run_btn = gr.Button("β–Ά Run OCR", variant="primary")
206
- out = gr.Textbox(lines=40, label="Output")
207
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
208
 
209
  if __name__ == "__main__":
 
1
  """
2
+ GLM-OCR Hugging Face Space app with client-side header/footer fallback for MaaS.
3
+ Works for every PDF and every image: uses API bboxes when available, else minimal band.
 
4
  """
5
+ # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
6
  import asyncio
7
  try:
8
  _orig_close = asyncio.BaseEventLoop.close
 
21
  import tempfile
22
  import yaml
23
  import gradio as gr
24
+
25
  import glmocr
26
 
27
  log = logging.getLogger("glmocr_app")
28
 
29
+ GLMOCR_BASE = os.path.dirname(glmocr.__file__)
30
+ CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
31
  FORMATTER_PATH = os.path.join(GLMOCR_BASE, "postprocess", "result_formatter.py")
32
 
33
+ DEFAULT_ZONE_FRAC = float(os.environ.get("GLMOCR_DEFAULT_ZONE_FRAC", "0.12"))
34
+ MIN_CROP_HEIGHT = int(os.environ.get("GLMOCR_MIN_CROP_HEIGHT", "112"))
35
+ MIN_CROP_PIXELS = int(os.environ.get("GLMOCR_MIN_CROP_PIXELS", "12544"))
36
+ PDF_HEADER_BAND_FRAC = float(os.environ.get("GLMOCR_PDF_HEADER_BAND", "0.15"))
37
+
38
+ # Single shared parser
39
+ _parser = None
40
+
41
+ def get_parser():
42
+ global _parser
43
+ if _parser is None:
44
+ from glmocr import GlmOcr
45
+ _parser = GlmOcr(
46
+ api_key=os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"),
47
+ mode="maas",
48
+ )
49
+ return _parser
50
 
51
+ # ---------------------------------------------------------------------------
52
+ # 1. Config
53
+ # ---------------------------------------------------------------------------
54
  try:
55
  with open(CONFIG_PATH, "r") as f:
56
  config = yaml.safe_load(f)
 
57
  config["pipeline"]["maas"]["enabled"] = True
58
+ config["pipeline"]["maas"]["api_key"] = os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV")
59
+
60
+ to_include_as_text = {"header", "footer"}
61
+ layout_section = config.get("pipeline", {}).get("layout", {})
62
+ if "label_task_mapping" in layout_section:
63
+ mapping = layout_section["label_task_mapping"]
64
+ abandon = mapping.get("abandon")
65
+ if isinstance(abandon, list):
66
+ mapping["abandon"] = [x for x in abandon if x not in to_include_as_text]
67
+ text_labels = mapping.get("text")
68
+ if isinstance(text_labels, list):
69
+ for label in to_include_as_text:
70
+ if label not in text_labels:
71
+ text_labels.append(label)
72
 
 
73
  formatter_section = config.get("pipeline", {}).get("result_formatter", {})
74
+ if "abandon" in formatter_section:
75
+ abandon = formatter_section["abandon"]
76
+ if isinstance(abandon, list):
77
+ formatter_section["abandon"] = [x for x in abandon if x not in to_include_as_text]
78
+ if "label_visualization_mapping" in formatter_section:
79
+ text_vis = formatter_section["label_visualization_mapping"].get("text")
80
+ if isinstance(text_vis, list):
81
+ for label in to_include_as_text:
82
+ if label not in text_vis:
83
+ text_vis.append(label)
84
 
85
  with open(CONFIG_PATH, "w") as f:
86
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
87
  except Exception:
88
  pass
89
 
90
+ # ---------------------------------------------------------------------------
91
+ # 2. result_formatter
92
+ # ---------------------------------------------------------------------------
93
  try:
94
  with open(FORMATTER_PATH, "r") as f:
95
  source = f.read()
96
+ for label in ('"header"', "'header'", '"footer"', "'footer'", '"doc_header"', "'doc_header'", '"doc_footer"', "'doc_footer'"):
 
97
  source = re.sub(r",\s*" + re.escape(label), "", source)
98
  source = re.sub(re.escape(label) + r"\s*,", "", source)
99
  source = re.sub(re.escape(label), "", source)
 
102
  except Exception:
103
  pass
104
 
 
 
105
 
 
 
 
 
 
 
 
 
 
 
 
106
  def get_top_gap_frac(regions, img_height):
107
  """
108
+ Find only the TOP gap the API missed using bbox_2d.
109
+ Returns fraction of image height, or None if no gap.
110
+ We never extract the bottom zone β€” the API already captures
111
+ footer content as text regions, and extracting bottom zone
112
+ causes duplication of body content on pages without real footers.
113
  """
114
  y_tops = []
115
  for r in regions:
 
117
  if bbox and len(bbox) >= 4:
118
  y_tops.append(bbox[1])
119
  if not y_tops:
120
+ return DEFAULT_ZONE_FRAC # no bbox info β€” use default band
121
  first_y = min(y_tops)
 
122
  return (first_y / img_height) if first_y > img_height * 0.08 else None
123
 
124
 
125
+ def extract_zone_text_pdf(pdf_path, page_num, y_start_frac, y_end_frac):
 
126
  try:
127
  import pymupdf as fitz
128
+ doc = fitz.open(pdf_path)
129
  page = doc[page_num]
130
  h, w = page.rect.height, page.rect.width
131
+ rect = fitz.Rect(0, h * y_start_frac, w, h * y_end_frac)
132
+ text = page.get_text(clip=rect).strip()
133
  doc.close()
134
  return text
135
  except Exception:
136
  return ""
137
 
138
 
139
+ def extract_pdf_text_in_band(pdf_path, page_num, y_start_frac, y_end_frac):
140
+ try:
141
+ import pymupdf as fitz
142
+ doc = fitz.open(pdf_path)
143
+ page = doc[page_num]
144
+ h = page.rect.height
145
+ y_lo = h * y_start_frac
146
+ y_hi = h * y_end_frac
147
+ words = page.get_text("words")
148
+ doc.close()
149
+ parts = []
150
+ for w in words:
151
+ if len(w) >= 5:
152
+ y0, y1 = float(w[1]), float(w[3])
153
+ if y0 < y_hi and y1 > y_lo:
154
+ parts.append(w[4])
155
+ return " ".join(parts).strip()
156
+ except Exception:
157
+ return ""
158
+
159
+
160
+ def ocr_zone(image_path, y_start_frac, y_end_frac):
161
+ """Run OCR on a horizontal band β€” used only for header on image files (non-PDF)."""
162
+ try:
163
+ from PIL import Image
164
+ img = Image.open(image_path).convert("RGB")
165
+ w, h = img.size
166
+ y0 = max(0, int(h * y_start_frac))
167
+ y1 = min(h, int(h * y_end_frac))
168
+ if y1 <= y0:
169
+ return ""
170
+ crop = img.crop((0, y0, w, y1))
171
+ cw, ch = crop.size
172
+ if ch < MIN_CROP_HEIGHT or (cw * ch) < MIN_CROP_PIXELS:
173
+ need_h = max(ch, MIN_CROP_HEIGHT)
174
+ need_w = max(cw, 1)
175
+ if (need_w * need_h) < MIN_CROP_PIXELS:
176
+ need_w = max(need_w, (MIN_CROP_PIXELS + need_h - 1) // need_h)
177
+ canvas = Image.new("RGB", (need_w, need_h), (255, 255, 255))
178
+ canvas.paste(crop, (0, 0))
179
+ crop = canvas
180
+ fd, path = tempfile.mkstemp(suffix=".jpg")
181
+ os.close(fd)
182
+ try:
183
+ crop.save(path, "JPEG", quality=92)
184
+ parser = get_parser()
185
+ out = parser.parse(path)
186
+ if not isinstance(out, list):
187
+ out = [out]
188
+ if out and getattr(out[0], "markdown_result", None):
189
+ return (out[0].markdown_result or "").strip()
190
+ finally:
191
+ try:
192
+ os.unlink(path)
193
+ except Exception:
194
+ pass
195
+ except Exception as e:
196
+ log.warning("[header] ocr_zone failed: %s", e, exc_info=True)
197
+ return ""
198
+
199
+
200
+ def get_page_md_and_regions(page_result):
201
  md = ""
202
  if hasattr(page_result, "markdown_result") and page_result.markdown_result:
203
  md = (page_result.markdown_result or "").strip()
204
  regions = []
205
  if hasattr(page_result, "json_result"):
206
  jr = page_result.json_result
207
+ if isinstance(jr, dict) and "regions" in jr:
208
+ regions = jr.get("regions") or []
209
+ elif isinstance(jr, list) and len(jr) > 0:
210
  r = jr[0] if isinstance(jr[0], list) else jr
211
  if isinstance(r, list):
212
  regions = r
213
+ elif isinstance(r, dict) and "regions" in r:
214
+ regions = r.get("regions") or []
215
  return md, regions
216
 
217
 
 
218
  def run_ocr(uploaded_file):
219
  if uploaded_file is None:
220
  return "Please upload a file."
221
  try:
222
  import pymupdf as fitz
223
 
224
+ path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
225
  is_pdf = path.lower().endswith(".pdf")
226
  parser = get_parser()
227
 
228
  if is_pdf:
229
+ doc = fitz.open(path)
230
+ page_images = []
231
  page_heights = []
232
  for i in range(len(doc)):
233
+ pix = doc[i].get_pixmap(matrix=fitz.Matrix(1.5, 1.5), alpha=False)
234
  img_path = os.path.join(tempfile.gettempdir(), f"maas_page_{i}.png")
235
  pix.save(img_path)
236
  page_images.append(img_path)
 
237
  page_heights.append(doc[i].rect.height * 1.5)
238
  doc.close()
239
  results = parser.parse(page_images)
240
  else:
241
+ page_images = [path]
242
  page_heights = []
243
+ results = parser.parse(path)
244
 
245
  if not isinstance(results, list):
246
  results = [results]
247
 
248
  all_pages = []
 
249
  for page_num, page_result in enumerate(results):
250
+ page_md, regions = get_page_md_and_regions(page_result)
251
  parts = []
252
 
253
  # ── HEADER at TOP ─────────────────────────────────
254
+ if page_num < len(page_images):
255
+ img_path = page_images[page_num]
256
+ hdr = ""
257
+ if is_pdf and page_num < len(page_heights):
258
+ top_gap = get_top_gap_frac(regions, page_heights[page_num])
259
+ if top_gap is not None:
260
+ hdr = extract_zone_text_pdf(path, page_num, 0, top_gap)
261
+ if not hdr:
262
+ hdr = extract_pdf_text_in_band(path, page_num, 0, PDF_HEADER_BAND_FRAC)
263
+ else:
264
+ # Image input β€” use OCR on top band
265
+ top_gap = get_top_gap_frac(regions, 1000)
266
+ if top_gap is not None:
267
+ hdr = ocr_zone(img_path, 0, top_gap)
268
+ if hdr:
269
+ parts.append(hdr)
270
 
271
  # ── BODY in MIDDLE ────────────────────────────────
272
  if page_md:
273
  parts.append(page_md)
274
 
275
+ # ── NO footer extraction ──────────────────────────
276
+ # The API already captures real footer text (copyright,
277
+ # address etc.) as text regions inside page_md.
278
+ # Extracting the bottom zone causes duplication on pages
279
+ # where the last body content sits near the page bottom.
280
 
281
  if parts:
282
  all_pages.append("\n\n".join(parts))
 
289
  return f"Error: {e}\n\n{traceback.format_exc()}"
290
 
291
 
 
292
  with gr.Blocks(title="GLM-OCR") as demo:
293
+ gr.Markdown("# GLM-OCR\nUpload a PDF or image. Headers and footers included.")
294
+ file_in = gr.File(label="Upload PDF or image", file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"])
295
+ run_btn = gr.Button("Run OCR", variant="primary")
296
+ out = gr.Textbox(lines=40, label="Output")
 
 
 
297
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
298
 
299
  if __name__ == "__main__":