--- name: add-real-ocr-layer classification: feature success_metric: | ≥70% of legitimate scans resolve at Layer-1 (Tesseract + pg_trgm) with zero Gemini calls; scan_attempts.layer distribution proves it. skip: - ia status: shipped created: 2026-05-27 depends_on: - server-side-scan-pipeline --- # Convoy: add-real-ocr-layer **As-shipped:** PR #38 (+ follow-up polish in PR #39). Layer-1 Tesseract + `pg_trgm` before Gemini escalation. Add a cheap local OCR + fuzzy DB match layer so most scans never hit Gemini. ## Why Every scan today (post convoy #2) calls Gemini Flash server-side. That is slow, costs money, and hits rate limits under bulk scanning. The audit proposed a two-layer pipeline: Layer-1 runs Tesseract on the card name strip + `pg_trgm` similarity against `cards.name`; only escalates to `/api/scan/identify` (Gemini) when confidence is low. Target: **≥70% Layer-1 hit rate** measured via `scan_attempts.layer`. ## Scope ### In scope - **Migration** — enable `pg_trgm` extension; GIN index on `cards (name gin_trgm_ops)`. - **`pages/api/cards/identify-by-text.js`** (new): - similarity > 0.85 → single match - 0.6–0.85 → disambiguation list - < 0.6 → `{ escalate: true }` (client falls back to `/api/scan/identify`) - **`tesseract.js`** dependency + **`lib/ocr-worker.js`** (new) — browser Worker wrapping Tesseract; OCR name strip before API calls. - **`components/CameraScanner.js`** — integrate Worker: try Layer-1 path first, escalate on low confidence. - **`docs/SCHEMA_MAP.md`** — index + extension documented. - **`.github/workflows/ci.yml`** — piggyback fix: extend `schema-map-fresh` job `if:` condition to include `migrations/**` paths (currently only watches `scripts/add-*`, `scripts/fix-*`, `setup-neon-db.js`, and `docs/SCHEMA_MAP.md` — misses post-`migration-tool` migrations). ### Out of scope - **OpenCV perspective transform / card boundary detection** — queue `improve-scan-card-detection` if Layer-1 hit rate stays below 70% after this lands. - **Retraining or custom ML models** — Tesseract + trigram is sufficient for v1. - **Scanner UX redesign** — convoy `redesign-scanner-flow` (#4). ## Roles invoked 1. `role-ux-reviewer` — Layer-1 vs escalation feedback (scanning status). 2. `role-architect` — similarity thresholds, Worker bundling, 2 briefs. 3. `role-implementer` — 2 briefs (Brief 2 depends on Brief 1). 4. `role-reviewer` + `role-design-system-auditor` + `role-a11y-auditor`. ## Todos - [ ] Architect: ratify similarity thresholds + Worker load strategy - [ ] Brief 1 — migration (pg_trgm) + identify-by-text route + SCHEMA_MAP + CI schema-map-fresh fix - [ ] Brief 2 — Tesseract Worker + CameraScanner integration - [ ] Post-ship: measure `scan_attempts.layer` distribution for 70% target ## Operator action required **None.** No new secrets. Tesseract runs client-side; pg_trgm is a Postgres extension enabled via migration. ## Multitask dispatch ### Slice dependencies ```yaml slice_dependencies: - brief: 1 depends_on: [] files: - migrations/* - pages/api/cards/identify-by-text.js - docs/SCHEMA_MAP.md - .github/workflows/ci.yml - brief: 2 depends_on: [1] files: - components/CameraScanner.js - lib/ocr-worker.js - package.json ``` Serial dispatch: Brief 2 after Brief 1 (Worker calls identify-by-text route). Post-PR audit: ``` /multitask role-reviewer + role-design-system-auditor + role-a11y-auditor ``` Group id: `audit-add-real-ocr-layer-`. ## CI impact | Workflow / job | Behavior | | --- | --- | | `schema-map-fresh` | **Modified** — `if:` paths include `migrations/**`. | | `forbidden-client-side-llm-keys` | Unchanged (no new client LLM URLs). | | `preview-smoke.yml` | Fires. | | `visual-diff.yml` | **Fires** — `components/CameraScanner.js` in paths. | No new grep gate. Total added CI time: ~0 beyond existing workflows. ## Decisions to ratify (architect) 1. **Similarity thresholds** — 0.85 / 0.6 defaults from audit; tune with sample set. 2. **Tesseract language data** — bundled vs CDN fetch; impact on first-load latency. 3. **`scan_attempts.layer` values** — recommend `1 | 2` (trgm vs gemini). ## Known constraints - **`pg_trgm` on Neon** — verify extension availability on prod tier. - **Worker + Turbopack** — confirm `tesseract.js` Worker path works under Next.js 16 default bundler; fallback `--webpack` only if architect documents regression. - **Layer-1 must not block camera** — Worker runs off main thread. ## Acceptance criteria 1. Migration applies cleanly; GIN index exists on `cards.name`. 2. `identify-by-text` returns match / disambig / escalate per thresholds. 3. CameraScanner tries Layer-1 before `/api/scan/identify`. 4. `scan_attempts.layer` populated for analytics. 5. `schema-map-fresh` fires when only `migrations/` changes. 6. Lint + vitest baseline preserved. ## Out of scope follow-ups - **`improve-scan-card-detection`** — if Layer-1 hit rate < 70%. - **`god-component-split`** — CameraScanner remains large; split is P2.