---
title: Add word-by-word captions
description: Word-by-word captions, or subtitles, on your video: each word appears as it is said, in your font and colors, and you can fix any word.
---
You add word-by-word captions, also called subtitles or karaoke captions, to your video. Each line builds up word by word as it is said, in your font and color, and the captions sit where the app's buttons do not cover them. The captions stay editable: fix a misheard name, change the style, or turn them off for one export.

Use invideo for this when you want captions you can still edit after they are made: they are a caption track on your timeline, not text burned into the file, until you export.

## Quickest path

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

```text
Add word-by-word captions to my timeline. Three or four words per line,
white text with a black outline, building up as I speak, in the lower
middle. My name is spelled "Siobhan". Price the transcript first and
show me frames before you export.
```

In a chat app, you drag the video into the project's Media panel in invideo, then ask in the chat. In a terminal, you name the file and the agent uploads it.

## What you need

The video with speech is all you must have. The rest sets the look.

- **The video.** Any video where someone talks: a talking head, a podcast, a tutorial, an interview.
- **Names and words to spell right.** Your name, brand names, slang.
- **The look.** Font, size, text color, an outline or a box behind.
- **Words per line.** One word at a time for punchy Shorts, a few words for most videos.
- **Where on screen.** Lower middle, center, or upper middle for vertical video.

## Steps

Your agent edits on a branch and tells you what changed at each step.

### Add the captions

The agent makes the captions from the transcript first.

<Steps>
<Step title="Bring in the video">
The agent uploads the video with `start_upload` and `add_media` onto the timeline URL, and creates a branch with `timeline_history`. Expected result: a sequence URL and a media bin id for the video, and a branch URL.
</Step>
<Step title="Transcribe the speech">
The agent prices `media` `audio.transcript` on the video's sequence URL from `add_media` with `dry_run`, tells you the price, and starts it with `max_credits`. A transcript refuses a media bin `asset_id`. The agent calls again after `retry_after_s` for the words. Each word has a start, an end and a speaker. Expected result: the full list of words with times.
</Step>
<Step title="Group the words into lines">
The agent splits the words into short lines at pauses and at the end of each phrase, with the number of words you asked for. It fixes names you gave it. It turns the times from seconds in the file into frames on the timeline. Expected result: a list of slides, each with its words and times.
</Step>
<Step title="Add the captions">
The agent runs `add_captions` with the slides, each with its `words`, and a `style` with your font, size, `color`, and `stroke` or `background_color`. Expected result: a caption clip on a video track, each line building up word by word as it is said.
</Step>
</Steps>

### Style, fix and export

Then the agent styles the captions, fixes words and checks them.

<Steps>
<Step title="Place and restyle">
The agent moves the caption clip with `set_position`, and changes the look later with `set_text_style`. For a font of your own, it brings in the font file with `add_media` and uses its id as `font`. Expected result: captions in your style, where you want them.
</Step>
<Step title="Fix the words">
The agent fixes a line with `set_caption_slide_text`. Words that did not change keep their timing. It splits a word in two with `split_caption_fragment`, or joins two pieces with `merge_caption_fragment`. Expected result: every word spelled right.
</Step>
<Step title="Check the captions before export">
The agent runs `capture` at a few lines, checks that the text is readable on a phone, and runs `check_timeline`. Expected result: the captured lines match the words said at those times.
</Step>
<Step title="Export the video">
The agent runs `export` with `format` video. For an export with no captions, it turns the caption clip off with `set_clip_enabled` and exports again. Expected result: an export card with status `completed` for each export.
</Step>
</Steps>

The same job from a terminal:

```bash
invideo agent register <project-url> --name <name>
export INVIDEO_AS=<name>
invideo asset upload <edit-url> ./talk.mp4
invideo media audio.transcript <sequence-url> --dry-run
invideo media audio.transcript <sequence-url> \
  --max-credits <price>
invideo edit show <edit-url> --json
invideo branch new <edit-url> --name captions --token <state_token>
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 \
  --token <state_token> --operations @captions.json
invideo edit capture <branch-url> --at 2,10,30 --out ./frames
invideo edit check <branch-url>
invideo edit export <branch-url> --format video --quality 1080 --json
invideo status <card-url> --wait
invideo notebook show <card-url> --out ./out
```

Where each value comes from:

- `<name>` is the name you choose in `invideo agent register`. `INVIDEO_AS` passes it to every later command. `<project-url>` is your project's URL, from `invideo project list`.
- `<edit-url>` is your video's timeline URL, from `invideo edit new` or the editor.
- `<sequence-url>` is the `url` that `invideo asset upload` returns, a `…/notebook/seq-N.M` URL. A transcript refuses a bin `--asset`.
- `<price>` is the dry run's `.estimated_credits`.
- `<state_token>` comes from `invideo edit show`: on `<edit-url>` for the branch, then on `<branch-url>` for the change. `<branch-url>` is the `.branch_url` that `invideo branch new` returns.
- `@captions.json` reads a bare JSON list of ops, the `operations` list of the call below.
- `<card-url>` is `.sequences[0]` of the export result. `invideo status` waits but saves nothing. `invideo notebook show` saves the video as `./out/export.mp4`, and the agent renames it.

The key MCP call, shown as the full call, adds two lines of captions with word timing and the style. `<branch-url>` is the branch URL `timeline_history` returns, and `<state_token>` comes from `get_timeline` on it. At 30 fps, a word said at 1.2 seconds starts on frame 36.

