rehan953 commited on
Commit
3e23bb2
·
verified ·
1 Parent(s): 5798892

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -9
app.py CHANGED
@@ -34,6 +34,10 @@ Primary goals for reconcile rate:
34
  (same normalized Date|Description|Amount keys), strip that suffix from table N. Fixes
35
  formats (e.g. Truist) where deposit rows are glued onto the withdrawals table and repeated
36
  under the proper deposits heading (K>=2; first table must keep >=1 data row).
 
 
 
 
37
  """
38
 
39
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -3099,9 +3103,12 @@ def normalize_html_tables(text: str) -> str:
3099
  # (Hardin County Bank: "CHASE CREDIT CRD EPAY 8319249882 500.00")
3100
  grid = _extract_fused_desc_amount(grid)
3101
 
 
 
 
3102
  # Fix 3: fused key-value rows in summary sections (no-space guard — safe on all PDFs)
3103
  grid = _fix_fused_keyvalue_rows(grid)
3104
- # Fix 16: duplicate data rows inside same Date|Description|Amount table (OCR repeats).
3105
  grid = _dedupe_duplicate_rows_in_da3_table(grid)
3106
  # Fix 12: split fused summary+daily-balance combined table, if detected.
3107
  split_tables = _split_combined_summary_daily_balance_grid(grid)
@@ -3142,24 +3149,113 @@ def _is_da3_header(grid):
3142
  )
3143
 
3144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3145
  def _dedupe_duplicate_rows_in_da3_table(grid):
3146
- """Drop repeated data rows within one Date|Description|Amount table (same normalized key)."""
 
 
 
 
3147
  if not grid or len(grid) < 2:
3148
  return grid
3149
  if not _is_da3_header(grid):
3150
  return grid
3151
- seen = set()
3152
- out = [grid[0]]
3153
  for row in grid[1:]:
3154
  cells = [str(c or "").strip() for c in row]
3155
  if not any(cells):
3156
  continue
 
 
 
 
 
 
 
 
 
 
3157
  k = _transaction_row_dedupe_key(cells)
3158
- if k in seen:
3159
- continue
3160
- seen.add(k)
3161
- out.append(row)
3162
- return out if len(out) < len(grid) else grid
 
 
 
 
 
 
 
 
3163
 
3164
 
3165
  def _row_text_full_ucb(grid, r: int) -> str:
 
34
  (same normalized Date|Description|Amount keys), strip that suffix from table N. Fixes
35
  formats (e.g. Truist) where deposit rows are glued onto the withdrawals table and repeated
36
  under the proper deposits heading (K>=2; first table must keep >=1 data row).
37
+ - Fix 19: DA3 rows whose Amount cell is not strict currency (e.g. fused card digits + merchant tail)
38
+ — take the rightmost plausible money token from Description+Amount text for the amount.
39
+ - Fix 20: DA3 in-table dedupe — cap consecutive identical normalized keys at 2 rows (keeps legitimate
40
+ back-to-back identical charges; still collapses 3+ OCR stutters).
41
  """
42
 
43
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
3103
  # (Hardin County Bank: "CHASE CREDIT CRD EPAY 8319249882 500.00")
3104
  grid = _extract_fused_desc_amount(grid)
3105
 
3106
+ # Fix 19: junk Amount cell but money token still present in row text (Truist MSBILL/PIN).
3107
+ grid = _repair_da3_garbled_amount_cells(grid)
3108
+
3109
  # Fix 3: fused key-value rows in summary sections (no-space guard — safe on all PDFs)
3110
  grid = _fix_fused_keyvalue_rows(grid)
3111
+ # Fix 16/20: duplicate data rows inside same Date|Description|Amount table (OCR repeats).
3112
  grid = _dedupe_duplicate_rows_in_da3_table(grid)
3113
  # Fix 12: split fused summary+daily-balance combined table, if detected.
3114
  split_tables = _split_combined_summary_daily_balance_grid(grid)
 
3149
  )
3150
 
3151
 
3152
+ _DA3_STRICT_AMOUNT = re.compile(r"^-?\$?\d{1,3}(?:,\d{3})*\.\d{2}-?$")
3153
+ _DA3_MONEY_TOKEN = re.compile(
3154
+ r"(?<![\d,])(-?\$?\d{1,3}(?:,\d{3})*\.\d{2}|-?\$?\d{2,6}\.\d{2})(?!\d)"
3155
+ )
3156
+
3157
+
3158
+ def _repair_da3_garbled_amount_cells(grid):
3159
+ """
3160
+ Fix 19: Amount column shows fused junk (letters, card digits) while the real debit/credit
3161
+ still appears as a money token in the row text — use the rightmost plausible amount.
3162
+ Only for Date|Description|Amount tables and strict MM/DD dates in column 0.
3163
+ """
3164
+ if not grid or len(grid) < 2 or not _is_da3_header(grid):
3165
+ return grid
3166
+
3167
+ date_re = re.compile(r"^\d{1,2}/\d{1,2}(?:/\d{2,4})?$")
3168
+ out = [list(grid[0])]
3169
+ changed = False
3170
+
3171
+ for row in grid[1:]:
3172
+ cells = [str(c or "").strip() for c in row]
3173
+ while len(cells) < 3:
3174
+ cells.append("")
3175
+ c0, c1, c2 = cells[0], cells[1], cells[2]
3176
+ if not date_re.match(c0):
3177
+ out.append(cells)
3178
+ continue
3179
+ c2s = c2.strip()
3180
+ if _DA3_STRICT_AMOUNT.match(c2s):
3181
+ out.append(cells)
3182
+ continue
3183
+ blob = f"{c1} {c2}".strip()
3184
+ if not blob:
3185
+ out.append(cells)
3186
+ continue
3187
+ found = _DA3_MONEY_TOKEN.findall(blob)
3188
+ if not found:
3189
+ out.append(cells)
3190
+ continue
3191
+ pick_raw = None
3192
+ for m in reversed(found):
3193
+ t = m.replace("$", "").replace(",", "")
3194
+ try:
3195
+ v = float(t)
3196
+ except ValueError:
3197
+ continue
3198
+ if 0.01 <= v <= 9_999_999.99:
3199
+ pick_raw = m.replace("$", "").strip()
3200
+ break
3201
+ if not pick_raw:
3202
+ out.append(cells)
3203
+ continue
3204
+ new_desc = c1
3205
+ for suf in (
3206
+ pick_raw,
3207
+ pick_raw.replace(",", ""),
3208
+ f"${pick_raw}",
3209
+ f"${pick_raw.replace(',', '')}",
3210
+ ):
3211
+ if suf and new_desc.rstrip().endswith(suf):
3212
+ new_desc = new_desc[: -len(suf)].rstrip()
3213
+ break
3214
+ out.append([c0, new_desc, pick_raw])
3215
+ changed = True
3216
+
3217
+ return out if changed else grid
3218
+
3219
+
3220
  def _dedupe_duplicate_rows_in_da3_table(grid):
3221
+ """
3222
+ Fix 16 + 20: Drop long runs of OCR-identical rows; allow up to 2 consecutive rows with the
3223
+ same normalized Date|Description|Amount key (legitimate duplicate charges). Non-consecutive
3224
+ repeats are kept (each run capped separately).
3225
+ """
3226
  if not grid or len(grid) < 2:
3227
  return grid
3228
  if not _is_da3_header(grid):
3229
  return grid
3230
+ data_rows = []
 
3231
  for row in grid[1:]:
3232
  cells = [str(c or "").strip() for c in row]
3233
  if not any(cells):
3234
  continue
3235
+ data_rows.append(row)
3236
+
3237
+ if not data_rows:
3238
+ return grid
3239
+
3240
+ out = [grid[0]]
3241
+ i = 0
3242
+ while i < len(data_rows):
3243
+ row = data_rows[i]
3244
+ cells = [str(c or "").strip() for c in row]
3245
  k = _transaction_row_dedupe_key(cells)
3246
+ j = i + 1
3247
+ while j < len(data_rows):
3248
+ ncells = [str(c or "").strip() for c in data_rows[j]]
3249
+ if _transaction_row_dedupe_key(ncells) != k:
3250
+ break
3251
+ j += 1
3252
+ run_len = j - i
3253
+ take = min(run_len, 2)
3254
+ for t in range(take):
3255
+ out.append(data_rows[i + t])
3256
+ i = j
3257
+
3258
+ return out
3259
 
3260
 
3261
  def _row_text_full_ucb(grid, r: int) -> str: