rehan953 commited on
Commit
d16a54d
·
verified ·
1 Parent(s): a47bd8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -0
app.py CHANGED
@@ -59,6 +59,12 @@ Primary goals for reconcile rate:
59
  when the preceding DA3 table was under a deposits/credits heading. Only strips the
60
  contiguous prefix of the debit table whose rows appear in the credit table; requires
61
  ≥1 data row to remain; entire function wrapped in try/except (safe no-op on any error).
 
 
 
 
 
 
62
  """
63
 
64
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -3510,6 +3516,122 @@ def _strip_credits_prefix_from_debit_da3_table(text: str) -> str:
3510
  return text
3511
 
3512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3513
  def stabilize_tables_and_text(page_md: str) -> str:
3514
  if not page_md:
3515
  return page_md
@@ -3535,6 +3657,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
3535
  stabilized = _dedupe_subset_transaction_tables_html(stabilized)
3536
  stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
3537
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
 
3538
  stabilized = _strip_orphan_continued_da3_flatline(stabilized)
3539
  stabilized = _strip_standalone_continued_banner_line(stabilized)
3540
  return close_unclosed_html(stabilized)
@@ -3662,6 +3785,7 @@ def run_ocr(uploaded_file):
3662
  stabilized = _dedupe_subset_transaction_tables_html(stabilized)
3663
  stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
3664
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
 
3665
  stabilized = _strip_standalone_continued_banner_line(stabilized)
3666
  elif is_pdf and is_nfcu_doc:
3667
  nfcu_summary = _extract_nfcu_summary_table(path, page_num)
@@ -3737,6 +3861,7 @@ def run_ocr(uploaded_file):
3737
  merged = _dedupe_subset_transaction_tables_html(merged)
3738
  merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
3739
  merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
 
3740
  merged = _strip_orphan_continued_da3_flatline(merged)
3741
  merged = _strip_standalone_continued_banner_line(merged)
3742
  return merged
 
59
  when the preceding DA3 table was under a deposits/credits heading. Only strips the
60
  contiguous prefix of the debit table whose rows appear in the credit table; requires
61
  ≥1 data row to remain; entire function wrapped in try/except (safe no-op on any error).
62
+ - Fix 29: Move a misplaced debit/withdrawal heading to before its orphan DA3 table when OCR
63
+ places the heading AFTER the table instead of before it. Pattern detected:
64
+ [credit heading+table] ... [orphan DA3 table] ... [debit heading] — the heading is
65
+ relocated to just before the orphan table. Guards: no debit heading already before
66
+ table; credit context must precede table; no other table between orphan and heading;
67
+ try/except wrapper (safe no-op on any error).
68
  """
69
 
70
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
3516
  return text
3517
 
3518
 
