diff options
| author | Yuval Adam <_@yuv.al> | 2026-03-18 15:47:15 +0100 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2026-03-18 15:51:12 +0100 |
| commit | 899394074c53b49987688987da31fd9a7841d1f7 (patch) | |
| tree | fd71deca63863c11902ba69d931463c97432f264 /youtube-transcript | |
Add youtube-transcript
Diffstat (limited to 'youtube-transcript')
| -rw-r--r-- | youtube-transcript/SKILL.md | 32 | ||||
| -rwxr-xr-x | youtube-transcript/yt_transcript.py | 109 |
2 files changed, 141 insertions, 0 deletions
diff --git a/youtube-transcript/SKILL.md b/youtube-transcript/SKILL.md new file mode 100644 index 0000000..e8625ef --- /dev/null +++ b/youtube-transcript/SKILL.md @@ -0,0 +1,32 @@ +--- +name: youtube-transcript +description: Fetch and save YouTube video transcripts as clean plain text. Use when the user provides a YouTube URL or wants to extract a transcript from a podcast, interview, or talk. +--- + +# YouTube Transcript + +Extracts clean, deduplicated plain-text transcripts from YouTube videos using `yt-dlp`. + +## Dependencies + +- `yt-dlp` (must be installed and on PATH) +- Python 3.10+ + +## Usage + +Fetch a transcript and save it to a file: + +```bash +python3 .pi/skills/youtube-transcript/yt_transcript.py "YOUTUBE_URL" -o transcript.txt +``` + +Options: +- `-l LANG` — subtitle language code (default: `en`) +- `-o FILE` — output file path (default: stdout) + +## Workflow + +When the user gives a YouTube URL: + +1. **Fetch transcript** — Run the script, optionally with `-o <path>` to save to a file. +2. **Review** — Read through the transcript and summarise the key points if the user asks. diff --git a/youtube-transcript/yt_transcript.py b/youtube-transcript/yt_transcript.py new file mode 100755 index 0000000..3215003 --- /dev/null +++ b/youtube-transcript/yt_transcript.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Extract clean plain-text transcripts from YouTube videos using yt-dlp.""" + +import argparse +from html import unescape +import re +import subprocess +import sys +import tempfile +from pathlib import Path + + +def download_subs(url: str, lang: str = "en") -> str: + """Download auto-generated subtitles and return the VTT content.""" + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "sub" + subprocess.run( + [ + "yt-dlp", + "--write-auto-sub", + "--write-sub", + "--skip-download", + "--sub-lang", lang, + "-o", str(out), + url, + ], + check=True, + capture_output=True, + text=True, + ) + + # prefer manual subs over auto-generated + for suffix in [f".{lang}.vtt", f".{lang}.vtt"]: + vtt = Path(f"{out}{suffix}") + if vtt.exists(): + return vtt.read_text(encoding="utf-8") + + # fallback: grab whatever vtt file was written + vtts = list(Path(tmp).glob("*.vtt")) + if vtts: + return vtts[0].read_text(encoding="utf-8") + + raise FileNotFoundError("No subtitles found. Try --list-subs on the video.") + + +def parse_vtt(vtt_text: str) -> str: + """Parse VTT content into clean deduplicated plain text.""" + lines: list[str] = [] + prev = "" + + for line in vtt_text.splitlines(): + # skip VTT header, metadata, timestamps, blank lines + if re.match(r"^(WEBVTT|Kind:|Language:|\s*$)", line): + continue + if "-->" in line: + continue + + # strip HTML-style tags (<c>, <b>, etc.) and formatting + clean = re.sub(r"<[^>]+>", "", line) + # decode HTML entities (> & < etc.) + clean = unescape(clean) + # normalize whitespace + clean = re.sub(r"\s+", " ", clean).strip() + + if not clean or clean == prev: + continue + + # handle overlapping cues: skip if current line is a suffix of prev + # or prev is a suffix of current (common in auto-subs) + if prev and (prev.endswith(clean) or clean.startswith(prev)): + # replace prev with the longer version + if clean.startswith(prev) and clean != prev: + lines[-1] = clean + prev = clean + continue + + lines.append(clean) + prev = clean + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Extract YouTube transcript as plain text") + parser.add_argument("url", help="YouTube video URL") + parser.add_argument("-l", "--lang", default="en", help="Subtitle language code (default: en)") + parser.add_argument("-o", "--output", help="Output file (default: stdout)") + args = parser.parse_args() + + try: + vtt = download_subs(args.url, args.lang) + except subprocess.CalledProcessError as e: + print(f"yt-dlp error: {e.stderr}", file=sys.stderr) + sys.exit(1) + except FileNotFoundError as e: + print(str(e), file=sys.stderr) + sys.exit(1) + + transcript = parse_vtt(vtt) + + if args.output: + Path(args.output).write_text(transcript, encoding="utf-8") + print(f"Saved to {args.output}", file=sys.stderr) + else: + print(transcript) + + +if __name__ == "__main__": + main() |
