Skip to main content
Glama
README.md
# yt-subtitle-mcp

一个 MCP 服务器:**优先抓 YouTube 现成字幕,没有字幕才用 Whisper 从音频转录。**

转录结果落盘成 Obsidian 友好的 Markdown(frontmatter + 带时间戳章节),
中文内容可选自动转成简体。

语音识别、字幕解析、繁简转换全部在本地完成,不上传音视频,也不需要
语音识别或翻译服务的 API key。

## 它做什么

输入一个 YouTube 链接,得到一份可读的转录稿。流程:

```
YouTube URL
  → 取元数据(含字幕清单、原声语种)
  → ① 有合适字幕?→ 下载 json3/VTT → 解析合并 → 落盘      【秒级,零 CPU】
  → ② 没有 → 下载音频 → 16kHz 单声道 wav
            → faster-whisper(CTranslate2 int8,纯 CPU 可跑)
  → 中文内容 → OpenCC 繁→简(可关)
  → 落盘 <输出目录>/<标题>.md
  → 缓存 <输出目录>/.cache/<videoId>.json
```

## 快速开始

```bash
git clone https://github.com/aisahpA/yt-subtitle-mcp.git
cd yt-subtitle-mcp
bash scripts/setup.sh
```

`setup.sh` 会建 Python venv、装 faster-whisper / yt-dlp(带浏览器指纹伪装)/ OpenCC、
装 Node 依赖,并做一次自检。**不需要 GPU。**

首次用某个模型时会自动下载权重(默认 `large-v3-turbo`,约 1.5GB;`small` 约 470MB)。
如果 `huggingface.co` 在你的网络下不可达,默认已配好 `hf-mirror.com` 镜像。

## 接入客户端

任何支持 stdio MCP 的客户端都可以,把 `server.js` 的**绝对路径**填进去。

<details open>
<summary>Claude Desktop / 通用 mcpServers 格式</summary>

```json
{
  "mcpServers": {
    "youtube-subtitle": {
      "command": "node",
      "args": ["/绝对路径/yt-subtitle-mcp/server.js"]
    }
  }
}
```
</details>

<details>
<summary>OpenCode(~/.config/opencode/opencode.json)</summary>

```json
{
  "mcp": {
    "youtube-subtitle": {
      "type": "local",
      "command": ["node", "/绝对路径/yt-subtitle-mcp/server.js"],
      "enabled": true,
      "environment": {
        "YTS_SUBTITLE_LANG": "zh",
        "YTS_TO_SIMPLIFIED": "1"
      }
    }
  }
}
```
</details>

<details>
<summary>DSH(~/.dsh/profiles/&lt;profile&gt;/cordis.patch.yml)</summary>

```yaml
- insert:
    - id: mcp-youtube-subtitle
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: youtube
        transport: stdio
        command: node
        args:
          - /绝对路径/yt-subtitle-mcp/server.js
        env:
          YTS_SUBTITLE_LANG: zh
          YTS_TO_SIMPLIFIED: '1'
        # 长视频转录可能几十分钟,默认 60s 必然超时
        toolCallTimeoutMs: 7200000
```
</details>

> ⚠️ **最重要的一条**:MCP 的 stdio transport **不继承父进程环境变量**
> (官方 SDK 只传 `HOME/LOGNAME/PATH/SHELL/TERM/USER`)。
> 所以**代理、模型、输出目录等配置必须写在客户端的 `env` 块里**,
> 写在 shell 的 `.zshrc` 里不会生效。这是最容易踩的坑。

## 工具

只暴露一个工具:**`get_video_transcript`**。

| 参数 | 类型 | 说明 |
|---|---|---|
| `url` | string | 视频链接或 11 位 video id |
| `preview_length` | `short`/`medium`/`long` | 返回的预览长度,默认 `medium`(约 4000 字) |
| `model` | Whisper 模型名 | 只对 Whisper 生效,不填用 `YTS_MODEL`(默认 `large-v3-turbo`)。见「转录质量与速度」 |
| `hotwords` | string | 额外领域词表(简体、空格分隔),叠在自动抽词后面。**换词会作废缓存并重跑** |
| `output_language` | `auto`/`zh`/`en` | 后续总结用什么语言,默认 `auto`(跟视频原声一致)。只影响返回值里的 `summaryHint`,不改转录本身 |
| `force` | boolean | 忽略缓存强制重跑 |

