feat: add Van's real baseline lane and stop size tokens naming products

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.
This commit is contained in:
tngo 2026-09-28 23:12:44 +09:00
parent 13ee7426e2
commit fd72251160
7 changed files with 180 additions and 22 deletions

View file

@ -1094,9 +1094,9 @@ class LiveSession:
source: str = SOURCE_FRAME,
) -> None:
try:
scores, text_rank = future.result()
scores, text_rank, text_read = future.result()
except Exception:
scores, text_rank = {}, []
scores, text_rank, text_read = {}, [], ""
finished_at = time.perf_counter()
with self._lock:
track = self._tracks.get(track_id)
@ -1173,7 +1173,9 @@ class LiveSession:
# chose. The newest crop is the evidence, not the mean: averaging
# in an empty observation halves a good confidence below the
# accept line, which is the dilution the cosine branch guards too.
decision = DecisionPolicy().decide(list(scores.items()), text_rank)
decision = DecisionPolicy().decide(
list(scores.items()), text_rank, text_read=text_read
)
else:
decision = decide_identity(mean_scores, method=method, scale=scale)
track.candidates = [