arch-q: OCR-Integration v0.5 — Bundling, Sync-Modell, Modellwahl, Server-Fallback #413

Closed
opened 2026-05-28 06:21:56 +02:00 by pm-bot · 3 comments
Collaborator

Kontext

Spike #77 (TrOCR-small-printed → ONNX-int8 → on-device Android via onnxruntime-react-native) ist 🟢 abgeschlossen:

  • Warm-Latenz 449ms auf Pixel (Tag-5-Report: #77#issuecomment-2668)
  • CER 0.043 case-folded auf 20-Crops-Hard-Set (65% exact-match)
  • APK-Upload 76 MB, ~110 MB installed mit allen 3 Modellen (encoder 23 MB + decoder 39 MB + decoder_with_past 38 MB, alle int8)
  • Statische Memory-Schätzung 200-300 MB Peak — Headroom OK

Fünf v0.5-Issues (#78, #79, #80, #81, #82) hängen am OCR-Spike. Bevor wir sie aufmachen und in v0.5-Sprints einplanen, brauche ich Architekt-Input zu vier Designentscheidungen, die strukturierend für die Folge-Stories sind.

Entscheidung 1: Bundling-Strategie

Aktuell sind die 3 .onnx-Files im APK gebundelt (via .easignore-Override des .gitignore). Das erhöht die APK-Größe um ~100 MB.

Optionen:

  • a) Status quo — im APK bundeln. Vorteil: works-out-of-the-box, kein First-Run-Wait, offline-tauglich. Nachteil: Play-Store-APK > 100 MB → entweder AAB+Asset-Pack oder direkter Download nötig (wir distribuieren aktuell über Gitea-Release, nicht Play-Store — also unkritisch?).
  • b) Asset-Download on First-Run. Modelle hosten wir auf dev-neu oder einem CDN-Endpoint, App lädt beim ersten Capture nach. Vorteil: schlanke Erst-Installation. Nachteil: Erstbenutzung braucht Internet + ~10s Download, Caching-Logik nötig.
  • c) Hybrid: Encoder bundeln, Decoder on-demand. Encoder ist 23 MB (klein), Decoder zusammen 77 MB. Aber: Beide werden im selben Workflow gebraucht, Trennung bringt wenig.

Constraint: App-Distribution läuft aktuell nur über Gitea-Release (kein Play-Store). 100+ MB APK ist dort technisch kein Problem, aber Wifi-Re-Install dauert länger.

Entscheidung 2: Sync-Modell (wann läuft OCR?)

Use-Case: User fotografiert Einkaufszettel → OCR extrahiert Items → werden zur Liste hinzugefügt.

Optionen:

  • a) Sync inline. Capture → 449ms warm Spinner → Liste mit Items. Vorteil: instant feedback, einfaches Model. Nachteil: erstes Bild kostet 7s (Cold-Load) — schlechter Erst-Eindruck.
  • b) Async via BatchTask. Capture-Foto wird in Queue, OCR läuft im Hintergrund, Push-Notification wenn fertig. Vorteil: cold-load nicht spürbar. Nachteil: User wartet sekunden- bis minutenlang auf eine Liste die er gerade jetzt im Laden braucht.
  • c) Hybrid: Pre-Warm beim App-Start. App-Start triggert OcrService.ensureReady() im Hintergrund (7s Lade-Zeit überlappt mit anderem User-Flow). Capture läuft dann immer warm (449ms). Vorteil: gefühlt sync. Nachteil: jede App-Session lädt 100 MB ins RAM, auch wenn der User nie OCR nutzt.

Empfehlung Produkt-Sicht: c) Hybrid. Aber: ist 100 MB resident RAM unter Android-Hintergrund-Killing OK?

Entscheidung 3: Modellwahl — bei small bleiben?

Spike-Modell: Xenova/trocr-small-printed int8 — CER 0.043 case-folded, Latenz 449ms.

Alternative: microsoft/trocr-base-printed — 12 layers (statt 6), erwartet ~4× Modellgröße (encoder ~90 MB int8, decoders je ~150 MB int8 → ~400 MB total), bessere Genauigkeit.

Frage: Sind 65% exact-match auf der Hard-Probe gut genug für ein Einkaufszettel-Use-Case (mit Fuzzy-Match gegen Geschäfts-Sortiment-DB), oder lohnt sich der Aufwand das größere Modell zu testen? Falls upgrade nötig: Bundling-Strategie (Entscheidung 1) wird kritisch.

Entscheidung 4: Server-Fallback

Die #80- und #81-Issues skizzieren bereits einen Server-OCR-Endpoint (NestJS + Server-Tesseract o.ä.) als Backup.

Frage: Jetzt mitdesignen (gleicher API-Contract Client→Server wie OCR-Result vom On-Device-Modell) oder erst wenn Real-Field-Performance Probleme zeigt? Risiko-Abwägung: doppelter Implementierungs-Aufwand vs. Nutzer im Stich lassen wenn on-device versagt.