### 返回什么

关键点:**它不返回全文**。一份 20 分钟视频的转录稿轻松几万字,直接塞进上下文
既贵又没必要。返回值是:

```json
{
  "video":     { "title": "...", "uploader": "...", "duration": "5:56", "views": "1571.3万" },
  "transcript": {
    "language": "zh-tw",  "source": "youtube-manual-caption",
    "characters": 1453,   "segments": 31,
    "fromCache": false,   "truncated": false,
    "convertedToSimplified": true
  },
  "savedTo":     "/…/transcripts/How to sound smart….md",
  "preview":     "有听见什么吗?答案是「没有」……",
  "summaryHint": "完整正文(1453 字)在 savedTo 指向的 Markdown 里……",
  "description": "视频简介(最多 1500 字)"
}
```

- **`savedTo` 才是正文所在。** `preview` 只用来判断"这份稿子对不对",
  `truncated: true` 表示 preview 被截断过。
- `transcript.source` 说明文字怎么来的:`youtube-manual-caption` /
  `youtube-auto-caption` / `whisper`。
- **`summaryHint` 是给模型看的下一步指令**,不是结果。总结由调用方模型
  读 `savedTo` 文件后自己写——本工具不做总结。
- `description` 是视频简介。它有时比正文更能说明"这视频到底在讲什么"。
- 走 Whisper 时还会多一个 `transcript.audio`,说明音频的去向:
  `{ kept: true, keptPath }`(本次保留)或 `{ reused: true, path }`(复用了旧的)。

## 使用方式

你不需要记工具名,**把链接丢进对话**就行,模型会自己调用:

```
转录一下 https://www.youtube.com/watch?v=xxxxxxxx
```

### 常见任务怎么问

| 你想做的 | 建议这样说 |
|---|---|
| 存进 vault 当资料 | `转录这个 <链接>`(默认落盘为 `<标题>.md`) |
| 快速知道讲了什么 | `总结这个 <链接>,300 字以内` |
| 引用原话 | `转录这个 <链接>,然后找出他讲 <某观点> 的原话` |
| 换个语言总结 | `转录这个 <链接>,用英文总结` |
| 怀疑字幕质量,重跑 | 明确说 `忽略缓存重新转录这个 <链接>` |
| 批量 | 一次给多个链接,一个链接一次调用 |

后三种会自然形成「工具落盘 → 模型读文件 → 加工」的流程,这也是它设计成这样
(只返回路径 + 预览)的原因。

### 效率上的四个事实

- **同一个视频问第二次 = 秒回**(`fromCache: true`),不重新下载、不重新转录
- **有现成字幕的视频约 7 秒**;没字幕要走 Whisper。默认模型
  (`large-v3-turbo`)下,5 分钟视频约 3 分钟、20 分钟视频约 14 分钟;
  传 `model: "small"` 可以快到 1/2(但质量差,见「转录质量与速度」)
- **改 `YTS_MODEL`、`YTS_HOTWORDS` 或传 `model`/`hotwords` 会让缓存自动失效**
  (同一视频用不同设置,结果本就该不同)。缓存的指纹包含模型、词表、语种、
  compute_type、vad、补录阈值——之前只比模型,结果"改了 hotwords 却拿到旧稿",
  这个坑真的踩过。失效后如果开着 `YTS_KEEP_AUDIO`,会复用留下的音频、只花转录时间
- **想强制重跑**就加 `force`,或直接说"忽略缓存"

### 和 Obsidian 的关系

落盘的就是标准 Markdown:frontmatter(标题/来源/频道/时长/语种/来源类型)
+ 带时间戳的转录章节。把 `YTS_OUTPUT_DIR` 指向 vault 目录,
出来的文件可以直接当笔记读、被检索、被双链引用。

## 转录质量与速度

先给结论:**默认用 `large-v3-turbo`。** 要快速草稿就在调用时传 `model: "small"`,
或用 `YTS_MODEL` 改默认。

实测数据(22 分钟中文访谈,Intel i7-8850H 12 核、无 GPU、int8)。
RTF = 处理耗时 ÷ 音频时长,越小越快:

| 模型 | RTF | 22 分钟视频耗时 | 中文表现 |
|---|---|---|---|
| `small` | 0.36 | 约 8 分钟 | 听错常用词,**还会整段丢失** |
| **`large-v3-turbo`(默认)** | 0.64 | 约 14 分钟 | 明显更准,本次样本上与 `large-v3` 持平 |
| `medium` | 1.61 | 约 35 分钟 | 与 turbo 相近,但慢 2.5 倍 |
| `large-v3` | 1.83 | 约 40 分钟 | 本次样本上**没有**比 turbo 更准 |

同一段音频的差距实例:

| 原文 | `small` | `large-v3-turbo` |
|---|---|---|
| 书香门第 | 书乡门帝 ❌ | 书香门第 ✅ |
| 延边 | 沿边 ❌ | 延边 ✅ |
| 换母语 | **整个词丢失** | 换母语 ✅ |

### 坑一:Whisper 会静默丢掉整个 30 秒窗口

faster-whisper 按 30 秒窗口推进。某一窗如果输出空文本,`transcribe.py` 会直接
`continue` 掉它(`if segment["start"] == segment["end"] or not text.strip()`),
时间轴上就出现一个洞——**不报错、不警告**。

实测那份 22 分钟视频里,3:33–3:56 一整段(讲托福/GRE 真题和进黑名单的那段)
就这么没了;而**把同一段音频单独切出来,用同样的 `small` 和默认参数就能正常转**。
所以这不是"模型听不见",是窗口推进的机制问题。

**哪些地方容易丢**:翻回原片对过,丢失的两段(3:33 和 7:02)都是**插入了另一段
视频的片段、声音换了一个人**的位置。声源一变,模型判定"这里不是说话"的概率就上去,
整窗被丢。这也解释了为什么补录能救回来——同一段音频换个窗口起点重新解码就正常了。

本项目的对策是**缺口补录**:主转录跑完后扫描时间轴缺口,把缺口重新切出来单独
转一遍,再按时间合并。这个兜底**与模型无关**(`small` 和 `large-v3` 都出现过整窗
丢失),控制它的是 `YTS_GAP_MIN_SECONDS`(默认 10 秒,设 `0` 关闭)。

补录有个反直觉的地方:**切多长决定成败**。同一个 23 秒缺口(同一台机器、同一个
`small`),只改"缺口前留几秒":

| 缺口前留 | 窗口长度 | 捞回字数 |
|---|---|---|
| 2 秒 | 27 秒 | 13 |
| 3–4 秒 | 29–31 秒 | **0** |
| **5–6 秒** | 32–33 秒 | **135** ✅ |
| 8 秒 | 35 秒 | 15 |
| 14 秒 | 41 秒 | 47 |

所以代码里按 5→6→8 秒的顺序试几组窗口,取捞回最多的一次,够本(约六成)
就提前收工。实测那次补录**第一组就命中 135 字,多花 13 秒**。

补录还有一个必须防的坑:**有时候正文其实没丢,只是主转录把时间戳推后了**。
TEDx 那支视频开场白被记在 19.3 秒、0–19 秒显示为空,补录把同一句话又转了一遍,
贴上去就成了"同一句话说两遍"。所以补录结果会跟缺口后面的已有段落比一次覆盖率,
像"同一句话"就丢掉(日志里记 `discarded_as_duplicate`)。

