feat: run the tag pass in parallel with recognition, in live and still modes
The engine owns the OCR backend, a shared OCR lock and a dedicated tag worker, so a tag pass can never occupy a classification worker or slow a frame. A live session plans rail bands on the frame loop (cheap), runs them on that worker, and attaches whatever the cache already knows; a still runs one synchronous pass. Tracks and detections now carry price_tag and reconciliation, and the summary carries price_tags plus compliance. Config can turn tags off per session. Compliance now fails closed on evidence quality. The first real run produced 106 "missing tag" violations on a shelf where only 8 tags were readable - our tag *detection* was incomplete, not the shelf. A row is only judged when at least half its facings carry tag evidence; otherwise it reports "tags detected for 14 of 91 facings in this row" as a hint. Missing-tag findings are one per row with a count, not one per facing, and price_mismatch still reports every disagreement. On add/image6.jpg the result is now 7 real price mismatches and no false violations. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
71b8bdd876
commit
a27841afa4
6 changed files with 407 additions and 27 deletions
|
|
@ -147,11 +147,11 @@ def track(track_id, box, sku="sku_003", state="recognized"):
|
|||
}
|
||||
|
||||
|
||||
BANDS = [band(1, [0, 300, 500, 340]), band(2, [0, 600, 500, 640])]
|
||||
BANDS = [band(1, [0, 300, 700, 340]), band(2, [0, 600, 700, 640])]
|
||||
|
||||
|
||||
class ComplianceTests(unittest.TestCase):
|
||||
def test_missing_tag_on_a_readable_row_is_a_violation(self):
|
||||
def test_missing_tag_on_a_covered_row_is_a_violation(self):
|
||||
tracks = [
|
||||
track(1, [10, 100, 90, 300]),
|
||||
track(2, [110, 100, 190, 300]),
|
||||
|
|
@ -163,6 +163,19 @@ class ComplianceTests(unittest.TestCase):
|
|||
findings = evaluate_compliance(tracks, tags, [tags[1]], BANDS)
|
||||
rules = [finding.rule for finding in findings]
|
||||
self.assertIn(RULE_MISSING_TAG, rules)
|
||||
missing = [finding for finding in findings if finding.rule == RULE_MISSING_TAG][0]
|
||||
self.assertEqual(missing.count, 1, "one finding per row, counting the facings")
|
||||
|
||||
def test_a_row_with_poor_tag_coverage_is_not_evaluated(self):
|
||||
"""The real failure mode: our detector found tags for only a few of many
|
||||
facings. Claiming the rest are missing would be a false violation."""
|
||||
tracks = [track(index, [10 + index * 100, 100, 90 + index * 100, 300]) for index in range(6)]
|
||||
tags = {index: tag(state=TAG_STATE_ABSENT) for index in range(6)}
|
||||
tags[0] = tag(state=TAG_STATE_READABLE, price=168, id="tag_a")
|
||||
findings = evaluate_compliance(tracks, tags, [tags[0]], BANDS)
|
||||
self.assertEqual([finding.rule for finding in findings], [RULE_UNREADABLE_TAGS])
|
||||
self.assertEqual(findings[0].severity, "hint")
|
||||
self.assertEqual(summarize_findings(findings)["violations"], 0)
|
||||
|
||||
def test_unreadable_row_yields_a_hint_never_a_violation(self):
|
||||
"""The demo image case: tags exist but cannot be read. Nothing may be claimed."""
|
||||
|
|
@ -188,15 +201,18 @@ class ComplianceTests(unittest.TestCase):
|
|||
self.assertEqual(findings, [])
|
||||
|
||||
def test_group_of_consecutive_facings_without_tags(self):
|
||||
tracks = [track(index, [10 + index * 100, 100, 90 + index * 100, 300]) for index in range(5)]
|
||||
# 5 facings: 2 tagged, 3 not. Coverage counts facings with *any* tag
|
||||
# evidence, so give the row enough evidence to be judged at all.
|
||||
tracks = [track(index, [10 + index * 100, 100, 90 + index * 100, 300]) for index in range(6)]
|
||||
tags = {
|
||||
0: tag(state=TAG_STATE_READABLE, price=168, id="tag_a"),
|
||||
1: tag(state=TAG_STATE_ABSENT),
|
||||
1: tag(state=TAG_STATE_READABLE, price=178, id="tag_b"),
|
||||
2: tag(state=TAG_STATE_ABSENT),
|
||||
3: tag(state=TAG_STATE_ABSENT),
|
||||
4: tag(state=TAG_STATE_READABLE, price=178, id="tag_b"),
|
||||
4: tag(state=TAG_STATE_ABSENT),
|
||||
5: tag(state=TAG_STATE_READABLE, price=188, id="tag_c"),
|
||||
}
|
||||
findings = evaluate_compliance(tracks, tags, [tags[0], tags[4]], BANDS)
|
||||
findings = evaluate_compliance(tracks, tags, [tags[0], tags[1], tags[5]], BANDS)
|
||||
groups = [finding for finding in findings if finding.rule == RULE_MISSING_TAG_GROUP]
|
||||
self.assertEqual(len(groups), 1)
|
||||
self.assertEqual(groups[0].count, 3)
|
||||
|
|
@ -208,13 +224,13 @@ class ComplianceTests(unittest.TestCase):
|
|||
tags = {
|
||||
0: tag(state=TAG_STATE_READABLE, price=168, id="tag_a"),
|
||||
1: tag(state=TAG_STATE_ABSENT),
|
||||
2: tag(state=TAG_STATE_ABSENT),
|
||||
2: tag(state=TAG_STATE_READABLE, price=188, id="tag_b"),
|
||||
}
|
||||
findings = evaluate_compliance(tracks, tags, [tags[0]], BANDS)
|
||||
findings = evaluate_compliance(tracks, tags, [tags[0], tags[2]], BANDS)
|
||||
self.assertNotIn(RULE_MISSING_TAG_GROUP, [finding.rule for finding in findings])
|
||||
self.assertEqual(
|
||||
len([finding for finding in findings if finding.rule == RULE_MISSING_TAG]), 2
|
||||
)
|
||||
missing = [finding for finding in findings if finding.rule == RULE_MISSING_TAG]
|
||||
self.assertEqual(len(missing), 1)
|
||||
self.assertEqual(missing[0].count, 1)
|
||||
|
||||
def test_orphan_tag(self):
|
||||
tracks = [track(1, [10, 100, 90, 300])]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue