Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -59,6 +59,12 @@ Primary goals for reconcile rate:
|
|
| 59 |
when the preceding DA3 table was under a deposits/credits heading. Only strips the
|
| 60 |
contiguous prefix of the debit table whose rows appear in the credit table; requires
|
| 61 |
≥1 data row to remain; entire function wrapped in try/except (safe no-op on any error).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
"""
|
| 63 |
|
| 64 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -3510,6 +3516,122 @@ def _strip_credits_prefix_from_debit_da3_table(text: str) -> str:
|
|
| 3510 |
return text
|
| 3511 |
|
| 3512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3513 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 3514 |
if not page_md:
|
| 3515 |
return page_md
|
|
@@ -3535,6 +3657,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 3535 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3536 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 3537 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
|
|
| 3538 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 3539 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 3540 |
return close_unclosed_html(stabilized)
|
|
@@ -3662,6 +3785,7 @@ def run_ocr(uploaded_file):
|
|
| 3662 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3663 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 3664 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
|
|
|
| 3665 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 3666 |
elif is_pdf and is_nfcu_doc:
|
| 3667 |
nfcu_summary = _extract_nfcu_summary_table(path, page_num)
|
|
@@ -3737,6 +3861,7 @@ def run_ocr(uploaded_file):
|
|
| 3737 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 3738 |
merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
|
| 3739 |
merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
|
|
|
|
| 3740 |
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 3741 |
merged = _strip_standalone_continued_banner_line(merged)
|
| 3742 |
return merged
|
|
|
|
| 59 |
when the preceding DA3 table was under a deposits/credits heading. Only strips the
|
| 60 |
contiguous prefix of the debit table whose rows appear in the credit table; requires
|
| 61 |
≥1 data row to remain; entire function wrapped in try/except (safe no-op on any error).
|
| 62 |
+
- Fix 29: Move a misplaced debit/withdrawal heading to before its orphan DA3 table when OCR
|
| 63 |
+
places the heading AFTER the table instead of before it. Pattern detected:
|
| 64 |
+
[credit heading+table] ... [orphan DA3 table] ... [debit heading] — the heading is
|
| 65 |
+
relocated to just before the orphan table. Guards: no debit heading already before
|
| 66 |
+
table; credit context must precede table; no other table between orphan and heading;
|
| 67 |
+
try/except wrapper (safe no-op on any error).
|
| 68 |
"""
|
| 69 |
|
| 70 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 3516 |
return text
|
| 3517 |
|
| 3518 |
|
| 3519 |
+
# --------------------------
|
| 3520 |
+
# Fix 29: Move misplaced debit heading to before its orphan DA3 table
|
| 3521 |
+
# --------------------------
|
| 3522 |
+
|
| 3523 |
+
# Centered div heading pattern (BoA / generic style)
|
| 3524 |
+
_CENTER_DIV_HDR_RE = re.compile(
|
| 3525 |
+
r'<div\s+align=["\']center["\']\s*>\s*(.*?)\s*</div>',
|
| 3526 |
+
re.IGNORECASE | re.DOTALL,
|
| 3527 |
+
)
|
| 3528 |
+
_HEADING_SCAN_FORWARD = 800 # chars after table end to search for misplaced heading
|
| 3529 |
+
|
| 3530 |
+
|
| 3531 |
+
def _fix29_move_debit_heading_before_orphan_table(text: str) -> str:
|
| 3532 |
+
"""
|
| 3533 |
+
Fix 29: When OCR places a debit/withdrawal section heading AFTER the withdrawals table
|
| 3534 |
+
instead of before it, move the heading to its correct position (just before the table).
|
| 3535 |
+
|
| 3536 |
+
Pattern detected:
|
| 3537 |
+
[credit/deposit DA3 table] ...no debit heading... [orphan DA3 table] [debit heading]
|
| 3538 |
+
where no other DA3 table separates the orphan table from the debit heading.
|
| 3539 |
+
|
| 3540 |
+
Action: move the debit heading to just before the orphan DA3 table.
|
| 3541 |
+
|
| 3542 |
+
Safety guards (ALL must pass):
|
| 3543 |
+
1. Orphan table must be a valid DA3 (Date|Description|Amount).
|
| 3544 |
+
2. A debit heading must appear AFTER the orphan table within _HEADING_SCAN_FORWARD chars.
|
| 3545 |
+
3. No debit heading appears immediately before the orphan table (within _HEADING_SCAN_WINDOW).
|
| 3546 |
+
4. A credit/deposit heading must exist before the orphan table (within 1200 chars).
|
| 3547 |
+
5. No other table appears between the orphan table end and the debit heading.
|
| 3548 |
+
6. Entire function wrapped in try/except -- returns input unchanged on any error.
|
| 3549 |
+
"""
|
| 3550 |
+
if not text or "<table" not in text.lower():
|
| 3551 |
+
return text
|
| 3552 |
+
try:
|
| 3553 |
+
tbl_pat = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 3554 |
+
matches = list(tbl_pat.finditer(text))
|
| 3555 |
+
if len(matches) < 2:
|
| 3556 |
+
return text
|
| 3557 |
+
|
| 3558 |
+
da3_matches = [m for m in matches if _parse_da3_grid_from_table_html(m.group(0))]
|
| 3559 |
+
if len(da3_matches) < 2:
|
| 3560 |
+
return text
|
| 3561 |
+
|
| 3562 |
+
moves = []
|
| 3563 |
+
|
| 3564 |
+
for i in range(1, len(da3_matches)):
|
| 3565 |
+
orphan_m = da3_matches[i]
|
| 3566 |
+
orphan_start = orphan_m.start()
|
| 3567 |
+
orphan_end = orphan_m.end()
|
| 3568 |
+
|
| 3569 |
+
# Guard 3: no debit heading already before the orphan table
|
| 3570 |
+
before_window = text[max(0, orphan_start - _HEADING_SCAN_WINDOW): orphan_start]
|
| 3571 |
+
before_clean = re.sub(r"<[^>]+>", " ", before_window)
|
| 3572 |
+
lines_before = [l.strip() for l in before_clean.splitlines() if l.strip()]
|
| 3573 |
+
last_meaningful = ""
|
| 3574 |
+
for l in reversed(lines_before):
|
| 3575 |
+
if len(l) > 3 and "---" not in l:
|
| 3576 |
+
last_meaningful = l
|
| 3577 |
+
break
|
| 3578 |
+
if _DEBIT_HEADING_KEYWORDS.search(last_meaningful):
|
| 3579 |
+
continue # already has debit heading before it
|
| 3580 |
+
|
| 3581 |
+
# Guard 4: credit context must precede the orphan table
|
| 3582 |
+
big_before = text[max(0, orphan_start - 1200): orphan_start]
|
| 3583 |
+
if not _CREDIT_HEADING_KEYWORDS.search(re.sub(r"<[^>]+>", " ", big_before)):
|
| 3584 |
+
continue
|
| 3585 |
+
|
| 3586 |
+
# Guard 5: no other table between orphan end and the potential debit heading
|
| 3587 |
+
next_tbl_m = tbl_pat.search(text, orphan_end)
|
| 3588 |
+
|
| 3589 |
+
# Guard 2: find debit heading after the orphan table (within forward window)
|
| 3590 |
+
dh_start = dh_end = None
|
| 3591 |
+
for div_m in _CENTER_DIV_HDR_RE.finditer(text, orphan_end, orphan_end + _HEADING_SCAN_FORWARD):
|
| 3592 |
+
div_clean = re.sub(r"<[^>]+>", "", div_m.group(1)).strip()
|
| 3593 |
+
if _DEBIT_HEADING_KEYWORDS.search(div_clean) and not _CREDIT_HEADING_KEYWORDS.search(div_clean):
|
| 3594 |
+
# Guard 5: no table should be between orphan_end and this heading
|
| 3595 |
+
if next_tbl_m and next_tbl_m.start() < div_m.start():
|
| 3596 |
+
break # another table intervenes
|
| 3597 |
+
dh_start = div_m.start()
|
| 3598 |
+
dh_end = div_m.end()
|
| 3599 |
+
break
|
| 3600 |
+
|
| 3601 |
+
if dh_start is None:
|
| 3602 |
+
continue
|
| 3603 |
+
|
| 3604 |
+
moves.append((orphan_start, dh_start, dh_end))
|
| 3605 |
+
|
| 3606 |
+
if not moves:
|
| 3607 |
+
return text
|
| 3608 |
+
|
| 3609 |
+
# Apply moves in reverse order to preserve offsets
|
| 3610 |
+
for orphan_start, dh_start, dh_end in reversed(moves):
|
| 3611 |
+
orphan_m2 = next((m for m in tbl_pat.finditer(text) if m.start() == orphan_start), None)
|
| 3612 |
+
if not orphan_m2:
|
| 3613 |
+
continue
|
| 3614 |
+
orphan_end = orphan_m2.end()
|
| 3615 |
+
|
| 3616 |
+
heading_html = text[dh_start: dh_end]
|
| 3617 |
+
gap = text[orphan_end: dh_start]
|
| 3618 |
+
orphan_html = text[orphan_start: orphan_end]
|
| 3619 |
+
part_before = text[:orphan_start]
|
| 3620 |
+
part_after = text[dh_end:]
|
| 3621 |
+
|
| 3622 |
+
text = part_before + heading_html + "\n\n" + orphan_html + gap.rstrip() + part_after
|
| 3623 |
+
log.info(
|
| 3624 |
+
"Fix 29: moved debit heading (was at offset %d) to before orphan DA3 table (offset %d)",
|
| 3625 |
+
dh_start, orphan_start,
|
| 3626 |
+
)
|
| 3627 |
+
|
| 3628 |
+
return text
|
| 3629 |
+
|
| 3630 |
+
except Exception as e:
|
| 3631 |
+
log.warning("_fix29_move_debit_heading_before_orphan_table failed: %s", e)
|
| 3632 |
+
return text
|
| 3633 |
+
|
| 3634 |
+
|
| 3635 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 3636 |
if not page_md:
|
| 3637 |
return page_md
|
|
|
|
| 3657 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3658 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 3659 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
| 3660 |
+
stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
|
| 3661 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 3662 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 3663 |
return close_unclosed_html(stabilized)
|
|
|
|
| 3785 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3786 |
stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
|
| 3787 |
stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
|
| 3788 |
+
stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
|
| 3789 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 3790 |
elif is_pdf and is_nfcu_doc:
|
| 3791 |
nfcu_summary = _extract_nfcu_summary_table(path, page_num)
|
|
|
|
| 3861 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 3862 |
merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
|
| 3863 |
merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
|
| 3864 |
+
merged = _fix29_move_debit_heading_before_orphan_table(merged) # Fix 29
|
| 3865 |
merged = _strip_orphan_continued_da3_flatline(merged)
|
| 3866 |
merged = _strip_standalone_continued_banner_line(merged)
|
| 3867 |
return merged
|