---
title: Make a music video with lip sync and beat sync
description: A music video cut on the beat of your song, with speed ramps on the drops and the singer's lips synced to the vocal, on a timeline you can keep editing.
---
You get a music video cut to your song, with cuts on the beat, also called beat sync, and the singer's mouth matched to the vocal, also called lip sync. Your agent reads the song's tempo, beats and sections, cuts your performance and b-roll shots on them, ramps the speed into the drops, and re-syncs the lips on performance shots that drift.

Use invideo for this when you want the beat map and the cut in one place: the agent reads the beats, places every cut on a frame you can move later, and runs lip sync only on the shots that need it.

## Quickest path

Set up your agent first with [invideo for AI agents](/agents). Then give it this prompt.

```text
Make a music video for ./song.wav from the clips in ./shots. Cut on the
beat in the verses and on alternate beats in the chorus. Ramp the speed
into the drop. Lip-sync the performance shots to the vocal. Price the
lip sync first. Work on a branch.
```

In a chat app, you drag the song and the clips into the project. In a terminal, you name the folders and the agent uploads them.

## What you need

A clean song file gives the best beat map. Performance shots work best when the singer faces the camera.

- **The song.** The final master, as WAV or MP3.
- **Performance shots.** The singer miming or singing the track. Name the section each one covers.
- **B-roll.** Shots for the verses and the breaks.
- **The vocal, if you have it.** A vocal stem makes lip sync more exact. Without one, the agent splits it from the song.
- **The shape.** 16:9 for YouTube, 9:16 for Reels and TikTok.

## Steps

The agent works on a branch and tells you the timecodes of each section.

<Steps>
<Step title="Bring in the song and the shots">
The agent sets up in three calls.

1. It makes a timeline with `new_timeline`.
2. It creates a branch with `timeline_history` and `action: "create_branch"`.
3. It adds the files with `add_media` on the branch URL.

Expected result: a sequence URL and a media bin id for the song and each shot.
</Step>
<Step title="Map the song and mark the sections">
The agent runs `media` with `audio.sections` for verse, chorus and bridge, `audio.markers` for the tempo, phrases and drops, and `audio.beats` for strong and weak beats. It turns each time into a frame: seconds times the frame rate. Then it places the song on the music track `A2` with `insert_media`, and adds a marker per section with `add_marker`, named "Verse 1", "Chorus 1" and so on. Expected result: section markers you can see on the timeline.
</Step>
<Step title="Cut on the beats">
The agent places each shot with `place_media`, starting on a beat frame, and cuts with `split_clip` and `trim_clip` so each cut lands on a beat. It sends all the cuts in one `edit_timeline` call. Expected result: a new `state_token`, and cuts that land on the beat.
</Step>
<Step title="Ramp the speed into the drop">
The agent sends `retime` kind `ramp` on the shot before a drop, with `start_speed`, `end_speed` and the frame range. For a hit, it uses kind `freeze` to hold one frame. Expected result: the shot speeds up or holds where you asked. The shot's own sound is silent while its speed changes, so the song carries the sound.
</Step>
<Step title="Lip-sync the performance shots">
The agent works through each performance shot in four calls.

1. It cuts the song to the shot's length with `media` `audio.trim` on the song's sequence URL, or splits the vocal first with `audio.separate`. Each runs with `dry_run: true` first, and the agent tells you the price. For a price of 1 credit or more it waits for your yes. Then it runs it with `max_credits` set to that price and an `idempotency_key`.
2. It runs `find_models` with the job in words, picks one, and reads its inputs with `get_model`. A good pick is `sync_lipsync_v3.video`. Your agent can search for others with `find_models`.
3. It calls `generate` with the shot as `video`, the audio slice as `audio` and `dry_run: true`, and tells you the price. For a price of 1 credit or more it waits for your yes. Then it calls `generate` with `max_credits` set to that price and an `idempotency_key`. When the result lands, it reads the sequence URL with `read`, and never calls `generate` again to check.
4. It swaps the synced take in with `place_media` mode `replace`.

Expected result: a synced take in the same place on the timeline.
</Step>
<Step title="Check the sync, then export">
The agent runs `media` with `video.frames` on each synced take at a few sung words, and `capture` on the timeline at the same moments. It checks that the mouth is open on the open vowels. It runs `check_timeline`, then `export`. Expected result: an export card with status `completed`, and a list of any shot where the lips drift.
</Step>
</Steps>

The same job from a terminal:

```bash
invideo edit new <project-url> --name "Music video" --fps 24
invideo asset upload <edit-url> ./song.wav ./shots/*.mp4
invideo edit show <edit-url>
invideo branch new <edit-url> --name cut-v1 --token <state_token>
invideo edit show <branch-url>
invideo media audio.sections <branch-url> --asset m1
invideo media audio.markers <branch-url> --asset m1
invideo media audio.beats <branch-url> --asset m1
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @beat-cuts.json
invideo media audio.trim <song-sequence-url> --start-time 42.5 \
  --end-time 50.5 --dry-run
invideo media audio.trim <song-sequence-url> --start-time 42.5 \
  --end-time 50.5 --max-credits <price> --idempotency-key slice-chorus-1
invideo gen models <project-url> --query "lip sync a video to audio"
invideo gen model <project-url> --model sync_lipsync_v3.video
invideo gen run <page-url> --model sync_lipsync_v3.video --dry-run \
  -i video=<shot-url> -i audio=<slice-url> -i sync_mode=cut_off
invideo gen run <page-url> --model sync_lipsync_v3.video \
  --max-credits <price> --idempotency-key sync-chorus-1 \
  -i video=<shot-url> -i audio=<slice-url> -i sync_mode=cut_off
invideo status <thread-url> --wait
invideo notebook show <thread-url>
invideo edit capture <branch-url> --at 43,45,47,49 --out ./frames
invideo edit export <branch-url> --format video --quality 1080
```

