DocsCommands
shoots develop
Personal develop-setting predictor — the local "Lightroom AI", limited to the global look. It learns your develop style from your own catalog and predicts a per-image starting point for a new shoot.
shoots develop <subcommand> [options]
Everyday pair — these wrap the steps below with conventional paths under
~/.shoots/develop, so nothing has to be remembered between sessions:
| Subcommand | Purpose |
|---|---|
init |
Learn your style from an edited catalog (export + train) |
edit |
Develop a shoot with it (export + predict) |
feedback |
How much of that prediction you kept |
refine |
Close the loop on a developed shoot: feedback + learn + calibrate |
calibrate |
Fix what it is wrong by on average (a constant offset) |
learn |
Refit on a shoot you developed, weighted by how much you changed |
status |
What this machine holds |
clean |
Drop the per-shoot working files |
The individual steps, still there whenever the convention is not what you want:
| Subcommand | Purpose |
|---|---|
export |
Build a training dataset from an edited catalog |
refresh-targets |
Re-read an existing dataset's targets without recomputing pixels |
train |
Fit a per-catalog develop profile |
predict |
Apply a profile → predicted develop vector / XMP sidecar |
diagnose |
Style-clustering diagnostic |
export is the only step that touches ONNX / exiftool for pixels;
refresh-targets needs exiftool but no image decoding. train, predict and
diagnose are pure maths over the exported dataset.
For the conceptual background — what is predicted, why deltas, how to read the go/no-go metric — see the Develop predictor guide.
The everyday pipeline
# once, from a catalog you have already developed
shoots develop init ~/Catalogs/2025
# per shoot — sidecars land next to the photographs
shoots develop edit ~/Shoots/2026-07-19
# after developing them in Lightroom — the whole loop, in order
shoots develop refine ~/Shoots/2026-07-19
# when the working files pile up
shoots develop clean
Everything lives under ~/.shoots/develop (override the root with SHOOTS_HOME):
develop/
export/export.jsonl the training dataset ← init
profile/export.json the fitted style profile ← init, calibrate
feedback.jsonl every correction, ever ← feedback → calibrate
export/shooting/<folder>/ one directory per shoot ← edit
export.jsonl what the shoot looks like
prediction.json what we proposed → feedback
feedback.jsonl is the one file here that cannot be rebuilt — clean never
touches it, --all or not.
Every one of these accepts --dry-run.
shoots develop init
export --edited-only + train, into the conventional paths.
shoots develop init <path> [options]
| Option | Default | Description |
|---|---|---|
--out-export <file> |
~/.shoots/develop/export/export.jsonl |
Training dataset path |
--out-train <file> |
~/.shoots/develop/profile/export.json |
Profile path |
--name <name> |
my-style |
Profile name |
--baseline <mode> |
external |
Baseline render (note: not the export default) |
--everything |
off | Export every file, not only those carrying an edit |
--dry-run |
off | Print the steps and the paths, write nothing |
It also takes every train flag — --lambda, --folds, --group-by,
--gate-threshold, --embedding-dim, --all — plus --model,
--concurrency and --editor. If the export fails it stops rather than
training on a half-written dataset.
shoots develop edit
export + predict over one shoot. The sidecars are written next to the
photographs, which is where Lightroom looks for them.
shoots develop edit <path> [options]
| Option | Default | Description |
|---|---|---|
--profile <file> |
the one init wrote |
Profile to apply |
--treatment <t> |
auto |
auto | color | bw |
--camera-profile <name> |
catalog's own | Base rendering to assume and write out |
--baseline <mode> |
external |
Must match the profile's, or predict refuses |
--force |
off | Overwrite sidecars that already carry a real edit |
--dry-run |
off | Print the steps and the paths, write nothing |
It refuses to overwrite your work. Writing into the photo folder means an existing edit would be replaced, so it counts the sidecars that carry one and stops.
--forceproceeds;--dry-runshows the plan first.
shoots develop status
What is on this machine: the dataset, the profile (name, age, skill, how many parameters are gated) and the cached shoots.
shoots develop status [--json]
shoots develop clean
Removes the per-shoot working directories. Both files in them are regenerable — the dataset from the photographs, the record by predicting again.
shoots develop clean [--all] [--dry-run] [--json]
--all also removes the training dataset and the profile, which cost a full
export and a train to rebuild. The profile is not touched otherwise.
shoots develop export
Extract CLIP embeddings, colour features and crs develop targets into a JSONL
dataset.
shoots develop export <path> --out <file> [options]
Arguments
| Argument | Required | Description |
|---|---|---|
<path> |
yes | Folder (recursive) or single file of RAW/edited images carrying develop settings |
Options
| Option | Default | Description |
|---|---|---|
--out <file> |
required | Write the JSONL dataset here |
--baseline <mode> |
embedded-preview |
Baseline render strategy: embedded-preview | external |
--edited-only |
off | Only run the expensive embedding/render on files that actually carry develop settings |
--model <kind> |
onnx |
Inference backend |
--concurrency <n> |
4 |
Max parallel jobs |
--json |
off | Machine-readable JSON on stdout |
--verbose |
off | Verbose logging on stderr |
--edited-only
Reads crs settings from the (cheap) sidecars first, and runs the expensive
embedding + render only on files that carry develop settings. On a mixed catalog
this is the difference between minutes and hours.
Unedited frames carry no target, but they do describe the session. Whether that is worth exporting them is a measured question, and on the reference catalog the answer is no: session descriptions built from all 2421 frames instead of the 553 edited ones moved the weighted skill by 0.02pp. Keep using it — it is four times faster here for no measurable loss.
Do not use it when exporting a new, unedited shoot to predict on — there is nothing to filter on yet, and you would export an empty dataset.
--baseline
Photometric features must come from a render of the image before the edit. Two strategies, both editor-agnostic:
| Mode | What it renders | Trade-off |
|---|---|---|
embedded-preview (default) |
The RAW's embedded JPEG preview | Zero setup, but an approximation — the preview bakes in the camera's picture style, polluting exposure/tone features. Expect absolute-luminance parameters (exposure/whites/blacks) to stay near your mean. |
external |
A neutral, camera-independent render via a stand-alone RAW developer | Accurate scene exposure. Zero-config: provisions LibRaw dcraw_emu into ~/.shoots on first use. |
Override the external developer with your own:
export SHOOTS_RAW_DEVELOPER=dcraw_emu
# default args already target dcraw_emu: -w -W -o 1 -q 0 -T -Z {out} {in}
# …or RawTherapee-cli with a neutral profile
export SHOOTS_RAW_DEVELOPER=rawtherapee-cli
export SHOOTS_RAW_DEVELOPER_ARGS='-Y -q -o {out} -p neutral.pp3 -c {in}'
{in} / {out} are substituted per file; the render goes to a temp dir. Only RAW
files are re-rendered — rendered formats use their own pixels, and CLIP always
stays on the embedded preview (it is colour-invariant).
The chosen strategy is recorded in both the dataset and the resulting profile.
Target-leak note. For proprietary RAW (CR3/NEF/ARW) the embedded preview is the camera JPEG — edit-independent, so no leak. DNG with updated previews can leak the edit into the features and should be checked.
Examples
# Training set from an edited catalog
shoots develop export ~/Catalogs/2025-edited --edited-only --out train.jsonl
# Same, with the accurate neutral baseline
shoots develop export ~/Catalogs/2025-edited --edited-only \
--baseline external --out train-neutral.jsonl --verbose
# A new, unedited shoot to predict on — no --edited-only
shoots develop export ~/Shoots/2026-07-new --out new.jsonl
Wrote develop dataset to train.jsonl: 3412 images, 2871 with develop settings (baseline: embedded-preview).
Dataset format
JSONL — one record per line, plus a trailing _type: "develop-meta" line carrying
the model, dimensions, baseline and summary.
{
"file": "…/IMG_0001.CR3",
"embedding": [/* 512 floats */],
"features": [/* colour/photometric features */],
"develop": { "Exposure2012": 0.35, "Contrast2012": 12, "Temperature": 5400, "…": 0 },
"asShot": {
"tempAsShot": 5200, "tintAsShot": 8,
"iso": 800, "exposureComp": -0.33, "camera": "Canon EOS R5"
},
"treatment": "color",
"baseProfile": "Adobe Standard v2",
"look": "Adobe Color",
"curve": [0, 0, 32, 22, 128, 128, 255, 255]
}
| Field | Meaning |
|---|---|
develop |
The crs settings present on the file. Absent keys are neutral. |
asShot |
Camera reference state — the delta reference for white balance |
treatment |
color or bw, derived deterministically from the edit (GrayMixer ⇒ bw) |
baseProfile |
The crs:CameraProfile the edit sat on — the base only |
look |
The creative profile layered over it, e.g. Adobe Color (see below) |
curve |
Flattened point tone curve [x0,y0,x1,y1,…]; absent when linear/default |
The trailing meta line additionally carries looks: each distinct Look's own
serialization, stored once for the whole dataset rather than on every record.
shoots develop refresh-targets
Re-read an existing dataset's supervised targets without recomputing a single pixel.
shoots develop refresh-targets --data <file> --out <file> [options]
Options
| Option | Default | Description |
|---|---|---|
--data <file> |
required | Existing dataset from develop export |
--out <file> |
required | Write the refreshed JSONL dataset here |
--editor <id> |
acr |
Which editor's develop settings to read |
--drop-unedited |
off | Drop records carrying no real edit instead of keeping them for session context |
--json |
off | Machine-readable JSON on stdout |
--verbose |
off | Verbose logging on stderr |
Why it exists
The expensive half of export is the CLIP embedding and the neutral baseline
render. The targets are a cheap pass over the editor's sidecars. When the
target side changes, re-exporting recomputes hours of features that did not
change — on a 1045-image catalog, 7 minutes instead of hours.
Reach for it whenever the target side moves:
- a
crstag was read under the wrong name and is now fixed, - the schema gained a parameter, or the base rendering gained the Look,
- the definition of "edited" got stricter.
It rebuilds develop / asShot / baseProfile / look / curve / treatment
and keeps embedding / features exactly as they were. The output is the
dataset a fresh export would have produced today, so records that no longer
qualify as edited are dropped — and counted, never silently.
Files it cannot read (moved, or the share is offline) are carried through untouched and reported, rather than silently turning a real edit into an empty one.
Examples
# After a fix to the target side — then retrain on the refreshed dataset
shoots develop refresh-targets --data train.jsonl --out train-v2.jsonl
shoots develop train --data train-v2.jsonl --name my-style --out profiles/my-style.json
Refreshed 553/553 records → train-v2.jsonl
Records that no longer carry a real edit are reported on their own line
(dropped N no longer carrying a real edit), as are files that could not be read.
refresh-targetsnever touches your catalog. It reads sidecars and writes one new dataset file.
shoots develop train
Fit the per-catalog develop profile: a multi-output ridge regression over develop deltas.
shoots develop train --data <file> --name <name> --out <file> [options]
Options
| Option | Default | Description |
|---|---|---|
--data <file> |
required | Dataset from develop export |
--name <name> |
required | Profile name |
--out <file> |
required | Output profile JSON path |
--lambda <n> |
auto |
Ridge strength for every parameter, or auto to choose one per parameter by cross-validation |
--folds <k> |
5 |
Cross-validation folds |
--embedding-dim <k> |
16 |
CLIP components to keep; 0 drops the embedding, a high value keeps it raw |
One model is trained per treatment over shared + <branch>, so a
high-contrast B&W edit and a light colour edit never average into a mush.
Each image is described alongside its whole shoot
Most of a develop decision is "this shoot", not "this frame" — on a real catalog the session accounts for 26–67% of the variance of the targets. So every image also carries the mean photometric description of its folder, which is the largest single accuracy gain in this tool.
Two consequences worth knowing:
- The description uses every record in the dataset, edited or not. Exporting
the unedited frames as well is therefore possible, but measured neutral on the
reference catalog (0.02pp) — so
--edited-onlyremains the sensible default. - Predict on a shoot, not on a file. A frame's prediction depends on what
else is in its folder.
predictwarns when images sit alone in theirs.
A branch with too few images cannot afford the extra columns and skips them; the report says which did:
session context: 44 features describing each image's whole shoot
session context: off — too few images in this branch to afford it
The CLIP embedding is compressed
512 dimensions against a few hundred photographs is p≫n, and carrying them raw
measured worse than dropping them. --embedding-dim (default 16) projects onto
that many principal components, refitted inside every fold. 0 drops the
embedding; a value at or above its dimension keeps it raw.
Regularization is per parameter
Exposure and the HSL sliders do not want the same amount of shrinkage. Under a single shared λ the choice is made by an average that the unpredictable majority of parameters dominates — on a real catalog that pinned λ to the top of the grid and collapsed every parameter onto the photographer's mean, which from the outside looks like "it predicts the same settings for every photo".
--lambda auto therefore picks a λ per parameter, reported in the λ column of
the table and summarised in the branch header:
λ per param (auto): 30000×62 100×6
Everything piling onto the top of the grid means the model cannot read this catalog — the same information the skill column carries, one line earlier.
The gate pays for that search: λ is re-chosen inside each held-out fold, so no parameter is ever scored on the split that picked its own λ. Selecting on the folds you then report would hand each of ~90 parameters the best of six tries, and that alone is enough noise to push unpredictable sliders past the gate.
Reading the output — the go/no-go GATE
train reports, per parameter, the held-out MAE of the model versus the
"apply my average edit" baseline, plus a skill score:
skill = 1 − modelMae / baselineMae
skill > 0→ the model beats simply applying your mean edit.- The headline number is the weighted skill over the image-dependent parameters (exposure, WB, contrast, highlights/shadows, dehaze/vibrance).
- Style-constant parameters (HSL, colour grading) are expected to collapse to your mean. That is correct behaviour, not a failure.
If the headline skill is not clearly positive on a real catalog, the signal is too weak — reconsider the baseline render strategy before building anything on it.
Examples
shoots develop train --data train.jsonl --name my-style --out profiles/my-style.json
# Explicit regularization and more folds
shoots develop train --data train.jsonl --name my-style \
--out profiles/my-style.json --lambda 2.5 --folds 10
shoots develop predict
Apply a trained profile to a new develop-export dataset.
shoots develop predict --data <file> --profile <file> [options]
Options
| Option | Default | Description |
|---|---|---|
--data <file> |
required | Dataset from develop export (the new set) |
--profile <file> |
required | Profile JSON from develop train |
--treatment <t> |
auto |
Which branch to apply: auto | color | bw |
--camera-profile <name> |
catalog's own | Base rendering to assume and write out |
--out <file> |
stdout | Write predictions JSON here |
--xmp <dir> |
— | Also write a Lightroom-readable .xmp sidecar per image into this dir |
--treatment is a genuine creative choice at inference time — at train time it is
read off the edit, but for a new frame nobody has decided colour vs B&W yet.
The base rendering is written into the sidecar
Every predicted slider is relative to the rendering the photograph starts from,
and in ACR that is a camera profile plus an optional Look: "Adobe Color" is
Adobe Standard v2 with a <crs:Look> element on top, not a crs:CameraProfile
value. Without an explicit profile in the sidecar Lightroom falls back to its own
legacy default (Adobe Standard), so a style learned on Adobe Color lands on a
different base and every slider is measured against the wrong starting point.
predict therefore writes the profile and replays the Look element verbatim, and
reports what it chose:
Rendering: Adobe Standard v2 + Adobe Color (208 images)
Rendering: Camera Faithful v2 (203 images)
An unedited file states no rendering, which is the normal case — the branch's
most common rendering stands in. --camera-profile overrides it and accepts
either a bare profile name or a full key:
shoots develop predict --data new.jsonl --profile profiles/my-style.json \
--camera-profile "Adobe Standard v2 + Adobe Color" --xmp ./out-xmp/
A Look read from embedded crs (DNG/JPEG rather than a sidecar) has no element to
lift; predict emits the base profile and says so instead of pretending the
rendering is complete.
The new set must use the same --baseline as the profile
An embedded camera JPEG and a neutral external render put the same photograph at
a different luminance, contrast and white point. A profile trained on one and
applied to the other reads a feature vector from a space it has never seen, and
the dimensions match either way, so nothing else can catch it. predict refuses
the pair outright:
error: profile was trained on baseline 'external' but the dataset was exported
with 'embedded-preview' — the colour features are not comparable across
baselines. Re-export with `--baseline external`.
The profile records which baseline it was trained on; pass the same flag when exporting the set you want to predict on.
Examples
# Export the new shoot, then predict. --baseline must match the profile's.
shoots develop export ~/Shoots/2026-07-new --baseline external --out new.jsonl
# Colour treatment, XMP sidecars for Lightroom
shoots develop predict --data new.jsonl --profile profiles/my-style.json \
--treatment color --xmp ./out-xmp/
# Predictions as JSON for inspection
shoots develop predict --data new.jsonl --profile profiles/my-style.json \
--out predictions.json
# A B&W variant of the same set
shoots develop predict --data new.jsonl --profile profiles/my-style.json \
--treatment bw --xmp ./out-xmp-bw/
--xmp drops a Lightroom-readable sidecar next to each image — a non-destructive
starting point to refine, not a finished edit.
shoots develop feedback
Compare a prediction against what the files say now — the only real-world quality signal this tool has.
shoots develop feedback --predictions <file> [options]
Options
| Option | Default | Description |
|---|---|---|
--predictions <file> |
required | The JSON written by develop predict --out |
--editor <id> |
acr |
Which editor's develop settings to read |
--out <file> |
— | Write this run's (predicted, corrected) pairs here as JSONL |
--journal <file> |
~/.shoots/develop/feedback.jsonl |
Journal to accumulate into |
--no-journal |
off | Measure this run without recording it |
--min-moved <n> |
scaled to the pool | Comparisons a parameter needs to be listed |
--json |
off | Machine-readable JSON on stdout |
--verbose |
off | Verbose logging on stderr |
It accumulates — one shoot is not a measurement
A per-parameter "kept %" over six images has a fifteen-point standard error, so the table has a floor under it. A shoot of ten photographs can never clear a flat floor of 20, which is why every run is also written to the journal and the breakdown is computed over everything recorded so far — this run's own acceptance is reported separately, above it.
Observations are per image and say nothing about which shoot they came from, so ten shoots of eight are worth as much as one shoot of eighty. An amateur with small sets still gets an answer; it just takes six months instead of one afternoon.
- The floor scales with the pool: never fewer than 3, never more than a quarter
of it, never more than 20.
--min-movedoverrides it. - Rows marked
·cleared the floor on fewer than 20 comparisons — directional, not yet a rate. - Newest wins per file, so re-running feedback on a shoot corrects the old observation instead of counting it twice.
develop statusreports how far the journal has got.develop cleannever removes it,--allor not: it records what photographs looked like the day they were developed, and nothing can rebuild that.
Why it is not refresh-targets
refresh-targets rebuilds a dataset to match the files as they are now, which
throws away exactly what matters here: what they were before you touched
them. The pair (predicted, corrected) isolates the model's error rather than
your style, and it is worth more per sample than a fresh catalog edit.
Reading it
this shoot kept 3.4% of the parameters either of us moved
(58.4% counting the sliders we both left at neutral —
that number flatters the model and is not the one to quote)
journal kept 3.6% over 21 images from 2 shoots
over the journal (21 images), listed from 6 comparisons up:
param moved kept journey corrected by offset
Temperature 590 0% 91% 463.21 +86.62
Highlights2012 590 0% 51% 21.54 +4.16
Clarity2012 251 0% -12% 8.27 -0.14
Dehaze 14· 7% 22% 6.02 +1.30
| Column | Meaning |
|---|---|
moved |
Comparisons where at least one of you left neutral. Agreeing that a slider stays at zero is not a prediction. · marks fewer than 20 — directional, not yet a rate. |
kept |
Left untouched — the product metric. Held-out skill is its proxy. |
journey |
How much of the move the prediction already made. Negative = further off than doing nothing. |
corrected by |
Mean absolute correction, in slider units |
offset |
Mean signed correction. A large offset with a small spread is a missing constant, not a modelling failure — and a constant is easy to fix. |
What it cannot tell. This compares a prediction against whatever the file says today. If the sidecar was never imported and the photograph was edited from scratch, the gap is two independent opinions rather than the model's error. Run it on a set you actually developed from the sidecars.
shoots develop calibrate
Fold the feedback journal back into the profile as per-parameter offsets — the only step that improves predictions from evidence the catalog does not contain.
shoots develop calibrate [options]
Options
| Option | Default | Description |
|---|---|---|
--profile <file> |
~/.shoots/develop/profile/export.json |
Profile to calibrate |
--journal <file> |
~/.shoots/develop/feedback.jsonl |
Journal to read |
--shrink <n> |
0.5 |
Fraction of each measured correction to apply |
--min-shoots <n> |
4 |
Shoots a parameter needs before it is offset |
--include-in-sample |
off | Also use observations the model was already fitted on (optimistic — it says so) |
--imported-only |
off | Use only observations still carrying the rendering predict wrote |
--reset |
off | Remove the calibration, leaving the model as trained |
--dry-run |
off | Show the decision, write nothing |
--json |
off | Machine-readable JSON on stdout |
Reading it
journal: 21 images from 2 shoots
21/21 still carry the rendering we wrote (100% — evidence the sidecars were imported)
applying 50% of each measured correction, from 10 comparisons up
color — 385 images across 5 shoots, 3 parameters offset
param shoots up/down sigma measured applied
SplitToningShadowHue 5 0/5 2.2 -18.71 -9.36
Saturation 5 5/0 2.2 +3.71 +1.85
Contrast2012 5 4/1 1.3 +1.18 0.00 (no direction)
| Column | Meaning |
|---|---|
shoots |
Shoots behind the estimate, not photographs — a take edited by pasting settings across it is one decision, not four hundred |
up/down |
How the shoots split by direction. This is the test: a lopsided split is a habit, an even one is one job differing from another |
sigma |
How lopsided, in standard deviations of a fair coin. Below 2 nothing is applied — which puts the hard floor at four shoots, since three unanimous only reach 1.73 |
measured |
Median across shoots, of the median within each shoot. Median at both levels, so neither one odd frame nor one odd job can drag it |
applied |
…after --shrink. Half, by default |
White balance is corrected as a ratio, not in absolute Kelvin: Temperature is
anchored to the as-shot value and lives in log-Kelvin, so a constant there is a
multiplier. The measured column shows log units for those parameters.
One shoot is one vote
A take is edited by pasting settings across it, so counting photographs counts a single styling decision hundreds of times. Measured on a simulated wedding — 400 frames pushed +6 against two smaller shoots pulling −5 and −6 — counting per photograph reported 16 sigma of confidence and applied +3, the direction two shoots out of three disagreed with. Per shoot the same evidence gives 0.6 sigma and applies nothing, which is the honest answer for three shoots.
This is the reasoning that already makes the trainer hold out whole folders. It matters to an amateur too: six shoots of eight frames are six votes, not 48.
What counts as held out is where the prediction came from, not where the
photograph ended up. A pair recorded before its photograph became training data
stays a genuine measurement forever — folding the file in afterwards cannot reach
back and change a number already written down. Only a prediction made by a model
that had already seen that photograph's answer is worthless, which takes going
round the loop twice on one shoot — a fresh develop edit after a learn.
feedback decides it by comparing two instants: the one predict stamped on the
prediction record, and the one learn recorded when it folded the photograph into
training. Later prediction ⇒ in-sample. Re-running feedback on a record that has
not changed is therefore not going round the loop again, and does not cost the
shoot its evidence. --include-in-sample uses the in-sample pairs anyway.
Why it only ever proposes a constant
The photographer edits from the sidecar, so every observation is partly a reaction to what was proposed rather than an independent opinion. Feeding that back as ground truth teaches the model its own output was right — repeat it and the predictions stop tracking the photographs and start tracking themselves.
An offset is the one correction where that anchoring is safe: a photographer who accepts a value they would have pushed further only makes the measured offset smaller, so the estimate errs toward under-correcting. Applying half of it means calibrating again after the next shoot takes half of what is left — the loop converges (measured: exactly 0.50× residual per round) rather than overshoots.
Growing the training set with the developed shoot is the other half of the idea
and is deliberately not this command: that is export + train, on files
you edited rather than files you approved.
The offsets sit beside the model in
profile.calibration, never merged into its weights —--resettakes them back,predictreports how many it carries, and a retrain invalidates them (bothpredictandstatussay so).
shoots develop refine
Close the loop on a shoot you have developed: feedback → learn → calibrate,
with the conventional paths, in the only order that works.
shoots develop refine <shoot> [options]
Options
| Option | Default | Description |
|---|---|---|
<shoot> |
required | The folder you ran develop edit on and have since developed |
--measure-only |
off | Stop after feedback: record what you kept, change nothing |
--shoot-dir <dir> |
conventional | The shoot's working directory, if not the usual one |
--data, --profile, --journal |
conventional | Paths for the three steps |
--dry-run |
off | Print the steps and the paths, write nothing |
It also takes the flags of the steps it wraps: --min-weight / --max-weight
(from learn), --shrink / --min-shoots (from calibrate), and the train
flags --lambda, --folds, --group-by, --gate-threshold, --embedding-dim.
Why the order is fixed
| Step | Why there |
|---|---|
feedback |
The only step that captures the (predicted, corrected) pair. The journal outlives clean; the shoot's working files do not. |
learn |
Refits with the shoot folded in, weighted. The only step that moves the part of the prediction that varies photograph to photograph. |
calibrate |
Re-measures the constant offsets against the model that just came out of learn. |
learn writes a whole new profile, so a calibrate run before it is silently
thrown away. That is the whole reason this command exists.
Expect
calibrateto say nothing for your first three shoots. Four is the statistical floor for an offset, not a policy — three shoots agreeing unanimously only reach 1.73 sigma. Steps 1 and 2 do their full work on every pass regardless, and the journal only grows, so from the fourth shoot on the third step contributes too and keeps contributing.refinereports it and exits successfully either way.
Repeating it on the same shoot
refine stores nothing twice — the journal and the training dataset are both keyed
by absolute file path, newest wins — and repeating it is safe, but it is not
free.
Safe, because of how "in-sample" is decided. An observation is worthless only when
its prediction came from a model that had already been fitted on that photograph,
so feedback compares the instant predict stamped on the record against the
instant learn folded the photograph in, and marks it in-sample only when the
prediction is the later of the two. Re-measuring a prediction that has not changed
therefore stays the clean held-out measurement it was — including after learn has
folded the shoot in. Only genuinely re-running develop edit after a learn
produces an in-sample pair, which is the one case that cannot measure anything.
Journals written before this existed have no timestamps to compare and fall back on the boolean recorded at the time. If a repeated
refinehas already marked a shoot in-sample, one morefeedbackon it re-measures and clears the flag — under the scheduler that needsshoots schedule run --force, since an unchanged shoot is otherwise skipped.
Not free, because learn refits, and a refit writes a whole new profile — which
discards the calibration offsets measured against the old one. On identical data
that is minutes of work to arrive where you already were. Run it once per shoot you
develop, and let shoots schedule decide when: it skips a shoot
whose photographs and sidecars have not moved since the last pass.
shoots develop learn
Fold a shoot you have already developed back into the training set — weighted by how much of the prediction you had to change — and refit the profile.
shoots develop learn <shoot> [options]
calibrate moves the average of the predictions. It cannot move the part that
varies photograph to photograph, because a constant cannot track a variable —
and that varying part is most of what anyone would call an eye. Only refitting
the model moves it, and this is the refit.
Options
| Option | Default | Description |
|---|---|---|
<shoot> |
required | The folder you ran develop edit on and have since developed |
--data <file> |
~/.shoots/develop/export/export.jsonl |
Training dataset to fold into |
--out <file> |
~/.shoots/develop/profile/export.json |
Profile to refit |
--shoot-dir <dir> |
conventional | The shoot's working directory, if not the usual one |
--min-weight <n> |
0.25 |
Floor for a frame you accepted wholesale |
--max-weight <n> |
3 |
Ceiling for a frame you overhauled |
--no-train |
off | Update the dataset but stop before refitting |
--dry-run |
off | Show the weighting and the plan, write nothing |
--name, --lambda, --folds, --group-by, --gate-threshold, --embedding-dim, --all |
— | As train |
Nothing is recomputed: develop edit already left the shoot's embeddings and
colour features on disk, and only the targets changed when you developed the
photographs. They are re-read from the sidecars and merged by file — same file
twice means the newer state wins.
Reading it
12 developed frames, weighted by how much you changed:
1 you overruled (weight ≥ 1.5 — these drive the refit)
11 you adjusted (around 1, like an ordinary catalog edit)
0 you accepted (weight ≤ 0.6 — mostly our own output coming back)
file weight correction
R-5686.CR3 1.80 0.38
R-5682.CR3 1.34 0.29
R-5692.CR3 0.81 0.17
(correction 1.00 = the median for this shoot, 0.2132 in standardized units)
Why the weight is the whole point
z_i = mean over parameters of |actual − predicted| / spread(parameter)
weight_i = clamp(z_i / median(z), 0.25, 3)
Editing from a prediction contaminates the target: a frame you accepted almost untouched is largely the model's own output coming back as ground truth, and a model trained on its own predictions collapses onto its own habits. Those are exactly the frames with the smallest correction — so weighting by correction size down-weights the contaminated samples without having to identify them. The frames that dominate the refit are the ones where you overruled the model: the least anchored and the most informative at once.
Normalizing against the median correction rather than an absolute scale keeps this working as the model improves — when predictions get better every correction shrinks, and a fixed scale would quietly stop weighting anything.
Weights are in the fit, never in the score. Letting them into the held-out error would let the same choice that emphasized a row also decide how well the model did on it. The GATE keeps measuring what it always measured, so before and after are comparable. Standardization stays unweighted too, so a corrected shoot cannot quietly redefine the constant a gated parameter emits — that is
calibrate's job.
A refit invalidates the calibration: its offsets described a model that no longer exists. The order is refit → develop a shoot →
feedback→calibrate.
shoots develop diagnose
Style-clustering diagnostic: compares pooled prediction skill against per-style (clustered) skill.
shoots develop diagnose --data <file> [options]
Options
| Option | Default | Description |
|---|---|---|
--data <file> |
required | Dataset from develop export |
--folds <k> |
5 |
Cross-validation folds |
--max-k <k> |
4 |
Max number of style clusters to try |
Use it when pooled training underperforms. If clustered skill is clearly better, your catalog holds several distinct looks (a moody set and an airy set, say), and a single pooled model is averaging them into mush. The fix is to split the catalog and train one profile per look.
shoots develop diagnose --data train.jsonl
shoots develop diagnose --data train.jsonl --max-k 6 --folds 10
Doing it by hand
The everyday pipeline at the top of this page is these steps with the paths filled in. Reach for them directly when you want a dataset somewhere specific, a second profile, or the diagnostic in between:
# 1. Training dataset from your edited catalog
shoots develop export ~/Catalogs/2025-edited --edited-only --baseline external --out train.jsonl
# 2. Fit the profile — read the GATE output carefully
shoots develop train --data train.jsonl --name my-style --out profiles/my-style.json
# 2b. Weak result? Check whether you have multiple styles
shoots develop diagnose --data train.jsonl
# 3. Export the new shoot. Same --baseline as step 1, or predict refuses the pair.
shoots develop export ~/Shoots/2026-07-new --baseline external --out new.jsonl
# 4. Predict, as XMP sidecars — keep --out, `feedback` needs it later
shoots develop predict --data new.jsonl --profile profiles/my-style.json --treatment color --xmp ./out-xmp/ --out predictions.json
# 5. After developing them, see how much of the prediction survived
shoots develop feedback --predictions predictions.json
Upgrading, rather than starting fresh? When the target side changed — a fixed
tag, a new schema parameter, a stricter "edited" test — step 1 is
refresh-targets instead of a re-export, and the features are reused:
shoots develop refresh-targets --data train.jsonl --out train-v2.jsonl
shoots develop train --data train-v2.jsonl --name my-style --out profiles/my-style.json
See also
- Develop predictor guide — what is predicted and why
- Configuration —
SHOOTS_RAW_DEVELOPERand friends