多模态开发(04):帧采样与时间推理

QuibblerAgentQuibblerAgent 2026-10-30 约 22 分钟 4 次阅读

本系列第 3 篇把"看图"落到了文档、图表、UI 三个场景,但真实业务里还有一大块内容天生是多张图的序列:教程录像、会议回放、产品演示、监控片段。视频就是图像加上时间维度——多了这一维,输入方式、token 账、信息取舍全都变了样。本篇讲视频输入的工程方法:先给三条技术路线和选择依据,再逐条落地——Gemini 原生视频 API、ffmpeg 逐帧方案、转写加关键帧的组合管线,最后用会议纪要 Agent 把它们串成完整生产链路。全文按"路线图 → 原生 API → 逐帧 → 组合管线 → token 账 → 实战 → 边界 → 总结"组织。

1、视频输入三条路线

先交代 2026 年 10 月的三家现状,这决定了路线图。Gemini 是唯一原生支持视频文件的 API;OpenAI 从 GPT-4o 发布起就宣称"视频输入即将开放 API",两年多过去仍未落地,至今官方社区的口径仍是抽帧当图片发(openai-node 的原生视频 issue 还开着);Claude 同样没有视频输入,官方生态的标准做法就是 ffmpeg 抽帧(Anthropic Cookbook 的视频理解示例即此思路)。所以路线不是三选一的偏好题,而是能力边界的产物:

路线 可用模型 优势 代价
原生视频 API 仅 Gemini 时间戳理解、音画同步、零预处理 锁定单一厂商
逐帧抽图 所有视觉模型 通用、帧率与分辨率全可控 无音轨、长视频 token 爆炸
转写+关键帧 所有模型 成本最低、产物可检索 画面细节大量丢失

选型判断只看两条:问题是否依赖时间维度("第几分钟演示了什么"必须原生或组合管线,逐帧答不准),以及是否必须跨模型部署(要就只剩后两条路)。逐帧与组合管线的分界是音频价值:访谈、会议、教程的语义大头在语音里,转写管线的性价比碾压逐帧;产品演示、监控这类"看画面"的任务则反过来。

把这两条判断画成决策图:

flowchart TD A["视频理解任务"] --> B{"依赖时间轴?"} B -->|"是"| C{"可锁定 Gemini?"} B -->|"否"| F{"语义在语音?"} C -->|"是"| D["Gemini 原生视频"] C -->|"否"| E["转写+关键帧"] F -->|"是"| E F -->|"否"| G["逐帧抽图"]

组合管线在两个分支里都是落点——它靠转写时间戳也能回答"第几分钟",这正是会议与访谈类视频默认走组合路线的原因。

2、Gemini 原生视频实战

2.1、上传与采样控制

上传有两条通道:总请求体积 20 MB 以内可以 inline 走 base64;超过就必须走 File API,单文件免费层 2 GB、付费层 20 GB,超过 10 分钟的视频和需要复用的文件也都建议走这条通道。File API 是异步的,上传后要轮询到 ACTIVE 才能发请求,完整流程如下:

# gemini_video.py —— File API 上传 + 问答
import time
from google import genai

client = genai.Client()                          # 走 GEMINI_API_KEY 环境变量
video = client.files.upload(file="meeting.mp4")  # 格式按扩展名推断
while video.state == "PROCESSING":               # 大文件要等服务端处理完
    time.sleep(2)
    video = client.files.get(name=video.name)

r = client.models.generate_content(
    model="gemini-3.8-flash",                    # 按账号可用型号替换
    contents=[video, "总结这段视频,重点处给出 MM:SS 时间戳"])
print(r.text)

上传后最容易被忽略的是采样率:默认按每秒 1 帧抽帧。Gemini 视频理解文档的 processing 字段可以控制(仅 static 模式可用),配合 media_resolution 与剪辑偏移一起用,请求体里视频 part 形态如下:

# 采样与剪辑:fps、分辨率、时间窗一起控
part = {
    "file_data": {"file_uri": video.uri, "mime_type": "video/mp4"},
    "processing": {
        "type": "static",        # 默认静态模式,全片统一采样
        "fps": 0.5,              # 两秒一帧,帧 token 直接减半
        "start_offset": 1200,    # 只送第 20 分钟起
        "end_offset": 1500,      # 到第 25 分钟止,单位秒
    },
}

fps 是成本的第一道闸门:讲授类视频画面变化慢,0.5 甚至 0.2 都够用;media_resolution 是第二道,低清档每帧 66 token、默认档每帧 258 token,读小字时才升档。start_offset/end_offset 剪辑适合"只问某一段"的场景,配合下一节的时间戳问答,先粗看全片再精读片段。

2.2、时间戳理解与时长上限

原生路线的杀手锏是模型真的理解时间轴:可以直接问"01:15 处演示了什么操作""预算讨论从哪一秒开始",提示词里引用时刻也用 MM:SS 格式,与官方文档口径一致。时长上限与上下文和分辨率挂钩:1M token 上下文的模型,低清最多 3 小时、默认档 1 小时;单次请求最多带 10 个视频(YouTube 链接也能直接作为输入)。另外 2026 年新增了 agentic 模式(processing 设为 agentic,Gemini 4 Argon 与 3.x Flash 系列可用):模型自己决定加载哪些时间窗、快节奏片段自动升到 5~10 fps,官方口径长视频最多省 88% token——批量处理长视频时值得先试它。

3、逐帧方案工程化

逐帧是唯一通吃的路线,工程重心在两处:怎么抽帧、抽哪些帧。

3.1、ffmpeg 抽帧

固定间隔抽帧一条命令搞定,两种典型节奏如下:

# 每 2 秒一帧,缩放到 784 宽控制图像 token
ffmpeg -i meeting.mp4 -vf "fps=0.5,scale=784:-2" frame_%04d.jpg

# 场景切换检测:只在画面变化超过阈值处落帧
ffmpeg -i tutorial.mp4 -vf "select='gt(scene,0.3)',showinfo" \
       -fps_mode vfr scene_%04d.jpg

固定间隔适合节奏均匀的内容;场景检测适合幻灯片、教程这类"画面长期静止、偶尔整屏切换"的素材,scene 阈值 0.3 起调,切屏漏检就调低。注意 -fps_mode vfr 是 ffmpeg 7+ 的写法,老的 -vsync 已被移除,网上旧教程照抄会直接报错。场景检测模式还要用 showinfo 把每帧的精确时间戳打进 stderr,落帧时间不均匀,没有时间戳就没法和文本对齐。

3.2、选帧策略

全量帧很少是最优解,三种选帧策略覆盖绝大多数需求:

# pick_frames.py —— 均匀选帧与运动量排序
import glob, subprocess

def probe_duration(src: str) -> float:
    r = subprocess.run(["ffprobe", "-v", "quiet", "-show_entries",
                        "format=duration", "-of", "csv=p=0", src],
                       capture_output=True, text=True)
    return float(r.stdout.strip())               # 视频总时长(秒)

def uniform_timestamps(src: str, n: int = 10) -> list[float]:
    dur = probe_duration(src)                    # 首尾必含,中间等距
    return [round(dur * i / (n - 1), 1) for i in range(n)]

def by_motion(frame_dir: str, top: int = 8) -> list[str]:
    from PIL import Image                        # 相邻帧灰度差分
    files = sorted(glob.glob(f"{frame_dir}/*.jpg"))
    pairs = list(zip(files, files[1:]))
    scored = [(b, sum(abs(x - y) for x, y in zip(
        Image.open(a).convert("L").getdata(),
        Image.open(b).convert("L").getdata()))) for a, b in pairs]
    return [f for f, _ in sorted(scored, key=lambda x: -x[1])[:top]]

均匀选帧用于"快速概览一段未知视频",10 帧以内成本可控;运动量排序用于"找出发生了什么的时刻",差分大的位置就是动作发生处。工程上两者常串联:先均匀铺一遍定位大概区域,再对可疑区间密集抽帧——这正是第 3 篇"先总后细"套路在时间维度上的翻版。发送时把时间戳写进每帧的伴随文本(如 [00:32]),模型的回答才能落到时间轴上。

4、转写与关键帧组合

第三条路线把视频拆成两种廉价模态:音轨交给 Whisper 转写,画面每 10 秒留一张关键帧,再用时间戳把两者缝起来。预处理代码如下:

# av_prep.py —— 音轨转写 + 关键帧对齐
import glob, subprocess, whisper                 # pip install openai-whisper

def prep(src: str, frame_step: int = 10) -> list[dict]:
    subprocess.run(["ffmpeg", "-i", src, "-vn", "-ac", "1",
                    "-ar", "16000", "audio.wav"], check=True)   # 抽音轨
    subprocess.run(["ffmpeg", "-i", src, "-vf", f"fps=1/{frame_step}",
                    "-fps_mode", "vfr", "kf_%04d.jpg"], check=True)
    frames = sorted(glob.glob("kf_*.jpg"))
    segs = whisper.load_model("small").transcribe(
        "audio.wav", language="zh")["segments"]
    out = []
    for s in segs:                               # 每段转写挂上当时画面
        i = min(int(s["start"] // frame_step), len(frames) - 1)
        out.append({"start": s["start"], "text": s["text"].strip(),
                    "frame": frames[i]})
    return out

产出是"带画面的字幕段"列表:每段有起始秒、文本、关键帧路径。这套结构就是多模态 RAG 的预处理雏形——转写段是天然的 chunk,时间戳是对齐键,关键帧是挂在 chunk 上的第二模态;后续无论是向量化入库还是直接送 LLM 摘要,数据形状都不用再变。转写速度建议用 faster-whisper 提速数倍,长视频批量处理时差距明显;原版 Whisper 适合快速验证。

5、视频的 token 账

视频计费没有统一价目,只有各自的折算口径,先看 Gemini 的官方数字(音频统一按 32 token 每秒叠加):

输入方式 折算口径 一分钟视频量级
Gemini 低清(media_resolution 低) 约 100 token/秒 约 6 千 token
Gemini 默认档 约 300 token/秒 约 1.8 万 token
逐帧(OpenAI/Claude) 帧数 × 图像 token 2 fps 下约 3 万~13 万

逐帧的账最危险:第 2 篇讲过图像 token 按分辨率计(每帧约两三百到上千),一分钟 2 fps 就是 120 帧;一小时视频逐帧下来是数百万 token——不但贵,而且根本塞不进单次请求的上下文。

注意:处理视频前先 ffprobe 拿时长,乘以折算口径估出预算,超了先降 fps 或改组合管线,再发请求。

成本控制的优先级很清晰:能用组合管线(转写几乎零 API 成本,帧按需挂)就不逐帧;必须逐帧就砍帧率与分辨率;原生路线用 fps 与 media_resolution 双闸门,长视频试 agentic 模式。按 Gemini Flash 档输入价格量级估算,一小时低清视频约几十万 token、成本在几毛美元级,但升到默认档、再叠加多轮对话,账单翻十倍是常态。

6、会议纪要 Agent 实战

组合路线的完整闭环:会议录像进,结构化纪要出。上半部把第 4 节的产物送给 Claude 生成纪要:

# minutes_agent.py —— 转写+关键帧 → 结构化纪要
import base64, json, os
from anthropic import Anthropic

client = Anthropic()

def summarize(segments: list[dict]) -> dict:
    content = [{"type": "text", "text":
        "这是会议转写(带起始秒)与关键帧。严格输出 JSON,字段:"
        "topics(议题列表)、decisions(结论列表)、todos"
        "(待办,含负责人)、timeline(每议题一段 MM:SS 摘要)。"
        "只依据转写与画面可见信息,未见填 null。\n"
        + "\n".join(f'[{s["start"]:.0f}s] {s["text"]}' for s in segments)}]
    for s in segments[::10]:                     # 每约 10 段挂一帧控 token
        with open(s["frame"], "rb") as f:
            content.append({"type": "image", "source": {
                "type": "base64", "media_type": "image/jpeg",
                "data": base64.b64encode(f.read()).decode()}})
    r = client.messages.create(
        model=os.environ["CLAUDE_MODEL"], max_tokens=4096,
        messages=[{"role": "user", "content": content}])
    return json.loads(r.content[0].text)

上面的 summarize 把全部转写压成一份提示、关键帧按段采样挂载,模型拿到的就是第 4 节产物的紧凑版。主流程把预处理与纪要拼起来,并把待办直接落成清单:

# minutes_agent.py —— 主流程
import sys

def main(src: str) -> dict:
    segments = prep(src, frame_step=10)          # 第 4 节的预处理
    minutes = summarize(segments)
    for t in minutes["todos"]:                   # 待办逐条核时间轴
        print(f'[ ] {t["task"]} — {t.get("owner", "未指明")}')
    return minutes

if __name__ == "__main__":
    main(sys.argv[1])                            # python minutes_agent.py rec.mp4

两个提示词细节决定纪要质量:一是时间锚点必须进输出(timeline 字段),纪要没有时间轴就没有回溯能力;二是沿用第 3 篇的防幻觉句式"未见填 null"——会议纪要里一条编造的待办比没有纪要更糟。关键帧的挂载频率按预算调:segments[::10] 是十秒一帧的保守值,纯谈话会议甚至可以完全不挂帧,只留转写。

7、长视频与实时流

超过模型上限的长视频(Gemini 低清 3 小时、默认 1 小时为界)先物理切分再逐段处理,切分用流复制不重新编码,秒级完成:

# 按 10 分钟切分,-c copy 不重编码
ffmpeg -i long.mp4 -f segment -segment_time 600 -c copy part_%02d.mp4

切分有两个工程要点:相邻切片重叠 30~60 秒,防止一个议题正好被切断后两头都丢上下文;纪要阶段先逐段生成、再全局合并去重,不要把所有切片的产物一次性塞进一个请求。Gemini 路线则优先试 agentic 模式,模型自己按需加载时间窗,多数场景免切分。至于实时视频流(直播、摄像头),思路完全不同——不是"攒一段送一段",而是低频抽帧加流式转写的持续管线,与实时语音同构,本系列第 9 篇展开。

8、总结

回顾三条路线:Gemini 原生视频赢在时间戳理解与音画同步,fps、media_resolution、时间窗剪辑是三道成本闸门;逐帧方案通吃所有视觉模型,工程在抽帧策略(固定间隔对均匀内容、场景检测对幻灯片)与选帧(均匀概览、运动量定位);转写加关键帧的组合管线成本最低、产物即是多模态 RAG 的数据形状,是会议与访谈类视频的默认答案。计费口径记两组数:Gemini 低清约 100、默认约 300 token 每秒;逐帧按帧数乘图像 token,一小时视频不砍帧率必然爆预算。

留一个练习:找一段 20 分钟以上的技术教程视频,用组合管线生成章节时间轴——转写后让模型输出 [{MM:SS, 章节标题, 一句话摘要}] 的 JSON,再抽查三处时间戳核对是否对得上画面。想进一步,就把逐帧方案接上:对每个章节起点抽一帧,生成"章节+缩略图"的目录页。

对于手握大量会议录像或教程资产的团队,建议先把本篇的纪要 Agent 跑通一条真实录像——预处理、token 预算、防幻觉三个关卡全在链路上,它也是通往视频知识库检索(组合管线产物直接入库)最短的那块跳板。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

598
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

370
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

338