From cd80837c863a81c5ed80f291e0d86b2a97e95a03 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 9 Feb 2026 00:24:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(bilibili):=20=E6=94=B9=E8=BF=9B=E5=AD=97?= =?UTF-8?q?=E5=B9=95=E6=8F=90=E5=8F=96=EF=BC=8C=E6=94=AF=E6=8C=81=20WBI=20?= =?UTF-8?q?API=20=E5=92=8C=E5=A4=9A=E8=AF=AD=E8=A8=80=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 WBI API 获取字幕信息(支持 AI 字幕) - 优先选择中文字幕(ai-zh > zh-CN > zh-Hans) - 添加硬字幕提取实施计划 - 改进错误处理,静默失败不影响主流程 --- .../bilibili-hard-subtitle-extraction.md | 138 ++++++++++++++++++ .../src/extractors/bilibili_extractor.py | 92 ++++++++++-- 2 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 memory/plans/bilibili-hard-subtitle-extraction.md diff --git a/memory/plans/bilibili-hard-subtitle-extraction.md b/memory/plans/bilibili-hard-subtitle-extraction.md new file mode 100644 index 0000000..3c9fe5f --- /dev/null +++ b/memory/plans/bilibili-hard-subtitle-extraction.md @@ -0,0 +1,138 @@ +# B站硬字幕提取系统 - 实施计划 + +**Goal:** 集成硬字幕提取功能到 obsidian-kb,在官方字幕不可用时,使用 PaddleOCR 从视频画面提取硬字幕 + +**Approach:** 模块化设计,使用 B站 API 获取视频流,OpenCV 流式读取,PaddleOCR 识别,优先官方字幕,降级硬字幕 + +**Estimated Total Time:** 180-240 分钟 + +--- + +## Checkpoint 1: 环境准备和依赖安装 (20 min) + +- [ ] Task 1.1: 安装 PaddleOCR 依赖 (~10 min) + - **Action:** 执行 `pip3 install paddlepaddle paddleocr --break-system-packages` + - **Verify:** 运行 `python3 -c "from paddleocr import PaddleOCR; print('✅ PaddleOCR 安装成功')"` + +- [ ] Task 1.2: 验证 PaddleOCR 可用性 (~5 min) + - **Action:** 创建测试脚本,测试简单的文本识别 + - **Verify:** 成功识别测试图像中的文字 + +- [ ] Task 1.3: 更新 obsidian-kb requirements.txt (~5 min) + - **Action:** 添加 `paddlepaddle>=2.5.0` 和 `paddleocr>=2.7.0` 到依赖列表 + - **Verify:** 文件更新正确,版本号有效 + +--- + +## Checkpoint 2: 创建 VideoStreamExtractor 模块 (30 min) + +- [ ] Task 2.1: 创建基础模块结构 (~5 min) + - **Action:** 创建 `src/extractors/hard_subtitle_extractor.py`,定义类和基本方法 + - **Verify:** 文件创建成功,类结构合理 + +- [ ] Task 2.2: 实现 B站 API 获取视频流 URL (~10 min) + - **Action:** 使用 B站 playurl API 获取视频流,参考 bilibili.py 的实现 + - **Verify:** 能成功获取测试视频的流 URL + +- [ ] Task 2.3: 实现 OpenCV 流式读取视频帧 (~15 min) + - **Action:** 使用 `cv2.VideoCapture` 从 URL 直接读取,按间隔提取帧 + - **Verify:** 能成功提取 10 帧测试图像并保存 + +--- + +## Checkpoint 3: 创建 SubtitleDetector 模块 (25 min) + +- [ ] Task 3.1: 实现默认字幕区域定义 (~5 min) + - **Action:** 定义底部、居中、顶部等预设区域 + - **Verify:** 配置正确,区域坐标合理 + +- [ ] Task 3.2: 实现自动字幕区域检测 (~15 min) + - **Action:** 使用 OpenCV 检测文字密集区域(通过边缘检测或 MSER) + - **Verify:** 对测试帧能自动识别字幕位置 + +- [ ] Task 3.3: 实现区域缓存机制 (~5 min) + - **Action:** 缓存检测结果,避免重复计算 + - **Verify:** 同一视频只检测一次,后续使用缓存 + +--- + +## Checkpoint 4: 创建 OCRProcessor 模块 (30 min) + +- [ ] Task 4.1: 集成 PaddleOCR (~10 min) + - **Action:** 初始化 PaddleOCR 实例,配置中英文识别 + - **Verify:** 能成功识别测试图像中的文字 + +- [ ] Task 4.2: 实现批量帧识别 (~10 min) + - **Action:** 批量处理视频帧,返回识别结果和置信度 + - **Verify:** 能处理 10 帧图像并返回结果 + +- [ ] Task 4.3: 实现结果过滤和清洗 (~10 min) + - **Action:** 根据置信度阈值过滤,去除噪声和重复 + - **Verify:** 低质量结果被过滤,保留高质量识别 + +--- + +## Checkpoint 5: 集成主流程 (35 min) + +- [ ] Task 5.1: 创建 HardSubtitleExtractor 主类 (~10 min) + - **Action:** 协调 VideoStreamExtractor、SubtitleDetector、OCRProcessor + - **Verify:** 类结构清晰,职责分离合理 + +- [ ] Task 5.2: 实现降级逻辑 (~10 min) + - **Action:** 官方字幕为空时,调用硬字幕提取 + - **Verify:** 测试无官方字幕的视频能正确降级 + +- [ ] Task 5.3: 更新 bilibili_extractor.py 集成 (~15 min) + - **Action:** 在 BilibiliExtractor 中集成硬字幕提取 + - **Verify:** 现有功能不受影响,新增功能可用 + +--- + +## Checkpoint 6: 配置和文档 (20 min) + +- [ ] Task 6.1: 添加配置项 (~10 min) + - **Action:** 在 config.yaml 添加 hard_subtitle 配置节 + - **Verify:** 配置项完整,注释清晰 + +- [ ] Task 6.2: 更新文档 (~10 min) + - **Action:** 更新 SKILL.md,添加硬字幕提取说明 + - **Verify:** 文档准确,包含使用示例 + +--- + +## Checkpoint 7: 测试和验证 (40 min) + +- [ ] Task 7.1: 单元测试各模块 (~15 min) + - **Action:** 测试视频流提取、区域检测、OCR 识别 + - **Verify:** 每个模块独立测试通过 + +- [ ] Task 7.2: 集成测试完整流程 (~15 min) + - **Action:** 测试有官方字幕、无官方字幕、有硬字幕三种情况 + - **Verify:** 各种场景都能正确处理 + +- [ ] Task 7.3: 性能优化和调整 (~10 min) + - **Action:** 调整截帧间隔、批量大小等参数 + - **Verify:** 提取速度和准确度达到预期 + +--- + +## Verification Criteria + +- [ ] 所有 7 个检查点完成 +- [ ] PaddleOCR 能正确识别硬字幕 +- [ ] 优先使用官方字幕,降级到硬字幕 +- [ ] 配置项灵活可调 +- [ ] 文档完整清晰 +- [ ] 不影响现有 obsidian-kb 功能 + +--- + +## Execution Options + +**Plan complete and saved to `memory/plans/bilibili-hard-subtitle-extraction.md`. Two execution options:** + +**1. Single-Agent (this session)** - 我按顺序执行任务,每个检查点汇报进度 + +**2. Dispatch Multiple Agents (parallel)** - 派生子代理并行处理独立任务,然后整合结果 + +**Which approach?** diff --git a/skills/obsidian-kb/src/extractors/bilibili_extractor.py b/skills/obsidian-kb/src/extractors/bilibili_extractor.py index 0472250..792bfdd 100644 --- a/skills/obsidian-kb/src/extractors/bilibili_extractor.py +++ b/skills/obsidian-kb/src/extractors/bilibili_extractor.py @@ -447,6 +447,13 @@ class BilibiliExtractor(Extractor): """ 提取视频字幕 + 支持的字幕类型: + 1. 官方 CC 字幕(需要 UP 主手动上传) + 2. AI 自动生成字幕(标记为 ai-zh) + 3. WebVTT 格式字幕 + + 注意:硬字幕(嵌入视频画面内)无法提取 + Args: page: Playwright page对象 bvid: BV号 @@ -457,30 +464,87 @@ class BilibiliExtractor(Extractor): subtitles = [] try: - # 获取 aid - aid = await page.evaluate('() => window.__INITIAL_STATE__?.videoData?.aid') - if not aid: + # 首先通过 API 获取视频信息(包含 cid 和 aid) + api_url = "https://api.bilibili.com/x/web-interface/view" + response = await page.request.get(api_url, params={'bvid': bvid}) + + if not response.ok: return '' - # API获取字幕信息 - api_url = "https://api.bilibili.com/x/player/v2" - response = await page.request.get(api_url, params={'aid': aid, 'bvid': bvid}) + data = await response.json() + if data.get('code') != 0: + return '' - if response.ok: - data = await response.json() - if data.get('code') == 0: - subtitle_info = data.get('data', {}).get('subtitle', {}) + video_data = data.get('data', {}) + cid = video_data.get('cid') + aid = video_data.get('aid') + + if not cid: + return '' + + # 方法1: 使用 WBI API(推荐,支持 AI 字幕) + player_api_url = "https://api.bilibili.com/x/player/wbi/v2" + player_response = await page.request.get(player_api_url, params={'aid': aid, 'cid': cid}) + + if player_response.ok: + player_data = await player_response.json() + if player_data.get('code') == 0: + subtitle_info = player_data.get('data', {}).get('subtitle', {}) subtitles_list = subtitle_info.get('subtitles', []) if subtitles_list: - subtitle_url = subtitles_list[0].get('subtitle_url', '') + # 优先选择中文字幕(包括 AI 字幕) + subtitle_item = None + for sub in subtitles_list: + # 优先级: ai-zh > zh-CN > zh-Hans > 其他 + lan = sub.get('lan', '') + if lan == 'ai-zh': + subtitle_item = sub + break + elif lan in ['zh-CN', 'zh-Hans', 'zh']: + if not subtitle_item: + subtitle_item = sub + + # 如果没有找到中文,使用第一个 + if not subtitle_item: + subtitle_item = subtitles_list[0] + + if subtitle_item: + subtitle_url = subtitle_item.get('subtitle_url', '') + if subtitle_url: + full_url = f"https:{subtitle_url}" + sub_response = await page.request.get(full_url) + if sub_response.ok: + sub_data = await sub_response.json() + subtitles = self._parse_subtitle_json(sub_data) + # 如果找到字幕,直接返回 + if subtitles: + lines = [] + for sub in subtitles: + lines.append(f"[{sub['from']} - {sub['to']}] {sub['content']}") + return '\n'.join(lines) + + # 方法2: 如果 WBI 失败,使用旧版 API + player_api_url = "https://api.bilibili.com/x/player/v2" + player_response = await page.request.get(player_api_url, params={'bvid': bvid, 'cid': cid}) + + if player_response.ok: + player_data = await player_response.json() + if player_data.get('code') == 0: + subtitle_info = player_data.get('data', {}).get('subtitle', {}) + subtitles_list = subtitle_info.get('subtitles', []) + + if subtitles_list: + subtitle_item = subtitles_list[0] + subtitle_url = subtitle_item.get('subtitle_url', '') if subtitle_url: full_url = f"https:{subtitle_url}" - response = await page.request.get(full_url) - if response.ok: - sub_data = await response.json() + sub_response = await page.request.get(full_url) + if sub_response.ok: + sub_data = await sub_response.json() subtitles = self._parse_subtitle_json(sub_data) except Exception as e: + # 静默失败,不影响视频信息提取 pass # 转换为多行字符串