**成本上限**:片头/片尾的缺口通常是音乐或掌声,捞不出东西却会跑满所有尝试
("够本"阈值对静音永远达不到)。实测一个 19 秒的片头缺口跑满 5 组、只捞回
15 个字。所以边缘缺口只试一次、中间缺口上限 3 组;正常一次补录
只多花 3–13 秒。

已知限制:补录段和主转录在缺口边界上可能重复几个字(实测 1000 字里重复 8 字,
因为两侧对同一个词的写法不同,去重匹配不到)。补了几处、补回多少字会出现在
返回值的 `gapRetry` 和缓存 JSON 里,能逐条核对。

### 坑二:`hotwords` 能救专有名词——而且能自动生成,不用手工维护

同一段音频、同一个 `small` 模型:

| `hotwords` | 结果 |
|---|---|
| 不传 | 托福→托付 ❌、新东方→心动方 ❌、李笑来→李销来 ❌、凶悍→凶汗 ❌ |
| 人工给「托福 GRE 新东方 真题 黑名单 李笑来」(简体) | **全对** ✅ |
| **从标题自动抽的 5 个词**(简体) | 与人工挑的**持平** ✅ |
| 给视频标题里的繁体「李笑來」 | 基本没用 ❌ |

所以默认会**自动从标题抽词**(不够再取简介):转成简体、按标点切块、只留 2–8 字的
纯中文/拉丁块、短的优先、最多 5 个。实在这段视频的标题抽出的是
`李笑来 他戒烟时 新东方名师 朗读重塑大脑 中国比特币首富`——人名和惯用语都修对了。
用 `YTS_AUTO_HOTWORDS=0` 关掉,用 `YTS_HOTWORDS` 或工具参数 `hotwords` 叠加手动词
(补自动抽不到的名字)。

三条注意事项:

- **必须简体**。繁体词基本救不回简体输出。
- **保持"词表"形态**(空格分隔,别塞整句/带标点)。有过一次整句标题当 hotwords 后
  解码崩坏的记录(`avg_logprob` −2.34、中英日韩乱码),虽然没能稳定复现,但没必要冒险。
- 它和 `initial_prompt` 不是一回事:后者会改变断句和标点风格,实测还漏了 21 秒,**不要用**。

### 坑三:置信度抓不到错别字,只能抓"解码崩坏"

本来想让工具直接告诉你"哪几段可疑",实测**做不到**:同一个 `small` 跑出来的段落,
`avg_logprob` 全都在 −0.21 / −0.23,**错的和对的一样自信**
(`李孝萊`、`凶汗`、`名诗`、`必权人` 全在正常区间里)。

它唯一有用的是抓崩坏:那次中英日韩乱码的崩坏,`avg_logprob` 掉到 −2.34,一眼可辨。
所以返回值里只在真的出现异常段时才带 `lowConfidence`(阈值 −1.0),
平时不要指望它替你找错别字。

**结论:想少错就换更大的模型(默认已是 `large-v3-turbo`),词表靠自动抽 + 偶尔手工补。**

### 试过但没用的旋钮

省得你再折腾一遍。在本次样本上,以下改动**都没有可测量的改善**:

- 开关 VAD(`--no-vad`)、调 `min_silence_duration_ms`
- 强制 `YTS_LANGUAGE=zh`(与 `auto` 输出逐字相同)
- 放宽 `log_prob_threshold` / `no_speech_threshold`(**阈值全关掉照样整窗丢失**)
- `beam_size` 5 → 10
- `compute_type` `int8` → `int8_float32`

真正有效的只有三件:**换更大的模型、给对 `hotwords`、补录缺口**。

## 配置

全部通过环境变量,写在 MCP 客户端的 `env` 块里。

本项目自己的配置项统一用 `YTS_` 前缀。此外还有几个沿用业界通用名的变量
(`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`、`HF_ENDPOINT` 等),
它们不带前缀,因为换名字反而会让通用工具认不出来。

### 字幕

