All checks were successful
Build & Deploy / deploy (push) Successful in 1m14s
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 <noreply@anthropic.com>
106 lines
4.2 KiB
Markdown
106 lines
4.2 KiB
Markdown
# Business Card Scanner
|
||
|
||
Visitenkarten abfotografieren, automatisch freistellen, auslesen und als
|
||
Kontakt aufs iPhone holen. Selbst gehostete PWA, ein Container.
|
||
|
||
## Was die App macht
|
||
|
||
**Stapelscan.** Ein Foto von zwanzig Karten auf dem Tisch reicht. OpenCV findet
|
||
die Kartenrechtecke, entzerrt sie perspektivisch und schneidet sie einzeln aus –
|
||
ohne manuelle Nacharbeit. Jeder Zuschnitt geht als Bild an das Vision-Modell,
|
||
das Name, Firma, Position und Kontaktdaten als strukturiertes JSON zurückgibt.
|
||
Ein vorgeschalteter OCR-Schritt wäre kontraproduktiv: er würde Layout und
|
||
Schriftgrößen wegwerfen, also genau die Information, aus der die Feldzuordnung
|
||
entsteht.
|
||
|
||
**Metadaten.** Aufnahmezeit und GPS kommen aus den EXIF-Daten des Fotos, nicht
|
||
aus dem Browser – Karten werden oft erst abends am Schreibtisch abfotografiert,
|
||
der Browserstandort wäre dann das Wohnzimmer statt der Messe. Die Koordinaten
|
||
löst Nominatim in einen Ortsnamen auf. Fehlen EXIF-Daten, greift der
|
||
Browserstandort als Rückfallebene.
|
||
|
||
**Übersicht.** Alle Karten mit Volltextsuche über Name, Firma, Ort und Notiz.
|
||
Filter für „nicht exportiert“, „unvollständig“ und „mit Erinnerung“.
|
||
|
||
**Export.** Ein Tippen auf *In Kontakte speichern* liefert eine vCard 3.0, die
|
||
iOS direkt in die Kontakte-App übernimmt. Die Karte gilt danach als exportiert.
|
||
|
||
**Notizen und Erinnerungen.** Freitext pro Karte. Erinnerungen mit Termin gehen
|
||
als Mail raus; verpasste Termine holt der Scheduler beim nächsten Start nach.
|
||
|
||
## Stack
|
||
|
||
Python 3.12, FastAPI, SQLite, OpenCV, Anthropic SDK. Frontend ohne Build-Schritt:
|
||
statisches HTML, CSS und JavaScript, als PWA installierbar.
|
||
|
||
## Lokal starten
|
||
|
||
```bash
|
||
python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
|
||
cp .env.example .env # APP_PASSWORD leer lassen schaltet die Anmeldung ab
|
||
DATA_DIR=./data .venv/bin/uvicorn app.main:app --reload --port 8099
|
||
```
|
||
|
||
Ohne `ANTHROPIC_API_KEY` läuft alles außer der Extraktion: Karten werden
|
||
freigestellt und gespeichert, die Felder bleiben leer.
|
||
|
||
```bash
|
||
.venv/bin/pytest -q
|
||
```
|
||
|
||
## Deployment
|
||
|
||
Tag-Push nach den Konventionen in [DEPLOY.md](DEPLOY.md). Vor dem ersten Tag:
|
||
|
||
1. **Secret `DOTENV`** anlegen (Repo → Einstellungen → Actions → Secrets), Inhalt
|
||
nach dem Muster in [.env.example](.env.example). `SECRET_KEY` mit
|
||
`openssl rand -hex 32` erzeugen.
|
||
2. **Proxy Host** im Nginx Proxy Manager:
|
||
`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`
|
||
|
||
### Zugang
|
||
|
||
Die App hängt hinter dem öffentlichen Proxy, nicht im Tailnet. Sie enthält
|
||
personenbezogene Daten Dritter, deshalb ist `APP_PASSWORD` auf dieser Domain
|
||
Pflicht – ohne Passwort ist sie für jeden erreichbar, der die Adresse kennt.
|
||
Wer zusätzlich absichern will, trägt im NPM unter *Access List* eine
|
||
IP-Beschränkung auf den Tailnet-Bereich ein.
|
||
|
||
### Kosten
|
||
|
||
Ein Kartenzuschnitt kostet ungefähr zwei Cent bei `claude-opus-5` (Standard).
|
||
Ein Stapel von zwanzig Karten liegt damit bei etwa 40 Cent.
|
||
`ANTHROPIC_MODEL=claude-haiku-4-5` in der `.env` drückt das auf einen Bruchteil,
|
||
zulasten der Trefferquote bei kleiner Schrift und unruhigen Layouts.
|
||
|
||
## Aufnahmebedingungen für den Stapelscan
|
||
|
||
Die Freistellung ist reine Geometrie und braucht Kontrast zum Untergrund:
|
||
|
||
- dunkler, matter, einfarbiger Untergrund
|
||
- Karten berühren sich nicht
|
||
- Kamera möglichst parallel zur Tischplatte
|
||
- volle Auflösung – deshalb die native Kamera, nicht der Livestream
|
||
|
||
Findet die Pipeline kein Kartenrechteck, behandelt sie das ganze Foto als eine
|
||
Karte und sagt das in der Oberfläche. Einzelne Fehlgriffe lassen sich pro Karte
|
||
über *Bearbeiten* korrigieren.
|
||
|
||
## Verzeichnisse
|
||
|
||
```
|
||
app/ FastAPI-Anwendung
|
||
segment.py OpenCV-Pipeline: Karten finden und entzerren
|
||
extract.py Vision-Modell, strukturiertes JSON
|
||
imaging.py EXIF-Auswertung, Bildkonvertierung
|
||
vcard.py vCard 3.0
|
||
reminders.py Scheduler für die Erinnerungsmails
|
||
static/ PWA: HTML, CSS, JavaScript, Service Worker
|
||
tests/ pytest, inklusive synthetischer Stapelfotos
|
||
```
|
||
|
||
Persistente Daten liegen im Volume `business-card-scanner-data` unter `/data`:
|
||
`cards.db` und die Kartenbilder in `images/`.
|