rehan953 commited on
Commit
7d3501d
Β·
verified Β·
1 Parent(s): 3f65a80

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -82
app.py CHANGED
@@ -1,6 +1,5 @@
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:
@@ -20,52 +19,78 @@ import re
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,134 +99,186 @@ try:
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:
98
  bbox = r.get("bbox_2d") if isinstance(r, dict) else getattr(r, "bbox_2d", None)
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)
159
- page_heights.append(doc[i].rect.height * 1.5)
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
 
190
- return "\n\n---\n\n".join(all_pages) if all_pages else "(No content)"
191
-
192
  except Exception as e:
193
  import traceback
194
  log.exception("run_ocr failed: %s", e)
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__":
 
1
  """
2
+ GLM-OCR Hugging Face Space app with client-side header fallback for MaaS.
 
3
  """
4
  import asyncio
5
  try:
 
19
  import tempfile
20
  import yaml
21
  import gradio as gr
22
+
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
+ DEFAULT_ZONE_FRAC = float(os.environ.get("GLMOCR_DEFAULT_ZONE_FRAC", "0.12"))
32
+ MIN_CROP_HEIGHT = int(os.environ.get("GLMOCR_MIN_CROP_HEIGHT", "112"))
33
+ MIN_CROP_PIXELS = int(os.environ.get("GLMOCR_MIN_CROP_PIXELS", "12544"))
34
+ PDF_HEADER_BAND_FRAC = float(os.environ.get("GLMOCR_PDF_HEADER_BAND", "0.15"))
35
+
36
+ _parser = None
37
+
38
+ def get_parser():
39
+ global _parser
40
+ if _parser is None:
41
+ from glmocr import GlmOcr
42
+ _parser = GlmOcr(
43
+ api_key=os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV"),
44
+ mode="maas",
45
+ )
46
+ return _parser
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # 1. Config
50
+ # ---------------------------------------------------------------------------
51
  try:
52
  with open(CONFIG_PATH, "r") as f:
53
  config = yaml.safe_load(f)
54
  config["pipeline"]["maas"]["enabled"] = True
55
+ config["pipeline"]["maas"]["api_key"] = os.environ.get("GLMOCR_API_KEY", "4570c28bdea5493c9efae9dae68edc66.sGbA9DLlcX1GlvqV")
56
+
57
+ to_include_as_text = {"header", "footer"}
 
58
  layout_section = config.get("pipeline", {}).get("layout", {})
59
  if "label_task_mapping" in layout_section:
60
  mapping = layout_section["label_task_mapping"]
61
+ abandon = mapping.get("abandon")
62
+ if isinstance(abandon, list):
63
+ mapping["abandon"] = [x for x in abandon if x not in to_include_as_text]
64
+ text_labels = mapping.get("text")
65
+ if isinstance(text_labels, list):
66
+ for label in to_include_as_text:
67
+ if label not in text_labels:
68
+ text_labels.append(label)
69
+
70
  formatter_section = config.get("pipeline", {}).get("result_formatter", {})
71
+ if "abandon" in formatter_section:
72
+ abandon = formatter_section["abandon"]
73
+ if isinstance(abandon, list):
74
+ formatter_section["abandon"] = [x for x in abandon if x not in to_include_as_text]
75
  if "label_visualization_mapping" in formatter_section:
76
+ text_vis = formatter_section["label_visualization_mapping"].get("text")
77
+ if isinstance(text_vis, list):
78
+ for label in to_include_as_text:
79
+ if label not in text_vis:
80
+ text_vis.append(label)
81
+
82
  with open(CONFIG_PATH, "w") as f:
83
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
84
  except Exception:
85
  pass
86
 
87
+ # ---------------------------------------------------------------------------
88
+ # 2. result_formatter
89
+ # ---------------------------------------------------------------------------
90
  try:
91
  with open(FORMATTER_PATH, "r") as f:
92
  source = f.read()
