rehan953 commited on
Commit
59146dd
·
verified ·
1 Parent(s): 04e0fcf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -11
app.py CHANGED
@@ -53,8 +53,10 @@ Primary goals for reconcile rate:
53
  debits. Reorder to Credits + ITM table + Electronic Debits + merged debits; drop dup keys from the
54
  second table against the first; merge remaining orphan rows into the debits table.
55
  - Fix 17i: After Fix 17b splits ITM prefix from debits, OCR often leaves *credit* rows (POS Return, DDA
56
- Transfer IN, Apple Cash INST R / CUPERTINO) at the *end* of the debits table — move that contiguous
57
- tail from Electronic Debits back to Electronic Credits (same statement layout as the PDF).
 
 
58
  - Fix 17e: Repeat EC/ED fused + 17d passes until stable (handles chained replacements). Centered section titles
59
  match both align=\"center\" and style=\"text-align:center\" on divs.
60
  - Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
@@ -94,6 +96,9 @@ Primary goals for reconcile rate:
94
  relocated to just before the orphan table. Guards: no debit heading already before
95
  table; credit context must precede table; no other table between orphan and heading;
96
  try/except wrapper (safe no-op on any error).
 
 
 
97
  - Fix 20-override: _dedupe_duplicate_rows_in_da3_table now uses the PDF text-layer frequency
98
  map (_tl_freq_context) as its cap so legitimate repeated transactions (e.g. 6x
99
  Temporary Credit Reversal on the same date/amount) are preserved rather than
@@ -3021,18 +3026,20 @@ def _is_ucb_electronic_credit_row_after_split(grid, r: int) -> bool:
3021
 
3022
 
3023
  def _ucb_move_trailing_credit_rows_from_ed_to_ec(g_ec, g_ed):
3024
- """Fix 17i: contiguous tail of credit-like rows at bottom of g_ed -> append to g_ec."""
 
 
 
 
3025
  if not g_ec or not g_ed or len(g_ed) < 2 or not _is_da3_header(g_ec) or not _is_da3_header(g_ed):
3026
  return g_ec, g_ed
3027
- i = len(g_ed) - 1
3028
- while i >= 1 and _is_ucb_electronic_credit_row_after_split(g_ed, i):
3029
- i -= 1
3030
- tail_start = i + 1
3031
- if tail_start >= len(g_ed):
3032
  return g_ec, g_ed
3033
- credit_tail = [list(g_ed[r]) for r in range(tail_start, len(g_ed))]
3034
- g_ed_new = [list(g_ed[0])] + [list(g_ed[r]) for r in range(1, tail_start)]
3035
- g_ec_new = [list(g_ec[0])] + [list(r) for r in g_ec[1:]] + credit_tail
 
3036
  max_c = max(len(r) for r in g_ec_new + g_ed_new)
3037
  for row in g_ec_new + g_ed_new:
3038
  while len(row) < max_c:
@@ -3483,6 +3490,82 @@ def _ucb_prune_ed_fragment_dupes_vs_prev_table(text: str) -> str:
3483
  return text
3484
 
3485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3486
  def _split_ucb_fused_ec_ed_headings_single_table_html(text: str) -> str:
3487
  if not text or "electronic credits" not in text.lower():
3488
  return text
@@ -3547,6 +3630,7 @@ def _split_ucb_deposits_electronic_sections_html(text: str) -> str:
3547
  nxt = _split_ucb_fused_ec_ed_headings_single_table_html(nxt)
3548
  nxt = _ucb_strip_redundant_ed_subset_fragment(nxt)
3549
  nxt = _ucb_prune_ed_fragment_dupes_vs_prev_table(nxt)
 
3550
  if nxt == text:
3551
  break
3552
  text = nxt
@@ -4265,6 +4349,20 @@ def _fix29_move_debit_heading_before_orphan_table(text: str) -> str:
4265
  orphan_m = da3_matches[i]
4266
  orphan_start = orphan_m.start()
4267
  orphan_end = orphan_m.end()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4268
 
4269
  before_window = text[max(0, orphan_start - _HEADING_SCAN_WINDOW): orphan_start]
4270
  before_clean = re.sub(r"<[^>]+>", " ", before_window)
@@ -4326,6 +4424,73 @@ def _fix29_move_debit_heading_before_orphan_table(text: str) -> str:
4326
  return text
4327
 
4328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4329
  def stabilize_tables_and_text(page_md: str) -> str:
4330
  if not page_md:
4331
  return page_md
@@ -4359,6 +4524,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
4359
  stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
4360
  stabilized = _strip_orphan_continued_da3_flatline(stabilized)
4361
  stabilized = _strip_standalone_continued_banner_line(stabilized)
 
