5cedcaefe1
Add a dev CLI (uv run python -m vision_service.cli <image>) that runs a recognizer on an image file and prints the parsed plate(s) + confidence + region — fast feedback with no HTTP. Also a package.json `recognize` script and a vision-recognize entry point. Verified fast-alpr for real: installed the `alpr` extra, downloaded the YOLOv9 + CCT ONNX weights (~11MB, cached offline under ~/.cache), and ran recognition on the project's test image → "5AU5341" at 1.000 confidence, region "Czech Republic", ~40ms on CPU, via both the CLI and POST /analyze. Fixes result parsing against the actual fast-alpr API: ocr.confidence is a LIST of per-character confidences (not a scalar) — reduced to one plate confidence via the MIN (a plate is only as trustworthy as its weakest character); also surface ocr.region. Extracted the per-result mapping into a pure plate_from_alpr_result + _reduce_confidence and unit-tested them (no model weights needed). 7 tests pass; ruff + mypy strict clean; full turbo build/lint/test green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""Unit tests for fast-alpr result parsing — no model weights required (the objects
|
|
are duck-typed stand-ins shaped like fast-alpr's ALPRResult)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from vision_service.recognizer import _reduce_confidence, plate_from_alpr_result
|
|
|
|
|
|
def test_reduce_confidence_takes_min_of_list() -> None:
|
|
# The weakest character governs trust in the whole plate.
|
|
assert _reduce_confidence([0.99, 0.80, 0.95]) == 0.80
|
|
|
|
|
|
def test_reduce_confidence_handles_scalar_and_junk() -> None:
|
|
assert _reduce_confidence(0.7) == 0.7
|
|
assert _reduce_confidence(None) == 0.0
|
|
assert _reduce_confidence([]) == 0.0
|
|
assert _reduce_confidence("nope") == 0.0
|
|
|
|
|
|
def _fake_result(text: str, conf: list[float], region: str | None = None) -> SimpleNamespace:
|
|
box = SimpleNamespace(x1=10, y1=20, x2=110, y2=60)
|
|
return SimpleNamespace(
|
|
ocr=SimpleNamespace(text=text, confidence=conf, region=region),
|
|
detection=SimpleNamespace(bounding_box=box),
|
|
)
|
|
|
|
|
|
def test_plate_from_result_maps_fields() -> None:
|
|
plate = plate_from_alpr_result(_fake_result("5AU5341", [0.999, 0.9995, 0.97], "Czech Republic"))
|
|
assert plate is not None
|
|
assert plate.text == "5AU5341"
|
|
assert plate.confidence == 0.97 # min of the per-character list
|
|
assert plate.region == "Czech Republic"
|
|
assert plate.bbox is not None
|
|
assert (plate.bbox.x1, plate.bbox.y1, plate.bbox.x2, plate.bbox.y2) == (10, 20, 110, 60)
|
|
|
|
|
|
def test_plate_from_result_skips_empty_text() -> None:
|
|
assert plate_from_alpr_result(_fake_result("", [0.9])) is None
|
|
assert plate_from_alpr_result(SimpleNamespace(ocr=None, detection=None)) is None
|