rehan953 commited on
Commit
c3719d8
·
verified ·
1 Parent(s): 51d561c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -27
app.py CHANGED
@@ -17,11 +17,13 @@ Primary goals for reconcile rate:
17
  8) reconstruct mid-table separator rows split across columns by OCR (e.g. 'Card account # XXXX 2 | | 889')
18
  - footer extraction enabled on all pages with deduplication guard (no double-print if body OCR already captured it)
19
  - Fix 4: cross-validate OCR table row counts against PDF text layer; inject missing duplicate rows
20
- (safe no-op for scanned PDFs and non-transaction pages)
 
21
  - Fix NF-1: extract Navy Federal "Summary of your deposit accounts" table from PDF text layer
22
  using spatial word positions (GLM-OCR fails on this wide-column layout)
23
  - Fix 13: collapse Date|Description|Amount|Description|Amount tables to 3 columns; fix fused
24
  Beginning/Ending balance rows (UCB-style wide summary tables)
 
25
  - Post-pass: drop later duplicate subset Date|Description|Amount tables (repeated deposit blocks)
26
  """
27
 
@@ -2702,32 +2704,46 @@ def _normalize_double_desc_amount_header_table(grid):
2702
  """
2703
  Fix 13: Collapse OCR tables with a duplicated column group:
2704
  Date | Description | Amount | Description | Amount
2705
- Common on United Community Bank-style statements where a second
2706
- Description/Amount pair holds sub-totals (e.g. Average Ledger Balance).
2707
  Output: Date | Description | Amount
2708
-
2709
- Strict guard: header row must match exactly those five labels (case/spacing
2710
- normalized). No bank name checks.
2711
  """
2712
  if not grid or len(grid) < 2:
2713
  return grid
2714
 
2715
- raw_h = [str(c or "").strip() for c in grid[0]]
2716
- header_norm = [re.sub(r"\s+", " ", h).lower() for h in raw_h]
2717
- if len(header_norm) != 5:
2718
- return grid
2719
- if header_norm != ["date", "description", "amount", "description", "amount"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2720
  return grid
2721
 
2722
- date_only_re = re.compile(r"^\d{1,2}/\d{1,2}/\d{4}$")
2723
- date_prefix_re = re.compile(r"^(\d{1,2}/\d{1,2}/\d{4})\s*(.*)$")
2724
  money_cell_re = re.compile(r"^-?\$?\d{1,3}(?:,\d{3})*(?:\.\d{2})-?$")
2725
 
2726
  def _is_money(s):
2727
  if not s or not str(s).strip():
2728
  return False
2729
- t = str(s).strip()
2730
- return bool(money_cell_re.match(t))
2731
 
2732
  def _extract_money(s):
2733
  if not s:
@@ -2737,28 +2753,28 @@ def _normalize_double_desc_amount_header_table(grid):
2737
 
2738
  out = [["Date", "Description", "Amount"]]
2739
 
2740
- for row in grid[1:]:
2741
  cells = [str(c or "").strip() for c in row]
2742
- while len(cells) < 5:
2743
  cells.append("")
2744
- c0, c1, c2, c3, c4 = cells[:5]
 
 
2745
 
2746
- # Skip completely empty rows
2747
- if not any(cells[:5]):
2748
  continue
2749
 
2750
  # A) Fused date + label in col0, amount in col1 (Beginning/Ending Balance, etc.)
2751
  mpre = date_prefix_re.match(c0)
2752
  if mpre and _is_money(c1):
2753
  date_s = mpre.group(1).strip()
2754
- tail = (mpre.group(2) or "").strip()
2755
- low = (c0 + " " + tail).lower()
2756
  if "beginning balance" in low:
2757
  desc = "Beginning Balance"
2758
  elif "ending balance" in low:
2759
  desc = "Ending Balance"
2760
  else:
2761
- desc = tail if tail else "Transaction"
2762
  out.append([date_s, desc, c1])
2763
  continue
2764
 
@@ -2782,12 +2798,71 @@ def _normalize_double_desc_amount_header_table(grid):
2782
  out.append([date_s, desc, amt])
2783
  continue
2784
 
2785
- # C) Default: use first Date/Description/Amount triple; ignore empty second pair
 
 
 
 
 
2786
  out.append([c0, c1, c2])
2787
 
2788
  return out if len(out) >= 2 else grid
2789
 
2790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2791
  def _normalize_fused_date_posted_amount_table(grid):
2792
  """
2793
  Normalize 2-column OCR tables with fused header like:
@@ -2915,12 +2990,15 @@ def normalize_html_tables(text: str) -> str:
2915
  last = m.end() # must set here before continue since line below is outside try
2916
  continue
2917
 
 
 
 
 
 
 
2918
  grid = _drop_truly_empty_columns(grid)
2919
  grid = _merge_blank_header_text_columns(grid)
2920
 
2921
- # Fix 13: Date|Description|Amount|Description|Amount → 3 columns (UCB-style)
2922
- grid = _normalize_double_desc_amount_header_table(grid)
2923
-
2924
  # Fix 5: reconstruct mid-table separator rows split across columns by OCR
