本文档回答四个问题:有哪些档位、质量/速度/体积怎么权衡、快门与并行是怎么实现的、4K60 得要多大的机器。 所有档位都是可选项 —— 不传任何新参数时走
balanced,行为等价于旧的 1080p30 默认。
相关入口:scripts/render_video.mjs(渲染器)、scripts/blur_integrate.py(快门积分器)、
scripts/bench_render.py(基准工装)。
通道 = 中间帧存成什么格式,与最终编码质量(--crf / --preset)解耦:
换通道只换中间帧容器,不代表要降成片码率。
硬规则(SKILL.md 确认点 5):动手渲染之前,把下表摆给用户挑,不许默认开工; 选定值写进
consent.json的render_channel,由gate_check.py --phase render挡在渲染之前。 用户在打样阶段选定后可沿用同一轮;换通道/换档位要重新确认一次。
| 通道 | 参数 | 相对速度(纯 CSS 图形帧并行 / 满幅照片帧) | 画质(客观口径) | 中间帧体积(1080p · 纯图形帧实测) | 定位 |
|---|---|---|---|---|---|
| PNG-fast ★默认推荐 | --png-fast |
×1.02 / ×4.4 | 逐像素无损(PSNR 99 dB、最大差 0) | 0.10 MB/帧(8525 帧 ≈ 0.85 GB) | 纯 CSS/MG 图形帧上与 JPEG q95 同速、体积更小、还是无损 → 默认就用它 |
| JPEG q95 | --jpeg --jpeg-quality 95 |
×1.00 / ×13 | PSNR 41.65 dB,低于 x264 crf18 成片自身失真 | 0.12 MB/帧(8525 帧 ≈ 1.0 GB) | 含满幅照片 / 重合成帧、或 4K 终稿时的首选(照片帧上 PNG 是 582ms/帧的黑洞) |
| PNG(1.4.x 默认) | 不带开关 | ×1.16 / ×1.0(基准) | 逐像素无损 | 0.08 MB/帧 | 仅 --profile legacy 逐位复现旧成片、或 master 极限档 |
| JPEG q82 | --jpeg --jpeg-quality 82 |
最快 | 略低于 q95,仍高于多数成片编码失真 | ~0.08 MB/帧 | 只做打样/迭代预览,不用于交付 |
体积口径提醒:上表体积为纯 CSS/MG 图形帧(大面积平色 + 文字)实测;同一批通道在 满幅照片 / 重合成帧上会整体抬高一到两个数量级(精细 PNG 单帧可达 1.6–2.0 MB)。
★ 纯 CSS/MG 图形帧上的实测(2026-10-05 · 1920×1080 · 本机 6 浏览器并行)—— 本文件的默认推荐据此修订: 上表的「×13 / ×4.4 / ×1.0」倍率是满幅照片口径。对纯 CSS/MG 图形帧(大面积平色 + 文字, 也就是本技能绝大多数片子的样子),实测吞吐为 JPEG q95 27.14 帧/秒 · PNG-fast 26.65 帧/秒(×1.02) · PNG 精细 23.41 帧/秒(×1.16), 体积 PNG 精细 0.08 MB/帧 < PNG-fast 0.10 < JPEG q95 0.12。 即:纯图形帧上 PNG-fast 与 JPEG q95 基本同速,而且体积更小 —— PNG 的 deflate 对平色块极其高效, 对「锐利文字 + 纯色块」比对 JPEG 更省。「慢 3×、占盘 8×」只对照片/高熵帧成立。 推论:纯 CSS/MG 片子默认走 PNG-fast —— 拿逐像素无损几乎不付代价; 而 JPEG q95 的「快 13×」在这类帧上并不存在,选它的理由只剩「含照片帧 / 4K 终稿时更稳」。
为什么 JPEG q95 不是「降质」:中间帧要再被 x264 压一次才成为成片,
所以中间帧的相对失真只要低于成片编码自身的失真,就完全不可观测 ——
q95 的 PSNR 41.65 dB 正好低于 x264 crf18 的自身失真,等于「零代价」换来 13× 编码速度与 1/5 磁盘。
而 PNG/PNG-fast 的「逐像素无损」在这一环没有任何可观测收益(它的收益只在
--profile legacy 逐位复现、或中间帧被当素材二次调色的场合才兑现)。
询问话术(照抄,四条都要列全 —— 这是硬要求):
渲染通道你要哪条? ① PNG-fast(推荐 —— 纯 CSS/MG 图形帧上实测与 JPEG q95 同速、体积更小,且逐像素无损) ② JPEG q95(含满幅照片/重合成帧、或 4K 终稿时首选;照片帧上 PNG 是 582ms/帧的黑洞。 不失质:失真低于成片自身编码失真) ③ PNG(最慢,只有要逐位复现 1.4.x 老成片时才选) ④ JPEG q82(最快,只适合打样)
自动推荐口径(给建议,但仍要用户点头):
| 片子情况 | 建议通道 | 理由 |
|---|---|---|
| 默认(绝大多数片子,纯 CSS/MG 图形帧) | --png-fast |
与 JPEG q95 同速、体积更小,且逐像素无损 → 默认就用它 |
| 帧里有满幅照片 / 重合成 | --jpeg --jpeg-quality 95(首选)或 --png-fast |
PNG 在照片帧上是 582ms/帧,是最大的时间与磁盘黑洞 |
| 4K 终稿 | --jpeg --jpeg-quality 95 或 --png-fast |
4K 精细 PNG 单帧 2–6 MB,磁盘与编码都会被吃干 |
| 中间帧必须逐像素无损(合规 / 归档 / 二次调色) | --png-fast |
无损,且纯图形帧上几乎不付速度代价 |
| 看节奏的打样 | --profile draft + --preview |
档位内已含 png-fast,不必再指定通道 |
档位表里的
shot字段(draft/balanced/final=png-fast)保持原样不动 —— 它现在与推荐口径是同一个值:不传通道参数就走 PNG-fast,两者已不再冲突。 只有照片 / 4K 场景才显式加--jpeg --jpeg-quality 95。
为什么换通道是最大的提速杠杆:中间帧编码在浏览器进程内串行,
--concurrency 完全无效(实测并发 1/3/6 路总吞吐 1.80 / 1.86 / 1.87 帧/秒)。
想快只有两条路 —— 换通道,或 --workers 真·多进程。
本机端到端实测(i7-10700 / 16 逻辑核 / 1080p30 / 纯 CSS 图形帧 /
--workers 8):--shutter 0 --jpeg --jpeg-quality 95→ 18.4 帧/秒,8525 帧 9 分 7 秒渲完。 对照:同一片子开默认快门(180° / 8 样本)→ 12–110 帧/分,要 10 小时,且中途写满磁盘 (见下方「开快门之前先把磁盘峰值算出来」)。
--profile 给的是成套预设;任何显式开关(--quality / --fps / --shutter / --workers / --crf / --preset)
都会覆盖档位里的对应项。查看全部档位:
node <skill>/scripts/render_video.mjs . --list-profiles| 档位 | 画质 | 帧率 | 快门 | 截图模式 | CRF / preset | workers | 用途 |
|---|---|---|---|---|---|---|---|
legacy |
1080p | 项目 fps(默认 30) | 关 | png(无损精细) |
18 / medium | 0(单浏览器 + 场景级并发) | 逐位复现 v1.4.x 旧成片 |
draft |
1080p | 30 | 关 | png-fast |
20 / veryfast | 6 | 打样 / 迭代,最快 |
balanced |
1080p | 30 | 180° | png-fast |
18 / medium | 6 | 默认推荐(质量/速度平衡) |
final |
4K | 60 | 180° | png-fast |
16 / slow | 8 | 终稿(耗时约 1080p30 的 8–12×) |
master |
4K | 60 | 180° | png(精细) |
14 / veryslow | 8 | 极限画质(只在最终交付用,很慢) |
legacy 的 fps: 0 表示「不覆盖项目 fps」——所以它渲出来的帧数与旧版完全一致,这是向后兼容的硬证据。
画质只改 deviceScaleFactor,不改任何布局:同一份 HTML 只是采样更密,构图逐像素不变。
--quality |
目标高度 | 1920×1080 项目上的 deviceScaleFactor | 输出分辨率 |
|---|---|---|---|
1080p |
1080 | 1× | 1920 × 1080 |
2k |
1440 | 1.333× | 2560 × 1440 |
4k |
2160 | 2× | 3840 × 2160 |
项目画布不是 1080 高时,scale 按
目标高度 / 项目高度换算。所以--quality是「输出多高」, 与项目本身写的是 1080 还是 720 无关。
--quality 与 --fps 可任意组合,不被档位绑死:
node <skill>/scripts/render_video.mjs . --quality 2k --fps 60 --shutter 180 --workers 8三个旋钮各管一件事,互不重叠:
| 旋钮 | 影响画面 | 影响体积 | 影响耗时 |
|---|---|---|---|
--quality(分辨率) |
清晰度 | ↑↑ | ↑↑(像素平方) |
--fps(帧率) |
流畅度(运动连贯) | ↑ | ↑(帧数线性) |
--shutter(运动模糊) |
运动段不重影 | 几乎不变 | ↑↑(直到"动帧") —— 兜底判据阈值=0,见 §3 |
--crf / --preset |
编码失真 | ↑↓ | ↑(preset 越慢编码越久) |
截图模式(png/png-fast/jpeg) |
中间帧精度 | 中间帧体积 | ↑↑↑(最大杠杆) |
--workers(并行) |
无 | 无 | ↓(≈ 核数上限) |
要点:
- 中间帧编码是最大的耗时项(满幅照片 / 复杂合成帧上,PNG 编码能占 96% 的帧时间,且与并发无关)。
--png-fast(★默认推荐):逐像素无损。本机实测纯 CSS/MG 图形帧上与 JPEG q95 同速 (6 进程并行 ×1.02;体积还更小,0.10 vs 0.12 MB/帧);照片帧口径约 4.4×。 「慢 3×、占盘 8×」只对照片/高熵帧成立。--jpeg --jpeg-quality 95:含满幅照片 / 重合成帧、或 4K 终稿时的首选 —— 照片帧上约 13× 提速、 体积约 1/5;q95 实测 PSNR 41.65 dB,已低于 x264 crf18 自身的失真,成片里不可观测。
preset只影响编码,不影响画质:medium→slow/veryslow是省体积换时间,不是变清晰。 要明显提升清晰度请调--quality或降--crf。- 运动模糊不加体积:它只改「动帧」的像素,被判为静帧的帧原样落盘,码率几乎不变, 但观感提升明显。注意:省下的是体积,不是时间 —— 时间代价见 §3。
--workers只降时间:不改变任何像素输出(帧是确定性的、按帧号分派)。
不是 blur 滤镜,是线性光下的多样本积分。 一个 180° 快门意味着曝光窗 = 半个帧周期:
openS = (shutter / 360) / fps # 曝光窗长度(秒)
t_i = t + openS * (i/K - 0.5) # 在这一窗内均匀取 K 个采样时刻
对每个「动帧」,渲染器在这些时刻各截 1 张,共 K 张(默认 --samples 8),
再由 blur_integrate.py 在线性光下逐像素平均:
- sRGB → 线性光(用 uint16 LUT,不是每像素
pow); - 累加到 uint32;
- 除以样本数、截回 uint16。
为什么必须在线性光下平均:sRGB 是伽马压缩过的,直接平均等于在非线性域做插值, 暗部会偏亮、亮部会偏暗,快速横移的边缘会出现「灰边」。线性光平均才是物理上正确的运动模糊。
快门的代价是「每个动帧 K 次截图 + K 张样本落盘 + 一次积分」。而一部片子里真正需要拖影的 往往只有几处运镜,其余是静态排版或入场动画的尾段。三级闸门从粗到细把它们摘出去:
| 级别 | 开关 | 粒度 | 判据 |
|---|---|---|---|
| ① 场景级 | --shutter-only <id,id> |
一个场景 | 白名单:只有列出的场景开快门,其余走零成本单张(不采样、不落盘、不积分) |
| ② 帧级(可靠) | --motion-hold <px>(默认 1) |
一帧 | 页面导出 window.__motion(t0,t1) 时,位移 < N 个设备像素的帧直接单张落盘 |
| ③ 帧级(兜底) | 自动,无开关 | 一帧 | 没有 __motion 时退回 first.equals(last) 逐字节比对 |
★ ② 和 ③ 差一个数量级。 ③ 的阈值等于 0 —— 快门窗口只有 (shutter/360)/fps ≈ 16.7 ms,
任何亚像素级抗锯齿差异都会让两张 PNG 不等。实测一部 31 场 / 8525 帧的片子,
③ 只判掉 ~20% 的帧(那 20% 是真的逐字节完全没动),剩下 80% 全按 8 样本付费,
截图吞吐从 18.4 降至 2.9 帧/秒(≈6.4×)。② 才是「没用到动效就不开快门」的
可靠做法 —— 它的判据是作者声明的真实位移,不是像素巧合。
要省快门成本,就让帧诚实导出
window.__motion(t0, t1)。 写法见motion-library.md;assets/motion.js的screenTravel(c0, c1)直接返回这段运镜 在屏幕上走了多少 px,套进__motion即可。
⚠ 一个反直觉的坑:为了让静态帧「别死在那儿」,很多人会给整场加一次全时长缓慢推镜。
它每帧只走零点几像素(肉眼毫无拖影),但让每一帧都非零 → ②变成无操作、③必然不等
→ 整场全额付快门成本。慢推镜本身是闸门能摘掉的那类(< 1 px/帧),前提是你导出了 __motion;
没导出,它在大盘上就等于「全场动帧」。
- 样本数仍按
--motion-gap(默认 24 px)从位移反推,夹在--samples-min–--samples-max(默认 4–12)。 - 反过来:快动作 > 80 px/帧 若不开快门,会重影成串(每帧是一个瞬间,人眼补不出来)——
这是唯一真正非开不可的场景,也正是
--shutter-only该点名的场景。
被判定为 hold 的帧不写样本、直接沿用单张 —— 元数据里分别记作
still_frames(总数)· motion_holds(② 判掉的)· shutter_frames(真正走了快门的帧数)·
shutter_scenes(① 白名单,null = 全片开)。这些帧在成片里是干净的硬切
(切场景的那一帧就该是硬的,不该被「糊」)。
看日志核对(截图结束时会打印两行):
截图完成:8525 帧 / 41230 次截图 · 其中 1830 帧是 hold(位移闸门 0 + 逐字节 1830) · 耗时 512s · 16.6 帧/秒
快门覆盖面:8525/8525 帧走快门采样(100.0%) —— 其余 0 帧是零成本单张,不产生采样也不进积分
位移闸门 0是告警信号:说明这片子的帧没导出window.__motion,② 完全没起作用。 只要 ② 真正生效,快门覆盖面会明显小于 100%。
--workers N 启动 N 个相互独立的 Chromium 进程,按帧号分派(worker i 负责 frame % N == i)。
关键:不是在一个浏览器里开 N 个 page。Chrome 的截图编码在一个浏览器进程内是串行的, 同浏览器开多 page 并发是白挂的(拿不到加速)。必须真的多进程。
日志会明确打印 N 个**独立浏览器**进程级按帧步进 以区别于旧行为。
--resume 跳过已经落到磁盘的帧与已经积分的快门目录。判据是「同名帧文件是否存在」
(fs.existsSync),不是 mtime —— 改过帧内容但文件名没变时它不会重渲,
要强制重渲请先把那些帧删掉(或换 --only 点名那几场)。
两条路径都支持(v2.0.6 起对齐):
- 路径 B(
--workers N):把帧展开成清单后过滤掉已存在的帧 / 快门目录; - 路径 A(精细 PNG,单浏览器):按全局帧号判同名帧在不在盘上 ——
shotOne()写的本来就是sc.gf + i + 1,所以判据可以精确到帧; 整场都在盘上时连页面都不加载,直接进入合成。
⚠ v2.0.6 之前,路径 A 的
--resume是静默 no-op:不报错、也不跳帧 (与--workers在useLegacy下同样无效同族)。更糟的是看门狗挂死时的提示 恰恰写着「加--resume可直接接着跑」—— 等于把人骗进第二次白跑。 而精细 PNG 只能走路径 A,所以这条路径恰恰是最需要续渲的那条。
4K 长片渲到一半挂掉时,重跑加 --resume 即可从断点继续,不重头来。
每渲 N 帧(默认 150)重启一次浏览器,4K 长片防内存泄漏 / OOM。
blur_integrate.py 用多进程(默认 min(核数-1, 12))跑积分,与截图阶段解耦,同样支持续算。
- 每个
setupPage/ 截图 / 启动 / 关闭都带超时(HX_SHOT_TIMEOUT_MS,默认 60s); - 单帧失败最多重试 3 次(
HX_FRAME_RETRY),仍失败则重启浏览器再试; - 全局看门狗:若
SHOT_TIMEOUT + 45s(默认 105s)内没有新帧产出,直接退出码 3 并 按路径给出续跑办法,而不是无限挂着。 两条路径都必须按「帧」打点:路径 B 在doneFrames++处,路径 A 在逐帧循环里pokeWatchdog()。v2.0.6 之前路径 A 一次都没打点,而它的doneFrames又只在 整场渲完才累加 → 看门狗量到的其实是「单场耗时」,等价于 「单场超过 105s 即判挂死」。实测 1080p 精细 PNG 约 3.5 帧/秒 ⇒ 任何超过 ~12 秒的场必被误杀, 且丢掉的正是那一场的后半段(见lessons.md#132)。
基准工装会合成一个复杂度可控的项目(N 场 × 每秒动元素),在同机上多档位各渲一遍:
python <skill>/scripts/bench_render.py \
--out .scratch/bench --scenes 2 --sec 1.0 --fps 30 \
--configs legacy,draft,balanced,4k30,4k60 [--heavy] [--json]fps= 渲染帧数 / 截图耗时(不含编码、不含积分)——看的是截图吞吐;- 相对
legacy的加速比才是重点,绝对值随机器和帧内容变化很大; --heavy打开「满幅渐变 + 多重 shadow + 半透明叠加」模拟高熵帧(照片型), 这是 PNG 最慢、--png-fast/--jpeg收益最大的场景。
★ 先看真实片子的端到端,微基准只用来比档位之间的相对关系。 2026-10-05 一条 3 分 20 秒 / 1080p30 / 5982 帧的纯 CSS/MG 片子(本机 6 浏览器进程、 快门 180°/8 样本只开 3 场),端到端实测:
--png-fast(默认)469.2 s ≈ 7 分 49 秒、--jpeg --jpeg-quality 95460.7 s(差 +1.85%)。派生:全片平均截图吞吐 16.4 帧/秒、平均单次截图 30.5 ms、快门只占 13.7% 耗时(落在 35.3% 的帧上)。 下面 5.1 的合成项目只有 2 场 × 1.0s,切场 / 预载开销占比过高,绝对吞吐不可外推到整片 —— 拿它比档位、比回归可以,别拿它估一条片子的墙钟。
工装会自动把每个档位的
--fps同步到项目声明的总帧数(layout._total.total_frames)—— 渲染器采信声明帧数(有意为之,避免帧数分歧导致 QC 假截断),所以工装必须同步改它, 否则4k60这种档位会按旧 fps 的帧数渲染、时长少一半,基准就不可比了。
2 场 × 1.0s、1080p 项目、--workers 由档位决定:
| 档位 | 输出 | fps | 帧数 | 截图 s | 积分 s | 总 s | 帧/秒 | 加速比 | 体积 MB | 截图模式 |
|---|---|---|---|---|---|---|---|---|---|---|
legacy |
1920×1080 | 30 | 60 | 21.4 | 0.0 | 23.6 | 2.8 | ×1.00 | 0.38 | png |
draft |
1920×1080 | 30 | 60 | 6.2 | 0.0 | 8.0 | 9.7 | ×3.47 | 0.37 | png-fast |
balanced |
1920×1080 | 30 | 60 | 16.6 | 7.1 | 27.5 | 3.6 | ×1.29 | 0.49 | png-fast |
4k30 |
3840×2160 | 30 | 60 | 16.9 | 5.9 | 26.4 | 3.5 | ×1.26 | 0.49 | png-fast |
4k60 |
3840×2160 | 60 | 120 | 25.4 | 8.8 | 37.5 | 4.7 | ×1.69 | 0.57 | png-fast |
三个关键结论:
- 并行比分辨率更划算:
4k30(4× 像素)的截图吞吐(3.5 帧/秒)几乎追平 1080p 的balanced(3.6 帧/秒)—— 因为 4K 档配了 8 个浏览器进程。legacy慢不是因为 1080p 贵,而是因为它是单浏览器串行; 换成多进程后,连 4K60 全片都只要 37.5 s。 draft是最强的加速杠杆:无快门 +png-fast+ 6 进程 = 9.7 帧/秒(×3.47)。 迭代期一律先用draft看节奏,定稿再上balanced/final。- ★ 快门会让「动帧」按 K 倍付费,而「哪些帧算动帧」不能指望自动判定。
(本行旧版写的是「hold 静帧只截 2 张即跳过,所以几乎不加时间」—— 已被 2026-10-05 的实测推翻:
兜底判据
first.equals(last)的阈值等于 0,一部 8525 帧的片子里只有 ~20% 被判成静帧。) 默认档选balanced而非draft,换来的运动段观感提升仍是成倍的 —— 但要真省下时间, 得靠--shutter-only/--motion-hold两级闸门,见 §3。
数字是本机量级,随机器与帧内容(纯 CSS 图形 ↔ 满幅照片)能差 10× 以上。 换机器后重跑同一命令对比自己的基线,别把这张表当承诺值。
这是「同速的前提下多拿到新能力」的依据(同一台机器、同一份 108 帧项目):
| 指标 | 优化前 | 优化后 | 说明 |
|---|---|---|---|
balanced 总耗时 |
70.4 s | 33.0 s | 快 2.1× |
| 其中积分 + 清理 | 53.6 s | 8.5 s | 快 6.3× |
| 逐帧并行加速比 | — | 10.2×(12 进程) | 证明并行真有效 |
| 样本目录清理 | ~121 s(96 文件) | 0.62 s | 见下 |
三个根因与修法:
- 并行被"删除"拖垮:旧实现把「删样本目录」放在积分的关键路径上, 而本机对用户目录下的批量删除有实时扫描节流(96 个文件要 ~121 s)。 → 把清理移出关键路径;改用整目录 rename 到系统 Temp 再删(0.084 s)。
- 积分本身吃内存带宽:逐像素
pow线性化 + 大量中间数组。 → 换成 uint16 LUT + uint32 累加,数值逐位不变(实测 PSNR 138.1 dB、最大差 0)。 - 刚写完的 PNG 被扫描器占用:保存偶发
PermissionError。 →_save_png带指数退避重试;探测灰度时用步进抽样(rgb[::8,::8])不再 reshape 大数组。
结论:v2.0 的默认档不是「加了功能所以变慢」,而是在同速的前提下多拿到了运动模糊、并行、断点续渲。
4K60 的原始工作量是 1080p30 的 4×(像素)× 2×(帧数)≈ 8×,再叠加更大的 PNG 编码,约 8–12×。
但实测墙钟差距远小于此,因为 4K 档配了更多并行进程 —— 本机 4k60(120 帧 / 8 进程)总耗时 37.5 s,
而 1080p30 的 balanced(60 帧 / 6 进程)是 27.5 s,只差 1.4×。换句话说:
并行能在很大程度上吃掉分辨率的代价,4K60 不是「8 倍慢」,而是「多核机器上完全可做」。
读表提醒:基准里各档位的
--workers不同(legacy1 /draft·balanced6 / 4K 8), 所以横向比总耗时是端到端体验,不是"同并行度下的纯分辨率代价"。要比纯分辨率代价,请固定--workers再跑。
| 资源 | 建议 | 说明 |
|---|---|---|
| CPU | ≥ 8 物理核(--workers 8 才吃得满) |
截图是 CPU 密集;核数 = 并行上限 |
| 内存 | ≥ 16 GB(长片 32 GB 更稳) | 每个 Chromium 进程数百分 MB–1 GB+ |
| 磁盘 | SSD,预留 10–80 GB | 有快门护栏时峰值 ≈ --shutter-flush × 1(默认 ≤8 GB);关掉护栏才会出现"全片之和",见下 |
| 浏览器 | Chrome / Edge 任一 | 自动探测;无需额外安装 |
快门把每个动帧拆成 --samples(默认 8)张样本图写进 render/shutter/f_XXXXXX/,
积分与清理原本只在全部截图结束之后做一次 —— 于是采样体积在截图阶段单调堆积,
峰值不是「最后一帧」而是全片之和:
不加护栏时的峰值 ≈ 帧数 × 样本数 × 单样本体积 + 帧数 × 单帧体积
实测(1080p / 满屏图形帧):单样本 1.33 MB、最终 JPEG 帧 0.23 MB(PNG 帧 1.6 MB)。
一部 284 s / 8525 帧的片子用默认快门(8 样本)→ 采样约 90 GB、加最终帧 ~104 GB。
两个后果:① 跑到一半 ENOSPC;② 采样涨到 ~20 GB 后速率从 110 帧/分掉到 12 帧/分
(进程活着、日志不报错、chrome 一个不少)—— 看起来像"渲染卡住",其实是磁盘 I/O 在打架。
--shutter-flush <GB>:样本攒到阈值就全体停手做一次「积分 + 清理」,然后一起继续。
峰值从「全片之和」压到「阈值 × 1」。
# 默认按可用磁盘自动取:可用空间的 25%,夹在 1–8 GB
node <skill>/scripts/render_video.mjs . --shutter 180 --shutter-flush 4
node <skill>/scripts/render_video.mjs . --shutter 180 --shutter-flush 0 # 关掉护栏(旧的"全程堆积"行为)- 必须是真屏障,不能只等"碰巧大家都空闲":8 个 worker 的帧间空隙只有微秒级,
"全部同时空闲"在长片里几乎不会自然发生(第一版护栏整片只触发了一次,等于没护栏)。
现在:攒够阈值 → 举
flushReq→ 各 worker 在帧边界停手 → 等active === 0且全员到齐 → 由最后一个到齐的人执行积分 → 统一放行。带FLUSH_WAIT_MS超时兜底, 凑不齐就放行退回堆积 —— 宁可慢,不可死锁。 - 为什么必须全体停手:
blur_integrate.py的收尾会把 shutter 根目录整个 rename 到 Temp 再删 (0.08 s,比逐个文件删快两个数量级)→ 期间绝不能有新样本落盘,否则连新样本一起删; 也不能有"写了一半的目录",否则会拿残缺样本积分、产出错误的模糊。 - 护栏只在并行路径(
--workers N)生效;单浏览器路径会打警告。 - 实况写进
out/<slug>.render.json:shutter_flush_bytes/shutter_flushes/shutter_peak_bytes—— 事后核对"到底堆了多少"看它,不用猜。 - 护栏不增加总积分工作量(总帧数不变),只是把原来集中在片尾的一次积分摊到途中; 代价是每次积分会暂停截图(本机 8 帧约 5 s;一次 276 帧约 109 s)。
⚠ 看门狗联动:积分期间没有新帧产出,而默认看门狗 105 s 内无新帧就判挂死退出 —— 一次 276 帧的积分要 109 s,不喂看门狗的话第一次积分就会被杀掉。渲染器已在积分前后
pokeWatchdog()。任何"长时间不产新帧但确实在干活"的新阶段都要记得喂它。
| 配置 | 截图吞吐 | 全片耗时 | 中间产物峰值 |
|---|---|---|---|
--shutter 180(8 样本,无护栏) |
1.8 → 0.2 帧/秒(越跑越慢) | ~10 小时(且写满盘) | ~104 GB → 中途 ENOSPC |
--shutter 180 --shutter-flush 4 |
由积分占比决定 | 截图 + 全部积分串行 | ≤ 4 GB |
--shutter 0 --jpeg q95 ★ |
18.4 帧/秒 | 9 分 7 秒 | 1.9 GB |
判据:先算峰值再对
df。不够时按代价从小到大选:①--shutter 0(丢掉运动模糊, 但 1080p30 的 MG 图形帧上肉眼几乎无感,且提速 8× 以上);② 降--samples; ③ 调小--shutter-flush;④ 分场景批渲。 排查顺序:先看du -sh render/shutter和df,再怀疑代码。
4K 强制建议:
- 中间帧用
--jpeg --jpeg-quality 95或--png-fast—— 4K 精细 PNG 会把磁盘和编码时间吃干; q95 的失真低于 crf16 自身,看不出。 - 一定加
--resume—— 4K60 长片中途失败概率不低,续渲省掉整轮重跑。 - 一定加
--recycle 150(或更小) —— 4K 长片不重启浏览器容易 OOM。 --workers别超过物理核数 —— 超了会互相抢 CPU 反而更慢。
# 4K60 终稿:JPEG 中间帧 + 续渲 + 定期重启
node <skill>/scripts/render_video.mjs . --profile final \
--jpeg --jpeg-quality 95 --workers 8 --recycle 120 --resume \
--audio audio/narration-full.mp3| 变量 | 默认 | 作用 |
|---|---|---|
HX_SHOT_TIMEOUT_MS |
60000 | 单次截图/启动/关闭的超时;挂死机器可调小 |
HX_FRAME_RETRY |
3 | 单帧最大重试次数(超过则重启浏览器) |
浏览器挂死的排查顺序:调小 HX_SHOT_TIMEOUT_MS → 降 --workers → 加 --resume 重跑。
| 你现在要干嘛 | 用什么 |
|---|---|
| 快速看节奏、迭代画面 | --profile draft --preview N |
| 出正式 1080p 成片 | --profile balanced --audio … |
| 要 2K/60 | --quality 2k --fps 60 --shutter 180 |
| 出 4K60 终稿 | --profile final --jpeg --jpeg-quality 95 --workers 8 --recycle 120 --resume |
| 像素级对比旧版 | --profile legacy |
| 验证本机性能 | python scripts/bench_render.py --configs legacy,draft,balanced |