16
0
0

一句话出片:HyperFrames 视频 skill 拆解

2026-10-29
2026-10-30
文章摘要
|

上一篇《HTML 转视频:HyperFrames 从崩溃到出片》记录了做一支 15 秒片头的全过程:GSAP 的 CDN 加载失败、headless Chrome 启动崩溃、中文字体没有声明、成片没有声音。文章发出去,人看懂了;但下一次让 AI 做另一支视频,同样的坑它还是会再踩一遍——因为文章是给人读的,不是给 agent 执行的。

于是我把这套流程固化成了一个 skill:hyperframes-video。现在只需要说「做一个 15 秒横屏视频,主题是《为什么用 Docker 部署中间件》,要有中文旁白」,它就会自己建项目、把依赖本地化、写 composition、生成旁白、渲染、校验,最后交付 MP4。

hyperframes-skill-contact-sheet.jpg

本文拆解这个 skill 的结构与设计取舍,并在附录给出全部源码。它真正解决的不是「怎么让 AI 会写 GSAP」,而是怎么把一次性的踩坑经验,变成可重复执行的运行时约束。

它解决什么问题

做第一支片子时,我花了大部分时间在四个和动画无关的地方:把 GSAP 从 CDN 搬进项目、绕过跑不起来的 headless Chrome、给中文补 @font-face、发现成片是静音的。这些步骤有一个共同点:它们都不是创作问题,但任何一个没做,片子就出不来或不能用。

把流程交给 AI 后,问题变成了另一个样子:

  • 提示词里写「别忘了本地化 GSAP」,它可能理解成「联网时用 CDN 更快」;

  • 提示词里写「注意 Chrome」,它看到 doctor 的绿勾就认为没问题;

  • 提示词里写「加个旁白」,它可能编一个 data-duration,被 check 打回;

  • 提示词里写「中文字体」,它可能把中文直接写进 PowerShell 命令,渲染出来满屏乱码。

失败后果不是「效果差一点」,而是:动画整条不执行、渲染直接中断、成片无声、文字变成方框。这些都不是靠模型「更聪明」能避免的,只能靠把执行顺序和验收标准写死。

这个 skill 的成功判据很具体:

  1. 用户只给主题,不需要回答环境问题;

  2. 换主题、换画幅后,不需要重新教它依赖怎么装;

  3. 出片后 check 0 error,ffprobe 能看到音轨;

  4. 它知道自己哪里没验证(比如画面构图、TTS 发音),并如实说出来。

核心矛盾

一篇教程可以假设读者有耐心、会排错;一个 skill 必须假设执行者只会照做,而且会在模糊处自行发挥。skill 的价值不在「写了多少知识」,而在「把哪些知识变成了不可绕过的步骤」。

拆解

这个 skill 由四件套组成:输入默认值、运行时规则、可执行脚本、输出验收。四者缺一不可——只有规则会变成一篇没人执行的文档,只有脚本会变成一套不会应变的死流程。

输入:默认值代替采访

用户说「做个视频」时,最不该做的就是反问五个问题。skill 里直接写死一套默认值:

项

默认

画幅

1920×1080;提到抖音/视频号/小红书/竖屏则 1080×1920

时长

15 秒(3 幕 × 5 秒)

帧率

30fps

音频

中文旁白;没提音乐就不加 BGM

视觉

深色底 + 单一强调色;标题 96–120px,正文 ≥28px

开工前只要求一句话回述计划,然后直接做。只有主题完全缺失、或必须用品牌/真人素材时才问一个问题。

这样做的好处是把决策成本从对话里拿掉。绝大多数视频需求并不真的需要定制,需要的是「先给我一版」。默认值本身就是一种产品判断:15 秒、三幕、有旁白,覆盖了大部分技术分享和产品演示场景。

规则:契约与可量化的画面底线

skill 把 HyperFrames 的契约压缩成五条,写在最显眼的位置:

  • 根元素带 data-composition-id、data-width、data-height、data-duration;

  • 有时间行为的元素带 data-start + data-duration,并加 class="clip";

  • 时间线 paused: true,注册到 window.__timelines[compositionId];

  • 旁白/音乐用独立 <audio>,data-duration 贴着媒体真实时长;

  • 只写确定性逻辑,禁止 Date.now()、Math.random()、运行时网络请求。

画面底线则直接用 check 的 Craft 规则,而不是「好看一点」这种无法执行的形容词:

  • 第 0 帧不能只有背景——它是社交平台缩略图;

  • 阅读字号 ≥28px(1080 短边的 2.6%);

  • 每行停留 ≥ 0.5 秒 + 0.3 秒/词,结尾句至少 2 秒;

  • 3–5 个缓动就够,别堆特效。

这些数字不是审美偏好,是 check 会计算的阈值。写进 skill 的规则如果无法验证,最后都会退化成口号;能验证的规则才能真正约束输出。

最典型的是依赖本地化。skill 没有把它写成「建议」,而是写成建项目之后的第 3 步:npm install gsap、复制到 vendor/、script src 指向本地。因为这一步一旦跳过,后面所有工作都没有意义。

脚本:把重复动作从上下文里拿掉

skill 带了三个可执行资产:

assets/starter/index.html——一份已经通过 check 的 15 秒三幕底版。它同时是模板和契约示例:中文用 @font-face 挂了 Microsoft YaHei / PingFang SC 两个 local() 源,GSAP 走本地 vendor/,首帧标题可见,字号和节奏都卡在阈值之上。agent 不需要从零写 HTML,只需要替换文案和配色。

scripts/render.ps1——把「浏览器回退 → check → snapshot → render → ffprobe」串成一条命令。它默认检测系统 Chrome,通过 HYPERFRAMES_BROWSER_PATH 绕开坏掉的 headless shell;用 cmd /c 调用 CLI,规避 PowerShell 5.1 把 npx 的 stderr 当成致命错误的老毛病;最后打印时长、编码、帧数和音轨。

scripts/make-vo.ps1——读取 UTF-8 台词文件,调用 Windows SAPI 生成语音,去首尾静音、逐条归一到 -16 LUFS、转 MP3,并打印每段真实时长。agent 拿到时长后直接写进 data-duration,不用猜。

脚本的意义不只是省 token。凡是需要「精确重复」的动作,都应该从模型上下文里移出去:装依赖的顺序、Chrome 的回退路径、音频时长的测量,这些交给模型每次重做,迟早会漏一步。

