Wiggly / Format Lab /

Cartoon Explainer

experimental
v1.2.0-experiment

Cartoon Explainer

Familiar characters explain a real idea through their own story world.

Matches saved runs

Give an agent the working Format—not screenshots of it

The kit contains the renderer, runner, rules, layouts, and assets. Unzip it and tell Claude or Codex to read SKILL.md. It must use the packaged renderer, not rebuild one.

Download runnable Format Kit

1. Agent loop

Claude or Codex can download this package, use its existing renderer, check what is missing, plan a lesson, render it, inspect it, and improve it up to two times.

What it needs

  • FISH_STUDIO_APIKEY in v3/.env.local
  • Node, FFmpeg, FFprobe, and Remotion installed locally
  • Serper only when a new story world needs assets

What it will not use

No OpenRouter planner, Replicate, GPU, image generation, or video generation. Secret values never enter the Format files or reports.

Agent skill

The complete loop an agent follows without the user explaining the Format.

SKILL.md
---
name: otaku-explainer-format
description: Use the Cartoon Explainer Format to make a short story-world lesson video without asking the user to explain how the Format works.
---

# Cartoon Explainer Agent Loop

You are operating a complete runnable Wiggly Format Kit. The user supplies a topic and chooses a packaged story world. This folder supplies the cast, voices, backgrounds, layouts, renderer, runner, audio rules, and quality checks.

## Use the packaged runtime

Do not rebuild, replace, translate, or imitate the renderer. Run the renderer and runner already included in this kit. A different framework or a new scene-timing loop is a failed use of the Format, even when its final video looks similar.

If you received the public Wiggly Repo URL, download and unzip the **Runnable Format Kit** first. Open its `v3` folder, run `npm install`, copy `.env.example` to `.env.local`, and add only the missing key values. Then follow the commands below.

The renderer deliberately adds a soft glow around the active speaker. This hides rough transparent-image edges and makes the speaker obvious. Preserve that behavior.

## Commands

Run commands from the downloaded kit's `v3` folder:

```bash
npm run smoke
npm run prototype:otaku -- check
npm run prototype:otaku -- init --run=<run-id> --topic="<topic>" --world=naruto
npm run prototype:otaku -- validate --run=<run-id>
npm run prototype:otaku -- render --run=<run-id> --approve-loop
npm run prototype:otaku -- inspect --run=<run-id>
npm run prototype:otaku -- finalize --run=<run-id>
```

Run `npm run smoke` first. It makes a tiny local test video and verifies the packaged renderer, assets, FFmpeg, Remotion, and audio mix without calling Fish or any paid provider.

`--approve-loop` records permission for one initial render and up to two focused improvement renders without storing secrets. If the configured voice model is free and the user already asked the agent to make the video, that request is enough; do not interrupt them for another approval. Ask first when a provider may charge money.

## Required loop

1. Read this file, `requirements.json`, `worlds/<world>.json`, `layouts.json`, `scene-contract.json`, `prompts/script-system.md`, and `quality.json`.
2. Run `npm run smoke`, then run `check`. If either fails, stop and report the exact missing local requirement. If `check` reports a missing key, ask the user to add the named key to `.env.local`. Never ask them to paste a secret into chat and never print its value.
3. Run `init`, then write 12–18 short scene records in the new run's `scene-plan.json`.
4. Use only packaged role names, backgrounds, layout IDs, and assets. Do not invent character coordinates.
5. Run `validate` before any media call. Fix every validation error first.
6. Show the user the scene plan, scene count, cast, and estimated duration. If any media call may cost money, ask once for approval covering no more than three total render attempts.
7. Run `render --approve-loop` for the first attempt. Later attempts use `render` without that flag.
8. Run `inspect`. Look at the full video and contact sheet. Record only concrete problems in `quality-report.json`.
9. Fix only the problems you found. Do not rewrite good scenes. Render and inspect again when needed.
10. Run `finalize` only when every automatic and human review check passes.

## Add a story world

When the user asks for a story world that is not packaged:

1. Run `check --needs-new-assets`. Ask for missing key names without exposing their values.
2. Research the show's characters, relationships, locations, and lore. Record the sources you used.
3. Map one character to each lesson role: learner, guide, and challenger. Keep the renderer, layouts, and scene contract unchanged.
4. Search Fish Audio's public models for each character, audition available samples, and record the chosen model IDs and why they fit.
5. Use Serper to find full-body character cutouts and wide backgrounds. Follow `prompts/image-search.md`; prefer existing transparent assets and use local cleanup when needed.
6. Source one quiet instrumental track using the music procedure below.
7. Add the inspected files and provenance to `assets.json`, then create `worlds/<world>.json` with the cast, voices, backgrounds, useful lore, claims to avoid, and selected music.
8. Validate the new world before rendering. If it requires special renderer, runner, layout, or schema code, stop: the Format is not portable yet.

Do not generate images unless the user separately approves it. Do not add an automated world-building workflow; the agent performs and documents this work.

### Source story-world music

1. Search Serper for `"<story world> background music instrumental"` and inspect the top three credible results.
2. Prefer an exact official or studio instrumental over a cover, remix, or re-orchestration unless the Format calls for one.
3. Download audio with `uvx yt-dlp --no-playlist -x --audio-format mp3 --audio-quality 0 <url>`.
4. Use FFprobe and FFmpeg to check duration, loudness, clipping, and trailing silence. Remove dead air, normalize with `loudnorm=I=-14:TP=-1.5:LRA=11`, and use a one-second crossfade when looping the track to the video length.
5. Verify that the loop has no silent seam and dialogue remains clear. If you cannot hear the candidates, do not claim that you auditioned them: use metadata and technical checks, then show the user playable options when taste affects the decision.
6. Record `localPath`, `sourceUrl`, `volume`, and `selectionReason` in the world pack. Keep the Format's default music when no candidate passes.

## Good result

A passing result:

- teaches the topic accurately in language a nontechnical viewer can repeat;
- sounds like a natural conversation, not a lecture pasted into character mouths;
- uses a story-world analogy that stays consistent;
- keeps every visible character grounded;
- fits every line inside the speech bubble;
- assigns the right voice to the active speaker;
- uses a clean music loop that stays below the dialogue; and
- leaves a clear final takeaway.

## Failure rules

- Never render an invalid plan.
- Never rebuild the packaged renderer or write a substitute timing/rendering pipeline.
- Scene duration comes from the actual voice file. Never add arbitrary silence after a voice clip.
- Use the full-length music bed prepared by the runner. Never loop a short track with a hard seam.
- Never exceed three attempts for one run.
- Never silently change providers or generate replacement images or video.
- If the packaged world lacks an asset, stop and explain what is missing. Use Serper only after the user approves sourcing a new asset.
- If attempt three still fails, stop and explain the blocker. Do not mark the run finished.
- Do not expose, copy, log, or store API key values.

Requirements

The key names and local tools needed, without any secret values.

requirements.json
{
  "environment": [
    {
      "name": "FISH_STUDIO_APIKEY",
      "requiredFor": ["render"],
      "description": "Generates one free S2.1 Pro voice clip for each scene."
    },
    {
      "name": "SERPER_API_KEY",
      "requiredFor": ["new-story-world-assets"],
      "description": "Finds character, background, and instrumental music candidates only when a story world is not already packaged."
    }
  ],
  "localTools": [
    { "name": "node", "requiredFor": ["check", "init", "validate", "render", "inspect", "finalize"] },
    { "name": "ffmpeg", "requiredFor": ["render", "inspect", "new-story-world-assets"] },
    { "name": "ffprobe", "requiredFor": ["render", "inspect", "new-story-world-assets"] },
    { "name": "uvx", "requiredFor": ["new-story-world-assets"] },
    { "name": "remotion", "requiredFor": ["render"] }
  ],
  "notUsed": [
    "OpenRouter",
    "Replicate",
    "GPU",
    "image generation",
    "video generation"
  ],
  "secretPolicy": "Read keys from the downloaded kit's v3/.env.local. Never write secret values to plans, logs, reports, or run records."
}

Danny Phantom world pack

Lesson roles mapped to characters, voices, backgrounds, lore, and music.

