Files
claude-video/scripts/download.py
T
bradautomates 29b8a2988a Initial commit: /watch skill v0.1.0
Give Claude the ability to watch any video — yt-dlp download, ffmpeg
frame extraction with auto-scaled fps, native-caption transcript with
Whisper (Groq/OpenAI) fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:40:34 +10:00

128 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Download a video via yt-dlp, or resolve a local file path.
Also fetches subtitles (manual first, then auto-generated) in VTT format so
transcribe.py can parse them without needing Whisper.
"""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlparse
VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".mov", ".m4v", ".avi", ".flv", ".wmv"}
def is_url(source: str) -> bool:
parsed = urlparse(source)
return parsed.scheme in ("http", "https")
def resolve_local(path: str) -> dict:
p = Path(path).expanduser().resolve()
if not p.exists():
raise SystemExit(f"File not found: {p}")
if p.suffix.lower() not in VIDEO_EXTS:
print(
f"[watch] warning: {p.suffix} is not a known video extension, proceeding anyway",
file=sys.stderr,
)
return {
"video_path": str(p),
"subtitle_path": None,
"info": {"title": p.name, "url": str(p)},
"downloaded": False,
}
def _pick_subtitle(out_dir: Path) -> Path | None:
candidates = sorted(out_dir.glob("video*.vtt"))
if not candidates:
return None
preferred = [c for c in candidates if ".en" in c.name]
return preferred[0] if preferred else candidates[0]
def _pick_video(out_dir: Path) -> Path | None:
for ext in (".mp4", ".mkv", ".webm", ".mov"):
for candidate in out_dir.glob(f"video*{ext}"):
return candidate
for candidate in out_dir.glob("video.*"):
if candidate.suffix.lower() in VIDEO_EXTS:
return candidate
return None
def download_url(url: str, out_dir: Path) -> dict:
if shutil.which("yt-dlp") is None:
raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp")
out_dir.mkdir(parents=True, exist_ok=True)
output_template = str(out_dir / "video.%(ext)s")
cmd = [
"yt-dlp",
"-N", "8",
"-f", "bv*[height<=720]+ba/b[height<=720]/bv+ba/b",
"--merge-output-format", "mp4",
"--write-info-json",
"--write-subs",
"--write-auto-subs",
"--sub-langs", "en,en-US,en-GB,en-orig",
"--sub-format", "vtt",
"--convert-subs", "vtt",
"--no-playlist",
"--ignore-errors",
"-o", output_template,
url,
]
# yt-dlp may exit non-zero if a subtitle variant fails (e.g. 429) even when
# the video itself downloaded fine. Treat "video file present" as success.
result = subprocess.run(cmd, stdout=sys.stderr, stderr=sys.stderr)
video = _pick_video(out_dir)
if video is None:
raise SystemExit(
f"yt-dlp did not produce a video file in {out_dir} (exit {result.returncode})"
)
subtitle = _pick_subtitle(out_dir)
info_path = out_dir / "video.info.json"
info: dict = {}
if info_path.exists():
try:
raw = json.loads(info_path.read_text())
info = {
"title": raw.get("title"),
"uploader": raw.get("uploader") or raw.get("channel"),
"duration": raw.get("duration"),
"url": raw.get("webpage_url") or url,
}
except Exception:
info = {"url": url}
return {
"video_path": str(video),
"subtitle_path": str(subtitle) if subtitle else None,
"info": info or {"url": url},
"downloaded": True,
}
def download(source: str, out_dir: Path) -> dict:
if is_url(source):
return download_url(source, out_dir)
return resolve_local(source)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("usage: download.py <url-or-path> <out-dir>", file=sys.stderr)
raise SystemExit(2)
result = download(sys.argv[1], Path(sys.argv[2]))
print(json.dumps(result, indent=2))