rehan953 commited on
Commit
6415913
·
verified ·
1 Parent(s): 5534d19

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -37
app.py CHANGED
@@ -18,11 +18,9 @@ logging.basicConfig(level=logging.INFO)
18
  MODEL_ID = "zai-org/GLM-OCR"
19
  OCR_API_PORT = 8080
20
  OCR_API_BASE = f"http://127.0.0.1:{OCR_API_PORT}"
21
- # Fixed runtime settings (no environment variables required).
22
  VLLM_MAX_MODEL_LEN = "8192"
23
  VLLM_GPU_MEMORY_UTIL = "0.75"
24
  VLLM_EXTRA_ARGS = ""
25
- # Keep output budget below context length to leave room for prompt tokens.
26
  GLMOCR_MAX_OUTPUT_TOKENS = 2048
27
 
28
  _parser = None
@@ -39,7 +37,8 @@ def _render_pdf_pages_to_images(pdf_path: str) -> List[str]:
39
  page = doc[i]
40
  pix = page.get_pixmap(matrix=fitz.Matrix(2.5, 2.5), alpha=False)
41
  img_path = os.path.join(
42
- tempfile.gettempdir(), f"glmocr_page_{os.getpid()}_{i}_{int(time.time()*1000)}.png"
 
43
  )
44
  pix.save(img_path)
45
  page_images.append(img_path)
@@ -54,19 +53,12 @@ def _start_vllm_if_needed() -> None:
54
  return
55
 
56
  cmd: List[str] = [
57
- "python",
58
- "-m",
59
- "vllm.entrypoints.openai.api_server",
60
- "--model",
61
- MODEL_ID,
62
- "--served-model-name",
63
- "glm-ocr",
64
- "--port",
65
- str(OCR_API_PORT),
66
- "--max-model-len",
67
- VLLM_MAX_MODEL_LEN,
68
- "--gpu-memory-utilization",
69
- VLLM_GPU_MEMORY_UTIL,
70
  ]
71
  if VLLM_EXTRA_ARGS:
72
  cmd.extend(VLLM_EXTRA_ARGS.split())
@@ -98,10 +90,8 @@ def _configure_glmocr_to_keep_header_footer() -> None:
98
  cfg = yaml.safe_load(f) or {}
99
 
100
  pipeline = cfg.setdefault("pipeline", {})
101
-
102
  maas = pipeline.setdefault("maas", {})
103
  maas["enabled"] = False
104
-
105
  ocr_api = pipeline.setdefault("ocr_api", {})
106
  ocr_api["api_host"] = "127.0.0.1"
107
  ocr_api["api_port"] = OCR_API_PORT
@@ -111,23 +101,17 @@ def _configure_glmocr_to_keep_header_footer() -> None:
111
  ocr_api["verify_ssl"] = False
112
  page_loader = pipeline.setdefault("page_loader", {})
113
  page_loader["max_tokens"] = GLMOCR_MAX_OUTPUT_TOKENS
114
-
115
  layout = pipeline.setdefault("layout", {})
116
  label_task_mapping = layout.setdefault("label_task_mapping", {})
117
  text_labels = set(label_task_mapping.get("text", []) or [])
118
  abandon_labels = set(label_task_mapping.get("abandon", []) or [])
119
  skip_labels = set(label_task_mapping.get("skip", []) or [])
120
-
121
  text_labels.update({"header", "footer"})
122
  abandon_labels.discard("header")
123
  abandon_labels.discard("footer")
124
-
125
- # Keep image-only decorative zones out of OCR by default.
126
- # If you want logo OCR too, move these to text_labels.
127
  skip_labels.update({"header_image", "footer_image"})
128
  abandon_labels.discard("header_image")
129
  abandon_labels.discard("footer_image")
130
-
131
  label_task_mapping["text"] = sorted(text_labels)
132
  label_task_mapping["abandon"] = sorted(abandon_labels)
133
  label_task_mapping["skip"] = sorted(skip_labels)
@@ -142,8 +126,6 @@ def get_parser():
142
  _start_vllm_if_needed()
143
  _configure_glmocr_to_keep_header_footer()
144
  from glmocr import GlmOcr
145
-
146
- # Force local self-hosted pipeline and avoid MaaS fallback.
147
  _parser = GlmOcr(mode="selfhosted")
148
  return _parser
149
 
@@ -153,8 +135,6 @@ def _extract_md(result: Any) -> str:
153
  return ""
154
 
155
  def _close_unbalanced_tables(chunk: str) -> str:
156
- # Some OCR chunks end mid-table near page boundaries, which can cause
157
- # subsequent content (including page separators) to render inside a cell.
158
  opens = len(re.findall(r"<table\b", chunk, flags=re.IGNORECASE))
