コンテンツ
HTML と Opus 5.5 でモーショングラフィックス動画を作る
ビートシートを書き、Opus 5.5 に Felo API 経由で決定論的な HTML シーンを書かせ、Playwright でフレームを取得して ffmpeg で MP4 にエンコードします。WorkBuddy、コード実行付き ChatGPT、Codex、Pi Agent、Claude Code で実行できます。
プロンプトをコピー
角括弧の項目を置き換えて、Claude Code、Codex、ターミナルにアクセスできる AI に貼り付けてください。Felo スキルをインストールし、クレジットを使う前に確認し、ワークフローを実行します。
[アイデア] について [duration] 秒の [縦 / 横] 動画を作成してください。 スタイル: [スタイル]。読者: [読者]。音声: [なし / ./audio.mp3 のライセンス済みトラック / TTS]。 あなたは [WorkBuddy / コード実行付き ChatGPT / Codex / Pi Agent / Claude Code] のターミナルがあるフォルダで実行しています。Node とヘッドレスブラウザを実行できない場合は、レンダリングしたふりをせず、停止して私に伝えてください。 次の順序で進めてください。 1. 正確な開始秒と終了秒を持つ3〜6ビートのストーリーボードを書く。 2. HTML ステップでは、Felo API 経由で Claude Opus 5.5 を呼び出す。モデル ID claude-opus-5-5 と私の FELO_API_KEY を使う。Claude のサブスクリプションや別の Anthropic キーを私に求めない。 3. Opus に、単一の自己完結型 HTML ファイルを依頼する。window.DURATION を設定し、async window.seek(t) を公開すること。外部アセット、CDN、Date.now()、requestAnimationFrame()、CSS トランジション、タイマーは禁止。すべての乱数値をシードし、同じ t が常に同じフレームを描くようにする。 4. t = 0、25%、50%、75%、最終フレームでプレビューする。すべてをレンダリングする前に、はみ出し、クリッピング、フォント、セーフエリアを確認する。 5. Playwright で全フレームをレンダリングし、ffmpeg で H.264 にエンコードし、音声トラックがあれば mux する。 6. 最初と最後のフレーム、最終ファイルパスを見せる。公開はしない。 ルール: - クレジットを消費する前に、Felo API 呼び出しと推定トークン量を示す。 - 私が渡したコピーだけを使うか、捏造したプレースホルダーコピーを明確に示す。 - 著作権のある映像、音楽、ロゴ、フォントを使わない。システムフォントと生成図形のみ。 - 私が長尺を求めない限り、最終動画は30秒未満にする。 - すべてを ./video/ に書き、実行したコマンドを要約する。
コピー内容にはインストール手順と安全ルールも含まれます。
完成するもの
実行時の出力例です。リアルタイム生成ではありません。
レンダーレポート
シーン: video/scene.html (18 KB) 長さ: 12秒 · 1080x1920 · 30fps · 360フレーム チェック: window.DURATION = 12 · seek(t) あり · Date.now / rAF / タイマーなし
プレビュー: preview-0ms.png · preview-3000ms.png · preview-6000ms.png · preview-9000ms.png エンコード: video/video.mp4 (H.264, 360フレーム) 音声: video/final.mp4 (AAC, 12.0秒)
実際の呼び出し
開発者向け:同じワークフローを実行順に並べたコピー可能なコード。
実際の API 呼び出しを表示
先にビートシートを書く
長さと3〜6個のビートを先に固定し、各ビートの開始・終了時刻、画面コピー、主な視覚変化を書きます。
Opus 5.5 で HTML シーンを生成する
Anthropic SDK の base URL を Felo に向け、モデル
claude-opus-5-5を使い、HTML のみを返すよう要求します。window.DURATIONとasync seek(t)を含めます。generate-scene.ts import { mkdir, writeFile } from "node:fs/promises"; import Anthropic from "@anthropic-ai/sdk"; const SYSTEM = `You write a single, self-contained HTML video scene. Requirements: - 1080x1920 (or 1920x1080), no external assets, system fonts only. - set window.DURATION = <seconds>. - expose async window.seek(t) that draws the frame for time t. - no Date.now(), no requestAnimationFrame(), no CSS transitions, no timers. - seed every random value; the same t must always produce the same frame. - return only the HTML file, no markdown fences.`; const brief = `12-second vertical motion graphic about [idea]. Beats: [0-3s hook], [3-7s point], [7-10s proof], [10-12s CTA]. Palette: [colors]. Type: system sans. Keep text inside a 64px safe area.`; const anthropic = new Anthropic({ apiKey: process.env.FELO_API_KEY, baseURL: "https://openapi.felo.ai/api", }); const message = await anthropic.messages.create({ model: "claude-opus-5-5", max_tokens: 32000, system: SYSTEM, messages: [{ role: "user", content: brief }], }); const raw = message.content .filter((block) => block.type === "text") .map((block) => block.text) .join(""); const html = raw.replace(/^```(?:html)?\s*/i, "").replace(/```\s*$/, "").trim(); await mkdir("video", { recursive: true }); await writeFile("video/scene.html", html); console.log(`scene.html: ${html.length} bytes`);出力例
scene.html: 18432 bytes
決定論を確認しキーフレームをプレビューする
t=0、25%、50%、75%、最終フレームでスクリーンショットを取り、文字のはみ出し、フォント、セーフエリアを確認します。
preview.ts import { chromium } from "playwright"; import { pathToFileURL } from "node:url"; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1080, height: 1920 }, deviceScaleFactor: 1, }); await page.goto(pathToFileURL("video/scene.html").href); await page.waitForFunction(() => typeof (window as any).seek === "function"); const duration = await page.evaluate(() => (window as any).DURATION as number); for (const t of [0, duration * 0.25, duration * 0.5, duration * 0.75]) { await page.evaluate(async (time) => { await (window as any).seek(time); }, t); await page.screenshot({ path: `video/preview-${Math.round(t * 1000)}ms.png` }); } await browser.close(); console.log(`duration ${duration}s`);出力例
duration 12s video/preview-0ms.png video/preview-3000ms.png video/preview-6000ms.png video/preview-9000ms.png
全フレームをレンダリングしてエンコードする
t = frame / fpsでseek(t)を呼び、スクリーンショットを撮り、ffmpeg で H.264 にエンコードします。render.ts import { spawn } from "node:child_process"; import { mkdir, rm } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import { chromium } from "playwright"; const FPS = 30; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1080, height: 1920 }, deviceScaleFactor: 1, }); await page.goto(pathToFileURL("video/scene.html").href); await page.waitForFunction(() => typeof (window as any).seek === "function"); const duration = await page.evaluate(() => (window as any).DURATION as number); const frames = Math.ceil(duration * FPS); await rm("video/frames", { recursive: true, force: true }); await mkdir("video/frames", { recursive: true }); for (let frame = 0; frame < frames; frame++) { await page.evaluate(async (time) => { await (window as any).seek(time); }, frame / FPS); await page.screenshot({ path: `video/frames/${String(frame).padStart(5, "0")}.png`, }); } await browser.close(); const ffmpeg = spawn("ffmpeg", [ "-y", "-framerate", String(FPS), "-i", "video/frames/%05d.png", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "video/video.mp4", ], { stdio: "inherit" }); await new Promise<void>((resolve, reject) => { ffmpeg.on("exit", (code) => { if (code === 0) resolve(); else reject(new Error(`ffmpeg exited with ${code}`)); }); }); console.log(`${frames} frames -> video/video.mp4`);出力例
360 frames -> video/video.mp4
音声を mux して最後まで見る
-c:v copyでライセンス済み音声を加え、長さと音量を確認し、最後まで通して見ます。mux.sh ffmpeg -i video/video.mp4 -i video/audio.wav \ -c:v copy -c:a aac -b:a 192k -shortest \ video/final.mp4 ffprobe -v error -show_entries format=duration \ -of default=nw=1:nk=1 video/final.mp4出力例
12.000000
同じモデルで反復する
失敗したフレームと希望する変更だけを伝え、現在の HTML と一緒に Opus に渡します。プレビューを再実行してから本番レンダリングします。
含まれるもの
- 先に作る分鏡とタイムライン
- Opus 5.5 が生成する決定論的 HTML シーン
- Playwright のフレームレンダリングスクリプトと ffmpeg エンコード
- 任意の音声トラック付き最終 MP4
- フレーム単位のレビューと修正ループ
やらないこと
- Opus 5.5 が出すのはコードであり、動画フレームではない。実際にフレームを取得するのはブラウザレンダラー
- Claude サブスクリプションは不要だが、Felo のモデル利用は従量制
- 実行環境には Node、Playwright Chromium、ffmpeg が必要
- 外部素材と CDN は不可。視覚は HTML/CSS/SVG/Canvas またはローカルファイルのみ
- 長い動画はシーン分割が必要。リアルタイム画面録画ではない
- ナレーションと音楽は別ステップ。ワークフローは提供された音声を mux するだけ
よくある質問
Opus 5.5 は直接動画を生成しますか?
いいえ。テキストとコードを出力し、HTML シーンを書きます。ブラウザと ffmpeg がシーンをフレームと MP4 に変えます。
Claude サブスクリプションは必要ですか?
不要です。Felo キーで Felo API 経由で `claude-opus-5-5` を呼び、従量制で利用します。別途 Claude サブスクリプションは要りません。
どの agent で実行できますか?
WorkBuddy、コード実行付き ChatGPT、Codex、Pi Agent、Claude Code、または Node と headless ブラウザを実行できる任意の agent です。
なぜシーンに seek(t) が必要ですか?
フレーム取得はリアルタイム再生ではありません。399 フレーム目を撮ってから 400 フレーム目を撮るまで時間が空くことがあるため、各フレームは t の純粋関数でなければなりません。
HyperFrames や Remotion を使えますか?
はい。長い合成や構造化されたプロジェクトに向いています。このレシピは単一 HTML ファイルに保ち、モデル呼び出しとレンダリングループを確認しやすくします。
12秒の動画の費用は?
プロンプト、出力トークン、反復回数によります。Felo モデルはトークン課金です。長いレンダリングの前に最新の料金を確認してください。
自分の音楽やナレーションを使えますか?
権利がある場合のみ使えます。先に動画をレンダリングし、ffmpeg でライセンス済み音声か自分の TTS を mux してください。ワークフローはナレーションを生成しません。
参考資料
このワークフローの参考にしたチュートリアルとプロジェクト。
関連ワークフロー
リサーチページをスライド資料に変える
ページを抽出し、LiveDoc の文脈として保存し、PPT Task API に deck を作らせ、完成したリンクが出るまでポーリングします。
ワークフローを開くコンテンツ製品ブリーフをランディングページに変える
読者を調査し、ブリーフを LiveDoc に保存し、その文脈から Landing Page タスクを作成し、ホストされたページが完成するまでポーリングします。
ワークフローを開くコンテンツ読者が実際に聞いている質問で1か月分のコンテンツを埋める
Chat でテーマを整理し、読者が X で聞いている質問を見つけ、参考動画の文字起こしからリアルな切り口を取り、すべての行に根拠を書いた4週間のコンテンツカレンダーを作ります。
ワークフローを開く自分の形に変える
同じレシピを、実際の進め方に合わせて調整します。
HyperFrames や Remotion プロジェクトに変える
長い合成や構造化された作品にはこれらのフレームワークを使います。モデル呼び出しは同じで、レンダリングコマンドだけ変わります。
正方形のSNS版を作る
ビートシートを保ち、1080x1080 にして文字サイズとセーフエリアを調整します。
1つのブリーフから3案を作る
同じブリーフで色やリズムの異なる3シーンを生成し、プレビュー後に最良の1つだけをレンダリングします。
さらに進む
このレシピの背景にあるリファレンスと、次に見るべきページ。