Independent project

Media Cull Suite: local-first photo and video culling that learns your taste

Three command-line tools and a React app that rank a shoot, learn a photographer's preferences, and feed a corrective camera recipe back before the next shoot, all on the user's own machine.

Status · v0.42 by its changelog as of 2026-06-10 (81 commits, 2026-05-16 to 2026-06-10); all four loops shipped; no commits since. Private repository.

Overview

A local-first culling suite that ranks a shoot, learns a photographer's taste, and closes a capture loop back to the camera so the next shoot comes out better, without uploading a single frame.

Media Cull Suite bundles a photo culling assistant (pca), a video clipping assistant (vca), and a shared launcher (mc) into one install that turns a folder of frames into a ranked, reviewed, exportable set. The unusual part is that it does not stop at the cull: it measures what went wrong on a shoot, proposes corrective camera settings for the next one, and can push them to a Sony α6700. Everything runs locally; no image, video, embedding, or transcript ever leaves the machine.

What would a culling tool look like if it treated the next shoot, not the current one, as the thing to improve?

The problem: the shoot is the easy part

A single outing produces hundreds or thousands of frames, and if there is video, hours of footage. Deciding what to keep is the slow, tedious part, and it is where most of a photographer's evening goes. Some tools that automate it upload the take to a cloud service, which is a non-starter for anything shot under an NDA, anything with minors in it, or anyone who simply does not want their raw files on someone else's server.

I wanted a culling assistant that ran entirely on my own machine, produced a ranked shortlist I could review quickly, and learned what I actually keep rather than what a generic aesthetic model prefers. Once that existed, a second question became more interesting: the cull already knows which frames failed and why. Could that feed back into how I shoot?

Four loops, one interaction pattern

The suite is organized around four loops rather than four features. Each one observes something about a shoot, diagnoses it, proposes a minimal action, waits for the human to approve, and learns from the approval. That five-beat shape is the only interaction pattern in the product, and a single front door routes a plain-English prompt ("cull the obvious rejects", "what should I dial in tomorrow", "have I delivered this before") to whichever loop it belongs to.

Capture
Observes last shoot's EXIF and technical failure scores, diagnoses a per-situation failure signature, and proposes a corrective recipe to load before the next shoot.
Cull
Ranks the full take with technical scores and a learned taste preset, proposes a shortlist plus the frames it is unsure about, and refits from your accept/reject decisions.
Memory
Keeps a career-spanning embedding index and a delivery ledger, flags near-duplicates of things you have already shot or delivered, and learns which repeats you dismiss as not meaningful.
Deliver
Treats stills and clips from one event as a single delivery, flags frames that drift from your house look, applies it in one click, and exports each named channel.
Figure 1 — The four loops share one engine and one approval surface. Only the capture loop reaches back in front of the shutter.

The capture loop: feeding the next shoot

The culling tools I know of work after the shutter. The capture loop is the part I have not seen elsewhere, and it was the first loop I made work end to end. It starts by clustering a shoot into situations by time of day, GPS, and CLIP scene type. For each situation it computes a failure signature: the fraction of frames underexposed, blurred, or noisy, the ISO and shutter medians, the keeper rate, and a plain-English diagnosis. Deterministic rules turn that signature into a capture recipe (exposure shift, Auto-ISO ceiling, shutter floor, white balance, metering, dynamic-range optimizer, creative look), with every line justified by the diagnosis that produced it.

The recipe is only half a loop. The other half is knowing whether acting on it worked, and that turned out to be the real gap: keeper rate was computed per project, situations had no identity across shoots, and recipes were fire-and-forget. So the loop got a backward axis: a global capture log keyed by a stable, user-confirmed situation type, recording which recipe was in effect for each shoot, and a trend view that pairs keeper rate with the objective failure metric that was worst at the start (so a curve survives "I just got pickier"). The trend is worded as association, not causation, and it refuses to draw a curve from one data point.

On top of that sit a pre-shoot briefing built from history ("here is the recipe in effect during your best past shoot of this type"), an offline, phone-styled HTML field card you generate at home and consult on a trail with no signal, and pca tonight, an in-trip evening ritual that analyzes today's card, logs it, pushes tomorrow's recipe, regenerates the field card, and prints the day-over-day curve within a single trip.

