Felo API PlatformFelo API Platform

內容

用 HTML 和 Opus 5.5 做一支動態圖像影片

先寫 beat sheet,讓 Opus 5.5 透過 Felo API 寫一個確定性的 HTML 場景,再用 Playwright 逐格擷取、ffmpeg 編碼成 MP4。可以在 WorkBuddy、帶程式碼執行的 ChatGPT、Codex、Pi Agent 或 Claude Code 裡跑。

建置時間45 分鐘難度進階更新於2026-10-30
適合誰創作者動態設計師獨立開發者

複製 prompt

把方括號裡的欄位替換掉,然後貼到 Claude Code、Codex 或任何能存取終端機的 AI。它會安裝 Felo skill、在花額度前詢問,並執行這套工作流程。

製作一支關於[想法]的 [時長] 秒 [直式 / 橫式] 影片。
風格:[風格]。受眾:[受眾]。音訊:[無 / ./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;如果有音軌,再混流音訊。
6. 展示首幀、尾幀和最終檔案路徑。不要發布。

規則:
- 消耗額度前,先展示 Felo API 呼叫和預估 token 用量。
- 只使用我提供的文案,或明確標示為佔位文案。
- 不要使用受著作權保護的素材、音樂、標誌或字型。只用系統字型和生成的圖形。
- 除非我要求長片,最終影片控制在 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 呼叫
  1. 先寫 beat sheet

    先固定長度和 3-6 個 beat,每個 beat 寫開始/結束時間、螢幕文案和主要視覺變化。

  2. 用 Opus 5.5 生成 HTML 場景

    用 Anthropic SDK 指向 Felo 的 base URL,模型 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
  3. 檢查確定性並預覽關鍵格

    在 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
  4. 逐格渲染並編碼

    依 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
  5. 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
  6. 用同一模型迭代

    只描述出錯的影格和想要的修改,帶上目前 HTML 讓 Opus 改,先重跑預覽再重渲染。

包含什麼

  • 先寫好的分鏡和時間軸
  • 一段由 Opus 5.5 生成的確定性 HTML 場景
  • Playwright 逐格渲染腳本和 ffmpeg 編碼步驟
  • 帶可選音軌的最終 MP4
  • 逐格 review 的修正循環

不做什麼

  • Opus 5.5 輸出的是程式碼,不是影片格;真正擷取影格的是瀏覽器渲染器
  • 不需要 Claude 訂閱,但 Felo 模型按用量計費
  • 執行環境仍需 Node、Playwright Chromium 和 ffmpeg
  • 不允許外部素材和 CDN,所有視覺必須來自 HTML/CSS/SVG/Canvas 或本地檔案
  • 長影片需要拆場景;這不是即時錄影
  • 旁白和音樂是另外的步驟,工作流程只 mux 你提供的音訊

常見問題

Opus 5.5 會直接生成影片嗎?

不會。它輸出文字和程式碼,寫出 HTML 場景;瀏覽器和 ffmpeg 再把場景變成影格和 MP4。

需要 Claude 訂閱嗎?

不需要。用 Felo Key 透過 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 秒影片要花多少?

取決於 prompt、輸出 token 和迭代次數。Felo 模型按 token 計費;長渲染前先看即時價格。

可以用自己的音樂和旁白嗎?

可以,前提是你有權利。先渲染影片,再用 ffmpeg mux 授權音軌或你自己的 TTS;工作流程不生成旁白。

參考資料

這套工作流程參考的教學和專案。

相關工作流程

改成你的版本

同一套配方,依你實際的交付方式調整。

改用 HyperFrames 或 Remotion 專案

更長或更結構化的合成用這些框架;模型呼叫不變,只換渲染指令。

做方形社群版本

保留 beat sheet,改成 1080x1080,調整字級和安全區。

一次生成三個變體

同一份 brief 生成三個不同配色/節奏的場景,先預覽再只渲染最好的一個。

這套配方背後的參考頁面,以及下一步可以看什麼。