跳到主要内容
IM智引科技

研究 / 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 / 相关推荐

接着读这些

按同一分类、系列与标签为你挑选。