DocsCommands
shoots rate
Score images for focus and aesthetics, suggest keywords, and map the result to a 0–5 star rating written to sidecars.
shoots rate <path> [options]
Backed by a local ONNX CLIP model (vit-b32-int8) running on
onnxruntime-node. Everything happens on your machine; nothing is uploaded.
Arguments
| Argument | Required | Description |
|---|---|---|
<path> |
yes | Folder (recursive) or single file to rate |
Options
| Option | Default | Description |
|---|---|---|
--model <kind> |
onnx |
Inference backend. Currently only onnx. |
--profile <name> |
street |
Rating profile: street, generic, portrait, wildlife, wedding, or a learned profile in ~/.shoots/profiles |
--write-xmp |
off | Write .xmp sidecars via exiftool instead of JSON sidecars |
--concurrency <n> |
4 |
Max parallel scoring jobs |
--dry-run |
off | Score and report, but write no sidecars |
--json |
off | Machine-readable JSON on stdout |
--verbose |
off | Verbose logging on stderr |
How a rating is produced
Three stages, all local:
1. Focus score
A technical gate. Below the profile's focusReject the frame is a technical
reject (0 stars) regardless of content. Between focusReject and focusSoft it is
capped at the profile's focusSoftCap.
2. Aesthetic merit — zero-shot, multi-aspect
The image embedding is compared against contrastive aspect prompt pairs
("a well-composed photograph" vs "a badly composed photograph"). Each aspect
yields a probability in [0,1]:
| Aspect | What it probes |
|---|---|
overall |
General photographic quality |
composition |
Framing and structure |
exposure |
Technical exposure quality |
subject |
Subject presence and isolation |
sharpness |
Perceived acuity |
lighting |
Quality of light |
storytelling |
Narrative / moment |
The profile decides how those combine into a single merit score. Aspects a profile does not weight count zero.
3. Stars
The merit score is compared against the profile's descending cut-offs; the first one it clears wins. The focus gate then caps the result if needed.
This is why --profile matters more than any other flag. A street profile
scores content and ignores technical craft; a beginner-friendly profile rewards
nailing exposure and sharpness. Same photo, legitimately different stars. See
Rating profiles.
Keywords
Zero-shot keywording against a vocabulary whose text embeddings ship precomputed inside the model archive — the runtime only needs the image encoder plus a cosine similarity, so no text encoder or tokenizer is loaded.
Output formats
JSON sidecars (default)
<file>.shoots.json, written next to each image:
{
"file": "D:/raw/IMG_0001.CR3",
"model": "clip-vit-b32-int8",
"profile": "street",
"stars": 4,
"scores": { "focus": 0.812, "aesthetic": 0.591 },
"aspects": [
{ "name": "overall", "score": 0.612 },
{ "name": "composition", "score": 0.554 },
{ "name": "exposure", "score": 0.701 },
{ "name": "subject", "score": 0.588 },
{ "name": "sharpness", "score": 0.744 },
{ "name": "lighting", "score": 0.566 },
{ "name": "storytelling", "score": 0.573 }
],
"keywords": ["street", "urban", "candid"],
"generatedAt": "2026-07-28T09:14:22.104Z"
}
The full per-aspect breakdown is recorded regardless of the profile's weights — provenance, so you can re-derive a rating under a different profile without re-running inference.
XMP sidecars (--write-xmp)
<file>.xmp, written via exiftool, carrying XMP:Rating and XMP:Subject.
Lightroom, Bridge and Capture One read these directly.
rate --write-xmprefuses to overwrite an existing.xmpand reports that file as an error. Your own edits are never clobbered. Move or delete the existing sidecars if you truly want to re-rate.
Examples
First look — score without writing anything
shoots rate ./raw --dry-run
★★★★☆ IMG_0001.CR3 focus=0.812 aesthetic=0.591 [street, urban, candid]
★★☆☆☆ IMG_0002.CR3 focus=0.774 aesthetic=0.531 [portrait, indoor]
☆☆☆☆☆ IMG_0003.CR3 focus=0.201 aesthetic=0.498 [blurry, motion]
482/482 rated with clip-vit-b32-int8 (profile: street) (dry run, no sidecars written)
Rate a wedding with the matching profile
shoots rate D:/Shoots/2026/smith-wedding --profile wedding --write-xmp
Lightroom will show the stars on import.
Compare profiles on the same folder
for p in street generic portrait wedding; do
echo "── $p"
shoots rate ./sample --profile "$p" --dry-run --json \
| jq -r '.results[] | "\(.stars)★ \(.file | split("/") | last)"'
done
The best way to pick a profile is to run it on 50 frames you have already judged yourself, and see which one agrees with you.
Use your own learned profile
# Trained via the preference-learning pipeline — see ../preference-learning.md
shoots rate ./raw --profile my-eye --write-xmp
my-eye resolves to ~/.shoots/profiles/my-eye.json. Built-in names win over
user profiles, so avoid naming a learned profile street.
Full JSON pipeline
# The 5-star selects
shoots rate ./raw --json | jq -r '.results[] | select(.stars == 5) | .file'
# Copy 4+ star frames into a selects folder
shoots rate ./raw --json \
| jq -r '.results[] | select(.stars >= 4) | .file' \
| xargs -I{} cp {} ./selects/
# Star distribution
shoots rate ./raw --json | jq -r '.results[].stars' | sort | uniq -c
On a constrained machine
shoots rate ./raw --concurrency 2
ONNX inference holds real memory per parallel job.
JSON output
{
"command": "rate",
"model": "clip-vit-b32-int8",
"profile": "street",
"dryRun": false,
"results": [
{
"file": "D:/raw/IMG_0001.CR3",
"stars": 4,
"focus": 0.812,
"aesthetic": 0.591,
"aspects": [ { "name": "overall", "score": 0.612 } ],
"keywords": ["street", "urban", "candid"],
"sidecar": "D:/raw/IMG_0001.CR3.shoots.json",
"model": "clip-vit-b32-int8"
}
],
"errors": [],
"summary": { "total": 482, "rated": 482, "failed": 0 }
}
sidecar is null under --dry-run.
Exit codes
| Code | When |
|---|---|
0 |
All files rated |
1 |
At least one file failed (e.g. an existing .xmp blocking a write) |
2 |
Unknown --model or --profile, an invalid learned profile, or the model / exiftool could not be provisioned |
Notes and limits
- The ratings are strict by design. The default
streetprofile is unforgiving: on a real shoot the mass of frames legitimately lands at 0–1 stars. That is the point — you want the tail, not a flattering average. Usegenericorweddingfor a more forgiving bar. - Only
streetis calibrated against a real, hand-judged shoot. The others are reasonable starting priors. The fix is not a better preset — it is learning your own profile. - Some genres want aspects the current model lacks — emotion and expression, most obviously for weddings. Those arrive with a future model archive.
- The model is licensed for commercial redistribution. The aesthetic path is deliberately zero-shot CLIP (MIT) rather than an AVA-trained aesthetic scorer, because AVA-derived weights are not commercially clean.
See also
- Rating profiles — what each profile weights, and how to pick
- Preference learning — train a profile on your own eye
cull— non-ML blur detection; run it before ratingembeddings— the profile-neutral export