All articles
Build Guides

Build a YouTube-to-Blog Repurposing Agent with Whisper, Groq, and Cloudflare Workers

FDE Coach EditorialAugust 3, 202610 min read

What We're Building

We're engineering a fully automated content repurposing agent that takes a YouTube URL and outputs a formatted, SEO-friendly blog post. No manual transcription, no expensive SaaS subscriptions. The pipeline runs entirely on free-tier services and open-source tools.

Feature list:

  • Accepts any public YouTube URL
  • Downloads audio stream only (no video fetch, saving bandwidth)
  • Transcribes speech to text using OpenAI's open-source Whisper model
  • Generates a structured blog post (headline, introduction, key takeaways, body, conclusion) via Groq's free LLM API
  • Deployed as a serverless Cloudflare Worker—no persistent server costs
  • Returns the final blog post as JSON or plain markdown

This isn't just a script; it's a production-pattern pipeline you can extend to podcasts, webinars, or internal meeting recordings. If you've ever needed to scale yourself by automating the tedious parts of content creation, this architecture mirrors the kind of rapid prototyping we teach at FDE Coach—turning a messy customer problem into a shipped prototype in a single week.

Architecture Overview

Before we write code, let's trace the request path. The flow is linear but involves three distinct compute environments: a Cloudflare Worker (edge runtime), a local or containerized environment for yt-dlp/Whisper, and Groq's inference API.

Important constraint: Cloudflare Workers have a CPU time limit (10ms-50ms free tier) and cannot run native binaries like ffmpeg or Python. Therefore, we split the workload. The Worker acts as an orchestrator: it triggers a separate compute step for downloading/transcribing, or we use a pre-built container. For this guide, we'll run the yt-dlp and Whisper steps locally (or on a free CI runner) and expose the result to the Worker, which then calls Groq. This keeps everything free and deployable.

Prerequisites (All Free Tier)

You need accounts and API keys. Everything here has a generous free tier—no credit card required for most.

ToolPurposeFree Tier LimitSignup Link
Cloudflare WorkersServerless deployment100,000 requests/dayworkers.cloudflare.com
Groq CloudLLM inference (Llama 3, Mixtral)~30 requests/min, free creditsconsole.groq.com
Python 3.10+Local script runtimeN/A (open source)python.org
yt-dlpYouTube audio extractionOpen sourcegithub.com/yt-dlp/yt-dlp
OpenAI WhisperSpeech-to-textOpen source (local GPU/CPU)github.com/openai/whisper
Node.js + npmWrangler CLIOpen sourcenodejs.org

Install the local tools:

# Install yt-dlp
pip install yt-dlp

# Install Whisper (CPU version, or use 'openai-whisper' for GPU)
pip install openai-whisper

# Install Wrangler CLI for Cloudflare Workers
npm install -g wrangler

Step 1: Project Setup and Wrangler Configuration

Create a new directory and initialize a Cloudflare Worker project.

mkdir youtube-blog-agent
cd youtube-blog-agent
wrangler init

