B 站技术区的视频,动辄 40 分钟起。我有个习惯------每看到一个"标记稍后看"的视频,它基本就永远躺在列表里了。
上个月我写了一个工具:把视频丢进去,10 分钟后给你一份 300 字的摘要,包含核心观点和时间轴。不是看标题和字幕瞎猜------是真的提取画面,让 AI 看画面内容。
这篇文章把完整实现给你。核心技术:ffmpeg 提取关键帧 → Vision API 理解每一帧 → LLM 串联成摘要。
整体思路
视频太长,AI 不可能一帧帧看。核心策略是采样:
视频文件(.mp4)
→ ffmpeg 每 30 秒抽一帧(关键帧变化大的地方多抽)
→ 每帧交给 Vision API 描述画面内容
→ 把所有帧的描述拼起来
→ LLM 生成主题摘要 + 时间轴
这跟 10 万行日志分析(场景四)的思路一模一样------采样,不是全量。
第一步:ffmpeg 提取关键帧
go
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
// 从视频中按时间间隔抽取关键帧
func extractFrames(videoPath, outputDir string, intervalSec int) ([]string, error) {
os.MkdirAll(outputDir, 0755)
// ffmpeg 每 N 秒抽一帧,保存为 jpg
outputPattern := filepath.Join(outputDir, "frame-%04d.jpg")
cmd := exec.Command("ffmpeg",
"-i", videoPath,
"-vf", fmt.Sprintf("fps=1/%d", intervalSec), // 每 intervalSec 秒一帧
"-q:v", "3", // 高质量
"-y",
outputPattern,
)
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg 提取帧失败: %w", err)
}
// 收集所有帧文件
entries, _ := os.ReadDir(outputDir)
var frames []string
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".jpg") {
frames = append(frames, filepath.Join(outputDir, e.Name()))
}
}
return frames, nil
}
// 获取视频总时长(秒)
func getVideoDuration(videoPath string) (float64, error) {
cmd := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
videoPath,
)
out, err := cmd.Output()
if err != nil {
return 0, err
}
return strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
}
一个 40 分钟的视频,每 30 秒一帧 = 80 帧。每帧约 100KB,总共 8MB------一张流量的量。
第二步:Vision API 描述每一帧
go
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"bytes"
)
type FrameDescription struct {
FrameNum int
Timestamp string // "MM:SS" 格式
Description string
}
// 单帧描述
func describeFrame(imagePath string, frameNum int, timestamp string) (*FrameDescription, error) {
data, _ := os.ReadFile(imagePath)
b64 := base64.StdEncoding.EncodeToString(data)
prompt := `请用一句话描述这张视频截图的画面内容(中文,不超过30字)。
只描述你看到的,不要推测。如果画面包含文字,把文字内容写出来。`
// 调 Vision API(复用上一篇的 askWithImage)
text, err := askWithImageBase64(b64, prompt)
if err != nil {
return nil, err
}
return &FrameDescription{
FrameNum: frameNum,
Timestamp: timestamp,
Description: strings.TrimSpace(text),
}, nil
}
// 批量处理,控制并发
func describeAllFrames(frames []string, intervalSec int) ([]FrameDescription, error) {
results := make([]FrameDescription, len(frames))
sem := make(chan struct{}, 3) // 并发 3 个请求
for i, frame := range frames {
sem <- struct{}{}
go func(idx int, path string) {
defer func() { <-sem }()
ts := fmt.Sprintf("%02d:%02d",
idx*intervalSec/60,
idx*intervalSec%60,
)
desc, err := describeFrame(path, idx+1, ts)
if err == nil {
results[idx] = *desc
} else {
results[idx] = FrameDescription{
FrameNum: idx + 1,
Timestamp: ts,
Description: fmt.Sprintf("[处理失败: %v]", err),
}
}
}(i, frame)
}
// 等待所有完成
for j := 0; j < 3; j++ {
sem <- struct{}{}
}
return results, nil
}
80 帧 × 3 并发 = 约 90 秒处理完(每帧 3 秒左右)。
第三步:LLM 串联成摘要
go
func generateVideoSummary(descriptions []FrameDescription, duration float64) (string, error) {
// 构建完整的帧描述文本
var builder strings.Builder
builder.WriteString(fmt.Sprintf("视频总时长: %d 分 %d 秒\n\n帧内容描述:\n",
int(duration)/60,
int(duration)%60,
))
for _, d := range descriptions {
builder.WriteString(
fmt.Sprintf("[%s] 帧%d: %s\n", d.Timestamp, d.FrameNum, d.Description),
)
}
prompt := fmt.Sprintf(`你是一个视频摘要助手。以下是每隔30秒提取的视频帧描述。
请根据这些描述生成:
## 视频主题
(一句话概括)
## 核心内容
(3-5 个要点,每个不超过 40 字,用 - 列出)
## 时间轴
(按时间段标注主要内容,如 [00:00-05:00] xxx)
## 一句话总结
如果某段时间没有有效信息(画面无变化或无法识别),标注为"画面静默/无变化"。
帧描述:
%s`, builder.String())
return callLLM("你是一个视频摘要助手。准确、简洁、不要编造任何信息。", prompt)
}
实测结果
我拿一个 38 分钟的前端架构演讲视频做测试:
输入: 76 帧截图,每帧一行描述
输出摘要:
## 视频主题
前端微前端架构从巨石应用到微前端的演进过程及工程化实践
## 核心内容
- 巨石应用 3 个痛点:构建慢、部署耦合、技术栈锁定
- Module Federation 实现运行时加载子应用
- 共享依赖策略:singleton vs shared
- 子应用间通信通过 EventBus,避免直接耦合
- 部署策略:独立构建 + 动态路由 + 灰度发布
## 时间轴
[00:00-08:00] 问题背景:为什么需要微前端
[08:00-18:00] 技术方案:Module Federation 原理演示
[18:00-28:00] 工程实践:代码组织 & 通信方案
[28:00-35:00] 部署 & 灰度发布
[35:00-38:00] 总结 & Q&A
## 一句话总结
微前端解决的不是技术问题,是组织协作问题。
对比实际看完视频的结论------完全正确。核心要点都抓到了,时间轴准确度在 ± 2 分钟内。
成本分析
| 步骤 | 数据量 | 成本 |
|---|---|---|
| 提取帧 | 38 分钟视频 → 76 张 JPEG | 0(本地 ffmpeg) |
| Vision API 描述 | 76 帧 | $0.01 × 76 ≈ ¥5.50 |
| LLM 摘要 | ~5000 token | ¥0.005 |
| 合计 | 约 ¥5.50 |
比我自己花 38 分钟看完,省了 35 分钟,花了 5 块 5。这个 ROI 我自己觉得太值了。
优化技巧
-
跳过画面无变化的部分。 可以用 ffmpeg 的
scene滤镜检测画面变化:select='gt(scene,0.1)'------只有变化超过 10% 的帧才保留。 -
优先识别带文字的画面。 演讲 PPT 画面比人脸画面有价值得多。可以先用简单的文字检测 skip 掉纯人脸的帧。
-
不只看画面,也读字幕。 结合 ffmpeg 字幕提取 + 画面帧描述 = 完整理解。字幕提取:
bashffmpeg -i video.mp4 -map 0:s:0 output.srt
这个工具的核心价值
不是「自动看视频」这么简单。是可批量 + 可检索。
你看完 100 个视频可能忘掉 80 个。但如果每个视频都有一份结构化摘要,你可以搜索「微前端 Module Federation 部署」,立刻找到所有相关视频和对应的时间点。
这也是我接下来要做的事------把我收藏的所有技术视频跑一遍,建一个可搜索的技术视频知识库。
下一篇进入一个更有趣的领域:让 AI 自己写代码、自己跑、看结果再调整。Code Interpreter。