输出:机器验收与诚实交付

skill 要求交付前必须核对四件事:

  1. check 的 Lint / Runtime / Audio 都是 0 error;

  2. 抽帧内容随时间变化,证明动画真的在走;

  3. ffprobe 的时长、帧数、音轨符合预期;

  4. 音频的 true peak ≤ -1 dBTP,旁白覆盖对应幕、没有削波。

同时要求明确说出没验证的部分。这不是客套,而是因为 agent 有一个真实盲区:它未必能看图、听声。机器指标能证明「有画面、有音轨、没超时」,但证明不了「构图好看、发音正确」。把这条写进 skill,交付时就不会假装全都验证过了。

踩坑与解决

这个 skill 的规则几乎都是从实际失败里长出来的。下面每条都对应一次真实的翻车。

1. AI 跳过依赖本地化,动画静默不执行

问题:模板默认从 jsdelivr 拉 GSAP,提示词里不写清楚,agent 会直接沿用。
复现:同一份 composition 只把 script src 换成 CDN,check 报 request_failed: net::ERR_TIMED_OUT。
根因:CDN 不可达时库根本没加载,整条时间线不执行——不是动画变卡,是动画不存在。
解决:把本地化写成第 3 步,并规定 <script src="vendor/gsap.min.js">;不用「建议」这种可以被权衡的词。
验证:Runtime 0 error,抽帧哈希互不相同。

2. AI 相信 doctor 的绿勾,Chrome 启动即崩

问题:doctor 显示 Chrome ✓,agent 就认为环境没问题。
复现:check 报 Target closed,底层是 0xC000007B(可执行文件与 DLL 位数不匹配)。
根因:doctor 只检查文件是否存在,不启动浏览器。
解决:skill 直接给出系统 Chrome 的回退命令,并说明「doctor 绿不等于能跑」。
验证:check 能跑完 Layout 的 9 个采样点。

3. AI 忘记声音,或者给音频编一个时长

问题:composition 里没有 <audio>,渲染出来就是静音;或者 data-duration 比音频长,check 报 clip_media_fit。
复现:check 的 Audio 段写 audio_none;或渲染时槽位被裁到媒体长度。
根因:渲染器不会凭空加声音;音频时长必须实测,不能估。
解决:把「默认中文旁白」写进规格;用 make-vo.ps1 生成并打印时长;规定 data-duration 向下取两位。
验证:ffprobe 出现 AAC 流,audioCount: 3、hasAudio: true。

4. AI 在 PowerShell 里塞中文,渲染成乱码

问题:中文台词直接写进命令字符串,或 Get-Content/Set-Content 默认按 GBK 解码 UTF-8 文件。
复现:渲染出来是 鑷墭绠′腑闂翠欢 之类的字。
根因:PowerShell 5.1 的默认编码与无 BOM 的 UTF-8 文件不匹配。
解决:台词写进 UTF-8 .txt;脚本用 [System.IO.File]::ReadAllText(..., UTF8) 读取;改文件用显式 UTF-8。
验证:抽帧里中文正常,不是方框。

5. AI 看不见画面,却假装验收通过

问题:check 通过后,agent 倾向于宣布「完成」,但构图、遮挡、TTS 发音都没看过。
复现:Craft 能过,画面仍可能文字贴边、英文换行难看。
根因:模型不一定具备视觉/听觉输入。
解决:skill 把「没验证的部分」列为必报项,并要求交付抽帧路径让用户抽查。
验证:交付报告里明确区分「机器验证过」与「需要人工确认」。

6. 试跑之后,规则才真正完整

技能写完后,我让另一个 agent 只拿 skill、不给上下文,做一支全新的 15 秒视频。它一次跑通,但报告了 9 个摩擦点,其中最值得修的是:

  • skill 没写「用 starter 覆盖 init 生成的空壳 index.html」,它自己猜的;

  • 它不知道该用哪个中文音色、旁白相对幕起点偏移多少;

  • 它把整片 LUFS(-19.1)当成不达标,而逐条旁白其实已经归一到 -16;

  • render.ps1 早期版本的 stderr 处理会让 PowerShell 打印吓人的红字。

这四条被写回 skill 之后,流程才从「能跑」变成「换了人也能跑」。skill 的测试不是看它第一次能不能成,而是看失败报告里还有多少需要临场发挥。

可迁移的方法论

这套结构不只适用于视频。任何「把经验交给 AI 执行」的场景,都可以照搬:

  • 把失败模式写成步骤,而不是注意事项。 「建议本地化依赖」会被权衡掉;「第 3 步:复制到 vendor」不会。

  • 用默认值代替采访。 大多数需求不需要定制,需要的是先给一版可改的东西。

  • 用可执行底版承载契约。 一份能跑的 starter,比十段解释更能让 agent 理解边界。

  • 把需要精确重复的动作脚本化。 环境回退、时长测量、渲染参数,交给脚本比交给记忆可靠。

  • 验收必须机器可验证。 无法验证的规则最后都会变成口号。

  • 交付时区分「已验证」和「未验证」。 承认盲区不会降低可信度,假装全能才会。

试跑结果

让一个没有上下文的 agent 只用这个 skill 做《为什么用 Docker 部署中间件》:

项

结果

规格

1920×1080、30fps、15.0 秒、三幕

check

Lint 0 error / 3 warning(单文件场景嵌套)、Runtime 0、Layout 0 issue、Contrast 21/21、Craft 0、Audio 0

成片

H.264 + AAC 48kHz 双声道,450 帧,1.27 MB

音频

整体 -19.1 LUFS,true peak -1.9 dBTP,旁白覆盖 0.6–12.7 秒

动画

1.2s / 6s / 11.5s 三张抽帧哈希互不相同

过程

一次通过,无需人工修 composition

成品预览

这不是「AI 会做视频了」,而是「一次描述之后,环境、依赖、音频、验收都不再需要人重复交代」。前者是能力,后者才是工程。

附录:skill 完整源码

以下六个文件与本地安装版本一致,可以直接复制成 ~/.config/opencode/skills/hyperframes-video/ 下的目录结构。

hyperframes-video/
├── SKILL.md
├── assets/starter/index.html
├── scripts/render.ps1
├── scripts/make-vo.ps1
└── references/
    ├── authoring.md
    └── troubleshooting.md

A. SKILL.md:入口与流程

