首页
/ OpenMontage 音乐驱动的逐帧合成作者:Frame Worker 如何构建 beat-synced 视频的单帧构图

OpenMontage 音乐驱动的逐帧合成作者:Frame Worker 如何构建 beat-synced 视频的单帧构图

2026-09-08 17:13:53作者:秋泉律Samson

本文聚焦 OpenMontage 仓库中 music-to-video Agent Skill 的关键一环——Frame worker(逐帧构图作者)。它是一条把音乐轨转化为节拍同步视频(beat-synced video)的自动化流水线(SKILL 定义见 .agents/skills/music-to-video/SKILL.md)中负责构建单帧合成文件的专用子代理角色。读完本文,你将掌握:frame-worker 的职责边界、它在一次调度中收到的全部输入、必须无条件遵守的框架约束(composition 契约、确定性规则、时间轴注册规范),以及如何按"读取计划 → 转换帧内时间 → 编写单帧 HTML → 自检"的完整流程交付一个可被组装器无缝拼接的帧文件。

角色定位:一个帧 = 一个文件 = 一个子代理

music-to-video 的工作流里,编排者(orchestrator)把整条音乐轨切成若干帧(frame),每一帧都是一个独立的合成文件 compositions/frames/<frame_id>.html,由一个 frame-worker 负责构建。多个 sibling worker 并行构建其余帧。这种"一帧一文件一代理"的设计(见 SKILL.md 的 Step 4)让帧数量跟踪的是"不同的视觉处理(treatment)",而不是节拍数——快节奏音乐不会导致子代理数量爆炸。额外的密度应放进组(group)内部

frame-worker 的职责可以用一句口号概括:follow the manual, fetch the materials, assemble(照着手册、取来素材、组装成型)。

  • 故事板(STORYBOARD.md)告诉它 WHAT:该帧包含哪些组(group)、每个组用的是模板 / 图元 / 资产、具体文案、品牌视觉、以及真实的节拍锚点秒数。
  • 它自己决定 HOW:如何抓取素材、把素材绑定到本帧的音频秒数上、微定时(micro-timing)、布局、命名空间。

换言之,frame-worker 是"执行者"而非"策划者"。完整的分工协议可以参考 sub-agents/frame-worker.md 以及它所遵循的 hyperframes-core 契约文档。

调度输入(Dispatch Context):一次派发的完整上下文

每个 frame-worker 被派发时只会获得一份精确的上下文,文件路径全部相对于 PROJECT_DIR(项目根目录)。根据 SKILL.md Step 4 的派发包定义,这份上下文包含:

输入 内容
PROJECT_DIR 项目根目录,所有路径相对它解析
frame_id 帧文件的 stem,例如 02-f2。必须原样用作 composition id、window.__timelines 的 key 以及文件名 compositions/frames/<frame_id>.html(组装器按它匹配)
## Frame N 位于 STORYBOARD.md 中,含 span_secpacingmoodfeel,以及 ### Groups 组列表
audiomap.json 时间真相(timing truth),analyze-beatgrid.py 一次性产出,见 scripts/analyze-beatgrid.py;frame-worker 必须直接使用给定的秒数
frame.md 品牌定义(调色板 + 字体),每一个视觉 token 都从这里取
Materials(素材) 模板组引用 references/templates//index.html;自由组引用 references/motion-primitives//index.html;资产组引用已暂存的 assets/…
Canvas <width>×<height> 画布尺寸,以及本帧的 pacing
反馈 如果此前一轮派发带有 lint / validate 反馈,必须逐条处理

模板的 data-composition-variables 属性定义了参数的语义。以 references/templates/card-flyby/index.html 为例,其 <html> 上声明了 themebgColorcardslandingsyaw 等变量,每个变量带 typelabeldefault——这就是 frame-worker 在把 params 绑定进模板时需要理解的全部语义来源。

Groups 的三种形态:故事板告诉你要建什么

## Frame N 块内的 ### Groups 列表规定了本帧内部要切分出的若干组,每组恰好是以下三种之一(详见 references/storyboard-format.md):

  1. template(模板组)——template:<id> + params(键取自模板目录条目)+ role_bindings(真实的 audiomap 锚点秒数,绑定到模板的"角色"如 phrase / climax)+ copy 文案。
  2. free_design(自由设计组)——free_design:{dominant_system, primitives, density_topology} + anchors[](真实的节拍 / onset 秒数)+ copydominant_system 描述主导视觉系统(如 per-onset typography),primitives 引用 references/motion-primitive-catalog.md 中的 L0 图元(如 content-swapbraam-punch),density_topology 描述密度累积方式。
  3. asset(资产组)——asset:{treatment, clips, anchors?, overlay_copy?},见 references/montage.mdtreatmentbeat_cut(每个锚点一个 clip,仅允许出现在 beat_cut 帧)/ ken_burns(单个 clip 的慢速推近,适配 phrase_flow)/ bg_under_text(clip 压暗后垫在模板或自由组底下)。

