import base64 import binascii import io import json import logging import time import traceback from typing import Any, Dict import requests from fastapi import Request from PIL import Image import tools from vllm_batcher import vllm_ocr_batcher from vllm_tools import page_ocr_to_response logger = logging.getLogger("surya.endpoint") class OcrRequestError(Exception): """Client-side error in an OCR request. Maps to HTTP 400.""" # ---- helpers moved verbatim from api.py ------------------------------------ def _load_base64_image(p, copy_highres: bool = False): start = time.perf_counter() filetype = p.type.lower() allowed_types = ["png", "jpg", "jpeg", "gif"] if filetype not in allowed_types: raise ValueError("Unsupported file type") pil_image = Image.open(io.BytesIO(p._decoded_file)).convert("RGB") logger.info( "api_decode_image duration_ms=%.2f size_bytes=%s image_size=%s copy_highres=%s", (time.perf_counter() - start) * 1000, len(p._decoded_file), pil_image.size, copy_highres, ) return pil_image, pil_image.copy() if copy_highres else pil_image def _error_response(e: Exception): tb = traceback.extract_tb(e.__traceback__) frame = tb[-1] if tb else None return { "data": [], "message": json.dumps( { "error info": str(e), "error at": frame.filename if frame else "unknown", "errort line at": frame.lineno if frame else 0, } ), "code": 500, } def _success_response(data: Any) -> Dict[str, Any]: return {"data": data, "message": "success", "code": 200} def _log_exception(endpoint: str, request: Request, e: Exception, results: Dict[str, Any]): request_id = getattr(request.state, "request_id", "-") logger.exception( "endpoint_failed request_id=%s endpoint=%s code=%s message=%s", request_id, endpoint, results.get("code"), results.get("message"), exc_info=True, ) def _ocr_response_data(pred): if hasattr(pred, "text_lines"): text_json = pred.model_dump() text_lines = "\n".join([p.text for p in pred.text_lines]) return {"ocr_text_json": text_json, "text_lines": text_lines} return page_ocr_to_response(pred) def _dump_model_or_list(value): if hasattr(value, "model_dump"): return value.model_dump() if isinstance(value, list): return [ item.model_dump() if hasattr(item, "model_dump") else item for item in value ] return value # ---- new helpers for the OpenAI endpoint ----------------------------------- def load_image_from_url(url: str) -> Image.Image: """Decode an OpenAI `image_url` (data: base64 URL or http(s): URL) to RGB.""" if url.startswith("data:"): header, _, b64 = url.partition(",") if "base64" not in header or not b64: raise OcrRequestError("image_url data URL must be base64-encoded") try: raw = base64.b64decode(b64, validate=True) except binascii.Error as exc: raise OcrRequestError("image_url contains invalid base64") from exc elif url.startswith("http://") or url.startswith("https://"): try: resp = requests.get(url, timeout=10) resp.raise_for_status() except Exception as exc: raise OcrRequestError(f"could not fetch image_url: {exc}") from exc raw = resp.content else: raise OcrRequestError("image_url must be a data: or http(s): URL") try: return Image.open(io.BytesIO(raw)).convert("RGB") except Exception as exc: raise OcrRequestError(f"could not decode image: {exc}") from exc def extract_last_image(messages) -> Image.Image: """Return the last image found across the chat messages' content parts.""" url = None for msg in messages: content = msg.content if isinstance(content, list): for part in content: if getattr(part, "type", None) == "image_url" and part.image_url: url = part.image_url.url if url is None: raise OcrRequestError("no image_url content part found in messages") return load_image_from_url(url) def ocr_via_batcher( image: Image.Image, *, skip_text_detection: bool, recognize_math: bool, request_id: str, ) -> Dict[str, Any]: """Run page OCR through the shared request batcher. The annotated image is discarded, so we request with_bboxes=False to maximise batch coalescing with the primary /suya_ocr_vllm endpoint.""" _, pred, _ = vllm_ocr_batcher.submit( image, image, skip_text_detection=skip_text_detection, recognize_math=recognize_math, with_bboxes=False, request_id=request_id, ) return _ocr_response_data(pred) def table_image(image: Image.Image, skip_table_detection: bool) -> Dict[str, Any]: """Run table-structure recognition; mirrors the /suya_table_rec response shape.""" _, pred = tools.table_recognition(image, image.copy(), skip_table_detection) text_json = _dump_model_or_list(pred) text_lines = "" if isinstance(pred, list): text_lines = "\n".join([item.html for item in pred if getattr(item, "html", None)]) elif hasattr(pred, "text_lines"): text_lines = "\n".join([line.text for line in pred.text_lines]) return {"ocr_text_json": text_json, "text_lines": text_lines, "elapsed_seconds": None}