worlds/danny-phantom.json
{
  "id": "danny-phantom",
  "label": "Danny Phantom",
  "music": {
    "localPath": "assets/audio/danny-phantom-background.mp3",
    "volume": 0.16,
    "sourceUrl": "https://www.youtube.com/watch?v=s8N1O07Lxpw",
    "selectionReason": "The top exact-match search result is the official instrumental rather than a cover or remix. Its trailing silence was removed before making a crossfaded loop."
  },
  "roles": {
    "learner": {
      "character": "danny-phantom",
      "voice": "14f06ac475944bb7a0ef5cc958f07462",
      "behavior": "Curious, brave, and restates the lesson in plain words."
    },
    "guide": {
      "character": "tucker-foley",
      "voice": "e3e1212180ee4f87ab0730db54d1a8e2",
      "behavior": "Tech-obsessed friend who explains the mechanism clearly."
    },
    "challenger": {
      "character": "vlad-plasmius",
      "voice": "23231b8547f04d79bebc51bb2a23b5ab",
      "behavior": "Confident antagonist who adds a warning, edge case, or correction."
    }
  },
  "backgrounds": ["amity-park", "fenton-lab", "ghost-zone"],
  "lore": [
    "the Fenton Portal connects Earth to the separate Ghost Zone",
    "portal controls send specific commands across that boundary",
    "Tucker is Danny's tech-obsessed best friend",
    "Vlad is an experienced half-ghost and Danny's antagonist",
    "Amity Park is Danny's home and the place he protects",
    "ghost-hunting equipment follows specific controls and limits"
  ],
  "avoid": [
    "claiming Tucker built the Fenton Portal",
    "treating Vlad as a trusted teacher",
    "invented ghost powers or portal rules",
    "random technical words that do not map to the story-world action"
  ],
  "researchSources": [
    "https://en.wikipedia.org/wiki/Danny_Phantom",
    "https://dannyphantom.fandom.com/wiki/Danny_Phantom_(character)",
    "https://dannyphantom.fandom.com/wiki/Tucker_Foley",
    "https://dannyphantom.fandom.com/wiki/Vlad_Plasmius",
    "https://dannyphantom.fandom.com/wiki/Fenton_Portal",
    "https://dannyphantom.fandom.com/wiki/Ghost_Zone"
  ],
  "voiceResearch": {
    "danny-phantom": {
      "query": "Danny Phantom",
      "selectionReason": "Highest-use English result with a youthful, energetic sample and 1,700 prior tasks."
    },
    "tucker-foley": {
      "query": "Tucker Foley",
      "selectionReason": "English public model with an available energetic, friendly sample."
    },
    "vlad-plasmius": {
      "query": "Vlad Plasmius",
      "selectionReason": "Only exact English public result; its sample has the confident antagonist delivery the role needs."
    }
  }
}

Naruto world pack

Lesson roles mapped to characters, voices, backgrounds, lore, and music.

worlds/naruto.json
{
  "id": "naruto",
  "label": "Naruto",
  "roles": {
    "learner": { "character": "naruto", "voice": "c658e20af0364df6b73bdcef998b6a8b", "behavior": "Curious, impatient, and repeats the lesson in plain words." },
    "guide": { "character": "kakashi", "voice": "6fb91544d42b4d57a5b408c398459237", "behavior": "Calm teacher who makes the main idea accurate." },
    "challenger": { "character": "orochimaru", "voice": "8ecce156f83e4fa0bb75f99b30894e63", "behavior": "Adds tension, a sharper example, or a correction." }
  },
  "backgrounds": ["konoha", "naruto-forest", "training-ground"],
  "lore": ["ninja villages", "missions", "scrolls", "jutsu", "summoning contracts", "messenger birds"],
  "avoid": ["invented canon claims", "modern apps appearing inside the story world"]
}

SpongeBob SquarePants world pack

Lesson roles mapped to characters, voices, backgrounds, lore, and music.

worlds/spongebob.json
{
  "id": "spongebob",
  "label": "SpongeBob SquarePants",
  "music": {
    "localPath": "assets/audio/spongebob-background.mp3",
    "volume": 0.14,
    "sourceUrl": "https://www.youtube.com/watch?v=3xWZB1SoWE8",
    "selectionReason": "Hawaiian Cocktail is an exact SpongeBob production instrumental and is calm enough to sit below dialogue. Its leading and trailing silence were removed before loudness normalization."
  },
  "roles": {
    "learner": {
      "character": "spongebob",
      "voice": "9845e056f37b470d9a1005e41c864e25",
      "behavior": "Cheerful and curious; repeats the mechanism in simple words to prove he understands it."
    },
    "guide": {
      "character": "sandy-cheeks",
      "voice": "783d32b03d0c4ff28dd66455364d8665",
      "behavior": "A practical scientist who explains each electric vehicle part accurately and plainly."
    },
    "challenger": {
      "character": "plankton",
      "voice": "fc33048c58604823aa19ace52e4842c9",
      "behavior": "An overconfident inventor who suggests one tempting shortcut or misconception for Sandy to correct."
    }
  },
  "backgrounds": ["bikini-bottom", "sandy-treedome", "boating-school"],
  "lore": [
    "Bikini Bottom is SpongeBob's underwater home",
    "Sandy is an inventor and scientist who lives in a treedome",
    "Mrs. Puff's Boating School is where SpongeBob practices driving boatmobiles",
    "Plankton is an overconfident inventor and SpongeBob's recurring rival",
    "Sandy can test an electric boatmobile as a lesson device without claiming it is show canon"
  ],
  "avoid": [
    "claiming the lesson's electric boatmobile is a canon invention from the show",
    "pretending an electric vehicle battery connects directly to the wheels",
    "saying regenerative braking recovers all energy or replaces the regular brakes",
    "turning the explanation into unrelated SpongeBob catchphrases",
    "invented character relationships or locations"
  ],
  "researchSources": [
    "https://afdc.energy.gov/vehicles/how-do-all-electric-cars-work.html",
    "https://www.energy.gov/cmei/femp/articles/electric-vehicle-technology-overview-federal-fleet-training",
    "https://spongebob.fandom.com/wiki/Bikini_Bottom",
    "https://spongebob.fandom.com/wiki/Sandy_Cheeks",
    "https://spongebob.fandom.com/wiki/Mrs._Puff%27s_Boating_School",
    "https://spongebob.fandom.com/wiki/Sheldon_J._Plankton"
  ],
  "voiceResearch": {
    "spongebob": {
      "query": "Spongebob Squarepants",
      "selectionReason": "Highest-use exact English public result with an available sample and more than 31,000 prior tasks."
    },
    "sandy-cheeks": {
      "query": "Sandy Cheeks",
      "selectionReason": "Highest-use exact English public result with an available sample and more than 4,000 prior tasks."
    },
    "plankton": {
      "query": "Plankton",
      "selectionReason": "Highest-use exact English public result that also provides an audition sample."
    }
  }
}

Yu-Gi-Oh world pack

Lesson roles mapped to characters, voices, backgrounds, lore, and music.

worlds/yugioh.json
{
  "id": "yugioh",
  "label": "Yu-Gi-Oh",
  "roles": {
    "learner": { "character": "yugi", "voice": "a27f617599df41f9aa6e95bb57ac69ba", "behavior": "Asks the clear question and states the final lesson." },
    "guide": { "character": "kaiba", "voice": "710a3478dcc4477d8aae292c2de85964", "behavior": "Confident expert who explains the rule precisely." },
    "challenger": { "character": "joey", "voice": "2ef97f8ba13a4f71a7e291a3998af362", "behavior": "Adds a practical example, joke, or objection." }
  },
  "backgrounds": ["duel-arena", "duel-club", "domino-city"],
  "lore": ["decks", "cards", "duels", "turns", "combos", "trap cards"],
  "avoid": ["invented card rules", "switching the learner and guide roles without a reason"]
}

Approved layouts

Reusable two- and three-character positions. Scene writers do not invent coordinates.

layouts.json
{
  "layouts": {
    "two-balanced": [
      { "x": 8, "bottom": 2, "width": 52, "rotate": -1 },
      { "x": 56, "bottom": 3, "width": 41, "rotate": 0 }
    ],
    "two-first-focus": [
      { "x": 8, "bottom": 2, "width": 58, "rotate": -2 },
      { "x": 60, "bottom": 3, "width": 36, "rotate": 0 }
    ],
    "two-second-focus": [
      { "x": 8, "bottom": 3, "width": 45, "rotate": 0 },
      { "x": 52, "bottom": 2, "width": 47, "rotate": 1 }
    ],
    "three-balanced": [
      { "x": 2, "bottom": 2, "width": 38, "rotate": -2 },
      { "x": 33, "bottom": 3, "width": 37, "rotate": 0 },
      { "x": 64, "bottom": 2, "width": 35, "rotate": 2 }
    ]
  }
}

Original reference

What this proves

  1. Rebuild: Recreate the reference format with a reusable scene contract.
  2. New topic: Teach a different lesson without changing the renderer.
  3. New world: Research and package different characters without adding world-specific code.

2. Instructions

Format instructions

What this Format does and how to run it.

README.md
# Cartoon Explainer

Turn a real lesson into a short conversation between familiar characters. A curious lead asks the obvious questions, an expert explains the idea through the story world, and a third character adds tension or a joke.

This package is the first Wiggly Format repository experiment. It contains the instructions, inputs, assets, prompts, scene slots, renderer, audio rules, quality checks, and exact run records needed to recreate its outputs.

An agent can operate the package without an OpenRouter planning call. Start with `SKILL.md`; it explains how to check requirements, create and validate a scene plan, request one render-loop approval, inspect the result, and stop after three attempts.

## Start from the public Wiggly Repo

1. Download and unzip the **Runnable Format Kit**.
2. Open its `v3` folder and run `npm install`.
3. Run `npm run smoke`. This free local test proves the renderer, assets, FFmpeg, Remotion, and audio mix work.
4. Copy `.env.example` to `.env.local` and add the requested key values.
5. Tell Claude or Codex: “Read `public/format-repositories/otaku-explainer-v1/SKILL.md` and use the packaged renderer. Do not rebuild it.”

The kit contains the real runner, renderer, dependencies, rules, layouts, and required assets. A fresh agent may write a new scene plan or story-world pack, but it must not recreate the renderer or timing pipeline.

## Formula

1. Open with a question that names the confusing idea.
2. Explain it one step at a time through story-world objects and actions.
3. Let the challenger introduce a believable mistake, warning, or joke.
4. Correct the mistake and complete the lesson.
5. End with the lead character saying the core idea back in one simple line.

Each scene uses the same visual grammar: a slowly panning story-world background, two or three grounded character cutouts, one visible speaker, one speech bubble, and an optional short callout.

Scene writers choose lesson roles and an approved layout. `worlds/*.json` maps the roles to characters and voices, while `layouts.json` owns the coordinates. This keeps a new topic from inventing a new renderer.

## Agent commands

From `v3`:

```bash
npm run smoke
npm run prototype:otaku -- check
npm run prototype:otaku -- init --run=<run-id> --topic="<topic>" --world=naruto
npm run prototype:otaku -- validate --run=<run-id>
npm run prototype:otaku -- render --run=<run-id> --approve-loop
npm run prototype:otaku -- inspect --run=<run-id>
npm run prototype:otaku -- finalize --run=<run-id>
```

The Fish key stays in `v3/.env.local`. Serper is needed only when a run adds a new story world. `SKILL.md` tells the agent how to research lore, choose voices, source and inspect assets, and create the world pack without adding another command or changing the renderer. The package never needs OpenRouter, Replicate, a GPU, image generation, or video generation.

## Proof runs

The Format page discovers every packaged world from `worlds/*.json`, every scene plan from `scenes/*.json`, and every finished proof from `outputs/*.run.json`. These files are the source of truth; the README and page do not maintain separate lists.

Every proof uses the same scene contract and `renderer/OtakuFormatRenderer.tsx`. The tracked videos, contact sheets, run records, and quality reports show whether the agent loop works across topics and story worlds.

## Editing

Assets and deterministic scene values can be replaced without regenerating the script. Changing a prompt or instruction marks the package **Needs rerun**, because an old output must never pretend to reflect a new prompt.

3. User inputs

User inputs

The topic and story world the Format needs. The world pack supplies the cast.

inputs.json
{
  "fields": [
    {
      "id": "topic",
      "label": "What should the characters explain?",
      "type": "text",
      "required": true
    },
    {
      "id": "storyWorld",
      "label": "Which story world should teach it?",
      "type": "story-world",
      "required": true,
      "source": "worlds/*.json"
    }
  ],
  "defaults": {
    "topic": "Compilers versus interpreters",
    "storyWorld": "naruto"
  }
}

4. Fixed assets

Naruto

Naruto

assets/images/naruto-clean.png
Kakashi

Kakashi

assets/images/kakashi-alpha.png
Orochimaru

Orochimaru

assets/images/orochimaru-clean.png
Yugi

Yugi

assets/images/yugi-clean.png
Kaiba

Kaiba

assets/images/kaiba-clean.png
Joey

Joey

assets/images/joey.webp
Danny Phantom

Danny Phantom

assets/images/danny-phantom.webp
Tucker Foley

Tucker Foley

assets/images/tucker-foley.webp
Vlad Plasmius

Vlad Plasmius

assets/images/vlad-plasmius.webp
SpongeBob SquarePants

SpongeBob SquarePants

assets/images/spongebob.webp
Sandy Cheeks

Sandy Cheeks

assets/images/sandy-cheeks.png
Plankton

Plankton

assets/images/plankton.webp
Konoha

Konoha

assets/images/bg-konoha.jpg
Forest

Forest

assets/images/bg-naruto-forest.jpg
Training ground

Training ground

assets/images/bg-training-ground.jpg
Duel arena

Duel arena

assets/images/bg-duel-arena.jpg
Duel club

Duel club

assets/images/bg-duel-club.jpg
Domino City

Domino City

assets/images/bg-domino-city.jpg
Amity Park

Amity Park

assets/images/bg-amity-park.webp
Fenton lab

Fenton lab

assets/images/bg-fenton-lab.webp
Ghost Zone

Ghost Zone

assets/images/bg-ghost-zone.jpg
Bikini Bottom

Bikini Bottom

assets/images/bg-bikini-bottom.webp
Sandy's treedome

Sandy's treedome

assets/images/bg-sandy-treedome.webp
Mrs. Puff's Boating School

Mrs. Puff's Boating School

assets/images/bg-boating-school.webp

Fixed assets

Character cutouts, backgrounds, source links, and local files.

