wth2jhl.online

Back

一个 HTML 文件,几个 data-start / data-duration 属性,一条 paused 状态的 GSAP timeline——这就是 HyperFrames 的全部”视频工程文件”。渲染时它从不播放视频,而是一帧一帧地问:“第 90 帧长什么样?“然后让 Chrome 用 HeadlessExperimental.beginFrame 原子地吐出那一帧的像素,交给 FFmpeg 编码成 MP4。

同样的输入,永远得到字节级相同的输出。这就是”确定性渲染”(deterministic rendering)——它让 CI 可以测试视频、让 AI Agent 可以放心地批量生成视频、让分布式集群可以把一条 timeline 切成 N 段并行渲染再无损拼回。

这就是 heygen-com/hyperframes ↗ 在做的事。HeyGen 把自己视频产品底层的合成与渲染引擎开源了出来,官方一句话定位:

“Write HTML. Render video. Built for agents.”

这篇文章会带你从核心理念、确定性渲染机制、媒体处理、分布式渲染管线、monorepo 架构、Agent Skills 教材体系、Catalog 生态、Studio/WebMCP,一路拆到这个 36 万行 TypeScript 项目的工程实践与短板。

HyperFrames 演示:左侧 HTML 代码变成右侧渲染出的视频

README 头图(原生保留):左边是 HTML 合成文件,右边是渲染出的 MP4 —— “HTML in, video out”


一、项目速览:定位、体量与采用者#

1.1 一句话定位#

HyperFrames = 把 HTML/CSS/媒体/可 seek 动画,编译成确定性 MP4 视频的开源渲染引擎 + 一整套教 AI Agent 做视频的技能教材。

三种使用姿势:

姿势入口适合谁
CLI 手动模式npx hyperframes init my-video → preview → render开发者本地做视频
Agent 技能模式npx skills add heygen-com/hyperframes,然后对 Claude Code / Cursor / Codex 说”帮我做个产品发布视频”AI 编程代理(官方主推)
嵌入式渲染核心@hyperframes/sdk + @hyperframes/player + 云渲染适配器把”HTML→视频”当成基础设施的产品方

1.2 体量数据(v0.8.31,2026-09-04 仍在高频提交)#

维度数字说明
仓库体积258 MB / 7122 文件clone 下来就能感受到”重”
业务源码364,083 行 TS/TSX(不含测试)14 个 package 的 monorepo
测试代码292,436 行,933 个测试文件测试/业务比接近 0.8:1
Agent Skills20 个已发布技能,41,575 行 Markdown文档量堪比一个中型代码库
官方文档26 万+ 行 mdx/md(含 catalog 文档)Mintlify 站点 hyperframes.heygen.com
Catalog 资产155 blocks + 220 components + 13 examplesnpx hyperframes add <name> 即装即用
版本节奏v0.8.31,PR 编号已到 #3750最近 50 个 commit 里 James Russo 占 33 个
运行要求Node.js ≥ 22 + FFmpeg,monorepo 用 Bun渲染依赖本机 Chrome headless shell

HyperFrames Logo

项目 logo(原生保留,来自仓库 assets/logo.png)

1.3 谁在生产环境用它(ADOPTERS.md 原表摘录)#

组织用法
HeyGen驱动其视频产品线全线 AI 视频合成与渲染(本项目就是从这里开源出来的)
tldraw自动生成 PR 讲解视频:GSAP 动画代码 diff + 旁白 + 字幕
TanStack探索短视频代码演示与文档视频
reap.videoAgent-first 的 AI 视频剪辑与渲染
Typeframe语音视频转 MP4,带逐词打字字幕
THU-MAIC(清华)OpenMAIC AI 互动课堂的一键 MP4 导出
PandaStudio桌面视频编辑器里的动效图形、动画字幕、叠加层

值得注意的两点:tldraw 用它做 PR→视频 的自动化管线(这正是官方 /pr-to-video 技能的来源场景);清华 THU-MAIC 用它做 AI 生成课堂的导出层。“HTML 当视频源文件”这条路已经有人在生产环境走通了。


二、核心理念:HTML 就是视频源文件#

2.1 与 Remotion 的正面对比(官方文档原生代码)#

视频圈的先行者 Remotion 用 React 组件描述视频;HyperFrames 用纯 HTML。官方文档 docs/guides/hyperframes-vs-remotion.mdx 直接放了两份”同题作文”——同一个 3 秒标题卡(淡入、停留、淡出),互不注水:

Remotion 版:组件是当前帧号的函数,时间数学自己算(以帧为单位):

HyperFrames 版:HTML 声明”什么东西什么时候在屏幕上”,GSAP timeline 声明”它怎么动”,全部在一个文件里(以秒为单位):

