{"id":"baoyu-youtube-transcript","name":"baoyu-youtube-transcript","summary":"YouTubeの動画トランスクリプト/字幕およびカバー画像をURLまたはビデオIDでダウンロードできます。","body":"# YouTube Transcript\n\nDownloads transcripts (subtitles/captions) from YouTube videos. Works with both manually created and auto-generated transcripts. No API key or browser required — uses YouTube's InnerTube API directly and automatically falls back to `yt-dlp` when YouTube blocks the direct API path.\n\nFetches video metadata and cover image on first run, caches raw data for fast re-formatting.\n\n## Script Directory\n\nScripts in `scripts/` subdirectory. `{baseDir}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.\n\n| Script | Purpose |\n|--------|---------|\n| `scripts/main.ts` | Transcript download CLI |\n\n## Usage\n\n```bash\n# Default: markdown with timestamps (English)\n${BUN_X} {baseDir}/scripts/main.ts <youtube-url-or-id>\n\n# Specify languages (priority order)\n${BUN_X} {baseDir}/scripts/main.ts <url> --languages zh,en,ja\n\n# Without timestamps\n${BUN_X} {baseDir}/scripts/main.ts <url> --no-timestamps\n\n# With chapter segmentation\n${BUN_X} {baseDir}/scripts/main.ts <url> --chapters\n\n# With speaker identification (requires AI post-processing)\n${BUN_X} {baseDir}/scripts/main.ts <url> --speakers\n\n# SRT subtitle file\n${BUN_X} {baseDir}/scripts/main.ts <url> --format srt\n\n# Translate transcript\n${BUN_X} {baseDir}/scripts/main.ts <url> --translate zh-Hans\n\n# List available transcripts\n${BUN_X} {baseDir}/scripts/main.ts <url> --list\n\n# Force re-fetch (ignore cache)\n${BUN_X} {baseDir}/scripts/main.ts <url> --refresh\n```\n\n## Options\n\n| Option | Description | Default |\n|--------|-------------|---------|\n| `<url-or-id>` | YouTube URL or video ID (multiple allowed) | Required |\n| `--languages <codes>` | Language codes, comma-separated, in priority order | `en` |\n| `--format <fmt>` | Output format: `text`, `srt` | `text` |\n| `--translate <code>` | Translate to specified language code | |\n| `--list` | List available transcripts instead of fetching | |\n| `--timestamps` | Include `[HH:MM:SS → HH:MM:SS]` timestamps per paragraph | on |\n| `--no-timestamps` | Disable timestamps | |\n| `--chapters` | Chapter segmentation from video description | |\n| `--speakers` | Raw transcript with metadata for speaker identification | |\n| `--exclude-generated` | Skip auto-generated transcripts | |\n| `--exclude-manually-created` | Skip manually created transcripts | |\n| `--refresh` | Force re-fetch, ignore cached data | |\n| `-o, --output <path>` | Save to specific file path | auto-generated |\n| `--output-dir <dir>` | Base output directory | `youtube-transcript` |\n\n## Optional Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER` | Passed to `yt-dlp --cookies-from-browser` during fallback, e.g. `chrome`, `safari`, `firefox`, or `chrome:Profile 1` |\n\n## Input Formats\n\nAccepts any of these as video input:\n- Full URL: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`\n- Short URL: `https://youtu.be/dQw4w9WgXcQ`\n- Embed URL: `https://www.youtube.com/embed/dQw4w9WgXcQ`\n- Shorts URL: `https://www.youtube.com/shorts/dQw4w9WgXcQ`\n- Video ID: `dQw4w9WgXcQ`\n\n## Output Formats\n\n| Format | Extension | Description |\n|--------|-----------|-------------|\n| `text` | `.md` | Markdown with frontmatter (incl. `description`), title heading, summary, optional TOC/cover/timestamps/chapters/speakers |\n| `srt` | `.srt` | SubRip subtitle format for video players |\n\n## Output Directory\n\n```\nyoutube-transcript/\n├── .index.json                          # Video ID → directory path mapping (for cache lookup)\n└── {channel-slug}/{title-full-slug}/\n    ├── meta.json                        # Video metadata (title, channel, description, duration, chapters, etc.)\n    ├── transcript-raw.json              # Raw transcript snippets from YouTube API (cached)\n    ├── transcript-sentences.json        # Sentence-segmented transcript (split by punctuation, merged across snippets)\n    ├── imgs/\n    │   └── cover.jpg                    # Video thumbnail\n    ├── transcript.md                    # Markdown transcript (generated from sentences)\n    └── transcript.srt                   # SRT subtitle (generated from raw snippets, if --format srt)\n```\n\n- `{channel-slug}`: Channel name in kebab-case\n- `{title-full-slug}`: Full video title in kebab-case\n\nThe `--list` mode outputs to stdout only (no file saved).\n\n## Caching\n\nOn first fetch, the script saves:\n- `meta.json` — video metadata, chapters, cover image path, language info\n- `transcript-raw.json` — raw transcript snippets from YouTube API (`{ text, start, duration }[]`)\n- `transcript-sentences.json` — sentence-segmented transcript (`{ text, start: \"HH:mm:ss\", end: \"HH:mm:ss\" }[]`), split by sentence-ending punctuation (`.?!…。？！` etc.), timestamps proportionally allocated by character length, CJK-aware text merging\n- `imgs/cover.jpg` — video thumbnail\n\nSubsequent runs for the same video use cached data (no network calls). Use `--refresh` to force re-fetch. If a different language is requested, the cache is automatically refreshed.\n\nWhen YouTube returns anti-bot / blocked responses on the direct InnerTube path, the script retries with alternate client identities and then falls back to `yt-dlp` if available. If fallback is needed but `yt-dlp` is unavailable, the agent should decide how to make `yt-dlp` available and continue rather than pushing the installation decision to the user.\n\nSRT output (`--format srt`) is generated from `transcript-raw.json`. Text/markdown output uses `transcript-sentences.json` for natural sentence boundaries.\n\n## Workflow\n\nWhen user provides a YouTube URL and wants the transcript:\n\n1. Run with `--list` first if the user hasn't specified a language, to show available options\n2. **Always single-quote the URL** when running the script — zsh treats `?` as a glob wildcard, so an unquoted YouTube URL causes \"no matches found\": use `'https://www.youtube.com/watch?v=ID'`\n3. Default: run with `--chapters --speakers` for the richest output (chapters + speaker identification)\n3. The script auto-saves cached data + output file and prints the file path\n4. For `--speakers` mode: after the script saves the raw file, follow the speaker identification workflow below to post-process with speaker labels\n\nWhen user only wants a cover image or metadata, running the script with any option will also cache `meta.json` and `imgs/cover.jpg`.\n\nWhen re-formatting the same video (e.g., first text then SRT), the cached data is reused — no re-fetch needed.\n\n## Chapter & Speaker Workflow\n\n### Chapters (`--chapters`)\n\nThe script parses chapter timestamps from the video description (e.g., `0:00 Introduction`), segments the transcript by chapter boundaries, groups snippets into readable paragraphs, and saves as `.md` with a Table of Contents. No further processing needed.\n\nIf no chapter timestamps exist in the description, the transcript is output as grouped paragraphs without chapter headings.\n\n### Speaker Identification (`--speakers`)\n\nSpeaker identification requires AI processing. The script outputs a raw `.md` file containing:\n- YAML frontmatter with video metadata (title, channel, date, cover, description, language)\n- Video description (for speaker name extraction)\n- Chapter list from description (if available)\n- Raw transcript in SRT format (pre-computed start/end timestamps, token-efficient)\n\nAfter the script saves the raw file, spawn a sub-agent (use a cheaper model like Sonnet for cost efficiency) to process speaker identification:\n\n1. Read the saved `.md` file\n2. Read the prompt template at `{baseDir}/prompts/speaker-transcript.md`\n3. Process the raw transcript following the prompt:\n   - Identify speakers using video metadata (title → guest, channel → host, description → names)\n   - Detect speaker turns from conversation flow, question-answer patterns, and contextual cues\n   - Segment into chapters (use description chapters if available, else create from topic shifts)\n   - Format with `**Speaker Name:**` labels, paragraph grouping (2-4 sentences), and `[HH:MM:SS → HH:MM:SS]` timestamps\n4. Overwrite the `.md` file with the processed transcript (keep the YAML frontmatter)\n\nWhen `--speakers` is used, `--chapters` is implied — the processed output always includes chapter segmentation.\n\n## Error Cases\n\n| Error | Meaning |\n|-------|---------|\n| Transcripts disabled | Video has no captions at all |\n| No transcript found | Requested language not available |\n| Video unavailable | Video deleted, private, or region-locked |\n| IP blocked | Too many requests, try again later |\n| Age restricted | Video requires login for age verification |\n| bot detected | The script retries alternate clients and then `yt-dlp`; if fallback tooling is missing, the agent should resolve that itself, otherwise if it still fails try `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER=safari` (or your browser) |","author":"@JimLiu","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/JimLiu/baoyu-skills/tree/main/skills/baoyu-youtube-transcript","license":"MIT","category":null,"lang":"en","tokens":2165,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"prompts/speaker-transcript.md","size":4695,"sha256":"80358550e9d9b8d700cd54fe1e95572646459ab7780539bd0bc2af5a58760123"},{"path":"scripts/main.test.ts","size":5644,"sha256":"36a3fb2598b2dd93784bd4021cea5819d870322c94f4c1952aac9af9a6ee6c3b"},{"path":"scripts/main.ts","size":8568,"sha256":"c838bb93c88702d06eef79e22e0861a256dc3996841ae26a592431236917ddc5"},{"path":"scripts/shared.ts","size":3455,"sha256":"c4fff1de796b6a5891583976415ec00b43ba2355c1e8cfe8ac951315f1b23053"},{"path":"scripts/storage.ts","size":1925,"sha256":"fe1703fd3f9e7348e2b5ba9a7669ba948877b9876aa8e92747525ed395e14312"},{"path":"scripts/transcript.ts","size":13144,"sha256":"e16b95229c96530ec30b261f2f69b847c092a04606d54fad6a32cad2800423db"},{"path":"scripts/types.ts","size":2407,"sha256":"9508ea254840a3061b66903e0642ca781146472e416b58191e07ecb3093c9e3d"},{"path":"scripts/youtube.ts","size":20270,"sha256":"0b647db74efed75f0dfa7500f2ef535b3167c479831feee8d48ba68c2f7c5236"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[{"code":"code.eval","kind":"dangerous-code","where":"scripts/transcript.ts:16","excerpt":"exec(","message":"evaluates code at runtime","severity":"warn"},{"code":"net.endpoints","kind":"exfiltration","excerpt":"consent.youtube.com, i.ytimg.com, www.youtube.com, youtu.be","message":"bundled scripts reach 4 external host(s)","severity":"warn"}],"scannedAt":"2026-08-22","hasScripts":true,"networkEndpoints":["consent.youtube.com","i.ytimg.com","www.youtube.com","youtu.be"]}}