视频管线¶
Explanation · 原理进阶
一句话理解
视频 = 图像流 + 音频流 + 时间。最核心的工程动作是抽帧(把连续视频转成有限帧集合),之后对关键帧跑图像管线 + 对音频跑音频管线,最后再做"时序聚合"形成视频级向量。
TL;DR
- 抽帧策略决定后续所有质量:固定时间间隔、关键帧、镜头变化检测
- 帧级 embedding → 时序聚合(mean / attention)得视频级
- 音频流独立处理:ASR + audio embedding
- 长视频分段处理,避免单段过长
- 存储分层:原视频 / 代表帧 / 音频文件分别放对象存储
完整流水线¶
flowchart LR
upload[上传视频] --> store[(对象存储<br/>原视频)]
store --> probe[探针<br/>时长/分辨率/编码]
store --> frames[抽帧<br/>ffmpeg]
store --> audio[音频抽取<br/>ffmpeg]
frames --> shot[镜头分割]
shot --> keyframe[代表帧选择]
keyframe --> image_pipe[图像管线<br/>CLIP embedding]
audio --> audio_pipe[音频管线<br/>ASR + embedding]
image_pipe --> agg[时序聚合<br/>帧向量 → 视频向量]
agg --> tbl[(Iceberg/Lance: video_assets)]
audio_pipe --> tbl
keyframe -.URI.-> tbl
一、探针(Probing)¶
先 ffprobe 拿元数据:
关键字段:
duration(秒)width/height/fpscodec(H.264 / H.265 / VP9 / AV1)bit_rate- 是否包含音频流
长视频判断:> 10 分钟 → 分段处理;> 1 小时 → 必须分段。
二、抽帧策略¶
三种主流:
方案 A:固定时间间隔¶
最简单。每 N 秒抽一帧。
适合:短视频、均匀内容。
方案 B:镜头变化检测(Shot Detection)¶
检测相邻帧的显著视觉变化(通常用帧间差异 / histogram / SIFT 特征综合判定)· 在变化点切分镜头 · 每个镜头取代表帧(通常中间帧)。
# ffmpeg 的 scene 过滤器基于帧间差异打分 · 0.3 是常见阈值(0.2-0.4 视内容类型调)
ffmpeg -i video.mp4 -vf "select='gt(scene,0.3)',showinfo" -vsync vfr frame_%04d.jpg
或用 Python PySceneDetect。
适合:电影、剪辑视频、新闻。
方案 C:固定数量¶
限定总帧数(比如 32 帧),等间隔取。
适合:训练数据预处理、固定预算场景。
选型规则:
- 检索目的 → 方案 B(每镜头一帧质量最好)
- 快速标注 → 方案 A
- 训练 → 方案 C
三、镜头 + 代表帧¶
一个镜头可能有几十上百帧,需要代表帧:
- 中间帧 —— 最简单
- 亮度 / 颜色方差最高帧 —— 信息丰富的一帧
- blur 检测后最清晰的帧 —— 规避运动模糊
代表帧存到对象存储(keyframe_uri),供前端展示 + embedding。
四、音频流单独处理¶
- 降采样到 16kHz(ASR 标准)
- 单声道(mono)
- 走 音频管线 跑 ASR + audio embedding
五、Embedding 与时序聚合¶
帧级 embedding 完事后,需要聚合成视频级向量供检索:
简单平均(mean pooling)¶
快、稳、够用。
Attention Pooling¶
用 transformer 聚合,更精细。成本高,通常离线训练一个 pooling model。
保留多向量(ColBERT-style)¶
视频级不聚合,检索时做"视频级 Top-K = Σ 帧级相似度"。
默认推荐:平均池化起步,必要时再升级。
六、视频检索的特殊性¶
视频查询有两种模式:
- "找相似片段"(模糊匹配)—— 视频级向量足够
- "定位到具体秒数"(精确)—— 保留帧级 + offset
对于精确定位,表结构应包含:
CREATE TABLE video_frame_vectors (
video_id BIGINT,
frame_idx INT,
ts_sec FLOAT,
clip_vec VECTOR<FLOAT, 512>,
keyframe_uri STRING
) USING lance
PARTITIONED BY (bucket(32, video_id));
前端展示结果时能定位 video_id + ts_sec。
七、表结构¶
CREATE TABLE video_assets (
video_id BIGINT,
raw_uri STRING,
duration_sec FLOAT,
width INT,
height INT,
fps FLOAT,
keyframe_uris ARRAY<STRING>,
caption_aggregate STRING,
transcript_full STRING,
video_vec VECTOR<FLOAT, 512>,
audio_vec VECTOR<FLOAT, 512>,
text_vec VECTOR<FLOAT, 1024>,
embedding_version STRING,
owner STRING,
visibility STRING,
tags ARRAY<STRING>,
ts TIMESTAMP
) USING iceberg
PARTITIONED BY (days(ts), bucket(16, video_id));
生产级 Pipeline 设计要点¶
视频比图像更重——单条视频可达 GB 级 · 单次处理 10 分钟+。生产级注意:
| 问题 | 做法 |
|---|---|
| 同资产幂等 | video_id(或视频文件 SHA256)作主键 · 重跑产出一致 |
| 大文件失败局部重试 | 抽帧阶段失败不重整条视频 · 按帧段 checkpointing(成功的帧段入库 · 失败段重抽) |
| 毒文件隔离 | 损坏编码 / 不支持格式 / 音视频同步错 · DLQ 表记录 + 失败原因 · 不阻塞主流 |
| 异步 GPU worker | embedding 推理和视频解码分离不同 worker(解码 CPU 多 · embedding GPU 多)· 通过对象存储中间层解耦 |
| 中间产物存储 | 抽出的帧图 / 关键帧 / 音轨 · 写对象存储 · 湖表只存 URL · 避免表爆 |
| 回填成本巨大 | 模型升级要全量 re-embedding 很贵 · 提前估算 · 优先升级热门视频 · 冷视频延迟升级 |
和 管线韧性 横切主题呼应 · 视频管线对 Backpressure / Schema Evolution / DLQ 需求更高。
陷阱¶
- 抽帧过密 —— 1 小时视频抽 1 帧/秒 = 3600 帧,存储和 embedding 成本爆
- 忽略镜头变化 —— 节奏慢的视频大量重复帧
- 不处理旋转 —— 手机拍的竖屏视频被当横屏处理
- Long-tail 编码格式 —— HEVC / ProRes 需要特定 ffmpeg build
- 音画不同步 —— 音频独立 embed 但不对齐时间轴时的 debug 噩梦
相关¶
延伸阅读¶
- ffmpeg 官方文档
- PySceneDetect: https://www.scenedetect.com/
- VideoCLIP / X-CLIP 系列论文