一条 30 秒的成片,表面上看是一个连贯的故事,实际上是六到十段三到五秒的 AI 生成片段拼出来的。拼的时候剪映也好、脚本也好,只管把段落首尾相接,接缝处两边的画面是不是同一个机位、同一刻的动作,没人校验。观众看到的是"这里好像跳了一帧",运营这边却说不清跳在哪一秒、是素材坏了还是剪辑正常。
这篇文章给出一套只读的逐帧体检办法:用 ffmpeg 的 signalstats 滤镜把每一帧与前一帧的差异量打出来,找出异常尖峰,再用前后帧的 PSNR 把尖峰分成"换镜头的接缝"和"同镜头的闪烁"两类。全部结论来自对本机 270 条已发布成片中 14 条样本的一手实测,文末附踩到的两个判据坑。
指标:YDIF 是什么
signalstats 是 ffmpeg 自带的逐帧统计滤镜,其中 YDIF 字段的含义是:当前帧与前一帧在亮度平面上的平均绝对差。画面完全静止时接近 0,剧烈运动时到十几,整个画面构图突变(换镜头)时会冲到几十。
把每帧的 YDIF 打出来的命令是这样的:
ffmpeg -hide_banner -i in.mp4 -an \
-vf "signalstats,metadata=print:key=lavfi.signalstats.YDIF:file=-" \
-f null -
metadata=print 会按帧输出两行:一行帧头(带 pts_time),一行键值(lavfi.signalstats.YDIF=数值)。用 Python 逐行配对解析,就得到一条完整的时间序列——30 秒 24 帧的片子是 721 个点,10 秒的片子是 241 个点。
一手数据:14 条成片的分布
从已发布归档里按时间均匀抽了 14 条(2026-08-29 到 2026-10-29),全部 24fps、720p。统计结果:
| 文件 | 时长 | YDIF中位 | P95 | 最大 | 尖峰数 |
|---|---|---|---|---|---|
| 巨猫占领CBD | 30s | 3.64 | 10.11 | 62.24 | 6 |
| 宿舍电竞房改造 | 30s | 5.99 | 21.61 | 72.34 | 10 |
| 一镜到底玄关擦皮鞋 | 30s | 5.14 | 10.86 | 17.55 | 0 |
| 深夜厨房递草莓 | 30s | 1.88 | 4.37 | 8.34 | 0 |
| 课堂放引力炮 | 10s | 7.03 | 22.28 | 48.29 | 0 |
| 屏幕里点一句她真换了 | 30s | 1.27 | 7.15 | 12.91 | 0 |
| 壁纸她说我犯规 | 30s | 1.02 | 4.09 | 10.02 | 0 |
| 兔女郎机械臂挠痒 | 30s | 2.56 | 7.21 | 61.00 | 5 |
| 夫妻互挠不许笑 | 30s | 0.95 | 3.38 | 50.10 | 3 |
| 下载 (3) | 30s | 4.54 | 24.78 | 72.67 | 7 |
| 换上长裙她照样笑场 | 30s | 0.98 | 7.64 | 10.37 | 0 |
| 顶着狂风她还是按到了 | 30s | 4.60 | 13.67 | 47.55 | 9 |
| 羽毛掸子刚伸出 | 10s | 2.05 | 4.50 | 66.75 | 3 |
| 双人硬扛主持胳肢 | 30s | 2.43 | 8.60 | 65.56 | 6 |
第一个结论直接从表里读出来:YDIF 中位数跨片差了 7.4 倍(0.95 到 7.03)。同样是 AI 生成的短片,有的镜头几乎不动(中位 1.0 上下),有的全程大幅运动(中位 7.0)。所以任何"YDIF 超过 X 就是异常"的绝对阈值都不可跨片复用,门槛必须片内自适应。
第一版判据怎么错的
找异常尖峰的朴素想法:某帧 YDIF 突然抬到基线数倍、且下一帧立刻回落——"回落"说明画面又恢复了正常,那一帧就是毛刺。第一版探针用的正是这个判据,门槛取 中位数 + 8 × MAD(MAD 是中位绝对偏差,比标准差抗尖峰污染)。
结果抽帧目视一查就穿了帮。以"羽毛掸子"那条 1.958 秒的尖峰为例,把前后 0.55 秒按每秒 20 帧拼成网格图看:尖峰前是演播厅全景,尖峰后直接切到了人物特写——这不是闪烁,是机位切换。
判据错在哪?"下一帧回落"只能证明新镜头本身动得少,不能证明画面回到了原来的构图。高运动镜头切到静止镜头,硬切也会长成"孤立尖峰"的样子。这个样本里"夫妻互挠"的三个尖峰比值高达 46 到 52 倍,全是同一类误报。
修正判据:比较尖峰前后两帧
区分"换镜头"和"单帧闪烁",正确的问法不是"下一帧有没有回落",而是"画面有没有回到原来的构图"。做法:取尖峰前 2 帧的图和尖峰后 1 帧的图,用 ffmpeg 的 psnr 滤镜比较:
# t_pre = 尖峰时刻 - 2/24,t_post = 尖峰时刻 + 1/24
ffmpeg -ss $t_pre -i in.mp4 -frames:v 1 -y pre.png
ffmpeg -ss $t_post -i in.mp4 -frames:v 1 -y post.png
ffmpeg -i pre.png -i post.png -filter_complex psnr -f null -
# stderr 里读 "PSNR ... average:xx.xx"
同一镜头延续的话,前后两帧只差三帧的运动,PSNR 通常在 25dB 以上;换了镜头则构图都变了,PSNR 会掉到 15dB 以下。两类之间隔着一条很宽的沟,阈值取 25dB 相当稳健。
一个实现细节:PNG 输入是 RGB 域,psnr 输出里没有 y: 字段,要解析 average:;第一版正则按 y: 写,37 个样本全部解析失败,静默落进了默认分类——这也是个教训,滤镜输出的字段名跟输入的像素格式有关。
分类结果:37 个尖峰全部是接缝
对 8 条有尖峰的片子跑分类,37 个尖峰的前后帧 PSNR 分布在 7.94 到 12.83 dB,无一达到 25dB——全部是换镜头的段间接缝,零个"同镜头闪烁"。
再看尖峰的间隔规律。"羽毛掸子"三个尖峰在 1.958、4.958、7.958 秒,正好每 3.0 秒一个;"双人硬扛"六个尖峰的间隔是 4.17、3.58、4.21、4.04、7.83 秒。全部 8 条片子的尖峰间隔集中在 2 到 5.5 秒,中位约 3.3 秒——这正是主流视频模型单次生成一段的时长。也就是说,30 秒成片里被 YDIF 照出来的接缝,就是生成片段的首尾拼接点。
这个结果对运营有两个直接用处:
- 接缝密度是成片质量的面板。观众感知到的"跳一下"多半发生在接缝处;哪条片子接缝又多又密,哪条就需要重点人工复核。
- 接缝清单是切片复用的索引。想把 30 秒拆成单镜头素材二次分发,接缝时刻表就是现成的切点——比跑一遍场景检测更贴合实际素材边界。
门槛塌陷:第二个坑
"换上长裙她照样笑场"那条的统计很反常:YDIF 中位只有 0.98、MAD 0.41,按 中位 + 8×MAD 门槛只有 4.2,但全片 P95 高达 7.64——结果一个"尖峰"没检出,"硬切"却报了 184 个。
原因是分布重尾:片子前段几乎静止(把中位数和 MAD 都拖得很低),后段持续剧烈运动,大量正常帧越过塌陷的门槛。修法是给门槛加下限:max(中位 + 8×MAD, P95 × 1.5),或者干脆分时段各算各的基线。本机最终版取的是前者,重跑后 184 个假报归零。
落地与边界
这套体检是纯只读的:一条 30 秒片子逐帧统计约 2 秒,抽帧加 PSNR 每个尖峰再花 2 秒,14 条样本全量跑完 33 秒。可以挂在发布闸门的规格体检旁边,输出两样东西:接缝时刻清单(给人工复核)和接缝间隔分布(给切片复用)。
边界也要说清:尖峰法只抓"高运动切低运动"这一个方向的接缝,"高切高"的接缝会落进另一类(下一帧不回落)或者干脆混在正常运动里漏掉——样本里 4 条零尖峰的片子不代表没有接缝,只代表没有这个方向的接缝。想要完整接缝表,还得把"前后帧 PSNR"这个判据推广到全部高于门槛的帧上,代价是多花几倍时间。另外"一镜到底"那条确实全程无接缝(0 尖峰 0 硬切),片名即证据,倒是省了复核。
一句话总结:逐帧差分找异常,前后帧 PSNR 定性质,门槛片内自适应——三步把"好像跳了一帧"变成一张可以拿去复核的时刻表。