首页
/ HyperFrames Seam Gate 解析:用 ledger.json + 双脚本为多场景视频切点做"生成—验证"闭环

HyperFrames Seam Gate 解析:用 ledger.json + 双脚本为多场景视频切点做"生成—验证"闭环

2026-09-05 10:45:28作者:傅爽业Veleda

HyperFrames 的 motion-doctrine 技能用一套零依赖的 Node 脚本(seam-stamp.mjs 生成 + seam-gate.mjs 验证)把"切点处进出向量必须一致"这条运动法则从口头规范变成了可执行断言:以项目根目录的 ledger.json 作为向量账本(vector ledger),先由 stamp 脚本从账本生成主时间轴上的接缝(seam)代码,再由 gate 脚本驱动无头 Chrome 逐帧测量、判定 exit 0/1。读完本文,你能完整掌握 ledger.json 的 schema 与每个字段的取值约定、stamp/gate 两条命令的全部参数与默认值,以及 gate 报告中每一行检查(ledgerzero-overlapz-sign-scancarrier-* 等)背后的判定阈值与测量原理。

Seam Gate 在整个 motion-doctrine 中的位置

motion-doctrine 是 HyperFrames 动画技能体系中的"总纲":它要求整片只选一个主导运动方向("the current",默认向左),并且每个切点(seam)处,前一场景如何退出决定后一场景如何进入——同轴、同向、速度匹配、两侧都切在运动中途(向量法则,见 SKILL.md 的 Part 1)。

但法则必须落到构建门禁(build gate)上才有约束力,这就是 Seam Gate 的角色。官方给出的制作顺序是:

vector ledger (ledger.json)
  → STAMP 主时间轴接缝 (scripts/seam-stamp.mjs)
  → 每个阶段选一条持续运动路线
  → 构建 comps
  → VERIFY (scripts/seam-gate.mjs)

其中手写代码只用于 Tier-A 的 morph / match-cut;由 stamp 生成的接缝"天然通过 gate"(pass by construction),因为它们是同一份账本的确定性投影。

两个脚本与运行前提

Seam Gate 是一对"生成 + 验证"脚本,位于 .claude/skills/motion-doctrine/scripts/ 目录:

  • seam-stamp.mjs(143 行)— 从 ledger.json 生成主时间轴的接缝代码块;
  • seam-gate.mjs(600 行)— 数值验证器,提供 verifyprobe 两个子命令。

运行前提(源自 seam-gate.mdseam-gate.mjs 头部注释):

  • 零 npm 依赖:只需 Node ≥ 22(gate 直接使用 Node 22 内置的 WebSocket/fetch,走原始 CDP 协议)+ 一个本地 Chrome;
  • Chrome 自动发现顺序(见 findChrome):环境变量 CHROME_PATH~/.cache/puppeteer/chrome-headless-shell~/.cache/puppeteer/chrome → macOS 系统 Chrome;找不到则报错提示设置 CHROME_PATH
  • 页面以 1920×1080 视口加载(Emulation.setDeviceMetricsOverride),等待 HyperFrames 运行时就绪信号 window.__playerReady && window.__renderReady && window.__player(见 openPage),该运行时契约与 packages/core 中注入到页面的全局对象一致。

核心命令三件套(<SKILL_DIR>.claude/skills/motion-doctrine):

# STAMP: 从 ledger 写主接缝块(base sets + 全部 wrapper tweens)
node <SKILL_DIR>/scripts/seam-stamp.mjs --ledger ledger.json --write index.html

# verify: 验证 ledger 中的每一个 seam(exit 0 = 门禁通过)
node <SKILL_DIR>/scripts/seam-gate.mjs verify --ledger ledger.json --project <project-dir>

# 复用已在运行的 preview server(改完 comp 后要重启它——bundle 会过期!)
node <SKILL_DIR>/scripts/seam-gate.mjs verify --ledger ledger.json --url http://localhost:5244

# probe: 发现某切点时刻附近的所有运动元素(用于编写/修正 ledger 行)
node <SKILL_DIR>/scripts/seam-gate.mjs probe --t 44.8 --project <project-dir>

两种服务模式的取舍(源码见 ensureServer):