```json
{
  "url": "<branch-url>",
  "timeline_id": "T1",
  "expected_state_token": "<state_token>",
  "operations": [
    { "op": "add_captions", "track": "V2",
      "slides": [
        { "text": "Hi, I'm Siobhan", "start_f": 36, "end_f": 75,
          "words": [
            { "text": "Hi,", "start_f": 36, "end_f": 48 },
            { "text": "I'm", "start_f": 48, "end_f": 57 },
            { "text": "Siobhan", "start_f": 57, "end_f": 75 } ] },
        { "text": "and this is my kitchen", "start_f": 80,
          "end_f": 132 }
      ],
      "style": { "font_size": 72, "font_weight": 800,
                 "color": "#ffffff", "stroke": true, "stroke_color": "#000000",
                 "stroke_width": 6, "align": "center" } }
  ]
}
```

## Burned-in styles from a model

For a designed caption look in one step, the agent can run a subtitle model on the video instead. These burn the captions into a new video file, so you edit them by running the model again, not on the timeline.

- **`invideo_subtitles`.** Pick a `subtitle_style` such as `SUBTITLE_HIGHLIGHT` or `BOLD_TEXT_BEHIND`, and a `safe_zone` of `tiktok` or `instagram`.
- **`veed_subtitles`.** Pick a `preset`, a `position`, a `vocabulary` list for names, and a `translation_language` for translated subtitles, which costs more.

The agent runs `find_models` with "burned-in subtitles" to pick one, and reads its inputs with `get_model`. It calls `generate` with `dry_run: true`, the video's sequence URL as `video` and the style, and tells you the price, which goes by video length. 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`. Expected result: a sequence URL for the new video, read with `read`. The agent never calls `generate` again to check.

```bash
invideo gen models <project-url> --query "burned-in subtitles"
invideo gen model <project-url> --model invideo_subtitles
invideo gen run <page-url> --model invideo_subtitles --dry-run \
  -i video=<sequence-url> -i subtitle_style=SUBTITLE_HIGHLIGHT \
  -i safe_zone=tiktok
invideo gen run <page-url> --model invideo_subtitles \
  --max-credits <price> --idempotency-key subs-1 \
  -i video=<sequence-url> -i subtitle_style=SUBTITLE_HIGHLIGHT \
  -i safe_zone=tiktok
invideo status <thread-url> --wait
invideo notebook show <thread-url>
```

`<project-url>` is the video's project. `<page-url>` is a notebook page of the project, listed by `invideo notebook show <project-url>`. `<sequence-url>` is the video's sequence URL, from `invideo asset upload` or an export card. `<price>` is the dry run's `.estimated_credits`. `<thread-url>` is the `url` the paid `gen run` returns, and `invideo notebook show` on it lists the new video's sequence URL.

## Captions file out

invideo exports video, not a captions file. For an SRT or VTT file, the agent reads the caption slides with `get_timeline` in mode `collection` and writes the file itself. See the page on an SRT captions file for an LMS.

## 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 captions, the style, the fixes, turning captions on and off, and the export.
- **Costs credits:** the transcript, once per file, or a run of `invideo_subtitles` or `veed_subtitles`, priced by the length of the video.

Captions take the agent a few minutes once the transcript is back. See [Credits and what they cost](/account/credits) for prices.

## If something goes wrong

Most problems come from a misheard word, lines that are too long, or captions under the app's buttons.

| 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. |
| 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. |
| A name is spelled wrong | Fix the line with `set_caption_slide_text`. Words that did not change keep their timing. |
| Lines are too long to read | Ask for fewer words per line. The agent builds the slides again. |
| `add_captions` refused | A word sits outside its slide, or a slide's text is empty. Each word's times must fall inside its slide. |
| Captions are behind the app's buttons | Move the caption clip with `set_position`, or add guides with `create_guide`. |
| A line shows all at once, not word by word | The slide has no `words`. Add them with their times from the transcript. |
| `audio.transcript` refused with `code: "not_found"`, or CLI exit 2 | The call named a media bin asset. Pass the video's sequence URL from `add_media` or `invideo asset upload`. |
| `invideo status --wait` ends and the folder is empty | `status` does not download. Run `invideo notebook show <card-url> --out <dir>`. |
| Avoid: promising the spoken word in its own color on the timeline | Instead: the words build up as spoken. For a burned-in highlight look, try a subtitle model style such as `SUBTITLE_HIGHLIGHT`. |
| `audio.transcript` returns a status, not words | It is still working. Call it again after `retry_after_s`. Only the first call spends credits. |
| Avoid: burning in captions when you may change them | Instead: use `add_captions` on the timeline, and export when they are right. |
| Avoid: saying done without looking | Instead: `capture` a few lines and read them against the transcript. |

## Prompts to try

Each prompt is a real variation of the job.

```text
Add one-word-at-a-time captions to my Short, big and bold in the center,
each word popping in. Use my font ./fonts/brand.otf.
```

```text
Add clean captions to my tutorial, two lines at most, white on a dark
box at the bottom. Then export two versions: one with captions and one
without.
```

```text
Add Spanish subtitles to my video with veed_subtitles. Price it first,
and keep my brand name "Lumio" spelled as it is.
```

## When to use something else

invideo makes captions in the picture, not a closed caption track inside the file. For SCC files, or broadcast closed captions, use a captioning service such as Rev or 3Play Media. For a sidecar SRT or VTT, the agent writes it from the caption slides.

## Related

- [Make an SRT captions file for an LMS](/how-to/training/srt-for-an-lms)
- [Make Reels and Shorts from your footage: hook, captions, transitions, sound effects](/how-to/creators/reels-and-shorts-from-your-footage)
- [Cut ums, pauses and retakes](/how-to/creators/cut-ums-and-retakes)
- [Repurpose one video for every platform](/how-to/creators/repurpose-for-every-platform)
- [Make closed captions and audio description for ADA Title II and Section 508](/how-to/training/captions-for-ada-508)
- [Check your video before you say it is done](/how-to/check-your-video)