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>
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:
|
||
`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/`.
|