PR #24 merged: Async batch OCR: POST /ocr/batch for roll packs (#14)
Closes #14.
What
POST /ocr/batch — OCR many crops in one async job instead of forcing the client to fire N strictly-sequential /ocr calls (each ~8s) and hand-throttle around the LimitInflight 429s.
Mirror /transcribe:
POST /ocr/batchwith{ "crops": [ {"box": [x,y,w,h], "image": "<base64>"}, … ] }(1–200 crops) →202job.- Poll
GET /jobs/{id}untilcompleted. result.resultsis an array (submission order) of{box, text, error}—boxechoed back verbatim so you can map each result to its source;erroris non-null only for crops that failed (one bad crop never sinks the pack).
Why it satisfies "drain within the inflight cap"
The submit is cheap (persist + queue), so it isn't under LimitInflight. The actual OCR runs in OcrBatchJob on the single queue worker, and the vision sidecar is serial — so crops OCR one at a time and the batch never blows the cap the way client-side fan-out does.
Notable
/jobs/{id}is now type-aware. It previously hard-coded the transcribeverbose_jsonshape; a batch result would have been mangled. ExtractedJobPresenter(branches ontype) and moved polling to a genericJobController./transcribeoutput is unchanged.- Crops are validated + persisted up front — a malformed crop is a fast
422before any job is queued. - README capability table updated (also adds the
/ocr+/detectrows that shipped earlier but were never documented).
Tests
tests/Feature/OcrBatchTest.php: queues+202 envelope, per-crop {box,text} mapping & temp cleanup, per-crop error isolation, type-aware polling, validation (empty/oversized/invalid-base64), auth.
Follow-ups (not in scope)
- The OCR engine itself (#13) — benchmarked separately; recommendation incoming. This PR is transport only; it'll pick up
engine=for free once #13 lands. - Optional hardening: retry only the failed crops rather than relying on per-crop error capture.
🤖 Generated with Claude Code