背景:视频理解的难点不只是”多传几张图”
很多团队做完图片理解后,会自然地把视频理解看成”把视频拆成多张图片再问模型”。这个思路能跑通 Demo,但进入生产后会很快遇到三个问题。

第一,成本不可控。 视频不是一张图,而是按时间展开的图像序列。一个 30 秒视频按 1 FPS 采样是 30 帧,按 5 FPS 采样就是 150 帧;如果每帧再使用高分辨率处理,media token、上传时间、编码时间和模型延迟都会明显上升。
第二,质量不可解释。 模型答错时,你需要知道它到底看到了哪些帧,哪些关键动作被跳过,时间戳有没有偏移,字幕和音频是否被纳入,长视频是否只取了前几分钟。如果只保存最终 prompt,很难复盘。
第三,工程链路更长。 视频输入涉及文件上传、转码、解码、抽帧、时间戳对齐、音轨处理、分段、缓存、模型调用和结果回灌。任何一层都可能失败——文件还在处理、视频损坏、某些帧无法解码、上传体积超限、时间戳丢失、模型只看到了缩略帧。
Gemini API 的视频理解文档明确说明,Gemini 可以处理视频,支持描述、分割、抽取信息、回答视频内容问题,并可以引用视频中的特定时间戳;它也区分 File API、Cloud Storage、Inline Data 和 YouTube URL 等输入方式。vLLM 的多模态输入文档也把视频解码后端、帧恢复、预抽帧序列和视频元数据作为单独问题处理。这说明视频输入已经不是简单附件,而是一条需要治理的数据管线。
核心原理:视频输入要同时管理内容、时间和预算
视频文件不是模型输入的最终形态
模型最终看到的通常不是原始 MP4 文件本身,而是由平台或自建服务从视频中提取出的视觉表示:帧、patch、视觉 token、时间片段、音频片段或字幕。不同平台会隐藏一部分处理细节,但生产系统仍要在进入模型前掌握最关键的元数据:
type VideoInputProfile = {
videoId: string;
sourceUri: string;
mimeType: "video/mp4" | "video/webm" | "video/quicktime";
durationSec: number;
fps: number;
width: number;
height: number;
bytes: number;
hasAudio: boolean;
decodeProfile: string;
samplingProfile: string;
mediaResolution: "low" | "medium" | "high";
};
这份 profile 不是为了文档好看,而是为了让后续预算、缓存、回放和质量评估有共同基线。
Media Token 预算要按帧计算
Gemini 的 media resolution 文档说明,media_resolution 用来控制 Gemini API 处理图片、视频、PDF 等媒体输入时分配的最大 token 数,从而平衡质量、延迟和成本。文档还列出视频场景中不同分辨率的 token 近似口径:
| 分辨率 | 每帧 token 估算 | 适用场景 |
|---|---|---|
| Low / Medium | ~70 tokens | 普通视频摘要、动作识别 |
| High | ~280 tokens | 密集文本、小细节 OCR |
这个信息对生产设计非常关键。它意味着视频预算可以抽象为:
media_tokens ≈ sampled_frames × tokens_per_frame(resolution)
如果一个 10 分钟视频按 1 FPS 采样,大约是 600 帧。即使每帧只按 70 token 估算,也已经是 42,000 media tokens,还不含文本 prompt、字幕、音频转写和模型输出。对于客服质检、课堂摘要、会议录像、监控片段分析等场景,如果不先做采样和预算,成本很容易失控。
时间戳是视频理解的第一公民
视频问题经常不是”画面里有什么”,而是”什么时候发生了什么”。例如:
- 用户在哪一秒点击了错误按钮
- 事故发生前 5 秒出现了什么
- 讲师在第几分钟解释了某个概念
- 监控画面中人物何时进入、何时离开
- 商品视频里展示缺陷的是哪一段
因此,抽帧不能只保存图片,还要保存帧索引、原始 FPS、时间戳、采样策略和总帧数。vLLM 文档也指出,当客户端预先抽帧并发送 video/jpeg 序列时,可以通过 media_io_kwargs 传入 fps、frames_indices、total_num_frames、duration 等元数据,以保留原始视频的时间信息。
一个可回放的采样记录应类似这样:
{
"video_id": "vid_20260709_001",
"duration": 1800.0,
"fps": 30.0,
"total_num_frames": 54000,
"sampling_profile": "scene_aware_32_frames_v2",
"sampled_frames": [
{ "frame_index": 0, "timestamp": 0.0, "reason": "start" },
{ "frame_index": 4500, "timestamp": 150.0, "reason": "scene_change" },
{ "frame_index": 12600, "timestamp": 420.0, "reason": "query_relevant" }
]
}
没有这类记录,模型说”在 07:00 左右出现异常”时,平台很难判断它是真的看到了这一段,还是根据稀疏帧猜的。
工程落地:一条可上线的视频输入管线
1. 文件准入:先判断能不能进管线
视频进入模型前,先做文件准入,而不是直接上传。准入层至少检查:
- MIME 类型和扩展名是否匹配
- 文件大小是否超过 inline 上限
- 时长是否超过任务允许范围
- 分辨率和帧率是否异常
- 是否有音轨、字幕轨
- 是否可被解码
- 是否包含过多黑屏、纯色帧或损坏片段
Gemini 文档建议:较大文件、较长视频、需要复用的视频使用 File API;小视频可以 inline;大于 20MB 总请求或显著时长的视频应使用 Files API。生产系统也应采用类似分流——短视频走同步或 inline,大视频走异步上传和处理状态轮询。
type VideoAdmissionDecision =
| { action: "inline"; reason: string }
| { action: "upload_file_api"; reason: string }
| { action: "async_job"; reason: string }
| { action: "reject"; reason: string };
function decideVideoAdmission(v: VideoInputProfile): VideoAdmissionDecision {
if (v.bytes > 20 * 1024 * 1024 || v.durationSec > 60) {
return { action: "upload_file_api", reason: "large_or_long_video" };
}
if (v.durationSec > 1800) {
return { action: "async_job", reason: "long_video_requires_segmented_processing" };
}
if (!v.mimeType.startsWith("video/")) {
return { action: "reject", reason: "unsupported_mime_type" };
}
return { action: "inline", reason: "small_one_off_video" };
}
准入决策要写入日志。否则同一类视频有时同步、有时异步,后续排障会很混乱。
2. 解码与探测:把 FFmpeg / OpenCV 变成可观测步骤
视频输入的第一类故障经常发生在解码阶段:容器格式可读但某些帧损坏,音轨异常,时长元数据不准,变帧率视频时间戳不连续。FFmpeg 文档提供了 stream selection、mapping 等基础能力,vLLM 文档则说明它的视频解码后端可以选择 OpenCV、PyAV 或 TorchCodec,而这些后端最终都依赖 FFmpeg。
生产系统应把解码探测作为显式步骤:
ffprobe -v error \
-show_entries format=duration,size \
-show_streams \
-of json input.mp4
探测结果至少写入:duration、fps、width、height、codec、audio streams、subtitle streams、frame count estimate、probe error。不要等模型调用失败后才发现视频文件本身不可用。
3. 采样策略:不要所有视频都用固定 FPS
最简单的采样是固定 FPS,例如每秒取一帧。但这并不总是最优。
| 视频类型 | 推荐策略 | 说明 |
|---|---|---|
| 普通视频摘要 | 均匀采样、低 FPS | 目标是覆盖整体内容 |
| 操作录像、监控、体育、教学演示 | 场景切分 + 关键帧 + 问题相关采样 | 依赖关键动作定位 |
| 屏幕录制、代码演示、表格、PPT 录屏 | 关键帧 + 间隔采样,高分辨率 | 需要读小字 |
近期视频理解研究也反复指出,长视频理解的难点在于从大量帧中选出和问题相关的关键片段。GenS 论文讨论了 long-form video 中大量帧带来的计算负担,并提出查询相关的 frame sampler;GroundVTS 也指出均匀采样可能造成关键帧稀疏和时间线索丢失。生产系统不一定要直接采用论文算法,但应避免把”固定每秒一帧”当成唯一策略。
一个实用的采样配置可以这样写:
video_sampling_profiles:
summary_low_cost:
strategy: uniform
target_frames: 24
media_resolution: low
use_case: general_summary
action_tracking:
strategy: scene_aware
target_frames: 64
media_resolution: medium
keep_timestamps: true
use_case: event_localization
screen_recording_ocr:
strategy: keyframe_plus_interval
target_frames: 40
media_resolution: high
keep_timestamps: true
use_case: text_heavy_video
4. 时间戳对齐:采样帧必须能回到原视频
采样时不要只输出图片文件,还要输出 frame manifest:
type SampledFrame = {
frameId: string;
frameIndex: number;
timestampSec: number;
width: number;
height: number;
reason: "uniform" | "keyframe" | "scene_change" | "query_relevant";
imageSha256: string;
};
模型回答中若引用”12:46 附近”,后端应该能定位到哪些输入帧覆盖了这一时间段,并把截图、原始帧、采样参数拿出来复盘。
对于预抽帧再上传的链路,尤其要保留 fps、frames_indices、total_num_frames、duration。否则模型看到的是一串图片,而不是有时间结构的视频。
5. Media Token 预算:先预算,再采样,再请求
视频请求应先估算预算,再决定采样策略:
type VideoBudget = {
maxMediaTokens: number;
maxFrames: number;
resolution: "low" | "medium" | "high";
allowAdaptiveSampling: boolean;
};
function estimateMediaTokens(frames: number, resolution: "low" | "medium" | "high") {
const tokensPerFrame = resolution === "high" ? 280 : 70;
return frames * tokensPerFrame;
}
function chooseFrameCount(durationSec: number, budget: VideoBudget) {
const byBudget = Math.floor(budget.maxMediaTokens / estimateMediaTokens(1, budget.resolution));
const byProfile = durationSec < 60 ? 24 : durationSec < 600 ? 48 : 96;
return Math.min(byBudget, byProfile, budget.maxFrames);
}
这个估算不必追求和供应商账单完全一致,但要足够用于准入、告警和成本归因。真实 token 用量仍应以供应商返回的 usage 或平台账单为准。
6. 缓存:缓存文件,也缓存采样结果
视频缓存至少分三层:
| 缓存层 | 内容 | 说明 |
|---|---|---|
| 原始文件缓存 | 同一 video sha256 不重复上传或处理 | 避免重复传输和存储 |
| 探测缓存 | duration、fps、codec、streams 等元数据 | 不用每次重新探测 |
| 采样缓存 | 同一视频、同一 profile、同一分辨率下的帧序列 | 复用已抽取的帧 |
缓存 key 应包含完整维度:
video_sample:{video_sha256}:{sampling_profile}:{resolution}:{decoder_version}:{preprocess_version}
不要只用 video_id,因为同一个视频在不同任务中可能使用不同采样策略和不同分辨率。
质量评估:视频管线要能回放,而不是只看最终答案
视频理解评估至少要覆盖三类任务:
- 整体摘要:模型是否覆盖主要场景、人物、动作、结论
- 时间定位:模型是否能回答”某个事件发生在什么时候”
- 细节识别:模型是否能读出屏幕文字、表格、UI 状态或小物体
每条评测样本应保存:原视频版本、采样 profile、抽帧 manifest、模型版本、media resolution、提示词版本和人工参考答案。
{
"case_id": "video_eval_ui_bug_001",
"video_sha256": "...",
"task_type": "screen_recording_ocr",
"expected_timestamps": [125.0, 138.5],
"expected_observation": "用户点击保存后出现权限错误提示",
"sampling_profile": "screen_recording_ocr_v2",
"media_resolution": "high"
}
如果模型答错,应先判断:关键帧是否被采到?采到了但分辨率不够?时间戳对齐是否错误?还是模型理解失败?这比直接更换模型更有效。
适用场景
这套视频输入管线适合以下场景:
- 客服质检、会议录像摘要、课堂视频问答
- 操作录屏分析、UI 测试、Computer-use 回放
- 安防片段、事故视频、质检视频的事件定位
- 商品视频、培训视频、直播切片的内容理解
- 长视频离线分析与批处理任务
不适合直接套用的场景包括:强实时视频通话、医疗影像诊断、法律证据原件分析、工业缺陷检测。这些场景需要更严格的模型评估、人工复核和合规链路。
常见误区
误区一:帧越多越好
帧越多,成本和延迟越高,但质量不一定线性提升。对普通摘要,少量代表帧可能足够;对事件定位,关键是采到关键时刻,而不是平均塞入更多帧。
误区二:只保存抽出的图片,不保存时间戳
没有时间戳,视频就退化成图片集合。模型输出无法回到原视频,质量评估也无法判断关键事件是否被采到。
误区三:所有视频都用高分辨率
高分辨率适合读小字、UI、表格和细节;普通动作识别和摘要通常低/中分辨率即可。Gemini 文档也建议一般视频使用 low 或 medium,高分辨率主要用于 text-heavy 或小细节场景。
误区四:把解码失败当成模型失败
很多视频问题发生在模型之前:文件损坏、帧读取失败、变帧率时间戳异常、音轨缺失。解码和采样步骤必须有独立错误码。
误区五:长视频全部一次性送入模型
长视频更适合分段处理:先按章节、场景或时间窗口切分,再做局部分析,最后做汇总。一次性塞入过多帧既贵,也难以解释。
上线检查清单
文件准入:
- 是否限制文件大小、时长、分辨率、帧率和 MIME 类型
- 是否区分 inline、File API、Cloud Storage、异步任务
- 是否记录原始文件 hash 和来源
- 是否处理文件仍在 processing 或 failed 的状态
解码与采样:
- 是否用 ffprobe 或等价工具记录 duration、fps、codec、streams
- 是否有统一采样 profile
- 是否保存 frameIndex、timestamp、reason、image hash
- 是否支持损坏帧跳过、恢复或失败标记
预算与成本:
- 是否按采样帧数和 media resolution 估算 media tokens
- 是否按任务类型限制 max frames
- 是否记录实际 usage、延迟和成本
- 是否支持超预算时自适应降低采样密度
质量与回放:
- 是否有视频黄金样本集
- 是否覆盖摘要、事件定位、细节识别三类任务
- 是否能回放同一视频、同一采样策略、同一模型版本
- 是否区分采样失败、解码失败、模型理解失败
结论
LLM 视频理解不是把 MP4 传给模型这么简单。真正的生产问题在于:哪些视频能进来,抽哪些帧,按什么时间轴对齐,花多少 media token,失败后怎么回放,质量怎么评估。
建议团队先把视频输入管线拆成五个可观测步骤:文件准入 → 解码探测 → 帧采样 → 时间戳对齐 → 预算检查。只要这五步稳定,后续无论接 Gemini、OpenAI 图片帧方案、自建 vLLM 多模态模型,还是离线批处理,都能有清晰的成本边界和排障路径。
参考资料
- Google Gemini API, Video understanding: https://ai.google.dev/gemini-api/docs/video-understanding
- Google Gemini API, Media resolution: https://ai.google.dev/gemini-api/docs/media-resolution
- Google Cloud / Vertex AI, Video understanding: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/video-understanding
- vLLM, Multimodal Inputs: https://docs.vllm.ai/en/latest/features/multimodal_inputs/
- FFmpeg Documentation: https://ffmpeg.org/ffmpeg.html
- OpenAI API, Images and vision: https://platform.openai.com/docs/guides/images-vision
- Generative Frame Sampler for Long Video Understanding: https://arxiv.org/abs/2503.09146