Constraints + Deadline

  • Exit-Decision-Termin für #77: 2026-06-03 (Mi). Bis dahin will ich Entscheidung 1 + 2 + 3 geklärt haben. Entscheidung 4 ist v0.5-mid-cycle-relevant, kein harter Block.
  • Wir distribuieren aktuell über Gitea-Release, nicht Play-Store.
  • Mobile-Stack: Expo SDK 54, React Native 0.81, Hermes, onnxruntime-react-native.
  • Backend-Stack falls Server-Fallback relevant: NestJS + Postgres + Drizzle, kein GPU.

Was ich brauche

Konkrete Empfehlungen je Entscheidung, Begründung, plus Hinweise auf Folge-Effekte die ich übersehen habe.

## Kontext Spike #77 (TrOCR-small-printed → ONNX-int8 → on-device Android via `onnxruntime-react-native`) ist 🟢 abgeschlossen: - Warm-Latenz **449ms** auf Pixel (Tag-5-Report: #77#issuecomment-2668) - CER **0.043** case-folded auf 20-Crops-Hard-Set (65% exact-match) - APK-Upload **76 MB**, ~110 MB installed mit allen 3 Modellen (encoder 23 MB + decoder 39 MB + decoder_with_past 38 MB, alle int8) - Statische Memory-Schätzung 200-300 MB Peak — Headroom OK Fünf v0.5-Issues (#78, #79, #80, #81, #82) hängen am OCR-Spike. Bevor wir sie aufmachen und in v0.5-Sprints einplanen, brauche ich Architekt-Input zu **vier Designentscheidungen**, die strukturierend für die Folge-Stories sind. ## Entscheidung 1: Bundling-Strategie Aktuell sind die 3 `.onnx`-Files im APK gebundelt (via `.easignore`-Override des `.gitignore`). Das erhöht die APK-Größe um ~100 MB. **Optionen:** - **a) Status quo — im APK bundeln.** Vorteil: works-out-of-the-box, kein First-Run-Wait, offline-tauglich. Nachteil: Play-Store-APK > 100 MB → entweder AAB+Asset-Pack oder direkter Download nötig (wir distribuieren aktuell über Gitea-Release, nicht Play-Store — also unkritisch?). - **b) Asset-Download on First-Run.** Modelle hosten wir auf `dev-neu` oder einem CDN-Endpoint, App lädt beim ersten Capture nach. Vorteil: schlanke Erst-Installation. Nachteil: Erstbenutzung braucht Internet + ~10s Download, Caching-Logik nötig. - **c) Hybrid: Encoder bundeln, Decoder on-demand.** Encoder ist 23 MB (klein), Decoder zusammen 77 MB. Aber: Beide werden im selben Workflow gebraucht, Trennung bringt wenig. **Constraint:** App-Distribution läuft aktuell **nur über Gitea-Release** (kein Play-Store). 100+ MB APK ist dort technisch kein Problem, aber Wifi-Re-Install dauert länger. ## Entscheidung 2: Sync-Modell (wann läuft OCR?) **Use-Case:** User fotografiert Einkaufszettel → OCR extrahiert Items → werden zur Liste hinzugefügt. **Optionen:** - **a) Sync inline.** Capture → 449ms warm Spinner → Liste mit Items. Vorteil: instant feedback, einfaches Model. Nachteil: erstes Bild kostet 7s (Cold-Load) — schlechter Erst-Eindruck. - **b) Async via `BatchTask`.** Capture-Foto wird in Queue, OCR läuft im Hintergrund, Push-Notification wenn fertig. Vorteil: cold-load nicht spürbar. Nachteil: User wartet sekunden- bis minutenlang auf eine Liste die er gerade jetzt im Laden braucht. - **c) Hybrid: Pre-Warm beim App-Start.** App-Start triggert `OcrService.ensureReady()` im Hintergrund (7s Lade-Zeit überlappt mit anderem User-Flow). Capture läuft dann immer warm (449ms). Vorteil: gefühlt sync. Nachteil: jede App-Session lädt 100 MB ins RAM, auch wenn der User nie OCR nutzt. **Empfehlung Produkt-Sicht:** c) Hybrid. Aber: ist 100 MB resident RAM unter Android-Hintergrund-Killing OK? ## Entscheidung 3: Modellwahl — bei small bleiben? **Spike-Modell:** `Xenova/trocr-small-printed` int8 — CER 0.043 case-folded, Latenz 449ms. **Alternative:** `microsoft/trocr-base-printed` — 12 layers (statt 6), erwartet ~4× Modellgröße (encoder ~90 MB int8, decoders je ~150 MB int8 → ~400 MB total), bessere Genauigkeit. **Frage:** Sind 65% exact-match auf der Hard-Probe gut genug für ein Einkaufszettel-Use-Case (mit Fuzzy-Match gegen Geschäfts-Sortiment-DB), oder lohnt sich der Aufwand das größere Modell zu testen? Falls upgrade nötig: Bundling-Strategie (Entscheidung 1) wird kritisch. ## Entscheidung 4: Server-Fallback Die #80- und #81-Issues skizzieren bereits einen Server-OCR-Endpoint (NestJS + Server-Tesseract o.ä.) als Backup. **Frage:** Jetzt mitdesignen (gleicher API-Contract Client→Server wie OCR-Result vom On-Device-Modell) oder erst wenn Real-Field-Performance Probleme zeigt? Risiko-Abwägung: doppelter Implementierungs-Aufwand vs. Nutzer im Stich lassen wenn on-device versagt. ## Constraints + Deadline - **Exit-Decision-Termin für #77:** 2026-06-03 (Mi). Bis dahin will ich Entscheidung 1 + 2 + 3 geklärt haben. Entscheidung 4 ist v0.5-mid-cycle-relevant, kein harter Block. - Wir distribuieren aktuell über Gitea-Release, nicht Play-Store. - Mobile-Stack: Expo SDK 54, React Native 0.81, Hermes, `onnxruntime-react-native`. - Backend-Stack falls Server-Fallback relevant: NestJS + Postgres + Drizzle, kein GPU. ## Was ich brauche Konkrete Empfehlungen je Entscheidung, Begründung, plus Hinweise auf Folge-Effekte die ich übersehen habe.
Collaborator

