vivekchakraverty commited on
Commit
ba80657
·
verified ·
1 Parent(s): 597c768

Screenshots: 2-phase (HTTPS-proxy resolve + plain media-proxy chunked download + local ffmpeg)

Browse files
Files changed (1) hide show
  1. pipeline/frames.py +103 -56
pipeline/frames.py CHANGED
@@ -1,16 +1,18 @@
1
- """Stage 5 + 6: real screenshots via short yt-dlp clip downloads + ffmpeg, at weighted
2
- timestamps.
3
-
4
- For each timestamp we download a ~1.5 s clip with yt-dlp's ``download_ranges`` (a couple
5
- hundred KB) and let ``ffmpeg`` extract frames from that *local* file. We grab 3 candidates
6
- around the target and keep the sharpest (Laplacian variance) to avoid a blurry/transition
7
- frame. Downloading (rather than ffmpeg-seeking the remote stream) is required because the
8
- stream lives on ``googlevideo.com`` and must go through the residential proxy — and when
9
- ``YT_PROXY`` is an HTTPS/TLS-wrapped proxy (needed to hide the target host from egress DPI)
10
- ffmpeg can't talk to it, but yt-dlp can.
11
-
12
- The capture timestamp itself comes from the weighted indicator: a blend of the LLM's
13
- suggested timestamp and the transcript segment timing of the text the step quotes.
 
 
14
  """
15
  from __future__ import annotations
16
 
@@ -18,8 +20,8 @@ import difflib
18
  import os
19
  import subprocess
20
 
 
21
  from yt_dlp import YoutubeDL
22
- from yt_dlp.utils import download_range_func
23
 
24
 
25
  # --------------------------------------------------------------------------- weighting
@@ -84,15 +86,79 @@ def _sharpness(path: str) -> float:
84
  return float(lap.var())
85
 
86
 
87
- def _best_frame_from_clip(clip: str, out_dir: str, idx: int, target: float) -> str | None:
88
- """Extract candidates near ``target`` seconds into a *local* ``clip`` and keep the
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  sharpest. ffmpeg reads a local file here, so no proxy/network is involved."""
90
  cands = []
91
- for k, dt in enumerate((-0.4, 0.0, 0.4)):
92
  p = os.path.join(out_dir, f"shot_{idx}_{k}.jpg")
93
- ss = max(0.0, target + dt)
94
- cmd = ["ffmpeg", "-y", "-loglevel", "error", "-ss", f"{ss:.2f}",
95
- "-i", clip, "-frames:v", "1", "-q:v", "2", p]
96
  try:
97
  subprocess.run(cmd, capture_output=True, text=True, timeout=60)
98
  except subprocess.TimeoutExpired:
@@ -113,51 +179,32 @@ def _best_frame_from_clip(clip: str, out_dir: str, idx: int, target: float) -> s
113
 
114
  def capture_shots(times: dict[int, float], video_id: str, out_dir: str,
115
  cookiefile: str | None = None, proxy: str | None = None,
116
- progress=None, *, half: float = 0.75,
117
- max_height: int = 480) -> dict[int, dict]:
118
- """Download a short clip around each ``{step_index: time}`` and keep the sharpest
119
- frame; return ``{idx: {"time", "path"}}``. One yt-dlp call fetches every section."""
 
120
  os.makedirs(out_dir, exist_ok=True)
121
  items = list(times.items())
122
  if not items:
123
  return {}
124
- clips_dir = os.path.join(out_dir, "_clips")
125
- os.makedirs(clips_dir, exist_ok=True)
126
- ranges = [(max(0.0, t - half), t + half) for _, t in items]
127
- opts = {
128
- "quiet": True, "no_warnings": True,
129
- "format": (f"bv*[height<={max_height}][vcodec!=none]/"
130
- f"best[height<={max_height}]/best[vcodec!=none]/best"),
131
- "paths": {"home": clips_dir},
132
- "outtmpl": {"default": "sec_%(section_start)0.2f.%(ext)s"},
133
- "download_ranges": download_range_func(None, ranges),
134
- "force_keyframes_at_cuts": True,
135
- }
136
- if cookiefile:
137
- opts["cookiefile"] = cookiefile
138
- if proxy:
139
- opts["proxy"] = proxy
140
- # yt-dlp trusts only certifi's own bundle by default and ignores SSL_CERT_FILE.
141
- # When an HTTPS-proxy CA has been installed into a combined bundle (SSL_CERT_FILE,
142
- # set by app._install_proxy_ca), switch yt-dlp to the default cert path so it
143
- # honors that file — this is what lets it validate the self-signed proxy cert.
144
- if os.environ.get("SSL_CERT_FILE"):
145
- opts["compat_opts"] = ["no-certifi"]
146
- with YoutubeDL(opts) as ydl:
147
- ydl.download([f"https://www.youtube.com/watch?v={video_id}"])
148
 
