Files
skills/skills/youtube-transcript/fetch_transcript.py
2026-08-26 21:31:30 -04:00

140 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fetch a YouTube video's title and transcript through yt-dlp."""
import argparse
import glob
import json
import os
import shutil
import subprocess
import sys
import tempfile
def get_metadata(yt_dlp, url):
cmd = [
yt_dlp,
"--dump-json",
"--no-warnings",
"--skip-download",
url,
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
except subprocess.CalledProcessError as error:
print(f"Error executing yt-dlp: {error.stderr}", file=sys.stderr)
sys.exit(1)
except Exception as error:
print(f"Unexpected error: {error}", file=sys.stderr)
sys.exit(1)
def pick_english_lang(info):
"""Return (lang_code, is_auto) for the best English caption track."""
subs = info.get("subtitles") or {}
auto = info.get("automatic_captions") or {}
preferred = ["en", "en-US", "en-GB"]
for lang in preferred:
if lang in subs:
return lang, False
for lang in subs:
if lang.startswith("en"):
return lang, False
for lang in preferred:
if lang in auto:
return lang, True
for lang in auto:
if lang.startswith("en"):
return lang, True
return None
def download_subtitle(yt_dlp, url, lang, is_auto, outdir):
cmd = [
yt_dlp,
"--skip-download",
"--no-warnings",
"--sub-format",
"json3",
"--sub-langs",
lang,
"--write-auto-subs" if is_auto else "--write-subs",
"-o",
os.path.join(outdir, "%(id)s.%(ext)s"),
url,
]
try:
subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as error:
print(f"Error downloading subtitles with yt-dlp: {error.stderr}", file=sys.stderr)
sys.exit(1)
matches = glob.glob(os.path.join(outdir, "*.json3"))
if not matches:
print("yt-dlp did not produce a json3 subtitle file.", file=sys.stderr)
sys.exit(1)
return matches[0]
def extract_text_from_json3(path):
with open(path, "r", encoding="utf-8") as file:
data = json.load(file)
text_parts = []
for event in data.get("events", []):
for segment in event.get("segs", []) or []:
text = segment.get("utf8", "")
if text.strip() and text != "\n":
text_parts.append(text.strip())
return " ".join(text_parts)
def parse_args():
parser = argparse.ArgumentParser(
description="Fetch a YouTube video's title and English transcript as JSON."
)
parser.add_argument(
"url",
metavar="youtube_url",
help="YouTube URL or video ID.",
)
parser.add_argument(
"--yt-dlp",
default="yt-dlp",
help="Path to yt-dlp. Defaults to yt-dlp from PATH.",
)
return parser.parse_args()
def main():
args = parse_args()
print("Fetching video metadata...", file=sys.stderr)
info = get_metadata(args.yt_dlp, args.url)
title = info.get("title", "Unknown_Title")
pick = pick_english_lang(info)
if pick is None:
print(f"No English subtitles found for video: {title}", file=sys.stderr)
sys.exit(1)
lang, is_auto = pick
tmpdir = tempfile.mkdtemp(prefix="yt-transcript-")
try:
caption_kind = "auto captions" if is_auto else "captions"
print(f"Downloading {caption_kind} ({lang})...", file=sys.stderr)
sub_path = download_subtitle(args.yt_dlp, args.url, lang, is_auto, tmpdir)
transcript = extract_text_from_json3(sub_path)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
output = {"title": title, "transcript": transcript}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()