HyperFrames Registry 流水线可以把 Scene JSON 经过 Schema 和语义验证,编译为引用独立 Block Sub-composition 的 index.html,再通过 lint、check、snapshot 和 preview 验证,让 AI 主要负责选组件、填数据和编排时间。
这篇教程要搭建什么
上一篇《从 AI Coder 到 AI Layout Driver》介绍了数据驱动 HyperFrames 的总体架构。这一篇直接把架构落成一个最小流水线。
我们将创建:
- 一个可复用的
terminal-commandBlock - 一份 Block Registry 描述文件
- 一份 Scene JSON Schema
- 一个跨场景语义验证器
- 一个 Composition Builder
- 一套固定 Theme 和变量接口
- 一条从 JSON 到 HyperFrames Preview 的命令链
最终 Agent 只需要输出:
{
"scene_id": "03-install",
"block": "terminal-command",
"start": 6,
"duration": 5.5,
"track": 1,
"theme": "code-editorial",
"motion": "type-and-confirm",
"props": {
"command": "npm install hyperframes",
"output": "Installed successfully"
}
}
Builder 会把它转换为符合 HyperFrames 合约的 Host Composition。
最终项目结构
hf-video-factory/
hyperframes.json
package.json
frame.md
data/
video.json
audio-map.json
schemas/
video.schema.json
scene.schema.json
contracts/
terminal-command.json
registry/
blocks/
terminal-command/
terminal-command.html
registry-item.json
themes/
theme.css
scripts/
validate-scenes.mjs
build-composition.mjs
compositions/
index.html
terminal-command.html
assets/
fonts/
images/
audio/
video/
snapshots/
output/
为了方便理解,教程把 Registry 源文件和实际安装后的 Composition 都列出来:
registry/blocks/:组件库源文件compositions/:当前视频项目实际使用的 Block
如果只在一个项目内部使用,也可以直接维护 compositions/;当多个项目需要共享时,再独立托管 Registry。
第一步:先检查现有 Registry
不要在已有 Block 能满足需求时重复开发。
先搜索:
npx hyperframes catalog
npx hyperframes catalog --type block
npx hyperframes catalog --type component
npx hyperframes catalog --type block --tag title-card
npx hyperframes catalog --json
如果找到合适的 Block:
npx hyperframes add data-chart
默认安装位置由 hyperframes.json 控制:
{
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
"paths": {
"blocks": "compositions",
"components": "compositions/components",
"assets": "assets"
}
}
官方 Block 一般安装到:
compositions/<block-name>.html
Component 一般安装到:
compositions/components/<component-name>.html
这篇教程假设现有 Registry 没有符合我们视觉和变量要求的终端场景,因此创建一个自定义 Block。
第二步:定义 Block Contract
不要先写 HTML。先明确 Block 对外承诺什么。
创建:
contracts/terminal-command.json
内容:
{
"name": "terminal-command",
"version": "1.0.0",
"type": "hyperframes:block",
"description": "显示一条终端命令及执行结果",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": {
"min": 3,
"default": 6,
"max": 15
},
"themes": [
"code-editorial",
"terminal-green"
],
"motion_presets": [
"type-and-confirm",
"command-only"
],
"props": {
"eyebrow": {
"type": "string",
"required": false,
"maxLength": 32,
"default": "TERMINAL"
},
"command": {
"type": "string",
"required": true,
"maxLength": 88
},
"output": {
"type": "string",
"required": false,
"maxLength": 160,
"default": "Done"
}
}
}
Contract 的作用是让 Builder 和 Agent 都知道:
- Block 支持哪些字段
- 文字最长多少
- 时长允许多大范围
- 可以选择哪些主题
- 可以选择哪些 Motion Preset
- 支持什么画幅
没有 Contract 时,Agent 很容易传入 Block 根本无法显示的内容。
第三步:创建 Registry Item
HyperFrames Registry 中,一个 Block 的标准目录是:
registry/blocks/terminal-command/
terminal-command.html
registry-item.json
创建 registry-item.json:
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "terminal-command",
"type": "hyperframes:block",
"title": "Terminal Command",
"description": "A reusable terminal command scene for technical explainers",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 8,
"tags": [
"terminal",
"developer",
"tutorial",
"title-card"
],
"files": [
{
"path": "terminal-command.html",
"target": "compositions/terminal-command.html",
"type": "hyperframes:composition"
}
]
}
registry-item.json 描述的是 Registry 如何发现和安装文件;前面的 Block Contract 则描述我们的业务系统如何验证 Props、Theme 和 Motion。两者职责不同。
第四步:编写独立 Terminal Block
创建:
registry/blocks/terminal-command/terminal-command.html
由于它会作为 Sub-composition 加载,Composition 根必须放在 <template> 内,相关样式和脚本也必须位于模板内。
<!doctype html>
<html
lang="zh-CN"
data-composition-variables='[
{
"id": "eyebrow",
"type": "string",
"label": "Eyebrow",
"default": "TERMINAL",
"maxLength": 32
},
{
"id": "command",
"type": "string",
"label": "Command",
"default": "npm install hyperframes",
"maxLength": 88
},
{
"id": "output",
"type": "string",
"label": "Output",
"default": "Installed successfully",
"maxLength": 160
},
{
"id": "theme",
"type": "enum",
"label": "Theme",
"default": "code-editorial",
"options": [
{
"value": "code-editorial",
"label": "Code Editorial"
},
{
"value": "terminal-green",
"label": "Terminal Green"
}
]
},
{
"id": "motion",
"type": "enum",
"label": "Motion",
"default": "type-and-confirm",
"options": [
{
"value": "type-and-confirm",
"label": "Type and Confirm"
},
{
"value": "command-only",
"label": "Command Only"
}
]
}
]'
>
<body>
<template>
<style>
*, *::before, *::after {
box-sizing: border-box;
}
#tc-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
color: #f6f7fb;
font-family: Inter, system-ui, sans-serif;
}
#tc-background {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 72% 18%, rgba(255, 107, 74, 0.18), transparent 36%),
linear-gradient(145deg, #12131c, #090a10 70%);
}
#tc-stage {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 96px;
}
#tc-terminal {
width: 1420px;
min-height: 590px;
border: 2px solid rgba(255, 255, 255, 0.12);
border-radius: 34px;
background: rgba(26, 28, 41, 0.96);
box-shadow: 0 42px 120px rgba(0, 0, 0, 0.42);
overflow: hidden;
}
#tc-header {
display: flex;
align-items: center;
gap: 16px;
height: 92px;
padding: 0 34px;
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}
.tc-dot {
width: 20px;
height: 20px;
border-radius: 50%;
}
.tc-dot-red { background: #ff5f57; }
.tc-dot-yellow { background: #febc2e; }
.tc-dot-green { background: #28c840; }
#tc-eyebrow {
margin-left: auto;
color: rgba(246, 247, 251, 0.55);
font-size: 24px;
font-weight: 700;
letter-spacing: 0.16em;
}
#tc-body {
padding: 76px 76px 84px;
font-family: "Fira Code", ui-monospace, monospace;
}
#tc-command-line,
#tc-output-line {
margin: 0;
font-size: 48px;
line-height: 1.45;
}
#tc-prompt {
color: #ff6b4a;
}
#tc-command {
color: #ffffff;
}
#tc-output-line {
margin-top: 52px;
color: #aeb3c6;
}
#tc-cursor {
display: inline-block;
width: 22px;
height: 52px;
margin-left: 12px;
background: #ff6b4a;
vertical-align: -9px;
}
#tc-root[data-theme="terminal-green"] #tc-background {
background:
radial-gradient(circle at 72% 18%, rgba(72, 247, 178, 0.18), transparent 36%),
linear-gradient(145deg, #07110f, #030706 70%);
}
#tc-root[data-theme="terminal-green"] #tc-prompt,
#tc-root[data-theme="terminal-green"] #tc-cursor {
color: #48f7b2;
background: #48f7b2;
}
</style>
<div
id="tc-root"
data-composition-id="terminal-command"
data-start="0"
data-width="1920"
data-height="1080"
data-duration="8"
>
<div
id="tc-background-clip"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
>
<div id="tc-background"></div>
</div>
<div
id="tc-content-clip"
class="clip"
data-start="0"
data-duration="8"
data-track-index="1"
>
<div id="tc-stage">
<div id="tc-terminal">
<div id="tc-header">
<span class="tc-dot tc-dot-red"></span>
<span class="tc-dot tc-dot-yellow"></span>
<span class="tc-dot tc-dot-green"></span>
<span id="tc-eyebrow" data-var-text="eyebrow">TERMINAL</span>
</div>
<div id="tc-body">
<p id="tc-command-line">
<span id="tc-prompt">$</span>
<span id="tc-command"></span>
<span id="tc-cursor"></span>
</p>
<p id="tc-output-line" data-var-text="output">
Installed successfully
</p>
</div>
</div>
</div>
</div>
</div>
<script src="../vendor/gsap.min.js"></script>
<script>
(function () {
window.__timelines = window.__timelines || {};
const variables = window.__hyperframes.getVariables();
const root = document.getElementById("tc-root");
const commandEl = document.getElementById("tc-command");
const outputEl = document.getElementById("tc-output-line");
const cursorEl = document.getElementById("tc-cursor");
root.dataset.theme = variables.theme;
const command = variables.command || "";
const duration = 8;
const typeStart = 1.05;
const typeDuration = Math.min(2.6, Math.max(0.8, command.length * 0.045));
const tl = gsap.timeline({ paused: true });
tl.from("#tc-terminal", {
y: 70,
opacity: 0,
scale: 0.97,
duration: 0.75,
ease: "power3.out"
}, 0.25);
const steps = Math.max(1, command.length);
for (let index = 1; index <= steps; index += 1) {
const progress = index / steps;
tl.set(commandEl, {
textContent: command.slice(0, index)
}, typeStart + progress * typeDuration);
}
tl.set(outputEl, { opacity: 0, y: 24 }, 0);
if (variables.motion === "type-and-confirm") {
tl.to(outputEl, {
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out"
}, typeStart + typeDuration + 0.35);
}
tl.set(cursorEl, { opacity: 0 }, duration - 0.8);
window.__timelines["terminal-command"] = tl;
})();
</script>
</template>
</body>
</html>
这份 Block 的关键点
- Sub-composition 内容放在
<template>内 - Composition ID 与
window.__timelineskey 一致 - 所有 ID 使用
tc-前缀,降低组装冲突 - 根元素有确定尺寸和时长
- 背景放在全画幅子元素上
- Timeline 使用
{ paused: true } - Timeline 同步创建
- 命令打字使用预计算的有限步骤,不依赖无限 Cursor 动画
- Theme 和 Motion 使用枚举变量
- 直接文本替换使用
data-var-text - 复杂逻辑只在初始化时读取一次变量
示例假设 GSAP 已按项目约定保存到对应本地路径。真实项目应沿用脚手架和已安装 Block 的依赖方式,不要在正式渲染时依赖临时远程网络。
第五步:定义 Scene Schema
创建:
schemas/scene.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/schemas/scene.schema.json",
"type": "object",
"required": [
"scene_id",
"block",
"block_version",
"start",
"duration",
"track",
"theme",
"motion",
"props"
],
"properties": {
"scene_id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"block": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"block_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"start": {
"type": "number",
"minimum": 0
},
"duration": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 60
},
"track": {
"type": "integer",
"minimum": 0,
"maximum": 20
},
"theme": {
"enum": [
"code-editorial",
"terminal-green"
]
},
"motion": {
"enum": [
"type-and-confirm",
"command-only"
]
},
"props": {
"type": "object",
"required": ["command"],
"properties": {
"eyebrow": {
"type": "string",
"maxLength": 32
},
"command": {
"type": "string",
"minLength": 1,
"maxLength": 88
},
"output": {
"type": "string",
"maxLength": 160
}
},
"additionalProperties": false
},
"audio": {
"type": "object",
"properties": {
"cue": {
"type": "string"
},
"sync": {
"enum": ["scene", "sentence", "word"]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
为什么 Scene Schema 还写了具体 Props
实际系统可以采用两种方式:
- 所有 Block 共用一个大型 Scene Schema
- 通用 Scene Schema + 每个 Block 自己的 Props Schema
Block 增多后,第二种更容易维护:
scene.schema.json
blocks/
terminal-command.schema.json
big-number.schema.json
process-flow.schema.json
Validator 先验证 Scene 通用字段,再根据 block 加载对应 Props Schema。
第六步:定义整条视频数据
创建:
data/video.json
{
"schema_version": "1.0",
"composition": {
"id": "main",
"width": 1920,
"height": 1080,
"duration": 15,
"fps": 30
},
"scenes": [
{
"scene_id": "01-install",
"block": "terminal-command",
"block_version": "1.0.0",
"start": 0,
"duration": 7,
"track": 1,
"theme": "code-editorial",
"motion": "type-and-confirm",
"props": {
"eyebrow": "INSTALL",
"command": "npm install hyperframes",
"output": "Installed successfully"
}
},
{
"scene_id": "02-render",
"block": "terminal-command",
"block_version": "1.0.0",
"start": 7,
"duration": 8,
"track": 1,
"theme": "terminal-green",
"motion": "type-and-confirm",
"props": {
"eyebrow": "RENDER",
"command": "npx hyperframes render --output out.mp4",
"output": "Video written to out.mp4"
}
}
]
}
同一个 Block 被实例化两次,只替换数据、主题和时间。
第七步:安装 Schema Validator
可以使用 Ajv:
npm install -D ajv
创建:
scripts/validate-scenes.mjs
import fs from "node:fs";
import path from "node:path";
import Ajv from "ajv";
const root = process.cwd();
function readJson(relativePath) {
return JSON.parse(
fs.readFileSync(path.join(root, relativePath), "utf8")
);
}
const video = readJson("data/video.json");
const sceneSchema = readJson("schemas/scene.schema.json");
const contract = readJson("contracts/terminal-command.json");
const ajv = new Ajv({ allErrors: true, strict: true });
const validateScene = ajv.compile(sceneSchema);
const errors = [];
const seenSceneIds = new Set();
for (const scene of video.scenes) {
if (!validateScene(scene)) {
errors.push({
scene: scene.scene_id,
type: "schema",
details: validateScene.errors
});
continue;
}
if (seenSceneIds.has(scene.scene_id)) {
errors.push({
scene: scene.scene_id,
type: "duplicate-scene-id"
});
}
seenSceneIds.add(scene.scene_id);
if (scene.block !== contract.name) {
errors.push({
scene: scene.scene_id,
type: "unknown-block",
value: scene.block
});
}
if (scene.block_version !== contract.version) {
errors.push({
scene: scene.scene_id,
type: "block-version-mismatch",
expected: contract.version,
actual: scene.block_version
});
}
if (
scene.duration < contract.duration.min ||
scene.duration > contract.duration.max
) {
errors.push({
scene: scene.scene_id,
type: "duration-out-of-range"
});
}
if (scene.start + scene.duration > video.composition.duration) {
errors.push({
scene: scene.scene_id,
type: "scene-exceeds-composition"
});
}
}
for (let i = 0; i < video.scenes.length; i += 1) {
for (let j = i + 1; j < video.scenes.length; j += 1) {
const a = video.scenes[i];
const b = video.scenes[j];
if (a.track !== b.track) continue;
const overlaps =
a.start < b.start + b.duration &&
b.start < a.start + a.duration;
if (overlaps) {
errors.push({
type: "same-track-overlap",
scenes: [a.scene_id, b.scene_id]
});
}
}
}
if (errors.length > 0) {
console.error(JSON.stringify({ ok: false, errors }, null, 2));
process.exit(1);
}
console.log(JSON.stringify({
ok: true,
scenes: video.scenes.length,
duration: video.composition.duration
}, null, 2));
真实项目还应该检查:
- Registry 中是否存在 Block 文件
- 媒体文件是否存在
- Theme 和 Motion 是否同时被 Contract 支持
- Props 长度是否符合 Block Contract
- Audio Cue 是否存在
- 同轨重叠是否明确允许
- Composition ID 是否会冲突
- 当前画幅是否被 Block 支持
第八步:编译 Composition
创建:
scripts/build-composition.mjs
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const video = JSON.parse(
fs.readFileSync(path.join(root, "data/video.json"), "utf8")
);
function escapeAttribute(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("'", "'")
.replaceAll('"', """)
.replaceAll("<", "<")
.replaceAll(">", ">");
}
function sceneHost(scene) {
const variableValues = {
...scene.props,
theme: scene.theme,
motion: scene.motion
};
return `
<div
data-composition-id="${escapeAttribute(scene.block)}"
data-composition-src="./${escapeAttribute(scene.block)}.html"
data-start="${scene.start}"
data-duration="${scene.duration}"
data-track-index="${scene.track}"
data-width="${video.composition.width}"
data-height="${video.composition.height}"
data-variable-values='${escapeAttribute(JSON.stringify(variableValues))}'
></div>`;
}
const html = `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=${video.composition.width}, height=${video.composition.height}"
/>
<title>Generated HyperFrames Composition</title>
<style>
html, body {
margin: 0;
width: ${video.composition.width}px;
height: ${video.composition.height}px;
overflow: hidden;
}
#root {
position: relative;
width: ${video.composition.width}px;
height: ${video.composition.height}px;
overflow: hidden;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="${escapeAttribute(video.composition.id)}"
data-start="0"
data-width="${video.composition.width}"
data-height="${video.composition.height}"
data-duration="${video.composition.duration}"
>
${video.scenes.map(sceneHost).join("\n")}
</div>
<script src="./vendor/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["${escapeAttribute(video.composition.id)}"] =
gsap.timeline({ paused: true });
</script>
</body>
</html>`;
const outputPath = path.join(root, "compositions/index.html");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, html);
console.log(JSON.stringify({
ok: true,
output: "compositions/index.html",
scenes: video.scenes.length
}, null, 2));
Builder 的边界
Builder 负责:
- 把 Scene 转换为 Host
- 绑定变量
- 写入时间和 Track
- 生成主 Composition
Builder 不负责:
- 临时生成任意 CSS
- 临时生成任意动画代码
- 自动忽略 Schema 错误
- 在渲染时请求网络
- 静默替换未知 Block
遇到未知能力时应该失败,而不是猜测。
第九步:复制或安装 Block
如果自定义 Registry 还没有部署,可以先把源文件复制到项目:
cp \
registry/blocks/terminal-command/terminal-command.html \
compositions/terminal-command.html
如果已经维护了远程 Registry,可以在 hyperframes.json 中指定 Registry URL,再使用:
npx hyperframes add terminal-command
不要在同一个并行任务里让多个 Agent 同时修改 Registry 安装目录,避免文件和清单竞争。
第十步:运行完整流水线
1. 数据验证
node scripts/validate-scenes.mjs
2. 编译 Composition
node scripts/build-composition.mjs
3. 快速结构检查
npx hyperframes lint
4. 最终检查
npx hyperframes check --strict-variables
--strict-variables 可以把未声明变量、类型不匹配和非法枚举值提升为错误。
5. 抽取关键帧
npx hyperframes snapshot --at 0.5,3.5,6.5,7.5,11,14.5
检查:
| 时间 | 预期 |
|---|---|
| 0.5s | 第一实例开始进入 |
| 3.5s | 第一条命令和结果可见 |
| 6.5s | 第一实例准备结束 |
| 7.5s | 第二实例进入,没有第一实例残影 |
| 11s | 第二条命令已经完成 |
| 14.5s | 最终状态仍然可读 |
6. 最终预览
npx hyperframes preview
7. 人工确认后渲染
npx hyperframes render \
--quality high \
--output output/terminal-demo.mp4
8. 验证文件
test -s output/terminal-demo.mp4
ffprobe \
-v error \
-show_format \
-show_streams \
output/terminal-demo.mp4
怎样接入 Audio Map
创建:
data/audio-map.json
{
"version": "1.0",
"duration": 15,
"cues": {
"install": {
"start": 0.6,
"duration": 4.8,
"events": {
"command": 1.0,
"result": 3.4
}
},
"render": {
"start": 7.4,
"duration": 5.2,
"events": {
"command": 7.9,
"result": 11.2
}
}
}
}
Scene 引用 Cue:
{
"audio": {
"cue": "install",
"sync": "word"
}
}
Builder 可以在编译时:
- 把 Cue 的绝对时间转换为 Block 内部相对时间
- 验证 Cue 是否位于 Scene 时间窗口中
- 把相对时间作为 Block 变量传入
- 或选择一个匹配的 Motion Recipe
不要让 Block 自己读取整个 TTS 文件格式。先统一为稳定的 Audio Map,才能替换语音服务而不重写所有 Block。
怎样扩展 Theme
建议 Theme 使用语义名称:
{
"theme": "code-editorial"
}
而不是在 Scene 中直接传任意颜色。
可以维护:
themes/
code-editorial.css
terminal-green.css
swiss-light.css
Contract 明确每个 Block 支持哪些 Theme:
{
"themes": [
"code-editorial",
"terminal-green"
]
}
如果 Scene 请求:
{
"theme": "neon-rainbow"
}
Validator 应该直接失败,而不是让 Builder 临时发明配色。
怎样管理 Block 版本
视频数据应记录:
{
"block": "terminal-command",
"block_version": "1.0.0"
}
升级策略可以是:
- Patch:修复不改变视觉结果的错误
- Minor:增加可选 Props 或 Theme
- Major:修改 DOM、时间或视觉行为
旧视频应固定到它通过验证时的 Block 版本。否则升级 Registry 后重新渲染,画面可能发生变化。
同样值得固定:
- Scene Schema 版本
- Builder 版本
- Theme 版本
- Motion Recipe 版本
- HyperFrames CLI 版本
- GSAP 和其他运行时版本
- 字体和媒体文件
什么时候使用 Web Components
Web Components 可以在 Block 内部发挥作用,例如:
<hf-terminal-view
data-command="npm install hyperframes"
data-output="Installed successfully"
></hf-terminal-view>
适合:
- 多个 Block 共享同一 UI 结构
- 需要封装复杂 DOM
- 需要单独测试视图组件
- 浏览器预览和非视频页面也要复用
但需要注意:
- Custom Element 初始化必须同步完成
- 不在
connectedCallback中发起关键网络请求 - 不在组件内部创建无限动画
- 不让组件自己控制媒体播放
- Block 的 Timeline 仍由 HyperFrames Composition 注册
- 全局组装后 ID 仍然必须唯一
推荐关系:
Registry Block
↓
内部使用 Web Component
↓
Composition Timeline 控制内部可动画节点
常见失败
Scene 通过 Schema,但视频仍然失败
因为 Schema 只检查数据形状。继续运行:
Semantic Validator
→ HyperFrames check
→ Snapshot
两个 Block 在同一 Track 重叠
Validator 应该默认拒绝,除非 Scene 显式声明:
{
"allow_overlap": true
}
并且 Builder 能证明这是转场或叠加设计。
Block 安装后变量无效
检查两种 JSON 是否混淆:
data-composition-variables:变量声明数组data-variable-values:变量值对象
Block 在主页面空白
检查:
- Host ID 是否匹配内部 Composition ID
- 子文件根是否位于
<template> - Timeline key 是否匹配 Composition ID
data-composition-src路径是否正确- 子文件样式和脚本是否也在模板中
- 是否存在重复 ID
Builder 生成的 HTML 属性损坏
所有写入 HTML 属性的 JSON 都必须正确转义。不要直接字符串拼接未处理的用户输入。
同一数据重复构建结果不一致
检查:
- Builder 是否写入构建时间戳
- 输出顺序是否依赖对象遍历或文件系统顺序
- Block 是否使用随机数
- Theme 是否依赖远程字体
- Registry 是否在构建时自动获取 latest
- 依赖版本是否固定
推荐的 package scripts
可以把流水线固化到 package.json:
{
"scripts": {
"video:validate": "node scripts/validate-scenes.mjs",
"video:build": "node scripts/build-composition.mjs",
"video:check": "npm run video:validate && npm run video:build && hyperframes check --strict-variables",
"video:preview": "hyperframes preview",
"video:render": "hyperframes render --quality high --output output/video.mp4"
}
}
执行:
npm run video:check
npm run video:preview
# 确认后
npm run video:render
这样 Agent 不需要重新记忆完整命令链,只执行项目已经定义好的生产入口。
下一步可以增加什么
当 Terminal Block 跑通后,可以继续增加:
title-card
process-flow
big-number
before-after
product-demo
quote
cta
caption-track
每新增一个 Block,都应该同时增加:
- Registry Item
- Block Contract
- Props Schema
- 默认变量
- Snapshot 检查点
- 支持的 Theme
- 支持的 Motion Recipe
- 版本记录
否则 Registry 很快会变成另一个不可维护的代码目录。
总结
这套流水线把职责拆得非常清楚:
AI Agent
→ 选择 Block、填 Scene JSON、编排时间
JSON Schema
→ 检查字段和类型
Semantic Validator
→ 检查场景、时间、版本和能力关系
Composition Builder
→ 生成稳定的 HyperFrames Host HTML
Registry Block
→ 提供已经验证的画面结构和动画
HyperFrames CLI
→ 检查、预览和渲染
真正的数据驱动不是把 JSON 在浏览器里临时转换成 DOM,而是建立一条:
数据可验证
→ 源码可重复生成
→ 时间轴可确定 Seek
→ 画面可检查
→ 成品可追溯
的完整生产链。
系列导航
- HyperFrames 原理详解:AI Agent 如何把 HTML 渲染成视频
- HyperFrames Skills 实战:从 Brief 到 MP4 生成第一条视频
- HyperFrames 进阶:如何提高画面质量与生产效率
- 玩转 HyperFrames:从 AI Coder 到 AI Layout Driver
- HyperFrames 官方 Registry 指南:让 Agent 自动查、拉、挂、验
- HyperFrames Registry 实战:Block + JSON Schema 视频流水线
配套资源:
参考资料
RELATED / 相关推荐
接着读这些
按同一栏目、标签与技术栈为你挑选。
玩转 HyperFrames:从 AI Coder 到 AI Layout Driver
通过 Block Registry、Scene JSON、设计 token 和音频时间数据,把 HyperFrames 从每次现场写代码升级为可复用、可验证的视频生产系统。
HyperFrames 进阶:如何提高 AI 视频的画面质量与生产效率
解决 AI 视频像网页幻灯片、动画杂乱和每次从零生成的问题,建立可复用的设计规范、场景 Block、媒体与批量生产流程。
HyperFrames Block Registry Starter Kit
可复制的 HyperFrames 数据驱动视频底座,包含 Scene Schema、Block Contract、Theme Token、Validator、Builder 和 Agent Prompt 模板。