Yearly Pay yearly and get 2 months free on every paid plan. See plans
Sign in Start free

Narrated motion explainers with ToRun MCP and Remotion

Last updated 11 Nov 2026

What you can make

A coding agent that runs commands on your computer, such as Claude Code, opencode or Cursor, can make a short narrated explainer video with ToRun. The agent writes the script, asks ToRun for the scene images, the narration and background music, and renders the video on your computer with Remotion, a library that turns React components into video.

What you need

  • A coding tool connected to the ToRun MCP server that can run commands and write files in a folder: opencode, Claude Code or Cursor.
  • Node.js 18 or later. The first render downloads the browser Remotion renders with.
  • A paid plan's allowance or a wallet balance: API calls never use the free allowance. See how calls are paid.

Remotion has its own license: it is free for individuals and very small companies, and larger companies need a company license. Check remotion.dev/license before you use it for work.

The starter project

Four files. Ask your agent to create them in an empty folder, or copy them yourself, then run npm install there. Generated files go in a public folder next to package.json.

package.json:

{
  "name": "motion-explainer",
  "private": true,
  "scripts": {
    "studio": "remotion studio src/index.ts",
    "render": "remotion render src/index.ts Explainer out/explainer.mp4"
  },
  "dependencies": {
    "@remotion/cli": "4.0.539",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "remotion": "4.0.539"
  }
}

video.json holds the whole video. Each scene has a caption shown on screen, the narration text say, the image prompt, the file names of its image and narration, and its length in seconds:

{
  "width": 1920,
  "height": 1080,
  "fps": 30,
  "music": { "file": "music.mp3", "volume": 0.12 },
  "scenes": [
    {
      "caption": "Every team has a backlog.",
      "say": "Every team has a backlog, and it only grows.",
      "prompt": "flat illustration of a wall covered in sticky notes, soft morning light, no text",
      "image": "scene-1.png",
      "narration": "scene-1.mp3",
      "seconds": 5.5
    },
    {
      "caption": "Sort it by what users feel.",
      "say": "Sort it by what your users feel, not by what is loudest.",
      "prompt": "flat illustration of hands arranging cards into three columns on a desk, no text",
      "image": "scene-2.png",
      "narration": "scene-2.mp3",
      "seconds": 6
    },
    {
      "caption": "Ship the top one this week.",
      "say": "Then ship the top one this week.",
      "prompt": "flat illustration of a small paper boat leaving a harbour at sunrise, no text",
      "image": "scene-3.png",
      "narration": "scene-3.mp3",
      "seconds": 5
    }
  ]
}

src/index.ts:

import {registerRoot} from 'remotion';
import {Root} from './Root';

registerRoot(Root);

src/Root.tsx plays the scenes one after another. Each image zooms in slowly, the caption rises in, the scene's narration starts with it, and the music plays quietly under the whole video:

import React from 'react';
import {AbsoluteFill, Composition, Html5Audio, Img, interpolate, Series, staticFile, useCurrentFrame} from 'remotion';
import data from '../video.json';

// "say" and "prompt" are the script: the narration text and the image prompt. Rendering reads the rest.
type Scene = {image: string; narration?: string; seconds: number; caption?: string; say?: string; prompt?: string};
type Video = {width: number; height: number; fps: number; music?: {file: string; volume: number}; scenes: Scene[]};

const video = data as Video;
const clamp = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const;
const framesOf = (scene: Scene) => Math.round(scene.seconds * video.fps);

// One scene: the image slowly zooms in, the caption rises in, the narration plays from the scene's start.
const SceneView: React.FC<{scene: Scene; frames: number}> = ({scene, frames}) => {
  const frame = useCurrentFrame();
  const fade = interpolate(frame, [0, 10, frames - 10, frames], [0, 1, 1, 0], clamp);
  const zoom = interpolate(frame, [0, frames], [1, 1.08]);
  const rise = interpolate(frame, [8, 26], [40, 0], clamp);
  return (
    <AbsoluteFill style={{backgroundColor: 'black', opacity: fade}}>
      <Img src={staticFile(scene.image)} style={{width: '100%', height: '100%', objectFit: 'cover', transform: `scale(${zoom})`}} />
      {scene.caption ? (
        <div
          style={{
            position: 'absolute', left: '6%', right: '6%', bottom: '8%', transform: `translateY(${rise}px)`,
            color: 'white', fontFamily: 'Arial, sans-serif', fontSize: video.height / 18, fontWeight: 700,
            textShadow: '0 2px 16px rgba(0,0,0,0.7)',
          }}
        >
          {scene.caption}
        </div>
      ) : null}
      {scene.narration ? <Html5Audio src={staticFile(scene.narration)} /> : null}
    </AbsoluteFill>
  );
};