assets.json
{
  "characters": [
    {
      "id": "naruto",
      "label": "Naruto",
      "localPath": "assets/images/naruto-clean.png",
      "postprocess": "remove-white-and-trim",
      "searchQuery": "Naruto Uzumaki full body transparent png render",
      "sourcePage": "https://www.pngitem.com/middle/hJmTRwx_naruto-naruto-shippuden-anime-naruto-render-hd-png/",
      "sourceUrl": "https://www.pngitem.com/pimgs/m/487-4873619_naruto-naruto-shippuden-anime-naruto-render-hd-png.png"
    },
    {
      "id": "kakashi",
      "label": "Kakashi",
      "localPath": "assets/images/kakashi-alpha.png",
      "searchQuery": "Kakashi Hatake full body transparent png render",
      "sourcePage": "https://freepngimg.com/png/32495-kakashi-hd",
      "sourceUrl": "https://freepngimg.com/save/32495-kakashi-hd/900x2332"
    },
    {
      "id": "orochimaru",
      "label": "Orochimaru",
      "localPath": "assets/images/orochimaru-clean.png",
      "postprocess": "remove-white-and-trim",
      "searchQuery": "Orochimaru full body transparent png render",
      "sourcePage": "https://www.pngitem.com/middle/iJbimTT_renders-de-naruto-y-naruto-shippuden-naruto-orochimaru/",
      "sourceUrl": "https://www.pngitem.com/pimgs/m/282-2823667_renders-de-naruto-y-naruto-shippuden-naruto-orochimaru.png"
    },
    {
      "id": "yugi",
      "label": "Yugi",
      "localPath": "assets/images/yugi-clean.png",
      "postprocess": "remove-checkerboard-and-trim",
      "searchQuery": "Yami Yugi full body transparent png render",
      "sourcePage": "https://www.pngaaa.com/detail/4492094",
      "sourceUrl": "https://image.pngaaa.com/94/4492094-middle.png"
    },
    {
      "id": "kaiba",
      "label": "Kaiba",
      "localPath": "assets/images/kaiba-clean.png",
      "postprocess": "remove-checkerboard-and-trim",
      "searchQuery": "Seto Kaiba full body transparent png render",
      "sourcePage": "https://www.pngaaa.com/detail/4354853",
      "sourceUrl": "https://image.pngaaa.com/853/4354853-middle.png"
    },
    {
      "id": "joey",
      "label": "Joey",
      "localPath": "assets/images/joey.webp",
      "searchQuery": "Joey Wheeler full body transparent png render",
      "sourcePage": "https://all-worlds-alliance.fandom.com/wiki/Joey_Wheeler",
      "sourceUrl": "https://static.wikia.nocookie.net/all-worlds-alliance/images/d/d2/JoeyWheelerMD.png/revision/latest/scale-to-width-down/340?cb=20200516064401"
    },
    {
      "id": "danny-phantom",
      "label": "Danny Phantom",
      "localPath": "assets/images/danny-phantom.webp",
      "searchQuery": "Danny Phantom full body transparent png render",
      "sourcePage": "https://dannyphantom.fandom.com/wiki/Danny_Phantom_(character)",
      "sourceUrl": "https://static.wikia.nocookie.net/dpwikia/images/3/3c/Render-_Danny_Phantom_stock_image.png/revision/latest?cb=20250529003148"
    },
    {
      "id": "tucker-foley",
      "label": "Tucker Foley",
      "localPath": "assets/images/tucker-foley.webp",
      "searchQuery": "Tucker Foley full body transparent png render",
      "sourcePage": "https://dannyphantom.fandom.com/wiki/Tucker_Foley",
      "sourceUrl": "https://static.wikia.nocookie.net/dpwikia/images/a/ac/Render-_Tucker_Foley.png/revision/latest?cb=20250529003317"
    },
    {
      "id": "vlad-plasmius",
      "label": "Vlad Plasmius",
      "localPath": "assets/images/vlad-plasmius.webp",
      "searchQuery": "Vlad Plasmius full body transparent png render",
      "sourcePage": "https://dannyphantom.fandom.com/wiki/Vlad_Plasmius",
      "sourceUrl": "https://static.wikia.nocookie.net/dpwikia/images/6/67/Render-_Vlad_Plasmius_pose.png/revision/latest?cb=20260610224348"
    },
    {
      "id": "spongebob",
      "label": "SpongeBob SquarePants",
      "localPath": "assets/images/spongebob.webp",
      "searchQuery": "SpongeBob full body transparent PNG stock art",
      "sourcePage": "https://spongebob.fandom.com/wiki/SpongeBob_SquarePants_(character)/gallery",
      "sourceUrl": "https://static.wikia.nocookie.net/spongebob/images/5/5f/SpongeBobStockArt-2D.png/revision/latest?cb=20251102074746"
    },
    {
      "id": "sandy-cheeks",
      "label": "Sandy Cheeks",
      "localPath": "assets/images/sandy-cheeks.png",
      "searchQuery": "Sandy Cheeks full body spacesuit transparent PNG render",
      "sourcePage": "https://commons.wikimedia.org/wiki/File:Sandy_Cheeks_(transparent).png",
      "sourceUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Sandy_Cheeks_%28transparent%29.png/500px-Sandy_Cheeks_%28transparent%29.png"
    },
    {
      "id": "plankton",
      "label": "Plankton",
      "localPath": "assets/images/plankton.webp",
      "searchQuery": "Plankton full body transparent PNG stock art",
      "sourcePage": "https://spongebob.fandom.com/wiki/Sheldon_J._Plankton/gallery",
      "sourceUrl": "https://static.wikia.nocookie.net/spongebob/images/7/7a/Laughton.png/revision/latest?cb=20221113005908"
    }
  ],
  "backgrounds": [
    {
      "id": "konoha",
      "label": "Konoha",
      "localPath": "assets/images/bg-konoha.jpg",
      "searchQuery": "Naruto Konoha village background no characters",
      "sourcePage": "https://www.peakpx.com/en/hd-wallpaper-desktop-fzdbf",
      "sourceUrl": "https://w0.peakpx.com/wallpaper/589/265/HD-wallpaper-konohagakure-anime-naruto-the-leaf-village.jpg"
    },
    {
      "id": "naruto-forest",
      "label": "Forest",
      "localPath": "assets/images/bg-naruto-forest.jpg",
      "searchQuery": "Naruto forest background no characters",
      "sourcePage": "https://www.pinterest.com/ideas/naruto-backgrounds/941180977477/",
      "sourceUrl": "https://s-media-cache-ak0.pinimg.com/originals/ed/ed/05/eded05d47a6e14ef62a7c275589f16bb.jpg"
    },
    {
      "id": "training-ground",
      "label": "Training ground",
      "localPath": "assets/images/bg-training-ground.jpg",
      "searchQuery": "Naruto training ground background no characters",
      "sourcePage": "https://sketchfab.com/3d-models/training-field-naruto-stage-glb-c6cd7082ee864ec984b11f8cdaeefa64",
      "sourceUrl": "https://media.sketchfab.com/models/c6cd7082ee864ec984b11f8cdaeefa64/thumbnails/f612af92dae545d2be1ec358e21e2b23/1e62e315d3684beab34923a07f51edd6.jpeg"
    },
    {
      "id": "duel-arena",
      "label": "Duel arena",
      "localPath": "assets/images/bg-duel-arena.jpg",
      "searchQuery": "Yu-Gi-Oh duel arena background no characters",
      "sourcePage": "https://yugioh.fandom.com/wiki/Dueling_Arena",
      "sourceUrl": "https://static.wikia.nocookie.net/yugioh/images/c/ce/DuelingArena-EN-Anime-DM.png/revision/latest?cb=20080930233413"
    },
    {
      "id": "duel-club",
      "label": "Duel club",
      "localPath": "assets/images/bg-duel-club.jpg",
      "searchQuery": "Yu-Gi-Oh classroom background no characters",
      "sourcePage": "https://yugioh.fandom.com/wiki/Duel_Club",
      "sourceUrl": "https://static.wikia.nocookie.net/yugioh/images/d/d0/Dc.png/revision/latest/scale-to-width-down/1200?cb=20170903092444"
    },
    {
      "id": "domino-city",
      "label": "Domino City",
      "localPath": "assets/images/bg-domino-city.jpg",
      "searchQuery": "Yu-Gi-Oh city background no characters",
      "sourcePage": "https://backgrounds.gallery/backgrounds/753/1150640-Paradise%20City",
      "sourceUrl": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/items/1150640/cacb64095456a03c8e04e9e886d938c1f504d29c.jpg"
    },
    {
      "id": "amity-park",
      "label": "Amity Park",
      "localPath": "assets/images/bg-amity-park.webp",
      "searchQuery": "Danny Phantom Amity Park street background no characters wide",
      "sourcePage": "https://dannyphantom.fandom.com/wiki/Amity_Park",
      "sourceUrl": "https://static.wikia.nocookie.net/dpwikia/images/c/c0/S01e16_AP_welcome_sign.png/revision/latest?cb=20160627025704"
    },
    {
      "id": "fenton-lab",
      "label": "Fenton lab",
      "localPath": "assets/images/bg-fenton-lab.webp",
      "searchQuery": "Danny Phantom Fenton lab background no characters",
      "sourcePage": "https://dannyphantom.fandom.com/wiki/Fenton_Portal",
      "sourceUrl": "https://static.wikia.nocookie.net/dpwikia/images/f/fe/S01e04_Fenton_Ghost_Portal.png/revision/latest?cb=20160428225359"
    },
    {
      "id": "ghost-zone",
      "label": "Ghost Zone",
      "localPath": "assets/images/bg-ghost-zone.jpg",
      "searchQuery": "Danny Phantom Ghost Zone landscape ground background no characters wide",
      "sourcePage": "https://www.ign.com/wikis/nick-all-star-brawl/Ghost_Zone",
      "sourceUrl": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/nick-all-star-brawl/1/19/Ghost_Zone.jpg"
    },
    {
      "id": "bikini-bottom",
      "label": "Bikini Bottom",
      "localPath": "assets/images/bg-bikini-bottom.webp",
      "searchQuery": "SpongeBob Bikini Bottom city background no characters wide",
      "sourcePage": "https://spongebob.fandom.com/wiki/Bikini_Bottom",
      "sourceUrl": "https://static.wikia.nocookie.net/spongebob/images/2/26/Bubbletown_002.png/revision/latest/scale-to-width-down/1200?cb=20181028171458"
    },
    {
      "id": "sandy-treedome",
      "label": "Sandy's treedome",
      "localPath": "assets/images/bg-sandy-treedome.webp",
      "searchQuery": "Sandy treedome exterior background no characters wide",
      "sourcePage": "https://spongebob.fandom.com/wiki/Sandy_Cheeks%27_treedome",
      "sourceUrl": "https://static.wikia.nocookie.net/spongebob/images/f/f2/Sandy%2C_Help_Us%21_002.png/revision/latest/scale-to-width-down/1200?cb=20231007030731"
    },
    {
      "id": "boating-school",
      "label": "Mrs. Puff's Boating School",
      "localPath": "assets/images/bg-boating-school.webp",
      "searchQuery": "Mrs. Puff Boating School exterior background no characters wide",
      "sourcePage": "https://spongebob.fandom.com/wiki/Mrs._Puff%27s_Boating_School",
      "sourceUrl": "https://static.wikia.nocookie.net/spongebob/images/1/12/Lighthouse_Louie_001.png/revision/latest?cb=20200527190216"
    }
  ]
}

