Suya OCR API — vLLM-backed, OpenAI-compatible OCR service
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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Capture per-page OCR text, bboxes, and latency from the vLLM OCR endpoint."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def extract_capture(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
data = body.get("data") or {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
blocks = ((data.get("ocr_text_json") or {}).get("blocks")) or []
|
||||
boxes = [list(b["bbox"]) for b in blocks if isinstance(b, dict) and b.get("bbox")]
|
||||
return {
|
||||
"text": data.get("text_lines", "") or "",
|
||||
"boxes": boxes,
|
||||
"elapsed_seconds": data.get("elapsed_seconds"),
|
||||
}
|
||||
|
||||
|
||||
def write_capture(cap: Dict[str, Any], out_dir: Path, stem: str) -> None:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / f"{stem}.txt").write_text(cap["text"], encoding="utf-8")
|
||||
(out_dir / f"{stem}.json").write_text(
|
||||
json.dumps({"boxes": cap["boxes"], "elapsed_seconds": cap["elapsed_seconds"]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _payload(image_path: Path) -> Dict[str, Any]:
|
||||
suffix = image_path.suffix.lstrip(".").lower() or "png"
|
||||
return {
|
||||
"file": base64.b64encode(image_path.read_bytes()).decode("utf-8"),
|
||||
"type": "jpg" if suffix == "jpeg" else suffix,
|
||||
"skip_text_detection": False,
|
||||
"skip_table_detection": False,
|
||||
"recognize_math": False,
|
||||
"ocr_with_boxes": True,
|
||||
}
|
||||
|
||||
|
||||
def capture_page(url: str, image_path: Path, out_dir: Path, timeout: float) -> Dict[str, Any]:
|
||||
resp = requests.post(url, json=_payload(image_path), timeout=timeout)
|
||||
cap = extract_capture(resp.json())
|
||||
if cap["elapsed_seconds"] is None:
|
||||
# Latency is a headline metric; a successful response with no timing means
|
||||
# the response contract changed. Surface it loudly instead of silently
|
||||
# dropping the page from the latency stats.
|
||||
print(
|
||||
f"WARNING: no elapsed_seconds in OCR response for {image_path.name}; "
|
||||
"latency for this page will be missing",
|
||||
file=sys.stderr,
|
||||
)
|
||||
write_capture(cap, out_dir, image_path.stem)
|
||||
return cap
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Capture OCR text+boxes+latency per image.")
|
||||
parser.add_argument("images", nargs="+", type=Path)
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--out-dir", type=Path, required=True)
|
||||
parser.add_argument("--timeout", type=float, default=900)
|
||||
args = parser.parse_args()
|
||||
for image in args.images:
|
||||
cap = capture_page(args.url, image, args.out_dir, args.timeout)
|
||||
print(f"captured {image.name}: {len(cap['boxes'])} boxes, {cap['elapsed_seconds']}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user