Skip to content

Fix cut boundaries: timeline drift, audio-guided edges, phrase cutter - #8

Open
duncsdownunder wants to merge 3 commits into
nateherkai:mainfrom
duncsdownunder:main
Open

Fix cut boundaries: timeline drift, audio-guided edges, phrase cutter#8
duncsdownunder wants to merge 3 commits into
nateherkai:mainfrom
duncsdownunder:main

Conversation

@duncsdownunder

@duncsdownunder duncsdownunder commented Sep 10, 2026

Copy link
Copy Markdown

Three related fixes for cut boundaries, found while cutting nine short ads out of a 52-minute podcast with this kit.

1. Timeline drift (commits 2db07f9, 4e9dd06)

cut-silences and apply-cuts computed the retimed transcript from exact float subtraction while ffmpeg cut on frame boundaries, and the filtergraph wrote trim times at millisecond precision, which rounds a 33.333ms boundary past the frame and drops it. Across a few hundred cuts the transcript drifted 1–3s from the render. Fix: snap delete edges to the source frame grid (only with --video) and write trim times at microsecond precision.

Controlled A/B on one identical 45-cut clip:

build drift at end of clip
before 0.27s
snap only, ms precision 0.31s (no improvement)
snap + µs precision 0.01s, rendered duration matches the transcript exactly

2. Audio-guided cut edges (this commit)

ASR word timestamps are 50–150ms coarse and speakers usually start the next word inside that window, so a cut at word.end lands on the first syllable of the following word. apply-cuts.mjs now moves each delete edge to the quietest 30ms nearby (up to 150ms outward, 30ms inward, on a 16 kHz mono extract of the source) and never past the neighbouring kept words, before frame snapping. On by default with --video; --no-snap-audio keeps the old behaviour. The EDL records both requested and snapped times. Helper: scripts/lib/audio-edges.mjs, with its own tests (tests/audio-edges.test.mjs).

Measured on a 41-cut set: last-80ms energy before each cut went from speech level (≈-30 dB) to -31…-78 dB.

3. scripts/cut-phrases.mjs

The phrase-driven cutter that produced the ads: name each segment's first and last words, resolve them against short ground-truth transcripts of the source file itself (never a retimed transcript), snap, cut with dense keyframes (-g 30, the renderer otherwise warns about seek failures), assemble, then re-transcribe the assembly and diff it against the intended text.

Docs

  • cut-mistakes/SKILL.md: retimed-transcript precision section (fix 1), new "Audio-guided edges" section (fix 2), pointer to cut-phrases.
  • cut-silences/SKILL.md: precision section with the A/B numbers.
  • short-form-edit/SKILL.md: cut-phrases, keyframes, and lint + validate + --strict before every render.
  • docs/WORKFLOW.md: the new flag and script.

npm test: 14/14. Skills synced to .agents/.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PbLt6aLpY5gruwSHS2AXaC

duncsdownunder and others added 3 commits September 10, 2026 17:02
Both apply-cuts.mjs and cut-silences.mjs computed retimed transcript
timestamps by exact float subtraction, then fed those same floats to
ffmpeg's trim/atrim filters, which can only cut on real frame boundaries.
Each cut silently lost a fraction of a frame versus the math, and the
error compounded linearly with cut count (measured up to ~1.5s drift
across 163 cuts on a real 48-minute podcast render), so any downstream
tool trusting the retimed transcript against the actual rendered video
would land on the wrong word once enough cuts had accumulated.

Snap every cut boundary to the nearest real video frame (detected via
ffprobe) before computing the retimed transcript, when --video is given.
Testing on real footage shows this substantially reduces drift but does
not fully eliminate it (real frame spacing isn't perfectly uniform), so
both SKILL.md files now document the residual limitation and require
re-transcribing the final render to diff against intended text before
calling any such cut "done" -- frame strips alone can't catch a uniform
time offset on a locked-off single camera.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbLt6aLpY5gruwSHS2AXaC
…holds

The previous commit snapped cut boundaries to real frame times but then
wrote them into the ffmpeg filtergraph with toFixed(3). A 30fps frame
boundary is 33.333...ms, so millisecond rounding pushed the value past the
frame's real timestamp, ffmpeg dropped the boundary frame, video ran one
frame short of its sample-exact audio, and concat shifted every later
segment a few ms in the same direction. Net effect: the snap did nothing.

Controlled A/B on one identical 45-cut clip, drift measured by
re-transcribing each render:
  pre-fix                       0.27s at end of file
  snap + ms-precision (previous) 0.31s -- no improvement
  snap + us-precision (this)     0.01s, rendered duration matches the
                                 transcript's claim exactly

The previous commit message and both SKILL.md files overclaimed a
substantial reduction that was never measured against a controlled
baseline; the docs now state the measured comparison and keep the
"re-transcribe the final render and diff" requirement as defense in depth.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbLt6aLpY5gruwSHS2AXaC
…bugs

Second cut-boundary fix from the podcast cutdowns project. The first (frame
snapping at microsecond precision) fixed timeline drift; this one fixes cuts
landing on the first syllable of the next word.

Cause: ASR word timestamps are 50-150ms coarse and speakers usually start
the next word inside that window, so a cut at word.end (plus any pad) clips
the following onset. apply-cuts.mjs now moves each delete edge to the
quietest 30ms nearby (up to 150ms outward, 30ms inward, on a 16 kHz mono
extract) and never past the neighbouring kept words, before frame snapping.
On by default with --video; --no-snap-audio restores the old behaviour.
The helper is scripts/lib/audio-edges.mjs with its own tests.

scripts/cut-phrases.mjs is the phrase-driven cutter used to build the nine
ads: name each segment's first and last words, resolve them against short
ground-truth transcripts of the source file itself, snap, cut with dense
keyframes, assemble, then re-transcribe the assembly and diff it against
the intended text.

Skill and workflow docs updated: audio-guided edges section in
cut-mistakes, cut-phrases + keyframe + strict-render guidance in
short-form-edit, apply-cuts flag in WORKFLOW.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbLt6aLpY5gruwSHS2AXaC
@duncsdownunder duncsdownunder changed the title Fix cut boundary drift in cut-silences/cut-mistakes retiming Fix cut boundaries: timeline drift, audio-guided edges, phrase cutter Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant