From 1f29d5eb27b9b72898777aa4a469b3647e5c5332 Mon Sep 17 00:00:00 2001 From: Lucas Orth Date: Sun, 6 Sep 2026 12:36:24 +0200 Subject: [PATCH] =?UTF-8?q?effort-Parameter=20modellabh=C3=A4ngig=20senden?= =?UTF-8?q?,=20Design-Hierarchie,=20iOS-Datumsfeld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fehler: claude-haiku-4-5 lehnt output_config.effort mit einem 400 ab, jede Karte lief in einen Lesefehler. effort und der server-seitige Fallback werden jetzt nur an Modelle geschickt, die sie annehmen; ein unbekanntes Modell mit engerem Parametersatz wird einmal ohne Zusatzparameter wiederholt, statt die Karte zu verlieren. Tests decken die Zuordnung ab. Design: Grau kommt als Hierarchiestufe dazu. Drei Ebenen für Fläche (weiß / grau gefüllt / schwarz), Text (schwarz / --ink-2 / --ink-3) und Linie (3px / 1px schwarz / graue Haarlinie). Vorher trug alles dieselbe 3px-Kante und dasselbe Schwarz, dadurch war keine Ordnung erkennbar. Konkret: nicht gewählte Filter treten zurück, Listenzeilen bekommen drei Textstufen und Statuspillen statt einer gleichförmigen Zeile, das Kopfband über Listen ist grau hinterlegt, zweitrangige und zerstörende Aktionen sind leiser als die primäre. iOS: Datumsfelder zentrieren ihren Wert und sacken in der Höhe ein. Die WebKit-Pseudoelemente ziehen sie auf die Form der übrigen Felder. Dazu die Domain in der Dokumentation auf scanner.lucas-orth.de. Co-Authored-By: Claude Opus 5 --- DEPLOY.md | 2 +- README.md | 2 +- app/extract.py | 52 ++++--- static/app.js | 28 ++-- static/index.html | 2 +- static/style.css | 345 +++++++++++++++++++++++++++++------------- tests/test_extract.py | 35 +++++ 7 files changed, 324 insertions(+), 142 deletions(-) create mode 100644 tests/test_extract.py 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 @@ - `; 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 → -