feat(carwash): advisory vehicle category from the entry camera — mapping, pre-select, downgrade flag
The app plumbing for venue-modules.md §"Vehicle category from vision"; the model is the open half (no bundled recognizer emits body_type yet, so the desk shows nothing until phase A lands in the vision service). - Shared: VEHICLE_CLASSES vocabulary, VehicleRead, CARWASH_VISION_THRESHOLD_DEFAULT, reason code carwash.categoryDowngrade; settings/order/lookup views carry the read. - Vision contract: /analyze vehicle.body_type + confidence (service schema); the Node client normalises to the vocabulary and drops the rest. - Record: snapshot.ts stores the read in the plate's device_events row (or its own when the plate was unreadable); vehicleForIdentity() resolves it like the plate. - Car wash: carwash_categories.vision_classes (site mapping "car, sedan → Vetura"), carwash_config.vision_threshold (signed config_change when it moves), four vision columns on orders — migration 0030. Lookup returns vision + suggestedCategoryId. - Desk pre-selects the mapped category and shows the read + snapshot thumbnail; Setup offers class chips per category and the threshold. Operator decides. - Flag: a read at/above the threshold whose mapped category prices HIGHER than the chosen one signs one `anomaly` (both categories/prices, operator, snapshot) and stores its id on the order. Equal/upgrade/unsure/unmapped → nothing. Recorded only, never blocks, no reason prompt (user, 2026-09-06). Tests in carwash.test.ts; wiki venue-modules (As built), opencv-anpr-service, log. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -23,6 +23,8 @@ import type { FastifyBaseLogger } from "fastify";
|
||||
// transport + contract adapter only.
|
||||
|
||||
/** Plate bounding box (pixels, top-left origin) — mirrors the service schema. */
|
||||
import { isVehicleClass, type VehicleClass } from "@parking/shared";
|
||||
|
||||
export interface PlateBBox {
|
||||
readonly x1: number;
|
||||
readonly y1: number;
|
||||
@@ -40,12 +42,19 @@ export interface VisionPlate {
|
||||
readonly region?: string | null;
|
||||
}
|
||||
|
||||
/** The raw /analyze response shape (the Python contract). `vehicle` is reserved for
|
||||
* Job 2 (vehicle verification) — not yet produced. */
|
||||
/** The vehicle attributes stage of /analyze (advisory). `body_type` is one of the shared
|
||||
* VEHICLE_CLASSES vocabulary (the service's raw label is normalised there); a stub or a
|
||||
* plate-only recognizer sends null. */
|
||||
export interface VisionVehicle {
|
||||
readonly bodyType: VehicleClass;
|
||||
readonly confidence: number;
|
||||
}
|
||||
|
||||
/** The raw /analyze response shape (the Python contract). */
|
||||
interface AnalyzeResponse {
|
||||
readonly plate: VisionPlate | null;
|
||||
readonly plates: VisionPlate[];
|
||||
readonly vehicle: unknown | null;
|
||||
readonly vehicle: { body_type?: string | null; confidence?: number | null } | null;
|
||||
readonly low_confidence: boolean;
|
||||
readonly model_version: string;
|
||||
readonly took_ms: number;
|
||||
@@ -61,6 +70,8 @@ export interface VisionResult {
|
||||
/** True when the best plate is below the confidence floor — treat as advisory only
|
||||
* and fall back to the ticket/manual path. */
|
||||
readonly lowConfidence: boolean;
|
||||
/** The vehicle's body type, when the service ran that stage and named a known class. */
|
||||
readonly vehicle: VisionVehicle | null;
|
||||
readonly modelVersion: string;
|
||||
readonly tookMs: number;
|
||||
}
|
||||
@@ -124,10 +135,16 @@ export class VisionClient {
|
||||
const best = res.plate ?? null;
|
||||
const lowConfidence =
|
||||
res.low_confidence || (best != null && best.confidence < this.#minConfidence);
|
||||
const v = res.vehicle;
|
||||
const vehicle: VisionVehicle | null =
|
||||
v && isVehicleClass(v.body_type) && typeof v.confidence === "number"
|
||||
? { bodyType: v.body_type, confidence: Math.max(0, Math.min(1, v.confidence)) }
|
||||
: null;
|
||||
return {
|
||||
plate: best,
|
||||
plates: Array.isArray(res.plates) ? res.plates : [],
|
||||
lowConfidence,
|
||||
vehicle,
|
||||
modelVersion: res.model_version ?? "unknown",
|
||||
tookMs: typeof res.took_ms === "number" ? res.took_ms : 0,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user