Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -44,8 +44,8 @@ HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
|
| 44 |
# Logging config β edit these two constants to match your HF dataset repo
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
|
| 47 |
-
LOG_REPO_ID = "PrathameshRaut/VisionModelLogs"
|
| 48 |
-
LOG_FILENAME = "extraction_logs.xlsx"
|
| 49 |
|
| 50 |
LOG_HEADERS = [
|
| 51 |
"timestamp_utc",
|
|
@@ -54,9 +54,9 @@ LOG_HEADERS = [
|
|
| 54 |
"file_type",
|
| 55 |
"document_type",
|
| 56 |
"parameters_requested",
|
| 57 |
-
"model_raw_thinking",
|
| 58 |
-
"extracted_json",
|
| 59 |
-
"status",
|
| 60 |
"error_detail",
|
| 61 |
]
|
| 62 |
|
|
@@ -112,18 +112,17 @@ def _download_or_create_workbook() -> openpyxl.Workbook:
|
|
| 112 |
ws.title = "Logs"
|
| 113 |
ws.append(LOG_HEADERS)
|
| 114 |
_style_header_row(ws)
|
| 115 |
-
# Set column widths
|
| 116 |
col_widths = {
|
| 117 |
-
"A": 22,
|
| 118 |
-
"B": 30,
|
| 119 |
-
"C": 16,
|
| 120 |
-
"D": 14,
|
| 121 |
-
"E": 22,
|
| 122 |
-
"F": 35,
|
| 123 |
-
"G": 60,
|
| 124 |
-
"H": 60,
|
| 125 |
-
"I": 12,
|
| 126 |
-
"J": 40,
|
| 127 |
}
|
| 128 |
for col, width in col_widths.items():
|
| 129 |
ws.column_dimensions[col].width = width
|
|
@@ -160,7 +159,6 @@ def append_log(
|
|
| 160 |
):
|
| 161 |
"""Thread-safe: download β append row β upload."""
|
| 162 |
if not HF_TOKEN:
|
| 163 |
-
# Logging silently skipped if token not configured
|
| 164 |
return
|
| 165 |
|
| 166 |
row = [
|
|
@@ -170,7 +168,7 @@ def append_log(
|
|
| 170 |
file_type,
|
| 171 |
document_type,
|
| 172 |
", ".join(parameters),
|
| 173 |
-
(raw_thinking or "")[:5000],
|
| 174 |
json.dumps(extracted_json, ensure_ascii=False) if extracted_json else "",
|
| 175 |
status,
|
| 176 |
error_detail[:1000],
|
|
@@ -183,7 +181,6 @@ def append_log(
|
|
| 183 |
|
| 184 |
ws.append(row)
|
| 185 |
|
| 186 |
-
# Style the new data row
|
| 187 |
data_font = Font(name="Arial", size=10)
|
| 188 |
wrap_align = Alignment(vertical="top", wrap_text=True)
|
| 189 |
row_idx = ws.max_row
|
|
@@ -191,7 +188,6 @@ def append_log(
|
|
| 191 |
cell.font = data_font
|
| 192 |
cell.alignment = wrap_align
|
| 193 |
|
| 194 |
-
# Zebra striping: light blue on even rows
|
| 195 |
if row_idx % 2 == 0:
|
| 196 |
fill = PatternFill("solid", start_color="DCE6F1")
|
| 197 |
for cell in ws[row_idx]:
|
|
@@ -199,7 +195,6 @@ def append_log(
|
|
| 199 |
|
| 200 |
_upload_workbook(wb)
|
| 201 |
except Exception as e:
|
| 202 |
-
# Logging must never crash the main API
|
| 203 |
print(f"[LOG WARNING] Failed to write log entry: {e}")
|
| 204 |
|
| 205 |
|
|
@@ -242,15 +237,6 @@ def prepare_file(file_bytes: bytes, content_type: str, filename: str) -> tuple[b
|
|
| 242 |
# ---------------------------------------------------------------------------
|
| 243 |
|
| 244 |
def clean_raw_text(text: str) -> str:
|
| 245 |
-
"""
|
| 246 |
-
Remove base64-encoded image data that Sarvam Document Intelligence
|
| 247 |
-
carries over from PDFs/images into the markdown output.
|
| 248 |
-
These blobs are never useful for field extraction and can easily push
|
| 249 |
-
the prompt far beyond sarvam-m's 7 192-token context window.
|
| 250 |
-
Two patterns are handled:
|
| 251 |
-
1. Markdown image syntax: 
|
| 252 |
-
2. Bare data URIs that leaked outside markdown syntax
|
| 253 |
-
"""
|
| 254 |
# Pattern 1 β markdown image embed
|
| 255 |
text = re.sub(
|
| 256 |
r'!\[[^\]]*\]\(data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]+\)',
|
|
@@ -258,8 +244,7 @@ def clean_raw_text(text: str) -> str:
|
|
| 258 |
text,
|
| 259 |
flags=re.DOTALL,
|
| 260 |
)
|
| 261 |
-
# Pattern 2 β bare data URI
|
| 262 |
-
# accidentally removing short legitimate strings)
|
| 263 |
text = re.sub(
|
| 264 |
r'data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]{100,}',
|
| 265 |
'',
|
|
@@ -277,7 +262,11 @@ def extract_text_with_sdk(file_bytes: bytes, filename: str) -> str:
|
|
| 277 |
if not SARVAM_API_KEY:
|
| 278 |
raise HTTPException(status_code=500, detail="SARVAM_API_KEY is not configured on the server")
|
| 279 |
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
suffix = Path(filename).suffix.lower()
|
| 283 |
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
@@ -317,19 +306,13 @@ def extract_text_with_sdk(file_bytes: bytes, filename: str) -> str:
|
|
| 317 |
except FileNotFoundError:
|
| 318 |
pass
|
| 319 |
|
|
|
|
| 320 |
# ---------------------------------------------------------------------------
|
| 321 |
# Step 2: Extract structured JSON via SarvamAI SDK chat completions
|
| 322 |
# ---------------------------------------------------------------------------
|
| 323 |
|
| 324 |
def strip_think_tags(text: str) -> str:
|
| 325 |
-
"""
|
| 326 |
-
Remove <think>β¦</think> reasoning blocks from sarvam-m output.
|
| 327 |
-
Handles both complete tags and truncated blocks where the closing
|
| 328 |
-
tag was cut off due to max_tokens being reached mid-response.
|
| 329 |
-
"""
|
| 330 |
-
# Pass 1: remove complete <think>β¦</think> blocks
|
| 331 |
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
| 332 |
-
# Pass 2: remove any unclosed/truncated <think> block that runs to end-of-string
|
| 333 |
text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
|
| 334 |
return text.strip()
|
| 335 |
|
|
@@ -341,6 +324,9 @@ async def extract_json_via_chat(doc_type: str, raw_text: str, parameters: list[s
|
|
| 341 |
description = SUPPORTED_TYPES[doc_type]
|
| 342 |
param_list = "\n".join(f" - {p}" for p in parameters)
|
| 343 |
|
|
|
|
|
|
|
|
|
|
| 344 |
prompt = f"""You are a document data extraction assistant. Below is the full text extracted from a {description}.
|
| 345 |
--- DOCUMENT TEXT START ---
|
| 346 |
{raw_text}
|
|
@@ -355,24 +341,43 @@ Rules:
|
|
| 355 |
5. Respond everything in english language only. Also convert date into proper DD/MM/YYYY format.
|
| 356 |
Respond with the JSON object only."""
|
| 357 |
|
| 358 |
-
# Run the synchronous SDK call in a thread so we don't block the event loop
|
| 359 |
def _call_sdk() -> str:
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
temperature=0,
|
| 365 |
-
top_p=1,
|
| 366 |
-
max_tokens=2048,
|
| 367 |
)
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
try:
|
| 371 |
raw = await asyncio.to_thread(_call_sdk)
|
|
|
|
|
|
|
| 372 |
except Exception as exc:
|
| 373 |
raise HTTPException(status_code=500, detail=f"Sarvam chat error: {exc}") from exc
|
| 374 |
|
| 375 |
-
# Remove <think> blocks
|
| 376 |
raw = strip_think_tags(raw)
|
| 377 |
|
| 378 |
# Strip markdown code fences if present
|
|
@@ -388,6 +393,7 @@ Respond with the JSON object only."""
|
|
| 388 |
except json.JSONDecodeError:
|
| 389 |
return {"raw_response": raw, "parse_error": "Model did not return valid JSON"}
|
| 390 |
|
|
|
|
| 391 |
# ---------------------------------------------------------------------------
|
| 392 |
# Routes
|
| 393 |
# ---------------------------------------------------------------------------
|
|
@@ -440,8 +446,8 @@ async def extract_document(
|
|
| 440 |
filename = file.filename or "document"
|
| 441 |
file_size = len(file_bytes)
|
| 442 |
|
| 443 |
-
raw_text
|
| 444 |
-
extracted
|
| 445 |
|
| 446 |
try:
|
| 447 |
processed_bytes, processed_name = prepare_file(file_bytes, content_type, filename)
|
|
@@ -463,7 +469,7 @@ async def extract_document(
|
|
| 463 |
)
|
| 464 |
raise
|
| 465 |
|
| 466 |
-
# Strip base64 image blobs before sending to
|
| 467 |
raw_text = clean_raw_text(raw_text)
|
| 468 |
|
| 469 |
try:
|
|
|
|
| 44 |
# Logging config β edit these two constants to match your HF dataset repo
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
|
| 47 |
+
LOG_REPO_ID = "PrathameshRaut/VisionModelLogs"
|
| 48 |
+
LOG_FILENAME = "extraction_logs.xlsx"
|
| 49 |
|
| 50 |
LOG_HEADERS = [
|
| 51 |
"timestamp_utc",
|
|
|
|
| 54 |
"file_type",
|
| 55 |
"document_type",
|
| 56 |
"parameters_requested",
|
| 57 |
+
"model_raw_thinking",
|
| 58 |
+
"extracted_json",
|
| 59 |
+
"status",
|
| 60 |
"error_detail",
|
| 61 |
]
|
| 62 |
|
|
|
|
| 112 |
ws.title = "Logs"
|
| 113 |
ws.append(LOG_HEADERS)
|
| 114 |
_style_header_row(ws)
|
|
|
|
| 115 |
col_widths = {
|
| 116 |
+
"A": 22,
|
| 117 |
+
"B": 30,
|
| 118 |
+
"C": 16,
|
| 119 |
+
"D": 14,
|
| 120 |
+
"E": 22,
|
| 121 |
+
"F": 35,
|
| 122 |
+
"G": 60,
|
| 123 |
+
"H": 60,
|
| 124 |
+
"I": 12,
|
| 125 |
+
"J": 40,
|
| 126 |
}
|
| 127 |
for col, width in col_widths.items():
|
| 128 |
ws.column_dimensions[col].width = width
|
|
|
|
| 159 |
):
|
| 160 |
"""Thread-safe: download β append row β upload."""
|
| 161 |
if not HF_TOKEN:
|
|
|
|
| 162 |
return
|
| 163 |
|
| 164 |
row = [
|
|
|
|
| 168 |
file_type,
|
| 169 |
document_type,
|
| 170 |
", ".join(parameters),
|
| 171 |
+
(raw_thinking or "")[:5000],
|
| 172 |
json.dumps(extracted_json, ensure_ascii=False) if extracted_json else "",
|
| 173 |
status,
|
| 174 |
error_detail[:1000],
|
|
|
|
| 181 |
|
| 182 |
ws.append(row)
|
| 183 |
|
|
|
|
| 184 |
data_font = Font(name="Arial", size=10)
|
| 185 |
wrap_align = Alignment(vertical="top", wrap_text=True)
|
| 186 |
row_idx = ws.max_row
|
|
|
|
| 188 |
cell.font = data_font
|
| 189 |
cell.alignment = wrap_align
|
| 190 |
|
|
|
|
| 191 |
if row_idx % 2 == 0:
|
| 192 |
fill = PatternFill("solid", start_color="DCE6F1")
|
| 193 |
for cell in ws[row_idx]:
|
|
|
|
| 195 |
|
| 196 |
_upload_workbook(wb)
|
| 197 |
except Exception as e:
|
|
|
|
| 198 |
print(f"[LOG WARNING] Failed to write log entry: {e}")
|
| 199 |
|
| 200 |
|
|
|
|
| 237 |
# ---------------------------------------------------------------------------
|
| 238 |
|
| 239 |
def clean_raw_text(text: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
# Pattern 1 β markdown image embed
|
| 241 |
text = re.sub(
|
| 242 |
r'!\[[^\]]*\]\(data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]+\)',
|
|
|
|
| 244 |
text,
|
| 245 |
flags=re.DOTALL,
|
| 246 |
)
|
| 247 |
+
# Pattern 2 β bare data URI
|
|
|
|
| 248 |
text = re.sub(
|
| 249 |
r'data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]{100,}',
|
| 250 |
'',
|
|
|
|
| 262 |
if not SARVAM_API_KEY:
|
| 263 |
raise HTTPException(status_code=500, detail="SARVAM_API_KEY is not configured on the server")
|
| 264 |
|
| 265 |
+
# FIX: increased timeout so the polling loop doesn't give up too early
|
| 266 |
+
client = SarvamAI(
|
| 267 |
+
api_subscription_key=SARVAM_API_KEY,
|
| 268 |
+
timeout_in_seconds=180,
|
| 269 |
+
)
|
| 270 |
|
| 271 |
suffix = Path(filename).suffix.lower()
|
| 272 |
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
|
|
| 306 |
except FileNotFoundError:
|
| 307 |
pass
|
| 308 |
|
| 309 |
+
|
| 310 |
# ---------------------------------------------------------------------------
|
| 311 |
# Step 2: Extract structured JSON via SarvamAI SDK chat completions
|
| 312 |
# ---------------------------------------------------------------------------
|
| 313 |
|
| 314 |
def strip_think_tags(text: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
|
|
|
| 316 |
text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
|
| 317 |
return text.strip()
|
| 318 |
|
|
|
|
| 324 |
description = SUPPORTED_TYPES[doc_type]
|
| 325 |
param_list = "\n".join(f" - {p}" for p in parameters)
|
| 326 |
|
| 327 |
+
# Truncate to avoid blowing context window
|
| 328 |
+
raw_text = raw_text[:6000] if len(raw_text) > 6000 else raw_text
|
| 329 |
+
|
| 330 |
prompt = f"""You are a document data extraction assistant. Below is the full text extracted from a {description}.
|
| 331 |
--- DOCUMENT TEXT START ---
|
| 332 |
{raw_text}
|
|
|
|
| 341 |
5. Respond everything in english language only. Also convert date into proper DD/MM/YYYY format.
|
| 342 |
Respond with the JSON object only."""
|
| 343 |
|
|
|
|
| 344 |
def _call_sdk() -> str:
|
| 345 |
+
# FIX: increased timeout β sarvam-105b reasoning can take >60s
|
| 346 |
+
client = SarvamAI(
|
| 347 |
+
api_subscription_key=SARVAM_API_KEY,
|
| 348 |
+
timeout_in_seconds=180,
|
|
|
|
|
|
|
|
|
|
| 349 |
)
|
| 350 |
+
|
| 351 |
+
# FIX: retry up to 3 times β 105b occasionally returns null content
|
| 352 |
+
for attempt in range(3):
|
| 353 |
+
response = client.chat.completions(
|
| 354 |
+
model="sarvam-105b",
|
| 355 |
+
messages=[{"role": "user", "content": prompt}],
|
| 356 |
+
temperature=0,
|
| 357 |
+
top_p=1,
|
| 358 |
+
max_tokens=2048,
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# FIX: guard against null content before calling .strip()
|
| 362 |
+
content = response.choices[0].message.content
|
| 363 |
+
if content:
|
| 364 |
+
return content.strip()
|
| 365 |
+
|
| 366 |
+
print(
|
| 367 |
+
f"[WARN] Attempt {attempt + 1}/3: Sarvam returned null content "
|
| 368 |
+
f"(finish_reason={response.choices[0].finish_reason!r}), retrying..."
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
raise ValueError("Sarvam returned null content after 3 attempts")
|
| 372 |
|
| 373 |
try:
|
| 374 |
raw = await asyncio.to_thread(_call_sdk)
|
| 375 |
+
except ValueError as exc:
|
| 376 |
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
| 377 |
except Exception as exc:
|
| 378 |
raise HTTPException(status_code=500, detail=f"Sarvam chat error: {exc}") from exc
|
| 379 |
|
| 380 |
+
# Remove <think> blocks
|
| 381 |
raw = strip_think_tags(raw)
|
| 382 |
|
| 383 |
# Strip markdown code fences if present
|
|
|
|
| 393 |
except json.JSONDecodeError:
|
| 394 |
return {"raw_response": raw, "parse_error": "Model did not return valid JSON"}
|
| 395 |
|
| 396 |
+
|
| 397 |
# ---------------------------------------------------------------------------
|
| 398 |
# Routes
|
| 399 |
# ---------------------------------------------------------------------------
|
|
|
|
| 446 |
filename = file.filename or "document"
|
| 447 |
file_size = len(file_bytes)
|
| 448 |
|
| 449 |
+
raw_text = ""
|
| 450 |
+
extracted = {}
|
| 451 |
|
| 452 |
try:
|
| 453 |
processed_bytes, processed_name = prepare_file(file_bytes, content_type, filename)
|
|
|
|
| 469 |
)
|
| 470 |
raise
|
| 471 |
|
| 472 |
+
# Strip base64 image blobs before sending to chat model
|
| 473 |
raw_text = clean_raw_text(raw_text)
|
| 474 |
|
| 475 |
try:
|