Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -30,6 +30,10 @@ Primary goals for reconcile rate:
|
|
| 30 |
Electronic Debits (PDF text order); drop orphan empty EC/ED headers that followed the merged table
|
| 31 |
- Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
|
| 32 |
(later subset of earlier, earlier subset of later, or identical row sets)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
|
| 35 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -3133,7 +3137,9 @@ def _is_da3_header(grid):
|
|
| 3133 |
h = [re.sub(r"\s+", " ", str(c or "").strip().lower()) for c in grid[0]]
|
| 3134 |
if len(h) != 3:
|
| 3135 |
return False
|
| 3136 |
-
return h[0] == "date" and "description" in h[1] and
|
|
|
|
|
|
|
| 3137 |
|
| 3138 |
|
| 3139 |
def _dedupe_duplicate_rows_in_da3_table(grid):
|
|
@@ -3266,6 +3272,77 @@ def _split_ucb_deposits_electronic_sections_html(text: str) -> str:
|
|
| 3266 |
return _UCB_DEP_CONT_BLOCK.sub(_repl, text)
|
| 3267 |
|
| 3268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3269 |
def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
| 3270 |
"""
|
| 3271 |
Remove duplicate 3-column Date|Description|Amount tables:
|
|
@@ -3275,7 +3352,6 @@ def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
|
| 3275 |
- Identical row sets β drop the later table.
|
| 3276 |
- Fuzzy overlap (β₯92% of later rows match earlier) β drop later (Electronic Debits vs main).
|
| 3277 |
- Runs multiple passes until stable.
|
| 3278 |
-
|
| 3279 |
Row matching uses normalized keys so OCR variants (e.g. "0 27" vs "027", backticks)
|
| 3280 |
do not prevent duplicate tables from being detected.
|
| 3281 |
"""
|
|
@@ -3397,6 +3473,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 3397 |
stabilized = "\n\n".join(out_blocks)
|
| 3398 |
stabilized = convert_plaintext_bank_sections(stabilized)
|
| 3399 |
stabilized = normalize_html_tables(stabilized)
|
|
|
|
| 3400 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3401 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3402 |
return close_unclosed_html(stabilized)
|
|
@@ -3525,6 +3602,7 @@ def run_ocr(uploaded_file):
|
|
| 3525 |
# _patch_ocr_with_textlayer may append Case B rows from the PDF text layer
|
| 3526 |
# with fused UCB-style cells; run the same HTML table pass again (Fix 13/14).
|
| 3527 |
stabilized = normalize_html_tables(stabilized)
|
|
|
|
| 3528 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3529 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3530 |
elif is_pdf and is_nfcu_doc:
|
|
@@ -3607,6 +3685,7 @@ def run_ocr(uploaded_file):
|
|
| 3607 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 3608 |
# Cross-page: UCB section split + same transaction tables dedupe once on full doc.
|
| 3609 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
|
|
|
| 3610 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 3611 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 3612 |
return merged
|
|
@@ -3633,4 +3712,3 @@ with gr.Blocks(title="GLM-OCR") as demo:
|
|
| 3633 |
|
| 3634 |
if __name__ == "__main__":
|
| 3635 |
demo.launch()
|
| 3636 |
-
|
|
|
|
| 30 |
Electronic Debits (PDF text order); drop orphan empty EC/ED headers that followed the merged table
|
| 31 |
- Post-pass: dedupe 3-col Date|Description|Amount tables when one is a duplicate fragment
|
| 32 |
(later subset of earlier, earlier subset of later, or identical row sets)
|
| 33 |
+
- Fix 18: adjacent DA3 tables β if the last K data rows of table N match the first K rows of table N+1
|
| 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
|
|
|
|
| 3137 |
h = [re.sub(r"\s+", " ", str(c or "").strip().lower()) for c in grid[0]]
|
| 3138 |
if len(h) != 3:
|
| 3139 |
return False
|
| 3140 |
+
return h[0] == "date" and "description" in h[1] and (
|
| 3141 |
+
h[2] == "amount" or h[2].startswith("amount")
|
| 3142 |
+
)
|
| 3143 |
|
| 3144 |
|
| 3145 |
def _dedupe_duplicate_rows_in_da3_table(grid):
|
|
|
|
| 3272 |
return _UCB_DEP_CONT_BLOCK.sub(_repl, text)
|
| 3273 |
|
| 3274 |
|
| 3275 |
+
def _parse_da3_grid_from_table_html(table_html: str):
|
| 3276 |
+
"""Return grid if table is Date|Description|Amount; else None."""
|
| 3277 |
+
try:
|
| 3278 |
+
p = TableGridParser()
|
| 3279 |
+
p.feed(table_html)
|
| 3280 |
+
grid = _build_grid(p.rows)
|
| 3281 |
+
if grid and _is_da3_header(grid):
|
| 3282 |
+
return grid
|
| 3283 |
+
except Exception:
|
| 3284 |
+
pass
|
| 3285 |
+
return None
|
| 3286 |
+
|
| 3287 |
+
|
| 3288 |
+
def _trim_adjacent_da3_suffix_duplicating_next_table_prefix(text: str) -> str:
|
| 3289 |
+
"""
|
| 3290 |
+
Remove trailing data rows from a DA3 table when they are repeated as the opening
|
| 3291 |
+
rows of the immediately following DA3 table (OCR/LLM glued the next section onto
|
| 3292 |
+
the prior table). Requires overlap length >= 2 and leaves at least one data row
|
| 3293 |
+
in the first table. Safe no-op when patterns do not match.
|
| 3294 |
+
"""
|
| 3295 |
+
if not text or "<table" not in text.lower():
|
| 3296 |
+
return text
|
| 3297 |
+
|
| 3298 |
+
pattern = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 3299 |
+
|
| 3300 |
+
for _ in range(48):
|
| 3301 |
+
matches = list(pattern.finditer(text))
|
| 3302 |
+
if len(matches) < 2:
|
| 3303 |
+
break
|
| 3304 |
+
changed = False
|
| 3305 |
+
for i in range(len(matches) - 1):
|
| 3306 |
+
g1 = _parse_da3_grid_from_table_html(matches[i].group(0))
|
| 3307 |
+
g2 = _parse_da3_grid_from_table_html(matches[i + 1].group(0))
|
| 3308 |
+
if not g1 or not g2 or len(g1) < 3 or len(g2) < 2:
|
| 3309 |
+
continue
|
| 3310 |
+
d1 = g1[1:]
|
| 3311 |
+
d2 = g2[1:]
|
| 3312 |
+
if len(d1) < 2 or len(d2) < 2:
|
| 3313 |
+
continue
|
| 3314 |
+
max_k = min(len(d1), len(d2))
|
| 3315 |
+
best_k = 0
|
| 3316 |
+
for k in range(max_k, 1, -1):
|
| 3317 |
+
ok = True
|
| 3318 |
+
for j in range(k):
|
| 3319 |
+
c1 = [str(x or "").strip() for x in d1[-k + j][:3]]
|
| 3320 |
+
c2 = [str(x or "").strip() for x in d2[j][:3]]
|
| 3321 |
+
while len(c1) < 3:
|
| 3322 |
+
c1.append("")
|
| 3323 |
+
while len(c2) < 3:
|
| 3324 |
+
c2.append("")
|
| 3325 |
+
if _transaction_row_dedupe_key(c1) != _transaction_row_dedupe_key(c2):
|
| 3326 |
+
ok = False
|
| 3327 |
+
break
|
| 3328 |
+
if ok:
|
| 3329 |
+
best_k = k
|
| 3330 |
+
break
|
| 3331 |
+
if best_k < 2:
|
| 3332 |
+
continue
|
| 3333 |
+
if len(d1) - best_k < 1:
|
| 3334 |
+
continue
|
| 3335 |
+
new_grid = [g1[0]] + d1[:-best_k]
|
| 3336 |
+
new_html = _grid_to_html(new_grid)
|
| 3337 |
+
m = matches[i]
|
| 3338 |
+
text = text[: m.start()] + new_html + text[m.end() :]
|
| 3339 |
+
changed = True
|
| 3340 |
+
break
|
| 3341 |
+
if not changed:
|
| 3342 |
+
break
|
| 3343 |
+
return text
|
| 3344 |
+
|
| 3345 |
+
|
| 3346 |
def _dedupe_subset_transaction_tables_html(text: str) -> str:
|
| 3347 |
"""
|
| 3348 |
Remove duplicate 3-column Date|Description|Amount tables:
|
|
|
|
| 3352 |
- Identical row sets β drop the later table.
|
| 3353 |
- Fuzzy overlap (β₯92% of later rows match earlier) β drop later (Electronic Debits vs main).
|
| 3354 |
- Runs multiple passes until stable.
|
|
|
|
| 3355 |
Row matching uses normalized keys so OCR variants (e.g. "0 27" vs "027", backticks)
|
| 3356 |
do not prevent duplicate tables from being detected.
|
| 3357 |
"""
|
|
|
|
| 3473 |
stabilized = "\n\n".join(out_blocks)
|
| 3474 |
stabilized = convert_plaintext_bank_sections(stabilized)
|
| 3475 |
stabilized = normalize_html_tables(stabilized)
|
| 3476 |
+
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 3477 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3478 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3479 |
return close_unclosed_html(stabilized)
|
|
|
|
| 3602 |
# _patch_ocr_with_textlayer may append Case B rows from the PDF text layer
|
| 3603 |
# with fused UCB-style cells; run the same HTML table pass again (Fix 13/14).
|
| 3604 |
stabilized = normalize_html_tables(stabilized)
|
| 3605 |
+
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 3606 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 3607 |
stabilized = _dedupe_subset_transaction_tables_html(stabilized)
|
| 3608 |
elif is_pdf and is_nfcu_doc:
|
|
|
|
| 3685 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 3686 |
# Cross-page: UCB section split + same transaction tables dedupe once on full doc.
|
| 3687 |
if all_pages and merged and not merged.startswith("Error") and "<table" in merged.lower():
|
| 3688 |
+
merged = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(merged)
|
| 3689 |
merged = _split_ucb_deposits_electronic_sections_html(merged)
|
| 3690 |
merged = _dedupe_subset_transaction_tables_html(merged)
|
| 3691 |
return merged
|
|
|
|
| 3712 |
|
| 3713 |
if __name__ == "__main__":
|
| 3714 |
demo.launch()
|
|
|