---
name: hyperframes-video
description: 用 HTML/CSS + GSAP 通过 HyperFrames 把一句话需求做成可播放 MP4:片头、文章配套短视频、产品/数据演示、横屏与竖屏动效视频,覆盖离线依赖、headless Chrome、中文字体、配音与渲染校验。只要用户说做视频、做片头、把文章/文案做成短视频、生成 MP4、HTML 转视频、HyperFrames,或反馈视频没声音/渲染报错,就使用本技能;即使用户没提 HyperFrames 也应主动使用。
---
​
# HyperFrames 视频生产
​
把「一句话需求」直接做成可播放的 MP4。默认:15 秒、1920×1080、30fps、三幕、中文旁白。用户只给主题时不要再盘问,按默认规格开工。
​
HyperFrames 的契约很窄:HTML/CSS 负责画面,GSAP 负责时间线,CLI 用 headless Chrome 逐帧截图再由 FFmpeg 编码。真正容易翻车的是依赖,不是动画。
​
## 0. 先定规格(含默认值)
​
| 项 | 默认 |
| --- | --- |
| 画幅 | 横屏 1920×1080;提到抖音/视频号/小红书/竖屏 → 1080×1920 |
| 时长 | 15 秒(3 幕 × 5 秒);信息多可 18–20 秒 |
| 帧率 | 30fps |
| 音频 | 中文旁白;用户没提音乐就不加 BGM |
| 视觉 | 深色底 + 单一强调色;标题 96–120px,正文 ≥28px |
| 字幕 | 屏幕上的关键句就是字幕,不另做字幕轨 |
​
开工前用一句话回述计划(「15 秒横屏,三幕:标题 → 三点 → 结论,中文旁白」),然后直接做。只有主题完全缺失、或必须用品牌/真人素材时才问一个问题。
​
## 1. 环境自检
​
```powershell
node -v          # 需要 >= 22
ffmpeg -version
npx hyperframes@0.8.214 doctor
```
​
`doctor` 报 Chrome 绿不代表能启动。若 `check` 抛 `Target closed` 或 `0xC000007B`,把浏览器指向系统 Chrome:
​
```powershell
$env:HYPERFRAMES_BROWSER_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
```
​
```bash
export HYPERFRAMES_BROWSER_PATH="/path/to/chrome"
```
​
只在当前 shell 设置,不要改全局配置。若系统没有 Chrome,再用 `npx hyperframes browser clear` 后 `npx hyperframes browser ensure --force` 重下缓存。
​
## 2. 建项目
​
```powershell
npx hyperframes@0.8.214 init <slug>
cd <slug>
```
​
项目已有就复用,不要重下。`init` 会尝试从 GitHub 克隆 skills,失败不影响出片;联网后补 `npx hyperframes skills update`。`package.json` 已把 CLI 版本钉死,优先用 `npm run check` / `npm run render`。
​
脚手架生成的是空壳 `index.html`,用本技能自带的底版覆盖:
​
```powershell
Copy-Item "<本技能目录>\assets\starter\index.html" .\index.html -Force
```
​
底版是 15 秒横屏三幕;竖屏就把 `data-width` / `data-height` / `html` 尺寸换成 1080×1920,并加大上下留白。
​
## 3. 依赖本地化(跳过这步大概率卡住)
​
GSAP 默认走 CDN,离线或内网会直接 `net::ERR_TIMED_OUT`,动画整条不执行。装进项目:
​
```powershell
npm install gsap@3.14.2 --no-audit --no-fund
New-Item -ItemType Directory -Force vendor | Out-Null
Copy-Item node_modules\gsap\dist\gsap.min.js vendor\gsap.min.js
```
​
引用 `<script src="vendor/gsap.min.js"></script>`,不要用 CDN。
​
任何 `font-family` 都必须在 CSS 里声明,否则 lint 报 `font_family_without_font_face`。系统字体用 `local()`:
​
```css
@font-face {
  font-family: "Video Sans";
  src: local("Microsoft YaHei"), local("PingFang SC");
}
```
​
字体栈写 `"Video Sans", sans-serif`:一个自定义族名挂多个 `local()` 源,Windows 命中雅黑,macOS 命中苹方,只声明一次。
​
## 4. 写 composition
​
以 `assets/starter/index.html` 为底版(已按契约写好、可直接渲染)。核心契约:
​
- 根元素:`data-composition-id`、`data-width`、`data-height`、`data-duration`,可选 `data-fps`
- 有时间行为的元素:`data-start` + `data-duration`,并加 `class="clip"`(`.clip` 提供整帧盒模型)
- 时间线必须 `paused: true`,注册到 `window.__timelines[compositionId]`
- 旁白/音乐用独立 `<audio>`;`data-duration` 必须贴着音频真实时长,否则 `check` 报 `clip_media_fit`
- 只写确定性逻辑:禁止 `Date.now()`、`Math.random()`、运行时网络请求
​
```html
<div id="root" data-composition-id="main" data-start="0"
     data-duration="15" data-width="1920" data-height="1080" data-fps="30">
  <section class="scene clip" data-start="0" data-duration="5">…</section>
</div>
<script>
  window.__timelines = window.__timelines || {};
  const tl = gsap.timeline({ paused: true });
  tl.fromTo("#title", { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: "power3.out" }, 0);
  tl.seek(0);
  window.__timelines["main"] = tl;
</script>
```
​
画面规则(`check` 的 Craft 段会按这些量化):
​
- 第 0 帧不能只有背景——它是社交平台缩略图,标题一开始就在
- 阅读字号 ≥28px(1080 短边的 2.6%)
- 每行停留 ≥ 0.5 秒 + 0.3 秒/词;结尾句至少留 2 秒
- 每幕 5 秒,最后一幕留出静止时间;3–5 个缓动足够,别堆特效
​
## 5. 音频
​
没有 `<audio>` 就是静音,`check` 的 Audio 段会写 `audio_none`。默认用 Windows SAPI 生成中文旁白(离线,不用装模型)。
​
默认音色是 `Microsoft Huihui Desktop`(zh-CN)。先确认它存在,不存在就让脚本报错后按提示换一个 zh-CN 音色:
​
```powershell
Add-Type -AssemblyName System.Speech
(New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() |
  ForEach-Object { $_.VoiceInfo.Name + " | " + $_.VoiceInfo.Culture.Name }
```
​
步骤:
​
1. 在 `audio/` 写 `vo-1.txt`、`vo-2.txt`…(UTF-8,一句一幕)
2. 运行 `scripts/make-vo.ps1 -AudioDir audio`(英文可用 `-Voice "Microsoft Zira Desktop"`,语速用 `-Rate` 调):生成、去首尾静音、逐条响度归一到 -16 LUFS、转 MP3,并打印每段时长
3. 按打印的时长挂载:
​
```html
<audio id="vo1" src="audio/vo-1.mp3" data-start="0.55" data-duration="3.56"></audio>
<audio id="vo2" src="audio/vo-2.mp3" data-start="5.55" data-duration="2.80"></audio>
<audio id="vo3" src="audio/vo-3.mp3" data-start="10.55" data-duration="3.59"></audio>
```
​
时间换算:`data-start = 幕起点 + 0.5s`。幕起点 0/5/10 → 旁白 0.55/5.55/10.55。`data-duration` 用脚本打印的秒数向下取两位(3.597 → 3.59),别向上取整,否则 `clip_media_fit`。
​
用户自带音频时,用 `data-media-start` 裁剪、`data-volume` 调增益;旁白与 BGM 同时存在时给 BGM 压到 0.1–0.2。没有可用语音时,先交付静音版并明确说明,不要假装有声音。
​
## 6. 校验与出片
​
```powershell
npx hyperframes check
npx hyperframes snapshot --at 1.2,6,11.5
npx hyperframes render -o renders/video.mp4
ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_type,codec_name,width,height,nb_frames,sample_rate,channels -of default=noprint_wrappers=1 renders/video.mp4
```
​
`scripts/render.ps1 -Project <dir> -Output renders/video.mp4` 会把系统 Chrome 回退、check、snapshot、render、ffprobe 串起来。脚本用 `cmd /c` 调用 CLI,避开 PowerShell 5.1 把 npx 的 stderr 当 `NativeCommandError` 的问题;退出码仍然会拦住失败。
​
必须核对:
​
- `check` 0 error;Lint/Runtime/Audio 都要看
- 抽帧哈希或内容随时间变化(证明动画在走)
- ffprobe 的时长、帧数、音轨是否符合预期
- 音频用 `npx hyperframes audio renders/video.mp4`:逐条旁白已归一到 -16 LUFS,整片因为幕间留白常读到 -18~-20 LUFS,这是正常的;验收看 true peak ≤ -1 dBTP、旁白覆盖对应幕、没有削波。该工具按信号而不是语义分类,可能把旁白标成 `sfx`,也会报结尾静音段——都不用管。
​
两个容易误判的输出:
​
- `check` 里的 `Snapshots: disabled` 是默认行为,抽帧要用单独的 `snapshot` 命令;`snapshot` 会自动补一帧 14.75s,正常。
- 中英混排(`docker compose up -d`)不要为了塞下而缩字号;先换行或删字,保持阅读字号 ≥28px,再用抽帧看宽度。
​
Craft 警告(`first_frame_empty`、`text_below_floor`、`line_unread`)不等于失败,但影响可发布性:能修就修,修不了要在交付时说明。
​
## 7. 交付
​
报告:MP4 路径、封面抽帧路径、时长/分辨率/编码/大小、音轨信息。明确说出没验证的部分(例如画面构图、TTS 发音),把决定权留给用户。
​
## 故障速查
​
| 现象 | 处理 |
| --- | --- |
| `font_family_without_font_face` | 给每个字体加 `@font-face`,系统字体用 `local()` |
| `0xC000007B` / `Target closed` | `HYPERFRAMES_BROWSER_PATH` 指向系统 Chrome,或清缓存重下 |
| `net::ERR_TIMED_OUT` 加载 gsap | CDN 不可达,改本地 `vendor/gsap.min.js` |
| `clip_media_fit` | `data-duration` 改成音频真实时长 |
| `audio_none` | 加 `<audio>` 并按第 5 步挂载 |
| 中文变乱码 | 文件按 UTF-8 读写;PowerShell 显式 `[Text.Encoding]::UTF8` |
| `nested_structure_needs_subcomposition` | 单文件短片可接受;要复用就拆 sub-composition |
​
更多细节见 `references/authoring.md` 和 `references/troubleshooting.md`。

