rehan953 commited on
Commit
31f684f
Β·
verified Β·
1 Parent(s): 37508d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -4
app.py CHANGED
@@ -13,6 +13,8 @@ Primary goals for reconcile rate:
13
  4) clean common header artifacts (e.g. "DESCRIPTIONBeginning Balance", "BALANCE$3,447.10")
14
  5) recover fused first data row generically (two-signal guard: text fusion + money fusion must both fire)
15
  6) promote misplaced header row: when real column headers land in a data row, restructure the grid
 
 
16
  """
17
 
18
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -68,7 +70,7 @@ ENABLE_CONTRAST = True
68
  DEFAULT_ZONE_FRAC = 0.12
69
  PDF_HEADER_BAND_FRAC = 0.10
70
 
71
- ENABLE_FOOTER_OCR = False
72
  PDF_FOOTER_BAND_FRAC = 0.88
73
 
74
  MIN_CROP_HEIGHT = 112
@@ -572,6 +574,7 @@ _HEADER_KW_PATTERNS = [
572
  r"OPENING\s+BAL\.?",
573
  ]
574
 
 
575
  # Pre-compiled: each pattern anchored at start, case-insensitive
576
  _HEADER_KW_RES = [
577
  re.compile(r"^(" + p + r")(.*)", re.IGNORECASE | re.DOTALL)
@@ -823,6 +826,100 @@ def _promote_misplaced_header_row(grid):
823
  return new_grid
824
 
825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  # --------------------------
827
  # Normalizer entry point
828
  # --------------------------
@@ -863,6 +960,9 @@ def normalize_html_tables(text: str) -> str:
863
  # Fix 2: misplaced header row promotion (keyword-score guard β€” safe on clean PDFs)
864
  grid = _promote_misplaced_header_row(grid)
865
 
 
 
 
866
  out.append(_grid_to_html(grid) if grid else table_html)
867
  except Exception:
868
  out.append(table_html)
@@ -1007,7 +1107,12 @@ def run_ocr(uploaded_file):
1007
  if page_md and page_md.strip():
1008
  parts.append(stabilize_tables_and_text(page_md.strip()))
1009
 
1010
- # Optional footer
 
 
 
 
 
1011
  if ENABLE_FOOTER_OCR and page_num < len(page_images):
1012
  ftr = ""
1013
  if is_pdf:
@@ -1017,7 +1122,18 @@ def run_ocr(uploaded_file):
1017
  if not (ftr and ftr.strip()):
1018
  ftr = ocr_zone(page_images[page_num], fs, 1.0)
1019
  if ftr and ftr.strip():
1020
- parts.append(normalize_money_glyphs(ftr.strip()))
 
 
 
 
 
 
 
 
 
 
 
1021
 
1022
  if parts:
1023
  all_pages.append("\n\n".join(parts))
@@ -1046,4 +1162,4 @@ with gr.Blocks(title="GLM-OCR") as demo:
1046
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
1047
 
1048
  if __name__ == "__main__":
1049
- demo.launch()
 
13
  4) clean common header artifacts (e.g. "DESCRIPTIONBeginning Balance", "BALANCE$3,447.10")
14
  5) recover fused first data row generically (two-signal guard: text fusion + money fusion must both fire)
15
  6) promote misplaced header row: when real column headers land in a data row, restructure the grid
16
+ 7) fix fused key-value rows in summary sections (e.g. Interest Summary) appended to transaction tables
17
+ - footer extraction enabled on all pages with deduplication guard (no double-print if body OCR already captured it)
18
  """
19
 
20
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
70
  DEFAULT_ZONE_FRAC = 0.12
71
  PDF_HEADER_BAND_FRAC = 0.10
72
 
73
+ ENABLE_FOOTER_OCR = True
74
  PDF_FOOTER_BAND_FRAC = 0.88
75
 
76
  MIN_CROP_HEIGHT = 112
 
574
  r"OPENING\s+BAL\.?",
575
  ]
576
 
577
+
578
  # Pre-compiled: each pattern anchored at start, case-insensitive
