FastAPI service wrapping the Surya-OCR-2 model (datalab-to) served through vLLM: legacy /v1/api/ai/* endpoints, an OpenAI-compatible /v1/chat/completions endpoint, a coalescing request batcher, a local OCR CLI, Docker packaging, multilingual example outputs, and quantization/concurrency benchmarks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
import io
|
|
import time
|
|
|
|
from fastapi import APIRouter, File, Request, UploadFile
|
|
from PIL import Image
|
|
|
|
from tools import (
|
|
ocr,
|
|
text_detection,
|
|
layout_detection,
|
|
table_recognition,
|
|
extract_text_from_image,
|
|
)
|
|
from vllm_batcher import vllm_ocr_batcher
|
|
from vllm_tools import vllm_backend_info
|
|
from surya.endpoint.schemas import ApiResponse, Info
|
|
from surya.endpoint.service import (
|
|
_dump_model_or_list,
|
|
_error_response,
|
|
_load_base64_image,
|
|
_log_exception,
|
|
_ocr_response_data,
|
|
_success_response,
|
|
logger,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/home")
|
|
def home():
|
|
return "<h1>Welcome to SURYA OCR API!</h1>"
|
|
|
|
|
|
@router.post("/v1/api/ai/suya_ocr", include_in_schema=False, response_model=ApiResponse)
|
|
@router.post("/v1/api/ai/suya_ocr/", response_model=ApiResponse)
|
|
def run_ocr(p: Info, request: Request):
|
|
try:
|
|
pil_image, pil_image_highres = _load_base64_image(p)
|
|
rec_img, pred, box_img = ocr(
|
|
pil_image,
|
|
pil_image_highres,
|
|
p.skip_text_detection,
|
|
p.recognize_math,
|
|
with_bboxes=p.ocr_with_boxes,
|
|
)
|
|
return _success_response(_ocr_response_data(pred))
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("suya_ocr", request, e, results)
|
|
return results
|
|
|
|
|
|
@router.get("/v1/api/ai/suya_ocr_vllm/health", response_model=ApiResponse)
|
|
async def suya_ocr_vllm_health():
|
|
return _success_response(vllm_backend_info())
|
|
|
|
|
|
@router.post("/v1/api/ai/suya_ocr_vllm", include_in_schema=False, response_model=ApiResponse)
|
|
@router.post("/v1/api/ai/suya_ocr_vllm/", response_model=ApiResponse)
|
|
def run_ocr_vllm(p: Info, request: Request):
|
|
try:
|
|
pil_image, pil_image_highres = _load_base64_image(p)
|
|
start = time.perf_counter()
|
|
_, pred, _ = vllm_ocr_batcher.submit(
|
|
pil_image,
|
|
pil_image_highres,
|
|
skip_text_detection=p.skip_text_detection,
|
|
recognize_math=p.recognize_math,
|
|
with_bboxes=False,
|
|
request_id=getattr(request.state, "request_id", "-"),
|
|
)
|
|
logger.info(
|
|
"api_vllm_submit_wait_complete request_id=%s duration_ms=%.2f",
|
|
getattr(request.state, "request_id", "-"),
|
|
(time.perf_counter() - start) * 1000,
|
|
)
|
|
return _success_response(_ocr_response_data(pred))
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("suya_ocr_vllm", request, e, results)
|
|
return results
|
|
|
|
|
|
@router.post("/v1/api/ai/suya_text_det", include_in_schema=False, response_model=ApiResponse)
|
|
@router.post("/v1/api/ai/suya_text_det/", response_model=ApiResponse)
|
|
def run_text_det(p: Info, request: Request):
|
|
try:
|
|
pil_image, _ = _load_base64_image(p)
|
|
det_img, text_pred = text_detection(pil_image)
|
|
text_lines = text_pred.model_dump(exclude=["heatmap", "affinity_map"])
|
|
return _success_response({"text_lines": text_lines})
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("suya_text_det", request, e, results)
|
|
return results
|
|
|
|
|
|
@router.post("/v1/api/ai/suya_layout_det", include_in_schema=False, response_model=ApiResponse)
|
|
@router.post("/v1/api/ai/suya_layout_det/", response_model=ApiResponse)
|
|
def run_layout_det(p: Info, request: Request):
|
|
try:
|
|
pil_image, _ = _load_base64_image(p)
|
|
layout_img, pred = layout_detection(pil_image)
|
|
text_lines = pred.model_dump(exclude=["segmentation_map"])
|
|
return _success_response({"text_lines": text_lines})
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("suya_layout_det", request, e, results)
|
|
return results
|
|
|
|
|
|
@router.post("/v1/api/ai/suya_table_rec", include_in_schema=False, response_model=ApiResponse)
|
|
@router.post("/v1/api/ai/suya_table_rec/", response_model=ApiResponse)
|
|
def run_table_rec(p: Info, request: Request):
|
|
try:
|
|
pil_image, pil_image_highres = _load_base64_image(p)
|
|
|
|
table_img, pred = table_recognition(
|
|
pil_image, pil_image_highres, p.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([p.text for p in pred.text_lines])
|
|
|
|
return _success_response({"ocr_text_json": text_json, "text_lines": text_lines})
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("suya_table_rec", request, e, results)
|
|
return results
|
|
|
|
|
|
@router.post("/image2text", response_model=ApiResponse)
|
|
async def image_to_text(request: Request, file: UploadFile = File(...)):
|
|
try:
|
|
contents = await file.read()
|
|
image = Image.open(io.BytesIO(contents))
|
|
logger.info(
|
|
"image2text_upload request_id=%s filename=%s content_type=%s size_bytes=%s image_size=%s",
|
|
getattr(request.state, "request_id", "-"),
|
|
file.filename,
|
|
file.content_type,
|
|
len(contents),
|
|
image.size,
|
|
)
|
|
full_text = extract_text_from_image(image)
|
|
return _success_response(full_text)
|
|
except Exception as e:
|
|
results = _error_response(e)
|
|
_log_exception("image2text", request, e, results)
|
|
return results
|