Lucas Orth bf0a397597
All checks were successful
Build & Deploy / deploy (push) Successful in 10m41s
Anlass, Dubletten, Rückseite, Teilen; Startseite und App-Anmutung
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>
2026-09-06 15:09:08 +02:00
2026-09-06 11:46:52 +02:00

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

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:

  1. Secret DOTENV anlegen (Repo → Einstellungen → Actions → Secrets), Inhalt nach dem Muster in .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/.

Description
No description provided
Readme 235 KiB
Languages
Python 57.8%
JavaScript 26.8%
CSS 9.7%
HTML 4.7%
Dockerfile 1%