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 - **`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. 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 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). `profiles: [train]` one-off job (next increment).
**Deploy notes.** Bind the published port to the host's **Netbird address** (`COLLECTOR_BIND`), **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) ## 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 `/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 (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 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. - [[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. - [[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. - [[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. - [[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). - [[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). - [[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: 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 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]]. 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."