Add youtube transcript skill #22

Merged
alexion merged 2 commits from youtube-transcript-skill into main 2026-08-26 21:35:50 -04:00
4 changed files with 210 additions and 1 deletions
Showing only changes of commit 85eefb191b - Show all commits

View File

@@ -32,7 +32,23 @@
nixpkgs.lib.listToAttrs (
map (skill: {
inherit (skill) name;
value = mkSkill { inherit pkgs; inherit (skill) name src; };
value = mkSkill (
{
inherit pkgs;
inherit (skill) name src;
}
// nixpkgs.lib.optionalAttrs (skill.name == "youtube-transcript") {
commands = [
{
path = "fetch_transcript";
env = {
PYTHON = "${pkgs.python3}/bin/python3";
YT_DLP = "${pkgs.yt-dlp}/bin/yt-dlp";
};
}
];
}
);
}) discoveredSkills
);
in

View File

@@ -0,0 +1,45 @@
---
name: youtube-transcript
description: Fetch the transcript and title of a YouTube video as JSON. Use when the user provides a YouTube URL and needs spoken captions for analysis, summarization, quoting, or search.
---
# YouTube Transcript
Fetch a YouTube video's title and full transcript with `yt-dlp` captions.
Prefer manual English subtitles.
Fall back to automatic English subtitles.
## Requirements
The Nix-packaged skill provides Python 3 and `yt-dlp`.
A source checkout requires Python 3 and `yt-dlp` on `PATH`.
## Usage
Run from this skill directory:
```bash
./fetch_transcript "<youtube_url>"
```
## Output
Prints a JSON object to stdout:
```json
{
"title": "Video title",
"transcript": "full transcript text as a single string"
}
```
Progress logs go to stderr.
On failure, the script exits non-zero and writes the error to stderr.
## Notes
- Only English captions are attempted.
- The language order is `en`, `en-US`, `en-GB`, then any `en*` caption.
- Manual captions are preferred over automatic captions.
- The transcript is plain text with timing and formatting removed.
- Videos without English captions fail.

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env sh
set -eu
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
: "${PYTHON:=python3}" # mkSkill:PYTHON
: "${YT_DLP:=yt-dlp}" # mkSkill:YT_DLP
exec "$PYTHON" "$dir/fetch_transcript.py" --yt-dlp "$YT_DLP" "$@"

View File

@@ -0,0 +1,139 @@
#!/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()