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:
parent
13ee7426e2
commit
fd72251160
7 changed files with 180 additions and 22 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue