Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2643,6 +2643,88 @@ def _split_fused_date_transaction_header(grid):
|
|
| 2643 |
new_grid.append(split_row)
|
| 2644 |
return new_grid
|
| 2645 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2646 |
def normalize_html_tables(text: str) -> str:
|
| 2647 |
"""
|
| 2648 |
For every <table>...</table>:
|
|
@@ -2694,6 +2776,10 @@ def normalize_html_tables(text: str) -> str:
|
|
| 2694 |
# Fix NF-2: split fused "Date Transaction Detail" header/cell.
|
| 2695 |
grid = _split_fused_date_transaction_header(grid)
|
| 2696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2697 |
# Fix 8: split fused multi-keyword header cell (First Horizon Bank)
|
| 2698 |
grid = _split_fused_multicolumn_header(grid)
|
| 2699 |
|
|
|
|
| 2643 |
new_grid.append(split_row)
|
| 2644 |
return new_grid
|
| 2645 |
|
| 2646 |
+
def _normalize_fused_date_posted_amount_table(grid):
|
| 2647 |
+
"""
|
| 2648 |
+
Normalize 2-column OCR tables with fused header like:
|
| 2649 |
+
col1: "Date posted Transaction description [Reference number]"
|
| 2650 |
+
col2: "Amount"
|
| 2651 |
+
into stable 3-column transaction layout:
|
| 2652 |
+
Date | Transaction Description | Amount
|
| 2653 |
+
Strict guard: only activates on that specific fused-header signature.
|
| 2654 |
+
"""
|
| 2655 |
+
if not grid or len(grid) < 2:
|
| 2656 |
+
return grid
|
| 2657 |
+
|
| 2658 |
+
ncols = max(len(r) for r in grid if isinstance(r, list))
|
| 2659 |
+
if ncols < 2:
|
| 2660 |
+
return grid
|
| 2661 |
+
|
| 2662 |
+
def _row_cells(r):
|
| 2663 |
+
return [str(c or "").strip() for c in (r + [""] * (ncols - len(r)))]
|
| 2664 |
+
|
| 2665 |
+
hdr_idx = None
|
| 2666 |
+
for ri in range(min(6, len(grid))):
|
| 2667 |
+
cells = _row_cells(grid[ri])
|
| 2668 |
+
row_text = " ".join(cells).lower()
|
| 2669 |
+
if (
|
| 2670 |
+
"date posted transaction description" in row_text
|
| 2671 |
+
and "amount" in row_text
|
| 2672 |
+
):
|
| 2673 |
+
hdr_idx = ri
|
| 2674 |
+
break
|
| 2675 |
+
if hdr_idx is None:
|
| 2676 |
+
return grid
|
| 2677 |
+
|
| 2678 |
+
date_re = re.compile(r"^\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?$")
|
| 2679 |
+
out = []
|
| 2680 |
+
|
| 2681 |
+
for ri, row in enumerate(grid):
|
| 2682 |
+
cells = _row_cells(row)
|
| 2683 |
+
if ri == hdr_idx:
|
| 2684 |
+
out.append(["Date", "Transaction Description", "Amount"])
|
| 2685 |
+
continue
|
| 2686 |
+
if ri < hdr_idx:
|
| 2687 |
+
# Keep pre-header text as non-transaction row.
|
| 2688 |
+
joined = " ".join(c for c in cells if c).strip()
|
| 2689 |
+
out.append(["", joined, ""])
|
| 2690 |
+
continue
|
| 2691 |
+
|
| 2692 |
+
c1 = cells[0].strip()
|
| 2693 |
+
c2 = cells[1].strip() if len(cells) > 1 else ""
|
| 2694 |
+
|
| 2695 |
+
# Skip duplicated inline sub-headers that often appear in fused OCR tables.
|
| 2696 |
+
lower_c1 = c1.lower()
|
| 2697 |
+
if (
|
| 2698 |
+
lower_c1.startswith("date posted transaction description")
|
| 2699 |
+
or lower_c1 in {"ach additions", "ach deductions", "other additions", "other deductions", "service charges and fees"}
|
| 2700 |
+
):
|
| 2701 |
+
continue
|
| 2702 |
+
|
| 2703 |
+
date = ""
|
| 2704 |
+
desc = c1
|
| 2705 |
+
if c1:
|
| 2706 |
+
parts = c1.split()
|
| 2707 |
+
if parts and date_re.match(parts[0]):
|
| 2708 |
+
date = parts[0]
|
| 2709 |
+
desc = " ".join(parts[1:]).strip()
|
| 2710 |
+
|
| 2711 |
+
# If amount is empty in column 2, try extracting trailing amount from description.
|
| 2712 |
+
amount = c2
|
| 2713 |
+
if not amount and desc:
|
| 2714 |
+
m = re.search(r"(-?\$?\d{1,3}(?:,\d{3})*(?:\.\d{2})-?)\s*$", desc)
|
| 2715 |
+
if m:
|
| 2716 |
+
amount = m.group(1)
|
| 2717 |
+
desc = desc[:m.start()].strip()
|
| 2718 |
+
|
| 2719 |
+
if not (date or desc or amount):
|
| 2720 |
+
continue
|
| 2721 |
+
out.append([date, desc, amount])
|
| 2722 |
+
|
| 2723 |
+
# Keep at least header + 1 row to avoid degrading clean tables on accidental match.
|
| 2724 |
+
if len(out) < 2:
|
| 2725 |
+
return grid
|
| 2726 |
+
return out
|
| 2727 |
+
|
| 2728 |
def normalize_html_tables(text: str) -> str:
|
| 2729 |
"""
|
| 2730 |
For every <table>...</table>:
|
|
|
|
| 2776 |
# Fix NF-2: split fused "Date Transaction Detail" header/cell.
|
| 2777 |
grid = _split_fused_date_transaction_header(grid)
|
| 2778 |
|
| 2779 |
+
# Fix 11: normalize fused "Date posted Transaction description ... / Amount"
|
| 2780 |
+
# two-column tables into stable Date/Description/Amount structure.
|
| 2781 |
+
grid = _normalize_fused_date_posted_amount_table(grid)
|
| 2782 |
+
|
| 2783 |
# Fix 8: split fused multi-keyword header cell (First Horizon Bank)
|
| 2784 |
grid = _split_fused_multicolumn_header(grid)
|
| 2785 |
|