| name | core |
| description | Core vcut usage guide. Read this before running any vcut command. Carries the session flow that runs an edit, the round methodology, the approval boundary, presets, and a verb table. Per-command deep dives load on demand through vcut skills get core --section <name>. |
| allowed-tools | Bash(vcut:*), Bash(npx @crafter/vcut:*) |
vcut core
Find what is worth cutting, propose it, and let a human approve before anything is rendered.
This document is the part every edit needs. The reasoning behind each command — why a
threshold is what it is, what eleven runs taught, how a fused retake defeats a window loop —
lives in sections you load when the question comes up:
vcut skills list
vcut skills get core --section cut
Loading this file is not loading the manual. It is loading the workflow and the index to the
rest, which is roughly a fifth of what the whole document costs. A two-minute clip should not
pay for the eleven-runs appendix.
The session is the workflow
Open a session, then work in refs. Everything below is one flow, and it is the one to
reach for unless you know why you are not:
vcut open recording.mp4 --preset clean --lang es --transcript words.srt
vcut peek recording.mp4 --ref b042
vcut cut recording.mp4 --refs b042..b044 --kind repetition --reason "..."
vcut commit recording.mp4 --output master.mp4 --campaign my-video
vcut rounds recording.mp4 --diff
open runs detect once, caches it, and numbers the speech blocks between the silences it
measured as refs — b001, b002, and so on, in time order. A ref names a block the way a
browser snapshot names an element: something later verbs point at instead of a raw millisecond
pair you retyped from another output. That is the whole reason the session exists. A cut named
by ref cannot land on the wrong words, because the span comes from a block the session already
measured rather than a number copied while looking at a different command's output.
commit builds the EDL and renders it in one call, --audio-only by default, and records the
round in the session so rounds --diff can answer "what changed" without you diffing two
transcripts by hand.
Every finding reaches the session, whatever coordinate system it arrived in
This is the part that used to be missing, and its absence is why an agent with the whole manual
in context still built EDLs by hand: a finding born in milliseconds had no path into refs, so
the stateless pipeline won by default. It has one now, and there is no finding that cannot enter
a session.
| The finding came from | It carries | Feed it to |
|---|
semantic export (a transcript line) | nearestRef on every line | vcut cut <media> --refs <nearestRef> |
say, silences, peek (a raw position) | atMs, startMs, endMs | vcut cut <media> --start-ms <n> --end-ms <n> |
open / suspects (a ranked position) | nearestRef per suspect | vcut cut <media> --refs <ref> |
nonspeech --verify (a classifier span) | master-time startMs/endMs | map through the EDL, then --start-ms/--end-ms |
| a boundary no ref's edges reach | seconds you measured | vcut cut <media> --span <startS>..<endS> |
--start-ms/--end-ms takes the same milliseconds say, silences, and semantic export
already emit — no seconds conversion, no arithmetic. It is not a lesser input than --refs: a
cut proposed either way accumulates in the same proposals.json, shows up the same in
rounds --diff, and gets its removedText quoted back the same at propose time.
So the choice of workflow is never decided by which coordinate system a finding happens to be
in. Every one of them ends at vcut cut.
Pick the grain by the defect, not by preference: refs are block-grain, right when the cut is a
whole silence-bounded block (dead air, a fumble between pauses); --start-ms/--end-ms is
word-grain, right when the cut lives inside a block (a retake mid-sentence, a filler between
words). Both are first-class. A run whose defects are mostly mid-block retakes will correctly
use ms for nearly every cut, and that is the design working, not the session flow failing.
Do not read ~/.vcut/sessions/ directly
The session directory is not an interface. Reading detect.json, refs.json, or
proposals.json out of it and working from the raw contents is unsupported, and it costs you
the thing the session is for: the verbs check gen, resolve stale refs by name, take the
advisory lock, patch the cached transcript path to one that still resolves, and quote
removedText from the session's own transcript. A hand-read of those files does none of that,
and the layout is free to change between releases precisely because the verbs are the contract.
If a verb does not answer a question you have about a session, that is worth an issue, not a
cat.
The escape hatch: the stateless pipeline
When you actually need this: a one-off cut with no second round, or a script driving vcut
with no long-lived working directory.
vcut detect recording.mp4 --preset clean --lang es --transcript words.srt
vcut semantic export --detect detect.json
vcut edl build --detect detect.json --semantic proposals.json --output cut-1.mp4 --campaign x
vcut render --edl edl.json --audio-only --output cut-1.wav
It is the layer underneath the session verbs, not a separate procedure — cut/commit call
the exact same build seam. What it costs you is everything the session tracks: you number
edl-$N.json/cut-$N.wav by hand, retype --detect/--semantic per round, hand-edit a
proposals file, and get no rounds --diff. On a multi-round edit that adds up to the friction
the session verbs exist to remove, which is why this is the exception and not the default.
Full detail: vcut skills get core --section workflow.
The round, and why one is never enough
A round is: propose, build, render --audio-only, transcribe that render, semantic review,
read, propose again. Anything short of the full sequence does not count as one, because the
reading is the part that finds things.
Never stop at one round. Each class of defect only becomes visible once the one before it
is gone, so the empty round has to come after a round that found something — it reads a text
the previous round produced and nobody has seen. Four runs on one recording separate cleanly on
this and nothing else: the three that stopped at one shipped a repetition, and the shortest of
them cut 33.78% while declaring itself done, against 44.04% for the run made to keep going.
That run found the largest cut in the file, a three-attempt retake, in round two, on material
round one had already declared clean.
Stop when a round proposes nothing, not when the removal percentage looks respectable and not
when the rounds start finding less. Diminishing returns is what convergence looks like from the
inside, one round before the end, every time.
The exception is the empty round that ends the loop, and only when the round before it proposed
nothing either: an unchanged file re-rendered and re-transcribed to confirm it is unchanged
buys nothing.
This is enforced, not just stated (#36). Below two committed rounds, commit's next
hints and rounds's summary refuse the converged framing and name the missing pass instead of
suggesting approval — roundsGate.status: 'insufficient-rounds' in both. The second
committed round must contain a real propose pass against this round's render transcript;
verification of round 1's own output does not count. --single-round on commit is the
deliberate override for a genuine one-round edit (a trivial clip), recorded in the session, not
inferred from a clean-looking run. Preview renders and the human approval boundary are
untouched by this — it is a framing gate on what the CLI calls "done," not a lock on rendering.
commit also checks for standing spoken edit markers, without waiting for semantic review
(#38). A finding class that only lives inside a verb the loop never forces to run is optional
by construction: a run committed four gated rounds through open/cut/commit and never
invoked review once, and shipped the same spoken rewind marker two prior runs had already
caught. commit now runs the same metaSpeech check itself, on every round that has a cached
transcript, and folds the result into its own JSON (metaSpeech, always present, [] when
clean) and --human output — the one artefact a run reads in full every round, unprompted. A
round with standing findings gets a hint naming them first, ahead of every other next step. It
never auto-cuts and never blocks a render; the human decision boundary is unchanged. Detail:
vcut skills get core --section commit, --section semantic.
Method, invariants, and the full stopping condition:
vcut skills get core --section rounds-methodology, --section invariants.
Facts that save a round each
Verify on the audio-only render; mux video once, at the end. commit defaults to
--audio-only and render --audio-only is the standalone equivalent. audit correlates
waveforms and nonspeech --verify classifies audio — neither reads a frame, so both take the
.wav directly, same as joins. Audio-only is far cheaper than video, but it is not free and
it is not constant: it costs roughly 1 second per 14 seconds of audio the cut keeps
(measured below). One run spent 69 of its 105 seconds of tool time on two video renders, the
second purely to feed checks that never needed a frame.
vcut render --edl edl.json --audio-only
vcut audit --edl edl.json --render cut.wav
vcut nonspeech cut.wav --verify
vcut joins --edl edl.json --render cut.wav --report report.json
render blocks, so never write a poll loop. It runs in the foreground until ffmpeg exits
and prints a progress line to stderr per report. There is nothing to poll a file for and
nothing to grep a process table for; when the command returns, the render is done. --quiet
drops the progress lines and renders the same file.
Know the cost before you call it: a video render runs roughly real time per minute of source
— a 10-minute source is a multi-minute foreground call. --audio-only is much cheaper but
still scales with the cut: about 1 second of wall clock per 14 seconds of kept audio, so a
490-second cut is a ~35-second call, not an instant one. If your harness caps tool calls at a
default timeout, raise the timeout for that one call instead of backgrounding it. One run read
this exact rule, hit a 180-second default timeout on a 9-minute render, and spiraled into the
poll-loop behavior the rule forbids; a 600-second timeout on the same call finishes in the
foreground with no ceremony.
Batch verified cuts into one commit. Every commit re-renders the whole cut from scratch,
so its cost is set by how much audio the cut keeps, not by how much you changed since the last
one. Committing after each individual find pays that full price per find. The wall-clock math
from the run that made this a rule: six commits in one session on a 700-second source, ~9
minutes each, 50+ minutes of foreground ffmpeg, and the last commit re-rendered every second of
audio the first five had already rendered. Propose every cut you have evidence for, then commit
once and verify the batch. The rounds gate still wants a real second pass — batching is about
how many commits a round costs, never about skipping a round.
Where the time actually goes (measured on a synthetic 700-second source, 490 seconds kept):
loudness normalisation is ~95% of an audio-only render, it is single-threaded, and it is linear
in kept audio — the floor no flag removes. Segment count is nearly free now: 22 segments and
180 segments over the same kept audio cost 6.9s and 8.7s on a 120-second source. Before 0.19.1
segment count dominated instead (293s at 180 segments against 43s today, same output bytes),
which is what made per-find commits cost minutes.
A whole-file transcript averages a repeat away, so commit re-listens in short windows for
you. Reading the render's transcript end to end catches content that stopped making sense.
It does not catch content that makes sense twice: a model reading ninety seconds collapses two
attempts at the same line into one, because one line is the likelier sentence. The same audio cut
to sixteen seconds returns both. This is not a redundant check with reading, it is the other half
of it, and it is the reason commit now runs the verify --windows sweep over its own render
every round with no flag, and holds roundsGate.status at repeated-phrases-unresolved while a
repeat stands, quoting the offending phrase.
The run that made this a rule shipped a render it had verified as clean, having run audit
(91/91 correlate), joins, nonspeech --verify twice, and a full re-transcription of the render.
verify --windows on that same file returns 18 repeated phrases, one of them a sentence a human
had flagged by ear before the run started. That run had the verb installed and never reached for
it, because it had read the whole manual and verify appeared only in the --help command table.
Budget for it: the sweep costs roughly 1 second per 5 seconds of render at the default
concurrency (measured: 358-second render, 44 windows, 66 seconds, concurrency 4, ~1.16GB resident
per concurrent whisper process). Round 1 sweeps the whole render; from round 2 on, only the spans
that round actually changed get re-transcribed, and everything else carries forward from the
previous round's findings. Re-sweep the whole thing at any time with vcut verify --windows <render> --lang <code>.
--jq <expr> filters and reshapes, so never reach for python3 -c. --fields projects to
dot paths; --jq does the structural work — filter a nonspeech list to the
vocalization-suspect spans, sort by startMs, pull the spans past a position. Both imply
--json and are mutually exclusive. vcut --help carries the supported subset.
The habit this exists to break, measured: one run wrote python3 -c forty-five times and used
--jq once, and its own retro found no missing operation — pure habit. The rule: if the JSON
came out of a vcut command, the filter goes on that same command as --jq, not on a file you
saved and re-parsed. Piping to a file first is how vcut output stops looking like vcut output.
Python is for what genuinely is not vcut JSON (an SRT is text, not JSON), nothing else.
vcut nonspeech cut.wav --verify --jq '.spans[] | select(.reading == "vocalization-suspect")'
say --transcribe --words measures a word boundary, so never bisect one by hand. When the
transcript and a re-transcribed window disagree about where a word starts — a fused region, a
retake's near edge, any boundary worth doubting — that is the arbiter: it extracts exactly
--at..--through, re-transcribes it with word-level cues, and returns every word in absolute
source milliseconds, one transcription for the whole span. The alternative is what a run
actually did: six to eight --transcribe calls at shrinking windows, then raw ffmpeg -ss/-t
plus a fresh transcription to build its own ground truth, about a third of its budget for one
number. Shrinking a window does not converge on the answer, because a short window reads like a
clean start wherever you open it.
vcut say source.mp4 --transcribe --words --at 550.0 --through 553.0 --lang es
semantic merge combines proposal files, so never merge JSON by hand. Two rounds of
proposals, re-sorted by startMs, in one call:
vcut semantic merge round-1.json round-2.json --out proposals.json
One edl build emits both formats. --report-json <path> writes the full JSON report to
disk regardless of stdout mode, so --human on stdout and the report joins --report wants
come from the same run rather than a second build.
vcut edl build --detect detect.json --semantic proposals.json \
--output cut.mp4 --campaign x --human --report-json report.json
Human decision boundary
vcut proposes. The human decides.
| vcut may propose | The human decides |
|---|
| silence cuts | delivery quality |
| review candidates | semantic changes |
| crop options | acceptable jump cuts |
| which mistakes stay human |
There is no approve command, and that is the design. Approval means editing the EDL: set
approval.status to "approved" and each segment's approval to "approved". No CLI verb
does this, because a verb would be a thing an agent can call, and this is the one step that must
not be automatable. Never make that edit on the human's behalf, not even when they say the
preview looks good: hand them the path and let them do it, or ask them to say explicitly that
they want you to write it. Everything before this point is reversible; this is what makes a
master.
commit only ever drafts and previews — it does not write approval and does not accept
--mode master. Never render a master without explicit approval. Never overwrite source media.
session gc never runs on its own, and --apply is required even then.
Presets
Presets carry thresholds proven in production. Do not invent new ones.
| Preset | Threshold | Use |
|---|
noisy (default) | -20 dB | Events, ambient noise |
clean | -30 dB | Studio, talking head |
podcast | -35 dB | Intentional pauses |
When the recording matches a row, use it. When it does not, start at clean and let the
numbers move you: most speech recorded on purpose sits closer to a room than to an event, and
being one step too conservative costs a round while being too aggressive costs syllables. Read
the removal percentage against the target for the content type, then change the preset rather
than --min-silence or --margin, and rebuild.
| Content | Expected removal |
|---|
| Event or interview | 30-45% |
| Tutorial or screencast | 15-25% |
| Scripted talking head | 10-20% |
Word clamping needs word-level timestamps, one cue per word, from a large model, asked for
a verbatim transcript. A sentence-level SRT turns clamping off. Getting this wrong is not an
error, it is a worse cut:
trx transcribe <input> --words --language es --preset verbatim -m large-v3-turbo --output-dir "$(dirname <input>)"
Why each of those flags is load-bearing, and the two warnings detect prints about transcript
drift: vcut skills get core --section detect.
detect does not look for filler words, and that is deliberate. A word list cannot tell
filler from ordinary use and never survives a new language. Fillers are the model's job, through
vcut semantic, with kind: "filler" in the proposal schema.
Output contract
Every command writes data to stdout and diagnostics to stderr. JSON is emitted automatically
when stdout is not a TTY, so an agent never needs --json. Exit code 2 means the invocation was
wrong, 1 means the run failed.
Positions are seconds, everywhere. --at, --from, --source, --master all take
seconds; the JSON that comes back speaks milliseconds. The one deliberate exception is
cut --start-ms/--end-ms, which takes the milliseconds those JSON outputs emit so a finding
needs no conversion to become a proposal.
Four resolutions on the same run, never four truths. --human reads in a few lines what the
JSON answers in a few hundred. --fields <a.b,c,d> projects to exactly the dot paths you name.
--jq <expr> filters and reshapes. Full JSON when you need everything.
Ask about several positions at once. locate --sources 20,53.86,61.2,
say --positions 19.5,30.0,41.9, and say --at X --through Y each answer in one call what was
being built out of shell loops with a JSON parser inside.
Every JSON output carries vcutVersion. The manual is read once and cached in an agent's
context while the CLI can change underneath it. A version you do not recognise is a reason to
run vcut --help again rather than trust what you read earlier in the session.
Selected outputs carry next: a short array of {question, verb} with real values filled
in. A hint, not an instruction.
Run vcut schema <name> for the field-by-field contract instead of parsing --help.
Which instrument answers which question
| Question | Verb |
|---|
| What is the map of this recording, cached across calls? | open <media> |
| What is really at a position — transcript, audio, blocks, level, aligned? | peek <media> (--ref <ref> | --at <s>) |
| Propose a cut against a session and see what it removes? | cut <media> --refs|--start-ms/--end-ms|--span |
| Build and render everything a session has accumulated? | commit <media> --output <path> --campaign <id> |
| What changed between two committed rounds? | rounds <media> --diff |
| Did this edit catch what an approved human edit caught? | compare --edl <path> --reference <approved edit> |
| Where should I look first in a long file? | suspects --detect detect.json |
| What in this file is worth cutting? | detect <input> |
| What is said at a position, from the existing transcript? | say --transcript ... --at <s> |
| What is actually said there, when the transcript may have averaged it away? | say --transcribe --at <s> |
| Where exactly does a word start, when those two disagree? | say --transcribe --words --at <a> --through <b> |
| Where exactly, at sub-second resolution, does a boundary belong? | silences <media> --from <s> --to <s> --min 0.08 |
| Where does a retake's boundary really fall? | converge --phrase "..." --from <s> |
| Where does the telling I am keeping start (a retake's near edge)? | say --transcribe --words over converge's two edges |
| What audible sound does the transcript not see at all? | nonspeech <render> --verify |
| What does the render say twice, that reading its transcript once cannot show me? | verify --windows <render> (commit runs it every round) |
| What text is a semantic span about to remove? | at propose time, or → |
Sections
Load one when the question comes up. vcut skills list prints this table from the installed
version.
| Section | Read it when |
|---|
workflow | running a whole edit end to end, including the stateless path in full |
rounds-methodology | working a round: what to read, how to widen a span, when a boundary lies |
invariants | deciding whether the edit is done |
how-hard-to-cut | deciding whether a cut is worth making |
semantic | writing proposals against the exported lines |
muletillas | a filler is audible in the render and invisible in every transcript |
classifier | wondering why non-speech needs a model and not a statistic |
eleven-runs | about to run this for the first time — 7 habits, four failures |
detect | picking a preset, or reading a drift warning |
open | refs, gen, and what a session caches |
peek | four views of a position and viewsDisagree |
cut | ref ranges, --start-ms, --span, --list/--drop, --literal for a verified boundary |
commit | what a commit builds, renders, and records |
rounds | comparing two committed rounds |
compare | grading an EDL against an approved human edit, and the eval loop |
session | the store, gc, and the advisory lock |
edl-build | removedText, driftSuspect, boundary warnings, --crop |
render | audio-only, progress, loudness, reproducibility |
|
When something comes out wrong
vcut skills get debug
Read it before diagnosing a render that sounds off, a word that seems cut in half, dead air that
survived a cut, or a transcript whose positions do not match the audio. Every method in it is
cheap; none of them is the obvious one. It exists because for each of those questions there is a
more rigorous-looking instrument that cannot tell the hypotheses apart, and reaching for it is
how confident wrong answers get written down.