Visitenkarten-Scanner: Stapelscan, Extraktion, Übersicht, vCard-Export

Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest
und als Kontakt bereitstellt.

- Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken,
  entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt
  das ganze Foto als eine Karte.
- Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit
  JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und
  Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht.
- Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname
  über Nominatim, Browserstandort nur als Rückfallebene.
- Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske.
- Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster
  Termine nach einem Neustart.
- vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert.
- Anmeldung über ein Passwort, Sitzung als signiertes Cookie.
- Deployment per Tag-Push nach den Konventionen in DEPLOY.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-09-06 11:45:56 +02:00
commit ab993e98e1
37 changed files with 3458 additions and 0 deletions

105
README.md Normal file
View File

@@ -0,0 +1,105 @@
# 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:
`business-card-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/`.