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
|
|
@ -193,6 +193,88 @@ class LabelLifecycleTests(unittest.TestCase):
|
|||
engine.shutdown()
|
||||
|
||||
|
||||
class TagPassIntegrationTests(unittest.TestCase):
|
||||
"""The tag pass must reach the live result without blocking frames."""
|
||||
|
||||
def make_tagged_session(self):
|
||||
from ocr import FakeOcr, OcrLine
|
||||
|
||||
def reader(crop):
|
||||
height, width = crop.shape[:2]
|
||||
return [
|
||||
OcrLine("コーヒー", 0.8, [int(width * 0.05), int(height * 0.05), int(width * 0.4), int(height * 0.3)]),
|
||||
OcrLine("150", 0.9, [int(width * 0.05), int(height * 0.4), int(width * 0.35), int(height * 0.8)]),
|
||||
]
|
||||
|
||||
session, engine, detector, matcher = make_session()
|
||||
# Rail bands come from product rows, so the shelf needs a row of facings.
|
||||
detector.boxes = [
|
||||
{"id": index, "box": [100 + index * 120, 100, 200 + index * 120, 260],
|
||||
"confidence": 0.9, "class_id": 0, "width": 100, "height": 160,
|
||||
"center": [150 + index * 120, 180]}
|
||||
for index in range(3)
|
||||
]
|
||||
engine.set_ocr_engine(FakeOcr(reader=reader))
|
||||
self.assertTrue(session.enable_tags())
|
||||
return session, engine, detector
|
||||
|
||||
def test_track_gains_a_price_tag_and_a_reconciliation(self):
|
||||
import time as _time
|
||||
|
||||
session, engine, detector = self.make_tagged_session()
|
||||
result = None
|
||||
for frame_id in range(1, 12):
|
||||
result = process(session, frame_id)
|
||||
_time.sleep(0.05) # the tag pass runs on its own worker
|
||||
if any(
|
||||
(track.get("price_tag") or {}).get("state") == "readable"
|
||||
for track in result["tracks"]
|
||||
):
|
||||
break
|
||||
track = result["tracks"][0]
|
||||
self.assertIn("price_tag", track, "the tag pass should attach to the facing above it")
|
||||
self.assertEqual(track["price_tag"]["price"], 150)
|
||||
self.assertIn("reconciliation", track)
|
||||
self.assertEqual(result["summary"]["price_tags"]["readable"], 1)
|
||||
engine.shutdown()
|
||||
|
||||
def test_frames_are_not_blocked_by_ocr(self):
|
||||
"""A slow OCR engine must not slow the frame loop: frames keep coming."""
|
||||
import time as _time
|
||||
|
||||
from ocr import FakeOcr, OcrLine
|
||||
|
||||
def slow_reader(crop):
|
||||
_time.sleep(0.5)
|
||||
height, width = crop.shape[:2]
|
||||
return [OcrLine("150", 0.9, [10, 10, int(width * 0.3), int(height * 0.6)])]
|
||||
|
||||
session, engine, detector, _ = make_session()
|
||||
detector.boxes = [
|
||||
{"id": index, "box": [100 + index * 120, 100, 200 + index * 120, 260],
|
||||
"confidence": 0.9, "class_id": 0, "width": 100, "height": 160,
|
||||
"center": [150 + index * 120, 180]}
|
||||
for index in range(3)
|
||||
]
|
||||
engine.set_ocr_engine(FakeOcr(reader=slow_reader))
|
||||
self.assertTrue(session.enable_tags())
|
||||
started = _time.perf_counter()
|
||||
for frame_id in range(1, 4):
|
||||
process(session, frame_id)
|
||||
elapsed = _time.perf_counter() - started
|
||||
self.assertLess(elapsed, 1.0, "three frames must not wait for a 0.5 s OCR pass")
|
||||
engine.shutdown()
|
||||
|
||||
def test_config_can_turn_tags_off(self):
|
||||
session, engine, _ = self.make_tagged_session()
|
||||
config = session.configure(tags=False)
|
||||
self.assertFalse(config["tags"])
|
||||
self.assertTrue(config["tags_available"])
|
||||
result = process(session, 1)
|
||||
self.assertNotIn("price_tag", result["tracks"][0])
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
class FramePipelineTests(unittest.TestCase):
|
||||
def test_latest_frame_wins_and_drops_are_counted(self):
|
||||
session, engine, _, _ = make_session()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue