Skip to content

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.enabledKRITES_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 with krites 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_risk must 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. See NOTICE.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.