三角洲行动直播切片 AI 系统 — 项目文档

一、项目简介

本项目是一个游戏直播自动切片系统,针对「三角洲行动」等 FPS 游戏直播,通过语音转写 + 画面 OCR + 弹幕联合分析自动识别高光时刻,并使用强化学习(偏好学习)逐步学习用户切片偏好,最终输出可直接发布的短视频片段。

核心功能

  1. 语音转写 — 从 FLV 视频提取音频,通过 faster-whisper 将主播语音转为带时间戳的文本,并做情绪/语速/音量分析
  2. 弹幕分析 — 解析抖音弹幕 JSON 数据,计算密度曲线、检测峰值、提取关键词和高频词
  3. 画面 OCR — GPU 硬件解码视频帧,PaddleOCR 识别击杀信息和比分面板,精确到秒级定位击杀时刻
  4. 多模态联合分析 — 语音情绪 + 弹幕密度 + OCR 事件三流融合,自动生成候选切片并评分
  5. RL 偏好学习 — 用成对偏好学习替代硬编码规则评分,每次用户标注最佳切片后模型自动训练改进
  6. 双模式切割 — 同时生成长切片(15-150s,全类型高光)和短切片(<30s,仅 PvP 击杀真人)

工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FLV 视频 ──→ ffmpeg 提取音频 ──→ faster-whisper 转写 ──→ 带时间戳文本 + 情绪标注
│ │
├──→ ffmpeg NVDEC 取帧 ──→ PaddleOCR 击杀检测 ──→ 击杀事件列表 │
│ │
JSON 弹幕 ──→ 时间归一化 ──→ 密度/峰值/关键词/高频词分析 │

┌───────────────────────────────────────────────┘

事件推断 → 锚点聚类 → 候选生成 → 去重合并


RL 模型评分 (35 维特征 → 0-10 评分)
┌─ Warm Start:合成数据拟合规则系统
└─ 在线学习:用户标注 → 成对偏好训练


人工确认 → ffmpeg NVENC GPU 切割 → 视频片段输出

二、开发过程

2.1 项目起点

项目起步于原版 delta-force-clip skill,使用硬编码规则评分——手工设置每个特征的加减分值(”弹幕密度 > 3x 基线 +2 分,出现击杀事件 +3 分”)。规则系统可以产出及格的切片(约 70% 可用率),但存在两个问题:

  • 不同剪辑师的偏好差异大,规则无法适应
  • 调整评分逻辑需要直接修改 Python 源码,门槛高

RL 版的目标是将评分替换为可学习的偏好模型,让用户通过标注行为而非修改代码来定制切片策略。

2.2 迭代过程

Phase 1 — 规则系统基线

  • 实现完整的多模态分析 pipeline:语音转写 → 弹幕分析 → 画面 OCR → 事件推断 → 候选生成 → 评分 → 切割
  • 事件推断:从语音中检测击杀/死亡/灭队/救援,从 OCR 中检测击杀/对局结束
  • 锚点聚类:基于时间窗口合并邻近事件,生成候选片段
  • 硬编码评分:score_candidate() 按特征加减分,输出 0-10 分
  • GPU 加速:ffmpeg NVENC/NVDEC 硬件编解码,PaddleOCR GPU 推理

Phase 2 — RL 模型设计

  • 设计 35 维特征向量(弹幕密度 4 + 弹幕内容 4 + 高频词 4 + 语音情绪 5 + 游戏事件 8 + OCR 事件 4 + 叙事结构 3 + 位置 1 + 其他 2)
  • 实现 FeatureExtractor:从候选片段提取固定长度特征
  • 实现 PairwisePreferenceLearner:基于 LogisticRegression 的成对偏好学习器
  • 数据增强:每个正对 (A-B, 1) 生成负对 (B-A, 0),保证二分类信号

Phase 3 — Warm Start 合成数据初始化

  • 生成 3000 个随机特征向量,用经验分布模拟真实数据
  • 用原规则系统为每个样本计算伪评分,Linear Regression 拟合 → 得到近似规则的初始权重
  • OCR 特征 ×2.5、高频词特征 ×2.0 显式增强(画面客观证据 + 弹幕话题信号)

Phase 4 — 反馈收集与训练

  • 实现 FeedbackStore:JSONL 持久化 + .npz 特征向量缓存
  • 每次标注自动生成 N×(M-N) 个训练对
  • 增量训练:用 partial_fit + 高样本权重加速适应新反馈
  • --feedback 交互式标注、--retrain 批量重训练、--reset-model 回退

Phase 5 — 实际使用迭代

  • 第一版:自动分析 25 个候选 → 用户手工筛选 12 个 + 后补对局结算
  • 反馈训练:11 个选中 vs 14 个未选中 → 9 个成对比较 → 权重更新
  • 关键发现:用户舍弃了所有弹幕高潮/纯搞笑类片段,偏好纯击杀内容
  • 弹幕关键词词典评估:好评词/笑声词权重下降,灭队/对局结束权重上升

三、技术栈

层级 技术 用途
语言 Python 3.10+ 全部代码
语音转写 faster-whisper large-v3 CUDA float16 推理,中文语音→文本
音频处理 ffmpeg, soundfile, numpy 音频提取、RMS 能量、语速计算
画面 OCR PaddleOCR GPU 击杀信息区域文字识别
视频处理 ffmpeg NVENC/NVDEC GPU 硬件解码取帧 + 硬件编码切割
机器学习 scikit-learn LogisticRegression 成对偏好学习分类器
序列化 joblib, numpy .npz 模型权重持久化 + 特征向量缓存
弹幕协议 JSON (抖音格式) events[].content 聊天消息解析
数据格式 JSON (转录/分析/切割信息), JSONL (反馈历史)
分词 jieba (可选) 弹幕中文分词,不可用时回退字符 n-gram

四、技术实现细节

4.1 音频提取与语音转录

音频提取

1
ffmpeg -i input.flv -ac 1 -ar 16000 -y output.wav
  • -ac 1 单声道,-ar 16000 16kHz(Whisper 标准输入)
  • 输出 PCM WAV,约 256 kbps

语音转录:使用 faster-whisper large-v3 模型,CUDA + float16 推理。参数配置:

1
2
3
4
5
6
7
model = WhisperModel("large-v3", device="cuda", compute_type="float16")
segments_gen, info = model.transcribe(
audio_path, language="zh",
word_timestamps=True, beam_size=5,
vad_filter=True,
vad_parameters={"min_silence_duration_ms": 500, "speech_pad_ms": 400},
)

每段输出 [start, end, text],转写过程中每 50 段增量保存,防止中断丢失。51 分钟音频转写耗时约 2 分钟。

情绪分析

  • RMS 能量:音频采样值的均方根,反映音量
  • 语速:字符数/时长,>8 chars/s 标记快速
  • 关键词匹配:激动词(卧槽/离谱/啊?)、笑声词(哈哈/笑死)
  • 音量突增:当前段 RMS > 前 20 段均值 × 2

4.2 画面 OCR 击杀检测

每 1 秒从视频截取一帧,裁剪到击杀信息区域 (665, 596, 1256, 931),送入 PaddleOCR:

1
2
3
4
5
ffmpeg_cmd = [
"ffmpeg", "-hwaccel", "cuda", "-i", video_path,
"-vf", "crop=1256:931:665:596,fps=1",
"-f", "image2pipe", "-vcodec", "png", "-"
]

检测三类事件:

  • PvP 击杀:匹配”淘汰”+ 玩家名模式,玩家名包含 #数字 标识
  • PvE 击杀:匹配 AI 敌人名称(如”护卫兵”),自动忽略,不参与评分
  • 对局结束:匹配”对局时间”或”战绩总结”文字

单帧 OCR 去重:同一击杀事件连续出现时,取置信度最高的帧。
首次运行保存 OCR 缓存(.ocr_cache.json),后续运行直接读取。

4.3 弹幕分析

抖音弹幕 JSON 格式:

1
2
3
4
{
"metadata": {"room_id": "...", "danmaku_count": 938, ...},
"events": [{"type": "chat", "nickname": "...", "content": "...", "timestamp": 1779872583}, ...]
}

时间归一化:rel_time = event.timestamp - first_event.timestamp,所有时间戳转为相对秒数。

密度计算:2 秒分桶,统计每桶弹幕数。基线 = 均值 × 1.5。超级峰值 ≥ 5x 基线,普通峰值 ≥ 3x 基线。

关键词分类

1
2
3
4
POS_DM = ['666', '牛逼', '牛', '强', '厉害', '帅', '猛', '太强', '好强', '无敌', '太猛', '操作']
NEG_DM = ['无聊', '没意思', '困了', '睡了']
LAUGH_DM = ['哈哈', '笑死', '哈哈哈哈', 'hhh', '233', '好笑', '搞笑']
EXCITED_DM = ['卧槽', '我靠', 'wc', '不是吧', '太假了', '离谱', '啊?']

特征计算:dm_pos_ratio = 含好评词的弹幕数 / 总弹幕数,以此类推。

高频词提取

1
2
3
def extract_top_words(texts, top_n=10):
# 先尝试 jieba 分词,不可用则回退字符 2-gram
# 返回: {words: [(词, 频次)], max_freq, hot_word_count, has_hot_topic, top_ratio}

has_hot_topic 当单一关键词占比 > 40% 时触发(权重 +0.39,是弹幕类最有效特征)。

4.4 RL 模型评分

特征提取

从候选片段提取 35 维向量(FEATURE_NAMES 定义在 feature_extractor.py):

类别 特征 维数
弹幕密度 duration, anchor_count, max_dm_ratio, dm_count, has_super_dm, has_dm_peak 6
弹幕内容 dm_pos_ratio, dm_neg_ratio, dm_laugh_ratio, dm_excited_count 4
高频词 top_word_max_freq, top_word_count, top_word_ratio, has_hot_topic 4
语音情绪 voice_excited_count, voice_surge_count, voice_fast_count, has_voice_excited, has_volume_surge 5
游戏事件 kill_count, has_squad_wipe, has_clutch, has_loot_found, has_match_start, has_match_end, has_death, has_revive 8
OCR 事件 ocr_kill_count, has_ocr_kill, has_ocr_match_end, ocr_confidence_max 4
叙事结构 narrative_full, narrative_start_end, has_climax 3
对局位置 round_position 1

Warm Start

用 3000 个采样自经验分布的随机特征向量模拟真实数据,原规则系统为每个样本计算伪评分,Linear Regression 拟合得到初始权重。OCR 特征 ×2.5、高频词特征 ×2.0 显式增强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 伪规则评分(近似原 score_candidate)
s = 3.0
if has_super_dm: s += 5 if max_dm_ratio >= 10 else 3
if dm_pos_ratio > 0.3: s += 2
if dm_laugh_ratio > 0.2: s += 2
if dm_neg_ratio > 0.3: s -= 3
if has_ocr_kill: s += 4 if kill_count >= 1 else 3
# ... 等约 30 条规则
y[i] = np.clip(s, 0, 10)

# 拟合
lr = LinearRegression().fit(X, y)
coef[OCR_INDICES] *= 2.5 # OCR 增强
coef[WORD_INDICES] *= 2.0 # 高频词增强

偏好学习

1
2
3
4
5
6
7
8
9
10
11
class PairwisePreferenceLearner:
def fit(self, X_diff):
# 数据增强:每个正对 (A-B, 1) 生成负对 (B-A, 0)
X_aug = np.vstack([X_diff, -X_diff])
y_aug = np.hstack([np.ones(n), np.zeros(n)])
# StandardScaler 归一化 + LogisticRegression(C=1.0, fit_intercept=False)
self.model.fit(self.scaler.fit_transform(X_aug), y_aug)