模式 行为 适用场景
--project <dir>(推荐) 随机取 5380–5399 端口,npx --yes hyperframes preview --foreground --no-open --port <port> 起一个全新 preview server,等待 /api/projects 就绪(120s 超时),结束后自动 kill 避免旧 bundle 假象;且启动时显式删除环境变量 HYPERFRAME_RUNTIME_URL——源码注释指出:该变量取值错误时会让请求"静默失败成 200 HTML",必须保证采样到的是真实构建产物
--url <preview-url> 复用你正在运行的 server 快速迭代时复用 IDE 预览;comp 有编辑后必须重启 server,否则你验证的是过期构建

server 启动后,gate 会请求 /api/projects 解析第一个项目 id,最终导航到 /api/projects/<id>/preview/comp/index.html 这个 comp 预览页。验证完成后,退出码语义为:任一检查 FAIL → exit 1;全部 PASS/WARN → exit 0;参数/环境错误 → exit 2(见 main),因此可以直接串进 CI。--json 输出机器可读的完整结果数组,--fps 默认 30(ledger.json 顶层的 fps 优先)。

STAMP:把账本行编译成主时间轴代码

node <SKILL_DIR>/scripts/seam-stamp.mjs --ledger ledger.json --write index.html

stamp 的输出是替换/插入 index.html// <seams:auto> … // </seams:auto> 标记之间的代码块(带"do not hand-edit"头注释和再生成命令)。若标记不存在,它会定位 window.__timelines["main"] = tl; 注册行并插在其后——定位不到两者之一则直接抛错(见 写入逻辑)。这正是 changelog-video 技能 模板骨架中预留接缝块的原因:该技能的 build spec 明确要求主时间轴必须命名为 tl,因为 stamp 生成的都是 tl.to / tl.set 调用。

账本里每条 seam 的可选 stamp 参数

参数 含义 默认值(源码核对)
exit.dur / entry.dur 覆盖出/入时长 X/Y seam:exit 0.34s / entry 0.42s;Z seam:exit 0.21s / entry 0.5sseam-stamp.mjs#L92-L120
entry.travel 入场起始偏移量(xPercent/yPercent) 10;想要"软入场"观感用 8
blur Z seam 的模糊峰值(px) 18(整帧 zoom);文字缩放场景用 10

生成代码的两类形态

stamp 会先做静态账本自检:同一行的 exit.axis/direntry.axis/dir 不一致时直接抛错 mismatched … fix the PLAN, not the stamp——与 SKILL 中"修计划,别修缓动"的口号对应(seam-stamp.mjs#L85-L90)。

X/Y seampower3.in 出 + power4.out 入的镜像缓动对):

tl.to("#el-hook", { xPercent: -12, autoAlpha: 0, duration: 0.34, ease: "power3.in" }, cut - 0.34);
tl.set("#el-hook", { autoAlpha: 0 }, cut);          // 硬切:切点帧直接归零,杜绝叠化
tl.fromTo("#el-claim", { xPercent: 10, autoAlpha: 0.35 },
  { xPercent: 0, autoAlpha: 1, duration: 0.42, ease: "power4.out", immediateRender: false }, cut);

注意三处设计:出方在 cut - dur 就启动、切点帧仍在运动中("cut mid-motion");tl.set 在切点把出方强制隐藏(保证 gate 的 zero-overlap 检查);入方 fromTo 起点带 0.35 透明度且 immediateRender: false,避免时间轴注册瞬间就把未轮到的场景画出来。

Z seam(scale = "Z 轴",blur 作为景深伴随效果):出方向 dir=-1(pull)收到 scale 0.8dir=+1(push)推到 scale 1.18,均配 power3.in + blur(18px) + autoAlpha→0none 缓动);入方 fromTo超规格状态起飞——pull 从 scale 1.25("以更大的规模抵达")、push 从 scale 0.78,落点 scale 1.0expo.out、透明度 0.15→1、blur 归零。这些预置状态同时写进文件开头的 base gsap.set 清单(见 场景清单与 Z 预置):第一个场景可见,其余 autoAlpha: 0,Z 入场元素额外携带起始 scale 与 blur。

match-cut / morph 行只生成两个可见性 set(切点帧出方 autoAlpha: 0、入方 autoAlpha: 1)加一行注释:载体交接(carrier handoff)是 Tier-A 工作,必须手写,但载体行必须保留在 ledger.json 里供 gate 检查位置连续性。

ledger.json:向量账本的数据格式

ledger.json 放在项目根目录,一个 seam 一行,"向量账本即数据"(the vector ledger as data)。完整示例(原文档示例,可直接作为起步模板):