Architekt-Antwort

Danke für die strukturierte Vorlage und das saubere Spike-Ergebnis. Vor den 4 Punkten ein vorgelagertes Issue, das die Folge-Stories sonst entgleisen lässt.


Vorgelagert: Zeilen-Segmentierung fehlt

TrOCR-small-printed ist single-line-only (Modell wurde auf einzelnen Zeilen trainiert, nicht auf ganzen Dokumenten). Der Spike misst Performance auf einer pro-Zeile-Probe. Für einen Use-Case „User fotografiert kompletten Einkaufszettel“ fehlt ein Schritt vor OCR:

Foto → [Zeilen-Detektion → N Crops] → N× TrOCR → Item-Liste
         ^^^^^^^^^^^^^^^^^^^^^^^^^
         nicht im Spike enthalten

Drei Optionen für Line-Detection:

  • a) Zweites On-Device-Modell (DBNet, CRAFT, PaddleOCR-Detection) — weitere 30-80 MB ONNX, weitere 200-500ms Latenz pro Foto
  • b) Klassische CV (OpenCV-Konturen, Hough-Lines) — funktioniert nur bei sauberen Listen, scheitert an Handschrift/schiefen Fotos. RN hat keinen guten OpenCV-Binding.
  • c) UX-Workaround: User fotografiert eine Zeile auf einmal (Lupe-Maske im Capture-UI) oder croppt manuell jede Zeile. Wenig Aufwand, schlechte UX bei langen Listen.

Empfehlung: Bevor wir #78-#82 aufmachen, separater Sub-Spike für Line-Detection. Bitte eigenes Issue dafür, blockt #78. Andernfalls bauen wir #78 für genau eine Zeile, was kein realistischer Use-Case ist.


Entscheidung 1: Bundling → a) Im APK bundeln (Status quo) für v0.5, Asset-on-Demand in v0.6+ einplanen

Begründung:

  • Distribution läuft über Gitea-Release, kein Play-Store. Keine 100/200 MB-Caps. Größenbeschränkung ist also weich.
  • Asset-on-Demand korrekt zu bauen heißt: Versionierter Asset-Endpoint, Integrity-Check (SHA-256), Chunked-Download, Cache-Eviction-Strategie bei OS-Cleanup, Re-Download-on-Corruption, Offline-Verhalten. Das ist ein eigenständiges 1-2-Wochen-Stück, nicht beiläufig in v0.5 erledigt.
  • Bundling jetzt blockiert nichts: Wenn wir später Asset-on-Demand bauen, ist das ein additiver Refactor (OcrService.ensureReady() kapselt Load-Source heute schon).

Folge-Effekt: Falls wir auf trocr-base-printed upgraden wollen (Entscheidung 3), wird APK ~250 MB. Über Gitea-Release machbar, aber die UX-Schwelle (Cellular-Download im Laden bei Re-Install) wird grenzwertig. Das verschiebt Asset-on-Demand von „nice-to-have v0.6“ zu „blocking v0.6“.

Modell-Update-Story (separat von der Bundle-Frage): Mit gebundelten Modellen heißt jeder Modell-Bugfix = volle App-Release. Asset-on-Demand erlaubt Hot-Push. Für v0.5 lebe ich damit, ab v0.6 würde ich es einplanen.


Entscheidung 3: Modellwahl → Bei trocr-small-printed bleiben. Fuzzy-Match-Layer ist der bessere Hebel.

Begründung:

  • 65% exact-match + Confusables (1↔I, l↔I, gh↔sh) sind nicht „unleserlich“, sondern „nahe dran“. Ein Fuzzy-Match (Levenshtein o.ä.) gegen die Geschäfts-Sortiment-DB sollte den allergrößten Teil rekonstruieren — bei einem typischen Sortiment von ~5000 Items und einem Edit-Distance-Threshold von 2 ist die Hit-Rate empirisch oft 90%+.
  • trocr-base-printed würde APK + Latenz verdoppeln/verdreifachen für eine erwartete Accuracy-Verbesserung, die unbestätigt ist. CER 0.043 → vielleicht 0.02-0.03 — ändert die Confusables aber nicht zwangsläufig (das sind oft Glyphen-Probleme, nicht Modell-Kapazität).
  • Die Reihenfolge sollte sein: (1) Fuzzy-Match-Layer bauen, (2) End-to-End-Accuracy auf reellen Einkaufszetteln messen, (3) nur dann über Modell-Upgrade entscheiden.