B. assets/starter/index.html:15 秒三幕底版

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <script src="vendor/gsap.min.js"></script>
    <style>
      @font-face {
        font-family: "Video Sans";
        src: local("Microsoft YaHei"), local("PingFang SC");
      }
      :root {
        --ground: #0b0f10;
        --ink: #f5f7f6;
        --muted: #8a9a94;
        --accent: #00ed64;
        --card: #141b19;
        --font-display: "Video Sans", sans-serif;
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--ground);
      }
      #root {
        position: relative;
        width: 100%;
        height: 100%;
        overflow: hidden;
        font-family: var(--font-display);
        color: var(--ink);
        background:
          radial-gradient(circle at 82% 18%, rgba(0, 237, 100, 0.12), transparent 42%),
          radial-gradient(circle at 12% 88%, rgba(0, 237, 100, 0.07), transparent 46%),
          var(--ground);
      }
      .grid {
        position: absolute;
        inset: 0;
        background-image:
          linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
          linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
        background-size: 80px 80px;
        mask-image: radial-gradient(circle at 50% 50%, #000 30%, transparent 78%);
      }
      .scene {
        position: absolute;
        inset: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 120px 160px;
      }
      .kicker {
        display: flex;
        align-items: center;
        gap: 16px;
        font-size: 30px;
        letter-spacing: 0.24em;
        color: var(--accent);
        text-transform: uppercase;
      }
      .kicker::before {
        content: "";
        width: 44px;
        height: 2px;
        background: var(--accent);
      }
      h1 {
        margin-top: 40px;
        font-size: 108px;
        font-weight: 700;
        line-height: 1.12;
        letter-spacing: -0.02em;
        text-align: center;
      }
      h1 em {
        font-style: normal;
        color: var(--accent);
      }
      .sub {
        margin-top: 36px;
        font-size: 34px;
        color: var(--muted);
        letter-spacing: 0.06em;
      }
      .rule {
        width: 220px;
        height: 3px;
        margin-top: 56px;
        background: linear-gradient(90deg, var(--accent), transparent);
        transform-origin: left center;
      }
      .steps {
        display: flex;
        gap: 36px;
        margin-top: 64px;
      }
      .card {
        width: 400px;
        padding: 44px 40px 48px;
        border: 1px solid rgba(255, 255, 255, 0.08);
        border-radius: 20px;
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), var(--card);
      }
      .card .idx {
        font-size: 30px;
        font-weight: 700;
        color: var(--accent);
        letter-spacing: 0.1em;
      }
      .card h2 {
        margin-top: 22px;
        font-size: 42px;
        font-weight: 600;
      }
      .card p {
        margin-top: 16px;
        font-size: 30px;
        line-height: 1.6;
        color: var(--muted);
      }
      .quote {
        position: relative;
        max-width: 1320px;
        padding-left: 48px;
        font-size: 76px;
        font-weight: 600;
        line-height: 1.35;
      }
      .quote::before {
        content: "";
        position: absolute;
        left: 0;
        top: 8px;
        bottom: 8px;
        width: 6px;
        border-radius: 3px;
        background: var(--accent);
      }
      .credit {
        margin-top: 40px;
        font-size: 30px;
        letter-spacing: 0.14em;
        color: var(--muted);
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="15"
      data-width="1920"
      data-height="1080"
      data-fps="30"
    >
      <div class="grid" id="grid"></div>
​
      <section id="scene1" class="scene clip" data-start="0" data-duration="5">
        <div class="kicker" id="s1-kicker">系列名 · 第 01 期</div>
        <h1 id="s1-title">这里放<em>主标题</em></h1>
        <div class="sub" id="s1-sub">一句副标题,说清它解决什么</div>
        <div class="rule" id="s1-rule"></div>
      </section>
​
      <section id="scene2" class="scene clip" data-start="5" data-duration="5">
        <div class="kicker" id="s2-kicker">三步讲清</div>
        <div class="steps">
          <div class="card" id="s2-card1">
            <div class="idx">01</div>
            <h2>起因</h2>
            <p>先说是哪个真实问题</p>
          </div>
          <div class="card" id="s2-card2">
            <div class="idx">02</div>
            <h2>过程</h2>
            <p>只留关键步骤和命令</p>
          </div>
          <div class="card" id="s2-card3">
            <div class="idx">03</div>
            <h2>结果</h2>
            <p>用可复现的结果收尾</p>
          </div>
        </div>
      </section>
​
      <section id="scene3" class="scene clip" data-start="10" data-duration="5">
        <div class="quote" id="s3-quote">把唯一的结论放在这一屏。</div>
        <div class="credit" id="s3-credit">替换成你的来源或命令</div>
      </section>
    </div>
​
    <script>
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });
​
      tl.fromTo("#grid", { opacity: 0, scale: 1.06 }, { opacity: 1, scale: 1, duration: 1.4, ease: "power2.out" }, 0);
      tl.fromTo("#s1-title", { filter: "blur(14px)", y: 24 }, { filter: "blur(0px)", y: 0, duration: 0.9, ease: "power4.out" }, 0);
      tl.fromTo("#s1-kicker", { opacity: 0, x: -24 }, { opacity: 1, x: 0, duration: 0.6, ease: "power3.out" }, 0.15);
      tl.fromTo("#s1-sub", { opacity: 0, y: 18 }, { opacity: 1, y: 0, duration: 0.7, ease: "power2.out" }, 0.75);
      tl.fromTo("#s1-rule", { scaleX: 0 }, { scaleX: 1, duration: 0.9, ease: "power3.inOut" }, 0.95);
      tl.to("#scene1", { opacity: 0, duration: 0.45, ease: "power2.in" }, 4.55);
