rehan953 commited on
Commit
1bd9b5c
·
verified ·
1 Parent(s): 594145e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -6
app.py CHANGED
@@ -8,6 +8,7 @@ Primary goal for reconcile rate:
8
  - preserve right-most columns (often "Balance") by higher DPI render + right padding
9
  - keep tables as tables (convert markdown pipe tables -> HTML table)
10
  - return ---page-separator--- between pages
 
11
  """
12
 
13
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -28,6 +29,8 @@ import os
28
  import re
29
  import html
30
  import tempfile
 
 
31
  from typing import List, Tuple
32
 
33
  import yaml
@@ -259,6 +262,116 @@ def close_unclosed_html(md: str) -> str:
259
  return md
260
 
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  def looks_like_markdown_table(block: str) -> bool:
263
  """Detect simple markdown pipe tables."""
264
  lines = [ln.rstrip() for ln in block.strip().splitlines() if ln.strip()]
@@ -312,7 +425,7 @@ def normalize_money_glyphs(text: str) -> str:
312
 
313
 
314
  def stabilize_tables_and_text(page_md: str) -> str:
315
- """Convert markdown pipe tables to HTML and close tags."""
316
  if not page_md:
317
  return page_md
318
  page_md = normalize_money_glyphs(page_md)
@@ -326,6 +439,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
326
  out_blocks.append(b)
327
 
328
  stabilized = "\n\n".join(out_blocks)
 
329
  return close_unclosed_html(stabilized)
330
 
331
 
@@ -425,13 +539,11 @@ def run_ocr(uploaded_file):
425
  he = header_end_frac if header_end_frac is not None else DEFAULT_ZONE_FRAC
426
  fs = footer_start_frac if footer_start_frac is not None else (1.0 - DEFAULT_ZONE_FRAC)
427
 
428
- # clamp
429
  he = max(0.02, min(0.25, he))
430
  fs = max(0.75, min(0.98, fs))
431
 
432
  parts = []
433
 
434
- # Header inclusion: PDF text -> band words -> OCR band
435
  hdr = ""
436
  if is_pdf:
437
  hdr = extract_zone_text_pdf(path, page_num, 0, he)
@@ -442,11 +554,9 @@ def run_ocr(uploaded_file):
442
  if hdr and hdr.strip():
443
  parts.append(fix_account_number(normalize_money_glyphs(hdr.strip())))
444
 
445
- # Main OCR markdown, stabilized
446
  if page_md and page_md.strip():
447
  parts.append(stabilize_tables_and_text(page_md.strip()))
448
 
449
- # Optional footer
450
  if ENABLE_FOOTER_OCR and page_num < len(page_images):
451
  ftr = ""
452
  if is_pdf:
@@ -469,7 +579,6 @@ def run_ocr(uploaded_file):
469
  return f"Error: {e}\n\n{traceback.format_exc()}"
470
 
471
  finally:
472
- # cleanup rendered images
473
  for p in page_images:
474
  try:
475
  if isinstance(p, str) and p.endswith(".png") and "glmocr_page_" in os.path.basename(p):
 
8
  - preserve right-most columns (often "Balance") by higher DPI render + right padding
9
  - keep tables as tables (convert markdown pipe tables -> HTML table)
10
  - return ---page-separator--- between pages
11
+ - expand colspan/rowspan in HTML tables so header and data rows align (fixes reconciliation)
12
  """
13
 
14
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
29
  import re
30
  import html
31
  import tempfile
32
+ from collections import defaultdict
33
+ from html.parser import HTMLParser
34
  from typing import List, Tuple
35
 
36
  import yaml
 
262
  return md
263
 
264
 