一个帧的多个组在时间上铺满该帧跨度(tile the span),组与组之间帧内顺序衔接。资产只是叠加在同一节拍脊柱上的"可选项"——排版与模板才是地板,没有用户素材时视频依然完整

框架内"固定不变"的部分:照实呈现,不得发挥

frame-worker 文档明确列出哪些东西是它无权改动的:

  • No plan = stop(无计划就停手):如果 ### GroupsTBD 或空(意味着 Step 3 被跳过),必须上报并什么都不写——绝不自行发明组、模板或文案
  • 计划已定,按文建造:组、模板/图元、文案、品牌和锚点都写死在 ## Frame 块里,照写即可。如果计划确实有错(模板或文案不对),停下来上报——由编排器回到 Step 3 重新规划。
  • 转场归组装器管:组装器在帧与帧之间做硬切(hard-cut),frame-worker 只负责本帧内部的 group→group 切换。
  • 音频住在根 index.html 上,本帧是无声的:frame 文件不挂任何音频,BGM 由组装器在 track 上统一挂载(见 scripts/assemble-index.mjs)。
  • GSAP 由宿主加载:frame 内部不要 <script src="gsap">,直接使用全局 gsap
  • 时长 = 帧跨度:时间轴按 0 … span_len 构建。

这一组约束的核心是让"WHAT 归计划、HOW 归执行"的边界在代码层面成立:任何可能造成多帧不连贯、时间错位、重复声音或外部依赖的创作自由度都被提前关闭。

底层契约:frame 文件必须是一份合法子合成(sub-composition)

frame-worker 写出的 compositions/frames/<frame_id>.html,在 HyperFrames 运行时里本质上是一份子合成(sub-composition)。通用法则(子合成形态、时间轴注册、确定性、布局)都定义在 hyperframes-corereferences/sub-compositions.md 中,frame-worker 文档要求先读它再动手。核心事实如下。

<template> 是传输容器,不是装饰