93
+ for label in ('"header"', "'header'", '"footer"', "'footer'", '"doc_header"', "'doc_header'", '"doc_footer"', "'doc_footer'"):
 
94
  source = re.sub(r",\s*" + re.escape(label), "", source)
95
  source = re.sub(re.escape(label) + r"\s*,", "", source)
96
  source = re.sub(re.escape(label), "", source)
 
99
  except Exception:
100
  pass
101
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ def get_header_zone(regions, norm_height=1000):
104
+ """Return only the top gap fraction. Never touch the bottom."""
105
+ if not regions:
106
+ return None
 
 
 
107
  y_tops = []
108
  for r in regions:
109
  bbox = r.get("bbox_2d") if isinstance(r, dict) else getattr(r, "bbox_2d", None)
110
  if bbox and len(bbox) >= 4:
111
  y_tops.append(bbox[1])
112
  if not y_tops:
113
+ return None
114
+ return min(y_tops) / norm_height
 
 
 
115
 
116
 
117
+ def extract_zone_text_pdf(pdf_path, page_num, y_start_frac, y_end_frac):
 
118
  try:
119
  import pymupdf as fitz
120
+ doc = fitz.open(pdf_path)
121
  page = doc[page_num]
122
  h, w = page.rect.height, page.rect.width
123
+ rect = fitz.Rect(0, h * y_start_frac, w, h * y_end_frac)
124
+ text = page.get_text(clip=rect).strip()
125
  doc.close()
126
  return text
127
  except Exception:
128
  return ""
129
 
130
 
131
+ def extract_pdf_text_in_band(pdf_path, page_num, y_start_frac, y_end_frac):
132
+ try:
133
+ import pymupdf as fitz
134
+ doc = fitz.open(pdf_path)
135
+ page = doc[page_num]
136
+ h = page.rect.height
137
+ y_lo = h * y_start_frac
138
+ y_hi = h * y_end_frac
139
+ words = page.get_text("words")
140
+ doc.close()
141
+ parts = []
142
+ for w in words:
143
+ if len(w) >= 5:
144
+ y0, y1 = float(w[1]), float(w[3])
145
+ if y0 < y_hi and y1 > y_lo:
146
+ parts.append(w[4])
147
+ return " ".join(parts).strip()
148
+ except Exception:
149
+ return ""
150
+
151
+
152
+ def ocr_zone(image_path, y_start_frac, y_end_frac):
153
+ try:
154
+ from PIL import Image
155
+ img = Image.open(image_path).convert("RGB")
156
+ w, h = img.size
157
+ y0 = max(0, int(h * y_start_frac))
158
+ y1 = min(h, int(h * y_end_frac))
159
+ if y1 <= y0:
160
+ return ""
161
+ crop = img.crop((0, y0, w, y1))
162
+ cw, ch = crop.size
163
+ if ch < MIN_CROP_HEIGHT or (cw * ch) < MIN_CROP_PIXELS:
164
+ need_h = max(ch, MIN_CROP_HEIGHT)
165
+ need_w = max(cw, 1)
166
+ if (need_w * need_h) < MIN_CROP_PIXELS:
167
+ need_w = max(need_w, (MIN_CROP_PIXELS + need_h - 1) // need_h)
168
+ canvas = Image.new("RGB", (need_w, need_h), (255, 255, 255))
169
+ canvas.paste(crop, (0, 0))
170
+ crop = canvas
171
+ fd, path = tempfile.mkstemp(suffix=".jpg")
172
+ os.close(fd)
173
+ try:
174
+ crop.save(path, "JPEG", quality=92)
175
+ parser = get_parser()
176
+ out = parser.parse(path)
177
+ if not isinstance(out, list):
178
+ out = [out]
179
+ if out and getattr(out[0], "markdown_result", None):
180
+ return (out[0].markdown_result or "").strip()
181
+ finally:
182
+ try:
183
+ os.unlink(path)
184
+ except Exception:
185
+ pass
186
+ except Exception as e:
187
+ log.warning("[header] ocr_zone failed: %s", e, exc_info=True)
188
+ return ""
189
+
190
+
191
+ def get_page_md_and_regions(page_result):
192
  md = ""
193
  if hasattr(page_result, "markdown_result") and page_result.markdown_result:
194
  md = (page_result.markdown_result or "").strip()
195
  regions = []
196
  if hasattr(page_result, "json_result"):
197
  jr = page_result.json_result
198
+ if isinstance(jr, dict) and "regions" in jr:
199
+ regions = jr.get("regions") or []
200
+ elif isinstance(jr, list) and len(jr) > 0:
201
  r = jr[0] if isinstance(jr[0], list) else jr
202
  if isinstance(r, list):
203
  regions = r
204
+ elif isinstance(r, dict) and "regions" in r:
205
+ regions = r.get("regions") or []
206
  return md, regions
207
 
208
 
 
209
  def run_ocr(uploaded_file):
210
  if uploaded_file is None:
211
  return "Please upload a file."
212
  try:
213
  import pymupdf as fitz
214
 
215
+ path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)
216
  is_pdf = path.lower().endswith(".pdf")
