S9 (a1): slip reader — zero-API OCR
tesseract.js (self-hosted WASM, Apache-2.0) + pure per-book layout parsers (DK/FD/MGM/Caesars) with per-field confidence and needs_review honesty — the reader never guesses. POST /api/slips/parse (auth, free 1/day paid 10/day, 4MB cap) + Next proxy. Gated /slip page: upload or paste, manual-correct UI, per-leg grades through the normal engine (refusals render honestly), add-all to Parlay Lab, share card. Vision model upgrade logged post-revenue. 2574 -> 2608 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -880,6 +880,40 @@ phased plan in the Session-57 conversation / BUILD-STATE Next section).
|
||||
- Box-side ops (Uptime Kuma, Coolify deploy-failure webhook, ntfy phone
|
||||
setup) = `docs/OPS-RUNBOOK.md`.
|
||||
|
||||
## Slip Reader (Session 9, A1 board — non-obvious)
|
||||
- **`src/services/slipReader.js`** — PURE per-book OCR-text layout parsers
|
||||
(DK/FD/MGM/Caesars) + `detectBook` + `parseSlipText(text, bookHint)`. The
|
||||
NEVER-GUESS contract: every field carries a confidence; below
|
||||
`CONFIDENCE_THRESHOLD` (0.6) the field is NULL + the leg `needs_review:true`.
|
||||
Values are USER-SLIP values (`source:'user_slip'`) — never written to any
|
||||
market cache. Stat labels normalize via `STAT_ALIASES` → the scan-route
|
||||
vocabulary (a unit test cross-checks every alias against VALID_STAT_TYPES);
|
||||
players via `playerName` normalizeName/nameKey.
|
||||
- **Caesars head split is alias-anchored:** "Pete Alonso Home Runs Over 0.5"
|
||||
is regex-ambiguous (greedy name eats "Home" → stat "runs" = WRONG-stat
|
||||
fabrication). `splitPlayerStat` matches the LONGEST known stat alias the
|
||||
head ENDS with; no alias → stat null AND player low-confidence (boundary
|
||||
unknowable). BetMGM legs carry "@ -115" — don't pre-filter its lines with
|
||||
the game-row `@` heuristic; the Over/Under grammar is the filter.
|
||||
- **OCR = tesseract.js v7 (Apache-2.0), lazy-required** in `recognizeImage`
|
||||
so the parsers load without WASM. ~46MB on disk (core WASM); eng
|
||||
traineddata (~11MB) downloads ONCE at first recognize and caches at
|
||||
`TESSERACT_CACHE_PATH` (default `.tesseract-cache/`) — first prod OCR call
|
||||
needs outbound network, then it's fully offline.
|
||||
- **`POST /api/slips/parse`** (requireAuth): base64/data-URL image ≤4MB
|
||||
decoded OR raw `text` (paste path, also what hermetic route tests use).
|
||||
Daily quota `slips:{user}:{YYYY-MM-DD}` — free 1/day, paid 10/day; Redis
|
||||
counter with in-memory mirror (max of both enforces). Validation runs
|
||||
BEFORE the quota is burned. Injectable via `router.__internals.setDeps`.
|
||||
Next proxy: `web/src/app/api/slips/parse/route.ts` (S25 rule).
|
||||
- **`/slip` page** (added to GATED_ROUTES): upload/paste → editable leg rows
|
||||
(nulled fields amber; editing clears the flag) → each leg graded via the
|
||||
EXISTING `POST /api/scan` (refusals render "NO GRADE — insufficient data")
|
||||
→ `useParlay().addLeg` per graded leg → share card is copy-text (no OG
|
||||
plumbing). Sport selector is MLB/NBA/WNBA (ParlayLeg's sport union).
|
||||
- **Vision-model upgrade is POST-REVENUE** (specs/vyndr-roadmap.md) — the
|
||||
route contract is the stable interface; only the extraction engine swaps.
|
||||
|
||||
## Active Skills
|
||||
- vyndr-voice (all user-facing output)
|
||||
- prop-analysis (grading methodology)
|
||||
|
||||
Reference in New Issue
Block a user