R RenderComp
remotion typescript react ai-agent component-library video-automation

AIエージェントが選べるRemotionライブラリの設計

執筆: RenderComp チーム 編集方針

Remotionの設計思想は一文で言い表せます。動画フレームは、Propsから決定論的に計算される純粋な関数の出力です。同じPropsを渡せば、何度レンダリングしても100フレーム目の画素は変わりません。この性質がRemotionをプログラマブルなビデオ制作に適したツールにしており、AIエージェントとの親和性も同じ原理から来ています。

LLMベースのエージェントがビデオ生成タスクを受けると、「どのコンポーネントを使うか」「各Propsに何を渡すか」「どう組み合わせるか」という三つの判断をコードとして出力します。エージェントはコンポーネントのPropsに値を渡すだけで、タイミング・アニメーション・合成を制御できます。この判断の精度を左右するのが、コンポーネントAPIの設計品質です。

人間の開発者であれば、IDEのオートコンプリートやソースコードを読んでPropsの意図を把握できます。エージェントが持つ手がかりは型定義とJSDocコメントのみです。コンポーネントライブラリがエージェントに対して「発見可能」かどうかは、この二点だけで決まります。


エージェントが踏む落とし穴

エージェントに汎用的なRemotionコードを生成させると、特定のパターンで繰り返し失敗します。最も多いのはタイミングのハードコードです。

// エージェントが生成しがちなコード
const MyText: React.FC<{ text: string }> = ({ text }) => {
  const frame = useCurrentFrame();
  // fpsを30と決め打ちしている
  const opacity = Math.min(frame / 30, 1);
  return <div style={{ opacity }}>{text}</div>;
};

このコンポーネントは30fpsの合成では正しく動きますが、60fpsの環境に組み込むとフェードインが2倍の時間をかけます。エージェントがuseVideoConfigを知らなかったわけではなく、コンポーネントのPropsにfps情報が含まれておらず、内部でfpsを取得すべきことが型から読み取れなかったのです。

もうひとつ頻出するのが、値域の誤解です。delay?: numberというPropsがあるとき、それがフレーム数なのかミリ秒なのかを型情報だけでは判断できません。エージェントは文脈から推測しますが、推測は外れます。


エージェント向けProps設計

エージェントが確実に使えるProps契約を設計するには、タイミングの正規化・値域の明示・不正な組み合わせの型レベルでの排除という三点を意識します。

フレーム数やfpsをPropsに直接持たせず、コンポーネント内部でuseVideoConfigから取得する設計にすると、エージェントはfpsを意識しなくてよくなります。TypeScriptの型システムは数値の範囲を表現できないため、JSDocの@rangeアノテーションで値域を補います。判別共用体を使って関連するパラメータのセットをまとめると、エージェントが矛盾した値を生成する余地を減らせます。

次のコードは、これらを適用したフェードイン・フェードアウトのラッパーコンポーネントです。

import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  Easing,
} from "remotion";

/**
 * 子要素をフェードイン・フェードアウトで包むラッパー。
 */
export type FadeWrapProps = {
  /** フェードインにかけるフレーム数。 @range [0, 60] @default 15 */
  fadeIn: number;
  /** フェードアウトにかけるフレーム数。 @range [0, 60] @default 15 */
  fadeOut: number;
  easing: "linear" | "ease-in" | "ease-out" | "ease-in-out";
  children: React.ReactNode;
};

const EASING_MAP = {
  linear: Easing.linear,
  "ease-in": Easing.in(Easing.quad),
  "ease-out": Easing.out(Easing.quad),
  "ease-in-out": Easing.inOut(Easing.quad),
} satisfies Record<FadeWrapProps["easing"], (t: number) => number>;

export const FadeWrap: React.FC<FadeWrapProps> = ({
  fadeIn,
  fadeOut,
  easing,
  children,
}) => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();

  // fadeIn=0 のとき入力配列に重複が生じるため、最低1フレームを確保する
  const safeIn = Math.max(fadeIn, 1);
  const safeOut = Math.max(fadeOut, 1);

  const opacity = interpolate(
    frame,
    [0, safeIn, durationInFrames - safeOut, durationInFrames],
    [0, 1, 1, 0],
    {
      easing: EASING_MAP[easing],
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
    }
  );

  return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;
};

satisfies演算子を使うことで、EASING_MAPのキーがFadeWrapProps["easing"]の列挙値と一致しているかをコンパイル時に検証しています。durationInFramesuseVideoConfigから取得しているため、このコンポーネントは任意のfpsと任意の長さのSequenceに正しく適応します。

エージェントが渡すべき値はフレーム数の整数と4択のeasingのみで、値域は@rangeアノテーションで明示されています。


コンポーネントレジストリ

複数のコンポーネントをエージェントが選択するためには、ライブラリ全体のインベントリが必要です。型定義ファイルを並べるだけでは不十分で、各コンポーネントが「何をするものか」「どの場面で使うか」をエージェントが参照できる形式で提供します。

// registry.ts

export type ComponentEntry = {
  /** コンポーネントの役割を一文で説明する */
  description: string;
  /** エージェントがコンポーネントを選ぶ際の用途キーワード */
  useCases: string[];
  /** Propsの例(エージェントが参照するデフォルト値) */
  exampleProps: Record<string, unknown>;
  /** このコンポーネントが要求するRemotionの最小バージョン */
  remotionMinVersion: string;
};

export const componentRegistry = {
  FadeWrap: {
    description:
      "子要素をフェードイン・フェードアウトで包むラッパー。テキスト・画像・任意のReactノードに使用できる。",
    useCases: ["タイトル登場", "シーン転換", "要素の退場"],
    exampleProps: { fadeIn: 15, fadeOut: 15, easing: "ease-out" },
    remotionMinVersion: "4.0.0",
  },
  SlideIn: {
    description:
      "指定した方向からスライドしながら登場するアニメーションコンポーネント。",
    useCases: ["リスト項目の登場", "テキストの強調", "サイドパネル"],
    exampleProps: { direction: "left", distancePx: 60, durationInFrames: 20, easing: "ease-out" },
    remotionMinVersion: "4.0.0",
  },
} satisfies Record<string, ComponentEntry>;

このレジストリをJSONとしてシリアライズすれば、エージェントのツール呼び出しの応答として渡せます。エージェントはuseCasesを参照して用途に合ったコンポーネントを選び、examplePropsをベースに値を調整します。


springアニメーションのラッパー設計

Remotionのspring関数はフレームとfpsを直接引数に取るため、エージェントが呼び出すと値域の扱いが難しくなります。ライブラリ側でラップし、エージェントに見せるAPIを物理的に意味のある変数に置き換えると、生成品質が上がります。

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

/**
 * springアニメーションの現在値(from〜to)を返すフック。
 *
 * @param stiffness - 物理的な硬さ。値が大きいほど素早く収束する。 @range [50, 400] @default 120
 * @param dampingRatio - 減衰比。1.0 以上でオーバーシュートなし。 @range [0.5, 2.0] @default 1.0
 * @param delay - アニメーション開始を遅らせるフレーム数。 @range [0, 120] @default 0
 * @param from - 開始値。 @default 0
 * @param to - 終了値。 @default 1
 */
export function useSpringValue({
  stiffness = 120,
  dampingRatio = 1.0,
  delay = 0,
  from = 0,
  to = 1,
}: {
  stiffness?: number;
  dampingRatio?: number;
  delay?: number;
  from?: number;
  to?: number;
} = {}) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // dampingRatio(減衰比 ζ)をRemotionのdamping係数に変換する
  // c = 2 * ζ * sqrt(k)(質量 m = 1 を仮定)
  const progress = spring({
    frame: Math.max(0, frame - delay),
    fps,
    config: {
      stiffness,
      damping: dampingRatio * 2 * Math.sqrt(stiffness),
    },
  });

  return from + progress * (to - from);
}

エージェントはこのフックを使うことで、fpsの値を意識せずにspringアニメーションを組み込めます。dampingRatio = 1.0が臨界減衰でオーバーシュートなし、0.7前後でほど良い弾力感が出ると伝えるだけで、エージェントは適切な値を選べます。@rangeアノテーションがその範囲をLLMのコンテキストに渡します。


Sequenceを使った合成パターン

複数のコンポーネントをタイムライン上に並べるとき、Sequenceのオフセットをハードコードせずに計算で求めるパターンが、エージェントにとって扱いやすいAPIを生み出します。

import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";

type TimelineItem = {
  component: React.ReactNode;
  /** 開始フレーム */
  from: number;
  /** 表示フレーム数。未指定の場合は合成の残り長さ */
  durationInFrames?: number;
};

