Commit graph

4 commits

Author SHA1 Message Date
kiyo9w
00367a95c9 feat: a synthetic store walk, and the live tuning it forced out
/v1/demo/plan + /v1/demo/frame give a demo the one thing a still image cannot:
motion. The virtual camera pans a shelf row left to right, steps up, and pans
back - a store employee sweeping a shelf - over a real shelf photo, ~1 ms per
frame, deterministic, and wrapping so an unattended demo loops. The client drives
only `step`, so the walk's speed follows whatever frame rate it can sustain.

Three real defects came out of measuring the walk rather than a still:

- Bands were keyed by on-screen position, so a walking camera minted a new key
  every few pixels of pan and re-read the same shelf continuously. Keyed by row
  now; OCR stops chasing the window.
- The classification budget was consumed by retries of products already leaving
  the frame. Retries now skip anything near the edge, and a track gets 2 attempts.
- The pan advance was 0.22 of the window per step: a facing was in view for one
  or two frames, so nothing could ever be classified (9 labelled tracks in 20 s).
  0.06 keeps a product in view for about a second.

Measured on the walk (20 s, 1280x720 frames, one client): orb labels 296 tracks
across 21 SKUs with the first label at 0.2 s, while hybrid manages 13 tracks /
4 SKUs at 5.4 s - so the live default for a demo is the fast method, with hybrid
kept for tap-to-inspect.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 18:09:23 +09:00
kiyo9w
a27841afa4 feat: run the tag pass in parallel with recognition, in live and still modes
The engine owns the OCR backend, a shared OCR lock and a dedicated tag worker, so
a tag pass can never occupy a classification worker or slow a frame. A live
session plans rail bands on the frame loop (cheap), runs them on that worker, and
attaches whatever the cache already knows; a still runs one synchronous pass.
Tracks and detections now carry price_tag and reconciliation, and the summary
carries price_tags plus compliance. Config can turn tags off per session.

Compliance now fails closed on evidence quality. The first real run produced 106
"missing tag" violations on a shelf where only 8 tags were readable - our tag
*detection* was incomplete, not the shelf. A row is only judged when at least
half its facings carry tag evidence; otherwise it reports "tags detected for 14
of 91 facings in this row" as a hint. Missing-tag findings are one per row with a
count, not one per facing, and price_mismatch still reports every disagreement.
On add/image6.jpg the result is now 7 real price mismatches and no false
violations.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 13:58:18 +09:00
kiyo9w
5f84f4f2a7 feat: recognition server API, reference stream client, and live label memory
app.py becomes the recognition server: /v1 REST (health, catalog + template
images, samples, recognize, classify, inspect, stock) plus /v1/stream over
WebSocket, with token auth, size limits, one consistent error shape, and the
legacy /api/sessions routes kept but now backed by the real engine instead of
the fake adapter. Default bind is 127.0.0.1; LAN and TLS are opt-in.

stream_client.py is the reference non-browser client (camera, video file, or a
pan across a still) and prints results/s, RTT, detect time and label stability.

Live quality work driven by measurements:
- Labels are remembered per place: a fresh track covering a recently recognized
  product inherits the label instead of queueing for classification again. On a
  continuous pan this raised recognized products in view from 9/66 to 17-21/66
  and cut time-to-first-label to ~1.6 s.
- Classification priority is newest-first, because sorting by area spends the
  budget on products that are already leaving the frame.
- Per-session classification concurrency raised to 12.

hybrid_fast (ORB shortlist then hybrid) is implemented but NOT the default: it
is 3.4x faster (13 ms vs 44 ms p50) yet agreed with hybrid on only 47% of
accepted crops across 553 crops, far below the 98% adoption rule. It stays
available for experiments; the numbers are in the commit and the benchmark.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 12:31:17 +09:00
kiyo9w
afb59b487f feat: live recognition session (track, classify once, label)
One LiveSession per streaming client. Each frame runs detection and tracking,
then classification is scheduled for products that are new, big enough, fully in
frame and not already settled; results are cached on the track so detection
never waits for classification. Empty-slot gaps are smoothed over frames and
quality hints (glare, blur, too far) come from a 320 px downscale.

Correctness work that the acceptance run forced out:

- Classification submissions happen OUTSIDE the session lock. add_done_callback
  runs the callback inline when the future already completed, so attaching it
  while holding the lock deadlocked the session on the second frame.
- A label is dropped only when its box has drifted off the product for two
  consecutive frames (panning slides boxes onto neighbours), and the anchor is
  the box the crop came from, not wherever the track moved by the time
  classification finished.
- An id that disappears and comes back is a restart, not the same object: the
  old label is cleared instead of silently labelling a neighbour. relabels and
  restarts counters are monotonic per id so clients can tell a re-verification
  from a silent flip.
- Track ids are tracked per session (tracking.py) and counters survive pruning.

Measured on the synthetic pan (30 frames, 248 label observations): 20
re-verifications, 0 unexplained label changes, detection 20 ms/frame.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 12:21:44 +09:00