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

39
docker-compose.yml Normal file
View File

@@ -0,0 +1,39 @@
# Fester Projektname. Ohne das leitet Compose ihn aus dem Verzeichnisnamen
# ab - und der ist im CI-Job ein anderer als auf dem Host.
name: business-card-scanner
services:
app:
# Versionstag ermoeglicht Rollback ohne Rebuild.
image: business-card-scanner:${IMAGE_TAG:-latest}
build: .
# Fester Name: das ist die Adresse, auf die der Proxy zeigt.
container_name: business-card-scanner
restart: unless-stopped
# Wird im Workflow aus dem Secret DOTENV erzeugt.
env_file: .env
volumes:
- business-card-scanner-data:/data
networks:
- nginx-proxy-manager_default
# Pflicht: der Deploy-Schritt nutzt --wait und macht das zum Gate.
healthcheck:
test:
- CMD
- python
- -c
- import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4).status == 200 else 1)
interval: 60s
timeout: 5s
retries: 3
# Kein ports-Mapping: Der Proxy erreicht den Container ueber das gemeinsame
# Netz unter seinem Container-Namen.
networks:
nginx-proxy-manager_default:
external: true
volumes:
business-card-scanner-data:
# Fester Name, damit das Volume nicht am Projektnamen haengt.
name: business-card-scanner-data