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、在花额度前询问,并执行这套 workflow。

制作一条关于[想法]的 [时长] 秒 [竖版 / 横版] 视频。
风格:[风格]。受众:[受众]。音频:[无 / ./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 秒)

实际调用

给开发者:同一套 workflow 的可复制代码,按执行顺序排列。

展开实际 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 或本地文件
  • 长视频需要拆场景;这不是实时录屏
  • 旁白和音乐是单独步骤,workflow 只 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;workflow 不生成旁白。

参考资料

这套 workflow 参考的教程和项目。

相关 workflow

改成你的版本

同一套配方,按你实际的交付方式调整。

改用 HyperFrames 或 Remotion 项目

更长或更结构化的合成用这些框架;模型调用不变,只换渲染命令。

做方形社媒版本

保留 beat sheet,改成 1080x1080,调整字号和安全区。

一次生成三个变体

同一份 brief 生成三个不同配色/节奏的场景,先预览再只渲染最好的一个。

这套配方背后的参考页面,以及下一步可以看什么。