149
- import glob
150
  out: dict[int, dict] = {}
151
  for n, (idx, t) in enumerate(items):
152
  if progress:
153
  progress((n + 1) / max(1, len(items)), desc=f"Screenshot {n + 1}/{len(items)}")
154
- start = max(0.0, t - half)
155
- matches = [m for m in glob.glob(os.path.join(clips_dir, f"sec_{start:.2f}.*"))
156
- if not m.endswith(".part")]
157
- if not matches:
158
- continue
159
- # t sits ``t - start`` seconds into its clip (== half, except when clamped at 0)
160
- path = _best_frame_from_clip(matches[0], out_dir, idx, t - start)
161
  if path:
162
  out[idx] = {"time": t, "path": path}
 
 
 
 
163
  return out
 
1
+ """Stage 5 + 6: real screenshots via a one-time low-res video download + ffmpeg, at
2
+ weighted timestamps.
3
+
4
+ Two proxies are used because of how the Space's egress is filtered:
5
+
6
+ * Resolution hits ``youtube.com`` and must go through ``YT_PROXY`` an HTTPS/TLS-wrapped
7
+ proxy that hides the target host from the egress DPI that otherwise resets YouTube.
8
+ * The media itself lives on ``googlevideo.com`` (not DPI-filtered) and is many MB, which
9
+ the TLS proxy can't sustain so it's downloaded through ``YT_MEDIA_PROXY``, a plain
10
+ HTTP proxy, in chunked Range requests (dodging YouTube's single-stream throttling).
11
+
12
+ We download one capped-resolution copy, then ``ffmpeg`` reads the *local* file to grab 3
13
+ candidates around each timestamp and keeps the sharpest (Laplacian variance). The capture
14
+ timestamp comes from the weighted indicator: a blend of the LLM's suggested timestamp and
15
+ the transcript segment timing of the text the step quotes.
16
  """
17
  from __future__ import annotations
18
 
 
20
  import os
21
  import subprocess
22
 
23
+ import requests
24
  from yt_dlp import YoutubeDL
 
25
 
26
 
27
  # --------------------------------------------------------------------------- weighting
 
86
  return float(lap.var())
87
 
88
 
