docs(wiki): phase B training path and hardware decisions — recorded, not built

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-07 10:00:18 +02:00
parent 8bfc29db2a
commit 1a0fe59488
5 changed files with 100 additions and 1 deletions
+2 -1
View File
@@ -106,7 +106,8 @@ Three surfaces, nothing else — it must not grow into a fleet console:
- **`GET /export/labels.csv`** — reviewed, usable rows: item, booth, crop path, the reviewer's
label, the operator's category + classes, the camera's class + confidence, downgraded, at.
Crops are not packaged: the phase-B trainer runs **on the same host** (its GPU) and reads them
off the volume — `docker-compose.collector.yml` carries the `trainer` seam as a commented
off the volume ([[bodytype-classifier-training]]: CPU-only, the Xeon is enough) —
`docker-compose.collector.yml` carries the `trainer` seam as a commented
`profiles: [train]` one-off job (next increment).
**Deploy notes.** Bind the published port to the host's **Netbird address** (`COLLECTOR_BIND`),
@@ -0,0 +1,84 @@
---
title: Body-type classifier (phase B) — training path and hardware
type: decision
status: decided 2026-09-07; NOT built (user: "no build just yet")
related: [vision-review-outbox, opencv-anpr-service, venue-modules, fleet-deployment-komodo, technology-stack]
---
# Body-type classifier (phase B) — training path and hardware
The Car Wash category suggestion needs SUV vs sedan, which the phase-A COCO detector cannot give
([[opencv-anpr-service]] §Vehicle body type). Phase B is a **classifier over the detector's crop**,
trained on the reviewer's labels gathered through the [[vision-review-outbox]]. This page records
what the loop looks like, what it runs on, and what is deliberately not done. Discussed and decided
with the user on 2026-09-07; **nothing here is built yet** — the user will say when.
## The loop (as designed)
Today the loop stops at the reviewer's verdict: the collector holds labels + crops and can export
`labels.csv`. Nothing trains, nothing updates a booth. The rest of the path, each step a place
where a person decides:
1. **Train** — a one-off job (`apps/trainer`, Python/uv like the vision service) on the
collector's host reads the export and the crops straight off the collector volume, splits by
TIME (validation = newer cars than training, so the number reflects tomorrow's traffic), and
fine-tunes a small **BSD-licensed torchvision backbone** (the licence rule applies to weights as
much as code; timm/ImageNet weights only if their terms are checked). Outputs three files: the
ONNX classifier, a sidecar (class list, preprocessing constants, version), and a metrics report
(accuracy per class + confusion matrix). It **refuses to write the model** below a validation
floor the owner sets — a bad model never becomes a file. Class imbalance (nine sedans in ten)
is weighted in the loss and reported; classes with too few labels are dropped from that run.
2. **Evaluate before anything ships** — the owner reads the report. 85–95 % on frontal gate views
is the expectation once tuned; enough to *flag*, never to *bill* (the flag records, the site
threshold exists for exactly this).
3. **Publish** — weights are not code and do not live in git: a versioned file in the Gitea
package registry / a release asset, fetched by URL like the YOLOX weights.
4. **Bake and build** — the vision Dockerfile fetches that version at build time; a second stage in
`vehicle.py` runs the classifier on the detector's box and replaces `car` with the finer class
when confident, else keeps YOLOX's answer. One model path setting like the YOLOX one; off when
unset. The contract, the mapping chips and the flag do not change — the vocabulary already holds
sedan/hatchback/suv/minivan/pickup.
5. **Deploy** — a TAG bump on the booth's stack. **A booth gets a model the way it gets code**: a
pinned release you can see and roll back. No runtime model fetch (air-gapped appliance,
read-only model path — [[vision-service-hardening]]).
Retrain when the labels have grown meaningfully (every few hundred new verdicts at first). First
run needs roughly **200 reviewed crops per class that matters** (Vetura and SUV at least). Once a
model exists, its predictions on the *unlabelled* pile checked against a small reviewed sample are
the ongoing accuracy check without labelling everything — which is why every entry is sent, not a
sample ([[vision-review-outbox]] §The entry stream).
## Hardware (decided 2026-09-07)
What the owner has: an **NVIDIA Quadro FX 3800** (in hand, not installed), and in
`art-docker-station` an **Intel Xeon E3-1225 v5** (4 Skylake cores, AVX2, no AVX-512) with the
**Intel HD P530** iGPU.
- **Quadro FX 3800 — stays in the drawer.** 2009, GT200, compute capability 1.3, 1 GB. CUDA dropped
that generation in 2015; no PyTorch build of the last decade can use it. Installing it buys a
heater and a driver problem.
- **HD P530 — not for training.** Usable for *inference* via OpenVINO, irrelevant here: inference
runs on the booths' CPUs, which already do YOLOX in ~250 ms.
- **The Xeon does the job.** The problem is small (a few thousand 224-px crops, ten classes, a
small pretrained backbone). Two modes the trainer should offer:
- *Feature extraction + a small head* — run every crop once through the frozen backbone, cache
the feature vectors, train a classifier on top: minutes for a few thousand crops, seconds to
retrain when labels arrive. Expected to carry most of the accuracy on frontal gate views.
- *Full fine-tune* — unfreeze and train end to end: roughly an hour per run on four cores with
a mobile-sized backbone. The step to take when the cheap mode plateaus.
Training is occasional and unattended, so an hour on a CPU is a non-issue; the data is already
on that host, so nothing moves.
- **Consequences for the build:** the trainer image is **CPU-only PyTorch** (< 1 GB, not the 5 GB
CUDA build); the `trainer` seam in `docker-compose.collector.yml` drops the NVIDIA device
reservation (one-line change if a modern card ever lands in the host; the trainer should pick
up CUDA when present).
- **If faster is ever wanted:** a used mid-range card of the last few generations (~€200) turns
the hour into a minute, given a slot and a PSU. **Renting a cloud GPU is rejected**: the crops
would leave the premises, and even scrubbed of plates and site that runs against the whole
privacy design of the outbox.
## Not built
`apps/trainer`, the classifier stage in `vehicle.py`, the publish step, the compose `trainer`
profile (still commented, still with the GPU reservation to remove). First real run waits for
the first few hundred reviewed labels on the collector.
+3
View File
@@ -251,6 +251,9 @@ service's `/health` each tick and shows a **"Vision" chip** in the booth footer
## Vehicle body type (advisory) — the vehicle stage, phase A (2026-09-06)
> Phase B (the classifier that knows SUV from sedan), its training loop and the hardware it runs on
> are decided on [[bodytype-classifier-training]] — not built yet.
`/analyze` populates `vehicle.body_type` + `vehicle.confidence` from the shared vocabulary
(car, sedan, hatchback, suv, minivan, pickup, van, truck, bus, motorcycle). Node records it beside
the plate and the Car Wash desk pre-selects the category the site maps it to; the operator
+1
View File
@@ -133,6 +133,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
- [[dingtian-vs-mqtt]] — transport choice: direct HTTP/UDP now, MQTT parked until multi-lane scale.
- [[session-model]] — business layer start: session = projection; transient-first; pay-on-foot. New event types.
- [[vision-service]] — build a host-side ANPR + vehicle-verification service; replaces edge-LPR; scoped AGPL exception.
- [[bodytype-classifier-training]] — phase B (SUV vs sedan) training path: trainer on the collector host → evaluate → publish weights → bake into the vision image → TAG bump; runs on the Xeon E3-1225 v5 CPU (feature-extraction head first, full fine-tune later), Quadro FX 3800 unusable, cloud GPU rejected (crops stay on premises). Decided 2026-09-07, NOT built.
- [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python.
- [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events).
- [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. Auto-updater mirrors signed releases to public `mca/public_releases` (source repo is private — field appliances have no Gitea creds).
+10
View File
@@ -3133,3 +3133,13 @@ in-process) + `enqueueEntry()` (crop + camera class, no order/operator/category)
kind column, operator agreement is wash-only. Setup line shows "1 in N entries sampled". Also:
Setup → Car wash is a two-column grid (the master-data card was squeezed at max-w-2xl). Tests
on both sides. Updated [[vision-review-outbox]].
## [2026-09-07] decide | Phase B training path + hardware — recorded, not built
User asked "now what about the training" and then "let's talk hardware". Recorded on the new
[[bodytype-classifier-training]]: the five-step loop (train on the collector host → evaluate with a
floor → publish weights to the registry → bake into the vision image → TAG bump; a booth gets a
model the way it gets code, never a runtime fetch); ~200 reviewed crops per class before the first
run; the Quadro FX 3800 is unusable (cc 1.3), the HD P530 irrelevant, the Xeon E3-1225 v5 is enough
(feature-extraction head in minutes, full fine-tune ~1 h); trainer image = CPU-only torch, the
compose seam drops the GPU reservation; cloud GPU rejected (crops stay on premises). Linked from
[[opencv-anpr-service]], [[vision-review-outbox]], index. User: "No build just yet."