动效 KINETIC
一套视觉语言 + 零依赖逐帧渲染器。每一帧都是时间的纯函数:画面、字幕、音效、配乐共用一条时间轴,没有手打关键帧。
它从两条爆款动效片里提炼而来(拆解见 references/viral-laws.md),并已在 tasks/opus-kinetic-20261001/ 跑通一条完整片(examples/opus-kinetic/)。
五条规律(每个镜头都要过一遍)
- 一句一象:每个语义节拍给一个观众"一眼认出"的具体物件——做内容的人认得框选框、声波、关键帧、时间线、渲染条、口型、提示词框。运动方式也要有意思:「没有」就划掉,「推翻」就推倒,「藏到身后」就真的藏到人物身后。
- 踩点:大字落在说到它的那个音节(
at('词'));纯画面段每个切点落在音乐拍上(beat(k));每个可见动作都有一个对应音效,音效只回应可见动作。 - 一线到底:全片一条光——开场是点燃的扫描线,中间是框选线、声波基线、穿过场景的动效,结尾收成一个点写出落版后熄灭。能变形就不硬切。
- 张弛:密集段后必须有留白(一个全静音的拍、一个骨白底的小字);一个持续变化的数字撑起悬念(本片是"人手打的关键帧:0")。
- 质感:严格四色、子帧运动模糊、辉光、色散脉冲、团簇颗粒;工具痕迹式的小标注让人愿意暂停。
语言规格
| 项 | 规格 |
|---|---|
| 色板 | 夜 #0A0A0D · 骨白 #F1EDE4 · 信号橙 #FF5A1F · 钴蓝 #3448FF(只给"机器/数据")· 灰 #8B8983 · 石墨 #26262C |
| 字体 | Noto Sans SC(中文,900 砸字 / 500 正文)· Archivo VF(拉丁与数字展示)· JetBrains Mono(标注)· 宋体只用于留白拍 |
| 字号下限(1920×1080) | 标注 ≥28px、说明 ≥36px、大字 ≥76px;成片缩到 390px 宽仍要读得清 |
| 八个动作 → 音效 | 点燃 click · 拉线 zip · 扫描 scan · 框选 blip/select · 砸字 impact/hit · 解码 tick · 吸入 suck · 冲击波 drop |
| 人物状态 | 满幅(W 1.0 / M ~1.2 / C ~1.4 跳切推拉)· 框选拖拽成窗 · 角落小窗(右下)· 抠像入景(字在人后) |
| 字幕 | 一个自然分句一张卡、无标点、型号连写(Opus5.5);单层夜色实底 8px 圆角 46px;未读灰 → 已读骨白连续扫色,扫色前沿带信号橙光 |
| 安全 | 任何在人物前方的字/图形与真实头部框(含帽子)保持 ≥30px;人物后方的字(抠像层之下)不受限。用 qa_face.py 证明,不靠目测 |
流程(命令细节见 references/pipeline.md)
- 脚本 → 节拍表:先写自然口播,再给每个语义节拍选一个物件(references/devices.md)。不为了动效改口播。
- 配音与人物:按项目工作流生成(Fish Audio → HeyGen 等)。真人实拍直接用原片。
- 对齐:
tools/align_script.py—— 脚本为准、ASR 只给时间;CJK 全局对齐 + 静音感知插值(ASR 会把句首字拉进前一个停顿,会把5.5的点当句号)。 - 剪辑:
tools/build_edit.py—— 整体变速(画面声音同步,口型不受影响)、长气口压缩成跳切、插入纯画面段,输出唯一timeline.json(所有下游只认它)。 - 人物资料:抽帧 →
tools/vision_analyze.swift(Vision 人像蒙版 + 脸/嘴框)→tools/prep_media.py mattes|faces|thumbs|barcode。 - 写片子:一个
film.js模块(参照示例)。每个元素锚定at('词')或beat(k);用engine/devices.js的词汇;ev(t, 'sfx')在同一处登记音效与后期脉冲。 - 验证:
render.mjs --mode stills抽帧看图 →--mode qa+qa_face.py(0 违规才继续)。 - 声音:
render_dialog.py(各来源响度匹配)→--mode cues→sfx.py→mix.py(节拍点剪配乐、人声压低、无人声推高、静音拍、静态增益 + 真峰值限幅到目标响度)。 - 成片:
render.mjs --mode frames分块无损母版 → 一次 H.264 编码 → 项目机械检查。
真人实拍怎么用
同一套方法:抠像后让大字从人物身后穿过、把口播里的数字变成可见的计数器/进度条/圆环、跳切处做推拉、原声整体响度匹配。示例的 10 秒案例就是一段 4K 实拍口播(references/pipeline.md)。
封面(3:4 / 4:3)
同一个引擎出静帧,和成片一个味道(命令见 references/pipeline.md,写法见 examples/opus-kinetic/cover.js):
- 人物:从数字人原片挑一帧——正脸、睁眼、自然笑。先用
faces.jsonl的嘴框高宽比和正脸程度粗筛,再看图定。无损截帧 →vision_analyze … 1.0(全分辨率蒙版)→prep_media.py cutout(把墙面颜色从发丝、帽檐边缘分离,深色底上不出白边)。 - 版式:标题 ≥150 px(按 1080 宽算),缩到 180 px 宽仍要读得清;成片字标藏在人物身后(3:4 用刊头式,帽顶压住字的下沿);标题句末落一颗光点。另外最多加 2–3 个成片里的小物件(选框手柄、图层标签、划掉的 REC、一行统计)。字和帽檐不要相切:要么明确叠压,要么留 ≥40 px。
- 安全与平台:
--mode qa+qa_face.py必须 0 违规;画面底部约 10% 留给平台的播放量、时长角标。 - 模块第一行调用
setSize(w, h);版式数字按脸的中心摆放,换一帧不用改代码。
已踩过的坑(别再踩)
- ASR 会漏掉片段开头的字:送 ASR 前补 1 秒静音,再减回去。
- 版本号里的点(
5.5)不是句读;对齐与字幕都要按数字内小数点处理。 ffmpeg -ss … -c copy会退到前一个关键帧,片段起点不准;需要精确起点就解码抽帧/抽音频并用互相关验证。- 帧缓存会回收 ImageBitmap:长期使用的图(缩略图、点云源)用
loadImage单独持有。 - TTS 人声峰值很尖(PLR 可达 24 dB),直接提音量会削波;先做语音式动态响度归一(
loudnorm带真峰值上限),再混音。 - zsh 不按空格拆变量:渲染分块用 bash 脚本。
- 动态
loudnorm会让人声/音乐的平衡"呼吸";母带只用静态增益 + 限幅。 - 配乐换成完整版 WAV 后,19 kHz 以上的内容(加上限幅器自己产生的)会让 AAC 编码后的真峰值比母带高约 1 dB:
mix.json的loudness.lowpass: 19000放在母带链最后,编码后仍守住上限。 - 预览版换完整版前先互相关核对对齐(Epidemic 的预览与 WAV 逐样本对齐),再按两者响度差微调
fullDb,人声与配乐的比例就和审过的版本一致。