From 7cdb53051ff4c544cb55fa59a7aa00b54a4e63e0 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 12 Mar 2026 10:20:41 +0100 Subject: [PATCH] feat: PaddleOCR Service (PP-OCRv5 Latin auf x86_64) Microservice fuer PaddleOCR auf Hetzner. FastAPI mit /ocr und /health Endpoints, API-Key Auth, 4GB Memory Limit, Modell-Cache Volume. Co-Authored-By: Claude Opus 4.6 --- docker-compose.coolify.yml | 31 ++++++++++++- paddleocr-service/Dockerfile | 16 +++++++ paddleocr-service/main.py | 71 ++++++++++++++++++++++++++++++ paddleocr-service/requirements.txt | 7 +++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 paddleocr-service/Dockerfile create mode 100644 paddleocr-service/main.py create mode 100644 paddleocr-service/requirements.txt diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index 32cc969..9c9afc7 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -15,6 +15,7 @@ networks: volumes: valkey_data: embedding_models: + paddleocr_models: services: @@ -141,6 +142,34 @@ services: networks: - breakpilot-network + # ========================================================= + # OCR SERVICE (PaddleOCR PP-OCRv5 Latin) + # ========================================================= + paddleocr-service: + build: + context: ./paddleocr-service + dockerfile: Dockerfile + container_name: bp-core-paddleocr + expose: + - "8095" + environment: + PADDLEOCR_API_KEY: ${PADDLEOCR_API_KEY:-} + volumes: + - paddleocr_models:/root/.paddleocr + deploy: + resources: + limits: + memory: 4G + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8095/health"] + interval: 30s + timeout: 10s + start_period: 120s + retries: 3 + restart: unless-stopped + networks: + - breakpilot-network + # ========================================================= # HEALTH AGGREGATOR # ========================================================= @@ -153,7 +182,7 @@ services: - "8099" environment: PORT: 8099 - CHECK_SERVICES: "valkey:6379,consent-service:8081,rag-service:8097,embedding-service:8087" + CHECK_SERVICES: "valkey:6379,consent-service:8081,rag-service:8097,embedding-service:8087,paddleocr-service:8095" healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:8099/health"] interval: 30s diff --git a/paddleocr-service/Dockerfile b/paddleocr-service/Dockerfile new file mode 100644 index 0000000..e10f22c --- /dev/null +++ b/paddleocr-service/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1-mesa-glx libglib2.0-0 curl \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8095 +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD curl -f http://127.0.0.1:8095/health || exit 1 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8095"] diff --git a/paddleocr-service/main.py b/paddleocr-service/main.py new file mode 100644 index 0000000..1195c5e --- /dev/null +++ b/paddleocr-service/main.py @@ -0,0 +1,71 @@ +"""PaddleOCR Remote Service — PP-OCRv5 Latin auf x86_64.""" + +import io +import os + +import numpy as np +from fastapi import FastAPI, File, Header, HTTPException, UploadFile +from PIL import Image + +app = FastAPI(title="PaddleOCR Service") + +_engine = None +API_KEY = os.environ.get("PADDLEOCR_API_KEY", "") + + +def get_engine(): + global _engine + if _engine is None: + from paddleocr import PaddleOCR + + _engine = PaddleOCR( + lang="latin", + use_angle_cls=True, + show_log=False, + ) + return _engine + + +@app.get("/health") +def health(): + return {"status": "ok", "model": "PP-OCRv5-latin"} + + +@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") + + img_bytes = await file.read() + img = Image.open(io.BytesIO(img_bytes)).convert("RGB") + img_np = np.array(img) + + engine = get_engine() + result = engine.ocr(img_np) + + words = [] + for line in result[0] or []: + box, (text, conf) = line[0], line[1] + x_min = min(p[0] for p in box) + y_min = min(p[1] for p in box) + x_max = max(p[0] for p in box) + y_max = max(p[1] for p in box) + words.append( + { + "text": text, + "left": int(x_min), + "top": int(y_min), + "width": int(x_max - x_min), + "height": int(y_max - y_min), + "conf": round(conf * 100, 1), + } + ) + + return { + "words": words, + "image_width": img_np.shape[1], + "image_height": img_np.shape[0], + } diff --git a/paddleocr-service/requirements.txt b/paddleocr-service/requirements.txt new file mode 100644 index 0000000..47f8951 --- /dev/null +++ b/paddleocr-service/requirements.txt @@ -0,0 +1,7 @@ +paddlepaddle>=3.0.0 +paddleocr>=2.9.0 +fastapi>=0.110.0 +uvicorn>=0.25.0 +python-multipart>=0.0.6 +Pillow>=10.0.0 +numpy>=1.24.0