def predict_score(self, features):
raw = self.model.decision_function(self.scaler.transform(features.reshape(1, -1)))
return np.clip(raw[0], 0.0, 10.0)

关键设计:fit_intercept=False,确保 score = w · features,权重直接可解释为特征贡献。

特征版本管理

特征列表变更时递增 FEATURE_VERSION,旧模型自动失效,重新 Warm Start:

1
FEATURE_VERSION = 2  # 修改 FEATURE_NAMES 时必须递增

4.5 反馈存储与训练

1
2
3
4
5
6
7
8
9
10
11
12
class FeedbackStore:
def save_feedback(self, run_id, video_path, selected_ids,
candidate_features, candidate_scores, ...):
# 1. 保存特征缓存: features/{run_id}.npz
np.savez_compressed(features_path, **{cid: feats for ...})
# 2. 追加反馈记录: feedback_history.jsonl
entry = {'run_id', 'selected_ids', 'selected_scores', 'all_scores', ...}
jsonl.append(entry)

def build_training_pairs(self, min_score_gap=1.0):
# 选中 vs 未选中 + 分数差 >= min_score_gap → 正对
# 返回: X_diff (n_pairs × n_features), pair_info

训练时机:每次 --feedback 标注后立即增量训练,--retrain 用全部历史反馈批量重训练。

4.6 视频切割

1
2
3
4
ffmpeg -hwaccel cuda -hwaccel_output_format cuda \
-i input.flv -ss {start} -to {end} \
-c:v h264_nvenc -preset p1 -cq 20 \
-c:a aac -b:a 128k output.mp4
  • 使用 NVENC 硬件编码,-preset p1 最快,-cq 20 质量控制
  • 短切片(<30s)额外使用 scale 滤镜裁剪为 1440×1080
  • 实际切割在源素材起止点基础上前后各加 30s padding

4.7 缓存机制

缓存 路径 说明
转录缓存 {video_dir}/transcript_output/transcript.json Whisper 转录结果,幂等跳过
音频缓存 {video_dir}/transcript_output/audio.wav 已提取音频,幂等跳过
OCR 缓存 {video_path}.ocr_cache.json 画面 OCR 事件列表,避免重复 GPU 推理
模型权重 model_data/model_weights.pkl joblib 序列化的 PairwisePreferenceLearner
特征缓存 model_data/features/{run_id}.npz 每次运行的候选特征向量
反馈记录 model_data/feedback_history.jsonl 用户标注历史

五、核心技术难点

5.1 OCR 画面识别的精确性

问题:击杀信息在屏幕上仅持续 2-3 秒,文字半透明叠加在游戏画面上,检测窗口窄且干扰多。