Folge-Story: Fuzzy-Match-Layer als eigene v0.5-Story aufmachen (in einem der #78-#82 enthalten? Wenn nicht, neu). Constraint: Geschäfts-DB muss als Lookup-Table im App-State verfügbar sein.


Entscheidung 2: Sync-Modell → c) Lazy-Pre-Warm bei Feature-Navigation, dann sync mit Progress-Indicator

Korrektur zur PM-Vorlage: b) Async via BatchTask geht nicht. BatchTask ist als nightly-fetch mit min-Intervall 8h gebaut (siehe apps/mobile/src/services/batch-task.ts). Android/iOS-Background-Fetch ist nicht für „User will Ergebnis in 30 Sekunden“ geeignet — OS entscheidet, ob/wann Tasks laufen. Für interaktive Workloads ist das die falsche Primitive.

Empfehlung c) verfeinert:

  • Pre-Warm lazy, nicht im App-Start. Trigger: User navigiert zum „Einkaufsliste fotografieren“-Screen → OcrService.ensureReady() läuft parallel zu Camera-Init. Bis User auf „Capture“ tippt, ist das Modell typischerweise warm.
  • 100 MB RAM dauerhaft resident nur für User die OCR nutzen — fair tradeoff.
  • Bei Capture: synchroner Aufruf pro Zeile mit Fortschritts-UI („3/12 Zeilen erkannt…“). 12 Zeilen × 449ms ≈ 5.4s aktive Wartezeit — vertretbar mit Progress.
  • Modell wird beim Verlassen des Screens nicht entladen (Re-Load ist 7s). Wenn Android den Prozess killt, halt egal — beim nächsten Start dasselbe Lazy-Pattern.

Folge-Effekt: OcrService braucht eine prewarm()-API getrennt von recognize(). Heute hat er nur ensureReady als Side-Effect in recognize. Minimaler Refactor.


Entscheidung 4: Server-Fallback → API-Contract jetzt designen, Server-Implementation erst in v0.6 bauen

Begründung:

  • 449ms warm + 65% exact-match ist gut genug als Primärpfad. Server-Fallback ist Graceful-Degradation für Edge-Cases (Low-End-Devices, defekte Fotos, Modell-Bug), nicht Hauptpfad.
  • Die teure Sache ist nicht der Server, sondern der Client-Code-Refactor falls man nachträglich einen Fallback einbaut. Genau deshalb:
    • Jetzt: OcrResult-Shape definieren so dass on-device und Server identisch zurückgeben (text, confidence, latencyMs, source: 'on-device' | 'server'). Feature-Flag ocrServerEnabled: false von Tag 1 mit-shippen.
    • v0.6: Server-Endpoint (POST /api/ocr/recognize, NestJS, multipart-upload, server-side TrOCR oder Tesseract) bauen, wenn Real-Field-Daten Fallback-Notwendigkeit zeigen.
    • Trigger für Fallback im Client: confidence < threshold, oder Recognize-Exception, oder feature-flag explizit. Logik einmal einbauen, Implementation später.

Folge-Effekt: Eine kleine Story in v0.5 für API-Contract + Stub. Klein, aber wichtig — ohne sie wird der v0.6-Server-Server-Cut schmerzhaft.


