Faceless YouTube Content Engine
Build a CLI pipeline that turns a topic into a narrated YouTube video.
Introduction
30 Second Summary
Faceless YouTube channels pump out educational videos that rack up millions of views without ever showing a face on camera. Behind the scenes, creators stitch together AI voiceovers, stock footage, and captions into a repeatable production pipeline.
In this project, you will build a prompt-driven content engine inside Claude Code. Type a topic, and Claude Code walks you through a stage-by-stage pipeline that produces a narrated, captioned video with stock footage.
What You'll Build
You will type something like "make a video about how black holes work" into Claude Code and walk away with a finished MP4 you can play, share, or upload to YouTube.
By the end of this project, you'll have:
- A prompt-driven script generator where you describe any topic and get back a structured, hook-first educational script as a markdown file you can read and edit.
- An AI voiceover and subtitle pipeline that turns your script into a natural-sounding MP3 narration with perfectly timed captions, all generated locally with no extra API key required.
- A complete video assembly workflow that fetches stock footage from Pexels, stitches it with your voiceover, and burns in captions to produce a finished MP4 you can play and share.
- Secret Mission: Add a metadata generation stage that produces YouTube-ready titles, descriptions, tags, and thumbnail prompts so your video is fully upload-ready.
Are there any prerequisites?
You need a Claude Pro or Max subscription with Claude Code installed, and Python 3 installed on your Mac. This project is written for macOS. Step 1 covers installing the remaining tools.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Pipeline Tools
You have a topic picked out. Before Claude Code can orchestrate your video pipeline, it needs tools installed on your machine to do the heavy lifting.
In this step, you'll install FFmpeg for video assembly and edge-tts for AI voiceover generation. You'll also grab a free API key from Pexels for stock footage and set up your project workspace.
In this step, get ready to:
- Install FFmpeg and edge-tts on your machine.
- Get a free Pexels API key for stock footage.
- Create your project folder and open Claude Code.
Install FFmpeg and edge-tts
FFmpeg is a command-line tool for processing video and audio. It handles everything from stitching clips together to burning in subtitles. You'll install it through Homebrew, the standard package manager for macOS.
- Install FFmpeg by running this command in your terminal:
brew install ffmpeg
What does this command do?
Homebrew downloads and installs FFmpeg along with all the codecs it needs for video encoding, audio processing, and subtitle rendering. This can take a few minutes depending on your internet speed.
- Confirm FFmpeg installed correctly by running:
ffmpeg -version
✔️ I see version info
FFmpeg is ready to go. You should see a version number (like 7.x or 8.x) along with a list of enabled features. Move on to the next install.
ⓧ brew: command not found
Homebrew is not installed on your Mac. Install it by running:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
The installer will ask for your password and confirmation. After it finishes, close and reopen your terminal, then run brew install ffmpeg again.
ⓧ Something else went wrong
If the install stalled or showed errors, try running brew update first, then retry brew install ffmpeg. Make sure you have a stable internet connection.
Now install edge-tts, a free text-to-speech tool that uses Microsoft Edge's neural voices to generate natural-sounding narration. It needs no API key and no Microsoft account.
- Install edge-tts by running:
pip install edge-tts
- Confirm edge-tts installed correctly by running:
edge-tts --version
✔️ I see a version number
You should see something like edge-tts 7.2.8. Both tools are ready.
ⓧ Command not found
Your pip install may have placed edge-tts outside your shell's PATH. Try installing with pip3 instead:
pip3 install edge-tts
Then retry edge-tts --version. If it still fails, run python3 -m edge_tts --version to confirm the package is installed.
Why edge-tts over other voice tools?
edge-tts is completely free with no API key, no account, and no usage limits. Alternatives like ElevenLabs or OpenAI TTS charge per character and require API keys. For a content engine you want to reuse across many videos, a free tool with no rate limits is the better foundation.
Get a free Pexels API key
Your video pipeline needs stock footage to play behind the narration. Pexels provides a free video search API with 200 requests per hour, which is more than enough for this project.
- Open pexels.com/api in your browser.
- Click the button to get started and create a free Pexels account (or sign in if you already have one).
- After signing in, request your API key. You'll receive it instantly on the page.
- Copy your API key and keep it handy for the next substep.
What is the Pexels API?
Pexels is a free stock photo and video library. Its API lets your Python scripts search for videos by keyword and download them directly. Your pipeline will use it to automatically fetch footage that matches each scene in your script.
Create the project folder and open Claude Code
With your tools installed and API key ready, it's time to create the project workspace where all your pipeline files will live.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir content-engine
cd content-engine
- Create the subdirectories your pipeline stages will use by running:
mkdir scripts audio clips output
- Confirm the folders were created by running:
ls
You should see four folders listed: audio, clips, output, and scripts.
What are these folders for?
- scripts/ holds your written video scripts as markdown files.
- audio/ stores the AI-generated voiceover MP3 and subtitle SRT files.
- clips/ collects the stock video clips downloaded from Pexels.
- output/ is where the final assembled video lands.
Now store your Pexels API key so your scripts can use it without exposing the key in your code.
- Create a .env file in the content-engine folder by running:
echo 'PEXELS_API_KEY=your-api-key-here' > .env
- Open .env in a text editor and replace your-api-key-here with the actual Pexels API key you copied earlier.
- Save the file.
Keep your API key safe
The .env file keeps your API key separate from your code. Later, python-dotenv will load this key into your Python scripts automatically. Never paste API keys directly into your Python files.
Your scripts will also need two Python packages: requests for calling the Pexels API and python-dotenv for loading the .env file.
- Create a requirements.txt file by running:
cat > requirements.txt << 'EOF'
requests==2.32.3
python-dotenv==1.1.0
EOF
- Install the dependencies by running:
pip install -r requirements.txt
Seeing permission errors on pip install?
Try using pip3 instead of pip, or add the --user flag to install packages for your user only. If you use a Python version manager, make sure the correct Python is active.
Help me fix pip permission errors in my content-engine project.
Finally, open Claude Code inside your project so it knows your folder structure from the start.
- Launch Claude Code in the content-engine directory by running:
claude
What is Claude Code?
Claude Code is a terminal-based AI assistant included with your Claude Pro subscription. It can read your project files, run shell commands, and edit code. By opening it inside content-engine/, it immediately sees your folder structure and can act as your pipeline orchestrator.
Before you continue, predict what you'd expect to see in Claude Code's output. Does it mention your project files?
Claude Code opens an interactive chat session in your terminal. You should see a welcome message and a prompt where you can type. Claude Code has access to your content-engine/ folder and all the subdirectories you just created.
Claude Code not starting?
Make sure you're signed into your Claude Pro or Max plan. If claude is not recognized, you may need to install Claude Code first. Run the install command from the Anthropic docs, then try claude again.
Help me get Claude Code running in my terminal.
✔️ Awesome, I've got everything!
Great. Confirm that your .env and requirements.txt files are saved in content-engine/.
ⓧ I'd like to double check the full code
Here is what each file should contain. Compare them against yours.
.env
PEXELS_API_KEY=your-api-key-here
requirements.txt
requests==2.32.3
python-dotenv==1.1.0
Make sure your actual Pexels API key replaces your-api-key-here in .env.
Your pipeline workspace is fully set up. Next up, you'll use Claude Code to generate your first video script and see it become a real, editable file.
Generate and Edit Your First Script
Your tools are installed and your project folder is ready. Now it's time to create the raw material that everything else builds on: your video script.
Before any voiceover or stock footage, you need structured, hook-first content that sounds right when read aloud. Claude Code is your writing partner here. You'll describe a topic and iterate until the script is exactly what you want.
In this step, get ready to:
- Prompt Claude Code to generate a structured educational script.
- Review and edit the script until you're happy with the content.
- Add scene markers that the later pipeline stages will use to find matching stock footage.
Prompt Claude Code to generate your script
Every video in your pipeline starts as a Markdown script. Claude Code will generate this script file for you based on a topic prompt, saving it directly to your project folder.
- In your Claude Code session, type the following prompt to generate your first script:
Write me a 60-second educational script about [[VIDEO_TOPIC="your chosen topic, e.g. how black holes work"]]. Structure it with a hook that grabs attention in the first 5 seconds, 3 to 4 main sections that each explain one key idea, and a closing call to action. Save the script to scripts/script.md.
What does this prompt do?
You're asking Claude Code to generate a complete video script and save it to scripts/script.md. The 60-second target gives Claude a length constraint so the script stays concise.
The hook-first structure mirrors what successful YouTube videos use: grab attention immediately, deliver value in clear sections, and close with a reason to subscribe.
Claude Code will create the file and write the content directly. Before you read its output in the chat, what structure do you expect to see in the script?
- Confirm the script file was created by running this command:
cat scripts/script.md
You should see a full script with a punchy opening hook, 3 to 4 clearly separated sections, and a closing call to action. This is your first real pipeline output.
Script file not created?
If cat shows "No such file or directory", Claude Code may not have saved the file. Return to your Claude Code session and type: "Save the script you just wrote to scripts/script.md".
If Claude Code asks for permission to write files, allow it. Claude Code needs write access to create files in your project folder.
Still stuck? Help me troubleshoot why Claude Code didn't create my script file.
Review and iterate on the script
A first draft is never the final version. This is the moment to shape the script until it sounds like something you'd actually want to watch.
- Read through the full script that Claude Code generated.
- Pick at least one thing to change and ask Claude Code to update it. For example, type something like this:
Rewrite the hook in scripts/script.md to open with a surprising statistic instead.
What kinds of edits work well?
You can ask Claude Code to make any kind of change. Here are some ideas to try:
- "Make the second section shorter and punchier."
- "Change the tone to sound more casual and conversational."
- "Add a specific example or analogy to the third section."
- "The call to action feels generic. Make it more specific to the topic."
Each time you give feedback, Claude Code updates scripts/script.md directly. You can keep iterating until the script reads exactly the way you want.
- After your edit, verify the change by running:
cat scripts/script.md
You should see your edit reflected in the script. The section you asked Claude Code to change now reads differently from the original.
Add scene markers for the visual pipeline
Your script reads well, but the later pipeline stages need a machine-readable hook to know what footage to fetch. Each section of the script needs a [SCENE: description] marker that describes what the viewer should see on screen during that section.
What are scene markers?
A scene marker looks like [SCENE: time-lapse of stars in the night sky]. In Step 4, your fetch_clips.py script will extract these descriptions and use them as search queries on Pexels to download matching stock video clips.
The more specific and visual the description, the better the footage matches. "Sunset over the ocean" works better than "nature scene".
How many sections does your script have? You should expect one scene marker per section. Let's check if any markers already exist.
- Check for existing scene markers by running this command:
grep -n '\[SCENE:' scripts/script.md
✔️ I see scene markers listed
Your script already has scene markers. Read through each one and check whether the descriptions are specific enough to find good stock footage.
If any markers are vague (like [SCENE: science] or [SCENE: illustration]), ask Claude Code to make them more descriptive. For example: "Make the scene markers in scripts/script.md more specific and visual so they work well as stock footage search terms."
ⓧ No output or no markers found
Your script does not have scene markers yet. Ask Claude Code to add them by typing the following:
Add a [SCENE: description] tag at the start of each section in scripts/script.md. Each scene description should be a short, specific visual description I can use to search for stock footage. For example, [SCENE: time-lapse of stars in the night sky].
- After Claude Code updates the file, re-run the check to confirm the markers are in place:
grep -n '\[SCENE:' scripts/script.md
You should now see one [SCENE: description] marker per section, each with a specific visual description.
Markers look too generic?
If the scene descriptions are broad (like "technology" or "education"), tell Claude Code to rewrite them with specific, filmable imagery. Good markers describe a shot you can picture: "close-up of hands typing on a laptop keyboard" beats "computer" every time.
Need help improving them? Help me write better scene markers for stock footage searches.
Take a final look at your completed script. Before you run this, predict what you'll see: a hook, how many body sections, and a call to action at the end.
- View the final version of your script by running:
cat scripts/script.md
You should see a complete script with a hook opening, 3 to 4 sections each tagged with a [SCENE: description] marker, and a closing call to action. This is the foundation your entire video builds on.
Your script is locked in and ready for the next stage. Next up, you'll turn those words into a natural-sounding AI voiceover with perfectly timed subtitles.
Create the Voiceover and Subtitles
Your script is polished, structured, and ready to go. Right now it exists only as silent text on a screen. Before you can build a video, those words need to become a voice.
In this step, you'll use edge-tts through Claude Code to turn your script into a natural-sounding AI narration with perfectly timed subtitles. You'll hear your video for the first time and iterate on the voice and pacing until it sounds right.
In this step, get ready to:
- Generate an AI voiceover from your script using edge-tts.
- Create a synchronized SRT subtitle file that matches the narration.
- Iterate on the voice, speed, and pacing until you're happy with the result.
Prompt Claude Code to generate your voiceover
The edge-tts tool converts text into speech using Microsoft Edge's neural text-to-speech voices. It produces both an audio file and a subtitle file in one command, with no API key required.
- In your Claude Code session, ask Claude Code to read the script from scripts/script.md and generate a voiceover using edge-tts with the en-US-GuyNeural voice.
- Tell it to save the audio to audio/voiceover.mp3 and the subtitles to audio/voiceover.srt.
Claude Code will show you the command it plans to run and ask for your permission before executing it. You'll see something like this:
edge-tts --voice en-US-GuyNeural --text "..." --write-media audio/voiceover.mp3 --write-subtitles audio/voiceover.srt
What does this command do?
- --voice en-US-GuyNeural selects a natural-sounding American English male voice. This is the voice that will narrate your video.
- --text passes the full script text for edge-tts to read aloud. Claude Code extracts this from your script file automatically.
- --write-media saves the audio as an MP3 file.
- --write-subtitles generates a timed SRT subtitle file that stays in sync with the audio.
- Approve the command when Claude Code asks for permission.
- After Claude Code finishes, confirm the files were created by running:
ls audio/
You should see voiceover.mp3 and voiceover.srt listed in the output.
Files not appearing in audio/?
- Make sure Claude Code actually ran the command. Check that you approved it when prompted. If it's still pending, type y to approve.
- If you see a connection error, check your internet connection. edge-tts needs an active connection to reach Microsoft's TTS service.
- If you see an audio-related error, ask Claude Code to try a different voice such as en-US-AriaNeural and regenerate.
Why isn't edge-tts generating my voiceover files?
Listen and iterate on the voice
The files exist, but you haven't heard them yet. Before moving on, you need to listen to the narration and decide if the voice, pacing, and pronunciation work for your video.
- Play the voiceover by running:
open audio/voiceover.mp3
- Listen to the full narration. Pay attention to the pacing, pronunciation of any technical terms, and overall tone.
What should I listen for?
Focus on three things: does the voice match the tone of your topic (serious vs. casual), is the speed comfortable for a viewer to follow, and does it pronounce your key terms correctly.
If something feels off, you can change the voice entirely or adjust the speed. Both are quick fixes in the next bullets.
If the voice doesn't fit your content, you can swap it. edge-tts offers dozens of neural voices across different languages, genders, and styles.
- Ask Claude Code to list available voices by running:
edge-tts --list-voices
- Browse the list and pick a voice that fits your content. Each entry shows a ShortName like en-US-AriaNeural or en-GB-SoniaNeural.
- If you want a different voice, ask Claude Code to regenerate the voiceover using your chosen voice name.
You can also adjust the speaking speed without changing the voice.
- For faster delivery, ask Claude Code to add --rate=+10% to the edge-tts command and regenerate.
- For slower, more deliberate pacing, ask Claude Code to use --rate=-50% instead.
- After each change, replay the audio with open audio/voiceover.mp3 to hear the difference.
Start with the default speed
The default pacing of en-US-GuyNeural works well for most educational content. Only adjust the rate if the narration feels rushed or too slow after listening.
Once you're happy with the voice and pacing, it's time to verify the subtitle file.
Before you look at the subtitles, what do you expect the SRT file to contain? Think about what information a video player would need to display captions at the right moment.
- Preview the first 20 lines of your subtitle file by running:
head -20 audio/voiceover.srt
You should see numbered entries, each with a timestamp range and a line of text from your script. Each entry tells a video player exactly when to show and hide that caption.
What is an SRT file?
SRT (SubRip Text) is a simple subtitle format. Each entry has a sequence number, a start and end timestamp, and the text to display. Video players and FFmpeg both understand this format natively.
Because edge-tts generates both the audio and the SRT together, the timestamps are perfectly synchronized with the narration. No manual timing needed.
SRT file is empty or missing timestamps?
- If the file is empty, the voiceover generation may have partially failed. Ask Claude Code to regenerate both the MP3 and SRT files together.
- Make sure the --write-subtitles flag was included in the command Claude Code ran. Without it, only the audio file is generated.
Why is my SRT subtitle file empty or malformed?
Your video now has a voice. You've got a polished MP3 narration and a perfectly synced subtitle file, both ready to layer on top of visuals. Next up, you'll fetch stock video clips from Pexels to match each scene in your script.
Fetch Stock Footage from Pexels
In the last step, you generated a voiceover and subtitle file from your script. Your video has a voice now, but no visuals to go with it.
This step builds a Python script that reads the [SCENE: description] markers in your script and fetches matching stock video clips from Pexels automatically. You will see the raw clips and can swap any that do not fit your content.
In this step, get ready to:
- Have Claude Code write a clip-fetching script that calls the Pexels video search API.
- Run the script to download one stock clip per scene from your script.
- Review your clips and replace any that do not match your content.
Have Claude Code write the clip-fetching script
Your script has [SCENE: description] markers that describe what viewers should see during each section. A Python script can parse these markers, search the Pexels API for matching footage, and download one clip per scene.
- In Claude Code, ask Claude to create a Python script called fetch_clips.py with these capabilities:
- Reads scripts/script.md and extracts all [SCENE: ...] markers using regex.
- Searches the Pexels video search API for each scene description and downloads one landscape-oriented clip per scene.
- Saves clips to the clips/ folder with numbered filenames like clip_01.mp4 and clip_02.mp4.
- Loads the Pexels API key from the .env file using python-dotenv.
- Supports re-fetching a single clip by passing a scene number and a new search term as command-line arguments.
Claude Code will generate fetch_clips.py and save it to your project root. Once it finishes, verify that the script includes the key API search function. Look for something like this:
def search_pexels_video(query, api_key):
"""Search Pexels for a video matching the query."""
url = "https://api.pexels.com/v1/videos/search"
headers = {"Authorization": api_key}
params = {"query": query, "per_page": 1, "orientation": "landscape"}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if not data.get("videos"):
print(f" No videos found for: {query}")
return None
video_files = data["videos"][0]["video_files"]
hd_files = [f for f in video_files if f.get("height", 0) >= 720]
chosen = hd_files[0] if hd_files else video_files[0]
return chosen["link"]
What does this function do?
This is the core of your clip-fetching pipeline. It calls the Pexels video search endpoint with each scene description as the search query.
- The Authorization header passes your Pexels API key directly. Pexels does not use a Bearer prefix.
- It filters for HD files (720p or higher) to keep your video looking sharp.
- The orientation: "landscape" parameter ensures clips match a standard YouTube video format.
Also verify that the script's main block supports two modes. When run without arguments, it fetches all clips. When run with a scene number and a new search term, it re-fetches just that one clip:
if __name__ == "__main__":
if not PEXELS_API_KEY:
print("Error: PEXELS_API_KEY not found in .env file.")
sys.exit(1)
if len(sys.argv) == 3:
scene_num = int(sys.argv[1])
new_query = sys.argv[2]
fetch_single_clip(scene_num, new_query, PEXELS_API_KEY, CLIPS_DIR)
else:
scenes = extract_scenes(SCRIPT_PATH)
print(f"Found {len(scenes)} scenes in script.")
fetch_all_clips(scenes, PEXELS_API_KEY, CLIPS_DIR)
print("Done fetching clips.")
Why two modes?
The first mode (python fetch_clips.py) downloads all clips at once. The second mode (python fetch_clips.py 2 "new query") replaces a single clip without re-downloading everything.
This lets you swap out just the clips that do not match your content without waiting for the full batch to download again.
✔️ Awesome, I've got everything!
Great. Make sure fetch_clips.py is saved in your project root. Claude Code should have taken care of that already.
ⓧ I'd like to double check the full code
Ask Claude Code to show you the full contents of fetch_clips.py. Verify that it includes all of these sections, in order:
- Imports: os, re, sys, json, requests, Path, and load_dotenv.
- Constants: PEXELS_API_KEY, CLIPS_DIR, and SCRIPT_PATH.
- extract_scenes function that parses [SCENE: ...] markers with regex.
- search_pexels_video function that calls the Pexels API and returns an HD video link.
- download_clip function that streams the video file to disk.
- fetch_all_clips function that loops through every scene and downloads a clip for each.
- fetch_single_clip function that re-downloads one clip with a new search term.
- Main block: checks for the API key, then either fetches all clips or re-fetches a single clip based on command-line arguments.
If anything is missing, tell Claude Code what is missing and ask it to update the file.
Run the script and review your clips
The script is ready. Before you run it, think about what you expect. How many clips should it download? The answer is the number of [SCENE: ...] markers in your scripts/script.md file.
- Ask Claude Code to run the clip-fetching script by running:
python fetch_clips.py
You should see output showing Claude Code searching and downloading each clip, one scene at a time. It looks something like this:
Found 4 scenes in script.
Searching for clip 1: a planet orbiting a star
Downloading clip 1...
Saved: clips/clip_01.mp4
Searching for clip 2: light bending around a massive object
Downloading clip 2...
Saved: clips/clip_02.mp4
...
What should I see?
The script prints one search-and-download cycle per scene. The number of clips matches the number of [SCENE: ...] markers in your script. If a scene description is too vague and Pexels finds nothing, the script prints a "No videos found" message and skips that clip.
- Confirm the clips were downloaded by running:
ls clips/
You should see numbered MP4 files like clip_01.mp4, clip_02.mp4, and so on. One file per scene in your script.
No clips downloaded?
- If you see "PEXELS_API_KEY not found", double check that your .env file is in the project root (content-engine/) and contains PEXELS_API_KEY= followed by your actual key.
- If you see "No [SCENE: ...] markers found", open scripts/script.md and confirm it has markers in exactly the format [SCENE: your description here]. The word SCENE must be uppercase.
- If individual scenes return "No videos found", the scene description may be too abstract. Ask Claude Code to rewrite that scene marker with a more concrete visual description.
Why aren't my Pexels clips downloading?
- Open the clips/ folder in Finder and watch each clip to see if it matches the scene description in your script.
Some clips will be a great fit. Others might be too generic or completely off-topic. That is normal. The re-fetch mode lets you swap individual clips without re-downloading everything.
- If a clip does not match, re-fetch it by telling Claude Code to run the script with the scene number and a better search term. For example, to replace clip 2 with a different search:
python fetch_clips.py 2 "nebula galaxy"
What does this command do?
The first argument (2) is the scene number. The second argument ("nebula galaxy") is the new Pexels search term. The script replaces clip_02.mp4 with the new result. Repeat for any other clips you want to swap.
- After swapping, re-watch the replaced clip in Finder to confirm it fits your scene better.
- Keep swapping until you are happy with every clip. You can re-fetch the same scene as many times as you need by changing the search term.
Write better search terms
Pexels responds best to short, concrete visual terms. "astronaut floating" works better than "the loneliness of space exploration". If your original scene markers are too abstract, ask Claude Code to rewrite them as simple visual descriptions.
You now have a voiceover, subtitles, and a folder of reviewed stock clips. Next up, you will stitch everything together into a final video with burned-in captions.
Assemble the Final Video
Your pipeline now has all the raw ingredients: a polished script, a voiceover with matching subtitles, and a folder full of stock clips. They are separate files sitting in separate folders, doing nothing together.
This step stitches everything into a single MP4. Claude Code will write a Python script that calls FFmpeg to concatenate your clips, overlay the voiceover audio, and burn in the captions. You will also create a CLAUDE.md file that turns your project into a reusable content engine across future sessions.
In this step, get ready to:
- Have Claude Code write an assembly script that combines clips, voiceover, and captions into one video.
- Run the assembly, watch the output, and iterate on subtitle size and clip timing.
- Create a CLAUDE.md file that documents the full pipeline so future Claude Code sessions start with context.
Have Claude Code create the assembly script
The assembly script needs to do three things: concatenate your stock clips into one video stream, overlay your voiceover audio, and burn the SRT subtitles directly into the video frames.
- In your Claude Code session, tell Claude Code to create an assembly script by typing something like:
Create a Python script called assemble.py that uses FFmpeg to:
1. Find all clip_*.mp4 files in clips/ and concatenate them
2. Scale each clip to 1920x1080 and trim it so all clips together match the voiceover duration
3. Overlay the voiceover audio from audio/voiceover.mp3
4. Burn in subtitles from audio/voiceover.srt with white text and black outline
5. Output to output/final_video.mp4 using H.264 encoding
Use subprocess to call ffmpeg, not a Python wrapper library.
Why give Claude Code this level of detail?
FFmpeg has hundreds of flags and filter options. By specifying the resolution, encoding, and subtitle style up front, you steer Claude Code toward a working script on the first attempt instead of going back and forth.
Claude Code will create assemble.py in your project root. Open the file to review what it generated.
- Verify the top of the file defines the paths to your project folders:
import subprocess
import sys
from pathlib import Path
CLIPS_DIR = Path("clips")
AUDIO_PATH = Path("audio/voiceover.mp3")
SUBTITLES_PATH = Path("audio/voiceover.srt")
OUTPUT_PATH = Path("output/final_video.mp4")
def get_duration(file_path):
"""Get the duration of a media file in seconds using ffprobe."""
result = subprocess.run(
[
"ffprobe",
"-v", "quiet",
"-show_entries", "format=duration",
"-of", "csv=p=0",
str(file_path),
],
capture_output=True,
text=True,
)
return float(result.stdout.strip())
What does this section do?
The four path constants point at the folders your earlier steps created. The get_duration function calls ffprobe (bundled with FFmpeg) to measure how long your voiceover is in seconds. This duration drives everything else.
- Scroll down to the assemble_video function. You should see a loop that builds an FFmpeg filter for each clip:
def assemble_video(clips_dir, audio_path, subtitles_path, output_path):
"""Assemble clips, audio, and subtitles into a final video."""
clip_files = sorted(clips_dir.glob("clip_*.mp4"))
if not clip_files:
print("Error: No clips found in clips/ directory.")
sys.exit(1)
if not audio_path.exists():
print("Error: Voiceover audio not found at audio/voiceover.mp3")
sys.exit(1)
audio_duration = get_duration(audio_path)
clip_duration = audio_duration / len(clip_files)
output_path.parent.mkdir(parents=True, exist_ok=True)
concat_file = clips_dir / "concat_list.txt"
filter_parts = []
inputs = []
for i, clip in enumerate(clip_files):
inputs.extend(["-i", str(clip)])
filter_parts.append(
f"[{i}:v]scale=1920:1080:force_original_aspect_ratio=decrease,"
f"pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,"
f"trim=duration={clip_duration},setpts=PTS-STARTPTS[v{i}]"
)
How does clip timing work?
The script divides your voiceover duration evenly across all clips. If your voiceover is 60 seconds and you have 4 clips, each clip gets trimmed to 15 seconds.
Each clip is also scaled to 1920x1080 with letterboxing so clips of different sizes line up cleanly without stretching.
- Further down in the same function, verify you see the subtitle burn-in filter and the FFmpeg command:
concat_inputs = "".join(f"[v{i}]" for i in range(len(clip_files)))
filter_parts.append(
f"{concat_inputs}concat=n={len(clip_files)}:v=1:a=0[merged]"
)
subtitles_escaped = str(subtitles_path).replace("\\", "/").replace(":", "\\:")
filter_parts.append(
f"[merged]subtitles='{subtitles_escaped}':force_style="
f"'FontSize=24,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,"
f"Outline=2,MarginV=30'[final]"
)
filter_complex = ";".join(filter_parts)
cmd = [
"ffmpeg", "-y",
*inputs,
"-i", str(audio_path),
"-filter_complex", filter_complex,
"-map", "[final]",
"-map", f"{len(clip_files)}:a",
"-c:v", "libx264",
"-preset", "medium",
"-crf", "23",
"-c:a", "aac",
"-b:a", "192k",
"-shortest",
"-movflags", "+faststart",
str(output_path),
]
What do the FFmpeg flags do?
- The subtitles filter burns captions directly into the video frames with white text (PrimaryColour) and a black outline for readability.
- -c:v libx264 encodes video using H.264, the most widely supported codec. -crf 23 sets the quality level.
- -movflags +faststart rearranges the file so it starts playing immediately when streamed online.
- At the bottom of the file, confirm you see the execution logic and the entry point:
print("Assembling video...")
print(f" Clips: {len(clip_files)}")
print(f" Audio duration: {audio_duration:.1f}s")
print(f" Clip duration each: {clip_duration:.1f}s")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("FFmpeg error:")
print(result.stderr)
sys.exit(1)
print(f"Video assembled: {output_path}")
if __name__ == "__main__":
assemble_video(CLIPS_DIR, AUDIO_PATH, SUBTITLES_PATH, OUTPUT_PATH)
What does this section do?
The script prints a summary of clip count and duration before running FFmpeg. If FFmpeg returns an error, the script prints the full error output so you can debug. On success, it confirms the output path.
✔️ Awesome, I've got everything!
Great. Make sure you have saved assemble.py in the root of your content-engine folder.
ⓧ I'd like to double check the full code
Open assemble.py in your editor and scroll through the file. Confirm these sections appear in order:
- Imports (subprocess, sys, Path) and four path constants (CLIPS_DIR, AUDIO_PATH, SUBTITLES_PATH, OUTPUT_PATH).
- A get_duration function that calls ffprobe to measure a media file's length.
- An assemble_video function that builds a filter chain, constructs the ffmpeg command, and runs it.
- An if __name__ == "__main__": block that calls assemble_video with the four path constants.
If any section is missing, ask Claude Code to add it.
Run the assembly and watch your video
The script is ready. Before you run it, think about what you expect to see. You have clips of varying lengths and a voiceover of a specific duration.
Do you think every clip will fit perfectly on the first try, or will something look off?
- Tell Claude Code to run the assembly script:
Run python assemble.py
Claude Code runs the script. You should see output showing the clip count, total audio duration, and how long each clip segment will be. This takes a few seconds to a minute depending on your clip count and lengths.
- Open output/final_video.mp4 in your video player to watch it.
- Watch the full video from start to finish. Pay attention to the clip transitions, subtitle readability, and audio sync.
What should I look for?
The first render is rarely perfect. Common things to check:
- Are the captions readable against the video footage? If the text is too small or hard to see, you will fix that next.
- Do any clips feel too short or too long for their scene? The even distribution may not match the pacing of your script.
- Does the voiceover start and end at the right time? The total video duration should match the voiceover length.
- If the subtitles are too small, tell Claude Code to increase the font size:
The subtitles in the video are too small to read. Increase the FontSize in assemble.py and re-run the assembly.
- If a specific clip feels too short, tell Claude Code which one:
The third clip is too short. Give it more time in the video and re-run.
- After each change, Claude Code re-runs assemble.py and produces an updated output/final_video.mp4. Watch the updated video to confirm the fix.
- Keep iterating until you are happy with the subtitle size, clip timing, and overall pacing.
Seeing an FFmpeg error?
- If the script prints an error about missing files, check that audio/voiceover.mp3, audio/voiceover.srt, and your clip files in clips/ all exist.
- If you see a subtitle filter error, ask Claude Code to check the path escaping in the subtitles_escaped variable. Special characters in your folder path can cause issues.
- If the video has no audio, confirm the -map flag references the correct audio input index (it should be the last input after all the clip inputs).
Ask Claude Code for help debugging.
Set up CLAUDE.md to tie the full pipeline together
Right now, your pipeline works because you and Claude Code built it together step by step. If you start a fresh Claude Code session tomorrow, Claude will not remember any of it.
CLAUDE.md solves this. It is a markdown file at the project root that Claude Code reads at the start of every session. Whatever you put in it becomes Claude Code's starting knowledge.
- Tell Claude Code to create the pipeline documentation:
Create a CLAUDE.md file in the project root that documents our content pipeline.
Include:
- The folder structure (scripts/, audio/, clips/, output/)
- Each pipeline stage in order: script generation, voiceover with edge-tts, clip fetching with fetch_clips.py, video assembly with assemble.py
- The commands for each stage
- A note that I will provide a topic and you should walk me through each stage, producing editable files at each step
What makes a good CLAUDE.md?
Keep it under 200 lines. Use markdown headers and bullets. Make instructions specific and concrete rather than vague.
The goal is that a future Claude Code session can read this file and immediately know how to walk you through the full pipeline from topic to finished video.
- Review the CLAUDE.md file Claude Code created. Confirm it lists all four pipeline stages and references fetch_clips.py and assemble.py by name.
Now test that it actually works across sessions.
- Exit your current Claude Code session by typing /exit.
- Start a fresh Claude Code session by running:
claude
- In the fresh session, prompt Claude Code with a new topic:
Make a video about quantum computing
Before Claude Code responds, what do you expect it to do first? If CLAUDE.md is working, Claude Code should start at the first pipeline stage (writing a script) rather than asking what you mean or trying to generate everything at once.
Claude Code should begin by generating a new scripts/script.md for the new topic, following the pipeline stages documented in CLAUDE.md. You do not need to run through the full pipeline again. Confirming that Claude Code knows the stages is enough.
Claude Code did not follow the pipeline?
- Make sure CLAUDE.md is in the project root (the content-engine/ folder), not inside a subfolder.
- Check that the file is named exactly CLAUDE.md (all caps, .md extension). Claude Code will not pick up a differently named file.
- Confirm you started the new session from inside the content-engine/ directory.
Help me get CLAUDE.md working.
Your content engine is complete. You have a reusable pipeline that takes any topic and walks you through producing a narrated, captioned video with stock footage.
Secret mission
Generate YouTube-Ready Metadata
Your video has clips, narration, and captions. But a video without a title, description, and tags is invisible on YouTube. In this mission, you add a metadata generation stage to your pipeline that produces SEO-friendly YouTube metadata and a thumbnail prompt, making your video truly upload-ready.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine with free tools and a free Pexels API key. There are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- content-engine/ project folder on your Desktop (scripts, audio, clips, output video, Python files).
- FFmpeg (installed via Homebrew).
- edge-tts, requests, and python-dotenv Python packages (installed via pip).
Keep everything running
No action needed. Choose this if you plan to keep making videos with your content engine.
- Your content-engine/ folder stays on your Desktop, ready for new topics.
- Open Claude Code in the project folder anytime and prompt it with a new video topic.
- Your Pexels API key remains active and free (200 requests per hour, 20,000 per month).
- FFmpeg, edge-tts, and the Python packages stay installed on your machine for future projects.
Pause - I'll come back to this later
Close your Claude Code session to free up your terminal. Your project files and installed tools stay in place for when you return.
- If Claude Code is still running, press Ctrl+C or type /exit to close the session.
- Your content-engine/ folder, installed tools, and Pexels API key all stay in place.
- When you are ready to come back, open your terminal, navigate to ~/Desktop/content-engine, and run claude to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and uninstall the tools you added during this project.
- If Claude Code is still running, press Ctrl+C or type /exit to close the session.
- Delete the project folder by running:
rm -rf ~/Desktop/content-engine
- Uninstall FFmpeg by running:
brew uninstall ffmpeg
- Uninstall the Python packages by running:
pip uninstall edge-tts requests python-dotenv -y
- Your Pexels account and API key are free and remain active. No action is needed to manage them.
Nice Work!
Nice Work!
Nice work! You just built a full content engine inside Claude Code. Give it a topic, and it walks you through every stage of producing a narrated, captioned YouTube video you can upload straight away.
You've learned how to:
- Built a prompt-driven content pipeline in Claude Code that takes a topic and produces a narrated video with stock footage and burned-in captions.
- Generated natural-sounding AI voiceovers with edge-tts and automatically fetched matching stock footage from Pexels using Python scripts.
- Created a CLAUDE.md file that gives Claude Code persistent memory of your pipeline, so every new session starts ready to produce videos.
- Secret Mission: Added a metadata generation stage that produces YouTube-ready titles, descriptions, tags, and thumbnail prompts.
Ready to quiz yourself?