Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -119,6 +119,11 @@ Primary goals for reconcile rate:
|
|
| 119 |
five columns using trailing amount in the description cell and TD-specific debit/credit
|
| 120 |
routing. Fires on \"continued\" in-table text and/or TD body cues (TD ZELLE, LENDINGCLUB,
|
| 121 |
etc.). Safe no-op elsewhere.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
"""
|
| 123 |
|
| 124 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -139,7 +144,7 @@ import os
|
|
| 139 |
import re
|
| 140 |
import html
|
| 141 |
import tempfile
|
| 142 |
-
from typing import List, Tuple
|
| 143 |
from collections import Counter, defaultdict
|
| 144 |
from html.parser import HTMLParser
|
| 145 |
|
|
@@ -4956,6 +4961,139 @@ def _strip_check_image_ocr_junk_html(text: str) -> str:
|
|
| 4956 |
return text
|
| 4957 |
|
| 4958 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4959 |
def _dedupe_adjacent_duplicate_html_tables(text: str) -> str:
|
| 4960 |
"""
|
| 4961 |
Drop consecutive <table> blocks that are identical after grid parse (same header + row multiset),
|
|
@@ -5028,6 +5166,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 5028 |
_tl_freq_context.clear()
|
| 5029 |
|
| 5030 |
page_md = normalize_money_glyphs(page_md)
|
|
|
|
| 5031 |
|
| 5032 |
blocks = re.split(r"\n\s*\n", page_md.strip())
|
| 5033 |
out_blocks = []
|
|
@@ -5043,6 +5182,8 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 5043 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5044 |
stabilized = normalize_html_tables(stabilized)
|
| 5045 |
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
|
|
|
|
|
|
| 5046 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 5047 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5048 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
@@ -5054,6 +5195,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 5054 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 5055 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5056 |
stabilized = _strip_check_image_ocr_junk_html(stabilized) # Fix 31
|
|
|
|
| 5057 |
return close_unclosed_html(stabilized)
|
| 5058 |
|
| 5059 |
# --------------------------
|
|
@@ -5176,6 +5318,8 @@ def run_ocr(uploaded_file):
|
|
| 5176 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5177 |
stabilized = normalize_html_tables(stabilized)
|
| 5178 |
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
|
|
|
|
|
|
| 5179 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 5180 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5181 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
@@ -5254,6 +5398,9 @@ def run_ocr(uploaded_file):
|
|
| 5254 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
| 5255 |
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 5256 |
merged = _strip_standalone_continued_banner_line(merged)
|
|
|
|
|
|
|
|
|
|
| 5257 |
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 5258 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 5259 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|
|
|
|
| 119 |
five columns using trailing amount in the description cell and TD-specific debit/credit
|
| 120 |
routing. Fires on \"continued\" in-table text and/or TD body cues (TD ZELLE, LENDINGCLUB,
|
| 121 |
etc.). Safe no-op elsewhere.
|
| 122 |
+
- Fix MD-1: Markdown fidelity — (a) collapse adjacent HTML tables that are the same register
|
| 123 |
+
(identical multiset of date + debit + credit + balance per row, descriptions may differ);
|
| 124 |
+
keep the table with richer description text so output matches the PDF once. (b) Common OCR
|
| 125 |
+
typos (e.g. CINTHIAL→CINTHIA L) and double-escaped ampersands (&amp;→&).
|
| 126 |
+
Gap between tables must not contain another <table> or page-separator.
|
| 127 |
"""
|
| 128 |
|
| 129 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 144 |
import re
|
| 145 |
import html
|
| 146 |
import tempfile
|
| 147 |
+
from typing import List, Optional, Tuple
|
| 148 |
from collections import Counter, defaultdict
|
| 149 |
from html.parser import HTMLParser
|
| 150 |
|
|
|
|
| 4961 |
return text
|
| 4962 |
|
| 4963 |
|
| 4964 |
+
def _fix_markdown_fidelity_typos_and_escapes(text: str) -> str:
|
| 4965 |
+
"""Fix MD-1: common OCR name glitches and double HTML-escaped ampersands in OCR markdown."""
|
| 4966 |
+
if not text:
|
| 4967 |
+
return text
|
| 4968 |
+
text = re.sub(
|
| 4969 |
+
r"\bCINTHIAL\s+BARTON\b",
|
| 4970 |
+
"CINTHIA L BARTON",
|
| 4971 |
+
text,
|
| 4972 |
+
flags=re.IGNORECASE,
|
| 4973 |
+
)
|
| 4974 |
+
text = text.replace("&amp;", "&")
|
| 4975 |
+
return text
|
| 4976 |
+
|
| 4977 |
+
|
| 4978 |
+
def _gap_ok_loose_register_pair(ma, mb, text: str) -> bool:
|
| 4979 |
+
gap = text[ma.end() : mb.start()]
|
| 4980 |
+
if re.search(r"<table", gap, re.IGNORECASE):
|
| 4981 |
+
return False
|
| 4982 |
+
if re.search(r"page-separator", gap, re.IGNORECASE):
|
| 4983 |
+
return False
|
| 4984 |
+
return True
|
| 4985 |
+
|
| 4986 |
+
|
| 4987 |
+
def _norm_register_amount_key(s: str) -> str:
|
| 4988 |
+
t = str(s or "").strip().replace("$", "").replace(",", "").replace("−", "-")
|
| 4989 |
+
if not t or t in ("---", "—"):
|
| 4990 |
+
return ""
|
| 4991 |
+
try:
|
| 4992 |
+
return f"{float(t):.2f}"
|
| 4993 |
+
except ValueError:
|
| 4994 |
+
return t
|
| 4995 |
+
|
| 4996 |
+
|
| 4997 |
+
def _register_row_loose_key_cells(cells: List[str]) -> Optional[Tuple[str, str, str, str]]:
|
| 4998 |
+
if len(cells) < 5:
|
| 4999 |
+
return None
|
| 5000 |
+
d = str(cells[0]).strip()
|
| 5001 |
+
if not re.match(r"^\d{1,2}/\d{1,2}(?:/\d{2,4})?$", d):
|
| 5002 |
+
return None
|
| 5003 |
+
deb = _norm_register_amount_key(cells[2])
|
| 5004 |
+
cre = _norm_register_amount_key(cells[3])
|
| 5005 |
+
bal = _norm_register_amount_key(cells[4])
|
| 5006 |
+
return (d, deb, cre, bal)
|
| 5007 |
+
|
| 5008 |
+
|
| 5009 |
+
def _register_multiset_and_score(grid) -> Tuple[Optional[Counter], int]:
|
| 5010 |
+
if not grid or len(grid) < 2:
|
| 5011 |
+
return None, 0
|
| 5012 |
+
ncols = max(len(r) for r in grid if isinstance(r, list))
|
| 5013 |
+
if ncols < 5:
|
| 5014 |
+
return None, 0
|
| 5015 |
+
c = Counter()
|
| 5016 |
+
desc_score = 0
|
| 5017 |
+
for row in grid[1:]:
|
| 5018 |
+
cells = [str(x or "").strip() for x in row]
|
| 5019 |
+
while len(cells) < ncols:
|
| 5020 |
+
cells.append("")
|
| 5021 |
+
cells = cells[: max(5, ncols)]
|
| 5022 |
+
if len(cells) < 5:
|
| 5023 |
+
continue
|
| 5024 |
+
k = _register_row_loose_key_cells(cells[:5])
|
| 5025 |
+
if not k:
|
| 5026 |
+
continue
|
| 5027 |
+
c[k] += 1
|
| 5028 |
+
desc_score += len(cells[1])
|
| 5029 |
+
if sum(c.values()) < 3:
|
| 5030 |
+
return None, 0
|
| 5031 |
+
return c, desc_score
|
| 5032 |
+
|
| 5033 |
+
|
| 5034 |
+
def _dedupe_adjacent_loose_register_duplicate_tables(text: str) -> str:
|
| 5035 |
+
"""
|
| 5036 |
+
Fix MD-1: Drop adjacent duplicate bank register <table> blocks when date+debit+credit+balance
|
| 5037 |
+
multiset matches but description text differs (OCR double-emission). Keeps the richer table.
|
| 5038 |
+
"""
|
| 5039 |
+
if not text or "<table" not in text.lower():
|
| 5040 |
+
return text
|
| 5041 |
+
try:
|
| 5042 |
+
tbl_pat = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 5043 |
+
matches = list(tbl_pat.finditer(text))
|
| 5044 |
+
if len(matches) < 2:
|
| 5045 |
+
return text
|
| 5046 |
+
|
| 5047 |
+
def _grid(m):
|
| 5048 |
+
p = TableGridParser()
|
| 5049 |
+
p.feed(m.group(0))
|
| 5050 |
+
return _build_grid(p.rows)
|
| 5051 |
+
|
| 5052 |
+
keep_indices: List[int] = [0]
|
| 5053 |
+
for j in range(1, len(matches)):
|
| 5054 |
+
prev_i = keep_indices[-1]
|
| 5055 |
+
prev_m, cur_m = matches[prev_i], matches[j]
|
| 5056 |
+
if not _gap_ok_loose_register_pair(prev_m, cur_m, text):
|
| 5057 |
+
keep_indices.append(j)
|
| 5058 |
+
continue
|
| 5059 |
+
ga, gb = _grid(prev_m), _grid(cur_m)
|
| 5060 |
+
ma, sa = _register_multiset_and_score(ga)
|
| 5061 |
+
mb, sb = _register_multiset_and_score(gb)
|
| 5062 |
+
if ma is None or mb is None or ma != mb:
|
| 5063 |
+
keep_indices.append(j)
|
| 5064 |
+
continue
|
| 5065 |
+
if sb >= sa:
|
| 5066 |
+
keep_indices[-1] = j
|
| 5067 |
+
log.info(
|
| 5068 |
+
"Fix MD-1: loose-dup register — kept table at offset %d (desc score %d vs %d)",
|
| 5069 |
+
cur_m.start(),
|
| 5070 |
+
sb,
|
| 5071 |
+
sa,
|
| 5072 |
+
)
|
| 5073 |
+
else:
|
| 5074 |
+
log.info(
|
| 5075 |
+
"Fix MD-1: dropped loose-dup register table at offset %d",
|
| 5076 |
+
cur_m.start(),
|
| 5077 |
+
)
|
| 5078 |
+
|
| 5079 |
+
drop = set(range(len(matches))) - set(keep_indices)
|
| 5080 |
+
if not drop:
|
| 5081 |
+
return text
|
| 5082 |
+
|
| 5083 |
+
out: List[str] = []
|
| 5084 |
+
pos = 0
|
| 5085 |
+
for i, m in enumerate(matches):
|
| 5086 |
+
out.append(text[pos : m.start()])
|
| 5087 |
+
if i not in drop:
|
| 5088 |
+
out.append(m.group(0))
|
| 5089 |
+
pos = m.end()
|
| 5090 |
+
out.append(text[pos:])
|
| 5091 |
+
return "".join(out)
|
| 5092 |
+
except Exception as e:
|
| 5093 |
+
log.warning("_dedupe_adjacent_loose_register_duplicate_tables failed: %s", e)
|
| 5094 |
+
return text
|
| 5095 |
+
|
| 5096 |
+
|
| 5097 |
def _dedupe_adjacent_duplicate_html_tables(text: str) -> str:
|
| 5098 |
"""
|
| 5099 |
Drop consecutive <table> blocks that are identical after grid parse (same header + row multiset),
|
|
|
|
| 5166 |
_tl_freq_context.clear()
|
| 5167 |
|
| 5168 |
page_md = normalize_money_glyphs(page_md)
|
| 5169 |
+
page_md = _fix_markdown_fidelity_typos_and_escapes(page_md)
|
| 5170 |
|
| 5171 |
blocks = re.split(r"\n\s*\n", page_md.strip())
|
| 5172 |
out_blocks = []
|
|
|
|
| 5182 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5183 |
stabilized = normalize_html_tables(stabilized)
|
| 5184 |
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
| 5185 |
+
stabilized = _dedupe_adjacent_loose_register_duplicate_tables(stabilized)
|
| 5186 |
+
stabilized = _fix_markdown_fidelity_typos_and_escapes(stabilized)
|
| 5187 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 5188 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5189 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
|
|
| 5195 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 5196 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5197 |
stabilized = _strip_check_image_ocr_junk_html(stabilized) # Fix 31
|
| 5198 |
+
stabilized = _fix_markdown_fidelity_typos_and_escapes(stabilized)
|
| 5199 |
return close_unclosed_html(stabilized)
|
| 5200 |
|
| 5201 |
# --------------------------
|
|
|
|
| 5318 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 5319 |
stabilized = normalize_html_tables(stabilized)
|
| 5320 |
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
| 5321 |
+
stabilized = _dedupe_adjacent_loose_register_duplicate_tables(stabilized)
|
| 5322 |
+
stabilized = _fix_markdown_fidelity_typos_and_escapes(stabilized)
|
| 5323 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 5324 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5325 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
|
|
| 5398 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
| 5399 |
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 5400 |
merged = _strip_standalone_continued_banner_line(merged)
|
| 5401 |
+
merged = _dedupe_adjacent_duplicate_html_tables(merged)
|
| 5402 |
+
merged = _dedupe_adjacent_loose_register_duplicate_tables(merged)
|
| 5403 |
+
merged = _fix_markdown_fidelity_typos_and_escapes(merged)
|
| 5404 |
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 5405 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 5406 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|