Auslesen im Hintergrund: /api/scan antwortet, sobald die Karten freigestellt und gespeichert sind, und stößt die Extraktion als Task an. Ein Zwanzigerstapel blockierte vorher den Upload für die ganze Dauer aller Modellaufrufe. Die Oberfläche zeigt "wird gelesen" und lädt nach, solange etwas offen ist. Zwei Folgen davon sind mitbehandelt: Beim Schreiben der Ergebnisse steht COALESCE, damit ein Handeintrag während des Lesens nicht überschrieben wird, und offene Karten werden beim Start nachgeholt, statt dauerhaft in der Warteschlange zu hängen. Bildaufbereitung des Zuschnitts: - Der Einzug zieht die erkannten Ecken um 1,5 % zur Mitte, damit kein Untergrund im Zuschnitt bleibt. - Die Beleuchtung wird ausgeglichen (Division durch eine weichgezeichnete Fassung), damit das Papier weiß wird statt grau. Dunkle Karten bleiben unangetastet - bei ihnen ist das Dunkle das Papier, kein Schatten. - Das Modell meldet die nötige Drehung im Schema; das gespeicherte Bild wird entsprechend gedreht. Geometrisch ist die Lage nicht bestimmbar. Sammelexport entfernt: Der Button "Alle als vCard" ist weg, mit ihm der Endpunkt /vcf sowie vcard.build_many und db.execute_many, die dadurch keinen Aufrufer mehr hatten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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. Der Zuschnitt wird dabei leicht enger gesetzt als die erkannte Kante, damit kein Untergrund stehen bleibt, und die Beleuchtung wird ausgeglichen, damit das Papier weiß wird statt grau.
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. Das Modell sagt außerdem, wie herum die Karte gehört – geometrisch ist das nicht bestimmbar – und das gespeicherte Bild wird entsprechend gedreht.
Die Antwort wartet nicht auf das Modell. Der Scan liefert die freigestellten Karten sofort zurück, das Auslesen läuft danach im Hintergrund weiter; die Oberfläche lädt nach. Ein Zwanzigerstapel blockiert damit keine Minute im Upload. Wer eine Karte in der Zwischenzeit von Hand ergänzt, verliert die Eingabe nicht – das Modell füllt nur leere Felder. Bricht der Server mitten im Lauf ab, holt er die offenen Karten beim nächsten Start nach.
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. Bewusst einzeln: ein Sammelexport schiebt zwanzig Kontakte ungeprüft ins Adressbuch.
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
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.
.venv/bin/pytest -q
Deployment
Tag-Push nach den Konventionen in DEPLOY.md. Vor dem ersten Tag:
- Secret
DOTENVanlegen (Repo → Einstellungen → Actions → Secrets), Inhalt nach dem Muster in .env.example.SECRET_KEYmitopenssl rand -hex 32erzeugen. - Proxy Host im Nginx Proxy Manager:
scanner.lucas-orth.de→business-card-scanner:8080, danach Let's-Encrypt-Zertifikat und „Force SSL“. - 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/.