/**
 * タイムラインアイテムの配列を受け取り、Sequenceとして配置するコンポーネント。
 */
export const Timeline: React.FC<{ items: TimelineItem[] }> = ({ items }) => {
  const { durationInFrames } = useVideoConfig();

  return (
    <AbsoluteFill>
      {items.map((item, i) => (
        <Sequence
          key={i}
          from={item.from}
          durationInFrames={
            item.durationInFrames ?? durationInFrames - item.from
          }
        >
          {item.component}
        </Sequence>
      ))}
    </AbsoluteFill>
  );
};

エージェントがこのTimelineコンポーネントを使う場合、渡すべきデータ構造はTimelineItem[]のみです。Sequenceのネスト構造を直接組み立てる必要がなく、エージェントが生成するコードは次のようになります。

<Timeline
  items={[
    { component: <TitleCard text="Chapter 1" />, from: 0, durationInFrames: 60 },
    {
      component: (
        <FadeWrap fadeIn={10} fadeOut={10} easing="ease-out">
          <BodyText text="..." />
        </FadeWrap>
      ),
      from: 50, // タイトルカードとオーバーラップ
    },
  ]}
/>

from: 50でタイトルカードと意図的にオーバーラップさせる表現も、エージェントが自然に記述できます。


自然言語メタデータとツール定義

エージェントが型定義を超えた情報を取得できるよう、各コンポーネントに静的なメタデータを付与するパターンが有効です。

FadeWrap.meta = {
  category: "transition",
  description: "子要素をフェードイン・フェードアウトで包む",
  agentTips: [
    "fadeIn + fadeOut の合計が durationInFrames を超えないようにする",
    "テキスト登場には easing: 'ease-out' が最も自然に見える",
    "静止画に使う場合、FadeWrapの外側ではなく内側にAbsoluteFillを置く",
  ],
} as const;

agentTipsフィールドは、型情報では伝えきれない実装上の制約をエージェントに渡すためのものです。型システムで強制するよりも自然言語で伝えた方が、エージェントの生成品質が向上するケースがあります。

エージェントが関数呼び出し形式でコンポーネントを操作する場合は、ZodスキーマをJSON Schemaに変換してLLMのツール定義にそのまま使えます。

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const FadeWrapSchema = z.object({
  fadeIn: z
    .number()
    .int()
    .min(0)
    .max(60)
    .default(15)
    .describe("フェードインのフレーム数"),
  fadeOut: z
    .number()
    .int()
    .min(0)
    .max(60)
    .default(15)
    .describe("フェードアウトのフレーム数"),
  easing: z
    .enum(["linear", "ease-in", "ease-out", "ease-in-out"])
    .default("ease-out")
    .describe("イージング関数の種別"),
});

// Anthropic Messages APIのtool定義として使える形式
export const fadeWrapTool = {
  name: "render_fade_wrap",
  description:
    "子要素をフェードイン・フェードアウトで包むコンポーネントのPropsを生成する。テキスト・画像の登場・退場シーンに使用する。",
  input_schema: zodToJsonSchema(FadeWrapSchema),
};

Zodスキーマが唯一の情報源として機能し、型定義・バリデーション・ツール定義の三点に自動的に展開されます。コンポーネントの仕様を変更したとき、Zodスキーマを更新するだけで三点が同期します。


まとめ

エージェントが使いやすいRemotionコンポーネントライブラリは、人間向けのものとは設計の重点が異なります。

タイミングの正規化とJSDocによる値域の明示は、エージェントが間違った値を渡す確率を下げます。コンポーネントレジストリとツール定義の統合は、エージェントがライブラリの全体像を把握してコンポーネントを選択できる基盤を作ります。agentTipsのような自然言語メタデータは、型システムが表現できない実装上の制約を補います。

既存のライブラリをエージェント対応に改修する場合は、JSDocと@rangeアノテーションの追加から始めると、コードを変更せずに改善の効果が得られます。RenderCompのテンプレートカタログに収録されているコンポーネントも、同様の設計原則に従ってAPIを整備しています。springのラッパーやレジストリパターンは、規模にかかわらずそのまま適用できます。

販売中

1,000以上のRemotionテンプレートを一括入手

買い切り(一括払い)・サブスクなし・生涯アップデート無料。TypeScript製。

料金プランを見る →