All articles
Build Guides

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

FDE Coach EditorialJuly 22, 20269 min read

What We're Building

A single Python script that takes a YouTube URL and outputs a publication-ready blog post. No paid APIs, no GPU instances, no nonsense.

Feature list:

  • Downloads audio from any public YouTube video using yt-dlp
  • Runs local transcription via OpenAI Whisper (base model – fast, free, runs on CPU)
  • Chunks long transcripts and feeds them to Groq’s free-tier LLM
  • Outputs a structured markdown blog post with title, summary, and section headings
  • Handles videos up to 2 hours without OOMing
  • Entire pipeline runs on a free Colab notebook or your laptop

This is the same pattern I used when a client needed to turn 50 podcast episodes into SEO pages in a weekend. Let's build it.

Architecture Overview

The flow is linear but each stage is isolated. This matters because you can swap Whisper for Groq's hosted Whisper endpoint if you need speed, or swap Groq for a local Ollama model if you need total airgap. The chunker is the unsung hero – it prevents context window blowouts and keeps the LLM output coherent across long transcripts.

Prerequisites (All Free)

ToolPurposeFree Tier LimitLink
Python 3.10+RuntimeUnlimitedpython.org
yt-dlpAudio downloadUnlimitedgithub.com/yt-dlp/yt-dlp
OpenAI WhisperLocal transcriptionUnlimited (CPU)github.com/openai/whisper
Groq APILLM structuring30 req/min, ~7k tokens/reqconsole.groq.com
ffmpegAudio processing (yt-dlp dep)Unlimitedffmpeg.org

No credit card needed for any of these. Groq's free tier gives you access to Llama 3.1 70B and Mixtral 8x7B – more than enough for structuring text. Sign up at console.groq.com, generate an API key, and you're in.

If you're new to building agents that automate content workflows, this pipeline shares DNA with the GitHub Issue Triager we built previously – same pattern of free LLM + structured output, different domain.

Step 1: Environment Setup

# Create and activate a virtual environment
python -m venv yt2blog
source yt2blog/bin/activate  # Windows: yt2blog\Scripts\activate

# Install dependencies
pip install yt-dlp openai-whisper groq langchain

# ffmpeg is required by yt-dlp for audio extraction
# macOS: brew install ffmpeg
# Ubuntu: sudo apt install ffmpeg
# Windows: choco install ffmpeg

Set your Groq API key as an environment variable:

export GROQ_API_KEY="gsk_your_key_here"

Step 2: Download Audio with yt-dlp

We extract audio only – no video download. The bestaudio format selector grabs the highest quality audio stream, and we convert to mp3 for Whisper compatibility.

import yt_dlp
import os

def download_audio(youtube_url: str, output_dir: str = "audio") -> str:
    """Download audio from YouTube, return path to mp3 file."""
    os.makedirs(output_dir, exist_ok=True)
    
    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
        'outtmpl': f'{output_dir}/%(title)s.%(ext)s',
        'quiet': True,
        'no_warnings': True,
    }
    
    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(youtube_url, download=True)
        title = info.get('title', 'output')
        # yt-dlp sanitizes filenames internally
        sanitized_title = title.replace('/', '_').replace('\\', '_')
        return f"{output_dir}/{sanitized_title}.mp3"

Why mp3 and not wav? Whisper resamples everything to 16kHz mono internally. A 192kbps mp3 is indistinguishable from lossless for speech transcription and saves 90% disk space on long videos.

Step 3: Transcribe with OpenAI Whisper

We use the base model – 74M parameters, ~1GB RAM, transcribes 30 minutes of audio in ~2 minutes on a modern CPU. For production pipelines on longer content, bump to small if you have RAM headroom.

import whisper

def transcribe_audio(audio_path: str, model_size: str = "base") -> str:
    """Transcribe mp3 file to text using local Whisper."""
    model = whisper.load_model(model_size)
    result = model.transcribe(
        audio_path,
        fp16=False,  # CPU inference
        language='en',  # Set to None for auto-detect
        verbose=False
    )
    return result['text']

The fp16=False flag is critical on CPU. Without it, Whisper tries to load weights in half-precision and falls over on machines without CUDA. This single flag has burned more engineers than any other Whisper config.

If you're transcribing non-English content, set language=None and Whisper will auto-detect from the first 30 seconds. The base model supports 99 languages.

Step 4: Structure the Blog Post with Groq

This is where the free LLM earns its keep. Raw transcripts are walls of text with filler words, false starts, and zero structure. We chunk the transcript (8,000 characters per chunk with 500-char overlap), process each chunk through Groq, then stitch the structured output.

from groq import Groq
import json

def chunk_text(text: str, chunk_size: int = 8000, overlap: int = 500) -> list[str]:
    """Split transcript into overlapping chunks."""
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

def structure_transcript(transcript: str, groq_api_key: str) -> str:
    """Convert raw transcript to structured blog post via Groq."""
    client = Groq(api_key=groq_api_key)
    chunks = chunk_text(transcript)
    
    blog_sections = []
    
    for i, chunk in enumerate(chunks):
        system_prompt = """You are a technical blog editor. Convert the following transcript 
segment into well-structured markdown. Preserve all technical details, remove filler 
words, and organize into logical sections with ## headings. Do not add information 
not present in the transcript. Output raw markdown."""
        
        response = client.chat.completions.create(
            model="llama-3.1-70b-versatile",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"Transcript segment {i+1}/{len(chunks)}:\n\n{chunk}"}
            ],
            temperature=0.3,
            max_tokens=4096,
        )
        blog_sections.append(response.choices[0].message.content)
    
    return "\n\n".join(blog_sections)

Model choice: llama-3.1-70b-versatile on Groq runs at ~250 tokens/second on the free tier. For shorter videos, mixtral-8x7b-32768 gives you a 32k context window and can handle the entire transcript in one shot, skipping the chunker entirely.

Temperature 0.3 keeps the output factual. Transcript structuring is not creative writing – you want consistent section headers and no hallucinated content.

Step 5: The Complete Orchestrator

Wire everything together with a clean main function:

import os
import sys

def youtube_to_blog(youtube_url: str, output_path: str = "blog_post.md"):
    """End-to-end: YouTube URL -> structured blog post."""
    groq_key = os.environ.get("GROQ_API_KEY")
    if not groq_key:
        raise ValueError("Set GROQ_API_KEY environment variable")
    
    print(f"[1/3] Downloading audio from {youtube_url}...")
    audio_path = download_audio(youtube_url)
    
    print(f"[2/3] Transcribing with Whisper (base)...")
    transcript = transcribe_audio(audio_path)
    print(f"      Transcript length: {len(transcript)} characters")
    
    print(f"[3/3] Structuring with Groq...")
    blog_post = structure_transcript(transcript, groq_key)
    
    with open(output_path, 'w') as f:
        f.write(blog_post)
    
    print(f"Done! Blog post saved to {output_path}")
    return blog_post

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python yt2blog.py <youtube_url>")
        sys.exit(1)
    youtube_to_blog(sys.argv[1])

Running the Pipeline

# Make sure your Groq key is set
export GROQ_API_KEY="gsk_..."

# Run on any YouTube video
python yt2blog.py "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

# Output: blog_post.md in current directory

Expected runtime on a 2023 MacBook Air (M2, 8GB):

  • 10-minute video: ~90 seconds total
  • 60-minute video: ~8 minutes total (transcription dominates)
  • 2-hour video: ~20 minutes

If you're on a Colab free tier instance, expect 2-3x slower transcription but identical LLM performance since Groq runs on their infrastructure.

For a deeper look at building agents that automate real workflows, the Competitor Monitoring Agent guide walks through a similar pattern with Playwright and scheduled execution.

Sensible Extensions

Add speaker diarization: Swap whisper.transcribe for whisper.transcribe with word_timestamps=True, then use pyannote.audio (free on Hugging Face) to label speakers. Output becomes interview-style with "Speaker A:" and "Speaker B:" prefixes.

Batch processing: Wrap the orchestrator in a loop over a CSV of YouTube URLs. Add a tqdm progress bar and parallelize transcription with concurrent.futures – Groq handles concurrent LLM calls natively.

SEO metadata generation: Add a second Groq call that takes the final blog post and outputs title, meta description, and 5 tags. Feed that into your CMS API.

Direct publishing: Wire the output to the WordPress REST API or a static site generator. One function call from YouTube URL to live blog post.

YouTube chapter extraction: Parse info['chapters'] from yt-dlp's metadata and use those timestamps to segment the transcript before structuring. Results in perfectly aligned blog sections.

Common Pitfalls

"Whisper OOM on long audio" – Whisper loads the entire audio file into memory. For videos over 2 hours, use pydub to split the mp3 into 30-minute segments before transcription, then concatenate transcripts.

"Groq rate limit hit" – The free tier is 30 requests per minute. If processing many chunks, add time.sleep(2) between Groq calls. For batch jobs, process during off-peak hours (UTC 00:00-08:00).

"Transcript has repeated phrases" – Whisper's base model sometimes hallucinates repetitions on quiet audio. Bump to the small model or preprocess audio with a noise gate via pydub.

"yt-dlp fails on age-restricted videos" – Add 'cookiefile': 'cookies.txt' to ydl_opts and export your browser cookies. Legal for your own content, check ToS for third-party videos.

"Blog post reads like a transcript" – Crank Groq temperature to 0.5 and add "Rewrite conversationally, remove all um/ah filler" to the system prompt. The chunker overlap prevents context loss at boundaries.

If you're thinking about turning this into a product, the FDE to Founder piece covers why pipeline-building skills translate directly to startup velocity.

FAQ

Q: Why local Whisper instead of Groq's hosted Whisper endpoint? A: Groq offers hosted Whisper but it counts against your rate limit and has per-file size caps. Local Whisper is truly unlimited and offline-capable. For a batch of 100 videos, local is the clear winner.

Q: Can I use this for non-English content? A: Yes. Set language=None in the Whisper call and update the Groq system prompt to specify the target language. The base model handles 99 languages.

Q: What if the video has no speech? A: Whisper returns an empty string. Add a guard clause after transcription: if len(transcript.strip()) < 100: raise ValueError("No speech detected").

Q: How do I deploy this as an API? A: Wrap the orchestrator in a FastAPI endpoint, add Redis for job queuing, and deploy on Railway or Render free tiers. The GitHub Issue Triager guide covers a similar FastAPI deployment pattern.

Q: Is this legal for any YouTube video? A: Downloading content you don't own may violate YouTube's ToS. This tool is intended for your own content or content you have explicit permission to repurpose. Always check the video's license.

Q: What's the max video length this handles? A: With the chunker, effectively unlimited. I've processed 4-hour livestreams. The bottleneck is Whisper's memory usage on the full audio file – for videos over 2 hours, pre-split the audio into 30-minute segments.

Q: Can I use Ollama instead of Groq? A: Absolutely. Swap the Groq client for LangChain's Ollama integration pointing at a local Llama 3.1 instance. Same prompt, same output quality, zero API dependency. Slower on CPU but fully airgapped.


This pipeline is the foundation of several production content repurposing systems I've built for clients. The pattern scales from one video to thousands. Start with one, measure the output quality, then automate the rest.

#content-repurposing#youtube#whisper#blogging

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