Skip to content
Merged
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

## 核心特性

- **多平台支持**:YouTube、Bilibili、抖音、小红书、小宇宙播客、Apple Podcast,工厂模式自动匹配下载器
- **多平台支持**:YouTube、Bilibili、抖音、小红书、微信视频号(经 MediaResolverAPI)、小宇宙播客、Apple Podcast,工厂模式自动匹配下载器
- **双引擎转录**:CapsWriter-Offline(通用转录)+ FunASR(说话人识别)
- **智能文本处理**:LLM 自动校对 ASR 错误、专有名词纠错、按说话人采样+置信度降级的说话人推断、内容总结
- **处理深度可控**:`processing_options` 开关按任务控制是否校对/总结,分层缓存产物只增不减,重复请求自动复用已有层
Expand All @@ -28,7 +28,7 @@
- [funasr_spk_server:funasr server 对应暴露 api,支持音视频转写,分角色,自动合并相同人物的话。](https://github.com/zj1123581321/funasr_spk_server)
- [CapsWriter-Offline:CapsWriter 的离线版,一个好用的 PC 端的语音输入工具,支持热词、LLM处理。](https://github.com/HaujetZhao/CapsWriter-Offline)
- [youtube_download_api:YouTube 视频下载服务,作为 yt-dlp 的可选替代后端。](https://github.com/zj1123581321/youtube_download_api)(可选)
- MediaResolverAPI:短视频 URL → 无水印直链 + 元数据的集中解析服务,可选地接管抖音/小红书解析(可选,见[使用指南](docs/guides/media_resolver.md))。
- MediaResolverAPI:短视频/视频号 URL → 无水印直链 + 元数据的集中解析服务,可选地接管抖音/小红书/视频号解析(可选,见[使用指南](docs/guides/media_resolver.md))。
- OpenAI 兼容的 API,比如 Deepseek,量大管饱。

---
Expand All @@ -55,7 +55,7 @@ uv sync
# 配置服务
cp config/config.example.jsonc config/config.jsonc
# 编辑 config.jsonc,填写 api.auth_token、tikhub.api_key 等
# 可选:抖音/小红书改走 MediaResolverAPI 集中解析,设
# 可选:抖音/小红书/视频号改走 MediaResolverAPI 集中解析,设
# downloaders.use_media_resolver=true 并配置 media_resolver 段
# (使用指南:docs/guides/media_resolver.md)

Expand Down
33 changes: 22 additions & 11 deletions docs/guides/media_resolver.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# MediaResolverAPI 集成指南(抖音 / 小红书解析
# MediaResolverAPI 集成指南(抖音 / 小红书 / 微信视频号解析

> 适用版本:v1(接管 **抖音 + 小红书**)。其它平台(B 站 / YouTube / 小宇宙)不受影响,仍走原生下载器。
> 适用版本:v1(接管 **抖音 + 小红书 + 微信视频号**)。其它平台(B 站 / YouTube / 小宇宙)不受影响,仍走原生下载器。

## 这是什么

[MediaResolverAPI](https://github.com/) 是一个独立的「短视频 URL → 无水印直链 + 元数据」解析服务,
内置 TikHub 多端点降级 + Cobalt 兜底。本项目可选地把**抖音 / 小红书的解析**外包给它,从而:
内置 TikHub 多端点降级 + Cobalt 兜底。本项目可选地把**抖音 / 小红书 / 微信视频号的解析**外包给它,从而:

- 把易碎的 TikHub 解析逻辑集中到专用服务,本仓库退化为「下载 + 转录 + LLM」;
- 抖音改为下载**完整 mp4 再由 CapsWriter 提取音轨**(而非旧版直接抓 `music.play_url` 的 mp3)——
对套用热门 BGM 模板的口播视频,提取的是**视频自带人声**而非背景乐,转录更准。
对套用热门 BGM 模板的口播视频,提取的是**视频自带人声**而非背景乐,转录更准;
- 支持微信视频号(`https://weixin.qq.com/sph/<sph_code>`)链接转录,通过 MediaResolverAPI 的流式代理端点边解密边拉取。

> ⚠️ **行为变更**:开启后抖音下载体积由 mp3 增大为 mp4。长视频可能撞 `storage.max_download_size_mb`
> 上限,或 CapsWriter 一次性入内存的限制。短视频无影响。
Expand All @@ -19,16 +20,22 @@
| 你的情况 | 建议 |
|---------|------|
| 抖音/小红书解析经常失败、想集中维护解析逻辑 | ✅ 启用 |
| 需要转录微信视频号链接 | ✅ 启用(视频号必须依赖 MediaResolverAPI) |
| 已部署 MediaResolverAPI 服务并有 API Key | ✅ 启用 |
| 只转录 B 站/YouTube/小宇宙 | 无需启用(默认 off,不影响) |
| 没有 MediaResolverAPI 服务 | 保持 off,继续用内置 TikHub 直连 |
| 没有 MediaResolverAPI 服务 | 保持 off,继续用内置 TikHub 直连(视频号不可用) |

默认 **关闭**。开关打开前请确认 MediaResolverAPI 服务可达。

## 前置条件

1. 一个可访问的 MediaResolverAPI 服务(自建或他人提供),拿到 `base_url` 与 `X-API-Key`。
2. 服务健康检查:
2. 微信视频号转录前提:
- 必须设置 `downloaders.use_media_resolver: true`(无原生下载器兜底);
- 若配置了 `security.download_url_allowlist` 安全下载白名单,MediaResolverAPI 服务域名/IP 须在允许列表中;
- 视频号 `video_url` 指向 resolver 自己的流式代理端点(`/api/stream/wechat_channels/{sph_code}`),下载时下载器会自动携带 `X-API-Key` 鉴权头(第三方 CDN 直链则不会携带);
- resolver 流式端点受上游并发限制,若单进程并发超限会返回 429(Too Many Requests)。
3. 服务健康检查:

```bash
curl http://<your-host>:<port>/health
Expand Down Expand Up @@ -58,21 +65,21 @@

> **Docker 注意**:容器内访问宿主机服务用 `host.docker.internal`,不要写 `localhost` / `127.0.0.1`。

配置改完无需改代码——`factory` 会在 `use_media_resolver=true` 时自动把抖音/小红书路由到
配置改完无需改代码——`factory` 会在 `use_media_resolver=true` 时自动把抖音/小红书/视频号路由到
`MediaResolverDownloader`,并跳过旧的 `DouyinDownloader` / `XiaohongshuDownloader`。

## 使用

开关打开后,正常提交抖音/小红书链接即可,无需任何额外参数:
开关打开后,正常提交抖音/小红书/视频号链接即可,无需任何额外参数:

```bash
curl -X POST http://localhost:8000/api/transcribe \
-H "Authorization: Bearer <your-auth-token>" \
-H "Content-Type: application/json" \
-d '{"url": "https://v.douyin.com/xxxxxxx/"}'
-d '{"url": "https://weixin.qq.com/sph/AOzokRxWHz"}'
```

支持的链接形态:`douyin.com` / `v.douyin.com` 短链 / `xiaohongshu.com` / `xhslink.com` 短链。
支持的链接形态:`douyin.com` / `v.douyin.com` 短链 / `xiaohongshu.com` / `xhslink.com` 短链 / `weixin.qq.com/sph/<sph_code>` 视频号链接

## 错误与提示对照

Expand All @@ -86,6 +93,7 @@ curl -X POST http://localhost:8000/api/transcribe \
| 图文/已删除/私密等无视频内容 | 该内容无可转录视频 | 否 |
| 全部解析源失败 | 解析失败,稍后再试 | 否 |
| 服务端错误(HTTP 5xx) | 解析服务异常 | 是 |
| 流式端点并发超限(HTTP 429) | 解析服务繁忙,稍后再试 | 是 |

> 注:当前 MediaResolverAPI 的 `error` 仅返回文案、无结构化 `error.code`,因此「图文/删除」类终态
> 可能被笼统归为「解析失败,稍后再试」。若你维护该服务,建议为终态返回 `error.code` 以便精确区分。
Expand All @@ -98,18 +106,21 @@ MediaResolverAPI 返回的视频直链在下载前会经过 **SSRF 校验**(`u
## 回退

若启用后遇到问题,把 `downloaders.use_media_resolver` 改回 `false` 即可立即回到内置 TikHub 直连下载器,
无需回滚代码(旧下载器在迁移期保留)。
无需回滚代码(旧下载器在迁移期保留;视频号链接在未启用时将回退至通用下载器)。

## 故障排查

| 现象 | 排查 |
|------|------|
| 提示「鉴权失败」 | 检查 `media_resolver.api_key`;用 `curl -H "X-API-Key: <key>"` 直接打 `/api/resolve` 验证 |
| 提示「解析服务暂不可用」 | 检查 `base_url` 可达性、`/health`、Docker 内是否误用 localhost |
| 视频号下载失败(401) | 确认下载请求发往 resolver 域名并携带了 `X-API-Key` |
| 视频号下载失败(429) | MediaResolverAPI 流式并发超限,稍后重试或调整 resolver 服务并发能力 |
| 抖音下载撞大小上限 | 调高 `storage.max_download_size_mb`,或对长视频暂时关闭开关 |
| 想确认走了哪个下载器 | 看日志 `为URL创建下载器: ..., 类型: MediaResolverDownloader` |

## 相关文档

- 设计与实现决策记录:[docs/designs/media-resolver-integration.md](../designs/media-resolver-integration.md)
- 后续 v2 规划(多平台/观测/CDN 兜底):见仓库 `TODOS.md`

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 视频号(wechat_channels)接入独立审查

- **审查对象**:`ea71bf3f9167faa732b25928bb959116aac8c4f4..dedd376e6def0ea04b5b9f8ca89168abd4c8e6ad`(H0 冻结,7 commit)
- **risk-tier**:personal(P1 = 数据丢失 / 静默出错 / 崩溃)
- **本轮新证据**:① `urlparse` netloc 矩阵(大小写/端口/userinfo/尾点/IPv6/无 scheme);② `ocr-review` status=`reviewed`(minimax);③ H0 `factory.py` 实例生命周期;④ `urlparse` 抛异常面。同一份 diff 复读不算新证据,以上均是生成结论前新跑的。

## 结论

**pass**。无 P1;无阻塞合并的 P2。OCR 5 条均为测试/可维护性意见,本仓判定不升级。

## Findings

无。

## 锁定决策(6 条)

1. **平台字符串 `wechat_channels`**:符合。H0 `git grep` 生产路径的平台标识均为 `wechat_channels`(`url_parser.py:84,293`、`views.py:611`、`history.html:921`)。`shipinhao` 零命中。存量 `config.wechat` / `wechat_webhook` 是企微通知通道,不是视频平台标识。
2. **`X-API-Key` 仅 netloc 相等时携带**:符合。`media_resolver.py:222-229` 用 `urlparse(...).netloc.lower()` 精确相等,且 `_resolver_netloc` 为空则不带头。基类 `base.py:224` 走 `headers=self.download_headers or None`。测试锁 CDN 不带、resolver 流式端点带(`TestConditionalDownloadHeaders`)。
3. **复用 `download_headers` + 403 重下也设头**:符合。首次 `download_file:238` 与 force_refresh 后 `download_file:269` 都调用 `_prepare_download_headers`;重解析语义(反查 → force_refresh → SSRF 校验 → 再下,失败抛 `DownloadFailedError`)相对 base 未改。测试 `test_reresolve_applies_correct_headers_on_retry` 锁重下带 key。
4. **不新增配置项;flag off 落 GenericDownloader**:符合。`config/` 无 diff。`test_downloader_factory.py`:flag on → resolver,flag off + sph URL → `GenericDownloader`。
5. **sph 不进 `SHORT_URL_DOMAINS`**:符合。`url_parser.py:90-95` 仍只有 `b23.tv` / `youtu.be` / `v.douyin.com` / `xhslink.com`;sph 走 `PATTERNS['wechat_channels']`。
6. **静态资产变更递增 `CACHE_NAME`**:符合。base `vta-static-v4` → H0 `vta-static-v5`;`tests/unit/web/test_frontend_auth.py` 断言同步。

## 本轮新角度(4 条)

### 1. netloc 相等判定绕过面

查了,无发现(泄露方向干净;漏带头不是静默成功)。

对 `_prepare_download_headers` 同一谓词跑矩阵:

| 形态 | 结果 |
|---|---|
| 大小写 / 尾斜杠 / `#fragment` | 匹配,该带头 |
| `https://host:8000` vs `http://host:8000`(同 netloc) | 匹配(spec 比的是 netloc 不是 scheme) |
| 无 scheme 的 `host:port` | `_resolver_netloc=""` → 永不带头 |
| 缺省端口 vs 显式 `:80`/`:443`、尾点、userinfo、IPv6 字面量不同写法 | 字符串不等 → 不带头 |
| CDN `cdn.example.com` vs resolver netloc | 从未匹配 |

泄露(不该带却带了):矩阵无此格;只有 CDN 的 netloc 真等于 resolver 才会带,那就是同一主机。
漏带(该带没带):owner 配置畸形或上游 stream URL 与 `base_url` 形态不一致 → 视频号 401/下载失败,不是静默错结果。按 review-discipline,可信配置畸形 ≤P2,且与 spec「netloc 相等」字面一致;要求规范化会反着 spec。接受不修。

### 2. `_prepare_download_headers` try/except pass 与静默出错

查了,无发现(不构成 P1 静默出错)。

`except Exception: pass` 之后恒执行 `self.download_headers = {}`(fail-closed)。`urlparse` 对 str 几乎不抛;会抛的是非 str / 非法 IPv6。下载 URL 已经 `validate_url_safe`。缺 key 的视频号流式端点 → 401 → `DownloadFailedError`,不会「下到文件却没鉴权还当成功」。CDN 路径空头是正确行为。OCR 建议补 debug 日志:可维护性 P3,接受不修。

### 3. 实例级 `download_headers` 串扰

查了,无发现(真实使用方式下不会跨任务共享实例)。

`create_downloader`(`factory.py:46`)每次 `MediaResolverDownloader()` 新实例,无进程级单例。每次 `download_file` 开头重设 headers;403 重下对 `fresh_url` 再设一次。顺序复用同一实例(先视频号后 CDN)会被第二次 `_prepare` 清掉 key。并发意见在 personal 档 ≤P2,且当前工厂不会让两任务共享同一实例。

### 4. 熵增

查了,无熵 +1。新增 `_prepare_download_headers` 有两个调用点(首次 + 重下),不是转发-only、不是无第二消费者。`_resolver_netloc` / `_resolver_api_key` 是 `__init__` 一次缓存,不是第二事实源。无新配置项、无新文件、无单实现接口。

## OCR 前置

- `ocr-review` status=`reviewed`(primary minimax);verifier 两条腿不可用,5 条均为 `unverified`。
- 工具标注 → 本仓判定(P1 两问:真实使用会触发吗?后果能否接受?):

| # | 工具 | 本仓 | 两问 | 处置 |
|---|---|---|---|---|
| 1 | except pass 无日志 / low | P3 | 触发:几乎不。后果:空头 + 401,非静默错 | 接受不修 |
| 2 | 测试 `headers == {key}` 过严 / medium | 不成立 | 测试形态,不是生产缺陷 | 不采纳 |
| 3 | 测试写入 `_resolver_netloc` 绕过 `__init__` / high | 不成立 | 生产仍走 `urlparse(base_url)`;测试缺口不导致运行时漏带头 | 不采纳 |
| 4 | 403 测试靠 `HTTPError` 注释不准 / medium | 不成立 | 重下路径另有 `force_refresh` 断言 | 不采纳 |
| 5 | 重下缺「fresh netloc 不同」用例 / medium | P3 测试缺口 | 不触发生产静默错 | 接受不修 |

## Backlog(不占本卡结论)

- `can_handle` 仍用 `domain in url` 子串(与抖音/小红书同一存量模式),非本 diff 引入。
- OCR #1/#5:except 无日志、测试未覆盖「重下换 netloc」;P3,接受不修。
1 change: 1 addition & 0 deletions retro/acceptance-log.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
{"ts":"2026-08-24T22:55:34+08:00","dispatch_id":"dlg-20260824-143605-1d04d3","task_id":"VideoTranscriptAPI-20260824-05","repo":"VideoTranscriptAPI","executor":"codex","model":"gpt-5.6-luna","effort":"xhigh","runtime":"systemd","task_type":"review","complexity":"S","rounds":1,"wall_clock_min":9,"diff_lines":198,"commits":["7ed9f7460e01c56e35b04a00e872865bd6730145","712055c14adaafc0c54c2e52157cf95113d5db5d"],"commits_resolution":"partial","outcome":"accepted","scope_discipline":3,"taste":2,"note":"R1 verdict pass-with-backlog: docs/sessions/260824-summary-trunc/reviews/summary-truncation-r1-verdict.md (commit 7ed9f74 on PR #65 branch); 0 P1/P2; 降层三问+熵增+旁路排查齐全; 全量 pytest exit 0","report_directory":"20260824-143610-big-codex-VideoTranscriptAPI","decisions_ref":"PR #65"}
{"ts":"2026-08-24T22:57:20+08:00","dispatch_id":"dlg-20260824-133558-fd0c2f","task_id":"VideoTranscriptAPI-20260824-04","repo":"VideoTranscriptAPI","executor":"cursor","model":"composer-2.5","effort":"unknown","runtime":"systemd","task_type":"backend-logic","complexity":"L","rounds":1,"wall_clock_min":10,"diff_lines":533,"commits":["5fd47cad624acedddfaf0f697250e79fb99f9845"],"commits_resolution":"partial","outcome":"accepted","scope_discipline":3,"taste":2,"note":"主脑独立验收: 全量 pytest 绿(exit 0); 红验 base 红; R1 verdict pass-with-backlog: docs/sessions/260824-summary-trunc/reviews/summary-truncation-r1-verdict.md; 实现提交 5fd47cad624acedddfaf0f697250e79fb99f9845","report_directory":"20260824-133602-big-cursor-VideoTranscriptAPI","decisions_ref":"PR #65"}
{"ts":"2026-08-24T23:18:07+08:00","dispatch_id":"dlg-20260824-145748-1737de","task_id":"VideoTranscriptAPI-20260824-06","repo":"VideoTranscriptAPI","executor":"grok","model":"grok-4.6","effort":"unknown","runtime":"systemd","task_type":"review","complexity":"S","rounds":1,"wall_clock_min":17,"diff_lines":142,"commits":["e344a8ddea8c631735e0b6decc0cafef89fb2ce8"],"commits_resolution":"partial","outcome":"accepted","scope_discipline":3,"taste":3,"note":"R2 对抗视角 verdict pass-with-backlog: docs/sessions/260824-summary-trunc/reviews/summary-truncation-r2-verdict.md (commit e344a8ddea8c631735e0b6decc0cafef89fb2ce8); 0 P1, 2 P2(其一转修复轮), 3 注入红验, 全量 pytest exit 0","report_directory":"20260824-145758-big-grok-VideoTranscriptAPI","decisions_ref":"PR #65"}
{"ts":"2026-09-02T03:05:50+08:00","dispatch_id":"dlg-20260901-181636-fe8dd0","task_id":"VideoTranscriptAPI-20260902-01","repo":"VideoTranscriptAPI","executor":"agy","model":"gemini-3.7-flash-high","effort":"unknown","runtime":"systemd","task_type":"backend-logic","complexity":"S","rounds":2,"wall_clock_min":17,"diff_lines":233,"commits":["dedd376e6def0ea04b5b9f8ca89168abd4c8e6ad","dd6098b7678b50e3486a738c8f6f56d9dcc798f8","dda22e7eec9273e547edcb5d9d7a708fdfe9b099","76678f93b6b7d75381f4e24e270dc7d58c8632ce","541061b8b66ed911cc6ad94f2df54ff06f0c31ae","c840bbc42572cca49bf2861fdc0de0ad66250823","4228f5397845ae840c26280c6a7b2f9415594a9e"],"commits_resolution":"resolved","outcome":"accepted","scope_discipline":3,"taste":1,"note":"视频号接入,head dedd376e6def0ea04b5b9f8ca89168abd4c8e6ad:agy 实现 6 commit + 1 轮修复(swjs 缓存版本回退方向反了,根因拆卡漏列前端鉴权测试的硬编码版本断言,scope-miss)。主脑验收:diff 逐行审 + base 红验抽查 7 条全红 + 全量单测绿;OCR reviewed 无 P1;cursor 独立 review pass 无 P1,收敛。","report_directory":"20260901-181642-quick-agy-VideoTranscriptAPI","rework_origin":"scope-miss"}
2 changes: 2 additions & 0 deletions src/video_transcript_api/api/routes/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ async def transcribe_video(
title = "小红书内容转录"
elif "douyin.com" in display_url:
title = "抖音视频转录"
elif "weixin.qq.com" in display_url:
title = "视频号视频转录"

notification_router = get_notification_router()
notification_router.send_view_link(
Expand Down
1 change: 1 addition & 0 deletions src/video_transcript_api/api/routes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ def generate_download_filename(title: str, platform: str, content_type: str) ->
"xiaohongshu": "小红书",
"xiaoyuzhou": "小宇宙",
"apple_podcast": "Apple播客",
"wechat_channels": "视频号",
"generic": "自定义",
}

Expand Down
Loading
Loading