{
  "fps": 30,
  "seams": [
    {
      "id": "hook→claim",
      "cut": 4.2,
      "technique": "cut-the-curve LEFT",
      "exit": { "selector": "#el-hook", "axis": "x", "dir": -1 },
      "entry": { "selector": "#el-claim", "axis": "x", "dir": -1 }
    },

    {
      "id": "claim→payoff",
      "cut": 10.2,
      "technique": "inverse zoom-through",
      "exit": { "selector": "#el-claim", "axis": "z", "dir": -1 },
      "entry": { "selector": "#el-payoff", "axis": "z", "dir": -1, "scanRoot": "#el-payoff" }
    },

    {
      "id": "ui→player (match cut)",
      "cut": 60.6,
      "type": "match-cut",
      "carrier": { "out": "#resting-card", "in": "#product-video" }
    }
  ]
}

字段约定(与源码解析逻辑逐一对应):

  • cut — 主时间轴(master clock)上的秒数,即"入方点火"的那一帧;
  • type"cut"(默认,做完整向量检查)、"match-cut" / "morph"(只查载体连续性 + overlap;运动允许恰好在边界开始);
  • axis"x""y""z"(z 即 scale)。dir 是运动符号:x 轴 −1 = 向左;y 轴 −1 = 向上;z 轴 +1 = push(变大)、−1 = pull(变小)。gate 与 stamp 都按此约定把 axis 映射到 xPercent/yPercent/scale
  • selector — 真正承载接缝运动的元素。主时间轴动的是 wrapper 时用 wrapper 选择器;接缝运动写在 sub-comp 内部时用 comp 内 hero(id 或 [data-hf-id=…])。probe 子命令会告诉你该用哪个;
  • entry.scanRoot(Z seam)— 用于扫描"入场元素内部自带入场动效是否在和 Z 符号打架"的子树根,默认取 entry selector;
  • carrier — 普通 "cut" 行可选;match-cut/morph 行必填out/in 两个 rect 必须在切点 ±1 帧处匹配(中心 12px / 尺寸 5% 容差,且祖先 transform 自动计入——因为测量走 getBoundingClientRect)。

technique 是给人看的标注(如 cut-the-curve LEFTinverse zoom-through),不参与判定。

VERIFY:每条检查行到底在断言什么

verify 对每个 seam 在四个时刻采样:cut-0.1scut-1帧cut+1帧cut+0.1sdt = 1/fps,fps 取自 ledger 顶层,缺省 30)。页内 harness(__seamGate)的测量原语(HARNESS)值得单独说明,因为它决定了所有判定口径:

  • 累计不透明度 cumOp:沿祖先链连乘 opacity,途中遇到 display:nonevisibility:hidden 直接归 0;
  • rect 测量getBoundingClientRect() 取中心点(x/y 速度)与宽度比 es = rect.width / layoutWidth(z 速度)。由于 getBoundingClientRect 天然包含祖先 wrapper 的 transform,外层 scale 无需单独补偿——这正是原文档最后一句"Velocities are measured on getBoundingClientRect(x/y 用中心,z 用 width-ratio),祖先 transform 自动计入"的实现依据;
  • 可见性判定:累计 opacity > 0.04 且面积 > 16px² 且在 1920×1080 屏内。