const Explainer: React.FC = () => (
  <AbsoluteFill style={{backgroundColor: 'black'}}>
    <Series>
      {video.scenes.map((scene, i) => (
        <Series.Sequence key={i} durationInFrames={framesOf(scene)}>
          <SceneView scene={scene} frames={framesOf(scene)} />
        </Series.Sequence>
      ))}
    </Series>
    {video.music ? <Html5Audio src={staticFile(video.music.file)} volume={video.music.volume} loop /> : null}
  </AbsoluteFill>
);

export const Root: React.FC = () => (
  <Composition
    id="Explainer"
    component={Explainer}
    width={video.width}
    height={video.height}
    fps={video.fps}
    durationInFrames={video.scenes.reduce((sum, scene) => sum + framesOf(scene), 0)}
  />
);

How the agent makes the video

  1. Script. The agent writes the scenes into video.json: one idea per scene, a caption of a few words, one or two sentences to say. Ask it to show you the script before it generates anything.

  2. Balance. It calls account_balance to see what the account can spend. This call is free.

  3. Images. For each scene it calls image_generate with the scene's prompt and size 1792x1024, and saves the image as public/scene-1.png, public/scene-2.png and so on. If an image is not ready within the wait time, the tool returns a job id, and the agent asks job_status until the image is ready.

  4. Narration. For each scene it calls tts_generate with the scene's say text and a voice, for example nova, and saves the audio as public/scene-1.mp3 and so on.

  5. Timing. It sets each scene's seconds to the length of its narration plus about half a second. tts_generate returns that length as duration_seconds, measured from the audio file. If the answer also says duration_estimated: true, the length could only be estimated from the text, so the agent measures the file. To check a file yourself:

    npx remotion ffprobe -v error -show_entries format=duration -of csv=p=0 public/scene-1.mp3
    
  6. Music (optional). It calls music_generate with a prompt such as "calm instrumental background, soft piano", with_vocals set to false and duration_seconds close to the video's length. The tool returns a job id; the agent asks job_status until the track is ready and saves it as public/music.mp3. Without music, remove the music line from video.json.

  7. Preview and render. npm run studio opens a preview in your browser. npm run render writes out/explainer.mp4.

Every file link a tool returns downloads without signing in. Keep the whole link, including the part after the question mark:

curl -L -o public/scene-1.png "<the image link>"

Ask your agent

Make a 30-second explainer about how our app's sync works, with the Remotion starter project from torun.ai/docs/mcp-motion-explainers. Four scenes. Show me the script in video.json first. Then generate one image per scene with no text in it, narrate each scene with the voice nova, add quiet instrumental music, time each scene to its narration, and render.

For a vertical video, ask for width 1080 and height 1920 in video.json, and images with size 1024x1792.

What it costs

  • image_generate, tts_generate and music_generate are paid calls, charged in Runs at the same prices as in the app. A video with four scenes and music is four images, four narrations and one track.
  • account_balance and job_status are free.
  • Rendering runs on your computer. Changing captions, timing or motion and rendering again costs nothing on ToRun; generating an image or a narration again is a new paid call.
  • When one generation would cost more than the key's threshold, the tool answers with the price first, and your agent asks you before it runs the call.
  • Everything the agent generates is also saved to your ToRun library.

Tips

  • Ask for images with no text in them. The caption carries the words, and image models often misspell text.
  • Keep each scene to one idea and one or two sentences of narration.
  • Pick one voice and keep it for the whole video.
  • Keep the music at a volume between 0.1 and 0.15, so the narration stays clear.
Stuck? The forum usually answers within hours; support within 24 hours. Ask the forum Contact support