159
  closes = len(re.findall(r"</table>", chunk, flags=re.IGNORECASE))
160
  missing = max(0, opens - closes)
@@ -173,11 +153,135 @@ def _extract_md(result: Any) -> str:
173
  return _close_unbalanced_tables(str(md).strip()) if md else ""
174
 
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  def _clean_markdown(md: str) -> str:
177
  if not md:
178
  return md
179
 
180
- # Drop frequently repeated bank footer/header lines.
 
 
 
181
  drop_line_patterns = [
182
  r"Call 1-800-937-2000 for 24-hour Bank-by-Phone services.*",
183
  r"Bank Deposits FDIC Insured \| TD Bank, N\.A\. \| Equal Housing Lender.*",
@@ -194,8 +298,7 @@ def _clean_markdown(md: str) -> str:
194
  cleaned_lines.append(line)
195
  md = "\n".join(cleaned_lines)
196
 
197
- # Remove long legal notice section that repeats and is usually not needed
198
- # for transaction extraction.
199
  legal_start_markers = [
200
  "## FOR CONSUMER ACCOUNTS ONLY",
201
  "## FOR CONSUMER LOAN ACCOUNTS ONLY",
@@ -217,9 +320,13 @@ def _clean_markdown(md: str) -> str:
217
  kept.append(line)
218
  md = "\n".join(kept)
219
 
220
- # Remove duplicate separators and excessive blank lines.
221
  md = re.sub(r"(?:\n\s*){3,}", "\n\n", md)
222
- md = re.sub(r"(?:\n\s*---page-separator---\s*\n){2,}", "\n\n---page-separator---\n\n", md)
 
 
 
 
223
  return md.strip()
224
 
225
 
@@ -233,7 +340,6 @@ def run_ocr(file_obj):
233
  is_pdf = path.lower().endswith(".pdf")
234
  if is_pdf:
235
  page_images = _render_pdf_pages_to_images(path)
236
- # Parse one rendered page per request item so we can preserve page boundaries.
237
  result = parser.parse(page_images)
238
  else:
239
  result = parser.parse(path)
@@ -241,13 +347,16 @@ def run_ocr(file_obj):
241
  return md
242
  except Exception as e:
243
  import traceback
244
-
245
  log.exception("run_ocr failed: %s", e)
246
  return f"Error: {e}\n\n{traceback.format_exc()}"
247
  finally:
248
  for p in page_images:
249
  try:
250
- if isinstance(p, str) and p.endswith(".png") and "glmocr_page_" in os.path.basename(p):
 
 
 
 
251
  os.unlink(p)
252
  except Exception:
253
  pass
@@ -271,4 +380,4 @@ def build_demo():
271
 
272
 
273
  if __name__ == "__main__":
274
- build_demo().launch()
 
18
  MODEL_ID = "zai-org/GLM-OCR"
19
  OCR_API_PORT = 8080
20
  OCR_API_BASE = f"http://127.0.0.1:{OCR_API_PORT}"
 
21
  VLLM_MAX_MODEL_LEN = "8192"
22
  VLLM_GPU_MEMORY_UTIL = "0.75"
23
  VLLM_EXTRA_ARGS = ""
 
24
  GLMOCR_MAX_OUTPUT_TOKENS = 2048
25
 
26
  _parser = None
 
37
  page = doc[i]
38
  pix = page.get_pixmap(matrix=fitz.Matrix(2.5, 2.5), alpha=False)
39
  img_path = os.path.join(
40
+ tempfile.gettempdir(),
41
+ f"glmocr_page_{os.getpid()}_{i}_{int(time.time()*1000)}.png",
42
  )
43
  pix.save(img_path)
44
  page_images.append(img_path)
 
53
  return
54
 
55
  cmd: List[str] = [
56
+ "python", "-m", "vllm.entrypoints.openai.api_server",
57
+ "--model", MODEL_ID,
58
+ "--served-model-name", "glm-ocr",
59
+ "--port", str(OCR_API_PORT),
60
+ "--max-model-len", VLLM_MAX_MODEL_LEN,
61
+ "--gpu-memory-utilization", VLLM_GPU_MEMORY_UTIL,
 
 
 
 
 
 
 
62
  ]
63
  if VLLM_EXTRA_ARGS:
64
  cmd.extend(VLLM_EXTRA_ARGS.split())
 
90
  cfg = yaml.safe_load(f) or {}
91
 
92
  pipeline = cfg.setdefault("pipeline", {})
 
93
  maas = pipeline.setdefault("maas", {})
94
  maas["enabled"] = False
 
95
  ocr_api = pipeline.setdefault("ocr_api", {})
96
  ocr_api["api_host"] = "127.0.0.1"
97
  ocr_api["api_port"] = OCR_API_PORT
 
101
  ocr_api["verify_ssl"] = False
102
  page_loader = pipeline.setdefault("page_loader", {})
103
  page_loader["max_tokens"] = GLMOCR_MAX_OUTPUT_TOKENS
 
104
  layout = pipeline.setdefault("layout", {})
105
  label_task_mapping = layout.setdefault("label_task_mapping", {})
106
  text_labels = set(label_task_mapping.get("text", []) or [])
107
  abandon_labels = set(label_task_mapping.get("abandon", []) or [])
108
  skip_labels = set(label_task_mapping.get("skip", []) or [])
 
109
  text_labels.update({"header", "footer"})
110
  abandon_labels.discard("header")
111
  abandon_labels.discard("footer")
 
 
 
112
  skip_labels.update({"header_image", "footer_image"})
113
  abandon_labels.discard("header_image")
114
  abandon_labels.discard("footer_image")
 
115
  label_task_mapping["text"] = sorted(text_labels)
116
  label_task_mapping["abandon"] = sorted(abandon_labels)
117
  label_task_mapping["skip"] = sorted(skip_labels)
 
126
  _start_vllm_if_needed()
127
  _configure_glmocr_to_keep_header_footer()
128
  from glmocr import GlmOcr
 
 
129
  _parser = GlmOcr(mode="selfhosted")
130
  return _parser
131
 
 
135
  return ""
136
 
137
  def _close_unbalanced_tables(chunk: str) -> str:
 
 
138
  opens = len(re.findall(r"<table\b", chunk, flags=re.IGNORECASE))
139
  closes = len(re.findall(r"</table>", chunk, flags=re.IGNORECASE))
140
  missing = max(0, opens - closes)
 
153
  return _close_unbalanced_tables(str(md).strip()) if md else ""
154
 
155
 
156
+ def _get_cell_text(cell) -> str:
157
+ """Extract clean text from a BeautifulSoup table cell."""
158
+ # Replace <br> tags with space before getting text
159
+ for br in cell.find_all("br"):
160
+ br.replace_with(" ")
161
+ text = cell.get_text(separator=" ", strip=True)
162
+ # Collapse multiple spaces
163
+ text = re.sub(r"\s+", " ", text).strip()
164
+ return text
165
+
166
+
167
+ def _html_table_to_markdown(table_html: str) -> str:
168
+ """
169
+ Convert a single HTML table to a clean markdown table.
170
+ - Removes empty columns that only contain whitespace
171
+ - Handles colspan/rowspan by ignoring them (takes first value)
172
+ - Works for both regular tables and bordered tables
173
+ """
174
+ try:
175
+ from bs4 import BeautifulSoup
176
+ soup = BeautifulSoup(table_html, "html.parser")
177
+ table = soup.find("table")
178
+ if not table:
179
+ return table_html
180
+ except ImportError:
181
+ # Fallback: regex-based if bs4 not available
182
+ return _html_table_to_markdown_regex(table_html)
183
+
184
+ # Collect all rows from thead + tbody + direct tr
185
+ all_rows = []
186
+ for tr in table.find_all("tr"):
187
+ cells = []
188
+ for cell in tr.find_all(["td", "th"]):
189
+ cells.append(_get_cell_text(cell))
190
+ if cells:
191
+ all_rows.append(cells)
192
+
193
+ if not all_rows:
194
+ return table_html
195
+
196
+ # Normalize all rows to same width
197
+ max_cols = max(len(r) for r in all_rows)
198
+ all_rows = [r + [""] * (max_cols - len(r)) for r in all_rows]
199
+
200
+ # Remove columns that are empty in ALL rows
201
+ non_empty_cols = []
202
+ for col_idx in range(max_cols):
203
+ if any(all_rows[row_idx][col_idx] for row_idx in range(len(all_rows))):
204
+ non_empty_cols.append(col_idx)
205
+
206
+ all_rows = [[row[i] for i in non_empty_cols] for row in all_rows]
207
+
208
+ if not all_rows:
209
+ return table_html
210
+
211
+ num_cols = len(all_rows[0])
212
+ md_lines = []
213
+
214
+ # First row as header
215
+ md_lines.append("| " + " | ".join(all_rows[0]) + " |")
216
+ md_lines.append("| " + " | ".join(["---"] * num_cols) + " |")
217
+
218
+ # Remaining rows
219
+ for row in all_rows[1:]:
220
+ # Pad or trim to match header width
221
+ padded = (row + [""] * num_cols)[:num_cols]
222
+ md_lines.append("| " + " | ".join(padded) + " |")
223
+
224
+ return "\n".join(md_lines)
225
+
226
+
227
+ def _html_table_to_markdown_regex(table_html: str) -> str:
228
+ """Regex-based fallback for HTML table conversion when bs4 is unavailable."""
229
+ row_re = re.compile(r"<tr[^>]*>(.*?)</tr>", re.IGNORECASE | re.DOTALL)
230
+ cell_re = re.compile(r"<t[dh][^>]*>(.*?)</t[dh]>", re.IGNORECASE | re.DOTALL)
231
+ tag_re = re.compile(r"<[^>]+>")
232
+
233
+ def clean_cell(c: str) -> str:
234
+ c = re.sub(r"<br\s*/?>", " ", c, flags=re.IGNORECASE)
235
+ c = tag_re.sub("", c)
236
+ c = c.replace("&nbsp;", " ").replace("&amp;", "&").replace("&gt;", ">").replace("&lt;", "<")
237
+ return re.sub(r"\s+", " ", c).strip()
238
+
239
+ rows = []
240
+ for tr in row_re.findall(table_html):
241
+ cells = [clean_cell(c) for c in cell_re.findall(tr)]
242
+ if cells:
243
+ rows.append(cells)
244
+
245
+ if not rows:
246
+ return table_html
247
+
248
+ max_cols = max(len(r) for r in rows)
249
+ rows = [r + [""] * (max_cols - len(r)) for r in rows]
250
+
251
+ # Remove empty columns
252
+ non_empty_cols = [i for i in range(max_cols) if any(rows[j][i] for j in range(len(rows)))]
253
+ rows = [[r[i] for i in non_empty_cols] for r in rows]
254
+
255
+ if not rows:
256
+ return table_html
257
+
258
+ num_cols = len(rows[0])
259
+ md = ["| " + " | ".join(rows[0]) + " |", "| " + " | ".join(["---"] * num_cols) + " |"]
260
+ for row in rows[1:]:
261
+ padded = (row + [""] * num_cols)[:num_cols]
262
+ md.append("| " + " | ".join(padded) + " |")
263
+
264
+ return "\n".join(md)
265
+
266
+
267
+ def _convert_all_html_tables(md: str) -> str:
268
+ """Find and replace all HTML tables in the markdown with clean markdown tables."""
269
+ table_re = re.compile(r"<table[\s\S]*?</table>", re.IGNORECASE)
270
+
271
+ def replace_table(match: re.Match) -> str:
272
+ return _html_table_to_markdown(match.group(0))
273
+
274
+ return table_re.sub(replace_table, md)
275
+
276
+
277
  def _clean_markdown(md: str) -> str:
278
  if not md:
279
  return md
280
 
281
+ # Convert all HTML tables to clean markdown tables first
282
+ md = _convert_all_html_tables(md)
283
+
284
+ # Drop frequently repeated bank footer/header lines
285
  drop_line_patterns = [
286
  r"Call 1-800-937-2000 for 24-hour Bank-by-Phone services.*",
287
  r"Bank Deposits FDIC Insured \| TD Bank, N\.A\. \| Equal Housing Lender.*",
 
298
  cleaned_lines.append(line)
299
  md = "\n".join(cleaned_lines)
300
 
301
+ # Remove long legal notice sections
 
302
  legal_start_markers = [
303
  "## FOR CONSUMER ACCOUNTS ONLY",
304
  "## FOR CONSUMER LOAN ACCOUNTS ONLY",
 
320
  kept.append(line)
321
  md = "\n".join(kept)
322
 
323
+ # Remove duplicate separators and excessive blank lines
324
  md = re.sub(r"(?:\n\s*){3,}", "\n\n", md)
325
+ md = re.sub(
326
+ r"(?:\n\s*---page-separator---\s*\n){2,}",
327
+ "\n\n---page-separator---\n\n",
328
+ md,
329
+ )
330
  return md.strip()
331
 
332
 
 
340
  is_pdf = path.lower().endswith(".pdf")
341
  if is_pdf:
342
  page_images = _render_pdf_pages_to_images(path)
 
343
  result = parser.parse(page_images)
344
  else:
345
  result = parser.parse(path)
 
347
  return md
348
  except Exception as e:
349
  import traceback
 
350
  log.exception("run_ocr failed: %s", e)
351
  return f"Error: {e}\n\n{traceback.format_exc()}"
352
  finally:
353
  for p in page_images:
354
  try:
355
+ if (
356
+ isinstance(p, str)
357
+ and p.endswith(".png")
358
+ and "glmocr_page_" in os.path.basename(p)
359
+ ):
360
  os.unlink(p)
361
  except Exception:
362
  pass
 
380
 
381
 
382
  if __name__ == "__main__":
383
+ build_demo().launch()