"""The /analyze response contract — the shape the Node VisionClient adapter consumes. Mirrors the first-cut API in wiki/entities/opencv-anpr-service.md: { plate: {text, confidence, bbox}|null, vehicle: {...}|null, modelVersion, tookMs } Job 2 (vehicle attributes) is scaffolded as an optional field, not yet populated — fast-alpr is plate-only; the vehicle stage is built later on the same ONNX runtime. """ from __future__ import annotations from pydantic import BaseModel, Field class BBox(BaseModel): """Plate bounding box in pixels (top-left origin).""" x1: int y1: int x2: int y2: int class PlateResult(BaseModel): text: str # The plate's confidence = the MIN of fast-alpr's per-character confidences (a plate # is only as trustworthy as its weakest character). See recognizer.py. confidence: float = Field(ge=0.0, le=1.0) bbox: BBox | None = None # Predicted issuing region/country (advisory; fast-alpr's global model emits this). region: str | None = None class VehicleResult(BaseModel): """Job 2 — vehicle attributes. `body_type` is ADVISORY: the Node server records it beside the plate and the Car Wash desk pre-selects the site category it maps to; the operator decides, a disagreement is flagged, nothing is ever gated on it. Values come from the shared vocabulary (car, sedan, hatchback, suv, minivan, pickup, van, truck, bus, motorcycle) — anything else is ignored by Node. Phase A (a COCO detector) emits car/truck/bus/motorcycle; the finer classes need the body-type classifier. Not yet produced by any bundled recognizer.""" colour: str | None = None body_type: str | None = None # Confidence of `body_type` (0–1). Node compares it to the site's threshold. confidence: float | None = Field(default=None, ge=0.0, le=1.0) make: str | None = None model: str | None = None class AnalyzeResponse(BaseModel): # The single best plate, or null when none was found. plate: PlateResult | None = None # All plates found (a frame may contain several vehicles). plates: list[PlateResult] = Field(default_factory=list) vehicle: VehicleResult | None = None # True when the best plate is below the confidence floor — Node should treat the # read as advisory only and prefer the ticket path. See fail-state-safety. low_confidence: bool = False model_version: str took_ms: float class HealthResponse(BaseModel): status: str recognizer: str ready: bool model_version: str detail: str | None = None