REEL-FROM-SCRIPT · v7 是做什么的?
一个根据你未编写的脚本、需要逆向分析的参考素材,以及一个毫无历史记忆的全新对话,制作一段 9:16 竖屏短片——它看起来经过精心导演,且绝不会出现制作事故。
v5 引入了布局并将设计硬编码到其中。v6 将所有数值移入了 PARAMS.json,并在渲染前放置了一个验证器。v7 是两个真实短片教会 v6 的成果——六色域循环、封面海报、每场卡片动画、连续场景跨度、自适应转场、音频陷阱,以及一个拒绝交付不良成片的单一 ./build.sh + scripts/qc.py。
承诺: 人类唯一编辑的文件是
PARAMS.json。唯一有权说 "不" 的是prepare.py/qc.py。其他一切都是生成的,并且每个生成的内容都可以通过一个命令复现。
0 · 如果你是新手:前10分钟
export PATH=/home/user/node22/bin:$PATH # Node >= 22
cd <project> && ./build.sh --from render # 或从头开始遵循第3节
然后按此顺序阅读:第1节(外观如何构建)→ 第3节(管道)→ 第6节(设计手册)→ 第9节(故障目录)。不要自己发明布局:骨架就是设计。
永远不要做这四件事: 手动输入时长(测量配音)、将十六进制或像素值粘贴到合成中(修改 PARAMS.json)、手写 index.html(生成它)、或未经查看联系表就交付)。
1 · 为什么输出看起来是设计的(七条法则)
这些是该系列每个好镜头背后的实际决策。它们是规则,不是品味。
1. 一个固定到底部的锚定网格。 导语 → 主角 → 正文 → 芯片 → 主持人。下半部分从不移动:芯片始终处于同一纵坐标,主持人始终在相同位置被画框裁切。只有上半部分会重新构图。这就是为什么九个不同的场景读起来像一部电影。
2. 主角是一个词,不是一个句子。 1-3 个词,使用紧凑的显示字体,约占帧宽的 17-25%,行距紧密(0.88)。完整的句子放在芯片中(3-6 个词,供静音观看者阅读),细节放在卡片中。如果你不能用 3 个词说清楚,那么这个节拍就是两个节拍。
3. 每个节拍都使用不同的背景。 六个色域(奶油色 / 栗色 / 近黑色 / 鼠尾草色 / 海军蓝 / 薰衣草色)循环,确保没有两个相邻的节拍相同。颜色变化本身就是转场的另一半。prepare.py 如果发现两个连续场景共享一个色域,则会构建失败。
4. 证据,而非装饰。 卡片的存在是为了展示事物:文件列表、差异对比、浏览器、上传、仪表。如果一个节拍没有产物,则它是 type-led 类型,并且没有卡片。一张只有两行的卡片看起来有缺陷;四到六行看起来是刻意的(或者占卡片面积 ≥ 55% 的模拟图)。
5. 第一帧是海报。 场景 1 在 t=0 时构图(data-cover="1"):导语、主角、卡片和角色已经出现在屏幕上,芯片在 0.65 秒时着陆。一个以空白域开头就开始播放的短片会失去滚动观看的用户。
6. 动画仅限变换,并且尽早着陆。 入场动画持续 0.24-0.34 秒,使用 power2/3/4.out 缓出函数,并在节拍的前半部分完成。永远不要动画文本内容,永远不要对数字进行补间,永远不要让一个 1.5 秒的场景花费 0.6 秒进行转场。主角上的缓慢漂移使最后一秒保持活力。
7. 声音是标点,不是壁纸。 每秒 ≤ 1 个提示音:每次切换时一个弹跳音、UI 上的按键音、两个最大转折点上的渐强音、揭示时刻的钟琴音、行动号召上的通知/叮咚音。背景音乐音量设为 0.15,低于 −16 LUFS 的配音;母带 −14 LUFS / ≤ −1 dBTP。
2 · 契约:什么是固定的,什么是你的
不变量——经过验证,不可凭感觉编辑
| 不变量 | 强制方式 |
|---|---|
| 每个文本/背景组合对可读 | WCAG ≥ 4.5:1,对 10 个渲染对和 6 个色域对 |
| 主角是主角,并且始终适应 | ≥ 120 px,推导尺寸,必须适合 --hero-lines 在 --hero-band 内 |
| 主角位于导语下方,从不接触卡片 | 推导的 band + --gap,硬性失败 |
| 正文在手机上可读 | ≥ 24 px 且 ≥ bodyMin |
| 无内容隐藏在平台 UI 下 | 芯片底部 ≤ 安全区域底部,≥ 55% 可用区域 |
| 芯片永远不会与卡片碰撞 | cardTop + cardHmax ≤ chipTop |
| 主持人始终被画框裁切,从不覆盖芯片 | presenterTop + h > 1.0,presenterTop ≥ chipBottom |
| 两个连续场景永远不共享色域 | 从每个合成中读取 data-field |
| 第 0 帧是封面 | data-cover + 快照门控 |
| 每个场景使用不同姿势 | 每个场景一个姿势文件,在审查时检查 |
| 交付物可播放且音量足够 | qc.py: 分辨率,fps,帧,LUFS,真峰值,静默空洞 |
变量——真正属于你的: 调色板值(6 个色域 + 4 个纸张色调,在对比度允许的前提下)· 格式(9:16, 4:5, 1:1, 720p)· 安全区域插入 · 在地板范围内的每个网格和比例 · 每场景原型 · 场景数量 · 词语 · 时长 · 姿势集 · 提示列表 · 磁盘上的字体。
如果你想要一个验证器无法检查的布局,你需要修改骨架并添加相应的检查。这就是自由的代价,也是输出不可能糟糕的原因。
3 · 管道(顺序不可协商)
| # | 步骤 | 命令 | 为什么在这里 |
|---|---|---|---|
| 0 | 逆向工程参考素材 | 第4节 | 你无法匹配一个你没有测量过的外观 |
| 1 | 事实 | — | 迟到的声明会迫使重新录制 |
| 2 | 脚本 → 章节 | assets/vo/sections.json | 每个节拍一个想法,1.2–9 秒 |
| 3 | 先做配音 | python3 scripts/process_vo.py | timeline.json 是时钟;画面根据它剪裁 |
| 4 | 外观 | PARAMS.json | 调色板,格式,网格,排版地板 |
| 5 | 门控 | python3 scripts/prepare.py | 在对比度 / 排版 / 碰撞 / 色域节奏失败 |
| 6 | 场景 | ./scenes.sh | 每个节拍调用一次 new_scene.py,注入 token |
| 7 | 根 | python3 scripts/build_index.py | 连续跨度,自适应转场,无手写 index.html |
| 8 | 检查 | npx --yes [email protected] lint | 0 个错误是门控 |
| 9 | 查看 | npx … snapshot --at "0,1.5,4,…" | 唯一能捕捉到“正确但丑陋”的步骤 |
| 10 | 渲染 | npx … render --output renders/_raw.mp4 | 对于 1080×1920 的 32 秒视频,软件 GL 约需 1 分 45 秒 |
| 11 | 混音 | python3 scripts/mix_audio.py | 配音 + 背景音乐 + 提示音 → renders/master.wav (−14 LUFS) |
| 12 | 混流 + 交付 | build.sh 步骤 6–7 | mp4 + .srt + 缩略图,然后 qc.py |
./build.sh 一步运行 3 到 12。./build.sh --from render 在配音已测量后跳过配音步骤。
步骤 3 详解——配音是时钟
# 每个章节一个片段,在 assets/vo/ 中命名为 vo1.mp3 … voN.mp3
python3 scripts/process_vo.py
它会修剪静音,仅当设置了 script_target 时才应用 atempo,将每个片段归一化到 −16 LUFS,然后写入 assets/vo/timeline.json:每个章节的 start/dur/end + root_duration。下游任何部分都不能知道此文件中不存在的时长。
使用天城体书写 say 中的印地语,用拉丁字母书写英语技术词汇(resume,job description,ATS score,DM)—— hi-IN 配音能正确识别韵律,同时仍能说出英语单词。caption 是 .srt 文件显示的内容。
步骤 7 详解——根是机械化的
build_index.py 按数字顺序查找 compositions/s*.html,读取测量的跨度,使它们连续(0 → 剪切 → 剪切 → … → 根,这样 VO 中 0.30 秒的间隙就不会产生黑场),在每次剪切前 0.36 秒触发一个转场,转场颜色为即将到来场景的色域,并让最后一个场景持续到音频的尾部。转场时间会缩短到一个短场景长度的 34-62% —— 一个 1.4 秒的场景无法容纳一个 0.62 秒的转场。
步骤 11 详解——两个音频陷阱
alimiter默认情况下level=true:它会自动将电平提升回上限,所以你的“限制器”变成了一个压缩器+增益补偿,混音会落在 −12.5 LUFS,而你的目标是 −14。始终设置level=false。- 在单次线性通道中使用
loudnorm会过冲。使用ebur128测量,精确移动target − measured,然后限制。你可以验证的算术胜过一个会猜测的滤波器。
4 · 阶段 0——逆向工程参考视频
当需求是“观看这个并匹配它”时,测量它。十分钟,五个命令:
ffprobe -v error -show_entries format=duration:stream=width,height,r_frame_rate,codec_type -of default=nw=1 ref.mp4
ffmpeg -i ref.mp4 -vf "fps=1,scale=400:-1,tile=6x6" -frames:v 1 sheet.jpg # 整部电影作为一张图像
python3 - <<'P' # 每秒的色域调色板
from PIL import Image; import glob
for f in sorted(glob.glob("frames/f_*.jpg"))[::2]:
im = Image.open(f).convert("RGB"); w,h = im.size
print(f[-7:-4], im.getpixel((3,3)), im.getpixel((w-4,3)))
P
ffmpeg -i ref.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le audio.wav # 用于脚本
从中读取出:时长和剪辑节奏(每个节拍多长)、色域调色板(采样角落,而非中间)、排版行为(紧凑大写?逐词构建?高亮框?)、布局(角色在哪里,文本块在哪里)、UI 产物(哪些模拟图)、声音(提示音密度)。这六个数字就是整个艺术指导。
如果需求提供了参考素材但没有脚本,那么参考素材就是脚本:转录音频(faster-whisper,int8,base 模型 → 对于 32 秒的印地英语约需 7 秒)并从帧中读取屏幕上的文案。不要重写文案——一个系列有自己的声音。
翻译,不要复制。 这里的参考素材是 1280×720;交付物是 1080×1920。相同的调色板,相同的排版,相同的模拟图——但主角区域、芯片锚点和主持人裁切是作为比例重新推导的。在 9:16 格式中,参考素材的 8-10 个词块会低于 24 像素的正文地板,因此文案被不同地拆分:主角 1-3 个词,芯片一句,卡片细节。
5 · 文件
PARAMS.json 唯一的设计文件
tokens.css 生成的收据(勿编辑)
index.html 由 build_index.py 生成(勿编辑)
SKILL.md · COMPONENTS.md · README.md
build.sh 整个管道
scenes.sh 每个场景,可复现
components/ 7 个卡片主体,可使用 --card-body @… 传递
compositions/
_scene-base.html.tmpl 核心骨架——零字面十六进制,零字面像素
wipe.html 唯一的转场
sN-*.html 由 new_scene.py 生成
_examples/ 三个已实现场景(未挂载:build_index 只读取 compositions/s*.html)
scripts/
process_vo.py 配音 → timeline.json
prepare.py 门控:地板 + token 注入
new_scene.py 基础模板 → 正确的场景
build_index.py 时间线 + 场景 → 根
mix_audio.py 配音 + 背景音乐 + 提示音 → master.wav
make_deliverables.py .srt + 缩略图
qc.py 一个命令,PASS/FAIL 基于 10 个门控
cutout.py · gen_sfx.py 素材
assets/{fonts,presenter,vo,sfx,music}/
6 · 设计手册(实际该选什么)
节拍 → 原型。 证据 → card-led。一个数字 → stat-led(里程表,--digits)。陈述句、过渡、品牌卡片 → type-led。开场钩子和行动号召几乎总是 card-led,因为两者都需要一个产物(证明问题的文件列表;证明行动号召的评论线程)。
节拍 → 色域。 冷暖交替,从不重复。一个实用的六色域循环:accent → ground → ink → secondary → tertiary → quaternary。深色域(ink,tertiary)会使主持人消失,因此那里的光晕设为 0.20 → 0.42(骨架从 data-field 执行此操作);在工具揭示和“AI 完成了工作”的节拍中使用它们,戏剧性有助于效果。
节拍 → 姿势。 相同角色,相同服装,每个场景一个姿势,与台词的 emotional 匹配:耸肩(疑问)· 扶额(问题)· 呈现(提议)· 托腮(好奇)· 回忆(回想)· 竖拇指(胜利)· 指(步骤)· 灵光一现(啊哈)· CTA(请求)。从基础图像生成缺失的姿势,并使用 cutout.py 进行遮罩;保持白色背景规则和大腿中部裁切,以便干净地抠像。
屏幕上的文字。 主角:情绪化的动词或数字(SAME RESUME?,CUSTOMIZE KARO,TAILOR,ATS SCORE)。芯片:静音观看者需要的句子(har company mein bhej rahe ho?)。卡片标题:产物的名称(applied / 12 companies)。卡片元数据:一个微小的事实(this week,matched 3 of 7)。
当一个节拍太短无法设计时(1-2 秒)。 保留场景,去掉第二层:主角 + 芯片 + 主持人,type-led,一次揭示。不要将一张卡片和一个交错列表塞进 1.4 秒——它看起来像故障。
当一个节拍很长时(8-9 秒,通常是 CTA)。 间隔 1.8-2 秒安排三次揭示(评论 → DM → 链接),让缓慢漂移拖出尾巴。一个 8 秒的静态帧是“看起来廉价”的最常见原因。
转场。 一个转场,持续 0.34-0.62 秒,颜色 = 即将到来场景的色域,方向交替。没有其他转场类型:一致性使八个切换感觉像是有作者创作的。
最后一秒。 最后一帧必须是一个经过设计的帧,包含手柄和网站——在那里暂停的观看者应该能够采取行动。
7 · 音频手册
| 时刻 | 提示音 | 增益 |
|---|---|---|
| 每次剪切 | pop | 0.50 |
| 两个最大的转折 | riser 在剪切前 0.3 秒 | 0.28–0.30 |
| UI 元素着陆 | click,key | 0.38–0.45 |
| 列表逐个出现 | key ×N,间隔 0.12 秒 | 0.40 |
| 揭示 / 分数 | chime | 0.36–0.42 |
| CTA | notify,然后 1.7 秒后 ping | 0.32–0.40 |
| 错误 / 拒绝 | error(谨慎使用——它只作为喜剧元素阅读一次) | 0.30 |
提示音偏移量是相对于章节的,在 assets/sfx/cues.json 中("vo7+1.68"),因此重新录制的配音不会使设计失效。永远不要让提示音落在单词的第一个音节上;一个按键音属于该音节开始后的 20-60 毫秒处。