Geanimeerde uitlegvideo's met voice-over met ToRun MCP en Remotion
Laatst bijgewerkt op 11 nov 2026
Wat je kunt maken
Een coding agent die opdrachten op je computer uitvoert, zoals Claude Code, opencode of Cursor, kan met ToRun een korte uitlegvideo met voice-over maken. De agent schrijft het script, vraagt ToRun om de scèneafbeeldingen, de voice-over en de achtergrondmuziek, en rendert de video op je computer met Remotion, een bibliotheek die React-componenten omzet in video.
Wat je nodig hebt
- Een codingtool die verbonden is met de MCP-server van ToRun en in een map opdrachten kan uitvoeren en bestanden kan schrijven: opencode, Claude Code of Cursor.
- Node.js 18 of nieuwer. De eerste render downloadt de browser waarmee Remotion rendert.
- Het tegoed van een betaald abonnement of saldo in je wallet: API-aanroepen gebruiken nooit het gratis tegoed. Zie hoe aanroepen worden betaald.
Remotion heeft een eigen licentie: het is gratis voor particulieren en zeer kleine bedrijven, en grotere bedrijven hebben een bedrijfslicentie nodig. Bekijk remotion.dev/license voordat je het voor je werk gebruikt.
Het startproject
Vier bestanden. Vraag je agent om ze in een lege map te maken, of kopieer ze zelf, en voer daar daarna npm install uit. Gegenereerde bestanden komen in een map public naast 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 bevat de hele video. Elke scène heeft een caption die op het scherm verschijnt, de voice-overtekst say, de afbeeldings-prompt, de bestandsnamen van de afbeelding en de voice-over, en de lengte 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 speelt de scènes na elkaar af. Elke afbeelding zoomt langzaam in, het onderschrift schuift omhoog in beeld, de voice-over van de scène begint tegelijk en de muziek speelt zacht onder de hele 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)}
/>
);
Hoe de agent de video maakt
Script. De agent schrijft de scènes in
video.json: één idee per scène, een onderschrift van een paar woorden, een of twee zinnen om uit te spreken. Vraag hem je het script te laten zien voordat hij iets genereert.Saldo. Hij roept
account_balanceaan om te zien wat het account kan uitgeven. Deze aanroep is gratis.Afbeeldingen. Voor elke scène roept hij
image_generateaan met depromptvan de scène ensize1792x1024, en slaat hij de afbeelding op alspublic/scene-1.png,public/scene-2.pngenzovoort. Als een afbeelding niet binnen de wachttijd klaar is, geeft de tool een job-id terug, en de agent vraagtjob_statustot de afbeelding klaar is.Voice-over. Voor elke scène roept hij
tts_generateaan met desay-tekst van de scène en een stem, bijvoorbeeldnova, en slaat hij de audio op alspublic/scene-1.mp3enzovoort.Timing. Hij zet de
secondsvan elke scène op de lengte van de voice-over plus ongeveer een halve seconde.tts_generategeeft die lengte terug alsduration_seconds, gemeten aan het audiobestand. Staat er in het antwoord ookduration_estimated: true, dan kon de lengte alleen op basis van de tekst worden geschat, dus meet de agent het bestand. Zo controleer je een bestand zelf:npx remotion ffprobe -v error -show_entries format=duration -of csv=p=0 public/scene-1.mp3Muziek (optioneel). Hij roept
music_generateaan met een prompt zoals "calm instrumental background, soft piano",with_vocalsopfalseenduration_secondsin de buurt van de lengte van de video. De tool geeft een job-id terug; de agent vraagtjob_statustot het nummer klaar is en slaat het op alspublic/music.mp3. Zonder muziek verwijder je de regelmusicuitvideo.json.Voorbeeld en render.
npm run studioopent een voorbeeld in je browser.npm run renderschrijftout/explainer.mp4.
Elke bestandslink die een tool teruggeeft, kun je downloaden zonder in te loggen. Bewaar de hele link, inclusief het deel na het vraagteken:
curl -L -o public/scene-1.png "<the image link>"
Vraag het je agent
Maak een uitlegvideo van 30 seconden over hoe de synchronisatie van onze app werkt, met het Remotion-startproject van torun.ai/docs/mcp-motion-explainers. Vier scènes. Laat me eerst het script in video.json zien. Genereer daarna per scène een afbeelding zonder tekst erin, spreek elke scène in met de stem nova, voeg rustige instrumentale muziek toe, stem elke scène af op de voice-over en render.
Voor een staande video vraag je in video.json om width 1080 en height 1920, en om afbeeldingen met size 1024x1792.
Wat het kost
image_generate,tts_generateenmusic_generatezijn betaalde aanroepen, afgerekend in Runs tegen dezelfde prijzen als in de app. Een video met vier scènes en muziek bestaat uit vier afbeeldingen, vier voice-overs en één nummer.account_balanceenjob_statuszijn gratis.- Het renderen gebeurt op je computer. Onderschriften, timing of beweging aanpassen en opnieuw renderen kost niets bij ToRun; een afbeelding of voice-over opnieuw genereren is een nieuwe betaalde aanroep.
- Als één generatie meer zou kosten dan de drempel van de sleutel, antwoordt de tool eerst met de prijs en vraagt je agent het je voordat hij de aanroep uitvoert.
- Alles wat de agent genereert, wordt ook opgeslagen in je ToRun-bibliotheek.
Tips
- Vraag om afbeeldingen zonder tekst erin. Het onderschrift draagt de woorden, en beeldmodellen spellen tekst vaak verkeerd.
- Beperk elke scène tot één idee en een of twee zinnen voice-over.
- Kies één stem en houd die de hele video aan.
- Houd het muziekvolume tussen
0.1en0.15, zodat de voice-over duidelijk blijft.