解决方案

  • 裁剪到固定信息区域 (665, 596, 1256, 931),排除画面主体干扰
  • 每 1 秒 1 帧高密度采样(51 分钟 = 3045 帧),不漏检
  • 连续帧去重 + 置信度取最大值,避免同一击杀重复计数
  • PvP/PvE 区分:通过玩家名格式(#数字 vs 固定名)过滤人机
  • OCR 结果缓存到 JSON 文件,后续运行直接读取

5.2 偏好学习的冷启动

问题:RL 模型从零开始(随机权重)时,前几次分析评分毫无意义,用户体验极差。

解决方案

  • Warm Start:用 3000 个采样的随机特征向量 + 原规则系统伪评分 → Linear Regression 拟合得到初始权重
  • OCR 和高频词特征显式增强(×2.5 / ×2.0),反映这些信号的客观性
  • 初始模型行为 ≈ 规则系统,但权重是连续可微调的值
  • Warm Start 后的模型已有合理的排序能力,用户第一次分析就能看到有意义的评分

5.3 特征工程与词典覆盖

问题:关键词词典(好评/笑声/惊叹)比较通用,覆盖不全。例如”拉闸””天才”在实战弹幕中出现 16/15 次,但未被任何词典收录,无法参与特征计算。

解决方案

  • 当前:高频词特征(has_hot_topictop_word_ratio)部分弥补,不依赖具体词典的通用高频信号
  • 未来:自动发现高频新词 → 提示用户加入词典;从特征级权重下沉到词级权重

5.4 短切片与长切片的协调

问题:长切片提供上下文(15-150s),短切片提供精剪(<30s 仅 PvP 击杀),两者评分标准不同但共享同一个 RL 模型。

解决方案

  • 短切片使用”OCR 必录”机制:PvP 击杀检测到即强制收录,不依赖评分
  • 短切片评分只影响优先级排序,不影响是否收录
  • 长切片仍由 RL 模型全权评分
  • 两者在 analysis_result.json 中通过 clip_type 字段区分

5.5 用户偏好的快速适应

问题:单次标注样本量小(10-25 个候选),传统批量训练容易过拟合。

解决方案

  • 成对比较天然做数据增强:11 个选中 × 14 个未选中 = 154 个理论对
  • 加入 min_score_gap=1.0 过滤噪音对(分数接近的跳过)
  • partial_fit + 高样本权重(learning_rate × 10)加速适应新反馈
  • 每次训练后输出 Top-N 权重变化,用户可直观理解模型学到了什么

六、项目使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 标准用法:分析 + 切割
/delta-force-clip-rl /path/to/recording_dir

# 带反馈训练(标注最佳切片 → 自动训练)
/delta-force-clip-rl /path/to/recording_dir --feedback

# 批量重训练(积累足够反馈后用全部历史数据)
/delta-force-clip-rl /path/to/recording_dir --retrain

# 重置模型(如果学歪了,回到 Warm Start)
/delta-force-clip-rl /path/to/recording_dir --reset-model

# 调整最大切片数
/delta-force-clip-rl /path/to/recording_dir --max-clips 30

底层命令

1
2
3
4
5
6
7
8
9
# 步骤1: 语音转写
python transcribe_pipeline.py <video.flv> --output-dir transcript_output/

# 步骤2: 联合分析(RL 评分)
python analyze_clips_rl.py <danmaku.json> <transcript.json> <duration> \
--vision <video.flv> --output analysis_result.json

# 步骤3: 确认后切割
python cut_clips.py <video.flv> analysis_result.json --output-dir clips/

输出结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
recordings/{主播名}/{日期}/
├── {时间}.flv # 源视频
├── {时间}.danmaku.json # 弹幕数据
├── transcript_output/
│ ├── audio.wav # 已提取音频
│ └── transcript.json # 转录 + 情绪标注
├── analysis_result.json # 切片方案
└── clips/
├── long/ # 长切片
│ ├── 第1局_000056_000221.mp4
│ └── ...
├── short/ # 短切片(PvP击杀)
└── clip_info.json # 切片元信息

七、项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
delta-force-clip-rl/
├── skill.md # Skill 定义与工作流程文档
├── transcribe_pipeline.py # 语音转写管线(ffmpeg + faster-whisper + 情绪分析)
├── analyze_clips_rl.py # 多模态联合分析 + RL 评分 + 反馈收集(主入口)
├── cut_clips.py # GPU 视频切割(ffmpeg NVENC)
├── vision_ocr.py # 画面 OCR 击杀检测(PaddleOCR + ffmpeg NVDEC)
├── rl_model/ # RL 模型模块
│ ├── feature_extractor.py # 35 维特征提取 + 关键词词典 + 高频词统计
│ ├── scorer.py # RL 评分器(Warm Start / predict / 训练 / 持久化)
│ ├── train.py # 成对偏好学习器(LogisticRegression + 数据增强)
│ └── feedback_store.py # 反馈存储(JSONL + .npz 特征缓存 + 训练对构建)
└── model_data/ # 模型持久化数据
├── model_weights.pkl # 序列化模型(joblib)
├── feature_names.json # 特征名列表 + 版本号
├── feedback_history.jsonl # 用户标注历史
└── features/ # 特征向量缓存
├── run_xxxxxxxxxx.npz
└── ...