Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,12 @@ Scope (intentionally small):
|
|
| 8 |
- Footer band: same pattern, with light dedup so we do not paste a full
|
| 9 |
transaction dump twice when the body already captured it
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
Explicitly omitted vs the heavy Space build:
|
| 12 |
- No HTML table rewriting, text-layer row injection, institution-specific
|
| 13 |
splits (UCB / Navy / TD / First Horizon / …), or doc-wide dedupe passes
|
|
@@ -55,19 +61,33 @@ log = logging.getLogger("glmocr_simple_app")
|
|
| 55 |
logging.basicConfig(level=logging.INFO)
|
| 56 |
|
| 57 |
# ---------------------------------------------------------------------------
|
| 58 |
-
# Settings
|
| 59 |
# ---------------------------------------------------------------------------
|
| 60 |
|
| 61 |
-
GLMOCR_API_KEY =
|
| 62 |
if not GLMOCR_API_KEY:
|
| 63 |
log.warning("GLMOCR_API_KEY is not set; GlmOcr() will fail until you export it.")
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
ENABLE_CONTRAST = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
DEFAULT_ZONE_FRAC = 0.12
|
| 73 |
PDF_HEADER_BAND_FRAC = 0.10
|
|
@@ -78,9 +98,35 @@ PDF_FOOTER_BAND_FRAC = 0.88
|
|
| 78 |
MIN_CROP_HEIGHT = 112
|
| 79 |
MIN_CROP_PIXELS = 112 * 112
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
_parser = None
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def get_parser():
|
| 85 |
global _parser
|
| 86 |
if glmocr is None:
|
|
@@ -186,7 +232,7 @@ def ocr_zone(image_path, y_start_frac, y_end_frac):
|
|
| 186 |
fd, path = tempfile.mkstemp(suffix=".jpg")
|
| 187 |
os.close(fd)
|
| 188 |
try:
|
| 189 |
-
crop.save(path, "JPEG", quality=
|
| 190 |
parser = get_parser()
|
| 191 |
out = parser.parse(path)
|
| 192 |
if not isinstance(out, list):
|
|
@@ -308,7 +354,7 @@ def light_stabilize_markdown(page_md: str) -> str:
|
|
| 308 |
|
| 309 |
def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
| 310 |
import pymupdf as fitz
|
| 311 |
-
from PIL import Image
|
| 312 |
|
| 313 |
doc = fitz.open(pdf_path)
|
| 314 |
page_images: List[str] = []
|
|
@@ -319,9 +365,7 @@ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
|
| 319 |
pix = page.get_pixmap(matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False)
|
| 320 |
|
| 321 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 322 |
-
|
| 323 |
-
if ENABLE_CONTRAST:
|
| 324 |
-
img = ImageEnhance.Contrast(img).enhance(1.12)
|
| 325 |
|
| 326 |
w, h = img.size
|
| 327 |
pad_l = int(w * PAD_LEFT_FRAC)
|
|
@@ -335,7 +379,7 @@ def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
|
| 335 |
img = canvas
|
| 336 |
|
| 337 |
img_path = os.path.join(tempfile.gettempdir(), f"glmocr_page_{os.getpid()}_{i}.png")
|
| 338 |
-
img.save(img_path, "PNG", compress_level=
|
| 339 |
page_images.append(img_path)
|
| 340 |
page_heights.append(img.height)
|
| 341 |
|
|
|
|
| 8 |
- Footer band: same pattern, with light dedup so we do not paste a full
|
| 9 |
transaction dump twice when the body already captured it
|
| 10 |
|
| 11 |
+
Universal image pipeline (same for every PDF, no keywords / no bank logic):
|
| 12 |
+
- Higher rasterization scale + extra white padding so fine print, boxed
|
| 13 |
+
section labels, and right-aligned amounts sit farther from the clip edge.
|
| 14 |
+
- Mild contrast + unsharp mask on every raster sent to the model so
|
| 15 |
+
thin rules and small glyphs are easier to read before recognition.
|
| 16 |
+
|
| 17 |
Explicitly omitted vs the heavy Space build:
|
| 18 |
- No HTML table rewriting, text-layer row injection, institution-specific
|
| 19 |
splits (UCB / Navy / TD / First Horizon / …), or doc-wide dedupe passes
|
|
|
|
| 61 |
logging.basicConfig(level=logging.INFO)
|
| 62 |
|
| 63 |
# ---------------------------------------------------------------------------
|
| 64 |
+
# Settings — tuned for dense financial PDFs; applies to every document
|
| 65 |
# ---------------------------------------------------------------------------
|
| 66 |
|
| 67 |
+
GLMOCR_API_KEY = os.environ.get("GLMOCR_API_KEY", "").strip()
|
| 68 |
if not GLMOCR_API_KEY:
|
| 69 |
log.warning("GLMOCR_API_KEY is not set; GlmOcr() will fail until you export it.")
|
| 70 |
|
| 71 |
+
# Rasterization: higher scale = more pixels per PDF point (helps small type,
|
| 72 |
+
# boxed headers, and narrow columns). Same constant for all uploads.
|
| 73 |
+
RENDER_SCALE = 2.85
|
| 74 |
+
|
| 75 |
+
# White margin as a fraction of page width/height after render. Extra right
|
| 76 |
+
# margin helps right-aligned currency columns that hug the page edge.
|
| 77 |
+
PAD_LEFT_FRAC = 0.03
|
| 78 |
+
PAD_RIGHT_FRAC = 0.10
|
| 79 |
+
PAD_TOP_FRAC = 0.015
|
| 80 |
+
PAD_BOTTOM_FRAC = 0.015
|
| 81 |
+
|
| 82 |
ENABLE_CONTRAST = True
|
| 83 |
+
# Slight contrast lift only; same factor for every file.
|
| 84 |
+
CONTRAST_FACTOR = 1.12
|
| 85 |
+
|
| 86 |
+
# Subtle edge enhancement after contrast (helps hairlines and small digits).
|
| 87 |
+
ENABLE_UNSHARP = True
|
| 88 |
+
UNSHARP_RADIUS = 0.85
|
| 89 |
+
UNSHARP_PERCENT = 65
|
| 90 |
+
UNSHARP_THRESHOLD = 2
|
| 91 |
|
| 92 |
DEFAULT_ZONE_FRAC = 0.12
|
| 93 |
PDF_HEADER_BAND_FRAC = 0.10
|
|
|
|
| 98 |
MIN_CROP_HEIGHT = 112
|
| 99 |
MIN_CROP_PIXELS = 112 * 112
|
| 100 |
|
| 101 |
+
# PNG compression 0–9; lower = less loss before GLM-OCR (same for all PDFs).
|
| 102 |
+
PAGE_PNG_COMPRESS_LEVEL = 3
|
| 103 |
+
# JPEG quality for small header/footer crops sent to the API.
|
| 104 |
+
ZONE_JPEG_QUALITY = 95
|
| 105 |
+
|
| 106 |
_parser = None
|
| 107 |
|
| 108 |
|
| 109 |
+
def _enhance_raster_for_ocr(img):
|
| 110 |
+
"""
|
| 111 |
+
Improve legibility of every raster passed to GLM-OCR (full pages and
|
| 112 |
+
header/footer crops). No document text or keywords — same pipeline for
|
| 113 |
+
all PDFs and images.
|
| 114 |
+
"""
|
| 115 |
+
from PIL import ImageEnhance, ImageFilter
|
| 116 |
+
|
| 117 |
+
if ENABLE_CONTRAST:
|
| 118 |
+
img = ImageEnhance.Contrast(img).enhance(CONTRAST_FACTOR)
|
| 119 |
+
if ENABLE_UNSHARP:
|
| 120 |
+
img = img.filter(
|
| 121 |
+
ImageFilter.UnsharpMask(
|
| 122 |
+
radius=UNSHARP_RADIUS,
|
| 123 |
+
percent=UNSHARP_PERCENT,
|
| 124 |
+
threshold=UNSHARP_THRESHOLD,
|
| 125 |
+
)
|
| 126 |
+
)
|
| 127 |
+
return img
|
| 128 |
+
|
| 129 |
+
|
| 130 |
def get_parser():
|
| 131 |
global _parser
|
| 132 |
if glmocr is None:
|
|
|
|
| 232 |
fd, path = tempfile.mkstemp(suffix=".jpg")
|
| 233 |
os.close(fd)
|
| 234 |
try:
|
| 235 |
+
crop.save(path, "JPEG", quality=ZONE_JPEG_QUALITY)
|
| 236 |
parser = get_parser()
|
| 237 |
out = parser.parse(path)
|
| 238 |
if not isinstance(out, list):
|
|
|
|
| 354 |
|
| 355 |
def render_pdf_pages_to_images(pdf_path: str) -> Tuple[List[str], List[int]]:
|
| 356 |
import pymupdf as fitz
|
| 357 |
+
from PIL import Image
|
| 358 |
|
| 359 |
doc = fitz.open(pdf_path)
|
| 360 |
page_images: List[str] = []
|
|
|
|
| 365 |
pix = page.get_pixmap(matrix=fitz.Matrix(RENDER_SCALE, RENDER_SCALE), alpha=False)
|
| 366 |
|
| 367 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 368 |
+
img = _enhance_raster_for_ocr(img)
|
|
|
|
|
|
|
| 369 |
|
| 370 |
w, h = img.size
|
| 371 |
pad_l = int(w * PAD_LEFT_FRAC)
|
|
|
|
| 379 |
img = canvas
|
| 380 |
|
| 381 |
img_path = os.path.join(tempfile.gettempdir(), f"glmocr_page_{os.getpid()}_{i}.png")
|
| 382 |
+
img.save(img_path, "PNG", compress_level=PAGE_PNG_COMPRESS_LEVEL)
|
| 383 |
page_images.append(img_path)
|
| 384 |
page_heights.append(img.height)
|
| 385 |
|