All articles
Build Guides

Build a YouTube-to-Blog Repurposing Agent Using Whisper and Gemini Free Tier

FDE Coach EditorialJuly 14, 202611 min read

What We're Building and Why It Matters

We're building a single Python script that accepts a YouTube URL and outputs a formatted, publication-ready blog post. No manual transcription. No copy-pasting into a chatbot. Just a pipeline that respects the free tiers of every tool involved.

As a Forward Deployed Engineer, you're constantly looking for leverage. Content repurposing is a high-signal automation target because it bridges the gap between video-first audiences and text-first SEO traffic. This agent gives you a repeatable asset you can schedule, chain into a CMS, or hook into an n8n workflow.

Feature list:

  • Downloads audio-only from any public YouTube video using yt-dlp
  • Transcribes speech to text with OpenAI Whisper (local, open-source, no API costs)
  • Generates a structured blog post with headings, a meta description, and a compelling intro using Google Gemini's free tier
  • Handles videos up to ~30 minutes comfortably within free-tier rate limits
  • Outputs clean Markdown you can paste directly into a CMS

Architecture: The Audio-to-Article Pipeline

The pipeline is linear, but each stage is isolated so you can swap components later. Think of it as three pure functions chained together with a tiny orchestration layer.

Whisper runs locally—your machine does the heavy lifting on STT. Gemini handles the structured generation. No GPU required for Whisper if you use the base or small model, though medium gives better accuracy at the cost of RAM.

Prerequisites and Free-Tier Setup

Everything here is free. No credit card needed for Whisper. Google Gemini's free tier gives you 1,500 requests per day with the gemini-1.5-flash model—more than enough for personal use.

What you need installed:

  • Python 3.10+python.org/downloads
  • yt-dlppip install yt-dlp (also requires ffmpeg; install via brew install ffmpeg on macOS or apt install ffmpeg on Linux)
  • openai-whisperpip install openai-whisper
  • google-generativeaipip install google-generativeai
  • A Google AI Studio API key — Get yours at aistudio.google.com/apikey. Free tier, no billing setup required.

Set your API key as an environment variable:

export GEMINI_API_KEY="your-key-here"

Whisper will download the model on first run. The base model is ~142MB; small is ~466MB. Both run fine on CPU.

Step 1: Extracting YouTube Audio with yt-dlp

yt-dlp is the Swiss Army knife for video downloading. We'll extract audio-only in a format Whisper can consume directly—16kHz mono WAV is ideal, but .m4a works and avoids an extra conversion step.

import subprocess
import os

def download_audio(youtube_url: str, output_dir: str = "./audio") -> str:
    """
    Downloads audio from a YouTube video and returns the file path.
    Uses yt-dlp to extract audio in m4a format.
    """
    os.makedirs(output_dir, exist_ok=True)
    
    # yt-dlp template: output as video title, audio-only
    output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
    
    cmd = [
        "yt-dlp",
        "-f", "bestaudio[ext=m4a]",  # best audio quality in m4a container
        "-o", output_template,
        "--no-playlist",
        "--extract-audio",
        youtube_url
    ]
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    
    if result.returncode != 0:
        raise RuntimeError(f"yt-dlp failed: {result.stderr}")
    
    # Find the downloaded file by listing the output directory
    files = os.listdir(output_dir)
    audio_files = [f for f in files if f.endswith(".m4a")]
    
    if not audio_files:
        raise FileNotFoundError("No audio file found after download")
    
    # Return the most recently created file (handles title collisions)
    audio_files.sort(key=lambda f: os.path.getctime(os.path.join(output_dir, f)), reverse=True)
    return os.path.join(output_dir, audio_files[0])

Why m4a and not wav? Whisper handles m4a natively. Skipping the wav conversion saves disk I/O and removes an ffmpeg subprocess call. If you hit format issues, fall back to --audio-format wav.

Step 2: Transcribing Audio with OpenAI Whisper

Whisper is a local model. No API calls, no rate limits. Load it once, transcribe many files. The small model hits the sweet spot between accuracy and speed on CPU.

import whisper

def transcribe_audio(audio_path: str, model_size: str = "small") -> str:
    """
    Transcribes an audio file using OpenAI Whisper.
    Returns the full transcript text.
    """
    # Load model (cached after first run)
    model = whisper.load_model(model_size)
    
    # Transcribe with default settings
    result = model.transcribe(audio_path)
    
    return result["text"]

For videos over 30 minutes, consider the tiny model for speed or chunk the audio manually. The free tier pipeline works best for videos under 25 minutes—typical for tutorials, talks, and vlogs.