判定阈值常量(seam-gate.mjs#L39-L44):

const VIS = 0.04;          // 累计不透明度低于此值 = 不可见
const EPS_XY = 15;         // px/s —— 慢于此视为"静止"(exit 没在动 / entry 从静止起)
const EPS_Z = 0.04;        // effective-scale 单位/s
const SPEED_RATIO = 3;     // 入/出速度比超过 3:1(或 <1:3)→ WARN
const CARRIER_POS_TOL = 12; // 载体中心偏移容差 px
const CARRIER_SIZE_TOL = 0.05; // 载体尺寸差容差 5%

原文档给出的"报告行 ↔ 规则"对照表,结合源码的完整解读如下:

报告行 对应法则 源码中的判定逻辑
ledger 计划内一致性:exit/entry 向量(axis + dir)必须匹配 纯静态比较,运行采样前执行;不匹配即 FAIL 并报"mirrored/mixed vector in the PLAN"(verify
exit-moving / entry-moving 规则 1/3——不许"已静止再切",不许"从静止起入" 双侧速度绝对值须 ≥ EPS(15 px/s 或 0.04 es/s),否则报 "exit settled before the boundary" / "entry starts from rest"
exit-visible / entry-visible 采样窗口内该侧必须真实可见 出方在 cut-0.1s 可见、入方在 cut+0.1s 可见,否则报具体 op 值
exit-direction / entry-direction 规则 3——实测符号必须等于账本符号 sgn(velocity) !== dir 即 FAIL;z 轴会额外标注 "(mirrored zoom)"
speed-match(WARN) 向量法则第 3 条——入场初速 ≈ 出场末速 速度比 >3 或 <1/3 才 WARN("want ~1"),不阻断门禁
zero-overlap 规则 6——每一帧只允许一侧可见,切点不是叠化 入方在 cut-1帧 或出方在 cut+1帧 仍可见即 FAIL,报文中点明"reads as a dissolve"
z-sign-scan 规则 7——新场景自带的入场不得与接缝 Z 符号相斗 scanRoot 子树(上限 900 元素、忽略 <32px 与 op≤0.1 的元素)在 cut+1帧 → cut+0.1s 间测 scale 速度,符号与 entry.dir 相反的逐个列出(最多前 5 个)
carrier-position / carrier-size / carrier 规则 3/4——载体 rect 连续性,祖先 scale 计入 切点两侧载体中心差 ≤12px 且宽度差 ≤5% 才 PASS,报实测 Δpos/Δsize;match-cut/morph 无 carrier 行会 WARN "nothing to verify"

输出形态:人读模式下每个 seam 一块(■ id (cut @Xs, type) ✓/✗ N FAIL + 逐行 PASS/WARN/FAIL check detail),末尾汇总 SEAM GATE: PASSED/FAILED — N fail, M warn across K seams 并据此定退出码;--json 则直接 dump 全部结果数组。

一个典型失败案例的排障链路(结合 SKILL 中的经验):zero-overlap FAIL 的常见根因是 clip-gating——data-start 早于入场 tween 的 clip 会在初始不透明度下被提前 un-hide。修复口径:初始 autoAlpha: 0 data-start = 切点时间,不要更早。此外记住"编辑重开接缝":任何对场景首/尾约 1 秒的改动(包括按新配音重排时间)都使该边界审计失效,必须重跑 verifier。

PROBE:切点附近的"运动元素探测器"

编写账本行时最常遇到的问题是:这个切点真正在动的是哪个元素、符号是什么?probe 就是为此设计的:

node <SKILL_DIR>/scripts/seam-gate.mjs probe --t 44.8 --project <project-dir>

工作原理(probe):在 #root 下做四次全树扫描(t-±windowt±1帧--window 默认 0.1s),两两配对求 vx(中心 x 速度)、vy(中心 y)、vscale(宽度比速度)与透明度变化,过滤掉静止元素(阈值同 EPS),按速度量级降序取前 14 个输出,并附上人类语义提示:

PROBE @ 44.8s (window ±0.1s, 1f = 0.033s)

— OUTGOING side (44.70 → 44.77) — movers:
  #el-claim                                vx    -820  vy       0  vscale   0.000  op 1.00→0.41  (640×280)
  ...
Use these selectors + signs to write the ledger row (x-: left, y-: up, scale+: push, scale-: pull).

拿到 selector 与符号后照抄进 ledger.json 即可——这条"probe 发现 → ledger 落行 → stamp 生成 → verify 关闭"的循环,就是 Seam Gate 的日常使用闭环。

小结:门禁、账本与"修计划不修缓动"

seam-gate.md 放回上下文,Seam Gate 的工程价值在于三点:其一,把运动法则中"人眼可辨但难以复核"的要求(同轴同向、切在运动中、零叠化、Z 符号一致、载体连续)翻译成带明确阈值(15 px/s、0.04 es/s、12px/5%、3:1)的数值断言,可进 CI;其二,"账本 → 生成"让普通接缝零手写——stamp 与 gate 共享同一份 ledger.json,生成物按构造通过验证,人力集中到 Tier-A 的 carrier 交接上;其三probe 把"找运动载体"这一最费时的定位工作自动化,保证账本里的 selector 与真实 DOM 运动一一对应。

脚本查不到的仍归作者:编辑重开接缝、音频即时钟(按 VO 真实词时间戳重排场景)、clip-gating 陷阱——这三条在 motion-doctrine/SKILL.md 的 Seam Gate 一节有完整表述。验证通过(exit 0)之后,一个 seam 才算"完成"。

登录后查看全文
热门项目推荐
相关项目推荐