用一个 API 搭建 YouTube 转录管道 | SandBase
四次调用,从频道名走到字幕轨道:解析频道、列出视频、读取视频信息、发现字幕——一把 SandBase key,不用 OAuth。

把 YouTube 内容喂给 LLM——做摘要、语义搜索或建数据集——归根到底是一个问题:怎么从”我关心的一个频道”走到某个视频的字幕轨道,而且不靠爬虫?这篇教程用 SandBase YouTube API 把这条精确的四次调用路径从头到尾走一遍:它带你从一个频道走到某个视频可用的字幕轨道,然后你再请求想要的那条轨道。仍需一把 SandBase key,但不用 Google Cloud 项目或 OAuth。端点参考只保证响应信封(id、status、model、outputs[0].data);下面的业务字段名是示例结构,不是保证的 schema,请以真实响应为准核对。
如果你想先看完整的端点全景,从 YouTube 公开数据 API hub 开始。这一篇是落地的管道。
先说结论
- 四次调用:
channel-id→channel-videos→video-info→video-captions。- 每次都是
POST /v1/api/youtube/<path>,一把SANDBASE_API_KEY;响应共用{ id, status, model, outputs }信封。- 把
channel_id和每个video_id作为稳定的键在各步之间往下传。- 只是公开、只读的字幕;仍需一把 SandBase key,但你这边没有 Google Cloud 项目或 OAuth。
管道全貌
| 步骤 | 端点 | 输入 | 你拿到 |
|---|---|---|---|
| 1. 解析频道 | youtube/web/channel-id | channel_name | 稳定的 channel_id |
| 2. 列出视频 | youtube/web-v2/channel-videos | channel_id | 近期视频 + continuation_token |
| 3. 读取视频 | youtube/web-v2/video-info | video_id | 标题、作者、分类、字幕轨道 |
| 4. 拉取字幕 | youtube/web-v2/video-captions | video_id | 可用的字幕语言 |
端点的 API 参考是每个参数名和响应路径的权威来源。
第 1 步 —— 解析频道
你通常从一个给人看的名称开始,而不是 id。把它一次性转成稳定的 channel_id,之后反复复用:
import os
import requests
BASE = "https://api.sandbase.ai/v1/api/youtube"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call(path: str, payload: dict) -> dict:
resp = requests.post(f"{BASE}/{path}", headers=HEADERS, json=payload, timeout=120)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "completed":
raise RuntimeError(body.get("error", {}).get("message", "request did not complete"))
return body["outputs"][0]["data"]
channel = call("web/channel-id", {"channel_name": "NASA"})
# 用 .get() 防御式访问——channel_id 这类业务字段是示例结构,不是保证的 schema
channel_id = channel.get("channel_id")
print(channel_id) # 例如一个频道 id 字符串
这个 call 帮助函数在碰 outputs 之前先判 status——下面每一步都复用它。只有外层信封是保证的;业务字段请防御式读取。
第 2 步 —— 列出频道的视频
把 channel_id 传进去列出近期上传。响应带一个 videos 数组和一个用于翻页的 continuation_token:
listing = call("web-v2/channel-videos", {"channel_id": channel_id})
# 用 .get() 防御式访问——videos 数组及其字段是示例结构,不是保证的 schema
videos = listing.get("videos", [])
first = videos[0] if videos else {}
print(first.get("video_id"), "-", first.get("title"))
listing 可能带一个 videos 数组,含 video_id、title、view_count、published_time、duration 等字段;把这些字段名当作示例结构,以一份真实响应为准核对。当某个端点接受可选的 continuation_token 请求参数时,把它传回去取下一页。
第 3 步 —— 读取视频的元数据
拉字幕之前,先读一下视频,确认它有字幕轨道,并把元数据记进你的记录:
info = call("web-v2/video-info", {"video_id": first.get("video_id")})
# 用 .get() 防御式访问——这些业务字段是示例结构,不是保证的 schema
print(info.get("title"), "|", info.get("author"), "|", info.get("category"))
print(info.get("view_count"), "views,", info.get("length_seconds"), "seconds")
captions 字段可能列出该视频可用的字幕轨道,每条带类似 base_url 的字段;把这些字段名当作示例结构,以一份真实响应为准核对。当它存在时,就是第 4 步会返回东西的信号。
video-info 在你请求某条字幕轨道之前先确认字幕是否可用。
第 4 步 —— 拉取字幕
最后,请求该视频的字幕轨道:
result = call("web-v2/video-captions", {"video_id": first.get("video_id")})
# 用 .get() 防御式访问——captions 列表及其字段是示例结构,不是保证的 schema
tracks = result.get("captions", [])
languages = [t.get("language_code") for t in tracks]
print(languages[:6])
不带语言调用 video-captions 通常会返回一个 captions 列表,是可用的语言轨道(每条带类似 language_code 和 language_name 的字段);把这些字段名当作示例结构,以一份真实响应为准核对。这一步是发现轨道——它本身不是字幕文本。选你需要的轨道——通常是 en——用那个 language_code 请求它,取回该轨道的字幕内容。下面是一个示例响应结构;请把其中数值当作示例,以一份真实响应为准核对具体字段:
{
"id": "5858627b-350e-47fc-8888-b81a70bf9628",
"status": "completed",
"model": "youtube/web-v2/video-captions",
"outputs": [
{
"data": {
"video_id": "IwZVXmQdX1E",
"captions": [
{ "language_code": "en", "language_name": "English" },
{ "language_code": "ar", "language_name": "Arabic" }
]
}
}
]
}
video-captions 列出该视频可用的语言轨道。
把它串起来
完整的循环——解析一次,然后遍历视频——长这样:
channel_id = call("web/channel-id", {"channel_name": "NASA"}).get("channel_id")
listing = call("web-v2/channel-videos", {"channel_id": channel_id})
# 全程用 .get() 防御式访问——业务字段是示例结构,不是保证的 schema
for video in listing.get("videos", [])[:10]:
info = call("web-v2/video-info", {"video_id": video.get("video_id")})
if not info.get("captions"):
continue # 跳过没有字幕轨道的视频
caps = call("web-v2/video-captions", {"video_id": video.get("video_id")})
english = [t for t in caps.get("captions", []) if t.get("language_code") == "en"]
if english:
# 把英文轨道交给你的转录/subtitle 拉取和索引步骤
index_transcript(video.get("title"), video.get("video_id"))
因为每次调用共用同一个信封和同一个 call 帮助函数,加重试或速率退避是一处改动的事。要跨整个频道做更高量级的采集,先按线上 YouTube API 列表确认有哪些可用端点,接入前对照参考确认它的参数。
处理粗糙的边角
- 跳过没有字幕的视频。 请求
video-captions之前先检查info.get("captions");不是每个上传都有轨道。 - 有意识地翻页。
channel-videos可能返回一个continuation_token;把它作为请求参数传回去取更早的上传,别假设一页就是整个频道。 - 对
status分支。failed或timeout的请求带error而没有outputs。call帮助函数已经强制这一点。 - 尊重速率限制。 遇到 HTTP 429 用退避。
- 读 schema。 字段名和嵌套会随端点版本不同——先看一份真实响应、对准路径,再动手。
为什么这比自己写爬虫强
你当然可以用无头浏览器或下载工具拼出这套东西,但维护成本涨得很快。爬虫会在 YouTube 改标记时崩掉,需要轮换代理来躲封锁,还逼着你为每个字段去解析 HTML。这条管道每次调用都给你带命名字段(video_id、title、captions)的结构化 JSON,所以你的代码依赖的是一份稳定契约,而不是页面布局。鉴权是一把 API key,而不是一个带每日配额的 Google Cloud 项目;而且因为四次调用共用一个信封,重试、日志和错误处理都集中在一个帮助函数里。当你超出逐视频调用的规模时,同样的标识符可以带进线上列表确认为可用的端点,读取逻辑不用重写。
代价是你通过一个统一层去读公开数据,而不是从头到尾自己掌控抓取。对大多数摘要、搜索和数据集流程来说,这恰恰是你想要的取舍:少搭管道,多花时间在真正的模型工作上。
常见问题
需要 Google 账号或 OAuth 吗?
不需要。你用 SANDBASE_API_KEY 向 SandBase 鉴权。这条管道读的是公开、只读的字幕,你这边不用 Google Cloud 项目、不用 YouTube Data API 配额,也不用 OAuth。
频道的视频怎么翻页?
channel-videos 返回一个 continuation_token;下一次调用时把它作为请求参数带回去,取更早的上传。有意识地翻页,别假设第一页就是整个频道。
video-captions 会返回字幕文本吗?
不会。不带语言调用它是在发现可用轨道——一个 captions 列表,每条带 language_code/language_name。选你需要的轨道(通常是 en),用那个 language_code 请求它,取回该轨道的字幕内容。
下一步
你现在有了一条可复用的”频道到转录”管道,建立在四次公开、只读的调用上。从这里出发,你可以为语义搜索索引转录、用 LLM 做摘要,或者建数据集。