---
name: yoh
description: Make and edit AI video stories on Yoh (yoh.app) through the Yoh MCP server, the yoh.app pages in a browser, or both together — start from an idea, a finished script or the user's own video file, cast it, storyboard it, generate the clips and render the video, or change an existing one (clips, dialogue, the timeline, captions, overlays, characters, voices, music), while the user watches the progress on the real page. Use whenever the user wants to create a Yoh story, episode, series, short drama, ad or explainer video, upload or cut a video into Yoh, or change, look at or render one, or mentions a yoh.app link.
---

# Yoh: make and edit AI video stories

You are working on real videos in the user's Yoh account. Every tool call
writes to their account immediately, and generation spends their credits.

## The experience to aim for

The user types one line in chat ("make this into a 30 s ad", "cut my video
into clips") and, within seconds, **sees the real Yoh page open and fill in**
while you keep working. They should never have to ask "is it doing anything?"
or go looking for the result.

- **Open the page the moment a job starts**, not when it ends: the Script
  page while `create_story` writes, the upload page while a video is cut,
  the Generate All list while clips are made. Each answer's `links` says which
  (table in mode 1 below).
- **Open it once per job**, then keep the tab and poll MCP. Never open a new
  tab or window on every poll.
- **Say what changed in one short line** when something changes ("script
  written: 4 clips, 2 characters", "cut: 60%", "clip 3 video done"), not on
  every poll, and finish with what to look at.
- **Ask only what you can't decide** (what it is about, and spend approvals).
  Pick sensible defaults and say what you picked.

How to open a page, best first:
1. A browser tool (Claude in Chrome, Codex's browser, the desktop app's
   browser): navigate your one reused tab.
2. No browser tool, but a shell on the user's own computer (Claude Code or
   Codex running locally): open it in their normal browser, where they are
   already signed in: `open "<url>"` (macOS), `xdg-open "<url>"` (Linux),
   `start "" "<url>"` (Windows). You cannot see that window, so keep checking
   through MCP (`get_story`, `get_clip` with `withFrames`).
3. Neither (a cloud sandbox, CI): print the link on its own line.

## Use this skill across AI clients

This uses the portable `SKILL.md` / Agent Skills format. The workflow can be
used in clients that support that format, including Claude Code, Codex, Cursor,
VS Code agent harnesses, and Gemini CLI. Install the same Yoh skill file in
the skills directory supported by that client; the instructions do not need a
model-specific rewrite. Clients that do not load skills can use the workflow
as project instructions or through a plugin when they support plugins.

The skill, MCP connection, and browser are separate capabilities: installing
the skill teaches the workflow, connecting `https://www.yoh.app/api/mcp` (or the
staging URL) supplies Yoh tools, and browser access lets the assistant inspect
the visual result. Choose the mode below from the tools actually available in
the current session. The combined browser + MCP mode works only when both
capabilities are available together.

## Choosing a mode

There are three ways to use this skill. Pick one at the start and say which.

| You have | Mode | Best for |
|---|---|---|
| `yoh` MCP tools **and** a browser tool (Claude in Chrome in Claude Code or the Claude apps, Codex's browser) | **1. MCP + browser** (recommended) | Interactive work: you do it through MCP and check every step on the real page |
| A browser tool, no `yoh` MCP tools | **2. Browser only** | Users who have not connected MCP; steps that exist only in the UI |
| `yoh` tools, no browser tool | **3. MCP only** | Scripts, CI, agencies making many variants headless; on the user's own computer, still open each job's page with the shell (above) |

If a piece is missing (no `yoh` tools, no browser tool, or a tool answers
"Unauthorized"), do not stop and do not just send a link: walk the user through
"Getting connected" below, one step at a time, then come back here.

### 1. MCP + browser: you drive both

You do the work through MCP and **open each result in the browser yourself**
to check it. Do not hand the user links and wait for them to click.

- **Sign-in.** Open https://www.yoh.app in one tab (staging: staging.yoh.app, to
  match the server). If OAuth asks to sign in, ask the user once to sign in
  there with the same account they want MCP to use, review the requested
  permissions, and approve or deny them. Wait for the client to finish
  connecting. Never type their email
  code, password or payment details. A page that says the story is not found
  usually means the browser is on another account.
- **One tab.** Open one tab for the session and reuse it: navigate it to the
  next link rather than opening a new tab per step.
- **When credits are short.** If a quote says `sufficient: false` or a tool
  returns an "insufficient_credits" refusal, stop before confirming or generating. Open
  its `topUpUrl` in the same browser tab and tell the user to add credits or
  choose a subscription themselves. Wait for them to finish, then call
  `get_credits` and request a fresh quote before continuing. If no browser tool
  is available, give them the link and wait.
- **The loop.** Do the step through MCP → navigate the tab to the `links`
  entry the answer carries → read the page or take a screenshot and check the
  result looks right → continue. Which link:

  | After | Open | Check |
  |---|---|---|
  | `create_story`, script edits (`update_clips`, dialogue) | `links.workspace` (the Script table) | clips, lines, cast in the order you meant |
  | `finish_video_upload` (your own video) | `links.upload` while it cuts, then `links.workspace` | the video, its title and cover; clips appearing as the cut runs |
  | cast art (`generate_all_asset_images`, `regenerate_character_sheet`) | `links.workspace`, the cast | each sheet and background, faces consistent |
  | one clip (`generate_clip_video`, `edit_clip_video`, `regenerate_clip`, `swap_asset`) | `links.clip` (the clip panel) | the new version plays and shows what was asked |
  | `generate_all_clips` / `generate_all_videos` | `links.generate` (the Generate All list) | clips filling in, any failed clip's reason |
  | timeline, captions, overlays | `links.editor` (or `links.editorClip`) | text layers and overlays sit where they should, nothing overlaps |
  | a version (`create_story_version(s)`) | `links.generate`, then `links.translation` (a language), `links.framing` (a format), `links.fix` (clips a step left failed) | steps left; lines read naturally; every clip sits well in the new frame |
  | `render_story` | the finished job's `videoUrl` | the MP4 plays, right length and frame |

- **Waiting.** Poll through MCP (`get_job`, `get_run`) every 15-30 seconds
  while the page shows progress. The browser and MCP read the same data, and
  the episode page refreshes itself: a finished run shows up there, and so
  does an MCP write within about 10 seconds while the tab is visible (it waits
  while someone types in a field there or a save of its own is pending). If it
  still shows the old state after that, reload it.
- **The advanced editor autosaves.** Reload it after an MCP timeline or
  overlay change, and never edit the same story there and through MCP at the
  same time: its autosave can overwrite your MCP changes.
- **What each side is for.** Use the browser for what MCP cannot judge
  (picture and motion quality, faces, caption and text-layer layout, how the
  page reads) and for steps that exist only in the UI. Use MCP for every
  change it has a tool for.
- **Spend once.** A paid step runs either through MCP's quote → confirm or
  through the UI's own price dialog, never both for the same action. Prefer
  MCP; the quote rule below applies either way.

Example: a 16 s 9:16 ad in English and Japanese.
1. `create_series`, `add_series_asset` (the product, itemType "product"),
   `estimate_story`; after the user's yes, `create_story` with
   `contentType: "ad_spot"`, `durationSec: 16`, `aspectRatio: "9:16"`,
   `locale: "en"`, `scope: "story"`. Poll `get_job`.
2. Open `links.workspace`: read the two 8 s clips and their lines back to the
   user, screenshot the cast sheets once the cast art is done.
3. `generate_all_clips` (quote, yes, confirm). Open its `links.generate` and
   poll `get_run` until it finishes.
4. Open each clip's `links.clip`, play the video, check the product and faces.
5. `create_story_version` with `language: "ja"` (quote, yes, confirm); poll `get_story_version`
   until prepare is done, open its `links.translation` and check the lines,
   then `run_story_version_step` step `voice` (quote, yes, confirm).
6. `render_story` on both stories (quote, yes, confirm), poll `get_job`, open
   each `videoUrl` and check the length and frame.

Limits: needs a browser tool and the user signed in on it. With no browser
tool, use mode 3 and hand the user the links at the end.

### 2. Browser only: the app's own screens

No API key and no MCP: you work in yoh.app in the user's browser, through the
same screens they would use. Ask them to sign in once (never type their code
or password). Then:

- **Create**: Create → Story (https://www.yoh.app/create/story): the idea, the
  look, the prompt, inside a series, never a standalone episode.
- **Script**: the episode page's Script table; click a clip to open its panel
  (descriptions, assets, dialogue, references, video settings).
- **Characters**: open a character from the cast to edit it; pick the image
  model in its model picker before generating.
- **Generate All**: the episode's Generate All. It shows the price in a
  consent dialog: read the price and the balance to the user and wait for a yes
  before you click to accept.
- **Edit by hand**: the advanced editor (timeline, captions, overlays,
  per-clip tools).
- **Versions**: Make another version / Create version for another language,
  format or style, then its Generate list and translation review.
- **Render / export**: the advanced editor's export; download the MP4.

Every spend goes through the UI's consent dialog, with the user's yes for that
price. Never type payment details, buy credits or change a plan. Look at each
page before you act, and again after it changes.

Example: the same 16 s 9:16 ad: Create → Story with the product picture,
read the script on the Script table, Generate All (price dialog, user's yes),
watch the clips arrive, Make another version in Japanese, export both.

Limits: slower than MCP, no batch tools, and only what the UI offers.

### 3. MCP only: headless

No browser tool. Everything below this section is the reference for it. If
you run on the user's own computer, still open each job's page in their
browser with the shell (see "The experience to aim for"); in CI or a cloud
sandbox, put the links in your report.

- Spend: every ★ tool quotes first; pass the `confirmToken` only after the
  user said yes to that price (see "Spending credits").
- Wait: poll `get_job` (a `jobId`) or `get_run` (a `runId`) every 15-30
  seconds; check `result.status`, not only `status`. Stop with
  `cancel_generation`.
- Scale: `create_story_versions` for languages × formats × styles,
  `generate_all_clips` / `generate_all_videos` per story, several stories in
  parallel; `list_story_versions` is the variation sheet; `list_renders` the
  finished cuts.
- Deliver: the finished render job's `videoUrl` is the MP4 to download. Put
  the `links` in your report so a person can open any story later.

Example: the 16 s 9:16 ad in en + ja for a client, no browser:
`create_series` and `add_series_asset` (the product), `estimate_story`,
`create_story` (as in mode 1), poll `get_job`; `get_asset_images` until the
cast art is done; `generate_all_clips`, poll `get_run`; `get_clip` with
`withFrames: 3` on each clip; `create_story_versions` with
`[{ language: "ja" }]`, poll `get_story_version` until prepare is done,
`run_story_version_step` step `voice`; `render_story` for both, poll
`get_job`, download each `videoUrl`; report the `links`.

Limits: you cannot see video, so use `get_clip` with `withFrames` before you
call a clip good, and UI-only steps are out of reach.

## Getting connected, step by step

Use this whenever the `yoh` tools are missing, a tool answers "Unauthorized",
the browser is on the wrong account, or the user asks how to set Yoh up.
Guide them **one step at a time**: say what the step is for in one line, do
every part you can yourself (commands, config files), tell them the one thing
only they can do (sign in, approve, click in their app's settings), wait for
them, then check it worked before moving on. Never ask for, read or type their
email code, password, API key or payment details.

The server is `https://www.yoh.app/api/mcp` (staging:
`https://staging.yoh.app/api/mcp`). Use the `www` address exactly:
`yoh.app` redirects, and the sign-in checks the address it was given.

**Step 1. What do we have?** Check in one go and tell the user in one line:
the `yoh` tools (listed and answering), a browser tool, a shell on their own
computer, and this skill (you are reading it). Then do only the steps for
what is missing.

**Step 2. A Yoh account.** Open https://www.yoh.app in their browser (your
browser tool, or `open` / `xdg-open` / `start` from the shell) and ask them to
sign up or sign in themselves. Wait until they say they are in.

**Step 3. Connect Yoh to this app** (sign-in, no API key):

- **Claude Code.** Run it yourself:
  `claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcp`
  Then ask the user to type `/mcp`, pick `yoh`, choose **Authenticate**, sign in
  to Yoh in the browser that opens with the account they want to use, review
  the permissions and approve. (In a shell you can start it with
  `claude mcp login yoh` instead.) `/mcp` then shows `yoh` connected.
- **Codex** (CLI, IDE extension and the ChatGPT desktop app share
  `~/.codex/config.toml`). Run `codex mcp add yoh --url https://www.yoh.app/api/mcp`,
  then `codex mcp login yoh` and ask the user to finish the sign-in in the
  browser. Add `tool_timeout_sec = 300` and `startup_timeout_sec = 20` under
  `[mcp_servers.yoh]` in `~/.codex/config.toml` (the 60 s default is too short
  for some Yoh tools), then ask them to restart Codex. Check with
  `codex mcp list` or `/mcp`.
- **Claude desktop app / claude.ai.** The user does this in their settings:
  Customize → Connectors → **+ Add** → **Add custom connector**, name `Yoh`,
  URL as above, **Add**, then **Connect** and sign in to Yoh. In each chat, turn
  it on from **+** → Connectors. On a Team or Enterprise plan an owner adds the
  connector first (Organization settings → Connectors), then members press
  **Connect**.
- **Cursor.** Write `~/.cursor/mcp.json` (merge, don't overwrite other servers):
  `{"mcpServers":{"yoh":{"url":"https://www.yoh.app/api/mcp"}}}`. The user
  then opens Cursor's MCP settings, signs in to `yoh` when asked and approves.
- **ChatGPT.** The user adds it at chatgpt.com: Plugins → **+** → **Add custom
  MCP server**, name `Yoh`, URL as above, sign in with OAuth, create it, then in
  a chat type `@` and pick Yoh. Some plans need an admin to allow custom
  servers first; if the menu is missing, say so.
- **Any other client:** add a remote (streamable HTTP) MCP server at the URL
  above and sign in when it asks.

The sign-in page shows which Yoh account is signed in in that browser: it must
be the account they want MCP to use. They can see and disconnect apps later in
Settings → API keys → Connected apps.

**No sign-in in this client (or CI)? Use an API key instead.** The user
creates it at https://www.yoh.app/settings/api-keys (`stories:read` +
`stories:write` to edit, plus `stories:generate` to spend) and puts it in an
environment variable such as `YOH_API_KEY` themselves; never paste it into the
chat or a file you share. Claude Code:
`claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcp --header "Authorization: Bearer $YOH_API_KEY"`.
Codex: `bearer_token_env_var = "YOH_API_KEY"` under `[mcp_servers.yoh]`.
Other configs: the header `Authorization: Bearer <key>`.

**Step 4. Check it works.** Call `get_credits` and tell the user which
balance you see. Open https://www.yoh.app in the browser and check the
account there is the same one (same name and balance). If they differ, ask
them to sign in to the right account in the browser, or connect again.

**Step 5. The browser, so you can see the work** (mode 1):

- **Claude Code:** start with `claude --chrome`, or type `/chrome` and enable
  it. It needs the Claude in Chrome extension, signed in to the same Claude
  account; after the first enable, restart Chrome. `/chrome` shows the status.
- **Claude desktop app:** Settings → Connectors → turn on Claude in Chrome
  (per chat).
- **Codex:** in the ChatGPT desktop app, use the built-in browser
  (`@Browser`), or Settings → Computer Use → install the extension and pick
  the browser with `@`. In the Codex CLI with no browser tool, open pages with
  the shell (see "The experience to aim for").

Then open https://www.yoh.app in that browser once, and ask the user to sign
in there if it is signed out.

**Step 6. The skill** (only if this file did not come from an installed skill):
Claude Code: `mkdir -p ~/.claude/skills/yoh && curl -fsSL https://www.yoh.app/skills/yoh/SKILL.md -o ~/.claude/skills/yoh/SKILL.md`
(then `/yoh`). Codex: the same file at `~/.agents/skills/yoh/SKILL.md` (then
`$yoh`). Claude apps: Customize → Skills → upload a ZIP of a `yoh` folder
holding `SKILL.md` (code execution must be on).

**Step 7. First task.** Say it is ready and offer something small ("want a
16-second story to try it?"): `estimate_story` first, and open the page the
moment the job starts.

**If something goes wrong:**
- `yoh` tools missing or "Unauthorized": Claude Code `/mcp` → `yoh` →
  Authenticate (or Reconnect); Codex `codex mcp login yoh`; claude.ai / ChatGPT:
  open the connector and connect again.
- Pages say "not found" or show other stories: the browser is on another
  account, or on staging while MCP is on production (or the reverse).
- A tool times out: raise the client's tool timeout (Codex `tool_timeout_sec`,
  Claude Code the server's `"timeout"`). Long jobs return a `jobId` or `runId`
  to poll; never start them twice.
- Codex refuses to run with a ChatGPT account on its default model: start it
  with `-m gpt-6-luna`.
- Chrome "extension is not connected": the extension and the app must be
  signed in to the same Claude account; `/chrome` → Reconnect.

## When to look in the browser

Tool results say what was asked for; only the page shows what was made. In
mode 1, open the page yourself (one reused tab) and look **at each of these
moments**, without waiting to be asked:

1. **After connecting:** the same account and balance as `get_credits`.
2. **Right after a job starts** (`create_story`, an upload, Generate All, a
   version, a render): open its link so the user watches it fill in.
3. **When the script is written:** the Script table, clips, lines and cast in
   the order you meant, before any paid video.
4. **When cast art is done:** every character sheet, background and item;
   faces consistent, styles matching.
5. **After every clip video:** play it in the clip panel; check the people,
   the product, the motion and what was asked.
6. **During Generate All:** the list as clips land; read any failed clip's
   reason on the page.
7. **After timeline, caption or overlay edits:** the editor (reload it first);
   nothing overlaps, text sits where it should.
8. **Before and after anything paid:** the balance before, the result after.
   If the app shows its own price dialog, the user answers it, not you.
9. **When a tool reports a failure, or the user says something looks wrong:**
   look at the page before you retry or explain.
10. **Before you say "done":** the finished result: the rendered video plays,
    the right length and frame.

Without a browser tool (mode 3), use `get_clip` with `withFrames` and
`get_asset_images` at the same moments, and open the page in the user's own
browser with the shell when you are on their computer.

# Tool reference (modes 1 and 3)

The sections below are the workflows. "Every tool, by task" at the end lists
all the tools.

## Before you touch anything

1. Read the `yoh://guide/timeline` resource if your client can read MCP
   resources. It is the full rule set (durations, trims, caption lanes,
   overlay rows), and it is what the server enforces.
2. `get_credits` once, so you can tell the user what they can afford.
3. On an existing story: `get_story` for structure, `get_timeline` for what
   is where in seconds. Each clip's `number` is the "Clip N" the user sees
   (1-based); `order` is only a raw sort key that may start at 0. Any clip
   argument takes the number too ("3" or "clip 3") in place of the id. Each
   character's `sheet.status` says what the app says: ready, making, failed
   (with `reason`, refunded) or missing.
4. For anything visual, **look before you judge**: `get_clip` with
   `withImage: true` shows the storyboard, and `withFrames: 3` samples real
   frames from the generated video. You cannot watch video. Without frames you
   are only reading the prompt that made it, not the result.
   `get_asset_images` does the same for a character, scene or item.

## Three ways to start

| The user has | Start with | Then |
|---|---|---|
| An idea ("a cat who runs a ramen shop") | `create_story` with the idea as `prompt` | the writer develops it; read the script back before any video |
| A finished script (shots, lines, speakers) | `create_story` with the script pasted as `prompt`, as it is | the app keeps it word for word; pass `storyInputMode: "follow_script"` to insist (it stops with the reason rather than rewrite) |
| Their own video file | `upload_video` → run its command → `finish_video_upload` | the video is cut into editable clips (see "Starting from the user's own video") |

Do not rewrite or "improve" a user's script before sending it unless they ask:
paste it as it is. The prompt takes up to 5,000 characters and one story is at
most 192 seconds; a longer script is several episodes in the same series, one
`create_story` each. An outline, synopsis or beat list is an idea, not a
script: the writer develops it.

## Planning any request: pick the building blocks

Most asks are a combination. Break the request into these blocks, say the plan
in two or three lines, price the paid parts (`estimate_story` or each quote),
then run it while the user watches the page.

| Block | Tools |
|---|---|
| **Start** from an idea or script | `create_story` (`scope: "story"` first) |
| **Start** from their footage | `upload_video` → curl → `finish_video_upload` |
| **Bring in** a song, logo, extra footage or a portrait | `upload_file` (any size, by curl) or `upload_media` (small image) |
| **Cast** a real person or mascot from a picture | `add_character` with `imageUrl` → ★ `regenerate_character_sheet` |
| **Write** new shots between existing ones | `add_clip` with `afterClipId`, then `update_clips` |
| **Make** the media | ★ `generate_all_clips` (or ★ `regenerate_clip` / ★ `generate_clip_video` per clip) |
| **Fix** a shot | the symptom table in "Check the work yourself" |
| **Swap** person, product or place | ★ `swap_asset` |
| **Change** the look, language or frame | ★ `create_story_versions` → ★ `run_story_version_step` |
| **Continue** the story from a shot | ★ `continue_story_from_clip` |
| **Assemble** on the timeline | `reorder_clips`, `trim_clip`, `resize_clip`, `move_clip`, `close_gaps`, `export_clip_to_freeform` + `update_overlay` (volume) |
| **Dress** it | `onScreenText` via `update_clips`, `update_caption_settings`, `add_overlay`, ★ `generate_soundtrack` |
| **Check** it | `list_story_media` + ffmpeg/ffprobe, `get_clip` with `withFrames` |
| **Deliver** | ★ `render_story`, `list_renders`, `list_story_media` |

Say plainly what the app cannot do instead of improvising around it (see
"What is not possible yet" at the end of the recipes).

## Making a new story

Ask only what you cannot sensibly decide: what it is about, and the language
if it is not obvious. Pick sensible defaults for the rest and say what you
picked (9:16 for social, 16:9 otherwise; 60 seconds unless they say).

1. **Series first.** Every story lives in a series; never make a standalone
   one. `list_series` and reuse the one it belongs to, or `create_series`
   with a title and `styleGuide.cinematicStyle` (the look every episode
   inherits). Then pass that `seriesId` to `create_story`. It must be a
   series the user can add episodes to: another user's answers
   `series_not_found` (404), one they only view `series_forbidden` (403),
   nothing charged.
   For an ad, add the product to the series first (`add_series_asset` with
   itemType "product" and its picture) and pass its id to `create_story` as
   `contentTypeConfig.heroItemId` with `contentType: "ad_spot"`. Its name and
   description fill `heroProductName` / `heroTagline` when you leave them out.
   An id that is not an item of that series is refused with
   `hero_item_not_found` (`allowed` lists the series items), and `heroItemId`
   without `seriesId` with `hero_item_needs_series`.
2. **Price the whole plan.** `estimate_story` is free and creates nothing.
   Show the user the total for the steps you intend to run.
3. **Write it.** `create_story` with `scope: "story"` (text only, fast and
   cheap). Length is `durationSec`, 15 to 192 seconds; anything longer is
   several episodes in the same series. About one 8-second clip per 8 seconds,
   at least 2: 16 s is 8+8, 20 s is 8+6+6, 32 s is 4 clips. Short ads (16-30 s)
   land on the length asked, within a couple of seconds. Quote, get a yes, confirm, then poll
   `get_job` and read the result with `get_story`.
4. **Let them read the script.** Summarize the clips and lines and fix what
   they want with `update_clips`, `update_dialogue_line`, `add_clip`. Words
   are cheap to change now and expensive after video exists.
5. **Cast.** The writer creates the characters, and a background for every
   place two or more clips share (props only when the user wants them:
   `add_scene`, `add_item`). Every character needs its sheet and every shared
   background its picture before a clip with them can be drawn or animated:
   the video tools refuse with `assets_required` (HTTP 409 `ASSETS_REQUIRED`
   over v1; nothing charged; it names the missing sheets, backgrounds and
   clips). `create_story` starts that cast art itself unless `castArt: false`
   (its quote includes the cast-art cap); otherwise check `get_asset_images`,
   then `generate_all_asset_images` draws only what is still missing.
   `create_story` also takes `stylePreset` (a `list_style_presets` visual id)
   and `characterStyle` (`match_story` redraws cast in the story style, the
   default; `keep_own` keeps their look; hired characters always keep theirs).
   `list_voices` and `set_character_voice` are free.
6. **Make the video.** `generate_all_clips` draws what each clip needs and
   generates every missing clip video as one run (the app's Generate All:
   poll its `runId` with `get_run`), and is safe to re-run after a partial
   failure. To redo only some clips, pass `clipIds` (and
   `includeExisting: true` to replace videos they already have). Then look at a few clips with `withFrames` and say
   honestly how they came out.
7. **Render** with `render_story` when they want one finished MP4. The
   finished job carries the `videoUrl`; the file is named
   `<title>_<aspect>_<language>.mp4`, and `list_renders` gives each cut's
   width, height, duration, aspect and language. Renders for many variants
   queue (2 at once per account; `get_job` phase "waiting_for_render_slot")
   rather than fail.

`scope: "full"` does all of this in one paid call. Offer it only when the user
says they want it done without checking in.

**Same ad, another language, format or style:** `create_story_version`
(every clip keeps its media, clips are cropped or fitted to the new frame, the
script is translated). A format or style version is free; a language version
answers a quote first (its translation, charged when it is created): show it,
then call again with `confirmToken`. Poll `get_story_version` until `prepare` is
done (it measures speech and translates; if it failed, `run_story_version_step`
with step `prepare` restarts it for free). Then run the paid steps with
`run_story_version_step`, quote first: a style or aspect version needs
`visual` (restyle / AI re-render), a language version needs `voice` (re-voice,
about 5 credits a line), `all` runs both under one quote. A quote's `skipped`
lists clips it can't price or run; they are not charged. To retry only the
clips that failed, pass their `clipIds`. Calling `create_story_versions` again
with the same items within the hour returns the versions it already made
(`reused: true`), never new copies. Review the framing
(`set_clip_framing`, free) and, for a language, the lines
(`get_version_translation`). A lipsync note lists clips whose mouths still
move in the old language: only `regenerate_clip` changes that, at the full
video price.

For many variants (languages × formats × styles) use `create_story_versions`,
the cost-aware order: it makes each new look once, and its `waiting` list names
the languages to make FROM that look once its `visual` step is done (call it
again with that version's id). The restyle is then paid once per look, and each
language only re-voices. Never restyle the master clip by clip
(`edit_clip_video` with `restyleStyleId`) to make a version: that replaces the
original's videos. `list_story_versions` is the variation sheet: every version
made from a story and from its versions (`depth`), with its language, kind,
status and links.

To re-make a story with different characters or places from scratch (a paid
full re-make: every image and video is made again), use the template tools
(`create_template_version`, then `list_template_assets`,
`update_template_version`, `commit_template_version`;
`generate_template_version` is the paid step).

## Starting from the user's own video

The user gives you a video file (a path, a file they dropped into the chat, or
a link). It becomes an episode whose clips are the user's real footage, cut at
its scene changes. Free: nothing is charged until a paid tool is confirmed.

1. **Check the file.** MP4, MOV or WebM, up to 1 GB and 10 minutes. If
   `ffprobe` is there, read the length first; over the limit, offer to trim or
   split it with `ffmpeg` (ask first: it is their file). A link: download it
   to a local file with `curl -L -o`, only if it is theirs to use.
2. **Series.** As always, the episode goes into a series: `list_series`, or
   `create_series` named after what the video is.
3. **`upload_video`** with `seriesId`, `fileName` and `localPath` (the
   absolute path; only used to write the command). Leave the title and
   description out unless the user gave them: the reader writes them from the
   video. The answer's `upload.command` is one curl line.
4. **Run that command** in the shell. It sends the file straight to storage
   (the URL is single-use, good for 2 hours) and must exit 0. A big file takes
   a while: tell the user it is uploading.
5. **`finish_video_upload`** with the `storyId`. It answers "verifying" while
   the video is checked: call again in about 20 seconds. Then it fills an
   empty title and description from the video, sets the episode's style and
   the show's category, picks a cover, and starts the cut. Refused with
   "upload_not_received" (409) if the file never arrived: run the command again.
6. **Show it.** Open `links.upload` now: the user watches the cut's progress
   there. Poll `get_video_upload` every 15-30 seconds until "ready" (or
   "cut_failed": call `finish_video_upload` again to retry the cut).
7. **Then it is a normal story.** Open `links.workspace`, `get_story` to read
   the clips (each has its own footage as the video, plus a description,
   motion and timed captions read from it), and carry on with the user's ask:
   `edit_clip_video` to change the footage, `continue_story_from_clip` to
   write what happens next, captions, overlays, music, `render_story`. The
   clips start with no cast: to swap a person or product, add the asset with a
   picture (`add_character`, `add_item`), put it on the clips with
   `update_clips`, then `swap_asset`.
   **Captions:** if the video already has its own subtitles burned in,
   `get_video_upload` / `get_story` say `burnedSubtitles: true` and the
   episode starts with Yoh's captions off (`captionsOn: false`), so the
   video doesn't show two rows of text. Tell the user. If they want Yoh's
   captions too (or the detection was wrong), call
   `update_caption_settings` with `captionsOn: true`; `captionsOn: false`
   turns them off again. In the editor it's the eye on the Captions row.

Pass `cutIntoClips: false` to keep it as one uploaded video (the upload page
can cut it later).

Example: "here's my trip video, make it a 30 s reel with captions"
`list_series` → `upload_video` → run the command → `finish_video_upload`
(again after "verifying") → open `links.upload` → poll `get_video_upload` to
"ready" → open `links.workspace` → `get_story`, pick the clips that fit 30 s,
`delete_clip` the rest (after a yes) → `update_caption_settings` →
`render_story` (quote, yes, confirm) → open the `videoUrl`.

## Writing clips that come out right

These rules come from reviewing many real runs. Follow them whenever you write
or edit clip text (`add_clip`, `update_clip`, `update_clips`).

- **`description`** is what happens: 1-3 plain sentences (15-60 words). No
  camera words, lighting, colours, timestamps, sound lists or on-screen text.
- **`stillPrompt`** has four short labelled lines, 60-120 words in all:
  - **Shot:** framing, camera height, and where each subject sits in frame
    ("Mara frame-left, looking right").
  - **Subjects:** exact cast names, pose, what each hand holds.
  - **Setting:** the location's name and 3-5 visible details.
  - **Light:** the location's light.
  It shows the moment the action starts, not a poster.
- **`motionDescription`** is timed beats, one per line ("0-3s: ..."), for one
  continuous shot with ONE camera move whose size you spell out ("push from
  medium to close over 4 s"). Put each speaker's exact quoted words in the beat
  where they are said; a wordless clip quotes nothing. After a big length
  change, set it to null so it is written again.
- **Never describe a named character's face, hair, body or outfit** in clip
  text. The character sheet supplies the look, and restated words drift from
  it (an invented cape, a new hairstyle). Mention clothing only when it changes
  (wet, torn, hat off). No style or medium words either ("anime",
  "photoreal", "CGI"): the story style is added to every prompt already.
- **Names in Title Case**, never capitals (a model read a capitalised name
  aloud).
- **Time of day and light belong to the place.** Give every clip a `sceneId`
  and set the scene's `timeOfDay` and `lighting` with `update_scene`. A real
  change of time is a new scene ("Rooftop - Dusk"), not a clip edit. Clip
  text that varies the time makes the sky change between shots in one place.
- **Continuity:** each character keeps one side of frame in a location, an
  action that continues across a cut resumes rather than restarts, and a prop
  stays in the same hand.
- **One speaker per clip.** An exchange is shot / reverse-shot across clips.
  Speech must fit the clip: about 14 characters a second in English and other
  Latin scripts, 6 in Japanese, 4.5 in Chinese and Korean; the longest clip is
  12 s. Longer lines go to a new clip (ask before cutting the user's words).
  A narrator is off-screen voice, never lip-synced. Sounds go in audio fx,
  never in dialogue.
- **On-screen words** (titles, taglines, lower thirds, prices, CTAs) go only
  in `onScreenText` (`{text, role, start, end, position}`), never in a
  prompt: burned-in text comes out misspelled and can't be translated. In
  9:16 keep text out of the top 14 % and bottom 35 %, and off faces and the
  product. Never invent a URL, code, price or offer.
- **Sheets before pictures.** Every character needs its sheet, and every place
  used by 2+ clips its background, before a clip with them can be drawn
  (`assets_required` otherwise). A sheet in the wrong style or with open flags
  counts as missing: check each character's `sheet` in `get_story` and redraw
  with ★ `regenerate_character_sheet`.
- **Real people:** for a real person's face use a realistic `stylePreset` and
  `characterStyle: "keep_own"`, and a model that accepts real people
  (`list_models` says which: Seedance 2.0, MiniMax H3, Kling V3 Omni). Compare
  the sheet with their photo; the sheet model tends to beautify faces.
- **Keep Generate All in order** (no `parallel: true` when consistency
  matters): each clip continues from the previous clip's last frames.

## Check the work yourself

You cannot watch a video, but you can measure it. After any generation,
check before you call it good, and fix what you find with the right tool.

1. `list_story_media` gives every file (sheets, backgrounds, clip images,
   videos and voice reads, every clip's earlier takes, the cover, the
   uploaded source, the soundtrack, timeline media and finished renders) with
   a public URL and a tidy `path`. Earlier takes let you compare versions of a
   shot side by side before choosing one with `select_clip_version`. On a machine with a shell,
   download what you need: `curl -L --create-dirs -o "<path>" "<url>"`. (For
   the user, `links.downloadAll` is the same set as one zip in their browser.)
2. Look with ffmpeg / ffprobe when they are installed (if not, use
   `get_clip` with `withFrames: 3`, which samples frames for you):
   - Length, size, audio: `ffprobe -v error -show_entries format=duration:stream=width,height,codec_type -of json clip.mp4`
   - Frames to look at: `ffmpeg -i clip.mp4 -vf "fps=1,scale=480:-1" frames/%02d.jpg`, then read the images.
   - One contact sheet per clip: `ffmpeg -i clip.mp4 -vf "fps=1,scale=320:-1,tile=4x2" -frames:v 1 sheet.jpg`
   - Unwanted cuts inside a clip: `ffmpeg -i clip.mp4 -vf "select='gt(scene,0.35)',showinfo" -f null - 2>&1 | grep pts_time`
   - Silence or speech in a clip that should be quiet: `ffmpeg -i clip.mp4 -af silencedetect=n=-35dB:d=0.4 -f null - 2>&1 | grep silence_`
   - Black or frozen ends: `blackdetect` and `freezedetect` filters.
3. Compare: each clip's frames against the character's sheet and the
   background picture (same face, outfit, set, sky?), neighbouring clips
   against each other (same side of frame, same light), and on-screen text in
   the editor link (MCP can't judge layout).
4. Fix by symptom, then check again:

| You see | Use |
|---|---|
| Right shot, wrong face | ★ `enhance_clip_image`, then ★ `generate_clip_video` |
| Good frame, one thing wrong ("remove the sign", "make it dusk") | ★ `edit_clip_image` |
| Wrong composition | edit `stillPrompt`, then ★ `regenerate_clip` |
| Wrong in every shot (face, outfit) | fix the portrait or description → ★ `regenerate_character_sheet` → ★ `regenerate_clip` on its clips |
| `get_story` sheet status "flagged" or "off_style" (clip videos wait) | look at the sheet: `keep_character_sheet` keeps it (free), or ★ `regenerate_character_sheet` redraws it |
| Set or sky changes between shots | `update_scene` (`timeOfDay`, `lighting`), redraw the background (★ `regenerate_asset_image`), then regenerate |
| Video mostly right ("night", "push in", "remove the logo") | ★ `edit_clip_video` (keeps motion and length) |
| Wrong person, product or place | ★ `swap_asset` |
| Bad read or wrong voice | `set_character_voice`, then ★ `revoice_clip`; `set_revoice_enabled` switches back for free |
| Lips off the words | `get_audio_sync`, then `fix_audio_sync` |
| Garbled logo or text in the picture | remove it with ★ `edit_clip_image`, put the words in `onScreenText`, the real logo as an image overlay |
| Wrong style for one shot | ★ `edit_clip_video` with `restyleStyleId` (never use this to make a whole version) |

Tell the user what you checked, what you found and what you fixed, with the
clip numbers.

## Recipes

Each recipe is a tested combination of tools. Adapt them; ask only what you
must, and quote every paid step.

### Music video from footage they already made, plus their own face

The user has about 3 minutes of AI video (made elsewhere), a song, and a
portrait, and wants themselves singing in it.

1. `upload_video` the footage → curl → `finish_video_upload` → poll
   `get_video_upload` to "ready". It is cut into clips at its scene changes.
   Over 10 minutes or 1 GB: split it with ffmpeg first and upload the parts
   as episodes, or trim it.
2. `upload_file` the song (keep its URL) and the portrait.
3. `add_character` with the portrait as `imageUrl` (realistic style,
   `keep_own` for their real face), then ★ `regenerate_character_sheet`.
   Compare the sheet with the photo before going on.
4. Find where the singing goes. Download the song and, if `whisper` is
   installed, get timed lyrics (`whisper song.mp3 --word_timestamps True`);
   otherwise ask the user for the lyric lines and their times.
5. For each sung line, `add_clip` after the right footage clip
   (`afterClipId`), with the character on it, a `description` ("Steven sings
   to camera on the rooftop"), a `sceneId`, and the lyric as one dialogue line
   (one singer per clip, fitting the clip length: about 14 characters a
   second; split long lines across clips). Use a delivery like "sung" when
   you re-voice.
6. ★ `generate_all_clips` (only new clips are missing video). The model
   mouths and voices the lyric with the character's voice; it can't use the
   song's own vocal (see "What is not possible yet").
7. Check each new clip (`list_story_media`, frames, face against the sheet)
   and fix with the symptom table.
8. Assemble last, in the editor or with the timeline tools: `reorder_clips`
   for the running order, `trim_clip` / `resize_clip` so each sung clip
   starts on its lyric's time, `close_gaps`. Then `add_overlay` type "sound"
   with the song URL from 0 s over the whole length, and mute the clips' own
   audio so only the song plays: `export_clip_to_freeform` each clip and
   `update_overlay` its `styles.volume` to 0 (do this last: exported clips no
   longer ripple).
9. ★ `render_story`; open the `videoUrl`. With ffmpeg, check the render
   length matches the song.

### One ad, many markets

Product picture → `add_series_asset` (itemType "product") → `create_story`
with `contentType: "ad_spot"`, `heroItemId`, the user's tagline and benefit,
`aspectRatio: "9:16"` → read the script → ★ `generate_all_clips` → put the
CTA and offer in `onScreenText` on the last clip, the real logo as an image
overlay (`upload_file` + `add_overlay`) for the last 2 s → check →
`create_story_versions` for each language × format (each new look once, its
languages from it) → ★ `run_story_version_step` → review each version's
lines (`get_version_translation`) and framing (`set_clip_framing`, free) →
★ `render_story` each → `list_renders`. A pasted approved ad script needs
`storyInputMode: "follow_script"`, or the ad writer rewrites it.

### Same ad, other spokesperson or product

`add_character` (or `add_item`) with the new picture → ★ `swap_asset` with
`scope: "story"` and `redo: "video"` (keeps motion and length) → check
faces → render. For A/B, make a version first so the original stays.

### Restyle or relocalize a finished story

`list_style_presets` (kind "visual") → `create_story_version` with
`style` (and/or `language`, `aspectRatio`) → ★ `run_story_version_step`
step "visual" (style) or "voice" (language) → check. A language version
re-voices but mouths keep the old language: ★ `regenerate_clip` the
on-camera speaking clips if lip sync matters. Translated `onScreenText` is
not in the translation review: read it with `get_story`, fix with
`update_clips`.

### Their footage, continued as a story

Upload and cut (above) → `add_character` for the people in it (crop a face
from a downloaded frame with ffmpeg: `ffmpeg -ss 2 -i clip.mp4 -frames:v 1
-vf "crop=400:400:600:120" face.jpg`, then `upload_file` or `imageBase64`) →
★ `regenerate_character_sheet` → ★ `continue_story_from_clip` from the last
clip ("what happens next") → ★ `generate_all_clips` → check that new clips
match the footage's look (frames side by side).

### Trailer or teaser from a long story

`get_story` → pick the strongest 6-10 clips → copy the story first
(`create_story_version` with no changes is a free copy) → in the copy
`delete_clip` the rest and `reorder_clips` → `trim_clip` each to its best
2-3 s (use the scene-change and silence checks to find them) → title cards
in `onScreenText` → ★ `generate_soundtrack` → ★ `render_story`.

### Storyboard first, for a pitch

`create_story` with `scope: "story"` and `castArt` on → ★
`generate_storyboard` (images only) → `list_story_media` → the user gets the
frames (or `links.downloadAll`: the zip has a storyboard PDF and CSV) →
video only after they approve.

### Consistency pass on an existing story

`list_story_media` → download sheets, backgrounds and clip videos → contact
sheet per clip → compare faces, outfits, sky and side of frame clip by clip →
list problems with clip numbers and the fix for each (symptom table) → quote
the fixes together → apply → check again.

### What is not possible yet (say so, don't improvise)

- Lip sync to the user's own recorded vocal: clips sing with the
  character's generated voice; the song plays over them.
- Setting the background music to their file or changing its volume: use a
  sound overlay instead of the soundtrack.
- Per-track volume of uploaded footage (speech vs background) and the
  "people and things found in your video" review: in the app only (the
  upload page and the clip's audio panel).
- Exact 15.0 s / 30.0 s ads and 4:5 frames.

## Changing who is in a clip

`update_clip` (or `update_clips` for many) sets a clip's `characterIds`,
`itemIds` and `sceneId`. Each list replaces the old one, so send the whole list.
The asset must already be in the story (`add_character`, `add_item`). Also fix
the description and any dialogue that names the old character.

Changing the cast does not change the picture: the existing image and video
still show the old one. `regenerate_clip` redraws the image and then the video
in one run (the video never starts against the old image);
`generate_clip_video` redraws only the video.

## Swapping a character, product or background

`swap_asset` puts one story asset in place of another in one clip or in every
clip that uses it, and keeps the script. A character's spoken lines become the
new character's and are re-voiced in their voice (their saved voice, else the
story's automatic pick) inside the same video step; the quote lists that part
(`voice`: lines, credits). If the new voice cannot be recorded, the visual
switch is kept, the voice part is refunded and the run answers
`voice.status: "not_recorded"`. It is the app's Switch asset, run per clip,
with one quote for the whole scope. `redo` is
`image` (the still), `video` (the existing video, motion and length kept) or
"image_video" (both). When each clip lands, its cast and dialogue speakers name the
new asset. The new asset needs a picture first: `add_character`, `add_item`
or `add_scene` with `imageUrl` (the url `upload_media` answers) or
`imageBase64`, or `add_series_asset`.

- Swap the spokesperson in the whole ad: `{ scope: "story", assetType:
  "character", from: <old>, to: <new>, redo: "video" }`.
- Swap the product in clip 3: `{ scope: "clip", clipId: <clip 3>, assetType:
  "item", from: <old>, to: <new>, redo: "image_video" }`.
- Put the whole ad on a beach: `{ scope: "story", assetType: "scene", from:
  <office>, to: <beach>, redo: "video" }`.

Quote first, get a yes, then confirm. Each clip step is the app's own run
(poll its `runId` with `get_run`); the results show as new clip versions (kind
`switch`). With `image_video`, a clip's image and video steps run side by side
(they write different media). A clip where a switch to that asset is already
running, in the app or from an earlier call, is listed in `skipped` with its
`runId`. If some steps do not start (each one's
`error` says why, nothing charged for them), the answer is `status: "partial"`
with `retry.steps`: fix the cause, then call `swap_asset` again with the same
arguments and the SAME `confirmToken`; only those steps run. Started steps are
never sent twice. For a single clip,
`edit_clip_image` and `edit_clip_video` also take `switchAsset`.

## Looks: series characters and hired characters

A series character or background can have several looks (other outfits,
styles, times of day). All of these are free; tell the user what changed.

- **Series library.** `get_series` and `list_series_assets` show each asset
  with its `looks`. `update_series_asset` edits a series character, background
  or item (name, description, voice, item type); episodes pick the change up
  the next time they draw from it. `use_series_look_as_main` makes one look the
  main picture and sheet (History keeps the old one). `remove_series_look`
  removes one look (ids from `list_series_assets`); episodes that used it keep
  their pictures. Ask the user before removing one. `delete_series_asset`
  removes the whole asset from the library: it can't be undone from here,
  episodes that used it are unlinked (they keep their copy), and a
  character's public page is unpublished, so ask first.
- **Hired characters.** A character hired from another creator can bring more
  looks than its main sheet. `list_hire_looks` lists them (names only until
  the hire is paid, pictures after) and which are in the episode. A character
  hired without a sheet brings no looks, only its portrait (`originalArtwork`);
  its sheet is made in the episode.
  `choose_hire_looks` with `lookIds` sets the extra looks it brings (the ones
  left out are removed; the main sheet always stays). Added looks arrive ready
  at no charge and apply only to a clip they are picked for; clips that used a
  removed look go back to the usual look and are marked out of date. It is the
  same as Add / Remove on the hire page.

## The two planes

- **Story plane**: projected from the clips. Address items by `clipId` and
  dialogue-line id, never by a numeric overlay id. Story video and caption
  items cannot be deleted or split: hide them (`hide_clip_video`) or export
  them (`export_clip_to_freeform`).
- **Freeform plane**: user overlays on rows `freeform-0..N`, addressed by
  numeric id. No two overlays may overlap on the same row.

Moving a clip on the video row **slides** every later clip with it. Resizing
anchors the start: dragging the end changes duration, dragging the start is a
head trim.

## Spending credits

Tools marked ★ never spend on the first call. They return a quote with a
`confirmToken`.

**Show the user the quote and wait for a real answer before passing the token.**
Do not chain quote → confirm in one breath because the tool made it easy. If
they have not said yes to this specific spend, you do not have consent.

**Review the settings with the price.** Every quote returns `settings`: each
setting the run will use (frame, length, resolution, style, language, models,
captions, cast art, voices), with `value`, a plain `label` and `source`
(`chosen` or `default`), plus `settingsLine` and, when something is unusual,
`attention` lines (a price far above usual, every clip video charged up front,
a frame or style that differs from the series' earlier episodes). Show the user
the price, the settings line (say which are defaults) and any attention line,
then ask them to confirm or change one. A change means a fresh quote: call the
tool again with that argument and show the new price and settings. Pass
`confirmToken` once they agree to that exact quote; never ask a second "are you
sure". This is the app's Review & generate step, so the agent and the app show
the same settings and the same price.

Generation is asynchronous. Poll what the tool gave you, every 15–30 seconds:
a `jobId` with `get_job`; a `runId` without a jobId with `get_run` (every clip
tool answers one: clip images and videos, edits, `regenerate_clip`,
`generate_storyboard`, `generate_all_videos`, `generate_all_clips`,
`swap_asset`'s steps, the grid reference, and an asset image / character sheet
that outlived its wait; a batch's `result.results` lists each clip); a
re-voice with `get_revoice` and its jobId; a soundtrack with `get_soundtrack`.
A finished run can still be a refunded failure: check `result.status` and
`result.error`, not only `status`.

A story job that rendered nothing answers `videoUrl: null` and
`firstClipVideoUrl`: that is one clip, never the finished ad. A failed image
job has a `failure` like a video job, and "settlement": "pending" means a
refund is still to come. Quotes say `balance` and `sufficient`;
an "insufficient_credits" refusal gives `required`, `balance` and `topUpUrl`:
tell the user the numbers and the link.

**Stop when asked.** `cancel_generation` with the `jobId` (or a batch
`runId`: `generate_storyboard`, `generate_all_videos`, `generate_all_clips`,
`regenerate_clip`) is the app's Stop: nothing further starts, work already sent to the
provider finishes and is kept, and everything not delivered is refunded. It
answers `cancelled`, `stopping` (a clip is in flight: poll `get_job` until it
reads `cancelled`) or `already_finished`. It is free. A single clip run's
`runId` (`generate_clip_video`, `edit_clip_video`, a clip image) cannot be
stopped: it answers `not_cancellable` (409); the run finishes and settles
itself (a failure is refunded).

**Several stories at once; the app's rule within one.** You may work on
several stories at the same time (one `create_story` per story, in parallel).
Every clip tool starts the app's own run at once, as the clip panel or
Generate All would; there is no job queue. Inside `generate_all_videos`,
`generate_all_clips` and `regenerate_clip` (the app's Generate All) clips run
one at a time in story order, each continuing the previous clip's video for
style continuity. Pass `parallel: true` to `generate_all_videos` or
`generate_all_clips` only when the user wants speed over continuity: a few
clips run at once with no previous-clip video (`parallel` on a single-clip
tool is ignored). The same request again (same `Idempotency-Key`, or the same
arguments a moment later) joins the run already started; it is never charged
twice. While an API job works on the story (a `create_story` full run,
`render_story`, a story run), a clip tool is refused with `API_JOB_RUNNING`
(409, nothing charged, with `activeJobId`): retry when that job finishes.
One live video per clip: while a clip's video is still being made (the user's
Generate in the app, another call of yours, a run still waiting on a slow
provider), a new video or video edit on that clip is refused with
`CLIP_VIDEO_IN_PROGRESS` (409, nothing charged, with `runId` when known): poll
that run, then retry. Generate All (`generate_all_videos`,
`generate_all_clips`) never refuses the whole story for one clip: a clip whose
video is still being made, whose lines are longer than its video, or that has
nothing to work from is left out and listed in `skipped` with its `reason`
(`in_progress` with `runId`, `dialogue_too_long` with `code` and `details`
such as `suggestedDurationSeconds`, `references_required`), not charged; the
rest run. When every named clip is left out the start answers that clip's
refusal (409 `DIALOGUE_LONGER_THAN_VIDEO` with its numbers, or
`CLIP_VIDEO_IN_PROGRESS`).
`render_story` and other story-wide jobs are refused with `STORY_BUSY` (409,
nothing charged) while a clip run is working, whether you or the user in the
app started it (`activeJobScope` `app_clip_video` / `app_clip_image`): poll
your run, or wait a minute and retry.

An edit-only key (`stories:read` + `stories:write`) cannot reach these tools at
all. If a spend tool returns 403, say so plainly rather than trying to route
around it: they need a key with `stories:generate` from https://www.yoh.app/api-keys.

## Things that will bite you

- Editing a clip's text, description or dialogue does **not** change the
  generated media. Say so, and offer to regenerate rather than letting the user
  think the video changed.
- `generate_clip_video` re-generates from the clip's references and discards the
  current video. `edit_clip_video` is video-to-video and keeps subject,
  composition and motion. For "make it night", "push the camera in", "remove the
  sign", use the edit. It can also restyle the footage (`restyleStyleId`) or put one of
  the story's characters, places or items into it by their picture
  (`switchAsset`) — one of the two per call; the prompt is then optional.
  An edit keeps the source length; pass `resolution` to change it (default the
  clip's, then the story's, then 720p, as in the app).
- `generate_clip_video` and `regenerate_clip` take `durationSeconds` and
  `resolution` for that run, like the clip panel's pills (they are saved on the
  clip). Tell the user the quote's `credits`: that is what gets charged.
- A named `videoModel` that can't make the story's aspect is refused
  (`invalid_video_settings`, `allowed.aspectRatio` / `allowed.videoModel`);
  `get_story`, `get_clip` and clip edits return `resolutionMade` (what is made).
- A clip whose lines are longer than the video it would make is not run
  (`DIALOGUE_LONGER_THAN_VIDEO`, HTTP 409 over v1, nothing charged). Its
  details give `clipId`, `speechSeconds`, `videoSeconds` and
  `suggestedDurationSeconds`: run it again with that `durationSeconds`
  (or `set_clip_duration`), or shorten the lines when the suggestion is null.
- Same edit-vs-redraw split on images: `edit_clip_image` rewrites the current
  frame from an instruction, `regenerate_clip_image` draws a new one from the
  clip description, and `enhance_clip_image` keeps the frame but pulls faces
  back into likeness. Pick the one that matches what the user actually asked.
- Model and style ids are not guesses: `list_models` lists the models the app
  offers (durations, resolutions, references, which take realistic people) and
  `list_style_presets` with kind `visual` lists the restyle / version looks.
- Voice casting (`list_voices` → `set_character_voice`) is free, but it only
  affects dialogue rendered afterwards. If they want to HEAR it, the clips need
  re-voicing, which does cost.
- `regenerate_asset_image` redraws a character's likeness.
  `regenerate_character_sheet` keeps the face and redraws only the sheet. They
  are not interchangeable.
- Deleting a character (`delete_character`) removes it from every clip, as
  the app does. Its dialogue lines are kept with no speaker: the result lists
  them in `linesNeedingSpeaker`. Ask the user who says them and set it with
  `update_dialogue_line` `speakerId`; a clip video can't be made until then.
  A role with a licensed face or a waiting hire can't be deleted (409).
- Rule violations come back as `{code, rule, allowed}`. Read `allowed` and
  correct the call; the server never silently clamps a value you asked for.
- `create_story`, `create_series` and `upload_video` (a draft episode) are
  **one-way**: there is no delete for any of them from here, on purpose. Get the details right with the user first, and
  tell them removing the result means opening the app. Never imply you can
  clean it up. Everything smaller (clips, dialogue, FX, overlays, cast, sets,
  props) you can undo yourself. Series-library assets are the exception:
  `delete_series_asset` can't be undone from here.
- `create_template_version` creates a template session.
  `cancel_template_version` stops pending work, but does not delete a committed
  story or generated media; removing those requires the app.
- Reads and writes are capped at 60/min each, confirmed spends at 5/min. Each
  tool call counts once (a quote counts as a read; a `swap_asset` over many
  clips is one spend). If you get `rate_limited`, the
  error says `retryAfter` in seconds. Wait that long and carry on rather than
  giving up or retrying straight away. Batching (`update_clips`, and reading
  `get_story` once instead of `get_clip` per shot) is how you avoid it.
- Editing a whole episode is one call, not twenty. `update_clips` takes an entry
  per clip, each with its own wording and length, and `reorder_clips` takes the
  whole finished running order. A `reorder_clip` per shot is slow and leaves
  the story in orders nobody asked for while it works through them.
  `set_all_clips` puts ONE value on every clip and refuses title/description
  outright. Asked to "rewrite every description", write each clip its own line
  through `update_clips`, never the same line everywhere. Both reject the
  entire batch if any entry is bad, so you never have to work out how much of
  it landed.
- Clip edits are the editor's own save. A visual edit (description, motion,
  `stillPrompt`, cast, items, scene, sound cues) on a clip that already has an
  image or video answers `mediaOutOfDate`: tell the user that media is now out
  of date and offer to regenerate it. `update_clip` sets `stillPrompt` (the
  prompt the clip's image is drawn from; null clears it). `duplicate_clip`
  copies the clip with its dialogue, sound cues, storyline and media.
  Refusals (nothing written; say why, don't retry the same call):
  `CLIP_IN_HIRE` (409, the clip is in an unfinished hire and keeps its hired
  character), `version_locked` (409, an approved collaboration version or a
  hire take still in review), `DELIVERY_EDIT_CONFLICT` (409),
  `CASTING_STALE` (409, made before the casting changed), `story_locked`,
  `NON_SPEECH_DIALOGUE` (400, put sounds in audio fx), `LAST_CLIP` (400),
  `BRANCH_POINT` (409, delete the storyline first), `CLIP_WORKSPACE` (409).
- Collaborators with edit access to a story can use the free edit tools with
  their own key, as in the app; paid tools stay with the story's owner.
- If the story is open in a browser tab while you edit, its autosave can
  overwrite your timeline changes. Ask the user to reload the tab after you
  finish.

## When a shot is holding too much narration

Any dialogue tool, and `get_clip` / `get_story`, will hand back
`speechOverflowSeconds` and a `warning` when a clip's narration needs more
seconds than the clip has. The clip has already grown as far as it can on its
own; past that the line timings get squeezed against its end.

Two ways out, and the choice is the user's, not yours:

**Split the shot**: keeps every word.

```
add_clip            { storyId, title, description, duration }
remove_dialogue_line{ storyId, clipId: crowded, lineId }
add_dialogue_line   { storyId, clipId: theNewOne, text, speakerId }
reorder_clips       { storyId, clipIds: [...whole running order...] }
```

**Or shorten the line**: `update_dialogue_line` with fewer words.

Say which clips overflow and by how much, propose the split, and let them
decide. If the script came from a client, assume the words are not yours to
cut unless they say so.

## Give them the link

Every result about a story, clip, job, version, render or series carries
`links`: `workspace` (the episode page), `script`, `editor` (the advanced
editor), and when a clip is named `clip` (the script page with that clip's
panel open) and `editorClip` (the advanced editor on that clip). Versions add
`generate`, `fix`, `translation` (a language version) and `framing` (a format
version); a Generate All start (`generate_all_clips`, `generate_all_videos`)
adds `generate`, its progress list; a show's episode adds `series`. In mode 1, open the relevant one in your browser tab and check it
(the table in "Choosing a mode" says which). Otherwise, after a visual change
or a render, hand the user the relevant one. They cannot see the video from
the terminal, and telling them what you did without showing them where to look
is half an answer.

## Asking Yoh's own assistant

`ask_assistant` sends a plain request ("make clip 3 funnier") to the assistant
behind the Script page's Ask AI. Each turn costs a few credits: its real AI
cost, up to a small per-turn quote (`billing.quotedCredits`, `billing.chargedCredits`
in the answer). It changes nothing: you get its reply, the
edits it proposes (each with a `changeId`) and a quote for any paid step. Show
the user what it suggests, then `apply_assistant` with the `changeId`s they
keep. A paid change runs only with its quote's `confirmToken`, after the user
agreed to the credits. `undo_assistant` puts an applied run back. For a precise
edit you already know, use the clip tools yourself.

## Working style

Orient, propose, then act. For anything destructive or paid, say what you are
about to do in one line and let the user stop you. When you are done with a
visual change, look at the result and tell them what actually changed, not
what you asked for.

## Every tool, by task

This is the full list (135 tools). Your client's tool list (MCP `tools/list`)
has each tool's arguments, and https://www.yoh.app/docs#mcp documents them. ★ spends
credits and quotes first.

- **Read**: `list_stories`, `get_story`, `get_timeline`, `get_clip`, `get_credits`, `list_models`, `get_asset_images`, `list_renders`, `list_audio_assets`, `get_soundtrack`.
- **Wait and stop**: `get_job`, `get_run`, `cancel_generation`.
- **Series**: `list_series`, `get_series`, `create_series`, `update_series`, `list_series_assets`, `add_series_asset`, `update_series_asset`, `delete_series_asset`, `remove_series_look`, `use_series_look_as_main`.
- **Make a story**: `estimate_story`, ★ `create_story`, ★ `continue_story_from_clip`, `update_story`.
- **Start from a video, bring in files, download everything**: `upload_video`, `finish_video_upload`, `get_video_upload`, `upload_file`, `list_story_media`.
- **Cast, sets, props**: `add_character`, `update_character`, `delete_character`, `add_scene`, `update_scene`, `delete_scene`, `add_item`, `update_item`, `delete_item`, `keep_character_sheet`, ★ `regenerate_asset_image`, ★ `regenerate_character_sheet`, ★ `generate_all_asset_images`.
- **Hired character looks**: `list_hire_looks`, `choose_hire_looks`.
- **Voices**: `list_voices`, `set_character_voice`.
- **Clips**: `add_clip`, `update_clip`, `update_clips`, `set_all_clips`, `delete_clip`, `duplicate_clip`, `reorder_clip`, `reorder_clips`, `set_clip_duration`, `select_clip_version`, `select_clip_image_version`.
- **Dialogue and sound cues**: `add_dialogue_line`, `update_dialogue_line`, `remove_dialogue_line`, `hide_dialogue_line`, `add_audio_fx`, `update_audio_fx`, `remove_audio_fx`.
- **Clip images**: ★ `regenerate_clip_image`, ★ `edit_clip_image`, ★ `enhance_clip_image`, ★ `generate_storyboard`, ★ `generate_clip_grid_reference`.
- **Clip videos**: ★ `generate_clip_video`, ★ `edit_clip_video`, ★ `regenerate_clip`, ★ `generate_all_videos`, ★ `generate_all_clips`, ★ `swap_asset`.
- **Re-voice and sync**: ★ `revoice_clip`, `get_revoice`, `set_revoice_enabled`, `select_revoice_take`, `undo_revoice`, `get_audio_sync`, `fix_audio_sync`.
- **Music**: ★ `generate_soundtrack`, `remove_soundtrack`.
- **Timeline**: `move_clip`, `resize_clip`, `trim_clip`, `reset_clip_timing`, `close_gaps`, `move_caption`, `hide_clip_video`, `pin_dialogue_read`, `export_clip_to_freeform`, `export_caption_to_freeform`.
- **Captions and styles**: `update_caption_settings`, `set_caption_style`, `list_style_presets`, `apply_overlay_preset`.
- **Overlays (freeform plane)**: `list_overlays`, `add_overlay`, `update_overlay`, `move_overlay`, `resize_overlay`, `split_overlay`, `duplicate_overlay`, `delete_overlay`, `delete_freeform_row`, `upload_media`.
- **Versions (language, format, style)**: ★ `create_story_version`, ★ `create_story_versions`, `get_story_version`, `list_story_versions`, ★ `run_story_version_step`, `set_clip_framing`, `get_version_translation`, `edit_version_translation`.
- **Template re-make**: `create_template_version`, `get_template_version`, `list_template_destinations`, `list_template_assets`, `update_template_version`, `analyze_template_version`, `get_template_analysis`, `accept_template_adaptation`, `quote_template_version`, `commit_template_version`, ★ `generate_template_version`, `cancel_template_version`, `fork_template_version`.
- **Render**: ★ `render_story`.
- **Yoh's assistant**: `ask_assistant`, `apply_assistant`, `undo_assistant`.
