Files
stackdex_neu/.docs/prd.md
Matthias a60a76b797 Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
2026-04-19 21:11:32 +02:00

323 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# StackDex — PRD: Kartenerkennung MVP
> **Scope:** Zuverlässige Erkennung von Name, Nummer/Set und Seltenheit einer physischen Pokémon-Karte.
> Primärpfad über Mistral OCR (via Convex), mit On-Device-Fallback für Offline-Situationen und Foundation-Models-Enhancement auf kompatiblen Geräten.
---
## 1 Ziel
Einen durchgängigen Erkennungsfluss bauen, der aus einem Foto einer Pokémon-Karte drei Kerndaten extrahiert und dem Nutzer zur Bestätigung anzeigt:
| Datum | Beispiel |
|---|---|
| **Kartenname** | Glurak |
| **Nummer / Set** | 4/102 · Base Set |
| **Seltenheit** | Holo Rare (★) |
**Differenzierung:** Kostenlose Konkurrenten setzen typischerweise auf reine On-Device-OCR, die bei holografischen Oberflächen, schlechter Beleuchtung und älteren Sets regelmäßig versagt. StackDex setzt bewusst auf Cloud-OCR als Primärpfad für signifikant bessere Erkennungsraten.
---
## 2 Eingabequellen
Beide Quellen werden gemeinsam ausgeliefert (Entscheidung aus Planung: Slice B).
### 2.1 Kamera
- Eigener Kamerascreen auf Basis von `AVCaptureSession` + `AVCaptureDevice.RotationCoordinator` (iOS 17+).
- Der Nutzer rahmt die Karte und löst die Aufnahme **explizit** per Shutter-Button aus.
- Kein Live-Erkennungsmodus; Analyse startet erst nach Capture.
### 2.2 Fotobibliothek
- Kompakter Recent-Images-Strip direkt im Scan-Screen unterhalb des Kamerabilds.
- Nutzt `PHPhotoLibrary` mit vollständiger Autorisierungsbehandlung:
- `.authorized` → Zugriff auf Recents.
- `.limited` → eingeschränkter Zugriff + `presentLimitedLibraryPicker(from:)` als Recovery.
- `.denied` / `.restricted` → In-Context-Hinweis mit „Einstellungen öffnen" + Fallback auf Kamera oder manuelle Eingabe.
- Berechtigungen werden **just-in-time** angefragt, nicht beim App-Start.
### 2.3 Bildlebenszyklus
Aufgenommene/importierte Bilder werden **nur temporär** für die Analyse verwendet und danach verworfen — kein dauerhaftes Speichern im MVP.
---
## 3 Erkennungs-Pipeline
### 3.1 Überblick — Dreistufiges Modell
```
Foto (Capture / Import)
Preprocessing (on-device, immer)
├── Online? ──► Stufe 1: Mistral OCR via Convex Action
│ │
│ ▼
│ Strukturierte Extraktion
│ │
│ ├── iOS 26 + Foundation Models verfügbar?
│ │ ▼
│ │ Stufe 1a: Foundation Models Enhancement
│ │ │
│ │ ▼
│ │ Ergebnis-Screen
│ │
│ └── sonst ──► Regex/Heuristik-Extraktion
│ │
│ ▼
│ Ergebnis-Screen
└── Offline? ──► Stufe 2: On-Device Vision/OCR
Strukturierte Extraktion (Regex/Heuristik + optional Foundation Models)
Ergebnis-Screen (mit Offline-Hinweis)
```
### 3.2 Preprocessing (immer, on-device)
Läuft vor jeder Erkennungsstufe, unabhängig vom gewählten Pfad:
- **Default:** Vollbild-Analyse ohne manuellen Crop.
- **Bildoptimierung:** Automatische Rotation und leichter Perspektivausgleich, wenn sie die Erkennungsqualität verbessern.
- **Bildkompression:** Für den Mistral-Upload wird das Bild auf eine sinnvolle Auflösung skaliert (Bandbreite vs. Qualität), z. B. max. 2048px auf der langen Seite.
- **Fallback bei schwacher Confidence:** Optionaler Crop-/Refinement-Schritt mit festem Karten-Seitenverhältnis (≈ 2.5:3.5).
### 3.3 Stufe 1 — Mistral OCR (Primärpfad, online)
Der bevorzugte Erkennungsweg bei bestehender Internetverbindung.
#### 3.3.1 Ablauf
1. App prüft Konnektivität.
2. Bild wird per `POST` an einen **Convex HTTP Action Endpoint** gesendet.
3. Der HTTP Action ruft die Mistral OCR API auf (`mistral-ocr-latest` oder `mistral-ocr-2512`).
4. Mistral gibt strukturiertes Markdown mit dem gesamten erkannten Kartentext zurück.
5. HTTP Action gibt das OCR-Ergebnis als JSON-Response an die App zurück.
#### 3.3.2 Convex als Proxy
Mistral OCR wird **nicht direkt vom Gerät** aufgerufen, sondern über Convex:
- **HTTP Action statt Client Action:** Convex-Docs empfehlen, Actions nicht direkt über den Swift Client aufzurufen (Anti-Pattern). Stattdessen wird ein dedizierter HTTP Action Endpoint exponiert, den die App per `URLSession` oder den Convex Swift Client (`ConvexMobile`) via `action()` anspricht.
- API-Key bleibt serverseitig und wird nie an den Client ausgeliefert.
- Ermöglicht serverseitiges Logging, Rate-Limiting und Kostenmonitoring.
- Vorbereitend für den nächsten Schritt, in dem Convex das OCR-Ergebnis direkt gegen die Pokewallet-Datenbank matchen kann.
#### 3.3.3 Kostenrahmen
Mistral OCR berechnet ~$12 pro 1.000 Seiten/Bilder. Bei typischem Hobbynutzer-Volumen (1050 Scans/Monat) sind die Kosten marginal. Für das MVP ist kein nutzerseitiges Limit nötig, aber serverseitiges Monitoring sollte ungewöhnliche Spikes erkennen.
### 3.4 Stufe 1a — Foundation Models Enhancement (optional, iOS 26+)
Wenn das Gerät iOS 26+ läuft und `SystemLanguageModel.availability == .available`:
- Das Mistral-OCR-Markdown wird lokal an eine `LanguageModelSession` übergeben.
- Foundation Models extrahiert die drei Zielfelder typsicher via `@Generable`:
```swift
import FoundationModels
@Generable
struct CardRecognitionResult {
@Guide(description: "The Pokémon card name including suffixes like ex, V, VMAX, GX")
let cardName: String
@Guide(description: "Card number in format like 4/102 or 025/198",
.pattern(/\d{1,3}\s*\/\s*\d{1,3}/))
let cardNumber: String
@Guide(description: "The set name or abbreviation, e.g. Base Set, Scarlet & Violet")
let setIdentifier: String
@Guide(description: "Rarity level of the card",
.anyOf(["Common", "Uncommon", "Rare", "Holo Rare",
"Ultra Rare", "Illustration Rare", "Special Art Rare",
"Hyper Rare", "Secret Rare"]))
let rarity: String
}
```
- Vorteil: Robuster als Regex bei ungewöhnlichen Layouts, mehrsprachigen Karten oder fragmentiertem OCR-Output.
- Wenn Foundation Models nicht verfügbar → Fallback auf Regex/Heuristik-Extraktion (siehe 3.6).
#### 3.4.1 Foundation Models Error-Handling
Die `LanguageModelSession` kann mehrere relevante Fehler werfen, die alle zum Regex/Heuristik-Fallback führen:
- **`guardrailViolation`** — Pokémon-Kartennamen oder -Beschreibungen könnten unbeabsichtigt Guardrails triggern. Bei diesem Fehler: Fallback auf Regex.
- **`unsupportedLanguageOrLocale`** — Vor dem Aufruf `SystemLanguageModel.default.supportsLocale()` prüfen; Deutsch ist unterstützt, aber nicht alle Locales.
- **`rateLimited`** — On-Device-Modell hat Rate-Limits. Bei Batch-Scans relevant.
- **`decodingFailure`** — `@Generable`-Struct konnte nicht befüllt werden. Fallback auf Regex.
Generell: Jeder Foundation-Models-Fehler wird still abgefangen und führt zum Regex-Pfad — der Nutzer bekommt davon nichts mit.
### 3.5 Stufe 2 — On-Device Vision/OCR (Offline-Fallback)
Greift nur, wenn keine Internetverbindung besteht.
#### 3.5.1 OCR-Konfiguration
Basiert auf `VNRecognizeTextRequest` (Vision Framework):
- **Recognition Level:** `.accurate` als Default.
- **Sprachen:** Konfigurierbar — mindestens `en` und `de`.
- **Language Correction:** Aktiviert, aber als Tuning-Punkt (kann für Eigennamen kontraproduktiv sein).
- **Custom Words:** Statische Wortliste im Bundle mit häufigen Pokémon-Namen, Set-Abkürzungen und Seltenheits-Begriffen.
#### 3.5.2 Offline-UX
- Der Ergebnis-Screen zeigt einen dezenten Hinweis: „Offline erkannt — Ergebnis kann weniger genau sein".
- Kein Blocker — der Nutzer kann das Ergebnis trotzdem bestätigen oder korrigieren.
- Wenn Foundation Models verfügbar (iOS 26+), wird der Vision-Rohtext zusätzlich durch die `@Generable`-Extraktion geschickt.
### 3.6 Regex/Heuristik-Extraktion (Fallback für strukturierte Felder)
Wird verwendet, wenn Foundation Models nicht verfügbar sind — sowohl nach Mistral OCR als auch nach On-Device Vision:
#### 3.6.1 Kartenname
- Typischerweise der größte/prominenteste Textblock im oberen Kartendrittel.
- Heuristiken: Position (oberes Drittel), Schriftgröße relativ zum Rest, Abwesenheit von Ziffern.
- Sonderfälle: Mehrteilige Namen („Dunkles Glurak"), Zusätze wie „ex", „V", „VMAX", „GX" gehören zum Namen.
#### 3.6.2 Nummer / Set
- Format typischerweise `NNN/NNN` im unteren Kartenbereich.
- Ergänzende Set-Kürzel (z. B. „SV", „BS", „EX") in unmittelbarer Nähe der Nummer.
- Regex-Pattern: `\d{1,3}\s*/\s*\d{1,3}`, ergänzt um optionale Präfixe/Suffixe.
#### 3.6.3 Seltenheit
- Primär über das **Seltenheitssymbol** im unteren rechten Bereich (●, ◆, ★ etc.).
- Sekundär über erkannten Text: „Common", „Uncommon", „Rare", „Holo Rare", „Ultra Rare" und Varianten.
- Mapping-Tabelle von Symbol → Seltenheitsstufe, erweiterbar für neuere Seltenheitsklassen.
### 3.7 Confidence-Modell
Für jedes der drei Felder wird eine interne Confidence berechnet (high / medium / low), basierend auf:
- **Mistral-Pfad:** Vollständigkeit des Markdown-Outputs + Extraktionserfolg aller drei Felder.
- **On-Device-Pfad:** OCR-eigener Confidence-Wert pro Textblock + strukturelle Plausibilität.
- **Beide Pfade:** Vollständigkeit (alle drei Felder gefunden?), Format-Plausibilität (Nummer im erwarteten Schema?).
| Gesamt-Confidence | Verhalten |
|---|---|
| **High** | Ergebnis direkt anzeigen, ggf. mit dezenter Bestätigungsaufforderung. |
| **Medium** | Ergebnis anzeigen mit Hinweis „Treffer prüfen". |
| **Low** | Crop-/Refinement-Option anbieten + manuelle Korrekturmöglichkeit. |
---
## 4 Ergebnis-Screen
### 4.1 Inhalte
- **Kartenname** — editierbar.
- **Nummer / Set** — editierbar.
- **Seltenheit** — editierbar (Auswahl aus bekannten Stufen).
- **Confidence-Hinweis** — bei Medium/Low als menschenlesbarer Text, z. B. „Bitte prüfe die erkannten Daten".
- **Erkennungsquelle** — dezenter Hinweis, ob Mistral (Cloud) oder On-Device (Offline) verwendet wurde.
### 4.2 Aktionen
| Aktion | Beschreibung |
|---|---|
| **Bestätigen** | In diesem MVP-Schritt: Bestätigung loggen / in temporärem State halten. Persistenz folgt im nächsten Schritt. |
| **Korrigieren** | Inline-Editing der drei Felder, dann bestätigen. |
| **Erneut scannen** | Zurück zum Scan-Screen für neuen Versuch. |
| **Manuell eingeben** | Wechsel in eine Freitext-Eingabe aller drei Felder (dient gleichzeitig als No-Match-Recovery). |
### 4.3 No-Match-Verhalten
Wenn die Pipeline keines der drei Felder mit brauchbarer Confidence extrahieren kann:
- Nutzer wird in den manuellen Eingabe-Flow geleitet, **vorausgefüllt mit OCR-Fragmenten** soweit verfügbar.
- Kein Blocker-Dialog — der Übergang soll sich nahtlos anfühlen.
---
## 5 Navigation & UI (nur erkennungsrelevant)
- **Scannen-Tab** ist der zentrale Einstieg für diesen Slice.
- Layout: Kamera-Vollbild oben, Recent-Photos-Strip unten (Entscheidung: Scan-Tab Layout A).
- Ergebnis-Screen wird per `NavigationStack` gepusht.
- Kein Collection-Switcher oder Target-Anzeige in diesem Schritt — kommt mit der Sammlung.
- **Loading-State:** Nach Capture/Import zeigt die App einen Erkennungs-Indikator, während der Mistral-Call läuft. Erwartete Latenz: 13 Sekunden.
---
## 6 Technische Rahmenbedingungen
| Thema | Entscheidung |
|---|---|
| Deployment Target | **iOS 26** (SwiftUI), um Foundation Models nutzen zu können |
| Lokale Persistenz | SwiftData, in diesem Slice nur für temporäre Scan-Ergebnisse / Dev-Logging |
| Kamera-API | AVCaptureSession + RotationCoordinator |
| Primäre OCR | **Mistral OCR API** via Convex Action |
| Fallback-OCR | VNRecognizeTextRequest (Vision), greift offline |
| Strukturierte Extraktion | **Foundation Models** (`@Generable`) wenn verfügbar, sonst Regex/Heuristik |
| Foto-Zugriff | PHPhotoLibrary (mit Limited-Library-Handling) |
| Backend | **Convex** — in diesem Slice als OCR-Proxy (HTTP Action Endpoint), vorbereitet für Pokewallet-Matching im nächsten Schritt |
| Netzwerk | Erforderlich für Primärpfad; Offline-Fallback funktionsfähig |
---
## 7 Qualitätskriterien für den Slice
Der Erkennungs-Slice gilt als abgeschlossen, wenn:
1. **Mistral-Pfad (online):** Kartenname in ≥ 90 %, Nummer/Set in ≥ 85 %, Seltenheit in ≥ 80 % korrekt erkannt.
2. **On-Device-Pfad (offline):** Kartenname in ≥ 75 %, Nummer/Set in ≥ 70 %, Seltenheit in ≥ 65 % korrekt erkannt.
3. Bei schwacher Confidence wird **nie** ein falsches Ergebnis ohne Hinweis als sicher dargestellt.
4. Der manuelle Korrektur-/Eingabeweg ist jederzeit erreichbar.
5. Der Wechsel zwischen Online- und Offline-Pfad erfolgt **transparent** — der Nutzer muss nicht wissen, welcher Pfad aktiv ist (außer dem dezenten Hinweis im Ergebnis).
6. Kamera- und Foto-Berechtigungen werden korrekt behandelt, inklusive `.limited` und `.denied` Recovery.
7. Bilder werden nach Analyse zuverlässig verworfen.
8. Convex HTTP Action Endpoint ist deployt und erreichbar; API-Key ist serverseitig geschützt.
9. Foundation Models Enhancement aktiviert sich automatisch auf kompatiblen Geräten ohne Nutzereingriff.
---
## 8 Bewusst ausgeschlossen (nächster Schritt)
- Pokewallet-API-Integration (Matching erkannter Daten gegen Kartendatenbank).
- Sammlung / SwiftData-Persistenz der erkannten Karten.
- Stack-Logik, Duplikat-Handling, Condition-Buckets.
- Auth, Sync, Multi-Device.
- Preisanzeige, Bewertung, Collection-Übersicht.
- Sharing / Export.
- Nutzerseitige Scan-Limits oder Kostenweitergabe.
---
## 9 Offene Punkte innerhalb dieses Slices
| # | Frage | Empfehlung |
|---|---|---|
| 1 | Bildformat für Mistral-Upload: Base64 inline im HTTP Action Body oder Signed URL via Convex File Storage? | Signed URL über Convex File Storage — vermeidet große Payloads im HTTP Request Body. |
| 2 | Soll die Custom-Words-Liste für Vision-OCR statisch gebündelt oder dynamisch erweiterbar sein? | Statisch im Bundle als v1, dynamisch wenn Backend-Metadaten verfügbar sind. |
| 3 | Wie wird die Erkennungsqualität systematisch getestet? | Testset mit 3050 Fotos verschiedener Karten/Sets/Sprachen, aufgeteilt nach Mistral- und On-Device-Pfad. |
| 4 | Soll der Crop-Refinement-Schritt ein freies Rect oder ein festes Karten-Seitenverhältnis verwenden? | Festes Seitenverhältnis (≈ 2.5:3.5) mit minimalem Padding. |
| 5 | Braucht der Ergebnis-Screen eine Vorschau des gescannten Bilds? | Ja — kleines Thumbnail zur Orientierung, wird aber nicht persistiert. |
| 6 | Soll Convex die strukturierte Extraktion serverseitig übernehmen (z. B. via Mistral Chat nach OCR)? | Vorerst nein — Extraktion bleibt client-seitig (Foundation Models oder Regex). Kann in Stufe 2 auf den Server wandern, wenn Pokewallet-Matching hinzukommt. |
| 7 | Timeout/Retry-Strategie für den Mistral-Call? | 10s Timeout, 1 automatischer Retry, dann Fallback auf On-Device mit Hinweis. |
| 8 | Auswirkung von iOS 26 Deployment Target auf Nutzerbasis? | Vertretbar für Neustart ohne Bestandsnutzer. Genaue Gerätekompatibilität für iOS 26 prüfen. |
---
## 10 Doc-Validierung (Sosumi + Context7)
- **Foundation Models Framework** — API-Shape bestätigt: `SystemLanguageModel`, `LanguageModelSession`, `@Generable`, `@Guide` mit `.anyOf()` und `.pattern()` existieren wie im PRD beschrieben. Availability-Check über `.availability` Property mit `.available`, `.unavailable(.deviceNotEligible | .appleIntelligenceNotEnabled | .modelNotReady)`. Sprachunterstützung prüfbar via `supportsLocale()`.
- **`GenerationGuide.pattern(_:)`** — Die Methode heißt `.pattern()`, nicht `.regex()`. PRD-Code-Beispiel wurde korrigiert.
- **`VNRecognizeTextRequest`** — Seit iOS 13 verfügbar, nicht deprecated. Properties `customWords`, `recognitionLanguages`, `usesLanguageCorrection`, `recognitionLevel` bestätigt.
- **`AVCaptureDevice.RotationCoordinator`** — Seit iOS 17 verfügbar, kompatibel mit iOS 26 Target.
- **Convex Swift Client (`ConvexMobile`)** — Unterstützt `mutation()` und `action()` Calls. Convex-Docs empfehlen jedoch, Actions nicht direkt vom Client aufzurufen (Anti-Pattern). PRD wurde auf HTTP Action Endpoint angepasst.
- **Convex File Storage** — Upload via `generateUploadUrl()` Mutation + `POST` an Signed URL bestätigt. Passt für Bild-Upload vor OCR.
- **Foundation Models Error-Handling** — Relevante Fehlertypen (`guardrailViolation`, `decodingFailure`, `rateLimited`, `unsupportedLanguageOrLocale`) im PRD als Fallback-Auslöser dokumentiert.