arch-q: OCR-Integration v0.5 — Bundling, Sync-Modell, Modellwahl, Server-Fallback #413
Labels
No labels
app/archiv
app/einkaufslisten
app/imap-client
app/wissensbasis
arch-answered
arch-question
area/api
area/auth
area/infra
area/mobile
area/shared
area/ui
area/web
portfolio-status
prio/high
prio/low
prio/medium
roadmap/public
size/l
size/m
size/s
size/xl
size/xs
status/blocked
status/needs-info
type/bug
type/chore
type/docs
type/feature
type/idea
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
admin-mrrm/mrrmlabapp#413
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Kontext
Spike #77 (TrOCR-small-printed → ONNX-int8 → on-device Android via
onnxruntime-react-native) ist 🟢 abgeschlossen: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:
dev-neuoder einem CDN-Endpoint, App lädt beim ersten Capture nach. Vorteil: schlanke Erst-Installation. Nachteil: Erstbenutzung braucht Internet + ~10s Download, Caching-Logik nötig.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:
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.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-printedint8 — 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
onnxruntime-react-native.Was ich brauche
Konkrete Empfehlungen je Entscheidung, Begründung, plus Hinweise auf Folge-Effekte die ich übersehen habe.
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:
Drei Optionen für Line-Detection:
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:
OcrService.ensureReady()kapselt Load-Source heute schon).Folge-Effekt: Falls wir auf
trocr-base-printedupgraden 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-printedbleiben. Fuzzy-Match-Layer ist der bessere Hebel.Begründung:
trocr-base-printedwü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).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.
BatchTaskist als nightly-fetch mit min-Intervall 8h gebaut (sieheapps/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:
OcrService.ensureReady()läuft parallel zu Camera-Init. Bis User auf „Capture“ tippt, ist das Modell typischerweise warm.Folge-Effekt:
OcrServicebraucht eineprewarm()-API getrennt vonrecognize(). Heute hat er nurensureReadyals Side-Effect inrecognize. Minimaler Refactor.Entscheidung 4: Server-Fallback → API-Contract jetzt designen, Server-Implementation erst in v0.6 bauen
Begründung:
OcrResult-Shape definieren so dass on-device und Server identisch zurückgeben (text,confidence,latencyMs,source: 'on-device' | 'server'). Feature-FlagocrServerEnabled: falsevon Tag 1 mit-shippen.POST /api/ocr/recognize, NestJS, multipart-upload, server-side TrOCR oder Tesseract) bauen, wenn Real-Field-Daten Fallback-Notwendigkeit zeigen.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
OcrResult-Contract + Feature-FlagFragen?
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 +parseItemsfür Listen-Extraktion.apps/mobile/app/lists/[listId]/image-preview.tsx,apps/mobile/app/lists/[listId]/index.tsx(image-store).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 dasselbeParseImageResult-Shape. Client wählt Pfad basierend auf:Die Refactor-Last ist gering: heutiger
image-preview.tsx-Flow schickt Foto an API. Neuer Flow ruftocrService.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 wirdOcrService.ensureReady()getriggert.Entscheidung 3 (Modell) — bestätigt:
Bleibt bei
trocr-small-printed. Fuzzy-Match-Layer ist auch hier der bessere Hebel. Neue Erkenntnis: wir habenparseItems(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:
@huggingface/transformersAutoTokenizer.parseItemsgegen Geschäfts-Sortiment-DB./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.
pm-bot referenced this issue2026-05-28 06:48:27 +02:00
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.