연간 연간 결제 시 모든 유료 요금제에서 2개월 무료. 요금제 보기
로그인 무료로 시작

ToRun MCP와 Remotion으로 만드는 내레이션 모션 설명 동영상

마지막 업데이트: 11 11월 2026

만들 수 있는 것

Claude Code, opencode, Cursor처럼 컴퓨터에서 명령을 실행하는 코딩 에이전트는 ToRun으로 내레이션이 들어간 짧은 설명 동영상을 만들 수 있습니다. 에이전트가 대본을 쓰고, ToRun에 장면 이미지, 내레이션, 배경 음악을 요청한 뒤, React 컴포넌트를 동영상으로 바꿔 주는 라이브러리인 Remotion으로 컴퓨터에서 동영상을 렌더링합니다.

필요한 것

  • ToRun MCP 서버에 연결되어 있고 명령을 실행하며 폴더에 파일을 쓸 수 있는 코딩 도구: opencode, Claude Code, Cursor.
  • Node.js 18 이상. 처음 렌더링할 때 Remotion이 렌더링에 쓰는 브라우저를 내려받습니다.
  • 유료 요금제의 사용량이나 지갑 잔액. API 호출은 무료 사용량을 쓰지 않습니다. 호출 비용 결제 방식을 참고하세요.

Remotion에는 자체 라이선스가 있습니다. 개인과 아주 작은 회사는 무료로 쓸 수 있고, 그보다 큰 회사는 회사 라이선스가 필요합니다. 업무에 쓰기 전에 remotion.dev/license를 확인하세요.

스타터 프로젝트

파일은 네 개입니다. 에이전트에게 빈 폴더에 만들어 달라고 하거나 직접 복사한 다음, 그 폴더에서 npm install을 실행하세요. 생성된 파일은 package.json 옆의 public 폴더에 들어갑니다.

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에는 동영상 전체가 담깁니다. 각 장면에는 화면에 표시되는 caption, 내레이션 텍스트 say, 이미지 prompt, 이미지와 내레이션의 파일 이름, 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는 장면을 차례로 재생합니다. 각 이미지가 천천히 확대되고, 자막이 아래에서 올라오며, 장면의 내레이션이 함께 시작되고, 음악은 동영상 내내 조용히 깔립니다.

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)}
  />
);

에이전트가 동영상을 만드는 방법

  1. 대본. 에이전트가 장면을 video.json에 씁니다. 장면마다 아이디어 하나, 몇 단어로 된 자막, 말할 내용 한두 문장입니다. 무엇이든 생성하기 전에 대본을 먼저 보여 달라고 하세요.

  2. 잔액. account_balance를 호출해 계정에서 쓸 수 있는 금액을 확인합니다. 이 호출은 무료입니다.

  3. 이미지. 장면마다 그 장면의 prompt와 size 1792x1024로 image_generate를 호출하고, 이미지를 public/scene-1.png, public/scene-2.png 같은 식으로 저장합니다. 대기 시간 안에 이미지가 준비되지 않으면 도구가 작업 ID를 돌려주고, 에이전트는 이미지가 준비될 때까지 job_status에 묻습니다.

  4. 내레이션. 장면마다 그 장면의 say 텍스트와 목소리(예: nova)로 tts_generate를 호출하고, 오디오를 public/scene-1.mp3 같은 식으로 저장합니다.

  5. 타이밍. 각 장면의 seconds를 내레이션 길이에 약 0.5초를 더한 값으로 정합니다. tts_generate는 오디오 파일에서 측정한 이 길이를 duration_seconds로 돌려줍니다. 응답에 duration_estimated: true도 있다면 길이를 텍스트로만 추정할 수 있었다는 뜻이므로 에이전트가 파일을 측정합니다. 파일을 직접 확인하려면:

    npx remotion ffprobe -v error -show_entries format=duration -of csv=p=0 public/scene-1.mp3
    
  6. 음악(선택). "calm instrumental background, soft piano" 같은 프롬프트, false로 설정한 with_vocals, 동영상 길이에 가까운 duration_seconds로 music_generate를 호출합니다. 도구가 작업 ID를 돌려주면 에이전트는 트랙이 준비될 때까지 job_status에 묻고, public/music.mp3로 저장합니다. 음악이 필요 없다면 video.json에서 music 줄을 지우세요.

  7. 미리 보기와 렌더링. npm run studio는 브라우저에서 미리 보기를 엽니다. npm run render는 out/explainer.mp4를 씁니다.

도구가 돌려주는 모든 파일 링크는 로그인 없이 내려받을 수 있습니다. 물음표 뒤의 부분까지 포함해 링크 전체를 보관하세요.

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

에이전트에게 요청하기

torun.ai/docs/mcp-motion-explainers의 Remotion 스타터 프로젝트로 우리 앱의 동기화가 어떻게 작동하는지 설명하는 30초짜리 설명 동영상을 만들어 줘. 장면은 네 개. 먼저 video.json에 있는 대본을 보여 줘. 그다음 장면마다 글자가 없는 이미지를 하나씩 생성하고, 장면마다 nova 목소리로 내레이션을 넣고, 잔잔한 연주 음악을 더하고, 각 장면 길이를 내레이션에 맞춘 다음 렌더링해 줘.

세로 동영상을 원하면 video.json에서 width를 1080, height를 1920으로 하고, 이미지는 size 1024x1792로 요청하세요.

비용

  • image_generate, tts_generate, music_generate는 유료 호출이며 앱과 같은 가격으로 Runs 단위로 청구됩니다. 음악이 들어간 4장면 동영상이라면 이미지 4장, 내레이션 4개, 트랙 1개입니다.
  • account_balance와 job_status는 무료입니다.
  • 렌더링은 컴퓨터에서 실행됩니다. 자막, 타이밍, 움직임을 바꿔 다시 렌더링해도 ToRun에서는 비용이 들지 않습니다. 이미지나 내레이션을 다시 생성하면 새 유료 호출이 됩니다.
  • 한 번의 생성이 키의 기준 금액보다 비싸면 도구가 먼저 가격을 알려 주고, 에이전트는 호출을 실행하기 전에 사용자에게 묻습니다.
  • 에이전트가 생성한 모든 것은 ToRun 라이브러리에도 저장됩니다.

팁

  • 글자가 없는 이미지를 요청하세요. 말은 자막이 전하고, 이미지 모델은 글자를 자주 틀리게 씁니다.
  • 장면마다 아이디어 하나와 내레이션 한두 문장만 담으세요.
  • 목소리를 하나 정해 동영상 전체에서 그대로 쓰세요.
  • 내레이션이 잘 들리도록 음악 볼륨은 0.1과 0.15 사이로 유지하세요.
막히셨나요? 포럼은 보통 몇 시간 안에, 지원팀은 24시간 안에 답변합니다. 포럼에 질문하기 지원팀에 문의