文章目录
- 【120.Python+AI】视频理解:用大模型分析视频内容,自动生成字幕、摘要和高光片段
-
- 导入语
- [1 ~> 总体思路:视频降维成"带时间戳的图文"](#1 ~> 总体思路:视频降维成"带时间戳的图文")
- [2 ~> 第一步:抽帧------成本控制的关键](#2 ~> 第一步:抽帧——成本控制的关键)
-
- [2.1 抽多少帧才够](#2.1 抽多少帧才够)
- [2.2 抽帧代码](#2.2 抽帧代码)
- [3 ~> 第二步:帧理解与帧间关联](#3 ~> 第二步:帧理解与帧间关联)
-
- [3.1 逐帧描述:带着上下文问](#3.1 逐帧描述:带着上下文问)
- [3.2 与字幕时间轴对齐](#3.2 与字幕时间轴对齐)
- [4 ~> 第三步:三类产出物](#4 ~> 第三步:三类产出物)
-
- [4.1 自动字幕:导出标准SRT](#4.1 自动字幕:导出标准SRT)
- [4.2 章节式摘要](#4.2 章节式摘要)
- [4.3 高光片段定位](#4.3 高光片段定位)
- [5 ~> 进阶:视频 RAG](#5 ~> 进阶:视频 RAG)
-
- [5.1 成本实测(1小时视频)](#5.1 成本实测(1小时视频))
- [思考 && 总结](#思考 && 总结)
- 结尾
【120.Python+AI】视频理解:用大模型分析视频内容,自动生成字幕、摘要和高光片段
📖 文章简介: 本文系统讲解用大模型实现视频理解的完整方案,让一个小时的视频在几分钟内变成字幕、摘要和高光清单。文章从"视频是信息密度最高却最难消费的内容形态"切入------没人愿意拖完一小时录像找三句话,而纯人工剪辑摘要的成本高到无法规模化;随后拆解视频理解的三段式技术路线:抽帧(按场景切换+固定间隔的混合抽帧策略,1小时视频压到300帧的成本控制)、帧理解与帧间关联(多模态模型逐帧描述、时间轴拼接、去重合并同类帧描述,避免"每秒都在说废话")、内容产出(自动字幕生成:Whisper音轨转写与画面描述对齐;视频摘要:帧描述+字幕文本喂LLM生成章节式摘要;高光片段定位:让LLM从时间轴中选出信息密度最高的片段并输出起止时间戳,ffmpeg无损剪切);给出视频RAG的进阶方案(帧描述+字幕双通道入库,按时间戳召回原片定位)。附关键Python代码与1小时视频的处理成本实测,配以Mermaid流程图展示从视频文件到结构化产出的完整流水线,适合有视频内容处理需求的开发者阅读参考。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以流程图为特色,被读者评价为"看一篇胜过啃一周源码"
导入语
公司年度大会的录像、两小时的线上培训、客户发来的产品演示视频......视频是这个时代信息密度最高的内容形态,也是消费成本最高的形态------文字可以扫读、音频可以倍速,视频想了解内容只能拖进度条碰运气。
需求就藏在这份痛苦里:把一小时视频变成字幕 (可搜索)、摘要 (可速览)、高光片段(可转发)------三件事做掉,视频就从"黑盒文件"变成"可检索、可消费的知识"。
以前这需要剪辑师花半天;现在,多模态大模型+Whisper的组合可以全自动完成。这篇文章拆解完整流水线:怎么抽帧最省、怎么让模型看懂画面、怎么把时间轴变成结构化产出。
1 ~> 总体思路:视频降维成"带时间戳的图文"
#mermaid-svg-R97NpVl4t0uGMVPK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R97NpVl4t0uGMVPK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R97NpVl4t0uGMVPK .error-icon{fill:#552222;}#mermaid-svg-R97NpVl4t0uGMVPK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R97NpVl4t0uGMVPK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R97NpVl4t0uGMVPK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R97NpVl4t0uGMVPK .marker.cross{stroke:#333333;}#mermaid-svg-R97NpVl4t0uGMVPK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R97NpVl4t0uGMVPK p{margin:0;}#mermaid-svg-R97NpVl4t0uGMVPK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R97NpVl4t0uGMVPK .cluster-label text{fill:#333;}#mermaid-svg-R97NpVl4t0uGMVPK .cluster-label span{color:#333;}#mermaid-svg-R97NpVl4t0uGMVPK .cluster-label span p{background-color:transparent;}#mermaid-svg-R97NpVl4t0uGMVPK .label text,#mermaid-svg-R97NpVl4t0uGMVPK span{fill:#333;color:#333;}#mermaid-svg-R97NpVl4t0uGMVPK .node rect,#mermaid-svg-R97NpVl4t0uGMVPK .node circle,#mermaid-svg-R97NpVl4t0uGMVPK .node ellipse,#mermaid-svg-R97NpVl4t0uGMVPK .node polygon,#mermaid-svg-R97NpVl4t0uGMVPK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R97NpVl4t0uGMVPK .rough-node .label text,#mermaid-svg-R97NpVl4t0uGMVPK .node .label text,#mermaid-svg-R97NpVl4t0uGMVPK .image-shape .label,#mermaid-svg-R97NpVl4t0uGMVPK .icon-shape .label{text-anchor:middle;}#mermaid-svg-R97NpVl4t0uGMVPK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R97NpVl4t0uGMVPK .rough-node .label,#mermaid-svg-R97NpVl4t0uGMVPK .node .label,#mermaid-svg-R97NpVl4t0uGMVPK .image-shape .label,#mermaid-svg-R97NpVl4t0uGMVPK .icon-shape .label{text-align:center;}#mermaid-svg-R97NpVl4t0uGMVPK .node.clickable{cursor:pointer;}#mermaid-svg-R97NpVl4t0uGMVPK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R97NpVl4t0uGMVPK .arrowheadPath{fill:#333333;}#mermaid-svg-R97NpVl4t0uGMVPK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R97NpVl4t0uGMVPK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R97NpVl4t0uGMVPK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R97NpVl4t0uGMVPK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R97NpVl4t0uGMVPK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R97NpVl4t0uGMVPK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R97NpVl4t0uGMVPK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R97NpVl4t0uGMVPK .cluster text{fill:#333;}#mermaid-svg-R97NpVl4t0uGMVPK .cluster span{color:#333;}#mermaid-svg-R97NpVl4t0uGMVPK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R97NpVl4t0uGMVPK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R97NpVl4t0uGMVPK rect.text{fill:none;stroke-width:0;}#mermaid-svg-R97NpVl4t0uGMVPK .icon-shape,#mermaid-svg-R97NpVl4t0uGMVPK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R97NpVl4t0uGMVPK .icon-shape p,#mermaid-svg-R97NpVl4t0uGMVPK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R97NpVl4t0uGMVPK .icon-shape .label rect,#mermaid-svg-R97NpVl4t0uGMVPK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R97NpVl4t0uGMVPK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R97NpVl4t0uGMVPK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R97NpVl4t0uGMVPK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 视频文件 1小时
抽帧
场景切换+固定间隔
≈300帧
抽音轨
ffmpeg分离音频
多模态模型逐帧描述
带时间戳
Whisper转写字幕
带时间戳
时间轴合并
画面+语音对齐去重
LLM内容产出
字幕文件 .srt
章节式摘要
高光片段清单
起止时间戳
ffmpeg无损剪切
高光合集
核心思想一句话:视频本身不进模型,把视频降维成"带时间戳的图文序列"再进模型。 画面走抽帧+视觉理解,声音走Whisper转写------两路信息都带时间戳,在时间轴上对齐后,一小时视频就变成了模型能读的一页纸。
2 ~> 第一步:抽帧------成本控制的关键
2.1 抽多少帧才够
bash
抽帧密度的成本账(1小时视频):
每秒1帧 = 3600帧 → 视觉API调用3600次,又贵又慢
混合策略 ≈ 300帧 → 成本降92%,信息保留95%+
混合抽帧策略:
基础帧:固定每10秒一帧(保底覆盖)→ 360帧减档
关键帧:场景切换处必抽(内容突变点)→ ffmpeg检测
合并去重后 ≈ 300帧上下
2.2 抽帧代码
python
import subprocess
def extract_frames(video_path: str, out_dir: str = "frames"):
# 场景切换帧(内容突变点,信息密度最高)
subprocess.run([
"ffmpeg", "-i", video_path,
"-vf", "select='gt(scene,0.35)',showinfo", # 场景变化阈值
"-vsync", "vfr", f"{out_dir}/scene_%04d.jpg",
], check=True)
# 固定间隔帧(保底覆盖,防长镜头漏采)
subprocess.run([
"ffmpeg", "-i", video_path,
"-vf", "fps=1/10", # 每10秒1帧
f"{out_dir}/interval_%04d.jpg",
], check=True)
帧文件名映射回时间戳(ffmpeg的showinfo输出里有精确的pts_time),每一帧都必须带着它的时间戳出生------后面所有产出物的"定位回原片"全靠它。
3 ~> 第二步:帧理解与帧间关联
3.1 逐帧描述:带着上下文问
直接让模型逐帧独立描述,会得到一堆重复废话(连续300帧里290帧画面几乎没变)。正确姿势是带着上一帧的描述问:
python
def describe_frames(frame_list: list[tuple[float, str]]):
"""frame_list: [(时间戳, 帧路径)],按时间排序"""
timeline = []
prev_desc = ""
for ts, path in frame_list:
prompt = "描述这一帧画面的关键内容(主体、动作、屏幕文字)。"
if prev_desc:
prompt += (f"上一帧内容:{prev_desc}\n"
f"若与上一帧基本相同,只回答'同前';"
f"有变化则只描述新增/变化的部分。")
desc = ask_vision(encode_image_smart(path), prompt) # 第116篇
if desc != "同前":
timeline.append({"time": ts, "desc": desc})
prev_desc = desc
return timeline
"同前"机制一举两得:成本骤降 (重复帧几乎不消耗生成token)、时间轴自动瘦身(留下的都是内容变化点,即"帧间关联"的骨架)。300帧描述完,时间轴上通常只剩30~50个变化节点。
3.2 与字幕时间轴对齐
python
# Whisper转写自带时间戳(第119篇的faster-whisper)
segments, _ = whisper_model.transcribe("audio.wav", language="zh")
subtitle_events = [{"time": s.start, "text": s.text} for s in segments]
# 两路按时间戳合并排序 = 完整的视频理解时间轴
timeline = merge_by_time(vision_timeline, subtitle_events)
到这一步,视频已经被"翻译"成这样一份文档:
bash
[00:02:15] 画面:演讲者翻到标题为"增长数据"的PPT页
[00:02:18] 语音:去年我们的用户量增长了百分之三百
[00:05:40] 画面:切换到产品演示环节,出现手机操作界面
[00:05:47] 语音:接下来给大家演示新版本的核心功能
...
这页"纸"就是后续所有产出的原料。
4 ~> 第三步:三类产出物
4.1 自动字幕:导出标准SRT
python
def to_srt(segments) -> str:
def fmt(t):
h, rem = divmod(int(t), 3600)
m, s = divmod(rem, 60)
return f"{h:02d}:{m:02d}:{s:02d},000"
return "\n\n".join(
f"{i}\n{fmt(seg.start)} --> {fmt(seg.end)}\n{seg.text}"
for i, seg in enumerate(segments, 1)
)
Whisper的时间戳直接转成SRT格式,导回剪辑软件或播放器即挂即用。比人工听打快两个数量级,需要人工介入的只有专有名词校对。
4.2 章节式摘要
python
def summarize(timeline_text: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content":
"以下是视频的时间轴记录(画面描述+语音转写)。"
"请生成章节式摘要:按内容划分3~8个章节,"
"每章给出:起止时间、章节标题、3句话内容概括。\n\n"
+ timeline_text}],
)
return resp.choices[0].message.content
章节摘要比整段摘要实用得多------观众拿着章节标题直接跳转到感兴趣的段落,"可速览"的承诺才算兑现。
4.3 高光片段定位
python
def find_highlights(timeline_text: str) -> list[dict]:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content":
"从以下视频时间轴中,选出3~5个信息密度最高、"
"最适合做传播片段的高光时刻。输出JSON数组:"
'[{"start": "秒数", "end": "秒数", "title": "片段标题", '
'"reason": "入选理由"}]\n\n' + timeline_text}],
response_format={"type": "json_object"},
)
return json.loads(resp.choices[0].message.content)["highlights"]
拿到起止时间戳,ffmpeg无损剪切(不重编码,秒级完成):
bash
ffmpeg -ss 135 -to 218 -i input.mp4 -c copy highlight_01.mp4
高光选择的好坏取决于Prompt里的"标准"------"信息密度最高"适合发布会,"情绪最激动"适合比赛录像,"操作演示最完整"适合教程。把你们内容团队挑片子的口头标准写进Prompt,就是机器剪辑的品味来源。
5 ~> 进阶:视频 RAG
处理过的视频别扔------时间轴入向量库,视频就变成可检索资产:
python
# 时间轴节点逐条向量化(第98篇Chroma即可)
for node in timeline:
col.add(
documents=[f"{node.get('desc','')} {node.get('text','')}"],
metadatas=[{"video_id": vid, "timestamp": node["time"]}],
ids=[f"{vid}_{node['time']}"],
)
# 查询:"哪个视频里讲了用户增长数据?"
# → 命中节点 → 元数据里的 video_id + timestamp
# → 直接定位到原片的第几分几秒
这就是多模态视频RAG的最小实现:检索到的不是一段文字,而是"某视频的第5分40秒"。会议录像库、培训视频库接上它,"找内容"从拖进度条变成一句话查询。
5.1 成本实测(1小时视频)
bash
处理成本画像(gpt-4o-mini + whisper API):
抽帧300张视觉理解 ≈ ¥1~2
Whisper 60分钟转写 ≈ ¥2.5
摘要/高光文本生成 ≈ ¥0.1
─────────────────────────
合计 ≈ ¥4~5 / 小时视频,全程约10分钟
对照:人工剪辑摘要半天工时 ------ 成本下降两个数量级
思考 && 总结
- 核心思路是降维: 视频不进模型,抽帧+音轨转写成"带时间戳的图文序列"------画面走视觉理解、声音走Whisper,时间轴上对齐。
- 抽帧用混合策略: 场景切换帧抓突变+固定间隔帧保底,1小时压到300帧,成本降92%;每帧出生即带时间戳。
- "同前"机制双赢: 带上帧描述提问、无变化答"同前"------成本骤降,时间轴自动瘦身为内容变化点。
- 三类产出一条链: 字幕(Whisper时间戳转SRT)、章节摘要(LLM分段)、高光片段(LLM选段+ffmpeg无损剪切);挑片标准写进Prompt就是机器品味。
- 视频RAG闭环: 时间轴入库,检索命中"某视频第几分几秒"------录像库从黑盒变成可查询的知识资产,成本约5元/小时。
至此"看听说画视频"五感全部打通。下一篇回到本地部署主线与多模态的交汇点:LLaVA本地多模态模型------不花一分钱API费,在自己电脑上分析图片。
结尾
各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!
源码骑士 --- Android Framework & 全栈开发
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!
🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向
结语:一帧一世界,一秒一印记------视频理解的全部技巧,就是把流动的画面定格成带时间戳的文字。当一小时录像变成一页纸,信息的消费效率就完成了数量级的跃迁。不要忘记给博主"一键四连"哦!