"""A synthetic collector volume: the collector's `items` table (same DDL as apps/collector src/db.ts) + JPEG crops. Classes are told apart by COLOUR so even a random-init backbone's features separate them — the tests check the plumbing (split, floor, export, sidecar), not accuracy on real cars.""" from __future__ import annotations import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path import numpy as np import pytest DDL = """ 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 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, vision_confidence REAL NOT NULL, vision_category_id TEXT, downgraded INTEGER NOT NULL DEFAULT 0, image_width INTEGER NOT NULL, image_height INTEGER NOT NULL, plate_blurred INTEGER NOT NULL, image_path TEXT NOT NULL, received_at TEXT NOT NULL, review_label TEXT, reviewed_at TEXT, reviewer TEXT ); """ COLOURS = {"sedan": (200, 40, 40), "suv": (40, 200, 40), "van": (40, 40, 200), "truck": (200, 200, 40)} def write_jpeg(path: Path, colour: tuple[int, int, int], rng: np.random.Generator) -> None: import cv2 path.parent.mkdir(parents=True, exist_ok=True) h, w = int(rng.integers(120, 200)), int(rng.integers(160, 260)) img = np.empty((h, w, 3), np.uint8) img[:] = colour[::-1] # BGR noise = rng.integers(-20, 20, size=img.shape, dtype=np.int16) img = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8) cv2.imwrite(str(path), img, [cv2.IMWRITE_JPEG_QUALITY, 85]) @pytest.fixture def collector_dir(tmp_path: Path) -> Path: """40 labelled crops per class for sedan/suv/van, 5 for truck (below the minimum), a few unusable, a few pending, one labelled row whose file is missing.""" rng = np.random.default_rng(1) con = sqlite3.connect(tmp_path / "collector.sqlite") con.executescript(DDL) t0 = datetime(2026, 9, 1, tzinfo=timezone.utc) n = 0 def add(label: str | None, reviewed: bool, kind: str = "wash", missing: bool = False) -> None: nonlocal n n += 1 item = f"item-{n:04d}" rel = f"crops/booth-2/{item}.jpg" colour = COLOURS.get(label or "sedan", (128, 128, 128)) if not missing: write_jpeg(tmp_path / rel, colour, rng) at = (t0 + timedelta(minutes=10 * n)).isoformat().replace("+00:00", "Z") reviewed_at = ( (t0 + timedelta(days=1, minutes=n)).isoformat().replace("+00:00", "Z") if reviewed else None ) con.execute( "INSERT INTO items (id, booth, kind, order_ref, at, service, vision_class, vision_confidence, " "image_width, image_height, plate_blurred, image_path, received_at, review_label, " "reviewed_at, reviewer) " "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( item, "booth-2", kind, "o", at, "wash", "car" if label != "truck" else "truck", 0.9, 200, 150, 1, rel, at, label if reviewed else None, reviewed_at, "reviewer" if reviewed else None, ), ) # Interleaved in time so every class exists on both sides of the time split. for i in range(40): for label in ("sedan", "suv", "van"): add(label, True) if i % 8 == 0: add("truck", True) add("unusable", True) add("unusable", True) add("sedan", True, missing=True) for _ in range(6): add(None, False, kind="entry") con.commit() con.close() return tmp_path