Spaces:
Sleeping
Sleeping
File size: 9,988 Bytes
ba80657 f6a6455 ba80657 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e f6a6455 f07812e 6e9f89d f07812e ba80657 6e9f89d f07812e ba80657 f07812e ba80657 6e9f89d f07812e f6a6455 6e9f89d ba80657 f07812e 6e9f89d ba80657 6e9f89d f07812e ba80657 f07812e ba80657 f6a6455 d790783 | 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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | """Stage 5 + 6: real screenshots via a one-time low-res video download + ffmpeg, at
weighted timestamps.
Two proxies are used because of how the Space's egress is filtered:
* Resolution hits ``youtube.com`` and must go through ``YT_PROXY`` — an HTTPS/TLS-wrapped
proxy that hides the target host from the egress DPI that otherwise resets YouTube.
* The media itself lives on ``googlevideo.com`` (not DPI-filtered) and is many MB, which
the TLS proxy can't sustain — so it's downloaded through ``YT_MEDIA_PROXY``, a plain
HTTP proxy, in chunked Range requests (dodging YouTube's single-stream throttling).
We download one capped-resolution copy, then ``ffmpeg`` reads the *local* file to grab 3
candidates around each timestamp and keeps the sharpest (Laplacian variance). The capture
timestamp comes from the weighted indicator: a blend of the LLM's suggested timestamp and
the transcript segment timing of the text the step quotes.
"""
from __future__ import annotations
import difflib
import os
import subprocess
import requests
from yt_dlp import YoutubeDL
# --------------------------------------------------------------------------- weighting
def _clamp(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x))
def best_match_segment(quote: str, t_llm: float, segs: list[dict]) -> dict:
"""The transcript segment a step refers to: best text match, else nearest to t_llm."""
quote = (quote or "").strip().lower()
if quote:
ratio, best = max(
((difflib.SequenceMatcher(None, quote, s["text"].lower()).ratio(), s) for s in segs),
key=lambda t: t[0])
if ratio >= 0.3:
return best
return min(segs, key=lambda s: abs((s["start"] + s["end"]) / 2 - t_llm))
def pick_time(step: dict, segs: list[dict], *, w_llm: float = 0.4, w_whisper: float = 0.6,
lead: float = 1.0, max_drift: float = 20.0) -> float:
"""Weighted capture time (seconds). No snapping — we grab on demand at any T.
Transcript timing is ground truth for *when* the text is spoken; the LLM time is its
guess of *what* matters. Blend them, clamping a far-off LLM time back into the matched
segment so a hallucinated timestamp can't drag the shot off-topic.
"""
seg = best_match_segment(step.get("quote", ""), float(step.get("t_llm", 0.0)), segs)
t_whisper = (seg["start"] + seg["end"]) / 2 + lead
t_llm = float(step.get("t_llm", t_whisper))
if abs(t_llm - t_whisper) > max_drift:
t_llm = _clamp(t_llm, seg["start"], seg["end"])
total = (w_llm + w_whisper) or 1.0
return max(0.0, (w_llm * t_llm + w_whisper * t_whisper) / total)
def compute_shot_times(steps: list[dict], segs: list[dict], *, w_llm: float = 0.4,
w_whisper: float = 0.6, lead: float = 1.0,
max_shots: int = 8) -> dict[int, float]:
"""Return ``{step_index: capture_time}`` for the top-``max_shots`` steps by importance."""
if not segs:
return {}
order = sorted(range(len(steps)), key=lambda i: -float(steps[i].get("importance", 0.5)))
chosen = set(order[:max_shots])
return {
i: pick_time(steps[i], segs, w_llm=w_llm, w_whisper=w_whisper, lead=lead)
for i in range(len(steps)) if i in chosen
}
# -------------------------------------------------------------------- download + grab
def _sharpness(path: str) -> float:
"""Variance of the Laplacian (higher = sharper). Used to reject blurry frames."""
import numpy as np
from PIL import Image
im = np.asarray(Image.open(path).convert("L"), dtype=np.float64)
if im.shape[0] < 3 or im.shape[1] < 3:
return 0.0
lap = (-4 * im[1:-1, 1:-1] + im[:-2, 1:-1] + im[2:, 1:-1]
+ im[1:-1, :-2] + im[1:-1, 2:])
return float(lap.var())
def _resolve_media_url(video_id: str, max_height: int, cookiefile: str | None,
proxy: str | None) -> str | None:
"""Resolve a single-file (progressive/DASH) video URL <= ``max_height`` via the
HTTPS ``proxy``. Returns the ``googlevideo.com`` media URL, or None."""
opts = {
"quiet": True, "no_warnings": True, "skip_download": True,
"format": (f"bv*[height<={max_height}][ext=mp4]/bv*[height<={max_height}]/"
f"best[height<={max_height}]/best"),
}
if cookiefile:
opts["cookiefile"] = cookiefile
if proxy:
opts["proxy"] = proxy
# yt-dlp validates against certifi only and ignores SSL_CERT_FILE; switch it to
# the default cert path (which honors our combined bundle) so the self-signed
# HTTPS-proxy cert validates. See app._install_proxy_ca.
if os.environ.get("SSL_CERT_FILE"):
opts["compat_opts"] = ["no-certifi"]
with YoutubeDL(opts) as ydl:
info = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
if info.get("url"):
return info["url"]
vids = [f for f in info.get("formats", [])
if f.get("vcodec") not in (None, "none") and f.get("url")
and (f.get("height") or 0) <= max_height
and str(f.get("protocol", "")).startswith("http")]
if vids:
return sorted(vids, key=lambda f: f.get("height") or 0)[-1]["url"]
return None
def _download_media(url: str, dest: str, media_proxy: str | None,
chunk: int = 1 << 20, timeout: int = 30) -> None:
"""Download ``url`` to ``dest`` through the plain ``media_proxy`` using chunked Range
requests — small ranges dodge YouTube's single-stream throttling and stay within the
plain proxy's throughput (the TLS proxy can't sustain large transfers)."""
proxies = {"http": media_proxy, "https": media_proxy} if media_proxy else None
probe = requests.get(url, proxies=proxies, headers={"Range": "bytes=0-0"}, timeout=timeout)
probe.raise_for_status()
cr = probe.headers.get("Content-Range", "")
total = int(cr.split("/")[-1]) if "/" in cr and cr.split("/")[-1].isdigit() else 0
with open(dest, "wb") as fh:
if not total: # server ignored Range — fall back to a single stream
with requests.get(url, proxies=proxies, stream=True, timeout=timeout) as r:
r.raise_for_status()
for c in r.iter_content(chunk):
if c:
fh.write(c)
return
start = 0
while start < total:
end = min(start + chunk - 1, total - 1)
for attempt in range(3):
try:
rr = requests.get(url, proxies=proxies, timeout=timeout,
headers={"Range": f"bytes={start}-{end}"})
rr.raise_for_status()
fh.write(rr.content)
break
except requests.RequestException:
if attempt == 2:
raise
start = end + 1
def _grab_local(media_path: str, t: float, out_dir: str, idx: int) -> str | None:
"""Grab 3 candidates around ``t`` from the *local* ``media_path`` and keep the
sharpest. ffmpeg reads a local file here, so no proxy/network is involved."""
cands = []
for k, dt in enumerate((-0.5, 0.0, 0.5)):
p = os.path.join(out_dir, f"shot_{idx}_{k}.jpg")
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-ss", f"{max(0.0, t + dt):.2f}",
"-i", media_path, "-frames:v", "1", "-q:v", "2", p]
try:
subprocess.run(cmd, capture_output=True, text=True, timeout=60)
except subprocess.TimeoutExpired:
continue
if os.path.exists(p) and os.path.getsize(p) > 0:
cands.append(p)
if not cands:
return None
best = max(cands, key=_sharpness)
for p in cands:
if p != best:
try:
os.remove(p)
except OSError:
pass
return best
def capture_shots(times: dict[int, float], video_id: str, out_dir: str,
cookiefile: str | None = None, proxy: str | None = None,
media_proxy: str | None = None, progress=None, *,
max_height: int = 360) -> dict[int, dict]:
"""Download one capped-resolution copy of the video (resolve via ``proxy``, fetch
media via ``media_proxy``) and keep the sharpest frame per ``{step_index: time}``;
return ``{idx: {"time", "path"}}``."""
os.makedirs(out_dir, exist_ok=True)
items = list(times.items())
if not items:
return {}
url = _resolve_media_url(video_id, max_height, cookiefile, proxy)
if not url:
raise RuntimeError("Could not resolve a downloadable video stream URL.")
media = os.path.join(out_dir, "_video.bin")
if progress:
progress(0.0, desc="Downloading video for screenshots")
_download_media(url, media, media_proxy or proxy)
out: dict[int, dict] = {}
for n, (idx, t) in enumerate(items):
if progress:
progress((n + 1) / max(1, len(items)), desc=f"Screenshot {n + 1}/{len(items)}")
path = _grab_local(media, t, out_dir, idx)
if path:
out[idx] = {"time": t, "path": path}
try:
os.remove(media)
except OSError:
pass
return out
def capture_from_file(times: dict[int, float], video_path: str, out_dir: str,
progress=None) -> dict[int, dict]:
"""Extract the sharpest frame per ``{step_index: time}`` from an already-downloaded
local ``video_path`` (no network/proxy). Returns ``{idx: {"time", "path"}}``."""
os.makedirs(out_dir, exist_ok=True)
out: dict[int, dict] = {}
items = list(times.items())
for n, (idx, t) in enumerate(items):
if progress:
progress((n + 1) / max(1, len(items)), desc=f"Screenshot {n + 1}/{len(items)}")
path = _grab_local(video_path, t, out_dir, idx)
if path:
out[idx] = {"time": t, "path": path}
return out
|