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.
This commit is contained in:
323
.docs/prd.md
Normal file
323
.docs/prd.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 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 ~$1–2 pro 1.000 Seiten/Bilder. Bei typischem Hobbynutzer-Volumen (10–50 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: 1–3 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 30–50 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.
|
||||
Reference in New Issue
Block a user