| 变量 | 默认 | 说明 |
|---|---|---|
| `YTS_PREFER_SUBTITLES` | `auto` | `auto`=人工>自动>Whisper;`manual`=只认人工字幕;`off`=永远用 Whisper |
| `YTS_SUBTITLE_LANG` | 空 | 想要的字幕语种,如 `zh` / `zh-Hans` / `en`;不设则用视频原声语种 |
| `YTS_TO_SIMPLIFIED` | `1` | 中文内容(字幕或 Whisper 转录)繁→简转换,设 `0` 关闭 |
| `YTS_CONVERT_CONFIG` | `auto` | OpenCC 档:`auto`=按输入脚本自动挑;也可写死 `tw2sp`(含词汇本地化)/ `t2s`(纯字形) |
| `YTS_SUBTITLE_CHUNK_MS` | `10000` | 字幕合并成几秒一块 |

### 输出

| 变量 | 默认 | 说明 |
|---|---|---|
| `YTS_OUTPUT_DIR` | `<项目目录>/transcripts` | 转录 Markdown 落盘目录(**相对路径按项目目录解析**) |
| `YTS_OUTPUT_LANGUAGE` | `auto` | 后续总结用什么语言(`auto`/`zh`/`en`)。工具参数 `output_language` 可逐次覆盖 |
| `YTS_KEEP_AUDIO` | `0` | 保留 Whisper 用到的音频,便于换模型重跑(见下) |
| `YTS_AUDIO_DIR` | `<输出目录>/audio` | 音频保留位置(仅 `YTS_KEEP_AUDIO=1` 时使用) |
| `YTS_CACHE_DIR` | `<输出目录>/.cache` | 缓存目录 |
| `YTS_TMP_DIR` | 系统临时目录 | 下载音频的临时目录 |

**相对路径按项目目录解析,不是当前工作目录**——MCP 服务器启动时的 cwd
不可预测,按项目目录才稳定。想写到别处(比如 Obsidian vault 里)就给绝对路径:

```json
"YTS_OUTPUT_DIR": "/Users/you/Documents/my-vault/youtube"
```

### 保留音频

默认转完就删音频:它只为这一次转录服务,16kHz wav 约 **2MB/分钟**
(22 分钟视频约 42MB),留在磁盘上不划算。

但保留有它的用处——**换更大的模型重跑**。这时候音频是唯一的重复成本
(重新下载往往比转录本身还慢),留着就能只花转录时间:

```json
"YTS_KEEP_AUDIO": "1"
```

打开后:

- 音频从临时目录复制一份到 `YTS_AUDIO_DIR`(默认 `<输出目录>/audio/`),
  文件名是 `<videoId>.wav`,同一个视频重跑不会堆积多份
- **改了 `YTS_MODEL` 再跑同一个视频,缓存自动失效并复用这份音频**,不重新下载
- 想清理就手动删 `audio/` 目录,程序不主动回收

工具返回值里能看到实际动作:`transcript.audio` 为 `{ kept: true, keptPath }`
(本次保留)或 `{ reused: true, path }`(复用了旧的)。

### Whisper

| 变量 | 默认 | 说明 |
|---|---|---|
| `YTS_MODEL` | `large-v3-turbo` | `tiny`/`base`/`small`/`medium`/`large-v3`/`large-v3-turbo`。别降回 `small`,见「转录质量与速度」 |
| `YTS_HOTWORDS` | 空 | 手工叠加的领域词表(人名/术语),**简体、空格分隔**。默认已自动从标题抽词,这里只补抽不到的 |
| `YTS_AUTO_HOTWORDS` | `1` | `1` = 从标题(不够再取简介)自动抽 5 个词;`0` 关闭 |
| `YTS_GAP_MIN_SECONDS` | `10` | 时间轴缺口超过这么多秒就单独补录一遍(整窗丢失兜底),`0` 关闭 |
| `YTS_LANGUAGE` | `auto` | 强制语种,如 `zh` / `en`。实测强制 `zh` 与 `auto` 无差别 |
| `YTS_BEAM_SIZE` | `5` | beam search 宽度。实测 5→10 无可测量改善 |
| `YTS_THREADS` | CPU 核数-4 | 转录线程数 |
| `YTS_COMPUTE_TYPE` | `int8` | CPU 上 int8 最快;有 GPU 可换 `float16` |
| `YTS_DEVICE` | `cpu` | `cpu` 或 `cuda` |
| `YTS_TRANSCRIBE_TIMEOUT_MS` | 3 小时 | 转录超时 |

