Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -133,6 +133,9 @@ Primary goals for reconcile rate:
|
|
| 133 |
table when bbox/image markers + repeated check lines are present; skips chunks with YOUR CHECKS SEQUENCED.
|
| 134 |
- Fix MD-4: YOUR CHECKS SEQUENCED wide grid (N×DATE|CHECK#|AMOUNT per row) is unfolded to one DATE|CHECK#|AMOUNT
|
| 135 |
table so narrow viewers show every check, not only the first date column.
|
|
|
|
|
|
|
|
|
|
| 136 |
"""
|
| 137 |
|
| 138 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -4523,6 +4526,140 @@ def _dedupe_da3_by_date_amount_loose(text: str) -> str:
|
|
| 4523 |
return text
|
| 4524 |
|
| 4525 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4526 |
def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
| 4527 |
if not text or "<table" not in text.lower():
|
| 4528 |
return text
|
|
@@ -5377,6 +5514,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 5377 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5378 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 5379 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
|
|
|
| 5380 |
stabilized = _dedupe_da3_by_date_amount_loose(stabilized) # Fix 30
|
| 5381 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 5382 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
@@ -5514,6 +5652,7 @@ def run_ocr(uploaded_file):
|
|
| 5514 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5515 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 5516 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
|
|
|
| 5517 |
stabilized = _dedupe_da3_by_date_amount_loose(stabilized) # Fix 30
|
| 5518 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 5519 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
@@ -5596,6 +5735,7 @@ def run_ocr(uploaded_file):
|
|
| 5596 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 5597 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|
| 5598 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
|
|
|
| 5599 |
merged = _dedupe_da3_by_date_amount_loose(merged) # Fix 30
|
| 5600 |
merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
|
| 5601 |
merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
|
|
|
|
| 133 |
table when bbox/image markers + repeated check lines are present; skips chunks with YOUR CHECKS SEQUENCED.
|
| 134 |
- Fix MD-4: YOUR CHECKS SEQUENCED wide grid (N×DATE|CHECK#|AMOUNT per row) is unfolded to one DATE|CHECK#|AMOUNT
|
| 135 |
table so narrow viewers show every check, not only the first date column.
|
| 136 |
+
- Fix FH-1: Drop a leading Date|Description|Amount (DA3) table when its (date, amount) multiset is a
|
| 137 |
+
strict subset of a later DEPOSIT/WITHDRAWAL register (e.g. First Horizon OCR duplicate before
|
| 138 |
+
ACCOUNT SUMMARY). Safe no-op when no such pair exists.
|
| 139 |
"""
|
| 140 |
|
| 141 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 4526 |
return text
|
| 4527 |
|
| 4528 |
|
| 4529 |
+
def _fh_withdrawal_column_index(grid) -> Optional[int]:
|
| 4530 |
+
for row in grid[: min(6, len(grid))]:
|
| 4531 |
+
for ci, c in enumerate(row):
|
| 4532 |
+
if str(c or "").strip().upper() == "WITHDRAWAL":
|
| 4533 |
+
return ci
|
| 4534 |
+
return None
|
| 4535 |
+
|
| 4536 |
+
|
| 4537 |
+
def _fh_is_deposit_withdrawal_register(grid) -> bool:
|
| 4538 |
+
if not grid or len(grid) < 2:
|
| 4539 |
+
return False
|
| 4540 |
+
blob = " ".join(
|
| 4541 |
+
" ".join(str(c or "") for c in row) for row in grid[: min(5, len(grid))]
|
| 4542 |
+
).upper()
|
| 4543 |
+
return "DEPOSIT" in blob and "WITHDRAWAL" in blob and "DATE" in blob
|
| 4544 |
+
|
| 4545 |
+
|
| 4546 |
+
def _drop_da3_duplicate_of_later_deposit_withdrawal_register(text: str) -> str:
|
| 4547 |
+
"""
|
| 4548 |
+
Fix FH-1: Vision OCR may emit a spurious DA3 table (subset of withdrawals) before the real
|
| 4549 |
+
ACCOUNT HISTORY grid. Drop the DA3 when every (date, amount) row is covered by a later
|
| 4550 |
+
register's WITHDRAWAL column and that register has strictly more withdrawal rows.
|
| 4551 |
+
"""
|
| 4552 |
+
if not text or "<table" not in text.lower():
|
| 4553 |
+
return text
|
| 4554 |
+
try:
|
| 4555 |
+
pattern = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 4556 |
+
matches = list(pattern.finditer(text))
|
| 4557 |
+
if len(matches) < 2:
|
| 4558 |
+
return text
|
| 4559 |
+
|
| 4560 |
+
def _parse_grid(html: str):
|
| 4561 |
+
try:
|
| 4562 |
+
p = TableGridParser()
|
| 4563 |
+
p.feed(html)
|
| 4564 |
+
return _build_grid(p.rows)
|
| 4565 |
+
except Exception:
|
| 4566 |
+
return None
|
| 4567 |
+
|
| 4568 |
+
def _norm_da_key_date_amt(date_s: str, amt_s: str) -> Tuple[str, str]:
|
| 4569 |
+
d = re.sub(r"\s+", "", str(date_s or "").strip())
|
| 4570 |
+
a = re.sub(r"[\s$,]", "", str(amt_s or "").strip()).lower()
|
| 4571 |
+
a = a.replace("−", "-").replace("\u2013", "-")
|
| 4572 |
+
return (d, a)
|
| 4573 |
+
|
| 4574 |
+
def _frag_counter(g) -> Optional[Counter]:
|
| 4575 |
+
if not g or not _is_da3_header(g):
|
| 4576 |
+
return None
|
| 4577 |
+
c = Counter()
|
| 4578 |
+
for row in g[1:]:
|
| 4579 |
+
cells = [str(x or "").strip() for x in row]
|
| 4580 |
+
while len(cells) < 3:
|
| 4581 |
+
cells.append("")
|
| 4582 |
+
if not _DA3_LEADING_DATE.match(cells[0]):
|
| 4583 |
+
continue
|
| 4584 |
+
if not _DA3_STRICT_AMOUNT.match(cells[2]):
|
| 4585 |
+
continue
|
| 4586 |
+
c[_norm_da_key_date_amt(cells[0], cells[2])] += 1
|
| 4587 |
+
if sum(c.values()) < 2:
|
| 4588 |
+
return None
|
| 4589 |
+
return c
|
| 4590 |
+
|
| 4591 |
+
def _register_wd_counter(g) -> Optional[Counter]:
|
| 4592 |
+
if not _fh_is_deposit_withdrawal_register(g):
|
| 4593 |
+
return None
|
| 4594 |
+
wci = _fh_withdrawal_column_index(g)
|
| 4595 |
+
if wci is None:
|
| 4596 |
+
return None
|
| 4597 |
+
c = Counter()
|
| 4598 |
+
for row in g[1:]:
|
| 4599 |
+
cells = [str(x or "").strip() for x in row]
|
| 4600 |
+
while len(cells) <= wci:
|
| 4601 |
+
cells.append("")
|
| 4602 |
+
if not _DA3_LEADING_DATE.match(cells[0]):
|
| 4603 |
+
continue
|
| 4604 |
+
amt = cells[wci]
|
| 4605 |
+
if not amt.strip():
|
| 4606 |
+
continue
|
| 4607 |
+
if not _DA3_STRICT_AMOUNT.match(amt):
|
| 4608 |
+
continue
|
| 4609 |
+
c[_norm_da_key_date_amt(cells[0], amt)] += 1
|
| 4610 |
+
if sum(c.values()) < 3:
|
| 4611 |
+
return None
|
| 4612 |
+
return c
|
| 4613 |
+
|
| 4614 |
+
grids = [(m, _parse_grid(m.group(0))) for m in matches]
|
| 4615 |
+
drop_starts = set()
|
| 4616 |
+
|
| 4617 |
+
for i in range(len(grids) - 1):
|
| 4618 |
+
mi, gi = grids[i]
|
| 4619 |
+
if mi.start() in drop_starts:
|
| 4620 |
+
continue
|
| 4621 |
+
fk = _frag_counter(gi)
|
| 4622 |
+
if fk is None:
|
| 4623 |
+
continue
|
| 4624 |
+
n_frag = sum(fk.values())
|
| 4625 |
+
if n_frag > 48:
|
| 4626 |
+
continue
|
| 4627 |
+
for j in range(i + 1, len(grids)):
|
| 4628 |
+
mj, gj = grids[j]
|
| 4629 |
+
rk = _register_wd_counter(gj)
|
| 4630 |
+
if rk is None:
|
| 4631 |
+
continue
|
| 4632 |
+
n_reg = sum(rk.values())
|
| 4633 |
+
if n_reg <= n_frag:
|
| 4634 |
+
continue
|
| 4635 |
+
if not all(fk[k] <= rk.get(k, 0) for k in fk):
|
| 4636 |
+
continue
|
| 4637 |
+
drop_starts.add(mi.start())
|
| 4638 |
+
log.info(
|
| 4639 |
+
"Fix FH-1: dropped DA3 (%d rows) duplicate of later "
|
| 4640 |
+
"DEPOSIT/WITHDRAWAL register (%d wd rows)",
|
| 4641 |
+
n_frag,
|
| 4642 |
+
n_reg,
|
| 4643 |
+
)
|
| 4644 |
+
break
|
| 4645 |
+
|
| 4646 |
+
if not drop_starts:
|
| 4647 |
+
return text
|
| 4648 |
+
|
| 4649 |
+
out: List[str] = []
|
| 4650 |
+
last = 0
|
| 4651 |
+
for m in matches:
|
| 4652 |
+
out.append(text[last : m.start()])
|
| 4653 |
+
if m.start() not in drop_starts:
|
| 4654 |
+
out.append(m.group(0))
|
| 4655 |
+
last = m.end()
|
| 4656 |
+
out.append(text[last:])
|
| 4657 |
+
return "".join(out)
|
| 4658 |
+
except Exception as e:
|
| 4659 |
+
log.warning("_drop_da3_duplicate_of_later_deposit_withdrawal_register failed: %s", e)
|
| 4660 |
+
return text
|
| 4661 |
+
|
| 4662 |
+
|
| 4663 |
def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
| 4664 |
if not text or "<table" not in text.lower():
|
| 4665 |
return text
|
|
|
|
| 5514 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5515 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 5516 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 5517 |
+
stabilized = _drop_da3_duplicate_of_later_deposit_withdrawal_register(stabilized) # Fix FH-1
|
| 5518 |
stabilized = _dedupe_da3_by_date_amount_loose(stabilized) # Fix 30
|
| 5519 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 5520 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
|
|
| 5652 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 5653 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
| 5654 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 5655 |
+
stabilized = _drop_da3_duplicate_of_later_deposit_withdrawal_register(stabilized) # Fix FH-1
|
| 5656 |
stabilized = _dedupe_da3_by_date_amount_loose(stabilized) # Fix 30
|
| 5657 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 5658 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
|
|
| 5735 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 5736 |
merged = _patch_da3_msft_g_ref_amounts_across_tables(merged)
|
| 5737 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 5738 |
+
merged = _drop_da3_duplicate_of_later_deposit_withdrawal_register(merged) # Fix FH-1
|
| 5739 |
merged = _dedupe_da3_by_date_amount_loose(merged) # Fix 30
|
| 5740 |
merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
|
| 5741 |
merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
|