18 KiB
Phase 3: Processing Skills - Research
Researched: 2026-03-13 Domain: ASR (Automatic Speech Recognition) 和字幕处理集成 Confidence: HIGH
Summary
Phase 3 的核心目标是将现有的分散 ASR 工具整合为统一的管道步骤。关键发现:
-
现有资产高度可复用:
kb-whisper/whisper.py和kb-whisper-diarization/whisper_diarization.py已经实现了完整的功能(音频转换、ASR 识别、多格式输出),只需包装为管道步骤模式。 -
决策已锁定:OCR 明确排除,只关注 ASR/字幕处理。官方字幕优先,无字幕时使用 Whisper.cpp。HF_TOKEN 配置时自动启用说话者分离。
-
架构模式清晰:Phase 1/2 建立的
StepInput/StepOutputSchema、WorkflowState状态管理、retry_on_network_error重试机制可直接复用。
Primary recommendation: 创建 ASRProcessor 类,统一封装官方字幕下载和 Whisper.cpp 调用,复用现有代码逻辑,遵循 Phase 1/2 的管道模式。
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
ASR 引擎选择策略
- 优先级顺序:B站官方字幕 → Whisper.cpp 本地识别
- 官方字幕质量最高且无计算成本,优先使用
- 无官方字幕时,默认使用 Whisper.cpp 进行语音识别
- FunASR 保留作为备选,不作为默认选项
说话者分离
- HF_TOKEN 配置时自动启用带说话者分离的 Whisper(pyannote.audio)
- 无需显式参数控制,根据环境变量自动判断
- 说话者分离结果合并到标准输出格式中
输出格式
- 保留全部格式:txt(纯文本)+ srt(字幕)+ json(结构化数据)
- 与现有 ASR 工具输出保持一致
- JSON 包含 segments(带时间戳的分段)和完整文本
OCR 处理
- 不集成 OCR 功能
- Phase 3 专注于 ASR/字幕处理
- OCR 相关计划(03-02)移除
工作流集成
- ASR 作为管道步骤执行,完成后记录到 workflow_state.json
- B站字幕下载集成到 ASR 步骤(统一入口:先尝试官方字幕,无则 ASR)
- 输出文件与内容放在同一目录,不创建子目录
Schema 复用
- 复用 Phase 1 定义的 StepInput/StepOutput 格式
- ASR 输出符合统一的 JSON Schema 规范
- 包含 success、tool、segments、text 等标准字段
Claude's Discretion
- Whisper 模型默认选择(base/small/medium)
- 重试机制的具体参数(继承 Phase 1 的 tenacity 配置)
- 音频转换的临时文件处理策略
Deferred Ideas (OUT OF SCOPE)
- OCR 视频帧文字识别 — 不需要,明确排除
- 进度可见性(实时显示 ASR 进度)— v2 需求
- 并行 ASR 处理 — v2 需求
</user_constraints>
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| whisper.cpp | v1.7.1+ | 本地语音识别引擎 | 零依赖、高性能、CPU 友好 |
| ffmpeg | 4.x+ | 音频格式转换 | 行业标准,支持所有格式 |
| pydantic | v2 | Schema 验证 | Phase 1 已采用,类型安全 |
| pydantic-settings | v2 | 配置管理 | Phase 1 已采用,环境变量加载 |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| pyannote.audio | 3.x | 说话者分离 | HF_TOKEN 配置时自动启用 |
| torch | 2.x | pyannote 依赖 | 说话者分离需要 |
| tenacity | 8.x | 重试机制 | 继承 Phase 1 配置 |
| aiohttp | 3.x | 异步 HTTP | B站字幕 API 调用 |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
| Whisper.cpp | FunASR | FunASR 中文效果好,但需要额外依赖(modelscope),作为备选方案 |
| Whisper.cpp | OpenAI Whisper API | API 需要付费且依赖网络,本地处理更可靠 |
Installation:
# Whisper.cpp(如果未安装)
git clone https://github.com/ggml-org/whisper.cpp.git ~/whisper.cpp
cd ~/whisper.cpp && make
./models/download-ggml-model.sh base
# Python 依赖
pip install pydantic pydantic-settings tenacity aiohttp
# 说话者分离(可选,需要 HF_TOKEN)
pip install pyannote.audio torch
Architecture Patterns
Recommended Project Structure
kb_pipeline/
├── processors/ # 处理器模块(Phase 3 新增)
│ ├── __init__.py
│ ├── base.py # BaseProcessor 基类
│ ├── asr.py # ASRProcessor 实现
│ └── subtitle.py # 字幕下载逻辑
├── schemas.py # 已有 Schema 定义
├── state.py # 已有状态管理
├── config.py # 已有配置管理
└── retry.py # 已有重试机制
Pattern 1: ASRProcessor 统一入口
What: 将官方字幕下载和 Whisper.cpp 识别统一为单一处理器 When to use: 所有需要视频内容转文字的场景
Example:
# Source: 基于 kb-whisper/whisper.py 和 kb-bilibili-content/bilibili_content.py
from kb_pipeline.schemas import StepInput, StepOutput
from kb_pipeline.state import WorkflowStateManager
from kb_pipeline.config import get_settings
class ASRInput(StepInput):
"""ASR 步骤输入"""
video_path: str # 视频文件路径
content_type: str = "video" # video/zvideo
class ASROutput(StepOutput):
"""ASR 步骤输出"""
tool: str # "subtitle" | "whisper" | "whisper-diarization"
has_subtitle: bool
output_files: Dict[str, str] # {txt, srt, json}
segments: List[Dict]
text: str
duration_seconds: float
class ASRProcessor:
"""ASR 处理器 - 统一字幕下载和语音识别"""
def __init__(self, state_manager: WorkflowStateManager = None):
self.settings = get_settings()
self.state_manager = state_manager
async def process(self, input_data: ASRInput) -> ASROutput:
"""执行 ASR 处理
策略:
1. 先尝试下载官方字幕
2. 无官方字幕则调用 Whisper.cpp
3. HF_TOKEN 配置时自动启用说话者分离
"""
# Step 1: 尝试官方字幕
if input_data.platform == "bilibili":
subtitle_result = await self._try_official_subtitle(input_data)
if subtitle_result:
return subtitle_result
# Step 2: Whisper.cpp 识别
return await self._run_whisper(input_data)
async def _try_official_subtitle(self, input_data: ASRInput) -> Optional[ASROutput]:
"""尝试下载 B站官方字幕"""
# 复用 kb-bilibili-content/bilibili_content.py 的 download_subtitle 逻辑
...
async def _run_whisper(self, input_data: ASRInput) -> ASROutput:
"""执行 Whisper.cpp 识别"""
# 检查 HF_TOKEN 决定是否启用说话者分离
hf_token = self.settings.hf_token.get_secret_value()
if hf_token:
return await self._run_whisper_with_diarization(input_data)
else:
return await self._run_whisper_basic(input_data)
Pattern 2: 状态记录
What: ASR 完成后记录到 workflow_state.json When to use: 每次 ASR 处理完成后
Example:
# Source: Phase 1 状态管理模式
def record_asr_result(state_manager: WorkflowStateManager, result: ASROutput):
"""记录 ASR 结果到状态文件"""
if result.success:
state_manager.mark_step_completed(
"asr",
output_summary=f"tool={result.tool}, segments={len(result.segments)}"
)
else:
state_manager.mark_step_failed("asr", result.error_message)
Anti-Patterns to Avoid
- 不要重新实现音频转换逻辑:
kb-whisper/whisper.py的convert_to_wav()已经验证可用,直接复用 - 不要硬编码 Whisper 模型路径:使用
WHISPER_CPP_PATH环境变量 - 不要忽略临时文件清理:
_audio.wav临时文件处理后必须删除 - 不要在没有官方字幕时报告失败:返回
has_subtitle=False并继续 Whisper 流程
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| 音频格式转换 | 自己写 ffmpeg 调用 | kb-whisper/whisper.py:convert_to_wav() |
已处理采样率检测、格式验证 |
| SRT 时间解析 | 正则表达式解析 | kb-whisper/whisper.py:parse_srt_for_duration() |
已验证的时间解析逻辑 |
| Whisper 模型检查 | 自己检查文件存在 | kb-whisper/whisper.py:check_model() |
已处理模型下载提示 |
| 说话者分离 | 自己实现分离逻辑 | kb-whisper-diarization/whisper_diarization.py |
已集成 pyannote.audio |
| B站字幕下载 | 自己调用 API | kb-bilibili-content/bilibili_content.py:download_subtitle() |
已处理 API 调用和 SRT 转换 |
Key insight: 现有代码已经过验证,核心逻辑直接复用,只需包装为管道模式。
Common Pitfalls
Pitfall 1: Whisper.cpp 路径未配置
What goes wrong: 运行时报错 "未找到 whisper.cpp"
Why it happens: WHISPER_CPP_PATH 环境变量未设置,默认路径也不存在
How to avoid:
def get_whisper_cpp_path():
path = os.environ.get('WHISPER_CPP_PATH')
if path and os.path.exists(path):
return os.path.expanduser(path)
default = os.path.expanduser('~/whisper.cpp')
if os.path.exists(default):
return default
raise RuntimeError(
"未找到 whisper.cpp。请设置 WHISPER_CPP_PATH 或安装到 ~/whisper.cpp"
)
Warning signs: FileNotFoundError 在启动时
Pitfall 2: HF_TOKEN 未配置时尝试说话者分离
What goes wrong: pyannote.audio 报错 "需要设置 HF_TOKEN 环境变量" Why it happens: 说话者分离需要 HuggingFace 认证 How to avoid: 检查 HF_TOKEN 后再决定是否启用说话者分离
hf_token = settings.hf_token.get_secret_value()
if not hf_token:
# 回退到普通 Whisper
return await self._run_whisper_basic(input_data)
Warning signs: RuntimeError 包含 "HF_TOKEN"
Pitfall 3: 临时音频文件未清理
What goes wrong: 磁盘空间逐渐减少,*_audio.wav 文件堆积
Why it happens: ffmpeg 转换后的临时文件没有删除
How to avoid:
# 处理完成后清理
if audio_path != input_file and audio_path.endswith('_audio.wav'):
try:
os.remove(audio_path)
except Exception:
pass # 静默失败,不阻塞主流程
Warning signs: 目录中存在多个 *_audio.wav 文件
Pitfall 4: 超时设置不合理
What goes wrong: 长视频处理超时中断 Why it happens: Whisper 处理 1 小时视频可能需要 10+ 分钟 How to avoid: 设置合理超时(现有代码使用 3600 秒)
result = subprocess.run(cmd, timeout=3600) # 1 小时超时
Warning signs: TimeoutExpired 异常
Code Examples
官方字幕下载(复用现有代码)
# Source: kb-bilibili-content/bilibili_content.py
async def download_subtitle(self, bvid: str) -> Dict[str, Any]:
"""下载字幕并转换为 SRT 格式"""
result = {"success": False, "has_subtitle": False}
# 获取 CID
api_url = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}"
async with aiohttp.ClientSession() as session:
async with session.get(api_url) as resp:
data = await resp.json()
pages = data.get('data', [])
if not pages:
result["success"] = True # 没有分P是正常的
return result
cid = pages[0].get('cid', '')
# 获取字幕列表
async with session.get(f"https://api.bilibili.com/x/player/v2?bvid={bvid}&cid={cid}") as resp:
sub_data = await resp.json()
subtitles = sub_data.get('data', {}).get('subtitle', {}).get('list', [])
if not subtitles:
result["success"] = True # 没有字幕是正常的
return result
# 下载并转换为 SRT
# ... (转换逻辑)
result["has_subtitle"] = True
result["success"] = True
return result
Whisper.cpp 调用(复用现有代码)
# Source: kb-whisper/whisper.py
def run_whisper(whisper_path, audio_path, model, output_dir, language='zh'):
"""运行 Whisper 语音识别"""
cmd = [
os.path.join(whisper_path, 'whisper-cli'),
'-m', os.path.join(whisper_path, 'models', f'ggml-{model}.bin'),
'-f', audio_path,
'-of', os.path.join(output_dir, output_basename),
'-l', language,
'--output-txt', '--output-srt', '--output-json',
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
return result
说话者分离(复用现有代码)
# Source: kb-whisper-diarization/whisper_diarization.py
def run_diarization(audio_path, num_speakers=None):
"""运行 pyannote.audio 说话者分离"""
hf_token = os.environ.get('HF_TOKEN')
if not hf_token:
raise RuntimeError("需要设置 HF_TOKEN 环境变量")
from huggingface_hub import login
login(token=hf_token)
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1")
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
pipeline.to(device)
diarization = pipeline(audio_path, num_speakers=num_speakers)
# 解析结果
speakers = {}
for turn, _, speaker in diarization.itertracks(yield_label=True):
if speaker not in speakers:
speakers[speaker] = []
speakers[speaker].append({
"start": round(turn.start, 2),
"end": round(turn.end, 2)
})
return speakers
输出格式(标准 JSON)
{
"success": true,
"tool": "whisper",
"model": "base",
"language": "zh",
"input_file": "video.mp4",
"output_files": {
"txt": "video.txt",
"srt": "video.srt",
"json": "video.json"
},
"duration_seconds": 120.5,
"segments": [
{
"id": 0,
"start": 0.0,
"end": 3.2,
"text": "这是第一段字幕",
"speaker": "SPEAKER_00"
}
],
"text": "完整文本内容..."
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| 手动调用 whisper.py | 管道步骤封装 | Phase 3 | 统一入口,状态可追踪 |
| 硬编码模型选择 | 配置化模型选择 | Phase 3 | 灵活调整精度/速度 |
| 无说话者分离 | HF_TOKEN 自动启用 | Phase 3 | 访谈/对话视频支持 |
Deprecated/outdated:
kb-workflow中的手动 Whisper 调用步骤:将被ASRProcessor统一封装
Open Questions
-
Whisper 默认模型选择
- What we know: 现有代码默认使用
base模型 - What's unclear: 是否需要根据视频时长自动选择模型(短视频用 tiny,长视频用 base)
- Recommendation: 保持
base默认,Claude's Discretion 范围内可后续优化
- What we know: 现有代码默认使用
-
临时文件目录
- What we know: 当前在视频同目录创建
_audio.wav - What's unclear: 是否应该使用系统临时目录
- Recommendation: 保持现有行为(同目录),便于调试和断点续传
- What we know: 当前在视频同目录创建
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | pytest 8.x |
| Config file | pytest.ini |
| Quick run command | pytest tests/test_processors/ -x -v |
| Full suite command | pytest tests/ -v |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| ASR-01 | 官方字幕下载成功 | unit | pytest tests/test_processors/test_asr.py::test_official_subtitle -x |
❌ Wave 0 |
| ASR-02 | 无字幕时调用 Whisper | unit | pytest tests/test_processors/test_asr.py::test_whisper_fallback -x |
❌ Wave 0 |
| ASR-03 | HF_TOKEN 启用说话者分离 | unit | pytest tests/test_processors/test_asr.py::test_diarization_enabled -x |
❌ Wave 0 |
| ASR-04 | 输出 txt/srt/json 三格式 | unit | pytest tests/test_processors/test_asr.py::test_output_formats -x |
❌ Wave 0 |
| ASR-05 | 状态记录到 workflow_state | unit | pytest tests/test_processors/test_asr.py::test_state_recording -x |
❌ Wave 0 |
| ASR-06 | 临时文件清理 | unit | pytest tests/test_processors/test_asr.py::test_temp_file_cleanup -x |
❌ Wave 0 |
Sampling Rate
- Per task commit:
pytest tests/test_processors/test_asr.py -x -v - Per wave merge:
pytest tests/ -v - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
tests/test_processors/__init__.py— 测试模块初始化tests/test_processors/conftest.py— 共享 fixtures(mock Whisper、mock B站 API)tests/test_processors/test_asr.py— ASR 处理器测试tests/test_processors/test_subtitle.py— 字幕下载测试
Sources
Primary (HIGH confidence)
- kb-whisper/whisper.py - 已验证的 Whisper.cpp 封装实现
- kb-whisper-diarization/whisper_diarization.py - 已验证的说话者分离实现
- kb-bilibili-content/bilibili_content.py - 已验证的 B站字幕下载实现
- kb_pipeline/schemas.py - Phase 1 定义的 Schema 体系
- kb_pipeline/state.py - Phase 1 定义的状态管理
- kb_pipeline/config.py - Phase 1 定义的配置管理
Secondary (MEDIUM confidence)
- whisper.cpp GitHub - 官方仓库,确认安装方式
- pyannote.audio GitHub - 官方仓库,确认 HF_TOKEN 配置
Tertiary (LOW confidence)
- 无
Metadata
Confidence breakdown:
- Standard stack: HIGH - 基于现有代码和已验证的依赖
- Architecture: HIGH - 遵循 Phase 1/2 建立的模式
- Pitfalls: HIGH - 基于现有代码中的错误处理逻辑
Research date: 2026-03-13 Valid until: 30 days(稳定技术栈)