OpenMontage 音乐驱动的逐帧合成作者:Frame Worker 如何构建 beat-synced 视频的单帧构图
本文聚焦 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_sec、pacing、mood、feel,以及 ### 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> 上声明了 theme、bgColor、cards、landings、yaw 等变量,每个变量带 type、label 和 default——这就是 frame-worker 在把 params 绑定进模板时需要理解的全部语义来源。
Groups 的三种形态:故事板告诉你要建什么
## Frame N 块内的 ### Groups 列表规定了本帧内部要切分出的若干组,每组恰好是以下三种之一(详见 references/storyboard-format.md):
- template(模板组)——
template:<id>+params(键取自模板目录条目)+role_bindings(真实的 audiomap 锚点秒数,绑定到模板的"角色"如 phrase / climax)+copy文案。 - free_design(自由设计组)——
free_design:{dominant_system, primitives, density_topology}+anchors[](真实的节拍 / onset 秒数)+copy。dominant_system描述主导视觉系统(如 per-onset typography),primitives引用 references/motion-primitive-catalog.md 中的 L0 图元(如content-swap、braam-punch),density_topology描述密度累积方式。 - asset(资产组)——
asset:{treatment, clips, anchors?, overlay_copy?},见 references/montage.md。treatment∈beat_cut(每个锚点一个 clip,仅允许出现在beat_cut帧)/ken_burns(单个 clip 的慢速推近,适配phrase_flow)/bg_under_text(clip 压暗后垫在模板或自由组底下)。
一个帧的多个组在时间上铺满该帧跨度(tile the span),组与组之间帧内顺序衔接。资产只是叠加在同一节拍脊柱上的"可选项"——排版与模板才是地板,没有用户素材时视频依然完整。
框架内"固定不变"的部分:照实呈现,不得发挥
frame-worker 文档明确列出哪些东西是它无权改动的:
- No plan = stop(无计划就停手):如果
### Groups是TBD或空(意味着 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-core 的 references/sub-compositions.md 中,frame-worker 文档要求先读它再动手。核心事实如下。
<template> 是传输容器,不是装饰
当宿主加载一份子合成时,运行时执行的是:
fetchHTML 文件;- 用
DOMParser解析; - 找到
<template>元素,只克隆其内容进宿主插槽; <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;动画只碰属性白名单(opacity、x、y、scale、rotation、color、backgroundColor、borderRadius、transforms),绝不动画 display/visibility。完整的 data-* 属性表(data-start、data-duration、data-track-index、data-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 文档给出了精确到可执行的自检清单:
- 结构:
<template>包裹的#stage;data-composition-id== 时间轴 key == 文件名 stem ==<frame_id>;所有<style>/<script>都在<template>内;使用宿主的全局gsap。 - 时间轴:恰好一条暂停时间轴、已注册、以
tl.seek(0)收尾;data-duration== 帧跨度长度;本帧无声。 - 组可见性:每个组只在自己的帧内跨度显示、跨外隐藏;
t=0就能渲染;group→group 切换是 0ms;id / uniform 已加命名空间。 - 内容一致性:每个组的文本 / 调色板与其块的
params/copy一致,全部取自frame.md的品牌定义。 - 配速:
phrase_flow帧按乐句/能量配速。 - 确定性(seek-safe):遵守 determinism-rules.md——从索引推导变化;换文本 / 数字用
tl.set(而非随时间推进的状态累积)。 - 资产剪辑与终帧:资产 clip 为 muted
<video>、是#stage直接子元素、带data-start/data-duration/data-track-index且含 crossfade 后的 hard-killtl.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 设计压缩成可迁移到其它媒体自动化流水线的经验:
- 职责单向:计划(WHAT)与执行(HOW)严格分层,执行者永不发明内容,发现问题只上报不擅改。
- 时间统一换算:全轨道共享一个 audiomap 时间真相,每个子单元把它平移到自己的本地时间轴上,拼接时天然对齐。
- 契约即代码:composition id、时间轴 key、文件名三者同源,所有可视资源收进单一传输容器。
- 确定性优先:渲染靠时间值可复现,一切状态必须能由索引推导,为并行 seek 而生。
- 自检可执行:清单精确到"0ms 切换、前缀命名空间、muted video 直接子元素、终帧有意设计",让质量门禁可以自动化执行。
对任何希望把一段音乐"自动变成一支节拍卡点视频"的工程而言,frame-worker 提供的是一个把艺术判断(编排)与机械执行(单帧构建)解耦的成熟样本——编排者只回答"放什么",worker 只回答"怎么呈现",而渲染器则在两者之间保证了每一帧都可复现、可并行、可重试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00