Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -108,6 +108,11 @@ Primary goals for reconcile rate:
|
|
| 108 |
earlier larger DA3 table — catches OCR-truncated description duplicates such as
|
| 109 |
the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
|
| 110 |
full service-fee table with a longer description string.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"""
|
| 112 |
|
| 113 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -565,6 +570,156 @@ def _merge_blank_header_text_columns(grid):
|
|
| 565 |
new_grid.append([cell for idx, cell in enumerate(row) if idx < len(keep) and keep[idx]])
|
| 566 |
return new_grid
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
# --------------------------
|
| 569 |
# Shared keyword definitions
|
| 570 |
# --------------------------
|
|
@@ -2652,6 +2807,9 @@ def normalize_html_tables(text: str) -> str:
|
|
| 2652 |
grid = _drop_truly_empty_columns(grid)
|
| 2653 |
grid = _merge_blank_header_text_columns(grid)
|
| 2654 |
|
|
|
|
|
|
|
|
|
|
| 2655 |
grid = _reconstruct_separator_rows(grid)
|
| 2656 |
|
| 2657 |
grid, recovered_row = _clean_header_artifacts(grid)
|
|
|
|
| 108 |
earlier larger DA3 table — catches OCR-truncated description duplicates such as
|
| 109 |
the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
|
| 110 |
full service-fee table with a longer description string.
|
| 111 |
+
- Fix 32: Headless amount column — PDFs often leave the rightmost money column without a header;
|
| 112 |
+
OCR emits an empty <th>. Fill with \"Amount\" when the column is mostly currency.
|
| 113 |
+
Second pass: collapse Date|Transaction Description|Additions|Subtractions + empty 4th
|
| 114 |
+
when OCR splits \"Preauth Debit\" / \"Pre-Auth Credit\" into its own column before
|
| 115 |
+
the merchant text (East West–style) — merge back to Date|Description|Amount.
|
| 116 |
"""
|
| 117 |
|
| 118 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 570 |
new_grid.append([cell for idx, cell in enumerate(row) if idx < len(keep) and keep[idx]])
|
| 571 |
return new_grid
|
| 572 |
|
| 573 |
+
|
| 574 |
+
def _fill_headless_trailing_amount_headers(grid):
|
| 575 |
+
"""
|
| 576 |
+
Fix 32a: PDF layouts often omit a header above the rightmost amount column; OCR leaves a
|
| 577 |
+
trailing empty <th>. Downstream parsers then mis-associate columns. Fill only when the
|
| 578 |
+
column is predominantly strict currency (generic, bank-agnostic).
|
| 579 |
+
"""
|
| 580 |
+
if not grid or len(grid) < 2:
|
| 581 |
+
return grid
|
| 582 |
+
|
| 583 |
+
header = [str(c or "").strip() for c in grid[0]]
|
| 584 |
+
ncols = len(header)
|
| 585 |
+
if ncols < 2:
|
| 586 |
+
return grid
|
| 587 |
+
|
| 588 |
+
i = ncols - 1
|
| 589 |
+
while i >= 0 and header[i] == "":
|
| 590 |
+
i -= 1
|
| 591 |
+
empty_tail_indices = list(range(i + 1, ncols))
|
| 592 |
+
if not empty_tail_indices:
|
| 593 |
+
return grid
|
| 594 |
+
|
| 595 |
+
body = grid[1:]
|
| 596 |
+
new_header = list(header)
|
| 597 |
+
|
| 598 |
+
for j in empty_tail_indices:
|
| 599 |
+
total = 0
|
| 600 |
+
amt_hits = 0
|
| 601 |
+
for row in body:
|
| 602 |
+
if j >= len(row):
|
| 603 |
+
continue
|
| 604 |
+
total += 1
|
| 605 |
+
v = str(row[j] or "").strip()
|
| 606 |
+
if v and _is_amount_like(v):
|
| 607 |
+
amt_hits += 1
|
| 608 |
+
if total == 0:
|
| 609 |
+
continue
|
| 610 |
+
if (amt_hits / total) < 0.45:
|
| 611 |
+
continue
|
| 612 |
+
|
| 613 |
+
# Right-align column is the numeric amount; PDF often omits a label for it.
|
| 614 |
+
new_header[j] = "Amount"
|
| 615 |
+
|
| 616 |
+
grid[0] = new_header
|
| 617 |
+
return grid
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
_MMDD_DATE_RE = re.compile(r"^\d{1,2}[-/]\d{2}(?:[-/]\d{2,4})?$")
|
| 621 |
+
|
| 622 |
+
# Leading transaction-type cell when OCR splits category vs merchant (East West / similar).
|
| 623 |
+
_STACKED_TXN_TYPE_RE = re.compile(
|
| 624 |
+
r"^(?:Pre-Auth Credit|Pre-Auth Debit|Preauth Debit|"
|
| 625 |
+
r"Withdrawal Trns|Deposit(?:\s|$)|Outgoing Wire|Incoming Wire|"
|
| 626 |
+
r"Service Charge|ACH Debit|ACH Credit|ACH Payment|ACH PMT|"
|
| 627 |
+
r"Wire Transfer|POS Purchase|ATM Withdrawal|Transfer Trns)\b",
|
| 628 |
+
re.IGNORECASE | re.DOTALL,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def _collapse_four_col_stacked_transaction_type_to_three(grid):
|
| 633 |
+
"""
|
| 634 |
+
Fix 32b: When the PDF is logically Date | one description | amount, OCR sometimes emits
|
| 635 |
+
four columns: Date | short type (e.g. Preauth Debit) | merchant tail | amount, with an
|
| 636 |
+
optional empty 4th header cell. Collapse to three columns so markdown matches the PDF row
|
| 637 |
+
semantics. Requires every non-empty data row to match the pattern (safe no-op otherwise).
|
| 638 |
+
"""
|
| 639 |
+
if not grid or len(grid) < 3:
|
| 640 |
+
return grid
|
| 641 |
+
|
| 642 |
+
header = [str(c or "").strip() for c in grid[0]]
|
| 643 |
+
if len(header) != 4:
|
| 644 |
+
return grid
|
| 645 |
+
|
| 646 |
+
h0 = re.sub(r"\s+", " ", header[0]).strip().lower()
|
| 647 |
+
h1 = re.sub(r"\s+", " ", header[1]).strip().lower()
|
| 648 |
+
h2 = re.sub(r"\s+", " ", header[2]).strip().lower()
|
| 649 |
+
h3 = str(header[3] or "").strip().lower()
|
| 650 |
+
|
| 651 |
+
if h0 != "date":
|
| 652 |
+
return grid
|
| 653 |
+
if "transaction" not in h1 or "description" not in h1:
|
| 654 |
+
return grid
|
| 655 |
+
if not any(
|
| 656 |
+
k in h2
|
| 657 |
+
for k in (
|
| 658 |
+
"addition",
|
| 659 |
+
"subtraction",
|
| 660 |
+
"credit",
|
| 661 |
+
"debit",
|
| 662 |
+
"deposit",
|
| 663 |
+
"withdrawal",
|
| 664 |
+
)
|
| 665 |
+
):
|
| 666 |
+
return grid
|
| 667 |
+
if h3 not in ("", "amount") and not any(
|
| 668 |
+
k in h3 for k in ("addition", "subtraction", "credit", "debit")
|
| 669 |
+
):
|
| 670 |
+
return grid
|
| 671 |
+
|
| 672 |
+
def _row_matches_split_pattern(cells):
|
| 673 |
+
while len(cells) < 4:
|
| 674 |
+
cells = cells + [""]
|
| 675 |
+
c0, c1, c2, c3 = (
|
| 676 |
+
str(cells[0] or "").strip(),
|
| 677 |
+
str(cells[1] or "").strip(),
|
| 678 |
+
str(cells[2] or "").strip(),
|
| 679 |
+
str(cells[3] or "").strip(),
|
| 680 |
+
)
|
| 681 |
+
if not c0 or not _MMDD_DATE_RE.match(c0):
|
| 682 |
+
return False
|
| 683 |
+
if not c1 or not _STACKED_TXN_TYPE_RE.match(c1):
|
| 684 |
+
return False
|
| 685 |
+
if not c2:
|
| 686 |
+
return False
|
| 687 |
+
if _is_amount_like(c2):
|
| 688 |
+
return False
|
| 689 |
+
if not _is_amount_like(c3):
|
| 690 |
+
return False
|
| 691 |
+
return True
|
| 692 |
+
|
| 693 |
+
data_rows = [r for r in grid[1:] if any(str(c or "").strip() for c in r)]
|
| 694 |
+
if len(data_rows) < 2:
|
| 695 |
+
return grid
|
| 696 |
+
|
| 697 |
+
for row in data_rows:
|
| 698 |
+
cells = [str(c or "").strip() for c in row]
|
| 699 |
+
if not _row_matches_split_pattern(cells):
|
| 700 |
+
return grid
|
| 701 |
+
|
| 702 |
+
out = [
|
| 703 |
+
[
|
| 704 |
+
header[0],
|
| 705 |
+
header[1],
|
| 706 |
+
header[2],
|
| 707 |
+
]
|
| 708 |
+
]
|
| 709 |
+
for row in grid[1:]:
|
| 710 |
+
cells = [str(c or "").strip() for c in row]
|
| 711 |
+
while len(cells) < 4:
|
| 712 |
+
cells.append("")
|
| 713 |
+
if not any(cells):
|
| 714 |
+
out.append(["", "", ""])
|
| 715 |
+
continue
|
| 716 |
+
d0, t1, tail, amt = cells[0], cells[1], cells[2], cells[3]
|
| 717 |
+
desc = (t1 + " " + tail).strip()
|
| 718 |
+
out.append([d0, desc, amt])
|
| 719 |
+
|
| 720 |
+
return out
|
| 721 |
+
|
| 722 |
+
|
| 723 |
# --------------------------
|
| 724 |
# Shared keyword definitions
|
| 725 |
# --------------------------
|
|
|
|
| 2807 |
grid = _drop_truly_empty_columns(grid)
|
| 2808 |
grid = _merge_blank_header_text_columns(grid)
|
| 2809 |
|
| 2810 |
+
grid = _fill_headless_trailing_amount_headers(grid)
|
| 2811 |
+
grid = _collapse_four_col_stacked_transaction_type_to_three(grid)
|
| 2812 |
+
|
| 2813 |
grid = _reconstruct_separator_rows(grid)
|
| 2814 |
|
| 2815 |
grid, recovered_row = _clean_header_artifacts(grid)
|