Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
import logging
|
| 4 |
import os
|
|
|
|
| 5 |
import tempfile
|
| 6 |
import uuid
|
| 7 |
from typing import List, Tuple
|
|
@@ -10,10 +11,9 @@ log = logging.getLogger("glmocr_simple_app")
|
|
| 10 |
logging.basicConfig(level=logging.INFO)
|
| 11 |
|
| 12 |
# ββ Fine-tuned model repo on HuggingFace βββββββββββββββββββββββββββββββββββββ
|
| 13 |
-
# Loads from HF Hub at runtime β no local storage needed in the Space
|
| 14 |
MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
|
| 15 |
|
| 16 |
-
|
| 17 |
PAD_LEFT_FRAC = 0.035
|
| 18 |
PAD_RIGHT_FRAC = 0.10
|
| 19 |
PAD_TOP_FRAC = 0.018
|
|
@@ -27,9 +27,8 @@ UNSHARP_PERCENT = 76
|
|
| 27 |
UNSHARP_THRESHOLD = 1
|
| 28 |
|
| 29 |
PAGE_PNG_COMPRESS_LEVEL = 3
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
MAX_NEW_TOKENS = 3000 # resize longest side to this before inference
|
| 33 |
|
| 34 |
# ββ Model singleton βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
_model = None
|
|
@@ -88,6 +87,98 @@ def _resize_for_inference(img):
|
|
| 88 |
return img.resize(new_size, Image.LANCZOS)
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def _infer_image(image_path: str) -> str:
|
| 92 |
"""Run fine-tuned model on a single image file and return markdown string."""
|
| 93 |
import torch
|
|
@@ -127,13 +218,19 @@ def _infer_image(image_path: str) -> str:
|
|
| 127 |
**inputs,
|
| 128 |
max_new_tokens=MAX_NEW_TOKENS,
|
| 129 |
do_sample=False,
|
| 130 |
-
repetition_penalty=1.
|
|
|
|
| 131 |
)
|
|
|
|
| 132 |
result = processor.decode(
|
| 133 |
ids[0][inputs["input_ids"].shape[1]:],
|
| 134 |
skip_special_tokens=True,
|
| 135 |
)
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
finally:
|
| 139 |
try:
|
|
@@ -247,7 +344,5 @@ def _create_gradio_demo():
|
|
| 247 |
|
| 248 |
|
| 249 |
if __name__ == "__main__":
|
| 250 |
-
# Force model to load at startup
|
| 251 |
_load_model()
|
| 252 |
-
|
| 253 |
_create_gradio_demo().launch()
|
|
|
|
| 2 |
|
| 3 |
import logging
|
| 4 |
import os
|
| 5 |
+
import re
|
| 6 |
import tempfile
|
| 7 |
import uuid
|
| 8 |
from typing import List, Tuple
|
|
|
|
| 11 |
logging.basicConfig(level=logging.INFO)
|
| 12 |
|
| 13 |
# ββ Fine-tuned model repo on HuggingFace βββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 14 |
MERGED_MODEL_DIR = os.environ.get("MODEL_DIR", "SimpleCodeAI/glm-ocr-finetuned")
|
| 15 |
|
| 16 |
+
RENDER_SCALE = 2.0
|
| 17 |
PAD_LEFT_FRAC = 0.035
|
| 18 |
PAD_RIGHT_FRAC = 0.10
|
| 19 |
PAD_TOP_FRAC = 0.018
|
|
|
|
| 27 |
UNSHARP_THRESHOLD = 1
|
| 28 |
|
| 29 |
PAGE_PNG_COMPRESS_LEVEL = 3
|
| 30 |
+
MAX_IMAGE_SIDE = 1568
|
| 31 |
+
MAX_NEW_TOKENS = 3000
|
|
|
|
| 32 |
|
| 33 |
# ββ Model singleton βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
_model = None
|
|
|
|
| 87 |
return img.resize(new_size, Image.LANCZOS)
|
| 88 |
|
| 89 |
|
| 90 |
+
def _normalize_tables(text: str) -> str:
|
| 91 |
+
"""
|
| 92 |
+
Convert plain-text table rows (no pipe boundaries) into proper
|
| 93 |
+
markdown table rows with | boundaries.
|
| 94 |
+
|
| 95 |
+
Handles rows like:
|
| 96 |
+
10/01 CCD DEBIT, SOME MERCHANT 654.00
|
| 97 |
+
10/01 DEBIT 2,500.00
|
| 98 |
+
and header rows like:
|
| 99 |
+
POSTING DATE DESCRIPTION AMOUNT
|
| 100 |
+
"""
|
| 101 |
+
lines = text.split('\n')
|
| 102 |
+
result = []
|
| 103 |
+
i = 0
|
| 104 |
+
|
| 105 |
+
# Amount pattern: number like 1,234.56 or 123.45
|
| 106 |
+
amt_re = re.compile(r'[\d,]+\.\d{2}$')
|
| 107 |
+
# Date pattern at start of line: MM/DD
|
| 108 |
+
date_re = re.compile(r'^\d{2}/\d{2}\s')
|
| 109 |
+
# Header pattern
|
| 110 |
+
header_re = re.compile(
|
| 111 |
+
r'^(POSTING\s+DATE|DATE)\s+(DESCRIPTION|SERIAL\s+NO\.?|NO\.?\s+CHECKS)',
|
| 112 |
+
re.IGNORECASE
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
in_plain_table = False
|
| 116 |
+
|
| 117 |
+
while i < len(lines):
|
| 118 |
+
line = lines[i]
|
| 119 |
+
stripped = line.strip()
|
| 120 |
+
|
| 121 |
+
# Skip empty lines β reset table tracking
|
| 122 |
+
if not stripped:
|
| 123 |
+
in_plain_table = False
|
| 124 |
+
result.append(line)
|
| 125 |
+
i += 1
|
| 126 |
+
continue
|
| 127 |
+
|
| 128 |
+
# Already a proper markdown table row β pass through
|
| 129 |
+
if stripped.startswith('|'):
|
| 130 |
+
in_plain_table = False
|
| 131 |
+
result.append(line)
|
| 132 |
+
i += 1
|
| 133 |
+
continue
|
| 134 |
+
|
| 135 |
+
# Detect plain-text table header row
|
| 136 |
+
if header_re.match(stripped):
|
| 137 |
+
# Split by 2+ spaces
|
| 138 |
+
parts = re.split(r'\s{2,}', stripped)
|
| 139 |
+
parts = [p.strip() for p in parts if p.strip()]
|
| 140 |
+
if len(parts) >= 2:
|
| 141 |
+
result.append('| ' + ' | '.join(parts) + ' |')
|
| 142 |
+
result.append('| ' + ' | '.join(['---'] * len(parts)) + ' |')
|
| 143 |
+
in_plain_table = True
|
| 144 |
+
i += 1
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
# Detect plain-text data row starting with date
|
| 148 |
+
if date_re.match(stripped):
|
| 149 |
+
# Split by 2+ spaces to separate columns
|
| 150 |
+
parts = re.split(r'\s{2,}', stripped)
|
| 151 |
+
parts = [p.strip() for p in parts if p.strip()]
|
| 152 |
+
|
| 153 |
+
# If only one part (description runs together), try splitting off amount
|
| 154 |
+
if len(parts) == 1 and amt_re.search(stripped):
|
| 155 |
+
# Split last amount token off
|
| 156 |
+
m = re.search(r'^(.*?)\s+([\d,]+\.\d{2})$', stripped)
|
| 157 |
+
if m:
|
| 158 |
+
parts = [m.group(1).strip(), m.group(2).strip()]
|
| 159 |
+
|
| 160 |
+
if len(parts) >= 2:
|
| 161 |
+
result.append('| ' + ' | '.join(parts) + ' |')
|
| 162 |
+
in_plain_table = True
|
| 163 |
+
i += 1
|
| 164 |
+
continue
|
| 165 |
+
|
| 166 |
+
# Detect subtotal lines inside a plain table
|
| 167 |
+
if in_plain_table and re.match(r'^Subtotal:', stripped, re.IGNORECASE):
|
| 168 |
+
m = re.match(r'^(Subtotal:)\s+([\d,]+\.\d{2})', stripped, re.IGNORECASE)
|
| 169 |
+
if m:
|
| 170 |
+
result.append(f'| | **{m.group(1)}** | **{m.group(2)}** |')
|
| 171 |
+
i += 1
|
| 172 |
+
continue
|
| 173 |
+
|
| 174 |
+
# Not a table row
|
| 175 |
+
in_plain_table = False
|
| 176 |
+
result.append(line)
|
| 177 |
+
i += 1
|
| 178 |
+
|
| 179 |
+
return '\n'.join(result)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
def _infer_image(image_path: str) -> str:
|
| 183 |
"""Run fine-tuned model on a single image file and return markdown string."""
|
| 184 |
import torch
|
|
|
|
| 218 |
**inputs,
|
| 219 |
max_new_tokens=MAX_NEW_TOKENS,
|
| 220 |
do_sample=False,
|
| 221 |
+
repetition_penalty=1.15,
|
| 222 |
+
no_repeat_ngram_size=5,
|
| 223 |
)
|
| 224 |
+
|
| 225 |
result = processor.decode(
|
| 226 |
ids[0][inputs["input_ids"].shape[1]:],
|
| 227 |
skip_special_tokens=True,
|
| 228 |
)
|
| 229 |
+
|
| 230 |
+
# Normalize plain-text tables to proper markdown tables
|
| 231 |
+
result = _normalize_tables(result.strip())
|
| 232 |
+
|
| 233 |
+
return result
|
| 234 |
|
| 235 |
finally:
|
| 236 |
try:
|
|
|
|
| 344 |
|
| 345 |
|
| 346 |
if __name__ == "__main__":
|
|
|
|
| 347 |
_load_model()
|
|
|
|
| 348 |
_create_gradio_demo().launch()
|