docs: price-tag fields, compliance semantics, and the measured limits

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kiyo9w 2026-09-24 14:11:30 +09:00
parent 8f750c15c5
commit 6182f678eb

View file

@ -30,6 +30,8 @@ uv pip install --python .venv/bin/python -r requirements-dev.txt # first time
| `--classify-workers` | `5` | Classification thread pool size (measured default). |
| `--live-method` | `hybrid` | Default method for live sessions. |
| `--live-conf` | `0.1` | Detection threshold in live mode (the tracker gates). |
| `--no-tags` | off | Disable price-tag OCR entirely (`DORAYAKI_LIVE_TAGS=0`). |
| `DORAYAKI_OCR` | `auto` | OCR backend: `auto` (Vision on macOS), `vision`, `fake`, `none`. |
| `DORAYAKI_TORCH_THREADS` | `4` | torch CPU threads; 0 leaves torch's default. |
| `--max-sessions` | `4` | Concurrent live sessions. |
| `--ssl-certfile` / `--ssl-keyfile` | – | HTTPS (needed for a phone camera). |
@ -153,6 +155,45 @@ with a `file` field.
before identities arrive. The `image_id` stays valid for 15 minutes for
`classify`/`inspect`.
### Price tags (OCR)
When an OCR backend is available (macOS Vision; see the design doc), a still can
carry tag evidence. `?tags=true` runs the tag pass synchronously (measured ~0.9 s
for a six-band shelf) and adds, per detection:
```json
"price_tag": {"id": "tag_3f2a", "box": [91, 657, 250, 744], "state": "readable",
"price": 357, "regular_price": null, "discount": 0.24, "unit_price": 290.52,
"name": "ゼロノンコレステロール", "flags": ["sale", "discounted"],
"confidence": 0.82, "observations": 2, "settled": false},
"reconciliation": {"state": "confirmed | mismatch | unverified | no_tag | tag_only",
"signals": [{"kind": "price", "agrees": false, "item": 168, "tag": 269}],
"message": "tag says 269, catalog says 168"}
```
`price_tag.state` is `readable` (text parsed), `unreadable` (a tag was found but
its text was not read) or `absent` (no tag under this facing). The summary gains
`price_tags` counts and `compliance`:
```json
"price_tags": {"readable": 8, "unreadable": 18, "absent": 85},
"compliance": {"violations": 7, "by_rule": {"price_mismatch": 7},
"rows": [{"row_index": 2, "rule": "price_mismatch", "box": [...], "missing": 1}],
"hints": [{"rule": "unreadable_tags", "detail": "tags detected for 5 of 16 facings in this row"}]}
```
**Compliance fails closed on evidence quality.** A row is only judged when at
least 70% of its facings carry tag evidence (readable or unreadable); otherwise it
produces a hint such as `move closer: the price tags in this row could not be
read`, never a violation. Findings must also persist for 3 of the last 5 frames
before they publish. This is deliberate: the first implementation reported 106
missing-tag violations on a shelf where only 8 tags were readable, which was our
detection being incomplete, not the shelf.
`POST /v1/images/{image_id}/classify` re-reconciles against the cached tag bundle
and returns `reconciliation` (and `price_tag`) on each identity row, because in
photo mode the tags are read before the identities exist.
### `POST /v1/images/{image_id}/classify`
```json
@ -283,6 +324,21 @@ The slide's "約28~30ms per image" matches the measured **detection** time (28.7
the full detect-and-classify of a whole 170-product shelf is ~2 s because each product
is matched against the catalog separately.
## Price-tag OCR: what it can and cannot do (measured)
- **Capture distance decides everything.** At `add/image6.jpg`'s ~1 m (tags ~300x80 px,
digits ~22 px) macOS Vision reads a tag's printed fields reliably and the tag pass
finds 8 readable tags with names and prices in ~0.9 s. At `image.jpg`'s ~2 m (digits
~14 px) it reads none of them - the pass correctly reports nothing readable.
- **Tag detection is the weak link, not the OCR.** On a shelf with ~100 facings the
pass currently finds tags for a minority of them at this distance; the compliance
hints say so instead of inventing violations.
- **A single read can misread a digit.** The vote needs independent reads (live mode
accumulates them across frames; stills read each band twice at different upscales),
and a dead heat stays unread rather than guessing.
- **The demo catalog is fictional**, so tag prices legitimately disagree with it. That
is shown as `mismatch` with both values; it is not an error.
## Known limitations
- **No session totals.** Facings are counted only for products currently in view. A