579
  _HEADER_KW_RES = [
580
  re.compile(r"^(" + p + r")(.*)", re.IGNORECASE | re.DOTALL)
 
826
  return new_grid
827
 
828
 
829
+
830
+ # --------------------------
831
+ # Fix 3: Fused key-value rows in summary sections
832
+ # --------------------------
833
+
834
+ # Matches a value suffix that looks like a number or percentage fused onto the
835
+ # end of a label string. Examples:
836
+ # "Beginning Interest Rate0.00%" β†’ label="Beginning Interest Rate" value="0.00%"
837
+ # "Number of days in this Period31" β†’ label="Number of days in this Period" value="31"
838
+ # "Interest Earned this Period0.00" β†’ label="Interest Earned this Period" value="0.00"
839
+ # Pattern: label text (non-digit) followed by a numeric/percentage token at the end.
840
+ _FUSED_KV_RE = re.compile(
841
+ r"^(.+?)\s*(-?\d{1,3}(?:,\d{3})*(?:\.\d+)?%?)$",
842
+ re.DOTALL,
843
+ )
844
+
845
+
846
+ def _fix_fused_keyvalue_rows(grid):
847
+ """
848
+ Generic fix for rows where a label and its value are fused into the first
849
+ cell while all other cells in that row are empty.
850
+
851
+ This happens when a bank statement appends a summary section (e.g. Interest
852
+ Summary, Fee Summary) after the main transaction table without a clear
853
+ column separator. The OCR wraps it inside the same table and each line
854
+ becomes a single-cell row like:
855
+
856
+ <td>Beginning Interest Rate0.00%</td> <td></td> <td></td> ...
857
+
858
+ The fix:
859
+ - For every data row where exactly the first cell is non-empty and all
860
+ other cells are empty, AND the first cell ends with a numeric/percentage
861
+ token fused directly onto text (no space separator), split it into
862
+ label (col 0) and value (last column of the table).
863
+ - If the cell already has a natural space before the value it is NOT
864
+ fused β€” leave it alone. This prevents touching normal description rows
865
+ that happen to end with a number.
866
+
867
+ Guard conditions (no-op if not met β€” safe for all other tables):
868
+ - The row must have ALL cells empty except the first.
869
+ - The first cell must match the fused pattern (text immediately followed
870
+ by a digit with no whitespace gap before the digit run).
871
+ - The match must produce a non-empty label AND a non-empty value.
872
+ """
873
+ if not grid or len(grid) < 2:
874
+ return grid
875
+
876
+ ncols = len(grid[0])
877
+ new_grid = [grid[0]] # keep header unchanged
878
+
879
+ for row in grid[1:]:
880
+ # Guard: only process rows where first cell is non-empty and rest are empty
881
+ cells = [str(c or "").strip() for c in row]
882
+ if not cells:
883
+ new_grid.append(row)
884
+ continue
885
+
886
+ first = cells[0]
887
+ rest_empty = all(c == "" for c in cells[1:])
888
+
889
+ if not rest_empty or not first:
890
+ new_grid.append(row)
891
+ continue
892
+
893
+ # Guard: fused pattern requires NO whitespace immediately before the
894
+ # numeric suffix β€” "Rate0.00%" is fused, "Rate 0.00%" is not.
895
+ # We check by requiring the character just before the digit run is NOT
896
+ # a space.
897
+ fused_match = re.search(r"\S\d", first)
898
+ if not fused_match:
899
+ new_grid.append(row)
900
+ continue
901
+
902
+ m = _FUSED_KV_RE.match(first)
903
+ if not m:
904
+ new_grid.append(row)
905
+ continue
906
+
907
+ label = m.group(1).strip()
908
+ value = m.group(2).strip()
909
+
910
+ if not label or not value:
911
+ new_grid.append(row)
912
+ continue
913
+
914
+ # Build the fixed row: label in col 0, value in last col, rest empty
915
+ new_row = [""] * ncols
916
+ new_row[0] = label
917
+ new_row[ncols - 1] = value
918
+ new_grid.append(new_row)
919
+
920
+ return new_grid
921
+
922
+
923
  # --------------------------
924
  # Normalizer entry point
925
  # --------------------------
 
960
  # Fix 2: misplaced header row promotion (keyword-score guard β€” safe on clean PDFs)
961
  grid = _promote_misplaced_header_row(grid)
962
 
963
+ # Fix 3: fused key-value rows in summary sections (no-space guard β€” safe on all PDFs)
964
+ grid = _fix_fused_keyvalue_rows(grid)
965
+
966
  out.append(_grid_to_html(grid) if grid else table_html)
967
  except Exception:
968
  out.append(table_html)
 
1107
  if page_md and page_md.strip():
1108
  parts.append(stabilize_tables_and_text(page_md.strip()))
1109
 
1110
+ # Footer extraction.
1111
+ # Uses the same 3-step fallback as header: PDF text layer -> band words -> OCR crop.
1112
+ # Deduplication guard: if the footer text is already present in the main body OCR
1113
+ # output (accidental capture on some pages), we skip appending it to avoid
1114
+ # printing it twice. Comparison is done on a normalised lowercase snippet of the
1115
+ # first non-empty footer line so minor whitespace differences don't matter.
1116
  if ENABLE_FOOTER_OCR and page_num < len(page_images):
1117
  ftr = ""
1118
  if is_pdf:
 
1122
  if not (ftr and ftr.strip()):
1123
  ftr = ocr_zone(page_images[page_num], fs, 1.0)
1124
  if ftr and ftr.strip():
1125
+ ftr_clean = normalize_money_glyphs(ftr.strip())
1126
+ # Deduplication: check if the first meaningful line of the footer
1127
+ # already appears anywhere in the assembled page content so far.
1128
+ ftr_first_line = next(
1129
+ (ln.strip().lower() for ln in ftr_clean.splitlines() if ln.strip()),
1130
+ ""
1131
+ )
1132
+ already_present = ftr_first_line and any(
1133
+ ftr_first_line in part.lower() for part in parts
1134
+ )
1135
+ if not already_present:
1136
+ parts.append(ftr_clean)
1137
 
1138
  if parts:
1139
  all_pages.append("\n\n".join(parts))
 
1162
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
1163
 
1164
  if __name__ == "__main__":
1165
+ demo.launch()