三条规则撑起整个渲染(官方原文 “Three rules make it render”):

  1. 每个可见时间槽带 class="clip" + 稳定 id + data-start / data-duration / data-track-index;
  2. timeline 必须以 paused 状态创建;
  3. timeline 必须注册到 window.__timelines,key 与根元素的 data-composition-id 一致——漏了这条,渲染出来就是一帧静止画面。

2.2 合成契约:时间线长在 HTML 属性上#

docs/concepts/data-attributes.mdx 定义了完整的”时间元素”契约:

属性控制什么
data-start元素何时进入合成时间线(秒,或引用其他 clip 的 id)
data-duration时间槽长度
data-track-indexStudio 里显示在第几条泳道(渲染时忽略——轨道不是图层,层叠用 CSS z-index)

一个很顺手的设计:data-start 可以写相对引用——

data-start="intro"          <!-- intro 结束时开始 -->
data-start="intro + 0.5"    <!-- intro 结束后 0.5s -->
data-start="intro - 0.5"    <!-- 与 intro 结尾重叠 0.5s(交叉淡化) -->
html

引用只在同一合成内解析、被引用 clip 必须有已知 duration、引用链不允许成环。剪辑软件里”吸附到上一段结尾”的操作,在这里是一行 HTML 属性。

合成还能嵌套:<div data-composition-id="intro-anim" data-composition-src="compositions/intro-anim.html">——框架会抓取文件、取出 <template> 内容、挂载、执行脚本、注册它自己的 timeline。路径永远相对项目根解析,嵌套多深都不用写 ../。

2.3 为什么”HTML 原生”对 Agent 如此重要#

README 里有一句非常直白的话(原生引用):

“Web animation tutorials are written for humans who watch a browser. Your agent isn’t producing that. It’s writing HTML that renders to MP4.”

(网页动画教程是写给”盯着浏览器看的人类”的;你的 Agent 不是在产出那个——它在写”会被渲染成 MP4 的 HTML”。)

网页动画的常识在视频渲染语境下全部失效:

网页常识视频渲染现实
requestAnimationFrame 驱动动画渲染时根本没有”下一帧回调”,只有 seekFrame(n)
transition 自动播放CSS 动画必须可 seek,靠 Web Animations API 定住时间
<video> 自己播自己的渲染时视频被逐帧预提取成图片(第四章)
Date.now() / Math.random() 随便用直接破坏确定性,lint 会抓
性能差就掉帧渲染是 seek 驱动,再贵的帧也不会掉,只是慢

LLM 的训练语料里全是”给人看的网页动画教程”,几乎没有”给渲染器看的可 seek HTML”。这就是 HyperFrames 要花 4 万多行 Markdown 写 20 个 Skills 的原因——它本质上是在给 Agent 补一套视频渲染的”世界观”(第七章细讲)。


三、确定性渲染:BeginFrame + seek 协议(全文最硬核)#

3.1 官方定义:帧号是唯一变量#

docs/concepts/determinism.mdx 开篇(原生引用):

“Rendering never plays your video. It asks for one frame at a time, and the answer to ‘what does frame 90 look like?’ depends on exactly one thing that changes: the number 90.”

文档里那张 mermaid 流程图翻译成 ASCII:

帧 90 ──────────────┐
                    ▼
fps/宽高/变量        t = 90 / fps        每个动画都被 seek      同样的像素,
在第 0 帧前锁定 ───▶ (整数运算,      ───▶ 到精确的 t       ───▶ 每一次运行都相同
                     绝不查时钟)
                                           ▲
                                           └╌╌ 破坏条件:帧内读 Date.now、
                                               未播种的 Math.random、渲染中途 fetch
plaintext

五条确定性军规(原文浓缩):

  • No wall clock. 没有 Date.now()、没有 requestAnimationFrame、没有系统定时器
  • No unseeded randomness. 未播种的 Math.random() 每次渲染都给出不同的帧
  • No fetching mid-render. 所有资产必须在第一帧之前加载完
  • Fixed output size. fps、width、height 在第 0 帧前锁死
  • A finite length. 每个合成必须有已知终点

每一帧的四步流水(官方 Steps 原文翻译):

  1. Frame clock:引擎用整数运算算时间 time = floor(frame) / fps,从不查询真实时间;
  2. Seek:帧适配器收到 seekFrame(frame),把所有动画、DOM 变更、canvas 绘制移动到那个精确时刻——所有 GSAP timeline 都是 paused + seeked,从不 play;
  3. Capture:Chrome 的 HeadlessExperimental.beginFrame 一次原子操作抓取像素,不存在画了一半的帧;
  4. Encode:FFmpeg 把帧序列编成 MP4,并混入 <audio> / <video> 元素的音轨。

3.2 深入源码:warmup ticks——把”宿主机速度”从变量里消除#

真正的工程深水区在 packages/engine/src/services/frameCapture.ts(4359 行)。它用 puppeteer-core 驱动 Chrome,在 window.__hf seek 协议之上实现了两条截帧路径:Chrome BeginFrame API(主路径)和 Page.captureScreenshot(回退路径)。