5. AI-generated content

Script prompt

How the lesson becomes dialogue that fits the story world.

prompts/script-system.md
# Script instruction

Write a 45–70 second conversation that teaches one confusing idea through the selected story world.

- The curious lead asks plain questions.
- The expert gives the accurate explanation.
- The third character adds tension, a useful correction, or one joke.
- Use short sentences and familiar words.
- Keep each speech bubble below 95 characters when possible.
- Use real story-world objects and character behavior. Do not paste technical words into random lore.
- End with the curious lead saying the core lesson back correctly.
- Do not use generic hype, fake stakes, or filler.

Image search rules

How to find grounded backgrounds and clean character art.

prompts/image-search.md
# Image search instruction

Find full-body transparent character renders and wide story-world backgrounds with no foreground characters. Prefer clean, recognizable art that matches the original show. Record the search query, source page, and direct image URL. Reject any background that makes the characters float or clips them at an impossible ground line.

6. Scene slots

Danny Phantom explains how APIs work

A validated scene-by-scene lesson plan.

scenes/danny-apis.json
{
  "id": "danny-phantom-how-apis-work",
  "title": "Danny Phantom explains how APIs work",
  "input": {
    "topic": "How APIs work",
    "storyWorld": "danny-phantom"
  },
  "scenes": [
    {
      "id": "api-01",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-balanced",
      "dialogue": "Tucker, how do two apps talk without giving each other full access?",
      "background": "fenton-lab",
      "estimatedDurationMs": 3700,
      "callout": { "label": "API?", "theme": "question" }
    },
    {
      "id": "api-02",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "They use an API, like the Fenton Portal between Earth and the Ghost Zone.",
      "background": "fenton-lab",
      "estimatedDurationMs": 4000,
      "callout": { "label": "SAFE DOORWAY", "theme": "cool" }
    },
    {
      "id": "api-03",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "The worlds stay separate, but the portal lets something cross between them.",
      "background": "ghost-zone",
      "estimatedDurationMs": 3900
    },
    {
      "id": "api-04",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "Right. An app sends a request, like a command entered into the portal controls.",
      "background": "fenton-lab",
      "estimatedDurationMs": 4100,
      "callout": { "label": "REQUEST", "theme": "warm" }
    },
    {
      "id": "api-05",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "And I need the correct control for the job, not just any button.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3500,
      "callout": { "label": "WHICH CONTROL?", "theme": "question" }
    },
    {
      "id": "api-06",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Exactly. That specific place to send a request is called an endpoint.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3700,
      "callout": { "label": "ENDPOINT", "theme": "violet" }
    },
    {
      "id": "api-07",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "The system does the work, then sends a response back through the doorway.",
      "background": "ghost-zone",
      "estimatedDurationMs": 3800,
      "callout": { "label": "RESPONSE", "theme": "cool" }
    },
    {
      "id": "api-08",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "Can the request say which ghost file or location I need?",
      "background": "amity-park",
      "estimatedDurationMs": 3300
    },
    {
      "id": "api-09",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "Yes. Those extra details are parameters. They make the request specific.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3900,
      "callout": { "label": "DETAILS", "theme": "warm" }
    },
    {
      "id": "api-10",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner", "guide"],
      "layout": "three-balanced",
      "dialogue": "Surely you would not let every foolish child control such a powerful portal.",
      "background": "ghost-zone",
      "estimatedDurationMs": 4100,
      "callout": { "label": "WHO GETS IN?", "theme": "gold" }
    },
    {
      "id": "api-11",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner", "challenger"],
      "layout": "three-balanced",
      "dialogue": "We would not. Many APIs check a secret key or token before they answer.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3900,
      "callout": { "label": "AUTH", "theme": "violet" }
    },
    {
      "id": "api-12",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner"],
      "layout": "two-first-focus",
      "dialogue": "Send the wrong command or key, and the system returns an error instead.",
      "background": "ghost-zone",
      "estimatedDurationMs": 3900,
      "callout": { "label": "ERROR", "theme": "warm" }
    },
    {
      "id": "api-13",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Both sides follow the same rules, so each request has a clear meaning.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3700,
      "callout": { "label": "SHARED RULES", "theme": "cool" }
    },
    {
      "id": "api-14",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "So an API is a controlled doorway where software can ask and get an answer.",
      "background": "amity-park",
      "estimatedDurationMs": 4000
    },
    {
      "id": "api-15",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner", "challenger"],
      "layout": "three-balanced",
      "dialogue": "You got it. Separate systems can work together without exposing everything.",
      "background": "fenton-lab",
      "estimatedDurationMs": 3800,
      "callout": { "label": "ASK + ANSWER", "theme": "gold" }
    }
  ]
}

Naruto explains how APIs work

A validated scene-by-scene lesson plan.

scenes/naruto-apis.json
{
  "id": "naruto-how-apis-work",
  "title": "Naruto explains how APIs work",
  "input": {
    "topic": "How APIs work",
    "storyWorld": "naruto"
  },
  "scenes": [
    {
      "id": "api-01",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-balanced",
      "dialogue": "Kakashi Sensei, how do two apps talk without sharing everything?",
      "background": "konoha",
      "estimatedDurationMs": 3600,
      "callout": { "label": "API?", "theme": "question" }
    },
    {
      "id": "api-02",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "They use an API, like a mission desk with a clear list of requests.",
      "background": "training-ground",
      "estimatedDurationMs": 3900,
      "callout": { "label": "MISSION DESK", "theme": "cool" }
    },
    {
      "id": "api-03",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "So I ask the desk instead of running into the secret scroll room?",
      "background": "konoha",
      "estimatedDurationMs": 3900
    },
    {
      "id": "api-04",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "Right. Your app sends a request that says exactly what it needs.",
      "background": "training-ground",
      "estimatedDurationMs": 3600,
      "callout": { "label": "REQUEST", "theme": "warm" }
    },
    {
      "id": "api-05",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-first-focus",
      "dialogue": "The other app does the work and sends back a response.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3400,
      "callout": { "label": "RESPONSE", "theme": "cool" }
    },
    {
      "id": "api-06",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-balanced",
      "dialogue": "Where do I send the request?",
      "background": "konoha",
      "estimatedDurationMs": 2500,
      "callout": { "label": "WHERE?", "theme": "question" }
    },
    {
      "id": "api-07",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "To an endpoint. Think of it as the right window at the mission desk.",
      "background": "training-ground",
      "estimatedDurationMs": 3900,
      "callout": { "label": "ENDPOINT", "theme": "violet" }
    },
    {
      "id": "api-08",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner", "guide"],
      "layout": "three-balanced",
      "dialogue": "And the desk should not obey every stranger who walks in.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3500,
      "callout": { "label": "AUTH", "theme": "gold" }
    },
    {
      "id": "api-09",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "That is why many APIs check a key or token before helping.",
      "background": "training-ground",
      "estimatedDurationMs": 3600
    },
    {
      "id": "api-10",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "Can my request include details, like which mission I want?",
      "background": "konoha",
      "estimatedDurationMs": 3500
    },
    {
      "id": "api-11",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "Yes. Those details are parameters, like a mission name and rank.",
      "background": "training-ground",
      "estimatedDurationMs": 3700,
      "callout": { "label": "DETAILS", "theme": "warm" }
    },
    {
      "id": "api-12",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner"],
      "layout": "two-balanced",
      "dialogue": "If you ask for a missing scroll, the response returns an error.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3600,
      "callout": { "label": "404", "theme": "warm" }
    },
    {
      "id": "api-13",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "APIs can also limit requests so one ninja cannot block the whole desk.",
      "background": "training-ground",
      "estimatedDurationMs": 3900,
      "callout": { "label": "RATE LIMIT", "theme": "violet" }
    },
    {
      "id": "api-14",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "So the apps stay separate, but the request rules let them work together!",
      "background": "konoha",
      "estimatedDurationMs": 4000
    },
    {
      "id": "api-15",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner", "challenger"],
      "layout": "three-balanced",
      "dialogue": "Exactly. An API is a safe, agreed way for software to ask and answer.",
      "background": "training-ground",
      "estimatedDurationMs": 3900,
      "callout": { "label": "ASK + ANSWER", "theme": "gold" }
    }
  ]
}