265
+ # ---- Colspan/rowspan expansion: align header and data rows for any PDF ----
266
+ class TableGridParser(HTMLParser):
267
+ """Parse <table> HTML into a list of rows; each row is a list of (text, colspan, rowspan)."""
268
+
269
+ def __init__(self):
270
+ super().__init__()
271
+ self.rows = []
272
+ self._current_row = []
273
+ self._cell_text = []
274
+ self._colspan = 1
275
+ self._rowspan = 1
276
+
277
+ def handle_starttag(self, tag, attrs):
278
+ if tag == "tr":
279
+ self._current_row = []
280
+ elif tag in ("td", "th"):
281
+ attrs_d = dict(attrs)
282
+ self._colspan = max(1, int(attrs_d.get("colspan", 1)))
283
+ self._rowspan = max(1, int(attrs_d.get("rowspan", 1)))
284
+ self._cell_text = []
285
+
286
+ def handle_endtag(self, tag):
287
+ if tag in ("td", "th"):
288
+ text = "".join(self._cell_text).strip().replace("\n", " ")
289
+ self._current_row.append((text, self._colspan, self._rowspan))
290
+ elif tag == "tr":
291
+ self.rows.append(self._current_row)
292
+
293
+ def handle_data(self, data):
294
+ self._cell_text.append(data)
295
+
296
+
297
+ def _build_grid(rows_data):
298
+ """
299
+ Build a 2D grid from rows_data (list of list of (text, colspan, rowspan)).
300
+ Expands colspan and rowspan so every row has the same number of columns.
301
+ """
302
+ if not rows_data:
303
+ return []
304
+ blocked = defaultdict(set)
305
+ grid = []
306
+
307
+ for r, row_cells in enumerate(rows_data):
308
+ grid.append([])
309
+ col = 0
310
+ for content, C, R in row_cells:
311
+ while col in blocked[r]:
312
+ grid[r].append("")
313
+ col += 1
314
+ for k in range(C):
315
+ grid[r].append(content if k == 0 else "")
316
+ for k in range(1, R):
317
+ blocked[r + k].add(col)
318
+ col += C
319
+
320
+ max_cols = max(len(row) for row in grid) if grid else 0
321
+ for row in grid:
322
+ while len(row) < max_cols:
323
+ row.append("")
324
+ return grid
325
+
326
+
327
+ def _grid_to_html(grid):
328
+ """Turn a 2D grid of cell texts into a single <table> HTML string (no colspan/rowspan)."""
329
+ if not grid:
330
+ return ""
331
+ lines = ["<table>"]
332
+ for i, row in enumerate(grid):
333
+ lines.append("<tr>")
334
+ tag = "th" if i == 0 else "td"
335
+ for cell in row:
336
+ escaped = (cell or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
337
+ lines.append(f"<{tag}>{escaped}</{tag}>")
338
+ lines.append("</tr>")
339
+ lines.append("</table>")
340
+ return "\n".join(lines)
341
+
342
+
343
+ def expand_colspan_rowspan_in_tables(text):
344
+ """
345
+ Find every <table>...</table> in text and replace with a version where
346
+ colspan and rowspan are expanded so all rows have the same number of cells.
347
+ Works for any PDF; prevents header/data misalignment that breaks reconciliation.
348
+ """
349
+ if not text or "<table" not in text.lower():
350
+ return text
351
+
352
+ pattern = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
353
+ result = []
354
+ last_end = 0
355
+
356
+ for match in pattern.finditer(text):
357
+ result.append(text[last_end : match.start()])
358
+ table_html = match.group(0)
359
+ try:
360
+ parser = TableGridParser()
361
+ parser.feed(table_html)
362
+ if parser.rows:
363
+ grid = _build_grid(parser.rows)
364
+ result.append(_grid_to_html(grid))
365
+ else:
366
+ result.append(table_html)
367
+ except Exception:
368
+ result.append(table_html)
369
+ last_end = match.end()
370
+
371
+ result.append(text[last_end:])
372
+ return "".join(result)
373
+
374
+
375
  def looks_like_markdown_table(block: str) -> bool:
376
  """Detect simple markdown pipe tables."""
377
  lines = [ln.rstrip() for ln in block.strip().splitlines() if ln.strip()]
 
425
 
426
 
427
  def stabilize_tables_and_text(page_md: str) -> str:
428
+ """Convert markdown pipe tables to HTML, expand colspan/rowspan, and close tags."""
429
  if not page_md:
430
  return page_md
431
  page_md = normalize_money_glyphs(page_md)
 
439
  out_blocks.append(b)
440
 
441
  stabilized = "\n\n".join(out_blocks)
442
+ stabilized = expand_colspan_rowspan_in_tables(stabilized)
443
  return close_unclosed_html(stabilized)
444
 
445
 
 
539
  he = header_end_frac if header_end_frac is not None else DEFAULT_ZONE_FRAC
540
  fs = footer_start_frac if footer_start_frac is not None else (1.0 - DEFAULT_ZONE_FRAC)
541
 
 
542
  he = max(0.02, min(0.25, he))
543
  fs = max(0.75, min(0.98, fs))
544
 
545
  parts = []
546
 
 
547
  hdr = ""
548
  if is_pdf:
549
  hdr = extract_zone_text_pdf(path, page_num, 0, he)
 
554
  if hdr and hdr.strip():
555
  parts.append(fix_account_number(normalize_money_glyphs(hdr.strip())))
556
 
 
557
  if page_md and page_md.strip():
558
  parts.append(stabilize_tables_and_text(page_md.strip()))
559
 
 
560
  if ENABLE_FOOTER_OCR and page_num < len(page_images):
561
  ftr = ""
562
  if is_pdf:
 
579
  return f"Error: {e}\n\n{traceback.format_exc()}"
580
 
581
  finally:
 
582
  for p in page_images:
583
  try:
584
  if isinstance(p, str) and p.endswith(".png") and "glmocr_page_" in os.path.basename(p):