Performance note: On an M1 Mac, small transcribes ~1 minute of audio per 10-15 seconds of wall-clock time. A 20-minute video takes ~3-4 minutes to transcribe. Acceptable for a one-shot script; optimize with faster-whisper if you're batching.

Step 3: Generating the Blog Post with Google Gemini

This is where the transcript becomes a blog post. We'll craft a prompt that forces structured output: a meta description, an H1 title, section headings, and body content. Gemini 1.5 Flash is fast and free-tier-friendly.

import google.generativeai as genai
import os

def generate_blog_post(transcript: str, video_title: str = "") -> str:
    """
    Generates a formatted blog post from a transcript using Gemini.
    Returns Markdown suitable for a CMS.
    """
    genai.configure(api_key=os.environ["GEMINI_API_KEY"])
    
    model = genai.GenerativeModel("gemini-1.5-flash")
    
    prompt = f"""You are a professional technical content writer. Convert the following YouTube video transcript into a well-structured blog post.

Video title: {video_title}

Requirements:
- Start with an SEO-friendly meta description (150-160 characters)
- Write a compelling H1 title
- Include 3-5 H2 sections with substantive paragraphs
- Use bullet points where appropriate
- End with a clear conclusion or call-to-action
- Output in clean Markdown format
- Do NOT include "Meta Description:" as a label; just output the description on its own line first

Transcript:
{transcript[:30000]}  # Truncate to fit context window if needed

Blog post:"""

    response = model.generate_content(prompt)
    return response.text

Why truncate at 30,000 characters? Flash's free tier context window is large (1M tokens), but transcripts longer than ~30k chars can produce rambling outputs. Truncating keeps the prompt focused. For longer videos, consider a map-reduce pattern: summarize chunks, then generate from the summary.

Step 4: The Complete Agent Script

Here's the full orchestrator. Save it as youtube_to_blog.py.

#!/usr/bin/env python3
"""
YouTube-to-Blog Repurposing Agent
Free tier pipeline: yt-dlp -> Whisper -> Gemini
"""

import subprocess
import os
import sys
import whisper
import google.generativeai as genai


def download_audio(youtube_url: str, output_dir: str = "./audio") -> str:
    os.makedirs(output_dir, exist_ok=True)
    output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
    
    cmd = [
        "yt-dlp",
        "-f", "bestaudio[ext=m4a]",
        "-o", output_template,
        "--no-playlist",
        "--extract-audio",
        youtube_url
    ]
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"yt-dlp failed: {result.stderr}")
    
    files = [f for f in os.listdir(output_dir) if f.endswith(".m4a")]
    if not files:
        raise FileNotFoundError("No audio file found")
    
    files.sort(key=lambda f: os.path.getctime(os.path.join(output_dir, f)), reverse=True)
    return os.path.join(output_dir, files[0])


def transcribe_audio(audio_path: str, model_size: str = "small") -> str:
    model = whisper.load_model(model_size)
    result = model.transcribe(audio_path)
    return result["text"]


def generate_blog_post(transcript: str, video_title: str = "") -> str:
    genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
    model = genai.GenerativeModel("gemini-1.5-flash")
    
    prompt = f"""You are a professional technical content writer. Convert the following YouTube video transcript into a well-structured blog post.

Video title: {video_title}

Requirements:
- Start with an SEO-friendly meta description (150-160 characters)
- Write a compelling H1 title
- Include 3-5 H2 sections with substantive paragraphs
- Use bullet points where appropriate
- End with a clear conclusion or call-to-action
- Output in clean Markdown format
- Do NOT include "Meta Description:" as a label; just output the description on its own line first

Transcript:
{transcript[:30000]}

Blog post:"""
    
    response = model.generate_content(prompt)
    return response.text


def main():
    if len(sys.argv) < 2:
        print("Usage: python youtube_to_blog.py <youtube_url>")
        sys.exit(1)
    
    youtube_url = sys.argv[1]
    
    print("📥 Downloading audio...")
    audio_path = download_audio(youtube_url)
    print(f"   Saved to: {audio_path}")
    
    print("🎙️  Transcribing with Whisper...")
    transcript = transcribe_audio(audio_path)
    print(f"   Transcript length: {len(transcript)} characters")
    
    print("✍️  Generating blog post with Gemini...")
    video_title = os.path.basename(audio_path).replace(".m4a", "")
    blog_post = generate_blog_post(transcript, video_title)
    
    output_path = audio_path.replace(".m4a", "_blog.md")
    with open(output_path, "w") as f:
        f.write(blog_post)
    
    print(f"✅ Blog post saved to: {output_path}")
    print("\n--- Preview (first 500 chars) ---")
    print(blog_post[:500])


