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%.
This commit is contained in:
tngo 2026-09-29 00:37:56 +09:00
parent 8ab17ccd5d
commit b6c7c4b667
2 changed files with 108 additions and 25 deletions

View file

@ -30,6 +30,51 @@ from engine import ( # noqa: E402
)
class TextLaneSignatureTests(unittest.TestCase):
"""The lane must score a crop against the row's name, not its packshot.
l09's packshot prints "マヨネーズ", which is l10's actual name. Scoring the
packshot's OCR as part of l09's signature let l09 explain an l10 crop's
text better than l10's own name could, so l09 won l10's facings. Measured
on image6: l10 correct went 2 -> 5 of 11, wrong 3 -> 1, precision 95% ->
97%, and the l09 hub alarm cleared.
"""
ROWS = [
{"sku_id": "sku_l09", "product_name": "ピュアセレクト コクうま カロリーカット65% 360g"},
{"sku_id": "sku_l10", "product_name": "ピュアセレクト マヨネーズ 450g"},
{"sku_id": "sku_l03", "product_name": "キューピー からしマヨネーズ 200g"},
]
def lane(self):
from identity_lanes import TextLane
return TextLane.from_catalog(self.ROWS, "no-such-dir", ocr_engine=None)
def test_a_read_of_the_plain_mayonnaise_names_the_plain_mayonnaise(self):
scores = self.lane().scores_from_text(
"AJINOMOTO とれて3日以内の 国産新鮮たまご ピュア セレクト マヨネーズ"
)
self.assertEqual(max(scores, key=scores.get), "sku_l10")
def test_a_read_of_the_variant_names_the_variant(self):
scores = self.lane().scores_from_text("ピュアセレクト コクうま カロリーカット65%")
self.assertEqual(max(scores, key=scores.get), "sku_l09")
def test_shared_brand_copy_alone_does_not_name_a_row(self):
"""Every row prints マヨネーズ, so it must not decide between them.
The lane scores l10 and l03 equally on that word alone; the policy's
margin gate is what turns the tie into no answer.
"""
from identity_lanes import DecisionPolicy
scores = self.lane().scores_from_text("マヨネーズ")
self.assertEqual(scores["sku_l10"], scores["sku_l03"])
decision = DecisionPolicy().decide([], scores, text_read="マヨネーズ")
self.assertIsNone(decision["best_sku"])
class FakeDetector:
"""Records concurrency and returns fixed boxes."""