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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -164
app.py CHANGED
@@ -1,8 +1,7 @@
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,79 +20,52 @@ import re
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,14 +74,24 @@ try:
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,120 +99,60 @@ def get_top_gap_frac(regions, img_height):
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)
@@ -238,46 +160,30 @@ def run_ocr(uploaded_file):
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))
283
 
@@ -289,11 +195,13 @@ def run_ocr(uploaded_file):
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__":
 
1
  """
2
+ GLM-OCR Hugging Face Space β€” MaaS mode.
3
+ Extracts header from top zone the API missed. No footer extraction.
4
  """
 
5
  import asyncio
6
  try:
7
  _orig_close = asyncio.BaseEventLoop.close
 
20
  import tempfile
21
  import yaml
22
  import gradio as gr
 
23
  import glmocr
24
 
25
  log = logging.getLogger("glmocr_app")
26
 
27
+ GLMOCR_BASE = os.path.dirname(glmocr.__file__)
28
+ CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
29
  FORMATTER_PATH = os.path.join(GLMOCR_BASE, "postprocess", "result_formatter.py")
30
 
31
+ # ── Fix config ────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  try:
33
  with open(CONFIG_PATH, "r") as f:
34
  config = yaml.safe_load(f)
35
  config["pipeline"]["maas"]["enabled"] = True
36
+ config["pipeline"]["maas"]["api_key"] = os.environ.get(
37
+ "GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"
38
+ )
39
+ to_include = {"header", "footer"}
40
  layout_section = config.get("pipeline", {}).get("layout", {})
41
  if "label_task_mapping" in layout_section:
42
  mapping = layout_section["label_task_mapping"]
43
+ if isinstance(mapping.get("abandon"), list):
44
+ mapping["abandon"] = [x for x in mapping["abandon"] if x not in to_include]
45
+ if isinstance(mapping.get("text"), list):
46
+ for lb in to_include:
47
+ if lb not in mapping["text"]:
48
+ mapping["text"].append(lb)
 
 
 
49
  formatter_section = config.get("pipeline", {}).get("result_formatter", {})
50
+ if isinstance(formatter_section.get("abandon"), list):
51
+ formatter_section["abandon"] = [x for x in formatter_section["abandon"] if x not in to_include]
 
 
52
  if "label_visualization_mapping" in formatter_section:
53
+ tv = formatter_section["label_visualization_mapping"].get("text")
54
+ if isinstance(tv, list):
55
+ for lb in to_include:
56
+ if lb not in tv:
57
+ tv.append(lb)
 
58
  with open(CONFIG_PATH, "w") as f:
59
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
60
  except Exception:
61
  pass
62
 
63
+ # ── Fix result_formatter ──────────────────────────────────────
 
 
64
  try:
65
  with open(FORMATTER_PATH, "r") as f:
66
  source = f.read()
67
+ for label in ('"header"', "'header'", '"footer"', "'footer'",
68
+ '"doc_header"', "'doc_header'", '"doc_footer"', "'doc_footer'"):
69
  source = re.sub(r",\s*" + re.escape(label), "", source)
70
  source = re.sub(re.escape(label) + r"\s*,", "", source)
71
  source = re.sub(re.escape(label), "", source)
 
74
  except Exception:
75
  pass
76
 
77
+ # ── Shared parser ─────────────────────────────────────────────
78
+ _parser = None
79
+ def get_parser():
80
+ global _parser
81
+ if _parser is None:
82
+ from glmocr import GlmOcr
83
+ _parser = GlmOcr(
84
+ api_key=os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"),
85
+ mode="maas",
86
+ )
87
+ return _parser
88
 
89
+ # ── Helpers ───────────────────────────────────────────────────
90
  def get_top_gap_frac(regions, img_height):
91
  """
92
+ Returns the fraction of page height that the API missed at the top.
93
+ Based on the y coordinate of the topmost bbox_2d region.
94
+ Returns None if no gap detected (API covered the full page top).
 
 
95
  """
