3D 摄像机字幕
将 After Effects 动态效果(3D 摄像机、景深、路径文字、Posterize Time 15、运动模糊)重建为面向 HyperFrames 的确定性逐帧绘制器。一个时钟同时绘制底图、遮罩和每个字形。无需任何 CSS 动画。
何时使用 / 何时不使用
- 使用 在静态摄像机的说话人头像片段(虚拟化身或真人),横向构图,中景或更宽景别,每段 5–20 秒。文字必须在说话人周围按深度演绎。
- 不使用 用于纯字幕、移动或手持摄像机(遮罩和底图变换假设画面固定)、极端特写(头部后方无空间)或者人物抠图模型无法分割的主体。
必需条件与副作用(完整列表)
- 工具:
- Node 22+ 以及固定版本的 HyperFrames CLI,
npx [email protected](从registry.npmjs.org下载)。以下所有命令均使用该确切版本。 - 环境 PATH 中包含
ffmpeg。 - Python 3,并安装
fonttools、brotli、numpy、pillow。
- Node 22+ 以及固定版本的 HyperFrames CLI,
- 遮罩:
npx [email protected] remove-background本地运行。首次运行时会下载人物分割模型(约 170 MB)至~/.cache/hyperframes/。 - 渲染时间: 合成在预览/渲染时从
cdn.jsdelivr.net加载 GSAP 3.14.2。 - 文字时钟: 用户已在本地运行的任意词级转录工具。起始时间准确性很重要;纯 Whisper 的起始会延迟。
- 字体: 用户提供的文件。请确认许可证允许嵌入。
- 手绘风格(可选): 需要安装并设置
p5-paint-animation技能(其设置会下载固定的 puppeteer、Chrome for Testing、p5 和 p5.brush)。build-sprites.py会无头运行;帧会被写入该技能的out/目录,随后被删除。 - 其他: 无需凭据、无付费调用、无任何上传。文件仅写入项目目录(
assets/、renders/)。
准备工作(每个片段执行一次)
- 截取与底图。
bash scripts/prep-take.sh <take.mp4> <start_s> <dur_s> <project>/assets会生成:portion.mp4:音频源。plate-tall.mp4:反射填充,每侧 240 px,顶部 480 px,底部 240 px,确保飞入、推拉和后退时不会露出边缘。person.webm:Alpha 遮罩,帧数相同。- 脚本随后会将底图和遮罩与截取片段进行比对(尺寸、帧率、帧数),如有任何不匹配则报错。
- 文字时钟。 截取片段的词级时间戳(以截取起始为基准的秒数)。
- 字体。 选择 2–3 个角色(字幕 / 英雄词 / 环),然后执行
python3 scripts/font-metrics.py assets/metrics.js key=font.woff2[@wght=…,opsz=…] …。- 可变字体必须在此处传递其轴,并在 CSS 中固定(
font-variation-settings、font-optical-sizing: none)。否则浏览器的 opsz 会跟随字号,导致文字重叠。
- 可变字体必须在此处传递其轴,并在 CSS 中固定(
- 轮廓。 在放置任何内容之前,先读取
person.webm遮罩中的头部框和肩线。
构建
- 将
assets/kit/{cam3d.js,tables.js,finish.js}和metrics.js复制到项目的assets/目录。 references/worked-film.html是完整的参考影片。它包含两个镜头、一个身体擦除、两种环类型、一个飞越、结局和调色;其媒体文件不包含在内。references/recipes.md包含深度优先分组、环绕环、阶梯和手绘文字的配方。C3.init(metrics, {W, H}):- 镜头保持教程的垂直视野(F = 1326.1·H/1080),因此
tables.js可在任意宽度下不缩放应用。 - 在宽度大于 1440 的帧上,将
(W − 1440) / 2加到wipe.x。
- 镜头保持教程的垂直视野(F = 1326.1·H/1080),因此
镜头语法
选择 4–6 个节拍。将每个口语从句拆分为其自己的组。
| 节拍 | 构建 | 规则 |
|---|---|---|
| 飞入重击 | camA 表(19 帧,从 502 px 低处开始)然后 linear 蠕动 | 底图 S0 1.1–1.3;画面永不落地 |
| 深度组 | 每个从句 2–3 个字幕组,位于 D 1250 / 1500 / 2200 | 近处更大且阴影更重;蠕动和推拉揭示视差 |
| 英雄词后方 | 英雄词放在底部深度(底图层)的后面层,跨头部排版 | 头部隐藏中间字母;≥ 60% 保持可读;上升字母保持在画框内 |
| 视差推拉 | swing 片段,tx 420 | 旧布局在 3 帧内淡出;下一个布局通过视差到达 |
| 身体擦除 | 说话人本身的遮罩,brightness(0) blur(),前缘来自 wipe.x | 镜头 A 裁剪在带子左侧,镜头 B 右侧;两侧均拉出 |
| 环绕环 | 说话人前方的水平圆环(中心 Z 2000),从上方 11–16°,旋转 | 词语在说话时从侧面小字出现,绕到前方,然后稳定;下半弧翻转以保持正立可读 |
| 飞越 | 短语位于 D 1500 / 2000 / 2500,摄像机在它们之间摆动 | 焦点跟随短语 |
| 阶梯式结局 | 推入片段;字幕词沿对角线越来越深越来越小 | 推入使得近处词语分开 |
规则
- 运动:
- 文字逐帧为 15 fps(其自身动画和它看到的摄像机)。底图和遮罩每帧移动。
- 运动模糊为 7 个重影,覆盖半个 15-fps 步长。在旋转环上使用
shutter: 0.5。 - 字幕比语音领先 0.2 秒,并对齐到 15-fps 网格:
fr(t) = 2·round((t − 0.2)·15)。 - 入场上升
ENTRY:字幕 30 px,英雄词 110–260 px。环词在路径上缓动最后 30–44 px。仅在相邻词尚未就位时使用RING_ENTRY的过冲。
- 深度:
- 每个组都有其自己的 D。底图 3000,字幕 1250–2400,环绕环 ≈ 1100–2700。
- 深度阴影
[6, 16, 0.5, 1500]按 1500/(D − cz) 缩放。 - 景深 σ = 6.7·|D − focus|/D px(1080 高度)。平字幕限制为 1.2 px;环字形接受全模糊量。
- 大小:
size/rise是着陆摄像机(tland)下的屏幕像素。精灵以其屏幕大小渲染。 - 间距:
- 环文字始终通过
ringLine(屏幕前进量;类型:track 1.10,间隙 26–30;手绘:track 1.0,间隙 28)。 - 旋转环每帧重新布局。
- 环文字始终通过
- 遮挡:
- 环绘制在说话人前方。
- 仅在文字 PREDICATED 要经过头部后方时,才将单词拆分为后/前实例(
side,zSplit)。
- 可读性:
- 字幕放在画面较暗的一侧。
- 跨越皮肤或浅色衣物的文字添加深色光晕。
- 屏幕上不显示结尾句号。
- 调色(可选):
finish.js提供曲线、肤色饱和度 1.12 和每帧重新滚动的颗粒。将不透明场景(底图 + 后方文字 + 遮罩)放入带滤镜的包裹器。为前方文字应用相同调色,颗粒叠加在其上,这样拆分为后方/前方的单词保持同色(参见references/worked-film.html)。
样式
- 编辑体: 紧凑的显示衬线体,英雄词用斜体;字幕用文本衬线体;奶油色搭配一种强调色。
- 粗体: 英雄词用粗犷的黑体,字幕用轻量体,一种饱和强调色(参考影片)。
- 手绘: 来自
scripts/hand/build-sprites.py的 p5.brush 手写精灵:- 每个字在起始时自行书写。
- 环将整个词放置,旋转到切线方向并按深度缩放,放在更大、更扁平的环上。
- 配方见
references/recipes.md。
验证
npx [email protected] check . --no-contrast。重影层会触发 WCAG 对比度检查;飞入和擦除接缝是故意重叠的。- 在预览页面中,粘贴
scripts/frame-bounds.js并运行__frameBounds(0, dur)。- 必须返回
[]:在可见的 5+ 步内,没有组超出画框。
- 必须返回
- 粘贴
scripts/glyph-gaps.js并在静止时运行__glyphGaps(t)。- 词间没有接触。
- 平铺单词测量为一个框(下降字母会导致误报)。斜体 f 的悬伸部分为负值。擦除带两侧的单词都会显示。
- 词间距:渲染的词间距 ≈ 一个空格的前进量。
- 为每个节拍截图,使用
npx [email protected] render . --crf 12渲染,并检查每个节拍的全分辨率帧。
限制
- 遮罩边缘在细发上会有闪烁。飞入时镜像填充会短暂可见。
- 占据整个画框的说话人后方空间很小:可放在页面顶部高度,或藏在肩膀后。
snapshot --at共享一个页面,因此颗粒在截图之间不会重新滚动。在渲染时评估颗粒。- 使用调色和大量重影层渲染的成本大约是普通渲染的 2 倍。