Configuration reference¶
Configuration keys krites reads, with types, defaults, and the environment-variable mapping.
How configuration works¶
krites uses the go-tool-base config layer: a YAML config file (written by
krites init, inspect the resolved values with krites config), overlaid by
environment variables. Every key <section>.<key> maps to an environment
variable KRITES_<SECTION>_<KEY>, uppercased with dots turned into underscores:
remove.enabled is KRITES_REMOVE_ENABLED. Config is hot-reloaded.
The heavy providers run locally and are off by default; turn each one on separately. Their models and the ONNX Runtime library download automatically on first use, verified and cached, so turning a capability on needs no manual downloads or paths.
ONNX Runtime¶
Face/eye analysis and object removal run on ONNX Runtime. krites obtains the
runtime library itself (bundled inside the macOS app, or fetched from the
phpboyscout approved artefact channel and cached), so you never install or
locate libonnxruntime. The resolution order is: onnxruntime.library_path (if
set) → a library bundled beside the executable → the cache → the channel.
The channel serves a signed manifest naming each file's digest and length, and
krites accepts it only when the release key embedded in the binary and the key
published over WKD agree. If that check can't be made (the key server is
unreachable, say), resolution fails rather than proceeding on one anchor.
Setting onnxruntime.library_path bypasses the channel entirely, which is the
escape hatch for an air-gapped machine or a platform with no published build.
krites previously downloaded the runtime straight from the upstream project's release page and checked it against a digest compiled into the binary. A digest pinned that way cannot be rotated or revoked and says nothing about who published the bytes; the artefacts it resolves today are the same files, with provenance attached. See 0013 §3.1.
| Key | Type | Default | Meaning |
|---|---|---|---|
onnxruntime.library_path |
string | (auto) | Optional override: a path to your own libonnxruntime. Leave unset to auto-provision. Shared by face analysis and object removal. |
Global settings vs per-shoot overrides¶
Configuration is a two-layer cascade:
- Global (
~/.krites/config.yaml) is the base. Set it from the studio's Settings panel (the gear on the shoots page) or withkrites config. - Per-shoot (
<shoot>/.krites/config.yaml) is a sparse override: only the keys that shoot deliberately differs on. Everything else inherits the global. Change a global key and every shoot that hasn't overridden it follows.
Only the cull-profile thresholds (cull.profile.*) are overridable per shoot.
The capabilities that belong to the whole install, the AI provider and key
(review.*) and whether the ONNX models run (face.enabled,
remove.enabled and their runtime paths), are global-only: they have no
per-shoot meaning. Saving from the
studio applies without a restart (the config is hot-reloaded; the object-removal
and review backends re-read it on the next request).
Core¶
| Key | Type | Default | Meaning |
|---|---|---|---|
log.level |
string | info |
Log verbosity (debug, info, warn, error). |
update.policy |
string | (compiled-in) | Self-update posture: disabled, prompt, or enabled. |
update.check_interval |
duration | (24h) | How often to check for updates (0 = every run). |
Face / eye analysis¶
The face/eye provider powers the closed-eye and blink, facing and expression cull signals. It is off by default; turning it on fetches and verifies the models. Thresholds are starting points to tune.
The keys that shape a measurement (face.detector, face.strategy,
face.ear.*, face.mesh.margin, face.smile.*, face.facing.* and the two
face.expr weights) are applied while measuring, so the first cull after a
change re-analyses every frame rather than re-judging from the cache.
| Key | Type | Default | Meaning |
|---|---|---|---|
face.enabled |
bool | false |
Turn face/eye analysis on. |
face.execution_provider |
string | cpu |
cpu or coreml (Apple Silicon; falls back to CPU). |
face.library_path |
string | (auto) | Optional legacy override; prefer onnxruntime.library_path. Unset = auto-provisioned. |
face.detector |
string | yunet |
Face detector: yunet (default), MIT, box plus 5 keypoints, strong on small and side-on faces; or ultraface, RFB-320, boxes only. Analysis-time: the next cull re-analyses. |
face.detector_model |
string | (pinned) | Override the face-detector ONNX path (must match face.detector). |
face.landmark_model |
string | (pinned) | Override the landmark ONNX path. |
face.strategy |
string | mediapipe |
mediapipe (default): the detector (face.detector) plus MediaPipe FaceMesh, for trained smile, frown and valence and mesh-derived eyes and facing, all Apache-2.0 or MIT. ear: the opt-in geometric fallback using InsightFace 2d106det (non-commercial). |
face.mesh_model |
string | (pinned) | Override the MediaPipe FaceMesh ONNX path (mediapipe strategy). |
face.blendshape_model |
string | (pinned) | Override the MediaPipe blendshape ONNX path (mediapipe strategy). |
face.mesh.margin |
float | 0.25 |
mediapipe only: fraction each detection box is expanded by before the FaceMesh crop: the margin-padded ROI the mesh is trained to lock onto, recovering roughly a third of no-lock faces. Calibrated as a pair with face.ear.*; changing it without recalibrating the anchors mis-scales eye detection. Analysis-time: the next cull re-analyses. |
face.ear.closed |
float | 0.10 mesh / 0.12 ear |
Eye-aspect-ratio at/below which eyes read fully closed. Default is strategy-specific: the FaceMesh eye geometry has a lower EAR scale than 2d106det, lowered further by the mesh ROI margin. |
face.ear.open |
float | 0.22 mesh / 0.26 ear |
Eye-aspect-ratio at/above which eyes read fully open. Strategy-specific default (see above). |
face.smile.closed |
float | 0.00 |
Mouth curvature at/below which the mouth reads unsmiling (geometric ear only). |
face.smile.open |
float | 0.10 |
Mouth curvature at/above which the mouth reads a full smile (geometric ear only). |
face.facing.frontal |
float | 0.10 |
Yaw asymmetry at/below which the face reads frontal. |
face.facing.turned |
float | 0.50 |
Yaw asymmetry at/above which the face reads turned away. |
face.expr.cheek_squint |
float | 0.0 |
mediapipe only: weight of the Duchenne (cheek-squint) term added to positive valence. Analysis-time: the next cull re-analyses. |
face.expr.brow_down |
float | 0.0 |
mediapipe only: weight of the brow-down term added to negative valence. Analysis-time: the next cull re-analyses. |
face.expr.neutral |
float | 0.0 |
Recentre the displayed valence: the raw smile − frown value treated as the neutral midpoint. Serve-time: hot-reloads, no re-cull. |
face.expr.gain |
float | 1.0 |
Scale the displayed valence toward the grumpy ↔ happy extremes (a modest smile reads more pronounced). Serve-time: hot-reloads, no re-cull. |
The default mediapipe strategy, and the ear fallback¶
The default face strategy is mediapipe: the detector (YuNet by default) finds the faces,
then one MediaPipe FaceMesh + blendshape pass per face yields a trained smile,
frown, and a two-sided valence (grumpy to happy) and the eyes and facing
signals, all from models licensed Apache-2.0 or MIT. A face whose mesh doesn't lock (small
/ heavily angled) reports unknown rather than a fabricated value. The studio's
diverging Expression axis reads the mean valence across the frame's faces, so
one unsmiling guest no longer reads a whole group shot as grumpy.
The ear strategy is an opt-in fallback that reads eyes + a geometric smile
from InsightFace 2d106det landmarks. It is not the default because 2d106det is
non-commercial and research-only, and the default stack is kept free of it. Select it explicitly, and its 2d106det model is fetched only
then:
face:
enabled: true
strategy: ear # opt-in geometric fallback (downloads the non-commercial 2d106det)
Re-cull after switching strategy; the first cull afterwards re-analyses every frame.
The two MediaPipe models (mediapipe-facemesh and mediapipe-blendshapes, both
1.0.0) resolve from the approved artefact channel under the same dual-anchor
check as the runtime above, and are cached beside it under the user cache
directory (phpboyscout/artifacts/). krites compiles in each model's digest and
refuses a channel file that differs, so a model change is always a krites
release. The detectors and the ear fallback's landmark model are still fetched
from their upstream hosts and checked against a compiled-in digest only. See
0038.
Object removal¶
The inpainting provider
backs krites remove and the studio's removal tool. Off by
default; turning it on fetches the selected backend's pinned model and checks it
against a digest compiled into krites.
| Key | Type | Default | Meaning |
|---|---|---|---|
remove.enabled |
bool | false |
Turn object removal on. |
remove.accept_licence_risk |
bool | false |
Hard gate: removal cannot run until this records explicit acceptance of the model licence terms (Places2 non-commercial training data; see the licence note below). Both this and remove.enabled must be true. |
remove.backend |
string | lama |
Inpaint backend: lama (reference-quality) or migan (fast/tiny). |
remove.execution_provider |
string | cpu |
cpu or coreml (Apple Silicon; falls back to CPU). |
remove.library_path |
string | (auto) | Optional legacy override; prefer onnxruntime.library_path. Unset = auto-provisioned. |
remove.model |
string | (pinned) | Override the model path (else the backend's pinned model is fetched). |
remove.margin |
float | 0.5 |
Context padding around the mask before the 512 crop (LaMa only). |
remove.timeout |
duration | 120s |
Per-inpaint deadline. |
Licence note. Both inpaint backends are trained on Places2 (non-commercial / no-redistribution). Removal is behind a hard gate:
remove.accept_licence_riskmust record explicit acceptance of the terms before the inpainter is ever constructed. The studio's Object removal settings show the disclosure and the acceptance; the CLI declines with the same message until it is set. SeeNOTICE.md.
AI review¶
The AI critic backs
krites review, a critic built on the multimodal
go/chat client. It is cloud-based and off by default: it sends the image off
the machine, so it stays inert until you choose a provider and enter its key.
| Key | Type | Default | Meaning |
|---|---|---|---|
review.provider |
string | (none) | AI provider: gemini or claude. Empty = inert. |
review.model |
string | (provider default) | Model id override. |
review.timeout |
duration | 180s |
Per-review deadline. |
The provider API key is entered in the studio and stored in the OS keychain, never in the config file or an environment variable.
Cull profile¶
The judgement (per-signal thresholds, hard gates versus soft floors, how
tightly bursts are grouped) lives under cull.profile.*. These are the keys that cascade
global → shoot: unset, they fall back to the built-in wedding-default seed;
set globally, they become your default; set in a shoot, they override it there.
Edit them from the studio (the global default in the Settings panel, per-shoot
overrides in a shoot's Cull profile panel) rather than by hand.
| Key | Type | Default | Meaning |
|---|---|---|---|
cull.profile.min_sharpness |
float | 50 |
Hard focus gate: below it, reject. |
cull.profile.soft_sharpness |
float | 150 |
Soft focus floor: below it (≥ min), maybe. |
cull.profile.max_clipped_highlights |
float | 0.10 |
Hard blown-highlight gate (0–1); 0 disables. |
cull.profile.max_clipped_shadows |
float | 0.30 |
Hard crushed-shadow gate (0–1); 0 disables. |
cull.profile.dedup_distance |
int | 8 |
Max perceptual-hash distance for a burst; 0 disables dedup. |
cull.profile.eye_open_soft |
float | 0.50 |
Eye-open soft floor; below it, maybe. 0 disables the eye signal. |
cull.profile.eye_open_hard |
float | 0 |
Eye-open hard gate; below it, reject. 0 (default) = off. |
cull.profile.min_face_box |
float | 0.10 |
Ignore faces smaller than this fraction of the frame's shorter side. |
cull.profile.smile_soft |
float | 0 |
Smile soft floor; 0 = off (expression informs ranking only). |
cull.profile.facing_soft |
float | 0 |
Facing-camera soft floor; 0 = off. |
cull.profile.frown_soft |
float | 0 |
Frown soft floor: a face grumpier than this valence (−1…1) is demoted to maybe, never rejected (0029). The threshold is negative (e.g. -0.30); 0 or any non-negative value = off. Re-cull to apply (populates the per-frame worst-valence signal). |
cull.profile.aesthetic_floor |
float | 0 |
Aesthetic maybe-floor; 0 = off. |
A per-shoot override lives in <shoot>/.krites/config.yaml under the same
cull.profile.* keys; unticking an override in the studio removes the key so the
field re-inherits the global. Migrating from an older krites: a shoot's legacy
.krites/profile.yaml is converted to sparse overrides on first open and backed
up to profile.yaml.pre-cascade.bak.
aesthetic_floor currently does nothing¶
The key is real, the ranking tier exists and the analysis cache has an
aesthetic slot, but no aesthetic scorer ships, so the tier is inert
and the floor never moves a verdict. Setting it to a non-zero value changes
nothing. See
Limitations.
The look catalog¶
The aesthetic (white balance, tone, grade or LUT) lives in a look
catalog rather than in flat config keys. It is stored per shoot in
<shoot>/.krites/develop.yaml, alongside the straighten and crop defaults.
Today the catalog contains exactly one look, neutral, and no krites command
or studio action writes a custom one. krites develop --look <anything-else>
fails with unknown look "x" — this shoot's catalog has: neutral, and because
neutral is the identity transform, applying it changes nothing at export.
A hand-written develop.yaml is read when it exists, so a custom catalog does
work, but that is the only route today, and the file format is not a stable
public interface. See
Limitations.
Diagnostics¶
Opt-in monitoring of the studio process, off by default. When enabled, a scrapeable Prometheus /metrics endpoint (Go
runtime, process, and build_info collectors) is served for watching live
heap / goroutines / RSS and catching leaks. It is guarded by the studio's
per-launch session credential (the browser's cookie, or a bearer token), so it
is never an unauthenticated open port. Provided by the
transport-metrics toolkit
module.
| Key | Default | Meaning |
|---|---|---|
diagnostics.metrics.enabled |
false |
Serve GET /metrics (guarded). |
diagnostics.pprof.enabled |
false |
Also serve net/http/pprof under /debug/pprof/ (same guard) for go tool pprof. |
Routes register at launch, so a change takes effect on the next studio start.
To read /metrics locally, load the studio in a browser once (which sets the
session cookie) and scrape with that cookie, or present the session bearer.
Self-update signature verification¶
krites update downloads a release and verifies it before installing. Verifying
only the SHA-256 in the release's own checksums.txt would be circular: the
binary and the checksum that "proves" it come from the same place, so anyone able
to publish a release controls both. krites therefore verifies an OpenPGP
signature over checksums.txt, made by a key held in AWS KMS that never leaves
it.
This is fail-closed. An update whose signature is missing, malformed, or made by an unrecognised key is refused, not warned about. (Enforcement was turned on one release after signing began, so that binaries already in the field, which have no embedded key and so can't verify anything, weren't stranded with a permanently failing update path.)
| Key | Default | Meaning |
|---|---|---|
update.require_signature |
true |
Refuse an update that has no valid signature. Setting false downgrades verification to advisory, which is only sensible when diagnosing a broken release. |
update.require_checksum |
framework default | Checksum enforcement, independent of the signature. |
The two trust anchors¶
The signing key is resolved from both an anchor compiled into the binary and one published on the public internet:
- Embedded: the certificate is baked in at build time
(
internal/trustkeys/keys/krites.asc). Always available, even offline, but only changeable by shipping a new binary. - WKD: the same certificate published under
[email protected]via Web Key Directory. This is what allows the key to be rotated or revoked without shipping a new binary.
They interact deliberately:
| Situation | Behaviour |
|---|---|
| The two anchors disagree | Always fatal. A mismatch is a tampering signal, and no configuration can suppress it. |
| WKD is unreachable (outage, offline, DNS) | Falls back to the embedded anchor with a warning. A CDN blip must never block updating. |
So "the crosscheck is not required" means unreachability is tolerated. It does not mean a disagreement is tolerated.
If an update is refused¶
A refusal is a genuine signal; treat it as one before working around it. Check
that the machine can reach openpgpkey.phpboyscout.uk, and that the release you
are updating to actually published a checksums.txt.sig. Reinstalling from the
published .dmg or tarball is the safe recovery. Those carry the same signature
and can be verified by hand against checksums.txt.sig.