Commit graph

98 commits

Author SHA1 Message Date
fc49ba60e6 Cache only text reads that could name a product
The per-track OCR cache kept any non-empty read, so a first crop that read
only a size ("400g") was replayed on every later attempt. A size matches every
400g product's size line, so the track could never be named even after a clean
crop arrived: the cached read replaced it. Only a read carrying identity text
is cached now, so the next attempt gets a fresh OCR.

`carries_identity` moves to module scope in live.py; a function-local import
shadowed the name at the cache site and raised UnboundLocalError.
2026-09-29 00:41:52 +09:00
b6c7c4b667 Score package text against the row's name, not its packshot's OCR
The text lane built each row's signature from its printed name plus the OCR of
its enrolled packshot, and scored a crop against that whole string. l09's
packshot prints "マヨネーズ", which is l10's actual name, so l09 could explain
an l10 crop's text better than l10's own name could. Every l10 facing reads
"ピュア セレクト マヨネーズ" with no "コクうま" and no "65", and l09 still won
or tied them.

The signature is now the row's name alone, weighted by inverse document
frequency over the catalog so copy every sibling prints counts for little and
the words that name one product dominate. The packshot OCR is gone: every row
has a name, so it added nothing but the confusion.

Measured on image6, local lane at live resolution:

  l10 correct    2/11 -> 5/11
  wrong          3 -> 1
  precision      95% -> 97%
  l09 hub share  34% (alarm) -> 29% (clear)

Full resolution end-to-end 59% at 88% precision, live 56% at 97%.
2026-09-29 00:37:56 +09:00
8ab17ccd5d Fix label frame mapping and reference upscaling; add the stream G1 harness
Three defects, all found by scoring the live path against ground truth.

Inherited labels carried frame id 0. `_inherit_labels` set `label_box` but
never `label_frame_id`, so every inherited label was mapped back to the source
image with the first frame's offset. During a pan that puts the box a whole
pan-distance from the product it names, which is why most streamed tracks
matched no labelled facing. It now stamps the current frame and anchors to the
raw detection box rather than the smoothed track box, which lags a pan.

The reference lane upscaled views smaller than a pyramid level. `sku_l09.jpg`
is 407x305, so its 400 px level was a 305->400 blow-up: a blur with no
high-frequency detail to disagree with, which won 31 of 54 crops on the shelf
it was cut from. A view is now used only at the levels it actually has.

`score_stream.py` measured the wrong thing. It counted any facing touching the
window by one pixel (23 of 47 "in-catalog" facings were less than half
visible), scored a correct text-lane answer as a false accept because it
checked enrollment before correctness, and counted tracks rather than facings,
which reported 138% coverage of a 24-facing window. It now takes the union of
the window rects the client showed, keeps facings at least half inside, checks
correctness first, and reports facings named separately from labels correct.

Measured on add/image6.jpg, stock-lopia-clean, 20 s per run:

  pan  backend   facings named   precision
  0    van        11/24 (46%)     52%
  0    local      11/24 (46%)    100%
  2    van        12/24 (50%)     45%
  2    local      18/24 (75%)    100%
  12   van        11/24 (46%)     28%
  12   local      16/24 (67%)     83%

eval_identity on the same catalog is unchanged by the upscale fix (local 61%
end-to-end at 88% precision full, 59% at 95% live), and 1 of 602 distractor
crops from the 11 out-of-catalog shelf photos reaches 10 inliers, none reach 20.
2026-09-29 00:30:17 +09:00
ff11529476 fix: cut whole quantity tokens, and gate the fill-in by default
Some checks failed
tests / model-free (pull_request) Has been cancelled
Review 2, item 2. The first version of this gate counted bigrams with a
non-digit character, which let the unit's own letters through: "400g 50%"
left "0G" and "0%", "450g カロリー50%" left "0%", and "500ml" left
"0M". All three still named products.

The quantity token is now cut whole - digits plus unit, in either script -
before counting, and the floor is three bigrams rather than two, because
"カロリ" is two and it is a descriptor every low-calorie package prints.

