Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -72,18 +72,21 @@ logging.basicConfig(level=logging.INFO)
|
|
| 72 |
# Settings — tuned for dense financial PDFs; applies to every document
|
| 73 |
# ---------------------------------------------------------------------------
|
| 74 |
|
|
|
|
| 75 |
GLMOCR_API_KEY = "cee1d52dd91a4ab591b3f6e105f8ad89.LgbQTECuzX0zrito"
|
| 76 |
if not GLMOCR_API_KEY:
|
| 77 |
-
log.warning(
|
|
|
|
|
|
|
| 78 |
|
| 79 |
# Rasterization: higher scale = more pixels per PDF point (helps small type,
|
| 80 |
# boxed headers, and narrow columns). Same constant for all uploads.
|
| 81 |
-
RENDER_SCALE = 3.
|
| 82 |
|
| 83 |
# White margin as a fraction of page width/height after render. Extra right
|
| 84 |
# margin helps right-aligned currency columns that hug the page edge.
|
| 85 |
PAD_LEFT_FRAC = 0.035
|
| 86 |
-
PAD_RIGHT_FRAC = 0.
|
| 87 |
PAD_TOP_FRAC = 0.018
|
| 88 |
PAD_BOTTOM_FRAC = 0.018
|
| 89 |
|
|
@@ -144,7 +147,10 @@ def get_parser():
|
|
| 144 |
if _parser is None:
|
| 145 |
from glmocr import GlmOcr
|
| 146 |
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
| 148 |
return _parser
|
| 149 |
|
| 150 |
|
|
@@ -575,6 +581,74 @@ def stabilize_table_markup(md: str, rounds: int = 4) -> str:
|
|
| 575 |
return cur
|
| 576 |
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
def looks_like_markdown_table(block: str) -> bool:
|
| 579 |
lines = [ln.rstrip() for ln in block.strip().splitlines() if ln.strip()]
|
| 580 |
if len(lines) < 2:
|
|
@@ -644,7 +718,10 @@ def light_stabilize_markdown(page_md: str) -> str:
|
|
| 644 |
else:
|
| 645 |
out_blocks.append(b)
|
| 646 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
|
| 650 |
def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
|
@@ -784,6 +861,8 @@ def run_ocr(uploaded_file):
|
|
| 784 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 785 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
| 786 |
merged = stabilize_table_markup(merged, rounds=4)
|
|
|
|
|
|
|
| 787 |
return merged
|
| 788 |
|
| 789 |
except Exception as e:
|
|
|
|
| 72 |
# Settings — tuned for dense financial PDFs; applies to every document
|
| 73 |
# ---------------------------------------------------------------------------
|
| 74 |
|
| 75 |
+
# Never commit secrets: Space / local runs use ZHIPU_API_KEY or GLMOCR_API_KEY.
|
| 76 |
GLMOCR_API_KEY = "cee1d52dd91a4ab591b3f6e105f8ad89.LgbQTECuzX0zrito"
|
| 77 |
if not GLMOCR_API_KEY:
|
| 78 |
+
log.warning(
|
| 79 |
+
"No ZHIPU_API_KEY or GLMOCR_API_KEY in environment; GlmOcr() will fail until you set one."
|
| 80 |
+
)
|
| 81 |
|
| 82 |
# Rasterization: higher scale = more pixels per PDF point (helps small type,
|
| 83 |
# boxed headers, and narrow columns). Same constant for all uploads.
|
| 84 |
+
RENDER_SCALE = 3.35
|
| 85 |
|
| 86 |
# White margin as a fraction of page width/height after render. Extra right
|
| 87 |
# margin helps right-aligned currency columns that hug the page edge.
|
| 88 |
PAD_LEFT_FRAC = 0.035
|
| 89 |
+
PAD_RIGHT_FRAC = 0.125
|
| 90 |
PAD_TOP_FRAC = 0.018
|
| 91 |
PAD_BOTTOM_FRAC = 0.018
|
| 92 |
|
|
|
|
| 147 |
if _parser is None:
|
| 148 |
from glmocr import GlmOcr
|
| 149 |
|
| 150 |
+
kw = {"mode": "maas"}
|
| 151 |
+
if GLMOCR_API_KEY:
|
| 152 |
+
kw["api_key"] = GLMOCR_API_KEY
|
| 153 |
+
_parser = GlmOcr(**kw)
|
| 154 |
return _parser
|
| 155 |
|
| 156 |
|
|
|
|
| 581 |
return cur
|
| 582 |
|
| 583 |
|
| 584 |
+
_THEAD_BLOCK = re.compile(r"<thead\b[^>]*>.*?</thead>", re.IGNORECASE | re.DOTALL)
|
| 585 |
+
_CURRENCY_SNIFF = re.compile(r"[\$€£]")
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def repair_thead_cell_semantics(md: str) -> str:
|
| 589 |
+
"""
|
| 590 |
+
Normalize header rows: cells inside <thead> should use <th>. Stray <td>
|
| 591 |
+
from OCR breaks rectangular header grids for parsers that expect <th> only
|
| 592 |
+
in thead. Institution-agnostic HTML repair only.
|
| 593 |
+
"""
|
| 594 |
+
if not md or "<thead" not in md.lower():
|
| 595 |
+
return md
|
| 596 |
+
|
| 597 |
+
def fix_block(m: re.Match) -> str:
|
| 598 |
+
block = m.group(0)
|
| 599 |
+
block = re.sub(r"<td(\b[^>]*?>)", r"<th\1", block, flags=re.IGNORECASE)
|
| 600 |
+
block = re.sub(r"</td\s*>", "</th>", block, flags=re.IGNORECASE)
|
| 601 |
+
return block
|
| 602 |
+
|
| 603 |
+
return _THEAD_BLOCK.sub(fix_block, md)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
def _table_cell_plain_texts(full_table: str) -> List[str]:
|
| 607 |
+
return [_cell_plain_text(m.group(0)) for m in _CELL.finditer(full_table)]
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
def strip_degenerate_html_tables(md: str) -> str:
|
| 611 |
+
"""
|
| 612 |
+
Drop tables that are almost certainly non-ledger layout: all-empty grids,
|
| 613 |
+
or large sparse grids with no digits and no currency symbols (blank
|
| 614 |
+
worksheets / decorative boxes). Pattern-based only; no bank or product
|
| 615 |
+
names. Conservative thresholds to avoid removing real sparse tables.
|
| 616 |
+
"""
|
| 617 |
+
if not md or "<table" not in md.lower():
|
| 618 |
+
return md
|
| 619 |
+
|
| 620 |
+
def should_drop(full: str) -> bool:
|
| 621 |
+
texts = _table_cell_plain_texts(full)
|
| 622 |
+
n = len(texts)
|
| 623 |
+
if n < 1:
|
| 624 |
+
return False
|
| 625 |
+
nonempty = sum(1 for t in texts if t.strip())
|
| 626 |
+
if nonempty == 0:
|
| 627 |
+
return True
|
| 628 |
+
joined = " ".join(texts)
|
| 629 |
+
compact = re.sub(r"\s+", " ", joined).strip()
|
| 630 |
+
L = len(compact)
|
| 631 |
+
financial = bool(re.search(r"\d", joined)) or bool(_CURRENCY_SNIFF.search(joined))
|
| 632 |
+
if financial:
|
| 633 |
+
return False
|
| 634 |
+
if n >= 12 and nonempty <= max(2, int(n * 0.06)):
|
| 635 |
+
return True
|
| 636 |
+
if n >= 8 and nonempty <= 1 and L < 80:
|
| 637 |
+
return True
|
| 638 |
+
return False
|
| 639 |
+
|
| 640 |
+
def repl_table(m: re.Match) -> str:
|
| 641 |
+
return "" if should_drop(m.group(0)) else m.group(0)
|
| 642 |
+
|
| 643 |
+
out = re.sub(
|
| 644 |
+
r"<table\b[^>]*>.*?</table>",
|
| 645 |
+
repl_table,
|
| 646 |
+
md,
|
| 647 |
+
flags=re.IGNORECASE | re.DOTALL,
|
| 648 |
+
)
|
| 649 |
+
return re.sub(r"\n{3,}", "\n\n", out)
|
| 650 |
+
|
| 651 |
+
|
| 652 |
def looks_like_markdown_table(block: str) -> bool:
|
| 653 |
lines = [ln.rstrip() for ln in block.strip().splitlines() if ln.strip()]
|
| 654 |
if len(lines) < 2:
|
|
|
|
| 718 |
else:
|
| 719 |
out_blocks.append(b)
|
| 720 |
merged = close_unclosed_html("\n\n".join(out_blocks))
|
| 721 |
+
merged = repair_thead_cell_semantics(merged)
|
| 722 |
+
merged = stabilize_table_markup(merged, rounds=4)
|
| 723 |
+
merged = strip_degenerate_html_tables(merged)
|
| 724 |
+
return merged
|
| 725 |
|
| 726 |
|
| 727 |
def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
|
|
|
| 861 |
merged = "\n\n---page-separator---\n\n".join(all_pages) if all_pages else "(No content)"
|
| 862 |
if merged and merged != "(No content)" and not merged.lstrip().startswith("Error:"):
|
| 863 |
merged = stabilize_table_markup(merged, rounds=4)
|
| 864 |
+
merged = repair_thead_cell_semantics(merged)
|
| 865 |
+
merged = strip_degenerate_html_tables(merged)
|
| 866 |
return merged
|
| 867 |
|
| 868 |
except Exception as e:
|