Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -752,7 +752,8 @@ def mask_non_monetary_long_numbers(md: str) -> str:
|
|
| 752 |
# Keep YYYYMMDD-like date compact tokens.
|
| 753 |
if len(tok) == 8 and tok.startswith(("19", "20")):
|
| 754 |
return tok
|
| 755 |
-
|
|
|
|
| 756 |
|
| 757 |
return _LONG_NUM_TOKEN.sub(repl, md)
|
| 758 |
|
|
@@ -1161,6 +1162,83 @@ def normalize_ambiguous_amount_balance_tables(md: str) -> str:
|
|
| 1161 |
)
|
| 1162 |
|
| 1163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1164 |
def _extract_daily_balance_by_date(md: str):
|
| 1165 |
"""
|
| 1166 |
Build a date->balance map from daily-balance style tables.
|
|
@@ -1672,6 +1750,7 @@ def run_ocr(uploaded_file):
|
|
| 1672 |
merged = mask_non_monetary_long_numbers(merged)
|
| 1673 |
merged = directionalize_amount_headers(merged)
|
| 1674 |
merged = normalize_ambiguous_amount_balance_tables(merged)
|
|
|
|
| 1675 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1676 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1677 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|
|
|
|
| 752 |
# Keep YYYYMMDD-like date compact tokens.
|
| 753 |
if len(tok) == 8 and tok.startswith(("19", "20")):
|
| 754 |
return tok
|
| 755 |
+
# Replace digits entirely so downstream cannot reinterpret ID tokens as money.
|
| 756 |
+
return "IDNUM"
|
| 757 |
|
| 758 |
return _LONG_NUM_TOKEN.sub(repl, md)
|
| 759 |
|
|
|
|
| 1162 |
)
|
| 1163 |
|
| 1164 |
|
| 1165 |
+
def normalize_amount_only_transaction_tables(md: str) -> str:
|
| 1166 |
+
"""
|
| 1167 |
+
Convert transaction-like tables with Date/Description/Amount (no Credit/Debit)
|
| 1168 |
+
into explicit Credit/Debit columns using generic lexical heuristics.
|
| 1169 |
+
"""
|
| 1170 |
+
if not md or "<table" not in md.lower():
|
| 1171 |
+
return md
|
| 1172 |
+
|
| 1173 |
+
credit_hint_re = re.compile(
|
| 1174 |
+
r"\b(deposit|deposits|credit|credits|incoming|received|return|refund|interest|pmt cr)\b",
|
| 1175 |
+
re.IGNORECASE,
|
| 1176 |
+
)
|
| 1177 |
+
debit_hint_re = re.compile(
|
| 1178 |
+
r"\b(debit|debits|withdrawal|withdrawals|check|checks|fee|fees|charge|charges|payment|purchase|ach|bill)\b",
|
| 1179 |
+
re.IGNORECASE,
|
| 1180 |
+
)
|
| 1181 |
+
|
| 1182 |
+
def repl_table(m: re.Match) -> str:
|
| 1183 |
+
full = m.group(0)
|
| 1184 |
+
rows = _extract_rows_plain_from_table(full)
|
| 1185 |
+
if len(rows) < 3:
|
| 1186 |
+
return full
|
| 1187 |
+
hdr = [c.strip().lower() for c in rows[0]]
|
| 1188 |
+
if any("credit" in c for c in hdr) or any("debit" in c for c in hdr):
|
| 1189 |
+
return full
|
| 1190 |
+
amount_idxs = [i for i, c in enumerate(hdr) if "amount" in c]
|
| 1191 |
+
date_idxs = [i for i, c in enumerate(hdr) if "date" in c]
|
| 1192 |
+
desc_idxs = [i for i, c in enumerate(hdr) if "description" in c or "memo" in c or "details" in c]
|
| 1193 |
+
if not amount_idxs or not date_idxs:
|
| 1194 |
+
return full
|
| 1195 |
+
amount_idx = amount_idxs[0]
|
| 1196 |
+
date_idx = date_idxs[0]
|
| 1197 |
+
desc_idx = desc_idxs[0] if desc_idxs else min(1, len(rows[0]) - 1)
|
| 1198 |
+
table_ctx = _cell_plain_text(full)
|
| 1199 |
+
|
| 1200 |
+
new_rows = [["Date", "Description", "Credit", "Debit"]]
|
| 1201 |
+
converted = 0
|
| 1202 |
+
for r in rows[1:]:
|
| 1203 |
+
if not r:
|
| 1204 |
+
continue
|
| 1205 |
+
padded = r + [""] * (max(amount_idx, date_idx, desc_idx) + 1 - len(r))
|
| 1206 |
+
date_txt = (padded[date_idx] or "").strip()
|
| 1207 |
+
if not _is_date_like(date_txt):
|
| 1208 |
+
continue
|
| 1209 |
+
desc_txt = (padded[desc_idx] or "").strip()
|
| 1210 |
+
amt = _parse_amount_or_none((padded[amount_idx] or "").strip())
|
| 1211 |
+
if amt is None:
|
| 1212 |
+
continue
|
| 1213 |
+
ctx = f"{table_ctx} {desc_txt}"
|
| 1214 |
+
credit = ""
|
| 1215 |
+
debit = ""
|
| 1216 |
+
if credit_hint_re.search(ctx) and not debit_hint_re.search(ctx):
|
| 1217 |
+
credit = _fmt_money(abs(amt))
|
| 1218 |
+
elif debit_hint_re.search(ctx) and not credit_hint_re.search(ctx):
|
| 1219 |
+
debit = _fmt_money(abs(amt))
|
| 1220 |
+
else:
|
| 1221 |
+
# Conservative default for unknown direction.
|
| 1222 |
+
debit = _fmt_money(abs(amt))
|
| 1223 |
+
new_rows.append([date_txt, desc_txt, credit, debit])
|
| 1224 |
+
converted += 1
|
| 1225 |
+
|
| 1226 |
+
if converted < 5:
|
| 1227 |
+
return full
|
| 1228 |
+
|
| 1229 |
+
html_rows = ["<tr>" + "".join(f"<th>{html.escape(c)}</th>" for c in new_rows[0]) + "</tr>"]
|
| 1230 |
+
for rr in new_rows[1:]:
|
| 1231 |
+
html_rows.append("<tr>" + "".join(f"<td>{html.escape(c)}</td>" for c in rr) + "</tr>")
|
| 1232 |
+
return "<table>\n" + "\n".join(html_rows) + "\n</table>"
|
| 1233 |
+
|
| 1234 |
+
return re.sub(
|
| 1235 |
+
r"<table\b[^>]*>.*?</table>",
|
| 1236 |
+
repl_table,
|
| 1237 |
+
md,
|
| 1238 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 1239 |
+
)
|
| 1240 |
+
|
| 1241 |
+
|
| 1242 |
def _extract_daily_balance_by_date(md: str):
|
| 1243 |
"""
|
| 1244 |
Build a date->balance map from daily-balance style tables.
|
|
|
|
| 1750 |
merged = mask_non_monetary_long_numbers(merged)
|
| 1751 |
merged = directionalize_amount_headers(merged)
|
| 1752 |
merged = normalize_ambiguous_amount_balance_tables(merged)
|
| 1753 |
+
merged = normalize_amount_only_transaction_tables(merged)
|
| 1754 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 1755 |
merged = enrich_transaction_tables_with_daily_balances(merged)
|
| 1756 |
# Keep transaction amounts untouched; add explicit snapshot for metadata extraction.
|