3519
+ # --------------------------
3520
+ # Fix 29: Move misplaced debit heading to before its orphan DA3 table
3521
+ # --------------------------
3522
+
3523
+ # Centered div heading pattern (BoA / generic style)
3524
+ _CENTER_DIV_HDR_RE = re.compile(
3525
+ r'<div\s+align=["\']center["\']\s*>\s*(.*?)\s*</div>',
3526
+ re.IGNORECASE | re.DOTALL,
3527
+ )
3528
+ _HEADING_SCAN_FORWARD = 800 # chars after table end to search for misplaced heading
3529
+
3530
+
3531
+ def _fix29_move_debit_heading_before_orphan_table(text: str) -> str:
3532
+ """
3533
+ Fix 29: When OCR places a debit/withdrawal section heading AFTER the withdrawals table
3534
+ instead of before it, move the heading to its correct position (just before the table).
3535
+
3536
+ Pattern detected:
3537
+ [credit/deposit DA3 table] ...no debit heading... [orphan DA3 table] [debit heading]
3538
+ where no other DA3 table separates the orphan table from the debit heading.
3539
+
3540
+ Action: move the debit heading to just before the orphan DA3 table.
3541
+
3542
+ Safety guards (ALL must pass):
3543
+ 1. Orphan table must be a valid DA3 (Date|Description|Amount).
3544
+ 2. A debit heading must appear AFTER the orphan table within _HEADING_SCAN_FORWARD chars.
3545
+ 3. No debit heading appears immediately before the orphan table (within _HEADING_SCAN_WINDOW).
3546
+ 4. A credit/deposit heading must exist before the orphan table (within 1200 chars).
3547
+ 5. No other table appears between the orphan table end and the debit heading.
3548
+ 6. Entire function wrapped in try/except -- returns input unchanged on any error.
3549
+ """
3550
+ if not text or "<table" not in text.lower():
3551
+ return text
3552
+ try:
3553
+ tbl_pat = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
3554
+ matches = list(tbl_pat.finditer(text))
3555
+ if len(matches) < 2:
3556
+ return text
3557
+
3558
+ da3_matches = [m for m in matches if _parse_da3_grid_from_table_html(m.group(0))]
3559
+ if len(da3_matches) < 2:
3560
+ return text
3561
+
3562
+ moves = []
3563
+
3564
+ for i in range(1, len(da3_matches)):
3565
+ orphan_m = da3_matches[i]
3566
+ orphan_start = orphan_m.start()
3567
+ orphan_end = orphan_m.end()
3568
+
3569
+ # Guard 3: no debit heading already before the orphan table
3570
+ before_window = text[max(0, orphan_start - _HEADING_SCAN_WINDOW): orphan_start]
3571
+ before_clean = re.sub(r"<[^>]+>", " ", before_window)
3572
+ lines_before = [l.strip() for l in before_clean.splitlines() if l.strip()]
3573
+ last_meaningful = ""
3574
+ for l in reversed(lines_before):
3575
+ if len(l) > 3 and "---" not in l:
3576
+ last_meaningful = l
3577
+ break
3578
+ if _DEBIT_HEADING_KEYWORDS.search(last_meaningful):
3579
+ continue # already has debit heading before it
3580
+
3581
+ # Guard 4: credit context must precede the orphan table
3582
+ big_before = text[max(0, orphan_start - 1200): orphan_start]
3583
+ if not _CREDIT_HEADING_KEYWORDS.search(re.sub(r"<[^>]+>", " ", big_before)):
3584
+ continue
3585
+
3586
+ # Guard 5: no other table between orphan end and the potential debit heading
3587
+ next_tbl_m = tbl_pat.search(text, orphan_end)
3588
+
3589
+ # Guard 2: find debit heading after the orphan table (within forward window)
3590
+ dh_start = dh_end = None
3591
+ for div_m in _CENTER_DIV_HDR_RE.finditer(text, orphan_end, orphan_end + _HEADING_SCAN_FORWARD):
3592
+ div_clean = re.sub(r"<[^>]+>", "", div_m.group(1)).strip()
3593
+ if _DEBIT_HEADING_KEYWORDS.search(div_clean) and not _CREDIT_HEADING_KEYWORDS.search(div_clean):
3594
+ # Guard 5: no table should be between orphan_end and this heading
3595
+ if next_tbl_m and next_tbl_m.start() < div_m.start():
3596
+ break # another table intervenes
3597
+ dh_start = div_m.start()
3598
+ dh_end = div_m.end()
3599
+ break
3600
+
3601
+ if dh_start is None:
3602
+ continue
3603
+
3604
+ moves.append((orphan_start, dh_start, dh_end))
3605
+
3606
+ if not moves:
3607
+ return text
3608
+
3609
+ # Apply moves in reverse order to preserve offsets
3610
+ for orphan_start, dh_start, dh_end in reversed(moves):
3611
+ orphan_m2 = next((m for m in tbl_pat.finditer(text) if m.start() == orphan_start), None)
3612
+ if not orphan_m2:
3613
+ continue
3614
+ orphan_end = orphan_m2.end()
3615
+
3616
+ heading_html = text[dh_start: dh_end]
3617
+ gap = text[orphan_end: dh_start]
3618
+ orphan_html = text[orphan_start: orphan_end]
3619
+ part_before = text[:orphan_start]
3620
+ part_after = text[dh_end:]
3621
+
3622
+ text = part_before + heading_html + "\n\n" + orphan_html + gap.rstrip() + part_after
3623
+ log.info(
3624
+ "Fix 29: moved debit heading (was at offset %d) to before orphan DA3 table (offset %d)",
3625
+ dh_start, orphan_start,
3626
+ )
3627
+
3628
+ return text
3629
+
3630
+ except Exception as e:
3631
+ log.warning("_fix29_move_debit_heading_before_orphan_table failed: %s", e)
3632
+ return text
3633
+
3634
+
3635
  def stabilize_tables_and_text(page_md: str) -> str:
3636
  if not page_md:
3637
  return page_md
 
3657
  stabilized = _dedupe_subset_transaction_tables_html(stabilized)
3658
  stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
3659
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
3660
+ stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
3661
  stabilized = _strip_orphan_continued_da3_flatline(stabilized)
3662
  stabilized = _strip_standalone_continued_banner_line(stabilized)
3663
  return close_unclosed_html(stabilized)
 
3785
  stabilized = _dedupe_subset_transaction_tables_html(stabilized)
3786
  stabilized = _drop_da3_misplaced_debit_after_deposit_heading_tables(stabilized)
3787
  stabilized = _strip_credits_prefix_from_debit_da3_table(stabilized) # Fix 28
3788
+ stabilized = _fix29_move_debit_heading_before_orphan_table(stabilized) # Fix 29
3789
  stabilized = _strip_standalone_continued_banner_line(stabilized)
3790
  elif is_pdf and is_nfcu_doc:
3791
  nfcu_summary = _extract_nfcu_summary_table(path, page_num)
 
3861
  merged = _dedupe_subset_transaction_tables_html(merged)
3862
  merged = _drop_da3_misplaced_debit_after_deposit_heading_tables(merged)
3863
  merged = _strip_credits_prefix_from_debit_da3_table(merged) # Fix 28
3864
+ merged = _fix29_move_debit_heading_before_orphan_table(merged) # Fix 29
3865
  merged = _strip_orphan_continued_da3_flatline(merged)
3866
  merged = _strip_standalone_continued_banner_line(merged)
3867
  return merged