AI Video Analysis Engine
Build a CLI tool that uses Gemini AI to analyze any video and produce structured reports.
Introduction
30 Second Summary
You have watched thousands of videos online. Some grab your attention instantly and others fade from memory within seconds. What if you could break down exactly what makes a video work, from its visual hooks to its spoken words to its scene structure, all in a few seconds flat?
In this project, you will build a Python command-line tool that analyzes any video using Google Gemini, the only major AI model with native video understanding. Paste a YouTube or Instagram URL, and get back a full structured breakdown of everything in the video.
What You'll Build
You run a single command in your terminal, paste in any video URL, and within a minute you have a detailed report telling you exactly what the video contains, how it is structured, and what makes it tick visually and aurally.
By the end of this project, you'll have:
- A CLI tool you run from your terminal that accepts YouTube URLs, Instagram Reel links, or local video files and produces a complete analysis with one command.
- Structured JSON analysis files covering transcription, scene-by-scene breakdowns, speaker identification, on-screen text, audio elements, and visual style for every video you analyze.
- A Markdown report generator that turns raw analysis into readable documents you can open, share, or reference any time.
- Secret Mission: batch mode that analyzes 15+ videos from a text file of URLs in one command, turning your tool into a content research engine.
Are there any prerequisites?
You need Cursor installed and basic comfort using a terminal. No Python experience is required. Step 1 walks you through every piece of setup from scratch.
Before We Start
Before We Start...
Before writing any code, this is your chance to lock in what you're building and why it matters to you. Committing to a specific type of video content grounds the project in something you care about rather than abstract exercises.
Set Up Your Environment
Before you can analyze any video, you need the foundational tools in place. Python runs your code, ffmpeg processes video files, and a Gemini API key gives you access to the only major AI model with native video understanding.
Without these foundational pieces, none of the later steps are possible. In this step, you will install Python, set up your project directory with all dependencies, and verify that your Gemini connection works end-to-end.
In this step, get ready to:
- Install Python and ffmpeg on your machine.
- Create a project directory with a virtual environment and all pip dependencies.
- Get a Gemini API key and test the connection from your terminal.
Install Python and ffmpeg
This project requires Python 3.9 or higher. Let's check if you already have it.
- Open a terminal inside Cursor by pressing Cmd+J (macOS) or Ctrl+J (Windows).
- Check your Python version by running this command:
python3 --version
Look at the output and pick the tab that matches what you see:
✔️ I see version 3.9 or higher
Your Python version is ready for this project. Move on to the ffmpeg check below.
ⓧ I see an older version
You need Python 3.9+ for this project. Install the latest Python alongside your existing version.
macOS
- Install the latest Python via Homebrew by running:
brew install python
- After the install finishes, close and reopen your terminal, then run python3 --version again to confirm you now see 3.9 or higher.
Homebrew not installed?
If you see "brew: command not found", install Homebrew first by running:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Follow the on-screen instructions, then re-run brew install python.
Windows
- Download the latest Python installer from python.org/downloads.
- Run the installer. On the first screen, check the box labeled Add Python to PATH, then click Install Now.
- After the install finishes, close and reopen your terminal, then run python --version to confirm you see 3.9 or higher.
ⓧ Command not found
Python is not installed yet. Let's install it now.
macOS
- Install Python via Homebrew by running:
brew install python
Homebrew also installs pip (the Python package manager) automatically.
Homebrew not installed?
If you see "brew: command not found", install Homebrew first by running:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Follow the on-screen instructions, then re-run brew install python.
- After the install finishes, close and reopen your terminal, then run python3 --version to confirm it prints 3.9 or higher.
Windows
- Download the latest Python installer from python.org/downloads.
- Run the installer. On the first screen, check the box labeled Add Python to PATH, then click Install Now.
- After the install finishes, close and reopen your terminal, then run python --version to confirm it prints 3.9 or higher.
Next, check if ffmpeg is installed. This tool handles video format merging and is required by the download library you will use later.
- Check for ffmpeg by running:
ffmpeg -version
Pick the tab that matches your result:
✔️ I see a version number
ffmpeg is installed and ready. Move on to creating your project.
ⓧ Command not found
ffmpeg is not installed. Let's add it now.
macOS
- Install ffmpeg via Homebrew by running:
brew install ffmpeg
- After the install finishes, run ffmpeg -version again to confirm it prints version information.
Windows
- Download a prebuilt ffmpeg binary from ffmpeg.org/download.html (use the Windows EXE builds from gyan.dev or BtbN linked on that page).
- Extract the downloaded archive and add the bin folder to your system PATH.
- Close and reopen your terminal, then run ffmpeg -version to confirm it prints version information.
Create your project and install dependencies
Now that your tools are installed, let's create a dedicated project folder with an isolated Python environment. This keeps your project's packages separate from any other Python work on your machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir video-analyzer
cd video-analyzer
- Confirm you are in the right place by running:
pwd
You should see a path ending in video-analyzer.
- Create a virtual environment by running:
python3 -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Your terminal prompt now shows (venv) at the beginning. This confirms the virtual environment is active.
What is a virtual environment?
A virtual environment is an isolated Python workspace. Packages you install here stay inside this project and do not affect anything else on your machine. The (venv) prefix in your prompt tells you the environment is active.
- Open the video-analyzer folder in Cursor by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the video-analyzer folder, then click Open.
- Create a new file called requirements.txt by clicking the new file icon in the Explorer sidebar.
- Paste the following content into requirements.txt:
google-genai
yt-dlp
python-dotenv
What are these packages?
google-genai is Google's official SDK for the Gemini API. yt-dlp is a powerful video downloader that supports YouTube, Instagram, and 1000+ other sites. python-dotenv loads environment variables from a .env file so your API key stays out of your code.
- Save requirements.txt.
- Install all three packages by running this command in your terminal (make sure (venv) is still showing in your prompt):
pip install -r requirements.txt
You will see pip downloading and installing several packages. This takes 30-60 seconds.
- Verify the packages installed correctly by running:
python3 -c "from google import genai; print('Ready')"
You should see Ready printed with no errors.
Seeing an import error?
- Make sure your terminal prompt starts with (venv). If it does not, re-activate the virtual environment using the command from the platform tab above.
- If you see "No module named google", re-run pip install -r requirements.txt and check for error messages during the install.
Why can't Python find the google module?
Get your Gemini API key and test the connection
Google AI Studio gives you an API key that unlocks Gemini's video analysis capabilities. The key is free to create, and you only pay per analysis you run (approximately $0.03 per short video).
- Open Google AI Studio in your browser and sign in with your Google account.
- If you are a new user, accept the Terms of Service. This auto-creates a project and API key for you.
- Click Get API key in the left sidebar.
- Click Create API key if you do not already have one listed.
- Copy the key. You will only see it once, so paste it somewhere safe temporarily.
What is an API key?
An API key is a unique identifier that tells Gemini who is making the request. It links your usage to your Google account for billing. Keep it private and never share it publicly.
Now let's store this key safely in your project and set up a couple of configuration files.
- Create a new file called .env in your video-analyzer folder (use the new file icon in the Explorer sidebar).
- Paste the following into .env, replacing your-api-key-here with the key you just copied:
GEMINI_API_KEY=your-api-key-here
- Save .env.
- Create another file called .env.example with the same content (this serves as a template for anyone else using your project):
GEMINI_API_KEY=your-api-key-here
- Save .env.example.
- Create a file called .gitignore and paste the following:
.env
venv/
downloads/
reports/
__pycache__/
Why a .gitignore?
The .gitignore file tells Git which files to never track. Your .env contains your secret API key, venv/ is a large folder of installed packages, and the other directories will hold generated files you do not need in version control.
- Save .gitignore.
- Confirm all your project files exist by running:
ls -a
You should see .env, .env.example, .gitignore, requirements.txt, and venv in the output.
Time for the real test. Before you run the next command, what do you think will happen? Will Gemini respond, or will something fail?
- Test your Gemini API connection by running:
python3 -c "
from dotenv import load_dotenv
load_dotenv()
from google import genai
client = genai.Client()
response = client.models.generate_content(
model='gemini-3.5-flash',
contents='Say hello in one sentence.'
)
print(response.text)
"
You should see Gemini respond with a short greeting. This proves your API key is valid, your packages are installed, and the connection to Gemini works.
Seeing an authentication error?
- Double-check that your .env file contains GEMINI_API_KEY= followed by your actual key with no spaces around the = sign.
- Make sure you are running the command from inside the video-analyzer folder (where .env lives). Run pwd to check.
- If your key was just created and you see a 401 error, try creating a new key in Google AI Studio and updating your .env file.
Help me fix my Gemini API connection error
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files: requirements.txt, .env, .env.example, and .gitignore.
ⓧ I'd like to double check the full code
Here is the exact content each file should contain:
google-genai
yt-dlp
python-dotenv
GEMINI_API_KEY=your-api-key-here
.env
venv/
downloads/
reports/
__pycache__/
Your environment is ready. You have Python, ffmpeg, a clean project structure, and a working Gemini connection. Next up, you will feed your first video into Gemini and see what it can understand.
Analyze Your First Video
Before building anything complex, you need to see Gemini actually understand a video. This step proves the pipeline works end-to-end: you'll upload a video, let Gemini process it, and get back a detailed description of everything it sees and hears.
The output will immediately reveal a limitation. Gemini returns a wall of unstructured prose that is impossible to compare across videos or query programmatically. That gap is exactly what the next step fixes with structured JSON output.
In this step, get ready to:
- Create the core analysis script that uploads a video and prints Gemini's response.
- Test the pipeline with a real video and observe the raw unstructured output.
Create the core analysis script
Your script needs to do four things: load the API key, upload the video file, wait for processing, and send it to Gemini with a prompt. The Files API handles video uploads separately from the generation call because video processing takes time.
- Create a new file called analyze.py in your video-analyzer folder by pasting the following code:
import sys
import os
import time
from dotenv import load_dotenv
from google import genai
# Load API key from .env
load_dotenv()
def analyze_video(video_path: str) -> str:
"""Upload video to Gemini and get a text analysis."""
client = genai.Client()
# Upload the video file to Gemini's Files API
print(f'Uploading video: {video_path}')
video_file = client.files.upload(file=video_path)
# Poll until Gemini finishes processing the video
while not video_file.state or video_file.state.name != 'ACTIVE':
print('Processing video...')
time.sleep(5)
video_file = client.files.get(name=video_file.name)
# Send the video with a simple analysis prompt
print('Analyzing video with Gemini...')
response = client.models.generate_content(
model='gemini-3.5-flash',
contents=[video_file, 'Analyze this video in detail. Describe what you see, hear, and observe.'],
)
return response.text
What does this code do?
- load_dotenv() reads your .env file so the Gemini client can find your API key automatically.
- client.files.upload(file=video_path) sends the video to Gemini's Files API for processing. Videos are stored at 1 frame per second and audio at 1Kbps.
- The while loop polls every 5 seconds because video processing is not instant. The file transitions to ACTIVE once Gemini is ready to analyze it.
- client.models.generate_content() sends both the processed video and your text prompt together, letting Gemini see and hear the full video in one call.
- Below the analyze_video function, add the CLI entry point that reads the file path from the command line:
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python analyze.py <video_path>')
sys.exit(1)
result = analyze_video(sys.argv[1])
print(result)
What does the main block do?
- sys.argv[1] grabs the first argument you type after python3 analyze.py in your terminal.
- If no argument is provided, it prints a usage hint and exits so the script does not crash with an unrelated error.
- Save analyze.py.
- Confirm the file exists by running:
ls analyze.py
You should see analyze.py listed in the output.
Don't see analyze.py?
- Make sure you saved the file in the video-analyzer folder, not a parent directory.
- Check that your terminal is inside video-analyzer by running pwd.
Help me figure out why my file isn't showing up in the right directory.
✔️ Awesome, I've got everything!
Great. Double check you have saved analyze.py and you are ready to test it with a real video.
ⓧ I'd like to double check the full code
Here is the complete analyze.py file so far:
import sys
import os
import time
from dotenv import load_dotenv
from google import genai
load_dotenv()
def analyze_video(video_path: str) -> str:
client = genai.Client()
print(f'Uploading video: {video_path}')
video_file = client.files.upload(file=video_path)
while not video_file.state or video_file.state.name != 'ACTIVE':
print('Processing video...')
time.sleep(5)
video_file = client.files.get(name=video_file.name)
print('Analyzing video with Gemini...')
response = client.models.generate_content(
model='gemini-3.5-flash',
contents=[video_file, 'Analyze this video in detail. Describe what you see, hear, and observe.'],
)
return response.text
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python analyze.py <video_path>')
sys.exit(1)
result = analyze_video(sys.argv[1])
print(result)
Test with a real video
You need a short video file on your computer to test the pipeline. Anything between 15 and 60 seconds works well.
- Find or record a short video. An Instagram Reel you have saved to your computer, a screen recording, or any .mp4 file works.
- Copy or move the video file into your video-analyzer folder so it is easy to reference.
Before you run the script, what do you think Gemini will return? Will it be a structured list, a single sentence, or something else?
- Run the analysis on your video file:
python3 analyze.py ./your_video.mp4
What should I see?
The script prints status messages as it works: uploading, processing, then analyzing. After 15-60 seconds (depending on video length), you will see multiple paragraphs of text describing everything Gemini observed in your video.
The output covers scenes, audio, spoken words, and visual elements. It is detailed and impressive.
Now look closely at the output. Try to answer these questions from it: "What was the hook in the first 3 seconds?" or "How many scene changes happened?"
You cannot reliably extract those answers from a paragraph of prose. The analysis is useful for reading, but impossible to compare across 15 videos or query programmatically.
Why is unstructured text a problem?
Imagine analyzing 30 videos from a creator you admire. You want to find which videos have hooks under 3 seconds, or which ones use background music.
With unstructured prose, you would have to read every single report manually. With structured JSON (which you will build in the next step), a single script can answer those questions across your entire knowledge base.
Seeing an error instead of analysis text?
- If you see a file-not-found error, double check the path to your video file matches exactly what you typed. Run ls to see files in your current directory.
- If you see an authentication error, verify your .env file contains GEMINI_API_KEY= followed by your key with no spaces around the equals sign.
- If processing seems stuck for over 2 minutes, your video may be too large. Try a shorter clip under 30 seconds.
Help me debug why my video analysis script is failing.
Your pipeline works. Gemini can see, hear, and describe everything in your video. Next up, you will engineer the prompt to return structured JSON so every analysis becomes queryable, comparable, and machine-readable.
Build Structured Analysis with Prompt Engineering
In the previous step, you proved that Gemini can understand video content. It returned a detailed analysis of everything it saw and heard.
The problem is that wall of unstructured text. You cannot reliably extract "what was the hook?" or "how many scene changes happened?" from a paragraph of prose. To build a knowledge base you can query across dozens of videos, every analysis needs to follow the same predictable structure. In this step, you will engineer the prompt to return structured JSON, then build functions that save that data as both a machine-readable JSON file and a human-readable Markdown report.
In this step, get ready to:
- Replace the generic prompt with a detailed JSON schema prompt that forces structured output.
- Add report generation that saves both JSON and Markdown files automatically.
- Test the full pipeline and verify your growing knowledge base directory.
Build the analysis prompt
The key to getting structured output from an LLM is telling it exactly what shape you want the data in. You will define a prompt that includes the full JSON schema as an example, so Gemini knows precisely which fields to populate.
- In analyze.py, find the import lines at the top of the file:
import sys
import os
import time
- Update the imports to include json and Path, so they look like this:
import sys
import os
import json
import time
from pathlib import Path
Now you need the structured prompt itself. This constant tells Gemini exactly which JSON fields to return for every video it analyzes.
- Below the load_dotenv() line, add this ANALYSIS_PROMPT constant:
ANALYSIS_PROMPT = """Analyze this video comprehensively and return your analysis as a JSON object with the following structure:
{
"title": "A descriptive title for this video",
"duration_estimate": "Estimated duration",
"summary": "2-3 sentence overview of the video",
"transcription": "Full transcription of all spoken words",
"scenes": [
{
"timestamp": "start-end time",
"description": "What happens visually in this scene",
"audio": "What is heard (speech, music, sound effects)",
"on_screen_text": "Any text visible on screen"
}
],
"speakers": [
{
"label": "Speaker identifier",
"description": "Brief description of the speaker",
"speaking_time_estimate": "Approximate speaking time"
}
],
"audio_elements": {
"music": ["Description of music used"],
"sound_effects": ["Notable sound effects"],
"ambient": ["Background audio"]
},
"on_screen_text": ["All text that appears on screen"],
"sections": [
{
"title": "Section name",
"timestamp": "start-end time",
"summary": "What this section covers"
}
],
"key_topics": ["Main topics covered"],
"visual_style": "Description of the visual style, editing, camera work"
}
Be thorough and precise. Include timestamps where possible. Return ONLY valid JSON, no markdown formatting."""
What does this prompt do?
By embedding the full JSON schema directly in the prompt, you are giving Gemini an unambiguous template to fill in. Each key tells the model exactly what information to extract.
The final instruction ("Return ONLY valid JSON, no markdown formatting") reduces the chance of Gemini wrapping its response in markdown code fences. You will still handle that case in code, but the instruction helps.
Next, update the analyze_video function to use this prompt and parse the JSON response.
- Replace the entire analyze_video function with this updated version that returns a parsed dictionary:
def analyze_video(video_path: str) -> dict:
client = genai.Client()
print(f'Uploading video: {video_path}')
video_file = client.files.upload(file=video_path)
while not video_file.state or video_file.state.name != 'ACTIVE':
print('Processing video...')
time.sleep(5)
video_file = client.files.get(name=video_file.name)
print('Analyzing video with Gemini...')
response = client.models.generate_content(
model='gemini-3.5-flash',
contents=[video_file, ANALYSIS_PROMPT],
)
# Strip markdown code fences if Gemini wraps the JSON
raw_text = response.text
if raw_text.startswith('```'):
raw_text = raw_text.split('\n', 1)[1]
raw_text = raw_text.rsplit('```', 1)[0]
return json.loads(raw_text)
What changed in this function?
- The return type changed from str to dict. Instead of raw text, you now get a structured Python dictionary.
- The prompt passed to generate_content is now ANALYSIS_PROMPT instead of the generic one-liner.
- The code fence stripping handles cases where Gemini ignores the "no markdown" instruction and wraps JSON in triple backticks anyway.
- json.loads() parses the raw text into a Python dictionary you can query by key.
You also need to temporarily update the main block so you can test the new structured output. You will replace it again later in this step.
- Find the if __name__ == '__main__': block at the bottom of the file. Replace the last two lines (result = analyze_video(sys.argv[1]) and print(result)) with:
analysis = analyze_video(sys.argv[1])
print(json.dumps(analysis, indent=2))
- Save analyze.py.
Before you run this, what do you expect to see? The same video analysis as before, but this time formatted as clean, indented JSON with labeled keys instead of freeform paragraphs.
- Test the structured output by running the same video from Step 2:
python3 analyze.py ./test_video.mp4
You should see neatly indented JSON printed to your terminal, with keys like title, scenes, speakers, and key_topics each filled with data about your video.
Seeing a JSONDecodeError?
Gemini occasionally returns text that is not valid JSON, especially for very short videos. Run the command again. LLM outputs are non-deterministic, and a second attempt usually succeeds.
If it keeps failing, check that your ANALYSIS_PROMPT ends with the closing triple quotes on the same line as the final instruction. A missing quote will cause a Python syntax error before the script even runs.
help me debug a JSONDecodeError when parsing Gemini's video analysis response
Add report generation
Structured JSON is powerful for machines, but you also want a report you can read at a glance. You will now add two functions: one that saves results as JSON and Markdown files, and one that transforms analysis data into a readable Markdown document.
- Below the analyze_video function in analyze.py, add the save_results function:
def save_results(analysis: dict, source: str, output_dir: str = 'reports') -> tuple:
os.makedirs(output_dir, exist_ok=True)
# Create a filesystem-safe filename from the video title
safe_title = ''.join(c if c.isalnum() or c in ' -_' else '' for c in analysis.get('title', 'untitled'))
safe_title = safe_title.strip().replace(' ', '_')[:50]
# Save the raw analysis as JSON
json_path = os.path.join(output_dir, f'{safe_title}.json')
with open(json_path, 'w') as f:
json.dump({'source': source, 'analysis': analysis}, f, indent=2)
# Save a human-readable Markdown report
md_path = os.path.join(output_dir, f'{safe_title}.md')
report = generate_markdown_report(analysis, source)
with open(md_path, 'w') as f:
f.write(report)
return json_path, md_path
What does save_results do?
- Creates a reports/ directory if it does not exist.
- Sanitizes the video title into a safe filename (removing special characters, capping at 50 characters).
- Writes the JSON analysis with its source URL or path, forming your queryable knowledge base.
- Calls generate_markdown_report() and writes the result as a .md file.
Now add the Markdown report generator. This function transforms the structured dictionary into a formatted document with headings, bullet points, and sections.
- Below save_results, add the first part of the generate_markdown_report function:
def generate_markdown_report(analysis: dict, source: str) -> str:
lines = []
lines.append(f"# {analysis.get('title', 'Video Analysis')}")
lines.append(f"\nSource: {source}")
lines.append(f"Duration: {analysis.get('duration_estimate', 'Unknown')}")
lines.append(f"\n## Summary\n\n{analysis.get('summary', 'No summary available.')}")
if analysis.get('sections'):
lines.append('\n## Sections\n')
for section in analysis['sections']:
lines.append(f"- **{section.get('timestamp', '')}** - {section.get('title', '')}: {section.get('summary', '')}")
if analysis.get('transcription'):
lines.append(f"\n## Transcription\n\n{analysis['transcription']}")
if analysis.get('scenes'):
lines.append('\n## Scene Breakdown\n')
for scene in analysis['scenes']:
lines.append(f"### {scene.get('timestamp', 'Unknown time')}\n")
lines.append(f"**Visual:** {scene.get('description', '')}")
lines.append(f"**Audio:** {scene.get('audio', '')}")
if scene.get('on_screen_text'):
lines.append(f"**On-screen text:** {scene['on_screen_text']}")
lines.append('')
- Continuing inside the same function, add the remaining sections directly below the scenes block:
if analysis.get('speakers'):
lines.append('\n## Speakers\n')
for speaker in analysis['speakers']:
lines.append(f"- **{speaker.get('label', '')}**: {speaker.get('description', '')} ({speaker.get('speaking_time_estimate', '')})")
if analysis.get('audio_elements'):
lines.append('\n## Audio Elements\n')
audio = analysis['audio_elements']
if audio.get('music'):
lines.append(f"**Music:** {', '.join(audio['music'])}")
if audio.get('sound_effects'):
lines.append(f"**Sound Effects:** {', '.join(audio['sound_effects'])}")
if audio.get('ambient'):
lines.append(f"**Ambient:** {', '.join(audio['ambient'])}")
if analysis.get('on_screen_text'):
lines.append('\n## On-Screen Text\n')
for text in analysis['on_screen_text']:
lines.append(f'- {text}')
if analysis.get('visual_style'):
lines.append(f"\n## Visual Style\n\n{analysis['visual_style']}")
if analysis.get('key_topics'):
lines.append('\n## Key Topics\n')
for topic in analysis['key_topics']:
lines.append(f'- {topic}')
return '\n'.join(lines)
What does this report generator do?
Each if block checks whether a section has data before rendering it. This handles videos where Gemini could not detect speakers or on-screen text gracefully.
The function builds a list of Markdown lines, then joins them at the end. This pattern avoids repeated string concatenation and makes the output order easy to rearrange later.
- Save analyze.py.
Test structured output and save reports
The last piece is wiring the new functions into the CLI so running the script automatically saves both files.
- Replace the entire if __name__ == '__main__': block at the bottom of analyze.py with this final version:
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python analyze.py <video_path>')
sys.exit(1)
analysis = analyze_video(sys.argv[1])
json_path, md_path = save_results(analysis, sys.argv[1])
print(f'\nAnalysis complete!')
print(f'JSON saved to: {json_path}')
print(f'Report saved to: {md_path}')
What does this do?
The main block now calls save_results after analysis, which writes both the JSON and Markdown files. It prints the file paths so you know exactly where to find your reports.
- Save analyze.py.
Before you run this, what do you expect to happen? The script should analyze the video, create a reports/ folder, and save two files inside it.
- Run the analysis again with the same video:
python3 analyze.py ./test_video.mp4
You should see output like this:
Analysis complete!
JSON saved to: reports/Your_Video_Title.json
Report saved to: reports/Your_Video_Title.md
- Confirm the files exist by running:
ls reports/
You should see a .json file and a .md file listed.
- Open the JSON file to verify the structure:
cat reports/*.json | python3 -m json.tool | head -30
You should see neatly formatted JSON with a source key and an analysis object containing title, transcription, scenes, speakers, and sections as distinct keys.
Reports directory not created?
Make sure you saved analyze.py after adding the save_results function. If the script printed JSON to the terminal but no files appeared, the main block is still using the old print(json.dumps(...)) line instead of calling save_results.
Also check that generate_markdown_report is defined before save_results calls it. Python resolves function names at call time (not definition time), so the order in the file does not matter as long as both functions exist before the script runs.
help me debug why my reports directory is not being created
✔️ Awesome, I've got everything!
Great. Double check you have saved analyze.py with all the new functions.
ⓧ I'd like to double check the full code
Here is the complete analyze.py at this point. Scroll through and confirm each section matches yours:
- Imports: sys, os, json, time, Path, load_dotenv, genai.
- ANALYSIS_PROMPT constant with the full JSON schema (title, duration_estimate, summary, transcription, scenes, speakers, audio_elements, on_screen_text, sections, key_topics, visual_style).
- analyze_video() function that uploads, polls, sends the prompt, strips code fences, and returns parsed JSON.
- save_results() function that creates reports/, sanitizes the title, writes JSON and Markdown.
- generate_markdown_report() function that builds Markdown with sections for summary, sections, transcription, scenes, speakers, audio elements, on-screen text, visual style, and key topics.
- Main block that calls analyze_video then save_results and prints the file paths.
The difference is dramatic. Instead of a wall of prose you cannot search, you now have a queryable JSON file with distinct keys for every dimension of the video. Your reports/ directory is the beginning of your knowledge base. Next up, you will add the ability to paste any YouTube or Instagram URL directly instead of manually saving videos first.
Add Video Downloading from URLs
You proved in the last step that Gemini can deeply understand a video from a single file path. But right now, analyzing a creator's content means manually saving every video to your computer first.
In this step, you will add a download module powered by yt-dlp. After this, you can paste any YouTube or Instagram URL directly into your terminal and the tool handles the rest.
In this step, get ready to:
- Create a download module that fetches videos from URLs.
- Integrate URL detection into the CLI so it auto-downloads before analyzing.
- Test the full pipeline with a public YouTube and Instagram URL.
Create the download module
Your analyzer currently only accepts a local file path. You need a separate module that takes any URL, downloads the video, and returns a local path your existing analyze_video() function can work with.
- Create a new file called downloader.py in your video-analyzer folder by pasting in the following code:
import os
import yt_dlp
def download_video(url: str, output_dir: str = "downloads") -> str:
"""Download a video from a URL using yt-dlp. Returns the local file path."""
# Create the downloads directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Template for naming the downloaded file
output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
# Configure yt-dlp for best quality MP4 output
ydl_opts = {
"format": "bestvideo+bestaudio/best",
"merge_output_format": "mp4",
"outtmpl": output_template,
"noplaylist": True,
"quiet": False,
}
# Download the video and determine the final filename
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
filename = ydl.prepare_filename(info)
if not filename.endswith(".mp4"):
filename = os.path.splitext(filename)[0] + ".mp4"
return filename
What does this code do?
The download_video() function wraps yt-dlp's Python API to download any video from a supported URL.
- format: "bestvideo+bestaudio/best" tells yt-dlp to grab the highest quality video and audio streams, then merge them. If that fails, it falls back to a single combined stream.
- merge_output_format: "mp4" ensures the merged output is always an MP4 file (which Gemini accepts).
- noplaylist: True prevents accidentally downloading an entire playlist when given a video URL from one.
- extract_info(url, download=True) fetches metadata and downloads the video in one call, returning an info dictionary.
- prepare_filename(info) reconstructs the output filename from the same template yt-dlp used, so you know exactly where the file landed.
- Save downloader.py.
- Verify the module loads correctly by running this command:
python3 -c "from downloader import download_video; print('Download module ready')"
You should see Download module ready printed with no errors.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate from inside your video-analyzer folder.
- Confirm you are running the command from inside the video-analyzer directory where downloader.py lives.
Still stuck? Help me fix my ModuleNotFoundError when importing downloader
Integrate downloading into the CLI
Now you need to teach analyze.py to detect whether the user passed a URL or a local file path, download the video if needed, and clean up afterwards.
- In analyze.py, find this import line near the top of the file:
from google import genai
- Add a new import directly below it, so the two lines look like this:
from google import genai
from downloader import download_video
- Next, scroll to the bottom of analyze.py and find the entire if __name__ == '__main__': block (the last 7 lines of the file).
- Delete that entire block and replace it with this main() function:
def main():
# Parse CLI arguments and route to the right handler
if len(sys.argv) < 2:
print('Usage: python analyze.py <video_path_or_url>')
print(' python analyze.py --batch <urls_file>')
sys.exit(1)
if sys.argv[1] == '--batch':
if len(sys.argv) < 3:
print('Error: --batch requires a file path.')
sys.exit(1)
run_batch(sys.argv[2])
else:
run_single(sys.argv[1])
What does this function do?
The main() function acts as a router. It checks what the user typed and sends the work to either run_single() (for a single video) or run_batch() (for processing a file of URLs). You will build run_batch() in the Secret Mission.
- Directly below the main() function you just added, paste the run_single() function, the run_batch() placeholder, and the entry point:
def run_single(source: str):
# Determine if the source is a URL or local file
video_path = source
is_url = source.startswith('http://') or source.startswith('https://')
if is_url:
print(f'Downloading video from: {source}')
video_path = download_video(source)
elif not os.path.exists(source):
print(f'Error: File not found: {source}')
sys.exit(1)
# Analyze and save results
analysis = analyze_video(video_path)
json_path, md_path = save_results(analysis, source)
print(f'\nAnalysis complete!')
print(f'JSON saved to: {json_path}')
print(f'Report saved to: {md_path}')
# Clean up downloaded file to save disk space
if is_url:
os.remove(video_path)
print('Cleaned up downloaded video.')
def run_batch(urls_file: str):
pass
if __name__ == '__main__':
main()
What does run_single() do?
This function handles the full lifecycle for one video:
- It checks if the input starts with http:// or https://. If so, it downloads the video first.
- If the input is a local path, it verifies the file exists before proceeding.
- After analysis, it automatically deletes the downloaded file to keep your disk clean. The report in reports/ is what you actually want to keep.
The run_batch() function is a stub for now. You will implement it in the Secret Mission.
- Save analyze.py.
- Verify the new CLI works by running the script with no arguments:
python3 analyze.py
You should see the updated usage message:
Usage: python analyze.py <video_path_or_url>
python analyze.py --batch <urls_file>
Seeing an ImportError for downloader?
- Make sure you added from downloader import download_video as the line directly below from google import genai at the top of analyze.py.
- Double check that downloader.py is saved in the same folder as analyze.py.
Need more help? Help me fix the ImportError for downloader in analyze.py
Test with YouTube and Instagram
The entire pipeline is now connected. Time to prove it works end-to-end with real URLs from YouTube and Instagram.
Before you run the next command, what do you think will happen? Will the tool download the video, upload it, analyze it, and save a report all in one go?
- Test with a public YouTube video by running this command (you can substitute any public YouTube URL):
python3 analyze.py "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
You will see yt-dlp download the video, then the familiar upload and analysis flow from the previous step. After about a minute, you should see output confirming both the JSON and Markdown report were saved to reports/.
- Confirm the report was created by running:
ls reports/
You should see new .json and .md files for the video you just analyzed.
- Now test with a public Instagram Reel URL:
python3 analyze.py "https://www.instagram.com/reel/YOUR_PUBLIC_REEL_ID/"
Replace the URL with any public Instagram Reel you want to analyze. The same download-analyze-report flow applies.
What about private Instagram content?
Public Reels and posts work directly with yt-dlp. Private content or stories require browser cookies, which is out of scope for this project. If you see an error about a private video, try a different public URL.
Download failing or timing out?
- Make sure ffmpeg is installed by running ffmpeg -version. Without it, yt-dlp cannot merge separate video and audio streams into one MP4.
- If you see a permissions or geo-restriction error, the video may not be available in your region. Try a different public video URL.
- For YouTube errors mentioning sign-in requirements, the video may be age-restricted. Choose a different unrestricted video.
Still stuck? Help me troubleshoot a yt-dlp download failure
✔️ Awesome, I've got everything!
Great. Double check you have saved both downloader.py and analyze.py.
ⓧ I'd like to double check the full code
Here is the complete downloader.py file:
import os
import yt_dlp
def download_video(url: str, output_dir: str = "downloads") -> str:
"""Download a video from a URL using yt-dlp. Returns the local file path."""
os.makedirs(output_dir, exist_ok=True)
output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
ydl_opts = {
"format": "bestvideo+bestaudio/best",
"merge_output_format": "mp4",
"outtmpl": output_template,
"noplaylist": True,
"quiet": False,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
filename = ydl.prepare_filename(info)
if not filename.endswith(".mp4"):
filename = os.path.splitext(filename)[0] + ".mp4"
return filename
For analyze.py, confirm the following pieces are present in order:
- The import block at the top, including from downloader import download_video.
- The ANALYSIS_PROMPT variable (unchanged from the previous step).
- The analyze_video() function (unchanged).
- The save_results() function (unchanged).
- The generate_markdown_report() function (unchanged).
- The new main() function that parses CLI arguments.
- The new run_single() function that handles URL detection, downloading, analyzing, and cleanup.
- The run_batch() stub (just pass).
- The if __name__ == '__main__': main() entry point at the very end.
You now have a tool that accepts URLs straight from your clipboard. No more manual downloads. Next up, you will extend this into a batch mode that can chew through an entire list of creator videos in one command.
Secret mission
Add Batch Mode to Analyze Multiple Videos
Analyzing videos one at a time is powerful. Analyzing 15+ videos in batch? That's where patterns emerge across creators, formats, and styles. Add batch mode to your analyzer so you can feed it a text file of URLs and walk away while it builds your knowledge base automatically.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no persistent services. The Gemini API is billed per use only when you run the tool.
Cost warning
Your Gemini API key remains active after this project. Each video analysis costs approximately $0.03 for a short video. If you do not plan to use the tool again, delete your API key to prevent any accidental or unauthorized usage charges.
Resources you used:
- video-analyzer project folder on your Desktop (contains all code, virtual environment, reports, and downloads).
- Gemini API key in Google AI Studio (billed per token when used).
- Uploaded videos in the Gemini Files API (automatically deleted after 48 hours).
Keep everything running
No action needed. Choose this if you plan to keep analyzing videos.
- The tool runs on-demand from your terminal. There are no background services or recurring charges.
- Your API key stays active but costs nothing until you run an analysis.
- Any videos you uploaded to the Gemini Files API are automatically deleted after 48 hours.
Pause - I'll come back to this later
Free up disk space while keeping the project intact. There are no running processes to stop.
- Delete the downloads/ folder inside your project to reclaim disk space from any leftover video files.
- Your API key and project code remain ready to use whenever you return.
Delete - I don't want to use this again
Remove all project files and revoke your API key to fully clean up.
- Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/video-analyzer
On Windows, run this instead:
Remove-Item -Recurse -Force ~\Desktop\video-analyzer
- Delete your Gemini API key to prevent any future charges:
- Open the Google Cloud API Credentials page at console.cloud.google.com/apis/credentials.
- Find the API key you used for this project and click the Actions icon next to it.
- Select Delete API key.
- Click Delete in the confirmation modal. Deletion takes a few minutes to propagate.
Nice Work!
Nice Work!
You just built a fully functional video analysis engine powered by multimodal AI. From a single terminal command, you can now extract structured intelligence from any video on the internet.
You've learned how to:
- Build a Python CLI tool that accepts local video files, YouTube URLs, and Instagram Reel links as input, routing each through a download-then-analyze pipeline.
- Integrate Google Gemini's native video understanding API with engineered prompts that produce structured JSON analysis covering scenes, transcription, speakers, audio elements, on-screen text, and visual style.
- Create a dual-output system that saves every analysis as both a human-readable Markdown report and a machine-readable JSON knowledge base designed for cross-video comparison in future projects.
- Secret Mission: Added batch mode to analyze multiple videos from a URL file in one command, with progress reporting and graceful error handling.
Ready to quiz yourself?