diff --git a/CHANGELOG.md b/CHANGELOG.md index 058c4c493..cde3a5606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,132 +7,210 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Releases 0.2.0, 0.2.1 and 0.3.0 are published on GitHub; their entries below are the content of those tags. The entry headed `Pre-release 0.1.0` is the earliest private -history of this codebase and was never published. This release is `[0.4.1]`. +history of this codebase and was never published. This release is `[0.5.0]`. ## [Unreleased] -### Added +## [0.5.0] - 2026-10-09 -- Detector hint for VLM labeling (#61): a prompt pack's `detector_hint_min_confidence_pct` - (0 = off, max 100) tells the VLM each item's stored detector class name, per image, when the - detector confidence is at least that percent. Served in the pack schema as `kind: "int"`; a new - `pack_field_out_of_range` validation code. Measured on a public COCO oracle (539 crops, local - VLM) it raised overall accuracy from 0.775 to 0.853 (0.7846 to 0.8478 in an arm with ten - validated classes seeded) and accuracy when answered from 0.850 to 0.918. Caveat: the detector - is COCO-trained and the oracle is COCO ground truth, so the gain may be optimistic for other - domains. A registry-prior variant (top validated classes plus pending proposals in the prompt) - was measured, showed no gain (and about -1 point with seeded classes) and was removed. -- Gateway mode (#93): `OP_GATEWAY_SUBPATHS=true` makes Cropwright's nginx serve - Grafana, Prometheus, OpenSearch Dashboards and MLflow under `/grafana/`, - `/prometheus/`, `/dashboards/` and `/mlflow/` of its one published port, so the UIs - stay on `127.0.0.1`. One switch in `docker-compose.yml` starts each UI serving from - its sub-path, the API serves the path-relative `resource_links` and MLflow run - links (an explicit `OP_*_URL` still wins and a `0` port still hides the link), and - nginx upgrades Grafana Live websockets and forwards `Host` and `X-Forwarded-*`. - Fail closed: the UIs refuse to start when published beyond loopback, and Grafana - refuses the default or an empty admin password. Prometheus and MLflow still have no - authentication (`SECURITY.md`). The link audit is in the monitoring guide. +The monorepo release. The backend and the Cropwright frontend (`frontend/`, formerly a separate +repository) are now built, tested, pinned and released together from this repository, with one +documentation site. It also delivers label confirmation (#119), the one-port gateway (#93), the +detector hint for VLM labeling (#61 item 2), a refreshed dependency set and the file splits of +#168. -### Fixed +**BREAKING CHANGES.** (1) The compose service `yolo-api` is renamed `api` (#62): every +`docker compose exec|logs|restart yolo-api`, `./openprocessor logs|restart yolo-api`, custom +override file, script and dashboard that names the old service must change, and an existing +install needs `docker compose up -d --remove-orphans` (the CLI, the installer, `make up` and +`scripts/setup.sh` now pass it themselves). (2) The unread `ocr_det_model`, `ocr_det_version`, +`ocr_det_input_size` and `ocr_det_prob_floor` region profile fields are removed (#181): a stored +or submitted profile that still carries one is rejected with `profile_field_unknown`, and +`region_text_engine_version` is now stamped `:`. (3) `POST /clusters/auto_promote` +and the auto-label auto-promote stage are gated on the accuracy audit (#119), so the +`curation-cluster-refresh` daemon promotes nothing until the audit clears a class. (4) The +experiment-tracking server and the trainer move to MLflow 3 (an existing `mlflow_data` volume +upgrades in place), and browsing MLflow by a DNS name needs `MLFLOW_ALLOWED_HOSTS`. -- The dashboard's "Last clustering" card no longer shows "—" for When and Method - while clusters exist (#197). The served `clusters.last_run_at` and `method` came only - from the auto-label job's state file, which the cluster-refresh daemon's synchronous - `POST /pipeline/auto_label` never writes. Every residual-clustering run now records - itself (`clustering/last_run.py`, called by `cluster_residuals` and - `assign_only_residuals`) and the stats serve that record. -- The shipped proposal-name denylist now also covers the scene and quality words a live COCO - oracle run produced (#193): `abstract*`, `*_background`, `*_object`, `scene*`, `empty*`, - `blank_*`, `*_image`, `shadow*`, `outdoors` and similar. The text-free generic pack and - `examples/prompt_packs/vehicle_wheel.json` carry it too, tested against the COCO class names so - no glob eats a real class. -- Cropwright's nginx now answers redirects with relative `Location` headers; the - `/OpenProcessor` redirect used to name the container port 8080. -- The `detector_disagreements` review tab lists the least confident VLM answers first (no recorded - confidence, then `low`, `medium`, `high`) instead of the keyword order high, low, medium; locate - follows the same order. The "Uncertain only" VLM scope description now matches the selector - (detector confidence below the limit or not recorded; no cluster-disagreement clause) (#195). -- Config-store writes (prompt packs, region profiles, open-vocabulary sets, VLM endpoints, - activations) now wait for an index refresh, so a clone followed at once by a GET no longer - answers 404 (#196). -- VLM scope `representatives` is bounded (#192): a labelled representative moving to its class - cluster no longer pulls in the next-nearest members. Each cluster's reps are claimed once and - stored, so total attempts stay within `per_cluster` per original cluster across refreshes, - restarts and policy edits. - -### Removed +### Added -- The region profile fields `ocr_det_model`, `ocr_det_version`, `ocr_det_input_size` and - `ocr_det_prob_floor` (#181): nothing read them since `PaddleOcrRegionDetector` was - deleted. They leave `DetectionProfile`, the `/region_profiles` body and schema (and the - `ocr_det_models` `choices_from` value), `examples/region_profiles/license_plate.json` and - the contracts. A stored or submitted profile that still carries one is rejected with a - message naming it (`profile_field_unknown`); delete the keys from the document. The - `ocr_engine_id` stamped as `region_text_engine_version` is now `:` - instead of `:+:`. `OCR_DET_MODEL` (the `/ocr` endpoint's Triton - model) is unchanged. +- The Cropwright frontend lives in `frontend/` (#85), merged with its scrubbed history (no squash; + the `cropwright-v0.1.0` and `cropwright-v0.1.1` tags are kept). It is an opt-in `cropwright` + compose profile on the stack's own network (UI bind follows `OP_UI_BIND_ADDRESS`, or + `CROPWRIGHT_BIND_ADDRESS` for it alone), built from `frontend/Dockerfile` by + `docker-compose.dev.yml`, and its tests read the generated `contracts/` directory through a + `$contracts` alias instead of a vendored copy. Frontend checks, tests, e2e and the changelog + rule run from the root workflows, path-filtered to `frontend/**` and `contracts/**`, and + Dependabot covers `frontend/`. The Cropwright documentation is a section of `docs-site/`. +- Gateway mode (#93): `OP_GATEWAY_SUBPATHS=true` makes Cropwright's nginx serve Grafana, + Prometheus, OpenSearch Dashboards and MLflow under `/grafana/`, `/prometheus/`, `/dashboards/` + and `/mlflow/` of its one published port, so the UIs stay on `127.0.0.1`. One switch in + `docker-compose.yml` starts each UI serving from its sub-path, the API serves path-relative + `resource_links` and MLflow run links (an explicit `OP_*_URL` still wins and a `0` port still + hides the link), and nginx upgrades Grafana Live websockets and forwards `Host` and + `X-Forwarded-*`. With the switch off the four sub-paths answer 404. Set it in `.env`; there is + no installer flag. The link audit is in the monitoring guide. +- Per-project VLM scope policy (#119): `GET/PUT /vlm/policy` sets which crops the automated VLM + class writers (the `curation-vlm-worker` and the `auto_label` VLM stage) may label: `scope` + `all` (the default, unchanged behaviour), `uncertain`, `representatives` or `off`, plus + `max_crops_per_day` and `sample_frac`. Explicit requests (`/vlm/label_cluster/{id}`, a + `cluster_id`-scoped run) are never limited. `auto_label/start` and `POST /pipeline/auto_label` + accept a `vlm_scope` override. The policy is cloned with the project. +- Items keep the detector's own answer (#119): `detector_class_name` (registry-name form), + `detector_confidence` and, when the label is a registry class, `detector_class_id`, written at + ingest whatever the `class_resolution` and never by a VLM, classifier or human relabel. Served + on the item wire; existing indexes get the three fields mapped on the next bootstrap. Items + ingested before this change have no detector fields (no backfill). +- Review tab `detector_disagreements` (#119): unvalidated items whose VLM class differs from the + detector's own class, listed by `GET /review/tabs` and `GET /review/{tab}`. +- Accuracy audit (#119): `POST /audit/start` draws a stratified sample (per detector class, + deterministic) of machine-labelled crops, `GET /audit/queue` lists those still waiting for a + human, and every human label on a drawn crop stamps `audit_outcome` (`agree`, `detector_wrong`, + `vlm_wrong`, `both_wrong`). `GET /audit/report` gives per-class detector and VLM precision with + Wilson 95% intervals, the confusion matrix and an `insufficient_sample` flag. +- Label confirmation in Cropwright (#119): a VLM scope panel on `/settings`; the detector's own + class and score beside a VLM-sourced label, with the label source worded "VLM suggestion", + "Human-confirmed" or "Auto-validated"; a Detector Disagreements tab on `/review`; a new `/audit` + page (draw a sample, per-class precision with intervals, confusion matrix, queue of crops + waiting for a human); and `/export` states how many crops are validated and that only validated + crops are exported. +- Detector hint for VLM labeling (#61 item 2): a prompt pack's `detector_hint_min_confidence_pct` + (int 0-100, 0 = off, the default) adds each item's stored detector class name and confidence to + the VLM prompt as a one-line hint when the detector confidence is at least that percent. Served + in the pack schema as `kind: "int"`; a new `pack_field_out_of_range` validation code. Measured + on a public COCO oracle (gemma-4-e4b, 200 images, 539 scored crops) it raised overall accuracy + from 0.7755 to 0.8534 and accuracy when answered from 0.8496 to 0.9182 (0.7846 to 0.8478 in a + second project seeded with validated classes); run-to-run noise is about 0.5 point. Caveat: the + detector is COCO-trained and the oracle is COCO ground truth, so the gain may be optimistic for + other domains. ### Changed -- **Breaking:** the compose service `yolo-api` is renamed `api` (#62). `docker compose exec yolo-api ...`, - `docker compose logs yolo-api` and `./openprocessor logs|restart yolo-api` become - `... api`; the Prometheus job and Loki service label are `api` too. Custom overrides, scripts - and dashboards that name the old service must change. The container name - (`-api`), the `op-api` network alias and all data volumes are unchanged. An existing - install is upgraded by `./openprocessor upgrade` (or `docker compose up -d --remove-orphans`), - which replaces the old container; every start path (`start`, `make up`, `scripts/setup.sh`, - the installer's control-plane-only path) now passes `--remove-orphans`, because the old - container holds the same name and would otherwise make the new one fail to create. +- The shipped proposal-name denylist also covers the scene and quality words a live COCO oracle + run produced (#193): `abstract*`, `*_background`, `*_object`, `scene*`, `empty*`, `blank_*`, + `*_image`, `shadow*`, `outdoors` and similar. The text-free generic pack and + `examples/prompt_packs/vehicle_wheel.json` carry it too, tested against the COCO class names so + no glob eats a real class. Strict-glob proposal noise measured 1-3% on the main arms and 4.9% + on the seeded arm with the hint on. +- **Breaking:** the compose service `yolo-api` is renamed `api` (#62). The Prometheus job and Loki + service label are `api` too. The container name (`-api`), the `op-api` network alias + and all data volumes are unchanged. An existing install is upgraded by `./openprocessor upgrade` + (or `docker compose up -d --remove-orphans`), which replaces the old container; every start + path (`start`, `make up`, `scripts/setup.sh`, the installer's control-plane-only path) now + passes `--remove-orphans`, because the old container holds the same name and would otherwise + make the new one fail to create. +- **Behaviour change:** `POST /clusters/auto_promote` (and the `auto_label` auto-promote stage) is + gated on the audit (#119): a class is promoted only with enough audited crops and an audited + detector precision of at least `promote_min_precision` (default 0.95); otherwise `409 + audit_required` or `409 audit_precision_low` lists the classes and nothing is written. + `force=true` bypasses the gate (logged); a dry run is never gated. `curation-cluster-refresh` + promotes nothing until the audit clears a class. +- `region_text_engine_version` is stamped `:` (was `:+:`) + after the unread detection fields were removed (see Removed). +- The backend and frontend dependency sets are refreshed (#151 and the Dependabot groups it + superseded): FastAPI 0.143 and Pydantic 2.14 (contracts regenerated; the route walks use + `iter_route_contexts` because FastAPI wraps included routers), SvelteKit 3 with + `@sveltejs/adapter-static` 4 on TypeScript 6 (options now in `sveltekit.options.js`), + Playwright 1.63, the backend group (#170: opencv-python-headless, pytest 9.1, pytest-playwright, + playwright), `aiohttp>=3.14.4`, `locust>=2.46.7`, nginx-unprivileged 1.31 for the docs and + Cropwright images, TypeScript 6.0.3 in `docs-site`, and the GitHub Actions group. Pydantic + models use `ConfigDict` and `SettingsConfigDict` (#156). `onnx` (needs `nvidia-modelopt[onnx]` + with `onnx~=1.21`) and `huggingface-hub` 2.x (`tokenizers` requires <2) are deliberately held + back; Dependabot ignores both with the reason in its config. +- The experiment-tracking server and the trainer client move to MLflow 3.17 together (#120). The + server image is `ghcr.io/mlflow/mlflow:v3.17.0` and the trainer pins `mlflow>=3.17,<4`. An + existing `mlflow_data` volume upgrades in place on first start: runs, artifacts and registered + models from 2.x stay readable. MLflow 3 answers `403 Invalid Host header` to any `Host` outside + its allowed list, so `docker-compose.yml` now sets that list: the in-network service and + container names plus the localhost and private-IP defaults. Browsing MLflow by a DNS name needs + `MLFLOW_ALLOWED_HOSTS` (it replaces the whole list; see `env.template`). - The segmenter image moves to PyTorch 2.14.1 (`pytorch/pytorch:2.14.1-cuda12.6-cudnn9-runtime`, superseding dependabot #147) and `alpine/git` v2.54.0. The new base is Ubuntu 24.04, so the - image now runs Python 3.12 (was 3.11), installs with `PIP_BREAK_SYSTEM_PACKAGES=1` and - replaces the base's `ubuntu` user so `appuser` keeps uid 1000. SAM 3 outputs match the - previous image. Rebuild the segmenter image to pick this up. -- The experiment-tracking server and the trainer client move to MLflow 3.17 together - (#120). The server image is `ghcr.io/mlflow/mlflow:v3.17.0` and the trainer pins - `mlflow>=3.17,<4`. An existing `mlflow_data` volume upgrades in place on first start: - runs, artifacts and registered models from 2.x stay readable. MLflow 3 answers - `403 Invalid Host header` to any `Host` outside its allowed list, so - `docker-compose.yml` now sets that list: the in-network service and container names - plus the localhost and private-IP defaults. Browsing MLflow by a DNS name needs - `MLFLOW_ALLOWED_HOSTS` (it replaces the whole list; see `env.template`). -- The seven mlflow 2.x CVEs are no longer in the Trivy allowlist. + image now runs Python 3.12 (was 3.11), installs with `PIP_BREAK_SYSTEM_PACKAGES=1` and replaces + the base's `ubuntu` user so `appuser` keeps uid 1000. SAM 3 outputs match the previous image. + Rebuild the segmenter image to pick this up. +- The release flow builds Cropwright from `frontend/`: it is the sixth built image in + `scripts/lib/image_keys.sh` (`cropwright`, pushed as `davidamacey/cropwright:`), so + `make release` pins its digest in `images.lock` with the other images. `cropwright.lock`, the + `CW_*` variables, the `-cw` second compose project and the staging of Cropwright files + into the deploy bundle are gone; the `cropwright` tier is a profile of the main compose project + and its port and bind address go in the install `.env`. `make release-verify` now uses a + throwaway compose project (it failed whenever a stack was running), and an installer run from a + release directory verifies itself against that release's `SHA256SUMS`. +- The version is `0.5.0` everywhere the version-consistency test checks, and the Cropwright + `package.json` and image build argument follow the monorepo version (its standalone numbering, + 0.1.x, ends here). +- No source file under `src/` or `scripts/` is over 700 lines, and the size hook has no + exemptions (#168, #62): `opensearch.py`, `curation_opensearch.py`, `end2end_export.py`, + `vlm_labeler.py`, the worker runner, the train router, `jobs.py`, `triton_promote.py`, the + ingest and search routers, `cascade_detect`, the clustering orchestrator, `visual_search`, + `face_identity` and `duplicate_detection` are split into cohesive modules or packages. The job + state helpers moved onto `FileJob`. No route, wire name or behaviour changes. +- The SARIF Trivy scan honours the CRITICAL/HIGH gate (`limit-severities-for-sarif`). -### Added +### Removed -- Per-project VLM scope policy (#119): `GET/PUT /vlm/policy` sets which crops the - automated VLM class writers (the `curation-vlm-worker` and the `auto_label` VLM - stage) may label: `scope` `all` (the default, unchanged behaviour), `uncertain`, - `representatives` or `off`, plus `max_crops_per_day` and `sample_frac`. Explicit - requests (`/vlm/label_cluster/{id}`, a `cluster_id`-scoped run) are never limited. - `auto_label/start` and `POST /pipeline/auto_label` accept a `vlm_scope` override. - The policy is cloned with the project. -- Items keep the detector's own answer (#119): `detector_class_name` (registry-name - form), `detector_confidence` and, when the label is a registry class, - `detector_class_id`, written at ingest whatever the `class_resolution` and never by - a VLM, classifier or human relabel. Served on the item wire; existing indexes get - the three fields mapped on the next bootstrap. Items ingested before this change - have no detector fields (no backfill). -- Review tab `detector_disagreements` (#119): unvalidated items whose VLM class differs - from the detector's own class, listed by `GET /review/tabs` and `GET /review/{tab}`. -- Accuracy audit (#119): `POST /audit/start` draws a stratified sample (per detector - class, deterministic) of machine-labelled crops, `GET /audit/queue` lists those still - waiting for a human, and every human label on a drawn crop stamps `audit_outcome` - (`agree`, `detector_wrong`, `vlm_wrong`, `both_wrong`). `GET /audit/report` gives - per-class detector and VLM precision with Wilson 95% intervals, the confusion matrix - and an `insufficient_sample` flag. -- `POST /clusters/auto_promote` (and the `auto_label` auto-promote stage) is gated on the - audit (#119): a class is promoted only with enough audited crops and an audited detector - precision of at least `promote_min_precision` (default 0.95); otherwise `409 - audit_required` or `409 audit_precision_low` lists the classes and nothing is written. - `force=true` bypasses the gate (logged); a dry run is never gated. Behaviour change: - `curation-cluster-refresh` promotes nothing until the audit clears a class. +- The region profile fields `ocr_det_model`, `ocr_det_version`, `ocr_det_input_size` and + `ocr_det_prob_floor` (#181): nothing read them since `PaddleOcrRegionDetector` was deleted + (#174). They leave `DetectionProfile`, the `/region_profiles` body and schema (and the + `ocr_det_models` `choices_from` value), `examples/region_profiles/license_plate.json`, the + contracts and Cropwright's fixtures. A stored or submitted profile that still carries one is + rejected with a message naming it (`profile_field_unknown`); delete the keys from the document. + `OCR_DET_MODEL` (the `/ocr` endpoint's Triton model) is unchanged. +- Dead code (#174): `PaddleOcrRegionDetector` (the module `cascade_detect/paddle_det.py`), + `ITEMS_CLUSTER_INDEX` and `assign_cluster_to_crop`, with their imports and stale docstrings. +- The seven mlflow 2.x CVEs are no longer in the Trivy allowlist. +- The standalone `cropwright-docs` service: the docs are a section of the one docs site. ### Fixed -- The VLM worker and the `auto_label` sweep no longer select frozen-holdout or excluded - items (#119). Before, only a `cluster_id`-scoped run left them out, so the sweep paid VLM calls - for items the label lock then refused or that were meant to be out of the pipeline. +- A clean install left the default detector `yolov11_small_trt_end2end` UNAVAILABLE: the installer seeds an FP32 output config while the TensorRT 11 NMS engine builds with FP16 `det_boxes` and `det_scores`, and the exporter keeps a differing config. A kept config now takes the built engine's output dtypes (found by the 0.5.0 acceptance run). +- VLM scope `representatives` is bounded (#192): a labelled representative moving to its class + cluster no longer pulls in the next-nearest members. Each cluster's reps are claimed once and + stored (`vlm_scope_reps`), so total attempts stay within `per_cluster` per original cluster + across refreshes, restarts and policy edits. +- The `detector_disagreements` review tab lists the least confident VLM answers first (no recorded + confidence, then `low`, `medium`, `high`) instead of the keyword order high, low, medium; locate + follows the same order. The "Uncertain only" VLM scope description now matches the selector + (detector confidence below the limit or not recorded; no cluster-disagreement clause) (#195). +- Config-store writes (prompt packs, region profiles, open-vocabulary sets, VLM endpoints, + activations) now wait for an index refresh (`refresh=wait_for`), so a clone followed at once by + a GET no longer answers 404 (#196). +- The dashboard's "Last clustering" card no longer shows "-" for When and Method while clusters + exist (#197). The served `clusters.last_run_at` and `method` came only from the auto-label + job's state file, which the cluster-refresh daemon's synchronous `POST /pipeline/auto_label` + never writes. Every residual-clustering run now records itself (`clustering/last_run.py`) and + the stats serve that record. +- The VLM worker and the `auto_label` sweep no longer select frozen-holdout or excluded items + (#119). Before, only a `cluster_id`-scoped run left them out, so the sweep paid VLM calls for + items the label lock then refused or that were meant to be out of the pipeline. +- No Cropwright page overflows horizontally at 430 px (#184): the project top bar wraps to two + rows below 768 px, `/review`'s tab strip and queue counter wrap instead of squeezing the tabs, + and on `/projects` an `sr-only` table header no longer widens the page. This is covered by the + stubbed e2e suite; it has not been checked against a live backend. +- `scripts/lib/download.sh` and `export_paddleocr.sh` looked for the running container by its bare + service name (`api`, `triton-server`) while containers are named `-`, so the + check never hit and the temporary-container branch always ran; both now resolve the service + through compose with an explicit project (#187). +- Cropwright's nginx answers redirects with relative `Location` headers (`absolute_redirect + off`); the `/OpenProcessor` redirect used to name the container port 8080. +- `/health` and `/projects/{p}/health` took a constant ~3 s whenever the optional VLM was not + running (the probe went through the labeling retry loop), so every UI page waited before + painting. The probe makes one attempt: 3.05 s to 0.02 s on a live stack. +- The documented dev setup installed `faiss-cpu` and `faiss-gpu-cu12` together and broke the + faiss tests; `CLAUDE.md` and `CONTRIBUTING.md` now use the CI recipe. +- The docs-vs-code and link checkers read untracked local files; they now read tracked files only. + The `api-contracts-drift` hook ran the system Python and crashed; it now prefers `.venv/bin/python`. +- The Cropwright clusters page clipped its toolbar at narrow widths; the class sidebar now stacks + above the content below `md`. + +### Security + +- Gateway mode fails closed: with `OP_GATEWAY_SUBPATHS=true` the monitoring UIs refuse to start + when published beyond loopback, and Grafana refuses the default or an empty admin password. + Prometheus and MLflow still have no authentication (`SECURITY.md`). +- Unchanged and stated plainly: the API has no authentication, and Grafana keeps the default + `admin` password on LAN-only installs (change it before exposing the stack). ## [0.4.1] - 2026-10-04 diff --git a/CLAUDE.md b/CLAUDE.md index 49e42624a..f4764d65e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ where things live, and the commands and rules that matter. Before making design decisions or judgment calls, read [`docs/VISION_AND_GOALS.md`](docs/VISION_AND_GOALS.md): what OpenProcessor is -for, the v0.4.1 feature scope, and the standards the code is held to (no dead +for, the v0.5.0 feature scope, and the standards the code is held to (no dead code, no compatibility shims, fail-closed isolation, class identity by name never index, review depth matched to real risk). diff --git a/SECURITY.md b/SECURITY.md index d95fc81fb..e27aa4de8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -59,8 +59,7 @@ computer. ### Release checksums are integrity, not authenticity The installer checks every downloaded file against the release's -`SHA256SUMS`, pins images by digest from `images.lock`, pins Cropwright's -files through `cropwright.lock`, and cross-checks each `images.lock` entry's +`SHA256SUMS`, pins images (Cropwright's included) by digest from `images.lock`, and cross-checks each `images.lock` entry's image repo against `scripts/lib/image_keys.sh`. These catch truncated or corrupted downloads and an internally inconsistent release. They do **not** prove who published the release: `SHA256SUMS` comes from the same origin as diff --git a/VERSION b/VERSION index 267577d47..8f0916f76 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.1 +0.5.0 diff --git a/docker-compose.yml b/docker-compose.yml index 8b524079f..c58579bb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,7 @@ x-gateway-wrap: &gateway_wrap | services: triton-server: - image: ${OP_TRITON_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-triton:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_TRITON_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-triton:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-triton logging: *default-logging restart: always @@ -132,7 +132,7 @@ services: - triton_net api: - image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-api logging: *default-logging restart: always @@ -315,7 +315,7 @@ services: cropwright: profiles: ['cropwright'] # CROPWRIGHT_IMAGE (a full reference, e.g. a digest pin) wins over CROPWRIGHT_TAG. - image: ${CROPWRIGHT_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/cropwright:${OP_IMAGE_TAG:-0.4.1}} + image: ${CROPWRIGHT_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/cropwright:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-cropwright # Soft scheduling floor for the static-file nginx, not a cap. mem_reservation: 64m @@ -376,7 +376,7 @@ services: # =================================================================== curation-detection-worker: - image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-detection-worker logging: *default-logging mem_reservation: 1g @@ -442,7 +442,7 @@ services: - triton_net curation-vlm-worker: - image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-vlm-worker logging: *default-logging mem_reservation: 512m @@ -481,7 +481,7 @@ services: - triton_net curation-auto-label-worker: - image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-auto-label-worker logging: *default-logging mem_reservation: 2g @@ -545,7 +545,7 @@ services: - triton_net curation-cluster-refresh: - image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_API_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-cluster-refresh logging: *default-logging mem_reservation: 1g @@ -581,7 +581,7 @@ services: # On-demand only (no `restart:` — see docker/evaluator/Dockerfile): # `docker compose --profile curation run --rm curation-evaluator`. curation-evaluator: - image: ${OP_EVALUATOR_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-evaluator:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_EVALUATOR_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-evaluator:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-evaluator logging: *default-logging profiles: @@ -623,7 +623,7 @@ services: # OP_SEGMENTER_URL=http://segmenter:8000 \ # docker compose --profile curation --profile segmenter up -d segmenter: - image: ${OP_SEGMENTER_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-segmenter:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_SEGMENTER_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-segmenter:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-segmenter logging: *default-logging profiles: @@ -692,7 +692,7 @@ services: # =================================================================== curation-trainer: - image: ${OP_TRAINER_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-trainer:${OP_IMAGE_TAG:-0.4.1}} + image: ${OP_TRAINER_IMAGE:-${OP_IMAGE_REPO:-davidamacey}/openprocessor-trainer:${OP_IMAGE_TAG:-0.5.0}} container_name: ${COMPOSE_PROJECT_NAME:-openprocessor}-trainer logging: *default-logging profiles: diff --git a/docs-site/docs/about/vision-and-goals.mdx b/docs-site/docs/about/vision-and-goals.mdx index 0e455611a..2490c80ba 100644 --- a/docs-site/docs/about/vision-and-goals.mdx +++ b/docs-site/docs/about/vision-and-goals.mdx @@ -9,7 +9,7 @@ The canonical copy of this document lives at `docs/VISION_AND_GOALS.md` in the repository. This page mirrors it. This document is the canonical statement of what OpenProcessor is for, what -the v0.4.1 release must deliver, and the standards the codebase is held to. +the v0.5.0 release must deliver, and the standards the codebase is held to. It exists so that anyone working in this repository can orient quickly: `CLAUDE.md` and `README.md` both link to it rather than restating it. @@ -35,10 +35,10 @@ Every stage is designed to be reused across domains by *configuring* it (detection profiles, prompt packs, class registries) rather than by forking the code per domain. -**Explicit owner scope decision:** the full v0.4.1 feature set below was +**Explicit owner scope decision:** the full v0.5.0 feature set below was deliberately *not* trimmed to ship faster. Nothing here is a stretch goal. -## What "done" means for v0.4.1 +## What "done" means for v0.5.0 - **Multiple isolated projects.** Each project is its own dataset workspace — its own indexes, its own directories, its own class @@ -111,7 +111,7 @@ release. touching data integrity, isolation, or security — an independent review before it lands, not batched into one release-day merge. -## What's explicitly not in v0.4.1, and why +## What's explicitly not in v0.5.0, and why These are real, tracked commitments, not abandoned ideas — they are sequenced deliberately rather than included now: @@ -120,7 +120,7 @@ sequenced deliberately rather than included now: strategy, GPU utilization, and a zero-copy pipeline (decode once, keep data resident on the GPU across models) are real, planned work — but they should be tuned against the *final* workload shape. Full-image - SAM3 detection has shipped in v0.4.1, so the call pattern is now known and + SAM3 detection has shipped in 0.4.1, so the call pattern is now known and optimization work (plan: `docs/design/triton_pipeline_optimization_plan.md`) follows the release. - Benchmarks, a technical white paper, and additional domain showcases diff --git a/docs-site/docs/api-reference/curation.mdx b/docs-site/docs/api-reference/curation.mdx index 3045cca87..f5990ea30 100644 --- a/docs-site/docs/api-reference/curation.mdx +++ b/docs-site/docs/api-reference/curation.mdx @@ -6,7 +6,7 @@ title: Curation routes # Curation routes :::info[Experimental] -The curation subsystem is experimental in v0.4.1. It is real and tested, +The curation subsystem is experimental in v0.5.0. It is real and tested, but backward compatibility across releases is not guaranteed until it graduates. It ships opt-in behind the `curation` compose profile. ::: diff --git a/docs-site/docs/deployment/security.mdx b/docs-site/docs/deployment/security.mdx index 91bc494b5..b10dd34ae 100644 --- a/docs-site/docs/deployment/security.mdx +++ b/docs-site/docs/deployment/security.mdx @@ -74,7 +74,7 @@ reverse proxy with authentication in front for anything wider. A specific `--bind` address also narrows Cropwright to that interface. See the [installer page](../getting-started/installer.mdx#network-access). -Release checksums (`SHA256SUMS`, `images.lock`, `cropwright.lock`) prove +Release checksums (`SHA256SUMS`, `images.lock`) prove integrity, not authenticity: they come from the same origin as the files they cover. diff --git a/docs-site/docs/getting-started/installer.mdx b/docs-site/docs/getting-started/installer.mdx index 918812c10..7874e698f 100644 --- a/docs-site/docs/getting-started/installer.mdx +++ b/docs-site/docs/getting-started/installer.mdx @@ -83,29 +83,22 @@ bash setup-openprocessor.sh --version "$V" `SHA256SUMS` comes from the same place as the files it covers. It proves **integrity** (the download is complete and uncorrupted), **not -authenticity**. The same holds for `images.lock` and `cropwright.lock`, which -pin exact digests and are covered by `SHA256SUMS`; the installer also checks +authenticity**. The same holds for `images.lock`, which +pins exact digests and are covered by `SHA256SUMS`; the installer also checks that each `images.lock` entry names the image repo expected for its key. Signed releases are follow-up work. ## Cropwright in the bundle -For v0.4.1 the Cropwright files ship **staged inside the deploy bundle**: the tarball -carries `cropwright-release//` for the tag pinned in `cropwright.lock` (`SHA256SUMS`, `docker-compose.yml`, `.env.example`), -built with `CW_RELEASE_DIR= scripts/release/build_deploy_bundle.sh`. The installer -uses those files with no fetch from Cropwright's repository, and still checks them -against `cropwright.lock`. The standalone Cropwright repository is private until the -0.5.0 monorepo, so a network fetch would fail. +Cropwright is built from `frontend/` in this repository and released with the backend. +Its image (`davidamacey/cropwright`) is pinned in `images.lock` like every other image, +and the `cropwright` tier is a profile of the main compose project, so the installer +needs nothing from a second repository. Its port and bind address go in the install +`.env` as `CROPWRIGHT_PORT` and `CROPWRIGHT_BIND_ADDRESS`. -Cropwright's compose file puts its documentation service behind the `docs` profile. The -installer starts only the `cropwright` service, so the `/cropwright/` docs route answers -502 until you enable the profile: - -```bash -docker compose -p -cw --env-file /cropwright/.env \ - --project-directory /cropwright -f /cropwright/docker-compose.yml \ - --profile docs up -d -``` +The Cropwright documentation is a section of this docs site, and its UI links into it. +The `/OpenProcessor/` route of Cropwright's nginx proxies to the docs container +(`docker-compose.docs.yml`) and answers 502 unless that container is running. ## Network access diff --git a/docs-site/docs/getting-started/introduction.mdx b/docs-site/docs/getting-started/introduction.mdx index b8a5cc34d..98a594ab8 100644 --- a/docs-site/docs/getting-started/introduction.mdx +++ b/docs-site/docs/getting-started/introduction.mdx @@ -54,7 +54,7 @@ The curation subsystem is **domain-agnostic**: nothing about "what an item or a region of interest is" is hardcoded. It is **bring-your-own-model**: you supply the item detector, and optionally a VLM, a region detector and training data. It ships **opt-in** behind the `curation` compose profile, is -**experimental** in v0.4.1 (no backward-compatibility guarantee yet), and, +**experimental** in v0.5.0 (no backward-compatibility guarantee yet), and, like the rest of the API, has **no authentication**. What a deployment configures, and where: diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 6473f8cf1..247022318 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -1,13 +1,13 @@ { "name": "openprocessor-docs", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "license": "AGPL-3.0-or-later", "packages": { "": { "name": "openprocessor-docs", - "version": "0.4.1", + "version": "0.5.0", "license": "AGPL-3.0-or-later", "dependencies": { "@docusaurus/core": "3.10.2", diff --git a/docs-site/package.json b/docs-site/package.json index eda0480e6..445b514a0 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -1,6 +1,6 @@ { "name": "openprocessor-docs", - "version": "0.4.1", + "version": "0.5.0", "private": true, "license": "AGPL-3.0-or-later", "scripts": { diff --git a/docs-site/src/data/roadmap.json b/docs-site/src/data/roadmap.json index 4fbd45749..69de2e012 100644 --- a/docs-site/src/data/roadmap.json +++ b/docs-site/src/data/roadmap.json @@ -3,17 +3,19 @@ "note": "Hand-maintained, not derived from an issue tracker. 'Shipped' means it is on main today; 'Planned' items are design work that has not landed and may change after measurement. Update this file when scope actually changes; never describe work that isn't planned.", "releases": [ { - "version": "v0.4.1", - "headline": "Core inference API plus an experimental curation backend", + "version": "v0.5.0", + "headline": "The monorepo release: backend and Cropwright in one repository, label confirmation, one-port gateway", "stage": "shipped", - "summary": "The Triton-backed core API (detection, faces, embeddings, OCR, search, ingest, analyze) and the opt-in /curation active-learning subsystem with its reference workers, trainer, evaluator, segmenter and VLM compose profiles.", + "summary": "The Triton-backed core API and the opt-in /curation active-learning subsystem, now released together with the Cropwright frontend (frontend/) from one repository and one docs site. Adds label confirmation (VLM scope policy, detector class kept beside VLM labels, accuracy audit, audit-gated auto-promote), the gateway, and a refreshed dependency set.", "items": [ - "YOLO11 (end2end, GPU NMS) and YOLO26 (NMS-free) served side by side; per-request model selection and runtime load/unload", - "SCRFD + ArcFace faces, MobileCLIP embeddings, PP-OCRv5 OCR pipeline, OpenSearch k-NN search", - "/curation: 145 method routes in the generated OpenAPI contract, covering ingest, clustering, review queues, VLM labeling, region cascade, export, training, bake-off and promote", - "Opt-in compose profiles: curation, segmenter, vlm, training, monitoring, benchmark", - "Generated OpenAPI contract for /curation under contracts/, checked for drift in pre-commit", - "Public, license-filtered sample data (make sample-coco, make sample-plates); nothing proprietary bundled" + "Backend and the Cropwright web UI in one repository, built and pinned together; one docs site", + "Label confirmation: per-project VLM scope policy, the detector's own class kept beside VLM labels, a detector_disagreements review tab, an accuracy audit with Wilson 95% intervals, and an audit gate on auto-promote", + "Optional detector hint for VLM prompt packs (detector_hint_min_confidence_pct, off by default; +7.8 points overall accuracy on a COCO oracle)", + "Gateway: Grafana, Prometheus, OpenSearch Dashboards and MLflow on sub-paths of the single Cropwright port (OP_GATEWAY_SUBPATHS)", + "Breaking: the compose service yolo-api is now api; the unread ocr_det_* region profile fields are removed", + "/curation: 266 method routes in the generated OpenAPI contract", + "Refreshed dependencies: FastAPI 0.143 and Pydantic 2.14, SvelteKit 3 and TypeScript 6, MLflow 3.17, PyTorch 2.14.1 segmenter base, Playwright 1.63", + "Opt-in compose profiles: curation, segmenter, vlm, training, monitoring, benchmark, cropwright" ] }, { diff --git a/docs/CURATION.md b/docs/CURATION.md index 3e7a6a215..b3a2173d3 100644 --- a/docs/CURATION.md +++ b/docs/CURATION.md @@ -1,6 +1,6 @@ # Curation and Active Learning -> **Status: experimental for v0.4.1.** This is a working, tested subsystem, +> **Status: experimental for v0.5.0.** This is a working, tested subsystem, > but it is new and its API can still change between releases. It ships > opt-in, behind Docker Compose profiles, and is off by default. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 959abd513..c6749f0dc 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -2,7 +2,7 @@ Complete guide for optimizing FastAPI and Triton Inference Server performance. -> **Status (v0.4.1):** this guide covers the API-layer tuning and benchmarks that are in +> **Status (v0.5.0):** this guide covers the API-layer tuning and benchmarks that are in > `main`. The GPU and Triton pipeline optimization (decode once, stay on the GPU) is > open work, tracked in #40 and planned in > [`design/triton_pipeline_optimization_plan.md`](design/triton_pipeline_optimization_plan.md); @@ -450,7 +450,7 @@ GPU model and the Triton, TensorRT, driver and torch versions. Set B numbers stay in private notes, and only an aggregate ratio or pass/fail is stated in public text. -### Baseline v0.4.1 (set A, public COCO, 4,000 images; commit 15f7bbb9) +### Baseline 0.4.1 (set A, public COCO, 4,000 images; commit 15f7bbb9) Host: single node, Triton + API on one A6000 (GPU 0, 32 API workers, multiprocess metrics on), PE image encoder + YOLO11s via Triton. Region profile off, open_vocab off, VLM/auto-label workers idle. Warmup 100 images excluded. One round per row (not the 3-round median). diff --git a/docs/README.md b/docs/README.md index a8a6cf61c..f83b0f1f4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ Documentation for OpenProcessor: the inference API and the curation subsystem. |---|---| | [README.md](../README.md) | Overview, feature list, quick start, first project, the cars and wheels example | | [INSTALLATION.md](../INSTALLATION.md) | The one-line installer, every flag, the `openprocessor` CLI, VLM selection, install from source | -| [VISION_AND_GOALS.md](VISION_AND_GOALS.md) | What the project is for, the v0.4.1 scope and the standards the code is held to | +| [VISION_AND_GOALS.md](VISION_AND_GOALS.md) | What the project is for, the v0.5.0 scope and the standards the code is held to | | [SECURITY.md](../SECURITY.md) | No authentication, LAN exposure, VLM URL policy, reporting | | [CONTRIBUTING.md](../CONTRIBUTING.md) | Dev setup, tests, contracts, doc checks, commit conventions | | [CLAUDE.md](../CLAUDE.md) | Orientation for AI coding agents working in the repo | diff --git a/docs/VISION_AND_GOALS.md b/docs/VISION_AND_GOALS.md index 872916d3e..6d91a21df 100644 --- a/docs/VISION_AND_GOALS.md +++ b/docs/VISION_AND_GOALS.md @@ -1,7 +1,7 @@ # OpenProcessor: Vision and Goals This document is the canonical statement of what OpenProcessor is for, what -the v0.4.1 release must deliver, and the standards the codebase is held to. +the v0.5.0 release must deliver, and the standards the codebase is held to. It exists so that anyone — human or AI agent — working in this repository can orient quickly: `CLAUDE.md` and `README.md` both link here rather than restating it. @@ -28,10 +28,10 @@ Every stage is designed to be reused across domains by *configuring* it (detection profiles, prompt packs, class registries) rather than by forking the code per domain. -**Explicit owner scope decision:** the full v0.4.1 feature set below was +**Explicit owner scope decision:** the full v0.5.0 feature set below was deliberately *not* trimmed to ship faster. Nothing here is a stretch goal. -## What "done" means for v0.4.1 +## What "done" means for v0.5.0 - **Multiple isolated projects.** Each project is its own dataset workspace — its own indexes, its own directories, its own class @@ -104,7 +104,7 @@ release. touching data integrity, isolation, or security — an independent review before it lands, not batched into one release-day merge. -## What's explicitly not in v0.4.1, and why +## What's explicitly not in v0.5.0, and why These are real, tracked commitments, not abandoned ideas — they are sequenced deliberately rather than included now: @@ -113,7 +113,7 @@ sequenced deliberately rather than included now: strategy, GPU utilization, and a zero-copy pipeline (decode once, keep data resident on the GPU across models) are real, planned work — but they should be tuned against the *final* workload shape. Full-image - SAM3 detection has shipped in v0.4.1, so the call pattern is now known and + SAM3 detection has shipped in 0.4.1, so the call pattern is now known and optimization work (plan: `docs/design/triton_pipeline_optimization_plan.md`) follows the release. - Benchmarks, a technical white paper, and additional domain showcases diff --git a/docs/releases/RELEASE_CHECKLIST.md b/docs/releases/RELEASE_CHECKLIST.md index a1368ef8f..2daabcd86 100644 --- a/docs/releases/RELEASE_CHECKLIST.md +++ b/docs/releases/RELEASE_CHECKLIST.md @@ -1,156 +1,194 @@ -# v0.4.1 release checklist +# v0.5.0 release checklist Every step is manual and owner-run. Run from a clean checkout of the release commit. Nothing here is automated by CI (images are built locally). -## images.lock: what is a placeholder +## images.lock: what is pinned, and what `make release` changes -All 16 entries in `images.lock` are development placeholders (`...dev0` to `...devf`, -not 64-hex digests) and must all be filled; the installer refuses them. +`images.lock` pins every image an install can pull, one `key=repo:tag@sha256:digest` +line per key. The keys come from `scripts/lib/image_keys.sh`, the one table the release +script and the installer share. After `make release` there are 17 lines: -- Built and pushed by step 4 (`davidamacey/...:0.4.1`): `api`, `triton`, `evaluator`, - `segmenter`, `trainer`. -- Third-party, resolved from upstream tags by step 4: the two `vlm_*` keys, +- Six images built from this repository and pushed by step 4 + (`davidamacey/...:0.5.0`, plus `latest`): `api` (`openprocessor`), `triton` + (`openprocessor-triton`), `evaluator` (`openprocessor-evaluator`), `segmenter` + (`openprocessor-segmenter`), `trainer` (`openprocessor-trainer`) and `cropwright` + (`cropwright`, built from `frontend/Dockerfile` with `frontend/` as its build context). +- Eleven third-party images, resolved from upstream tags by step 4: the two `vlm_*` keys, `opensearch`, `opensearch_dashboards`, `mlflow`, `prometheus`, `grafana`, `loki`, `alloy`, `node_exporter`, `dcgm_exporter`. Each tag must be pulled locally first (`docker pull `), because the script reads its RepoDigest. -`make release` rewrites `images.lock` entirely; do not hand-edit digests. +The committed `images.lock` still holds the 0.4.1 digests (16 lines, no `cropwright` row) +until step 4 rewrites it entirely; do not hand-edit it, and do not bump it ahead of the +release. In lock mode the installer dies with "images.lock has no 'cropwright' entry" for +every tier, because it requires a row for each build key, so a lock-mode install of this +branch is impossible until step 4. That is expected: `make release-verify` builds its bundle +with `ALLOW_UNPINNED_LOCK=1` and installs by `--image-tag`, and the installer tests pass with the +committed lock as it is. The version-consistency test does not compare lock tags with `VERSION`; +the bundle builder checks only that every line is digest-pinned and not `latest`. + +Cropwright is no longer released from its own repository or its own lock. There is no +`cropwright.lock`, no `CW_RELEASE_DIR` staging and no `cropwright-release/` directory in the +tarball; do not run `frontend/scripts/release/*` for 0.5.0. The `cropwright` tier is a compose +profile of the main project and takes its image from `images.lock`. ## Release decisions (#44, #63): all DECIDED -All decisions are final (owner, 4 Oct 2026). The only step left is the owner's go to publish. +Carried forward from 0.4.1; nothing here changes for 0.5.0 unless noted. -- **`:latest` tag: DECIDED.** v0.4.1 is the numbered release and `latest` is published - together with it, so `latest` equals `0.4.1` at release. `make release` pushes - `:0.4.1` and `:latest` for each image, and refuses a non-X.Y.Z version, so `latest` - can only ever point at a stable release. A later stable release moves it by running - `make release` for that version; a backport or an old line is pushed by hand with - `docker push` of the version tag only. The installer pins digests from `images.lock` - and never uses `latest`. Rationale: pinned digests keep installs reproducible and - `latest` is a convenience for manual pulls. +- **`:latest` tag: DECIDED.** v0.5.0 is the numbered release and `latest` is published + together with it, so `latest` equals `0.5.0` at release for all six images, including + `davidamacey/cropwright` (whose `latest` previously followed the standalone 0.1.x line). + `make release` pushes `:0.5.0` and `:latest` for each image, and refuses a non-X.Y.Z + version, so `latest` can only ever point at a stable release. A backport or an old line is + pushed by hand with `docker push` of the version tag only. The installer pins digests from + `images.lock` and never uses `latest`. - **Control-plane-only mode: DECIDED.** Kept as an optional installer flag (`--cpu --control-plane-only`), never the default and not part of the supported - production path. It starts only OpenSearch and the API (plus Cropwright if asked), with - no Triton, workers, VLM, segmenter, trainer or monitoring; inference routes answer 503. - The default install requires a GPU and builds the full stack for the chosen tiers. - Rationale: ship what is tested by default and keep the no-GPU path explicit and opt-in. -- **Unverified VLM catalog entries: DECIDED.** Kept, marked `unverified/experimental` - in the catalog label (`Unverified (experimental)`), the installer warnings and the - docs. The verified (`tested`) default is unchanged and unverified rows are never - auto-selected (explicit `--vlm-model-id` only). Rationale: users can opt in, but an - untested model that fails to load is not mistaken for a product bug. + production path. It starts only OpenSearch and the API (plus Cropwright if asked). +- **Unverified VLM catalog entries: DECIDED.** Kept, marked `unverified/experimental`; never + auto-selected (explicit `--vlm-model-id` only). - **OpenSearch heap and shard budget: DECIDED.** Default heap is half of host RAM, 2 GB - minimum, 30 GB cap, `-Xms` equal to `-Xmx`, memory locked; the installer also warns - when `vm.max_map_count` is below 262144. Each project costs 6 shards; the soft budget is - heap GB x 20 (`OP_SHARDS_PER_HEAP_GB`). Creating past it succeeds with a - `shard_budget_high` warning; past `cluster.max_shards_per_node` it is refused - (`409 shard_budget_exceeded`). Documented in `docs-site/docs/deployment/sizing-and-storage.mdx`. - Rationale: this is OpenSearch's own sizing guidance, and the existing warn/refuse - behavior already tells operators when to raise the heap. + minimum, 30 GB cap; each project costs 6 shards; the soft budget is heap GB x 20 + (`OP_SHARDS_PER_HEAP_GB`). See `docs-site/docs/deployment/sizing-and-storage.mdx`. - **Docs hosting: DECIDED.** Both. A public GitHub Pages site built from `docs-site/` by - `.github/workflows/docs.yml`, and a local docs container (`docker compose -f docker-compose.yml -f docker-compose.docs.yml up -d --build docs`, port 4613) for source checkouts. Rationale: Pages is free and - already built in CI; the container serves offline readers. Owner one-time steps (not done): - Settings > Pages > Source "GitHub Actions"; allow `main` on the `github-pages` environment; - run the workflow by hand once from `main`. The docs image is not in `images.lock` or - `make release` (installed stacks read the public site). -- **Trivy remainder (#110): DECIDED.** `linux-libc-dev` kernel-header CVEs (7 ids) and - libxml2 `CVE-2026-6653` are owner-accepted and allowlisted per CVE with a reason in - `scripts/release/trivy-allowlist.txt` (headers only, no kernel runs in a container, no - upstream fix); re-check each release and drop entries when a fixed package ships. - The trainer `mlflow` 2.x pin stays a documented known item (a 3.x move needs client - and server together). + `.github/workflows/docs.yml`, and a local docs container + (`docker compose -f docker-compose.yml -f docker-compose.docs.yml up -d --build docs`, + port 4613) for source checkouts. The Cropwright docs are a section of this one site. The + docs image is not in `images.lock` or `make release`. Owner one-time steps if not already + done: Settings > Pages > Source "GitHub Actions"; allow `main` on the `github-pages` + environment; run the workflow by hand once from `main`. +- **Trivy remainder (#110): DECIDED.** `linux-libc-dev` kernel-header CVEs and libxml2 + `CVE-2026-6653` are owner-accepted and allowlisted per CVE with a reason in + `scripts/release/trivy-allowlist.txt`; re-check each release and drop entries when a fixed + package ships. The mlflow 2.x entries are gone (MLflow 3.17, #120). ## Acceptance status and open items -- Installer acceptance passed 4 Oct 2026 (clean install, lifecycle, upgrade/rollback, - teardown, installed-stack e2e ingest to delete); fixes #105 to #109 are merged. -- Decided: Grafana keeps the default admin password for LAN-only installs; the docs - warn to change it before exposing beyond the LAN. +- Decided: Grafana keeps the default admin password for LAN-only installs; the docs warn to + change it before exposing the stack beyond the LAN. Gateway mode refuses the default. +- The API has no authentication (documented in `SECURITY.md`). +- #44 (clean-host install from the published artifacts) stays open until run against the + published 0.5.0 assets. +- The 430 px UI fix (#184) is covered by the stubbed e2e suite only. +- #119 and #61 live acceptance: record the results in `docs/releases/v0.5.0.md` (every + `TODO-LIVE` marker there must be replaced before step 10; `rg TODO-LIVE docs/releases` + must find nothing). - No open decisions remain except the owner's go to publish. -- Known items shipping in 0.4.1: the trainer `mlflow` 2.x pin (#110, allowlisted). - #111 (export OOM on a shared 12 GB card) is fixed in 0.4.1; #44 (clean-host install - from the published artifacts) stays open until run. #113 (`/models/status` sam3) is fixed: the installer sets - `OP_SEGMENTER_URL` with the segmenter tier. Allowlisted CVEs are re-checked each release. ## Scripted acceptance run (release gate) -Replaces the manual acceptance walk-through (issue #54, plan: -`docs/design/release_acceptance_plan.md`). Install the release candidate with the -installer into an ISOLATED compose project and install directory (non-default -`--port-base`, GPUs nothing else uses), then run: +Install the release candidate with the installer into an ISOLATED compose project and +install directory (non-default `--port-base`, GPUs nothing else uses; tests use GPUs 0 and 2), +then run: ```bash python3 scripts/datasets/fetch_coco_subset.py --out data/samples/coco_va # public COCO, once scripts/release/acceptance_run.sh \ --base-url http://localhost: --project-name \ --install-dir --models-dir /models \ - --project acc-v041 --images-dir data/samples/coco_va --report acceptance.json + --project acc-v050 --images-dir data/samples/coco_va --report acceptance.json # or: make acceptance ACC_ARGS="" ``` It exercises health, self-hosted docs, ingest of ~100 COCO images, clustering, VLM -labeling, human confirmation, holdout, export, a 2-epoch probe train, bake-off, -promote (forced when the gate rejects the undertrained model), inference via -`/detect?model_name=`, model delete, metrics and log-noise checks, and the installer -lifecycle (`--only installer_uninstall` and `--upgrade-version vX` are opt-in). A -cleanup trap always deletes the throwaway project and promoted models. Exit status is -non-zero if any required phase failed. The trainer refuses classes under 20 validated -crops, so the run confirms `--confirm-crops` (default 40) crops into one class and -trains a single-class subset; the model proves the pipeline, not accuracy. A project -slug is retired once deleted, so use a fresh `--project` per run; the JSON report has per-phase status, seconds -and evidence. Attach `acceptance.json` to the go/no-go record. Screenshot review -(plan phase K) stays manual. +labeling, human confirmation, holdout, export, a 2-epoch probe train, bake-off, promote +(forced when the gate rejects the undertrained model), inference via `/detect?model_name=`, +model delete, metrics and log-noise checks, and the installer lifecycle (`--only +installer_uninstall` and `--upgrade-version vX` are opt-in). A cleanup trap always deletes +the throwaway project and promoted models. Exit status is non-zero if any required phase +failed. A project slug is retired once deleted, so use a fresh `--project` per run (`acc-v050` +for this release); the JSON report has per-phase status, seconds and evidence. Attach +`acceptance.json` to the go/no-go record. Screenshot review (plan phase K) stays manual: +open the full-page desktop and 430 px captures and look at them. For 0.5.0 also exercise +label confirmation by hand (VLM scope `off`, an audit draw and report, the +`detector_disagreements` tab) and the gateway (`OP_GATEWAY_SUBPATHS=true` on loopback), and +write the results into the `TODO-LIVE` lines of `docs/releases/v0.5.0.md`. ## Steps -1. Pre-flight. Confirm `VERSION` is `0.4.1`, the `CHANGELOG.md` `[0.4.1]` date is the - release day (edit if not), and the tree is clean on the branch to release. - Re-read `docs/releases/v0.4.1.md` and `docs/design/README.md` (plan statuses) against what is merged, and run the full gate: `pytest tests/ -q --ignore=tests/live -n 16`, +1. Pre-flight. Confirm `VERSION` is `0.5.0` (also `pyproject.toml`, `docs-site/package.json` + and its lock, `frontend/package.json` and its lock, the `OP_IMAGE_TAG` defaults in + `docker-compose.yml`, `SCRIPT_VERSION` in `setup-openprocessor.sh`, the CLI default and the + roadmap; `tests/test_version_consistency.py` checks most of them), the `CHANGELOG.md` and + `frontend/CHANGELOG.md` `[0.5.0]` dates are the release day (edit if not), and the tree is + clean on the branch to release. Re-read `docs/releases/v0.5.0.md` and `docs/design/README.md` + (plan statuses) against what is merged, and run the full gate: + `.venv/bin/python -m pytest tests/ -q --ignore=tests/live -n 16`, `.venv/bin/pre-commit run --all-files`, - `.venv/bin/python scripts/codegen/generate_contracts.py --check`. + `.venv/bin/python scripts/codegen/generate_contracts.py --check`, and from `frontend/` + `npm ci && npm run check && npm test && npm run build`, plus `npm run test:e2e` (stubbed). + Also `cd docs-site && npm ci && npm run build` (it fails on a broken link). 2. Prove the asset set: `make release-verify` (runs `scripts/release/verify_release_assets.sh`; it touches no running stack; on a host with a live stack run it as `OP_PROJECT=op-relverify make release-verify` so the project name does not collide; - builds the assets to a temp dir, dry-runs the installer against them, checks - that a tampered tarball and installer are refused). -3. Build, Trivy-scan and dry-run the images, nothing pushed: `make release-dry-run`. - Trivy CRITICAL gate must pass (allowlist: `scripts/release/trivy-allowlist.txt`). + builds the assets to a temp dir, dry-runs the installer against them, checks that a + tampered tarball and installer are refused). +3. Build, Trivy-scan and dry-run the images, nothing pushed: `make release-dry-run`. It + builds all six images, including Cropwright from `frontend/`. The Trivy CRITICAL gate must + pass (allowlist: `scripts/release/trivy-allowlist.txt`). 4. Push images and pin them: `docker login`, pull the 11 third-party images, then - `make release`. It pushes `davidamacey/openprocessor{,-triton,-evaluator,-segmenter,-trainer}:0.4.1` - (plus `latest`), resolves the third-party digests, and writes `images.lock` and - `images.lock.sha256`. Check `rg 'dev[0-9a-f]$' images.lock` finds nothing. + `make release`. It pushes + `davidamacey/{openprocessor,openprocessor-triton,openprocessor-evaluator,openprocessor-segmenter,openprocessor-trainer,cropwright}:0.5.0` + (plus `latest`), resolves the third-party digests, and writes `images.lock` (17 lines) and + `images.lock.sha256`. Check `rg 'dev[0-9a-f]$' images.lock` finds nothing and that + `rg -c . images.lock` is 17 with a `cropwright=` line. 5. Commit the lock: `git add images.lock` then - `git commit -m "chore(release): pin v0.4.1 image digests"`. (`images.lock.sha256` is a + `git commit -m "chore(release): pin v0.5.0 image digests"`. (`images.lock.sha256` is a local artifact, not committed.) 6. Build the release assets from the committed tree: - `CW_RELEASE_DIR= scripts/release/build_deploy_bundle.sh v0.4.1`. For v0.4.1 the - Cropwright files named in `cropwright.lock` MUST be staged: they ship inside the tarball - (`cropwright-release/v0.1.1/`) because the standalone Cropwright repository is private - until the 0.5.0 monorepo. Cropwright's `docs` service is behind compose profile `docs`; - the installer does not enable it, so `/cropwright/` docs 502 until the profile is on - (documented in the installer docs and release notes). It refuses an unpinned lock. Output: - `dist/release-v0.4.1/` holding `openprocessor-deploy-v0.4.1.tar.gz`, `SHA256SUMS`, - `setup-openprocessor.sh`, `release-manifest.txt`. + `scripts/release/build_deploy_bundle.sh v0.5.0`. It refuses an unpinned lock and needs no + Cropwright files staged: Cropwright is in `images.lock`. Output: `dist/release-v0.5.0/` + holding `openprocessor-deploy-v0.5.0.tar.gz`, `SHA256SUMS`, `setup-openprocessor.sh`, + `release-manifest.txt`. 7. Check the real assets before publishing: - `cd dist/release-v0.4.1 && sha256sum --check --ignore-missing SHA256SUMS` (the tarball + `cd dist/release-v0.5.0 && sha256sum --check --ignore-missing SHA256SUMS` (the tarball and installer lines; the other lines name files inside the tarball). 8. Merge to the release branch with a merge commit (`--no-ff`, never squash), then push it. -9. Tag: `git tag -a v0.4.1 -m "OpenProcessor v0.4.1"` then `git push origin v0.4.1`. +9. Tag: `git tag -a v0.5.0 -m "OpenProcessor v0.5.0"` then `git push origin v0.5.0`. (`make release` refuses a tag that is not at HEAD, so tag after step 5.) 10. Publish (immediately, not a draft): - `gh release create v0.4.1 --title "OpenProcessor v0.4.1" --notes-file docs/releases/v0.4.1.md dist/release-v0.4.1/openprocessor-deploy-v0.4.1.tar.gz dist/release-v0.4.1/SHA256SUMS dist/release-v0.4.1/setup-openprocessor.sh dist/release-v0.4.1/release-manifest.txt` -11. GitHub org transfer: the docs name no transfer plan. Every link and installer URL uses - `github.com/davidamacey/OpenProcessor`. If the repo moves, update those URLs - (`rg 'davidamacey/OpenProcessor'`) and re-run `check_docs_vs_code.py` before step 10. + `gh release create v0.5.0 --title "OpenProcessor v0.5.0" --notes-file docs/releases/v0.5.0.md dist/release-v0.5.0/openprocessor-deploy-v0.5.0.tar.gz dist/release-v0.5.0/SHA256SUMS dist/release-v0.5.0/setup-openprocessor.sh dist/release-v0.5.0/release-manifest.txt` +11. After the release: move the repository to a GitHub organisation. Do this only once the + release is published, so 0.5.0 ships with the URLs the tag was built with; GitHub + redirects the old repository URL, but Pages, Docker Hub and the raw installer URL do not + all follow. The target organisation is not named in this tree. Work through every place + that carries the owner, in a follow-up change, then re-run + `check_docs_vs_code.py` and `scripts/codegen/check_naming_leaks.py`: + - `rg 'davidamacey/OpenProcessor'` for repository URLs: `setup-openprocessor.sh` (the + raw installer URL and release download URLs), `INSTALLATION.md`, `README.md`, + `SECURITY.md`, `CONTRIBUTING.md`, `docs-site/site.config.ts` (`githubRepo`, `url`, + `baseUrl`), `docs-site/docs/**`, `docs-site/src/data/roadmap.json` (`repo`), + `.github/ISSUE_TEMPLATE/config.yml` (security advisory link), the OCI `source` labels in + the Dockerfiles, `frontend/package.json`, and the tests that assert the URL + (`tests/installer/`, `tests/test_release_build_and_publish.py`). + - Docker Hub: the images are named `davidamacey/` (`OP_IMAGE_NAMESPACE` in + `scripts/release/build_and_publish.sh`, the `OP_IMAGE_REPO` defaults in the compose + files, `scripts/lib/image_keys.sh` consumers, `scripts/docker-build-push.sh`). A new + namespace needs the six repositories created, the images re-pushed by a new `make + release`, and a fresh `images.lock` (the installer checks each lock entry's repo against + the table). Old tags stay pullable under the old name. + - `CODEOWNERS` (a user handle becomes an organisation team) and `.github/dependabot.yml` + (reviewers, assignees). + - Repository secrets and variables (Docker Hub token, any Actions secrets) and the + `github-pages` environment, which do not transfer with every setting; re-create them + and re-run the docs workflow by hand so Pages serves from the new location. + - GitHub Pages: the published site URL changes with the owner, so update every link to it + and `site.config.ts` together. + - Local clones and the `origin` remote of any release worktree. ## Post-release verification -- `gh release view v0.4.1` lists the four assets and is not a draft. +- `gh release view v0.5.0` lists the four assets and is not a draft. - On a scratch directory: `curl -fsSLO` the release `setup-openprocessor.sh` and `SHA256SUMS`, `sha256sum --check --ignore-missing`, then - `bash setup-openprocessor.sh --version v0.4.1 --dry-run --dir /tmp/op-verify`. -- On a scratch host or `--dir`/`--project` pair that does not touch a live stack: - real install with `--version v0.4.1`, health check passes. -- `docker pull davidamacey/openprocessor:0.4.1` digest equals the `api` line in `images.lock`. -- Docker Hub shows the five repositories at `0.4.1` and `latest`. + `bash setup-openprocessor.sh --version v0.5.0 --dry-run --dir /tmp/op-verify`. +- On a scratch host or `--dir`/`--project` pair that does not touch a live stack: real + install with `--version v0.5.0 --all`, health check passes, Cropwright answers on its port + (this is the clean-host test of #44; close it when it passes). +- `docker pull davidamacey/openprocessor:0.5.0` digest equals the `api` line in + `images.lock`, and `docker pull davidamacey/cropwright:0.5.0` equals the `cropwright` line. +- Docker Hub shows the six repositories at `0.5.0` and `latest`. +- Upgrade check on a 0.4.1 install: `./openprocessor upgrade` replaces the `yolo-api` + container with `api` (no name-conflict error) and `./openprocessor status` is healthy. diff --git a/docs/releases/v0.5.0.md b/docs/releases/v0.5.0.md new file mode 100644 index 000000000..abd83304d --- /dev/null +++ b/docs/releases/v0.5.0.md @@ -0,0 +1,266 @@ +# OpenProcessor v0.5.0 + +The monorepo release. The backend and its web UI, Cropwright, now live in this one +repository (`frontend/`, formerly a separate repository), are built, tested, pinned and +released together, and share one documentation site. The release also delivers label +confirmation (#119), the one-port gateway (#93), the optional detector hint for VLM +labeling (#61 item 2), a refreshed dependency set (FastAPI, Pydantic, SvelteKit, +TypeScript, MLflow 3, PyTorch 2.14.1) and the file splits of #168. Everything in v0.4.1 +is unchanged in kind. Two changes are breaking and one changes what a running daemon does; +they come first. + +## Breaking and behaviour changes since 0.4.1 + +State plainly, because a script, an override file or a stored document may depend on them: + +- **The compose service `yolo-api` is now `api`** (#62). Every + `docker compose exec|logs|restart yolo-api`, `./openprocessor logs|restart yolo-api`, + override file, script and dashboard that names the old service must change to `api`. + The Prometheus job and the Loki service label are `api` too. The container name + (`-api`), the `op-api` network alias and all data volumes are unchanged. + Because the old container holds the same name, an existing install must be brought up + with `docker compose up -d --remove-orphans`; `./openprocessor upgrade`, the installer, + `make up` and `scripts/setup.sh` pass it for you. +- **The unread `DetectionProfile` fields `ocr_det_model`, `ocr_det_version`, + `ocr_det_input_size` and `ocr_det_prob_floor` are removed** (#181), together with the + dead `PaddleOcrRegionDetector`. This is a wire change: a stored or submitted region + profile that still carries one of those keys is **rejected** with `profile_field_unknown` + (the message names the key). Delete the keys from the document. The value stamped as + `region_text_engine_version` is now `:` instead of + `:+:`. The `/ocr` endpoint's `OCR_DET_MODEL` is unchanged. +- **Auto-promote is gated on the accuracy audit** (#119). `POST /clusters/auto_promote` and + the `auto_label` auto-promote stage promote a class only with enough audited crops and an + audited detector precision of at least `promote_min_precision` (default 0.95); otherwise + they answer `409 audit_required` or `409 audit_precision_low` and write nothing. + `force=true` bypasses the gate (logged); a dry run is never gated. The + `curation-cluster-refresh` daemon therefore promotes nothing until the audit clears a + class. +- **MLflow moves to 3.17, server and trainer together** (#120). An existing `mlflow_data` + volume upgrades in place on first start and old runs, artifacts and registered models stay + readable. MLflow 3 answers `403 Invalid Host header` for any `Host` outside its allowed + list; the compose file sets a list of the in-network names plus localhost and private-IP + defaults. Browsing MLflow by a DNS name needs `MLFLOW_ALLOWED_HOSTS` (it replaces the whole + list; see `env.template`). +- The segmenter image is rebuilt on PyTorch 2.14.1 and Ubuntu 24.04 (Python 3.12). SAM 3 + outputs match the previous image. +- The contract changes are additive except the profile fields above: new `/vlm/policy` and + `/audit/*` routes, the `detector_*` item fields, `kind: "int"` in the pack schema, and + `pack_field_out_of_range`. `scripts/codegen/generate_contracts.py --check` is clean. + +## Highlights + +**One repository, one docs site (#85)** +- Cropwright is in `frontend/` with its scrubbed history (merged, not squashed; the + `cropwright-v0.1.0` and `cropwright-v0.1.1` tags are kept). It is the opt-in `cropwright` + compose profile on the stack's own network and is built from `frontend/Dockerfile`. + Its tests read the generated `contracts/` directory, so a backend wire change fails a + frontend test. +- Its image (`davidamacey/cropwright`) is the sixth image built by `make release` and is + pinned in `images.lock` like the others. `cropwright.lock`, the `-cw` second + compose project and the staged Cropwright files in the deploy tarball are gone. +- Cropwright now follows the monorepo version (0.5.0; its standalone 0.1.x numbering ends at + 0.1.1). The Cropwright documentation is a section of the one docs site. + +**Gateway: the monitoring UIs on the one port (#93)** +- `OP_GATEWAY_SUBPATHS=true` (set it in `.env`; there is no installer flag) makes Cropwright's + nginx serve Grafana, Prometheus, OpenSearch Dashboards and MLflow under `/grafana/`, + `/prometheus/`, `/dashboards/` and `/mlflow/` of its single published port, so the UIs stay + on `127.0.0.1`. The API serves path-relative `resource_links` and MLflow run links in this + mode. With the switch off (the default) those four paths answer 404. +- Fail closed: the UIs refuse to start when published beyond loopback, and Grafana refuses the + default or an empty admin password. Prometheus and MLflow still have no authentication. + +**Label confirmation (#119, backend and UI)** +- A VLM label is a suggestion until a human validates it. `GET/PUT /vlm/policy` sets, per + project, which crops the automated VLM writers may label (`all`, the unchanged default, + `uncertain`, `representatives` or `off`, plus a daily budget and a sample fraction). +- The detector's own class, confidence and class id are kept on the item beside the VLM or + human label (`detector_class_name`, `detector_confidence`, `detector_class_id`); items + ingested before this release have none (no backfill). A `detector_disagreements` review tab + lists unvalidated items whose VLM class differs from the detector's. +- The accuracy audit (`POST /audit/start`, `GET /audit/queue`, `GET /audit/report`) draws a + deterministic stratified sample of machine-labelled crops and reports per-class detector and + VLM precision with Wilson 95% intervals, the confusion matrix and an `insufficient_sample` + flag. The auto-promote gate above reads it. +- Cropwright shows all of this: a VLM scope panel on `/settings`, the detector class beside a + VLM label with "VLM suggestion", "Human-confirmed" and "Auto-validated" wording, the + Detector Disagreements tab on `/review`, a new `/audit` page, and a note on `/export` that + only validated crops are exported. +- The VLM worker and the `auto_label` sweep no longer select frozen-holdout or excluded items. +- Two follow-up fixes ship with it: the `representatives` scope is bounded by persisted + per-cluster claims (`vlm_scope_reps`, #192), and `detector_disagreements` lists the least + confident VLM answers first (no recorded confidence, then low, medium, high, #195; the + "Uncertain only" copy now says what the selector does). +- Live-verified on COCO 200 images, GPU 0 (first pass, before the follow-up fixes): the + scope-policy counts, the detector class stored beside every VLM label, the + `detector_disagreements` count, the human-label lock, the holdout freeze and the export + (73 validated crops), the audit report against a manual count, and the `auto_promote` gate + codes. +- Second live pass over the fixes on the merged tree (COCO 200 images, 1,367 crops, local + VLM, GPU 0): the `representatives` scope with `per_cluster=5` attempted exactly the bound + (893 against 893 with stable clustering, 856 against 856 with the refresh daemon running; + the first pass attempted 1,237 against 438); a worker restart and a policy revision bump + after the pass did not re-open the pool (a restart during a pass was not tested). The + `detector_disagreements` tab ordered 169 items low, medium, high with no inversion (items + with no recorded VLM confidence, which sort first, were not present in the data). Scope + `uncertain` attempted 896 crops, exactly the crops with detector confidence below 0.8 or + none. Twenty clone-then-read loops of a prompt pack all read back immediately. The dashboard + "Last clustering" card shows the time and method after a worker-driven run. Full-page + screenshots at 1600 px and 430 px of settings, the disagreements tab, audit, export, + dashboard and clusters were viewed: no horizontal overflow, clipping, `null` or `undefined`. +- **Measured VLM labeling throughput** (plan step V1, local VLM on one GPU, batches of 32 + crops, 8 request threads): 10.0 crops/s over 200 crops in 20 s with no errors; the + continuous worker averaged 9.8 crops/s over 1,277 crops. + +**Detector hint for VLM labeling (#61 item 2)** +- A prompt pack's `detector_hint_min_confidence_pct` (int 0-100, 0 = off, **the default**) + adds each item's stored detector class name and confidence to the VLM prompt as a one-line + hint when the detector confidence is at least that percent. Replies are parsed as before; a + value outside 0-100 is rejected with `pack_field_out_of_range`. +- Measured on a public COCO oracle (local VLM, 200 images, 539 scored crops): overall + accuracy rose from 0.7755 to 0.8534 and accuracy when answered from 0.8496 to 0.9182. In a + second project seeded with validated classes overall accuracy rose from 0.7846 to 0.8478. + Run-to-run noise is about 0.5 point. Caveat: the detector is COCO-trained and the oracle is + COCO ground truth, so the gain may be optimistic for other domains; generalisation beyond + COCO is an open item for 0.6.0. +- The default proposal-name denylist is broader (scene and quality words such as `abstract*`, + `*_background`, `scene*`, `empty*`, tested so no glob eats a COCO class). Strict-glob + proposal noise measured 1-3% on the main arms and 4.9% on the seeded arm with the hint on. + +**Dependency refresh** +- FastAPI 0.143 and Pydantic 2.14 (contracts regenerated), SvelteKit 3 and + `adapter-static` 4 on TypeScript 6, MLflow 3.17, Playwright 1.63, the backend group + (opencv-python-headless, pytest 9.1, pytest-playwright), `aiohttp>=3.14.4`, + `locust>=2.46.7`, nginx-unprivileged 1.31, PyTorch 2.14.1 for the segmenter. `onnx` and + `huggingface-hub` 2.x are held back on purpose (their dependents pin lower versions). + +**Maintainability (#168, #62, #174)** +- No source file under `src/` or `scripts/` is over 700 lines and the size hook has no + exemptions: the OpenSearch clients, export, VLM labeler, worker runner, train router, + training jobs, ingest and search routers, cascade detection, clustering orchestrator and + three services were split into cohesive modules with no route or wire change. Unused + public symbols (`ITEMS_CLUSTER_INDEX`, `assign_cluster_to_crop`) were deleted. + +**Smaller fixes** +- A clean install no longer leaves the default detector (`yolov11_small_trt_end2end`) unavailable: the + installer seeds an FP32 `det_boxes`/`det_scores` config while the TensorRT 11 engine builds FP16, + and a kept config now takes the built engine's output dtypes. +- Cropwright pages no longer overflow horizontally at 430 px (#184): the top bar and the + `/review` tab strip wrap, and a hidden table header on `/projects` no longer widens the page. +- `scripts/lib/download.sh` and `export_paddleocr.sh` found the running container by its bare + service name, which never matched, so they always started a temporary container; both now + resolve it through compose (#187). +- `/health` no longer takes about 3 s when the optional VLM is not running. +- Cropwright's nginx redirects use relative `Location` headers. + +## Upgrade instructions from 0.4.1 + +- Upgrade with `./openprocessor upgrade` (or `./setup-openprocessor.sh --version v0.5.0`). It + backs up first, passes `--remove-orphans`, and `--rollback` restores the newest backup. + For a hand-run compose install: `docker compose pull` then + `docker compose up -d --remove-orphans`. +- Rename `yolo-api` to `api` in every script, override file and dashboard of your own. +- Delete `ocr_det_model`, `ocr_det_version`, `ocr_det_input_size` and `ocr_det_prob_floor` + from any stored or submitted region profile (and from `examples/region_profiles/` copies). +- Cropwright now ships in `images.lock`. If you installed it as a second compose project + (`-cw`) with 0.4.1, the upgrade replaces that with the `cropwright` profile of the + main project; stop the old one with `docker compose -p -cw down` if it is left + running. Cropwright 0.5.0 requires backend 0.5.0. +- Rebuild or pull the segmenter and the trainer images. MLflow data upgrades in place; if you + browse MLflow by a DNS name, set `MLFLOW_ALLOWED_HOSTS`. +- To use the gateway, set `OP_GATEWAY_SUBPATHS=true` in `.env` and restart; publish the + monitoring UIs on loopback only and use a non-default Grafana admin password. +- Decide whether `curation-cluster-refresh` auto-promotion should resume: run the audit + (`POST /audit/start`, then label the queue), or call auto_promote with `force=true`. +- Hard-refresh the browser (Ctrl/Cmd-Shift-R) after upgrading; Safari can serve a stale + cached bundle. + +## Requirements + +Unchanged from 0.4.1. + +- Linux, Docker with Compose v2, an NVIDIA GPU (Ampere or newer) with driver 535+ and the + NVIDIA Container Toolkit. The installer never installs packages. +- VRAM about 16 GB for `core` (6-8 GB cards use `--profile minimal`); the `curation`, + `segmenter`, `vlm` and `trainer` tiers add to that. 16 GB RAM minimum, 32 GB recommended. +- Disk about 60 GB for `core`, about 135 GB for every tier, plus 20 GB headroom. +- `vm.max_map_count` of at least 262144 (the installer warns and prints the fix). + +## Quick start + +```bash +curl -fsSL https://raw.githubusercontent.com/davidamacey/OpenProcessor/main/setup-openprocessor.sh | bash +``` + +Pin this release with `bash -s -- --version v0.5.0`; add `--dry-run` to see every command +first. To verify the download yourself, fetch `setup-openprocessor.sh` and `SHA256SUMS` +from this release and run `sha256sum --check --ignore-missing SHA256SUMS`. Then open +`http://localhost:4603/docs`. Full guide: `INSTALLATION.md`. + +## Known issues + +- **The API has no authentication.** Do not expose it; see `SECURITY.md`. The gateway and the + Cropwright port inherit this. +- **Grafana keeps the default `admin` password on LAN-only installs**; change it before + exposing the stack beyond your LAN. (Gateway mode refuses the default.) Prometheus and + MLflow have no authentication. +- **A clean-host install from the published artifacts has not yet been run (#44).** It stays + open until it is run against the published 0.5.0 assets. +- **The 430 px fix is covered by the stubbed e2e suite only.** It has not been checked against + a live backend on a phone-sized viewport. +- **The per-item detector hint may not generalise beyond COCO** (the measured gain used a + COCO-trained detector against COCO ground truth); validating it on other domains is open for + 0.6.0. It is off by default. +- **VLM labels are unvalidated suggestions** and are never trainable or holdout-eligible + until a human validates them. The default VLM scope is still `all`; set `/vlm/policy` to + `uncertain`, `representatives` or `off` to limit it. +- Follow-ups filed from #119 and the dependency work, none blocking: #166 (the audit fields are not yet on the item wire), #161 (`FileJob.read` + treats an unreadable state file as idle), #154 (the trainer still uses deprecated MLflow + registry stages), #153 (`ORJSONResponse` deprecation), #155 (SvelteKit `config.alias` + deprecation), #101 (pin the VLM connection to the validated IP). +- **Trivy, accepted classes** (allowlisted per CVE with a reason in + `scripts/release/trivy-allowlist.txt`, re-checked every release): libxml2 `CVE-2026-6653` + and `linux-libc-dev` kernel headers (headers only, no kernel runs in a container, no + upstream fix). The mlflow 2.x allowlist entries are gone with the MLflow 3 move. +- The first open of a very large cluster can be slow until the background `cluster_distance` + backfill finishes. Safari may show a stale cached bundle after an upgrade. +- Cropwright's `/OpenProcessor/` documentation route answers 502 unless the docs container + (`docker-compose.docs.yml`) is running. + +## Fixed issues + +Closed by this release: #85, #93, #120, #168, #174, #181, #184, #187. #119 (label +confirmation) and #61 (detector hint) are delivered and open until their live +acceptance is recorded; #62 (file splits, job state, service rename) is delivered and left +open for its remaining items. + +## Deferred + +- 0.6.0: GPU and Triton decode-once optimization (#40, baseline first, #45), smaller images + (#41), 12 GB card engine tuning (#42). +- Open: request-driven processing plans (#67), vector quantization benchmark (#56), video + analytics (#8), optional API authentication (#65). + +## Verification summary + +- **Scripted acceptance run** (`make acceptance`, isolated install of the release-candidate images + through the real installer, one GPU): 19 of 19 required phases passed (health, self-hosted docs, + ingest of 100 COCO images, detect and embed, clustering, VLM labeling, human confirmation, + holdout, export, a 2-epoch probe train, bake-off, forced promote, inference, model delete, + metrics, log noise, teardown). The run found one product defect, fixed before release: a clean + install left the default detector unavailable (FP32/FP16 output config mismatch, see Smaller fixes). + Not demonstrated in this run: the installer re-run, repair, upgrade and uninstall phases (the + re-run phase needs a GPU with enough free memory for the full model set; they are covered by + the installer test suite and the 4 Oct 2026 installer acceptance), and the VLM model switch. +- **Full gate on the release line:** 7,350 offline tests passed (14 skipped), + `pre-commit run --all-files` clean, contracts in sync, `make release-verify` passed (untouched + assets verify, a tampered tarball and a tampered installer are refused), `make release-dry-run` + built and Trivy-scanned all six images with no un-allowlisted CRITICAL finding; CI (pre-commit, + offline suite, frontend check/unit/e2e, image and repository scans) was green on every merged + pull request. + +## Credits and attribution + +Third-party code and licenses: `ATTRIBUTION.md` and the docs licenses page +(`docs-site/docs/reference/licenses.mdx`). Public docs and tests use license-checked public +datasets (COCO, Open Images). diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 582c20b58..907ded250 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2026-10-09 + +Cropwright joins the OpenProcessor repository as `frontend/` and from here on is versioned with +the monorepo: this release is 0.5.0, not 0.1.2, and requires OpenProcessor 0.5.0 (the earlier +standalone 0.1.x numbering ends at 0.1.1). Its image is built from `frontend/Dockerfile` by the +repository's release flow and pinned in `images.lock`. + ### Added - The served pack schema gains a `detector_hint_min_confidence_pct` row with `kind: "int"` (the optional per-item detector-name hint, 0 = off). The pack editor shows an unknown kind read-only and saves it unchanged, so the value round-trips; `PackSchemaField.kind` lists `int`. @@ -35,13 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `/export` says how many of the crops are validated and that only validated crops are exported; `/dashboard` says labels are not ground truth and names the VLM row "VLM suggestions". - The item wire carries `detector_class_name`, `detector_class_id` and `detector_confidence` (the detector's own class, kept next to the VLM or human label); `RawCrop` and the test fixtures list them. - -### Fixed - -- The VLM scope panel's "Cluster representatives" text no longer says the rest keep their cluster membership: a labeled representative moves to its class cluster, and the next members are not picked in its place (#192). -- No page overflows horizontally at 430px (#184). The project top bar wraps to two rows below 768px (logo, project switcher, Resources and the API chip on the first, the scrolling nav strip on the second; the wordmark and breadcrumb are hidden below 640px). `/review`'s tab strip and queue counter wrap the same way, so the tabs are no longer squeezed. On `/projects` an `sr-only` table header escaped its scroll container and widened the page; the container is now positioned so it clips it. New stubbed e2e `test_narrow_viewport_430.py` asserts no horizontal overflow at 430px on every project route and `/projects`. -- nginx uses `absolute_redirect off`, so redirects no longer point at container port 8080. -- The VLM scope panel's "Uncertain only" copy now says what the selector does: crops whose detector confidence is below the limit or was not recorded. It no longer promises low-confidence VLM answers or crops that disagree with their cluster (OpenProcessor #195). +- The frontend moved into the OpenProcessor repository with its history (OpenProcessor #85). Its tests read the repository's generated `contracts/` directory instead of a vendored copy, and the Cropwright documentation is a section of the one docs site (`docs-site/`). ### Changed @@ -59,6 +60,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 lists its sources explicitly, so root config scripts are not type-checked. - Dropped the `cookie` override; SvelteKit 3 depends on the fixed 2.x line. +### Fixed + +- The VLM scope panel's "Cluster representatives" text no longer says the rest keep their cluster membership: a labeled representative moves to its class cluster, and the next members are not picked in its place (#192). +- The VLM scope panel's "Uncertain only" copy says what the selector does: crops whose detector confidence is below the limit or was not recorded (OpenProcessor #195). +- No page overflows horizontally at 430px (#184). The project top bar wraps to two rows below 768px (logo, project switcher, Resources and the API chip on the first, the scrolling nav strip on the second; the wordmark and breadcrumb are hidden below 640px). `/review`'s tab strip and queue counter wrap the same way, so the tabs are no longer squeezed. On `/projects` an `sr-only` table header escaped its scroll container and widened the page; the container is now positioned so it clips it. New stubbed e2e `test_narrow_viewport_430.py` asserts no horizontal overflow at 430px on every project route and `/projects`. +- nginx uses `absolute_redirect off`, so redirects no longer point at container port 8080. + ## [0.1.1] - TBD Adopts the OpenProcessor 0.4.1 API. diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f43bb10cb..a9be2a79a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -15,7 +15,7 @@ RUN npm run build # rebuild never silently changes what's shipped; Dependabot bumps this. FROM nginxinc/nginx-unprivileged:1.31.2-alpine3.23 -ARG VERSION=0.1.1 +ARG VERSION=0.5.0 ARG REVISION=unknown LABEL org.opencontainers.image.title="Cropwright" \ org.opencontainers.image.description="From raw images to a trained detector, without labeling one box at a time. Labeling frontend for OpenProcessor." \ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 565d09c0e..bae2efd00 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "cropwright", - "version": "0.1.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cropwright", - "version": "0.1.1", + "version": "0.5.0", "license": "AGPL-3.0-only", "dependencies": { "svelte-dnd-action": "^0.9.80" diff --git a/frontend/package.json b/frontend/package.json index a569c0b32..cb3289a43 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "cropwright", "private": true, - "version": "0.1.1", + "version": "0.5.0", "description": "From raw images to a trained detector, without labeling one box at a time. Labeling frontend for OpenProcessor.", "license": "AGPL-3.0-only", "author": "OpenProcessor Contributors", diff --git a/openprocessor b/openprocessor index 7ac5e488b..3dc1d0d50 100755 --- a/openprocessor +++ b/openprocessor @@ -974,7 +974,7 @@ main() { cmd_uninstall "$@" ;; version) - echo "openprocessor CLI ${OP_CLI_VERSION:-0.4.1}" + echo "openprocessor CLI ${OP_CLI_VERSION:-0.5.0}" ;; help|--help|-h) show_help diff --git a/pyproject.toml b/pyproject.toml index 5847033fc..ac5e35fae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [project] name = "openprocessor" -version = "0.4.1" +version = "0.5.0" description = "Self-hosted AI-powered visual processing engine with object detection, face recognition, CLIP embeddings, OCR, and vector search" readme = "README.md" requires-python = ">=3.12" diff --git a/setup-openprocessor.sh b/setup-openprocessor.sh index ef7607e35..ab5c06005 100755 --- a/setup-openprocessor.sh +++ b/setup-openprocessor.sh @@ -74,7 +74,7 @@ OP_GH_DEFAULT_REF="${OP_GH_DEFAULT_REF:-main}" OP_IMAGE_NAMESPACE="${OP_IMAGE_NAMESPACE:-davidamacey}" OP_DOCS_URL="${OP_DOCS_URL:-}" -SCRIPT_VERSION="0.4.1" +SCRIPT_VERSION="0.5.0" EXIT_USAGE=2 EXIT_COLLISION=3 diff --git a/tests/installer/shims/docker b/tests/installer/shims/docker index 38bbc9745..e7a2525a9 100755 --- a/tests/installer/shims/docker +++ b/tests/installer/shims/docker @@ -125,7 +125,7 @@ cmd_compose() { if [[ " ${args[*]:-} " == *" --images "* ]]; then local k v repo tag repo="$(env_val "$envfile" OP_IMAGE_REPO)"; repo="${repo:-davidamacey}" - tag="$(env_val "$envfile" OP_IMAGE_TAG)"; tag="${tag:-0.4.1}" + tag="$(env_val "$envfile" OP_IMAGE_TAG)"; tag="${tag:-0.5.0}" local profiles wanted profiles=",$(env_val "$envfile" COMPOSE_PROFILES)," wanted="OP_API_IMAGE:openprocessor OP_TRITON_IMAGE:openprocessor-triton"