217
  parser = get_parser()
218
 
219
  if is_pdf:
220
+ doc = fitz.open(path)
221
+ page_images = []
 
222
  for i in range(len(doc)):
223
+ pix = doc[i].get_pixmap(matrix=fitz.Matrix(1.5, 1.5), alpha=False)
224
  img_path = os.path.join(tempfile.gettempdir(), f"maas_page_{i}.png")
225
  pix.save(img_path)
226
  page_images.append(img_path)
 
227
  doc.close()
228
  results = parser.parse(page_images)
229
  else:
230
+ page_images = [path]
231
+ results = parser.parse(path)
 
232
 
233
  if not isinstance(results, list):
234
  results = [results]
235
 
236
  all_pages = []
237
  for page_num, page_result in enumerate(results):
238
+ page_md, regions = get_page_md_and_regions(page_result)
239
+
240
+ he = get_header_zone(regions, 1000)
241
+ if he is None or he <= 0:
242
+ he = DEFAULT_ZONE_FRAC
243
+
244
  parts = []
245
 
246
+ # ── HEADER at TOP only ────────────────────────────
247
+ if page_num < len(page_images):
248
+ img_path = page_images[page_num]
249
+ hdr = ""
250
+ if is_pdf:
251
+ hdr = extract_zone_text_pdf(path, page_num, 0, he)
252
+ if not (hdr and hdr.strip()):
253
+ hdr = extract_pdf_text_in_band(path, page_num, 0, PDF_HEADER_BAND_FRAC)
254
+ if not (hdr and hdr.strip()):
255
+ hdr = ocr_zone(img_path, 0, he)
256
+ if hdr and hdr.strip():
257
+ parts.append(hdr.strip())
258
 
259
+ # ── BODY in MIDDLE ────────────────────────────────
260
  if page_md:
261
  parts.append(page_md)
262
 
263
+ # ── NO footer extraction ──────────────────────────
264
+ # The API already captures real footer text correctly.
265
+ # Extracting bottom zone duplicates body content.
266
+
267
  if parts:
268
  all_pages.append("\n\n".join(parts))
269
 
270
+ return "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
 
271
  except Exception as e:
272
  import traceback
273
  log.exception("run_ocr failed: %s", e)
274
  return f"Error: {e}\n\n{traceback.format_exc()}"
275
 
276
 
 
277
  with gr.Blocks(title="GLM-OCR") as demo:
278
+ gr.Markdown("# GLM-OCR\nUpload a PDF or image. Headers and footers included.")
279
+ file_in = gr.File(label="Upload PDF or image", file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"])
 
280
  run_btn = gr.Button("Run OCR", variant="primary")
281
+ out = gr.Textbox(lines=40, label="Output")
282
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
283
 
284
  if __name__ == "__main__":