Where the values come from: `<project-url>` from `invideo project new`, and `<edit-url>` from `invideo edit new`. `<state_token>` comes from `invideo edit show` on the URL the next command runs on: `<edit-url>` for `branch new`, then `<branch-url>`, read fresh, because the main token is stale on the branch. `<branch-url>` is the `branch_url` that `branch new` returns. `m1` is the song's bin id, which the reads that return at once take with `--asset`. `<song-sequence-url>` and `<shot-url>` are the `url` values `invideo asset upload` returns. `<slice-url>` is the sequence URL `audio.trim` returns. `<page-url>` comes from `invideo notebook show <project-url>`. `<price>` is the price each dry run returns, `credits` on a `media` op or `estimated_credits` on `gen run`. `<thread-url>` is the `url` of the paid `gen run`. The file in `--operations @beat-cuts.json` is a bare list of ops, like the `operations` list below.

The key MCP call cuts on two beats and ramps into the drop. This fence is the full MCP `edit_timeline` call. Each `at_f` is a beat time in seconds times the frame rate. The later cut goes first, because the right half of a split is a new clip.

```json
{
  "url": "<branch-url>",
  "timeline_id": "T1",
  "expected_state_token": "<state_token>",
  "operations": [
    { "op": "split_clip", "clip": "c4", "at_f": 264 },
    { "op": "split_clip", "clip": "c4", "at_f": 252 },
    { "op": "retime", "clip": "c6", "kind": "ramp",
      "from_f": 1008, "to_f": 1056,
      "start_speed": 1, "end_speed": 3, "shape": "ease_in" }
  ]
}
```

## Cost and time

Edits and exports are free. Generation and transcripts cost credits. Paid stock costs a small credit charge, or one of your workspace's iStock licenses. Your agent runs a dry run first and tells you the price before it spends anything.

- **Free:** the cuts, the speed ramps, `capture` and the export. `audio.sections`, `audio.markers`, `audio.beats` and `audio.trim` show their price in the dry run before anything starts.
- **Costs credits:** each lip sync with `sync_lipsync_v3.video` or `kling_lipsync`, and a vocal split with `audio.separate`.

The beat map and the cut take the agent a few minutes. Each lip sync takes a few minutes more. See [Credits and what they cost](/account/credits) for prices.

## If something goes wrong

Generated lip sync can drift on fast lyrics, on profile shots, and when the face is small in the frame. Check every synced take.

| Symptom | Fix |
| --- | --- |
| `get_model` or `generate` refuses the model as not found | Models retire. Run `find_models` with the job in words, use the best match, and tell the person which model you used. |
| The cuts feel a little late | Move them a frame or two earlier with `trim_clip`. People hear the beat before they see the cut. |
| The beats land between frames | Round each beat to the nearest frame. |
| The lips drift late in a shot | Lip-sync a shorter slice, or split the shot with `split_clip` and sync each part. |
| The lips do not move on a profile shot | Lip sync needs a visible mouth. Use a front-facing shot, or keep the profile shot on b-roll. |
| The synced take is a different length | Set `sync_mode` on `sync_lipsync_v3.video` to `cut_off`, so the output ends with the shorter input. |
| `retime` is refused | The clip loops. Place the shot again from the media bin with `place_media`, then retime the new clip. |
| `audio.trim` or `audio.separate` is refused as `not_found` | It got a bin id like `m1`. Pass the song's sequence URL instead. |
| Exit code 6, or `status` is `held` | A safety review, not a refusal. Wait, then run `invideo status <url> --wait` again. Do not resend. If it ends `failed`, change the input the review flagged. |
| Avoid: saying the sync is fine without looking | Instead: check `video.frames` at sung words on every synced take. |
| Avoid: calling `generate` again to check on a sync | Instead: `read` the thread URL or call `status`. |

## Prompts to try

Each prompt is a real variation of the job.

```text
Cut a 9:16 version of the chorus only, one cut per beat, with a freeze
frame on the last beat before the drop.
```

```text
Split the vocal from ./song.wav, then lip-sync the close-ups to the
vocal with kling_lipsync. Show me frames at a few sung words in each.
```

```text
Cut on every beat on one branch, and on every phrase from audio.markers
on another. Tell me which one you would pick and why.
```

## When to use something else

invideo has no audio-reactive effect that moves with the music on its own. For visuals that pulse with the sound, use a visualizer tool. Or cut on the beats as above, and add a zoom on the hits with a keyframe lane: `add_keyframe_lane` with `zoom_x` and `zoom_y`, then `add_keyframes`.

For a music video shot on many cameras at once, sync the cameras by timecode in a multicam editor like Premiere Pro or DaVinci Resolve first, then bring the angles in.

## Related

- [Cut a trailer or teaser from your film](/how-to/film/trailer-or-teaser)
- [Make an AI short film with a character that stays the same](/how-to/film/short-film-consistent-character)
- [Make a lyric video or text animation for your song](/how-to/creators/lyric-video)
- [Make a Spotify Canvas and a music visualizer](/how-to/creators/spotify-canvas-and-visualizer)
- [Check your video before you say it is done](/how-to/check-your-video)