"""PaddleOCR Remote Service — PP-OCRv5 Latin auf x86_64.""" import io import logging import os import numpy as np from fastapi import FastAPI, File, Header, HTTPException, UploadFile from PIL import Image logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="PaddleOCR Service") _engine = None _ready = False API_KEY = os.environ.get("PADDLEOCR_API_KEY", "") def get_engine(): global _engine if _engine is None: from paddleocr import PaddleOCR logger.info("Loading PaddleOCR model (first time may download)...") _engine = PaddleOCR( text_recognition_model_name="latin_PP-OCRv5_mobile_rec", use_doc_orientation_classify=False, use_doc_unwarping=False, use_textline_orientation=False, ) logger.info("PaddleOCR model loaded successfully") return _engine @app.on_event("startup") def startup_load_model(): """Pre-load model at startup so health check passes.""" global _ready try: get_engine() _ready = True logger.info("PaddleOCR ready to serve requests") except Exception as e: logger.error(f"Failed to load PaddleOCR model: {e}") @app.get("/health") def health(): if _ready: return {"status": "ok", "model": "PP-OCRv5-latin"} return {"status": "loading"} @app.post("/ocr") async def ocr( file: UploadFile = File(...), x_api_key: str = Header(default=""), ): if API_KEY and x_api_key != API_KEY: raise HTTPException(status_code=401, detail="Invalid API key") if not _ready: raise HTTPException(status_code=503, detail="Model still loading") img_bytes = await file.read() img = Image.open(io.BytesIO(img_bytes)).convert("RGB") img_np = np.array(img) engine = get_engine() result = engine.predict(img_np) words = [] for item in result: rec_texts = item.get("rec_texts", []) rec_scores = item.get("rec_scores", []) dt_polys = item.get("dt_polys", []) for text, score, poly in zip(rec_texts, rec_scores, dt_polys): if not text or not text.strip(): continue xs = [p[0] for p in poly] ys = [p[1] for p in poly] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) words.append( { "text": text.strip(), "left": int(x_min), "top": int(y_min), "width": int(x_max - x_min), "height": int(y_max - y_min), "conf": round(float(score) * 100, 1), } ) return { "words": words, "image_width": img_np.shape[1], "image_height": img_np.shape[0], }