Files
Mieterportal/server/ocr/main.py
T

104 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")}