Skip to content

krites cull

Description

Judge a shoot — analyse every frame and assign a keep / maybe / reject verdict with reasons (spec 0001 §4.2). For each frame krites measures model-free quality signals — focus (variance of the Laplacian) and exposure (blown-highlight / crushed-shadow clipping) — and resolves a verdict through a cull profile. Hard gates reject, a soft focus floor demotes to maybe, and a clean frame is a keep. Near-duplicate bursts are then grouped (perceptual hash): the sharpest frame is kept and the rest demoted to maybe, tagged with the burst they belong to (R-DUP-1).

krites proposes; you dispose — a verdict is a label written to .krites/verdicts.yaml, never a file operation (R-CULL-3). Originals are never touched.

Each cull also banks an analysis cache (.krites/analysis.yaml) — the raw per-frame signals plus the profile fingerprint in force — and, on every verdict, records the machine's proposed ruling alongside your final one. That captured history is what the Phase-4 learning loop will train on to auto-tune the profile to your taste (spec 0005); it also makes verdicts explainable and re-culls fast.

The shoot must already be registered with krites ingest.

Usage

krites cull [flags]

The shoot is taken from --shoot, or the current working directory if omitted (R-SCOPE-1).

Flags

Flag Description Default
--shoot Shoot directory to cull the current directory
--profile Cull profile to apply wedding-default
--reanalyze Recompute analysis, ignoring the cache false
--output Output format: text or json text

Examples

# Cull the shoot in the current directory
cd ~/shoots/smith-wedding && krites cull

# Or target it by path, with JSON output
krites cull --shoot ~/shoots/smith-wedding --output json

Text output:

Shoot "smith-wedding" — 4 frames
  keep 1   maybe 0   reject 3

  keep   01-sharp.jpg
  reject 02-flat.jpg    — out of focus (sharpness 0 below 50)
  reject 03-dark.jpg    — out of focus (sharpness 0 below 50); crushed shadows (100% over 30%)
  reject 04-bright.jpg  — out of focus (sharpness 0 below 50); blown highlights (100% over 10%)

The cull engine also judges eyes open vs. closed (spec 0004): the wedding-default profile carries an eye gate, and when a face provider is configured the worst-face eye-open probability drives the verdict —

  • a likely blink (below eye_open_soft, seed 0.50) demotes a frame to maybe with a subject blinking … reason;
  • eye_open_hard is disabled by default (0), so a closed-eye frame is never auto-rejected — it may be the only record of a moment, so you dispose (spec 0004 R-EYE-3). Raise it to opt into auto-reject;
  • only faces clearing min_face_box (seed 0.10 of the frame's shorter side) count, so a blinking guest in the far background doesn't demote a portrait;
  • within a near-duplicate burst, an open-eyed frame is kept over a sharper blink (R-EYE-6).

The face detection that feeds the eye signal is a local ONNX model (UltraFace detector + InsightFace landmarks → eye-aspect-ratio), run with no CGO. It is off by default (local-first, opt-in); enable it in config:

face:
  enabled: true
  library_path: /path/to/libonnxruntime.so   # or .dylib on macOS
  # detector_model / landmark_model are optional — when omitted, krites fetches
  # the pinned models into its cache once and verifies their checksums.
  # optional — the EAR→eye-open calibration (defaults are tuned to real frames):
  ear:
    closed: 0.12   # EAR at/below which eyes read fully closed
    open:  0.26    # EAR at/above which eyes read fully open

The ONNX models are fetched + checksum-verified automatically on first use; only the ONNX Runtime library path is supplied by hand (it's platform-specific).

With it on, the cull demotes blinks to maybesubject blinking (N% eye-open, below 50%) — and banks min_eye_open per frame. Off, the cull runs model-free (no faces ⇒ no eye penalty), exactly as before. The deterministic gate, signal projection and burst ranking are pure / WASM-safe; only the native adapter is gated behind config. See spec 0004.

Expression signal (smile / looking at camera)

When the face provider is on, the same landmark pass that judges eyes also reads two expression attributes per face — for free, no extra model (spec 0007): smile (mouth curvature) and facing-camera (a head-pose proxy from landmark symmetry — head orientation, not iris gaze). Both are probabilities in [0,1], worst-face across the frame like eyes.

Their default value is best-of-burst ranking and capture, not rejection. The wedding-default profile ships them verdict-neutral (smile_soft = 0, facing_soft = 0): a solemn ceremony face or a candid glance away is normal at a wedding, never a defect (R-EXPR-5). What they do by default:

  • within a near-duplicate burst, among open-eyed frames the better-smiling frame is preferred, then the one more facing the camera — the ranking chain is eyes-open ▸ expression ▸ aesthetic ▸ sharpness (R-RANK-1); a demotion names the tier that decided it (kept the smiling frame);
  • every frame banks min_smile / min_facing in the analysis cache, feeding the Phase-4 learner alongside sharpness and eyes (R-CAP-1).

Opt into demote-to-maybe by raising the soft floors in the profile (smile_soft / facing_soft), or tune the geometry anchors in config:

face:
  enabled: true
  smile:  { closed: 0.00, open: 0.10 }   # mouth-curvature → smile-probability ramp
  facing: { frontal: 0.10, turned: 0.50 } # yaw-asymmetry → facing-probability ramp

The mouth/pose landmark indices and these anchors are provisional — seeded, not yet calibrated against a real-frame distribution the way the EAR anchors were (a calibration harness does that before expression is trusted). That is exactly why expression ships verdict-neutral: an uncalibrated estimator can inform ranking but cannot move a verdict.

Aesthetic signal (deferred)

The cull carries an aesthetic ranking tier and an aesthetic cache slot, but no scorer backend ships yet (spec 0007 0007-DEC-1): the whole-frame "is this a strong photograph" model is deferred to Phase 4, where it is built as a CLIP+aesthetic-head so the learning loop gets a reusable per-frame embedding in the same pass. Until then the aesthetic tier is inert — the ranking chain runs eyes ▸ expression ▸ sharpness — and aesthetic_floor stays 0 (never a hard gate by design, R-ASC-2).

Notes & current limits

  • The wedding-default profile is the seed ruleset; its focus/exposure/eye thresholds are tunable starting points (spec 0001 §6). A configurable profile catalog (krites profile …) lands later; only wedding-default is available today.
  • Re-culls reuse the analysis cache — tuning a threshold and re-culling re-resolves verdicts from the cached signals (no re-decode, no model pass), so it's near-instant on a big shoot (R-GLOBAL-6). --reanalyze forces a full recompute; the cache is also invalidated automatically when a signal-affecting setting (the face-size floor, or turning eye detection on/off) changes.
  • Eye detection runs on the CPU execution provider today; the Apple-Silicon CoreML acceleration and automatic model fetch (so you don't supply the model paths by hand) are the remaining 0004 follow-ups. RAW frames decode via JPEG/PNG only for now (cull on embedded previews is later).