rehan953 commited on
Commit
71ff542
·
verified ·
1 Parent(s): 59146dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -0
app.py CHANGED
@@ -108,6 +108,11 @@ Primary goals for reconcile rate:
108
  earlier larger DA3 table — catches OCR-truncated description duplicates such as
109
  the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
110
  full service-fee table with a longer description string.
 
 
 
 
 
111
  """
112
 
113
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -565,6 +570,156 @@ def _merge_blank_header_text_columns(grid):
565
  new_grid.append([cell for idx, cell in enumerate(row) if idx < len(keep) and keep[idx]])
566
  return new_grid
567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  # --------------------------
569
  # Shared keyword definitions
570
  # --------------------------
@@ -2652,6 +2807,9 @@ def normalize_html_tables(text: str) -> str:
2652
  grid = _drop_truly_empty_columns(grid)
2653
  grid = _merge_blank_header_text_columns(grid)
2654
 
 
 
 
2655
  grid = _reconstruct_separator_rows(grid)
2656
 
2657
  grid, recovered_row = _clean_header_artifacts(grid)
 
108
  earlier larger DA3 table — catches OCR-truncated description duplicates such as
109
  the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
110
  full service-fee table with a longer description string.
111
+ - Fix 32: Headless amount column — PDFs often leave the rightmost money column without a header;
112
+ OCR emits an empty <th>. Fill with \"Amount\" when the column is mostly currency.
113
+ Second pass: collapse Date|Transaction Description|Additions|Subtractions + empty 4th
114
+ when OCR splits \"Preauth Debit\" / \"Pre-Auth Credit\" into its own column before
115
+ the merchant text (East West–style) — merge back to Date|Description|Amount.
116
  """
117
 
118
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
570
  new_grid.append([cell for idx, cell in enumerate(row) if idx < len(keep) and keep[idx]])
571
  return new_grid
572
 
