Scan-Pfad entschlacken, Container-Limits setzen

Drei Änderungen, die den Spitzenverbrauch eines Stapelscans senken:

- Zuschnitte werden nicht mehr auf 1400 px hochgerechnet. Auf einem
  Stapelfoto ist eine Karte nur ein paar hundert Pixel breit; sie
  aufzublasen erzeugt vier Mal so viele Pixel ohne mehr Information und
  kostet zusätzlich Bildtokens beim Modellaufruf. OUT_WIDTH ist jetzt
  eine Obergrenze.
- Die um 180 Grad gedrehte Fassung entsteht erst, wenn ein Ergebnis leer
  bleibt, statt für jede Karte auf Vorrat. Das halbiert die
  JPEG-Kodierungen im Normalfall.
- Zuschnitte werden einzeln kodiert und sofort freigegeben, statt
  gesammelt im Speicher zu liegen.

Dazu mem_limit und cpus für den App-Container: ein Ausreißer soll den
Container treffen, nicht den Host, auf dem auch Jitsi und MySQL laufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-09-06 12:22:37 +02:00
parent 30db433c9a
commit bc1981f1bd
6 changed files with 64 additions and 27 deletions

View File

@@ -11,7 +11,7 @@ import logging
import anthropic
from . import config
from . import config, imaging
log = logging.getLogger(__name__)
@@ -123,11 +123,12 @@ async def _ask(image_jpeg: bytes) -> dict:
return json.loads(text)
async def extract(image_jpeg: bytes, rotated_jpeg: bytes = None) -> dict:
async def extract(image_jpeg: bytes) -> dict:
"""Ein Kartenbild auslesen.
Bleibt das Ergebnis leer, wird einmal um 180 Grad gedreht nachgefasst:
wie herum die Karte auf dem Tisch lag, ist geometrisch nicht bestimmbar.
Die gedrehte Fassung entsteht erst hier, nicht auf Vorrat fuer jede Karte.
"""
if not available():
return _empty("Kein ANTHROPIC_API_KEY gesetzt")
@@ -137,9 +138,10 @@ async def extract(image_jpeg: bytes, rotated_jpeg: bytes = None) -> dict:
log.exception("Extraktion fehlgeschlagen")
return _empty(f"{type(exc).__name__}: {exc}")
if not _has_content(result) and rotated_jpeg is not None:
if not _has_content(result):
try:
retry = await _ask(rotated_jpeg)
rotated = await asyncio.to_thread(imaging.rotate_jpeg_180, image_jpeg)
retry = await _ask(rotated)
if _has_content(retry):
retry["_rotated"] = True
return retry
@@ -148,12 +150,12 @@ async def extract(image_jpeg: bytes, rotated_jpeg: bytes = None) -> dict:
return result
async def extract_all(pairs: list) -> list:
async def extract_all(images: list) -> list:
"""Alle Karten eines Fotos parallel auslesen."""
limit = asyncio.Semaphore(max(1, config.EXTRACT_CONCURRENCY))
async def one(upright: bytes, rotated: bytes) -> dict:
async def one(image_jpeg: bytes) -> dict:
async with limit:
return await extract(upright, rotated)
return await extract(image_jpeg)
return await asyncio.gather(*(one(u, r) for u, r in pairs))
return await asyncio.gather(*(one(image) for image in images))