### 网络

| 变量 | 默认 | 说明 |
|---|---|---|
| `HTTP_PROXY` / `HTTPS_PROXY` | 空 | 需要代理才能访问 YouTube 时**必须显式传**(见上面的警告) |
| `HF_ENDPOINT` | `https://hf-mirror.com` | 模型下载源 |
| `HF_HUB_DISABLE_XET` | `1` | 禁用 hf-xet 后端(国内镜像下会 401) |
| `YTS_HF_RETRIES` | `6` | 模型权重下载失败时的重试次数 |
| `YTS_IMPERSONATE` | `chrome` | yt-dlp 的浏览器指纹伪装目标 |

> `NO_PROXY` 请写成 `localhost,127.0.0.1,::1`。带方括号的 `[::1]` 会让
> Python 的 httpx 抛 `InvalidURL: Invalid port`,把模型下载打崩。

### 超时与路径(一般不用改)

| 变量 | 默认 | 说明 |
|---|---|---|
| `YTS_METADATA_TIMEOUT_MS` | 5 分钟 | 取元数据超时 |
| `YTS_DOWNLOAD_TIMEOUT_MS` | 20 分钟 | 下载音频超时 |
| `YTS_SUBTITLE_TIMEOUT_MS` | 3 分钟 | 下载单条字幕超时 |
| `YTS_TRANSCRIBE_TIMEOUT_MS` | 3 小时 | Whisper 转录超时 |
| `YTS_CONVERT_TIMEOUT_MS` | 2 分钟 | 繁简转换超时 |
| `YTS_PYTHON` | `<项目>/venv/bin/python` | 解释器路径 |
| `YTS_YTDLP` | `<项目>/venv/bin/yt-dlp` | yt-dlp 路径 |
| `YTS_FFMPEG` | 自动探测 | ffmpeg 路径;在 `/opt/local/bin`、`/usr/local/bin`、`/opt/homebrew/bin`、`/usr/bin` 里找第一个存在的,都找不到就交给 `PATH` |

后三个是给「不想用项目自带 venv / ffmpeg 不在默认位置」的情况准备的。
正常装了 `ffmpeg` 就不用管它们。

## 字幕挑选规则

按优先级排出**一条候选链**,前面的失败会继续试后面的:

| 顺序 | 轨道 | 说明 |
|---|---|---|
| 1 | 目标语种的**人工**字幕 | 最好:语种对、质量高 |
| 2 | 原声语种的**自动**字幕 | 内容忠实,实测能正常下载 |
| 3 | 原声语种的**人工**字幕 | 语种不对,但翻译质量高 |
| 4 | 外语**自动翻译轨** | 几乎必然 429,质量最差,放最后 |

书写系统不符会降权(要简中却给繁中),语种不符也降权。全部落空 → Whisper。

这条链值得维护,因为两条路径的代价差得很远。同一个 5:56 的视频实测:
现成人工字幕 **6.8 秒**,Whisper small **84.6 秒**,而且字幕质量更好
(官方译稿、带 `(笑声)` 这类非语音提示)。

**为什么必须是链而不是单选**:YouTube 对自动翻译轨常年返回 429。
若只挑一条,要简中时会因为翻译轨失败而整个退回 Whisper;
有了链就能退到「原声语种字幕」这类能下的轨道。

`YTS_PREFER_SUBTITLES` 三档:

| 值 | 行为 |
|---|---|
| `auto`(默认) | 人工 > 自动 > Whisper |
| `manual` | 只接受人工字幕,自动生成的一律走 Whisper |
| `off` | 完全不用字幕,永远 Whisper |

> **要中文输出请另读 [中文字幕指南](docs/chinese-subtitles.md)。**
> 中文场景有四个反直觉的坑(自动翻译轨拿不到、`zh-TW` 不等于繁体、
> 写 `zh-Hans` 反而更慢、Whisper 输出繁简混排),以及繁简转换档位的选择依据。

## 落盘格式

frontmatter 里记录转录来源,取值:

| `transcript_method` | 含义 |
|---|---|
| `youtube-manual-caption` | 人工字幕 |
| `youtube-auto-caption` | 自动生成字幕 |
| `whisper` | 本地语音识别(此时才有 `model:` 字段) |

`language` 记录**实际命中的轨道语种**(字幕轨如 `zh-tw`;Whisper 则记它识别出的
`zh`)。若做过繁简转换,
会额外有 `converted_to_simplified: true`——这样你能看出正文经过转换,
不会被 `zh-tw` 的标记误导。工具返回的 `transcript.source` 同义。
转换相关细节见 [中文字幕指南](docs/chinese-subtitles.md)。

## 排查

| 症状 | 原因与处理 |
|---|---|
| `HTTP Error 403` 下载音频/字幕 | yt-dlp 的客户端指纹被拒。已内置 `--impersonate chrome`(需 `curl_cffi`),确认 venv 里装了 |
| `curl: (35) Connection reset` 且 shell 里同样命令能跑 | 客户端没把代理传给 MCP 子进程,检查客户端 `env` 块 |
| `httpx.InvalidURL: Invalid port: ':1]'` | `NO_PROXY` 里有 `[::1]`,改成 `localhost,127.0.0.1,::1` |
| 模型下载 `SSL: UNEXPECTED_EOF` | `huggingface.co` 不可达,用默认的 `HF_ENDPOINT=https://hf-mirror.com` |
| 模型下载 `401 Unauthorized ... xethub.hf.co` | 保持 `HF_HUB_DISABLE_XET=1` |
| 工具调用 60 秒超时 | 客户端默认工具超时太短,设成 2 小时(见 DSH 示例的 `toolCallTimeoutMs`) |
| 一直走 Whisper、很慢 | 该视频确实没有你指定语种的字幕;调 `YTS_PREFER_SUBTITLES=manual` 可跳过 429 的翻译轨尝试 |

## 文件

| 文件 | 说明 |
|---|---|
| `server.js` | MCP 服务器:字幕优先流程 + 落盘 + 缓存 + 繁简转换调度 |
| `worker.py` | faster-whisper 转录 worker(stdout 只输出进度,结果写 JSON 文件) |
| `convert_t2s.py` | OpenCC 繁→简(从 stdin 读 JSON,写回 stdout) |
| `scripts/setup.sh` | 一键准备环境(venv、依赖、自检) |
| `docs/chinese-subtitles.md` | 中文字幕指南:四个坑、繁简转换档位、推荐组合 |

## 依赖

- [yt-dlp](https://github.com/yt-dlp/yt-dlp) —— 下载字幕与音频
- [faster-whisper](https://github.com/SYSTRAN/faster-whisper) / CTranslate2 —— 本地语音识别
- [OpenCC](https://github.com/BYVoid/OpenCC) —— 繁简转换
- [Model Context Protocol](https://github.com/modelcontextprotocol) —— MCP 标准

`ffmpeg` 需要单独安装(`brew install ffmpeg` / `apt install ffmpeg`)。

## License

MIT

TDQS

A4.6/5.0

Scored across 1 tool

Disambiguation5/5

Only one tool exists, so there is no possibility of confusing it with another tool. Its purpose is clearly defined in the description.

Naming Consistency5/5

The single tool name follows a clear and conventional verb_noun pattern (get_video_transcript), and there are no other names to cause inconsistency.

Tool Count3/5

With just one tool, the surface feels minimal but acceptable for a focused transcript-extraction service. Additional tools for cache management or language listing could be added, but the single tool covers the core request.

Completeness5/5

The tool covers the full workflow from retrieving subtitles to generating Markdown, including model selection and caching. There are no obvious missing operations within its stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues