Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -113,6 +113,12 @@ Primary goals for reconcile rate:
|
|
| 113 |
Additions/Subtractions tables to the PDF: credits as Number|Date|Description|Additions
|
| 114 |
(blank Number when unused); debits as Date|Description|Subtractions (no Number); merges
|
| 115 |
OCR column splits. Adjacent duplicate HTML tables (same header + row multiset) are removed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
"""
|
| 117 |
|
| 118 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -2867,6 +2873,216 @@ def _normalize_fused_date_posted_amount_table(grid):
|
|
| 2867 |
return grid
|
| 2868 |
return out
|
| 2869 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2870 |
def normalize_html_tables(text: str) -> str:
|
| 2871 |
if not text or "<table" not in text.lower():
|
| 2872 |
return text
|
|
@@ -2908,6 +3124,9 @@ def normalize_html_tables(text: str) -> str:
|
|
| 2908 |
|
| 2909 |
grid = _split_fused_date_transaction_header(grid)
|
| 2910 |
|
|
|
|
|
|
|
|
|
|
| 2911 |
grid = _normalize_fused_date_posted_amount_table(grid)
|
| 2912 |
|
| 2913 |
grid = _split_fused_multicolumn_header(grid)
|
|
|
|
| 113 |
Additions/Subtractions tables to the PDF: credits as Number|Date|Description|Additions
|
| 114 |
(blank Number when unused); debits as Date|Description|Subtractions (no Number); merges
|
| 115 |
OCR column splits. Adjacent duplicate HTML tables (same header + row multiset) are removed.
|
| 116 |
+
- Fix TD-1: TD Bank continuation register: (a) four OCR columns with DATE+DESCRIPTION fused +
|
| 117 |
+
DEBIT/CREDIT/BALANCE — split col0 into Date|Description; (b) three OCR columns
|
| 118 |
+
Date|Transaction Description|Amount where Amount is the running balance — expand to
|
| 119 |
+
five columns using trailing amount in the description cell and TD-specific debit/credit
|
| 120 |
+
routing. Fires on \"continued\" in-table text and/or TD body cues (TD ZELLE, LENDINGCLUB,
|
| 121 |
+
etc.). Safe no-op elsewhere.
|
| 122 |
"""
|
| 123 |
|
| 124 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
|
|
| 2873 |
return grid
|
| 2874 |
return out
|
| 2875 |
|
| 2876 |
+
|
| 2877 |
+
_TD_CONTINUED_BLOB_RE = re.compile(
|
| 2878 |
+
r"transactions\s+by\s+date.*continued|continued.*transactions\s+by\s+date",
|
| 2879 |
+
re.IGNORECASE | re.DOTALL,
|
| 2880 |
+
)
|
| 2881 |
+
|
| 2882 |
+
# GLM often omits the continued banner inside a standalone <table>; match TD register body text.
|
| 2883 |
+
_TD_REGISTER_BODY_SIGNAL_RE = re.compile(
|
| 2884 |
+
r"TD\s+ZELLE|\bZELLE\b.*\b(?:RECEIVED|SENT)\b|LENDINGCLUB|MM\s*&\s*(?:amp;)?\s*CB|"
|
| 2885 |
+
r"MAINTENANCE\s+FEE|OD\s+GRACE\s+FEE\s+REFUND|OVERDRAFT\s+PD",
|
| 2886 |
+
re.IGNORECASE,
|
| 2887 |
+
)
|
| 2888 |
+
|
| 2889 |
+
|
| 2890 |
+
def _is_td_four_col_ddcb_header(cells: List[str]) -> bool:
|
| 2891 |
+
"""TD continuation: one fused DATE DESCRIPTION cell + DEBIT + CREDIT + BALANCE."""
|
| 2892 |
+
if len(cells) < 4:
|
| 2893 |
+
return False
|
| 2894 |
+
a = re.sub(r"\s+", " ", str(cells[0] or "").strip().lower())
|
| 2895 |
+
b = re.sub(r"\s+", " ", str(cells[1] or "").strip().lower())
|
| 2896 |
+
c = re.sub(r"\s+", " ", str(cells[2] or "").strip().lower())
|
| 2897 |
+
d = re.sub(r"\s+", " ", str(cells[3] or "").strip().lower())
|
| 2898 |
+
fused = "date" in a and "description" in a
|
| 2899 |
+
deb = b == "debit" or b.startswith("debit")
|
| 2900 |
+
cre = "credit" in c and "debit" not in c
|
| 2901 |
+
bal = "balance" in d
|
| 2902 |
+
return fused and deb and cre and bal
|
| 2903 |
+
|
| 2904 |
+
|
| 2905 |
+
def _split_td_continuation_date_description_grid(grid):
|
| 2906 |
+
"""
|
| 2907 |
+
Fix TD-1: TD Convenience Checking continuation pages use four physical columns; split col0
|
| 2908 |
+
into Date + Description so json_data_extractor's five-way column map aligns with row cells.
|
| 2909 |
+
"""
|
| 2910 |
+
if not grid or len(grid) < 3:
|
| 2911 |
+
return grid
|
| 2912 |
+
|
| 2913 |
+
blob = " ".join(
|
| 2914 |
+
" ".join(str(c or "") for c in row) for row in grid[: min(30, len(grid))]
|
| 2915 |
+
)
|
| 2916 |
+
if not _TD_CONTINUED_BLOB_RE.search(blob):
|
| 2917 |
+
return grid
|
| 2918 |
+
|
| 2919 |
+
hdr_idx = None
|
| 2920 |
+
for ri in range(min(20, len(grid))):
|
| 2921 |
+
raw = [str(c or "").strip() for c in grid[ri]]
|
| 2922 |
+
while len(raw) < 4:
|
| 2923 |
+
raw.append("")
|
| 2924 |
+
if len(raw) >= 4 and _is_td_four_col_ddcb_header(raw[:4]):
|
| 2925 |
+
hdr_idx = ri
|
| 2926 |
+
break
|
| 2927 |
+
if hdr_idx is None:
|
| 2928 |
+
return grid
|
| 2929 |
+
|
| 2930 |
+
date_led_re = re.compile(r"^(\d{1,2}/\d{1,2}(?:/\d{2,4})?)\s+(.*)$", re.DOTALL)
|
| 2931 |
+
new_grid: List[List[str]] = []
|
| 2932 |
+
for ri, row in enumerate(grid):
|
| 2933 |
+
cells = [str(c or "").strip() for c in row]
|
| 2934 |
+
if ri < hdr_idx:
|
| 2935 |
+
new_grid.append(cells)
|
| 2936 |
+
continue
|
| 2937 |
+
if ri == hdr_idx:
|
| 2938 |
+
c = cells[:4] + [""] * max(0, 4 - len(cells))
|
| 2939 |
+
new_grid.append(
|
| 2940 |
+
[
|
| 2941 |
+
"DATE",
|
| 2942 |
+
"DESCRIPTION",
|
| 2943 |
+
c[1] or "DEBIT",
|
| 2944 |
+
c[2] or "CREDIT",
|
| 2945 |
+
c[3] or "BALANCE",
|
| 2946 |
+
]
|
| 2947 |
+
)
|
| 2948 |
+
continue
|
| 2949 |
+
while len(cells) < 4:
|
| 2950 |
+
cells.append("")
|
| 2951 |
+
if len(cells) == 4:
|
| 2952 |
+
m = date_led_re.match(cells[0].strip())
|
| 2953 |
+
if m:
|
| 2954 |
+
new_grid.append(
|
| 2955 |
+
[
|
| 2956 |
+
m.group(1).strip(),
|
| 2957 |
+
(m.group(2) or "").strip(),
|
| 2958 |
+
cells[1],
|
| 2959 |
+
cells[2],
|
| 2960 |
+
cells[3],
|
| 2961 |
+
]
|
| 2962 |
+
)
|
| 2963 |
+
else:
|
| 2964 |
+
new_grid.append([cells[0], "", cells[1], cells[2], cells[3]])
|
| 2965 |
+
elif len(cells) >= 5:
|
| 2966 |
+
new_grid.append(cells)
|
| 2967 |
+
else:
|
| 2968 |
+
while len(cells) < 5:
|
| 2969 |
+
cells.append("")
|
| 2970 |
+
new_grid.append(cells[:5])
|
| 2971 |
+
|
| 2972 |
+
return new_grid
|
| 2973 |
+
|
| 2974 |
+
|
| 2975 |
+
def _is_td_three_col_register_header(cells: List[str]) -> bool:
|
| 2976 |
+
"""OCR variant: Date | Transaction Description | Amount (Amount column is running balance)."""
|
| 2977 |
+
if len(cells) < 3:
|
| 2978 |
+
return False
|
| 2979 |
+
a = re.sub(r"\s+", " ", str(cells[0] or "").strip().lower())
|
| 2980 |
+
b = re.sub(r"\s+", " ", str(cells[1] or "").strip().lower())
|
| 2981 |
+
c = re.sub(r"\s+", " ", str(cells[2] or "").strip().lower())
|
| 2982 |
+
if a != "date":
|
| 2983 |
+
return False
|
| 2984 |
+
if "transaction" not in b or "description" not in b:
|
| 2985 |
+
return False
|
| 2986 |
+
if c not in ("amount", "balance") and "amount" not in c and "balance" not in c:
|
| 2987 |
+
return False
|
| 2988 |
+
return True
|
| 2989 |
+
|
| 2990 |
+
|
| 2991 |
+
def _td_trailing_amount_from_description(desc: str) -> Tuple[str, str]:
|
| 2992 |
+
"""Return (description_without_trailing_amount, amount_str or '')."""
|
| 2993 |
+
if not desc or not str(desc).strip():
|
| 2994 |
+
return "", ""
|
| 2995 |
+
s = str(desc).strip()
|
| 2996 |
+
m = re.search(r"(?:,\s*|\s)(-?\d{1,3}(?:,\d{3})*\.\d{2})\s*$", s)
|
| 2997 |
+
if not m:
|
| 2998 |
+
return s, ""
|
| 2999 |
+
amt = m.group(1).strip()
|
| 3000 |
+
left = s[: m.start()].strip().rstrip(",")
|
| 3001 |
+
return left, amt
|
| 3002 |
+
|
| 3003 |
+
|
| 3004 |
+
def _td_route_debit_credit(desc_raw: str, amt_str: str) -> Tuple[str, str]:
|
| 3005 |
+
"""Return (debit_cell, credit_cell) with positive amount string in one side."""
|
| 3006 |
+
if not amt_str:
|
| 3007 |
+
return "", ""
|
| 3008 |
+
u = str(desc_raw or "").upper()
|
| 3009 |
+
if (
|
| 3010 |
+
"ACH DEBIT" in u
|
| 3011 |
+
or "OVERDRAFT" in u
|
| 3012 |
+
or "ZELLE SENT" in u
|
| 3013 |
+
or "MAINTENANCE FEE" in u
|
| 3014 |
+
or "ATM DB" in u
|
| 3015 |
+
or "ATM DEBIT" in u
|
| 3016 |
+
or "ELECTRONIC PMT" in u
|
| 3017 |
+
or "PMT-WEB" in u
|
| 3018 |
+
):
|
| 3019 |
+
return amt_str, ""
|
| 3020 |
+
if (
|
| 3021 |
+
"ZELLE RECEIVED" in u
|
| 3022 |
+
or "OD GRACE FEE REFUND" in u
|
| 3023 |
+
or u.startswith("CREDIT,")
|
| 3024 |
+
or u.startswith("DEPOSIT")
|
| 3025 |
+
):
|
| 3026 |
+
return "", amt_str
|
| 3027 |
+
if "DEBIT" in u and "CREDIT," not in u[:12]:
|
| 3028 |
+
return amt_str, ""
|
| 3029 |
+
return "", amt_str
|
| 3030 |
+
|
| 3031 |
+
|
| 3032 |
+
def _expand_td_continuation_three_col_register_grid(grid):
|
| 3033 |
+
"""
|
| 3034 |
+
Fix TD-1b: GLM emits Date|Transaction Description|Amount where col3 is balance; expand to
|
| 3035 |
+
DATE|DESCRIPTION|DEBIT|CREDIT|BALANCE for json_data_extractor.
|
| 3036 |
+
"""
|
| 3037 |
+
if not grid or len(grid) < 2:
|
| 3038 |
+
return grid
|
| 3039 |
+
|
| 3040 |
+
blob = " ".join(
|
| 3041 |
+
" ".join(str(c or "") for c in row) for row in grid[: min(40, len(grid))]
|
| 3042 |
+
)
|
| 3043 |
+
if not (
|
| 3044 |
+
_TD_CONTINUED_BLOB_RE.search(blob)
|
| 3045 |
+
or _TD_REGISTER_BODY_SIGNAL_RE.search(blob)
|
| 3046 |
+
):
|
| 3047 |
+
return grid
|
| 3048 |
+
|
| 3049 |
+
hdr_idx = None
|
| 3050 |
+
for ri in range(min(15, len(grid))):
|
| 3051 |
+
raw = [str(c or "").strip() for c in grid[ri]]
|
| 3052 |
+
while len(raw) < 3:
|
| 3053 |
+
raw.append("")
|
| 3054 |
+
if _is_td_three_col_register_header(raw[:3]):
|
| 3055 |
+
hdr_idx = ri
|
| 3056 |
+
break
|
| 3057 |
+
if hdr_idx is None:
|
| 3058 |
+
return grid
|
| 3059 |
+
|
| 3060 |
+
date_re = re.compile(r"^\d{1,2}/\d{1,2}(?:/\d{2,4})?$")
|
| 3061 |
+
new_grid: List[List[str]] = []
|
| 3062 |
+
for ri, row in enumerate(grid):
|
| 3063 |
+
cells = [str(c or "").strip() for c in row]
|
| 3064 |
+
if ri < hdr_idx:
|
| 3065 |
+
new_grid.append(cells)
|
| 3066 |
+
continue
|
| 3067 |
+
if ri == hdr_idx:
|
| 3068 |
+
new_grid.append(["DATE", "DESCRIPTION", "DEBIT", "CREDIT", "BALANCE"])
|
| 3069 |
+
continue
|
| 3070 |
+
while len(cells) < 3:
|
| 3071 |
+
cells.append("")
|
| 3072 |
+
if len(cells) != 3:
|
| 3073 |
+
new_grid.append(cells)
|
| 3074 |
+
continue
|
| 3075 |
+
d0, d1, bal = cells[0], cells[1], cells[2]
|
| 3076 |
+
if not date_re.match(d0):
|
| 3077 |
+
new_grid.append([d0, d1, "", "", bal])
|
| 3078 |
+
continue
|
| 3079 |
+
desc_only, amt_s = _td_trailing_amount_from_description(d1)
|
| 3080 |
+
db, cr = _td_route_debit_credit(d1, amt_s)
|
| 3081 |
+
new_grid.append([d0, desc_only, db, cr, bal])
|
| 3082 |
+
|
| 3083 |
+
return new_grid
|
| 3084 |
+
|
| 3085 |
+
|
| 3086 |
def normalize_html_tables(text: str) -> str:
|
| 3087 |
if not text or "<table" not in text.lower():
|
| 3088 |
return text
|
|
|
|
| 3124 |
|
| 3125 |
grid = _split_fused_date_transaction_header(grid)
|
| 3126 |
|
| 3127 |
+
grid = _split_td_continuation_date_description_grid(grid)
|
| 3128 |
+
grid = _expand_td_continuation_three_col_register_grid(grid)
|
| 3129 |
+
|
| 3130 |
grid = _normalize_fused_date_posted_amount_table(grid)
|
| 3131 |
|
| 3132 |
grid = _split_fused_multicolumn_header(grid)
|