Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2422,6 +2422,148 @@ def _parse_nfcu_page(pdf_path: str, page_num: int) -> str:
|
|
| 2422 |
log.warning("_parse_nfcu_page failed (page %d): %s", page_num, e)
|
| 2423 |
return ""
|
| 2424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2425 |
def normalize_html_tables(text: str) -> str:
|
| 2426 |
"""
|
| 2427 |
For every <table>...</table>:
|
|
@@ -2638,6 +2780,12 @@ def run_ocr(uploaded_file):
|
|
| 2638 |
# Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
|
| 2639 |
if is_pdf and not is_nfcu_doc:
|
| 2640 |
stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2641 |
|
| 2642 |
# Fix 4D: append Johnson Bank Checks + Daily Account Balance
|
| 2643 |
# Run OUTSIDE _patch_ocr_with_textlayer so exceptions there don't block it.
|
|
|
|
| 2422 |
log.warning("_parse_nfcu_page failed (page %d): %s", page_num, e)
|
| 2423 |
return ""
|
| 2424 |
|
| 2425 |
+
def _extract_nfcu_summary_table(pdf_path: str, page_num: int) -> str:
|
| 2426 |
+
"""
|
| 2427 |
+
Navy-only additive helper:
|
| 2428 |
+
Rebuild "Summary of your deposit accounts" from PDF text-layer words.
|
| 2429 |
+
Returns a clean HTML table or "" (safe no-op on any miss/error).
|
| 2430 |
+
"""
|
| 2431 |
+
try:
|
| 2432 |
+
import pymupdf as fitz
|
| 2433 |
+
|
| 2434 |
+
doc = fitz.open(pdf_path)
|
| 2435 |
+
if page_num >= len(doc):
|
| 2436 |
+
doc.close()
|
| 2437 |
+
return ""
|
| 2438 |
+
page = doc[page_num]
|
| 2439 |
+
words = page.get_text("words") or []
|
| 2440 |
+
doc.close()
|
| 2441 |
+
if not words:
|
| 2442 |
+
return ""
|
| 2443 |
+
|
| 2444 |
+
# Group tokens into reading lines by y-position.
|
| 2445 |
+
buckets = []
|
| 2446 |
+
for w in words:
|
| 2447 |
+
if len(w) < 5:
|
| 2448 |
+
continue
|
| 2449 |
+
x0 = float(w[0])
|
| 2450 |
+
y0 = float(w[1])
|
| 2451 |
+
tok = str(w[4] or "").strip()
|
| 2452 |
+
if not tok:
|
| 2453 |
+
continue
|
| 2454 |
+
bi = None
|
| 2455 |
+
for i, (yb, _) in enumerate(buckets):
|
| 2456 |
+
if abs(yb - y0) <= 2.0:
|
| 2457 |
+
bi = i
|
| 2458 |
+
break
|
| 2459 |
+
if bi is None:
|
| 2460 |
+
buckets.append((y0, [(x0, tok)]))
|
| 2461 |
+
else:
|
| 2462 |
+
buckets[bi][1].append((x0, tok))
|
| 2463 |
+
|
| 2464 |
+
lines = []
|
| 2465 |
+
for y, arr in sorted(buckets, key=lambda x: x[0]):
|
| 2466 |
+
arr2 = sorted(arr, key=lambda x: x[0])
|
| 2467 |
+
toks = [t for _, t in arr2]
|
| 2468 |
+
text = " ".join(toks).strip()
|
| 2469 |
+
lines.append((y, toks, text))
|
| 2470 |
+
|
| 2471 |
+
anchor_idx = None
|
| 2472 |
+
for i, (_, _, text) in enumerate(lines):
|
| 2473 |
+
if "summary of your deposit accounts" in text.lower():
|
| 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 |
+
date_re = re.compile(r"^\d{2}-\d{2}$")
|
| 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
|
| 2490 |
+
if stop_re.search(text):
|
| 2491 |
+
break
|
| 2492 |
+
if not toks:
|
| 2493 |
+
continue
|
| 2494 |
+
if not date_re.match(toks[0]):
|
| 2495 |
+
continue
|
| 2496 |
+
|
| 2497 |
+
date = toks[0]
|
| 2498 |
+
rest = toks[1:]
|
| 2499 |
+
mvals = [t for t in rest if money_re.match(t)]
|
| 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 |
+
amount = ""
|
| 2506 |
+
balance = ""
|
| 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 |
+
rows.append([date, desc, "", amount, balance])
|
| 2518 |
+
|
| 2519 |
+
if not rows:
|
| 2520 |
+
return ""
|
| 2521 |
+
|
| 2522 |
+
hdr = ["Date", "Transaction", "Detail", "Amount($)", "Balance($)"]
|
| 2523 |
+
return _grid_to_html([hdr] + rows)
|
| 2524 |
+
except Exception:
|
| 2525 |
+
return ""
|
| 2526 |
+
|
| 2527 |
+
def _replace_nfcu_summary_table(page_md: str, summary_table_html: str) -> str:
|
| 2528 |
+
"""
|
| 2529 |
+
Replace the first table after 'Summary of your deposit accounts' with
|
| 2530 |
+
a rebuilt NFCU summary table. Safe no-op if anchor/table is missing.
|
| 2531 |
+
"""
|
| 2532 |
+
if not page_md or not summary_table_html:
|
| 2533 |
+
return page_md
|
| 2534 |
+
table_pattern = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 2535 |
+
lower = page_md.lower()
|
| 2536 |
+
anchor = lower.find("summary of your deposit accounts")
|
| 2537 |
+
|
| 2538 |
+
# Primary path: replace first table after the summary heading.
|
| 2539 |
+
if anchor >= 0:
|
| 2540 |
+
for m in table_pattern.finditer(page_md):
|
| 2541 |
+
if m.start() > anchor:
|
| 2542 |
+
old_table = m.group(0)
|
| 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, summary table is the 5-column table with
|
| 2547 |
+
# Date + Transaction + Detail + Amount + Balance headers.
|
| 2548 |
+
for m in table_pattern.finditer(page_md):
|
| 2549 |
+
tbl = m.group(0)
|
| 2550 |
+
p = TableGridParser()
|
| 2551 |
+
p.feed(tbl)
|
| 2552 |
+
g = _build_grid(p.rows)
|
| 2553 |
+
if not g:
|
| 2554 |
+
continue
|
| 2555 |
+
h = " ".join(str(c or "").strip().lower() for c in g[0])
|
| 2556 |
+
if (
|
| 2557 |
+
"date" in h
|
| 2558 |
+
and "transaction" in h
|
| 2559 |
+
and "detail" in h
|
| 2560 |
+
and "amount" in h
|
| 2561 |
+
and "balance" in h
|
| 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>:
|
|
|
|
| 2780 |
# Safe no-op for scanned PDFs (no text layer) and non-transaction pages.
|
| 2781 |
if is_pdf and not is_nfcu_doc:
|
| 2782 |
stabilized = _patch_ocr_with_textlayer(stabilized, path, page_num)
|
| 2783 |
+
elif is_pdf and is_nfcu_doc:
|
| 2784 |
+
# Navy-only additive fix: rebuild malformed summary table from
|
| 2785 |
+
# text layer, but keep GLM body/header/footer unchanged.
|
| 2786 |
+
nfcu_summary = _extract_nfcu_summary_table(path, page_num)
|
| 2787 |
+
if nfcu_summary:
|
| 2788 |
+
stabilized = _replace_nfcu_summary_table(stabilized, nfcu_summary)
|
| 2789 |
|
| 2790 |
# Fix 4D: append Johnson Bank Checks + Daily Account Balance
|
| 2791 |
# Run OUTSIDE _patch_ocr_with_textlayer so exceptions there don't block it.
|