一个精妙细节:warmup 时钟锁定。页面加载期间,引擎用 BeginFrame 持续”预热”驱动渲染管线;问题是预热时长取决于宿主机加载页面的真实耗时——快机器 tick 少、慢机器 tick 多,导致后续 frameTimeTicks 不一致。解法(原生代码,frameCapture.ts:599-606):

/**
 * Fixed warmup-loop iteration count used when `CaptureOptions.lockWarmupTicks`
 * is `true`. Picked to roughly match the median tick count observed by the
 * unlocked wall-clock loop during a typical 2s page load at 30fps — so
 * `beginFrameTimeTicks` lands in a similar range regardless of host speed.
 */
export const LOCKED_WARMUP_TICKS = 60;
ts

锁定模式下 warmup 循环完全无视 state.running,跑满恰好 60 次迭代才退出——于是 beginFrameTimeTicks 在任何机器上都落在同一区间。第 0 帧的时间起点由一条纯算术推导链决定(frameCapture.ts:652-694):

const BEGIN_FRAME_CAPTURE_HEADROOM_INTERVALS = 10;
const BEGIN_FRAME_COMMIT_LEAD_INTERVALS = 6;
const BEGIN_FRAME_PROBE_LEAD_INTERVALS = 5;

export function deriveBeginFrameTimeTicks(
  state: WarmupTickState,
  warmupIntervalMs: number,
  captureIntervalMs: number,
): number {
  const legacyCaptureTimeTicks =
    (state.ticks + BEGIN_FRAME_CAPTURE_HEADROOM_INTERVALS) * captureIntervalMs;
  // ...当输出帧率快于 warmup 帧率时,走"单调不回退"的 fallback...
}
ts

capture / commit / probe 三个时刻各自领先第 0 帧固定的 interval 数(10/6/5),全部由 warmup tick 数推出——整条时间轴没有一处读墙上时钟。注释里连边界情况都写明白了:tick 抛错时迭代计数不前进,因为 “Chrome’s beginFrame is best-effort during page load — the page hasn’t installed CDP listeners yet.”

3.3 __renderReady 门闩:没准备好就不许截帧#

packages/core/src/runtime/init.ts(注入到每个合成页面的运行时核心,2800+ 行)里,截帧 readiness 是一个显式状态机(原生注释,init.ts:2754-2757):

// __renderReady = timeline binding attempted, safe for deterministic seeking.
// Set after any GSAP batching has completed. renderSeek works with or
// without a GSAP timeline (CSS/WAAPI/Lottie compositions use adapters only).
window.__renderReady = true;
ts

任何一环没就绪,门闩立刻拉回 false:外部子合成没加载完(externalCompositionsReady)、GSAP timeline 还在构建(__hfTimelinesBuilding,等 hf-timelines-built 事件)、任何一个帧适配器没 settle(isAdapterReadinessSettled())——引擎轮询 window.__renderReady,为 false 就绝不截帧。

如果根 timeline 压根没绑定,运行时会打一条极其”响”的警告(原生):

[hyperframes] Root timeline not bound — render will freeze at t=0.
Root data-composition-id is "launch" but window.__timelines has no such key.
Registered timeline keys: [...].
Register the root timeline under its data-composition-id
(window.__timelines["launch"] = tl).
plaintext

连”你该怎么修”都写在错误信息里——这是给 Agent 看的错误信息设计:Agent 读到这条就能自我修复,不需要人肉 debug。

3.4 帧适配器:动画运行时的统一抽象#

docs/concepts/frame-adapters.mdx 定义了 FrameAdapter 接口(原生):

import type { FrameAdapter, FrameAdapterContext } from "@hyperframes/core";

type FrameAdapter = {
  id: string;
  init?: (context: FrameAdapterContext) => Promise<void> | void;
  getDurationFrames: () => number;
  seekFrame: (frame: number) => Promise<void> | void;
  destroy?: () => Promise<void> | void;
};
ts

契约六条:返回有限非负帧数;支持前向/后向/随机 seek;同一帧号重复 seek 结果相同;不用墙上时钟和未播种随机数;异步工作必须在截帧前完成;destroy() 释放监听器。

GSAP、CSS keyframes、Anime.js、Lottie、Three.js、WAAPI、TypeGPU 七大动画运行时都已内置适配器(packages/core/src/runtime/adapters/),普通作者不需要碰这个接口——只有给自定义可 seek 动画对象做宿主时才实现它。官方明确标注这是 experimental v0 API,v1 前签名可能变。

3.5 预览与渲染的一致性(parity)#