96
  y_tops = []
97
  for r in regions:
 
99
  if bbox and len(bbox) >= 4:
100
  y_tops.append(bbox[1])
101
  if not y_tops:
102
+ # No bbox info from API β€” default to extracting top 12%
103
+ return 0.12
104
  first_y = min(y_tops)
105
+ # Only treat as a missed header if gap is more than 8% of page
106
  return (first_y / img_height) if first_y > img_height * 0.08 else None
107
 
108
 
109
+ def extract_header_pdf(pdf_path, page_num, y_end_frac):
110
+ """Extract text from top zone of PDF page using clip rect."""
111
  try:
112
  import pymupdf as fitz
113
+ doc = fitz.open(pdf_path)
114
  page = doc[page_num]
115
  h, w = page.rect.height, page.rect.width
116
+ text = page.get_text(clip=fitz.Rect(0, 0, w, h * y_end_frac)).strip()
 
117
  doc.close()
118
  return text
119
  except Exception:
120
  return ""
121
 
122
 
123
+ def get_page_data(page_result):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  md = ""
125
  if hasattr(page_result, "markdown_result") and page_result.markdown_result:
126
  md = (page_result.markdown_result or "").strip()
127
  regions = []
128
  if hasattr(page_result, "json_result"):
129
  jr = page_result.json_result
130
+ if isinstance(jr, list) and len(jr) > 0:
 
 
131
  r = jr[0] if isinstance(jr[0], list) else jr
132
  if isinstance(r, list):
133
  regions = r
134
+ elif isinstance(jr, dict) and "regions" in jr:
135
+ regions = jr.get("regions") or []
136
  return md, regions
137
 
138
 
139
+ # ── Main OCR ──────────────────────────────────────────────────
140
  def run_ocr(uploaded_file):
141
  if uploaded_file is None:
142
  return "Please upload a file."
143
  try:
144
  import pymupdf as fitz
145
 
146
+ path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
147
  is_pdf = path.lower().endswith(".pdf")
148
  parser = get_parser()
149
 
150
  if is_pdf:
151
+ doc = fitz.open(path)
152
+ page_images = []
153
  page_heights = []
154
  for i in range(len(doc)):
155
+ pix = doc[i].get_pixmap(matrix=fitz.Matrix(1.5, 1.5), alpha=False)
156
  img_path = os.path.join(tempfile.gettempdir(), f"maas_page_{i}.png")
157
  pix.save(img_path)
158
  page_images.append(img_path)
 
160
  doc.close()
161
  results = parser.parse(page_images)
162
  else:
163
+ page_images = [path]
164
  page_heights = []
165
+ results = parser.parse(path)
166
 
167
  if not isinstance(results, list):
168
  results = [results]
169
 
170
  all_pages = []
171
  for page_num, page_result in enumerate(results):
172
+ page_md, regions = get_page_data(page_result)
173
  parts = []
174
 
175
+ # HEADER β€” extract top zone missed by API (PDF only)
176
+ if is_pdf and page_num < len(page_heights):
177
+ top_gap = get_top_gap_frac(regions, page_heights[page_num])
178
+ if top_gap is not None:
179
+ hdr = extract_header_pdf(path, page_num, top_gap)
180
+ if hdr:
181
+ parts.append(hdr)
 
 
 
 
 
 
 
 
 
 
182
 
183
+ # BODY β€” full API output (already includes real footer text)
184
  if page_md:
185
  parts.append(page_md)
186
 
 
 
 
 
 
 
187
  if parts:
188
  all_pages.append("\n\n".join(parts))
189
 
 
195
  return f"Error: {e}\n\n{traceback.format_exc()}"
196
 
197
 
198
+ # ── 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(label="Upload PDF or image",
202
+ file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"])
203
  run_btn = gr.Button("Run OCR", variant="primary")
204
+ out = gr.Textbox(lines=40, label="Output")
205
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
206
 
207
  if __name__ == "__main__":