Spaces:
Sleeping
Sleeping
Video search via Piped API with normalized engagement ranking + instance failover; Data API/yt-dlp fallback
Browse files
app.py
CHANGED
|
@@ -104,11 +104,8 @@ def _cookiefile(workdir: str) -> str | None:
|
|
| 104 |
return path
|
| 105 |
|
| 106 |
|
| 107 |
-
def _resolve_proxy(
|
| 108 |
-
|
| 109 |
-
user can route through their own residential proxy (see tools/home_proxy_panel.py).
|
| 110 |
-
It also backs screenshot downloads (capture_shots falls back to it)."""
|
| 111 |
-
proxy = (ui_proxy or "").strip() or os.environ.get("YT_PROXY", "").strip()
|
| 112 |
return proxy or None
|
| 113 |
|
| 114 |
|
|
@@ -125,15 +122,14 @@ def _resolve_api_key(ui_key: str | None) -> str | None:
|
|
| 125 |
return key or None
|
| 126 |
|
| 127 |
|
| 128 |
-
def check_access(
|
| 129 |
"""Health check: which secrets are set, the egress IP, and YouTube reachability."""
|
| 130 |
import requests
|
| 131 |
|
| 132 |
-
proxy = _resolve_proxy(
|
| 133 |
proxies = {"http": proxy, "https": proxy} if proxy else None
|
| 134 |
-
src = "your proxy field" if (yt_proxy or "").strip() else "YT_PROXY secret"
|
| 135 |
lines = [
|
| 136 |
-
f"- **Proxy**: {
|
| 137 |
f"- **Media proxy** (`YT_MEDIA_PROXY`, screenshots): "
|
| 138 |
f"{'set ✅' if _resolve_media_proxy() else 'not set ⚪ (falls back to YT_PROXY)'}",
|
| 139 |
f"- **Cookies** (`YT_COOKIES`): {'set ✅' if os.environ.get('YT_COOKIES') else 'not set ⚪'}",
|
|
@@ -222,7 +218,7 @@ def _collect_keywords(primary_kw, secondary_kw) -> dict:
|
|
| 222 |
return {"primary": primary, "secondary": secondary}
|
| 223 |
|
| 224 |
|
| 225 |
-
def run_pipeline(topic, hf_token, yt_api_key,
|
| 226 |
w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
|
| 227 |
content_brief="", progress=gr.Progress()):
|
| 228 |
"""Generator yielding (status_md, ranking_df, transcript, docx_file)."""
|
|
@@ -243,17 +239,18 @@ def run_pipeline(topic, hf_token, yt_api_key, yt_proxy, llm_model, vlm_model,
|
|
| 243 |
try:
|
| 244 |
api_key = _resolve_api_key(yt_api_key)
|
| 245 |
cookiefile = _cookiefile(workdir)
|
| 246 |
-
proxy = _resolve_proxy(
|
| 247 |
if proxy:
|
| 248 |
-
|
| 249 |
-
src = "your proxy" if (yt_proxy or "").strip() else "the `YT_PROXY` secret"
|
| 250 |
-
log.append(f"🔐 Routing transcript + screenshot requests through {src}.")
|
| 251 |
|
| 252 |
# 1. Search ------------------------------------------------------------------
|
| 253 |
progress(0.03, desc="Searching")
|
| 254 |
-
yield status(f"🔍 Searching top videos for **{topic}**…"), gr.update(), gr.update(), gr.update()
|
| 255 |
-
videos = search_mod.search_top5(topic)
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
# 2. Sentiment ranking (YouTube Data API comments) ---------------------------
|
| 259 |
if api_key:
|
|
@@ -339,12 +336,7 @@ def build_ui():
|
|
| 339 |
"Enter a topic, your **Hugging Face token** (LLM + vision model, billed to you) "
|
| 340 |
"and a **YouTube Data API key** (for comments). The Space picks the best video by "
|
| 341 |
"comment sentiment, pulls its transcript, writes an AEO-friendly tutorial, grabs "
|
| 342 |
-
"real screenshots at the right moments, and builds a **.docx**.
|
| 343 |
-
"> 🏠 **YouTube blocks this Space's datacenter IP.** Run your own residential proxy "
|
| 344 |
-
"with the **Home Proxy Panel** "
|
| 345 |
-
"([download / source](https://github.com/vivekchakraverty/tutorialmaker-home-proxy-panel)) "
|
| 346 |
-
"and paste its URL into **Your proxy URL** below — transcript + screenshots then route "
|
| 347 |
-
"through your home IP. Each user brings their own proxy."
|
| 348 |
)
|
| 349 |
with gr.Row():
|
| 350 |
with gr.Column(scale=2):
|
|
@@ -353,13 +345,6 @@ def build_ui():
|
|
| 353 |
placeholder="hf_… (Inference Providers permission)")
|
| 354 |
yt_api_key = gr.Textbox(label="YouTube Data API key", type="password",
|
| 355 |
placeholder="for comments (or set the YOUTUBE_API_KEY secret)")
|
| 356 |
-
yt_proxy = gr.Textbox(
|
| 357 |
-
label="Your proxy URL (recommended)", type="password",
|
| 358 |
-
placeholder="http://user:pass@bore.pub:12345 — from the Home Proxy Panel",
|
| 359 |
-
info="Run the Home Proxy Panel (github.com/vivekchakraverty/"
|
| 360 |
-
"tutorialmaker-home-proxy-panel) on your own machine and paste its "
|
| 361 |
-
"proxy URL here so YouTube requests exit from your residential IP. "
|
| 362 |
-
"Leave blank to use the Space's shared proxy, if configured.")
|
| 363 |
with gr.Column(scale=1):
|
| 364 |
llm_model = gr.Dropdown(LLM_CHOICES, value=LLM_CHOICES[0],
|
| 365 |
label="Tutorial LLM", allow_custom_value=True)
|
|
@@ -411,10 +396,10 @@ def build_ui():
|
|
| 411 |
transcript_box = gr.Textbox(label="Transcript preview", lines=10, max_lines=20)
|
| 412 |
docx_file = gr.File(label="Download tutorial (.docx)")
|
| 413 |
|
| 414 |
-
health_btn.click(check_access,
|
| 415 |
run_btn.click(
|
| 416 |
run_pipeline,
|
| 417 |
-
inputs=[topic, hf_token, yt_api_key,
|
| 418 |
w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
|
| 419 |
content_brief],
|
| 420 |
outputs=[status_md, ranking_df, transcript_box, docx_file],
|
|
|
|
| 104 |
return path
|
| 105 |
|
| 106 |
|
| 107 |
+
def _resolve_proxy() -> str | None:
|
| 108 |
+
proxy = os.environ.get("YT_PROXY", "").strip()
|
|
|
|
|
|
|
|
|
|
| 109 |
return proxy or None
|
| 110 |
|
| 111 |
|
|
|
|
| 122 |
return key or 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"- **Proxy** (`YT_PROXY`): {'set ✅' if proxy else 'not set ⚪'}",
|
| 133 |
f"- **Media proxy** (`YT_MEDIA_PROXY`, screenshots): "
|
| 134 |
f"{'set ✅' if _resolve_media_proxy() else 'not set ⚪ (falls back to YT_PROXY)'}",
|
| 135 |
f"- **Cookies** (`YT_COOKIES`): {'set ✅' if os.environ.get('YT_COOKIES') else 'not set ⚪'}",
|
|
|
|
| 218 |
return {"primary": primary, "secondary": secondary}
|
| 219 |
|
| 220 |
|
| 221 |
+
def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
|
| 222 |
w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
|
| 223 |
content_brief="", progress=gr.Progress()):
|
| 224 |
"""Generator yielding (status_md, ranking_df, transcript, docx_file)."""
|
|
|
|
| 239 |
try:
|
| 240 |
api_key = _resolve_api_key(yt_api_key)
|
| 241 |
cookiefile = _cookiefile(workdir)
|
| 242 |
+
proxy = _resolve_proxy()
|
| 243 |
if proxy:
|
| 244 |
+
log.append("🔐 `YT_PROXY` is set — transcript + stream requests route through it.")
|
|
|
|
|
|
|
| 245 |
|
| 246 |
# 1. Search ------------------------------------------------------------------
|
| 247 |
progress(0.03, desc="Searching")
|
| 248 |
+
yield status(f"🔍 Searching top videos for **{topic}** (engagement-ranked)…"), gr.update(), gr.update(), gr.update()
|
| 249 |
+
videos = search_mod.search_top5(topic, api_key=api_key, proxy=proxy)
|
| 250 |
+
eng_note = ""
|
| 251 |
+
if videos and "engagement" in videos[0]:
|
| 252 |
+
eng_note = " • ranked by likes+comments+subscribers−dislikes (normalized)"
|
| 253 |
+
yield status(f"Found {len(videos)} candidate videos{eng_note}."), gr.update(), gr.update(), gr.update()
|
| 254 |
|
| 255 |
# 2. Sentiment ranking (YouTube Data API comments) ---------------------------
|
| 256 |
if api_key:
|
|
|
|
| 336 |
"Enter a topic, your **Hugging Face token** (LLM + vision model, billed to you) "
|
| 337 |
"and a **YouTube Data API key** (for comments). The Space picks the best video by "
|
| 338 |
"comment sentiment, pulls its transcript, writes an AEO-friendly tutorial, grabs "
|
| 339 |
+
"real screenshots at the right moments, and builds a **.docx**."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
)
|
| 341 |
with gr.Row():
|
| 342 |
with gr.Column(scale=2):
|
|
|
|
| 345 |
placeholder="hf_… (Inference Providers permission)")
|
| 346 |
yt_api_key = gr.Textbox(label="YouTube Data API key", type="password",
|
| 347 |
placeholder="for comments (or set the YOUTUBE_API_KEY secret)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
with gr.Column(scale=1):
|
| 349 |
llm_model = gr.Dropdown(LLM_CHOICES, value=LLM_CHOICES[0],
|
| 350 |
label="Tutorial LLM", allow_custom_value=True)
|
|
|
|
| 396 |
transcript_box = gr.Textbox(label="Transcript preview", lines=10, max_lines=20)
|
| 397 |
docx_file = gr.File(label="Download tutorial (.docx)")
|
| 398 |
|
| 399 |
+
health_btn.click(check_access, outputs=health_md)
|
| 400 |
run_btn.click(
|
| 401 |
run_pipeline,
|
| 402 |
+
inputs=[topic, hf_token, yt_api_key, llm_model, vlm_model,
|
| 403 |
w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
|
| 404 |
content_brief],
|
| 405 |
outputs=[status_md, ranking_df, transcript_box, docx_file],
|