Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -43,6 +43,14 @@ Primary goals for reconcile rate:
|
|
| 43 |
(then subset dedupe can drop the stray duplicate block, e.g. under Deposits).
|
| 44 |
- Fix 22: Drop a single OCR “flat line” between pages: (continued) + DESCRIPTION + AMOUNT + many MM/DD
|
| 45 |
tokens (noise before a proper <table>).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
"""
|
| 47 |
|
| 48 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -64,7 +72,7 @@ import re
|
|
| 64 |
import html
|
| 65 |
import tempfile
|
| 66 |
from typing import List, Tuple
|
| 67 |
-
from collections import defaultdict
|
| 68 |
from html.parser import HTMLParser
|
| 69 |
|
| 70 |
import yaml
|
|
@@ -3143,6 +3151,59 @@ def _transaction_row_dedupe_key(cells):
|
|
| 3143 |
return (date_s, desc_s, amt_s)
|
| 3144 |
|
| 3145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3146 |
def _is_da3_header(grid):
|
| 3147 |
if not grid or len(grid[0]) < 3:
|
| 3148 |
return False
|
|
@@ -3400,12 +3461,15 @@ def _strip_orphan_continued_da3_flatline(text: str) -> str:
|
|
| 3400 |
out = []
|
| 3401 |
for line in lines:
|
| 3402 |
core = line.rstrip("\r\n")
|
|
|
|
|
|
|
|
|
|
| 3403 |
if (
|
| 3404 |
len(core) > 80
|
| 3405 |
and "(continued)" in core.lower()
|
| 3406 |
and "description" in core.lower()
|
| 3407 |
and "amount" in core.lower()
|
| 3408 |
-
and
|
| 3409 |
):
|
| 3410 |
continue
|
| 3411 |
out.append(line)
|
|
@@ -3570,6 +3634,8 @@ def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
|
| 3570 |
"Deposits (continued)" preview before the same rows appear in the full list).
|
| 3571 |
- Identical row sets → drop the later table.
|
| 3572 |
- Fuzzy overlap (≥92% of later rows match earlier) → drop later (Electronic Debits vs main).
|
|
|
|
|
|
|
| 3573 |
- Runs multiple passes until stable.
|
| 3574 |
Row matching uses normalized keys so OCR variants (e.g. "0 27" vs "027", backticks)
|
| 3575 |
do not prevent duplicate tables from being detected.
|
|
@@ -3653,6 +3719,23 @@ def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
|
| 3653 |
drop_idx.add(j)
|
| 3654 |
continue
|
| 3655 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3656 |
if not drop_idx:
|
| 3657 |
return t
|
| 3658 |
|
|
@@ -3675,6 +3758,136 @@ def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
|
| 3675 |
return out
|
| 3676 |
|
| 3677 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3678 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 3679 |
if not page_md:
|
| 3680 |
return page_md
|
|
@@ -3697,6 +3910,8 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 3697 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3698 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 3699 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
|
|
|
|
|
|
| 3700 |
return close_unclosed_html(stabilized)
|
| 3701 |
|
| 3702 |
# --------------------------
|
|
@@ -3820,6 +4035,7 @@ def run_ocr(uploaded_file):
|
|
| 3820 |
# Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
|
| 3821 |
if is_pdf and not is_nfcu_doc:
|
| 3822 |
stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
|
|
|
|
| 3823 |
# _patch_ocr_with_textlayer may append Case B rows from the PDF text layer
|
| 3824 |
# with fused UCB-style cells; run the same HTML table pass again (Fix 13/14).
|
| 3825 |
stabilized = normalize_html_tables(stabilized)
|
|
@@ -3827,6 +4043,7 @@ def run_ocr(uploaded_file):
|
|
| 3827 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3828 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 3829 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
|
|
|
| 3830 |
elif is_pdf and is_nfcu_doc:
|
| 3831 |
# Navy-only additive fix: rebuild malformed summary table from
|
| 3832 |
# text layer, but keep GLM body/header/footer unchanged.
|
|
@@ -3907,10 +4124,13 @@ def run_ocr(uploaded_file):
|
|
| 3907 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 3908 |
# Cross-page: UCB section split + same transaction tables dedupe once on full doc.
|
| 3909 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
|
|
|
| 3910 |
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 3911 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 3912 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|
| 3913 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
|
|
|
|
|
|
| 3914 |
return merged
|
| 3915 |
|
| 3916 |
except Exception as e:
|
|
|
|
| 43 |
(then subset dedupe can drop the stray duplicate block, e.g. under Deposits).
|
| 44 |
- Fix 22: Drop a single OCR “flat line” between pages: (continued) + DESCRIPTION + AMOUNT + many MM/DD
|
| 45 |
tokens (noise before a proper <table>).
|
| 46 |
+
- Fix 23: Doc-wide DA3 table dedupe using multiset of (date, amount) keys — catches duplicate sections when
|
| 47 |
+
OCR varies description text between copies (e.g. withdrawals repeated under Deposits).
|
| 48 |
+
- Fix 24: Run orphan flatline strip on merged PDF output and after text-layer patch so page-boundary
|
| 49 |
+
noise is removed consistently.
|
| 50 |
+
- Fix 25: After a centered “deposits + credits/interest” heading, if two DA3 tables appear back-to-back
|
| 51 |
+
and the first is mostly credit/deposit rows while the second is mostly debit/withdrawal-style
|
| 52 |
+
rows (generic keywords), drop the second — fixes OCR pasting the withdrawals block again.
|
| 53 |
+
- Fix 23b: Loose multiset keys use normalized float amounts so comma/spacing variants still match.
|
| 54 |
"""
|
| 55 |
|
| 56 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 72 |
import html
|
| 73 |
import tempfile
|
| 74 |
from typing import List, Tuple
|
| 75 |
+
from collections import Counter, defaultdict
|
| 76 |
from html.parser import HTMLParser
|
| 77 |
|
| 78 |
import yaml
|
|
|
|
| 3151 |
return (date_s, desc_s, amt_s)
|
| 3152 |
|
| 3153 |
|
| 3154 |
+
def _da3_loose_key(cells):
|
| 3155 |
+
"""
|
| 3156 |
+
(date, amount) only — for detecting duplicate DA3 tables when description drifts between OCR passes.
|
| 3157 |
+
Uses strict currency + float amount so junk cells do not create keys; 1,234.56 vs 1234.56 match.
|
| 3158 |
+
"""
|
| 3159 |
+
parts = [str(c or "").strip() for c in cells[:3]]
|
| 3160 |
+
while len(parts) < 3:
|
| 3161 |
+
parts.append("")
|
| 3162 |
+
date_s, desc_s, amt_s = parts[0], parts[1], parts[2]
|
| 3163 |
+
if not _DA3_LEADING_DATE.match(date_s):
|
| 3164 |
+
return None
|
| 3165 |
+
comb = f"{date_s} {desc_s}".lower()
|
| 3166 |
+
if re.search(r"\btotal\b", comb) and len(desc_s) > 25:
|
| 3167 |
+
return None
|
| 3168 |
+
if not _DA3_STRICT_AMOUNT.match(amt_s.strip()):
|
| 3169 |
+
return None
|
| 3170 |
+
t_amt = re.sub(r"[\s$,]", "", amt_s).replace("−", "-").replace("$", "")
|
| 3171 |
+
try:
|
| 3172 |
+
af = round(float(t_amt), 2)
|
| 3173 |
+
except ValueError:
|
| 3174 |
+
return None
|
| 3175 |
+
d_norm = re.sub(r"\s+", "", date_s)
|
| 3176 |
+
return (d_norm, af)
|
| 3177 |
+
|
| 3178 |
+
|
| 3179 |
+
def _row_counter_loose_da3(g):
|
| 3180 |
+
"""Multiset of (date, amount) for DA3 data rows with strict amounts."""
|
| 3181 |
+
c = Counter()
|
| 3182 |
+
if not g or len(g) < 2:
|
| 3183 |
+
return c
|
| 3184 |
+
for r in g[1:]:
|
| 3185 |
+
cells = [str(x or "").strip() for x in r]
|
| 3186 |
+
if not any(cells):
|
| 3187 |
+
continue
|
| 3188 |
+
k = _da3_loose_key(cells)
|
| 3189 |
+
if k:
|
| 3190 |
+
c[k] += 1
|
| 3191 |
+
return c
|
| 3192 |
+
|
| 3193 |
+
|
| 3194 |
+
def _overlap_ratio_counter(c_num, c_den):
|
| 3195 |
+
"""Fraction of c_num's multiset mass matched in c_den (min counts per key)."""
|
| 3196 |
+
tot = sum(c_num.values())
|
| 3197 |
+
if tot == 0:
|
| 3198 |
+
return 0.0
|
| 3199 |
+
hit = sum(min(c_num[k], c_den.get(k, 0)) for k in c_num)
|
| 3200 |
+
return hit / tot
|
| 3201 |
+
|
| 3202 |
+
|
| 3203 |
+
def _multiset_leq(c_small, c_big):
|
| 3204 |
+
return all(c_small[k] <= c_big.get(k, 0) for k in c_small)
|
| 3205 |
+
|
| 3206 |
+
|
| 3207 |
def _is_da3_header(grid):
|
| 3208 |
if not grid or len(grid[0]) < 3:
|
| 3209 |
return False
|
|
|
|
| 3461 |
out = []
|
| 3462 |
for line in lines:
|
| 3463 |
core = line.rstrip("\r\n")
|
| 3464 |
+
date_hits = len(re.findall(r"\d{2}/\d{2}", core)) + len(
|
| 3465 |
+
re.findall(r"\b\d{2}-\d{2}-\d{2}\b", core)
|
| 3466 |
+
)
|
| 3467 |
if (
|
| 3468 |
len(core) > 80
|
| 3469 |
and "(continued)" in core.lower()
|
| 3470 |
and "description" in core.lower()
|
| 3471 |
and "amount" in core.lower()
|
| 3472 |
+
and date_hits >= 3
|
| 3473 |
):
|
| 3474 |
continue
|
| 3475 |
out.append(line)
|
|
|
|
| 3634 |
"Deposits (continued)" preview before the same rows appear in the full list).
|
| 3635 |
- Identical row sets → drop the later table.
|
| 3636 |
- Fuzzy overlap (≥92% of later rows match earlier) → drop later (Electronic Debits vs main).
|
| 3637 |
+
- Fix 23: multiset overlap on (date, amount) only — drop when OCR varies description
|
| 3638 |
+
between two copies of the same section (e.g. withdrawals pasted twice).
|
| 3639 |
- Runs multiple passes until stable.
|
| 3640 |
Row matching uses normalized keys so OCR variants (e.g. "0 27" vs "027", backticks)
|
| 3641 |
do not prevent duplicate tables from being detected.
|
|
|
|
| 3719 |
drop_idx.add(j)
|
| 3720 |
continue
|
| 3721 |
|
| 3722 |
+
# Fix 23: multiset (date, amount) overlap — duplicate tables when description OCR differs.
|
| 3723 |
+
ci = _row_counter_loose_da3(gi)
|
| 3724 |
+
cj = _row_counter_loose_da3(gj)
|
| 3725 |
+
ni, nj = sum(ci.values()), sum(cj.values())
|
| 3726 |
+
if ni >= 6 and nj >= 6:
|
| 3727 |
+
oi_j = _overlap_ratio_counter(ci, cj)
|
| 3728 |
+
oj_i = _overlap_ratio_counter(cj, ci)
|
| 3729 |
+
if oi_j >= 0.88 and oj_i >= 0.88:
|
| 3730 |
+
drop_idx.add(i)
|
| 3731 |
+
break
|
| 3732 |
+
if _multiset_leq(ci, cj) and ni < nj and oi_j >= 0.9:
|
| 3733 |
+
drop_idx.add(i)
|
| 3734 |
+
break
|
| 3735 |
+
if _multiset_leq(cj, ci) and nj < ni and oj_i >= 0.9:
|
| 3736 |
+
drop_idx.add(j)
|
| 3737 |
+
continue
|
| 3738 |
+
|
| 3739 |
if not drop_idx:
|
| 3740 |
return t
|
| 3741 |
|
|
|
|
| 3758 |
return out
|
| 3759 |
|
| 3760 |
|
| 3761 |
+
_RE_CENTER_DIV_TWO_DA3_TABLES = re.compile(
|
| 3762 |
+
r'(<div\s+align=["\']center["\']\s*>\s*[^<]*?</div>\s*)'
|
| 3763 |
+
r'(<table[^>]*>.*?</table>)'
|
| 3764 |
+
r'(\s*<table[^>]*>.*?</table>)',
|
| 3765 |
+
re.IGNORECASE | re.DOTALL,
|
| 3766 |
+
)
|
| 3767 |
+
|
| 3768 |
+
|
| 3769 |
+
def _da3_row_credit_vs_debit_category(desc: str) -> str:
|
| 3770 |
+
"""
|
| 3771 |
+
Generic classification for Date|Description|Amount rows (middle column only).
|
| 3772 |
+
Used to detect OCR pasting a withdrawals table after a deposits heading.
|
| 3773 |
+
"""
|
| 3774 |
+
d = (desc or "").lower()
|
| 3775 |
+
if "debit card return" in d:
|
| 3776 |
+
return "credit"
|
| 3777 |
+
if "debit card" in d and "return" in d:
|
| 3778 |
+
return "credit"
|
| 3779 |
+
if "deposit" in d and "debit card" not in d[:60]:
|
| 3780 |
+
return "credit"
|
| 3781 |
+
if "incoming wire" in d or "wire ref" in d:
|
| 3782 |
+
return "credit"
|
| 3783 |
+
if "zelle business reversal" in d or ("reversal" in d and "zelle" in d):
|
| 3784 |
+
return "credit"
|
| 3785 |
+
if "lrm claims" in d or ("claims" in d and "adj" in d):
|
| 3786 |
+
return "credit"
|
| 3787 |
+
# Credits / tax refunds that omit the word "deposit" (common on statement continuations)
|
| 3788 |
+
if "adp tax" in d and "customer" in d:
|
| 3789 |
+
return "credit"
|
| 3790 |
+
if "debit card" in d:
|
| 3791 |
+
return "debit"
|
| 3792 |
+
if "ach corp debit" in d:
|
| 3793 |
+
return "debit"
|
| 3794 |
+
if "bus online ach settlement" in d:
|
| 3795 |
+
return "debit"
|
| 3796 |
+
if "zelle business payment to" in d:
|
| 3797 |
+
return "debit"
|
| 3798 |
+
if "visa money transfer debit" in d:
|
| 3799 |
+
return "debit"
|
| 3800 |
+
if "telephone payment" in d or "service charges" in d:
|
| 3801 |
+
return "debit"
|
| 3802 |
+
if "merch fee" in d:
|
| 3803 |
+
return "debit"
|
| 3804 |
+
if "deposit" in d:
|
| 3805 |
+
return "credit"
|
| 3806 |
+
return "unknown"
|
| 3807 |
+
|
| 3808 |
+
|
| 3809 |
+
def _da3_table_credit_debit_fractions(grid):
|
| 3810 |
+
"""Returns (debit_frac, credit_frac, unknown_frac, scored_row_count)."""
|
| 3811 |
+
deb = cre = unk = 0
|
| 3812 |
+
if not grid or len(grid) < 2:
|
| 3813 |
+
return 0.0, 0.0, 0.0, 0
|
| 3814 |
+
for row in grid[1:]:
|
| 3815 |
+
cells = [str(c or "").strip() for c in row]
|
| 3816 |
+
while len(cells) < 3:
|
| 3817 |
+
cells.append("")
|
| 3818 |
+
if not _DA3_LEADING_DATE.match(cells[0]):
|
| 3819 |
+
continue
|
| 3820 |
+
if not _DA3_STRICT_AMOUNT.match((cells[2] or "").strip()):
|
| 3821 |
+
continue
|
| 3822 |
+
cat = _da3_row_credit_vs_debit_category(cells[1])
|
| 3823 |
+
if cat == "debit":
|
| 3824 |
+
deb += 1
|
| 3825 |
+
elif cat == "credit":
|
| 3826 |
+
cre += 1
|
| 3827 |
+
else:
|
| 3828 |
+
unk += 1
|
| 3829 |
+
tot = deb + cre + unk
|
| 3830 |
+
if tot == 0:
|
| 3831 |
+
return 0.0, 0.0, 0.0, 0
|
| 3832 |
+
return deb / tot, cre / tot, unk / tot, tot
|
| 3833 |
+
|
| 3834 |
+
|
| 3835 |
+
def _drop_da3_misplaced_debit_after_deposit_heading_tables(text: str) -> str:
|
| 3836 |
+
"""
|
| 3837 |
+
Fix 25: PDF order is withdrawals → then deposits. OCR sometimes emits two DA3 tables
|
| 3838 |
+
after the deposits heading: real deposits, then a duplicate withdrawals block.
|
| 3839 |
+
Drop the second table when it is overwhelmingly debit-like and the first is credit-like.
|
| 3840 |
+
Repeats until no more matches (multiple duplicate blocks).
|
| 3841 |
+
"""
|
| 3842 |
+
if not text or "<table" not in text.lower():
|
| 3843 |
+
return text
|
| 3844 |
+
if "deposit" not in text.lower():
|
| 3845 |
+
return text
|
| 3846 |
+
|
| 3847 |
+
for _ in range(12):
|
| 3848 |
+
replaced = False
|
| 3849 |
+
pos = 0
|
| 3850 |
+
while True:
|
| 3851 |
+
m = _RE_CENTER_DIV_TWO_DA3_TABLES.search(text, pos)
|
| 3852 |
+
if not m:
|
| 3853 |
+
break
|
| 3854 |
+
div = m.group(1)
|
| 3855 |
+
t1 = m.group(2)
|
| 3856 |
+
t2 = m.group(3)
|
| 3857 |
+
sl = div.lower()
|
| 3858 |
+
if "deposit" not in sl or not any(k in sl for k in ("credit", "interest", "credits")):
|
| 3859 |
+
pos = m.start() + 1
|
| 3860 |
+
continue
|
| 3861 |
+
|
| 3862 |
+
g1 = _parse_da3_grid_from_table_html(t1)
|
| 3863 |
+
g2 = _parse_da3_grid_from_table_html(t2)
|
| 3864 |
+
if not g1 or not g2 or not _is_da3_header(g1) or not _is_da3_header(g2):
|
| 3865 |
+
pos = m.start() + 1
|
| 3866 |
+
continue
|
| 3867 |
+
|
| 3868 |
+
db1, cr1, unk1, n1 = _da3_table_credit_debit_fractions(g1)
|
| 3869 |
+
db2, cr2, _unk2, n2 = _da3_table_credit_debit_fractions(g2)
|
| 3870 |
+
if n1 < 5 or n2 < 10:
|
| 3871 |
+
pos = m.start() + 1
|
| 3872 |
+
continue
|
| 3873 |
+
# First table: deposits often have ADP/merchant lines scored as unknown — blend unk toward credit.
|
| 3874 |
+
credit_like_first = cr1 + 0.42 * unk1
|
| 3875 |
+
if credit_like_first < 0.30 and credit_like_first <= db1 + 0.05:
|
| 3876 |
+
pos = m.start() + 1
|
| 3877 |
+
continue
|
| 3878 |
+
if db2 < 0.74 or db2 <= cr2 + 0.07:
|
| 3879 |
+
pos = m.start() + 1
|
| 3880 |
+
continue
|
| 3881 |
+
|
| 3882 |
+
text = text[: m.start()] + div + t1 + text[m.end() :]
|
| 3883 |
+
replaced = True
|
| 3884 |
+
break
|
| 3885 |
+
if not replaced:
|
| 3886 |
+
break
|
| 3887 |
+
|
| 3888 |
+
return text
|
| 3889 |
+
|
| 3890 |
+
|
| 3891 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 3892 |
if not page_md:
|
| 3893 |
return page_md
|
|
|
|
| 3910 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3911 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 3912 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3913 |
+
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 3914 |
+
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 3915 |
return close_unclosed_html(stabilized)
|
| 3916 |
|
| 3917 |
# --------------------------
|
|
|
|
| 4035 |
# Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
|
| 4036 |
if is_pdf and not is_nfcu_doc:
|
| 4037 |
stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
|
| 4038 |
+
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 4039 |
# _patch_ocr_with_textlayer may append Case B rows from the PDF text layer
|
| 4040 |
# with fused UCB-style cells; run the same HTML table pass again (Fix 13/14).
|
| 4041 |
stabilized = normalize_html_tables(stabilized)
|
|
|
|
| 4043 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 4044 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 4045 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 4046 |
+
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 4047 |
elif is_pdf and is_nfcu_doc:
|
| 4048 |
# Navy-only additive fix: rebuild malformed summary table from
|
| 4049 |
# text layer, but keep GLM body/header/footer unchanged.
|
|
|
|
| 4124 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 4125 |
# Cross-page: UCB section split + same transaction tables dedupe once on full doc.
|
| 4126 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
| 4127 |
+
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 4128 |
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 4129 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 4130 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|
| 4131 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 4132 |
+
merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
|
| 4133 |
+
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 4134 |
return merged
|
| 4135 |
|
| 4136 |
except Exception as e:
|