Naruto explains compilers versus interpreters

A validated scene-by-scene lesson plan.

scenes/naruto-compilers.json
{
  "id": "naruto-compilers",
  "title": "Naruto explains compilers versus interpreters",
  "input": {
    "topic": "Compilers versus interpreters",
    "storyWorld": "naruto"
  },
  "scenes": [
    {
      "id": "nc-01",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "Kakashi Sensei... if compilers and interpreters both translate code...",
      "background": "konoha",
      "estimatedDurationMs": 3900,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nc-02",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "Why do Python and C++ work so differently?!",
      "background": "naruto-forest",
      "estimatedDurationMs": 3100,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nc-03",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Because they execute jutsu differently, Naruto.",
      "background": "training-ground",
      "estimatedDurationMs": 3000
    },
    {
      "id": "nc-04",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "A compiler prepares the entire forbidden scroll before the battle begins.",
      "background": "naruto-forest",
      "estimatedDurationMs": 4300,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-05",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "So C++ works like a prepared jutsu?",
      "background": "konoha",
      "estimatedDurationMs": 3000,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-06",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Exactly. The compiler checks the whole program first.",
      "background": "training-ground",
      "estimatedDurationMs": 3200,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-07",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "It turns everything into machine code, then runs it fast later.",
      "background": "konoha",
      "estimatedDurationMs": 3800,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-08",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "An interpreter is different. It reads instructions during battle...",
      "background": "naruto-forest",
      "estimatedDurationMs": 3900,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-09",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "One step at a time.",
      "background": "training-ground",
      "estimatedDurationMs": 2300,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-10",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-first-focus",
      "dialogue": "Yes. Python often runs line by line with an interpreter.",
      "background": "konoha",
      "estimatedDurationMs": 3600,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-11",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "Then why use interpreters at all?",
      "background": "naruto-forest",
      "estimatedDurationMs": 2700,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nc-12",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Because feedback is instant. If one hand sign fails, you see the error.",
      "background": "training-ground",
      "estimatedDurationMs": 4400,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-13",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Compilers catch many mistakes before the program runs.",
      "background": "konoha",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-14",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Interpreters find problems while the jutsu is already running.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3600,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-15",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "So compile errors are failed hand signs before battle...",
      "background": "training-ground",
      "estimatedDurationMs": 3500,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-16",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "And runtime errors are jutsu failing mid-fight?!",
      "background": "konoha",
      "estimatedDurationMs": 3200,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "nc-17",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "A compiler leaves you with one completed forbidden scroll.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "nc-18",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "Dattebayo... programming languages are basically ninja fighting styles!",
      "background": "training-ground",
      "estimatedDurationMs": 4200,
      "callout": {
        "label": "DUEL",
        "theme": "gold"
      }
    }
  ]
}

Naruto explains MCP

A validated scene-by-scene lesson plan.

scenes/naruto-mcp.json
{
  "id": "naruto-mcp",
  "title": "Naruto explains MCP",
  "input": {
    "topic": "Model Context Protocol",
    "storyWorld": "naruto"
  },
  "scenes": [
    {
      "id": "nm-01",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "Kakashi Sensei... why can't every AI tool just share its stuff?",
      "background": "konoha",
      "estimatedDurationMs": 3900,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nm-02",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Each tool guards its data like a different ninja village.",
      "background": "training-ground",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-03",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "So Claude needs a new jutsu for every app?",
      "background": "naruto-forest",
      "estimatedDurationMs": 3000,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nm-04",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Exactly. One for files. One for Slack. One for a database.",
      "background": "konoha",
      "estimatedDurationMs": 3900,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-05",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "That is where MCP becomes interesting.",
      "background": "naruto-forest",
      "estimatedDurationMs": 2900,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-06",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "Think of MCP as one scroll format every village can read.",
      "background": "training-ground",
      "estimatedDurationMs": 3600,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-07",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "The same scroll can ask for tools, files, and data?",
      "background": "konoha",
      "estimatedDurationMs": 3300,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nm-08",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Yes. The AI is the shinobi asking for help.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3200,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-09",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "An MCP server is the village gate offering approved tools.",
      "background": "training-ground",
      "estimatedDurationMs": 3500,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-10",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "It tells the AI what exists and how to use it.",
      "background": "konoha",
      "estimatedDurationMs": 3300,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-11",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "So I don't rebuild the road every time?",
      "background": "naruto-forest",
      "estimatedDurationMs": 2800,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "nm-12",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Right. Add a server, and the AI gets new abilities.",
      "background": "training-ground",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-13",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "But the village gate still decides what the AI may touch.",
      "background": "konoha",
      "estimatedDurationMs": 3600,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-14",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "One common rulebook... and many powerful jutsu.",
      "background": "naruto-forest",
      "estimatedDurationMs": 3100,
      "callout": {
        "label": "MCP",
        "theme": "violet"
      }
    },
    {
      "id": "nm-15",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-first-focus",
      "dialogue": "Dattebayo... MCP is basically the ninja network for AI!",
      "background": "training-ground",
      "estimatedDurationMs": 3700,
      "callout": {
        "label": "DUEL",
        "theme": "gold"
      }
    }
  ]
}

SpongeBob explains how electric vehicles work

A validated scene-by-scene lesson plan.

