Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -733,6 +733,88 @@ def strip_degenerate_html_tables(md: str) -> str:
|
|
| 733 |
return re.sub(r"\n{3,}", "\n\n", out)
|
| 734 |
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
# Match long digit runs even when OCR glues trailing letters (e.g. 9257403599CCD).
|
| 737 |
_LONG_NUM_TOKEN = re.compile(r"(?<!\d)\d{7,}(?!\d)")
|
| 738 |
|
|
@@ -1042,9 +1124,9 @@ def light_stabilize_markdown(page_md: str) -> str:
|
|
| 1042 |
else:
|
| 1043 |
out_blocks.append(coalesce_loose_ledger_lines(b))
|
| 1044 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
|
|
|
|
|
|
| 1045 |
merged = mask_non_monetary_long_numbers(merged)
|
| 1046 |
-
merged = directionalize_amount_headers(merged)
|
| 1047 |
-
merged = directionalize_amount_headers_by_nearby_context(merged)
|
| 1048 |
merged = repair_thead_cell_semantics(merged)
|
| 1049 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1050 |
merged = strip_degenerate_html_tables(merged)
|
|
@@ -1789,11 +1871,9 @@ def run_ocr(uploaded_file):
|
|
| 1789 |
|
| 1790 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 1791 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
|
|
|
|
|
|
| 1792 |
merged = mask_non_monetary_long_numbers(merged)
|
| 1793 |
-
merged = directionalize_amount_headers(merged)
|
| 1794 |
-
merged = directionalize_amount_headers_by_nearby_context(merged)
|
| 1795 |
-
merged = normalize_ambiguous_amount_balance_tables(merged)
|
| 1796 |
-
merged = normalize_amount_only_transaction_tables(merged)
|
| 1797 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1798 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1799 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|
|
|
|
| 733 |
return re.sub(r"\n{3,}", "\n\n", out)
|
| 734 |
|
| 735 |
|
| 736 |
+
# OCR sometimes emits a malformed leading pseudo-header row like:
|
| 737 |
+
# Date05/09/25 | TypeDeposit | Amount2,270.00 | ...
|
| 738 |
+
_GLUED_HEADER_ROW_RE = re.compile(r"date\d{1,2}/\d{1,2}|typedeposit|amount\d", re.IGNORECASE)
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def strip_malformed_leading_table_rows(md: str) -> str:
|
| 742 |
+
"""
|
| 743 |
+
Remove malformed leading rows in HTML tables when they are clearly bogus
|
| 744 |
+
header artifacts and the table already contains a proper header row below.
|
| 745 |
+
"""
|
| 746 |
+
if not md or "<table" not in md.lower():
|
| 747 |
+
return md
|
| 748 |
+
|
| 749 |
+
def repl_table(m: re.Match) -> str:
|
| 750 |
+
full = m.group(0)
|
| 751 |
+
trs = list(re.finditer(r"<tr\b[^>]*>.*?</tr>", full, flags=re.IGNORECASE | re.DOTALL))
|
| 752 |
+
if len(trs) < 2:
|
| 753 |
+
return full
|
| 754 |
+
|
| 755 |
+
first = trs[0].group(0)
|
| 756 |
+
second = trs[1].group(0)
|
| 757 |
+
first_plain = _cell_plain_text(first).lower()
|
| 758 |
+
second_plain = _cell_plain_text(second).lower()
|
| 759 |
+
|
| 760 |
+
first_is_malformed = bool(_GLUED_HEADER_ROW_RE.search(first_plain))
|
| 761 |
+
second_looks_header = (
|
| 762 |
+
("date" in second_plain and "description" in second_plain)
|
| 763 |
+
or ("posting date" in second_plain and "description" in second_plain)
|
| 764 |
+
or ("date" in second_plain and "balance" in second_plain)
|
| 765 |
+
)
|
| 766 |
+
if not (first_is_malformed and second_looks_header):
|
| 767 |
+
return full
|
| 768 |
+
|
| 769 |
+
return full.replace(first, "", 1)
|
| 770 |
+
|
| 771 |
+
return re.sub(
|
| 772 |
+
r"<table\b[^>]*>.*?</table>",
|
| 773 |
+
repl_table,
|
| 774 |
+
md,
|
| 775 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
def strip_subtotal_rows_from_transaction_tables(md: str) -> str:
|
| 780 |
+
"""
|
| 781 |
+
Remove subtotal/total line-item rows from transaction tables so aggregate
|
| 782 |
+
amounts are not mistaken as individual transactions.
|
| 783 |
+
"""
|
| 784 |
+
if not md or "<table" not in md.lower():
|
| 785 |
+
return md
|
| 786 |
+
|
| 787 |
+
def repl_table(m: re.Match) -> str:
|
| 788 |
+
full = m.group(0)
|
| 789 |
+
rows = list(re.finditer(r"<tr\b[^>]*>.*?</tr>", full, flags=re.IGNORECASE | re.DOTALL))
|
| 790 |
+
if len(rows) < 2:
|
| 791 |
+
return full
|
| 792 |
+
# Some tables start with a title row, then the actual header appears in row 2/3.
|
| 793 |
+
sample_headers = [_cell_plain_text(r.group(0)).lower() for r in rows[:4]]
|
| 794 |
+
is_txn_like = any(
|
| 795 |
+
("date" in hp or "posting date" in hp)
|
| 796 |
+
and ("description" in hp)
|
| 797 |
+
and ("amount" in hp or "debit" in hp or "credit" in hp)
|
| 798 |
+
for hp in sample_headers
|
| 799 |
+
)
|
| 800 |
+
if not is_txn_like:
|
| 801 |
+
return full
|
| 802 |
+
|
| 803 |
+
out = full
|
| 804 |
+
for r in rows[1:]:
|
| 805 |
+
rp = _cell_plain_text(r.group(0)).lower()
|
| 806 |
+
if "subtotal" in rp or rp.startswith("total "):
|
| 807 |
+
out = out.replace(r.group(0), "", 1)
|
| 808 |
+
return out
|
| 809 |
+
|
| 810 |
+
return re.sub(
|
| 811 |
+
r"<table\b[^>]*>.*?</table>",
|
| 812 |
+
repl_table,
|
| 813 |
+
md,
|
| 814 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
# Match long digit runs even when OCR glues trailing letters (e.g. 9257403599CCD).
|
| 819 |
_LONG_NUM_TOKEN = re.compile(r"(?<!\d)\d{7,}(?!\d)")
|
| 820 |
|
|
|
|
| 1124 |
else:
|
| 1125 |
out_blocks.append(coalesce_loose_ledger_lines(b))
|
| 1126 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
| 1127 |
+
merged = strip_malformed_leading_table_rows(merged)
|
| 1128 |
+
merged = strip_subtotal_rows_from_transaction_tables(merged)
|
| 1129 |
merged = mask_non_monetary_long_numbers(merged)
|
|
|
|
|
|
|
| 1130 |
merged = repair_thead_cell_semantics(merged)
|
| 1131 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1132 |
merged = strip_degenerate_html_tables(merged)
|
|
|
|
| 1871 |
|
| 1872 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 1873 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
| 1874 |
+
merged = strip_malformed_leading_table_rows(merged)
|
| 1875 |
+
merged = strip_subtotal_rows_from_transaction_tables(merged)
|
| 1876 |
merged = mask_non_monetary_long_numbers(merged)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1877 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1878 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1879 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|