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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -14
app.py CHANGED
@@ -8,7 +8,8 @@ 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
- - 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,9 +30,9 @@ import os
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
37
  import gradio as gr
@@ -262,9 +263,46 @@ def close_unclosed_html(md: str) -> str:
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__()
@@ -295,10 +333,7 @@ class TableGridParser(HTMLParser):
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)
@@ -325,7 +360,7 @@ def _build_grid(rows_data):
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>"]
@@ -341,11 +376,7 @@ def _grid_to_html(grid):
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
 
@@ -425,7 +456,7 @@ def normalize_money_glyphs(text: str) -> str:
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,7 +470,14 @@ def stabilize_tables_and_text(page_md: str) -> str:
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,11 +577,13 @@ def run_ocr(uploaded_file):
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,9 +594,11 @@ def run_ocr(uploaded_file):
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,6 +621,7 @@ def run_ocr(uploaded_file):
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):
 
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
+ - normalize HTML tables so header/data columns align (expand colspan/rowspan)
12
+ - clean common header artifacts (e.g. "DESCRIPTIONBeginning Balance", "BALANCE$3,447.10")
13
  """
14
 
15
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
30
  import re
31
  import html
32
  import tempfile
33
+ from typing import List, Tuple
34
  from collections import defaultdict
35
  from html.parser import HTMLParser
 
36
 
37
  import yaml
38
  import gradio as gr
 
263
  return md
264
 
265
 
266
+ def clean_table_header_artifacts(text: str) -> str:
267
+ """
268
+ Generic cleanup for common OCR artifacts in table headers:
269
+ - 'DESCRIPTIONBeginning Balance' -> 'DESCRIPTION'
270
+ - 'DESCRIPTIONEnding Balance' -> 'DESCRIPTION'
271
+ - 'BALANCE$3,447.10' -> 'BALANCE'
272
+ - 'BALANCE 3,447.10' -> 'BALANCE'
273
+ Works for any PDF; no bank-specific logic.
274
+ """
275
+ if not text:
276
+ return text
277
+
278
+ # DESCRIPTION + (Beginning/Ending Balance) glued
279
+ text = re.sub(
280
+ r"(>[^<]*\bDESCRIPTION)\s*(Beginning Balance|Ending Balance)\b([^<]*<)",
281
+ r"\1\3",
282
+ text,
283
+ flags=re.IGNORECASE,
284
+ )
285
+
286
+ # BALANCE with a number glued or appended (keep the word BALANCE only)
287
+ text = re.sub(
288
+ r"(>[^<]*\bBALANCE)\s*\$?\s*\d{1,3}(?:,\d{3})*(?:\.\d{2})?\s*([^<]*<)",
289
+ r"\1\2",
290
+ text,
291
+ flags=re.IGNORECASE,
292
+ )
293
+ text = re.sub(
294
+ r"(>[^<]*\bBALANCE)\$",
295
+ r"\1",
296
+ text,
297
+ flags=re.IGNORECASE,
298
+ )
299
+
300
+ return text
301
+
302
+
303
  # ---- Colspan/rowspan expansion: align header and data rows for any PDF ----
304
  class TableGridParser(HTMLParser):
305
+ """Parse <table> HTML into rows of (text, colspan, rowspan)."""
306
 
307
  def __init__(self):
308
  super().__init__()
 
333
 
334
 
335
  def _build_grid(rows_data):
336
+ """Expand colspan/rowspan into a rectangular grid."""
 
 
 
337
  if not rows_data:
338
  return []
339
  blocked = defaultdict(set)
 
360
 
361
 
362
  def _grid_to_html(grid):
363
+ """Emit a normalized table without colspan/rowspan."""
364
  if not grid:
365
  return ""
366
  lines = ["<table>"]
 
376
 
377
 
378
  def expand_colspan_rowspan_in_tables(text):
379
+ """Normalize every <table>...</table> in text."""
 
 
 
 
380
  if not text or "<table" not in text.lower():
381
  return text
382
 
 
456
 
457
 
458
  def stabilize_tables_and_text(page_md: str) -> str:
459
+ """Convert markdown pipe tables to HTML, clean header artifacts, normalize tables, and close tags."""
460
  if not page_md:
461
  return page_md
462
  page_md = normalize_money_glyphs(page_md)
 
470
  out_blocks.append(b)
471
 
472
  stabilized = "\n\n".join(out_blocks)
473
+
474
+ # 1) Fix common header text artifacts
475
+ stabilized = clean_table_header_artifacts(stabilized)
476
+
477
+ # 2) Normalize tables to a rectangular grid (expand colspan/rowspan)
478
  stabilized = expand_colspan_rowspan_in_tables(stabilized)
479
+
480
+ # 3) Close any unclosed tags to prevent bleed
481
  return close_unclosed_html(stabilized)
482
 
483
 
 
577
  he = header_end_frac if header_end_frac is not None else DEFAULT_ZONE_FRAC
578
  fs = footer_start_frac if footer_start_frac is not None else (1.0 - DEFAULT_ZONE_FRAC)
579
 
580
+ # clamp
581
  he = max(0.02, min(0.25, he))
582
  fs = max(0.75, min(0.98, fs))
583
 
584
  parts = []
585
 
586
+ # Header inclusion: PDF text -> band words -> OCR band
587
  hdr = ""
588
  if is_pdf:
589
  hdr = extract_zone_text_pdf(path, page_num, 0, he)
 
594
  if hdr and hdr.strip():
595
  parts.append(fix_account_number(normalize_money_glyphs(hdr.strip())))
596
 
597
+ # Main OCR markdown, stabilized
598
  if page_md and page_md.strip():
599
  parts.append(stabilize_tables_and_text(page_md.strip()))
600
 
601
+ # Optional footer
602
  if ENABLE_FOOTER_OCR and page_num < len(page_images):
603
  ftr = ""
604
  if is_pdf:
 
621
  return f"Error: {e}\n\n{traceback.format_exc()}"
622
 
623
  finally:
624
+ # cleanup rendered images
625
  for p in page_images:
626
  try:
627
  if isinstance(p, str) and p.endswith(".png") and "glmocr_page_" in os.path.basename(p):