把产品短片做成可执行规范:封面、节奏、音效、动图四件套
一条 52 秒的产品短片,从「靠感觉做」变成「按规范复现」:四件套的实测参数、为什么每段时长必须是 GIF 的整数遍、音效为什么全部本地合成而不进素材库、指针为什么要放大到界面宽的 4.5%,以及用「承接(carry)」替代「淡入淡出换片」的改造过程。
给一个桌面工具做宣传短片,第一次做完之后,用户(也就是我自己)给的反馈是四句话:封面满意、节奏满意、音效满意、动图满意。于是当场把它定成标准:以后都按这套做。
但"满意"是一句感受,不是规格。真正让标准落地的,是把这条片子里每一个可复制的决定写成参数。这篇文章是那份规范的公开版本 —— 四件套的规格、背后的取舍,以及后来被打回重做的两个地方。
一、反面教材:为什么要有规格
在没有规格之前,每做一条片子都是一次重新发明:版式随手定、段落时长靠感觉、音频要么忘了铺要么糊成一片。
结果就是看起来每一镜都不错,连起来像在翻 PPT —— 这句话不是我说的,是 onetake(一个专治"动效像 PPT"的开源技能)对这类成片的判词:
标题卡、淡出、界面镜头、淡出、功能卡片、淡出、Logo。每一镜单看也许都不错,合起来还是像在翻 PPT。原因是画面和画面之间没有任何联系,后一个场景只是把前一个替换掉了。
它把这件事量化了:给每个段落边界算"承接分"(carry score)—— 边界处有没有东西活下来、并且可见地变成下一段。它自己发布的片子 0.83 通过,而"节奏完美但像 PPT"的 v1 是 0.00,直接判不合格。
这套判据后来成了我改造自己管线的准绳,下面第三节会讲。
二、四件套规格
1. 节奏:六段式,时长由素材决定
开场卡 4s → 痛点卡 6s → 六段功能演示(各 7~10s)→ 结尾卡 6.5s ≈ 52s
演示段的时长不是随便定的,有一条硬规矩:
每段时长必须取 GIF 的整数遍。
演示动图是循环播放的。如果一段目标 8 秒、而 GIF 本体 2.92 秒,播 2.7 遍就切走 —— 观众会看到"同一个操作播到一半又从头开始"。取整数遍(这里是 2 遍 = 5.84 秒)之后,循环接缝正好落在段落边界,看不出循环。
代价是每段时长被素材"吸附"到最近的可选值,总时长会有几秒浮动。这个代价必须付 —— 时间轴精确到 0.1 秒,但观众记住的是"那个动作播了一半突然跳回去了"。
2. 音效:全部本地合成,不进素材库
这条是"听着舒服"的关键,也是最反直觉的一条:转场音效不是素材库里挑的,是算式算出来的。
每个段落边界铺一声 whoosh(0.45 秒):
# 一阶低通白噪 → 偏粉噪;固定随机种子,保证每次重建音效一致
prev = 0.94 * prev + 0.06 * white
env = min(1.0, t / 0.09) if t < 0.18 else 1.0 - (t - 0.18) / 0.27
buf[idx] += (white - prev) * 0.07 * env
BGM 走外部音乐,但电平由代码控:volume=0.48、淡入 2 秒 / 淡出 3 秒、整轨峰值归一到 0.95(只做防削波)。这样任何一首 BGM 进来都是同一个响度关系,不会出现"换首歌就盖住画面节奏"。
还有一条容易被忽略的:最后一个段落边界不铺音效 —— 留给结尾卡的淡出。边界音效是"换段"的信号,而成片结尾要的是"结束",多一声 whoosh 反而把收尾切碎。
3. 封面:文字压进安全区
B 站投稿封面要过两刀:首页是 16:9、个人空间是 4:3,只设一个比例约等于没设(会被换成视频首帧,表现为"黑封面")。所以设计稿按 1146×717 做,关键文字一律压在 X=72 起、Y 185~640 的安全区内。
视觉上用的是"深色渐变 + 三处光晕 + 右侧真实界面微倾 2°":左侧四层文案(钩子 → 痛点 → 主张 → 数据行),右侧贴着真实界面截图作为证据。
一个实现细节值得记下来:径向光晕如果直接把模糊后的 alpha 铺满图层,模糊会把 alpha 糊到图层边界被硬切,画面上留下一条可见的直角分界线。正确做法是让椭圆只占图层中间一半、模糊半径也小于到边缘的距离。
4. 动图:真实操作 + 放大指针
演示 GIF 的"真实性"是刻意的:用 gdigrab 录真实窗口渲染,用 SendInput 注入真实键鼠事件 —— 和手操走完全相同的输入路径,不是摆拍截图。每个场景先重启程序,保证起点一致、可重复录制。
但本机有个坑:gdigrab -draw_mouse 不生效,录出来的画面里根本没有指针。于是改成自研方案:记录注入过的光标轨迹,转 GIF 时逐帧叠加一个放大的指针。
GIF 合成参数:宽 1400、12fps、palettegen=stats_mode=diff + paletteuse=dither=bayer:bayer_scale=3。中间录像保留,调参数走 --gif-only 不重录。
三、被打回的两个地方
标准定完的当天,用户回看后又提了两条:转场相对单调、鼠标不太丝滑。
两条都不是"再加点效果"能解决的,都有确切的根因。
转场单调:屏幕级只有一个动作
原版六段演示全是「静态背景卡 + 居中 GIF 循环 + 整屏淡入淡出」—— 把每一段拆开看,屏幕级只有一个动作(fade),剩下的运动全发生在 GIF 内部那一小块。
拆解参考片(vibestart)发现它靠的是三种连续运动:卡片位移进场(从右侧滑入,不是淡出淡入)、卡片堆叠展开(一张横卡变成三张并排的斜向构图)、界面持续滚动(长页面顺滑下滚而不是切屏)。共同点是:没有一帧是死画面。
改造后:段落之间改用 xfade 左滑(位移 + 淡化组合,0.5 秒),静态卡加极缓慢推近(Ken Burns,整段放大 3%)。
这里有个工程细节:9 段的长滤镜链能不能跑通要先用两段验证。ffmpeg 的长滤镜链在长视频里有中段崩溃的前科(94 秒处 exit -1、无报错),所以先用 4s + 6s 两段测:4 + 6 - 0.5 = 9.5s / 285 帧 完全符合预期,再上全链。
还有一处必须联动改:音效的落点。原来音效铺在"段边界",改成 xfade 之后边界变成了 0.5 秒的交叉区间,音效要铺在交叉的中点(视觉上"换段"的那一刻)才对得上。
鼠标不丝滑:三个根因,都在我们自己的代码里
用户的原话是"鼠标也不太丝滑,注意:下次也要给鼠标加上放大特效"。查下来三个原因:
① 移动是瞬移的。 SetCursorPos(x, y) 一步到位,中间没有任何过程。后期叠加的指针因此是"闪现",不是"移动"。
② 指针位置没有插值。 轨迹取值的实现注释写得很直白 —— "取最近的前一个采样"。于是叠加出来的指针是阶跃跳动:在同一位置停几帧,然后突然跳一格。
③ 放大得不够。 指针边长是 72 物理像素,落到成片里只剩 25~31 像素(1280 宽下的实测值)。对比参考片的指针约 20 像素 —— 没有量级差,所以观众看不出"放大特效",只觉得是个普通的鼠标。
修法对应三条(这也是后来写进规范的部分):
move()改成分步缓动移动:按距离算步数(约每步 5 像素),smoothstep 缓动,每步 6~10 毫秒且每一步都记轨迹点;时长随距离增长但封顶 0.45 秒- 轨迹取值改成相邻采样之间线性插值(二分查找定位区间)
- 指针提到 100 物理像素(约占界面卡宽度 4.5%,验收线是不低于 3.5%),并加柔和投影 + 白描边,再加一个点击脉冲圈(点击瞬间在指针脚下扩散一个圆环,0.34 秒)
顺带修掉一个隐患:轨迹和点击数据以前只存在内存里,--gif-only 复用录像时会把指针和脉冲全丢掉。现在它们随录像落成一份 JSON —— 调参数不需要重录,键鼠也不用再被独占。
四、验收:把"感觉"换成判据
规范的最后一段是验收清单。它存在的意义是:别用感觉验收感觉。
- 帧数 == 时长 × fps(本次 1645 == 54.834 × 30)
- 整片
-f null解码无错 - 音轨峰值留余量(本次 mean −16.0 dB / max −5.9 dB,无削波)
- 每段时长 == GIF 整数遍
- 指针相邻帧位移连续(不允许"0 → 大跳变")
- 封面抓回线上比对,不看后台预览
- 元数据用接口复核(标题 / 时长 / 标签 / 分区 / 封面)
最后两条尤其重要:发布以"线上抓回"为验收。本地构建通过、后台显示成功,都不算数。
五、留下的那条边界
改造完成后我做过一次对照:旧版的指针在相邻帧之间大跳、又小又平;新版逐帧连续递进、明显更大 —— 这条对比图留下了。
但同时要诚实说清楚规范里没做到的部分:我们做到的"位移 + 淡化"只是承接(carry)的近似 —— 真正的承接是"同一个元素变形进下一段",那需要在 HTML 里重建界面、用逐帧渲染而不是录屏(onetake 走的就是这条路,它的 carry 分能到 0.83)。
录屏路线有它不可替代的价值:说服力来自真实证据。所以现在的分工是:真机功能演示走录屏 + 这套规范;纯动效的概念片,才轮到渲染路线。
规范本身也在版本里:v1 是"满意的那版",v2 是"被打回两条之后修的版",改了什么、为什么改、验证数据是多少,全部写在同一份文档里。下次开工前先读它,而不是重新发明一遍。