Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -38,6 +38,11 @@ Primary goals for reconcile rate:
|
|
| 38 |
— take the rightmost plausible money token from Description+Amount text for the amount.
|
| 39 |
- Fix 20: DA3 in-table dedupe — cap consecutive identical normalized keys at 2 rows (keeps legitimate
|
| 40 |
back-to-back identical charges; still collapses 3+ OCR stutters).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
"""
|
| 42 |
|
| 43 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -3153,6 +3158,8 @@ _DA3_STRICT_AMOUNT = re.compile(r"^-?\$?\d{1,3}(?:,\d{3})*\.\d{2}-?$")
|
|
| 3153 |
_DA3_MONEY_TOKEN = re.compile(
|
| 3154 |
r"(?<![\d,])(-?\$?\d{1,3}(?:,\d{3})*\.\d{2}|-?\$?\d{2,6}\.\d{2})(?!\d)"
|
| 3155 |
)
|
|
|
|
|
|
|
| 3156 |
|
| 3157 |
|
| 3158 |
def _repair_da3_garbled_amount_cells(grid):
|
|
@@ -3381,6 +3388,122 @@ def _parse_da3_grid_from_table_html(table_html: str):
|
|
| 3381 |
return None
|
| 3382 |
|
| 3383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3384 |
def _trim_adjacent_da3_suffix_duplicating_next_table_prefix(text: str) -> str:
|
| 3385 |
"""
|
| 3386 |
Remove trailing data rows from a DA3 table when they are repeated as the opening
|
|
@@ -3568,9 +3691,11 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 3568 |
|
| 3569 |
stabilized = "\n\n".join(out_blocks)
|
| 3570 |
stabilized = convert_plaintext_bank_sections(stabilized)
|
|
|
|
| 3571 |
stabilized = normalize_html_tables(stabilized)
|
| 3572 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 3573 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
|
|
|
| 3574 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3575 |
return close_unclosed_html(stabilized)
|
| 3576 |
|
|
@@ -3700,6 +3825,7 @@ def run_ocr(uploaded_file):
|
|
| 3700 |
stabilized = normalize_html_tables(stabilized)
|
| 3701 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 3702 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
|
|
|
| 3703 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3704 |
elif is_pdf and is_nfcu_doc:
|
| 3705 |
# Navy-only additive fix: rebuild malformed summary table from
|
|
@@ -3783,6 +3909,7 @@ def run_ocr(uploaded_file):
|
|
| 3783 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
| 3784 |
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 3785 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
|
|
|
| 3786 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 3787 |
return merged
|
| 3788 |
|
|
|
|
| 38 |
— take the rightmost plausible money token from Description+Amount text for the amount.
|
| 39 |
- Fix 20: DA3 in-table dedupe — cap consecutive identical normalized keys at 2 rows (keeps legitimate
|
| 40 |
back-to-back identical charges; still collapses 3+ OCR stutters).
|
| 41 |
+
- Fix 21: Cross-table DA3 repair for MSBILL / MICROSOFT#G… PIN rows — when Amount is junk in one table
|
| 42 |
+
but another table repeats the same authorization id with a strict amount, copy that amount
|
| 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
|
|
|
|
| 3158 |
_DA3_MONEY_TOKEN = re.compile(
|
| 3159 |
r"(?<![\d,])(-?\$?\d{1,3}(?:,\d{3})*\.\d{2}|-?\$?\d{2,6}\.\d{2})(?!\d)"
|
| 3160 |
)
|
| 3161 |
+
_MSFT_PIN_G_REF = re.compile(r"MICROSOFT#(G\d{6,16})", re.IGNORECASE)
|
| 3162 |
+
_DA3_LEADING_DATE = re.compile(r"^\d{1,2}/\d{1,2}(?:/\d{2,4})?$")
|
| 3163 |
|
| 3164 |
|
| 3165 |
def _repair_da3_garbled_amount_cells(grid):
|
|
|
|
| 3388 |
return None
|
| 3389 |
|
| 3390 |
|
| 3391 |
+
def _strip_orphan_continued_da3_flatline(text: str) -> str:
|
| 3392 |
+
"""
|
| 3393 |
+
Fix 22: Remove one-line OCR dumps like
|
| 3394 |
+
'…(continued) DATE DESCRIPTION AMOUNT($) 04/09 … 04/11 … 239.55' that duplicate the
|
| 3395 |
+
following HTML table and pollute markdown.
|
| 3396 |
+
"""
|
| 3397 |
+
if not text or "(continued)" not in text.lower():
|
| 3398 |
+
return text
|
| 3399 |
+
lines = text.splitlines(keepends=True)
|
| 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 len(re.findall(r"\d{2}/\d{2}", core)) >= 3
|
| 3409 |
+
):
|
| 3410 |
+
continue
|
| 3411 |
+
out.append(line)
|
| 3412 |
+
return "".join(out)
|
| 3413 |
+
|
| 3414 |
+
|
| 3415 |
+
def _patch_da3_msft_g_ref_amounts_across_tables(text: str) -> str:
|
| 3416 |
+
"""
|
| 3417 |
+
Fix 21: Build MICROSOFT#G… -> strict Amount from any DA3 row, then patch other DA3 rows
|
| 3418 |
+
with the same id whose Amount cell is not strict currency (junk fused into Amount).
|
| 3419 |
+
"""
|
| 3420 |
+
if not text or "<table" not in text.lower():
|
| 3421 |
+
return text
|
| 3422 |
+
if "microsoft#" not in text.lower():
|
| 3423 |
+
return text
|
| 3424 |
+
|
| 3425 |
+
tbl_pat = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 3426 |
+
|
| 3427 |
+
def _collect():
|
| 3428 |
+
g_map = {}
|
| 3429 |
+
ambiguous = set()
|
| 3430 |
+
for m in tbl_pat.finditer(text):
|
| 3431 |
+
g = _parse_da3_grid_from_table_html(m.group(0))
|
| 3432 |
+
if not g:
|
| 3433 |
+
continue
|
| 3434 |
+
for row in g[1:]:
|
| 3435 |
+
cells = [str(c or "").strip() for c in row]
|
| 3436 |
+
while len(cells) < 3:
|
| 3437 |
+
cells.append("")
|
| 3438 |
+
c0, c1, c2 = cells[0], cells[1], cells[2]
|
| 3439 |
+
if not _DA3_LEADING_DATE.match(c0):
|
| 3440 |
+
continue
|
| 3441 |
+
if not _DA3_STRICT_AMOUNT.match(c2):
|
| 3442 |
+
continue
|
| 3443 |
+
blob = f"{c0} {c1} {c2}"
|
| 3444 |
+
for gid in _MSFT_PIN_G_REF.findall(blob):
|
| 3445 |
+
k = gid.upper()
|
| 3446 |
+
if k in ambiguous:
|
| 3447 |
+
continue
|
| 3448 |
+
trip = (c0, c1, c2)
|
| 3449 |
+
prev = g_map.get(k)
|
| 3450 |
+
if prev is not None and prev != trip:
|
| 3451 |
+
ambiguous.add(k)
|
| 3452 |
+
g_map.pop(k, None)
|
| 3453 |
+
elif prev is None:
|
| 3454 |
+
g_map[k] = trip
|
| 3455 |
+
return g_map, ambiguous
|
| 3456 |
+
|
| 3457 |
+
g_map, ambiguous = _collect()
|
| 3458 |
+
if not g_map:
|
| 3459 |
+
return text
|
| 3460 |
+
|
| 3461 |
+
out_chunks = []
|
| 3462 |
+
last = 0
|
| 3463 |
+
changed_any = False
|
| 3464 |
+
for m in tbl_pat.finditer(text):
|
| 3465 |
+
out_chunks.append(text[last : m.start()])
|
| 3466 |
+
table_html = m.group(0)
|
| 3467 |
+
grid = _parse_da3_grid_from_table_html(table_html)
|
| 3468 |
+
if not grid:
|
| 3469 |
+
out_chunks.append(table_html)
|
| 3470 |
+
last = m.end()
|
| 3471 |
+
continue
|
| 3472 |
+
new_grid = [list(grid[0])]
|
| 3473 |
+
changed = False
|
| 3474 |
+
for row in grid[1:]:
|
| 3475 |
+
cells = [str(c or "").strip() for c in row]
|
| 3476 |
+
while len(cells) < 3:
|
| 3477 |
+
cells.append("")
|
| 3478 |
+
c0, c1, c2 = cells[0], cells[1], cells[2]
|
| 3479 |
+
if not _DA3_LEADING_DATE.match(c0):
|
| 3480 |
+
new_grid.append(row)
|
| 3481 |
+
continue
|
| 3482 |
+
if _DA3_STRICT_AMOUNT.match(c2):
|
| 3483 |
+
new_grid.append(row)
|
| 3484 |
+
continue
|
| 3485 |
+
blob = f"{c0} {c1} {c2}"
|
| 3486 |
+
ids = _MSFT_PIN_G_REF.findall(blob)
|
| 3487 |
+
if len(ids) != 1:
|
| 3488 |
+
new_grid.append(row)
|
| 3489 |
+
continue
|
| 3490 |
+
k = ids[0].upper()
|
| 3491 |
+
if k in ambiguous or k not in g_map:
|
| 3492 |
+
new_grid.append(row)
|
| 3493 |
+
continue
|
| 3494 |
+
d0, d1, d2 = g_map[k]
|
| 3495 |
+
new_grid.append([d0, d1, d2])
|
| 3496 |
+
changed = True
|
| 3497 |
+
if changed:
|
| 3498 |
+
out_chunks.append(_grid_to_html(new_grid))
|
| 3499 |
+
changed_any = True
|
| 3500 |
+
else:
|
| 3501 |
+
out_chunks.append(table_html)
|
| 3502 |
+
last = m.end()
|
| 3503 |
+
out_chunks.append(text[last:])
|
| 3504 |
+
return "".join(out_chunks) if changed_any else text
|
| 3505 |
+
|
| 3506 |
+
|
| 3507 |
def _trim_adjacent_da3_suffix_duplicating_next_table_prefix(text: str) -> str:
|
| 3508 |
"""
|
| 3509 |
Remove trailing data rows from a DA3 table when they are repeated as the opening
|
|
|
|
| 3691 |
|
| 3692 |
stabilized = "\n\n".join(out_blocks)
|
| 3693 |
stabilized = convert_plaintext_bank_sections(stabilized)
|
| 3694 |
+
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 3695 |
stabilized = normalize_html_tables(stabilized)
|
| 3696 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 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 |
|
|
|
|
| 3825 |
stabilized = normalize_html_tables(stabilized)
|
| 3826 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 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
|
|
|
|
| 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 |
|