1实现原理 · 为什么它能做到
画面与声音同源:先把 Strudel 曲目离线渲染成音频,再把每个音符事件映射回产出它的代码 token,视频由这些事件驱动——所以每次高亮都落在它自己的音符上,而不是后期对轨。
The music is rendered offline from the code, and the video is driven by Strudel's own note events, so every highlight lands on its note.
录音链路:record.mjs 在 127.0.0.1 上起一个只服务 recorder.html 与 node_modules 的静态服务,用静音无头 Chrome 逐 part 单独求值,导出每个音符的时间、所属 part、取值,以及产出它的代码字符区间,然后离线渲整轨 mix(含可选 ending)与 ._scope() part 的 solo stem。
// Serves recorder.html (the pinned @strudel/web build from node_modules) on 127.0.0.1, opens it in a … // muted headless Chrome, and for the track named in <project>/video.json: … // 1. evaluates every labelled part on its own and exports each note event with its time, part,
离线一致性的关键:recorder.html 只从本机 node_modules 载入钉版 @strudel/web,不从 strudel.cc 取任何东西;示例轨道是纯合成器无采样,所以首跑不需要额外的采样网络请求。
nothing is fetched from strudel.cc. Samples are only fetched if the track itself calls samples(...).
时间轴不是手写的:build-data.mjs 把 Strudel cycle 换算成视频秒,生成 window.SQ(token 高亮、段落 power-on、widget、能量曲线),把总时长写回 index.html 的 data-duration、把音频起点写成 pre。
const sec = (c) => +(PRE + c * SPC).toFixed(4); // strudel cycle -> video seconds … fs.writeFileSync(path.join(project, "data/events.js"), "window.SQ = " + JSON.stringify(out) + ";\n"); … // stamp the length into index.html; the audio stays the length of the recording
镜头也默认自动规划:不给 camera 就按段落 power-on 取景、build 段推近 all( 块、drop 后揭示新段落、结尾拉远到 sign-off;要覆盖才在 video.json 里写 camera 行。
Leave `camera` out to get the automatic plan: frame each part as it powers on, push in on the `all(` block during builds, reveal the newest part after a drop, pull wide after the last drop, and zoom to the sign-off.
录音质量是门禁而非事后补救:录制按每 4 小节打印电平并告警,要求反复录到 drop 峰值 ≤ -2 dBFS 且无削波;改曲就退回步骤 5-7 重录,改 video.json 只回步骤 6-7。
Fix the track and record again until the drops peak at or below -2 dBFS with no clipped samples.
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| cli | HyperFrames CLI(钉版 0.8.70,经 npx --yes 调用;check/snapshot/render/cloud render) |
| package | @strudel/web 1.3.0(AGPL-3.0,从 npm 装,不进仓库) |
| package | puppeteer-core 25.12.0 + @puppeteer/browsers 3.2.3 与 OFL 字体(@fontsource/jetbrains-mono、rubik-mono-one) |
| package | Chrome for Testing 149.0.7827.22(录音用;可用 CHROME=<path> 覆盖) |
| network | 成片构图在载入时从 jsDelivr 取 GSAP 3.14.2(故渲染并非完全离线) |
| api | HyperFrames 云渲染(可选,消耗用户账户额度,须用户显式同意才可走) |
| network | 轨道自身若调用 samples(...),录音时会去轨道写明的采样 URL 取音频(示例轨道无采样) |
4风险提醒 风险提醒:蓝色 · 知晓即可
- 轨道代码会在无头浏览器里被求值 — 录音即执行:record.mjs 对每个 labelled part 单独求值用户的 .strudel.js。来源不可信的轨道等于在浏览器沙箱里跑任意 JS,且 samples(...) 可指向任意 URL。
- 渲染并非完全离线 — 模板从 cdn.jsdelivr.net 取 GSAP 3.14.2,preview/render 时需要网络;离线机房或断网环境会失败(SKILL.md 已声明)。
- 两个大额/敏感外部动作 — Chrome for Testing 下载约 150MB;HyperFrames cloud render 消耗用户账户额度——后者 skill 要求先取得显式同意,但仍属需留意的成本面。
- 采样授权需自证 — 示例轨道无采样,但用户轨道一旦调用 samples(...),那些录音的授权归用户;skill 只提示『use only sounds the user owns or has cleared』,无校验机制。
- snapshot 的默认行为有隐私面 — 不带 --describe false 时 snapshot 会把帧上传给托管描述模型;skill 明文要求一律带上,但这是提示级约束,最终仍由 agent 执行。
5第二遍独立确认
- [ok] 『No credentials.』声明是否成立 — SKILL.md Rights/Side effects 段明写 'No credentials.';目录内 process.env 命中只有 record.mjs 的 CHROME 与 CFG 类入参,未见 key/token/cookie/keychain 读取,也无 .env 解析。
- [discrepancy] 『录音离线』是否等于全流程离线 — 录音段确实离线(127.0.0.1 静态服务 + 本地无头 Chrome + node_modules 里的 @strudel/web;recorder.html 注释明写 nothing is fetched from strudel.cc),但建仓(npm install、Chrome for Testing 下载)与渲染期构图载入 GSAP(cdn.jsdelivr.net)都要联网。SKILL.md 自己把这些写进 Side effects,故非隐瞒;只是『离线』的适用范围仅限录音。据此把档位定为蓝(官方源固定版本安装 + 产物期 CDN,与同 repo 先例 hyperframes-animation / hyperframes-core 同口径)。
- [ok] 高亮是否真由音符事件驱动(而非后期对齐) — 链路可读通:record.mjs 导出每音符的 locs(代码字符区间)→ build-data.mjs 用 byLoc 汇总成 token 时间表与段落/widget 时间 → 写成 window.SQ → 模板 app.js 消费。音画同源成立,不是后期对轨。
- [discrepancy] 是否误把 ffmpeg 列为依赖 — 本 skill 目录内不调用 ffmpeg:录音与 mix 走 Strudel 的离线渲染器,编码由 HyperFrames CLI 负责。故 external_deps 不列 ffmpeg(同批姊妹 skill 有列,本 skill 没有)。
- [ok] 大写 S 标签的拒绝逻辑是否真在代码里 — record.mjs 实有 'const soloing = labels.filter((l) => l.length > 1 && l.startsWith("S"));' 并在命中时 fail 提示改名——不是文档空话。
- [unlocatable] 录制耗时与云渲染额度的具体数值 — 『约 5-7 分钟/32 小节』与云渲染『消耗多少额度』只有文档声明,源码内无可复现依据;本次为只读侦查,未运行录音或渲染,故不作为已核事实使用。
6结论
90eabb744789abe6…ba7a0bb6d3