rehan953 commited on
Commit
603d40c
·
verified ·
1 Parent(s): f73f524

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -1
app.py CHANGED
@@ -164,7 +164,7 @@ if CONFIG_PATH:
164
  with open(CONFIG_PATH, "r", encoding="utf-8") as f:
165
  config = yaml.safe_load(f)
166
  config.setdefault("pipeline", {}).setdefault("maas", {})
167
- config["pipeline"]["maas"]["enabled"] = True
168
  config["pipeline"]["maas"]["api_key"] = GLMOCR_API_KEY
169
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
170
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
@@ -910,6 +910,288 @@ def light_stabilize_markdown(page_md: str) -> str:
910
  return merged
911
 
912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
914
  import pymupdf as fitz
915
  from PIL import Image
@@ -1047,6 +1329,9 @@ def run_ocr(uploaded_file):
1047
  merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
1048
  if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
1049
  merged = stabilize_table_markup(merged, rounds=4)
 
 
 
1050
  merged = repair_thead_cell_semantics(merged)
1051
  merged = strip_degenerate_html_tables(merged)
1052
  return merged
 
164
  with open(CONFIG_PATH, "r", encoding="utf-8") as f:
165
  config = yaml.safe_load(f)
166
  config.setdefault("pipeline", {}).setdefault("maas", {})
167
+ config["pipeline"]["maas"]["enabled"] = TrueCONFIG_PATH
168
  config["pipeline"]["maas"]["api_key"] = GLMOCR_API_KEY
169
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
170
  yaml.dump(config, f, default_flow_style=False, sort_keys=False)
 
910
  return merged
911
 
912
 
913
+ def _parse_amount_or_none(s: str):
914
+ t = (s or "").strip()
915
+ if not t:
916
+ return None
917
+ t = t.replace("$", "").replace(",", "").replace("(", "-").replace(")", "")
918
+ t = t.replace("−", "-").replace("–", "-").replace("—", "-")
919
+ if not re.search(r"\d", t):
920
+ return None
921
+ try:
922
+ return float(t)
923
+ except Exception:
924
+ return None
925
+
926
+
927
+ def _extract_rows_plain_from_table(full_table: str) -> List[List[str]]:
928
+ rows: List[List[str]] = []
929
+ for tr in re.finditer(r"<tr\b[^>]*>.*?</tr>", full_table, flags=re.IGNORECASE | re.DOTALL):
930
+ inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", tr.group(0), flags=re.IGNORECASE | re.DOTALL)
931
+ if not inner_m:
932
+ continue
933
+ inner = inner_m.group(1)
934
+ cells = [_cell_plain_text(m.group(0)) for m in _CELL.finditer(inner)]
935
+ if cells:
936
+ rows.append(cells)
937
+ return rows
938
+
939
+
940
+ def _extract_daily_balance_by_date(md: str):
941
+ """
942
+ Build a date->balance map from daily-balance style tables.
943
+ Supports compact statements with repeated Date/Balance column pairs.
944
+ """
945
+ out = {}
946
+ for tm in re.finditer(r"<table\b[^>]*>.*?</table>", md, flags=re.IGNORECASE | re.DOTALL):
947
+ t = tm.group(0)
948
+ rows = _extract_rows_plain_from_table(t)
949
+ if len(rows) < 2:
950
+ continue
951
+ hdr = [c.strip().lower() for c in rows[0]]
952
+ if not hdr:
953
+ continue
954
+ date_cols = [i for i, c in enumerate(hdr) if "date" in c]
955
+ bal_cols = [i for i, c in enumerate(hdr) if "balance" in c]
956
+ if not date_cols or not bal_cols:
957
+ continue
958
+ pairs = []
959
+ for di in date_cols:
960
+ bi = next((b for b in bal_cols if b > di), None)
961
+ if bi is not None:
962
+ pairs.append((di, bi))
963
+ if not pairs:
964
+ continue
965
+ for r in rows[1:]:
966
+ for di, bi in pairs:
967
+ if di >= len(r) or bi >= len(r):
968
+ continue
969
+ d = (r[di] or "").strip()
970
+ if not re.search(r"\b\d{1,2}/\d{1,2}(?:/\d{2,4})?\b", d):
971
+ continue
972
+ m = re.search(r"\d{1,2}/\d{1,2}(?:/\d{2,4})?", d)
973
+ if not m:
974
+ continue
975
+ key = m.group(0)
976
+ bal = _parse_amount_or_none(r[bi])
977
+ if bal is None:
978
+ continue
979
+ out[key] = bal
980
+ short = "/".join(key.split("/")[:2])
981
+ out[short] = bal
982
+ return out
983
+
984
+
985
+ def enrich_transaction_tables_with_daily_balances(md: str) -> str:
986
+ """
987
+ If transaction tables have Date+Amount but no Balance column, append a Balance
988
+ column using date-matched daily ending balances from the same document.
989
+ """
990
+ if not md or "<table" not in md.lower():
991
+ return md
992
+ dmap = _extract_daily_balance_by_date(md)
993
+ if not dmap:
994
+ return md
995
+
996
+ def repl(m: re.Match) -> str:
997
+ table = m.group(0)
998
+ if re.search(r"(?:colspan|rowspan)\s*=", table, flags=re.IGNORECASE):
999
+ return table
1000
+ tr_blocks = list(re.finditer(r"<tr\b[^>]*>.*?</tr>", table, flags=re.IGNORECASE | re.DOTALL))
1001
+ if len(tr_blocks) < 2:
1002
+ return table
1003
+ first = tr_blocks[0].group(0)
1004
+ h_inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", first, flags=re.IGNORECASE | re.DOTALL)
1005
+ if not h_inner_m:
1006
+ return table
1007
+ h_inner = h_inner_m.group(1)
1008
+ h_cells = list(_CELL.finditer(h_inner))
1009
+ if not h_cells:
1010
+ return table
1011
+ hdr_txt = [_cell_plain_text(c.group(0)).strip().lower() for c in h_cells]
1012
+ if any("balance" in c for c in hdr_txt):
1013
+ return table
1014
+ if not any("date" in c for c in hdr_txt):
1015
+ return table
1016
+ if not any(("amount" in c) or ("debit" in c) or ("credit" in c) for c in hdr_txt):
1017
+ return table
1018
+ date_idx = next((i for i, c in enumerate(hdr_txt) if "date" in c), -1)
1019
+ if date_idx < 0:
1020
+ return table
1021
+
1022
+ new_parts: List[str] = []
1023
+ cursor = 0
1024
+ added = 0
1025
+ for i, trm in enumerate(tr_blocks):
1026
+ new_parts.append(table[cursor : trm.start()])
1027
+ tr_seg = trm.group(0)
1028
+ inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", tr_seg, flags=re.IGNORECASE | re.DOTALL)
1029
+ if not inner_m:
1030
+ new_parts.append(tr_seg)
1031
+ cursor = trm.end()
1032
+ continue
1033
+ tr_open_m = re.search(r"<tr\b[^>]*>", tr_seg, flags=re.IGNORECASE)
1034
+ tr_open = tr_open_m.group(0) if tr_open_m else "<tr>"
1035
+ tr_close = "</tr>"
1036
+ inner = inner_m.group(1)
1037
+ cells = [c.group(0) for c in _CELL.finditer(inner)]
1038
+ if i == 0:
1039
+ new_inner = inner + "<th>Balance</th>"
1040
+ new_parts.append(tr_open + new_inner + tr_close)
1041
+ else:
1042
+ if date_idx >= len(cells):
1043
+ new_parts.append(tr_seg)
1044
+ cursor = trm.end()
1045
+ continue
1046
+ d_txt = _cell_plain_text(cells[date_idx])
1047
+ dm = re.search(r"\d{1,2}/\d{1,2}(?:/\d{2,4})?", d_txt or "")
1048
+ bal = dmap.get(dm.group(0)) if dm else None
1049
+ if bal is None and dm:
1050
+ bal = dmap.get("/".join(dm.group(0).split("/")[:2]))
1051
+ if bal is None:
1052
+ new_parts.append(tr_seg)
1053
+ cursor = trm.end()
1054
+ continue
1055
+ new_inner = inner + f"<td>{bal:,.2f}</td>"
1056
+ new_parts.append(tr_open + new_inner + tr_close)
1057
+ added += 1
1058
+ cursor = trm.end()
1059
+ new_parts.append(table[cursor:])
1060
+ if added < 3:
1061
+ return table
1062
+ return "".join(new_parts)
1063
+
1064
+ return re.sub(r"<table\b[^>]*>.*?</table>", repl, md, flags=re.IGNORECASE | re.DOTALL)
1065
+
1066
+
1067
+ def _parse_statement_edge_balances(md: str):
1068
+ start = None
1069
+ end = None
1070
+ m1 = re.search(
1071
+ r"(?:Beginning|Starting)\s+balance[^$\n]{0,80}\$?\s*(-?\d{1,3}(?:,\d{3})*(?:\.\d{2})?)",
1072
+ md,
1073
+ flags=re.IGNORECASE,
1074
+ )
1075
+ if m1:
1076
+ start = _parse_amount_or_none(m1.group(1))
1077
+ m2 = re.search(
1078
+ r"Ending\s+balance[^$\n]{0,80}\$?\s*(-?\d{1,3}(?:,\d{3})*(?:\.\d{2})?)",
1079
+ md,
1080
+ flags=re.IGNORECASE,
1081
+ )
1082
+ if m2:
1083
+ end = _parse_amount_or_none(m2.group(1))
1084
+ return start, end
1085
+
1086
+
1087
+ def synthesize_running_balance_columns(md: str) -> str:
1088
+ """
1089
+ Fallback when no explicit balance column exists: infer running balances from
1090
+ statement beginning/ending balance and signed transaction amounts.
1091
+ """
1092
+ if not md or "<table" not in md.lower():
1093
+ return md
1094
+ start_bal, end_bal = _parse_statement_edge_balances(md)
1095
+ if start_bal is None and end_bal is None:
1096
+ return md
1097
+
1098
+ table_re = re.compile(r"<table\b[^>]*>.*?</table>", re.IGNORECASE | re.DOTALL)
1099
+ tr_re = re.compile(r"<tr\b[^>]*>.*?</tr>", re.IGNORECASE | re.DOTALL)
1100
+
1101
+ tables = list(table_re.finditer(md))
1102
+ row_refs = []
1103
+ amounts = []
1104
+ headers = {}
1105
+
1106
+ for ti, tm in enumerate(tables):
1107
+ t = tm.group(0)
1108
+ if re.search(r"(?:colspan|rowspan)\s*=", t, flags=re.IGNORECASE):
1109
+ continue
1110
+ trs = list(tr_re.finditer(t))
1111
+ if len(trs) < 2:
1112
+ continue
1113
+ h_inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", trs[0].group(0), flags=re.IGNORECASE | re.DOTALL)
1114
+ if not h_inner_m:
1115
+ continue
1116
+ h_cells = [c.group(0) for c in _CELL.finditer(h_inner_m.group(1))]
1117
+ if not h_cells:
1118
+ continue
1119
+ htxt = [_cell_plain_text(c).strip().lower() for c in h_cells]
1120
+ if any("balance" in h for h in htxt):
1121
+ continue
1122
+ date_idx = next((i for i, h in enumerate(htxt) if "date" in h), -1)
1123
+ amt_idx = next((i for i, h in enumerate(htxt) if "amount" in h or "debit" in h or "credit" in h), -1)
1124
+ if date_idx < 0 or amt_idx < 0:
1125
+ continue
1126
+ headers[ti] = (date_idx, amt_idx)
1127
+ for ri, trm in enumerate(trs[1:], 1):
1128
+ inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", trm.group(0), flags=re.IGNORECASE | re.DOTALL)
1129
+ if not inner_m:
1130
+ continue
1131
+ cells = [c.group(0) for c in _CELL.finditer(inner_m.group(1))]
1132
+ if len(cells) <= max(date_idx, amt_idx):
1133
+ continue
1134
+ d_txt = _cell_plain_text(cells[date_idx])
1135
+ if not re.search(r"\d{1,2}/\d{1,2}(?:/\d{2,4})?", d_txt or ""):
1136
+ continue
1137
+ amt = _parse_amount_or_none(_cell_plain_text(cells[amt_idx]))
1138
+ if amt is None:
1139
+ continue
1140
+ row_refs.append((ti, ri))
1141
+ amounts.append(amt)
1142
+
1143
+ if len(amounts) < 20:
1144
+ return md
1145
+ if start_bal is None:
1146
+ start_bal = (end_bal or 0.0) - sum(amounts)
1147
+
1148
+ running = []
1149
+ cur = float(start_bal)
1150
+ for a in amounts:
1151
+ cur += float(a)
1152
+ running.append(cur)
1153
+ bal_by_ref = {row_refs[i]: running[i] for i in range(len(row_refs))}
1154
+
1155
+ pieces = []
1156
+ last = 0
1157
+ for ti, tm in enumerate(tables):
1158
+ pieces.append(md[last : tm.start()])
1159
+ t = tm.group(0)
1160
+ if ti not in headers:
1161
+ pieces.append(t)
1162
+ last = tm.end()
1163
+ continue
1164
+ date_idx, amt_idx = headers[ti]
1165
+ trs = list(tr_re.finditer(t))
1166
+ out_t = []
1167
+ cur2 = 0
1168
+ for idx, trm in enumerate(trs):
1169
+ out_t.append(t[cur2 : trm.start()])
1170
+ tr_seg = trm.group(0)
1171
+ inner_m = re.search(r"<tr\b[^>]*>(.*)</tr>", tr_seg, flags=re.IGNORECASE | re.DOTALL)
1172
+ tr_open_m = re.search(r"<tr\b[^>]*>", tr_seg, flags=re.IGNORECASE)
1173
+ tr_open = tr_open_m.group(0) if tr_open_m else "<tr>"
1174
+ if not inner_m:
1175
+ out_t.append(tr_seg)
1176
+ cur2 = trm.end()
1177
+ continue
1178
+ inner = inner_m.group(1)
1179
+ if idx == 0:
1180
+ out_t.append(tr_open + inner + "<th>Balance</th></tr>")
1181
+ else:
1182
+ b = bal_by_ref.get((ti, idx))
1183
+ if b is None:
1184
+ out_t.append(tr_seg)
1185
+ else:
1186
+ out_t.append(tr_open + inner + f"<td>{b:,.2f}</td></tr>")
1187
+ cur2 = trm.end()
1188
+ out_t.append(t[cur2:])
1189
+ pieces.append("".join(out_t))
1190
+ last = tm.end()
1191
+ pieces.append(md[last:])
1192
+ return "".join(pieces)
1193
+
1194
+
1195
  def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
1196
  import pymupdf as fitz
1197
  from PIL import Image
 
1329
  merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
1330
  if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
1331
  merged = stabilize_table_markup(merged, rounds=4)
1332
+ merged = enrich_transaction_tables_with_daily_balances(merged)
1333
+ merged = synthesize_running_balance_columns(merged)
1334
+ merged = stabilize_table_markup(merged, rounds=2)
1335
  merged = repair_thead_cell_semantics(merged)
1336
  merged = strip_degenerate_html_tables(merged)
1337
  return merged