vivekchakraverty commited on
Commit
f192b6c
·
verified ·
1 Parent(s): 670721e

Download video via RapidAPI (own-CDN) -> faster-whisper transcript + local screenshots -> delete; retire proxies

Browse files
Files changed (1) hide show
  1. app.py +70 -79
app.py CHANGED
@@ -47,8 +47,10 @@ _install_proxy_ca()
47
  import gradio as gr
48
 
49
  from pipeline import (
 
50
  captions as captions_mod,
51
  docx_builder,
 
52
  frames as frames_mod,
53
  search as search_mod,
54
  sentiment as sentiment_mod,
@@ -123,70 +125,46 @@ def _resolve_api_key(ui_key: str | None) -> str | None:
123
 
124
 
125
  def check_access():
126
- """Health check: which secrets are set, the egress IP, and YouTube reachability."""
127
  import requests
128
 
129
- proxy = _resolve_proxy()
130
- proxies = {"http": proxy, "https": proxy} if proxy else None
131
  lines = [
132
- f"- **Transcript API** (`TRANSCRIPTAPI_KEY`, no proxy needed): "
133
- f"{'set ✅' if os.environ.get('TRANSCRIPTAPI_KEY') else 'not set ⚪ (falls back to youtube-transcript-api via YT_PROXY)'}",
134
- f"- **Proxy** (`YT_PROXY`): {'set ✅' if proxy else 'not set ⚪'}",
135
- f"- **Media proxy** (`YT_MEDIA_PROXY`, screenshots): "
136
- f"{'set ✅' if _resolve_media_proxy() else 'not set ⚪ (falls back to YT_PROXY)'}",
137
- f"- **Cookies** (`YT_COOKIES`): {'set ✅' if os.environ.get('YT_COOKIES') else 'not set ⚪'}",
138
- f"- **Data API key** (`YOUTUBE_API_KEY`): {'set ✅' if os.environ.get('YOUTUBE_API_KEY') else 'not set ⚪'}",
139
  ]
140
- try:
141
- ip = requests.get("https://api.ipify.org", proxies=proxies, timeout=20).text.strip()
142
- lines.append(f"- **Egress IP** {'(via proxy)' if proxy else '(Space direct)'}: `{ip}`")
143
- except Exception as exc:
144
- lines.append(f"- **Egress IP**: ❌ {type(exc).__name__}")
145
- def probe(url):
146
- last = None
147
- for _ in range(2): # tolerate a single transient reset
148
- try:
149
- r = requests.get(url, proxies=proxies, timeout=20)
150
- return True, f"HTTP {r.status_code}"
151
- except Exception as exc:
152
- last = exc
153
- time.sleep(1.0)
154
- return False, f"{type(last).__name__}: {str(last)[:90]}"
155
-
156
- # Discriminating battery: neutral-small vs Google-family (same big cert as
157
- # YouTube, different hostname) vs YouTube itself. The pass/fail pattern says
158
- # whether it's a size/MTU blackhole, hostname-based egress filtering, or reset.
159
- probes = [
160
- ("example.com (neutral)", "https://example.com"),
161
- ("google.com/204 (Google cert, non-YT name)", "https://www.google.com/generate_204"),
162
- ("i.ytimg.com (YT image CDN)", "https://i.ytimg.com/generate_204"),
163
- ("youtubei.googleapis.com (YT API)", "https://youtubei.googleapis.com/generate_204"),
164
- ("youtube.com/robots.txt", "https://www.youtube.com/robots.txt"),
165
- ("youtube.com/ (large body)", "https://www.youtube.com/"),
166
- ]
167
- results = {}
168
- for label, url in probes:
169
- ok, msg = probe(url)
170
- results[label] = ok
171
- lines.append(f"- {'✅' if ok else '❌'} {label}: {msg}")
172
-
173
- yt_ok = results.get("youtube.com/robots.txt")
174
- if yt_ok:
175
- verdict = "### 🟢 YouTube is reachable — transcript + screenshots should work."
176
- elif results.get("google.com/204 (Google cert, non-YT name)"):
177
- verdict = ("### 🔴 YouTube is filtered by hostname.\n"
178
- "Google works but YouTube is reset → the network between the Space and the "
179
- "tunnel is dropping the plaintext `CONNECT www.youtube.com`. Fix: run the "
180
- "home proxy as an **HTTPS proxy** (TLS-wrapped) so the target host is hidden.")
181
- elif results.get("example.com (neutral)") and not results.get("google.com/204 (Google cert, non-YT name)"):
182
- verdict = ("### 🔴 Large TLS handshakes are being dropped (MTU blackhole).\n"
183
- "Small sites work; Google/YouTube (large cert chains) reset. Fix: clamp MSS on "
184
- "the tunnel path or switch tunnel provider (e.g. ngrok).")
185
  else:
186
- verdict = ("### 🔴 YouTube is NOT reachable from the Space.\n"
187
- "The proxy connects (egress IP works) but TLS to YouTube is failing. "
188
- "Try again in a minute or refresh **`YT_PROXY`** via the panel in "
189
- "[`tools/`](tools/README.md).")
190
  return verdict + "\n\n" + "\n".join(lines)
191
 
192
 
@@ -238,17 +216,14 @@ def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
238
 
239
  workdir = tempfile.mkdtemp(prefix="ytt_")
240
  frames_dir = os.path.join(workdir, "frames")
 
241
  try:
242
  api_key = _resolve_api_key(yt_api_key)
243
- cookiefile = _cookiefile(workdir)
244
- proxy = _resolve_proxy()
245
- if proxy:
246
- log.append("🔐 `YT_PROXY` is set — transcript + stream requests route through it.")
247
 
248
  # 1. Search ------------------------------------------------------------------
249
  progress(0.03, desc="Searching")
250
  yield status(f"🔍 Searching top videos for **{topic}** (engagement-ranked)…"), gr.update(), gr.update(), gr.update()
251
- videos = search_mod.search_top5(topic, api_key=api_key, proxy=proxy)
252
  eng_note = ""
253
  if videos and "engagement" in videos[0]:
254
  eng_note = " • ranked by likes+comments+subscribers−dislikes (normalized)"
@@ -269,12 +244,18 @@ def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
269
  yield (status(f"🏆 Picked **{best.get('title', best['video_id'])}** {picked_msg}."),
270
  ranking, gr.update(), gr.update())
271
 
272
- # 3. Transcript (transcriptapi.com primary, youtube-transcript-api fallback) --
273
- progress(0.3, desc="Transcript")
274
- tsrc = ("transcriptapi.com (no proxy)" if os.environ.get("TRANSCRIPTAPI_KEY")
275
- else "youtube-transcript-api via proxy")
276
- yield status(f"📝 Fetching the timestamped transcript — {tsrc}…"), ranking, gr.update(), gr.update()
277
- segs = transcribe_mod.get_segments(best["video_id"], proxy=proxy)
 
 
 
 
 
 
278
  transcript = transcribe_mod.transcript_text(segs)
279
  yield (status(f"Transcript ready ({len(segs)} segments)."),
280
  ranking, gr.update(value=transcript), gr.update())
@@ -295,27 +276,31 @@ def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
295
  yield (status(f"🔑 Primary keyword '{keywords['primary']}' appears {n}× in the post."),
296
  ranking, gr.update(value=transcript), gr.update())
297
 
298
- # 6. Screenshots (weighted timestamps -> yt-dlp clip download -> ffmpeg) ------
299
  selected, caps = {}, {}
300
  times = frames_mod.compute_shot_times(
301
  tut["steps"], segs, w_llm=float(w_llm), w_whisper=float(w_whisper),
302
  lead=float(lead), max_shots=int(max_shots))
303
  if times:
304
- progress(0.75, desc="Screenshots")
305
- yield status(f"🎞️ Capturing {len(times)} screenshots at weighted timestamps…"), ranking, gr.update(value=transcript), gr.update()
306
  try:
307
- selected = frames_mod.capture_shots(
308
- times, best["video_id"], frames_dir, cookiefile, proxy,
309
- _resolve_media_proxy(), progress)
310
  except Exception as exc:
311
- yield (status(f"⚠️ Couldn't fetch screenshots — text-only tutorial. "
312
- f"`{type(exc).__name__}: {str(exc)[:400]}`"),
313
  ranking, gr.update(value=transcript), gr.update())
314
  if selected:
315
- progress(0.88, desc="Captioning")
316
  yield status(f"✍️ Captioning {len(selected)} screenshots with `{vlm_model}`…"), ranking, gr.update(value=transcript), gr.update()
317
  caps = captions_mod.caption_frames(selected, tut["steps"], hf_token.strip(), vlm_model, progress)
318
 
 
 
 
 
 
 
319
  # 7. DOCX --------------------------------------------------------------------
320
  progress(0.96, desc="Building document")
321
  out_path = os.path.join(workdir, f"{_safe_name(tut['title'])}.docx")
@@ -331,6 +316,12 @@ def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
331
  except (transcribe_mod.TranscriptError, sentiment_mod.SentimentError,
332
  RuntimeError, ValueError) as exc:
333
  raise gr.Error(str(exc))
 
 
 
 
 
 
334
 
335
 
336
  def build_ui():
 
47
  import gradio as gr
48
 
49
  from pipeline import (
50
+ asr as asr_mod,
51
  captions as captions_mod,
52
  docx_builder,
53
+ downloader as downloader_mod,
54
  frames as frames_mod,
55
  search as search_mod,
56
  sentiment as sentiment_mod,
 
125
 
126
 
127
  def check_access():
128
+ """Health check: are the required secrets set and is the video-download API live?"""
129
  import requests
130
 
131
+ rk = os.environ.get("RAPIDAPI_KEY", "").strip()
 
132
  lines = [
133
+ f"- **Video download** (`RAPIDAPI_KEY`): {'set ✅' if rk else 'not set ❌ — required'}",
134
+ f"- **Comment sentiment** (`YOUTUBE_API_KEY`): "
135
+ f"{'set ✅' if os.environ.get('YOUTUBE_API_KEY') else 'not set ⚪ (sentiment skipped)'}",
136
+ "- **Transcript**: faster-whisper, local (no key/proxy needed) ",
137
+ "- **Search**: Piped API (no key needed)",
 
 
138
  ]
139
+
140
+ api_ok = None
141
+ if rk:
142
+ host = "youtube-video-fast-downloader-24-7.p.rapidapi.com"
143
+ try:
144
+ r = requests.get(f"https://{host}/get-video-info/dQw4w9WgXcQ",
145
+ headers={"X-RapidAPI-Key": rk, "X-RapidAPI-Host": host},
146
+ timeout=25)
147
+ if r.status_code == 200:
148
+ api_ok = True
149
+ lines.append("- Download API reachable and key valid (HTTP 200)")
150
+ elif r.status_code in (401, 403):
151
+ api_ok = False
152
+ lines.append(f"- ❌ Download API rejected the key (HTTP {r.status_code}) — "
153
+ "check RAPIDAPI_KEY and that you're subscribed")
154
+ else:
155
+ api_ok = False
156
+ lines.append(f"- ⚠️ Download API HTTP {r.status_code}: {r.text[:120]}")
157
+ except Exception as exc:
158
+ api_ok = False
159
+ lines.append(f"- ❌ Download API unreachable: {type(exc).__name__}")
160
+
161
+ if not rk:
162
+ verdict = "### 🔴 Set `RAPIDAPI_KEY` — it's required to download the source video."
163
+ elif api_ok:
164
+ verdict = ("### 🟢 Ready. Video downloads via the API; the transcript is produced "
165
+ "locally by faster-whisper (first run downloads the model, ~1 min).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  else:
167
+ verdict = "### 🔴 The video-download API isn't working — see the details below."
 
 
 
168
  return verdict + "\n\n" + "\n".join(lines)
169
 
170
 
 
216
 
217
  workdir = tempfile.mkdtemp(prefix="ytt_")
218
  frames_dir = os.path.join(workdir, "frames")
219
+ video_path = os.path.join(workdir, "source.mp4")
220
  try:
221
  api_key = _resolve_api_key(yt_api_key)
 
 
 
 
222
 
223
  # 1. Search ------------------------------------------------------------------
224
  progress(0.03, desc="Searching")
225
  yield status(f"🔍 Searching top videos for **{topic}** (engagement-ranked)…"), gr.update(), gr.update(), gr.update()
226
+ videos = search_mod.search_top5(topic, api_key=api_key)
227
  eng_note = ""
228
  if videos and "engagement" in videos[0]:
229
  eng_note = " • ranked by likes+comments+subscribers−dislikes (normalized)"
 
244
  yield (status(f"🏆 Picked **{best.get('title', best['video_id'])}** {picked_msg}."),
245
  ranking, gr.update(), gr.update())
246
 
247
+ # 3. Download the source video once (RapidAPI, own-CDN — no proxy) -----------
248
+ progress(0.25, desc="Downloading video")
249
+ yield status("⬇️ Downloading the video (server-side prep can take up to ~5 min)"), ranking, gr.update(), gr.update()
250
+ downloader_mod.download_video(best["video_id"], video_path, progress=progress)
251
+ size_mb = os.path.getsize(video_path) / 1024 / 1024
252
+ yield (status(f"Video downloaded ({size_mb:.0f} MB)."),
253
+ ranking, gr.update(), gr.update())
254
+
255
+ # 4. Transcript via faster-whisper on the local video ------------------------
256
+ progress(0.4, desc="Transcribing (faster-whisper)")
257
+ yield status("📝 Transcribing the audio with faster-whisper (can take a while on CPU)…"), ranking, gr.update(), gr.update()
258
+ segs = asr_mod.transcribe_file(video_path, progress=progress)
259
  transcript = transcribe_mod.transcript_text(segs)
260
  yield (status(f"Transcript ready ({len(segs)} segments)."),
261
  ranking, gr.update(value=transcript), gr.update())
 
276
  yield (status(f"🔑 Primary keyword '{keywords['primary']}' appears {n}× in the post."),
277
  ranking, gr.update(value=transcript), gr.update())
278
 
279
+ # 6. Screenshots from the local video (ffmpeg, no network) -------------------
280
  selected, caps = {}, {}
281
  times = frames_mod.compute_shot_times(
282
  tut["steps"], segs, w_llm=float(w_llm), w_whisper=float(w_whisper),
283
  lead=float(lead), max_shots=int(max_shots))
284
  if times:
285
+ progress(0.8, desc="Screenshots")
286
+ yield status(f"🎞️ Capturing {len(times)} screenshots from the video…"), ranking, gr.update(value=transcript), gr.update()
287
  try:
288
+ selected = frames_mod.capture_from_file(times, video_path, frames_dir, progress)
 
 
289
  except Exception as exc:
290
+ yield (status(f"⚠️ Couldn't extract screenshots — text-only tutorial. "
291
+ f"`{type(exc).__name__}: {str(exc)[:300]}`"),
292
  ranking, gr.update(value=transcript), gr.update())
293
  if selected:
294
+ progress(0.9, desc="Captioning")
295
  yield status(f"✍️ Captioning {len(selected)} screenshots with `{vlm_model}`…"), ranking, gr.update(value=transcript), gr.update()
296
  caps = captions_mod.caption_frames(selected, tut["steps"], hf_token.strip(), vlm_model, progress)
297
 
298
+ # Done with the video — delete it (keep only the .docx for download).
299
+ try:
300
+ os.remove(video_path)
301
+ except OSError:
302
+ pass
303
+
304
  # 7. DOCX --------------------------------------------------------------------
305
  progress(0.96, desc="Building document")
306
  out_path = os.path.join(workdir, f"{_safe_name(tut['title'])}.docx")
 
316
  except (transcribe_mod.TranscriptError, sentiment_mod.SentimentError,
317
  RuntimeError, ValueError) as exc:
318
  raise gr.Error(str(exc))
319
+ finally:
320
+ # Always delete the downloaded video (keep only the .docx for download).
321
+ try:
322
+ os.remove(video_path)
323
+ except OSError:
324
+ pass
325
 
326
 
327
  def build_ui():