Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -108,11 +108,11 @@ Primary goals for reconcile rate:
|
|
| 108 |
earlier larger DA3 table — catches OCR-truncated description duplicates such as
|
| 109 |
the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
|
| 110 |
full service-fee table with a longer description string.
|
| 111 |
-
- Fix 32: Headless
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
"""
|
| 117 |
|
| 118 |
# Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
|
|
@@ -610,8 +610,25 @@ def _fill_headless_trailing_amount_headers(grid):
|
|
| 610 |
if (amt_hits / total) < 0.45:
|
| 611 |
continue
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
|
| 616 |
grid[0] = new_header
|
| 617 |
return grid
|
|
@@ -622,38 +639,84 @@ _MMDD_DATE_RE = re.compile(r"^\d{1,2}[-/]\d{2}(?:[-/]\d{2,4})?$")
|
|
| 622 |
# Leading transaction-type cell when OCR splits category vs merchant (East West / similar).
|
| 623 |
_STACKED_TXN_TYPE_RE = re.compile(
|
| 624 |
r"^(?:Pre-Auth Credit|Pre-Auth Debit|Preauth Debit|"
|
| 625 |
-
r"Withdrawal Trns|Deposit(?:\s|$)|
|
|
|
|
| 626 |
r"Service Charge|ACH Debit|ACH Credit|ACH Payment|ACH PMT|"
|
| 627 |
r"Wire Transfer|POS Purchase|ATM Withdrawal|Transfer Trns)\b",
|
| 628 |
re.IGNORECASE | re.DOTALL,
|
| 629 |
)
|
| 630 |
|
| 631 |
|
| 632 |
-
def
|
| 633 |
"""
|
| 634 |
-
Fix 32b:
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
"""
|
| 639 |
if not grid or len(grid) < 3:
|
| 640 |
return grid
|
| 641 |
|
| 642 |
header = [str(c or "").strip() for c in grid[0]]
|
| 643 |
-
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
h2 = re.sub(r"\s+", " ", header[2]).strip().lower()
|
| 649 |
-
h3 = str(header[3] or "").strip().lower()
|
| 650 |
|
| 651 |
-
if
|
| 652 |
return grid
|
| 653 |
-
if "transaction" not in
|
| 654 |
return grid
|
| 655 |
if not any(
|
| 656 |
-
k in
|
| 657 |
for k in (
|
| 658 |
"addition",
|
| 659 |
"subtraction",
|
|
@@ -664,58 +727,83 @@ def _collapse_four_col_stacked_transaction_type_to_three(grid):
|
|
| 664 |
)
|
| 665 |
):
|
| 666 |
return grid
|
| 667 |
-
if
|
| 668 |
-
k in
|
| 669 |
):
|
| 670 |
return grid
|
| 671 |
|
| 672 |
-
def _row_matches_split_pattern(cells):
|
| 673 |
-
while len(cells) < 4:
|
| 674 |
-
cells = cells + [""]
|
| 675 |
-
c0, c1, c2, c3 = (
|
| 676 |
-
str(cells[0] or "").strip(),
|
| 677 |
-
str(cells[1] or "").strip(),
|
| 678 |
-
str(cells[2] or "").strip(),
|
| 679 |
-
str(cells[3] or "").strip(),
|
| 680 |
-
)
|
| 681 |
-
if not c0 or not _MMDD_DATE_RE.match(c0):
|
| 682 |
-
return False
|
| 683 |
-
if not c1 or not _STACKED_TXN_TYPE_RE.match(c1):
|
| 684 |
-
return False
|
| 685 |
-
if not c2:
|
| 686 |
-
return False
|
| 687 |
-
if _is_amount_like(c2):
|
| 688 |
-
return False
|
| 689 |
-
if not _is_amount_like(c3):
|
| 690 |
-
return False
|
| 691 |
-
return True
|
| 692 |
-
|
| 693 |
data_rows = [r for r in grid[1:] if any(str(c or "").strip() for c in r)]
|
| 694 |
-
if
|
| 695 |
return grid
|
| 696 |
|
|
|
|
| 697 |
for row in data_rows:
|
| 698 |
-
|
| 699 |
-
if
|
| 700 |
-
|
|
|
|
|
|
|
|
|
|
| 701 |
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
for row in grid[1:]:
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
continue
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
return out
|
| 721 |
|
|
@@ -2808,7 +2896,7 @@ def normalize_html_tables(text: str) -> str:
|
|
| 2808 |
grid = _merge_blank_header_text_columns(grid)
|
| 2809 |
|
| 2810 |
grid = _fill_headless_trailing_amount_headers(grid)
|
| 2811 |
-
grid =
|
| 2812 |
|
| 2813 |
grid = _reconstruct_separator_rows(grid)
|
| 2814 |
|
|
@@ -4649,6 +4737,69 @@ def _strip_check_image_ocr_junk_html(text: str) -> str:
|
|
| 4649 |
return text
|
| 4650 |
|
| 4651 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4652 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 4653 |
if not page_md:
|
| 4654 |
return page_md
|
|
@@ -4672,6 +4823,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 4672 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 4673 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 4674 |
stabilized = normalize_html_tables(stabilized)
|
|
|
|
| 4675 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 4676 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 4677 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
@@ -4804,6 +4956,7 @@ def run_ocr(uploaded_file):
|
|
| 4804 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 4805 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 4806 |
stabilized = normalize_html_tables(stabilized)
|
|
|
|
| 4807 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 4808 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 4809 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
|
|
| 108 |
earlier larger DA3 table — catches OCR-truncated description duplicates such as
|
| 109 |
the BoA service-fee page where a VIDDYOZE -1.11 fragment table appears after the
|
| 110 |
full service-fee table with a longer description string.
|
| 111 |
+
- Fix 32: Headless trailing <th> over currency cells — reuse the prior money header (Additions/
|
| 112 |
+
Subtractions) when present, else \"Amount\". Fix 32b aligns Date|Transaction Description|
|
| 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
|
|
|
|
| 610 |
if (amt_hits / total) < 0.45:
|
| 611 |
continue
|
| 612 |
|
| 613 |
+
left = j - 1
|
| 614 |
+
while left >= 0 and not new_header[left]:
|
| 615 |
+
left -= 1
|
| 616 |
+
prev = (new_header[left] if left >= 0 else "").strip()
|
| 617 |
+
pl = prev.lower()
|
| 618 |
+
if any(
|
| 619 |
+
k in pl
|
| 620 |
+
for k in (
|
| 621 |
+
"addition",
|
| 622 |
+
"subtraction",
|
| 623 |
+
"credit",
|
| 624 |
+
"debit",
|
| 625 |
+
"deposit",
|
| 626 |
+
"withdrawal",
|
| 627 |
+
)
|
| 628 |
+
):
|
| 629 |
+
new_header[j] = prev
|
| 630 |
+
else:
|
| 631 |
+
new_header[j] = "Amount"
|
| 632 |
|
| 633 |
grid[0] = new_header
|
| 634 |
return grid
|
|
|
|
| 639 |
# Leading transaction-type cell when OCR splits category vs merchant (East West / similar).
|
| 640 |
_STACKED_TXN_TYPE_RE = re.compile(
|
| 641 |
r"^(?:Pre-Auth Credit|Pre-Auth Debit|Preauth Debit|"
|
| 642 |
+
r"Withdrawal Trns|Withdrawal(?:\s|$)|Deposit(?:\s|$)|Deposit Transfe|"
|
| 643 |
+
r"Outgoing Wire|Incoming Wire|"
|
| 644 |
r"Service Charge|ACH Debit|ACH Credit|ACH Payment|ACH PMT|"
|
| 645 |
r"Wire Transfer|POS Purchase|ATM Withdrawal|Transfer Trns)\b",
|
| 646 |
re.IGNORECASE | re.DOTALL,
|
| 647 |
)
|
| 648 |
|
| 649 |
|
| 650 |
+
def _normalize_date_desc_money_table_alignment(grid):
|
| 651 |
"""
|
| 652 |
+
Fix 32b: Align OCR tables with common bank PDF layouts.
|
| 653 |
+
|
| 654 |
+
* Credits (Additions): Number | Date | Transaction Description | Additions — Number is often
|
| 655 |
+
blank in the PDF but must exist as a column; OCR may omit it or split description across
|
| 656 |
+
columns and park amounts under a bogus \"Amount\" header.
|
| 657 |
+
* Debits (Subtractions): PDF is usually Date | Description | Subtractions (no Number) —
|
| 658 |
+
when OCR uses the same 4-column split error, merge back to three columns.
|
| 659 |
+
|
| 660 |
+
Handles (a) Date | short type | merchant tail | amount, (b) Date | full desc | empty | amount,
|
| 661 |
+
(c) continuation rows with a blank date. Per-row merge (no single global mode) so mixed OCR
|
| 662 |
+
errors (e.g. wrapped merchant lines) still normalize.
|
| 663 |
"""
|
| 664 |
if not grid or len(grid) < 3:
|
| 665 |
return grid
|
| 666 |
|
| 667 |
header = [str(c or "").strip() for c in grid[0]]
|
| 668 |
+
n = len(header)
|
| 669 |
+
|
| 670 |
+
def _pad_row(cells, width):
|
| 671 |
+
cells = [str(c or "").strip() for c in cells]
|
| 672 |
+
while len(cells) < width:
|
| 673 |
+
cells.append("")
|
| 674 |
+
return cells[:width]
|
| 675 |
+
|
| 676 |
+
h0n = re.sub(r"\s+", " ", header[0]).strip().lower()
|
| 677 |
+
h1n = re.sub(r"\s+", " ", header[1]).strip().lower() if n > 1 else ""
|
| 678 |
+
h2n = re.sub(r"\s+", " ", header[2]).strip().lower() if n > 2 else ""
|
| 679 |
+
h3n = re.sub(r"\s+", " ", header[3]).strip().lower() if n > 3 else ""
|
| 680 |
+
|
| 681 |
+
money_label = header[2].strip() if n > 2 else ""
|
| 682 |
+
want_number_col = "addition" in h2n
|
| 683 |
+
|
| 684 |
+
# --- 3-column Date | Transaction Description | Additions/Subtractions: prepend Number for credits only ---
|
| 685 |
+
if n == 3:
|
| 686 |
+
if h0n != "date" or "transaction" not in h1n or "description" not in h1n:
|
| 687 |
+
return grid
|
| 688 |
+
if not any(
|
| 689 |
+
k in h2n
|
| 690 |
+
for k in (
|
| 691 |
+
"addition",
|
| 692 |
+
"subtraction",
|
| 693 |
+
"credit",
|
| 694 |
+
"debit",
|
| 695 |
+
"deposit",
|
| 696 |
+
"withdrawal",
|
| 697 |
+
)
|
| 698 |
+
):
|
| 699 |
+
return grid
|
| 700 |
+
if not want_number_col:
|
| 701 |
+
return grid
|
| 702 |
+
out = [["Number", header[0], header[1], money_label or "Additions"]]
|
| 703 |
+
for row in grid[1:]:
|
| 704 |
+
c = _pad_row(row, 3)
|
| 705 |
+
if not any(c):
|
| 706 |
+
out.append(["", "", "", ""])
|
| 707 |
+
continue
|
| 708 |
+
out.append(["", c[0], c[1], c[2]])
|
| 709 |
+
return out
|
| 710 |
|
| 711 |
+
if n != 4:
|
| 712 |
+
return grid
|
|
|
|
|
|
|
| 713 |
|
| 714 |
+
if h0n != "date":
|
| 715 |
return grid
|
| 716 |
+
if "transaction" not in h1n or "description" not in h1n:
|
| 717 |
return grid
|
| 718 |
if not any(
|
| 719 |
+
k in h2n
|
| 720 |
for k in (
|
| 721 |
"addition",
|
| 722 |
"subtraction",
|
|
|
|
| 727 |
)
|
| 728 |
):
|
| 729 |
return grid
|
| 730 |
+
if h3n not in ("", "amount") and not any(
|
| 731 |
+
k in h3n for k in ("addition", "subtraction", "credit", "debit")
|
| 732 |
):
|
| 733 |
return grid
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
data_rows = [r for r in grid[1:] if any(str(c or "").strip() for c in r)]
|
| 736 |
+
if not data_rows:
|
| 737 |
return grid
|
| 738 |
|
| 739 |
+
has_date_led = False
|
| 740 |
for row in data_rows:
|
| 741 |
+
c = _pad_row(row, 4)
|
| 742 |
+
if c[0] and _MMDD_DATE_RE.match(c[0]):
|
| 743 |
+
has_date_led = True
|
| 744 |
+
break
|
| 745 |
+
if not has_date_led:
|
| 746 |
+
return grid
|
| 747 |
|
| 748 |
+
def _merge_date_led_row(c0, c1, c2, c3):
|
| 749 |
+
split_ok = (
|
| 750 |
+
bool(c1)
|
| 751 |
+
and bool(_STACKED_TXN_TYPE_RE.match(c1))
|
| 752 |
+
and bool(c2)
|
| 753 |
+
and not _is_amount_like(c2)
|
| 754 |
+
and _is_amount_like(c3)
|
| 755 |
+
)
|
| 756 |
+
if split_ok:
|
| 757 |
+
return (c1 + " " + c2).strip(), c3
|
| 758 |
+
shift_ok = (
|
| 759 |
+
bool(c1)
|
| 760 |
+
and not _is_amount_like(c1)
|
| 761 |
+
and not (c2 or "").strip()
|
| 762 |
+
and _is_amount_like(c3)
|
| 763 |
+
)
|
| 764 |
+
if shift_ok:
|
| 765 |
+
return c1, c3
|
| 766 |
+
dep_ok = (
|
| 767 |
+
bool(c1)
|
| 768 |
+
and bool(_STACKED_TXN_TYPE_RE.match(c1))
|
| 769 |
+
and not (c2 or "").strip()
|
| 770 |
+
and _is_amount_like(c3)
|
| 771 |
+
)
|
| 772 |
+
if dep_ok:
|
| 773 |
+
return c1, c3
|
| 774 |
+
desc = ((c1 + " " + c2).strip() or c1).strip()
|
| 775 |
+
amt = c3 if _is_amount_like(c3) else ""
|
| 776 |
+
return desc, amt
|
| 777 |
+
|
| 778 |
+
ml = money_label or ("Additions" if want_number_col else "Subtractions")
|
| 779 |
+
|
| 780 |
+
if want_number_col:
|
| 781 |
+
out_h = ["Number", header[0], header[1], ml]
|
| 782 |
+
else:
|
| 783 |
+
out_h = [header[0], header[1], ml]
|
| 784 |
+
|
| 785 |
+
out = [out_h]
|
| 786 |
for row in grid[1:]:
|
| 787 |
+
c = _pad_row(row, 4)
|
| 788 |
+
c0, c1, c2, c3 = c[0], c[1], c[2], c[3]
|
| 789 |
+
if not any(c):
|
| 790 |
+
out.append([""] * len(out_h))
|
| 791 |
+
continue
|
| 792 |
+
if c0 and _MMDD_DATE_RE.match(c0):
|
| 793 |
+
desc, amt = _merge_date_led_row(c0, c1, c2, c3)
|
| 794 |
+
if want_number_col:
|
| 795 |
+
out.append(["", c0, desc, amt])
|
| 796 |
+
else:
|
| 797 |
+
out.append([c0, desc, amt])
|
| 798 |
continue
|
| 799 |
+
if want_number_col:
|
| 800 |
+
desc_t = (c1 + " " + c2).strip()
|
| 801 |
+
amt_t = c3 if _is_amount_like(c3) else ""
|
| 802 |
+
out.append(["", c0, desc_t, amt_t])
|
| 803 |
+
else:
|
| 804 |
+
desc_t = (c1 + " " + c2).strip()
|
| 805 |
+
amt_t = c3 if _is_amount_like(c3) else ""
|
| 806 |
+
out.append([c0, desc_t, amt_t])
|
| 807 |
|
| 808 |
return out
|
| 809 |
|
|
|
|
| 2896 |
grid = _merge_blank_header_text_columns(grid)
|
| 2897 |
|
| 2898 |
grid = _fill_headless_trailing_amount_headers(grid)
|
| 2899 |
+
grid = _normalize_date_desc_money_table_alignment(grid)
|
| 2900 |
|
| 2901 |
grid = _reconstruct_separator_rows(grid)
|
| 2902 |
|
|
|
|
| 4737 |
return text
|
| 4738 |
|
| 4739 |
|
| 4740 |
+
def _dedupe_adjacent_duplicate_html_tables(text: str) -> str:
|
| 4741 |
+
"""
|
| 4742 |
+
Drop consecutive <table> blocks that are identical after grid parse (same header + row multiset),
|
| 4743 |
+
with only whitespace between them. Catches OCR echo where the same credits/debits grid is emitted twice.
|
| 4744 |
+
"""
|
| 4745 |
+
if not text or "<table" not in text.lower():
|
| 4746 |
+
return text
|
| 4747 |
+
tbl_pat = re.compile(r"<table[^>]*>.*?</table>", re.DOTALL | re.IGNORECASE)
|
| 4748 |
+
matches = list(tbl_pat.finditer(text))
|
| 4749 |
+
if len(matches) < 2:
|
| 4750 |
+
return text
|
| 4751 |
+
|
| 4752 |
+
def _fingerprint(html: str):
|
| 4753 |
+
try:
|
| 4754 |
+
p = TableGridParser()
|
| 4755 |
+
p.feed(html)
|
| 4756 |
+
g = _build_grid(p.rows)
|
| 4757 |
+
if not g or len(g) < 3:
|
| 4758 |
+
return None
|
| 4759 |
+
hdr = tuple(str(c or "").strip() for c in g[0])
|
| 4760 |
+
body = []
|
| 4761 |
+
for r in g[1:]:
|
| 4762 |
+
if not any(str(c or "").strip() for c in r):
|
| 4763 |
+
continue
|
| 4764 |
+
body.append(tuple(str(c or "").strip() for c in r))
|
| 4765 |
+
if len(body) < 2:
|
| 4766 |
+
return None
|
| 4767 |
+
return (hdr, tuple(sorted(Counter(body).items())))
|
| 4768 |
+
except Exception:
|
| 4769 |
+
return None
|
| 4770 |
+
|
| 4771 |
+
def _gap_ok(ma, mb) -> bool:
|
| 4772 |
+
gap = text[ma.end() : mb.start()]
|
| 4773 |
+
return not re.search(r"<table", gap, re.IGNORECASE)
|
| 4774 |
+
|
| 4775 |
+
drop = set()
|
| 4776 |
+
ref = 0
|
| 4777 |
+
for j in range(1, len(matches)):
|
| 4778 |
+
if j in drop:
|
| 4779 |
+
continue
|
| 4780 |
+
fp_r = _fingerprint(matches[ref].group(0))
|
| 4781 |
+
fp_j = _fingerprint(matches[j].group(0))
|
| 4782 |
+
if fp_r is not None and fp_j is not None and fp_r == fp_j and _gap_ok(
|
| 4783 |
+
matches[ref], matches[j]
|
| 4784 |
+
):
|
| 4785 |
+
drop.add(j)
|
| 4786 |
+
else:
|
| 4787 |
+
ref = j
|
| 4788 |
+
|
| 4789 |
+
if not drop:
|
| 4790 |
+
return text
|
| 4791 |
+
|
| 4792 |
+
out: List[str] = []
|
| 4793 |
+
pos = 0
|
| 4794 |
+
for i, m in enumerate(matches):
|
| 4795 |
+
out.append(text[pos : m.start()])
|
| 4796 |
+
if i not in drop:
|
| 4797 |
+
out.append(m.group(0))
|
| 4798 |
+
pos = m.end()
|
| 4799 |
+
out.append(text[pos:])
|
| 4800 |
+
return "".join(out)
|
| 4801 |
+
|
| 4802 |
+
|
| 4803 |
def stabilize_tables_and_text(page_md: str) -> str:
|
| 4804 |
if not page_md:
|
| 4805 |
return page_md
|
|
|
|
| 4823 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 4824 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 4825 |
stabilized = normalize_html_tables(stabilized)
|
| 4826 |
+
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
| 4827 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 4828 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 4829 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|
|
|
|
| 4956 |
stabilized = _strip_orphan_continued_da3_flatline(stabilized)
|
| 4957 |
stabilized = _strip_standalone_continued_banner_line(stabilized)
|
| 4958 |
stabilized = normalize_html_tables(stabilized)
|
| 4959 |
+
stabilized = _dedupe_adjacent_duplicate_html_tables(stabilized)
|
| 4960 |
stabilized = _trim_adjacent_da3_suffix_duplicating_next_table_prefix(stabilized)
|
| 4961 |
stabilized = _split_ucb_deposits_electronic_sections_html(stabilized)
|
| 4962 |
stabilized = _patch_da3_msft_g_ref_amounts_across_tables(stabilized)
|