Choose "Hello World" Worker, TypeScript (or JavaScript—I'll use TypeScript for type safety). This generates a wrangler.toml and a src/index.ts.

Edit wrangler.toml to add your Groq API key as a secret (never hardcode keys):

name = "youtube-blog-agent"
main = "src/index.ts"
compatibility_date = "2024-10-15"

[vars]
# Public vars only; secrets set via CLI

Set your Groq API key:

wrangler secret put GROQ_API_KEY
# Paste your key from console.groq.com

Step 2: Downloading YouTube Audio with yt-dlp

We need a script that takes a YouTube URL, extracts the audio as a WAV file (16kHz mono, which Whisper expects), and saves it to disk. This runs locally or in a GitHub Actions runner.

Create download_audio.py:

import sys
import subprocess
import os

def download_audio(youtube_url: str, output_path: str = "audio.wav"):
    """
    Downloads audio from a YouTube video and converts to 16kHz mono WAV.
    """
    cmd = [
        "yt-dlp",
        "-f", "bestaudio",
        "--extract-audio",
        "--audio-format", "wav",
        "--audio-quality", "0",
        "--postprocessor-args", "-ar 16000 -ac 1",
        "-o", output_path,
        youtube_url
    ]
    subprocess.run(cmd, check=True)
    print(f"Audio saved to {output_path}")

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

Why 16kHz mono? Whisper's base model expects this sample rate; resampling during postprocessing avoids runtime errors and speeds up inference.

Step 3: Transcribing Audio with OpenAI Whisper

Create transcribe.py:

import sys
import whisper

def transcribe_audio(audio_path: str, model_size: str = "base") -> str:
    """
    Loads Whisper model and transcribes the audio file.
    Returns full transcript text.
    """
    model = whisper.load_model(model_size)
    result = model.transcribe(audio_path)
    return result["text"]

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python transcribe.py <audio.wav>")
        sys.exit(1)
    transcript = transcribe_audio(sys.argv[1])
    # Output to stdout so we can pipe or capture
    print(transcript)

Model sizing: base is ~142MB, runs on CPU in reasonable time for videos under 30 minutes. For longer content, use small or medium on a machine with a GPU (even a Colab free GPU works). The tradeoff is accuracy vs. speed—base is sufficient for clear English speech.

Step 4: Generating a Blog Post with Groq's LLM

Groq offers free access to Llama 3.1 8B and Mixtral 8x7B with blazing-fast inference. We'll craft a prompt that structures the raw transcript into a blog post.

Create a helper function inside your Worker (src/index.ts):

interface BlogPost {
  title: string;
  excerpt: string;
  body: string;
}

async function generateBlogPost(transcript: string, groqKey: string): Promise<BlogPost> {
  const systemPrompt = `You are an expert content strategist and technical writer. 
Given a raw transcript from a YouTube video, produce a well-structured blog post in Markdown.

Output MUST be valid JSON with keys: "title", "excerpt", "body".
- title: SEO-optimized, 50-70 characters
- excerpt: compelling meta description, 150-160 characters
- body: full markdown with H2/H3 headings, bullet points, and a FAQ section if relevant.

Do not include any text outside the JSON object.`;

  const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${groqKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "llama-3.1-8b-instant",
      messages: [
        { role: "system", content: systemPrompt },
        { role: "user", content: `Transcript:\n\n${transcript}` }
      ],
      temperature: 0.7,
      max_tokens: 4096
    })
  });

  if (!response.ok) {
    throw new Error(`Groq API error: ${response.statusText}`);
  }

  const data: any = await response.json();
  const content = data.choices[0].message.content;
  // Parse the JSON string from the LLM response
  return JSON.parse(content);
}

Why enforce JSON output? LLMs are non-deterministic. By constraining the output format in the system prompt, we avoid parsing nightmares downstream. This is a constraint injection pattern we explore deeper in Structuring Financial Prompts: How Constraint Injection Unlocks Useful LLM Advice Without Hallucination.

Step 5: Assembling the Cloudflare Worker

Now we wire everything into a single Worker endpoint. Since Workers can't run yt-dlp or Whisper natively, our Worker expects the transcript as input (from a local step). In production, you'd trigger a background job via Cloudflare Queues or a webhook, but for this free-tier build, we accept a POST with a YouTube URL, run the local scripts manually, then send the transcript to the Worker for blog generation.

Full src/index.ts:

export interface Env {
  GROQ_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // POST /generate expects { transcript: string }
    if (url.pathname === "/generate" && request.method === "POST") {
      try {
        const body: any = await request.json();
        const transcript = body.transcript;

        if (!transcript || transcript.length < 50) {
          return new Response(JSON.stringify({ error: "Transcript too short or missing" }), {
            status: 400,
            headers: { "Content-Type": "application/json" }
          });
        }

        const blogPost = await generateBlogPost(transcript, env.GROQ_API_KEY);

        return new Response(JSON.stringify(blogPost, null, 2), {
          headers: { "Content-Type": "application/json" }
        });
      } catch (err: any) {
        return new Response(JSON.stringify({ error: err.message }), {
          status: 500,
          headers: { "Content-Type": "application/json" }
        });
      }
    }

    // Health check
    return new Response("YouTube Blog Agent Worker", { status: 200 });
  }
};

async function generateBlogPost(transcript: string, groqKey: string): Promise<any> {
  // ... (function from Step 4)
}

Deploy the Worker:

wrangler deploy

You'll get a URL like https://youtube-blog-agent.<your-subdomain>.workers.dev.

