Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -733,6 +733,102 @@ def strip_degenerate_html_tables(md: str) -> str:
|
|
| 733 |
return re.sub(r"\n{3,}", "\n\n", out)
|
| 734 |
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
def _strip_trailing_money_tokens(rest: str, max_take: int = 2) -> Tuple[str, List[str]]:
|
| 737 |
"""Strip 1–2 currency tokens from the right of *rest*; return (description, tokens oldest-first)."""
|
| 738 |
cur = rest.rstrip()
|
|
@@ -904,6 +1000,8 @@ def light_stabilize_markdown(page_md: str) -> str:
|
|
| 904 |
else:
|
| 905 |
out_blocks.append(coalesce_loose_ledger_lines(b))
|
| 906 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
|
|
|
|
|
|
| 907 |
merged = repair_thead_cell_semantics(merged)
|
| 908 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 909 |
merged = strip_degenerate_html_tables(merged)
|
|
@@ -1445,6 +1543,8 @@ def run_ocr(uploaded_file):
|
|
| 1445 |
|
| 1446 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 1447 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
|
|
|
|
|
|
| 1448 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1449 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1450 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|
|
|
|
| 733 |
return re.sub(r"\n{3,}", "\n\n", out)
|
| 734 |
|
| 735 |
|
| 736 |
+
_LONG_NUM_TOKEN = re.compile(r"\b\d{7,}\b")
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
def mask_non_monetary_long_numbers(md: str) -> str:
|
| 740 |
+
"""
|
| 741 |
+
Prevent long reference/trace/account IDs from being interpreted as monetary
|
| 742 |
+
values by downstream transaction extraction.
|
| 743 |
+
"""
|
| 744 |
+
if not md:
|
| 745 |
+
return md
|
| 746 |
+
|
| 747 |
+
def repl(m: re.Match) -> str:
|
| 748 |
+
tok = m.group(0)
|
| 749 |
+
# Keep common money-like tokens (handled elsewhere with decimal cents).
|
| 750 |
+
if re.match(r"^\d+\.\d{2}$", tok):
|
| 751 |
+
return tok
|
| 752 |
+
# Keep YYYYMMDD-like date compact tokens.
|
| 753 |
+
if len(tok) == 8 and tok.startswith(("19", "20")):
|
| 754 |
+
return tok
|
| 755 |
+
return f"ID#{tok}"
|
| 756 |
+
|
| 757 |
+
return _LONG_NUM_TOKEN.sub(repl, md)
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
def directionalize_amount_headers(md: str) -> str:
|
| 761 |
+
"""
|
| 762 |
+
Convert ambiguous 'Amount' headers into 'Credit' or 'Debit' when table
|
| 763 |
+
context clearly indicates transaction direction (e.g. Deposits, Checks).
|
| 764 |
+
"""
|
| 765 |
+
if not md or "<table" not in md.lower():
|
| 766 |
+
return md
|
| 767 |
+
|
| 768 |
+
def map_header_token(label: str, ctx: str) -> str:
|
| 769 |
+
low = label.strip().lower()
|
| 770 |
+
if "amount" not in low:
|
| 771 |
+
return label
|
| 772 |
+
if re.search(r"\bcredit|deposit|deposits|incoming|received|interest earned|payment received\b", ctx):
|
| 773 |
+
return re.sub(r"amount", "Credit", label, flags=re.IGNORECASE)
|
| 774 |
+
if re.search(r"\bdebit|debits|withdrawal|withdrawals|check|checks|fee|fees|charge|charges|payment|payments\b", ctx):
|
| 775 |
+
return re.sub(r"amount", "Debit", label, flags=re.IGNORECASE)
|
| 776 |
+
return label
|
| 777 |
+
|
| 778 |
+
def repl_table(m: re.Match) -> str:
|
| 779 |
+
full = m.group(0)
|
| 780 |
+
# Table-local context from headers and first rows.
|
| 781 |
+
ctx = _cell_plain_text(full).lower()
|
| 782 |
+
changed = False
|
| 783 |
+
|
| 784 |
+
def repl_th(thm: re.Match) -> str:
|
| 785 |
+
nonlocal changed
|
| 786 |
+
tag = thm.group(0)
|
| 787 |
+
inner_m = re.search(r"<th\b[^>]*>(.*?)</th>", tag, flags=re.IGNORECASE | re.DOTALL)
|
| 788 |
+
if not inner_m:
|
| 789 |
+
return tag
|
| 790 |
+
inner = inner_m.group(1)
|
| 791 |
+
plain = _cell_plain_text(inner)
|
| 792 |
+
mapped = map_header_token(plain, ctx)
|
| 793 |
+
if mapped == plain:
|
| 794 |
+
return tag
|
| 795 |
+
changed = True
|
| 796 |
+
return re.sub(
|
| 797 |
+
r"(<th\b[^>]*>)(.*?)(</th>)",
|
| 798 |
+
rf"\1{html.escape(mapped)}\3",
|
| 799 |
+
tag,
|
| 800 |
+
count=1,
|
| 801 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
out = re.sub(r"<th\b[^>]*>.*?</th>", repl_th, full, flags=re.IGNORECASE | re.DOTALL)
|
| 805 |
+
if changed:
|
| 806 |
+
return out
|
| 807 |
+
# Fallback for OCR tables that put header row in <td>.
|
| 808 |
+
if "amount" in ctx and ("deposit" in ctx or "debit" in ctx or "withdraw" in ctx or "check" in ctx or "fee" in ctx):
|
| 809 |
+
out2 = re.sub(
|
| 810 |
+
r"(<td\b[^>]*>\s*)amount(\s*</td>)",
|
| 811 |
+
lambda mm: mm.group(1)
|
| 812 |
+
+ (
|
| 813 |
+
"Credit"
|
| 814 |
+
if ("deposit" in ctx or "credit" in ctx or "incoming" in ctx or "received" in ctx)
|
| 815 |
+
else "Debit"
|
| 816 |
+
)
|
| 817 |
+
+ mm.group(2),
|
| 818 |
+
full,
|
| 819 |
+
flags=re.IGNORECASE,
|
| 820 |
+
)
|
| 821 |
+
return out2
|
| 822 |
+
return full
|
| 823 |
+
|
| 824 |
+
return re.sub(
|
| 825 |
+
r"<table\b[^>]*>.*?</table>",
|
| 826 |
+
repl_table,
|
| 827 |
+
md,
|
| 828 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
|
| 832 |
def _strip_trailing_money_tokens(rest: str, max_take: int = 2) -> Tuple[str, List[str]]:
|
| 833 |
"""Strip 1–2 currency tokens from the right of *rest*; return (description, tokens oldest-first)."""
|
| 834 |
cur = rest.rstrip()
|
|
|
|
| 1000 |
else:
|
| 1001 |
out_blocks.append(coalesce_loose_ledger_lines(b))
|
| 1002 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
| 1003 |
+
merged = mask_non_monetary_long_numbers(merged)
|
| 1004 |
+
merged = directionalize_amount_headers(merged)
|
| 1005 |
merged = repair_thead_cell_semantics(merged)
|
| 1006 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1007 |
merged = strip_degenerate_html_tables(merged)
|
|
|
|
| 1543 |
|
| 1544 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 1545 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
| 1546 |
+
merged = mask_non_monetary_long_numbers(merged)
|
| 1547 |
+
merged = directionalize_amount_headers(merged)
|
| 1548 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1549 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1550 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|