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 (seeded 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, dots → underscores) — e.g.
remove.enabled → KRITES_REMOVE_ENABLED. Config is hot-reloaded.
The heavy providers are local-first and off by default — enable them per capability. Their models and the ONNX Runtime library both download automatically on first use (checksum-verified and cached) — enabling a capability 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 cannot 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 — path to your own libonnxruntime. Leave unset to auto-provision. Shared by face + 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 deviates 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 install-level capabilities — the AI provider + key (review.*) and
whether the ONNX models run (face.enabled, remove.enabled + 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 eyes-closed / blink and facing/smile cull signals. Off by default; enabling it fetches and checksum-verifies the pinned ONNX models. Thresholds are starting points to tune.
| 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 + 5 keypoints, strong on small/side faces; or ultraface — RFB-320, boxes only. Switching changes detection → re-cull to apply. |
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): UltraFace + MediaPipe FaceMesh — trained smile/frown/valence AND mesh-derived eyes/facing, all Apache-2.0/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 — re-cull to apply. |
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 — re-cull to apply. |
face.expr.brow_down |
float | 0.0 |
mediapipe only: weight of the brow-down term added to negative valence. Analysis-time — re-cull to apply. |
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: UltraFace detects,
then one MediaPipe FaceMesh + blendshape pass per face yields a trained smile,
frown, and bipolar valence (grumpy ↔ happy) and the eyes/facing signals —
all from models that are Apache-2.0/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 / research-only — so the default stack is kept
licence-clean. 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, to repopulate the analysis cache.
Object removal¶
The inpainting provider
backs krites remove and the studio's removal tool. Off by
default; enabling it fetches and checksum-verifies the selected backend's pinned
model.
| 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 surface the disclosure and acceptance; the CLI declines with the same message until it is set. SeeNOTICE.md.
AI review¶
The AI critic backs
krites review — a chat-backed critic over go-tool-base's
multimodal pkg/chat. Cloud-first and off by default: it egresses the image,
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 vs soft penalties, dedup
aggressiveness — 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 backend 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 process observability for the studio — off by default (local-first,
local-first). 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 cannot verify anything — were not 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 — 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.