Spaces:
Sleeping
Sleeping
| """Gradio Space: YouTube topic -> captioned .docx tutorial. | |
| Orchestrates the pipeline stages and streams progress/status to the UI. Heavy ML | |
| imports (torch/transformers/faster-whisper) are lazy inside the pipeline modules, so app | |
| startup stays fast. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import shutil | |
| import tempfile | |
| import gradio as gr | |
| from pipeline import ( | |
| captions as captions_mod, | |
| docx_builder, | |
| download as download_mod, | |
| frames as frames_mod, | |
| search as search_mod, | |
| sentiment as sentiment_mod, | |
| transcribe as transcribe_mod, | |
| tutorial as tutorial_mod, | |
| ) | |
| LLM_CHOICES = [ | |
| "deepseek-ai/DeepSeek-V3", | |
| "meta-llama/Llama-3.3-70B-Instruct", | |
| "openai/gpt-oss-120b", | |
| ] | |
| VLM_CHOICES = [ | |
| "Qwen/Qwen2.5-VL-72B-Instruct", | |
| "Qwen/Qwen2.5-VL-7B-Instruct", | |
| "meta-llama/Llama-3.2-90B-Vision-Instruct", | |
| ] | |
| def _cookiefile(workdir: str) -> str | None: | |
| """Materialize the YT_COOKIES secret (Netscape cookie file contents) to disk.""" | |
| data = os.environ.get("YT_COOKIES") | |
| if not data: | |
| return None | |
| path = os.path.join(workdir, "cookies.txt") | |
| with open(path, "w", encoding="utf-8") as fh: | |
| fh.write(data) | |
| return path | |
| def _ranking_rows(scored: list[dict]) -> list[list]: | |
| rows = [] | |
| for rank, v in enumerate(scored, start=1): | |
| rows.append([ | |
| rank, | |
| v.get("title", v["video_id"]), | |
| f"{v['positive_share'] * 100:.0f}%", | |
| v.get("n_comments", 0), | |
| v.get("note", "") or "ok", | |
| v["url"], | |
| ]) | |
| return rows | |
| def _safe_name(text: str) -> str: | |
| return re.sub(r"[^A-Za-z0-9._-]+", "_", text).strip("_")[:60] or "tutorial" | |
| def run_pipeline(topic, hf_token, llm_model, vlm_model, w_llm, w_whisper, lead, | |
| max_minutes, max_shots, progress=gr.Progress()): | |
| """Generator that yields (status_md, ranking_df, transcript, docx_file).""" | |
| log: list[str] = [] | |
| def status(msg: str): | |
| log.append(msg) | |
| return "\n\n".join(log) | |
| topic = (topic or "").strip() | |
| if not topic: | |
| raise gr.Error("Please enter a topic.") | |
| if not (hf_token or "").strip(): | |
| raise gr.Error("Please paste your Hugging Face token (used for the LLM + vision model).") | |
| workdir = tempfile.mkdtemp(prefix="ytt_") | |
| frames_dir = os.path.join(workdir, "frames") | |
| video_path = None | |
| try: | |
| cookiefile = _cookiefile(workdir) | |
| # 1. Search ------------------------------------------------------------------ | |
| progress(0.02, desc="Searching") | |
| yield status(f"🔍 Searching top videos for **{topic}**…"), gr.update(), gr.update(), gr.update() | |
| videos = search_mod.search_top5(topic) | |
| yield status(f"Found {len(videos)} candidate videos."), gr.update(), gr.update(), gr.update() | |
| # 2. Sentiment ranking ------------------------------------------------------- | |
| yield status("💬 Fetching comments and scoring sentiment…"), gr.update(), gr.update(), gr.update() | |
| best, scored = sentiment_mod.rank_by_sentiment(videos, cookiefile, progress) | |
| ranking = gr.update(value=_ranking_rows(scored)) | |
| yield (status(f"🏆 Picked **{best.get('title', best['video_id'])}** " | |
| f"({best['positive_share'] * 100:.0f}% positive)."), | |
| ranking, gr.update(), gr.update()) | |
| # 3. Download + audio -------------------------------------------------------- | |
| progress(0.25, desc="Downloading") | |
| yield status("⬇️ Downloading the chosen video…"), ranking, gr.update(), gr.update() | |
| video_path, duration = download_mod.download_video( | |
| best["url"], workdir, cookiefile, int(max_minutes)) | |
| wav = download_mod.extract_audio(video_path, workdir) | |
| # 4. Transcribe -------------------------------------------------------------- | |
| progress(0.4, desc="Transcribing") | |
| yield status("📝 Transcribing with Whisper (this is the slow part on CPU)…"), ranking, gr.update(), gr.update() | |
| segs = transcribe_mod.transcribe(wav, progress) | |
| transcript = transcribe_mod.transcript_text(segs) | |
| yield (status(f"Transcript ready ({len(segs)} segments)."), | |
| ranking, gr.update(value=transcript), gr.update()) | |
| # 5. Candidate frames, then DELETE the video -------------------------------- | |
| progress(0.6, desc="Extracting frames") | |
| candidates = frames_mod.extract_candidates(video_path, frames_dir, duration) | |
| frames_mod.delete_video(video_path) | |
| video_path = None | |
| yield (status(f"🎞️ Extracted {len(candidates)} candidate frames and " | |
| f"**deleted the downloaded video**."), | |
| ranking, gr.update(value=transcript), gr.update()) | |
| # 6. Tutorial text ----------------------------------------------------------- | |
| progress(0.72, desc="Writing tutorial") | |
| yield status(f"🤖 Generating tutorial with `{llm_model}`…"), ranking, gr.update(value=transcript), gr.update() | |
| tut = tutorial_mod.generate_tutorial(transcript, hf_token.strip(), llm_model) | |
| # 7. Weighted screenshot selection ------------------------------------------ | |
| selected = frames_mod.select_screenshots( | |
| tut["steps"], segs, candidates, | |
| w_llm=float(w_llm), w_whisper=float(w_whisper), lead=float(lead), | |
| max_shots=int(max_shots), | |
| ) | |
| yield (status(f"🖼️ Selected {len(selected)} screenshots via the weighted indicator."), | |
| ranking, gr.update(value=transcript), gr.update()) | |
| # 8. Captions ---------------------------------------------------------------- | |
| progress(0.85, desc="Captioning") | |
| yield status(f"✍️ Captioning screenshots with `{vlm_model}`…"), ranking, gr.update(value=transcript), gr.update() | |
| caps = captions_mod.caption_frames(selected, tut["steps"], hf_token.strip(), vlm_model, progress) | |
| # 9. DOCX -------------------------------------------------------------------- | |
| progress(0.95, desc="Building document") | |
| out_path = os.path.join(workdir, f"{_safe_name(tut['title'])}.docx") | |
| docx_builder.build_docx(tut, selected, caps, out_path, source_url=best["url"]) | |
| progress(1.0, desc="Done") | |
| yield (status("✅ Done! Download your tutorial below."), | |
| ranking, gr.update(value=transcript), gr.update(value=out_path)) | |
| except gr.Error: | |
| raise | |
| except (download_mod.DownloadError, RuntimeError, ValueError) as exc: | |
| raise gr.Error(str(exc)) | |
| finally: | |
| # Always remove the video if it somehow survived; keep frames/docx until the | |
| # response is sent (Gradio copies the returned file out). | |
| if video_path: | |
| frames_mod.delete_video(video_path) | |
| def build_ui(): | |
| with gr.Blocks(title="YouTube → Tutorial Post") as demo: | |
| gr.Markdown( | |
| "# 📝 YouTube → Tutorial Post Generator\n" | |
| "Enter a topic and your Hugging Face token. The Space picks the best video, " | |
| "transcribes it, and builds a **captioned `.docx` tutorial**. Your token is " | |
| "used only for the LLM + vision-model calls and **billed to your account**." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| topic = gr.Textbox(label="Topic", placeholder="e.g. Excel pivot tables for beginners") | |
| hf_token = gr.Textbox(label="Hugging Face token", type="password", | |
| placeholder="hf_… (Inference Providers permission)") | |
| with gr.Column(scale=1): | |
| llm_model = gr.Dropdown(LLM_CHOICES, value=LLM_CHOICES[0], | |
| label="Tutorial LLM", allow_custom_value=True) | |
| vlm_model = gr.Dropdown(VLM_CHOICES, value=VLM_CHOICES[0], | |
| label="Vision model (captions)", allow_custom_value=True) | |
| with gr.Accordion("Advanced settings", open=False): | |
| with gr.Row(): | |
| w_llm = gr.Slider(0.0, 1.0, value=0.4, step=0.05, label="Weight: LLM timestamp") | |
| w_whisper = gr.Slider(0.0, 1.0, value=0.6, step=0.05, label="Weight: Whisper timing") | |
| lead = gr.Slider(0.0, 5.0, value=1.0, step=0.5, label="Lead offset (s)") | |
| with gr.Row(): | |
| max_minutes = gr.Slider(2, 60, value=20, step=1, label="Max video length (min)") | |
| max_shots = gr.Slider(1, 15, value=8, step=1, label="Max screenshots") | |
| run_btn = gr.Button("Generate tutorial", variant="primary") | |
| status_md = gr.Markdown(label="Status") | |
| ranking_df = gr.Dataframe( | |
| headers=["#", "Title", "Positive", "Comments", "Note", "URL"], | |
| label="Sentiment ranking", interactive=False, wrap=True, | |
| ) | |
| transcript_box = gr.Textbox(label="Transcript preview", lines=10, max_lines=20, show_copy_button=True) | |
| docx_file = gr.File(label="Download tutorial (.docx)") | |
| run_btn.click( | |
| run_pipeline, | |
| inputs=[topic, hf_token, llm_model, vlm_model, w_llm, w_whisper, lead, max_minutes, max_shots], | |
| outputs=[status_md, ranking_df, transcript_box, docx_file], | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| build_ui().queue().launch() | |