scenes/spongebob-evs.json
{
  "id": "spongebob-how-evs-work",
  "title": "SpongeBob explains how electric vehicles work",
  "input": {
    "topic": "How electric vehicles work",
    "storyWorld": "spongebob"
  },
  "scenes": [
    {
      "id": "ev-01",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "Sandy, why doesn't your new boatmobile need gasoline?",
      "background": "boating-school",
      "estimatedDurationMs": 3400,
      "callout": { "label": "NO GAS?", "theme": "question" }
    },
    {
      "id": "ev-02",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Because it is electric. A battery stores the energy that moves it.",
      "background": "boating-school",
      "estimatedDurationMs": 3900,
      "callout": { "label": "BATTERY", "theme": "cool" }
    },
    {
      "id": "ev-03",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "So the battery is like a lunch box packed with electricity?",
      "background": "sandy-treedome",
      "estimatedDurationMs": 3600,
      "callout": { "label": "STORED ENERGY", "theme": "warm" }
    },
    {
      "id": "ev-04",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Yep. Press the pedal, and a controller decides how much power to send.",
      "background": "sandy-treedome",
      "estimatedDurationMs": 4100,
      "callout": { "label": "CONTROLLER", "theme": "violet" }
    },
    {
      "id": "ev-05",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-balanced",
      "dialogue": "An inverter prepares that electricity for the motor.",
      "background": "sandy-treedome",
      "estimatedDurationMs": 3400,
      "callout": { "label": "INVERTER", "theme": "cool" }
    },
    {
      "id": "ev-06",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "Then the motor turns the wheels instead of burning gas?",
      "background": "boating-school",
      "estimatedDurationMs": 3600,
      "callout": { "label": "MOTOR", "theme": "question" }
    },
    {
      "id": "ev-07",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Exactly. Electric energy becomes motion, so the boatmobile moves.",
      "background": "boating-school",
      "estimatedDurationMs": 3900,
      "callout": { "label": "WHEELS TURN", "theme": "gold" }
    },
    {
      "id": "ev-08",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner", "guide"],
      "layout": "three-balanced",
      "dialogue": "Bah! Connect the battery straight to the wheels. Maximum power!",
      "background": "bikini-bottom",
      "estimatedDurationMs": 3700,
      "callout": { "label": "BAD SHORTCUT", "theme": "warm" }
    },
    {
      "id": "ev-09",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner", "challenger"],
      "layout": "three-balanced",
      "dialogue": "That would be unsafe. The controller keeps the power smooth and controlled.",
      "background": "bikini-bottom",
      "estimatedDurationMs": 4300,
      "callout": { "label": "CONTROL", "theme": "violet" }
    },
    {
      "id": "ev-10",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "What happens when the battery runs low?",
      "background": "sandy-treedome",
      "estimatedDurationMs": 3100,
      "callout": { "label": "LOW BATTERY", "theme": "question" }
    },
    {
      "id": "ev-11",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Plug it in. The charger moves electricity from the grid into the battery.",
      "background": "sandy-treedome",
      "estimatedDurationMs": 4200,
      "callout": { "label": "CHARGE", "theme": "cool" }
    },
    {
      "id": "ev-12",
      "speakerRole": "challenger",
      "visibleRoles": ["challenger", "learner"],
      "layout": "two-first-focus",
      "dialogue": "Braking wastes all that beautiful speed!",
      "background": "boating-school",
      "estimatedDurationMs": 3100,
      "callout": { "label": "WASTED?", "theme": "question" }
    },
    {
      "id": "ev-13",
      "speakerRole": "guide",
      "visibleRoles": ["guide", "learner"],
      "layout": "two-second-focus",
      "dialogue": "Not always. Regenerative braking lets the motor send some energy back.",
      "background": "boating-school",
      "estimatedDurationMs": 4100,
      "callout": { "label": "ENERGY BACK", "theme": "gold" }
    },
    {
      "id": "ev-14",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide"],
      "layout": "two-first-focus",
      "dialogue": "It still uses regular brakes, and it cannot recover every bit of energy.",
      "background": "boating-school",
      "estimatedDurationMs": 4200,
      "callout": { "label": "SOME, NOT ALL", "theme": "neutral" }
    },
    {
      "id": "ev-15",
      "speakerRole": "learner",
      "visibleRoles": ["learner", "guide", "challenger"],
      "layout": "three-balanced",
      "dialogue": "Got it! Store, control, spin, recharge, and recover some energy when slowing.",
      "background": "bikini-bottom",
      "estimatedDurationMs": 4300,
      "callout": { "label": "EV COMPLETE", "theme": "gold" }
    }
  ]
}

Yu-Gi-Oh explains compilers versus interpreters

A validated scene-by-scene lesson plan.

scenes/yugioh-compilers.json
{
  "id": "yugioh-compilers",
  "title": "Yu-Gi-Oh explains compilers versus interpreters",
  "input": {
    "topic": "Compilers versus interpreters",
    "storyWorld": "yugioh"
  },
  "scenes": [
    {
      "id": "yc-01",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "Kaiba... if compilers and interpreters both run code...",
      "background": "duel-arena",
      "estimatedDurationMs": 3500,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "yc-02",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "Why do C++ and Python duel so differently?",
      "background": "domino-city",
      "estimatedDurationMs": 3000,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "yc-03",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "One prepares the whole deck. The other draws turn by turn.",
      "background": "duel-club",
      "estimatedDurationMs": 3700,
      "callout": {
        "label": "DUEL",
        "theme": "gold"
      }
    },
    {
      "id": "yc-04",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "A compiler checks every card before the duel begins.",
      "background": "duel-arena",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "yc-05",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "So C++ enters with the whole combo ready?",
      "background": "domino-city",
      "estimatedDurationMs": 3000,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "yc-06",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "challenger"
      ],
      "layout": "two-balanced",
      "dialogue": "Finally, Wheeler understands something.",
      "background": "duel-club",
      "estimatedDurationMs": 2600
    },
    {
      "id": "yc-07",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "The compiler turns the full program into machine code.",
      "background": "duel-arena",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "yc-08",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Then it can run that prepared deck very fast.",
      "background": "domino-city",
      "estimatedDurationMs": 3100,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "yc-09",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "And an interpreter?",
      "background": "duel-club",
      "estimatedDurationMs": 2200,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "yc-10",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "It reads one move at a time, like drawing every turn?",
      "background": "duel-arena",
      "estimatedDurationMs": 3500,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "yc-11",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Correct. Python often runs instructions line by line.",
      "background": "domino-city",
      "estimatedDurationMs": 3300,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "yc-12",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "That sounds slower. Why use it?",
      "background": "duel-club",
      "estimatedDurationMs": 2700,
      "callout": {
        "label": "?",
        "theme": "question"
      }
    },
    {
      "id": "yc-13",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Because when a card fails, ya know right away.",
      "background": "duel-arena",
      "estimatedDurationMs": 3100,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "yc-14",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "Compilers catch many errors before the duel starts.",
      "background": "domino-city",
      "estimatedDurationMs": 3200,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "yc-15",
      "speakerRole": "guide",
      "visibleRoles": [
        "guide",
        "challenger"
      ],
      "layout": "two-balanced",
      "dialogue": "Interpreters reveal errors while the program is running.",
      "background": "duel-club",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "yc-16",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "So compile errors are illegal cards before the duel...",
      "background": "duel-arena",
      "estimatedDurationMs": 3500,
      "callout": {
        "label": "FULL SCROLL",
        "theme": "warm"
      }
    },
    {
      "id": "yc-17",
      "speakerRole": "challenger",
      "visibleRoles": [
        "challenger",
        "learner"
      ],
      "layout": "two-balanced",
      "dialogue": "And runtime errors are your combo exploding mid-turn!",
      "background": "domino-city",
      "estimatedDurationMs": 3300,
      "callout": {
        "label": "ONE STEP",
        "theme": "cool"
      }
    },
    {
      "id": "yc-18",
      "speakerRole": "learner",
      "visibleRoles": [
        "learner",
        "guide"
      ],
      "layout": "two-balanced",
      "dialogue": "Programming languages are just different duel strategies!",
      "background": "duel-club",
      "estimatedDurationMs": 3400,
      "callout": {
        "label": "DUEL",
        "theme": "gold"
      }
    }
  ]
}

7. Renderer

Renderer

The visual rules: moving background, characters, bubble, props, and active speaker.

renderer/OtakuFormatRenderer.tsx
import type { CSSProperties } from "react";

export type OtakuCharacterPlacement = {
  asset: string;
  x: number;
  bottom: number;
  width: number;
  rotate?: number;
};

export type OtakuScene = {
  id: string;
  speaker: string;
  dialogue: string;
  background: string;
  estimatedDurationMs: number;
  durationMs?: number;
  audioPath?: string;
  characters: OtakuCharacterPlacement[];
  callout?: {
    label: string;
    theme: "neutral" | "question" | "warm" | "cool" | "violet" | "gold";
  };
};

export type OtakuAsset = {
  id: string;
  label: string;
  localPath: string;
};

export type OtakuAssetLibrary = {
  characters: OtakuAsset[];
  backgrounds: OtakuAsset[];
};

export type OtakuFormatRendererProps = {
  assets: OtakuAssetLibrary;
  durationMs: number;
  resolveAsset?: (localPath: string) => string;
  scene: OtakuScene;
  timeInSceneMs: number;
};

const packageBase = "/format-repositories/otaku-explainer-v1/";

const defaultResolveAsset = (localPath: string) => `${packageBase}${localPath}`;

type CalloutTheme = NonNullable<OtakuScene["callout"]>["theme"];

