Exports and retention
Exports are per project. Routes on this page are under
/curation/projects/{project}/; each project's exports live in its own
directory, {OP_PROJECTS_DATA_ROOT}/{slug}/exports/. Class ids in an
export are the project's own dense ids, written with the ordered class-name
list.
Freezing a test holdout first
POST /curation/projects/{project}/test_holdout/freeze marks a per-class test split once, so
every later export and every model comparison scores against the same
frozen items. GET /curation/projects/{project}/test_holdout/stats shows it. Items in the
holdout are protected: a class merge that would move them is refused.
Multi-class YOLO export
curl -s -X POST "$API/curation/projects/$P/export/yolo" -H 'content-type: application/json' -d '{}'
curl -s "$API/curation/projects/$P/export/status"
- Always every class.
POST /export/yolohas noclassesfield and rejects unknown keys. To train on a subset, either export with/export/single_class, or passhyperparameters.include_classestoPOST /curation/projects/{project}/train/start. - Split by source image. Items cut from one image always land in the
same split. Images holding a frozen holdout item go to
testwith all their objects; the rest of that class is split between train and val. - One image file and one label file per source image
(
images/<split>/<image_id>.<ext>,labels/<split>/<image_id>.txt), onecls cx cy w hline per validated object, relative to the full image. - Partially labeled images. By default an image is exported with its
validated objects even if it holds unlabeled ones, and the manifest
counts them (
unlabeled_items_on_exported_images); training preflight then warns (export_unlabeled_objects), because the detector will learn those objects as background.require_fully_labeled_images: trueleaves such images out instead. - Imported data round-trips. A frozen test split, negative frames (images with no box) and split assignments written by a dataset import are honored by the export, and an OpenProcessor export can be imported into another project with its labels, splits and frozen test SHA intact.
dataset_shahashes the written label content (which frames, which split, which boxes) plus the ordered class-name list, so a split change, a corrected box or a class rename all change it.- The export's
currentsymlink flips atomically on success, and training defaults to it.
Single-class and class-subset export
POST /curation/projects/{project}/export/single_class builds a narrowed dataset for one
class or a subset (class_ids), adds background and hard-negative frames
(empty_bg_ratio, human-marked false positives), can export whole frames
or item crops (image_mode), can take boxes from items or from the region
annotation (box_source), and records frozen_test_sha over the test
split's identity. Single-class versions live under
<export_root>/<profile_name>/<version>/ with their own current.
Listing and downloading
GET /curation/projects/{project}/export/datasetslists every export with itskind,dataset_sha, split counts andis_current.GET /curation/projects/{project}/export/registry/{artifact}downloads a frozen artifact of the current export (class_registry.json,data.yaml,manifest.json,label_stats.json); any other name is a404before the filesystem is touched.
Retention
Exports, training runs and caches would otherwise grow without bound.
| What | Policy | Knob |
|---|---|---|
Auto-named exports (YYYYMMDDTHHMMSSZ dirs) | Keep-last, applied best-effort after every successful export | OP_EXPORT_KEEP_LAST (default 5, 0 keeps all) |
| Export backlog | Same planner, by hand | prune_exports.py [--keep-last N] [--apply] |
| Finished training runs and bake-off output | Keep-last, by hand | prune_training_runs.py [--keep-last N] [--bakeoff-out-keep-last N] [--apply] |
| Crop cache | Pruned back toward 90% of the cap once exceeded (checked every ~256 ingest writes) | OP_CROP_CACHE_MAX_BYTES |
| Event log | Rotates to a single .1 backup | OP_EVENT_LOG_MAX_BYTES |
Both scripts live in scripts/curation/ and ship in the api image. Run them there:
docker compose exec -T api python scripts/curation/prune_exports.py --project $P # dry run
docker compose exec -T api python scripts/curation/prune_exports.py --project $P --apply
docker compose exec -T api python scripts/curation/prune_training_runs.py --project $P --keep-last 10
The dry runs of both scripts were run in the api container against a public
sample project. They printed Nothing to prune under data/projects/<slug>/exports (keep_last=5). and 0 training run(s) planned for removal (keep_last=10) with
Dry-run only. Pass --apply to remove. The --apply forms were not run.
Export pruning never removes a custom-named export directory, the
target of the current symlink, or any export a training job, a finished
run's lineage, or a queued/running bake-off still references. Training-run
pruning never removes a non-terminal run, a promoted run, or one a bake-off
still references, and never touches MLflow. Both scripts are dry runs
unless you pass --apply, and each visits every active and archived
project under that project's own binding (--project narrows it to one).