Spaces:
Sleeping
Sleeping
File size: 5,098 Bytes
aa5e3cc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """Resolve a *video source* (local path **or** URL) to a local file to analyze.
Supports any site handled by `yt-dlp` (YouTube, Vimeo, direct links, ...), so
the pipeline can ingest an online learning video by URL instead of requiring a
local download first.
Design notes
------------
* `yt-dlp` is a **local-analysis** dependency (`requirements-local.txt`), imported
lazily — the lightweight cloud demo never needs it.
* Downloads are capped to **720p** to stay MacBook-Air-M4 friendly.
* The `MAX_VIDEO_DURATION_SEC` guard is enforced: a longer video is **clipped to
the first N seconds** (with a warning) rather than silently processing a huge
file — consistent with the "short videos only" assumption.
⚠️ **Usage / licensing:** only download videos you have the right to use
(Creative Commons, public domain, your own, or with permission), and respect each
platform's Terms of Service. This tool does not grant any rights to third-party
content.
"""
from __future__ import annotations
import re
import warnings
from pathlib import Path
from typing import Dict, Tuple
from src.config import Config, CONFIG
from src.storage import work_dir
from src.utils import slugify
_URL_RE = re.compile(r"^https?://", re.IGNORECASE)
_MEDIA_EXTS = {".mp4", ".mkv", ".webm", ".mov", ".m4v", ".avi"}
def is_url(source: str) -> bool:
"""True if ``source`` looks like an http(s) URL (vs a local path)."""
return bool(_URL_RE.match(str(source).strip()))
def _require_ytdlp():
try:
import yt_dlp # type: ignore
return yt_dlp
except ImportError as exc: # pragma: no cover - environment dependent
raise RuntimeError(
"yt-dlp is required to ingest a video from a URL. "
"Install it with `pip install -r requirements-local.txt`."
) from exc
def probe_url(url: str, config: Config = CONFIG) -> Dict[str, object]:
"""Fetch metadata for a URL without downloading the media."""
yt = _require_ytdlp()
opts = {"quiet": True, "skip_download": True, "noplaylist": True, "no_warnings": True}
with yt.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url, download=False)
return {
"id": info.get("id"),
"title": info.get("title"),
"duration_sec": info.get("duration"),
"uploader": info.get("uploader"),
"webpage_url": info.get("webpage_url", url),
"license": info.get("license"),
}
def _download(url: str, video_id: str, config: Config, clip_to_max: bool = True) -> Path:
yt = _require_ytdlp()
out_dir = work_dir(video_id)
out_dir.mkdir(parents=True, exist_ok=True)
opts: Dict[str, object] = {
"quiet": True,
"no_warnings": True,
"noprogress": True,
"noplaylist": True,
"restrictfilenames": True,
"outtmpl": str(out_dir / "source.%(ext)s"),
# Keep it light: prefer <=720p, merge to mp4 when needed.
"format": "bestvideo[height<=720]+bestaudio/best[height<=720]/best",
"merge_output_format": "mp4",
}
# Duration guard -> clip to the first MAX_VIDEO_DURATION_SEC seconds.
info = probe_url(url, config)
duration = info.get("duration_sec") or 0
max_dur = config.max_video_duration_sec
if max_dur and duration and duration > max_dur:
if clip_to_max:
warnings.warn(
f"Video is {int(duration)}s (> MAX_VIDEO_DURATION_SEC={max_dur}); "
f"downloading only the first {max_dur}s."
)
opts["download_ranges"] = yt.utils.download_range_func(None, [(0, max_dur)])
opts["force_keyframes_at_cuts"] = True
else:
raise ValueError(
f"Video is {int(duration)}s, which exceeds "
f"MAX_VIDEO_DURATION_SEC={max_dur}."
)
with yt.YoutubeDL(opts) as ydl:
ydl.download([url])
candidates = [
p for p in out_dir.glob("source.*") if p.suffix.lower() in _MEDIA_EXTS
]
if not candidates:
raise RuntimeError(f"Download produced no media file for {url}")
# Largest file is the merged/best media.
return max(candidates, key=lambda p: p.stat().st_size)
def resolve_source(
source: str,
video_id: str = None,
config: Config = CONFIG,
) -> Tuple[Path, Dict[str, object]]:
"""Return ``(local_video_path, source_info)`` for a path or URL.
``source_info`` always carries a suggested ``video_id`` plus provenance the
pipeline records into the metrics artifact.
"""
if is_url(source):
meta = probe_url(source, config)
vid = video_id or slugify(str(meta.get("id") or meta.get("title") or "video"))
path = _download(source, vid, config)
meta.update({"video_id": vid, "source": "url", "source_url": source})
return path, meta
path = Path(source)
if not path.exists():
raise FileNotFoundError(f"Video not found: {path}")
return path, {
"video_id": video_id or slugify(path.stem),
"title": path.stem,
"source": "local",
"source_path": str(path),
}
|