diff --git a/DEPLOY.md b/DEPLOY.md index 2ab5d06..1257434 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -149,7 +149,7 @@ Neuer Proxy Host: | Feld | Wert | |---|---| -| Domain Names | `business-card-scanner.lucas-orth.de` | +| Domain Names | `scanner.lucas-orth.de` | | Scheme | `http` | | Forward Hostname / IP | `business-card-scanner` (= `container_name`) | | Forward Port | `8080` | diff --git a/README.md b/README.md index 9e3cd00..e7be845 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Tag-Push nach den Konventionen in [DEPLOY.md](DEPLOY.md). Vor dem ersten Tag: nach dem Muster in [.env.example](.env.example). `SECRET_KEY` mit `openssl rand -hex 32` erzeugen. 2. **Proxy Host** im Nginx Proxy Manager: - `business-card-scanner.lucas-orth.de` → `business-card-scanner` : `8080`, + `scanner.lucas-orth.de` → `business-card-scanner` : `8080`, danach Let's-Encrypt-Zertifikat und „Force SSL“. 3. **Deployen:** `git tag v1.0.0 && git push origin v1.0.0` diff --git a/app/extract.py b/app/extract.py index 54cfda7..74b2d62 100644 --- a/app/extract.py +++ b/app/extract.py @@ -52,9 +52,22 @@ Regeln: PROMPT = "Lies diese Visitenkarte aus." +# Nicht jedes Modell nimmt jeden Parameter an: die kleinen Modelle kennen +# output_config.effort nicht (400), der server-seitige Fallback gilt nur fuer +# die groessten. +_NO_EFFORT = ("claude-haiku-", "claude-sonnet-4-5") +_WITH_FALLBACK = ("claude-opus-5", "claude-fable-") + _client = None +def _output_config(with_effort: bool) -> dict: + settings = {"format": {"type": "json_schema", "schema": SCHEMA}} + if with_effort: + settings["effort"] = "low" # reines Ablesen, keine Denkarbeit + return settings + + def available() -> bool: return bool(config.ANTHROPIC_API_KEY) @@ -92,27 +105,28 @@ async def _ask(image_jpeg: bytes) -> dict: ], } ] - common = dict( - model=config.ANTHROPIC_MODEL, - max_tokens=2000, - system=SYSTEM, - messages=messages, - output_config={ - "format": {"type": "json_schema", "schema": SCHEMA}, - "effort": "low", # reines Ablesen, keine Denkarbeit - }, - ) + model = config.ANTHROPIC_MODEL + common = dict(model=model, max_tokens=2000, system=SYSTEM, messages=messages) + effort = not model.startswith(_NO_EFFORT) try: - response = await client.beta.messages.create( - betas=["server-side-fallback-2026-07-01"], - fallbacks="default", - **common, + if model.startswith(_WITH_FALLBACK): + response = await client.beta.messages.create( + betas=["server-side-fallback-2026-07-01"], + fallbacks="default", + output_config=_output_config(effort), + **common, + ) + else: + response = await client.messages.create( + output_config=_output_config(effort), **common + ) + except anthropic.BadRequestError as exc: + # Ein Modell mit engerem Parametersatz, als die Listen oben kennen. + # Einmal ohne Zusatzparameter nachfassen, statt die Karte zu verlieren. + log.warning("Anfrage abgelehnt (%s) - Wiederholung ohne Zusatzparameter", exc) + response = await client.messages.create( + output_config=_output_config(with_effort=False), **common ) - except anthropic.BadRequestError: - # Der Fallback-Parameter haengt am Modell. Ohne ihn laeuft die Anfrage - # weiter, sie bricht bei einer Ablehnung nur ohne Ersatzmodell ab. - log.warning("Server-side fallback abgelehnt, Anfrage ohne Fallback") - response = await client.messages.create(**common) if response.stop_reason == "refusal": return _empty("Vom Modell abgelehnt") diff --git a/static/app.js b/static/app.js index 0e5f668..df762fa 100644 --- a/static/app.js +++ b/static/app.js @@ -111,6 +111,20 @@ }); } + /** Zustand einer Karte als abgestufte Pillen: schwarz, was erledigt ist, + * grau umrandet, was noch offen oder auffaellig ist. */ + function cardTags(card) { + const tags = []; + if (card.place) tags.push(["", card.place]); + tags.push( + card.exported_at ? ["pill-solid", "exportiert"] : ["", "nicht exportiert"] + ); + if (card.extract_status !== "ok") tags.push(["", "Lesefehler"]); + return tags + .map(([extra, text]) => `${esc(text)}`) + .join(""); + } + // ----------------------------------------------------------------- Routing function go(route, cardId) { @@ -167,7 +181,7 @@ ` @@ -234,15 +248,7 @@ ${esc([card.job_title, card.company].filter(Boolean).join(" · ") || "Keine Firma erkannt")} - ${esc( - [ - card.place, - card.exported_at ? "exportiert" : "nicht exportiert", - card.extract_status !== "ok" ? "Lesefehler" : null, - ] - .filter(Boolean) - .join(" · ") - )} + ${cardTags(card)} ` ) .join("")} @@ -367,7 +373,7 @@
Karte entfernen
- `; diff --git a/static/index.html b/static/index.html index e3a3f6c..2f1f569 100644 --- a/static/index.html +++ b/static/index.html @@ -57,7 +57,7 @@ Foto aufnehmen → -