if __name__ == "__main__":
    main()

Running the Pipeline End-to-End

  1. Set your API key:

    export GEMINI_API_KEY="AIza..."
    
  2. Install dependencies:

    pip install yt-dlp openai-whisper google-generativeai
    
  3. Run the script:

    python youtube_to_blog.py "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    
  4. Output: A .md file appears in ./audio/ next to the downloaded audio. Open it in any Markdown editor or paste directly into your CMS.

Expected runtime for a 15-minute video: ~2 minutes on an M1 Mac (mostly transcription). The Gemini API call takes <5 seconds.

Extensions: From Script to Production Agent

This script is a solid foundation. Here's how to productionize it:

  • Batch processing: Wrap the script in a loop that reads URLs from a CSV or Airtable base. Add a --csv flag.
  • n8n integration: Trigger this script via an n8n Webhook node. The output can feed directly into a WordPress or Ghost CMS node. This pattern is similar to the automation flow we covered in our guide on building a WhatsApp customer support agent with n8n and Qdrant.
  • Speaker diarization: Add pyannote.audio to label speakers in the transcript. Gemini can then format interviews as Q&A blog posts.
  • Thumbnail extraction: Use yt-dlp --write-thumbnail to grab the video thumbnail for the blog's featured image.
  • Screenshot-to-code repurposing: If the video contains code demos, you could chain this with a screenshot-to-code agent using a free vision model to extract and format code blocks automatically.
  • Auto-publish to Medium/Dev.to: Use their APIs to post directly from the script. Add a --publish flag.

For FDEs looking to build more agent workflows like this, the skill of chaining free-tier APIs into production pipelines is one of the highest-leverage skills in the AI era. It's not about prompt engineering—it's about system design with LLMs as components.

Common Pitfalls and Debugging Tips

PitfallSymptomFix
yt-dlp not foundFileNotFoundError or command not recognizedInstall with pip install yt-dlp and ensure it's in PATH
ffmpeg missingyt-dlp downloads video instead of audiobrew install ffmpeg (macOS) or apt install ffmpeg (Linux)
Whisper OOMKilled process during transcriptionUse "base" or "tiny" model instead of "small"
Gemini API 429Rate limit errorFree tier: 1,500 requests/day, 15 RPM. Add time.sleep(5) between runs
Garbled transcriptVideo has heavy background music or multiple speakersPre-process audio with ffmpeg noise reduction, or use whisper with --task transcribe --language en
Blog post cuts off mid-sentenceTranscript too long for promptIncrease transcript[:30000] to [:50000] or implement chunking
API key not foundKeyError on os.environRun export GEMINI_API_KEY="..." in your terminal, not just in the script

FAQ

Q: Is this really completely free? A: Yes. Whisper runs locally (no API costs). yt-dlp is open-source. Gemini's free tier gives 1,500 requests/day—enough for 50+ blog posts daily. You'll never hit the limit in personal use.

Q: Can I use this for videos in other languages? A: Yes. Whisper auto-detects language and transcribes ~100 languages. Gemini 1.5 Flash handles multilingual generation. For non-English output, add "Write the blog post in [language]" to the prompt.

Q: What's the maximum video length this can handle? A: Whisper has no hard limit, but RAM usage scales with audio length. The small model handles ~45 minutes on 16GB RAM. Gemini's context window is huge (1M tokens), but we truncate at 30k chars for quality. For 2-hour videos, implement a map-reduce summarization step before generation.

Q: How do I improve blog post quality? A: The prompt is everything. Add style guides, tone instructions, or example posts as few-shot examples in the prompt. You can also pass the video's description and comments (via YouTube API) as additional context.

Q: Can I deploy this as a web app? A: Absolutely. Wrap it in a FastAPI endpoint, add a simple HTML form, and deploy on Railway or Render's free tier. The Whisper model will need to live on the server, so allocate at least 1GB RAM.

Q: What if I want to break into building agents like this professionally? A: The pipeline thinking demonstrated here—chaining tools, handling failure modes, designing for free-tier constraints—is exactly what FDE roles demand. If you're coming from a backend or frontend background, the transition is more about system design mindset than learning new frameworks. We've written about how to break into FDE roles from traditional engineering backgrounds.

#content-repurposing#whisper#gemini-api#automation

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now