---
name: beat-sync-editing
description: Cut a NodeTool timeline to music and shape its pacing — detect the beat grid, place cuts on phrases, pick a cut type, build speed ramps with time remap, and give the piece an arc. Use when clips should land on the music, when pacing drags or feels choppy, when a hit needs impact, or when transitions feel arbitrary. Not for what each shot contains.
featured: true
---

# Beat-Sync Editing → cuts, rhythm and retiming

Where a cut lands matters as much as what is on screen. This turns a track and a
brief into millisecond cut points on the timeline.

`motion-direction` sets the energy; this turns it into a grid and an arc.
`motion-graphics` carries the op contract for the calls named here.
`video-audio-continuity` decides where the bed comes from before there is a
grid to read: a track you add yourself, or one generated clip carrying every
scene.

## Get the grid from the audio, not from arithmetic

When the bed does not exist yet, generate it with the grid in mind: a Stable
Audio prompt states its BPM (`stable-audio-prompting`), and an ElevenLabs
`composition_plan` pins each section's length in milliseconds
(`elevenlabs-audio-prompting`), so the drop lands where the plan says instead
of where the model felt like putting it. Then measure it anyway — the
generated file, not the prompt, is the grid.

`detect_audio_events` on the music clip reports onsets and a tempo.

- `onsets.times` are in **seconds**. Every timeline op takes milliseconds, so
  multiply by 1000. This is the single most common mistake here.
- Read `tempo.reliable` before you build anything off `bpm`. Speech and room
  tone produce a confident-looking number from nothing. Unreliable means use the
  onsets directly.
- The first onset is the grid's offset. A grid anchored at 0 instead of beat one
  puts every cut a few frames early — the edit that feels "almost right".

With a reliable tempo:

```
ms_per_beat = 60000 / bpm
frames_per_beat = (60 / bpm) × fps          # fps comes from get_timeline
```

At 120 BPM: 500ms a beat, 15 frames at 30fps, a 2000ms bar. Round each
**cumulative** beat, never the step, or the grid drifts over a long edit.

Lay the grid down so you can see it, then pull edges onto it:

```json
{"op": "set_markers_from_beats", "onsets_ms": [0, 500, 1000], "count": 32}
{"op": "snap_to_beats", "targets": "all", "tolerance_ms": 60,
 "mode": "start", "action": "move"}
```

Either takes `onsets_ms`, or a `bpm` with an `offset_ms`. The 60ms default
tolerance is about two frames at 30fps — close enough to read as on the beat,
tight enough not to drag an edge onto a beat it was never near. `snap_to_beats`
reports every target including the ones nothing was in reach of, with the reason.
`action: "move"` keeps the clip's length and shifts what follows; `action:
"trim"` changes the length and leaves the neighbours alone. Move a title that has
to land on a hit; trim a picture clip whose out point has to meet the next shot.

For an animation that should follow tempo changes, store a `beat` anchor on the
animation: `{index: 3, scope: "clip", offsetMs: -20}` counts from the clip's
start; `scope: "sequence"` counts from the document tempo's `offsetMs`.
`index` is one-based, and `delayMs` adds another offset. This is distinct from
snapping a clip edge or baking a measured audio curve. `animate_clip` does not
accept `beat` in its tool input; write the animation's `beat` through the full
document with `set_timeline_document`. A stored animation needs its `id`,
`role`, `preset`, and `durationMs`; keep those and the rest of the animation
when adding `beat`. Keep the document's tempo, setup, media tracks, template
identity, clips, tracks, and markers in the write.

## Cut on phrases, not on beats

A cut on every beat is relentless and the viewer fatigues by beat eight. Cut on
2, 4 or 8 beats, and accent on the eighth grid sparingly.

| Personality | Cut every | Transition family | Retime |
|---|---|---|---|
| Playful | 4 beats, syncopated accents | Hard cut, occasional `push` | Light ramps |
| Premium | 8–16 beats, long holds | Hard cut, occasional `crossfade` | Slow, smooth |
| Corporate | 8 beats, steady | Hard cut only | None |
| Energetic | 1–2 beats at the peak | Hard cut, `zoom`, `push` | Aggressive, on the beat |

Put the single biggest visual on the drop. One piece, one peak.

## Shape an arc

Uniform pacing reads as flat regardless of content.

| Phase | Cut every | Shot length | Note |
|---|---|---|---|
| Establish | 8–16 beats | Longest | Set the place, let it breathe |
| Develop | 8 → 4 beats | Shortening | Accelerate to build |
| Climax | 1–2 beats | Shortest | The drop, the biggest visual, the ramp |
| Resolve | One long hold | Longest | The end card breathes after the peak |

## Hide the seam

A cut is invisible when motion carries the eye across it. Cut during a movement,
and match its direction and speed across the cut. For graphics, cut at a
transform peak — mid-travel, not at rest.

Default to the hard cut: two clips on the same track, meeting exactly. Reach for
anything else only when it is motivated.

| Cut | How, on the timeline | Reach for it when |
|---|---|---|
| Hard cut | Adjacent clips, no overlap, no `set_transition` | Nine cuts in ten |
| Match cut | Hard cut where both frames share a shape, position or motion vector | Two shots rhyme; the strongest move in motion design |
| Crossfade | `set_transition` `crossfade` on the incoming clip, clips overlapped by at least its `durationMs` | Two shots of the same scene |
| Dip | `dipToColor` with your own `color` | A chapter break |
| Wipe / push / slide | Those types with a `direction` naming the edge the incoming clip arrives from | A graphic feel, or lateral energy |
| Zoom | `zoom` | Pushing into the next beat |
| J-cut | The next clip's audio clip starts 4–12 frames before its picture, on its own audio track | Dialogue, reveals: it pulls the viewer forward |
| L-cut | The current clip's audio runs 12–24 frames under the next picture | Scene changes, ambience |
| Whip | A short `offsetX` move plus a `blur` effect either side of a hard cut | Hype, a scene jump |

Two things the engine does whether you asked or not:

- Two clips on one track whose times overlap dissolve across the overlap with no
  call at all. **An accidental overlap is an accidental dissolve.** When a cut
  looks soft where you wanted it hard, check `startMs` and `durationMs` first.
- A transition plays over the incoming clip's head against whatever is beneath
  it. With no overlap there is nothing under it and it reads as a fade from
  black.

J and L cuts are built by splitting picture and sound onto separate clips, not
by a transition: the shot's audio twin already sits on its own audio track after
an assemble, so move that clip rather than the picture.

## Speed ramps

`set_time_remap` maps timeline time onto source time. `t` runs 0..1 over the
clip's own window, must start at 0, end at 1 and ascend; `sourceMs` says which
millisecond of the source plays there. Descending `sourceMs` is a reverse, a flat
pair is a freeze.

Ramp into a hit and snap out, with the slowest frame on the beat. On a 2000ms
clip whose impact sits at 1200ms:

```json
{"op": "set_time_remap", "target": "Shot 3", "timeRemap": {"keyframes": [
  {"t": 0, "sourceMs": 0},
  {"t": 0.5, "sourceMs": 900, "easing": "easeIn"},
  {"t": 0.6, "sourceMs": 1000, "easing": "easeOut"},
  {"t": 1, "sourceMs": 2400}]}}
