---
title: Make closed captions and audio description for ADA Title II and Section 508
description: Make closed captions as an SRT or VTT file and an audio description track for a training video, ready for your accessibility review.
---
Make an accessible training video: closed captions, subtitles for deaf and hard of hearing viewers, and audio description for blind and low vision viewers. Your agent makes a captions file your player can turn on and off, and a separate audio description track or a described version of the video. These are the parts that ADA Title II and Section 508 reviews look for.

Use invideo for this when you want the captions and the description on the same timeline as the video, so a fix to the cut, the captions or the description stays in step.

invideo makes the files. It does not certify that a video meets ADA Title II, Section 508 or WCAG. An accessibility reviewer does that.

## Quickest path

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

```text
Make this training video <edit-url> accessible. On a branch: closed
captions as lesson.vtt from caption slides I can check, and an audio
description track that describes what is on screen in the pauses. Give me
the clean video, the VTT, a described version of the video, and the
description as its own audio file.
```

In a chat app, the agent gives you the VTT text to save and the export cards. In a terminal, it writes the files into your folder.

## What you need

These are the inputs and the choices.

- The video, on an invideo timeline or as a file.
- Your player or LMS, and what it takes: SRT or VTT, a second audio track, or a second video.
- Names and terms the transcript may get wrong.
- Who reviews the result for compliance.

## Steps

Captions come first, then the description, then the files.

<Steps>
<Step title="Transcribe the video">
The agent prices `audio.transcript` with `dry_run` and tells you the price. For a price of 1 credit or more it waits for your yes. Then it starts the transcript with `max_credits` set to that price, and calls again after `retry_after_s` for the words. It runs it on the video's sequence URL from `add_media`. It sees every word with its time and speaker.
</Step>
<Step title="Caption on the timeline">
The agent creates a branch with `timeline_history` and adds the captions with `add_captions`, with speaker names and the sounds that matter to deaf viewers, like "[alarm rings]". It sees a caption clip, and you check the words.
</Step>
<Step title="Write the closed captions file">
Closed captions are a sidecar file the viewer turns on, unlike burned-in open captions. The agent reads the slides with `get_timeline` mode `collection`, collection `caption_slide`, and writes the SRT or VTT file itself. It sees a file with one cue per slide.
</Step>
<Step title="Find the gaps and write the description">
The agent runs `media` `audio.content` for the silence spans, and `capture` at each scene to see what is on screen. It writes short lines that describe what a viewer cannot hear: text on screen, actions, a chart's point. You approve the lines. It sees your yes.
</Step>
<Step title="Voice the description lines">
1. The agent runs `find_models` with "narration voice", picks one, and reads its inputs with `get_model`. A good pick is `elevenlabs_text_to_speech.elevenlabs_tts_v4`. Your agent can search for others with `find_models`. It finds a voice with `search_library` in `elevenlabs_text_to_speech_voices`.
2. It calls `generate` with `dry_run: true`, each line as `prompt` and the `voice_id`, and tells you the price.
3. 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` per line. It gets a thread URL and, when the result lands, a sequence URL per line. It reads each URL with `read` and never calls `generate` again to check.
</Step>
<Step title="Place the lines on a described copy">
The agent copies the timeline with `duplicate_timeline`, adds an audio track named "Audio description" with `add_track`, and places each line in its gap with `insert_media`. The clean timeline stays untouched. It sees the track in `get_timeline`.
</Step>
<Step title="Export each file">
The agent exports the files your player needs, one `export` each.

1. The clean video: the clean timeline, with the captions off through `set_clip_enabled`.
2. The described version: the described copy.
3. The description alone: `export` with `mode` `audio`, with the other tracks muted through `set_audio_muted`.

It sees one export card per file.
</Step>
</Steps>

The terminal version:

```bash
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 accessible --token <state_token>
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @captions.json
invideo edit show <branch-url> --mode collection \
  --collection caption_slide --json > slides.json
invideo media audio.content <sequence-url>
invideo gen models <project-url> --query "narration voice"
invideo library search <project-url> \
  --library elevenlabs_text_to_speech_voices --query "clear narrator"
invideo gen model <project-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4
invideo gen run <page-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4 --dry-run \
  -i prompt="A worker lifts the red lever." -i voice_id=<voice-id>
invideo gen run <page-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4 \
  --max-credits <price> --idempotency-key ad-line-01 \
  -i prompt="A worker lifts the red lever." -i voice_id=<voice-id>
invideo status <thread-url> --wait
invideo notebook show <thread-url>
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @track.json
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @lines.json
invideo edit export <branch-url> --format video --quality 1080 --json
invideo status <card-url> --wait
invideo notebook show <card-url> --out ./described
invideo edit export <branch-url> --format video --mode audio \
  --audio-format wav --json
invideo status <audio-card-url> --wait
invideo notebook show <audio-card-url> --out ./ad-track
```

Where each value comes from:

- `<sequence-url>` is the video's `url` from `invideo asset upload`, a `…/notebook/seq-N.M` URL.
- `<thread-url>` is the `url` the paid `gen run` returns. `notebook show` on it lists the line's sequence URL.
- `<price>` is each dry run's `.estimated_credits`.
- `<edit-url>` is the video's timeline URL, from `invideo edit new` or the editor. `<project-url>` is its project.
- `<branch-url>` is the `branch_url` that `invideo branch new` returns.
- `<state_token>` comes from `invideo edit show` on the URL the next command runs on: `<edit-url>` for `branch new`, then `<branch-url>`, read again before each change.
- `<voice-id>` comes from the `elevenlabs_text_to_speech_voices` search. It works only with `elevenlabs_text_to_speech.elevenlabs_tts_v4`.
- `<page-url>` is a notebook page of the project, listed by `invideo notebook show <project-url>`.
- `@captions.json`, `@track.json` and `@lines.json` each read a bare JSON list of ops, like the two lists below.
- `<card-url>` and `<audio-card-url>` are `.sequences[0]` of each export answer. `invideo status` waits but saves nothing. `invideo notebook show` saves each file in its folder, and the agent renames them, for example to `lesson-described`. The `--file-name` export option does not name the saved file.

The file behind `@track.json` adds the description track and turns off the burned-in captions. `c22` is the caption clip's id from `invideo edit show`.

```json
[
  { "op": "add_track", "kind": "audio", "name": "Audio description" },
  { "op": "set_clip_enabled", "clip": "c22", "enabled": false }
]
```

The file behind `@lines.json` places the first line, in the next call. `A4` is the new track's id from `invideo edit show`. The `asset` is the voice line's sequence URL, from `invideo notebook show` on the paid `gen run` thread.

```json
[
  { "op": "insert_media", "asset": "<line-01-sequence-url>",
    "track": "A4", "at_f": 615, "ripple": false }
]
```

A VTT file the agent writes:

```text
WEBVTT

00:00:00.000 --> 00:00:02.500
[upbeat music]

00:00:02.800 --> 00:00:05.500
TRAINER: Welcome to the safety course.
```

## 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.

- Costs credits: the transcript and the voice for each description line.
- Free: the captions, the files, every edit and each export.
- `audio.content`: the dry run shows its price before anything starts.

A short video takes under an hour, plus your review. See [Credits](/account/credits) for prices.

## If something goes wrong

Most problems are files the player does not 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. |
| `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>`. |
| A caption word is wrong | The agent fixes the slide with `set_caption_slide_text`. |
| A description line does not fit its gap | On the described copy, open time with `edit_time_range` mode `insert`, and fill the picture with a still from `media` `video.extractFrameAtTime` on the video's sequence URL. |
| The player shows no captions | It needs the sidecar file. Upload the SRT or VTT in the player's caption settings. |
| Captions show twice | The video has burned-in captions and the file is on. Export with the caption clip off, using `set_clip_enabled`. |
| The audio file has the whole mix | Mute every other track with `set_audio_muted` before `export` with `mode` `audio`. |
| The VTT is missing the end | The read was paged. Continue with `next_offset` and the same `snapshot`. |
| CLI exit 3 | The edit changed since your read. Run `invideo edit show` and rebuild. |
| Avoid: say the video is compliant | Instead: say the files are ready for the accessibility review. |

## Prompts to try

Each prompt is a real variation of this job.

```text
Make SRT captions for every lesson in this project, with speaker
names and sound tags, for our accessibility review.
```

```text
Write an audio description script for this video and show it to me. Do
not voice it until I approve it.
```

```text
Make a described version of this video where the video pauses for each
description, and keep the normal version too.
```

## When to use something else

Compliance review is outside invideo. An accessibility reviewer or your organization's accessibility office checks the video against WCAG, the Web Content Accessibility Guidelines, which the ADA Title II rule and Section 508 both point to. They confirm which WCAG version and level applies to you. The criteria to ask about are Captions (Prerecorded) and Audio Description (Prerecorded). For SCC broadcast captions, convert the SRT with a captioning tool such as Subtitle Edit.

## Related

- [Make an SRT captions file for an LMS](/how-to/training/srt-for-an-lms)
- [Add word-by-word captions](/how-to/creators/word-by-word-captions)
- [Voice a script or narrate a course](/how-to/training/voice-a-script-or-narrate-a-course)
- [Check your video before you say it is done](/how-to/check-your-video)
- [Timeline edit operations](/reference/edit-operations)