const calloutColors: Record<CalloutTheme, [string, string]> = {
  neutral: ["#ffffff", "#64748b"],
  question: ["#ffe66d", "#ff7b00"],
  warm: ["#ff5d4a", "#7d160b"],
  cool: ["#55d6ff", "#1154b7"],
  violet: ["#c98cff", "#5a2ad1"],
  gold: ["#fff06a", "#6c41ff"],
};

const speechFontSize = (dialogue: string) => {
  if (dialogue.length > 82) return 30;
  if (dialogue.length > 64) return 34;
  if (dialogue.length > 46) return 38;
  return 44;
};

function Callout({ callout }: { callout?: OtakuScene["callout"] }) {
  if (!callout) return null;
  const [bright, dark] = calloutColors[callout.theme];
  return (
    <div style={{
      position: "absolute",
      zIndex: 2,
      left: "42%",
      top: "57%",
      width: 145,
      padding: "18px 14px",
      border: `5px solid ${dark}`,
      borderRadius: 24,
      background: `linear-gradient(135deg, ${bright}, ${dark})`,
      boxShadow: `0 10px 30px ${dark}88, inset 0 0 0 3px rgba(255,255,255,.35)`,
      color: "white",
      fontFamily: "Arial Black, sans-serif",
      fontSize: 20,
      lineHeight: 1,
      textAlign: "center",
      transform: "rotate(-7deg)",
    }}>{callout.label}</div>
  );
}

export function OtakuFormatRenderer({
  assets,
  durationMs,
  resolveAsset = defaultResolveAsset,
  scene,
  timeInSceneMs,
}: OtakuFormatRendererProps) {
  const progress = Math.max(0, Math.min(1, timeInSceneMs / Math.max(1, durationMs)));
  const background = assets.backgrounds.find((asset) => asset.id === scene.background);
  const [bright] = scene.callout ? calloutColors[scene.callout.theme] : ["#ffffff"];
  return (
    <div style={{
      position: "relative",
      width: 720,
      height: 1280,
      overflow: "hidden",
      background: "#101827",
      color: "#111",
      fontFamily: "Arial Rounded MT Bold, Arial, sans-serif",
    }}>
      {background ? (
        <img
          src={resolveAsset(background.localPath)}
          alt=""
          style={{
            position: "absolute",
            inset: "-5% auto -5% -3%",
            width: "116%",
            height: "110%",
            objectFit: "cover",
            objectPosition: "center bottom",
            transform: `translate3d(${-8 * progress}%, 0, 0) scale(1.04)`,
            filter: "saturate(.9) contrast(1.02) brightness(.83)",
          }}
        />
      ) : null}

      <div style={{
        position: "absolute",
        inset: 0,
        background: "linear-gradient(180deg, rgba(7,12,24,.34) 0%, rgba(7,12,24,0) 32%, rgba(7,12,24,.12) 68%, rgba(7,12,24,.58) 100%)",
      }} />

      <Callout callout={scene.callout} />

      {scene.characters.map((placement, index) => {
        const character = assets.characters.find((asset) => asset.id === placement.asset);
        if (!character) return null;
        const isSpeaking = placement.asset === scene.speaker;
        const transform = [
          `rotate(${placement.rotate || 0}deg)`,
          `scale(${isSpeaking ? 1.035 : 1})`,
        ].join(" ");
        const style: CSSProperties = {
          position: "absolute",
          zIndex: isSpeaking ? 4 : 3,
          left: `${placement.x}%`,
          bottom: `${placement.bottom}%`,
          width: `${placement.width}%`,
          maxHeight: "73%",
          objectFit: "contain",
          objectPosition: "center bottom",
          transform,
          transformOrigin: "center bottom",
          filter: isSpeaking
            ? `drop-shadow(0 0 7px ${bright}) drop-shadow(0 12px 8px rgba(0,0,0,.55))`
            : "drop-shadow(0 11px 7px rgba(0,0,0,.55)) brightness(.9)",
          opacity: isSpeaking ? 1 : 0.94,
        };
        return <img key={`${scene.id}-${placement.asset}-${index}`} src={resolveAsset(character.localPath)} alt={character.label} style={style} />;
      })}

      <div style={{
        position: "absolute",
        zIndex: 6,
        top: "5.2%",
        left: "9%",
        width: "72%",
        minHeight: 206,
        padding: "38px 38px 34px",
        border: "7px solid #101010",
        borderRadius: "48% 48% 44% 46% / 50% 46% 52% 48%",
        background: "#fff",
        boxShadow: `0 13px 0 rgba(0,0,0,.26), 0 0 0 7px ${bright}33`,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}>
        <div style={{
          color: "#0a0a0a",
          fontFamily: "Arial Rounded MT Bold, Arial, sans-serif",
          fontSize: speechFontSize(scene.dialogue),
          fontWeight: 800,
          lineHeight: 1.06,
          letterSpacing: -1.2,
          textAlign: "center",
          overflowWrap: "anywhere",
        }}>{scene.dialogue}</div>
        <div style={{
          position: "absolute",
          left: "24%",
          bottom: -28,
          width: 54,
          height: 54,
          borderRight: "7px solid #101010",
          borderBottom: "7px solid #101010",
          background: "white",
          transform: "rotate(38deg) skew(8deg, 8deg)",
        }} />
      </div>

      <div style={{
        position: "absolute",
        zIndex: 8,
        left: 24,
        bottom: 18,
        color: "rgba(255,255,255,.88)",
        fontSize: 20,
        fontWeight: 700,
        letterSpacing: 0.3,
        textShadow: "0 2px 8px rgba(0,0,0,.8)",
      }}>@Wiggly_Format_Lab</div>
    </div>
  );
}

8. Audio

Audio setup

Shared Fish model, speaking speed, and default music. World packs own their voices.

audio.json
{
  "provider": "fish-audio",
  "model": "s2.1-pro-free",
  "dialogue": {
    "format": "wav",
    "sampleRate": 44100,
    "speed": 1.08
  },
  "music": {
    "localPath": "assets/audio/background-music.mp3",
    "volume": 0.1,
    "loop": false,
    "preparation": "The runner trims or crossfades the packaged track into one full-length music bed for each video."
  }
}

9. Quality checks

Quality checks

The checks every rerun should pass.

quality.json
{
  "checks": [
    { "id": "same-renderer", "label": "Every proof uses the same renderer and scene contract." },
    { "id": "bubble-fit", "label": "Every line fits inside its speech bubble." },
    { "id": "grounded-characters", "label": "Feet or lower body meet a believable ground line." },
    { "id": "background-pan", "label": "Every scene has visible right-to-left background movement." },
    { "id": "voice-match", "label": "The active speaker uses the assigned Fish Audio voice." },
    { "id": "music-balance", "label": "Music has no vocals, clipping, or dead loop gap and stays below the dialogue." },
    { "id": "simple-language", "label": "A nontechnical viewer can repeat the lesson after watching." },
    { "id": "world-fit", "label": "The analogy uses objects and behavior from the selected story world." }
  ]
}

10. Final output

Danny Phantom explains how APIs work

danny-phantom · 15 scenes

Topic
How APIs work
Cast
danny-phantom, tucker-foley, vlad-plasmius
Voice model
fish-audio / s2.1-pro-free

Naruto explains how APIs work

naruto · 15 scenes

Topic
How APIs work
Cast
naruto, kakashi, orochimaru
Voice model
fish-audio / s2.1-pro-free

Naruto explains compilers versus interpreters

naruto · 18 scenes

Topic
Compilers versus interpreters
Cast
naruto, kakashi, orochimaru
Voice model
fish-audio / s2.1-pro-free

Naruto explains MCP

Naruto · 15 scenes

Topic
Model Context Protocol
Cast
naruto, kakashi, orochimaru
Voice model
fish-audio / s2.1-pro-free

SpongeBob explains how electric vehicles work

spongebob · 15 scenes

Topic
How electric vehicles work
Cast
spongebob, sandy-cheeks, plankton
Voice model
fish-audio / s2.1-pro-free

Yu-Gi-Oh explains compilers versus interpreters

Yu-Gi-Oh · 18 scenes

Topic
Compilers versus interpreters
Cast
yugi, kaiba, joey
Voice model
fish-audio / s2.1-pro-free

Agent runs

Drafts, the latest contact sheet, quality report, and final video appear here after refresh.

No agent run yet. The packaged proof videos above remain available.