feat: Add YouTube subtitle generation script
This commit is contained in:
1 parent
e59f2d4825
commit
5ecff074af
1 file changed
+86
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["yt-dlp"]
|
||||||
|
# ///
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
VIDEO_TO_SRT = Path(__file__).resolve().with_name("video_to_srt.py")
|
||||||
|
MAX_FILENAME_BYTES = 240
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> tuple[argparse.Namespace, list[str]]:
|
||||||
|
"""Parse command-line arguments; unknown options are returned to be forwarded to video_to_srt.py."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Download a YouTube video's audio and transcribe it with video_to_srt.py. "
|
||||||
|
"Options not listed here (--model, --speed-up, --language, --chunk-minutes, --api-key, --no-diarize, ...) are passed through.",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument("video", help="YouTube URL or video id")
|
||||||
|
parser.add_argument("--output-dir", "-d", type=Path, default=Path.cwd(), help="Directory for the SRT file")
|
||||||
|
return parser.parse_known_args()
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(text: str) -> str:
|
||||||
|
"""Make text safe for a filename: path separators and other reserved characters become '-', whitespace collapses."""
|
||||||
|
# [\\/:*?"<>|\x00-\x1f] = characters not allowed on common filesystems, plus control characters
|
||||||
|
text = re.sub(r'[\\/:*?"<>|\x00-\x1f]', "-", text)
|
||||||
|
return re.sub(r"\s+", " ", text).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(upload_date: str | None) -> str:
|
||||||
|
"""Convert yt-dlp's YYYYMMDD upload date to yyyy-mm-dd ('unknown-date' if missing)."""
|
||||||
|
if not upload_date or len(upload_date) != 8:
|
||||||
|
return "unknown-date"
|
||||||
|
return f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_filename(info: dict) -> str:
|
||||||
|
"""Build '<author> -- @<channel> -- <title> -- <yyyy-mm-dd>.srt' from yt-dlp metadata.
|
||||||
|
|
||||||
|
Author is the channel's display name, channel is its @handle (falls back to the channel id).
|
||||||
|
The title is shortened so the whole name stays under MAX_FILENAME_BYTES.
|
||||||
|
"""
|
||||||
|
author = sanitize(info.get("uploader") or info.get("channel") or "unknown")
|
||||||
|
handle = (info.get("uploader_id") or info.get("channel_id") or "unknown").lstrip("@")
|
||||||
|
suffix = f" -- @{sanitize(handle)} -- "
|
||||||
|
date = f" -- {format_date(info.get('upload_date'))}.srt"
|
||||||
|
title = sanitize(info.get("title") or info["id"])
|
||||||
|
budget = MAX_FILENAME_BYTES - len(f"{author}{suffix}{date}".encode())
|
||||||
|
while len(title.encode()) > budget:
|
||||||
|
title = title[:-1]
|
||||||
|
return f"{author}{suffix}{title.strip()}{date}"
|
||||||
|
|
||||||
|
|
||||||
|
def download_audio(url: str, directory: Path) -> tuple[Path, dict]:
|
||||||
|
"""Download the best audio-only stream into `directory`. Returns (file path, yt-dlp metadata)."""
|
||||||
|
options = {"format": "bestaudio/best", "outtmpl": str(directory / "%(id)s.%(ext)s"), "noplaylist": True, "quiet": True}
|
||||||
|
with yt_dlp.YoutubeDL(options) as downloader:
|
||||||
|
info = downloader.extract_info(url, download=True)
|
||||||
|
return Path(downloader.prepare_filename(info)), info
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Download the audio, transcribe it with video_to_srt.py and save the SRT under the metadata-based name."""
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
|
||||||
|
args, forwarded = parse_args()
|
||||||
|
url = args.video if "/" in args.video else f"https://www.youtube.com/watch?v={args.video}"
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_directory:
|
||||||
|
audio, info = download_audio(url=url, directory=Path(temp_directory))
|
||||||
|
output = args.output_dir / build_filename(info=info)
|
||||||
|
logger.info("Downloaded %s, writing %s", audio.name, output)
|
||||||
|
subprocess.run([str(VIDEO_TO_SRT), str(audio), "--output", str(output), *forwarded], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user