2925
  grid = _reconstruct_separator_rows(grid)
2926
 
@@ -3192,6 +3270,10 @@ def run_ocr(uploaded_file):
3192
  # Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
3193
  if is_pdf and not is_nfcu_doc:
3194
  stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
 
 
 
 
3195
  elif is_pdf and is_nfcu_doc:
3196
  # Navy-only additive fix: rebuild malformed summary table from
3197
  # text layer, but keep GLM body/header/footer unchanged.
 
17
  8) reconstruct mid-table separator rows split across columns by OCR (e.g. 'Card account # XXXX 2 | | 889')
18
  - footer extraction enabled on all pages with deduplication guard (no double-print if body OCR already captured it)
19
  - Fix 4: cross-validate OCR table row counts against PDF text layer; inject missing duplicate rows
20
+ (safe no-op for scanned PDFs and non-transaction pages); after injection, re-run normalize_html_tables
21
+ + subset dedupe so UCB-style fused cells from the text layer get Fix 13/14
22
  - Fix NF-1: extract Navy Federal "Summary of your deposit accounts" table from PDF text layer
23
  using spatial word positions (GLM-OCR fails on this wide-column layout)
24
  - Fix 13: collapse Date|Description|Amount|Description|Amount tables to 3 columns; fix fused
25
  Beginning/Ending balance rows (UCB-style wide summary tables)
26
+ - Fix 14: UCB 3-col tables where Beginning Balance amount is fused into description and Amount is $0.00
27
  - Post-pass: drop later duplicate subset Date|Description|Amount tables (repeated deposit blocks)
28
  """
29
 
 
2704
  """
2705
  Fix 13: Collapse OCR tables with a duplicated column group:
2706
  Date | Description | Amount | Description | Amount
2707
+ (or 4 cols if trailing Amount column was dropped). UCB-style summary tables
2708
+ put labels like "9 Credit(s) This Period" in the Date column.
2709
  Output: Date | Description | Amount
 
 
 