How to Run the Pipeline

Here's the end-to-end flow from your terminal:

# 1. Download audio from YouTube
python download_audio.py "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
# Output: audio.wav

# 2. Transcribe with Whisper
python transcribe.py audio.wav > transcript.txt
# transcript.txt now contains the full text

# 3. Send to Cloudflare Worker for blog generation
curl -X POST https://youtube-blog-agent.<your-subdomain>.workers.dev/generate \
  -H "Content-Type: application/json" \
  -d "{\"transcript\": \"$(cat transcript.txt | sed 's/"/\\"/g' | tr '\n' ' ')\"}"

The response is a JSON object with title, excerpt, and body (markdown). Pipe it to a file, and you have a publishable blog post.

Pro tip: If the transcript is very long (>4000 tokens), split it into chunks and generate sections incrementally, then stitch them together. This avoids hitting Groq's context limits on the free tier.

Sensible Extensions

Once this pipeline works, you can extend it in several high-impact directions:

  1. Full automation with GitHub Actions: Schedule a workflow that runs download_audio.py and transcribe.py on a cron trigger, then POSTs to your Worker. Free for public repos (2000 min/month).
  2. Direct YouTube-to-blog endpoint: Replace the local scripts with a Cloudflare Queue consumer that spawns a containerized job on a free cloud VM (Oracle Free Tier, always-free AMD instance) to handle the heavy lifting.
  3. Multi-language support: Whisper handles 99 languages. Add a language detection step and pass the target language to Groq's prompt for translation or localized content.
  4. SEO scoring: After generation, run the output through a second Groq call that critiques and scores the blog post for SEO factors, then iteratively improves it. This agentic loop is exactly the kind of system thinking we cover in Debugging Concurrent LLM Agents: What the qm Harness Exposes About State and Race Conditions.
  5. Store transcripts in R2: Cloudflare R2 has a generous free tier (10 GB). Store raw transcripts for future repurposing into newsletters, social posts, or Q&A databases.

Common Pitfalls and Debugging

yt-dlp fails with "Video unavailable"

  • Ensure the video is public and not age-restricted. Some regions block certain content; use a VPN if necessary.

Whisper runs out of memory

  • The large model requires ~10GB VRAM. Stick to base or small on CPU. If using Colab, enable GPU runtime.

Groq returns garbled JSON

  • The LLM occasionally wraps JSON in markdown fences (json ... ). Add a sanitization step: content.replace(/```json|```/g, '').trim().

Worker times out

  • Free Workers have a 10ms CPU time limit (paid: 50ms). Our Worker only calls Groq (a fetch), so CPU time is minimal. If you add processing, watch the limits.

Transcript is too long

  • Groq's free tier models have context windows (Llama 3.1 8B: 128k tokens, but response limits apply). Chunk transcripts into 3000-token segments, generate partial drafts, and merge.

FAQ

Q: Why not run everything in the Cloudflare Worker? A: Workers cannot execute arbitrary binaries (ffmpeg, Python). They excel at orchestration and API stitching. The split architecture keeps each component on its optimal runtime.

Q: Is this truly free for production use? A: For low volume (a few videos per day), yes. Cloudflare Workers free tier: 100k requests/day. Groq free tier: ~30 requests/min. Whisper runs on your machine. At scale, you'll hit Groq rate limits first.

Q: How accurate is Whisper base model? A: For clear English speech, ~95% word error rate (WER). Accented speech or technical jargon may drop to 85-90%. Use medium for production quality.

Q: Can I use this for podcasts? A: Absolutely. Replace the yt-dlp step with a direct audio URL fetch. The rest of the pipeline is identical.

Q: How do I make the blog post match my brand voice? A: Customize the system prompt in generateBlogPost() with examples of your writing style. Few-shot prompting with 2-3 examples of your past posts dramatically improves tone alignment.

Q: Where do I learn to build more agents like this? A: This pipeline is a classic FDE pattern—stitching APIs, handling constraints, and shipping fast. If you want to master turning messy requirements into shipped prototypes, FDE Coach builds this muscle systematically, exactly as covered in How FDEs Turn a Messy Customer Problem Into a Shipped Prototype in a Single Week.

#content-repurposing#transcription#serverless#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