LLM长视频理解管线:18万帧去重到41张关键帧
两小时的课程视频,25 帧每秒解出来是 180,000 张图。这个数字第一次跳出来的时候我盯着终端愣了几秒——按每张 200 KB 落盘就是 36 GB,别说喂给 LLM,光是写进磁盘都够呛。所以我做了一件很反直觉的事:不追求"看得更多",而是想办法把绝大多数帧直接扔掉,最后只留下 41 张。这套 LLM 视频理解管线我开源了,端到端跑完一门 2 小时课程大概 20 分钟,折合 5.9× 实时。
这里说的"实时"是这么理解的:视频本身时长 7200 秒,处理墙钟时间约 1220 秒,两者相除得到 5.9。数字不算惊艳,但比起"逐帧送模型"的做法已经是两个量级。这篇文章我想讲清楚三件事:那 41 张图是怎么被找出来的、这套管线的骨架为什么这么切、以及我在实测中翻过哪些车。适合正在做视频检索、课程知识库、会议纪要自动化的同学,也适合手上有一堆长视频不知道怎么下手的人。
1. 180,000 帧里真正携带信息的那 41 张,是怎么被找出来的
先说结论:41 张不是调出来的魔法数字,是"这门课实际讲了多少个稳定画面"的自然结果。理解这一点,后面所有参数选择都会变得顺理成章。
1.1 先算一笔账:长视频的信息密度低得离谱
2 小时等于 7200 秒,按 25 fps 到 180,000 帧。但如果按每秒抽 1 帧,候选集立刻缩到 7200 张,压缩比 25:1,而且几乎不损失"讲了什么"这个层面的信息——因为人说话一秒钟最多也就吐三五个词,画面在一秒钟内通常没有实质变化。
7200 张还能再砍。用场景切分找出"画面内容发生结构性变化"的时刻,一门正常节奏的课程大概落在 800 到 1200 个镜头之间。做完这一步,你会发现剩下的绝大多数镜头其实是同一页幻灯片被鼠标划了几下、光标闪了几下、讲师摄像头里人换了个姿势。这些差异在像素层面存在,在语义层面为零。
最后一级过滤是近似去重。我把每个镜头的代表帧转成 64 位感知哈希,汉明距离小于等于 6 的判为同一页。跑完这门课,剩下 41 张。平均每张代表 175 秒的视频内容,也就是接近三分钟一页——这个数字和这门课的 PPT 页数基本吻合,说明管线没有丢东西,只是把重复的丢干净了。
1.2 三类冗余要分开处理,别用同一把刀
很多人做抽帧只考虑"抽多少帧",但真正决定质量的是你打算消掉哪种冗余。我把它分成三类,每一类的处理策略完全不同:
时间冗余 指的是相邻帧之间超过 99% 的像素是一样的。这类冗余用场景切分和帧率降采样就能消掉,成本最低。
空间冗余 指的是同一时刻画面里有多路信息:讲师小窗、幻灯片主体、底部字幕条、右上角水印。这类冗余不是"删"的问题,是"选"的问题——你得决定哪块区域是信息主体。我的做法是把幻灯片区域作为 ROI,其余区域在判重时降权,但存档时保留全帧。
语义冗余 最麻烦,指的是换页动画、渐入渐出、鼠标轨迹这些"画面在变但内容没变"的情况。这类冗余没法靠单帧特征解决,必须靠多帧一致性投票。
把这三类分开之后,参数就不再是玄学,每一类冗余对应一组可解释的阈值。
1.3 "压缩"的准确定义:建立视觉索引,而不是丢帧
我特别想强调一点:这套管线的输出不是"41 张截图",而是"41 个带时间区间的视觉索引项"。每一项长这样:
| 字段 | 含义 | 示例 |
|---|---|---|
| frame_id | 索引号 | 17 |
| t_start | 起始时间(毫秒) | 4213500 |
| t_end | 结束时间(毫秒) | 4388000 |
| image | 代表帧路径 | frames/00017.jpg |
| phash | 感知哈希 | 0x3f8a... |
| dup_count | 被合并的重复帧数 | 317 |
| ocr_text | 该帧 OCR 文本 | "第 4 章 缓存穿透" |
| caption | 视觉模型描述 | 幻灯片列出三种缓存异常及应对策略 |
有了 t_start 和 t_end,任何一个由 LLM 生成的结论都能反查回原视频的具体时间点。这个"可回溯"的能力,才是把长视频喂给模型的真正价值所在——否则你得到的只是一段听起来很对但没法验证的摘要。
2. 管线骨架:四条支线并行推进,而不是一条串行流水线
第一版我是写成串行的:解码、抽帧、判重、OCR、ASR、打标签、总结,一步一步往下走。跑完一门课要将近 90 分钟,慢得让人想砸键盘。拆解之后发现,串行结构里有大量等待——ASR 在跑的时候 CPU 闲着,判重在跑的时候 GPU 闲着。
2.1 从输入到输出的阶段切分
重构之后,管线被切成四条支线,彼此只通过文件和时间轴通信:
| 阶段 | 输入规模 | 输出规模 | 主要成本 |
|---|---|---|---|
| 视觉支线:解码 + 抽帧 + 判重 | 180,000 帧 | 41 帧 | 视频解码(CPU/GPU 混合) |
| 语音支线:VAD + ASR | 7200 秒音频 | 约 900 条带时间戳文本 | 推理(GPU) |
| 文字支线:关键帧 OCR | 41 张图 | 41 段文本 | 推理(CPU 即可) |
| 归纳支线:VLM 打标 + LLM 汇总 | 41 图 + 900 段文本 | 1 份结构化笔记 | 推理(GPU) |
视觉支线和语音支线可以同时启动,因为它们读的是同一个文件但各自独立解码。OCR 支线必须等判重结束(它只处理 41 张),但它很轻,几十秒就完事。归纳支线必须等前面全部结束。
这个依赖关系看着简单,但它把总耗时从"各阶段之和"变成了"最长支线 + 收尾"。实测下来,视觉支线成了瓶颈,语音支线在它跑完之前就已经结束了。
2.2 为什么要"降采样判重、全分辨率存档"
这条是我踩坑之后定下来的规矩。第一版我直接用原分辨率算哈希,1920×1080 的图转灰度再缩放,每张图光预处理就要几十毫秒,7200 张跑下来十几分钟,全耗在没意义的像素搬运上。
改法很直接:判重时先把帧缩到 320 像素宽、转灰度、算 64 位哈希,然后立刻把这张小图扔掉,只保留哈希值和帧的时间戳。等到判重结束、确定了哪 41 个时刻是代表帧,再回头用精确 seek 把这几帧以原分辨率取出来存盘。
这一改带来三重收益:判重阶段的单帧处理时间从几十毫秒降到 1 毫秒以内;内存占用从"缓存全帧"变成"缓存 8 字节哈希";磁盘占用从 36 GB 降到几十 MB。代价是要多解一次码——但那 41 次精确 seek 的时间可以忽略不计。
提示:精确 seek 时记得往后多读几帧作为关键帧对齐的缓冲,因为很多编码格式的关键帧间隔是 250 帧,直接 seek 可能取到上一秒的画面。
2.3 支线之间的会合点:统一时间轴
所有支线统一使用毫秒整数时间戳,禁止用浮点秒。原因是 ASR 的词级时间戳、帧的时间戳、OCR 的帧归属,如果在浮点层面各自舍入,最后拼起来会出现几毫秒的错位,导致"这句话被归到下一页"这种看起来很蠢的错误。
我在判重阶段就把每个索引项的 t_start、t_end 定死,后续 ASR 文本按"落在这两个时间点之间"归属到某个索引项。归属规则是取文本中点:如果一条语音的中点落在 [t_start, t_end) 内,就归到这一页。跨页的语音允许被切成两段,切分点就是页边界。
3. 判重才是这套管线的真正难点,而不是抽帧
如果你问我这套东西哪里最容易做砸,我会说是判重。抽帧是一行 FFmpeg 参数的事,判重则要在"漏掉一页"和"多出一堆废页"之间找平衡,而且不同类型的视频平衡点完全不同。
3.1 从场景切分到近似重复:双阈值策略
我用的是两级过滤,两级阈值管的是完全不同的东西,千万别混。
第一级是 场景切分阈值 ,负责找"内容发生结构性突变"的时刻。用基于内容差异的检测器,阈值默认在 27 左右(0 到 255 的像素差尺度)。调低会切得太碎,把一次滚动操作切成好几个镜头;调高会漏掉切换,把两页不同的幻灯片合并成一个镜头。我的经验值:PPT 类课程 25 到 30,实拍讲解 30 到 40,屏幕录制演示 20 到 25。
第二级是 近似重复阈值 ,负责把"内容相同但像素有微小差异"的镜头合并。用 64 位感知哈希,汉明距离小于等于 6 判为同一页。这个 6 不是随便定的:64 位哈希下,随机两张图的距离期望是 32,同一张图加轻微噪声的距离通常在 0 到 8 之间,而换一页幻灯片通常会让距离跳到 15 以上。取 6 相当于在噪声区和真实差异区之间取了个偏保守的位置。
两级是串行的:先按场景切分出镜头列表,再对镜头代表帧两两做哈希比对,把连续的、距离小于阈值的镜头合并成一个索引项。注意是"连续"——不连续的相同画面(比如讲师中途翻回上一页)我保留为独立索引项,因为它在时间线上的含义不同。
3.2 光标、鼠标轨迹和转场动画的干扰处理
这三样是判重的主要噪声源,逐个说。
鼠标光标 的问题在于它会闪烁、会移动,一个 24×24 像素的白色箭头足以让感知哈希的低位发生变化。我试过在计算哈希前做一次形态学闭运算把小面积高亮区域抹掉,效果不错但慢。最后采用的是更简单粗暴的办法:计算哈希时只取画面中间 80% 的区域,四周各裁掉 10%。对绝大多数课程录制来说,光标活动区和工具栏都在边缘,裁掉之后干扰直接消失。
鼠标轨迹 问题更隐蔽。讲师用激光笔或者高亮工具在幻灯片上划重点时,画面上会出现一条持续变化的彩色线条,帧与帧之间这条线在动,哈希距离就在 3 到 10 之间摇摆。单帧比对会误判成"内容在变"。解法是用连续多帧投票:不光比较代表帧,还比较该镜头内均匀采样的 5 帧,只有当这 5 帧都判为"与前一页不同"时才认为是新页。
转场动画 (淡入淡出、推拉)会让切换点附近的帧处在一个"半页 A 半页 B"的中间状态。如果场景切分恰好切在这个中间状态,代表帧就是一张模糊的混合图。我的处理是:切分得到镜头边界后,把代表帧取在镜头内部 20% 位置而不是正中间,避开过渡区。
3.3 感知哈希的参数怎么调
| 参数 | 推荐值 | 调大的后果 | 调小的后果 |
|---|---|---|---|
| 哈希位宽 | 64 位 | 区分度下降,误合并增多 | 对噪声更敏感,误拆分增多 |
| 汉明距离阈值 | 5 到 8 | 漏页(两页被当成一页) | 废页增多(一页被拆成多页) |
| ROI 裁剪比例 | 四周各 10% | 边缘信息丢失 | 光标干扰放大 |
| 多帧投票数 | 5 | 判重变慢 | 动画误判增多 |
| 缩略图宽度 | 256 到 384 像素 | 预处理变慢 | 细节丢失,哈希不稳 |
调这套参数我建议你准备一段"已知答案"的视频:找一门你熟悉的课,人工数一下它有多少页,然后拿你的管线跑,看输出的索引项数和人工数差多少。我的经验是能控制在正负 10% 以内就算合格,因为这个量级的差异主要来自"封面页、结束页、纯黑过渡页"这类边缘情况,不影响下游理解。
4. ASR 和 OCR 落到同一根时间针上
视觉索引给了你"哪一页",语音和文字给了你"这一页在讲什么"。两者必须严丝合缝地对齐,否则最后的笔记会张冠李戴。
4.1 语音侧:VAD 切段加词级时间戳
语音识别这条支线我用的组合是:先做语音活动检测把静音段剔除,再按 30 秒切片送模型,最后用词级时间戳回填。
为什么必须开 VAD?因为课程视频里有大量静音——讲师思考、观众翻页、演示操作。如果不做 VAD 直接把 7200 秒音频喂进去,模型会在静音段产生幻觉文本,而且这些幻觉文本还会被分配时间戳,污染整条时间轴。开了 VAD 之后,有效语音大概只占 65% 到 75%,既省算力又提质量。
切片长度选 30 秒是个折中。太短会导致上下文断裂,模型在句子中间被截断,识别质量下降;太长会让单次推理的显存占用上升,而且一旦某一段出错,重试的成本变大。30 秒差不多是一到两个自然段的长度,模型能吃饱上下文,显存也稳。
词级时间戳是必需项,不是可选项。只有拿到每个词的起止时间,才能在页边界处精确切割语音文本。最初的版本我只用句级时间戳,结果是一门课里有十几处"跨页句子被整句归到下一页",读起来突兀。
4.2 文字侧:只在关键帧上做 OCR,成本立刻降两个数量级
这是整套管线里最划算的一个决策。如果对 7200 张候选帧都做 OCR,按每张 200 毫秒算就是 24 分钟,比整个视觉支线都贵。但既然判重已经把 7200 张压成了 41 张,OCR 就只需要跑 41 次,八秒钟的事。
而且质量还更好。因为判重阶段筛掉的是"正在过渡"和"几乎重复"的帧,留下来的是每个镜头最稳定的那一张,OCR 的信噪比自然是高的。
OCR 输出我做了两层过滤:置信度低于 0.6 的文本行直接丢弃,长度小于 2 个字符的丢弃。这两条过滤能消掉九成以上的噪声,主要来自页面边缘的装饰线条和水印。
有个细节值得提:OCR 结果要按纵坐标排序后拼接,不能按识别返回顺序。识别引擎是按检测框的某种内部顺序返回的,多数时候是从上到下,但遇到多栏布局或者表格时顺序会乱。手动按 y 中心排序再组合,输出的文本可读性提升非常明显。
4.3 时间轴对齐时常见的漂移与修正
即使前面都做对了,对齐阶段还是会有几类典型问题。
首尾漂移 :ASR 的第一条文本时间戳经常比真实语音晚 200 到 500 毫秒,因为模型的前处理会引入延迟。修正办法是在 VAD 阶段记录每个语音段的绝对起点,用它来校正 ASR 输出的相对时间戳。
页边界模糊 :讲师说"下一页我们看……"的时候,视觉上其实还没翻页。这种"提前预告"会导致那句话被归到上一页。这个我选择不修——因为从语义上讲,它确实属于上一页的收尾。过度追求完美对齐反而会让文本读起来断裂。
OCR 帧归属 :如果某一页的 OCR 文本是在页面切换动画中途截取的,可能只识别到半页内容。我的兜底策略是:当某个索引项的 OCR 文本长度明显短于相邻页(比如不到均值的 30%),就回退到该镜头内的第二候选帧重新识别一次。
5. 让视觉模型只干"打标签"这一件事
到这一步你手上有 41 张关键帧和它们的 OCR 文本。接下来是把图变成文字描述,让纯文本的 LLM 也能"看见"这些页。
5.1 关键帧描述为什么必须约束成结构化输出
第一版我直接问视觉模型"描述这张图",得到的是一段自由文本,长短不一,有的 20 字有的 200 字。问题在于这些描述要和 OCR 文本、语音文本拼在一起送进 LLM,长度失控会让总上下文难以预估。
改成强制结构化输出之后好很多。我给模型的指令大概是:只输出 JSON,包含 title(这页的主题,不超过 15 字)、type(slide/code/demo/discussion 四选一)、points(要点数组,最多 5 条,每条不超过 30 字)、must_keep(图中必须原样保留的专有名词或数字,最多 8 个)。
type 字段看起来可有可无,其实很有用。代码页后续可以走不同的摘要模板——代码页的重点是函数名和参数,演示页的重点是操作步骤,讨论页可能压根没有视觉信息,只能依赖语音。分型之后下游的处理策略能差异化。
must_keep 是我加的一个防幻觉机制。视觉模型有时会"理解"出图中不存在的概念,但让它把看到的原文列出来时,准确率会高很多。后续 LLM 汇总时我要求这些词必须原样出现在输出里,相当于给摘要加了一层锚点。
5.2 批量推理与显存、吞吐的平衡
41 张图听起来不多,但如果每张单独调用一次、每次带上完整的系统提示,光是 prompt 的重复计算就浪费不少。我的做法是把 4 到 8 张图打包成一批,共享系统提示,模型侧开启连续批处理。
批大小不是越大越好。实测在单张消费级显卡上,批大小从 1 提到 4 时吞吐几乎线性增长,提到 8 时增长放缓到 1.4 倍左右,再往上就开始因为显存压力触发换页,反而变慢。所以我固定在 6。
还有个容易忽略的点:图片送进去之前要先缩放。原图 1920 宽直接送,会被视觉编码器切成几十个 patch,算力开销是 768 宽的五六倍,而描述质量几乎没有提升——因为课程幻灯片的主体信息在那点分辨率下完全够用。我统一缩到长边 1024。
5.3 把 41 张图的标签和字幕拼成一份可检索的课程地图
所有支线的产物最后拼成一个结构化的笔记文件,形如:
{
"video": "course_01.mp4",
"duration_ms": 7200000,
"blocks": [
{
"frame_id": 17,
"t_start": 4213500,
"t_end": 4388000,
"visual": {"title": "缓存穿透与布隆过滤器", "type": "slide"},
"ocr": ["第 4 章 缓存异常", "布隆过滤器误判率计算"],
"speech": "……所以在高并发场景下,直接查库会给数据库造成很大压力",
"keywords": ["缓存穿透", "布隆过滤器", "误判率"]
}
]
}
这份结构本身就是"课程地图"。你可以按关键词检索,可以按时间区间切片,也可以整份丢给 LLM 做全局归纳。因为我保留了 complete 的时间区间,最终的笔记里每一段结论都可以标注来源时间点,读者点一下就能跳到原视频对应位置。
6. 5.9× 实时是怎么跑出来的:解码、并发与缓存
速度这块我做了三轮优化,每一轮都是先测再改,凭感觉优化基本等于白干。
6.1 先测出瓶颈在哪:解码往往比推理更贵
我一开始的直觉是"模型推理最慢",结果打了时间戳之后发现:视觉支线的 80% 时间花在视频解码上,模型推理只占很小一部分。原因很直白——180,000 帧全都要解出来才能判断,解码本身是绕不过去的。
确认瓶颈之后优化方向立刻清楚了:不是去换更小的模型,而是想办法让解码更快、让解码后的处理更轻。具体做了三件事:
一是把解码输出直接降尺度。FFmpeg 的 scale 滤镜可以在解码后立刻把画面缩到 320 宽,后续所有操作都在小图上进行,内存带宽压力骤降。
二是把帧率降采样放在解码管线里而不是 Python 侧。用
-vf fps=25,scale=320:-2
这种写法,让 FFmpeg 在 C 层完成丢弃和缩放,比在 Python 里逐帧判断快得多。
三是启用硬件解码。这一步在有独显的机器上能再砍掉一半解码时间,代价是要处理硬件解码器的帧格式转换,稍微麻烦一点。
ffmpeg -hwaccel auto -i input.mp4 \
-vf "fps=1,scale=320:-2,format=gray" \
-f rawvideo -pix_fmt gray pipe:1
这行命令把 2 小时视频抽成 7200 张 320 宽的灰度图,直接从标准输出以裸数据流吐出,不落盘。实测比"先抽成 jpg 再读"快三倍以上,因为省掉了编码和解码两轮 IO。
6.2 三处关键优化:分段并行、队列解耦、阶段级缓存
第一处是分段并行。7200 秒的视频如果按 10 分钟一段切成 12 段,每段独立解码和判重,然后用段边界做一次合并,理论上有接近线性的加速。代价是边界处可能出现一页被切成两段的情况,需要在合并时做一次跨段哈希比对。我的做法是把分段点前后的 30 秒重叠处理,重叠区只用于判重不用于计数。
第二处是队列解耦。解码进程和判重进程之间用一个有界队列连接,解码进程只管吐小图,判重进程只管算哈希。这样即使判重偶尔卡顿,解码也不会停下来等。队列长度设成 64 就够,太大反而增加内存压力且掩盖了真实的性能问题。
第三处是阶段级缓存,这个收益最大。我给每个阶段设计了独立的缓存 key:视觉缓存 key 是"文件内容哈希 + 抽帧参数 + 判重参数",ASR 缓存 key 是"文件内容哈希 + 切片长度 + 语言",OCR 缓存 key 是"关键帧哈希 + 识别语言"。key 不变就直接读缓存,秒出结果。调 LLM 提示词的时候,前三段全部命中缓存,我只等最后那段,迭代速度从每次 20 分钟变成每次 40 秒。
注意:缓存目录要有清理策略,否则跑几十个视频之后磁盘会悄悄涨到几十 GB。我按总容量上限做 LRU 淘汰,只保留最近 20 个视频的中间产物。
6.3 复现速度数据时的测量口径说明
"5.9× 实时"这个数字我想把口径说清楚,否则你复现出来对不上会觉得我在吹。
测量的是端到端墙钟时间,从读取视频文件开始,到写出最终结构化笔记为止,包含所有阶段的耗时和进程启停开销。分母是视频时长 7200 秒,分子实测约 1220 秒。
环境是单机、单张消费级显卡、16 核 CPU、NVMe 固态。如果换机械硬盘,判重阶段的随机读会明显拖慢;如果 CPU 核心数少于 8,分段并行的收益会打对折。
还有一个变量是视频内容类型。PPT 类课程因为画面稳定、判重命中率高,速度最快;实拍讲解类因为画面一直在动,判重成本上升,端到端速度大概会掉到 3× 到 4× 实时;如果视频里有大量动态画面(比如演示操作、视频素材回放),去重效果变差,速度可能只有 2× 左右。所以那个 5.9 是"这门课"的成绩,不是通用基准。
7. 实测翻车记录与修复过程
前面讲的是设计,这一节讲真实踩的坑。我觉得这些比设计思路更有参考价值,因为它们都是在"看起来一切正常"的情况下突然冒出来的。
7.1 缩略图全部命中同一张:哈希位宽惹的祸
最离谱的一次事故:跑完一门课,输出的 41 张代表帧长得一模一样,全是封面页。第一反应是判重太狠了,把阈值从 6 调到 2 重跑,结果变成 800 多张。
真正的原因藏在缩略图生成环节。我在保存代表帧时用了一个固定尺寸的转码模板,把不同时刻的帧都输出到同一个临时文件名,循环体里忘了加索引后缀,后一帧覆盖前一帧,最后磁盘上只剩最后写的那一张。而判重逻辑是对的——它记的是哈希和时间区间,只是取图时全取到了同一个文件。
这个坑的排查过程值得说一下。我当时的错误方向是去调哈希阈值,因为误以为是算法问题。正确做法应该是先做一次"断点验证":在判重结束、存图之前,把 41 个哈希值打印出来,确认它们互不相同。哈希不同而图片相同,问题必然出在存图环节。这一步只花了三分钟,而我之前在参数上浪费了两个小时。
提示:管线的每个阶段都应该有一个"不变量检查",比如判重阶段的不变量是"输出哈希集合的大小等于输出项数"。这个检查极其便宜,但能第一时间定位问题发生在哪一段。
7.2 代码截图被 OCR 识别成一堆乱码
第二类问题是 OCR 在代码页上的表现。课程里有大量 IDE 截图,深色主题白字,OCR 引擎识别出来一堆形近字符错误:
l
和
1
、
O
和
0
、
rn
和
m
。这些错误进入摘要之后,函数名全变了。
我的修复分三层。第一层是预处理:深色背景的图先做反色,让文字变成黑字白底,识别准确率立刻上一个台阶。第二层是识别参数:限定字符集,把易混淆的字符集配置成以编程语言关键字为主的白名单,同时把检测框的最小高度从默认的 8 像素提到 14 像素,过滤掉抗锯齿边缘产生的碎框。第三层是后处理:对识别出的每行代码做一次"标识符规范化",把孤立出现的数字 1 在字母上下文中替换成 l,反过来也做一遍,然后用语言特定的关键字表做校验。
这三层做完,代码页的 OCR 准确率大概从七成提到了九成五。但我的建议是别追求 100%——代码页的完整代码本来就不该靠 OCR 来获取,OCR 只负责提供"这一页在讲哪个函数"这样的定位信息就足够了。
7.3 摘要里出现视频中根本没提过的名词
第三类问题最隐蔽,也最危险:LLM 生成的笔记里出现了视频中从未提及的技术名词。这不是 OCR 错误导致的,是模型在归纳时的合理推测——它看到"缓存穿透"和"布隆过滤器"两个词同时出现,就顺手补充了一段"布隆过滤器通过多个哈希函数降低误判率"的说明,听起来完全正确,但讲师压根没讲。
这类幻觉在知识笔记场景下的危害特别大,因为它看起来太合理了。我的应对是把归纳改成"提取式"而非"生成式":让模型的任务变成"从给定的语音文本和 OCR 文本中筛选并组织信息",并在提示词里明确写"禁止引入输入文本之外的任何概念、术语或数字"。
为了防止模型嘴上答应、实际照编,我加了一道机械校验:对输出的摘要做一次词频比对,找出所有长度大于 4 个字符的技术词,检查它们是否出现在输入文本中。没有出现的词会被标记出来,由人(或者一次额外的模型调用)判断是幻觉还是同义改写。这个校验拦下了我测试集里大概八成的幻觉。
8. 复现清单:目录结构、依赖与运行顺序
如果你打算照着自己搭一套,这一节是我能给的最直接的抄作业材料。
8.1 目录结构与中间产物
project/
data/
raw/ # 原始视频
cache/
probe/ # 视频元信息缓存
hashes/ # 判重哈希(阶段一产物)
keyframes/ # 关键帧原图(阶段二产物)
asr/ # 语音识别结果
ocr/ # 关键帧文本
captions/ # 视觉模型标签
out/
timeline.json # 统一时间轴
notes.md # 最终笔记
src/
probe.py
dedup.py
asr.py
ocr.py
caption.py
assemble.py
按这个结构组织的好处是,每个阶段的产物独立落盘,任何一个阶段改了参数,只需要删掉对应的缓存目录重跑,前面的结果全部复用。
8.2 配置项的推荐取值
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 抽帧帧率 | 1 fps | 课程类足够,实拍类可提到 2 fps |
| 判重缩略图宽度 | 320 | 再小会丢细节导致哈希不稳 |
| 场景切分阈值 | 27 | PPT 类课程的经验值 |
| 汉明距离阈值 | 6 | 64 位哈希下的保守取值 |
| 多帧投票数 | 5 | 抑制动画和光标干扰 |
| ASR 切片长度 | 30 秒 | 兼顾上下文和显存 |
| 图片送入模型的最长边 | 1024 | 再大收益极小 |
| 视觉模型批大小 | 6 | 单卡显存与吞吐的平衡点 |
| 缓存容量上限 | 20 个视频 | 按 LRU 淘汰 |
8.3 运行顺序与验证方法
运行顺序就是按支线依赖来:先跑 probe 拿到视频元信息,然后视觉支线和语音支线并行启动,视觉支线跑完立刻跑 OCR,最后等三条支线全部结束再跑视觉打标和汇总。如果你懒得写并行调度,用串行也能跑通,只是慢一倍。
每个阶段我都留了一个自检命令。probe 阶段验证时长和帧率是否解析正确;判重阶段验证输出项数是否落在合理区间(时长秒数的 1% 到 3% 之间);ASR 阶段验证总文本长度和视频时长的比值是否在 10 到 25 字每秒之间;OCR 阶段验证有多少帧的文本为空(超过 30% 就要检查预处理);汇总阶段跑一次幻觉词频校验。
这些自检加起来不超过 200 行代码,但它们是整套管线能不能在无人值守下跑通几十个视频的关键。我个人在实际操作中的体会是:一条数据管线真正的成本从来不在最聪明的那个环节,而在最不起眼的那个校验上——一次静默的错误输出,可能让你花一整天去怀疑一个根本没问题的模型。
更多推荐
所有评论(0)