rehan953 commited on
Commit
11bf437
·
verified ·
1 Parent(s): f1ba689

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -38
app.py CHANGED
@@ -139,6 +139,10 @@ Primary goals for reconcile rate:
139
  - Fix FH-2: Normalize packed DAILY BALANCE SUMMARY and CHECKS PAID SUMMARY tables into one-row-per-item
140
  layouts; also promote multiline register headers where row0 is placeholder and row1 has true
141
  DATE/DESCRIPTION/DEPOSIT/WITHDRAWAL labels.
 
 
 
 
142
  """
143
 
144
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
@@ -164,16 +168,24 @@ from collections import Counter, defaultdict
164
  from html.parser import HTMLParser
165
 
166
  import yaml
167
- import gradio as gr
168
- import glmocr
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  log = logging.getLogger("glmocr_app")
171
  logging.basicConfig(level=logging.INFO)
172
 
173
- GLMOCR_BASE = os.path.dirname(glmocr.__file__)
174
- CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
175
- FORMATTER_PATH = os.path.join(GLMOCR_BASE, "postprocess", "result_formatter.py")
176
-
177
  # ============================================================
178
  # HARD-CODED SETTINGS (edit these numbers to tune quality/speed)
179
  # ============================================================
@@ -214,33 +226,35 @@ def get_parser():
214
  # ---------------------------------------------------------------------------
215
  # Best-effort config tweaks (safe to fail on read-only HF env)
216
  # ---------------------------------------------------------------------------
217
- try:
218
- with open(CONFIG_PATH, "r") as f:
219
- config = yaml.safe_load(f)
220
- config["pipeline"]["maas"]["enabled"] = True
221
- config["pipeline"]["maas"]["api_key"] = GLMOCR_API_KEY
222
- with open(CONFIG_PATH, "w") as f:
223
- yaml.dump(config, f, default_flow_style=False, sort_keys=False)
224
- except Exception:
225
- pass
 
226
 
227
  # Best-effort formatter tweak: avoid stripping header/footer labels
228
- try:
229
- with open(FORMATTER_PATH, "r") as f:
230
- source = f.read()
231
- for label in (
232
- '"header"', "'header'",
233
- '"footer"', "'footer'",
234
- '"doc_header"', "'doc_header'",
235
- '"doc_footer"', "'doc_footer'",
236
- ):
237
- source = re.sub(r",\s*" + re.escape(label), "", source)
238
- source = re.sub(re.escape(label) + r"\s*,", "", source)
239
- source = re.sub(re.escape(label), "", source)
240
- with open(FORMATTER_PATH, "w") as f:
241
- f.write(source)
242
- except Exception:
243
- pass
 
244
 
245
  # --------------------------
246
  # Header/footer helpers
@@ -3247,6 +3261,162 @@ def _expand_td_continuation_three_col_register_grid(grid):
3247
  return new_grid
3248
 
3249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3250
  def normalize_html_tables(text: str) -> str:
3251
  if not text or "<table" not in text.lower():
3252
  return text
@@ -3274,6 +3444,7 @@ def normalize_html_tables(text: str) -> str:
3274
 
3275
  grid = _drop_truly_empty_columns(grid)
3276
  grid = _merge_blank_header_text_columns(grid)
 
3277
  _seq_linear = _split_wide_checks_sequenced_grid(grid)
3278
  if _seq_linear is not None:
3279
  grid = _seq_linear
@@ -5916,12 +6087,17 @@ def run_ocr(uploaded_file):
5916
  except Exception:
5917
  pass
5918
 
5919
- with gr.Blocks(title="GLM-OCR") as demo:
5920
- gr.Markdown("# GLM-OCR\nUpload a PDF or image. Headers included; tables stabilized.")
5921
- file_in = gr.File(label="Upload PDF or image", file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"])
5922
- run_btn = gr.Button("Run OCR", variant="primary")
5923
- out = gr.Textbox(lines=40, label="Output (markdown)")
5924
- run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
 
 
 
 
 
5925
 
5926
  if __name__ == "__main__":
5927
- demo.launch()
 
139
  - Fix FH-2: Normalize packed DAILY BALANCE SUMMARY and CHECKS PAID SUMMARY tables into one-row-per-item
140
  layouts; also promote multiline register headers where row0 is placeholder and row1 has true
141
  DATE/DESCRIPTION/DEPOSIT/WITHDRAWAL labels.
142
+ - Fix SE-multi: Multi-account deposit summary tables (Beginning/Deposits/Withdrawals/Ending) sometimes get a
143
+ bogus OCR sub-row with tiny amounts (e.g. -$1 / $2 / $1) next to a real account row. Drop those
144
+ dust rows when another row has statement-scale balances, then recompute the TOTAL row from the
145
+ remaining account rows. Generic; no institution names.
146
  """
147
 
148
  # Patch asyncio first (before Gradio imports it) to suppress Python 3.13 cleanup noise
 
168
  from html.parser import HTMLParser
169
 
170
  import yaml
171
+
172
+ # Gradio and glmocr are optional at import: pro_processor only needs stabilize_tables_and_text
173
+ # (glm_ocr_client). HF Space / run_ocr still use glmocr + gradio when installed.
174
+ try:
175
+ import glmocr
176
+
177
+ GLMOCR_BASE = os.path.dirname(glmocr.__file__)
178
+ CONFIG_PATH = os.path.join(GLMOCR_BASE, "config.yaml")
179
+ FORMATTER_PATH = os.path.join(GLMOCR_BASE, "postprocess", "result_formatter.py")
180
+ except ImportError:
181
+ glmocr = None # type: ignore
182
+ GLMOCR_BASE = ""
183
+ CONFIG_PATH = ""
184
+ FORMATTER_PATH = ""
185
 
186
  log = logging.getLogger("glmocr_app")
187
  logging.basicConfig(level=logging.INFO)
188
 
 
 
 
 
189
  # ============================================================
190
  # HARD-CODED SETTINGS (edit these numbers to tune quality/speed)
191
  # ============================================================
 
226
  # ---------------------------------------------------------------------------
227
  # Best-effort config tweaks (safe to fail on read-only HF env)
228
  # ---------------------------------------------------------------------------
229
+ if CONFIG_PATH:
230
+ try:
231
+ with open(CONFIG_PATH, "r") as f:
232
+ config = yaml.safe_load(f)
233
+ config["pipeline"]["maas"]["enabled"] = True
234
+ config["pipeline"]["maas"]["api_key"] = GLMOCR_API_KEY
235
+ with open(CONFIG_PATH, "w") as f:
236
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
237
+ except Exception:
238
+ pass
239
 
240
  # Best-effort formatter tweak: avoid stripping header/footer labels
241
+ if FORMATTER_PATH:
242
+ try:
243
+ with open(FORMATTER_PATH, "r") as f:
244
+ source = f.read()
245
+ for label in (
246
+ '"header"', "'header'",
247
+ '"footer"', "'footer'",
248
+ '"doc_header"', "'doc_header'",
249
+ '"doc_footer"', "'doc_footer'",
250
+ ):
251
+ source = re.sub(r",\s*" + re.escape(label), "", source)
252
+ source = re.sub(re.escape(label) + r"\s*,", "", source)
253
+ source = re.sub(re.escape(label), "", source)
254
+ with open(FORMATTER_PATH, "w") as f:
255
+ f.write(source)
256
+ except Exception:
257
+ pass
258
 
259
  # --------------------------
260
  # Header/footer helpers
 
3261
  return new_grid
3262
 
3263
 
3264
+ def _parse_summary_table_money(cell: str) -> Optional[float]:
3265
+ """Parse a currency cell from a statement summary row; None if not numeric."""
3266
+ if cell is None:
3267
+ return None
3268
+ t = str(cell).strip().replace(",", "").replace("$", "").replace(" ", "")
3269
+ if not t or t in ("-", "—", "–"):
3270
+ return None
3271
+ t = t.replace("(", "-").replace(")", "")
3272
+ try:
3273
+ return float(t)
3274
+ except ValueError:
3275
+ return None
3276
+
3277
+
3278
+ def _format_summary_table_money(value: float) -> str:
3279
+ sign = "-" if value < 0 else ""
3280
+ v = abs(value)
3281
+ s = f"{v:,.2f}"
3282
+ return sign + s
3283
+
3284
+
3285
+ def _drop_phantom_micro_account_rows_in_multi_account_summary(grid):
3286
+ """
3287
+ Remove OCR phantom sub-account rows in credit-union / multi-account deposit summary tables
3288
+ (Acct | Beginning | Deposits | Withdrawals | Ending | …) when one row is dust and another
3289
+ is statement-scale; recompute TOTAL from remaining account rows.
3290
+ """
3291
+ if not grid or len(grid) < 4:
3292
+ return grid
3293
+
3294
+ header = [str(c or "").strip() for c in grid[0]]
3295
+ ncols = len(header)
3296
+ if ncols < 5:
3297
+ return grid
3298
+
3299
+ header_join_u = " ".join(h.upper() for h in header)
3300
+ if "DEPOSIT" not in header_join_u or "WITHDRAW" not in header_join_u:
3301
+ return grid
3302
+ if "BEGINNING" not in header_join_u and "PREVIOUS" not in header_join_u:
3303
+ return grid
3304
+ has_ending = "ENDING" in header_join_u or (
3305
+ "NEW" in header_join_u and "BALANCE" in header_join_u
3306
+ )
3307
+ if not has_ending:
3308
+ return grid
3309
+
3310
+ def _col_match(pred):
3311
+ for i, h in enumerate(header):
3312
+ hu = h.upper()
3313
+ if pred(hu):
3314
+ return i
3315
+ return None
3316
+
3317
+ i_beg = _col_match(lambda u: "BEGINNING" in u or ("PREVIOUS" in u and "BALANCE" in u))
3318
+ i_dep = _col_match(lambda u: "DEPOSIT" in u and "WITHDRAW" not in u)
3319
+ i_wd = _col_match(lambda u: "WITHDRAW" in u)
3320
+ i_end = _col_match(
3321
+ lambda u: "ENDING" in u
3322
+ or ("NEW" in u and "BALANCE" in u)
3323
+ or u.startswith("CLOSING")
3324
+ )
3325
+ if None in (i_beg, i_dep, i_wd, i_end):
3326
+ return grid
3327
+ if len({i_beg, i_dep, i_wd, i_end}) != 4:
3328
+ return grid
3329
+
3330
+ money_idxs = (i_beg, i_dep, i_wd, i_end)
3331
+
3332
+ # Dust vs statement scale (tuned for OCR rows like -1 / 2 / 0 / 1 next to 18k withdrawals)
3333
+ _DUST_MAX = 10.0
3334
+ _DUST_SUM = 15.0
3335
+ _REAL_MIN = 3000.0
3336
+
3337
+ def _pad_row(row):
3338
+ r = [str(c or "").strip() for c in row]
3339
+ while len(r) < ncols:
3340
+ r.append("")
3341
+ return r[:ncols]
3342
+
3343
+ account_rows = [] # (grid_index, row_padded, max_abs, sum_abs, vals tuple)
3344
+ total_idx: Optional[int] = None
3345
+
3346
+ for ri in range(1, len(grid)):
3347
+ row = _pad_row(grid[ri])
3348
+ fc = row[0].strip().upper()
3349
+ if fc == "TOTAL" or fc.startswith("TOTAL "):
3350
+ total_idx = ri
3351
+ continue
3352
+ vals = tuple(
3353
+ _parse_summary_table_money(row[j]) if j < len(row) else None
3354
+ for j in money_idxs
3355
+ )
3356
+ if all(v is None for v in vals):
3357
+ continue
3358
+ abs_vals = [abs(v) for v in vals if v is not None]
3359
+ max_abs = max(abs_vals) if abs_vals else 0.0
3360
+ sum_abs = sum(abs(v) for v in vals if v is not None)
3361
+ account_rows.append((ri, row, max_abs, sum_abs, vals))
3362
+
3363
+ if len(account_rows) < 2:
3364
+ return grid
3365
+
3366
+ global_max = max(t[2] for t in account_rows)
3367
+ if global_max < _REAL_MIN:
3368
+ return grid
3369
+
3370
+ phantom_ri = set()
3371
+ for ri, row, max_abs, sum_abs, vals in account_rows:
3372
+ if max_abs <= _DUST_MAX and sum_abs <= _DUST_SUM:
3373
+ phantom_ri.add(ri)
3374
+
3375
+ if not phantom_ri:
3376
+ return grid
3377
+
3378
+ kept_accounts = [
3379
+ (ri, row, vals)
3380
+ for ri, row, max_abs, sum_abs, vals in account_rows
3381
+ if ri not in phantom_ri
3382
+ ]
3383
+ if not kept_accounts:
3384
+ return grid
3385
+
3386
+ # Sum the four money columns across kept account rows
3387
+ sums = [0.0, 0.0, 0.0, 0.0]
3388
+ for _ri, _row, vals in kept_accounts:
3389
+ for k in range(4):
3390
+ if vals[k] is not None:
3391
+ sums[k] += vals[k]
3392
+
3393
+ new_grid: List[List[str]] = [list(header)]
3394
+ for ri in range(1, len(grid)):
3395
+ if ri in phantom_ri:
3396
+ continue
3397
+ if ri == total_idx:
3398
+ continue
3399
+ new_grid.append(_pad_row(grid[ri]))
3400
+
3401
+ if total_idx is not None:
3402
+ total_row = [""] * ncols
3403
+ total_row[0] = "TOTAL"
3404
+ for k, j in enumerate(money_idxs):
3405
+ total_row[j] = _format_summary_table_money(sums[k])
3406
+ old_total = _pad_row(grid[total_idx])
3407
+ for j in range(ncols):
3408
+ if j in (0,) + money_idxs:
3409
+ continue
3410
+ hj = header[j].upper() if j < len(header) else ""
3411
+ if "DIVIDEND" in hj or "YTD" in hj:
3412
+ total_row[j] = old_total[j] if old_total[j] else "0.00"
3413
+ elif old_total[j]:
3414
+ total_row[j] = old_total[j]
3415
+ new_grid.append(total_row)
3416
+
3417
+ return new_grid
3418
+
3419
+
3420
  def normalize_html_tables(text: str) -> str:
3421
  if not text or "<table" not in text.lower():
3422
  return text
 
3444
 
3445
  grid = _drop_truly_empty_columns(grid)
3446
  grid = _merge_blank_header_text_columns(grid)
3447
+ grid = _drop_phantom_micro_account_rows_in_multi_account_summary(grid)
3448
  _seq_linear = _split_wide_checks_sequenced_grid(grid)
3449
  if _seq_linear is not None:
3450
  grid = _seq_linear
 
6087
  except Exception:
6088
  pass
6089
 
6090
+ def _create_gradio_demo():
6091
+ import gradio as gr
6092
+
6093
+ with gr.Blocks(title="GLM-OCR") as demo:
6094
+ gr.Markdown("# GLM-OCR\nUpload a PDF or image. Headers included; tables stabilized.")
6095
+ file_in = gr.File(label="Upload PDF or image", file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"])
6096
+ run_btn = gr.Button("Run OCR", variant="primary")
6097
+ out = gr.Textbox(lines=40, label="Output (markdown)")
6098
+ run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
6099
+ return demo
6100
+
6101
 
6102
  if __name__ == "__main__":
6103
+ _create_gradio_demo().launch()