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:
@@ -0,0 +1,110 @@
|
||||
# A1 Session 9 — Slip Reader (zero-API OCR)
|
||||
|
||||
## What
|
||||
Upload a sportsbook bet-slip screenshot. VYNDR reads it (Tesseract OCR,
|
||||
self-hosted, free), extracts the legs, lets the user correct anything the
|
||||
OCR was unsure about, grades each leg through the normal engine (refusals
|
||||
apply), and hands the legs to the Parlay Lab. "VYNDR read my slip."
|
||||
|
||||
## Zero out-of-pocket
|
||||
- OCR = `tesseract.js` (npm, Apache-2.0). Bundles the WASM core — no
|
||||
external OCR service, no per-call cost. English traineddata (~11MB) is
|
||||
fetched once on first recognize and cached on disk (`.tesseract-cache/`).
|
||||
- No other new dependencies. Image decode for the acceptance harness uses
|
||||
the existing `sharp` dependency.
|
||||
|
||||
## Data semantics
|
||||
Extracted values are USER-SLIP values — the line/odds the user's book
|
||||
printed on their slip, labeled as such in the UI. They are never written
|
||||
into any market cache. Grading goes through the normal engine
|
||||
(`POST /api/scan` per leg); insufficient-data refusals render honestly.
|
||||
NEVER guess: a field the parser cannot read above threshold is returned
|
||||
`null` and the leg carries `needs_review: true`. Absent beats wrong.
|
||||
|
||||
## Endpoints
|
||||
### POST /api/slips/parse (Express) — auth required (`requireAuth`)
|
||||
Body (JSON):
|
||||
- `image`: base64 or data-URL image (≤ 4MB decoded), OR
|
||||
- `text`: raw OCR/slip text (skips OCR — used by tests + paste-text path)
|
||||
- `book` (optional hint): `draftkings | fanduel | betmgm | caesars`
|
||||
|
||||
Response 200:
|
||||
```json
|
||||
{
|
||||
"book": "draftkings",
|
||||
"legs": [
|
||||
{
|
||||
"player": "Aaron Judge", "player_key": "aaron judge",
|
||||
"stat": "total_bases", "stat_label": "Total Bases",
|
||||
"line": 1.5, "side": "over", "odds": -115,
|
||||
"confidence": { "player": 0.9, "stat": 0.95, "line": 0.95, "side": 0.95, "odds": 0.9 },
|
||||
"needs_review": false
|
||||
}
|
||||
],
|
||||
"needs_review": false,
|
||||
"source": "user_slip"
|
||||
}
|
||||
```
|
||||
Errors: 400 (no image/text, image too large, undecodable), 401 (no auth),
|
||||
429 (daily limit), 503 (OCR engine unavailable).
|
||||
|
||||
Rate limit: Redis counter `slips:{userId}:{YYYY-MM-DD}` (UTC day, 48h TTL),
|
||||
free = 1/day, paid (analyst/desk) = 10/day. In-memory fallback when Redis
|
||||
is degraded. 429 carries `used`/`limit`.
|
||||
|
||||
### Next proxy
|
||||
`web/src/app/api/slips/parse/route.ts` → `${BACKEND_URL}/api/slips/parse`,
|
||||
forwards the Authorization header (S25 rule).
|
||||
|
||||
## Parser design (`src/services/slipReader.js`)
|
||||
- PURE text parsers per book layout: `parseDraftKings`, `parseFanDuel`,
|
||||
`parseBetMGM`, `parseCaesars` + `detectBook(text)` + `parseSlipText(text,
|
||||
bookHint)`. Unit-tested on realistic OCR-text fixtures. No I/O.
|
||||
- Stat names normalize through `STAT_ALIASES` → the existing stat
|
||||
vocabulary (`VALID_STAT_TYPES` in src/routes/scan.js / STAT_LABELS in
|
||||
web/src/lib/gradeAdapter.js). Unknown stat → `stat: null`,
|
||||
`needs_review: true`.
|
||||
- Player names normalize through `src/utils/playerName` (`normalizeName`
|
||||
display + `nameKey`).
|
||||
- Side grammar per book: `Over 1.5` / `Under 1.5`, FanDuel `To Record 2+
|
||||
Total Bases` (2+ → line 1.5, side over), `Alt` prefixes stripped.
|
||||
- Per-field confidence in [0,1]; `CONFIDENCE_THRESHOLD = 0.6`. A field
|
||||
below threshold is nulled and flags the leg `needs_review`.
|
||||
- `recognizeImage(buffer)` lazy-requires tesseract.js (parsers stay
|
||||
loadable in tests without WASM).
|
||||
|
||||
## Frontend `/slip` (gated route)
|
||||
- Added to `GATED_ROUTES` in web/src/lib/routes.js.
|
||||
- Upload or paste a screenshot → POST /api/slips/parse → editable leg rows
|
||||
(each field pre-filled; needs_review fields amber). "User-slip values"
|
||||
label on the extracted numbers.
|
||||
- Confirm → each leg graded via existing `POST /api/scan` (per leg);
|
||||
refusals render as "NO GRADE — insufficient data".
|
||||
- "Add all to Parlay Lab" → `useParlay().addLeg` per graded leg.
|
||||
- Shareable result card ("VYNDR read my slip") — DOM card in existing
|
||||
VYNDR card styling + copy-to-clipboard share text. No new OG plumbing.
|
||||
|
||||
## Acceptance criteria
|
||||
1. DK/FD/MGM/Caesars OCR-text fixtures parse to correct legs (player,
|
||||
stat, line, side, odds) with confidences; garbage text → no legs, never
|
||||
fabricated ones.
|
||||
2. A leg with an unreadable field returns that field null +
|
||||
`needs_review: true`.
|
||||
3. Route: 401 unauthenticated; free tier blocked at 2nd parse in a day
|
||||
(429); paid tier allows 10.
|
||||
4. End-to-end: DK fixture → parse → normalized legs → the exact
|
||||
`POST /api/scan` request shape (`{sport, player, stat, line,
|
||||
direction}`). If a synthetic PNG can be OCR'd in this environment
|
||||
(SVG→PNG via sharp → tesseract.js), run the image path too; otherwise
|
||||
the parser acceptance stands, honestly labeled.
|
||||
5. Full jest suite green; `web` build exit 0.
|
||||
|
||||
## Test plan
|
||||
- `tests/unit/slipReader.test.js` — per-book fixtures, alias
|
||||
normalization, threshold/needs_review, never-guess, detectBook.
|
||||
- `tests/integration/slipsRoute.test.js` — auth 401, text-path parse,
|
||||
free 1/day + paid 10/day limits, 4MB cap, bad payloads.
|
||||
|
||||
## Post-revenue upgrade
|
||||
Vision-model slip reading (Claude/GPT-4V-class) replaces layout parsers —
|
||||
logged in specs/vyndr-roadmap.md as a post-revenue item.
|
||||
Reference in New Issue
Block a user