Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

**Long 4K renders and disk space, canvas density, a voice padded to 8 bits, and qa warns about a stale mix**
- Why: lessons from a 457 s HyperFrames canvas film with narration. Its 4K render refused to start (about 57 GB of temporary frames, 53 GB free); a per-frame `setTransform(1, 0, 0, 1, 0, 0)` dropped the 2× scale; padding the voice with `anullsrc` and the `concat` filter quantised it to 8 bits (2,000+ qa click warnings); `bin/vh mix` stopped on a `ding` given a `pitch`, and `bin/vh qa` then reported on the previous mix.wav without a hint.
- engines/README "出 4K": at 4K every capture is a screenshot (drawElement and Linux BeginFrame capture do not supersample), and multi-worker screenshot capture stores every frame as a JPEG beside the output first, an estimated 4 MB a frame; HyperFrames 0.8.82 refuses to start when its estimate is over 90 % of that disk's free space, and checks again from the first 10 frames' real sizes. Bitrate and render time vary a lot with the picture (this canvas film: about 26 Mbps and 1.4 s per second of film; the 3D intro film: about 125 Mbps and 5–8 s), so the "re-encode 4K over about 135 s" line holds for the intro film only. `HF_CAPTURE_PARALLEL_STREAM=true` streams the frames into the encoder (the 13,710 frames took about 11 minutes with 4 workers on an M3 Max), or `--workers 1`; `NODE_OPTIONS=--max-old-space-size=8192` is HyperFrames' own advice when its worker count may outgrow the heap. In the table: reset a canvas transform with `setTransform(dpr, 0, 0, dpr, 0, 0)`, not `(1, 0, 0, 1, 0, 0)` or `resetTransform()`; a WebGL layer drawn into the canvas sizes its canvas and viewport by the same dpr and computes positions in CSS pixels rather than from `gl_FragCoord`.
- `bin/vh qa` warns, never fails, when the mix may be stale: an input written more than 2 s after it (events.json, the beat map, `--events`, `--voice`, `--timeline`, `--words`, the inputs the stems' meta.json names; `qa mix` compares them with meta.json and checks only this), an SFX event landing after its end, or narration ending after it. The warning heads the report and rides on the last line, which the showcase and swatch scripts print.
- tools/audio/README: `t` is where a built-in lands (riser, swish_rev and tape end on it, so a riser starts `dur` earlier); which shaping keys each built-in takes (the four signals take none, nor a `variant`); `bin/vh mix` writes nothing when it stops on an error. Padding a voice with `-f lavfi -i anullsrc` and the `concat` filter can make it 8-bit: the lavfi input decodes as `pcm_u8`, and with the silence first (its length set by `d=` or an `atrim`) concat converts the voice to u8; neither a `pcm_s24le` output nor an `aformat` after concat helps (ffmpeg 8.0.1; silence last, or an input `-t`, happened to stay exact). Use `bin/vh tts --lead`, `adelay` and `apad`, or numpy; with concat, `aformat=sample_fmts=s32` on every input. `bin/vh tts`'s own gaps are unaffected (checked sample for sample). playbook/04 points to `--lead` (it works without `--beats`; the usage lines of `bin/vh tts` and tts.py no longer nest it under `--beats`, and sfx.py no longer says qa always stops on a wrong shaping key), the riser's landing point and the stale warning.

**`bin/vh textcheck`: canvas text covered by the picture, too faint, or out of the frame**
- Why: `npx hyperframes check` measures DOM text only; text a script draws into a `<canvas>` is pixels to it. A 457 s canvas film's own script measured its 106 reads this way and found 3 with too little contrast that frame review had missed. This makes that check a command any canvas film can run.
- A composition opts in with one line, `window.__vh = { draw: (t) => …, texts }` (`texts` optional). Before its scripts run, the check hooks the canvas's `fillText` and `strokeText`, so the film's own text functions stay as they are: kinetic type drawn one character at a time, a line drawn in parts, and a "·" or an arrow drawn as a shape still match the read.
Expand Down
2 changes: 1 addition & 1 deletion bin/vh
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ cmd_check() {
[ "$rc" != 1 ] && [ "$fail" = 0 ] # exit 1 on isolated frames, a spec mismatch or a scale FAIL; the other findings are for review
}
cmd_tts() {
local proj=${1:?usage: bin/vh tts <project> [provider] [voice] [zh|en] [instruct] [--beats map.json --snap beat|half|downbeat --lead s] [--align gemini] [--join block|all] [--resume]} pos=() deps=""
local proj=${1:?usage: bin/vh tts <project> [provider] [voice] [zh|en] [instruct] [--beats map.json --snap beat|half|downbeat] [--lead s] [--align gemini] [--join block|all] [--resume]} pos=() deps=""
shift
while [ $# -gt 0 ] && [ ${#pos[@]} -lt 4 ] && [[ "$1" != --* ]]; do pos+=("$1"); shift; done # positionals stop at the first --flag
local prov=${pos[0]:-qwen} voice=${pos[1]:-} lang=${pos[2]:-} instruct=${pos[3]:-} a prev=""
Expand Down
17 changes: 16 additions & 1 deletion engines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ HYPERFRAMES_RENDER_DETACHED=1 nohup caffeinate -i npx hyperframes render --quali

| 自己就对,不用改代码 | 要写代码 |
|---|---|
| DOM 文字、CSS 的线和边框、SVG | 2D `<canvas>`:宽高乘 `devicePixelRatio`,CSS 尺寸仍是合成尺寸,再 `ctx.scale(dpr, dpr)`,之后坐标照旧按 1080p 写(只开大画布、不 scale,画面会挤在左上角四分之一) |
| DOM 文字、CSS 的线和边框、SVG | 2D `<canvas>`:宽高乘 `devicePixelRatio`,CSS 尺寸仍是合成尺寸,再 `ctx.scale(dpr, dpr)`,之后坐标照旧按 1080p 写(只开大画布、不 scale,画面会挤在左上角四分之一)。每帧开头重置变换也要带上 dpr:`ctx.setTransform(dpr, 0, 0, dpr, 0, 0)`;写成 `setTransform(1, 0, 0, 1, 0, 0)` 或 `resetTransform()` 会清掉这个 2 倍,1080p 下看不出来,4K 下又挤回左上角(清屏、按设备像素整幅贴一张离屏层时可以先用单位矩阵,画完再设回 dpr) |
| `ctx.scale` 之后画的 canvas 内容,包括 `lineWidth`、`measureText` | 拿 `canvas.width`、`image.width`、`getImageData` 去算布局的地方:这些是物理像素,要除以 dpr |
| 4K 的 `<video>` 源(HyperFrames 按源分辨率取帧) | Three.js:`renderer.setPixelRatio(dpr)`,后期链 `composer.setPixelRatio(dpr)` |
| | 先画好、再 `drawImage` 进 2D canvas 的 WebGL 离屏层:它的 canvas 和 `viewport` 也乘 dpr,`drawImage` 的目标尺寸照旧按合成尺寸写;按 1080 开的话,这一层在 4K 下被放大发糊。着色器里的位置按 CSS 像素算(由 `uv` 和这块矩形的 CSS 尺寸换算),不用 `gl_FragCoord`,图案就不随 dpr 变 |
| | 着色器里的 `gl_PointSize` 按设备像素算,要乘 dpr,上限的 `min()` 也要乘 |
| | 1 px 的 WebGL 线永远是 1 个设备像素,4K 下变细;要粗细一致,用 `Line2` 或面片 |
| | `shadowBlur`、`ctx.filter` 的 blur、`UnrealBloomPass` 的核按设备像素算,4K 下光晕变紧;用 `gl_FragCoord` 算的图案频率加倍 |
Expand All @@ -73,6 +74,20 @@ HYPERFRAMES_RENDER_DETACHED=1 nohup caffeinate -i npx hyperframes render --quali

成本(介绍片 103 s,M3 Max):HyperFrames high 画质 1080p 约 4 分钟、509 MB,4K 约 9–13 分钟、1.5 GB(约 125 Mbps,2 个 worker)。发布前用 x264 CRF 18–19、`-preset slow` 重编码:1080p 297 MB,4K 1.04 GB。GitHub release 每个文件不能超过 2 GiB,按 high 画质的码率,超过约 135 s 的 4K 要先重编码。Blender 原生 4K 每帧约是 1080p 的 4 倍(`engines/blender.md`)。

**长片出 4K 先看磁盘。** 4K 一律走截屏(drawElement 和 Linux 的 BeginFrame 截帧都不支持 2 倍密度),而 0.8.82 的多 worker 截屏默认先把每一帧存成 JPEG,最后再编码;只有 `--workers 1` 默认是流式的。临时帧写在 `--output` 旁边的 `work-…` 文件夹里(Windows 上是系统临时目录),按每帧约 4 MB 估:一支 457 s 的片子(13710 帧)要约 57 GB。开渲前它按这个估算查那块盘,超过空余空间的 90% 就直接拒绝(那次空余 53 GB),截完前 10 帧再按实测大小查一次;报错开头是 `Disk capture may need ~… MB of temporary frame storage`。三个办法:

- 设 `HF_CAPTURE_PARALLEL_STREAM=true`:多个 worker 截下的帧直接流进编码器,不落盘。那支片子 4 个 worker 在 M3 Max 上渲了约 11 分钟,high 画质 1.4 GB;
- `--workers 1`:单个 worker 本来就是流式的,只是慢;
- 把 `--output` 指到空间够的盘上。

```bash
HF_CAPTURE_PARALLEL_STREAM=true NODE_OPTIONS=--max-old-space-size=8192 npx hyperframes render --resolution 4k --quality delivery --fps 30 --workers 4 --output out/final-4k.mp4
```

`NODE_OPTIONS` 把 Node 的堆开到 8 GB,这是 HyperFrames 自己的建议:它按每个 worker 约 640 MB、另留 1 GB 来估,这台 M3 Max 默认约 4 GB 的堆够 4 个;自动选出的 worker 比这多时,日志里会打出 `capture workers may exceed this process's V8 heap`,显式写了 `--workers` 就不提醒。那次渲染带着它,没试过去掉。放到后台跑时,照上文在这一行加 `HYPERFRAMES_RENDER_DETACHED=1`。

码率和耗时随画面差很多:这支 canvas 片子的 4K 约 26 Mbps,渲 1 s 片长约 1.4 s;上面的介绍片(3D)约 125 Mbps,要 5–8 s。所以上面"超过约 135 s 的 4K 要先重编码"是按介绍片的码率算的,自己的片子先量再定。

### 最小写法(0.8.82)

`bin/vh hf-init` 搭出来的 `index.html` 就是最小的合成(`bin/vh new short|promo|data|meme` 会自动跑它;`--aspect 4:5` / `3:4` 用竖屏模板,把视口、CSS 高度和 `data-height` 改成 1350 / 1440),手写时照着改;完整样板见 `showcase/02-short-leo-doppler/index.html`。`<body>` 里是这样:
Expand Down
6 changes: 4 additions & 2 deletions playbook/04-audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ bin/vh beats <任意音乐文件> # 外来音乐的节

按句合成,所以每句的起止时间都是**实测**的(`--join` 整段合成时除外,见 `tools/audio/README.md` 的"整段合成")。

**旁白晚一点进来**(片头先有几秒画面):合成时加 `--lead <秒>`,不对拍也能用,第一句从那里开始,前面是真静音,timeline 也按片子的时间写;片尾在 profile 混音里用 `dur=` 补齐或截到片长。合成以后再自己垫静音的,别用 `anullsrc` 加 `concat` 滤镜(会把整条人声悄悄量化成 8 bit),做法见 `tools/audio/README.md` 的"手动的响度和合成命令"。

**同一份稿,中文和英文旁白长度不同**(实测一段 3 句的稿子:中文 9.3s,英文 10.8s)。所以双语成片要各自按 `timeline.<lang>.json` 排时间:
- 画面可以共用,但节奏按语言重排;
- 或者只出一种语言的旁白,配中英双语字幕。
Expand Down Expand Up @@ -105,7 +107,7 @@ bin/vh beats <任意音乐文件> # 外来音乐的节
### 音效(`sfx`)

- **音效是独立的事件层**,和音乐分开:`audio/events.json` 写成 `[{t, sfx, gain_db, pan, dist}]`,后两项可选;内置音效还可以带 `variant` 和塑形参数(见下)。`sfx place` 输出 48 kHz 立体声。
- **`t` 是"落点"**:内置音效各自带落点偏移(例如 whoosh 的峰值、riser 的顶点),摆放时会自动对齐,保证声音峰值和动作在同一帧。
- **`t` 是"落点"**:内置音效各自带落点偏移(例如 whoosh 的峰值、riser 的顶点),摆放时会自动对齐,保证声音峰值和动作在同一帧。riser 因此写在它要冲到的那一刻,声音从 `t − dur` 开始;写成铺垫开始的时刻,整段声音就早了一个 `dur`,峰值落在铺垫刚开始的地方。
- **声像和距离**:事件可以带 `pan`(−1 最左,1 最右)和 `dist`(≥ 1,越远越轻、越闷)。pan 从画面上算,不要凭感觉写:取发声物体在那一刻的屏幕 x,`pan = 2·x / 画面宽度 − 1`,再乘 0.7–0.8 收一点;3D 场景用相机坐标。公式和细节见 `tools/audio/README.md`。
- **来源顺序**:先用有授权的录音素材(在 NOTES 的素材台账里记下来源和许可);缺的类别再用内置库补。自己的立体声素材会先折成单声道,当作一个点声源来摆。内置库有 21 个代码合成音效,都是 MIT 原创,用 numpy 合成,可以复现:
- 转场:whoosh、swish_rev、whip、swoosh_tonal、air、paper、tape、shimmer;
Expand Down Expand Up @@ -156,7 +158,7 @@ bin/vh qa mix $A/stems --beats $A/music.beats.json # 只看混音报告(词

片尾的 `fade` 从 dur − fade 开始压所有总线,最后一帧上的音效也会被压下去:showcase 01 用 0.1 s 时,11.955 s 光圈合拢的那一声被压低了 7–12 dB,改成 0.04 s 才保住它的起音。片尾有动作时,`fade` 要短于它离片尾的距离。

**混音报告**:`bin/vh qa mix <stems>`(或 `bin/vh qa … --stems` 输出的最后一段)逐句列出人声高出音乐多少、每个词有没有被盖住、每个音效相对锚点的响度。硬失败(退出码 1)是:有一句低于 profile 的硬下限;说话时 hero 音效超过它的上限;被盖住的词超过 profile 允许的比例;同一类音效的中位数偏出范围 3 LU 以上;timeline 有句子,人声 stem 却全是静音;一个 cue 对上了却听不见(只以"弱"通过,又被判 `BURIED`)。怎么读、怎么改见 `tools/audio/README.md`。
**混音报告**:`bin/vh qa mix <stems>`(或 `bin/vh qa … --stems` 输出的最后一段)逐句列出人声高出音乐多少、每个词有没有被盖住、每个音效相对锚点的响度。硬失败(退出码 1)是:有一句低于 profile 的硬下限;说话时 hero 音效超过它的上限;被盖住的词超过 profile 允许的比例;同一类音效的中位数偏出范围 3 LU 以上;timeline 有句子,人声 stem 却全是静音;一个 cue 对上了却听不见(只以"弱"通过,又被判 `BURIED`)。怎么读、怎么改见 `tools/audio/README.md`。`bin/vh mix` 报错退出时什么都不写,上一次的 `mix.wav` 还在,所以先看它的退出码;`bin/vh qa` 看到混音比事件表这些输入旧,会在报告开头和最后一行写 `STALE?`,只是警告,退出码不变。

**AAC 编码会抬高真峰值**:每个成片都要量编码后的文件(`ffmpeg -i final.mp4 -af ebur128=peak=true -f null -`),高于 −1.5 dBTP 就用更低的 `tp=` 重混。门禁是 WAV(`bin/vh qa mix.wav …`);成片 mp4 再跑一次,cue 的问题只作警告(`tools/audio/README.md` 的"AAC 编码和 cue check")。

Expand Down
Loading
Loading