
Video Transcript Downloader
Pull YouTube video transcripts into text files so agents and builders can research, summarize, or repurpose talk content offline.
Overview
Video Transcript Downloader is an agent skill for the Idea phase that downloads YouTube video transcripts into local text via a Node CLI.
Install
npx skills add https://github.com/steipete/agent-scripts --skill video-transcript-downloaderWhat is this skill?
- Node ESM CLI script using youtube-transcript-plus dependency
- Requires Node.js 20 or newer per package engines field
- Spawns child processes and writes transcript output to the filesystem
- Lightweight argument parser for flags and forwarded argv after --
- Private single-purpose package layout under agent-scripts
- Requires Node.js >= 20.0.0 per package engines
Adoption & trust: 650 installs on skills.sh; 4.3k GitHub stars; 1/3 security scanners passed (skills.sh audits).
What problem does it solve?
You found the answer in a long YouTube video but cannot search, cite, or pipe it into your agent without a transcript file.
Who is it for?
Quick research pulls from public YouTube URLs when you already run Node 20+ in your agent environment.
Skip if: Batch media pipelines needing official YouTube API quotas, non-YouTube platforms, or guaranteed caption availability on every video.
When should I use this skill?
You need YouTube caption text saved locally for summarization, research, or downstream agent tasks.
What do I get? / Deliverables
You obtain a text transcript on disk that you can summarize, chunk for RAG, or compare against your own notes in the next research step.
- Local transcript text output from a YouTube URL
- CLI-runnable fetch step for agent workflows
Recommended Skills
Journey fit
Transcripts support early discovery and research before you commit to a product direction or content plan. The tool fetches third-party video text for analysis, not for in-app UI or backend implementation.
How it compares
Focused transcript fetch script, not a full video editor or podcast hosting workflow.
Common Questions / FAQ
Who is video-transcript-downloader for?
Solo builders and agents who research from YouTube talks and need searchable text without manual copy-paste from captions.
When should I use video-transcript-downloader?
Use it during Idea research when validating topics, mining competitor demos, or preparing summaries before Validate scope decisions.
Is video-transcript-downloader safe to install?
It runs Node, touches the filesystem, and calls external transcript services; review the Security Audits panel on this Prism page and audit the script plus dependency before running on sensitive machines.
SKILL.md
READMESKILL.md - Video Transcript Downloader
node_modules npm-debug.log* package-lock.json.bak { "name": "video-transcript-downloader", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "video-transcript-downloader", "version": "1.0.0", "dependencies": { "youtube-transcript-plus": "^2.0.0" } }, "node_modules/youtube-transcript-plus": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/youtube-transcript-plus/-/youtube-transcript-plus-2.0.0.tgz", "integrity": "sha512-n4nnGVV3ZABnvk/sSF+W51c/+P6I23kLwwhOiYeKLeHOGfpjwCUthhoKgQk9dldJEgHCkbyJpuD3vkbBxWqDHA==", "license": "MIT", "engines": { "node": ">=20.0.0" } } } } { "name": "video-transcript-downloader", "version": "1.0.0", "private": true, "type": "module", "dependencies": { "youtube-transcript-plus": "^2.0.0" } } #!/usr/bin/env node import { spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { YoutubeTranscript } from "youtube-transcript-plus"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function die(message, code = 1) { process.stderr.write(String(message).trimEnd() + "\n"); process.exit(code); } function parseArgs(argv) { // Tiny no-deps parser. // - `--flag` => boolean // - `--key value` // - `--` => forward remaining args to yt-dlp const positional = []; const opts = {}; let i = 0; while (i < argv.length) { const a = argv[i]; if (a === "--") { opts.extra = argv.slice(i + 1); break; } if (!a.startsWith("--")) { positional.push(a); i += 1; continue; } const key = a.slice(2); const next = argv[i + 1]; const isValue = next !== undefined && !next.startsWith("--"); if (!isValue) { opts[key] = true; i += 1; continue; } if (opts[key] === undefined) opts[key] = next; else if (Array.isArray(opts[key])) opts[key].push(next); else opts[key] = [opts[key], next]; i += 2; } return { positional, opts }; } function toArray(v) { if (v === undefined) return []; if (Array.isArray(v)) return v; return [v]; } function which(cmd) { // Avoid shelling out to `which`; keep it portable + fast. const envPath = process.env.PATH || ""; const parts = envPath.split(path.delimiter); for (const p of parts) { const full = path.join(p, cmd); if (fs.existsSync(full)) return full; } return null; } function resolveBin(name, fallback) { return which(name) || (fallback && fs.existsSync(fallback) ? fallback : null); } function run(cmd, args, { cwd } = {}) { return new Promise((resolve) => { // Capture stdout + stderr to keep yt-dlp’s error context intact. const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] }); let out = ""; child.stdout.on("data", (d) => (out += d.toString())); child.stderr.on("data", (d) => (out += d.toString())); child.on("close", (code) => resolve({ code, out })); }); } function isYouTubeUrl(url) { return /(^https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\//i.test(url); } function extractYouTubeId(input) { if (!input) return null; const raw = String(input).trim(); if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) return raw; const m = raw.match(/(?:v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); return m ? m[1] : null; } function decodeHtmlEntities(input) { if (!input) return input; // Some transcripts come back double-encoded (e.g. "&#39;"). // Decode up to 2 passes; stop once stable. let text = input; for (let i = 0; i < 2; i++) { const decoded = text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))) .replace