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

@ -59,7 +59,7 @@ UNKNOWN = "unknown"
# Two independent label passes disagreed here, so the row is not ground truth.
# Scoring it would be scoring a coin flip.
EXCLUDE = "exclude"
LANES = ("local", "matcher", "gallery", "hybrid-verify")
LANES = ("local", "matcher", "van", "gallery", "hybrid-verify")
# A single SKU above this share of accepts is the failure the last audit found:
# every dark package collapsing onto one enrolled product.
HUB_SHARE = 0.30
@ -307,6 +307,40 @@ class MatcherLane(Lane):
return sorted(group_by_sku(scores).items(), key=lambda kv: kv[1], reverse=True)
class VanLane(MatcherLane):
"""Van's contract: the matcher's top SKU, always answered.
The `matcher` lane applies `decide_identity`, which is not what the original
demo did - it answered every crop with its best-scoring SKU and had no
accept line at all. Comparing the new system against the gated matcher
understates the baseline's coverage and overstates its precision, so the
completion gate's "Van" row is this lane: same scores, no gate.
"""
name = "van"
def decide(self, crop: Any, ranked: List[Tuple[str, float]], *, accept: float,
margin: float, scale: str) -> Dict[str, Any]:
from identity import IDENTITY_ACCEPTED
if not ranked:
return {
"status": IDENTITY_ACCEPTED, "method": self.name, "best_sku": None,
"score": 0.0, "confidence": 0.0, "identity_level": IDENTITY_ACCEPTED,
"ranked_candidates": [], "details": {}, "message": "no candidate",
"source": None,
}
best, score = ranked[0]
return {
"status": IDENTITY_ACCEPTED, "method": self.name, "best_sku": best,
"score": round(float(score), 2), "confidence": round(float(score), 4),
"identity_level": IDENTITY_ACCEPTED,
"ranked_candidates": [{"sku_id": sku, "score": round(float(v), 2)}
for sku, v in ranked[:5]],
"details": {}, "message": "Always-argmax baseline", "source": "van",
}
class LocalLaneAdapter(Lane):
"""The shipped default: RootSIFT pyramid plus the OCR text lane.
@ -333,11 +367,12 @@ class LocalLaneAdapter(Lane):
margin: float, scale: str) -> Dict[str, Any]:
from identity_lanes import DecisionPolicy
read = self.text_lane.read(crop)
text_rank = sorted(
self.text_lane.scores_from_text(self.text_lane.read(crop)).items(),
self.text_lane.scores_from_text(read).items(),
key=lambda kv: kv[1], reverse=True,
)
return DecisionPolicy(local_accept=accept).decide(ranked, text_rank)
return DecisionPolicy(local_accept=accept).decide(ranked, text_rank, text_read=read)
def _catalog_rows(stock_dir: str) -> List[Dict[str, Any]]:
@ -571,8 +606,10 @@ def main() -> int:
unenrolled = Counter(f["label"] for f in held if f["label"] != UNKNOWN and f["label"] not in enrolled_skus)
lanes: Dict[str, Lane] = {}
if "matcher" in wanted or "hybrid-verify" in wanted:
if "matcher" in wanted or "hybrid-verify" in wanted or "van" in wanted:
lanes["matcher"] = MatcherLane(args.stock_dir)
if "van" in wanted:
lanes["van"] = VanLane(args.stock_dir)
if "local" in wanted:
lanes["local"] = LocalLaneAdapter(args.stock_dir)
if "gallery" in wanted or "hybrid-verify" in wanted: