tenbrains · design spec
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.
Mirrors src/x/client.ts (the tweet oEmbed path): an undocumented-but-free endpoint, pure parsers split from the network layer, structured errors.
ytInitialPlayerResponsefetch + 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).
analyzeNo 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.
| Field | Source |
|---|---|
post.text | flattened transcript |
post.externalId | yt:<videoId> — distinct prefix, dedupes re-runs, no tweet-id collision |
post.url | canonical watch?v=<id> |
post.authorUsername | channel |
post.postedAt | upload date |
post.raw | video 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.
--summarize & --learnTwo composable follow-on actions run on the same fetched-and-analysed content in one call — pass either, both, or neither. General analyze options, but they earn their keep on long-form transcripts.
{ summary, keyPoints[] }), distinct from the terse analysis summary field.summarizeContent() + prompt + schema + mock — offline-testable like the rest.data.summary; meta.summarized = true; persisted in raw_json.--minutes, --ratings apply).learn today / learn done.Order in one invocation: fetch → analyse → (summarize) → (learn), all persisted against the same post.
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.
| Condition | Code | Exit |
|---|---|---|
| Not found / deleted / no caption tracks | NOT_FOUND | 3 |
| Private / age-restricted / region-blocked | PROVIDER_UNAUTHORIZED | 5 |
| Rate limited | PROVIDER_RATE_LIMITED | 5 |
| Network / timeout | PROVIDER_NETWORK | 5 |
| Markup changed, can't parse | PROVIDER_BAD_OUTPUT | 5 |
posts + analyses; metadata + summary in raw_json — no schema change (stays v3).--summarize condenses first when set.--transcript, one live smoke — the same shape as the shipped thread feature.| Risk | Mitigation |
|---|---|
| YouTube changes watch-page markup | Pure, fixture-tested parsers → one-line fix. Same risk class as existing oEmbed HTML parsing. |
| Video genuinely has no captions | Structured NOT_FOUND → --transcript @file. Audio transcription deferred. |
| ToS / gray-area scraping | Same posture as tweet oEmbed + archive paths — public captions, personal research; noted in docs. |
| Token cost on long transcripts | Soft warning in v1; --summarize condenses first (§4). |
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.--transcript @file as the manual fallback — defer audio/Whisper transcription entirely?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.