v1.1.0 to v1.1.0

41 added, 115 removed. Audit A to A.

---
name: baoyu-youtube-transcript
description: Downloads YouTube video transcripts/subtitles and cover images by URL or video ID. Supports multiple languages, translation, chapters, and speaker identification. Caches raw data for fast re-formatting. Use when user asks to "get YouTube transcript", "download subtitles", "get captions", "YouTube字幕", "YouTube封面", "视频封面", "video thumbnail", "video cover image", or provides a YouTube URL and wants the transcript/subtitle text or cover image extracted.
version: 1.1.0
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-youtube-transcript
requires:
anyBins:
- bun
- npx
---
# YouTube Transcript
- Downloads 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.
-
- Fetches video metadata and cover image on first run, caches raw data for fast re-formatting.
+ Works with manual and auto-generated transcripts. No API key or browser: uses YouTube's InnerTube API, falling back to `yt-dlp` when YouTube blocks the direct path. First run also fetches metadata and cover image and caches raw data for fast re-formatting.
## Script Directory
- Scripts 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.
+ `{baseDir}` = this SKILL.md's directory. `${BUN_X}` = `bun` if installed; else `npx -y bun` if `npx` available; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | Transcript download CLI |
## Usage
```bash
- # Default: markdown with timestamps (English)
- ${BUN_X} {baseDir}/scripts/main.ts <youtube-url-or-id>
-
- # Specify languages (priority order)
- ${BUN_X} {baseDir}/scripts/main.ts <url> --languages zh,en,ja
-
- # Without timestamps
- ${BUN_X} {baseDir}/scripts/main.ts <url> --no-timestamps
-
- # With chapter segmentation
- ${BUN_X} {baseDir}/scripts/main.ts <url> --chapters
-
- # With speaker identification (requires AI post-processing)
- ${BUN_X} {baseDir}/scripts/main.ts <url> --speakers
-
- # SRT subtitle file
+ ${BUN_X} {baseDir}/scripts/main.ts <youtube-url-or-id> # markdown with timestamps (English)
+ ${BUN_X} {baseDir}/scripts/main.ts <url> --languages zh,en,ja # language priority
+ ${BUN_X} {baseDir}/scripts/main.ts <url> --chapters --speakers
${BUN_X} {baseDir}/scripts/main.ts <url> --format srt
-
- # Translate transcript
${BUN_X} {baseDir}/scripts/main.ts <url> --translate zh-Hans
-
- # List available transcripts
${BUN_X} {baseDir}/scripts/main.ts <url> --list
-
- # Force re-fetch (ignore cache)
- ${BUN_X} {baseDir}/scripts/main.ts <url> --refresh
```
- ## Options
-
| Option | Description | Default |
|--------|-------------|---------|
| `<url-or-id>` | YouTube URL or video ID (multiple allowed) | Required |
| `--languages <codes>` | Language codes, comma-separated, in priority order | `en` |
- | `--format <fmt>` | Output format: `text`, `srt` | `text` |
- | `--translate <code>` | Translate to specified language code | |
- | `--list` | List available transcripts instead of fetching | |
- | `--timestamps` | Include `[HH:MM:SS → HH:MM:SS]` timestamps per paragraph | on |
+ | `--format <fmt>` | `text` (`.md`: frontmatter incl. `description`, title, summary, optional TOC/cover/timestamps/chapters/speakers) or `srt` | `text` |
+ | `--translate <code>` | Translate to language code | |
+ | `--list` | List available transcripts (stdout only, no file) | |
+ | `--timestamps` | `[HH:MM:SS → HH:MM:SS]` timestamps per paragraph | on |
| `--no-timestamps` | Disable timestamps | |
| `--chapters` | Chapter segmentation from video description | |
| `--speakers` | Raw transcript with metadata for speaker identification | |
| `--exclude-generated` | Skip auto-generated transcripts | |
| `--exclude-manually-created` | Skip manually created transcripts | |
- | `--refresh` | Force re-fetch, ignore cached data | |
- | `-o, --output <path>` | Save to specific file path | auto-generated |
+ | `--refresh` | Force re-fetch, ignore cache | |
+ | `-o, --output <path>` | Output file path | auto-generated |
| `--output-dir <dir>` | Base output directory | `youtube-transcript` |
- ## Optional Environment Variables
-
- | Variable | Description |
- |----------|-------------|
- | `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER` | Passed to `yt-dlp --cookies-from-browser` during fallback, e.g. `chrome`, `safari`, `firefox`, or `chrome:Profile 1` |
-
- ## Input Formats
-
- Accepts any of these as video input:
- - Full URL: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`
- - Short URL: `https://youtu.be/dQw4w9WgXcQ`
- - Embed URL: `https://www.youtube.com/embed/dQw4w9WgXcQ`
- - Shorts URL: `https://www.youtube.com/shorts/dQw4w9WgXcQ`
- - Video ID: `dQw4w9WgXcQ`
-
- ## Output Formats
+ Input: `https://www.youtube.com/watch?v=ID`, `https://youtu.be/ID`, `https://www.youtube.com/embed/ID`, `https://www.youtube.com/shorts/ID`, or bare video ID.
- | Format | Extension | Description |
- |--------|-----------|-------------|
- | `text` | `.md` | Markdown with frontmatter (incl. `description`), title heading, summary, optional TOC/cover/timestamps/chapters/speakers |
- | `srt` | `.srt` | SubRip subtitle format for video players |
+ Optional env: `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER` — passed to `yt-dlp --cookies-from-browser` during fallback (e.g. `chrome`, `safari`, `firefox`, `chrome:Profile 1`).
## Output Directory
```
youtube-transcript/
- ├── .index.json # Video ID → directory path mapping (for cache lookup)
- └── {channel-slug}/{title-full-slug}/
- ├── meta.json # Video metadata (title, channel, description, duration, chapters, etc.)
- ├── transcript-raw.json # Raw transcript snippets from YouTube API (cached)
- ├── transcript-sentences.json # Sentence-segmented transcript (split by punctuation, merged across snippets)
+ ├── .index.json # Video ID → directory path (cache lookup)
+ └── {channel-slug}/{title-full-slug}/ # channel name / full title, kebab-case
+ ├── meta.json # title, channel, description, duration, chapters, cover path, language info
+ ├── transcript-raw.json # raw API snippets `{ text, start, duration }[]`
+ ├── transcript-sentences.json # `{ text, start: "HH:mm:ss", end: "HH:mm:ss" }[]`
├── imgs/
│ └── cover.jpg # Video thumbnail
- ├── transcript.md # Markdown transcript (generated from sentences)
- └── transcript.srt # SRT subtitle (generated from raw snippets, if --format srt)
+ ├── transcript.md # from transcript-sentences.json
+ └── transcript.srt # from transcript-raw.json (--format srt)
```
- - `{channel-slug}`: Channel name in kebab-case
- - `{title-full-slug}`: Full video title in kebab-case
-
- The `--list` mode outputs to stdout only (no file saved).
-
- ## Caching
-
- On first fetch, the script saves:
- - `meta.json` — video metadata, chapters, cover image path, language info
- - `transcript-raw.json` — raw transcript snippets from YouTube API (`{ text, start, duration }[]`)
- - `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
- - `imgs/cover.jpg` — video thumbnail
-
- Subsequent 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.
+ `transcript-sentences.json` splits on sentence-ending punctuation (`.?!…。?!` etc.), merges across snippets (CJK-aware), and allocates timestamps proportionally by character length.
- When 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.
+ ## Caching & Fallback
- SRT output (`--format srt`) is generated from `transcript-raw.json`. Text/markdown output uses `transcript-sentences.json` for natural sentence boundaries.
+ - Repeat runs for the same video use the cache (no network). `--refresh` forces re-fetch; requesting a different language refreshes automatically.
+ - Any run caches `meta.json` and `imgs/cover.jpg`, so cover/metadata-only requests just need one run.
+ - On anti-bot / blocked responses the script retries alternate client identities, then `yt-dlp`. If `yt-dlp` is needed but missing, the agent decides how to make it available and continues — don't push the install decision to the user.
## Workflow
- When user provides a YouTube URL and wants the transcript:
-
- 1. Run with `--list` first if the user hasn't specified a language, to show available options
- 2. **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'`
- 3. Default: run with `--chapters --speakers` for the richest output (chapters + speaker identification)
- 3. The script auto-saves cached data + output file and prints the file path
- 4. For `--speakers` mode: after the script saves the raw file, follow the speaker identification workflow below to post-process with speaker labels
-
- When user only wants a cover image or metadata, running the script with any option will also cache `meta.json` and `imgs/cover.jpg`.
-
- When re-formatting the same video (e.g., first text then SRT), the cached data is reused — no re-fetch needed.
-
- ## Chapter & Speaker Workflow
-
- ### Chapters (`--chapters`)
-
- The 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.
+ 1. No language specified → run `--list` first to show options
+ 2. **Always single-quote the URL** (zsh globs `?` → "no matches found"): `'https://www.youtube.com/watch?v=ID'`
+ 3. Default: `--chapters --speakers` for the richest output
+ 4. The script saves cache + output and prints the file path
+ 5. `--speakers` → post-process per **Speaker Identification** below
- If no chapter timestamps exist in the description, the transcript is output as grouped paragraphs without chapter headings.
+ ## Chapters (`--chapters`)
- ### Speaker Identification (`--speakers`)
+ Parses chapter timestamps from the description (e.g., `0:00 Introduction`), segments by chapter, groups snippets into paragraphs, saves `.md` with a Table of Contents. No further processing. No chapter timestamps → grouped paragraphs without chapter headings.
- Speaker identification requires AI processing. The script outputs a raw `.md` file containing:
- - YAML frontmatter with video metadata (title, channel, date, cover, description, language)
- - Video description (for speaker name extraction)
- - Chapter list from description (if available)
- - Raw transcript in SRT format (pre-computed start/end timestamps, token-efficient)
+ ## Speaker Identification (`--speakers`)
- After the script saves the raw file, spawn a sub-agent (use a cheaper model like Sonnet for cost efficiency) to process speaker identification:
+ Implies `--chapters`. The script saves a raw `.md` with YAML frontmatter (title, channel, date, cover, description, language), the video description, the description's chapter list (if any), and the transcript in SRT format.
- 1. Read the saved `.md` file
- 2. Read the prompt template at `{baseDir}/prompts/speaker-transcript.md`
- 3. Process the raw transcript following the prompt:
- - Identify speakers using video metadata (title → guest, channel → host, description → names)
- - Detect speaker turns from conversation flow, question-answer patterns, and contextual cues
- - Segment into chapters (use description chapters if available, else create from topic shifts)
- - Format with `**Speaker Name:**` labels, paragraph grouping (2-4 sentences), and `[HH:MM:SS → HH:MM:SS]` timestamps
- 4. Overwrite the `.md` file with the processed transcript (keep the YAML frontmatter)
+ Spawn a sub-agent (a cheaper model like Sonnet is fine) to:
- When `--speakers` is used, `--chapters` is implied — the processed output always includes chapter segmentation.
+ 1. Read the saved `.md` and `{baseDir}/prompts/speaker-transcript.md`
+ 2. Process the transcript per the prompt: identify speakers from metadata (title → guest, channel → host, description → names) and conversation cues; segment into chapters (description chapters, else topic shifts); format with `**Speaker Name:**` labels, 2-4 sentence paragraphs, `[HH:MM:SS → HH:MM:SS]` timestamps
+ 3. Overwrite the `.md` with the result, keeping the YAML frontmatter
## Error Cases
| Error | Meaning |
|-------|---------|
| Transcripts disabled | Video has no captions at all |
| No transcript found | Requested language not available |
- | Video unavailable | Video deleted, private, or region-locked |
+ | Video unavailable | Deleted, private, or region-locked |
| IP blocked | Too many requests, try again later |
- | Age restricted | Video requires login for age verification |
- | 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) |
+ | Age restricted | Login required for age verification |
+ | bot detected | Script retries alternate clients, then `yt-dlp`; resolve missing fallback tooling yourself; if it still fails try `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER=safari` (or your browser) |