Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2470,20 +2470,29 @@ def _extract_nfcu_summary_table(pdf_path: str, page_num: int) -> str:
|
|
| 2470 |
|
| 2471 |
anchor_idx = None
|
| 2472 |
for i, (_, _, text) in enumerate(lines):
|
| 2473 |
-
|
|
|
|
| 2474 |
anchor_idx = i
|
| 2475 |
break
|
| 2476 |
if anchor_idx is None:
|
| 2477 |
return ""
|
| 2478 |
|
| 2479 |
-
money_re = re.compile(r"^[\d,]+\.\d{2}-?$")
|
| 2480 |
-
|
| 2481 |
stop_re = re.compile(
|
| 2482 |
r"(business checking\s*-|mbr business savings\s*-|date\s+transaction\s+detail|items paid|checking|savings)",
|
| 2483 |
re.IGNORECASE,
|
| 2484 |
)
|
| 2485 |
|
|
|
|
|
|
|
| 2486 |
rows = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2487 |
for _, toks, text in lines[anchor_idx + 1:]:
|
| 2488 |
if not text:
|
| 2489 |
continue
|
|
@@ -2491,35 +2500,51 @@ def _extract_nfcu_summary_table(pdf_path: str, page_num: int) -> str:
|
|
| 2491 |
break
|
| 2492 |
if not toks:
|
| 2493 |
continue
|
| 2494 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2495 |
continue
|
| 2496 |
|
| 2497 |
-
|
| 2498 |
-
|
| 2499 |
-
|
| 2500 |
-
desc_tokens = [t for t in rest if not money_re.match(t)]
|
| 2501 |
-
desc = " ".join(desc_tokens).strip()
|
| 2502 |
-
if not desc:
|
| 2503 |
continue
|
| 2504 |
|
| 2505 |
-
|
| 2506 |
-
|
| 2507 |
-
if len(mvals) >= 2:
|
| 2508 |
-
amount = mvals[0]
|
| 2509 |
-
balance = mvals[-1]
|
| 2510 |
-
elif len(mvals) == 1:
|
| 2511 |
-
# Beginning/ending balance rows usually carry the number in Balance.
|
| 2512 |
-
if re.search(r"\b(beginning|ending)\s+balance\b", desc, re.IGNORECASE):
|
| 2513 |
-
balance = mvals[0]
|
| 2514 |
-
else:
|
| 2515 |
-
amount = mvals[0]
|
| 2516 |
|
| 2517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2518 |
|
| 2519 |
if not rows:
|
| 2520 |
return ""
|
| 2521 |
|
| 2522 |
-
hdr = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2523 |
return _grid_to_html([hdr] + rows)
|
| 2524 |
except Exception:
|
| 2525 |
return ""
|
|
@@ -2543,8 +2568,7 @@ def _replace_nfcu_summary_table(page_md: str, summary_table_html: str) -> str:
|
|
| 2543 |
return page_md.replace(old_table, summary_table_html, 1)
|
| 2544 |
|
| 2545 |
# Fallback path: heading may be malformed/absent in OCR text.
|
| 2546 |
-
# For NFCU
|
| 2547 |
-
# Date + Transaction + Detail + Amount + Balance headers.
|
| 2548 |
for m in table_pattern.finditer(page_md):
|
| 2549 |
tbl = m.group(0)
|
| 2550 |
p = TableGridParser()
|
|
@@ -2554,16 +2578,58 @@ def _replace_nfcu_summary_table(page_md: str, summary_table_html: str) -> str:
|
|
| 2554 |
continue
|
| 2555 |
h = " ".join(str(c or "").strip().lower() for c in g[0])
|
| 2556 |
if (
|
| 2557 |
-
"
|
| 2558 |
-
and "
|
| 2559 |
-
and "
|
| 2560 |
-
and "
|
| 2561 |
-
and "
|
| 2562 |
):
|
| 2563 |
return page_md.replace(tbl, summary_table_html, 1)
|
| 2564 |
|
| 2565 |
return page_md
|
| 2566 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2567 |
def normalize_html_tables(text: str) -> str:
|
| 2568 |
"""
|
| 2569 |
For every <table>...</table>:
|
|
@@ -2612,6 +2678,9 @@ def normalize_html_tables(text: str) -> str:
|
|
| 2612 |
# Fix 2: misplaced header row promotion (keyword-score guard — safe on clean PDFs)
|
| 2613 |
grid = _promote_misplaced_header_row(grid)
|
| 2614 |
|
|
|
|
|
|
|
|
|
|
| 2615 |
# Fix 8: split fused multi-keyword header cell (First Horizon Bank)
|
| 2616 |
grid = _split_fused_multicolumn_header(grid)
|
| 2617 |
|
|
|
|
| 2470 |
|
| 2471 |
anchor_idx = None
|
| 2472 |
for i, (_, _, text) in enumerate(lines):
|
| 2473 |
+
tl = text.lower()
|
| 2474 |
+
if "summary of your deposit accounts" in tl or ("summary" in tl and "deposit accounts" in tl):
|
| 2475 |
anchor_idx = i
|
| 2476 |
break
|
| 2477 |
if anchor_idx is None:
|
| 2478 |
return ""
|
| 2479 |
|
| 2480 |
+
money_re = re.compile(r"^\$?[\d,]+\.\d{2}-?$")
|
| 2481 |
+
acct_no_re = re.compile(r"^\d{8,12}$")
|
| 2482 |
stop_re = re.compile(
|
| 2483 |
r"(business checking\s*-|mbr business savings\s*-|date\s+transaction\s+detail|items paid|checking|savings)",
|
| 2484 |
re.IGNORECASE,
|
| 2485 |
)
|
| 2486 |
|
| 2487 |
+
# NFCU summary table real columns from PDF:
|
| 2488 |
+
# Account | Previous Balance | Deposits/Credits | Withdrawals/Debits | Ending Balance | YTD Dividends
|
| 2489 |
rows = []
|
| 2490 |
+
pending_account = ""
|
| 2491 |
+
account_name_re = re.compile(
|
| 2492 |
+
r"^(business checking|mbr business savings|checking|savings|money market|certificate).*$",
|
| 2493 |
+
re.IGNORECASE,
|
| 2494 |
+
)
|
| 2495 |
+
|
| 2496 |
for _, toks, text in lines[anchor_idx + 1:]:
|
| 2497 |
if not text:
|
| 2498 |
continue
|
|
|
|
| 2500 |
break
|
| 2501 |
if not toks:
|
| 2502 |
continue
|
| 2503 |
+
t0 = toks[0].lower()
|
| 2504 |
+
tl = text.lower()
|
| 2505 |
+
|
| 2506 |
+
# Ignore split header lines ("Previous ...", "Balance Credits ...")
|
| 2507 |
+
if (
|
| 2508 |
+
"previous" in tl
|
| 2509 |
+
or "deposits" in tl
|
| 2510 |
+
or "withdrawals" in tl
|
| 2511 |
+
or "ending" in tl
|
| 2512 |
+
or "dividends" in tl
|
| 2513 |
+
) and not any(money_re.match(t) for t in toks):
|
| 2514 |
continue
|
| 2515 |
|
| 2516 |
+
# Account name can appear on its own line, followed by acct# + numeric columns.
|
| 2517 |
+
if account_name_re.match(text.strip()) and not any(money_re.match(t) for t in toks):
|
| 2518 |
+
pending_account = text.strip()
|
|
|
|
|
|
|
|
|
|
| 2519 |
continue
|
| 2520 |
|
| 2521 |
+
mvals = [t.replace("$", "") for t in toks if money_re.match(t)]
|
| 2522 |
+
acct_tokens = [t for t in toks if acct_no_re.match(t)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2523 |
|
| 2524 |
+
# Totals row: "Totals <five amounts>"
|
| 2525 |
+
if t0 == "totals" and len(mvals) >= 5:
|
| 2526 |
+
rows.append(["Totals", mvals[0], mvals[1], mvals[2], mvals[3], mvals[4]])
|
| 2527 |
+
continue
|
| 2528 |
+
|
| 2529 |
+
# Account numeric row: "<acctno> <five amounts>"
|
| 2530 |
+
if acct_tokens and len(mvals) >= 5:
|
| 2531 |
+
acct = acct_tokens[0]
|
| 2532 |
+
acct_label = (pending_account + " - " + acct).strip(" -") if pending_account else acct
|
| 2533 |
+
rows.append([acct_label, mvals[0], mvals[1], mvals[2], mvals[3], mvals[4]])
|
| 2534 |
+
pending_account = ""
|
| 2535 |
+
continue
|
| 2536 |
|
| 2537 |
if not rows:
|
| 2538 |
return ""
|
| 2539 |
|
| 2540 |
+
hdr = [
|
| 2541 |
+
"Account",
|
| 2542 |
+
"Previous Balance",
|
| 2543 |
+
"Deposits/Credits",
|
| 2544 |
+
"Withdrawals/Debits",
|
| 2545 |
+
"Ending Balance",
|
| 2546 |
+
"YTD Dividends",
|
| 2547 |
+
]
|
| 2548 |
return _grid_to_html([hdr] + rows)
|
| 2549 |
except Exception:
|
| 2550 |
return ""
|
|
|
|
| 2568 |
return page_md.replace(old_table, summary_table_html, 1)
|
| 2569 |
|
| 2570 |
# Fallback path: heading may be malformed/absent in OCR text.
|
| 2571 |
+
# For NFCU summary, header should contain balance/deposit/withdrawal/ytd signals.
|
|
|
|
| 2572 |
for m in table_pattern.finditer(page_md):
|
| 2573 |
tbl = m.group(0)
|
| 2574 |
p = TableGridParser()
|
|
|
|
| 2578 |
continue
|
| 2579 |
h = " ".join(str(c or "").strip().lower() for c in g[0])
|
| 2580 |
if (
|
| 2581 |
+
"previous" in h
|
| 2582 |
+
and "deposits" in h
|
| 2583 |
+
and "withdrawals" in h
|
| 2584 |
+
and "ending" in h
|
| 2585 |
+
and "dividends" in h
|
| 2586 |
):
|
| 2587 |
return page_md.replace(tbl, summary_table_html, 1)
|
| 2588 |
|
| 2589 |
return page_md
|
| 2590 |
|
| 2591 |
+
def _split_fused_date_transaction_header(grid):
|
| 2592 |
+
"""
|
| 2593 |
+
Fix fused header "Date Transaction Detail" -> separate Date + Transaction Detail.
|
| 2594 |
+
Strict guard: only when header has that exact fused phrase and also has
|
| 2595 |
+
Amount/Balance columns, so unrelated tables are untouched.
|
| 2596 |
+
"""
|
| 2597 |
+
if not grid or len(grid) < 2:
|
| 2598 |
+
return grid
|
| 2599 |
+
ncols = max(len(r) for r in grid if isinstance(r, list))
|
| 2600 |
+
if ncols < 3:
|
| 2601 |
+
return grid
|
| 2602 |
+
|
| 2603 |
+
header = [str(c or "").strip() for c in (grid[0] + [""] * (ncols - len(grid[0])))]
|
| 2604 |
+
first_h = re.sub(r"\s+", " ", header[0]).strip().lower()
|
| 2605 |
+
if first_h != "date transaction detail":
|
| 2606 |
+
return grid
|
| 2607 |
+
|
| 2608 |
+
hnorm = [re.sub(r"\s+", " ", str(c or "").strip().lower()) for c in header]
|
| 2609 |
+
has_amount = any("amount" in h for h in hnorm)
|
| 2610 |
+
has_balance = any("balance" in h for h in hnorm)
|
| 2611 |
+
if not (has_amount and has_balance):
|
| 2612 |
+
return grid
|
| 2613 |
+
|
| 2614 |
+
date_tok = re.compile(r"^\d{1,2}(?:[-/])\d{2}(?:[-/]\d{2,4})?$")
|
| 2615 |
+
|
| 2616 |
+
new_grid = []
|
| 2617 |
+
new_header = ["Date", "Transaction Detail"] + header[1:]
|
| 2618 |
+
new_grid.append(new_header)
|
| 2619 |
+
|
| 2620 |
+
for row in grid[1:]:
|
| 2621 |
+
cells = [str(c or "").strip() for c in (row + [""] * (ncols - len(row)))]
|
| 2622 |
+
first = cells[0]
|
| 2623 |
+
dval = ""
|
| 2624 |
+
tval = first
|
| 2625 |
+
if first:
|
| 2626 |
+
parts = first.split()
|
| 2627 |
+
if parts and date_tok.match(parts[0]):
|
| 2628 |
+
dval = parts[0]
|
| 2629 |
+
tval = " ".join(parts[1:]).strip()
|
| 2630 |
+
new_grid.append([dval, tval] + cells[1:])
|
| 2631 |
+
return new_grid
|
| 2632 |
+
|
| 2633 |
def normalize_html_tables(text: str) -> str:
|
| 2634 |
"""
|
| 2635 |
For every <table>...</table>:
|
|
|
|
| 2678 |
# Fix 2: misplaced header row promotion (keyword-score guard — safe on clean PDFs)
|
| 2679 |
grid = _promote_misplaced_header_row(grid)
|
| 2680 |
|
| 2681 |
+
# Fix NF-2: split fused "Date Transaction Detail" header/cell.
|
| 2682 |
+
grid = _split_fused_date_transaction_header(grid)
|
| 2683 |
+
|
| 2684 |
# Fix 8: split fused multi-keyword header cell (First Horizon Bank)
|
| 2685 |
grid = _split_fused_multicolumn_header(grid)
|
| 2686 |
|