```

Between 0.5 and 0.6 the source advances 100ms across 200ms of timeline: quarter
speed on the hit. `t` that repeats or goes backwards is
`time_remap_not_monotonic`, an error. `split_clip` and `trim_clip` refuse a
remapped clip, because re-deriving the curve silently is how a ramp becomes a
different move — clear the remap, cut, re-apply.

`speedMultiplier` on `set_clip_params` is the flat alternative: one rate for the
whole clip, no curve. Use it when nothing needs to ramp.

For rhythmic texture, `steppedTime: {fps}` quantizes one clip's sampling
clock. `temporalEcho: {copies, intervalMs, opacityDecay}` draws delayed copies,
which can trail a hit without duplicating clips by hand. A clip's `motionBlur`
sets `samplesPerFrame` and `shutterAngle` for its own shutter window. Values
above one request a minimum scene sample count; the scene uses the highest
request, capped at 32, and samples each layer evenly over its own shutter.
Set these fields in the full timeline document. `set_clip_params` refuses them.

If motion must follow the actual audio envelope or detected onsets, use
`bake_audio_animation`. It writes a source-anchored curve with `bakedFrom`
provenance through `set_baked_animation`; re-baking replaces that measured
curve. A beat anchor instead recomputes timing from the document tempo, while
markers and `snap_to_beats` move edit points.

## The plan, before the calls

Write the cut down before editing, in the units the ops take:

- Track spec: `bpm`, `fps`, the first onset in ms, the drop in ms, ms per beat.
- The four phases with their ms ranges and target shot length.
- Every cut as an absolute ms, its clip, and its cut type.
- Audio offsets in ms for each J and L cut.
- Each ramp as its `t` / `sourceMs` keyframes.
- The marker list you will pass to `set_markers_from_beats`.

## Check it

`validate_timeline` after every batch: it catches overlapping clips, a
transition longer than the clip carrying it, and clips shorter than a frame.

Then look and listen. `preview_timeline_frame` at the ms either side of a cut
says whether the seam is where you think it is; `analyze_audio` on the music
confirms the grid you built matches the file. Before calling the cut done,
`render_timeline` — a preview answers what one timecode looks like, a render
answers whether it plays.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Frantic | A cut on every beat | Cut on 2, 4 or 8 beat phrases |
| Almost on beat | Grid anchored at 0, ignoring the first onset | Anchor to the onset, in ms |
| Drift over a long edit | Rounding each step | Round each cumulative beat |
| Jarring cut | Placed at rest, between motions | Cut mid-motion, match direction |
| Soft where it should be hard | Overlapping clips on one track | Fix `startMs` and `durationMs` |
| Transition fades from black | Nothing beneath the incoming head | Overlap the clips by the transition's `durationMs` |
| Flat | Uniform pacing | Establish, develop, climax, resolve |
| Nothing lands | No single peak | One biggest visual, on the drop, then a hold |