官方 FAQ:“预览和渲染结果一致吗?“——每一帧都一致,因为两者跑同一个 hyperframe.runtime、producer 的 seek 行为是唯一真相源、__playerReady / __renderReady 双门闩保证合成完全加载后才截帧。但速度是两码事:预览受你的硬件限制实时播放;渲染是 seek 驱动逐帧进行——“A composition that stutters in preview still renders perfectly”,预览里卡成 PPT 的合成,渲染出来依然完美,只是慢。

本地渲染想跨机器字节级一致?官方答案是 Docker:

npx hyperframes render --docker --output output.mp4
bash

Docker 钉死 Chromium 版本、字体集、FFmpeg 编码器——“the platform stops being an input”(平台不再是一个输入变量)。


四、媒体的确定性:视频帧预提取、音频混流与时钟#

4.1 最关键的 trick:渲染时 <video> 根本不存在#

HTML 里的 <video> 是浏览器里最不可控的元素——解码时序、seek 精度、codec 支持全是坑。HyperFrames 的解法简单粗暴(packages/engine/src/services/videoFrameExtractor.ts 文件头注释,原生):

/**
 * Video Frame Extractor Service
 *
 * Pre-extracts video frames using FFmpeg for frame-accurate rendering.
 * Videos are replaced with <img> elements during capture.
 */
ts

渲染前用 FFmpeg 把合成里引用的每个视频逐帧预提取成图片序列(带内容寻址的提取缓存 extractionCache.ts,还有 GC 清扫机制),截帧时把 <video> 元素替换成指向对应帧图片的 <img>。于是”视频轨”在渲染器眼里退化成了最确定性的东西——静态图片。帧号 → 视频源帧的映射由 FrameLookupTable 承担,分布式 Plan V2 甚至直接在每个全局帧上求值这张表来推导 worker 需要的视频帧依赖(见第五章)。

配套的还有 mediaProxy.ts:按 codec 探测浏览器兼容性(服务端注入 window.__HF_MEDIA_CODEC_MAP__,含每个资产的 browserHostile 标记、canPlayType 用的代表性 MIME、alpha 通道检测),HEVC 之类浏览器不友好的源自动换 WebM/VP8 代理——预览里也用代理,保证所见即所得。

4.2 音频:FFmpeg 混流 + 效果链 + 自动化包络#

packages/engine/src/services/audioMixer.ts 把合成里的 <audio> / <video> 音轨交给 FFmpeg 处理,能力远超”混个音”:

  • 音量包络(audioVolumeEnvelope.ts):data-volume 之外支持随时间变化的自动化曲线;
  • 效果链(@hyperframes/core/audio-fx):EQ、压缩、限制、噪声门、饱和、延迟、混响、合唱、移相、bitcrush——挂在元素属性上声明;
  • 自动化(HF_AUDIO_AUTOMATION_ATTR):音量或任意效果参数随时间采样;
  • 子混音总线:<hf-audio-group> 让多条轨共享一条效果链、推子和自动化时钟;
  • voiceover carve:旁白出现时只在人声占据的频段对音乐床做 ducking,还带响度匹配(/hyperframes-audio 技能的核心场景)。

4.3 TransportClock:预览里”音频就是时钟”#

渲染时时间来自帧号整数运算;那预览播放时呢?packages/core/src/runtime/clock.ts 的 TransportClock 给了一个漂亮的答案(原生注释):

now(): number {
  // ...
  // Audio-master: when an audio source is attached, derive time
  // from it. Drift is impossible because audio IS the clock.
  if (this._audioSource) {
    // 从 HTMLMediaElement.currentTime 反推合成时间
    audioTime =
      ((el.currentTime - mediaStart) / (el.playbackRate > 0 ? el.playbackRate : 1)) *
        this._rate +
      compositionStart;
    // ...
  }
  // Monotonic fallback: performance.now() 差值
}
ts

有音频源时,时间直接由音频元素的 currentTime 反推——音频即时钟,漂移在物理上不可能发生(音画同步问题被连根拔掉);没有音频才回退到 performance.now() 单调钟。这是专业音视频播放器的经典手法(拿音频卡当主时钟),HyperFrames 把它做进了网页运行时。


五、渲染管线与分布式渲染:Plan V2 与 Lambda#

5.1 Producer 的六个 stage#

packages/producer/src/services/render/stages/ 把一次完整渲染切成清晰的阶段(文件名即架构):

compileStage        HTML 合成 → 编译(注入 runtime、解析变量、内联脚本处理)
extractVideosStage  预提取所有视频帧(内容寻址缓存)
captureStage        逐帧 seek + BeginFrame 截帧(另有 captureStreamingStage /
                    captureHdrStage 变体,HDR 有 hybrid/sequential 两种循环)
audioStage          FFmpeg 混流(pad/trim、包络、效果链、自动化)
encodeStage         帧序列 → 视频编码(含 GIF 分支)
assembleStage       音视频合流 → 最终 MP4
plaintext

