tenbrains · design spec

YouTube transcription

Let tenbrains analyze take a YouTube URL, pull the caption track for free, and run the transcript through the pipeline that already analyses tweets — same topic/summary/intent/concepts, same persistence, same --learn, search, and digest.

Draft · for team review Owner: @moeghashim Target: tenbrains 2.2.0 No new dependency No schema migration
# A transcript is just long-form content — it enters the existing pipeline
tenbrains analyze --url "https://youtube.com/watch?v=…"
# → fetch captions · analyse transcript · persist post + analysis
# meta.source = "youtube" · --learn / search / digest all work unchanged

01 Motivation

02 Goals & non-goals

Goals

  • Fetch a public transcript with no API key, no paid tier, no new dependency.
  • Reuse the whole downstream pipeline (persist → learn → search → digest → suggest).
  • Structured errors + a manual fallback — never crash.
  • Pure, fixture-tested parsers so a markup change is a one-line fix.

Non-goals (v1)

  • Audio transcription (Whisper / yt-dlp) for videos with no captions.
  • Chapter/timestamp segmentation or a transcript-viewer UI.
  • Playlist / channel bulk import.
  • Auto-translation beyond selecting an available track.

03 Fetch — free-first caption scrape

Mirrors src/x/client.ts (the tweet oEmbed path): an undocumented-but-free endpoint, pure parsers split from the network layer, structured errors.

STEP 1Watch pageGET the video's HTML
STEP 2Player responseextract ytInitialPlayerResponse
STEP 3Caption trackpick manual>ASR, lang>en>first
STEP 4Timedtextfetch baseUrl → flatten to text
No new dependency

fetch + regex/JSON parsing — the same toolkit the oEmbed path uses. Rejected: youtube-transcript (fragile, redundant), yt-dlp (external binary, breaks the self-contained stance), and the YouTube Data API captions.download (needs the owner's OAuth — the same dead end that makes X timelines a paid feature).

04 Integration — extend analyze

No parallel command. analyze detects a YouTube URL in --url and routes to the transcript fetcher, exactly as --thread routes differently from a plain tweet. Everything after ingestion runs unchanged.

FieldSource
post.textflattened transcript
post.externalIdyt:<videoId> — distinct prefix, dedupes re-runs, no tweet-id collision
post.urlcanonical watch?v=<id>
post.authorUsernamechannel
post.postedAtupload date
post.rawvideo metadata → existing posts.raw_json (no migration)
meta.source"youtube"

The analyzer prompt is tweet-tuned; pass an optional kind: "tweet" | "transcript" and swap one sentence to "You are analyzing a video transcript." Output schema untouched.

05 Errors & fallback

Reuses the existing PROVIDER_* scaffolding. Every "no captions" path points at the manual escape hatch analyze --transcript @file, which also makes the feature testable offline.

ConditionCodeExit
Not found / deleted / no caption tracksNOT_FOUND3
Private / age-restricted / region-blockedPROVIDER_UNAUTHORIZED5
Rate limitedPROVIDER_RATE_LIMITED5
Network / timeoutPROVIDER_NETWORK5
Markup changed, can't parsePROVIDER_BAD_OUTPUT5

06 Surface, storage, testing

analyze --url <youtube-url> # fetch + analyse captions
analyze --url <url> --lang es # prefer a caption language
analyze --transcript @file | - # supply a transcript manually (no network)
analyze --url <url> --learn # transcript → 7-day track (already works)

07 Risks

RiskMitigation
YouTube changes watch-page markupPure, fixture-tested parsers → one-line fix. Same risk class as existing oEmbed HTML parsing.
Video genuinely has no captionsStructured NOT_FOUND--transcript @file. Audio transcription deferred.
ToS / gray-area scrapingSame posture as tweet oEmbed + archive paths — public captions, personal research; noted in docs.
Token cost on long transcriptsSoft warning in v1; --summarize-first later.

08 Open decisions — team input

1
Fold into analyzerecommended vs. a dedicated tenbrains youtube command. Folding in reuses everything and adds no surface; a separate command is only worth it if video-specific features (chapters, timestamps, a viewer) are expected to grow.
2
Caption-only v1recommended with --transcript @file as the manual fallback — defer audio/Whisper transcription entirely?
3
Ship as a minor — 2.2.0. Purely additive; no breaking changes.
Effort

Closely matches the already-shipped thread feature: a ~200-line client module (pure parsers + orchestration), a ~40-line analyze integration, fixture tests + one offline command test + a live smoke, and docs. One focused PR through the existing branch → PR → CI (DCO + typecheck/lint/test) flow.