​
      tl.fromTo("#s2-kicker", { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.5, ease: "power3.out" }, 5.1);
      tl.fromTo("#s2-card1", { opacity: 0, y: 48 }, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 5.35);
      tl.fromTo("#s2-card2", { opacity: 0, y: 48 }, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 5.55);
      tl.fromTo("#s2-card3", { opacity: 0, y: 48 }, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 5.75);
      tl.to("#scene2", { opacity: 0, duration: 0.45, ease: "power2.in" }, 9.55);
​
      tl.fromTo("#s3-quote", { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.85, ease: "power3.out" }, 10.1);
      tl.fromTo("#s3-credit", { opacity: 0 }, { opacity: 1, duration: 0.6, ease: "power2.out" }, 10.9);
      tl.to("#scene3", { opacity: 0, duration: 0.5, ease: "power2.in" }, 14.4);
​
      tl.seek(0);
      window.__timelines["main"] = tl;
    </script>
  </body>
</html>

C. scripts/render.ps1:校验与出片

param(
  [string]$Project = ".",
  [string]$Output = "renders/video.mp4",
  [string]$At = "1.2,6,11.5",
  [string]$Quality = "looks",
  [string]$Chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe",
  [switch]$SkipSnapshots
)
​
$ErrorActionPreference = "Stop"
​
function Invoke-Hf {
  param([string[]]$Arguments)
  $quoted = $Arguments | ForEach-Object {
    if ($_ -match '[\s&|<>^]') { '"' + ($_ -replace '"', '""') + '"' } else { $_ }
  }
  $cmdline = "npx --yes hyperframes@0.8.214 " + ($quoted -join " ") + " 2>&1"
  cmd /c $cmdline | ForEach-Object { Write-Host $_ }
  $code = $LASTEXITCODE
  return $code
}
​
if (-not $env:HYPERFRAMES_BROWSER_PATH -and (Test-Path -LiteralPath $Chrome)) {
  $env:HYPERFRAMES_BROWSER_PATH = $Chrome
  Write-Host "browser: $Chrome"
}
​
Push-Location $Project
try {
  $code = Invoke-Hf -Arguments @("check")
  if ($code -ne 0) { throw "check failed (exit $code)" }
​
  if (-not $SkipSnapshots -and $At) {
    $code = Invoke-Hf -Arguments @("snapshot", "--at", $At)
    if ($code -ne 0) { throw "snapshot failed (exit $code)" }
  }
​
  $parent = Split-Path -Parent $Output
  if ($parent -and -not (Test-Path -LiteralPath $parent)) {
    New-Item -ItemType Directory -Force -Path $parent | Out-Null
  }
​
  $code = Invoke-Hf -Arguments @("render", "-o", $Output, "-q", $Quality)
  if ($code -ne 0) { throw "render failed (exit $code)" }
  if (-not (Test-Path -LiteralPath $Output)) { throw "expected output not found: $Output" }
​
  Write-Output ""
  Write-Output "ffprobe:"
  $previous = $ErrorActionPreference
  $ErrorActionPreference = "SilentlyContinue"
  ffprobe -v error `
    -show_entries format=duration,size `
    -show_entries stream=codec_type,codec_name,width,height,nb_frames,sample_rate,channels `
    -of default=noprint_wrappers=1 $Output 2>&1 | ForEach-Object { Write-Output $_ }
  $ErrorActionPreference = $previous
}
finally {
  Pop-Location
}

D. scripts/make-vo.ps1:中文旁白

param(
  [string]$AudioDir = "audio",
  [string]$Voice = "Microsoft Huihui Desktop",
  [int]$Rate = 5
)
​
$ErrorActionPreference = "Stop"
​
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
  throw "ffmpeg not found on PATH"
}
if (-not (Test-Path -LiteralPath $AudioDir)) {
  throw "audio directory not found: $AudioDir"
}
​
Add-Type -AssemblyName System.Speech
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
$voices = $synth.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }
if ($voices -notcontains $Voice) {
  throw "Voice '$Voice' not installed. Available: $($voices -join ', ')"
}
$synth.SelectVoice($Voice)
$synth.Rate = $Rate
​
$files = Get-ChildItem -LiteralPath $AudioDir -Filter "vo-*.txt" | Sort-Object Name
if (-not $files) {
  throw "no vo-*.txt files in $AudioDir"
}
​
foreach ($file in $files) {
  $text = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8).Trim()
  if (-not $text) { throw "empty script: $($file.Name)" }
​
  $wav = [System.IO.Path]::ChangeExtension($file.FullName, ".wav")
  $mp3 = [System.IO.Path]::ChangeExtension($file.FullName, ".mp3")
​
  $synth.SetOutputToWaveFile($wav)
  $synth.Speak($text)
  $synth.SetOutputToNull()
​
  $filter = "silenceremove=start_periods=1:start_threshold=-45dB:start_silence=0.05," +
            "areverse,silenceremove=start_periods=1:start_threshold=-45dB:start_silence=0.08,areverse," +
            "loudnorm=I=-16:TP=-1.5:LRA=11"
  $previous = $ErrorActionPreference
  $ErrorActionPreference = "SilentlyContinue"
  & ffmpeg -y -v error -i $wav -af $filter -codec:a libmp3lame -b:a 128k $mp3 2>&1 | ForEach-Object { Write-Host $_ }
  $code = $LASTEXITCODE
  $ErrorActionPreference = $previous
  if ($code -ne 0) { throw "ffmpeg failed for $($file.Name)" }
​
  $duration = (& ffprobe -v error -show_entries format=duration -of csv=p=0 $mp3 2>$null).Trim()
  Remove-Item -LiteralPath $wav -Force
​
  $seconds = [math]::Round([double]$duration, 2)
  Write-Output ("{0} -> {1} ({2}s)" -f $file.Name, (Split-Path $mp3 -Leaf), $seconds)
}

E. references/authoring.md:契约与分镜

# HyperFrames 创作参考
​
## 契约
​
### 根与时间
​
```html
<div id="root"
     data-composition-id="main"
     data-start="0"
     data-duration="15"
     data-width="1920"
     data-height="1080"
     data-fps="30">
```
​
- `data-composition-id` 唯一,时间线注册键必须一致
- `data-start` 是可见性开关:元素只在 `[start, start + duration)` 内出现
- `class="clip"` 不控制显隐,但提供整帧盒模型,lint 会提醒缺失
- `data-track-index` 只影响 Studio 显示,不参与渲染
​
### 时间线
​
```js
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo("#title", { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9 }, 0.2);
tl.seek(0);
window.__timelines["main"] = tl;
```
​
- 根时间线必须 paused,由运行时 seek
- 手动加进根时间线的子时间线不要 paused,否则不会前进
- 支持 `set` / `to` / `from` / `fromTo`
- 常用属性:`opacity`、`x`、`y`、`scale`、`rotation`、`width`、`height`、`visibility`
- 禁止非确定性来源:`Date.now()`、`Math.random()`、fetch 网络数据
​
### 媒体
​
```html
<audio src="audio/vo.mp3" data-start="0.5" data-duration="3.5" data-volume="1"></audio>
<video src="clip.mp4" data-start="0" data-duration="5"
       data-media-start="2" data-volume="0.8" data-has-audio="true"></video>
```
​
- `data-media-start`:从媒体第几秒开始播
- `data-fade-in` / `data-fade-out`:首尾增益斜坡
- 静音素材用 `muted`;有声素材不要加 `muted`,并标 `data-has-audio="true"`
- 同一时间多轨音频会混音,BGM 建议 `data-volume="0.1"` 到 `"0.2"`
​
### 嵌套与变量
​
```html
<div data-composition-id="card-pro"
     data-composition-src="compositions/card.html"
     data-start="5" data-duration="5"
     data-variable-values='{"title":"Pro"}'></div>
```
​
子 composition 在 `<html>` 上声明变量:
​
```html
<html data-composition-variables='[
  {"id":"title","type":"string","label":"标题","default":"Hello"},
  {"id":"color","type":"color","label":"颜色","default":"#22c55e"}
]'>
```
​
脚本里 `window.__hyperframes.getVariables()` 取合并结果;CLI 用 `--variables '{"title":"X"}'` 覆盖顶层。批量出片用 `--batch`。
​
## 分镜套路
​
### 标题幕(0–5s)
​
- kicker(系列/期数)→ 主标题 → 副标题 → 一条强调线
- 主标题 96–120px,2 行内;标题在第 0 帧就可见(可以模糊或位移,但不要透明到看不见)
- 入点:kicker 0.1s,标题 0–0.2s,副标题 0.8s,强调线 1.0s
- 退场:4.5s 淡出
​
### 三点幕(5–10s)
​
- 三张等宽卡片,编号 + 短标题 + 一行说明
- 卡片 5.2/5.4/5.6s 依次上浮,错峰 0.2s
- 卡片正文 ≥28px;宽度不够就减字,不要缩字号
​
### 结论幕(10–15s)
​
- 一句结论,加一条竖强调条
- 小字来源/命令放底部
- 结尾留 2 秒不动的画面,别让最后一帧空着
​
## 节奏与排版
​
- 1080p 短边 1080,阅读字号下限 28px;标题 96–120px
- 每行停留时间 ≥ 0.5s + 0.3s × 词数;结尾句再留 2s
- 安全区:横屏左右 120–160px,竖屏上下 220px(避开平台 UI)
- 背景深色 `#0b0f10` 一类的底,单一强调色(如 `#00ed64`),文字 `#f5f7f6`
- 网格/渐变只做低对比纹理,别抢文字
- 缓动 2–3 种:`power3.out`(入场)、`power2.in`(退场)、强调用 `power4.out`
- 中英混排(`docker compose up -d`)容易超宽:先换行或删字,别缩到 28px 以下;用 `snapshot` 确认实际宽度
- 每幕 5 秒是保守值;10 秒内的片子可以 3 幕 × 3 秒,但台词要更短
​
## 中文字体
​
```css
@font-face {
  font-family: "Video Sans";
  src: local("Microsoft YaHei"), local("PingFang SC");
}
```
​
字体栈写 `"Video Sans", sans-serif`。一个自定义族名挂多个 `local()` 源:Windows 命中雅黑,macOS 命中苹方,只声明一次,也不会在缺字体的平台上报 `Fonts FAILED`。不要依赖未声明的系统字体,lint 会拦。
​
## 音频制作
​
### 旁白(Windows SAPI)
​
```powershell
Add-Type -AssemblyName System.Speech
$s = New-Object System.Speech.Synthesis.SpeechSynthesizer
$s.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }
$s.SelectVoice("Microsoft Huihui Desktop")
$s.Rate = 5
$s.SetOutputToWaveFile("audio/vo-1.wav")
$s.Speak("第一句台词。")
```
​
台词写在 UTF-8 文本里,用 `[System.IO.File]::ReadAllText($path, [Text.Encoding]::UTF8)` 读取,避免 PowerShell 5.1 按 GBK 解码。
​
### 归一化
​
```powershell
ffmpeg -y -i audio/vo-1.wav -af "silenceremove=start_periods=1:start_threshold=-45dB,areverse,silenceremove=start_periods=1:start_threshold=-45dB,areverse,loudnorm=I=-16:TP=-1.5:LRA=11" -codec:a libmp3lame -b:a 128k audio/vo-1.mp3
```
​
### 挂载与校验
​
- `data-start = 幕起点 + 0.5s`(幕起点 0/5/10 → 0.55/5.55/10.55)
- `data-duration` 用 ffprobe 的真实时长,向下取两位,别留余量
- 每条旁白单独归一到 -16 LUFS;整片因为幕间留白,integrated loudness 常落在 -18~-20 LUFS,这不是问题
- 验收看 true peak ≤ -1 dBTP、旁白覆盖对应幕、没有削波
- `npx hyperframes audio renders/video.mp4` 按信号分类,可能把旁白标成 `sfx`,也会报结尾静音段,都属正常
​
### 其他声音来源
​
- 用户提供的 mp3/wav:直接挂 `<audio>`,用 `data-media-start` 裁剪
- 需要 BGM 但没素材:先交付旁白版并说明;不要凭空生成版权不明的音乐
- `doctor` 里的 whisper-cpp / Kokoro / MusicGen 都是可选依赖,未安装不影响出片
​
## 渲染选项
​
```powershell
npx hyperframes render -o renders/video.mp4 -q draft        # 快速预览
npx hyperframes render -o renders/video.mp4 -q looks        # 默认
npx hyperframes render -o renders/video.mp4 --format webm   # 透明背景
npx hyperframes render -o renders/video.mp4 --docker        # 确定性渲染
npx hyperframes render --resolution portrait                # 1080x1920
```
​
- `draft` 先看节奏,`looks` 出片,`delivery` 只在最终交付
- 竖屏用 `--resolution portrait`,前提是 composition 本身是 1080×1920
- 4 workers 通常是甜点;内存紧张用 `--low-memory-mode`
- `--strict` 让 lint error 直接失败,CI 里值得开
​
## 项目结构
​
```text
<project>/
├── index.html          # 主 composition
├── compositions/       # 子 composition
├── audio/              # 旁白、BGM
├── vendor/gsap.min.js  # 本地 GSAP
├── renders/            # 输出 MP4
├── hyperframes.json
└── meta.json
```
​
## 从需求到分镜的映射
​
- 「介绍一个工具」→ 标题 / 三个能力 / 一句结论
- 「把文章做成短视频」→ 文章标题 / 三个要点 / 核心矛盾
- 「数据/产品演示」→ 大数字 / 对比 / 行动号召
- 「竖屏口播」→ 一行一句,字号更大,每幕只放一句话