Pushing a recipe to a real camera

The camera push was deliberately kept off the critical path. A hand-dialed recipe from the field card is a perfectly valid delivery channel, so the loop was demonstrable without any hardware. Once that was true, I brought up a Sony α6700 over Sony's Camera Remote SDK through a small C++ helper that the Python side talks to over a JSON protocol. It connects, reads each property's writability, snaps requested values to what the body allows, and reports what it could not set as advisory rather than failing.

Verified on the body on 2026-06-09, with the mode dial in M: four of the six recipe settings the helper targets push live (creative look, metering, Auto-ISO ceiling, exposure compensation). The remaining two are inherent camera or SDK limits reported honestly: the Kelvin value for white balance is read-only over the SDK on this body (the mode switches to color temperature but the number must be dialed by hand), and the Auto-ISO minimum shutter is a P/A-mode feature that does not apply in M. The SDK also cannot write the MR dial slots, so --register MR1 records the slot in the local bank registry and prints the one-time on-camera save step. A mock controller runs the whole round trip without a camera, which is how the tests cover it.

Orchestrate, don't operate

About halfway through, the feature list was growing faster than the product's identity, so I wrote down what the suite is and is not. Lightroom, Capture One, and Resolve are instruments: they assume a skilled operator who wants the controls, and they win on depth of manual control. This suite is a conductor. The user states intent, the system orchestrates the steps minimally and reversibly, and it surfaces a decision only when it is genuinely the human's to make. The product is the orchestration layer; the editor is the escape hatch.

That thesis had a concrete cost. The React Flow node-graph editor, the most engineering-heavy surface in the codebase, is the most low-level, operator-oriented thing I could have shipped: it literally exposes the processing graph. It was worth building, but it works against the thesis, so it was frozen as "the control surface for when you want control" and the planned work to deepen it (a history scrubber, direct-manipulation handles, conditional nodes) was dropped. Every proposal any loop makes must instead satisfy a minimal-action contract: one sentence of why, one click to apply, always undoable, never silently touching originals. If an action cannot meet that contract, it belongs in the editor, not in a loop.

The privacy contract

Local-first is a hard guarantee, not a default setting. No image, video, audio, embedding, or transcript leaves the machine, and there is no telemetry. GPS is extracted from EXIF but hidden unless you opt in. Originals are never modified or deleted: a reject is a flag in the catalog, safe-delete moves files to a folder, and edits are non-destructive specs that are baked only on an explicit export. Transcription for video is opt-in and runs locally.

Faces are the one place this needed real care. Face detection is always on but anonymous: faces are counted and may be grouped into similarity clusters within a single shoot, with no names and no linkage across shoots. Recognizing a named person across shoots is a different thing, and adding it changed a stated promise in the privacy document, so it is strictly opt-in. Nothing is stored or matched until you create a profile yourself, the reference embedding and name live in one local JSON file, matches are applied only as non-destructive edit nodes and reported with their match score, and one command (or deleting that file) wipes it.

Where it stands

The suite is at v0.42 by its changelog as of 2026-06-10, after a 26-day build of 81 commits. All four loops are shipped end to end with the router in front of them, the Sony α6700 live push is verified on hardware, and the test suite collects 499 tests. There have been no commits since; the repository is private and the code has not been packaged or published.

The React + TypeScript app is replacing the original Streamlit interfaces: every loop has a React view and natural-language search was ported, with cross-shoot duplicate and coverage views still to move before the Streamlit surfaces are marked legacy. Further out: a native binary wrapper, bringing the video assistant into the evening ritual, a bundled demo dataset for a first run, and packaging. I have not measured the improvement curve on enough real shoots to report numbers, so the headline claim stays the loop itself, not a keeper-rate figure.

FAQ

Does anything get uploaded?

No. Images, video, audio, embeddings, and transcripts stay on the machine, and there is no telemetry. Model weights are fetched once by the upstream libraries, and an optional local Ollama model can break ties in the router; the router works without it.

Does it recognize people?

Detection is always on but anonymous within a shoot. Recognizing a named person across shoots is off until you create a profile yourself; the profile lives in one local file, matches are applied as non-destructive edit nodes and reported with their score, and one command removes the profile.

Does the camera push fully automate settings?

Partly. On the α6700, four of the six recipe settings the helper targets push live; the Kelvin white-balance value and the Auto-ISO minimum shutter are reported as advisory because the SDK or the body does not allow them. Hand-dialing from the field card remains a supported path.

Is it a Lightroom replacement?

No, and it is not trying to be. It orchestrates the cull, the delivery, and the next shoot's settings; for manual control there is a non-destructive editor, kept deliberately as an escape hatch rather than the product.

Can I use it?

Not yet. The repository is private, there is no package, and there have been no commits since v0.42 on 2026-06-10.

What I built

  • A ten-stage photo pipeline (ingest, metadata, previews, technical quality, faces, embeddings, semantic prompts, grouping, interest scoring, shortlist) and a video pipeline built on shot detection, CLIP embeddings, and local Whisper transcription, each producing keep/maybe/reject recommendations with per-item reasons.
  • Four closed loops (capture, cull, memory, deliver) that share one interaction pattern, plus a rules-first router (`pca ask`, `vca ask`, `mc ask`, and an Ask bar in the app) that sends a plain-English prompt to the right loop.
  • The capture loop end to end: per-situation failure signatures from EXIF and technical scores, corrective capture recipes, a pre-shoot briefing, an offline phone-styled field card, and a live push to a Sony α6700 through a small C++ helper over the Camera Remote SDK.
  • A per-photographer taste preset fit from the user's own accept/reject decisions, with shoot-over-shoot tracking of how often the first pass agreed with the final cull.
  • A persistent cross-shoot embedding index and delivery ledger that answer "have I shot or delivered this before?", and a delivery object that applies a consistent look and exports named channels (full-resolution gallery, 1080×1350 Instagram crop, web).
  • Three CLIs (pca 43 commands, vca 23, mc 17 plus subcommand groups), a FastAPI service, a React + TypeScript app with a view for every loop, and a 499-test suite.

How it works

  • Every loop has the same five beats: observe what happened, diagnose, propose a minimal action, let the human approve, learn from the approval.
  • The capture loop clusters one shoot into situations by time of day, GPS, and CLIP scene type, then computes a failure signature per situation: percent underexposed, blurred, and noisy, ISO and shutter medians, keeper rate, and a plain-English diagnosis.
  • Deterministic rules turn each signature into a capture recipe (exposure shift, Auto-ISO ceiling, shutter floor, white balance, metering, dynamic-range optimizer, creative look) with a one-line rationale per setting; no language model is involved.
  • A global capture log keys each shoot by a stable, user-confirmed situation type and records which recipe was in effect, so the next cull can show whether keeper rate and the objective failure metric moved.
  • The router checks any routing corrections you have recorded, then classifies the prompt with weighted trigger phrases; an optional local model through Ollama breaks ties only when the rules are unsure, and a deterministic offline path always exists.
  • Edits are non-destructive graphs stored in a per-project SQLite catalog; rejects are catalog flags, and originals change only on an explicit export.

Snapshot

Track
Independent project · photography tooling · local-first ML
Status
v0.42 by its changelog as of 2026-06-10 (81 commits, 2026-05-16 to 2026-06-10); all four loops shipped; no commits since. Private repository.
Focus
Four closed loopsCapture recipes to cameraRules-first prompt routerLocal-only by contract

Stack

  • Python
  • FastAPI
  • React + TypeScript
  • PyTorch
  • OpenCLIP
  • YOLOv8
  • faster-whisper
  • Sony Camera Remote SDK (C++ helper)
  • SQLite

Glossary

Culling
Reducing a shoot's full take to the frames worth keeping and editing.
Situation
A cluster of frames from one shoot that share time of day, location, and scene type; the unit the capture loop diagnoses.
Failure signature
Per-situation statistics (percent underexposed, blurred, noisy; ISO and shutter medians; keeper rate) plus a plain-English diagnosis.
Capture recipe
Corrective camera settings derived from a failure signature, with a one-line rationale per setting.
Keeper rate
The fraction of a shoot's frames the photographer accepted; paired with an objective failure metric so a pickier cull does not read as worse shooting.
CLIP
A model that embeds images and text in a shared space; used here for scene type, semantic prompts, and the cross-shoot memory index.