DecisionPolicy.decide gated the fill-in only when a read was passed, so the
unsafe behaviour was the default. Omitting the read now means no fill-in: a
caller with a ranking and no read has not shown the text can name anything.
TextLane.carries_identity was a dead duplicate of the module function; deleted.

Tests: the five cases from the review plus the default-gate case. Suite 284 ->
286. Evaluator unchanged: local 61%/77% full, 59%/95% live.
2026-09-28 23:46:03 +09:00
80e69e74df feat: refuse a label file whose rows carry no provenance
Some checks failed
tests / model-free (pull_request) Has been cancelled
F-01's target was 'every scored label carries provenance', and the fields
existed but nothing enforced them: a v1-style file with no 'source' loaded
exactly like the audited one, which is how 23% wrong labels went unnoticed.

load_labels now refuses such a file by name and index. The v2 labels load
unchanged; a stripped copy is refused. 3 tests. Suite 281 -> 284.
2026-09-28 23:31:53 +09:00
b4d7106d28 docs: record the OCR cache and the two review fixes
Some checks failed
tests / model-free (pull_request) Has been cancelled
The pacing paragraph described a defect that no longer exists. Replaced with
the measured before/after, and added the two review findings (the missing
always-argmax baseline lane, the size-token fill-in) with their test counts.
2026-09-28 23:27:29 +09:00
5f6de4d48a perf: read each package once per track, not once per attempt
Some checks failed
tests / model-free (pull_request) Has been cancelled
Vision took a crop from 5 ms to 25 ms, so re-reading the same package on every
attempt was the measured cause of the live coverage collapse: 789 -> 60 crops/s
at 8 workers, and 12/8/2/6 labels on the same clip at four pacings.

The reading is now cached on the track and later attempts re-run only the local
lane. Measured through the real server on add/image6.jpg:

  saturated pool   15.9 -> 49.4 crops/s (8 workers, 64 crops)
  per crop         142 -> 92 ms on a 300x400 crop
  3 s clip, send   19.7 mean labels (15-24, unstable) -> 24.0 (23-25, stable)
  12 s clip, send  48/131 -> 53/131; result pacing 53/134 -> 50/135

The 12 s numbers are within run-to-run noise; the short clip is where the
pacing sensitivity lived and it is now flat across pacings.

An empty read is not cached - a crop too small or too blurry to read says
nothing about the next one - and the cache is cleared with the rest of the
evidence on a method change or a drift relabel, so a reading never outlives the
product it described. The native crop lane skips OCR on the same rule.

Test: OCR runs once per track while the local lane keeps running. Disabling the
skip makes it fail 2 != 1. Suite 280 -> 281.
2026-09-28 23:25:09 +09:00
fd72251160 feat: add Van's real baseline lane and stop size tokens naming products
Some checks failed
tests / model-free (pull_request) Has been cancelled
Two review findings, both confirmed by measurement.

1. The evaluator had no always-argmax lane, so every "vs Van" claim compared
   against the *gated* matcher, which is not Van's contract - the original demo
   answered every crop with its best-scoring SKU and had no accept line. The new
   'van' lane is that contract: same scores, no gate. Against it the local lane
   wins both axes at both resolutions (full 61%/77% vs 52%/29%; live 59%/95% vs
   57%/32%), but the live coverage margin is one facing, not seven, and the 95%
   figure is live-resolution only - at full resolution the same lane reads 77%
   with 11 false accepts and a hub alarm.

2. A crop reading only "400g" scored 0.667 on three SKUs, above TEXT_ACCEPT, so
   the text lane named a product from a size line. The fill-in job now requires
   two non-digit bigrams; the veto is unchanged, so "65%" still contradicts a
   local answer. The raw OCR read travels with the ranking through
   score_crop_lanes for that check.