F. references/troubleshooting.md:故障排查

# HyperFrames 故障排查
​
按报错找条目。每条给:现象 → 根因 → 处理 → 验证。命令默认在项目根目录执行,CLI 版本 0.8.214。
​
## 1. 字体:font_family_without_font_face
​
```text
✗ font_family_without_font_face: Font families used without @font-face declaration:
microsoft yahei, pingfang sc. ... Text will fall back to a generic font ...
```
​
**根因**:CSS 里直接写系统字体名。HyperFrames 只能内嵌「自动解析列表」里的字体,系统字体必须显式声明,否则渲染器不保证拿到同一套字形。
​
**处理**:给每个字体补声明:
​
```css
@font-face {
  font-family: "Video Sans";
  src: local("Microsoft YaHei"), local("PingFang SC");
}
```
​
字体栈写 `"Video Sans", sans-serif`。一个族名挂多个 `local()` 源,Chrome 会按顺序取第一个已安装的字体;比分别声明两个族名更省事,也不会在缺字体的平台报 `Fonts FAILED`。
​
`local()` 的字体名必须和系统里的名字完全一致。用 `src: url(...)` 只在确实要打包字体文件时使用。
​
**验证**:`check` 的 Lint 变 0 error。
​
## 2. 浏览器:0xC000007B / Target closed
​
```text
✗ check_runtime_failure: Protocol error (Target.setDiscoverTargets): Target closed
Chrome exited with code 3221225595 when launched on its own.
0xC000007B: the executable and a DLL do not match (32-bit and 64-bit).
```
​
**根因**:缓存里的 `chrome-headless-shell` 文件在但跑不起来(DLL 位数不匹配的典型症状)。`doctor` 只做存在性检查,所以仍然显示 ✓。
​
**处理**(任选其一):
​
```powershell
$env:HYPERFRAMES_BROWSER_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
```
​
```powershell
npx hyperframes browser clear
npx hyperframes browser ensure --force
```
​
系统 Chrome 的路径常见为 `C:\Program Files\Google\Chrome\Application\chrome.exe`,Edge 在 `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`。
​
**验证**:`check` 能跑完 Layout 的 9 个采样点,不再在启动浏览器时退出。
​
## 3. CDN:net::ERR_TIMED_OUT
​
```text
✗ request_failed: Failed to load npm/gsap@3.14.2/dist/gsap.min.js: net::ERR_TIMED_OUT
2 error(s), 0 warning(s), 0 info(s)
Check failed
```
​
**根因**:模板默认从 jsdelivr 拉 GSAP。CDN 不可达时库根本没加载,整条时间线不执行——不是动画变卡,是动画不存在。
​
**处理**:
​
```powershell
npm install gsap@3.14.2 --no-audit --no-fund
New-Item -ItemType Directory -Force vendor | Out-Null
Copy-Item node_modules\gsap\dist\gsap.min.js vendor\gsap.min.js
```
​
```html
<script src="vendor/gsap.min.js"></script>
```
​
**验证**:Runtime 0 error;不同时间点抽帧内容不同。做对照实验时,只改 `script src` 即可区分「动画写错」和「库没加载」。
​
## 4. 音频:audio_none 与 clip_media_fit
​
```text
ℹ audio_none: The film has no sound: no clip in it plays audio.
```
​
**根因**:composition 里没有 `<audio>`。渲染不会凭空加声音。
​
```text
⚠ clip_media_fit: Audio "vo1" is 3.57s but its slot (data-duration) is 3.70s
```
​
**根因**:`data-duration` 与音频真实时长不一致;渲染时槽位会被裁到媒体长度。
​
**处理**:用 ffprobe 量真实时长,原样写入:
​
```powershell
ffprobe -v error -show_entries format=duration -of csv=p=0 audio/vo-1.mp3
```
​
```html
<audio id="vo1" src="audio/vo-1.mp3" data-start="0.55" data-duration="3.56"></audio>
```
​
**验证**:Audio 段不再报 `audio_none`;渲染日志有 `audioCount` 和 `hasAudio: true`;ffprobe 出现 `aac` 流;`npx hyperframes audio` 看整体 LUFS;逐条旁白归一到 -16,整片因幕间留白常读到 -18~-20,属正常。
​
## 5. 中文乱码
​
**现象**:渲染出来满屏 `鑷墭绠′腑闂翠欢` 之类的字。
​
**根因**:PowerShell 5.1 的 `Get-Content` / `Set-Content` 默认按 ANSI(中文系统是 GBK)解码无 BOM 的 UTF-8 文件,再写回就把中文毁了。这不是 HyperFrames 的问题。
​
**处理**:编辑中文 composition 时用编辑器,或用显式 UTF-8:
​
```powershell
$c = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
[System.IO.File]::WriteAllText($path, $c, (New-Object System.Text.UTF8Encoding($false)))
```
​
SAPI 读台词同理:使用 `ReadAllText(..., UTF8)`。
​
**验证**:`Select-String` 或编辑器里中文正常;渲染后抽帧确认字形不是方框。
​
## 6. 结构警告:nested_structure_needs_subcomposition
​
```text
⚠ <section id="scene1"> is a timeline element that contains nested <div id="s1-kicker">...
```
​
**根因**:根 composition 里一个「有时间」的容器又包了其他元素。Studio 时间线只显示顶层元素。
​
**处理**:单文件 15 秒短片可以接受;如果要多处复用或做长片,把每幕拆成 `compositions/*.html`,用 `data-composition-src` 挂载。
​
**验证**:Lint 警告消失,Studio 时间线每行对应一个 sub-composition。
​
## 7. Craft 警告不是错误
​
- `first_frame_empty`:第 0 帧只有背景。把标题的入点移到 0 秒,或让首帧自带关键画面。
- `text_below_floor`:阅读字号 <28px(1080 短边的 2.6%)。改字号或删字,别用缩放糊弄。
- `line_unread`:每行停留 < 0.5s + 0.3s/词,结尾句还需 2s。拉长镜头或缩短台词。
​
这些是「能不能发」的指标,不是「能不能渲」。时间紧可以先出片,但要在交付说明里写明。
​
## 8. 其他
​
- **`No composition found`**:当前目录没有 `index.html`,或 `-c` 指错了文件。`render` 的位置参数是项目目录,不是文件。
- **`FFmpeg not found`**:装 FFmpeg 并放进 PATH;`doctor` 会打印实际路径。
- **渲染结果和预览不一致**:字体和 Chrome 版本差异;用 `--docker` 做确定性渲染。
- **内存不足/卡死**:`--low-memory-mode`(固定 1 worker、截图采集),或把 `--frames-cache-dir` 指到空间更大的盘。
- **worker 太多反而慢**:4 个通常是甜点,18 核也不建议超过 8。
- **skills 安装失败**:`init` 需要从 GitHub 克隆 skills,失败不影响脚手架;联网后 `npx hyperframes skills update`。
​
## 9. PowerShell 5.1:NativeCommandError 吓人但不致命
​
```text
npx : [hyperframes] check: samples 1/9
+ CategoryInfo : NotSpecified: (...), RemoteException
+ FullyQualifiedErrorId : NativeCommandError
```
​
**根因**:PowerShell 5.1 把原生程序的 stderr 包装成 ErrorRecord;在 `$ErrorActionPreference = "Stop"` 下会直接中断,即使命令本身没问题。HyperFrames 的进度日志走 stderr,所以几乎每次都会触发。
​
**处理**(任选其一):
​
```powershell
# 1. 用技能自带脚本,它已用 cmd /c 规避
.\scripts\render.ps1 -Project <dir> -Output renders/video.mp4
​
# 2. 手动命令套一层 cmd
cmd /c "npx hyperframes@0.8.214 check 2>&1"
​
# 3. 临时放宽
$ErrorActionPreference = "Continue"
```
​
**验证**:看 `$LASTEXITCODE` 和最终的 `Check passed` / `Render complete`,不要被红字误导。
​
> 同理,`npx hyperframes audio` 会把旁白按信号标成 `sfx`,并报告片尾静音段;它只是分析器,不是验收结论。

参考

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

一句话出片:HyperFrames 视频 skill 拆解
/archives/yi-ju-hua-chu-pian-hyperframes-shi-pin-skill-chai-jie
作者
隔壁老陈
发布于
2026-10-29
许可协议
CC BY-NC-SA 4.0

评论