Curation workflow, end to end
The whole active-learning loop over the /curation API, in order. Every
step works from curl; a frontend such as Cropwright drives the same
routes. API=http://localhost:4603 below.
This page walks the full loop. You do not have to run all of it: detection only, detection with embeddings and clustering, and VLM-assisted labeling are different settings of the same stack. See Pipeline modes for which workers to start, what to switch off per project, and what the code cannot yet switch off.
0. Prerequisites
- Core stack up and the PE-Core encoders built (
make export-pe). Withoutpe_image_encoder, ingest writes nope_embedding, and semantic search, near-duplicate detection and clustering have nothing to work on. - OpenSearch indexes are created automatically, per project, when the project is created.
- You bring the models: an item detector (a stock COCO YOLO works to start), and optionally a VLM, a region detector and a segmenter. See Models you must supply.
1. Project
All curation data lives in a project. A fresh
stack has the default project; create one per dataset:
P=my-dataset
curl -s -X POST "$API/curation/projects" -H 'content-type: application/json' \
-d "{\"slug\": \"$P\", \"display_name\": \"My dataset\"}"
Every route below is under $API/curation/projects/$P/; the section text
omits that prefix. A project can copy another's settings at creation
(clone_settings_from), be archived (read-only) and be deleted (not
default). Projects share the models, workers and GPU, not their data.
2. Classes
Each project has its own class registry, a JSON file under its data
directory. Start from zero with POST /classes (see the
Quick start), copy
data/class_registry.example.json (a small warehouse example) and edit it,
seed every class a detector knows with
scripts/curation/seed_class_registry.py --project $P --model <detector.onnx>
so class ids can't drift from the model's order, or let a
dataset import create them. Names must match
^[a-z0-9_]+$. A class is identified by its name: datasets, combined
projects and promoted models are matched by name, never by id.
3. Ingest or import
| Source | Tool |
|---|---|
| Images on storage the API container mounts | POST /ingest/image, or scripts/curation/bulk_ingest.py --folder DIR --project $P (or a --manifest list) for a resumable bulk run with progress, cancel and failure reporting |
| Images only on your machine | scripts/curation/ingest_upload.py --project $P (uploads bytes to POST /ingest/upload; resuming relies on server-side content dedup) |
| Images only (no labels) from a labeled dataset | scripts/curation/import_labeled_dataset.py --images-only (skips every class) |
| An already-labeled YOLO / COCO dataset or an OpenProcessor export | POST /datasets/preview, then POST /datasets/imports (or scripts/curation/import_labeled_dataset.py); classes map by name, the import can be resumed and undone |
Ingest runs the item detector (OP_INGEST_PRIMARY_*), crops each
detection into an item, embeds it with PE-Core, rejects duplicates and
low-quality crops, and indexes items in bulk. Human labels already on an
item survive re-ingest. With a region profile active, each new item
is also seeded pending_detection for the detection worker.
4. Cluster
docker compose --profile curation up -d
curl -s -X POST "$API/curation/projects/$P/pipeline/auto_label/start?train_clusters=true"
curl -s "$API/curation/projects/$P/pipeline/auto_label/status" # poll until terminal
curl -s "$API/curation/projects/$P/clusters" | jq '.clusters[0]'
Clustering groups items by embedding (residual clustering reduces
pe_embedding by default). Cluster ids have fixed ranges: below 0 is
unassigned/noise, 0..N mirrors class ids, and ids from 10000 up are
candidate clusters that still need a class. Each card carries
cluster_kind, dominant class, purity, validated count, sub-clusters
and representatives; POST /clusters/refine/{cluster_id} splits one
further, and POST /clusters/auto_promote validates classifier-labelled
items in high-purity clusters. Promotion is gated on the accuracy audit
(section 5): every class a run
would promote needs at least min_per_class audited crops and an audited
detector precision of at least promote_min_precision (default 0.95), else
the call answers 409 audit_required or 409 audit_precision_low with the
classes listed and writes nothing. force=true bypasses the gate and is
logged; a dry run is never gated. curation-cluster-refresh calls
auto-promote as each project's pool grows, so it promotes nothing until the
audit clears the classes.
:::caution Known issue: a gated promote stops the retrain
The daemon treats the 409 audit_required answer as a failure. It logs
auto_promote failed and returns before it queues the centroid retrain, so a
project with an unaudited promotable class stops refreshing its clusters until the
audit passes. (issue #280). See
Pipeline modes.
:::
5. Label: VLM suggestions and review queues
- VLM assist. A project runs its active VLM endpoint and calls no VLM until it
activates one (the
envendpoint fromOP_VLM_URL/OP_VLM_MODELincluded), so nothing waits for a VLM. Switch an active project off withPOST /vlm/endpoints/deactivate(see VLM selection), or run the VLM for one job only withvlm=<name>(see the selective recipe). Runauto_label/start?run_vlm=true, label a cluster, a sub-cluster or a sample withPOST /assist/runs, or letcuration-vlm-workersuggest classes continuously. Register and activate other endpoints as in VLM selection;?vlm=<name>picks one for a single run. VLM answers that name no registry class surface as new-class proposals rather than being forced into a class. - Prompts. The VLM's prompts are a prompt pack;
test a draft on stored crops with
POST /prompt_packs/testbefore you activate it (Test on a crop). - Review queues.
GET /review/tabslists them; the main ones areall,mismatches(VLM answer didn't match the registry),vlm_low_conf,detector_disagreements(the VLM's class differs from the detector's own),outliers,uncertaintyandmodel_disagreements(both fed by the probe),primary_low_conf,classifier_blind_spots,new_class_proposals,importedandregions. - Accuracy audit. Nothing else measures how often the detector or the VLM
is right.
POST /audit/startdraws a stratified sample of machine-labelled, unvalidated, non-holdout crops that carry the detector's answer (min_per_classper detector class,sample_sizein total),GET /audit/queuelists them, and you label them through the normal label routes. Each human label stamps whether the detector, the VLM, both or neither was right.GET /audit/reportreturns per-class precision with Wilson 95% intervals, the{detector class: {human class: n}}confusion matrix and aninsufficient_sampleflag for a class with too few audited crops. Audited crops that a human labelled are ordinary human labels, so they are holdout-eligible. - Label writes.
PUT /crops/{crop_id}/label,PUT /crops/batch_label,POST /crops/move, exclude/discard, and undo for each. Every write is recorded inGET /crops/{crop_id}/history. A human label is locked: no worker, VLM pass, re-ingest or reprocess overwrites it. - Keyboard. Each project has its own keymap.
- Semantic search (
GET /search/text, withOP_SEMANTIC_SEARCH_ENABLED=1) finds items by description, e.g. "blurry close-up".
6. Regions of interest (optional)
A region profile describes a sub-region to find inside each item (a wheel on a car, a label, a defect) and which models find, verify and read it. Create or clone one, check it on a stored crop, then activate it:
curl -s -X POST "$API/curation/projects/$P/region_profiles/test" ... # one stored crop, writes nothing
curl -s "$API/curation/projects/$P/region_profiles/active/impact" # what activation affects
curl -s -X POST "$API/curation/projects/$P/region_profiles/vehicle_wheel/activate"
The detection worker's cascade selects up to max_regions_per_item
candidate boxes per item (region detector, then the segmenter leg), sends
one numbered overlay per item to the VLM, and writes a state, verdict and
optional text per box. Every write records its provenance in
region_detector_chain. Humans accept, reject or mark boxes false positive
one box at a time (PATCH /crops/{crop_id}/regions/{box_id},
POST /regions/batch_box_state) or set a whole-item status
(POST /regions/batch_status). Watch progress on
GET /ingest/region_drain, and see
Health and stall reasons when it stops moving.
The wheel example walks this on public data
and Multi-box regions explains the box
model.
After changing a profile, pack or VLM, re-run existing items with
POST /reprocess (see
Health and stall reasons). Locked
items and boxes are skipped.
7. Freeze, export, train, compare, promote
POST /test_holdout/freezeonce.POST /export/yolo(or/export/single_class). See Exports and retention.POST /train/preflight, thenPOST /train/start.POST /bakeoff/runto compare against earlier runs and baselines.POST /train/promote/{job_id}to serve the winner from Triton.POST /probe/run, then workmodel_disagreements, and loop back to step 4 of this page.
Details: Training, bake-off and promote.
8. Combine projects
To merge several projects into one new project, use
POST /curation/projects/combine/preview, then
POST /curation/projects/combine. Sources are only read; the target is
built beside them. See Combine projects.
Models you must supply
Nothing in the curation subsystem ships a pretrained region detector, VLM or trained dataset. A deployment provides:
| What | Required? | How |
|---|---|---|
| PE-Core-L14-336 image and text encoders | Yes | make export-pe; the Triton names pe_image_encoder / pe_text_encoder are fixed |
| Item detector | Yes, for ingest | Any end2end Triton model via OP_INGEST_PRIMARY_DETECTOR_MODEL |
| Secondary detector | No | Raw-output model via OP_INGEST_SECONDARY_DETECTOR_MODEL; also the source of backbone_embedding when exported with make export-dual-head |
| Region profile + region detector | No | A stored profile activated per project (Region profiles), or the deployment default from OP_REGION_PROFILE_PATH / OP_REGION_PROFILE; see advanced configuration |
| OCR recognition model | Only if regions carry text | The profile's ocr_rec_model |
| Segmenter | No | --profile segmenter (SAM 3, needs HF_TOKEN) or any service speaking the same protocol at OP_SEGMENTER_URL |
| VLM | No | --profile vlm, a VLM on the host (http://host.docker.internal:<port>/v1), or anywhere on the network; registered as an endpoint (VLM selection) |
| Training data and base weights | For training | Your exports; the trainer fetches base checkpoints unless hyperparameters.model is a local path |