# 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.