summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2026-03-18 15:47:15 +0100
committerYuval Adam <_@yuv.al>2026-03-18 15:51:12 +0100
commit899394074c53b49987688987da31fd9a7841d1f7 (patch)
treefd71deca63863c11902ba69d931463c97432f264
Add youtube-transcript
-rw-r--r--youtube-transcript/SKILL.md32
-rwxr-xr-xyoutube-transcript/yt_transcript.py109
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 (&gt; &amp; &lt; 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()