Files
parking_solution/apps/vision/tests/test_parsing.py
T
julian 5cedcaefe1 feat(vision): add recognize CLI + verify fast-alpr end-to-end
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
2026-06-19 15:46:03 +02:00

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