当宿主加载一份子合成时,运行时执行的是:

  1. fetch HTML 文件;
  2. DOMParser 解析;
  3. 找到 <template> 元素,只克隆其内容进宿主插槽
  4. <template> 之外的一切(包括整个 <head>被丢弃

所以凡是需要在真实渲染中存在的节点——markup、<style><script>——必须全部放进 <template>。一个常见的坑是把 <style> 写进 <head>:静态 lint 全绿、单文件独立 preview 完美,但合成渲染时所有样式都"掉在地上",文本以未样式化的默认大小出现在左上角(这就是 sub-compositions.md 中的 Pitfall 1)。

三个 ID 必须完全一致

宿主 slot 上的 data-composition-id == 内部模板根的 data-composition-id == window.__timelines 的注册 key。任何一处的拼写偏差都意味着运行时找不到时间轴:渲染日志会报 Sub-composition timelines not registered after 45000ms,每个错配 slot 白等 45 秒,最终只输出静态首帧(Pitfall 2)。frame-worker 自检清单的第一条正是核对这一等式的存在。

根元素用 #root 样式,不要用类名

合成渲染时编译器会把每个文件的 CSS 作用域化到自身的 data-composition-id 上:每条规则 S 变成 [data-composition-id="<id>"] S(后代选择器)。因此,如果根元素自带一个类且样式用它开写(如 .frame { … }),作用域化后规则无法再匹配根自身,整组规则静默失效(Pitfall 3)。#root 被作用域器特殊处理、可持续命中根元素,所以根必须用 #root 定义,后代用普通选择器。

时间轴注册契约(来自 determinism-rules.md

HyperFrames 是逐帧 seek 的渲染器:给定一个时间值就产出一帧像素,没有"播放"的概念。动画状态必须仅由时间值可复现。落实到 GSAP:

  • 时间轴在页面初始化期间同步创建:gsap.timeline({ paused: true })
  • 注册到 window.__timelines["<composition-id>"],key 必须等于 composition 根上的 data-composition-id
  • 不要为渲染关键运动调用 tl.play()
  • 不要async / Promise / setTimeout / 事件处理器里构建时间轴——渲染器可能在它们完成前就采样;
  • 结尾执行 tl.seek(0)(回到 0 时刻),保证 t=0 即可渲染;
  • 确定性红线:不得用 Date.now()performance.now()、未播种的 Math.random()、渲染期网络请求、hover/scroll/pointer 状态,不得使用 repeat: -1 这类无限循环。frame-worker 对应的要求是 "从索引推导变化、用 tl.set 换文本/数字"

布局层面还要注意:根元素固定像素帧尺寸并 overflow: hidden;主内容用 padding / flex / grid / max-width 排版,不用硬编码 top/left;动画只碰属性白名单(opacityxyscalerotationcolorbackgroundColorborderRadius、transforms),绝不动画 display/visibility。完整的 data-* 属性表(data-startdata-durationdata-track-indexdata-composition-src 等)见 data-attributes.md

构建四步:从读计划到落盘

frame-worker 的 Build 流程可以拆成明确的四步。

1. 读齐所有引用

读自己的 ## Frame 块、frame.md、以及每个被引用的模板/图元文件的全文,把配方复刻出来。模板的 DOM/CSS/tween 就是这份配方的参考实现。

2. 换算到帧内本地时间

所有锚点秒数都是从 audiomap.json 来的轨道秒数(track seconds)。因为组装器把各帧在轨道上无缝拼接(frames tile the track),故事板里任何时间都必须减去本帧的起始时间才能得到帧内时间:

local_t = track_t − span_sec[0]

例如一帧的 span_sec: [7.198, 17.598],故事板里一个 4.9s 的锚点在帧内实际是 4.9 − 7.198 ≈ −2.298s 之前需要特别核对取值;正确姿势是每个锚点先做整体平移再使用。

3. 编写 compositions/frames/<frame_id>.html

产物形态高度标准化:

  • 一个 <template> 包裹 #stage(其上带 data-composition-id="<frame_id>"),所有 <style> / <script> 都在这只 <template> 内;
  • 一条暂停的 gsap.timeline({paused:true}),注册在 window.__timelines["<frame_id>"],同步构建、以 tl.seek(0) 收尾;
  • 每个组拥有自己的容器,跨它的帧内跨度显示:tl.set("#g1",{autoAlpha:1}, start) 出场、tl.set("#g1",{autoAlpha:0}, end) 退场——这个 0ms 的属性切换就是 group→group 的硬切
  • 命名空间隔离:每个组的 id 和 shader uniform 都要加前缀 g1_ / g2_,避免同帧内跨组冲突;
  • fork 模板时内联其 DOM / CSS / tween,一律用宿主的全局 gsap
  • 若本帧是 phrase_flow(节拍网格不可靠、应按乐句/能量配速的宁静段),按乐句与能量密度配速,绝不把硬切锚到不可靠的网格上——这是 frame-skeleton.md 里"信任边界"的直接延伸。

资产组还要满足 montage 规则(详见 montage.md):<video> 一律 muted、必须是 #stage 的直接子元素(绝不能嵌进其它定时元素里,否则渲染器会把它冻结)、带 data-start / data-duration / data-track-index;clip 之间的淡出要对到下一锚点结束、紧接着用 tl.set(..., {opacity:0}, anchor)hard-kill 硬杀(对应 gsap_exit_missing_hard_kill lint 规则,非线形 seek 时残留帧会泄漏)。淡入淡出只能动画 opacity/autoAlpha,绝不碰 visibility/display

一个 frame 文件的最小骨架示意(结构源自 sub-compositions.md 的合法形态):

<!doctype html>
<html>
  <head><meta charset="UTF-8" /></head>
  <body>
    <template>
      <style>
        #stage { position: absolute; inset: 0; }
        #g1, #g2 { position: absolute; inset: 0; }
        /* ...每组内部样式... */
      </style>

      <div id="stage" data-composition-id="02-f2" data-width="1920" data-height="1080">
        <div id="g1"></div>
        <div id="g2"></div>
      </div>

      <script>
        window.__timelines = window.__timelines || {};
        const tl = gsap.timeline({ paused: true });
        // span_sec: [7.198, 17.598] → 帧内 0 … 10.4s
        tl.set("#g1", { autoAlpha: 1 }, 0);
        tl.set("#g1", { autoAlpha: 0 }, 3.2);   // 0ms 组切换
        tl.set("#g2", { autoAlpha: 1 }, 3.2);
        tl.set("#g2", { autoAlpha: 0 }, 10.4);
        tl.seek(0);
        window.__timelines["02-f2"] = tl;
      </script>
    </template>
  </body>
</html>

注意 02-f2 这个 id 同时是文件名 stem、data-composition-id、时间轴 key——三处一致,组装器与运行时才能对上号。

4. 自检并收尾

对照下面的 Self-check 清单逐条核对、就地修复。写完文件是终极动作:此后不再运行 hyperframes CLI(那些命令操作的是"已组装的项目",此时尚不存在,会报错在错误的文件上——见 SKILL.md Step 4),组装后的 lint / validate / inspect 由编排器在 Step 6 执行,发现问题会带着反馈再次派发本 worker。

Self-check:落盘前的七条硬性检查

frame-worker 文档给出了精确到可执行的自检清单:

  1. 结构<template> 包裹的 #stagedata-composition-id == 时间轴 key == 文件名 stem == <frame_id>;所有 <style> / <script> 都在 <template> 内;使用宿主的全局 gsap
  2. 时间轴:恰好一条暂停时间轴、已注册、以 tl.seek(0) 收尾;data-duration == 帧跨度长度;本帧无声。
  3. 组可见性:每个组只在自己的帧内跨度显示、跨外隐藏;t=0 就能渲染;group→group 切换是 0ms;id / uniform 已加命名空间。
  4. 内容一致性:每个组的文本 / 调色板与其块的 params / copy 一致,全部取自 frame.md 的品牌定义。
  5. 配速phrase_flow 帧按乐句/能量配速。
  6. 确定性(seek-safe):遵守 determinism-rules.md——从索引推导变化;换文本 / 数字用 tl.set(而非随时间推进的状态累积)。
  7. 资产剪辑与终帧:资产 clip 为 muted <video>、是 #stage 直接子元素、带 data-start / data-duration / data-track-index 且含 crossfade 后的 hard-kill tl.set终帧是有意设计的,主标题文本可读、不被画布边缘裁切。

这些条目大多能对应到真实 lint/渲染行为:第 1 条防止 Pitfall 1/2/3(样式丢落、时间轴找不到、根样式因类名失效);第 2、6 条保证逐帧 seek 渲染器在任何采样顺序下都能重现同一像素;第 7 条直接决定成片最后几秒是否"烂尾"。

组装之后:帧如何拼成整片

frame-worker 只交付单帧文件,但理解它写出的文件如何被消费有助于自检。编排器在 Step 5 运行确定性的 assemble-index.mjs:它按每个帧文件的累计 data-start 依次引用、把 assets/bgm.mp3 挂到根时间轴、并在帧与帧之间硬切——因为帧无缝隙地铺满轨道,所以不存在转场注入器。音频与宿主级媒体只存在于根 index.html,frame 自身无声这一约定由此在组装层得到保证。

随后 Step 6 对已组装的项目执行 hyperframes lint(结构)、hyperframes validate(无头 Chrome 抓 JS 错误与缺失资产)、hyperframes inspect(帧快照),失败时编排器会做最便宜的修复:直接编辑出错的 compositions/frames/NN-*.html——因此 frame-worker 在写文件阶段守住契约,就等于把 Step 6 的返工风险降到最低。若想了解调度侧如何保证并行与重试,可阅读 hyperframes-core/references/subagent-dispatch.md 的 DISPATCH / WAIT / 重派发规则。

小结:一份角色的五条方法论

把整个 frame-worker 设计压缩成可迁移到其它媒体自动化流水线的经验:

  1. 职责单向:计划(WHAT)与执行(HOW)严格分层,执行者永不发明内容,发现问题只上报不擅改。
  2. 时间统一换算:全轨道共享一个 audiomap 时间真相,每个子单元把它平移到自己的本地时间轴上,拼接时天然对齐。
  3. 契约即代码:composition id、时间轴 key、文件名三者同源,所有可视资源收进单一传输容器。
  4. 确定性优先:渲染靠时间值可复现,一切状态必须能由索引推导,为并行 seek 而生。
  5. 自检可执行:清单精确到"0ms 切换、前缀命名空间、muted video 直接子元素、终帧有意设计",让质量门禁可以自动化执行。

对任何希望把一段音乐"自动变成一支节拍卡点视频"的工程而言,frame-worker 提供的是一个把艺术判断(编排)与机械执行(单帧构建)解耦的成熟样本——编排者只回答"放什么",worker 只回答"怎么呈现",而渲染器则在两者之间保证了每一帧都可复现、可并行、可重试。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390