89
+ def _resolve_media_url(video_id: str, max_height: int, cookiefile: str | None,
90
+ proxy: str | None) -> str | None:
91
+ """Resolve a single-file (progressive/DASH) video URL <= ``max_height`` via the
92
+ HTTPS ``proxy``. Returns the ``googlevideo.com`` media URL, or None."""
93
+ opts = {
94
+ "quiet": True, "no_warnings": True, "skip_download": True,
95
+ "format": (f"bv*[height<={max_height}][ext=mp4]/bv*[height<={max_height}]/"
96
+ f"best[height<={max_height}]/best"),
97
+ }
98
+ if cookiefile:
99
+ opts["cookiefile"] = cookiefile
100
+ if proxy:
101
+ opts["proxy"] = proxy
102
+ # yt-dlp validates against certifi only and ignores SSL_CERT_FILE; switch it to
103
+ # the default cert path (which honors our combined bundle) so the self-signed
104
+ # HTTPS-proxy cert validates. See app._install_proxy_ca.
105
+ if os.environ.get("SSL_CERT_FILE"):
106
+ opts["compat_opts"] = ["no-certifi"]
107
+ with YoutubeDL(opts) as ydl:
108
+ info = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
109
+ if info.get("url"):
110
+ return info["url"]
111
+ vids = [f for f in info.get("formats", [])
112
+ if f.get("vcodec") not in (None, "none") and f.get("url")
113
+ and (f.get("height") or 0) <= max_height
114
+ and str(f.get("protocol", "")).startswith("http")]
115
+ if vids:
116
+ return sorted(vids, key=lambda f: f.get("height") or 0)[-1]["url"]
117
+ return None
118
+
119
+
120
+ def _download_media(url: str, dest: str, media_proxy: str | None,
121
+ chunk: int = 1 << 20, timeout: int = 30) -> None:
122
+ """Download ``url`` to ``dest`` through the plain ``media_proxy`` using chunked Range
123
+ requests — small ranges dodge YouTube's single-stream throttling and stay within the
124
+ plain proxy's throughput (the TLS proxy can't sustain large transfers)."""
125
+ proxies = {"http": media_proxy, "https": media_proxy} if media_proxy else None
126
+ probe = requests.get(url, proxies=proxies, headers={"Range": "bytes=0-0"}, timeout=timeout)
127
+ probe.raise_for_status()
128
+ cr = probe.headers.get("Content-Range", "")
129
+ total = int(cr.split("/")[-1]) if "/" in cr and cr.split("/")[-1].isdigit() else 0
130
+ with open(dest, "wb") as fh:
131
+ if not total: # server ignored Range — fall back to a single stream
132
+ with requests.get(url, proxies=proxies, stream=True, timeout=timeout) as r:
133
+ r.raise_for_status()
134
+ for c in r.iter_content(chunk):
135
+ if c:
136
+ fh.write(c)
137
+ return
138
+ start = 0
139
+ while start < total:
140
+ end = min(start + chunk - 1, total - 1)
141
+ for attempt in range(3):
142
+ try:
143
+ rr = requests.get(url, proxies=proxies, timeout=timeout,
144
+ headers={"Range": f"bytes={start}-{end}"})
145
+ rr.raise_for_status()
146
+ fh.write(rr.content)
147
+ break
148
+ except requests.RequestException:
149
+ if attempt == 2:
150
+ raise
151
+ start = end + 1
152
+
153
+
154
+ def _grab_local(media_path: str, t: float, out_dir: str, idx: int) -> str | None:
155
+ """Grab 3 candidates around ``t`` from the *local* ``media_path`` and keep the
156
  sharpest. ffmpeg reads a local file here, so no proxy/network is involved."""
157
  cands = []
158
+ for k, dt in enumerate((-0.5, 0.0, 0.5)):
159
  p = os.path.join(out_dir, f"shot_{idx}_{k}.jpg")
160
+ cmd = ["ffmpeg", "-y", "-loglevel", "error", "-ss", f"{max(0.0, t + dt):.2f}",
161
+ "-i", media_path, "-frames:v", "1", "-q:v", "2", p]
 
162
  try:
163
  subprocess.run(cmd, capture_output=True, text=True, timeout=60)
164
  except subprocess.TimeoutExpired:
 
179
 
180
  def capture_shots(times: dict[int, float], video_id: str, out_dir: str,
181
  cookiefile: str | None = None, proxy: str | None = None,
182
+ media_proxy: str | None = None, progress=None, *,
183
+ max_height: int = 360) -> dict[int, dict]:
184
+ """Download one capped-resolution copy of the video (resolve via ``proxy``, fetch
185
+ media via ``media_proxy``) and keep the sharpest frame per ``{step_index: time}``;
186
+ return ``{idx: {"time", "path"}}``."""
187
  os.makedirs(out_dir, exist_ok=True)
188
  items = list(times.items())
189
  if not items:
190
  return {}
191
+ url = _resolve_media_url(video_id, max_height, cookiefile, proxy)
192
+ if not url:
193
+ raise RuntimeError("Could not resolve a downloadable video stream URL.")
194
+ media = os.path.join(out_dir, "_video.bin")
195
+ if progress:
196
+ progress(0.0, desc="Downloading video for screenshots")
197
+ _download_media(url, media, media_proxy or proxy)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
 
199
  out: dict[int, dict] = {}
200
  for n, (idx, t) in enumerate(items):
201
  if progress:
202
  progress((n + 1) / max(1, len(items)), desc=f"Screenshot {n + 1}/{len(items)}")
203
+ path = _grab_local(media, t, out_dir, idx)
 
 
 
 
 
 
204
  if path:
205
  out[idx] = {"time": t, "path": path}
206
+ try:
207
+ os.remove(media)
208
+ except OSError:
209
+ pass
210
  return out