feat(carwash): entry-stream sampling for the review outbox; park-2 wired to the collector
Build & push images / images (push) Successful in 4m22s
Build & push images / images (push) Successful in 4m22s
The wash stream is small; the entry camera photographs every car in exactly the view the classifier is trained on. The booth can now queue entry vehicle reads as pure training material — crop + the camera's class, no order, no operator, no category. - Core announces every vehicle read (deviceEvents.emitVehicleRead from snapshot.ts); the Car Wash module listens, samples entry reads in-process (sampleEntry: exactly one in N) and queues them (enqueueEntry). CARWASH_REVIEW_ENTRY_SAMPLE=N; 1 = every entry (storage and bandwidth are not the limit — user); 0/unset = off. Forwarded by compose. - Packages carry kind: "wash" | "entry". Collector: kind column, entry meta validated without the operator fields, review screen shows an entry sample as such, export has a kind column, operator agreement computed from wash items only. Setup line shows "1 in N entries sampled"; status carries entrySample. - komodo: park-2's four review lines enabled (collector URL by Netbird DNS name, booth-2, the shared per-booth secret, every entry sampled) — the collector is up on the overlay. - Tests on both sides. Wiki: vision-review-outbox (entry stream + the internet-feed assessment), log. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -9,6 +9,9 @@ import type { VehicleClass } from "@parking/shared";
|
||||
export interface ItemRow {
|
||||
id: string;
|
||||
booth: string;
|
||||
/** "wash" = a desk decision (operator fields set); "entry" = a sampled entry read (pure
|
||||
* training material: crop + the camera's class, operator fields empty). */
|
||||
kind: "wash" | "entry";
|
||||
orderRef: string;
|
||||
at: string;
|
||||
operatorRef: string;
|
||||
@@ -44,11 +47,12 @@ export class CollectorDb {
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
id TEXT PRIMARY KEY,
|
||||
booth TEXT NOT NULL,
|
||||
kind TEXT NOT NULL DEFAULT 'wash',
|
||||
order_ref TEXT NOT NULL,
|
||||
at TEXT NOT NULL,
|
||||
operator_ref TEXT NOT NULL,
|
||||
operator_category_id TEXT NOT NULL,
|
||||
operator_category_name TEXT NOT NULL,
|
||||
operator_ref TEXT NOT NULL DEFAULT '',
|
||||
operator_category_id TEXT NOT NULL DEFAULT '',
|
||||
operator_category_name TEXT NOT NULL DEFAULT '',
|
||||
operator_classes TEXT NOT NULL DEFAULT '[]',
|
||||
service TEXT NOT NULL,
|
||||
vision_class TEXT NOT NULL,
|
||||
@@ -77,6 +81,7 @@ export class CollectorDb {
|
||||
return {
|
||||
id: r.id as string,
|
||||
booth: r.booth as string,
|
||||
kind: r.kind === "entry" ? "entry" : "wash",
|
||||
orderRef: r.order_ref as string,
|
||||
at: r.at as string,
|
||||
operatorRef: r.operator_ref as string,
|
||||
@@ -107,10 +112,10 @@ export class CollectorDb {
|
||||
insert(row: Omit<ItemRow, "reviewLabel" | "reviewedAt" | "reviewer">): void {
|
||||
this.#db
|
||||
.prepare(
|
||||
`INSERT INTO items (id, booth, order_ref, at, operator_ref, operator_category_id, operator_category_name,
|
||||
`INSERT INTO items (id, booth, kind, order_ref, at, operator_ref, operator_category_id, operator_category_name,
|
||||
operator_classes, service, vision_class, vision_confidence, vision_category_id, downgraded,
|
||||
image_width, image_height, plate_blurred, image_path, received_at)
|
||||
VALUES (@id, @booth, @orderRef, @at, @operatorRef, @operatorCategoryId, @operatorCategoryName,
|
||||
VALUES (@id, @booth, @kind, @orderRef, @at, @operatorRef, @operatorCategoryId, @operatorCategoryName,
|
||||
@operatorClasses, @service, @visionClass, @visionConfidence, @visionCategoryId, @downgraded,
|
||||
@imageWidth, @imageHeight, @plateBlurred, @imagePath, @receivedAt)`,
|
||||
)
|
||||
@@ -142,19 +147,21 @@ export class CollectorDb {
|
||||
* reviewer's class fell inside the operator's chosen category (agree) or outside
|
||||
* (disagree) — the honest-mistake / fraud rate the outbox exists for. */
|
||||
stats(): {
|
||||
booths: { booth: string; received: number; pending: number; reviewed: number }[];
|
||||
booths: { booth: string; received: number; pending: number; reviewed: number; entries: number }[];
|
||||
operators: { booth: string; operatorRef: string; reviewed: number; agree: number; disagree: number; unusable: number }[];
|
||||
} {
|
||||
const booths = this.#db
|
||||
.prepare(
|
||||
`SELECT booth, COUNT(*) AS received,
|
||||
SUM(CASE WHEN reviewed_at IS NULL THEN 1 ELSE 0 END) AS pending,
|
||||
SUM(CASE WHEN reviewed_at IS NOT NULL THEN 1 ELSE 0 END) AS reviewed
|
||||
SUM(CASE WHEN reviewed_at IS NOT NULL THEN 1 ELSE 0 END) AS reviewed,
|
||||
SUM(CASE WHEN kind = 'entry' THEN 1 ELSE 0 END) AS entries
|
||||
FROM items GROUP BY booth ORDER BY booth`,
|
||||
)
|
||||
.all() as { booth: string; received: number; pending: number; reviewed: number }[];
|
||||
.all() as { booth: string; received: number; pending: number; reviewed: number; entries: number }[];
|
||||
// Operator agreement is a WASH thing — an entry sample has no operator decision.
|
||||
const reviewed = this.#db
|
||||
.prepare("SELECT booth, operator_ref, operator_classes, review_label FROM items WHERE reviewed_at IS NOT NULL")
|
||||
.prepare("SELECT booth, operator_ref, operator_classes, review_label FROM items WHERE reviewed_at IS NOT NULL AND kind = 'wash'")
|
||||
.all() as { booth: string; operator_ref: string; operator_classes: string; review_label: string }[];
|
||||
const ops = new Map<string, { booth: string; operatorRef: string; reviewed: number; agree: number; disagree: number; unusable: number }>();
|
||||
for (const r of reviewed) {
|
||||
|
||||
Reference in New Issue
Block a user