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
|
|
@ -95,6 +95,7 @@ class ServerSettings:
|
|||
max_sessions: int = DEFAULT_MAX_SESSIONS
|
||||
live_method: str = "hybrid"
|
||||
live_conf: float = 0.1
|
||||
live_tags: bool = True
|
||||
log_level: str = "info"
|
||||
ssl_certfile: Optional[str] = None
|
||||
ssl_keyfile: Optional[str] = None
|
||||
|
|
@ -110,6 +111,7 @@ def server_settings_from_env() -> ServerSettings:
|
|||
max_sessions=int(os.environ.get("DORAYAKI_MAX_SESSIONS", base.max_sessions)),
|
||||
live_method=os.environ.get("DORAYAKI_LIVE_METHOD", base.live_method),
|
||||
live_conf=float(os.environ.get("DORAYAKI_LIVE_CONF", base.live_conf)),
|
||||
live_tags=os.environ.get("DORAYAKI_LIVE_TAGS", "1").lower() not in ("0", "false", "no"),
|
||||
log_level=os.environ.get("DORAYAKI_LOG_LEVEL", base.log_level),
|
||||
ssl_certfile=os.environ.get("DORAYAKI_SSL_CERTFILE") or None,
|
||||
ssl_keyfile=os.environ.get("DORAYAKI_SSL_KEYFILE") or None,
|
||||
|
|
@ -315,6 +317,7 @@ def create_app(
|
|||
method: Optional[str] = None,
|
||||
conf: Optional[float] = None,
|
||||
empty_slots: bool = True,
|
||||
tags: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
if method is not None:
|
||||
_require_method(method)
|
||||
|
|
@ -338,7 +341,7 @@ def create_app(
|
|||
except UndecodableImage as exc:
|
||||
raise _error("UNDECODABLE_IMAGE", str(exc), 422)
|
||||
try:
|
||||
return engine.recognize(
|
||||
result = engine.recognize(
|
||||
image,
|
||||
classify=classify,
|
||||
method=method,
|
||||
|
|
@ -348,6 +351,49 @@ def create_app(
|
|||
except UnknownMethod as exc:
|
||||
raise _error("BAD_REQUEST", str(exc), 400)
|
||||
|
||||
if tags:
|
||||
result = _attach_still_tags(result, image)
|
||||
return result
|
||||
|
||||
def _attach_still_tags(result: Dict[str, Any], image: Any) -> Dict[str, Any]:
|
||||
"""Run one synchronous tag pass over a still and merge its evidence."""
|
||||
ocr_engine = getattr(engine, "ocr_engine", None)
|
||||
if ocr_engine is None or not getattr(ocr_engine, "available", lambda: False)():
|
||||
result["price_tags"] = {"readable": 0, "unreadable": 0, "absent": 0}
|
||||
result["compliance"] = {"violations": 0, "by_rule": {}, "rows": [], "hints": []}
|
||||
result["ocr_engine"] = "none"
|
||||
return result
|
||||
|
||||
from reconcile import attach_tags_to_boxes
|
||||
from tags import TagService, TagSettings
|
||||
|
||||
boxes = [
|
||||
{"id": detection["id"], "box": detection["box"]} for detection in result["detections"]
|
||||
]
|
||||
identities = {
|
||||
int(detection["id"]): detection["identity"] for detection in result["detections"]
|
||||
}
|
||||
catalog_by_sku = {product["sku_id"]: product for product in engine.catalog()}
|
||||
service = TagService(ocr_engine, settings=TagSettings())
|
||||
started = time.perf_counter()
|
||||
bundle = attach_tags_to_boxes(service, image, boxes, catalog_by_sku, identities)
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
|
||||
for detection in result["detections"]:
|
||||
box_id = int(detection["id"])
|
||||
tag = bundle["by_box"].get(box_id)
|
||||
if tag is not None:
|
||||
detection["price_tag"] = tag
|
||||
reconciliation = bundle["reconciliations"].get(box_id)
|
||||
if reconciliation is not None:
|
||||
detection["reconciliation"] = reconciliation
|
||||
|
||||
result["price_tags"] = bundle["counts"]
|
||||
result["compliance"] = bundle["compliance"]
|
||||
result["ocr_engine"] = ocr_engine.name
|
||||
result["tag_timing_ms"] = round(elapsed_ms, 1)
|
||||
return result
|
||||
|
||||
@app.post("/v1/images/{image_id}/classify", dependencies=[Depends(authorize)])
|
||||
async def classify_image(image_id: str, body: ClassifyBody) -> Dict[str, Any]:
|
||||
method = _require_method(body.method)
|
||||
|
|
@ -418,9 +464,12 @@ def create_app(
|
|||
method=settings.live_method,
|
||||
conf=settings.live_conf,
|
||||
max_inflight_per_session=engine.settings.live_inflight,
|
||||
tags=settings.live_tags,
|
||||
),
|
||||
)
|
||||
session.refresh_catalog()
|
||||
if settings.live_tags:
|
||||
session.enable_tags()
|
||||
sessions[session_id] = session
|
||||
await websocket.accept()
|
||||
send_lock = asyncio.Lock()
|
||||
|
|
@ -506,6 +555,7 @@ def create_app(
|
|||
method=message.get("method"),
|
||||
conf=message.get("conf"),
|
||||
empty_slots=message.get("empty_slots"),
|
||||
tags=message.get("tags"),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
await send({"type": "error", "code": "BAD_MESSAGE", "message": "bad config values"})
|
||||
|
|
@ -733,6 +783,7 @@ def main() -> None:
|
|||
parser.add_argument("--classify-workers", type=int, default=None)
|
||||
parser.add_argument("--live-method", type=str, default=None, choices=list(CLASSIFY_METHODS))
|
||||
parser.add_argument("--live-conf", type=float, default=None)
|
||||
parser.add_argument("--no-tags", action="store_true", help="disable price-tag OCR")
|
||||
parser.add_argument("--max-sessions", type=int, default=None)
|
||||
parser.add_argument("--ssl-certfile", type=str, default=None)
|
||||
parser.add_argument("--ssl-keyfile", type=str, default=None)
|
||||
|
|
@ -755,6 +806,8 @@ def main() -> None:
|
|||
server.live_method = args.live_method
|
||||
if args.live_conf is not None:
|
||||
server.live_conf = args.live_conf
|
||||
if args.no_tags:
|
||||
server.live_tags = False
|
||||
if args.ssl_certfile is not None:
|
||||
server.ssl_certfile = args.ssl_certfile
|
||||
if args.ssl_keyfile is not None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue