Manifest reference
argus-curator 0.15.2.dev2+g97021d6 @ 97021d6PyPI (opens in new tab)Source (opens in new tab)Changelog (opens in new tab)
Imported from docs/manifest.md on argus-curator's dev branch at that commit. Fixes go upstream, not here.
An export writes manifest.jsonl into its destination folder: one JSON object
per line, one line per exported image. It is the handoff contract the rest
of the suite reads: argus-lens batch-captions it (an upload to its
/caption/manifest, or a path under its export_root), and argus-forge
builds training sets from it. Curator and lens share one TargetProfile, so a
manifest written here is captioned downstream with no remapping.
This page describes manifest 2.3, the version current releases write, and what changed in each earlier version.
Where it is written
POST /exportandPOST /export/streamwrite it whenwrite_manifestis true (the default), as<dest>/manifest.jsonl.ExportResult.manifest_pathnames the file, or is null when none was written.argus-curator scan ... --copy-to DIR(or--move-to,--symlink-to) writes it asDIR/manifest.jsonl. A scan with no destination writes none.- With
caption_urlset on the export request, curator also POSTs the file to that URL as a multipart upload (fieldmanifest, typeapplication/x-ndjson), for a one-click curate-then-caption run. The response'scaptionedsays whether that POST succeeded.
The file is UTF-8, one row per line, each row a complete JSON object. Rows exist only for files whose transfer succeeded: the manifest is a work list, and never claims a file that is not on disk. Re-exporting into the same destination overwrites the files and rewrites the manifest; each export plans in isolation.
Example row
Pretty-printed here; in the file each row is one line.
{
"manifest_version": "2.3",
"rel_path": "shoots/2026-07/IMG_0412.jpg",
"abs_path": "/data/images/shoots/2026-07/IMG_0412.jpg",
"exported_path": "shoots/2026-07/IMG_0412.jpg",
"exported_abs_path": "/data/out/training-set-v1/shoots/2026-07/IMG_0412.jpg",
"target_profile": {
"target_style": "photo",
"target_backend": "sdxl",
"checkpoint": null,
"target_category": "identity"
},
"primary_face_cluster": "face_2",
"primary_face_pose": "three_quarter",
"score": 0.8712,
"similar_group": 3,
"crop": { "left": 0, "top": 0, "right": 3000, "bottom": 3800 },
"primary_face": {
"bbox": [412.0, 188.0, 301.5, 380.0],
"yaw": -24.1,
"pitch": 3.2,
"roll": -1.8,
"sharpness": 812.4,
"eye_open": 0.29,
"mouth_open": 0.02,
"cluster_distance": 0.12,
"reference_distance": null,
"age_years": 31.4,
"landmarks": null
},
"set_age": { "years": 31.0, "cluster_id": "face_2", "faces": 42, "share": 0.95 }
}
Row fields (2.3)
Every key is always present in a 2.3 row; a field with no value is written as
null, not left out.
target_profile
The style and category values are argus-cortex's shared taxonomy.
crop
{"left", "top", "right", "bottom"}, integers, in pixels of the source as
displayed (after its EXIF orientation), with right and bottom exclusive.
The exported file is that region of the source, written upright. A scan that
cropped anything exports only by copy. See Crops.
primary_face
Measured on the exported file's frame: when the image was cropped, boxes and landmarks are in the cropped image's pixels.
See Face sources for how these are measured and used.
set_age
An age read off the set for frames with no face (--set-age). It is the
scan's, so when present it is the same object on every row, faces or not.
It is an inference about the set, not a reading of any one frame. See An age for the frames with no face.
Paths
exported_path is the path actually written under the export root. Consumers
must use it rather than re-deriving a location from rel_path, because the
two can differ:
- Flattened exports (
preserve_structure: false) de-collide duplicate basenames with a short hash suffix (stem-<hash>.ext). Collisions are detected case-insensitively and Unicode-normalised, so the result is safe on case-insensitive filesystems, and the export fails rather than overwrite if a unique name cannot be generated. - With
carry_sidecars, an image at the destination root whose stem ismanifest,run_statsorcuration_reportgets the same suffix in any export, so its sidecars cannot clash with the export's own files.
Prefer exported_abs_path: joining the export root onto exported_path means
rebuilding the server's layout rule somewhere that does not own it, and the
de-collision suffix cannot be reproduced by a client. A manifest is also often
read somewhere other than the folder it was written into (argus-lens receives
it as an upload), and no row names that folder.
The export response carries the same mappings as exported_paths and
exported_abs_paths (keyed by rel_path), so they are available even with
write_manifest: false.
Versions
2.0 is a new major because a 1.x consumer that derives destinations from
rel_path, or trusts every row to name a file on disk, misreads a 2.x manifest.
Compatibility
- A major bump (1 to 2) breaks the row's shape or meaning. A minor bump only adds fields or fixes behaviour to match what was documented.
- A consumer reads
manifest_versionand refuses a major it does not know, rather than misreading the rows. Within a known major it accepts any minor, ignores fields it does not know, and treats a field missing from an older row as null. - A consumer that needs a field added in a minor version checks that minor
too (for example,
primary_faceneeds 2.3). - To check before exporting, read
manifest_versionfromGET /health(it sits beside the packageversion; the two move independently) or from anyExportResult, which declares it whether or not a manifest was written.
argus-forge, for example, accepts majors in its SUPPORTED_MANIFEST_MAJORS
and requires exported_path from 2.0 on.
Schema
The row is published as ManifestRow (with ManifestFace, FaceLandmarks,
CropBox, SetAge and TargetProfile) in the wire-contract JSON Schema,
schema/curator-wire.schema.json (opens in new tab).
argus-curator schema regenerates it from the models, and CI fails if the
committed copy is stale. In Python, argus_curator.models.ManifestRow reads a
row:
from argus_curator.models import ManifestRow
with open("manifest.jsonl", encoding="utf-8") as f:
rows = [ManifestRow.model_validate_json(line) for line in f if line.strip()]
MANIFEST_VERSION in argus_curator.models is the version this package
writes.