Add youtube transcript skill
This commit is contained in:
18
flake.nix
18
flake.nix
@@ -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
|
||||
|
||||
45
skills/youtube-transcript/SKILL.md
Normal file
45
skills/youtube-transcript/SKILL.md
Normal 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.
|
||||
9
skills/youtube-transcript/fetch_transcript
Executable file
9
skills/youtube-transcript/fetch_transcript
Executable 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" "$@"
|
||||
139
skills/youtube-transcript/fetch_transcript.py
Executable file
139
skills/youtube-transcript/fetch_transcript.py
Executable 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()
|
||||
Reference in New Issue
Block a user