Imported from xy3/sumcap (
AGENTS.md). Install upstream withnpx skills add xy3/sumcap. Copyright stays with the author.
AGENTS.md — sumcap
What it is
A single-file Go CLI (main.go) that downloads YouTube captions via the undocumented InnerTube API, sends them to an AI provider for summarization, and caches the result to disk.
Supports four providers: DeepSeek, OpenAI (ChatGPT), Google Gemini, and Anthropic Claude.
Install / essential commands
| Action | Command |
|---|---|
| Install (users) | go install github.com/xy3/sumcap@latest |
| Build (local) | go build -o sumcap |
| Run | ./sumcap <url-or-video-id> |
| Run (dev) | go run . <url-or-video-id> |
| Force re-summarize | ./sumcap -f <url> |
| Word-limit summary | ./sumcap -l 100 <url> |
| Pick provider | ./sumcap -p claude <url> |
Requires at least one of DEEPSEEK_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, ANTHROPIC_API_KEY and Go 1.25+. No external dependencies, no semver tags, no releases — @latest picks the tip of main.
Architecture & data flow
URL/ID
→ extractVideoID() — regex parse (watch, youtu.be, embed, shorts, or bare 11-char ID)
→ detectProvider() — auto-detect from env vars (or explicit -p flag)
→ cacheFilePath() — check ~/.cache/sumcap/<videoID>[-<provider>].md (skip if exists, unless -f)
→ getPageHTML() — HTTP GET youtube.com/watch?v=ID
→ extractAPIKey() — regex for INNERTUBE_API_KEY from page HTML
→ getTranscriptURL() — POST to youtubei/v1/player (InnerTube API), traverse nested JSON for caption tracks
→ downloadTranscript() — GET the transcript XML (srv3 fmt stripped)
→ buildText() — XML → concatenated plain text (space-separated, HTML entities decoded)
→ summarize() — dispatch to provider-specific function
→ cache result to ~/.cache/sumcap/ then print to stdout
Key gotchas & non-obvious patterns
-
Two different HTTP clients: YouTube requests use
httpClient(a package-level var with a cookie jar), but all AI provider API calls usehttp.DefaultClientdirectly. If API calls start failing, check whether proxy/timeout settings apply uniformly. -
Fragile flag parsing: The
argsloop usesbreakin thedefaultcase, then has a second manual check after the loop. This works but is brittle. Adding new flags requires updating both the switch cases and theifcondition. If flags seem ignored, this is the likely cause. -
Provider auto-detection order: DeepSeek → OpenAI → Gemini → Claude. The first env var found wins. To force a provider regardless of env, use
-p. -
ASR (auto-generated) captions are deprioritized: The code prefers non-ASR English tracks. Only falls back to ASR English if no manual English track exists. If no English at all, it takes the first track in any language.
-
Caption track priority order: Manual English → ASR English → first available (any language).
-
Max caption length: Truncated at 80,000 characters before sending to AI (
maxTextLen). The truncation is a simple slice — no boundary-awareness. -
Go 1.25+: The module uses
go 1.25.0. This is a very recent Go version. Ensure the toolchain is available. -
Cache keying: Cache filename includes the word limit (e.g.,
dQw4w9WgXcQ-100.md) and the provider name for non-DeepSeek providers (e.g.,dQw4w9WgXcQ-claude.md). The-fflag skips cache reads. No TTL/invalidation mechanism. -
Hardcoded InnerTube client version: Uses
ClientName: "ANDROID",ClientVersion: "20.10.38". YouTube may block this version at any time — the first thing to debug when caption fetching fails. -
No tests: Zero test files exist. Any changes should ideally be verified manually.
Module path
github.com/xy3/sumcap (note: xy3 org, not a personal namespace).
Provider details
Each provider has its own request/response format:
| Provider | Env var | Format | Auth mechanism |
|---|---|---|---|
| DeepSeek | DEEPSEEK_API_KEY |
OpenAI-compatible | Authorization: Bearer header |
| OpenAI | OPENAI_API_KEY |
OpenAI-compatible | Authorization: Bearer header |
| Gemini | GEMINI_API_KEY |
Gemini-native | API key in query string |
| Claude | ANTHROPIC_API_KEY |
Claude-native | x-api-key header + anthropic-version header |
Caption track selection logic
getTranscriptURL() iterates over captionTracks[], looking for languageCode == "en" where kind does not contain "asr". Falls back to English ASR, then to track index 0. The fmt=srv3 suffix is stripped from the base URL.