4362
  return close_unclosed_html(stabilized)
4363
 
4364
  # --------------------------
@@ -4489,6 +4655,7 @@ def run_ocr(uploaded_file):
4489
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
4490
  stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
4491
  stabilized = _strip_standalone_continued_banner_line(stabilized)
 
4492
  elif is_pdf and is_nfcu_doc:
4493
  nfcu_summary = _extract_nfcu_summary_table(path, page_num)
4494
  if nfcu_summary:
@@ -4567,6 +4734,7 @@ def run_ocr(uploaded_file):
4567
  merged = _fix29_move_debit_heading_before_orphan_table(merged) # Fix 29
4568
  merged = _strip_orphan_continued_da3_flatline(merged)
4569
  merged = _strip_standalone_continued_banner_line(merged)
 
4570
  return merged
4571
 
4572
  except Exception as e:
 
53
  debits. Reorder to Credits + ITM table + Electronic Debits + merged debits; drop dup keys from the
54
  second table against the first; merge remaining orphan rows into the debits table.
55
  - Fix 17i: After Fix 17b splits ITM prefix from debits, OCR often leaves *credit* rows (POS Return, DDA
56
+ Transfer IN, Apple Cash INST R / CUPERTINO) in the debits grid — move those rows to Electronic
57
+ Credits (contiguous-tail case on g_ed after 17b, or any matching row via 17j).
58
+ - Fix 17j: When OCR emits a *second* Electronic Debits table (duplicate block), credit-only rows often appear
59
+ only there — relocate them to the nearest preceding Electronic Credits table (same patterns as 17i).
60
  - Fix 17e: Repeat EC/ED fused + 17d passes until stable (handles chained replacements). Centered section titles
61
  match both align=\"center\" and style=\"text-align:center\" on divs.
62
  - Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
 
96
  relocated to just before the orphan table. Guards: no debit heading already before
97
  table; credit context must precede table; no other table between orphan and heading;
98
  try/except wrapper (safe no-op on any error).
99
+ - Fix 31: On check / receipt image pages, GLM sometimes emits bogus table rows (e.g. \"Witness LA…\",
100
+ \"[hotline Control]\" after amounts, long trace ids). Strip those rows and bracket junk after
101
+ currency amounts; drop a stray \"## UCB\" heading before such tables. Safe no-op when absent.
102
  - Fix 20-override: _dedupe_duplicate_rows_in_da3_table now uses the PDF text-layer frequency
103
  map (_tl_freq_context) as its cap so legitimate repeated transactions (e.g. 6x
104
  Temporary Credit Reversal on the same date/amount) are preserved rather than
 
3026
 
3027
 
3028
  def _ucb_move_trailing_credit_rows_from_ed_to_ec(g_ec, g_ed):
3029
+ """Fix 17i: credit-like rows that landed in g_ed (POS Return, DDA Transfer IN, Apple INST R) -> g_ec.
3030
+
3031
+ OCR often orders them after outgoing debits on the same date block, so a contiguous tail scan
3032
+ misses them; we relocate every matching data row while preserving relative order.
3033
+ """
3034
  if not g_ec or not g_ed or len(g_ed) < 2 or not _is_da3_header(g_ec) or not _is_da3_header(g_ed):
3035
  return g_ec, g_ed
3036
+ credit_indices = [r for r in range(1, len(g_ed)) if _is_ucb_electronic_credit_row_after_split(g_ed, r)]
3037
+ if not credit_indices:
 
 
 
3038
  return g_ec, g_ed
3039
+ credit_rows = [list(g_ed[r]) for r in credit_indices]
3040
+ drop = set(credit_indices)
3041
+ g_ed_new = [list(g_ed[0])] + [list(g_ed[r]) for r in range(1, len(g_ed)) if r not in drop]
3042
+ g_ec_new = [list(g_ec[0])] + [list(r) for r in g_ec[1:]] + credit_rows
3043
  max_c = max(len(r) for r in g_ec_new + g_ed_new)
3044
  for row in g_ec_new + g_ed_new:
3045
  while len(row) < max_c:
 
3490
  return text
3491
 
3492
 
3493
+ _UCB_ED_DIV_AND_TABLE = re.compile(
3494
+ _UCB_CENTER_DIV + r"\s*Electronic\s+Debits\s*</div>\s*(?P<tbl><table[^>]*>.*?</table>)",
3495
+ re.DOTALL | re.IGNORECASE,
3496
+ )
3497
+ _UCB_EC_DIV_AND_TABLE = re.compile(
3498
+ _UCB_CENTER_DIV + r"\s*Electronic\s+Credits\s*</div>\s*(?P<tbl><table[^>]*>.*?</table>)",
3499
+ re.DOTALL | re.IGNORECASE,
3500
+ )
3501
+
3502
+
3503
+ def _ucb_relocate_credit_rows_from_ed_tables_to_ec_html(text: str) -> str:
3504
+ """
3505
+ Fix 17j: Credit-side rows (POS Return, DDA Transfer IN, Apple Cash INST R) sometimes appear only under a
3506
+ duplicate Electronic Debits table, not in the fused EC table — move them to the preceding EC table.
3507
+ """
3508
+ if not text or "electronic debits" not in text.lower():
3509
+ return text
3510
+ try:
3511
+ while True:
3512
+ replaced = False
3513
+ for ed_m in _UCB_ED_DIV_AND_TABLE.finditer(text):
3514
+ g_ed = _parse_da3_grid_from_table_html(ed_m.group("tbl"))
3515
+ if not g_ed or len(g_ed) < 2 or not _is_da3_header(g_ed):
3516
+ continue
3517
+ credit_ix = [
3518
+ r
3519
+ for r in range(1, len(g_ed))
3520
+ if _is_ucb_electronic_credit_row_after_split(g_ed, r)
3521
+ ]
3522
+ if not credit_ix:
3523
+ continue
3524
+ before = text[: ed_m.start()]
3525
+ ec_matches = list(_UCB_EC_DIV_AND_TABLE.finditer(before))
3526
+ if not ec_matches:
3527
+ continue
3528
+ ec_m = ec_matches[-1]
3529
+ g_ec = _parse_da3_grid_from_table_html(ec_m.group("tbl"))
3530
+ if not g_ec or len(g_ec) < 2 or not _is_da3_header(g_ec):
3531
+ continue
3532
+ drop = set(credit_ix)
3533
+ credit_rows = [list(g_ed[r]) for r in credit_ix]
3534
+ g_ed_new = [list(g_ed[0])] + [list(g_ed[r]) for r in range(1, len(g_ed)) if r not in drop]
3535
+ g_ec_new = [list(g_ec[0])] + [list(r) for r in g_ec[1:]] + credit_rows
3536
+ max_c = max(len(r) for r in g_ec_new + g_ed_new)
3537
+ for row in g_ec_new + g_ed_new:
3538
+ while len(row) < max_c:
3539
+ row.append("")
3540
+ new_ec = _grid_to_html(g_ec_new)
3541
+ if len(g_ed_new) <= 1:
3542
+ text = (
3543
+ text[: ec_m.start("tbl")]
3544
+ + new_ec
3545
+ + text[ec_m.end("tbl") : ed_m.start()]
3546
+ + text[ed_m.end() :]
3547
+ )
3548
+ else:
3549
+ new_ed = _grid_to_html(g_ed_new)
3550
+ text = (
3551
+ text[: ec_m.start("tbl")]
3552
+ + new_ec
3553
+ + text[ec_m.end("tbl") : ed_m.start()]
3554
+ + text[ed_m.start() : ed_m.start("tbl")]
3555
+ + new_ed
3556
+ + text[ed_m.end() :]
3557
+ )
3558
+ log.info("Fix 17j: relocated %d credit row(s) from Electronic Debits to Electronic Credits", len(credit_rows))
3559
+ replaced = True
3560
+ break
3561
+ if not replaced:
3562
+ break
3563
+ return text
3564
+ except Exception as e:
3565
+ log.warning("_ucb_relocate_credit_rows_from_ed_tables_to_ec_html failed: %s", e)
3566
+ return text
3567
+
3568
+
3569
  def _split_ucb_fused_ec_ed_headings_single_table_html(text: str) -> str:
3570
  if not text or "electronic credits" not in text.lower():
3571
  return text
 
3630
  nxt = _split_ucb_fused_ec_ed_headings_single_table_html(nxt)
3631
  nxt = _ucb_strip_redundant_ed_subset_fragment(nxt)
3632
  nxt = _ucb_prune_ed_fragment_dupes_vs_prev_table(nxt)
3633
+ nxt = _ucb_relocate_credit_rows_from_ed_tables_to_ec_html(nxt)
3634
  if nxt == text:
3635
  break
3636
  text = nxt
 
4349
  orphan_m = da3_matches[i]
4350
  orphan_start = orphan_m.start()
4351
  orphan_end = orphan_m.end()
4352
+ orphan_tbl_html = orphan_m.group(0)
4353
+
4354
+ # Do not treat UCB Electronic Credits ITM-only tables as "orphan debits" needing a heading
4355
+ # moved ahead of them — Fix 17b leaves ED after the ITM block; moving ED here stacks both
4356
+ # headings above ITM and leaves debits under the wrong banner.
4357
+ try:
4358
+ og = _parse_da3_grid_from_table_html(orphan_tbl_html)
4359
+ if og and (
4360
+ _ucb_table_credits_only_no_strong_debits(og)
4361
+ or _ucb_html_table_body_looks_itm_credits_only(orphan_tbl_html)
4362
+ ):
4363
+ continue
4364
+ except Exception:
4365
+ pass
4366
 
4367
  before_window = text[max(0, orphan_start - _HEADING_SCAN_WINDOW): orphan_start]
4368
  before_clean = re.sub(r"<[^>]+>", " ", before_window)
 
4424
  return text
4425
 
4426
 
4427
+ def _strip_check_image_ocr_junk_html(text: str) -> str:
4428
+ """
4429
+ Fix 31: Remove hallucinated rows on scanned check/receipt regions and normalize amount cells.
4430
+ """
4431
+ if not text or "<table" not in text.lower():
4432
+ return text
4433
+ try:
4434
+ text = re.sub(
4435
+ r"(\$[\d,]+\.\d{2})\s*\[[^\]]+\]",
4436
+ r"\1",
4437
+ text,
4438
+ )
4439
+ tbl_pat = re.compile(r"(<table[^>]*>)([\s\S]*?)(</table>)", re.IGNORECASE)
4440
+
4441
+ def _tr_effectively_empty(tr: str) -> bool:
4442
+ core = re.sub(r"<[^>]+>", " ", tr)
4443
+ return not core.strip()
4444
+
4445
+ def repl(m) -> str:
4446
+ open_tag, body, close_tag = m.group(1), m.group(2), m.group(3)
4447
+ low = body.lower()
4448
+ if not any(
4449
+ k in low
4450
+ for k in (
4451
+ "hotline",
4452
+ "witness la",
4453
+ "itm checking",
4454
+ "care none",
4455
+ "processed ",
4456
+ "course dut",
4457
+ "n00590409",
4458
+ )
4459
+ ):
4460
+ return m.group(0)
4461
+ kept: List[str] = []
4462
+ for trm in re.finditer(r"<tr\b[^>]*>[\s\S]*?</tr>", body, re.IGNORECASE):
4463
+ tr = trm.group(0)
4464
+ tl = tr.lower()
4465
+ if _tr_effectively_empty(tr):
4466
+ continue
4467
+ if "[hotline" in tl or "hotline control" in tl:
4468
+ continue
4469
+ if "witness la" in tl and ("course" in tl or "dut" in tl):
4470
+ continue
4471
+ if "itm checking witness" in tl or (
4472
+ "itm checking" in tl and "witness" in tl
4473
+ ):
4474
+ continue
4475
+ if "care none" in tl and "account" in tl:
4476
+ continue
4477
+ if "processed " in tl and " @ " in tl and "items" in tl:
4478
+ continue
4479
+ if re.search(r"\bN\d{15,}\b", tr):
4480
+ continue
4481
+ kept.append(tr)
4482
+ if not kept:
4483
+ return m.group(0)
4484
+ return open_tag + "".join(kept) + close_tag
4485
+
4486
+ text = tbl_pat.sub(repl, text)
4487
+ text = re.sub(r"\n## UCB\s*\n+(?=<table)", "\n", text, flags=re.IGNORECASE)
4488
+ return text
4489
+ except Exception as e:
4490
+ log.warning("_strip_check_image_ocr_junk_html failed: %s", e)
4491
+ return text
4492
+
4493
+
4494
  def stabilize_tables_and_text(page_md: str) -> str:
4495
  if not page_md:
4496
  return page_md
 
4524
  stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
4525
  stabilized = _strip_orphan_continued_da3_flatline(stabilized)
4526
  stabilized = _strip_standalone_continued_banner_line(stabilized)
4527
+ stabilized = _strip_check_image_ocr_junk_html(stabilized) # Fix 31
4528
  return close_unclosed_html(stabilized)
4529
 
4530
  # --------------------------
 
4655
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
4656
  stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
4657
  stabilized = _strip_standalone_continued_banner_line(stabilized)
4658
+ stabilized = _strip_check_image_ocr_junk_html(stabilized) # Fix 31
4659
  elif is_pdf and is_nfcu_doc:
4660
  nfcu_summary = _extract_nfcu_summary_table(path, page_num)
4661
  if nfcu_summary:
 
4734
  merged = _fix29_move_debit_heading_before_orphan_table(merged) # Fix 29
4735
  merged = _strip_orphan_continued_da3_flatline(merged)
4736
  merged = _strip_standalone_continued_banner_line(merged)
4737
+ merged = _strip_check_image_ocr_junk_html(merged) # Fix 31
4738
  return merged
4739
 
4740
  except Exception as e: