rehan953 commited on
Commit
25d1f6c
·
verified ·
1 Parent(s): 39680f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -6
app.py CHANGED
@@ -202,6 +202,7 @@ def _strip_code_fences(text: str) -> str:
202
 
203
  def _postprocess_markdown(text: str) -> str:
204
  text = _strip_code_fences(text)
 
205
  text = _normalize_tables(text)
206
  return text.strip()
207
 
@@ -215,11 +216,14 @@ def _build_prompt(strict_table_mode: bool = False) -> str:
215
  "3) Never drop right-most numeric columns (amount/balance).\n"
216
  "4) Keep page content order exactly.\n"
217
  "5) Do not summarize.\n"
 
 
 
218
  )
219
  if strict_table_mode:
220
  base += (
221
- "6) If a row starts with a date (MM/DD), output it as a table row and include trailing amount.\n"
222
- "7) Prefer explicit table rows over plain text for statement activity.\n"
223
  )
224
  return base
225
 
@@ -232,6 +236,69 @@ def _count_dated_rows(text: str) -> int:
232
  return len(re.findall(r"(?m)^\s*\d{2}/\d{2}\b", text))
233
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  def _looks_amount_missing(text: str) -> bool:
236
  """
237
  Heuristic: many dated activity rows but very low amount density.
@@ -257,7 +324,11 @@ def _looks_amount_missing(text: str) -> bool:
257
  return dated >= 12 and amts <= max(3, dated // 10)
258
 
259
 
260
- def _infer_image(image_path: str, strict_table_mode: bool = False) -> str:
 
 
 
 
261
  """Run fine-tuned model on a single image file and return markdown."""
262
  import torch
263
  from PIL import Image
@@ -272,12 +343,19 @@ def _infer_image(image_path: str, strict_table_mode: bool = False) -> str:
272
  try:
273
  img.save(resized_path, "PNG")
274
 
 
 
 
 
 
 
 
275
  messages = [
276
  {
277
  "role": "user",
278
  "content": [
279
  {"type": "image", "url": resized_path},
280
- {"type": "text", "text": _build_prompt(strict_table_mode)},
281
  ],
282
  }
283
  ]
@@ -374,7 +452,9 @@ def run_ocr(uploaded_file):
374
  all_pages: List[str] = []
375
 
376
  if not is_pdf:
377
- page_md = _infer_image(path, strict_table_mode=True)
 
 
378
  if page_md:
379
  all_pages.append(page_md)
380
  else:
@@ -387,6 +467,20 @@ def run_ocr(uploaded_file):
387
  temp_paths.append(img_path)
388
  page_md = _infer_image(img_path, strict_table_mode=False)
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  # Pass 2: retry for amount-missing pages
391
  if _looks_amount_missing(page_md):
392
  log.info(
@@ -395,7 +489,11 @@ def run_ocr(uploaded_file):
395
  )
396
  retry_img = _render_pdf_page_to_image(path, page_idx, RETRY_PROFILE)
397
  temp_paths.append(retry_img)
398
- retry_md = _infer_image(retry_img, strict_table_mode=True)
 
 
 
 
399
 
400
  # Keep better output by amount density.
401
  if _count_amounts(retry_md) > _count_amounts(page_md):
 
202
 
203
  def _postprocess_markdown(text: str) -> str:
204
  text = _strip_code_fences(text)
205
+ text = _html_to_markdown_tables(text)
206
  text = _normalize_tables(text)
207
  return text.strip()
208
 
 
216
  "3) Never drop right-most numeric columns (amount/balance).\n"
217
  "4) Keep page content order exactly.\n"
218
  "5) Do not summarize.\n"
219
+ "6) Output markdown only.\n"
220
+ "7) Do not output HTML tags (<table>, <tr>, <td>, <thead>, <tbody>, etc.).\n"
221
+ "8) Do not invent sample/demo rows.\n"
222
  )
223
  if strict_table_mode:
224
  base += (
225
+ "9) If a row starts with a date (MM/DD), output it as a table row and include trailing amount.\n"
226
+ "10) Prefer explicit table rows over plain text for statement activity.\n"
227
  )
228
  return base
229
 
 
236
  return len(re.findall(r"(?m)^\s*\d{2}/\d{2}\b", text))
237
 
238
 
239
+ def _html_to_markdown_tables(text: str) -> str:
240
+ """
241
+ Convert basic HTML table blocks into pipe markdown tables.
242
+ This keeps downstream parsing deterministic even if model emits HTML.
243
+ """
244
+ table_re = re.compile(r"<table[\s\S]*?</table>", re.IGNORECASE)
245
+ row_re = re.compile(r"<tr[\s\S]*?</tr>", re.IGNORECASE)
246
+ cell_re = re.compile(r"<t[dh][^>]*>([\s\S]*?)</t[dh]>", re.IGNORECASE)
247
+ br_re = re.compile(r"<br\s*/?>", re.IGNORECASE)
248
+ tag_re = re.compile(r"<[^>]+>")
249
+
250
+ def _clean_cell(s: str) -> str:
251
+ s = br_re.sub(" ", s)
252
+ s = tag_re.sub(" ", s)
253
+ s = s.replace("&nbsp;", " ")
254
+ s = s.replace("&amp;", "&")
255
+ s = re.sub(r"\s+", " ", s).strip()
256
+ return s
257
+
258
+ def _convert_one(table_html: str) -> str:
259
+ rows = []
260
+ for row_html in row_re.findall(table_html):
261
+ cells = [_clean_cell(c) for c in cell_re.findall(row_html)]
262
+ cells = [c for c in cells if c != ""]
263
+ if cells:
264
+ rows.append(cells)
265
+ if not rows:
266
+ return table_html
267
+
268
+ width = max(len(r) for r in rows)
269
+ norm_rows = [r + [""] * (width - len(r)) for r in rows]
270
+ header = norm_rows[0]
271
+ out = [
272
+ "| " + " | ".join(header) + " |",
273
+ "| " + " | ".join(["---"] * width) + " |",
274
+ ]
275
+ for r in norm_rows[1:]:
276
+ out.append("| " + " | ".join(r) + " |")
277
+ return "\n".join(out)
278
+
279
+ return table_re.sub(lambda m: _convert_one(m.group(0)), text)
280
+
281
+
282
+ def _is_hallucinated_table_block(text: str) -> bool:
283
+ """
284
+ Detect obvious synthetic outputs like repeated '100.00' rows or
285
+ placeholder transaction lists not present in statement pages.
286
+ """
287
+ low = text.lower()
288
+ if "<table" not in low and "| transaction | amount |" not in low:
289
+ return False
290
+
291
+ repeated_100 = len(re.findall(r"\b100\.00\b", text))
292
+ credit_card_rows = len(re.findall(r"\bcredit card\b", low))
293
+ bank_transfer_rows = len(re.findall(r"\bbank transfer\b", low))
294
+
295
+ if repeated_100 >= 18 and (credit_card_rows + bank_transfer_rows) >= 10:
296
+ return True
297
+ if re.search(r"<!--\s*row\s+\d+\s*-->", low):
298
+ return True
299
+ return False
300
+
301
+
302
  def _looks_amount_missing(text: str) -> bool:
303
  """
304
  Heuristic: many dated activity rows but very low amount density.
 
324
  return dated >= 12 and amts <= max(3, dated // 10)
325
 
326
 
327
+ def _infer_image(
328
+ image_path: str,
329
+ strict_table_mode: bool = False,
330
+ conservative_mode: bool = False,
331
+ ) -> str:
332
  """Run fine-tuned model on a single image file and return markdown."""
333
  import torch
334
  from PIL import Image
 
343
  try:
344
  img.save(resized_path, "PNG")
345
 
346
+ prompt = _build_prompt(strict_table_mode)
347
+ if conservative_mode:
348
+ prompt += (
349
+ "\n11) If unsure, keep original text line-by-line; do not fabricate values.\n"
350
+ "12) Never generate template/example/sample transaction rows.\n"
351
+ )
352
+
353
  messages = [
354
  {
355
  "role": "user",
356
  "content": [
357
  {"type": "image", "url": resized_path},
358
+ {"type": "text", "text": prompt},
359
  ],
360
  }
361
  ]
 
452
  all_pages: List[str] = []
453
 
454
  if not is_pdf:
455
+ page_md = _infer_image(path, strict_table_mode=True, conservative_mode=True)
456
+ if _is_hallucinated_table_block(page_md):
457
+ page_md = _infer_image(path, strict_table_mode=True, conservative_mode=True)
458
  if page_md:
459
  all_pages.append(page_md)
460
  else:
 
467
  temp_paths.append(img_path)
468
  page_md = _infer_image(img_path, strict_table_mode=False)
469
 
470
+ # Pass 1.5: recover from synthetic/hallucinated table outputs
471
+ if _is_hallucinated_table_block(page_md):
472
+ log.info(
473
+ "Page %d flagged as hallucinated-table; retrying conservative prompt.",
474
+ page_idx + 1,
475
+ )
476
+ page_md_retry = _infer_image(
477
+ img_path,
478
+ strict_table_mode=True,
479
+ conservative_mode=True,
480
+ )
481
+ if not _is_hallucinated_table_block(page_md_retry):
482
+ page_md = page_md_retry
483
+
484
  # Pass 2: retry for amount-missing pages
485
  if _looks_amount_missing(page_md):
486
  log.info(
 
489
  )
490
  retry_img = _render_pdf_page_to_image(path, page_idx, RETRY_PROFILE)
491
  temp_paths.append(retry_img)
492
+ retry_md = _infer_image(
493
+ retry_img,
494
+ strict_table_mode=True,
495
+ conservative_mode=True,
496
+ )
497
 
498
  # Keep better output by amount density.
499
  if _count_amounts(retry_md) > _count_amounts(page_md):