# Phase 3: Processing Skills - Research **Researched:** 2026-03-13 **Domain:** ASR (Automatic Speech Recognition) 和字幕处理集成 **Confidence:** HIGH ## Summary Phase 3 的核心目标是将现有的分散 ASR 工具整合为统一的管道步骤。关键发现: 1. **现有资产高度可复用**:`kb-whisper/whisper.py` 和 `kb-whisper-diarization/whisper_diarization.py` 已经实现了完整的功能(音频转换、ASR 识别、多格式输出),只需包装为管道步骤模式。 2. **决策已锁定**:OCR 明确排除,只关注 ASR/字幕处理。官方字幕优先,无字幕时使用 Whisper.cpp。HF_TOKEN 配置时自动启用说话者分离。 3. **架构模式清晰**:Phase 1/2 建立的 `StepInput`/`StepOutput` Schema、`WorkflowState` 状态管理、`retry_on_network_error` 重试机制可直接复用。 **Primary recommendation:** 创建 `ASRProcessor` 类,统一封装官方字幕下载和 Whisper.cpp 调用,复用现有代码逻辑,遵循 Phase 1/2 的管道模式。 ## 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 需求 ## 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:** ```bash # 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:** ```python # 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:** ```python # 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:** ```python 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 后再决定是否启用说话者分离 ```python 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:** ```python # 处理完成后清理 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 秒) ```python result = subprocess.run(cmd, timeout=3600) # 1 小时超时 ``` **Warning signs:** TimeoutExpired 异常 ## Code Examples ### 官方字幕下载(复用现有代码) ```python # 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 调用(复用现有代码) ```python # 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 ``` ### 说话者分离(复用现有代码) ```python # 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) ```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 1. **Whisper 默认模型选择** - What we know: 现有代码默认使用 `base` 模型 - What's unclear: 是否需要根据视频时长自动选择模型(短视频用 tiny,长视频用 base) - Recommendation: 保持 `base` 默认,Claude's Discretion 范围内可后续优化 2. **临时文件目录** - What we know: 当前在视频同目录创建 `_audio.wav` - What's unclear: 是否应该使用系统临时目录 - Recommendation: 保持现有行为(同目录),便于调试和断点续传 ## 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](https://github.com/ggml-org/whisper.cpp) - 官方仓库,确认安装方式 - [pyannote.audio GitHub](https://github.com/pyannote/pyannote-audio) - 官方仓库,确认 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(稳定技术栈)