Blog/开发者工具/

用一个 API 搭建 YouTube 转录管道 | SandBase

四次调用,从频道名走到字幕轨道:解析频道、列出视频、读取视频信息、发现字幕——一把 SandBase key,不用 OAuth。

深色电影质感画面:一个 YouTube 频道解析成视频列表和字幕转录流,汇入 agent 核心

把 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-idchannel_name稳定的 channel_id
2. 列出视频youtube/web-v2/channel-videoschannel_id近期视频 + continuation_token
3. 读取视频youtube/web-v2/video-infovideo_id标题、作者、分类、字幕轨道
4. 拉取字幕youtube/web-v2/video-captionsvideo_id可用的字幕语言

SandBase YouTube 端点参考,展示本管道用到的频道和字幕端点 端点的 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 步会返回东西的信号。

SandBase YouTube video-info API 参考,展示 video_id 参数和响应 schema 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" }
        ]
      }
    }
  ]
}

SandBase YouTube video-captions API 参考,展示带字幕语言轨道的响应 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 做摘要,或者建数据集。