573
+
574
+ def _fill_headless_trailing_amount_headers(grid):
575
+ """
576
+ Fix 32a: PDF layouts often omit a header above the rightmost amount column; OCR leaves a
577
+ trailing empty <th>. Downstream parsers then mis-associate columns. Fill only when the
578
+ column is predominantly strict currency (generic, bank-agnostic).
579
+ """
580
+ if not grid or len(grid) < 2:
581
+ return grid
582
+
583
+ header = [str(c or "").strip() for c in grid[0]]
584
+ ncols = len(header)
585
+ if ncols < 2:
586
+ return grid
587
+
588
+ i = ncols - 1
589
+ while i >= 0 and header[i] == "":
590
+ i -= 1
591
+ empty_tail_indices = list(range(i + 1, ncols))
592
+ if not empty_tail_indices:
593
+ return grid
594
+
595
+ body = grid[1:]
596
+ new_header = list(header)
597
+
598
+ for j in empty_tail_indices:
599
+ total = 0
600
+ amt_hits = 0
601
+ for row in body:
602
+ if j >= len(row):
603
+ continue
604
+ total += 1
605
+ v = str(row[j] or "").strip()
606
+ if v and _is_amount_like(v):
607
+ amt_hits += 1
608
+ if total == 0:
609
+ continue
610
+ if (amt_hits / total) < 0.45:
611
+ continue
612
+
613
+ # Right-align column is the numeric amount; PDF often omits a label for it.
614
+ new_header[j] = "Amount"
615
+
616
+ grid[0] = new_header
617
+ return grid
618
+
619
+
620
+ _MMDD_DATE_RE = re.compile(r"^\d{1,2}[-/]\d{2}(?:[-/]\d{2,4})?$")
621
+
622
+ # Leading transaction-type cell when OCR splits category vs merchant (East West / similar).
623
+ _STACKED_TXN_TYPE_RE = re.compile(
624
+ r"^(?:Pre-Auth Credit|Pre-Auth Debit|Preauth Debit|"
625
+ r"Withdrawal Trns|Deposit(?:\s|$)|Outgoing Wire|Incoming Wire|"
626
+ r"Service Charge|ACH Debit|ACH Credit|ACH Payment|ACH PMT|"
627
+ r"Wire Transfer|POS Purchase|ATM Withdrawal|Transfer Trns)\b",
628
+ re.IGNORECASE | re.DOTALL,
629
+ )
630
+
631
+
632
+ def _collapse_four_col_stacked_transaction_type_to_three(grid):
633
+ """
634
+ Fix 32b: When the PDF is logically Date | one description | amount, OCR sometimes emits
635
+ four columns: Date | short type (e.g. Preauth Debit) | merchant tail | amount, with an
636
+ optional empty 4th header cell. Collapse to three columns so markdown matches the PDF row
637
+ semantics. Requires every non-empty data row to match the pattern (safe no-op otherwise).
638
+ """
639
+ if not grid or len(grid) < 3:
640
+ return grid
641
+
642
+ header = [str(c or "").strip() for c in grid[0]]
643
+ if len(header) != 4:
644
+ return grid
645
+
646
+ h0 = re.sub(r"\s+", " ", header[0]).strip().lower()
647
+ h1 = re.sub(r"\s+", " ", header[1]).strip().lower()
648
+ h2 = re.sub(r"\s+", " ", header[2]).strip().lower()
649
+ h3 = str(header[3] or "").strip().lower()
650
+
651
+ if h0 != "date":
652
+ return grid
653
+ if "transaction" not in h1 or "description" not in h1:
654
+ return grid
655
+ if not any(
656
+ k in h2
657
+ for k in (
658
+ "addition",
659
+ "subtraction",
660
+ "credit",
661
+ "debit",
662
+ "deposit",
663
+ "withdrawal",
664
+ )
665
+ ):
666
+ return grid
667
+ if h3 not in ("", "amount") and not any(
668
+ k in h3 for k in ("addition", "subtraction", "credit", "debit")
669
+ ):
670
+ return grid
671
+
672
+ def _row_matches_split_pattern(cells):
673
+ while len(cells) < 4:
674
+ cells = cells + [""]
675
+ c0, c1, c2, c3 = (
676
+ str(cells[0] or "").strip(),
677
+ str(cells[1] or "").strip(),
678
+ str(cells[2] or "").strip(),
679
+ str(cells[3] or "").strip(),
680
+ )
681
+ if not c0 or not _MMDD_DATE_RE.match(c0):
682
+ return False
683
+ if not c1 or not _STACKED_TXN_TYPE_RE.match(c1):
684
+ return False
685
+ if not c2:
686
+ return False
687
+ if _is_amount_like(c2):
688
+ return False
689
+ if not _is_amount_like(c3):
690
+ return False
691
+ return True
692
+
693
+ data_rows = [r for r in grid[1:] if any(str(c or "").strip() for c in r)]
694
+ if len(data_rows) < 2:
695
+ return grid
696
+
697
+ for row in data_rows:
698
+ cells = [str(c or "").strip() for c in row]
699
+ if not _row_matches_split_pattern(cells):
700
+ return grid
701
+
702
+ out = [
703
+ [
704
+ header[0],
705
+ header[1],
706
+ header[2],
707
+ ]
708
+ ]
709
+ for row in grid[1:]:
710
+ cells = [str(c or "").strip() for c in row]
711
+ while len(cells) < 4:
712
+ cells.append("")
713
+ if not any(cells):
714
+ out.append(["", "", ""])
715
+ continue
716
+ d0, t1, tail, amt = cells[0], cells[1], cells[2], cells[3]
717
+ desc = (t1 + " " + tail).strip()
718
+ out.append([d0, desc, amt])
719
+
720
+ return out
721
+
722
+
723
  # --------------------------
724
  # Shared keyword definitions
725
  # --------------------------
 
2807
  grid = _drop_truly_empty_columns(grid)
2808
  grid = _merge_blank_header_text_columns(grid)
2809
 
2810
+ grid = _fill_headless_trailing_amount_headers(grid)
2811
+ grid = _collapse_four_col_stacked_transaction_type_to_three(grid)
2812
+
2813
  grid = _reconstruct_separator_rows(grid)
2814
 
2815
  grid, recovered_row = _clean_header_artifacts(grid)