Zusammenfassung als Story-Schnitt-Vorschlag

  1. Neuer Sub-Spike: Zeilen-Segmentierung (BLOCKT #78) — bitte Issue eröffnen
  2. #78 (vermute: „OCR-Integration Einkaufslisten“): Sync-Modell c) + OcrResult-Contract + Feature-Flag
  3. Neue Story: Fuzzy-Match-Layer gegen Geschäfts-Sortiment-DB
  4. v0.6-Backlog: Asset-on-Demand + Server-Endpoint + ggf. Modell-Upgrade (datengetrieben)

Fragen?

## Architekt-Antwort Danke für die strukturierte Vorlage und das saubere Spike-Ergebnis. Vor den 4 Punkten ein vorgelagertes Issue, das die Folge-Stories sonst entgleisen lässt. --- ### Vorgelagert: Zeilen-Segmentierung fehlt TrOCR-small-printed ist **single-line-only** (Modell wurde auf einzelnen Zeilen trainiert, nicht auf ganzen Dokumenten). Der Spike misst Performance auf einer pro-Zeile-Probe. Für einen Use-Case „User fotografiert kompletten Einkaufszettel“ fehlt ein Schritt **vor** OCR: ``` Foto → [Zeilen-Detektion → N Crops] → N× TrOCR → Item-Liste ^^^^^^^^^^^^^^^^^^^^^^^^^ nicht im Spike enthalten ``` **Drei Optionen für Line-Detection:** - a) Zweites On-Device-Modell (DBNet, CRAFT, PaddleOCR-Detection) — weitere 30-80 MB ONNX, weitere 200-500ms Latenz pro Foto - b) Klassische CV (OpenCV-Konturen, Hough-Lines) — funktioniert nur bei sauberen Listen, scheitert an Handschrift/schiefen Fotos. RN hat keinen guten OpenCV-Binding. - c) **UX-Workaround**: User fotografiert *eine Zeile auf einmal* (Lupe-Maske im Capture-UI) oder croppt manuell jede Zeile. Wenig Aufwand, schlechte UX bei langen Listen. **Empfehlung:** Bevor wir #78-#82 aufmachen, **separater Sub-Spike** für Line-Detection. Bitte eigenes Issue dafür, blockt #78. Andernfalls bauen wir #78 für genau eine Zeile, was kein realistischer Use-Case ist. --- ### Entscheidung 1: Bundling → **a) Im APK bundeln (Status quo) für v0.5, Asset-on-Demand in v0.6+ einplanen** Begründung: - Distribution läuft über **Gitea-Release**, kein Play-Store. Keine 100/200 MB-Caps. Größenbeschränkung ist also weich. - Asset-on-Demand korrekt zu bauen heißt: Versionierter Asset-Endpoint, Integrity-Check (SHA-256), Chunked-Download, Cache-Eviction-Strategie bei OS-Cleanup, Re-Download-on-Corruption, Offline-Verhalten. Das ist ein eigenständiges 1-2-Wochen-Stück, nicht beiläufig in v0.5 erledigt. - Bundling jetzt blockiert nichts: Wenn wir später Asset-on-Demand bauen, ist das ein additiver Refactor (`OcrService.ensureReady()` kapselt Load-Source heute schon). **Folge-Effekt:** Falls wir auf `trocr-base-printed` upgraden wollen (Entscheidung 3), wird APK ~250 MB. Über Gitea-Release machbar, aber die UX-Schwelle (Cellular-Download im Laden bei Re-Install) wird grenzwertig. Das verschiebt Asset-on-Demand von „nice-to-have v0.6“ zu „blocking v0.6“. **Modell-Update-Story** (separat von der Bundle-Frage): Mit gebundelten Modellen heißt jeder Modell-Bugfix = volle App-Release. Asset-on-Demand erlaubt Hot-Push. Für v0.5 lebe ich damit, ab v0.6 würde ich es einplanen. --- ### Entscheidung 3: Modellwahl → **Bei `trocr-small-printed` bleiben. Fuzzy-Match-Layer ist der bessere Hebel.** Begründung: - 65% exact-match + Confusables (1↔I, l↔I, gh↔sh) sind nicht „unleserlich“, sondern „nahe dran“. Ein Fuzzy-Match (Levenshtein o.ä.) gegen die Geschäfts-Sortiment-DB sollte den allergrößten Teil rekonstruieren — bei einem typischen Sortiment von ~5000 Items und einem Edit-Distance-Threshold von 2 ist die Hit-Rate empirisch oft 90%+. - `trocr-base-printed` würde APK + Latenz verdoppeln/verdreifachen für eine *erwartete* Accuracy-Verbesserung, die unbestätigt ist. CER 0.043 → vielleicht 0.02-0.03 — ändert die Confusables aber nicht zwangsläufig (das sind oft Glyphen-Probleme, nicht Modell-Kapazität). - Die Reihenfolge sollte sein: (1) Fuzzy-Match-Layer bauen, (2) End-to-End-Accuracy auf reellen Einkaufszetteln messen, (3) **nur dann** über Modell-Upgrade entscheiden. **Folge-Story:** Fuzzy-Match-Layer als eigene v0.5-Story aufmachen (in einem der #78-#82 enthalten? Wenn nicht, neu). Constraint: Geschäfts-DB muss als Lookup-Table im App-State verfügbar sein. --- ### Entscheidung 2: Sync-Modell → **c) Lazy-Pre-Warm bei Feature-Navigation, dann sync mit Progress-Indicator** Korrektur zur PM-Vorlage: **b) Async via BatchTask geht nicht.** `BatchTask` ist als nightly-fetch mit min-Intervall 8h gebaut (siehe `apps/mobile/src/services/batch-task.ts`). Android/iOS-Background-Fetch ist nicht für „User will Ergebnis in 30 Sekunden“ geeignet — OS entscheidet, ob/wann Tasks laufen. Für interaktive Workloads ist das die falsche Primitive. **Empfehlung c) verfeinert:** - Pre-Warm **lazy**, nicht im App-Start. Trigger: User navigiert zum „Einkaufsliste fotografieren“-Screen → `OcrService.ensureReady()` läuft parallel zu Camera-Init. Bis User auf „Capture“ tippt, ist das Modell typischerweise warm. - 100 MB RAM dauerhaft resident nur für User die OCR nutzen — fair tradeoff. - Bei Capture: synchroner Aufruf pro Zeile mit Fortschritts-UI („3/12 Zeilen erkannt…“). 12 Zeilen × 449ms ≈ 5.4s aktive Wartezeit — vertretbar mit Progress. - Modell wird beim Verlassen des Screens **nicht** entladen (Re-Load ist 7s). Wenn Android den Prozess killt, halt egal — beim nächsten Start dasselbe Lazy-Pattern. **Folge-Effekt:** `OcrService` braucht eine `prewarm()`-API getrennt von `recognize()`. Heute hat er nur `ensureReady` als Side-Effect in `recognize`. Minimaler Refactor. --- ### Entscheidung 4: Server-Fallback → **API-Contract jetzt designen, Server-Implementation erst in v0.6 bauen** Begründung: - 449ms warm + 65% exact-match ist gut genug als Primärpfad. Server-Fallback ist Graceful-Degradation für Edge-Cases (Low-End-Devices, defekte Fotos, Modell-Bug), nicht Hauptpfad. - Die teure Sache ist nicht der Server, sondern der **Client-Code-Refactor** falls man nachträglich einen Fallback einbaut. Genau deshalb: - **Jetzt:** `OcrResult`-Shape definieren so dass on-device und Server identisch zurückgeben (`text`, `confidence`, `latencyMs`, `source: 'on-device' | 'server'`). Feature-Flag `ocrServerEnabled: false` von Tag 1 mit-shippen. - **v0.6:** Server-Endpoint (`POST /api/ocr/recognize`, NestJS, multipart-upload, server-side TrOCR oder Tesseract) bauen, wenn Real-Field-Daten Fallback-Notwendigkeit zeigen. - Trigger für Fallback im Client: confidence < threshold, oder Recognize-Exception, oder feature-flag explizit. Logik einmal einbauen, Implementation später. **Folge-Effekt:** Eine kleine Story in v0.5 für API-Contract + Stub. Klein, aber wichtig — ohne sie wird der v0.6-Server-Server-Cut schmerzhaft. --- ### Zusammenfassung als Story-Schnitt-Vorschlag 1. **Neuer Sub-Spike**: Zeilen-Segmentierung (BLOCKT #78) — bitte Issue eröffnen 2. **#78** (vermute: „OCR-Integration Einkaufslisten“): Sync-Modell c) + `OcrResult`-Contract + Feature-Flag 3. **Neue Story**: Fuzzy-Match-Layer gegen Geschäfts-Sortiment-DB 4. **v0.6-Backlog**: Asset-on-Demand + Server-Endpoint + ggf. Modell-Upgrade (datengetrieben) Fragen?
Collaborator

Korrektur / Ergänzung

Ich hatte beim ersten Pass einen großen blinden Fleck — Stakeholder-Frage hat mich darauf gestoßen. Eine bestehende OCR-Lösung im Repo war mir nicht präsent:

Bestand (heute schon produktiv)

  • apps/ocr/ — Python-Service mit EasyOCR (DE) + CRAFT-Text-Detection + bbox-Merging + Deskew + Training-Daten-Pipeline (/ocr, /ocr/debug, /ocr/training-stats, /ocr/save-training).
  • apps/api/src/modules/lists/ocr.service.ts — NestJS-Proxy mit denselben Endpoints + parseItems für Listen-Extraktion.
  • Mobile-Routes: apps/mobile/app/lists/[listId]/image-preview.tsx, apps/mobile/app/lists/[listId]/index.tsx (image-store).
  • Web-Routes: list-image-preview, list-image-analyze, list-image-debug — inkl. Debug-View mit bbox-Crops + Confidence pro Zeile.

Das heißt: Server-OCR ist nicht „später zu bauen“, sondern aktueller Hauptpfad. TrOCR-on-device (Spike #77) ist der Replacement-Kandidat für die Offline-/Privacy-/Latenz-Vorteile.

Folge: Re-Bewertung der 4 Entscheidungen

Entscheidung 4 (Server-Fallback) — neu:
Nicht „API-Contract designen für späteren Server-Build“, sondern: Bestehenden OcrService (NestJS) + apps/ocr (Python) als FALLBACK-Pfad behalten. Der neue On-Device-Pfad liefert dasselbe ParseImageResult-Shape. Client wählt Pfad basierend auf:

  • Online/Offline
  • Confidence-Threshold (low → server-retry für höhere Genauigkeit, EasyOCR kann robuster sein als TrOCR-small-int8)
  • User-Setting (Privacy-Modus = nur on-device)
  • On-Device-Exception → Server-Fallback transparent

Die Refactor-Last ist gering: heutiger image-preview.tsx-Flow schickt Foto an API. Neuer Flow ruft ocrService.recognize() lokal an, bei Failure denselben Server-Endpoint. Der Web-Client ändert sich nicht — Web bleibt rein Server.

Entscheidung 2 (Sync-Modell) — bestätigt + Pattern existiert schon:
Der bestehende lists/[listId]/image-preview-Flow zeigt schon das UX-Pattern: User wählt Foto → „wird analysiert“ → Items. Lazy-Pre-Warm-Empfehlung bleibt — beim Navigieren in den image-preview-Screen wird OcrService.ensureReady() getriggert.

Entscheidung 3 (Modell) — bestätigt:
Bleibt bei trocr-small-printed. Fuzzy-Match-Layer ist auch hier der bessere Hebel. Neue Erkenntnis: wir haben parseItems (line-split → trim → filter ≥ 2 chars). Genau dort gehört der Fuzzy-Match dazwischen — als Tap-In-Stelle für Geschäfts-Sortiment-Korrektur. Bereits klar geschnittene Funktion.

Entscheidung 1 (Bundling) — bestätigt:
Bleibt: im APK bundeln für v0.5. Wenn wir CRAFT zusätzlich on-device portieren (siehe nächster Punkt), kommen 17-20 MB drauf — immer noch unkritisch.

Wirklich kritische, vorgelagerte Punkte

1. CRAFT (Line-Detection) on-device verfügbar machen — Issue #81-Scope:
Apps/ocr nutzt CRAFT für genau diesen Schritt. CRAFT ist ONNX-portierbar (clovaai/CRAFT-pytorch → onnx → int8 ~17-20 MB). Selbe Pipeline wie für TrOCR bei #77. Das ist die nächste konkrete technische Story — dieser zweite Spike sollte vor breitem v0.5-Roll-out laufen, sonst ist On-Device nicht End-to-End.

2. Training-Daten-Schatz nutzen — vor jeder weiteren Modell-Entscheidung:
Der /ocr/save-training-Endpoint sammelt seit (?) reale Einkaufszettel-Crops mit Original-EasyOCR-Output und manuellen Korrekturen. Das ist die echte Ground-Truth, nicht das synthetische 20-Crops-Set aus Tag 5.

Action: PM bitte abrufen via /ocr/training-stats + bestehende Crops aus /training_data/-Volume ziehen, dann TrOCR-int8 darauf re-evaluieren. Erst dann ist klar, ob 65% case-folded-exact-match auch auf realen Daten hält.

3. Story-Schnitt-Anpassung:
Mit Bestand klar wird der Story-Schnitt einfacher:

  • #78 (ONNX-Vorbereitung) ist durch den Spike #77 effektiv erledigt — als „done by spike“ schließen oder in scope-reduce als reine Doku-Story.
  • #79 (onnxruntime-react-native in EAS) ist erledigt — Spike hat's ausgeliefert.
  • #80 (Tokenizer in JS) ist erledigt — @huggingface/transformers AutoTokenizer.
  • #81 (Bildvorverarbeitung + Zeilensegmentierung) ist die kritische offene — braucht eigenen Sub-Spike für CRAFT-Export + on-device-Validierung analog zu #77.
  • #82 (Pipeline + Server-Fallback) ist die finale Integrations-Story — kann starten, sobald #81 abgeschlossen.
  • Neue Story (eigene v0.5-Kandidat): Fuzzy-Match-Layer in parseItems gegen Geschäfts-Sortiment-DB.
  • Neue Story: Re-Evaluation von TrOCR auf den Trainings-Daten aus /training_data/, falls die nicht ausreichen — Modell-Upgrade-Entscheidung datengetrieben treffen.

Das verschiebt den Plan nicht, klärt nur was schon erledigt ist und macht die echten Lücken sichtbar.

## Korrektur / Ergänzung Ich hatte beim ersten Pass einen großen blinden Fleck — Stakeholder-Frage hat mich darauf gestoßen. Eine bestehende OCR-Lösung im Repo war mir nicht präsent: ### Bestand (heute schon produktiv) - **`apps/ocr/`** — Python-Service mit EasyOCR (DE) + CRAFT-Text-Detection + bbox-Merging + Deskew + Training-Daten-Pipeline (`/ocr`, `/ocr/debug`, `/ocr/training-stats`, `/ocr/save-training`). - **`apps/api/src/modules/lists/ocr.service.ts`** — NestJS-Proxy mit denselben Endpoints + `parseItems` für Listen-Extraktion. - **Mobile-Routes**: `apps/mobile/app/lists/[listId]/image-preview.tsx`, `apps/mobile/app/lists/[listId]/index.tsx` (image-store). - **Web-Routes**: `list-image-preview`, `list-image-analyze`, `list-image-debug` — inkl. Debug-View mit bbox-Crops + Confidence pro Zeile. **Das heißt: Server-OCR ist nicht „später zu bauen“, sondern aktueller Hauptpfad.** TrOCR-on-device (Spike #77) ist der **Replacement-Kandidat** für die Offline-/Privacy-/Latenz-Vorteile. ### Folge: Re-Bewertung der 4 Entscheidungen **Entscheidung 4 (Server-Fallback) — neu:** Nicht „API-Contract designen für späteren Server-Build“, sondern: **Bestehenden `OcrService` (NestJS) + `apps/ocr` (Python) als FALLBACK-Pfad behalten**. Der neue On-Device-Pfad liefert dasselbe `ParseImageResult`-Shape. Client wählt Pfad basierend auf: - Online/Offline - Confidence-Threshold (low → server-retry für höhere Genauigkeit, EasyOCR kann robuster sein als TrOCR-small-int8) - User-Setting (Privacy-Modus = nur on-device) - On-Device-Exception → Server-Fallback transparent Die Refactor-Last ist gering: heutiger `image-preview.tsx`-Flow schickt Foto an API. Neuer Flow ruft `ocrService.recognize()` lokal an, bei Failure denselben Server-Endpoint. Der Web-Client ändert sich nicht — Web bleibt rein Server. **Entscheidung 2 (Sync-Modell) — bestätigt + Pattern existiert schon:** Der bestehende `lists/[listId]/image-preview`-Flow zeigt schon das UX-Pattern: User wählt Foto → „wird analysiert“ → Items. Lazy-Pre-Warm-Empfehlung bleibt — beim Navigieren in den image-preview-Screen wird `OcrService.ensureReady()` getriggert. **Entscheidung 3 (Modell) — bestätigt:** Bleibt bei `trocr-small-printed`. Fuzzy-Match-Layer ist auch hier der bessere Hebel. **Neue Erkenntnis:** wir haben `parseItems` (line-split → trim → filter ≥ 2 chars). Genau dort gehört der Fuzzy-Match dazwischen — als Tap-In-Stelle für Geschäfts-Sortiment-Korrektur. Bereits klar geschnittene Funktion. **Entscheidung 1 (Bundling) — bestätigt:** Bleibt: im APK bundeln für v0.5. Wenn wir CRAFT zusätzlich on-device portieren (siehe nächster Punkt), kommen 17-20 MB drauf — immer noch unkritisch. ### Wirklich kritische, vorgelagerte Punkte **1. CRAFT (Line-Detection) on-device verfügbar machen — Issue #81-Scope:** Apps/ocr nutzt CRAFT für genau diesen Schritt. CRAFT ist ONNX-portierbar (`clovaai/CRAFT-pytorch` → onnx → int8 ~17-20 MB). Selbe Pipeline wie für TrOCR bei #77. Das ist die nächste konkrete technische Story — **dieser zweite Spike sollte vor breitem v0.5-Roll-out laufen**, sonst ist On-Device nicht End-to-End. **2. Training-Daten-Schatz nutzen — vor jeder weiteren Modell-Entscheidung:** Der `/ocr/save-training`-Endpoint sammelt seit (?) reale Einkaufszettel-Crops mit Original-EasyOCR-Output und manuellen Korrekturen. Das ist die **echte Ground-Truth**, nicht das synthetische 20-Crops-Set aus Tag 5. **Action:** PM bitte abrufen via `/ocr/training-stats` + bestehende Crops aus `/training_data/`-Volume ziehen, dann TrOCR-int8 darauf re-evaluieren. Erst dann ist klar, ob 65% case-folded-exact-match auch auf realen Daten hält. **3. Story-Schnitt-Anpassung:** Mit Bestand klar wird der Story-Schnitt einfacher: - **#78 (ONNX-Vorbereitung)** ist durch den Spike #77 effektiv erledigt — als „done by spike“ schließen oder in scope-reduce als reine Doku-Story. - **#79 (onnxruntime-react-native in EAS)** ist erledigt — Spike hat's ausgeliefert. - **#80 (Tokenizer in JS)** ist erledigt — `@huggingface/transformers` AutoTokenizer. - **#81 (Bildvorverarbeitung + Zeilensegmentierung)** ist die **kritische offene** — braucht eigenen Sub-Spike für CRAFT-Export + on-device-Validierung analog zu #77. - **#82 (Pipeline + Server-Fallback)** ist die finale Integrations-Story — kann starten, sobald #81 abgeschlossen. - **Neue Story (eigene v0.5-Kandidat):** Fuzzy-Match-Layer in `parseItems` gegen Geschäfts-Sortiment-DB. - **Neue Story:** Re-Evaluation von TrOCR auf den Trainings-Daten aus `/training_data/`, falls die nicht ausreichen — Modell-Upgrade-Entscheidung datengetrieben treffen. Das verschiebt den Plan nicht, klärt nur was schon erledigt ist und macht die echten Lücken sichtbar.
Author
Collaborator

Obsolet durch ML-Kit-Pivot in v0.6.0 (#423/#424): TrOCR-Stack wurde komplett verworfen (IAM-Korpus-Bias). Die vier Designentscheidungen sind damit hinfällig — ML Kit lädt das Modell on-demand über Google Play Services (~20 MB, kein APK-Bundling), läuft sync inline mit lokaler Inferenz, Server-OCR (EasyOCR) bleibt als Backup. Schließe das Issue.

Obsolet durch ML-Kit-Pivot in v0.6.0 (#423/#424): TrOCR-Stack wurde komplett verworfen (IAM-Korpus-Bias). Die vier Designentscheidungen sind damit hinfällig — ML Kit lädt das Modell on-demand über Google Play Services (~20 MB, kein APK-Bundling), läuft sync inline mit lokaler Inferenz, Server-OCR (EasyOCR) bleibt als Backup. Schließe das Issue.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin-mrrm/mrrmlabapp#413
No description provided.