All checks were successful
Build & Deploy / deploy (push) Successful in 10m41s
Vier Funktionen: - Anlass: ein Feld auf der Startseite gilt für alle folgenden Scans und wird im Browser gemerkt. Ordnet den Bestand verlässlicher als der Ortsname aus den Koordinaten. Durchsuchbar, nachträglich änderbar. - Dubletten: die Detailansicht zeigt Karten, die dieselbe Person meinen könnten - gleiche E-Mail, gleiche Telefonnummer (verglichen werden die letzten acht Ziffern, damit +49 511 123456 und 0511/123456 aufeinander passen) oder gleicher Nachname bei gleicher Firma. - Rückseite: nachträglich ein zweites Bild zur Karte. Es füllt nur Felder, die die Vorderseite offen gelassen hat. - Schnellwahl bei Erinnerungen: in 3 Tagen / 1 Woche / 2 Wochen / 1 Monat, jeweils 9 Uhr. Das Datumsfeld braucht man damit selten. Teilen als vCard über das System-Teilenblatt, mit Download als Rückfallebene, wo es kein Teilenblatt gibt. Oberfläche: - Startseite führt jetzt: Anlass, große Aufnahmefläche mit Symbol statt Textzeile, leise Alternative, darunter die zuletzt erfassten Karten. Vorher stand dort eine Überschrift mit Zahlen und man musste raten. - Weniger Webseite, mehr App: kein Seitenrahmen auf dem Telefon, Symbole in der Tableiste mit Zählerblasen, kein Tap-Highlight, keine Textmarkierung auf Bedienelementen, Safe-Area oben, Einblendung beim Ansichtswechsel, Mindesthöhe 54 px für Schaltflächen. - Die Marken "exportiert" / "nicht exportiert" sind aus der Liste raus; dafür gibt es den Filter. Neue Spalten occasion und back_image_file kommen per ALTER TABLE in bestehende Datenbanken - CREATE TABLE IF NOT EXISTS rührt eine vorhandene Tabelle nicht an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.9 KiB
Markdown
137 lines
5.9 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. 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.
|
||
|
||
**Anlass.** Ein Feld auf der Startseite („Hannover Messe 2026“) gilt für alle
|
||
folgenden Scans und wird im Browser gemerkt. Verlässlicher als der Ortsname aus
|
||
den Koordinaten und die eigentliche Ordnung über den Bestand.
|
||
|
||
**Dubletten.** Auf Messen trifft man Leute wieder. Die Detailansicht zeigt
|
||
Karten, die dieselbe Person meinen könnten – gleiche E-Mail, gleiche
|
||
Telefonnummer (Schreibweise egal, verglichen werden die letzten acht Ziffern)
|
||
oder gleicher Nachname bei gleicher Firma.
|
||
|
||
**Rückseite.** Nachträglich ein zweites Bild zur Karte aufnehmen. Es wird
|
||
ausgelesen, füllt aber nur Felder, die die Vorderseite offen gelassen hat –
|
||
dort steht oft nur die Mobilnummer.
|
||
|
||
**Übersicht.** Alle Karten mit Volltextsuche über Name, Firma, Anlass, Ort und
|
||
Notiz. Filter für „nicht exportiert“, „unvollständig“ und „mit Erinnerung“.
|
||
|
||
**Export und Teilen.** *In Kontakte speichern* liefert eine vCard 3.0, die iOS
|
||
direkt in die Kontakte-App übernimmt. *Als vCard teilen* geht über das
|
||
System-Teilenblatt – per AirDrop, Mail oder Nachricht, ohne Umweg über den
|
||
Download. 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.
|
||
Schnellwahl für die üblichen Abstände, damit das Datumsfeld selten gebraucht
|
||
wird.
|
||
|
||
## 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
|
||
duplicates.py Ähnliche Karten finden
|
||
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/`.
|