"""mp-ocr – OCR-Dienst für das Mieterportal (Beleg-Analyse). Nimmt eine Datei-URL (oder Upload) entgegen, lädt den Beleg (PDF/Bild), extrahiert den Text mit Tesseract (Deutsch) und liefert ihn zurück. Nur intern im Docker-Netz erreichbar (mp-api ruft ihn auf). """ import base64 import binascii import io import urllib.parse import fitz # PyMuPDF import httpx import pytesseract from fastapi import FastAPI, HTTPException, UploadFile from PIL import Image from pydantic import BaseModel MAX_DATEI_BYTES = 20 * 1024 * 1024 # 20 MB DOWNLOAD_TIMEOUT = 20.0 app = FastAPI(title="mp-ocr", docs_url=None, redoc_url=None) class Anfrage(BaseModel): url: str @app.get("/healthz") def healthz(): return {"ok": True, "dienst": "mp-ocr"} def bild_text(img: Image.Image) -> str: return pytesseract.image_to_string(img, lang="deu").strip() def pdf_text(daten: bytes) -> str: teile = [] with fitz.open(stream=daten, filetype="pdf") as doc: for seite in doc: pix = seite.get_pixmap(dpi=200) img = Image.open(io.BytesIO(pix.tobytes("png"))) teile.append(bild_text(img)) return "\n".join(t for t in teile if t).strip() def datei_text(daten: bytes, dateiname: str) -> str: if dateiname.lower().endswith(".pdf") or daten[:5] == b"%PDF-": return pdf_text(daten) try: with Image.open(io.BytesIO(daten)) as img: return bild_text(img.convert("RGB")) except Exception: raise HTTPException(400, "Dateiformat nicht unterstützt (PDF/JPG/PNG erwartet).") def datenVonDataUrl(url: str) -> tuple[bytes, str]: """data:application/pdf;base64,…. → (bytes, name).""" kopf, _, payload = url.partition(",") if not payload: raise HTTPException(400, "Leere data-URL.") typ = kopf[5:].split(";")[0] or "application/octet-stream" if ";base64" in kopf: try: return base64.b64decode(urllib.parse.unquote(payload)), "beleg-data" except (binascii.Error, ValueError): raise HTTPException(400, "Ungültige base64-data-URL.") return urllib.parse.unquote_to_bytes(payload), "beleg-data" @app.post("/ocr") async def ocr_url(anfrage: Anfrage): """Beleg von einer URL herunterladen und per OCR auswerten.""" url = anfrage.url if url.startswith("data:"): daten, name = datenVonDataUrl(url) if len(daten) > MAX_DATEI_BYTES: raise HTTPException(413, "Datei größer als 20 MB.") return {"text": datei_text(daten, name)} if not url.startswith(("http://", "https://")): raise HTTPException(400, "Nur http(s)- oder data-URLs erlaubt.") try: async with httpx.AsyncClient(timeout=DOWNLOAD_TIMEOUT, follow_redirects=True) as client: resp = await client.get(url) except Exception as e: raise HTTPException(502, f"Download fehlgeschlagen: {e}") if resp.status_code != 200: raise HTTPException(502, f"Download: HTTP {resp.status_code}") if len(resp.content) > MAX_DATEI_BYTES: raise HTTPException(413, "Datei größer als 20 MB.") name = url.split("?")[0].rsplit("/", 1)[-1] or "beleg" return {"text": datei_text(resp.content, name)} @app.post("/ocr/upload") async def ocr_upload(datei: UploadFile): """Beleg direkt als Upload (multipart) auswerten.""" daten = await datei.read(MAX_DATEI_BYTES + 1) if len(daten) > MAX_DATEI_BYTES: raise HTTPException(413, "Datei größer als 20 MB.") return {"text": datei_text(daten, datei.filename or "beleg")}