@crafter/vcut
v0.23.0
Published
Cut dead air out of a recording, reproducibly. Agent-first CLI over ffmpeg.
Readme
npx @crafter/vcut recording.mp4Why
Cutting silence out of a talking-head recording is mechanical work an agent should do. What an agent should not do is decide which of your mistakes stay in, or overwrite your only copy of a take.
vcut splits those. It proposes cuts as data, and every destructive step is gated. The thresholds are not invented: they come from a pipeline that ran in production on real published video.
Quick Start
npm install -g @crafter/vcut # or: bun add -g @crafter/vcut
vcut doctor # checks ffmpeg and ffprobeRequires ffmpeg and ffprobe on your PATH. On macOS: brew install ffmpeg.
# 1. Find what is worth cutting
vcut detect recording.mp4 --preset clean > detect.json
# 2. Draft an edit decision list
vcut edl build --detect detect.json --output master.mp4 --campaign my-video
# 3. Iterate on the audio, which is where the decisions are
vcut render --edl edl.json --audio-only --output cut.wav
# 4. Preview it, watch it, then render the master
vcut render --edl edl.json --mode previewStep 3 exists because a round of edits asks audio questions and rendering the picture to answer them costs far more wall clock than the round needs. Audio-only is not instant either: it costs roughly 1 second per 14 seconds of audio the cut keeps, driven by kept audio rather than segment count.
In a terminal you get a summary:
recording.mp4 6m 22s
detected dead air ###................. 16.5% (1m 03s)
net after margins ##.................. 10.3% (~39s once 100ms is kept on each side)
silences 119 spans, 1m 03s
longest silence 1s at 6m 20s
filler words not scanned; a word list cannot tell filler from ordinary use. Run vcut semantic.
review candidates 1 (never cut automatically)
clipping: peak level -0.24 dB exceeds -1 dBFSPiped or captured, the same command emits JSON. No flag needed.
Commands
| Command | What it does |
|---------|--------------|
| vcut detect <input> | Silences, clipping, black and frozen frames |
| vcut suspects --detect | Where to look first, ranked, from the pauses detect measured |
| vcut edl build | Turns a detect report into a draft edit decision list |
| vcut semantic export\|check\|review | Hands the transcript to a model, takes proposals back, reads the result |
| vcut render | Renders an EDL; preview accepts proposals, master needs approval |
| vcut locate --edl | Translates between master time and source time |
| vcut audit --edl --render | Checks a render's audio against the EDL it came from |
| vcut joins --edl --render | Verifies every semantic join in one call, the post-render twin of edl build's removedText |
| vcut say <media> | Reads back what is spoken at a position, from a transcript or by asking the audio |
| vcut silences <media> | Speech/silence blocks over a range, at a threshold and minimum you choose |
| vcut converge <media> | Finds where a repeated phrase stops coming back |
| vcut nonspeech <render> [--verify] | Finds audible sound that is not language; --verify reads a window around each span instead of the whole-file transcript |
| vcut verify --windows <media> | Tiles the media into short windows, transcribes every one in parallel, and reports repeated phrases, truncated edges, and anomalies a whole-file transcript smooths away |
| vcut open <media> | Opens or resumes a content-addressed session, maps its blocks with stable refs, caches detection across calls |
| vcut peek <media> (--ref \| --at) | The four views of a position aligned in one call: cached transcript, re-transcribed audio, fine-resolution speech/silence blocks, level, and their disagreement named |
| vcut cut <media> --refs \| --span | Proposes a semantic cut against a session's block refs, shows what it removes before any build |
| vcut commit <media> [flags] | Builds and renders a session's accumulated proposals into a draft EDL, byte-identical to edl build run by hand |
| vcut rounds <media> [--diff N M] | A session's committed rounds; --diff compares two build reports (removalPercent, segments, semantic cuts by span overlap) |
| vcut session list\|gc [flags] | Lists sessions with size and lock state; gc clears disposable session cache (dry-run by default, --apply to delete) |
| vcut schema [name] | The JSON contract per command, versioned |
| vcut skills get core | The bundled agent manual, as markdown. --section <name> serves one deep-dive section; vcut skills list shows what sections exist |
| vcut init | Installs everything a first run needs, and reports what it could not |
| vcut doctor | Checks external dependencies |
| vcut setup classifier | Fetches the optional non-speech classifier |
| vcut <input> | Shorthand for vcut detect |
Cutting is a loop
Silence removal is the first round of several. Each class of defect only becomes visible once the one above it is gone: a pause two adjoining segments create together did not exist in either of them, a broken join only reads as broken once both sides are adjacent, and a discourse marker is inaudible inside loose speech and obvious inside tight speech.
The loop, its stopping condition, and what has already been tried and failed live in the manual the CLI serves, which always matches the version installed:
vcut skills get core # the procedure, the invariants, what eleven runs taught
vcut skills get debug # how to investigate a cut that came out wrongStop when a round proposes nothing, not when the removal percentage looks respectable, and never on the first round.
Beyond silence
--crop top:0.06frames the whole edit at once, so remembering the menu bar after cutting does not mean redoing every segment.--edge-fade(default 50ms) ramps each segment edge to zero. Not a crossfade: overlapping the sides would drift the audio against concatenated video.- Loudness is normalised to the
speechTargetLufsthe EDL declares, on the concatenated result rather than per segment. vcut nonspeech <render> --verifyfinds breaths, mic bumps, and stretched hesitations the transcript cleaned away, which neither the silence pass nor the transcript can see on their own.--verifyre-transcribes a short window around each span instead of checking it against the whole-file transcript, which is circular for exactly this class of sound.vcut setup classifierfetches what the underlying model needs; without it the check falls back to a human ear.
Presets
| Preset | Threshold | Use |
| --- | --- | --- |
| noisy (default) | -20 dB | Events, ambient noise |
| clean | -30 dB | Studio, talking head |
| podcast | -35 dB | Intentional pauses |
Tune with --min-silence (seconds, default 0.3) and --margin (seconds, default 0.10).
Filler words
Word-level timestamps mean one cue per word. A normal SRT has one cue per sentence, which is not enough to cut a single word without guessing. vcut tells you when this is the case instead of silently reporting zero.
whisper-cli -m ggml-large-v3-turbo.bin -f audio.wav -l es \
--max-len 1 --split-on-word --output-srttrx transcribe <input> --words --language es -m large-v3-turbo does the same from [email protected]
on. --split-on-word is not optional: without it --max-len 1 cuts at token boundaries, so
"Crafter" arrives as Cra + fter and the transcript looks word-level while breaking every
cut that relies on it. Measured on one recording: 26% of cues were fragments without the flag,
0% with it, and detect now warns when it sees this.
Ask for a large model. One cue per word means one cue per token, and what counts as a token
depends on the model. On the same three minutes of Spanish, small returns 26% of its cues as
word fragments, splitting "Crafter" into Cra + fter; large-v3-turbo returns 0% and costs
13 seconds. Fragments weaken word clamping and make the semantic export unreadable.
vcut detect recording.mp4 --transcript words.srt --lang esLists ship for es, en, and pt.
detect finds silence, not filler words. A word list matches tokens, not intent: Spanish este is filler in "y este, entonces" and a demonstrative in "en este caso", and no list survives a new language. Filler words are proposed by a model through vcut semantic, like every other judgement call.
For agents
npx skills add Railly/vcut # install the skill for Claude Code, Cursor, or any agentThe installed skill is a thin stub: it points at the CLI rather than copying its contents, so the guidance never drifts from the installed version.
vcut skills list # what the installed version ships, including core's sections
vcut skills get core # the small, always-loaded usage guide, as raw markdown
vcut skills get core --section cut # one deep-dive section, loaded only when needed
vcut skills get debug # how to investigate a cut that came out wrong
vcut schema detect # the JSON contract, versioneddebug is worth reading before diagnosing anything. Every method in it is cheap; none of
them is the obvious one. It exists because for each question 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.
JSON is emitted automatically when stdout is not a TTY, so an agent never needs --json. Data goes to stdout, diagnostics to stderr. Exit code 2 means the invocation was wrong, 1 means the run failed.
Guarantees
- Source media is never modified. Sources are hashed; a changed hash aborts a master render.
- Nothing is approved automatically. Segments are born
proposed, the EDLdraft. There is no--yes. - Renders are reproducible. The same EDL produces a byte-identical file, verified by the
sha256in the output. - The renderer checks its own work against the EDL: dimensions, pixel format, colour metadata, frame count, audio contract. A mismatch fails the run rather than shipping a bad file.
- Session cache never eats an approved edit.
commitwrites the EDL wherever--output/--edlpointed, never inside a session directorysession gccan clear; gc is dry-run unless--applyis passed, and a session a live process is writing to is always protected from it.
Limits
- Semantic cutting is proposal-only.
vcut semantic exporthands the transcript to a model as numbered lines andedl build --semanticfolds the proposals back in, each one markedsemanticRisk: material. vcut never calls a model itself: no dependency, no API key, same EDL for the same proposals file. - Audio ramps 50ms at each segment edge (
--edge-fade 0disables it). Not a crossfade: overlapping the two sides would shorten the render against concatenated video and drift the audio out of sync, so each side fades within its own segment. A joint under a fully continuous sentence can still be heard as a dip. - A silence detector decides by level, so a soft consonant under the threshold is cut like a pause. If a word loses its opening sound, the fix is the recording or a lower threshold, not a larger margin.
- Audio recorded separately works:
detect --audio mic.wavmeasures silence on that file, and the EDL carries both sources.edl build --audio-offset <ms>corrects two recorders that did not start together. - Noise reduction is not offered. There is no safe default: the right amount depends on the room, and on one measured recording a denoiser at a default setting pushed a weak syllable from -45 dB to -57, which is the same defect as a threshold set too high. Loudness normalisation is the part that is safe to automate, and it is on by default.
- No face tracking or automatic zoom.
Design
Why it is shaped this way: design-notes.md. Full documentation at vcut.crafter.run/docs.
License
MIT
