1实现原理 · 为什么它能做到
字幕来源是 YouTube 自带的 InnerTube 播放器接口,不是官方 Data API:脚本先从 watch 页 HTML 里现场正则抓出 INNERTUBE_API_KEY,再带上它 POST 播放器接口。
const apiKey = html.match(/"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/)?.[1];
被 YouTube 判为机器人时不是硬重试,而是换客户端身份:依次用 ANDROID / WEB / IOS 三套 client 声明与 UA 重试,全失败才落到 yt-dlp。
com.google.android.youtube/20.10.38 (Linux; U; Android 14; en_US; Pixel 8 Pro; Build/AP1A.240405.002)
yt-dlp 回退是『先探测、再按能力挑命令』:逐个试 yt-dlp / uvx --from yt-dlp yt-dlp / python3 -m yt_dlp,并要求 --help 里出现 --js-runtimes 与 --remote-components 才算合格。
helpText.includes("--js-runtimes") &&
最终调用是 spawnSync(命令, argv 数组),没有 shell 拼串;探测与执行都带 maxBuffer 上限。
const result = spawnSync(command.command, args, {
--translate 不调用任何翻译 API,只是给字幕轨 URL 追 &tlang=<code>,用 YouTube 自己的机器翻译轨。
url += `&tlang=${translateTo}`;
--speakers 采用『脚本出料 + sub-agent 加工』:脚本只落一份带 SRT 时间戳的原始 md,再让 agent 派一个便宜模型的 sub-agent 做说话人标注与章节切分。
spawn a sub-agent (use a cheaper model like Sonnet for cost efficiency)
缓存即数据面:首跑落 meta.json + transcript-raw.json + transcript-sentences.json + imgs/cover.jpg,`.index.json` 维护 videoId→目录映射,同视频再次运行零网络。
return existsSync(join(videoDir, "meta.json")) && existsSync(join(videoDir, "transcript-raw.json"));
文本输出不是原始 snippets 而是『句子』:按标点切句、时间戳按字符长度比例分配、CJK 合并。
split by sentence-ending punctuation (`.?!…。?!` etc.), timestamps proportionally allocated by character length, CJK-aware text merging
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| api | YouTube InnerTube player API(非公开接口) |
| network | YouTube watch 页(抓 INNERTUBE_API_KEY / visitorData) |
| network | 字幕轨 baseUrl(InnerTube/yt-dlp 返回,fmt=json3/xml) |
| network | YouTube 缩略图 CDN(封面) |
| network | consent.youtube.com 表单(仅字符串匹配,重试用 CONSENT cookie 再请求 watch 页) |
| cli | yt-dlp(直接二进制) |
| cli | uvx --from yt-dlp yt-dlp(免安装形态) |
| cli | python3 -m yt_dlp(已安装模块形态) |
| cli | bun / npx -y bun(脚本运行时) |
| network | yt-dlp --remote-components ejs:github 拉取的 GitHub 远端 EJS 组件 |
4风险提醒 风险提醒:红色 · 谨慎使用
- 逆向伪装官方客户端(判红主因) — 自建 youtubei 播放器请求 + 现场抓网页 key + ANDROID/IOS/WEB 身份冒充,绕开网页端 bot 检测;此类路径随 YouTube 风控调整而失效,也可能触发账号/IP 侧限制。
- 凭证委派给第三方 CLI — 设置 YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER 后,skill 把它透传为 yt-dlp --cookies-from-browser,由第三方二进制读取本机浏览器 cookie 库;共享机器或公司合规环境下不要设置该变量。
- 回退路径会拉远端组件执行 — --remote-components ejs:github 从 GitHub 取 EJS 组件,配 --js-runtimes bun 使用;对供应链敏感的环境应预置固定版本 yt-dlp 并禁用远端组件。
- 间接提示词注入面 — 视频标题/描述/字幕原样落盘并作为 sub-agent 的输入,注入文本可影响该 sub-agent 的输出(它会覆写 md 文件);建议对不可信视频的产物做人工过目。
- 自行补装依赖的授权过宽 — SKILL.md 明确要求 agent『自己想办法让 yt-dlp 可用』而非询问用户(the agent should decide how to make `yt-dlp` available and continue);在受管环境里等于放开了安装/拉取动作。
5第二遍独立确认
- [ok] InnerTube 端点与 key 现场抓取(无硬编码 key) — youtube.ts 含 INNERTUBE_URL/WATCH_URL 常量与 html.match(/"INNERTUBE_API_KEY":...) 抓取逻辑;全仓无硬编码 API key,只有会随版本过期的客户端版本号与 UA 常量。
- [ok] 三套客户端身份轮换(ANDROID/WEB/IOS)+ 对应头 — clientName "ANDROID"/"WEB"/"IOS"、X-YouTube-Client-Name / X-YouTube-Client-Version / X-Goog-Visitor-Id 头与 Pixel 8 Pro UA 均在源码中;与 SKILL.md 的『bot detected』说明一致。
- [ok] yt-dlp 回退的探测与参数 — 三个候选命令(yt-dlp / uvx / python3 -m yt_dlp),--version 探测后还要 --help 命中 --js-runtimes 与 --remote-components;执行参数为 -J --skip-download --js-runtimes bun --remote-components ejs:github(可选 --cookies-from-browser)。
- [ok] --translate 无任何外部翻译服务 — 只有 url += `&tlang=${translateTo}` 与 isTranslatable / translationLanguages 前置检查;无 Google Translate / LLM / HTTP 翻译调用,依赖面为零 npm 包。
- [ok] 零 npm 依赖、skill 内无 package.json — skill 目录仅 .ts 与 prompt 模板;全部 import 为 node 内置(fs/path/child_process/node:test/node:assert)与相对 .ts;根 package.json 的 workspaces 只覆盖 packages/*。
- [ok] 全 skill 唯一 env 读取 = YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER — 跨目录 grep process.env / env[ / YOUTUBE_ 仅命中该变量(读取一处、拼参数一处);未设置时回退路径不带 cookie,故凭证面为『闸门 + 委派』而非默认开启。
- [ok] 写盘清单与 SKILL.md 输出树一致 — 代码写入 .index.json / meta.json / transcript-raw.json / transcript-sentences.json / imgs/cover.jpg / transcript.md|.srt,与 SKILL.md 目录树逐项对齐;不落盘任何 token。
- [ok] 文档与代码的次要出入 — SKILL.md 工作流小节出现两个编号为 3 的步骤(文档笔误,不影响结论);--speakers 隐含 --chapters 的说明与代码分支一致。
6结论
dbf11afd644a4963…1567581c26