---
title: Make an AI short film with a character that stays the same
description: A short film made with AI video models where your character keeps the same face, hair and clothes in every shot, cut on a timeline you can edit.
---
You get an AI short film, also called an AI film or a generated short, where the main character looks the same in every shot. Same face, same hair, same jacket. The shots come from reference-to-video models, and your agent cuts them on an invideo timeline with sound, titles and credits.

Use invideo for this when you want the character, the shot list and the cut in one project: the agent keeps the character in the project's context, generates each shot from the same references, and the film stays editable after the cut.

## Quickest path

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

```text
Make a short film about Maya, a night-shift nurse who finds a lost dog.
Use the photos in ./maya as her reference. Write a shot list as a brief,
generate each shot with Maya from the same references, and check that
she looks the same in every shot. Put each act on its own branch.
Tell me the price before you generate anything.
```

In a chat app, you drag the reference photos into the project, then name them in the chat. In a terminal, you name the folder and the agent uploads it.

## What you need

The references matter most. A character drifts when the model has to guess.

- **Reference images of the character.** A front view, a side view and a full-body view, in even light. A character sheet on one image also works.
- **The story.** A few lines per act, or a full script.
- **The look.** Era, place, light and lens words, like "rainy city at night, 35mm, warm street light".
- **The shape.** 16:9 or 21:9 for a film, 9:16 for a vertical short.
- **The credits.** Your name, everyone who worked on it, and anything else you want on the end card.

## Steps

The agent works on a branch per act and tells you what changed at each step.

<Steps>
<Step title="Bring in the references">
The agent makes a project with `new_project` and adds each photo with `add_media`. For local files, it calls `start_upload`, sends the bytes, then calls `add_media` with the `upload_id`. Expected result: a sequence URL for each photo.
</Step>
<Step title="Write the character entry and the shot list">
The agent writes the character to `context/world/maya.md` with `write`: her face, hair, clothes and voice, with a link to each reference photo by its sequence URL. Then it makes a brief with `new_brief` and adds a `shot-list` note with `edit`, where each shot has a number, the act, the framing, the action and the prompt. Expected result: the entry in `read` on the project, and a brief URL. You change the shot list before any generation starts.
</Step>
<Step title="Generate each shot from the references">
The agent follows the generation pattern for every shot, with the same reference photos each time.

1. It runs `find_models` with the job in words, picks one, and reads its inputs with `get_model`. A good pick is `seedance_2_5.r2v`. Your agent can search for others with `find_models`, such as `veo_31.ingredients`, `wan_30_video.wan_30_reference` and `minimax_h3_max.reference`.
2. It calls `generate` with `dry_run: true` and the real inputs, 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`, then reads the sequence URL with `read` when the result lands. It never calls `generate` again to check.

Expected result: a thread URL, then a sequence URL per shot with status `completed`.
</Step>
<Step title="Control the start and end of a shot">
For a shot that must start and end on exact frames, like a door opening into the next scene, the agent makes the frames first, then the shot.

1. It runs `find_models` for an image from references, and `find_models` for video from a first and last frame. Good picks are `nanobanana_pro` for the frames and `seedance_2_5.frames` for the shot. Your agent can search for others with `find_models`. It reads each pick's inputs with `get_model`.
2. It calls `generate` for the two frames from the same references, then for the shot, each with `dry_run: true` first, and tells you the price.
3. For a price of 1 credit or more it waits for your yes. Then it runs each paid call with `max_credits` set to that price and an `idempotency_key`, and reads each sequence URL with `read`.

Expected result: a shot that starts and ends where you asked.
</Step>
<Step title="Check that the character stays the same">
The agent runs `media` with `video.frames` on each shot, then compares the face, hair and clothes by eye with the reference photos. Expected result: a list of shots that pass and shots that drift. You approve the takes you like, and the agent marks them with `mark_sequence`.
</Step>
<Step title="Cut each act on its own branch">
The agent makes a timeline with `new_timeline` and creates a branch per act with `timeline_history` action `create_branch`. It places the approved takes with `insert_media`, adds music and sound, and ends with a credits card made with `add_text` that names the models used. Expected result: a branch URL per act, and a new `state_token` after each edit.
</Step>
<Step title="Check, then export">
The agent runs `capture` across each act and `check_timeline`, and looks at the character in every captured frame. Then it runs `export` with `format` video. Expected result: an export card with status `completed`.
</Step>
</Steps>

The same job from a terminal:

```bash
invideo project new --title "Night Shift"
invideo asset upload <project-url> ./maya/*.jpg
invideo context write <project-url>/context/world/maya.md --markdown @maya.md
invideo context new-brief <project-url> --name "Night Shift" \
  --description "A short film in acts"
invideo gen models <project-url> --query "video from reference images"
invideo gen model <project-url> --model seedance_2_5.r2v
invideo gen run <page-url> --model seedance_2_5.r2v --dry-run \
  --input shot-01.json
invideo gen run <page-url> --model seedance_2_5.r2v --max-credits <price> \
  --idempotency-key shot-01 --input shot-01.json
invideo status <thread-url> --wait
invideo notebook show <thread-url>
invideo media video.frames <sequence-url> --at 0,2,4 --out ./check
invideo edit new <project-url> --name "Night Shift cut" --fps 24
invideo branch new <edit-url> --name act-1 --token <state_token>
invideo edit show <branch-url>
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @act-1.json
invideo edit capture <branch-url> --at 0,12,24,36 --out ./frames
invideo edit export <branch-url> --format video --quality 1080
```

Where the values come from: `<project-url>` from `invideo project new`, and `<page-url>` from `invideo notebook show <project-url>`. `<price>` is the price the dry run returns, its `estimated_credits`, and `<thread-url>` is the `url` the paid run returns. `<sequence-url>` is the shot's sequence URL, which `invideo notebook show <thread-url>` lists when the job is `completed`. `<edit-url>` comes 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. The file in `--operations @act-1.json` is a bare list of ops.

The file in `--input shot-01.json` is the bare inputs object, keyed by the names `get_model` lists, without the model id. The dry run and the paid run send the same file. `<maya-front-url>`, `<maya-side-url>` and `<maya-full-url>` are the sequence URLs `invideo asset upload` returns for the photos.

```json
{
  "prompt": "Maya, in blue scrubs, kneels by a wet dog. Rain, 35mm.",
  "reference_images": [
    "<maya-front-url>", "<maya-side-url>", "<maya-full-url>"
  ],
  "aspect_ratio": "21:9",
  "duration": "6"
}
```

The key MCP call prices one shot from the character's references. This fence is the full MCP `generate` call. On the MCP path the inputs sit under `inputs`. The agent sends the call again without `dry_run`, with `max_credits` set to that price.

```json
{
  "url": "<page-url>",
  "model": "seedance_2_5.r2v",
  "inputs": {
    "prompt": "Maya, in blue scrubs, kneels by a wet dog. Rain, 35mm.",
    "reference_images": [
      "<maya-front-url>", "<maya-side-url>", "<maya-full-url>"
    ],
    "aspect_ratio": "21:9",
    "duration": "6"
  },
  "dry_run": true,
  "idempotency_key": "night-shift-shot-07"
}
```

The credits card goes in one `edit_timeline` call. This fence is the bare list of ops that `--operations @act-1.json` takes. `start_f` and `end_f` are frames at the timeline's frame rate, after the last shot.

```json
[
  { "op": "add_text", "track": "V3", "start_f": 2880, "end_f": 3000,
    "text": "Directed by Ana Ruiz\nShots: Seedance 2.5, Kling 4.0" }
]
```

## 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:** every shot from `seedance_2_5.r2v`, `seedance_2_5.frames` or another video model, the frame images from `nanobanana_pro`, and any music or sound you generate. Shots made again because the character drifted cost again.
- **Free:** the character entry, the brief, the cut, the credits card, `capture` and every export. `video.frames` shows its price in the dry run before anything starts.

Each shot takes a few minutes to generate. A short film with retakes takes an afternoon or more. See [Credits and what they cost](/account/credits) for prices.

## If something goes wrong

Most problems come from thin references or a prompt that changes the character's look.

| 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 face changes from shot to shot | Pass the same reference photos in every shot, and describe the character the same way in every prompt. Generate the drifting shot again. |
| The clothes change | Name the clothes in the character entry and in the prompt. Add a full-body reference photo. |
| The character drifts in a frames shot | `seedance_2_5.frames` takes no reference images. Make the first and last frames from the references with `nanobanana_pro` 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. |
| Status `needs_confirmation` | Only you can confirm it, in invideo. The agent tells you where. |
| CLI exit 5 | A limit on credits or quota. Check the balance before the next shot. |
| A call times out | Retry with the same `idempotency_key`. It finds the first generation. |
| Avoid: calling `generate` again to check a shot | Instead: `read` the thread URL or call `status`. |
| Avoid: editing main | Instead: one branch per act from `timeline_history`. You pick what goes to main. |

## Prompts to try

Each prompt is a real variation of the job.

```text
Make a character sheet for Kai from these photos with nanobanana_pro,
save Kai to the project's world, then generate a few shots of Kai walking
through a market. Show me frames of each shot next to the sheet.
```

```text
Shot 4 drifts: her hair is shorter. Generate it again with the same
references and a prompt that names her long braid. Keep the old take.
```

```text
Cut act 2 on its own branch. End it on a short hold of the dog, then a
credits card with everyone who worked on it and the models you used.
```

## When to use something else

invideo does not train a custom model on your character. For a character model trained on many photos, use a training tool such as a LoRA trainer, then bring the shots into invideo for the cut.

Platforms such as YouTube, TikTok and Meta ask you to label realistic AI content when you upload. The credits card names the models, but the label itself is a setting in each platform's upload screen.

## Related

- [Generate a missing shot or extend a clip inside your edit](/how-to/film/generate-a-missing-shot)
- [Make storyboards and previs before the shoot](/how-to/film/storyboards-and-previs)
- [Use Claude Code as your film editor on an invideo timeline](/how-to/film/claude-code-as-film-editor)
- [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)
- [Get started with AI filmmaking](/guides/get-started-with-ai-filmmaking)