本記事はシニアエンジニア / アーキテクト / ソロデベロッパー向けに、Claude Code エージェントと Remotion を組み合わせた動画量産パイプライン「montage」(Claude Code エージェントと Remotion を組み合わせた動画コンテンツ量産パイプライン)の設計全体を公開する。ROI・コスト判断・経営視点は sister pillar 「動画コンテンツを AI で量産する経営判断と ROI」 に委ねる。ここではパイプライン DAG・Block Schema・Zod バリデーションという 3 つの設計要素を具体的な型定義とコードで解説する。
montage の骨格は researcher → scriptwriter → composer → reviewer という主要エージェント分業だ。各エージェントは厳密に型付けされた JSON を入出力し、Zod バリデーションが LLM 出力の品質ゲートとして機能する。最終的に Remotion の renderMedia が型安全な VideoSpec を受け取り、MP4 を書き出す。
Key takeaway 3 点:
- エージェント分業は「責任範囲の明確化」のためであり、単一エージェントでは障害局所化が難しいため 4 分割する
- ContentBlock 型(Block Schema)が Remotion コンポーネントとエージェント出力の唯一の接続点になる
- Zod バリデーションは「LLM が生成した JSON が Remotion に渡せるか」を判定するゲートであり、失敗時は差し戻しルールで上流エージェントに戻す
montage の全体アーキテクチャ — AI 動画生成 パイプライン 設計
4 エージェント(researcher / scriptwriter / composer / reviewer)の責任範囲
montage のパイプラインは config/pipelines/long-video.json として JSON で宣言されている。全 10 フェーズのうちエージェントが担う主要 4 工程を示す。
researcher ─→ analyst ─→ scriptwriter ─→ composer ─→ implement(skill) ─→ render(skill)
↑ │
reviewer ←──────────────────┘
(NG/WARN なら差し戻し)
各エージェントの責任範囲:
| エージェント | 入力型 | 出力型 | 責任 |
|---|---|---|---|
| researcher | ResearchInput | ResearchOutput | トピックに関するデータ収集(IR / 決算 / マクロ指標) |
| analyst | ResearchOutput | AnalystOutput | 収集データから洞察・ストーリーライン・強調数値を抽出 |
| scriptwriter | AnalystOutput | ScriptwriterOutput | シーン構成・ナレーション・字幕の決定 |
| composer | ScriptwriterOutput | ComposerOutput | 各シーンの Block Schema(映像構成)の設計 |
| reviewer | ImplementerOutput | ReviewResult | レンダリング後のスクリーンショットを品質チェック |
実際の long-video.json では researcher と scriptwriter の間に analyst(事実分析)、reviewer の後に thumbnailer / copywriter / record が続く 10+ フェーズ構成。本記事では設計思想を示す上で中核となる researcher / scriptwriter / composer / reviewer の 4 エージェントに絞って説明する。
DAG の設計思想 — なぜ逐次ではなく DAG か
montage のパイプラインは純粋な逐次実行ではなく DAG(有向非巡回グラフ)として設計されている。重要なのは reviewer が NG 判定を出したとき、原因に応じて上流の適切なエージェントに差し戻せる 構造を持つ点だ。
"retry": {
"rules": [
{ "cause": "scene-layout", "backTo": "composer", "description": "シーン構成・レイアウトの問題" },
{ "cause": "narration", "backTo": "scriptwriter", "description": "ナレーション不整合" },
{ "cause": "duration", "backTo": "implement", "description": "Duration 不足" },
{ "cause": "rendering-bug", "backTo": "system-developer", "description": "描画バグ(コード修正が必要)" }
],
"maxRetries": 3
}
「narration の問題」は scriptwriter に、「scene-layout の問題」は composer に戻す。この分岐があるため、パイプラインをフラットな if/else で書くよりも DAG として宣言的に表現する方が保守しやすい。
逐次実行のシンプルさを捨てて DAG にする理由は、失敗の原因が多様だからだ。動画品質の NG には「台本レベルの問題(scriptwriter)」「映像構成レベルの問題(composer)」「コードバグ(system-developer)」の 3 種類があり、それぞれ戻す先が異なる。単一の「retry して再実行」では原因に即した修正ができない。
各ノードの入出力の型定義
パイプラインの型定義は src/shared/types/pipeline.ts の PIPELINE_STAGE_TYPES に集約されている。
// src/shared/types/pipeline.ts
export const PIPELINE_STAGE_TYPES = {
researcher: {
input: "ResearchInput",
output: "ResearchOutput",
outputFile: "input.json",
},
scriptwriter: {
input: "AnalystOutput",
output: "ScriptwriterOutput",
outputFile: "scripted.json",
},
composer: {
input: "ScriptwriterOutput",
output: "ComposerOutput",
outputFile: "composed.json",
},
implement: {
input: "ComposerOutput",
output: "ImplementerOutput",
outputFile: "final.json",
},
reviewer: {
input: "ImplementerOutput",
output: "ReviewResult",
outputFile: "review/quality-score.json",
},
} as const;
各ステージの出力ファイルは output/{topic-id}/ 配下に配置される(input.json → analysis.json → scripted.json → composed.json → final.json)。エージェントは自分の outputFile に JSON を書き込み、次のエージェントがそのファイルを読む。ファイルシステムが「型付きキュー」として機能する設計だ。
Block Schema — Remotion コンポーネントとエージェントの接続
Block とは何か — Claude Code Remotion 動画生成における映像単位の抽象化
montage の映像階層は以下の通りだ:
Video → Chapter → Scene → Layer → Layout → Slot → ContentBlock
ContentBlock が最小の映像単位(原子的レンダリング単位)だ。1 つの ContentBlock は「バーチャートを描く」「テキストを表示する」「マップを描く」といった映像責務を 1 つ持つ。
Slot は Layout が分割した画面の一区画を指す。たとえば side-by-side レイアウトなら primary スロットと secondary スロットが存在し、各スロットに 1 つの ContentBlock を割り当てる。
Layer は Slot の集合で、z-index を持つ。background / content / overlay / ui の 4 タイプがあり、背景から前景へスタックされる。
Scene は Layer の集合で、ナレーションと字幕を持つ。1 シーン = 数秒〜十数秒の映像単位だ。
Chapter は Scene の集合で、テーマ的なまとまりを表す。1 本の動画は 1〜複数の Chapter から構成される。
この階層があることで、composer エージェントは「何を見せるか」を Layer・Slot・ContentBlock レベルで構造的に記述でき、Remotion コンポーネントはその構造を受け取って映像にレンダリングできる。
Block Schema の型定義(TypeScript + Zod)
ContentBlock の型定義は src/video/types/video.ts に集約されている:
// src/video/types/video.ts
export type ContentBlockType =
// Charts
| "bar-chart"
| "stacked-bar"
| "line-chart"
| "area-chart"
| "waterfall"
| "pie-chart"
// Data displays
| "metric-card"
| "number-highlight"
| "ranking-list"
| "data-table"
// Structured content
| "pros-cons-list"
| "timeline"
| "comparison"
// Text
| "text-block"
| "quote-block"
// Special
| "title-card"
| "outro-card"
// ... 計 58 種類
;
export type ContentBlock = {
id: string;
type: ContentBlockType;
data: Record<string, unknown>;
};
data フィールドが各ブロックタイプ固有のデータを持つ。bar-chart なら { items: [{label, value}], unit, valueFormat } といった形だ。
各ブロックタイプの data の形は src/video/types/block-data.ts の Zod スキーマで定義されている:
// src/video/types/block-data.ts(抜粋。実装には `showAxis` `sorted` などのフィールドも存在)
export const BarChartDataSchema = z.object({
title: z.string().optional(),
unit: z.string().optional(),
valueFormat: z.enum(["compact", "number", "jpy", "percent"]).optional(),
items: z.array(
z.object({
label: z.string(),
value: z.number(),
previousValue: z.number().optional(),
highlight: z.boolean().optional(),
color: z.string().nullable().optional(),
annotation: z.string().nullable().optional(),
})
).min(1),
variant: z.enum(["vertical", "horizontal"]).optional(),
showGrid: z.boolean().optional(),
referenceLines: z.array(ReferenceLineSchema).optional(),
});
この型定義が「composer エージェントが生成する JSON の仕様書」であり、「Remotion コンポーネントが期待するデータの契約」でもある。
エージェント出力 → Block Schema → Remotion コンポーネントのデータフロー
composer エージェント
└─→ composed.json(VideoSpec 形式)
└─→ implement(skill)
├─→ Zod バリデーション(validateBlockData)
├─→ 尺計算(ナレーション文字数 → 推定秒)
└─→ final.json(ImplementerOutput)
└─→ renderMedia(Remotion)
└─→ {topic-id}.mp4
composer が書き出す composed.json は VideoSpec 型に準拠する。implement スキルがこのファイルを読み込み、Zod バリデーションと尺計算を行って final.json を生成する。renderMedia は final.json を受け取ってレンダリングを実行する。
Zod バリデーション — エージェント出力の品質ゲート
なぜ LLM 出力に Zod バリデーションが必要か
LLM が生成する JSON は「それらしい」が「正しいとは限らない」。特に数値の桁や配列の要素数、列挙型の値は LLM が誤った値を生成しやすい。
montage では Remotion のレンダリング直前に validateBlockData が各 ContentBlock の data を Zod スキーマで検証する:
// src/video/types/block-data.ts
export function validateBlockData(
type: ContentBlockType,
data: unknown
): { success: boolean; error?: string } {
const schema = BLOCK_DATA_SCHEMAS[type];
if (!schema) return { success: true };
const result = schema.safeParse(data);
if (result.success) return { success: true };
return { success: false, error: result.error.message };
}
バリデーション対象のスキーマは BLOCK_DATA_SCHEMAS という辞書で管理する:
export const BLOCK_DATA_SCHEMAS: Partial<Record<ContentBlockType, z.ZodType>> = {
"bar-chart": BarChartDataSchema,
"line-chart": LineChartDataSchema,
"area-chart": AreaChartDataSchema,
"pie-chart": PieChartDataSchema,
"stacked-bar": StackedBarDataSchema,
// ... 60+ ブロックタイプ分
};
バリデーションが Remotion のレンダリング直前に置かれることで、「型が合わない JSON がレンダラーに届く」状況を防ぐ。
バリデーション失敗時のリトライ設計
Zod バリデーションが失敗した場合、implement スキルは ValidationResult を返す:
// src/shared/pipeline/types.ts
export type ValidationError = {
sceneId: string;
field: string;
message: string;
severity: "error" | "warning";
};
export type ValidationResult = {
valid: boolean;
errors: ValidationError[];
};
severity: "error" のエラーがある場合はパイプラインが止まる。パイプライン config の reviewer ステップの retry rules に従い、原因分類(scene-layout / narration / duration / rendering-bug)に応じて上流エージェントに差し戻す。
severity: "warning" のエラーは final.json に記録されるが、レンダリングは続行する。reviewer がスクリーンショットを確認したのち、品質スコアに応じて NG なら同じ差し戻しフローが走る。
エージェント分業 動画量産 設計における Zod スキーマの設計パターン
BarChartDataSchema に見られる設計パターンを整理する。
パターン 1: 必須フィールドと省略可能フィールドの明示
const BarChartDataSchema = z.object({
items: z.array(...).min(1), // 必須、最低 1 要素
title: z.string().optional(), // 省略可
valueFormat: z.enum([...]).optional(), // 省略可(省略時はデフォルト適用)
});
LLM はしばしば「本来省略可のフィールドを省略しない」か「本来必須のフィールドを省略する」誤りを犯す。スキーマを読めば LLM(composer エージェントのプロンプト)に「items は必須、title は省略可」と伝えられる。スキーマそのものがエージェントへの仕様書としても機能する。
パターン 2: 列挙型で有効値を制限する
valueFormat: z.enum(["compact", "number", "jpy", "percent"]).optional(),
variant: z.enum(["vertical", "horizontal"]).optional(),
z.enum で有効値を列挙することで、LLM が勝手に "percentage" や "yen" といった意味的に近いが無効な値を生成するのを防ぐ。
パターン 3: nullable と optional の使い分け
color: z.string().nullable().optional(),
annotation: z.string().nullable().optional(),
optional() は「フィールド自体がなくてもよい」、nullable() は「null 値を許容する」を意味する。両方付けることで undefined(キーなし)と null(明示的なリセット)の両方を許容する。Remotion コンポーネント側でこの区別を使い、null ならデフォルト色を適用する設計にできる。
4 エージェントの実装詳細
researcher — トピック調査と情報収集の設計
researcher は ResearchInput({ channelId, title, data } の形)を受け取り、ResearchOutput(input.json)を書き出す。データソースは config/profiles/scrape/ 以下のプロファイル JSON で宣言し、チャンネルごとに切り替えられる。株式チャンネル向けの equity プロファイルでは TDnet(適時開示)・企業 IR ページ・EDINET API・蓄積済みスプレッドシート・ニュース記事の 5 ソースを優先順位付きで定義している。
researcher の config は以下のように宣言される:
{
"phase": 1,
"type": "agent",
"name": "researcher",
"inputType": "ResearchInput",
"outputType": "ResearchOutput",
"config": {
"profiles": "config/profiles/scrape/",
"source": "brands/{channelId}.research.researchers"
},
"output": "input.json",
"intermediates": ["research-equity.json", "research-macro.json"],
"shared": true
}
shared: true は、同じ input.json を複数の後続エージェントが参照できることを示す。analyst も composer の途中フェーズも同じファイルを読む。
scriptwriter — 台本生成とナラティブ設計
scriptwriter は AnalystOutput(analysis.json)を受け取り、ScriptwriterOutput(scripted.json)を書き出す。
// src/shared/pipeline/types.ts
/** Scriptwriter output: VideoSpec with narration + subtitle filled in. */
export type ScriptwriterOutput = VideoSpec;
ScriptwriterOutput の実体は VideoSpec だ。ただし、このフェーズでは各 Scene の narration(ナレーションテキスト)と subtitle(字幕テキスト)が埋められているが、layers の中の ContentBlock の data は空か仮の値の可能性がある。「何を話すか」を scriptwriter が決め、「どう見せるか」は composer が決める分業だ。scriptwriter が台本の構造を決め、ContentBlock の具体的なデータは composer の責任範囲に委ねられる。
composer — Block Schema 生成と Remotion との接続
composer は montage パイプラインで最も複雑な判断をするエージェントだ。ScriptwriterOutput(scripted.json)を受け取り、各シーンの「どの ContentBlock をどの Slot に配置するか」を決め、ComposerOutput(composed.json)を書き出す。
composer の config には blockSchemas と layoutOptions への参照が含まれる:
{
"phase": 4,
"type": "agent",
"name": "composer",
"config": {
"template": "specs/channels/{channelId}/templates/{contentType}.json",
"directive": "brands/{channelId}.composer",
"blockSchemas": "specs/system/block-schemas.md",
"layoutOptions": "specs/system/layout-options.md"
},
"validation": {
"blockNames": "src/blocks/registry.ts",
"dataSchemas": "specs/system/block-schemas.md"
}
}
blockSchemas: "specs/system/block-schemas.md" は、composer エージェントが読む「Block の種類と各 Block の data フィールド仕様を記述した Markdown」だ。これが LLM への仕様書として機能し、Zod スキーマとの二重の型安全保証を形成する。
validation.blockNames は実装済みブロックのレジストリを指す。composer が存在しないブロックタイプを生成した場合、このバリデーションで早期検出できる。
reviewer — 品質チェックと差し戻し設計
reviewer は ImplementerOutput(final.json)と {topic-id}.mp4 を受け取り、スクリーンショットを目視確認して品質を判定する。
reviewer は判定結果として ReviewResult(review-result)を書き出す。NG 判定時は retry rules に従って差し戻す。
reviewer の特徴は 差し戻し先が動的に変わる 点だ。上述した retry rules を再掲する:
"rules": [
{ "cause": "scene-layout", "backTo": "composer" },
{ "cause": "narration", "backTo": "scriptwriter" },
{ "cause": "duration", "backTo": "implement" },
{ "cause": "rendering-bug", "backTo": "system-developer" },
{ "cause": "implementation-bug", "backTo": "system-developer" },
{ "cause": "brand-mismatch", "backTo": "composer" }
]
backTo: "system-developer" は特殊で、エージェントではなくコードを修正するべき問題を示す。コンポーネントの描画バグはプロンプトの再実行で解決しないため、エンジニアによるコード修正が必要と判定して人間に委譲する。
reviewer の postHook には record-review スキルが登録されており、品質スコアを reviews.jsonl に記録し、閾値超過で learning-loop を自動起動する設計になっている。
品質スコアの閾値は specs/quality/rubrics.json で定義されている。スコアは 1-5 の数値で、OK: 3.5(プロ品質以上)・WARN: 2.5(合格だが要改善)・NG: 0(再生成が必要)の 3 段階に分類される。reviewer が各ブロックの「データ可読性(0.30)・視覚バランス(0.25)・アニメーション品質(0.20)・データ正確性(0.25)」の 4 基準を重み付き平均でスコアリングし、閾値を下回ると retry rules が起動する。
Remotion との統合 — Remotion AI 自動化 パイプライン
Remotion の型定義とエージェント出力の型安全な接続
Remotion はコンポーネントの props を TypeScript で型付けできる。montage では VideoSpec 型がその props として機能する。
// src/video/types/video.ts(抜粋)
export type VideoSpec = {
meta: {
title: string;
theme: string;
format: FormatPreset;
fps: number;
width: number;
height: number;
estimatedDuration: number;
};
chapters: Chapter[];
transitions: TransitionType[];
};
Remotion の <Composition> コンポーネントは calculateMetadata で VideoSpec から尺を計算し、component に渡す。エージェント(composer)が書き出した composed.json → implement が処理した final.json → renderMedia の引数という経路全体が TypeScript の型で保護されている。
これが「エージェント出力と Remotion コンポーネントの型安全な接続」の実体だ。接続ポイントは VideoSpec という単一の型に集約されており、型が合わない JSON はバリデーションで弾かれてレンダラーに届かない。
renderMedia の呼び出し設計
render は implement スキルの後に実行される skill タイプのパイプラインステップだ:
{
"phase": 5,
"type": "skill",
"name": "render",
"inputType": "ImplementerOutput",
"outputType": "MP4",
"input": "final.json",
"output": "{topic-id}.mp4"
}
render スキルは npx remotion render CLI を実行する。--props で final.json のパスを渡し、src/Root.tsx を起点に Remotion が映像をレンダリングして output/{topic-id}/{topic-id}.mp4 を生成する。長尺動画では章単位で並列レンダリングし(render-by-chapter.ts)、その後 concat する設計になっている。
# render-by-chapter.ts が組み立てる Remotion CLI コマンドの例
npx remotion render \
src/Root.tsx \
<topic-id> \
--output=output/<topic-id>/chapters/<chapter-id>.mp4 \
--props='{"chapterIdFilter":"<chapter-id>"}' \
--frames=0-<durationInFrames-1>
レンダリング後の品質確認フロー
render スキルが MP4 を生成した後、reviewer エージェントがスクリーンショットを確認する。確認フローを示す:
render スキル
└─→ {topic-id}.mp4(出力)
└─→ reviewer エージェント
├─→ 各シーンのスクリーンショット取得
├─→ rubrics.json(品質評価ルーブリック)に照らして判定
└─→ ReviewResult(quality-score.json)
├─→ OK: thumbnailer / copywriter / record へ進む
└─→ NG: retry rules に従って差し戻し(最大 3 回)
reviewer の postHook は record-review スキルを呼び、quality-score.json を reviews.jsonl に追記する。これが将来的な learning-loop(品質改善フィードバック)の入力になる設計だ。
設計の判断記録 — なぜこの構造にしたか
4 エージェント分業にした理由(単一エージェントにしなかった理由)
単一エージェントに「リサーチから映像構成まで全部やれ」と指示することは技術的には可能だ。しかし 3 つの問題がある。
問題 1: コンテキスト汚染
1 本の動画を生成するために必要な情報量(決算データ・マクロ指標・チャンネル設定・ブロック仕様・ナレーション・字幕)は膨大だ。すべてを 1 つのエージェントの context に詰め込むと、重要な制約(「このブロックの items は最低 1 要素必要」など)をモデルが読み飛ばすリスクが高まる。
問題 2: 障害追跡が困難
「ナレーションが不整合」「バーチャートの数値が間違っている」「レイアウトが崩れている」——これら 3 種類の問題は原因が異なる。分業していれば「scriptwriter フェーズの問題か、composer フェーズの問題か」が特定できる。単一エージェントでは、出力の問題がどこに起因するかを追跡できない。
問題 3: スケールしない
複数の動画を並列生成する場合、researcher だけ先行して複数チャンネル分を実行し、その後 scriptwriter を並列起動する設計が取れる。batch エントリポイントと batch-research エントリポイントが存在するのはこのためだ。単一エージェントでは工程単位での並列化ができない。
Remotion を選んだ理由と代替手段との比較
Remotion を選んだ理由は主に 2 つだ。
理由 1: TypeScript で映像を記述できる
映像を React コンポーネントとして記述することで、エージェントが生成した JSON(VideoSpec)をコンポーネントの props として型安全に渡せる。ffmpeg + テンプレート文字列のアプローチでは、エージェント出力と映像の接続に型保証がない。
理由 2: コンポーネントの再利用と拡張
60+ 種類の ContentBlock は Remotion コンポーネントとして個別に実装されており、再利用可能だ。src/video/blocks/ は charts/・data/・structured/・text/・special/ の 5 カテゴリに分かれており、チャートだけで BarChartBlock・LineChartBlock・AreaChartBlock・StackedBarBlock・WaterfallBlock など 25 種類が実装されている。新しいブロックタイプを追加するときは、Zod スキーマを block-data.ts に追加し、コンポーネントを src/video/blocks/ に追加し、BLOCK_DATA_SCHEMAS に登録するという一定の手順がある。
Remotion を選んだ理由の一つは「映像の構造と TypeScript の型が一致する」点だ。ffmpeg を直接呼ぶアプローチでは映像レイアウトをパラメータ文字列で指定するため、エージェント出力と映像の接続に型保証がない。React コンポーネントを props に渡す Remotion のアプローチにより、VideoSpec が TypeScript の型として保護される構造が実現した。
Block Schema 設計の意思決定
Block Schema の設計で最も迷ったのは「どこまで型で固めるか」という問いだ。
data: Record<string, unknown> のようにすべて unknown にすれば柔軟だが、LLM が正しい形の JSON を生成できているか判断できなくなる。
逆に、すべてのフィールドを required にすると、LLM が省略可能なフィールドも埋めようとして不自然な値(たとえば annotation: "" のような空文字)を生成するリスクがある。
八雲が選んだ設計は「必須フィールドは strict、省略可フィールドは optional + 列挙型で有効値を制限」だ。これにより、LLM が生成する JSON はバリデーションを通る確率が高まり、かつ過剰な補完(必要のないフィールドを埋める)を防げる。
また、nullable() と optional() を使い分けることで、Remotion コンポーネント側で「フィールドなし → デフォルト値」「null → 明示的な無効化」を区別できる。この設計判断は実際の実装(BarChartDataSchema の color: z.string().nullable().optional())として型定義に反映されている。
まとめ — montage パイプライン実装チェックリスト
montage の設計を 3 つの観点で整理した。
パイプライン DAG: researcher → scriptwriter → composer → reviewer という 4 分業を JSON で宣言し、reviewer の差し戻しを原因分類付きで管理する。単一エージェントにしない理由は「コンテキスト汚染・障害追跡困難・並列化不可」の 3 点だ。
Block Schema: Video → Chapter → Scene → Layer → Slot → ContentBlock という階層がエージェントと Remotion コンポーネントの接続ポイントになる。ContentBlock の型定義(ContentBlockType + Zod スキーマ)がこの接続の型安全を保証する。
Zod バリデーション: validateBlockData が Remotion レンダリング直前に LLM 出力を検証する。バリデーション失敗は差し戻しルールで原因に応じた上流エージェントに戻す。
montage パイプライン 実装チェックリスト
config/pipelines/*.jsonでパイプラインの DAG を宣言するPIPELINE_STAGE_TYPESに各ステージの入出力型とファイル名を定義するContentBlockTypeに新しいブロックタイプを追加するsrc/video/blocks/に Remotion コンポーネントを追加するblock-data.tsに Zod スキーマを追加しBLOCK_DATA_SCHEMASに登録する- reviewer の retry rules に原因分類と差し戻し先を定義する
specs/system/block-schemas.mdを更新し composer エージェントが読む仕様書を最新に保つreviewersの rubrics.json に品質評価基準を定義する- implement スキルが ValidationResult を正しく返すことをテストで確認する
動画量産の経営判断・ROI・運用コストの観点は 「動画コンテンツを AI で量産する経営判断と ROI」(動画コンテンツを AI で量産する経営判断と ROI(business cluster sister pillar))を参照してほしい。