Build a YouTube-to-Blog Repurposing Agent with Groq Llama 3 and LlamaIndex
What We’re Building
A Python agent that takes a YouTube URL, pulls the transcript, splits it into manageable chunks, and generates a clean blog post with logical headings and a key-takeaways section. The output is a markdown draft ready for your CMS.
Core features:
- Zero-cost inference via Groq’s free Llama 3 API (no GPU, no credit card)
- Transcript extraction with
youtube-transcript-api(no YouTube Data API key) - Semantic chunking through LlamaIndex to preserve context across section boundaries
- Structured output: title, introduction, H2/H3 headings, body paragraphs, and a bulleted key-takeaways block
- Single Python script you can run from a terminal or wire into a no-code trigger
This isn’t a toy. It’s the same pattern I use to repurpose long-form technical talks into blog content when the raw transcript is 8,000+ words of rambling. The agent cuts through the noise and produces a readable draft that needs maybe 15 minutes of human polish.
Architecture: How the Pieces Fit
The flow is linear but each stage is swappable. You could replace youtube-transcript-api with a Whisper transcription step for videos without captions. You could swap Groq for a local Ollama model if you need air-gapped operation. The chunking layer ensures we never blow past Llama 3’s 8K context window while keeping related ideas together.
Prerequisites (All Free Tier)
- Python 3.10+ – python.org/downloads
- Groq API key (free) – Sign up at console.groq.com. Free tier gives you 30 requests/minute on Llama 3 8B and 70B models. No credit card.
- youtube-transcript-api –
pip install youtube-transcript-api. Pulls auto-generated or manual captions without OAuth. - LlamaIndex –
pip install llama-index-core llama-index-embeddings-huggingface. We use a local HuggingFace embedding model to avoid paying for OpenAI embeddings. - httpx – for Groq REST calls if you skip the Groq Python SDK. I’ll use raw
httpxto keep dependencies light.
That’s it. No vector database, no Pinecone, no paid embedding service.
Step 1: Project Setup and Dependencies
Create a new directory and a virtual environment:
mkdir yt-to-blog && cd yt-to-blog
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
Install the core packages:
pip install youtube-transcript-api llama-index-core llama-index-embeddings-huggingface httpx
Set your Groq API key as an environment variable:
export GROQ_API_KEY="gsk_your_key_here"
Create agent.py – we’ll build it piece by piece.
Step 2: Extract the YouTube Transcript
youtube-transcript-api needs the video ID, which is the string after v= in the URL. We’ll parse that with a quick regex.
import re
from youtube_transcript_api import YouTubeTranscriptApi
def extract_video_id(url: str) -> str:
pattern = r"(?:v=|\/)([\w-]{11})(?:\?|&|$)"
match = re.search(pattern, url)
if not match:
raise ValueError(f"Could not extract video ID from {url}")
return match.group(1)
def get_transcript(video_id: str) -> str:
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
# Each item is {'text': '...', 'start': 12.34, 'duration': 3.21}
full_text = " ".join(item["text"] for item in transcript_list)
return full_text
Why this works: The API scrapes YouTube’s caption endpoint. It handles auto-generated captions and manually uploaded ones. No API key, no quota (within reason). If a video has no captions at all, it raises NoTranscriptFound – we’ll handle that later.
Step 3: Chunk and Index with LlamaIndex
A 30-minute video can produce 5,000–8,000 words. Llama 3 8B has an 8K token context window, but we want to leave room for the system prompt and generated output. I target chunks of ~1,500 tokens with slight overlap so ideas don’t get severed mid-sentence.
We’ll use LlamaIndex’s IngestionPipeline with a SentenceSplitter and a local HuggingFaceEmbedding model. The embeddings aren’t strictly necessary for this linear flow, but they lay the groundwork if you later want to add semantic search across your transcript library.
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
def chunk_transcript(text: str, chunk_size: int = 1500, chunk_overlap: int = 200):
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap),
embed_model,
]
)
nodes = pipeline.run(documents=[Document(text=text)])
return nodes
Model choice: BAAI/bge-small-en-v1.5 is a 384-dimensional embedding model that runs on CPU in seconds. It’s free, small (130MB download), and good enough for chunk-level semantic similarity. First run downloads the model and caches it locally.
Step 4: Generate the Blog Post with Groq Llama 3
Groq’s API is OpenAI-compatible, so we use the /v1/chat/completions endpoint. The free tier supports llama3-8b-8192 and llama3-70b-8192. For blog generation, 8B is fast and surprisingly coherent.
The prompt is everything. We need structured output without asking for JSON (which Llama 3 sometimes mangles). Instead, we instruct it to output markdown with specific sections.
import httpx
import os
GROQ_API_KEY = os.environ["GROQ_API_KEY"]
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
SYSTEM_PROMPT = """\
You are a senior content editor. Given a transcript from a YouTube video, generate a polished blog post in markdown.
Rules:
- Start with a compelling title as an H1.
- Write a 2-3 sentence introduction that hooks the reader.
- Organize the body with H2 and H3 headings based on the natural flow of the content.
- End with an H2 section called "Key Takeaways" containing 4-6 bullet points.
- Do NOT include meta-commentary like "Here is the blog post." Just output the markdown.
- Preserve technical accuracy. Do not invent claims not present in the transcript.
"""
def generate_blog(chunks: list[str]) -> str:
# Concatenate chunk texts; Llama 3 8B handles ~6K tokens comfortably
combined_text = "\n\n".join(chunk.get_content() for chunk in chunks)
# Truncate if needed (rough estimate: 1 token ≈ 4 chars)
max_chars = 24000 # ~6K tokens
if len(combined_text) > max_chars:
combined_text = combined_text[:max_chars] + "...[truncated]"
headers = {
"Authorization": f"Bearer {GROQ_API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "llama3-8b-8192",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Transcript:\n\n{combined_text}"}
],
"temperature": 0.7,
"max_tokens": 4096,
}
response = httpx.post(GROQ_URL, headers=headers, json=payload, timeout=120.0)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
Temperature 0.7 gives enough creativity for engaging prose without hallucinating facts. If your transcript is highly technical, drop it to 0.3.
Step 5: Assemble the Full Agent Script
Here’s the complete agent.py. Copy, paste, set your key, and run.
import re
import os
import httpx
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api._errors import NoTranscriptFound
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core import Document
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
if not GROQ_API_KEY:
raise RuntimeError("Set GROQ_API_KEY environment variable")
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
SYSTEM_PROMPT = """\
You are a senior content editor. Given a transcript from a YouTube video, generate a polished blog post in markdown.
Rules:
- Start with a compelling title as an H1.
- Write a 2-3 sentence introduction that hooks the reader.
- Organize the body with H2 and H3 headings based on the natural flow of the content.
- End with an H2 section called "Key Takeaways" containing 4-6 bullet points.
- Do NOT include meta-commentary like "Here is the blog post." Just output the markdown.
- Preserve technical accuracy. Do not invent claims not present in the transcript.
"""
def extract_video_id(url: str) -> str:
pattern = r"(?:v=|\/)([\w-]{11})(?:\?|&|$)"
match = re.search(pattern, url)
if not match:
raise ValueError(f"Could not extract video ID from {url}")
return match.group(1)
def get_transcript(video_id: str) -> str:
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
return " ".join(item["text"] for item in transcript_list)
def chunk_transcript(text: str, chunk_size: int = 1500, chunk_overlap: int = 200):
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap),
embed_model,
]
)
nodes = pipeline.run(documents=[Document(text=text)])
return nodes
def generate_blog(chunks) -> str:
combined_text = "\n\n".join(chunk.get_content() for chunk in chunks)
max_chars = 24000
if len(combined_text) > max_chars:
combined_text = combined_text[:max_chars] + "...[truncated]"
headers = {
"Authorization": f"Bearer {GROQ_API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "llama3-8b-8192",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Transcript:\n\n{combined_text}"}
],
"temperature": 0.7,
"max_tokens": 4096,
}
resp = httpx.post(GROQ_URL, headers=headers, json=payload, timeout=120.0)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def main():
url = input("YouTube URL: ").strip()
video_id = extract_video_id(url)
print(f"[1/3] Fetching transcript for {video_id}...")
try:
transcript = get_transcript(video_id)
except NoTranscriptFound:
print("Error: This video has no captions available.")
return
print(f"Transcript length: {len(transcript)} characters")
print("[2/3] Chunking and indexing...")
chunks = chunk_transcript(transcript)
print(f"Created {len(chunks)} chunks")
print("[3/3] Generating blog post via Groq Llama 3...")
blog_md = generate_blog(chunks)
output_path = f"{video_id}_blog.md"
with open(output_path, "w") as f:
f.write(blog_md)
print(f"Blog post saved to {output_path}")
if __name__ == "__main__":
main()
Running the Agent
python agent.py
Paste a URL like https://www.youtube.com/watch?v=dQw4w9WgXcQ and wait 15–30 seconds depending on transcript length. You’ll get a markdown file you can drop straight into Hugo, Jekyll, or WordPress.
What you’ll see:
- Step 1 fetches the transcript (fast, usually under 2 seconds)
- Step 2 downloads the embedding model on first run (~30 seconds, cached after)
- Step 3 calls Groq (3–10 seconds for 8B, faster on 70B if you switch models)
Sensible Extensions
Add frontmatter: Modify the prompt to include YAML frontmatter with date, tags, and canonical_url pointing to the original video. This is huge for SEO.
Batch processing: Wrap main() in a loop that reads URLs from a CSV. Throttle to 30 RPM to stay within Groq’s free tier.
Multi-language support: YouTubeTranscriptApi.get_transcript(video_id, languages=['es', 'en']) fetches Spanish captions if available. Combine with llama3-8b-8192 which handles dozens of languages natively.
Trigger from n8n or Make: Expose this as a simple HTTP endpoint with FastAPI. When a new video publishes (YouTube webhook), your automation fires the agent and pushes the draft to your CMS. This pattern is similar to the automation flows we discuss in Build an AI Cron Newsletter Agent: RSS Feeds to Personalized Digest with Cloudflare Workers.
Quality gate with a second LLM pass: After generation, send the output back to Groq with a prompt like “Critique this blog post for factual errors and suggest improvements.” This catches hallucinations before you publish.
Common Pitfalls and Fixes
| Pitfall | Fix |
|---|---|
NoTranscriptFound error | The video has no captions. Use a different video or add a fallback that runs Whisper locally via faster-whisper. |
| Groq returns 429 (rate limit) | Free tier is 30 RPM / 14,400 TPM. Add time.sleep(2) between calls or batch during off-peak hours. |
| Blog post cuts off mid-sentence | Increase max_tokens to 8192 (the model’s max output) or reduce input chunk size so the combined prompt+completion fits. |
| Embedding model download hangs | First run pulls ~130MB from HuggingFace. If behind a proxy, set HF_ENDPOINT=https://hf-mirror.com for faster downloads in certain regions. |
| Generated headings don’t match content | Lower temperature to 0.3–0.4. High temperature with long transcripts can cause the model to drift. |
| Transcript is garbled auto-captions | YouTube’s auto-captions on technical jargon are rough. Consider adding a “clean transcript” step with a small LLM pass before chunking. |
If debugging feels familiar to the kind of signal-over-memorization thinking in The FDE Interview Loop: Preparing for Signal Over Leetcode Memorization, it’s because this is exactly the kind of real-system reasoning FDE roles demand.
FAQ
Q: Can I use this for videos without captions?
A: Not directly. You’d need to download the audio (e.g., yt-dlp) and transcribe with faster-whisper or Groq’s Whisper endpoint. That adds cost/complexity but is entirely doable.
Q: How good is the output really? A: Llama 3 8B produces a solid first draft. It captures the structure and key points. You’ll still want to fact-check claims, tighten prose, and add internal links. Think of it as turning a 2-hour writing task into a 15-minute editing task.
Q: Why LlamaIndex instead of just splitting on paragraphs?
A: SentenceSplitter respects sentence boundaries and adds overlap. Naive splitting breaks mid-thought. The embeddings are optional now but let you add retrieval-augmented generation later without rewriting the pipeline.
Q: What if my transcript is longer than 8K tokens?
A: The script truncates to ~6K tokens of input. For very long videos, implement a map-reduce pattern: generate a summary per chunk, then generate the final blog from those summaries. LlamaIndex’s SummaryIndex can help here.
Q: Is this production-ready? A: For internal tooling or personal use, absolutely. For a customer-facing product, add retry logic, better error handling, and a content moderation pass. The architecture scales—you’d just swap the free Groq tier for a paid plan or self-hosted model.
Q: Where do I go from here? A: If you’re building a portfolio of shipped artifacts to demonstrate forward-deployed engineering skills, this project fits the pattern we outline in The FDE Portfolio: Shipped Artifacts and Decision Logs to Get Hired. Document your decisions, measure quality improvements, and iterate.
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