fix(vision): self-heal local real ANPR — dev scripts sync the alpr extra

The dev box runs vision as bare `uv run uvicorn`, and a plain uv run/uv sync
re-resolves the venv to the lockfile DEFAULTS, stripping fast-alpr/onnxruntime.
So after any `pnpm dev` real ANPR silently degraded to "snapshot, no plate"
(diagnosed 2026-06-25: real reads through 06-22, venv frozen lean since 06-19,
no other env with fast_alpr). The BOOTH was never affected — it runs the Docker
image, which bakes `uv sync --frozen --extra alpr` at build (immutable, weights
pre-warmed); a booth ModuleNotFoundError is a STALE image (fix: booth.sh update).

Vision package.json dev/start/recognize now run `uv sync --extra alpr &&` first
so pnpm dev is self-healing; added a dev:stub escape hatch for a lean run.
Documented in wiki/decisions/vision-service-packaging.md ("Two runtimes, one
fragile") + a log entry.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-26 08:11:17 +02:00
parent b3cb67188e
commit 40ffa90dac
3 changed files with 47 additions and 4 deletions
+5 -3
View File
@@ -3,14 +3,16 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"//": "Thin shim so this Python service is a first-class node in the Turbo task graph (it is NOT a JS package — deps are managed by uv/pyproject.toml). Each script shells to Python tooling. See wiki/decisions/vision-service-packaging.md.", "//": "Thin shim so this Python service is a first-class node in the Turbo task graph (it is NOT a JS package — deps are managed by uv/pyproject.toml). Each script shells to Python tooling. See wiki/decisions/vision-service-packaging.md.",
"//alpr": "DEV self-heals real ANPR: `dev`/`start` run `uv sync --extra alpr` FIRST, because a plain `uv run` re-resolves the venv to the lockfile DEFAULTS and STRIPS fast-alpr (the cause of silent 'snapshot but no plate' after a prior pnpm dev). Syncing the extra here guarantees the recognizer survives every run. Use `dev:stub` for a lean, model-free local run. The BOOTH is unaffected — it runs the Docker image, which bakes `--extra alpr` at build (see Dockerfile + docker-compose.prod.yml).",
"scripts": { "scripts": {
"dev": "uv run uvicorn vision_service.app:app --reload --host 0.0.0.0 --port 8089", "dev": "uv sync --extra alpr && uv run uvicorn vision_service.app:app --reload --host 0.0.0.0 --port 8089",
"start": "uv run uvicorn vision_service.app:app --host 0.0.0.0 --port 8089", "dev:stub": "uv run uvicorn vision_service.app:app --reload --host 0.0.0.0 --port 8089",
"start": "uv sync --extra alpr && uv run uvicorn vision_service.app:app --host 0.0.0.0 --port 8089",
"lint": "uv run ruff check .", "lint": "uv run ruff check .",
"format": "uv run ruff format .", "format": "uv run ruff format .",
"typecheck": "uv run mypy vision_service", "typecheck": "uv run mypy vision_service",
"test": "uv run pytest -q", "test": "uv run pytest -q",
"recognize": "uv run python -m vision_service.cli", "recognize": "uv sync --extra alpr && uv run python -m vision_service.cli",
"build": "echo 'no build step (Python service; models fetched at deploy)'" "build": "echo 'no build step (Python service; models fetched at deploy)'"
} }
} }
+27 -1
View File
@@ -2,7 +2,7 @@
type: decision type: decision
tags: [parking, decisions, vision, anpr, monorepo, packaging] tags: [parking, decisions, vision, anpr, monorepo, packaging]
sources: [] sources: []
updated: 2026-06-19 updated: 2026-06-25
status: settled status: settled
--- ---
@@ -114,3 +114,29 @@ The skeleton is **built and wired** (no recognizer models yet):
> **Resolved 2026-06-22 → [[container-deployment]]:** the vision service now ships as the > **Resolved 2026-06-22 → [[container-deployment]]:** the vision service now ships as the
> `parking-vision` Docker image (uv base, `--extra alpr`), model weights **pre-warmed into the image > `parking-vision` Docker image (uv base, `--extra alpr`), model weights **pre-warmed into the image
> layer** at build (offline-first), and runs under **docker-compose** (base + per-env override). > layer** at build (offline-first), and runs under **docker-compose** (base + per-env override).
## Two runtimes, one fragile (the `uv run` strips-the-extra trap) — 2026-06-25
Real ANPR runs **completely differently on the two machines**, and only the dev path was fragile:
- **Booth (deployment) = the Docker image.** The `Dockerfile` runs `uv sync --frozen --extra alpr`
at build, so fast-alpr/onnxruntime are **baked into an immutable image layer** and the weights are
pre-warmed in. `docker-compose.prod.yml` forces `VISION_RECOGNIZER=fast_alpr`. Nothing at runtime
re-resolves the venv → **the booth's real ANPR cannot silently degrade.** (A booth
`ModuleNotFoundError: fast_alpr` is a STALE image, not this bug — fix with `booth.sh update` to pull
the current image.)
- **Dev machine = bare `uv run uvicorn …`** against `apps/vision/.venv`. **This is the trap:** a plain
`uv run` (or `uv sync` with no `--extra alpr`) re-resolves the venv to the lockfile **defaults** and
**REMOVES** the alpr stack — leaving the model weights orphaned in `~/.cache/open-image-models` but
no recognizer in the venv. So a dev box that ran real ANPR (weights downloaded, plate reads
recorded) silently degrades to "**snapshot captured but no plate**" after the next `pnpm dev`. This
exactly explains a gap observed 2026-06-25: real reads on 06-22, then nothing — the venv (frozen
since 06-19, lean) had been stripped, while the Docker/compose work (06-23) was an innocent
coincidence, not the cause.
**Fix (2026-06-25):** the vision `package.json` `dev`/`start`/`recognize` scripts now run
`uv sync --extra alpr &&` FIRST, so `pnpm dev` is **self-healing** — the recognizer survives every
run. A `dev:stub` script is the lean, model-free escape hatch. The booth (Docker) is untouched.
**Implication:** local real-ANPR and booth real-ANPR are now both reliable; CI/light contributors who
don't want the heavy stack use `dev:stub` or run the suite (tests are stub-mode, offline). See
[[opencv-anpr-service]].
+15
View File
@@ -1621,3 +1621,18 @@ passes `-v` (would wipe the signed [[append-only-event-chain|ledger]] volume); `
short-circuit before any Docker/.env requirement. Verified: prod `config` renders Caddy:80 + internal short-circuit before any Docker/.env requirement. Verified: prod `config` renders Caddy:80 + internal
server + pinned images + `fast_alpr`; dev `config` renders `:dev` images + `stub` + published ports. server + pinned images + `fast_alpr`; dev `config` renders `:dev` images + `stub` + published ports.
Documented in [[container-deployment]] ("Booth operator wrapper"). Documented in [[container-deployment]] ("Booth operator wrapper").
## [2026-06-25] fix | Local ANPR silently degraded — `uv run` strips the alpr extra
Diagnosed via the live DB (read-only `VACUUM INTO` copy) why entry `26799912337` recorded a snapshot
but no plate: the dev box's vision service was running **stub**, and earlier real ANPR had stopped.
Root cause (NOT the Docker/compose work, which was an innocent coincidence): the dev machine runs vision
as **bare `uv run uvicorn`** against `apps/vision/.venv`, and a plain `uv run`/`uv sync` re-resolves the
venv to the lockfile **defaults**, **stripping** fast-alpr/onnxruntime — so after any `pnpm dev` the
recognizer vanishes (weights orphaned in `~/.cache`, no module in the venv) and ANPR silently becomes
"snapshot, no plate". Evidence: 28 real reads through 06-22 (yolo-v9 model, ~99% conf), venv frozen lean
since 06-19, no other env with fast_alpr on the box. **The BOOTH was never affected** — it runs the
Docker image, which bakes `uv sync --frozen --extra alpr` at build (immutable, weights pre-warmed); a
booth `ModuleNotFoundError` is a STALE image (fix: `booth.sh update`). **Fix:** vision `package.json`
`dev`/`start`/`recognize` now `uv sync --extra alpr &&` first (self-healing), `.env` set to `fast_alpr`,
+ a `dev:stub` escape hatch. Restored real ANPR locally (`/health` → `fast_alpr` ready, model loaded from
cache, no download). Documented in [[vision-service-packaging]] ("Two runtimes, one fragile").