2710
  """
2711
  if not grid or len(grid) < 2:
2712
  return grid
2713
 
2714
+ def _hdr_tight(s):
2715
+ return re.sub(r"\s+", "", str(s or "").strip().lower())
2716
+
2717
+ # OCR often inserts spaces inside words; compare whitespace-stripped tokens.
2718
+ expected5_t = ["date", "description", "amount", "description", "amount"]
2719
+ expected4_t = ["date", "description", "amount", "description"]
2720
+
2721
+ def _row_is_ucb_header(cells):
2722
+ if len(cells) == 5:
2723
+ return [_hdr_tight(c) for c in cells] == expected5_t
2724
+ if len(cells) == 4:
2725
+ return [_hdr_tight(c) for c in cells] == expected4_t
2726
+ return False
2727
+
2728
+ hdr_idx = None
2729
+ ncols = 0
2730
+ for i in range(min(5, len(grid))):
2731
+ raw_try = [str(c or "").strip() for c in grid[i]]
2732
+ if _row_is_ucb_header(raw_try):
2733
+ hdr_idx = i
2734
+ ncols = len(raw_try)
2735
+ break
2736
+ if hdr_idx is None:
2737
  return grid
2738
 
2739
+ date_only_re = re.compile(r"^\d{1,2}/\d{1,2}/\d{2,4}$")
2740
+ date_prefix_re = re.compile(r"^(\d{1,2}/\d{1,2}/\d{2,4})\s*(.*)$")
2741
  money_cell_re = re.compile(r"^-?\$?\d{1,3}(?:,\d{3})*(?:\.\d{2})-?$")
2742
 
2743
  def _is_money(s):
2744
  if not s or not str(s).strip():
2745
  return False
2746
+ return bool(money_cell_re.match(str(s).strip()))
 
2747
 
2748
  def _extract_money(s):
2749
  if not s:
 
2753
 
2754
  out = [["Date", "Description", "Amount"]]
2755
 
2756
+ for row in grid[hdr_idx + 1 :]:
2757
  cells = [str(c or "").strip() for c in row]
2758
+ while len(cells) < ncols:
2759
  cells.append("")
2760
+ c0, c1, c2 = cells[0], cells[1] if len(cells) > 1 else "", cells[2] if len(cells) > 2 else ""
2761
+ c3 = cells[3] if len(cells) > 3 else ""
2762
+ c4 = cells[4] if len(cells) > 4 else ""
2763
 
2764
+ if not any(cells[:ncols]):
 
2765
  continue
2766
 
2767
  # A) Fused date + label in col0, amount in col1 (Beginning/Ending Balance, etc.)
2768
  mpre = date_prefix_re.match(c0)
2769
  if mpre and _is_money(c1):
2770
  date_s = mpre.group(1).strip()
2771
+ low = (c0 + " " + (mpre.group(2) or "")).lower()
 
2772
  if "beginning balance" in low:
2773
  desc = "Beginning Balance"
2774
  elif "ending balance" in low:
2775
  desc = "Ending Balance"
2776
  else:
2777
+ desc = (mpre.group(2) or "").strip() or "Transaction"
2778
  out.append([date_s, desc, c1])
2779
  continue
2780
 
 
2798
  out.append([date_s, desc, amt])
2799
  continue
2800
 
2801
+ # D) Summary / label rows: first column is NOT a date, second is money (Credits/Debits/Service)
2802
+ if not date_prefix_re.match(c0) and not date_only_re.match(c0.strip()) and _is_money(c1):
2803
+ out.append(["", c0, c1])
2804
+ continue
2805
+
2806
+ # C) Default: first Date|Description|Amount triple (ignore duplicate pair)
2807
  out.append([c0, c1, c2])
2808
 
2809
  return out if len(out) >= 2 else grid
2810
 
2811
 
2812
+ def _normalize_ucb_three_col_beginning_balance_fusion(grid):
2813
+ """
2814
+ Fix 14: UCB sometimes OCRs the summary as 3 columns (not 5), with the real
2815
+ beginning balance inside the description and $0.00 in the amount column:
2816
+ 02/01/2025 | Beginning Balance $3,562.49 Average Ledger Balance | $0.00
2817
+ Normalize to: Date | Beginning Balance | $3,562.49
2818
+ Guard: standard Date|Description|Amount header only.
2819
+ """
2820
+ if not grid or len(grid) < 2:
2821
+ return grid
2822
+
2823
+ def _tight(s):
2824
+ return re.sub(r"\s+", "", str(s or "").strip().lower())
2825
+
2826
+ h0 = [str(c or "").strip() for c in grid[0]]
2827
+ # Do not use max row width — OCR sometimes adds an extra empty column on some rows only.
2828
+ if len(h0) < 3:
2829
+ return grid
2830
+ if _tight(h0[0]) != "date" or _tight(h0[1]) != "description" or _tight(h0[2]) != "amount":
2831
+ return grid
2832
+
2833
+ date_only_re = re.compile(r"^\d{1,2}/\d{1,2}/\d{2,4}$")
2834
+
2835
+ def _extract_money(s):
2836
+ if not s:
2837
+ return ""
2838
+ m = re.search(r"(-?\$?\d{1,3}(?:,\d{3})*(?:\.\d{2})-?)", str(s))
2839
+ return m.group(1).strip() if m else ""
2840
+
2841
+ zeroish = {"", "$0.00", "0.00", "$0", ".00", "$.00", "0"}
2842
+
2843
+ out = [list(grid[0])]
2844
+ changed = False
2845
+ for row in grid[1:]:
2846
+ cells = [str(c or "").strip() for c in row]
2847
+ while len(cells) < 3:
2848
+ cells.append("")
2849
+ c0, c1, c2 = cells[0], cells[1], cells[2]
2850
+ if (
2851
+ date_only_re.match(c0.strip())
2852
+ and c1
2853
+ and "beginning balance" in c1.lower()
2854
+ ):
2855
+ mx = _extract_money(c1)
2856
+ c2n = c2.strip() if c2 else ""
2857
+ if mx and (not c2n or c2n in zeroish):
2858
+ out.append([c0.strip(), "Beginning Balance", mx])
2859
+ changed = True
2860
+ continue
2861
+ out.append([c0, c1, c2])
2862
+
2863
+ return out if changed else grid
2864
+
2865
+
2866
  def _normalize_fused_date_posted_amount_table(grid):
2867
  """
2868
  Normalize 2-column OCR tables with fused header like:
 
2990
  last = m.end() # must set here before continue since line below is outside try
2991
  continue
2992
 
2993
+ # Fix 13: MUST run before _drop_truly_empty_columns — otherwise the 5th column
2994
+ # can be removed and the UCB double-pair header no longer matches.
2995
+ grid = _normalize_double_desc_amount_header_table(grid)
2996
+ # Fix 14: same bank, 3-col OCR fused Beginning Balance + $0.00 amount column.
2997
+ grid = _normalize_ucb_three_col_beginning_balance_fusion(grid)
2998
+
2999
  grid = _drop_truly_empty_columns(grid)
3000
  grid = _merge_blank_header_text_columns(grid)
3001
 
 
 
 
3002
  # Fix 5: reconstruct mid-table separator rows split across columns by OCR
3003
  grid = _reconstruct_separator_rows(grid)
3004
 
 
3270
  # Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
3271
  if is_pdf and not is_nfcu_doc:
3272
  stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
3273
+ # _patch_ocr_with_textlayer may append Case B rows from the PDF text layer
3274
+ # with fused UCB-style cells; run the same HTML table pass again (Fix 13/14).
3275
+ stabilized = normalize_html_tables(stabilized)
3276
+ stabilized = _dedupe_subset_transaction_tables_html(stabilized)
3277
  elif is_pdf and is_nfcu_doc:
3278
  # Navy-only additive fix: rebuild malformed summary table from
3279
  # text layer, but keep GLM body/header/footer unchanged.