tenbrains · implementation report
tenbrains analyze now accepts a YouTube URL, fetches public captions for free, and runs the transcript through the existing research pipeline — including narrative summaries, persistence, learning tracks, search, and digest.
youtu.be URLs route through the existing analyze command.--summarize produces a narrative digest with key points; --learn builds the existing 7-day Feynman track. The flags compose in one call.posts.raw_json; analyses and tracks use the existing tables.--transcript <text|@file|->.The implementation in src/youtube/client.ts mirrors src/x/client.ts: dependency-free network orchestration around pure parsers, with structured errors at every boundary.
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).
YouTube's WEB timed-text URL can return HTTP 200 with an empty body. When that happens, tenbrains requests an equivalent caption track through the embedded Android player API and feeds it into the same pure timed-text parser. The Android client version is isolated and documented as a maintenance surface.
analyzeNo parallel command was added. analyze detects a YouTube URL in --url and routes to the transcript fetcher, exactly as --thread routes differently from a plain tweet.
| 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 now receives kind: "tweet" | "transcript" and uses "You are analyzing a video transcript." for video content. The analysis output schema remains unchanged.
--summarize & --learnTwo composable companion actions run on the same fetched content in one call — pass either, both, or neither. They are general analyze options and especially useful for 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.When requested, summarization is the condense-first pass: fetch → summarize → analyse the digest → learn. All outcomes persist against the same post.
The implementation reuses the existing PROVIDER_* scaffolding. Every "no captions" path points at the manual escape hatch analyze --transcript @file, which also keeps the complete workflow testable offline.
| Condition | Code | Exit |
|---|---|---|
| Not found / deleted / no caption tracks | NOT_FOUND | 3 |
Private / members-only / age-gated (LOGIN_REQUIRED) / 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 --summarize --learn command test, and a live caption smoke.tenbrains@2.2.0, and both required GitHub checks were green.| Risk | Mitigation |
|---|---|
| YouTube changes watch-page markup | Pure, fixture-tested parsers → one-line fix. Same risk class as existing oEmbed HTML parsing. |
| Android player client version expires | One isolated version marker and documented fallback path; update it when the endpoint stops returning tracks. |
| 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). |
analyzeshipped rather than adding a separate YouTube command.--transcript @file|- as the manual fallback. Audio and Whisper remain deferred.--lang es.Implemented in PR #8 and merged to main with signed-off commits. DCO and the full typecheck, lint, test, and build workflow passed before merge.