Point it at your footage. Get a film.
You have hours of video you will never watch. Cutroom watches it once, writes down what is in every shot, and then cuts films out of it — a new one whenever you ask, for about the price of a coffee.
Every shot in an archive, what it contains, and which films spend it. Filter by material, mood, time of day, or by what no cut has used yet — then click a node and it tells you what is in that clip and why a cut spent it. Each panel above is a real state of the page, reachable by URL.
On the archive it was built against: 453 shots logged once for $3.37, then two complete 14-minute films for under $2 each — sharing only 41% of their shots, because the second one was told nothing about what the film should be.
Analysis is expensive; story is cheap. So they are separated completely.
EXPENSIVE, BUILT ONCE CHEAP, RE-RUN FOREVER
┌───────────────────────┐ ┌───────────────────────┐
│ vault/shots/*.md │◀───────────│ vault/cuts/*.md │
│ one atom per clip │ wikilinks │ story assemblies │
│ IMMUTABLE │ │ ONE PER VERSION │
└───────────────────────┘ └───────────────────────┘
A vision model looks at sampled frames and transcripts once and writes one
note per shot. The assembly stage then never looks at a frame — it reads
only those notes. That is the whole trick: a new film is a new note in cuts/,
not a new analysis. Cuts are additive and never overwrite each other, so
alternate versions sit side by side and stay comparable.
vault/shots/*.md |
one note per clip: what's in it, mood, light, camera, the usable sub-range, who is in it and how that was proven |
vault/cuts/*.md |
an ordered edit with in/out points and a reason for every shot |
derived/cuts/*.json |
the same cut as a machine-readable EDL |
| rendered video | master + preview, plus every trimmed shot as its own file for import into an NLE |
*.html |
a self-contained interactive graph of the whole archive |
- Python 3.10+ for the model stages;
ffmpegandffprobeonPATH - An Anthropic API key; an OpenAI key if you want transcripts
pip install anthropic(pluspillowfor the WebP transcoder and diagram)
git clone <this repo> && cd <this repo>
python3 -m venv venv && ./venv/bin/pip install anthropic pillow
cp .env.example .env # add your keys
export ARCHIVE_SOURCE_DIR="/path/to/your/footage" # read-only, never writtenPaths resolve from the repo directory by default; set FILM_PROJECT_ROOT to
put outputs elsewhere. Nothing is hardcoded to the machine this was built on.
Every stage is idempotent — rerun and it skips finished work.
# Stage 1 — mechanical, no model tokens
python3 scripts/transcode_webp_*.py # only if your archive has animated WebP
python3 scripts/build_inventory_*.py # ffprobe everything → inventory.tsv
python3 scripts/extract_frames_v2_*.py # tiered frame budget → stills
python3 scripts/transcribe_*.py # Whisper → transcripts
# Stage 2 — the expensive one, once
./venv/bin/python scripts/log_shots_*.py --dry-run # measure real cost first
./venv/bin/python scripts/log_shots_*.py --limit 10 # pilot, read the notes
./venv/bin/python scripts/log_shots_*.py # everything
# Stage 3 — cheap and repeatable
./venv/bin/python scripts/assemble_cut_*.py
./venv/bin/python scripts/assemble_cut_*.py --open # withhold your premise
./venv/bin/python scripts/assemble_cut_*.py --brief "..."
# Stage 4 — render
./venv/bin/python scripts/render_cut_*.py --edl derived/cuts/cut-01-*.json --height 720
# Stage 5 — interactive graph
python3 scripts/build_graph_html_*.pyUse --dry-run. On stages 2–4 it resolves every file and counts real
tokens without generating or encoding, so you know the bill before you pay it.
These are the parts that make the output trustworthy rather than merely plausible. They are enforced in code, not requested in a prompt.
- The controlled vocabulary is a JSON schema. An out-of-vocabulary value is a 400 from the API, not a value that quietly rots your queries later.
- No claim without a source. A person is named in a shot note only when the
transcript or the clip's own filename says the name, and the validator
re-checks the quote against the actual text. A name that fails is dropped and
logged. Frames cannot tell you who someone is, so the honest output is a
count of visible people and
cast_evidence: none. - Ranges are picked, not invented. The usable sub-range of a clip must be chosen from real frame or transcript timestamps that were handed to the model, then clamped to the clip's true duration.
- Confidence is forced down when evidence is thin. A clip with no transcript and few frames cannot yield a confident range, and says so.
- The script does the arithmetic. Models are unreliable at summing forty durations to hit a target length, so the code sums the real numbers, compares to target, and makes exactly one bounded repair call. Never a loop.
- Every number is measured. Per-call
response.usageis written tologs/*.jsonl; cost reports read from there rather than estimating.
The pipeline is generic; the vocabulary and prompts are the example configuration and are where your edits go:
| what | where | why you'd change it |
|---|---|---|
| material classes (how clips are bucketed) | build_inventory_*.py |
yours are not "camping" and "zombie" |
| frame budget per class | extract_frames_v2_*.py |
a 6-second insert needs fewer frames than a 27-minute reference |
| logged fields + vocabulary | log_shots_*.py → VOCAB, SCHEMA |
mood/light/movement suit narrative footage; a lecture archive wants different axes |
| the second scoring axis | log_shots_*.py |
one axis is craft quality; the other should be whatever your project selects for |
| structure of a cut | assemble_cut_*.py |
this example uses songs as acts; yours might use chapters, days, or speakers |
| the premise | assemble_cut_*.py |
or pass --open and let the model derive the subject from the material |
--open is worth knowing about: it withholds your premise entirely and asks
the model what the archive actually is, from the distributions. In the case
study below, the premise-given and premise-withheld cuts shared only 41% of
their shots.
Each of these cost real debugging time, and each is now handled in code:
- ffmpeg cannot demux animated WebP at all. Not slowly — at all. Stage 1a
exists solely to route around it via Pillow. Rendering from an EDL's original
source_paththen fails on every such clip; the renderer re-joins to the proxy. - An archive is never one format. NTSC DV, anamorphic, 16 fps, 120 fps —
concatenating those without conforming them produces either a hard failure or
silent geometry corruption. Square the pixels first
(
scale=trunc(iw*sar/2)*2:ih) or 4:3 material stretches. -ssbefore-iis not frame-accurate on AVCHD, which shows up as frozen picture at the head of shots.--seek accuratefixes it.- A caught error must still delete its partial output. One failed transcode was correctly logged as failed and left on disk truncated, so every later stage planned against footage that did not exist.
- Thousands of ffmpeg spawns can wedge a machine into unkillable D state. Frame extraction uses one spawn per clip, not one per frame.
- Structured-output schemas reject
minItemsabove 1. Constraints like "3 or 4 sections" belong in the prompt plus a post-response check. - Byte-level checks lie about video. Counting unique frame hashes finds nothing, because re-encoded near-identical frames are not byte-identical. Use mean pixel difference, and validate any detector on a known-bad case first.
Operating rules live in AGENTS.md — the cross-tool convention
read by Cursor, Aider, Codex, Continue, Cline and others. CLAUDE.md is a
symlink to it, so Claude Code picks up the same file and there is only one copy
to maintain. If your tool reads neither, paste it in as context.
It is worth pointing an agent at, because the rules encode failures that are expensive to rediscover: which verification commands silently lie about video, why the shot vault must never be edited to fix a downstream bug, and which "defects" are actually creative choices to be asked about rather than fixed.
docs/case-study.md — 5.6 hours of unorganized camping
trip movies and several theme songs, logged into 453 shot atoms for $3.37, then
cut into two 14-minute films for under $2 each.
CC BY-NC 4.0 — Copyright (c) 2026 Kourosh Salehi-Ashtiani.
Free to use, modify and build on for non-commercial purposes. Two conditions, and they apply to modified versions exactly as they apply to unmodified ones:
- Cite this repository. Any use — as-is, adapted, or as a component of something larger — must credit it and link back.
- No commercial use without a separate licence. Contact the copyright holder.
Salehi-Ashtiani, K. (2026). Cutroom: log a video archive once,
assemble films from it forever. https://github.com/kouroshSA/cutroom
Licensed under CC BY-NC 4.0.
CITATION.cff provides this in machine-readable form, so GitHub offers a
Cite this repository button and reference managers can import it directly.
No warranty, no liability. This software is provided "as is", without
warranty of any kind, and the copyright holder is not liable for any damages
arising from its use — see Section 5 of the licence, restated plainly in
NOTICE. It drives ffmpeg over your files and spends money against
your API keys; back up your footage and watch your own bill.
Note that the non-commercial clause means this is source-available, not
OSI-approved open source. Full terms in LICENSE; the copyright
notice, warranty disclaimer and required citation are in NOTICE.
vault/ and derived/ are gitignored: they are output, and they contain
descriptions of whatever footage you pointed this at.
