跳到主要内容
BLKBLKTECH

指南 / ai

HyperFrames Registry 实战:Block + JSON Schema 视频流水线

从零创建可复用 HyperFrames Block、Scene JSON Schema、语义验证器和 Composition Builder,让 AI Agent 从现场写代码转向数据驱动的视频组装。

作者 BLKTECH 编辑部更新 2026年7月25日26 分钟难度 实战低成本HyperFramesJavaScriptJSON SchemaGSAPAI Agent

HyperFrames Registry 流水线可以把 Scene JSON 经过 Schema 和语义验证,编译为引用独立 Block Sub-composition 的 index.html,再通过 lint、check、snapshot 和 preview 验证,让 AI 主要负责选组件、填数据和编排时间。

这篇教程要搭建什么

上一篇《从 AI Coder 到 AI Layout Driver》介绍了数据驱动 HyperFrames 的总体架构。这一篇直接把架构落成一个最小流水线。

我们将创建:

  1. 一个可复用的 terminal-command Block
  2. 一份 Block Registry 描述文件
  3. 一份 Scene JSON Schema
  4. 一个跨场景语义验证器
  5. 一个 Composition Builder
  6. 一套固定 Theme 和变量接口
  7. 一条从 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.__timelines key 一致
  • 所有 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

实际系统可以采用两种方式:

  1. 所有 Block 共用一个大型 Scene Schema
  2. 通用 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("&", "&amp;")
    .replaceAll("'", "&#39;")
    .replaceAll('"', "&quot;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

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
→ 画面可检查
→ 成品可追溯

的完整生产链。

系列导航

  1. HyperFrames 原理详解:AI Agent 如何把 HTML 渲染成视频
  2. HyperFrames Skills 实战:从 Brief 到 MP4 生成第一条视频
  3. HyperFrames 进阶:如何提高画面质量与生产效率
  4. 玩转 HyperFrames:从 AI Coder 到 AI Layout Driver
  5. HyperFrames 官方 Registry 指南:让 Agent 自动查、拉、挂、验
  6. HyperFrames Registry 实战:Block + JSON Schema 视频流水线

配套资源:

参考资料

NEXT ACTION / 下一步

复制 Block Registry Starter Kit

把读到的方法变成一个小行动,完成后再回来迭代。

继续

RELATED / 相关推荐

接着读这些

按同一栏目、标签与技术栈为你挑选。