Tests: 3 for the size gate (policy + live) and the van lane's own row. Removing
the gate fails all three. Suite 276 -> 280.
2026-09-28 23:12:44 +09:00
13ee7426e2 fix: read --shelf-tags before the catalog swap
Some checks failed
tests / model-free (pull_request) Has been cancelled
load_shelf_skus ran after write_catalog, so a missing path or a malformed list
escaped as a raw traceback over a catalog that had already been replaced. It is
now read inside the try, before anything is written, so both cases fail with the
tool's own message and the destination is untouched.

Adds BadShelfListFailsBeforeTheSwapTests, which asserts the clean message and
that the output path does not exist. Re-injecting the ordering fails both tests
with the raw FileNotFoundError. Suite 274 -> 276.
2026-09-28 23:03:08 +09:00
62183295f1 docs: correct the record's test counts to 274
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:47:06 +09:00
5579e21187 docs: record the pyramid floor check
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:45:30 +09:00
200328527e feat: report views below the enrollment pyramid floor
Some checks failed
tests / model-free (pull_request) Has been cancelled
The protocol asks for a source at least 800px tall so a view can be stored as a
200/400/800px pyramid. Nothing enforced it, and the clean catalog's l09 view is
305px - the documented defect where its inlier scores dwarf every other
reference (85-129 against 10-40) and it wins facings it should not.

Reported, not refused: the view is usable and dropping it would remove the SKU
from the catalog entirely, which is worse than an incomparable score. The fix is
re-sourcing the packshot, which is a data task. The check names the view and its
height so the re-sourcing has a target. Suite 272 -> 274.
2026-09-28 22:45:25 +09:00
b38c66aa7b docs: record the enrollment protocol's reporting checks as done
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:44:32 +09:00
e53c1ef59b feat: finish the enrollment protocol's reporting checks
Some checks failed
tests / model-free (pull_request) Has been cancelled
Three items from DECISION §8's enrollment protocol were unimplemented and none
of them needed Tier-2 data:

- Eval reports now record provenance: catalog hash, label-file hash, image hash
  and the code revision, so a number can be traced to the inputs that produced
  it. The catalog hash covers the CSV bytes and the view names, not image bytes:
  a re-encoded view of the same product is the same catalog.
- rebuild_catalog flags cross-SKU near-duplicates as text-discriminated
  siblings. These are the near-unknown case no threshold rejects - the geometry
  is real because the sibling shares the brand's artwork - so they are reported
  rather than refused.
- --shelf-tags turns the completeness rule into a live-ready verdict. Without
  the shelf's SKU list the verdict is 'unknown', not a guess.

