---
title: Turn slides into a narrated video, one deck or hundreds
description: Turn a PDF or PowerPoint deck into a narrated training video with one slide per image, a voiceover and optional captions.
---
Turn a slide deck into a narrated video: a PowerPoint to video, a PDF to video, a deck with a voiceover. You get one image per slide, a narration for each slide, and an editable timeline where each slide stays on screen while its line is spoken. The same steps run for one deck or a folder of decks.

Use invideo for this when you want the slides and the voice on a real timeline, so you can fix one slide or one line later without rebuilding the video.

## Quickest path

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

```text
Make a narrated video from deck.pdf in a new invideo project. One image
per slide. Write a short script for each slide from its text, show me the
script, then voice it with a calm narrator. Hold each slide while its line
plays. Add captions. Show me frames before you export.
```

In a chat app, you drag the deck into the project. In a terminal, your agent names the file or the folder of decks and uploads them.

## What you need

These are the inputs your agent asks for.

- The deck as a PDF, or an office file such as PPTX.
- The script, or permission for the agent to write one from the slide text. Speaker notes do not come through `document.pages`, so paste them if you want them used.
- A voice. Your agent finds one with `search_library` and sends you the preview.
- The shape. Slides are usually 16:9.
- Captions on or off.

## Steps

The agent works on a branch, so the main timeline stays clean until you approve.

<Steps>
<Step title="Bring the deck in">
The agent uploads the file with `start_upload` and `add_media`. A PDF or office file goes to the project's files, not to a sequence. It sees the file's URL under `…/assets`, in a dated folder, and uses that URL as it is.
</Step>
<Step title="Make one image per slide">
The agent calls `media` with op `document.pages` on the file's URL that the upload returned. The dry run shows its price before anything starts. It sees an image sequence of each page on a new thread.
</Step>
<Step title="Write and approve the script">
The agent reads each slide image and writes one short line per slide. It shows you the script before it spends anything. It sees your yes, or your changes.
</Step>
<Step title="Voice each slide">
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`, with a voice from `search_library` in `elevenlabs_text_to_speech_voices`. Your agent can search for others with `find_models`.
2. It calls `generate` per slide with the line as `prompt`, the `voice_id` and `dry_run: true`, and tells you the price.
3. For a price of 1 credit or more it waits for your yes. Then it calls `generate` per slide with `max_credits` set to that price and an `idempotency_key`, like `deck-slide-01`. It gets a thread URL and, when the result lands, one sequence URL per slide. It reads each URL with `read` and never calls `generate` again to check.
</Step>
<Step title="Lay out the timeline">
The agent makes a timeline with `new_timeline`, creates a branch with `timeline_history`, and places each voice clip on an audio track and each slide on a video track with `insert_media`. It reads the new clips with `get_timeline`, then sets each slide's length to its line with `trim_clip` in a second call. It sees a new `state_token` after each `edit_timeline` call.
</Step>
<Step title="Add captions if you want them">
The agent prices `audio.transcript` with `dry_run` on each voice clip's sequence URL 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. Then it sends `add_captions`. It sees a caption clip on a video track above the slides.
</Step>
<Step title="Check, then export">
The agent calls `capture` at a few slide changes and `check_timeline`, then `export` with format `video`. It sees an export card with status `completed`.
</Step>
</Steps>

The terminal version of the same job:

```bash
invideo project new --title "Onboarding deck"
invideo asset upload <project-url> ./deck.pdf
invideo media document.pages <deck-url>
invideo library search <project-url> \
  --library elevenlabs_text_to_speech_voices --query "calm narrator"
invideo gen models <project-url> --query "narration voice"
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=@slide-01.txt -i voice_id=<voice-id>
invideo gen run <page-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4 \
  --max-credits <price> --idempotency-key deck-slide-01 \
  -i prompt=@slide-01.txt -i voice_id=<voice-id>
invideo status <thread-url> --wait
invideo notebook show <thread-url>
invideo edit new <project-url> --name "Onboarding v1"
invideo edit show <edit-url> --json
invideo branch new <edit-url> --name deck-v1 --token <state_token>
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 \
  --token <state_token> --operations @layout.json
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 \
  --token <state_token> --operations @lengths.json
invideo edit capture <branch-url> --at 2,15,40 --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:

- `<project-url>` comes from `invideo project new`, and `<edit-url>` from `invideo edit new`.
- `<deck-url>` is the `url` that `invideo asset upload` returns for the deck. It sits in a dated folder, so never type the path.
- `<voice-id>` comes from the `library search` line, a voice of `elevenlabs_text_to_speech.elevenlabs_tts_v4`.
- `<page-url>` is a notebook page of the project, listed by `invideo notebook show <project-url>`.
- `<price>` is the dry run's `.estimated_credits`.
- `<thread-url>` is the `url` the paid `gen run` returns. `notebook show` on it lists the voice line's sequence URL.
- `<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.
- `<card-url>` is `.sequences[0]` of the export answer. `invideo status` waits but saves nothing. `invideo notebook show` saves the video as `./out/export.mp4`, and the agent renames it.

The file behind `@layout.json` is the bare list of ops that `--operations` takes. It places one voice line and its slide at the start of the line. The `asset` values are sequence URLs: the voice take from `generate`, and the slide image from `document.pages`.

```json
[
  { "op": "insert_media", "asset": "<voice-01-sequence-url>",
    "track": "A1", "at_f": 0, "ripple": false },
  { "op": "insert_media", "asset": "<slide-01-sequence-url>",
    "track": "V1", "at_f": 0, "ripple": false }
]
```

The file behind `@lengths.json` sets the slide's length to its line, in a second call. `to_f` is the voice clip's end frame. `c2` and its current end, `from_f`, come from `invideo edit show` after the first call. `from_f` must be where the edge is now, as the agent last read it.

```json
[
  { "op": "trim_clip", "clip": "c2", "edge": "tail",
    "from_f": 150, "to_f": 450, "mode": "leave_gap",
    "linked": false }
]
```

### Many decks

For a folder of decks, the agent runs the same steps once per deck, one timeline per deck. Run it in parts: a few decks per session, then check those videos before the next batch. The agent keeps a list of which decks are done in a brief made with `new_brief`, so a new session picks up where the last stopped.

## 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: every timeline edit, `capture` and each export.
- `document.pages`: the dry run shows its price before anything starts.
- Costs credits: the voice from `elevenlabs_text_to_speech.elevenlabs_tts_v4`, and `audio.transcript` if you want captions.

A single deck takes a few minutes of agent time plus the voice generation. A large batch takes hours. See [Credits](/account/credits) for prices.

## If something goes wrong

Most problems are timing or file type.

| 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. |
| `document.pages` returns CLI exit 2 "Nothing at that URL" | The path was typed, not taken from the upload. Pass the `url` that `invideo asset upload` or `add_media` returned. |
| `invideo status --wait` ends and the folder is empty | `status` does not download. Run `invideo notebook show <card-url> --out <dir>`. |
| `document.pages` returns no thread | The agent reads the project and checks the file type. |
| `document.pages` makes nothing | The file must be a PDF or office file in the project's files. Upload it again with `start_upload`. |
| A slide changes before its line ends | The slide clip is shorter than its voice clip. Trim the slide's tail to the voice clip's end with `trim_clip`. |
| `trim_clip` is refused as stale | `from_f` must match the edge as last read. Call `get_timeline` and send the new value. |
| CLI exit 3 | The edit changed since your read. Run `invideo edit show` and rebuild the ops. |
| CLI exit 5 | Credits ran out. Stop the batch and tell the person. |
| `timeout` on `generate` | Do not call it again. `read` the thread or call `status`. |
| Avoid: resend `generate` to check on it | Instead: `read` the URL after `retry_after_s`. |
| Avoid: edit main | Instead: create a branch with `timeline_history`. |

## Prompts to try

Each prompt is a real variation of this job.

```text
Turn every PDF in ./decks into a narrated video. Same voice for all. Do
a few decks, show me the frames, then do the next few.
```

```text
Make a video from training.pptx. Use my speaker notes below as the script,
one note per slide. No captions. 16:9.
```

```text
Narrate this deck in a warm female voice, and add burned-in captions at
the bottom in white with a dark background.
```

## When to use something else

invideo does not read speaker notes from a PPTX. Paste them, or let your agent read the file on your computer first. For animated slide builds, export the deck from PowerPoint or Keynote as a video and bring that in. For a presenter on camera, record yourself or use an avatar tool such as HeyGen or Synthesia.

## Related

- [Update one line and re-render, keep the rest](/how-to/training/update-one-line)
- [Voice a script or narrate a course](/how-to/training/voice-a-script-or-narrate-a-course)
- [Turn a syllabus into a course of lesson videos](/how-to/training/online-course)
- [Make an SRT captions file for an LMS](/how-to/training/srt-for-an-lms)
- [Check your video before you say it is done](/how-to/check-your-video)
- [Know the cost before you spend](/how-to/cost-before-you-spend)