外围还有一圈工程化设施:freezePlan.ts(渲染计划冻结与哈希)、planHash.ts(canonical JSON stringify + sha256)、artifactTransaction.ts(产物原子事务)、observability.ts / renderFileLogger.ts(渲染可观测性)、perfSummary.ts(性能汇总)、captureCost.ts(截帧成本预估 + heap advisory)。

5.2 分布式 Plan V2:内容寻址的渲染计划#

单机之外,长视频/批量渲染需要把 timeline 切片分发到多个 worker。packages/producer/src/services/distributed/planV2.ts 文件头把设计讲得很清楚(原生):

/**
 * Content-addressed distributed plan transport.
 *
 * V2 deliberately separates transport from execution. The transport root is
 * a small immutable `plan.json` manifest plus sha256-addressed blobs. Workers
 * select and materialize only the dependencies for their role, then invoke
 * the shared execution functions on the verified local layout.
 *
 * Video-frame dependencies are derived by evaluating the engine's own
 * FrameLookupTable at every captured global frame. If legacy video metadata
 * is absent, v2 explicitly falls back to the full-source pack.
 */
ts

拆开看三个设计决策:

  1. 传输与执行分离:plan.json 是不可变小清单 + sha256 寻址 blob 池;worker 只物化自己角色需要的依赖,然后调用与单机完全相同的执行函数(plan / renderChunk / assemble 是共享原语)——分布式不是另一套代码路径;
  2. 视频帧依赖精确到帧:不是把源视频整包发给每个 worker,而是在每个全局帧上求值 FrameLookupTable,推导该 chunk 真正用到的帧——带宽和存储都省;
  3. 幂等与完整性:projectHash.ts、crossWorkerIdempotency.test.ts、PlanV2IntegrityError(带 PLAN_V2_INTEGRITY_UNRECOVERABLE 分级)——worker 重跑、乱序、部分失败都有明确语义。

配套的 chunkBoundary.ts / planSize.ts / planSizeCap.ts 处理切片边界与清单体积上限,rebuildExtractedFrames.ts 处理帧缓存重建。

5.3 AWS Lambda 适配器:Step Functions 驱动的分段渲染#

packages/aws-lambda 的 README 里有张原生架构图:

三个值得抄的工程细节:

  • 一个函数三种角色:plan / renderChunk / assemble 共用同一个 Lambda handler,按 event.Action 分发——部署面最小,Step Functions 状态机只管编排;
  • 状态机历史预算:handler 返回的 JSON 被刻意压到 200 字节以内(原文 “fits inside Step Functions’ history budget (under 200 bytes per chunk)”)——大结果全走 S3,状态机只传引用,避免 history 膨胀炸掉执行;
  • 客户端 SDK + CDK L2 构造:renderToLambda / getRenderProgress / computeRenderCost 加上 HyperframesRenderStack,采纳方在自己的 CDK app 里一行拉起整套拓扑。

GCP 侧有对称的 packages/gcp-cloud-run(Cloud Run + Workflows + Terraform module),examples/ 里还给了 k8s-jobs 版本——分布式渲染的后端是可插拔的。CLI 入口:hyperframes lambda deploy / render / progress,或者 HeyGen 托管的 hyperframes cloud render。


六、Monorepo 架构地图:14 个 package 各管什么#

Bun workspace 组织的 14 个 package(src 下 TS/TSX 行数,含测试):

Package行数职责
studio242K浏览器端预览/编辑界面(React + Tailwind),含 WebMCP
cli112K50+ 命令:init / preview / render / lint / check / publish / cloud / lambda / tts / transcribe / remove-background…
core73K合成契约:解析、注入运行时(window.__hf)、七大动画适配器、变量、编译器、音频 FX
producer66K渲染编排:六 stage 管线 + 分布式 Plan V2
engine51K截帧引擎:Puppeteer + BeginFrame + FFmpeg(视频帧提取/音频混流/分块编码)
parsers28KHTML 合成的独立解析层
studio-server22KStudio 的后端服务
lint17K合成 lint 规则(composition / media / gsap / captions / fonts / textures / slideshow / adapters 八族)
sdk15K无头、框架中立的合成编辑引擎(session / patch / history / variables)
player13K<hyperframes-player> 嵌入式 Web Component,零依赖
aws-lambda6.6KLambda handler + 客户端 SDK + CDK 构造
gcp-cloud-run4.4KCloud Run handler + SDK + Terraform 模块
shader-transitions3.6KWebGL 片元着色器转场(domain-warp、light-leak…),GSAP 驱动
sdk-playground1.6KSDK 演练场

依赖方向大致是:

cli / studio / sdk / player          ← 用户与 Agent 的接触面
        │
        ▼
     producer                        ← 渲染编排(六 stage + 分布式)
        │
        ▼
      engine                         ← Puppeteer + BeginFrame + FFmpeg
        │
        ▼
       core                          ← 合成契约 + 注入运行时(唯一真相源)
        ▲
        │
  parsers / lint / shader-transitions / aws-lambda / gcp-cloud-run