Adds test_rebuild_catalog.py (11 tests; the tool had no test file). Suite 261
-> 272. Also corrects score_crop_lanes' docstring, which still described the
live path as averaging local scores when the local scale decides on the newest
crop.
2026-09-28 22:44:07 +09:00
7fff0cf724 docs: record the live pacing dependence and the new test coverage
Some checks failed
tests / model-free (pull_request) Has been cancelled
The live label count is a function of frame pacing, not a fixed property: a
track needs two attempts to settle, so pushing frames faster than the classifier
drains them reports fewer labels (12/8/2/6 at full speed on the same clip, 14
paced, 38 of 30 frames). Precision stays 100% and 0 unknowns are accepted at
every pacing. Also records the local-scale and DecisionPolicy tests.
2026-09-28 22:41:39 +09:00
890e186182 test: cover the local scale branch and the decision policy
Some checks failed
tests / model-free (pull_request) Has been cancelled
The shipped default scale had no test: make_session only builds matcher-backend
engines, so scale == "local" in _on_classified never ran in the suite - and
that is the branch that was wrong twice (the rank re-sort, and the early return
that made the text lane's fill-in job unreachable).

Adds LocalScaleEngine, which injects the lanes' evidence into the live path, and
two live tests: a text-only read still labels the track, and a text veto blocks a
local accept. Adds DecisionPolicyTests for the veto/fill-in boundary, including
the lane-order-vs-value case that the rank re-sort broke.

Both regressions were re-injected to confirm the tests fail for the intended
reason: restoring the early return fails the fill-in test, and re-sorting the
lane output by value fails the order test. Suite 251 -> 261.
2026-09-28 22:41:24 +09:00
2b7dd0c193 docs: match the false-accept record to the evaluator's own columns
Some checks failed
tests / model-free (pull_request) Has been cancelled
The record said '2 false accepts of 48 unknowns', which conflated two fields:
the 2 are 0 unknown accepts plus 1 unenrolled in-catalog facing and 1 enrolled
misidentification. Also restores the hybrid-verify line the lane vocabulary
edit dropped, and reframes the l09 note: its hubness ratio is 1.76 full /
1.36 live against G7's limit of 2.0, so the gate passes and the 30% internal
share heuristic is what raises the alarm.
2026-09-28 22:36:27 +09:00
47c4273eec docs: record the measured l09 hub share at full resolution
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:35:40 +09:00
3c0df81629 docs: name the local lane in the evaluators' own lane vocabulary
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:34:06 +09:00
4aa1b35e39 docs: cite the repo command that reproduces the local-lane numbers
Some checks failed
tests / model-free (pull_request) Has been cancelled
2026-09-28 22:33:37 +09:00
ff1ed6ebc5 feat(eval): score the shipped local lane, and harden catalog enrollment
The default backend could not be scored by the repo's own evaluator, so the
number that justifies the architecture lived only in an ad-hoc script.

- eval_identity.py: `--lanes local` wraps identity_lanes.LocalLane and
  TextLane behind the same four ceilings as every other lane. `Lane.decide`
  is now the seam: the default runs the shared gate over the lane's ranking,
  and the fused local+text lane overrides it, so the production decision is
  what gets scored rather than a re-implementation of it.
- rebuild_catalog.py: one-directional image/CSV match (an enrolled image with
  no row refuses; a row with no image is a text-only coverage gap), packshot
  OCR against the catalog name, capture-session refusal from an evaluation
  manifest, and the 160 px floor. The CSV is copied byte-for-byte so all 12
  rows survive a rebuild, and unmanaged files are carried over.

Reproduced from the repo, live resolution: 59% end-to-end at 95% precision,
2 false accepts of 48 unknowns, 20.6 ms per crop.
2026-09-28 22:33:33 +09:00
aaed5d388f feat(identity): local-feature matcher plus text lane as the default backend
Replaces the DINO gallery as the recognition path with the measured design
from docs/research/2026-09-28-recognition-architecture/DECISION.md: RootSIFT
over a per-SKU reference pyramid, similarity-constrained RANSAC and a hue
check, fused with an OCR text lane that vetoes a match whose package text
names a different catalog row and answers on its own when geometry finds
nothing.

- identity_lanes.py: LocalLane, TextLane, DecisionPolicy. The text lane
  builds each row's signature from the catalog name plus its packshot OCR, so
  a row with no enrolled image is still answerable.
- engine.py: every backend groups scores by canonical SKU, so no
  view-suffixed id reaches the wire and a name and price always resolve.
  decide_crop fuses the lanes; score_crop_lanes feeds the live path. Both
  lanes are built in warm_up (non-fatally) so the first frame is not the slow
  one, and the text lane shares the engine's OCR lock.
- live.py: decides per crop on the lane's own rank order. The lane's order
  comes from its rank key and its value is only the gate, so re-sorting by
  value named a different SKU than the lane chose; averaging in an empty
  observation also diluted a good confidence below the accept line.
- identity.py: a `local` score scale whose gate is one absolute line, because
  the value is already a margin over the runner-up.
- app.py, run-demo-server.sh: `--identity local` is the default; the demo
  server no longer starts on the leaked `stock-lopia` catalog.
- eval_live.py: the replay vocabulary accepts `local`, so a recording from
  the shipped server can be scored.
- README.md: documents the three backends and what each is for.

Measured on the corrected image6 labels at live resolution: 36/61 in-catalog
facings named at 95% precision, 0 of 48 unknowns accepted, 11 labels sourced
from the text lane alone. Through the real server: /v1/recognize 90%
precision, /v1/stream 100% precision, no view-suffixed ids on either.
2026-09-28 22:27:21 +09:00
kiyo9w
25dbf132e7 docs: describe the mobile lane as shipped, and fix the capabilities example
Some checks failed
tests / model-free (pull_request) Has been cancelled
Two things in API.md described a state that no longer exists.

The mobile section called the app's scan screen "a timer-driven placeholder
today" and said wiring it "is a separate lane". The app ships the real client
(`data/shelf/LiveScanClient.kt`, `platform/CameraFrames`) and ADR-002 governs
it, so the section now names what the client actually is and what it needs from
this server, including the negotiated crop lane.

The control-message table showed `{"type":"capabilities","max_crop_bytes":N}`
as the opt-in. `handle_capabilities` refuses exactly that message - it requires
`"crop": true` - so the documented example returned `BAD_MESSAGE` and left the
lane off. Verified against a running server: the corrected message answers
`capabilities_ack`, the old one answers `BAD_MESSAGE`.
2026-09-28 19:10:31 +09:00
kiyo9w
0e00f0b932 docs: record the measured live accepted share
Some checks failed
tests / model-free (pull_request) Has been cancelled
The cosine-gate fix shipped without its end-to-end number. Measured on the same
walk (`stream_client.py --source add/image6.jpg --pan 12 --seconds 20 --pace
result`, store stock-lopia): 16 of 261 tracks labelled and 2 of 30 facings
recognized before, 83 of 255 and 6 of 31 after. The pre-fix figure was taken by
restoring the old 0.60/0.50 gate in place, measuring, and reverting.

The remaining unconfirmed tracks are products the 12-SKU catalog has no view
for, not refusals.
2026-09-28 18:01:44 +09:00
kiyo9w
eca03e0c91 refactor: delete the legacy session model
Some checks failed
tests / model-free (pull_request) Has been cancelled
Two session models shared one process. `/v1/*` and `/v1/stream` are the live
one; `/api/sessions*` and `/api/compare_live_vs_still` were the earlier model,
still constructed on every `create_app()` through `build_legacy_scans` even
though nothing called them. The web client and `stream_client.py` speak only
`/v1`.

Deleted: `observe.py` (ScanService/PerceptionService/PerceptionAdapter),
`yolo_adapter.py` (imported only by the legacy builder), `pipeline.py` and
`test_suite.py` (the old single-image CLI, reachable only from each other),
`test_observe.py` (its Session* tests), the eight legacy route handlers,
`build_legacy_scans` with its two facades, `create_fastapi_app`, and the
`scans` parameter on `create_app`.

`IdentityRankingTests` moved to `test_identity.py` before `test_observe.py` was
deleted: those five ranking rules are the live path's contract, not the legacy
model's. `test_api.py` keeps every `/v1` and WebSocket test.

Verified: 246 tests OK (2 skipped), and a route dump of a freshly built app
shows no `/api/*` path with all nine `/v1` routes and the WebSocket intact.
2026-09-28 17:47:50 +09:00
kiyo9w
004212a8cf fix: gate the live cosine aggregate at the gallery's own line
Some checks failed
tests / model-free (pull_request) Has been cancelled
The previous commit stopped the live path re-gating gallery scores at
COSINE_ACCEPT, but it passed zero accept lines. `aggregate_votes` divides by the
whole observation window, so one rejected crop beside one accepted 0.45 averages
to 0.225 - and a zero gate labelled that, at a score the gallery itself calls
unknown.

The aggregate now clears the gallery's own line: `head_threshold` (0.40) when a
fine-tuned head is loaded, `threshold` (0.55) otherwise. The margin gate is
still the only extra gate, so a head match between 0.40 and 0.60 is labelled.

Regression test: a rejected crop followed by an accepted 0.45 must not label the
track (fails on the zero-gate version).

Verified: `python -m unittest discover -s code -p 'test_*.py'` - 258 tests, OK,
2 skipped.
2026-09-28 17:36:43 +09:00
kiyo9w
84ca113b90 fix: stop re-gating gallery scores in the live path
Some checks failed
tests / model-free (pull_request) Has been cancelled
`score_crop` returns only scores the gallery already accepted: the raw 0.55
line, or the fine-tuned head's own 0.40. The live path then ranked that
aggregate with `decide_identity(scale="cosine")`, whose defaults rewrite to
COSINE_ACCEPT 0.60 / COSINE_AMBIGUOUS 0.50 - a second, higher line. Every head
match between 0.40 and 0.60 was therefore refused or downgraded to ambiguous,
which is the live regression: the head's own training views score 0.57-0.77.

`engine.inspect` and `_classify_with_gallery` already avoid this by ranking
through `decision_for_match`, which applies only the published margin. The live
path now does the same: on the cosine scale it passes explicit zero accept
lines, so the margin gate is the only extra gate.

Regression tests: a 0.45 head-scale match is labelled (fails on the previous
code), and a 0.45/0.44 pair inside the margin stays unnamed.

Verified: `python -m unittest discover -s code -p 'test_*.py'` - 257 tests, OK,
2 skipped.
2026-09-28 17:34:47 +09:00
kiyo9w
1bb1b7158b chore: run the Python suite from the repository root
Some checks failed
tests / model-free (pull_request) Has been cancelled
The workflow sat at `recognization/.github/workflows/`, but Actions only reads
`.github/workflows` at the repository root, so it never ran. Moved unchanged;
it already sets `working-directory: recognization`.

Verified locally with the same command the workflow runs:
`python -m unittest discover -s code -p 'test_*.py'` - 255 tests, OK, 2
real-model skips.
2026-09-28 17:26:58 +09:00
kiyo9w
3614b23a2c fix: serialize gallery inference so Metal cannot be entered twice
The server segfaulted a few seconds into every live scan that used the crop
lane. The crash report put the fault in libtorch's Metal shader library
(`MetalShaderLibrary::exec_unary_kernel` inside an unordered_map insert),
reached from `copy_cast_kernel_mps` under `clone`/`reshape`.

Reproduced deterministically: eight threads calling `embed()` on a fresh
process crash it, while the same eight threads after one warm forward are
fine. Metal's shader library is process-global and is not thread-safe while
kernels are still being compiled into it, which is exactly what the first
forward does. The detector already serializes its predict for this reason;
the gallery did not.

Two changes:

* A module-level `_MPS_INFER_LOCK` covers the whole MPS region of a forward -
  model load, `.to(device)`, the forward, and the normalize that follows it.
  Normalize dispatches its own Metal kernels, so leaving it outside the lock
  left the same first-compile race open. Module-level because the state being
  protected is the process's, not one gallery's.
* A cache hit skips the embedding forward, so the model used to be built and
  the kernels compiled by whichever classify worker asked first - up to eight
  racing into one process-wide cache. The load path now warms the backbone
  single-threaded under the existing lock, on the cache-hit branch too. The
  readiness path loads the gallery before the socket accepts, so the warmup
  is free. It is Metal-only and best-effort: a CPU gallery has no shader
  library to compile, and a cache hit is served without a usable backbone by
  contract.

The regression test records overlap rather than timing: a fake backbone that
sleeps and counts concurrent entries, run from four threads, asserting the
count never exceeds one. It fails on the previous gallery.py and needs no MPS.
2026-09-28 17:22:55 +09:00
kiyo9w
aa25f99699 fix: bound and gate the crop evidence lane
The crop lane asked the phone for a full-resolution crop of a facing the
frame could not decide. Four defects made that unsafe or useless:

* A capability was half-applied. `crop: true` without a usable
  `max_crop_bytes` enabled the lane on the client's say-so, leaving the two
  sides believing different limits. A capability is now believed whole or
  refused with BAD_MESSAGE, and the limit must be an integer in 1..1048576
  (bool is an int in Python and is not a limit).
* A small JPEG could claim enormous dimensions. The header is now read
  without decoding and anything over 4 megapixels is refused before the
  decode allocates it.
* A request was single-use, so an expired one stranded a facing only the
  crop lane could decide. The lane now spends the same max_attempts budget
  the frame path spends, refilled on method change and on settled-label
  drift, and a phone that never answers still cannot loop.
* A candidate could be asked about from a box the frame path would refuse:
  clipped by the frame edge, carried over from an earlier frame, or measured
  at a score the tracker does not trust. Both lanes now share one edge gate,
  and only a track whose box this frame actually measured is asked about.

Drift and method change also cleared `identity` but left `confirmed = True`,
which permanently blocked both the crop lane and label inheritance for that
track; both now clear it, without which the budget reset was dead code.

Blur and glare are refused as whole-capture faults, while `too_far` is
exempt on purpose: a distant shelf is the one frame a native-resolution crop
exists to decide.
2026-09-28 17:22:52 +09:00
kiyo9w
02e69acac7 fix: require cached gallery rows to keep the enrolled order
A cache key proves the store, not the rows inside the file. The old check
asked only that every cached row appear somewhere in the enrolled list, so a
row moved across SKUs still passed: the matrix that travelled with it would
then be filed under the wrong product, and a valid key would rename a SKU.

Rows now walk the enrolled list forward and never backwards, so a moved,
repeated or foreign row has nowhere left to match. Views that could not be
decoded stay absent without disturbing the order of the ones after them,
which is what keeps a shorter cache a hit rather than a re-embed.
2026-09-28 17:22:46 +09:00
kiyo9w
13a4e0f475 fix: reject crop evidence after track drift 2026-09-28 16:11:37 +09:00
kiyo9w
2de5740922 docs: describe optional crop protocol and gallery cache 2026-09-28 15:49:42 +09:00
kiyo9w
0d0e9f82b9 feat: add bounded crop evidence and keyed gallery cache 2026-09-28 15:49:34 +09:00
kiyo9w
69de9ea418 docs: require explicit backend for legacy walks 2026-09-28 14:42:12 +09:00
kiyo9w
6cad638c26 fix: record and require the walk identity backend 2026-09-28 14:42:03 +09:00
kiyo9w
ac315eb48e fix: refuse unavailable gallery and pin head trainer 2026-09-28 14:41:57 +09:00
kiyo9w
d5a7a56dff fix: bind tag projection to each frame origin 2026-09-28 14:41:49 +09:00
kiyo9w
05cf32e1d9 docs: describe cross-visit gallery replay 2026-09-28 14:17:26 +09:00
kiyo9w
44badcf193 fix: replay recorded walks with selected identity backend 2026-09-28 14:17:14 +09:00
kiyo9w
2798322417 fix: constrain trained head labels to enrolled catalog 2026-09-28 14:07:02 +09:00
kiyo9w
b937be8e30 perf: await pending frames without idle worker 2026-09-28 14:06:57 +09:00
kiyo9w
1064e356de perf: reuse sample dimensions and catalog digest 2026-09-28 14:06:34 +09:00
kiyo9w
7142f10cfe chore: remove stale gallery import 2026-09-28 13:44:46 +09:00
kiyo9w
19af7ce545 refactor: keep tag state in streamed track only 2026-09-28 13:41:59 +09:00
kiyo9w
e9279c58b3 docs: describe pinned local gallery asset 2026-09-28 13:41:52 +09:00
kiyo9w
ada00da88b fix: require explicit safe shelf enrollment paths 2026-09-28 13:41:43 +09:00
kiyo9w
745a29d979 fix: guard reserved sessions and bounded response state 2026-09-28 13:41:35 +09:00
kiyo9w
0e85c399bb fix: load hash-pinned DINOv2 weights without network 2026-09-28 13:41:27 +09:00
kiyo9w
f0e2baaf3d fix: anchor OCR passes and tags to shelf coordinates 2026-09-28 13:41:12 +09:00