plaintext

core 是所有路径的交汇点:预览(player/studio)和渲染(engine/producer)注入的是同一份 runtime——这就是”预览渲染一致性”的架构保证。hyperframe.runtime 由 core 构建产出(build:hyperframes-runtime),producer 的 htmlCompiler 在编译阶段把它内联进合成 HTML;连 hf-early-stub-inline(早期桩)都是 generated 目录里版本管理的产物。


七、Agent Skills:把”视频制作”写成教材#

这是 HyperFrames 最独特的部分,也是 “Built for agents” 的落点:20 个技能、41,575 行 Markdown,通过 skills.sh 生态分发(npx skills add heygen-com/hyperframes),兼容 Claude Code、Cursor、Gemini CLI、Codex。

7.1 三层技能结构#

7.2 路由器:一张优先级表决定一切#

skills/hyperframes/SKILL.md 的核心是一张按优先级匹配交付物的路由表(原生摘录):

PriorityRequestWorkflow
1Explicitly port an existing Remotion source/remotion-to-hyperframes
2Author a presentation, pitch deck, or navigable interactive deck/slideshow
3Add plain captions to existing talking-head footage without changing it/embedded-captions
4Add designed graphic overlays to existing talking-head footage/talking-head-recut
5Build a beat-synced video from a music track/music-to-video
6Short, unnarrated, motion-first unit, typically under 10s/motion-graphics
7Explain a GitHub pull request or code change from a PR reference/pr-to-video
8Market or showcase a website, product site, app, or company/product-launch-video
9Explain a topic with invented visuals, no product or site capture/faceless-explainer
10Any other custom video or composition/general-video

匹配的是交付物(deliverable),不是”顺嘴提到的词或文件类型”。路由器还内置了一套状态机:项目里有 BRIEF.md 就直接执行、有 hyperframes.json/STORYBOARD.md 就恢复上下文、全新请求才走 intent interview——问一次、写进 brief、之后所有环节只读 brief(原文 “The brief is the only routing artifact the workflow reads”)。这是把”需求澄清”做成了显式协议,防止 Agent 在长任务里漂移。

7.3 media-use:一个动词的”媒体操作系统”#

153 个文件的 /media-use 技能把视频制作里所有”找素材”的活收敛成一个动词 resolve(原生):

node <SKILL_DIR>/scripts/resolve.mjs --type <type> --intent "<description>" --project <dir>
bash

Returns one line: resolved <id> → <path> (<type>, <metadata>). All search noise stays on disk. (只返回一行结果;所有搜索噪音留在磁盘上,不进 Agent 上下文。)

Type解析成什么
bgm背景音乐(HeyGen 目录,1 万+ 曲目)
sfx音效(内置 19 文件库 + 目录)
image照片/背景(HeyGen 资产搜索,7.5 万+ 向量)
icon / logo图标/官方品牌标(svgl → simple-icons → GitHub avatar → favicon,绝不重画)
voiceTTS 配音(HeyGen 免费通道,可选本地 Kokoro)
grade / lut调色候选 / 可复用 .cube 文件

口号是 “resolve · generate · operate · remember — every media type, one skill, zero context noise”。最后那个词组是重点:技能设计者深知 Agent 的上下文窗口是稀缺资源,把搜索过程的噪音全部隔离在磁盘上,只让一行结论进入对话。这是 2026 年写 Agent 技能最值得抄的一条设计原则。

7.4 技能分发本身也被工程化了#

skills-manifest.json 给每个技能记了 hash 和文件数(embedded-captions 142 个文件、media-use 153 个);npx hyperframes skills update 只装核心集,创作工作流进入前才由路由器按需安装(“the router runs npx hyperframes skills update <workflow> before entering one”);仓库里还有 scripts/lint-skills.ts 和 check-skill-mirror.mjs 进 CI——技能文档当代码一样 lint、一样做一致性校验。连 Codex 插件打包都有专门命令(bun run package:codex-plugin,超过 Codex 100MB 上传限制直接 fail)。


八、Catalog 与 frame.md:素材生态与设计系统倒置#

8.1 Registry:shadcn 式的视频素材分发#

Catalog 走的是 shadcn/ui 的”代码归属你”路线——npx hyperframes add <name> 把 block/component 的源码直接装进你的项目:

npx hyperframes add flash-through-white   # shader transition(着色器转场)
npx hyperframes add instagram-follow      # social overlay(社交叠加层)
npx hyperframes add data-chart            # animated chart(动画图表)
bash

当前规模:155 个 blocks + 220 个 components + 13 个 examples。随手翻几个 block 名字就能感受到覆盖面:bar-chart-race(条形图竞赛)、code-diff / code-morph / code-particle-assemble(代码演示三件套)、camcorder-hud(摄像机 HUD)、chatgpt-exchange / claude-exchange(AI 对话重现)、cinematic-zoom、beat-freeze-cut(节拍冻帧切)……连 registry/components/texture-mask-text/masks/ 里都备好了树皮、大理石、金属等十几张纹理 mask。

registry/catalog-artifact/ 里还有 local-vectors.bin / local-vectors.json——catalog 的本地向量检索工件,Agent 搜素材时不用联网翻目录。

8.2 frame.md:把设计系统”为镜头倒置”#

这是 README 里最有意思的概念(原生引用):

“Every brand has a design.md. None of them were written for a camera. frame.md is the missing translation layer: it takes your web-context design spec and inverts it for the frame — the same tokens, the same rules, but rewritten so an AI agent can compose a promo video without guessing at scale or reaching for web chrome.”

(每个品牌都有 design.md,但没有一个是写给镜头看的。frame.md 就是缺失的翻译层:同一套 token、同一套规则,为画幅重写——让 AI Agent 不用猜比例、不会顺手把网页边框搬进视频。)

网页设计系统和视频设计系统的冲突是实打实的:网页字号 16px 起步、视频里 16px 等于看不清;网页靠滚动和 hover,视频只有时间和画幅;网页 chrome(导航栏、按钮态)进了视频就是噪音。frame.md 的输出是一份”DESIGN.md 超集”,官方配了成排的预置设计模板(hyperframes.dev/design),下面是仓库 README 原生引用的四套:

Biennale Yellow 设计模板

Biennale Yellow(原生保留,static.heygen.ai 官方图)

BlockFrame 设计模板

BlockFrame(原生保留)

Bold Poster 设计模板

Bold Poster(原生保留)

Cobalt Grid 设计模板

Cobalt Grid(原生保留)

README 里那句 slogan 也值得抄下来:“Atoms stay sacred. Composition stays free. Numbers come from the script.”(原子 token 神圣不可侵犯,组合方式自由,具体数值由脚本决定。)CLI 侧的 hyperframes claude-design 命令和 /hyperframes-creative 技能负责把 frame.md 落进项目。


九、Studio / SDK / Player / WebMCP:编辑与嵌入面#

9.1 SDK:无头编辑引擎#

@hyperframes/sdk(15K 行)自称 “Headless, framework-neutral HyperFrames composition editing engine”。核心抽象是 session:打开一个合成 → 拿 ElementHandle / SelectionProxy → 发 EditOp / JsonPatchOp → 订阅 PatchEvent → history.ts 管撤销重做 → persist-queue.ts 管落盘。变量系统是一等公民(StringVariable / NumberVariable / ColorVariable / BooleanVariable / EnumVariable / FontVariable / ImageVariable),合成在 HTML 上声明(原生):

<html
  data-composition-variables='[
  {"id":"title","type":"string","label":"Title","default":"Pro"},
  {"id":"accent","type":"color","label":"Accent color","default":"#6c5ce7"},
  {"id":"logo","type":"string","label":"Logo","default":"assets/logo.svg"}
]'
></html>
html

同一张”客户卡片”合成,换名字、换 logo、换主题色渲染 100 个版本,布局与动画纹丝不动——批量个性化视频(HeyGen 的主业)就架在这上面。测试文件名也很说明问题:session.render-faithful.test.ts(编辑后渲染忠实性)、session.subcomp.test.ts(子合成)、session.variables.test.ts。

9.2 Player:零依赖 Web Component#

<hyperframes-player src="./my-composition/index.html" controls></hyperframes-player>
html

沙箱 iframe 加载合成、自动探测尺寸和时长、响应式缩放。零依赖、任何框架可用、CDN 一行引入——把”视频”变成网页里一个可交互组件(能 seek、能暂停,而不是死的 MP4)。

9.3 WebMCP:让浏览器里的 Agent 直接开 Studio#

Studio 把自己的编辑能力注册成 WebMCP tools(packages/studio/src/webmcp/),浏览器里跑的 Agent 可以和人一起改同一个合成(官方文档原生示例):

studio_look                                      -> find the headline and copy its handle
studio_select    {"handle":"hf:abc123"}          -> share the target with the person in Studio
studio_inspect   {"handle":"hf:abc123"}          -> read its styles, text, and capabilities
studio_set_style {"handle":"hf:abc123",
                  "styles":{"color":"red"}}      -> write that source-backed element
studio_frame     {"time":2.4}                    -> capture the composition at 2.4 seconds
text

官方特别点出最后一个工具的价值:“It is what lets an agent judge a change instead of guessing at it.”——studio_frame 让 Agent 改完立刻截一帧看效果,形成”改→看→再改”的闭环。这和 CLI 侧的 hyperframes snapshot / hyperframes motion-shot(运动采样拼图)是同一个思想:给 Agent 装上眼睛。


十、值得抄的工程实践#

把这个仓库当”工程教材”看,有八条可以直接搬进自己的项目:

  1. 确定性当一等公民。“同输入同输出”不是口号,而是层层落地的机制:整数帧时钟、锁死 60 tick 的 warmup、__renderReady 门闩、视频帧预提取、Docker 钉死平台变量。做任何”AI 生成内容”的系统,可复现性都值得这个投入——它决定了你敢不敢让流水线无人值守。

  2. 预览与渲染共享同一运行时。parity 不靠”两边小心保持一致”,靠架构上只有一份 runtime(core 产出、htmlCompiler 内联)。任何”编辑态/生产态”分裂的系统都该抄这条。

  3. 给 Agent 的错误信息自带修复指令。“Root timeline not bound — render will freeze at t=0. Register the root timeline under its data-composition-id (window.__timelines[“launch”] = tl)“——现象、后果、修法一行给全。Agent 读到即自愈。

  4. 上下文噪音隔离。media-use 的 resolve 把搜索过程全部留在磁盘,只让一行结论进上下文。写 Agent 工具/技能时,“返回什么进上下文”要当成 API 设计问题对待。

  5. 技能文档进 CI。lint-skills、check-skill-mirror、skills-manifest 的 hash 校验——4 万行 Markdown 教材当代码维护,防止”文档腐烂”。

  6. 内容寻址的分布式计划。plan.json 小清单 + sha256 blob 池 + 按角色物化依赖,worker 执行路径与单机完全同源。比”把整个项目打包发给每台机器”优雅一个量级。

  7. 状态机历史预算意识。Lambda handler 返回值压到 200 字节内、大结果走 S3——用编排系统前先读它的计费/限额模型(Step Functions 按 history 事件收费)。

  8. 测试密度。933 个测试文件、29 万行测试代码,连 “warmup ticks 锁定后跨宿主机一致” 这种细节都有专门测试(frameCapture-warmupTicks.test.ts)。确定性渲染这种强承诺,只能靠这种密度兜底。

十一、局限与注意点#

  • 还没到 1.0:v0.8.31,FrameAdapter 明确标注 experimental v0 API,签名可能变;HTML schema 也在快速演进(最近 commit 还在改 slideshow 初始化时序)。
  • 依赖重:Node ≥ 22 + FFmpeg + 本地 Chrome headless shell;仓库 258MB,clone 和 CI 缓存都不轻松。渲染吃 CPU/内存,4K 长视频本地渲染要有耐心(官方专门写了 docs/guides/4k-rendering.mdx 和性能指南)。
  • 确定性的边界:不跑 Docker 时,字体和 Chrome 版本差异仍可能造成像素级偏移——官方自己也承认 “a local render can shift by a pixel from one machine to the next”。
  • 强绑 GSAP 生态:虽然支持七大动画运行时,但文档、技能、Studio 编辑能力都明显以 GSAP 为第一公民。
  • 媒体目录依赖 HeyGen 服务:media-use 的 BGM/图片/TTS 走 HeyGen 免费通道,离线或合规敏感场景要换成本地 Kokoro/自有素材。
  • bus factor 观感:最近 50 个 commit 里 33 个来自同一位作者(James Russo),开源治理还在早期。

十二、总结#

HyperFrames 表面是”HTML 转 MP4”的渲染器,拆开看是三样东西的合体:

  1. 一个确定性渲染引擎——BeginFrame 原子截帧 + 整数帧时钟 + 视频帧预提取 + 门闩式 readiness,把”浏览器这个混沌系统”驯服成可复现的帧生成器;
  2. 一套 Agent 教材——20 个技能、4 万行 Markdown、路由器 + 工作流 + 领域技能三层结构,把视频制作的领域知识翻译成 LLM 能执行的工作流;
  3. 一个内容基础设施——Registry 素材分发、frame.md 设计系统倒置、变量化批量渲染、Lambda/Cloud Run 分布式扩展。

它赌的方向很清晰:当视频的生产者从人变成 Agent,视频的源文件就该是 Agent 最擅长写的东西——HTML。Remotion 把视频写进 React,HyperFrames 把视频写回网页本身,再把”怎么给渲染器写网页”的隐性知识显性化成技能。tldraw 的 PR 视频管线和清华 OpenMAIC 的课堂导出说明这条路已经通了。

如果你的产品里有”程序化生成视频”的需求——批量个性化、文档转视频、Agent 自动剪辑——这个仓库值得完整读一遍;就算不用它,第三章的确定性机制和第七章的技能设计也值回票价。

参考链接#

深入解析 HyperFrames:HTML 网页如何被渲染成逐帧可复现的 MP4 视频
https://wth2jhl.online/blog/hyperframes-html-native-video-engine/
Author 小吴同学
Published at September 8, 2026