← Back to all free guides

The AI Skill That
Watches YouTube For You

Paste a link and your AI watches the whole thing, the frames and the audio, then answers anything you ask. No downloading. Reverse-engineer a viral video in about a minute. The skill is below to copy or download.

I built a skill that lets my AI watch a video. I paste a YouTube link, and instead of sitting through twenty minutes, it watches the whole thing and answers anything I ask about it.

It doesn’t stop at the captions. It looks at the frames, the cuts, the on-screen text and the b-roll, and it listens to the audio. That’s how it can tell you why a hook landed instead of only what was said.

It runs in Claude Code or Codex, it’s free to start, and you set it up once.

How It Actually Works

Claude can’t take a video file on its own. Google’s Gemini can, natively, with a free tier. So the skill lives in Claude Code or Codex and hands the video to Gemini to watch, which gives your AI eyes and ears on the footage.

01

The link goes straight in

For a public YouTube video, nothing gets downloaded. Gemini reads the URL straight from YouTube. Got a saved Reel or a Loom file instead? It sends that file.

02

It samples frames and reads the audio

Gemini reads still frames and the audio track together, so it knows a caption appeared at 00:04 and what was said over it.

03

It answers in a fixed structure

You get a summary, a scene-by-scene breakdown with timestamps, the transcript, every caption, the hook, turn and payoff, the pacing and the key moments. Then it answers your question. The structure is locked into the file, so every run comes back in the same shape.

The One Thing Almost Everyone Gets Wrong

By default, Gemini samples video at one frame per second. A Reel often cuts every half second or faster, so on the videos most people want to study, the default skips cuts and then confidently describes an edit it never saw.

That’s why this takes an --fps setting. Turn it to 8 or 10 and it reads the opening frame by frame. Pair it with --clip 0:00-0:05 so you only spend quota on the part you care about.

The Skill (Two Files)

Two files, and the split is why this stays reliable. The first tells your AI when to use it and how to make the judgment calls. The second does the technical work the same way every time, so it doesn’t drift from one run to the next.

File 1 of 2. Save as SKILL.md.

SKILL.md
--- name: watch-video description: Watch a YouTube link or local video file. Reads the frames and audio, then returns timestamped scenes, transcript, captions, hook structure and pacing. Use when I share a video. argument-hint: <youtube-url-or-file-path> [--clip 0:00-0:05] [--fps 8] [--prompt "..."] allowed-tools: Bash, Read --- # Watch Video Turn any YouTube link or local video file into a real breakdown: what is on screen, what is said, why it works, and how to use it. ## Prerequisites - Python 3.9 or newer. On a Mac, if `python3 --version` asks you to install the command line developer tools, say yes. - GEMINI_API_KEY set in the shell. Free key at https://aistudio.google.com/apikey - Nothing to pip install. The script uses only the Python standard library. ## Steps 1. Read the arguments I gave you: - **source** (required): a YouTube URL, or a path to a local video file - **--clip** (optional): analyze only part of it, like `0:00-0:05` - **--fps** (optional): how many frames per second to actually look at, default 1 - **--prompt** (optional): ask one specific thing instead of the full report - **--model** (optional): defaults to `gemini-flash-latest`, Google's alias for the newest Flash model 2. Decide the frame rate BEFORE you run it. This is the part that matters most: - **Short-form video** (Reels, TikToks, Shorts, ads) cuts every half second or faster. At the default of 1 frame per second you will miss most of the cuts and report the edit wrong. Use `--fps 6` to `--fps 10`. - **If I am asking about the hook**, use `--clip 0:00-0:05 --fps 10`. That reads the opening frame by frame. - **Long talking-head video**, tutorial, podcast, webinar, screen recording: leave the default. Raising it wastes quota and adds nothing. - **Never** put a high `--fps` on a long video. Always pair a high `--fps` with a `--clip`. 3. Run the script that sits in this skill's folder. In Claude Code, `${CLAUDE_SKILL_DIR}` points there. In Codex, use the folder path instead, usually `~/.agents/skills/watch-video/`. ```bash python3 "${CLAUDE_SKILL_DIR}/watch_video.py" "<source>" [flags] ``` 4. Show me the report as it came back. Do not summarize it away, do not renumber or round the timestamps, and do not add anything the analysis did not actually return. 5. If it errors, the script prints the exact fix underneath the error. Give me that one command instead of guessing at it. ## Rules, do not break these - Only describe what is actually in the video. Never invent a creator, a speaker, a brand, a statistic, or a quote. - Never invent a voiceover. Plenty of videos are silent, music only, or ambient noise only, and that is normal. Say so instead of filling the gap. - Use only the timestamps the analysis returned. Never estimate or round one to look tidy. - A private, unlisted, or age-restricted link cannot be read from the URL. Tell me to download the file and pass the path instead. - Keep my local video files local. Never upload anything I did not point you at.

File 2 of 2. Save as watch_video.py in the same folder. It uses only Python’s standard library, so there’s no pip install.

watch_video.py
#!/usr/bin/env python3 """ watch_video.py - let your AI watch a YouTube video or a local video file. Usage: python3 watch_video.py <youtube-url-or-file> [--prompt "..."] [--clip 0:00-0:05] [--fps N] [--model ...] Env: GEMINI_API_KEY - required. Free key at https://aistudio.google.com/apikey Standard library only. No pip install. """ import argparse import base64 import json import os import re import sys import time import urllib.error import urllib.request from pathlib import Path API = "https://generativelanguage.googleapis.com/v1beta" UPLOAD_API = "https://generativelanguage.googleapis.com/upload/v1beta" DEFAULT_MODEL = "gemini-flash-latest" INLINE_LIMIT = 20 * 1024 * 1024 # bigger files go through the Files API UPLOAD_TIMEOUT_SEC = 300 POLL_SEC = 3 MIME = { ".mp4": "video/mp4", ".mov": "video/mov", ".webm": "video/webm", ".avi": "video/avi", ".mpeg": "video/mpeg", ".mpg": "video/mpg", ".flv": "video/x-flv", ".wmv": "video/wmv", ".3gp": "video/3gpp", ".3gpp": "video/3gpp", } DEFAULT_PROMPT = """Analyze this video and return a structured markdown report using the exact sections below. ACCURACY RULES - these override everything else: 1. Report only what is actually in the video. Do not infer, guess, or fill in plausible-sounding detail. 2. Never invent a creator, presenter, narrator, or speaker name. If no name is shown on screen or clearly spoken, say the video has no identified creator. 3. Never fabricate a voiceover, dialogue, or transcript. Many videos are silent or have music only, and that is normal. If there is no speech, say "No speech detected." 4. Separate what you SEE from what you INFER. Label anything inferred with "(inferred)". 5. Use only real timestamps taken from the video. Never estimate or invent one. ## Summary Two to four sentences on what actually happens and who the video is for. ## Scene-by-Scene Breakdown Walk the video in order with `MM:SS` timestamps for every distinct cut, scene, or beat. For each one give: - what is on screen (people, setting, b-roll, product, UI) - what is said, if anything - any on-screen text or caption, quoted verbatim - the cut or transition that ends the beat ## Audio Report only what you actually hear. Valid answers include "No audio track present", "Silent - no speech, music, or effects detected", "Music only: [describe]", or a verbatim transcript with `MM:SS` timestamps if speech is genuinely present. ## On-Screen Text and Visuals Every caption, title card, or overlay, quoted verbatim with its timestamp. Then the caption style, the text placement, and any branding or products actually shown. ## Structure Identify these four beats with timestamps, and say plainly if one is missing: - The hook: the first 3 seconds, what is said and what is shown - The turn: where the video shifts or the tension is introduced - The payoff: the moment the promise is delivered - The close: how it ends and what the viewer is asked to do ## Pacing Average seconds per cut, the total number of cuts, and where the pace changes. ## Key Moments Three to seven `[MM:SS] Description` bullets a viewer would actually remember. Be concrete. When you are unsure, say so. Reporting less with confidence beats reporting more with confabulation.""" def die(msg, hint=None): print(f"ERROR: {msg}", file=sys.stderr) if hint: print(hint, file=sys.stderr) sys.exit(1) def log(msg): print(f"[info] {msg}", file=sys.stderr) def get_key(): key = os.environ.get("GEMINI_API_KEY", "").strip() if not key: die( "GEMINI_API_KEY is not set in this shell.", 'Fix: export GEMINI_API_KEY="your-key" (get a free key at https://aistudio.google.com/apikey)\n' "If you already added it to ~/.zshrc, open a new terminal or run: source ~/.zshrc", ) return key def is_youtube(s): return bool(re.match(r"https?://(www\.|m\.)?(youtube\.com|youtu\.be)/", s.strip(), re.I)) def to_seconds(stamp): """'0:05' or '1:23:45' or '90' -> '90s'""" parts = stamp.strip().split(":") try: nums = [float(p) for p in parts] except ValueError: die(f"Could not read the time '{stamp}'. Use MM:SS, like 0:05.") total = 0.0 for n in nums: total = total * 60 + n return f"{int(total)}s" def parse_clip(clip): if "-" not in clip: die("--clip needs a range, like --clip 0:00-0:05") start, end = clip.split("-", 1) return to_seconds(start), to_seconds(end) def request_json(url, payload=None, headers=None, method=None, raw=None, timeout=600): data = raw if raw is not None else (json.dumps(payload).encode() if payload is not None else None) req = urllib.request.Request(url, data=data, method=method or ("POST" if data else "GET")) req.add_header("Content-Type", "application/json") for k, v in (headers or {}).items(): req.add_header(k, v) try: with urllib.request.urlopen(req, timeout=timeout) as r: body = r.read() return json.loads(body) if body else {}, dict(r.headers) except urllib.error.HTTPError as e: detail = e.read().decode("utf-8", "replace")[:600] try: detail = json.loads(detail)["error"]["message"] except Exception: pass if e.code == 400 and "API key not valid" in detail: die("Your GEMINI_API_KEY was rejected.", "Get a fresh key at https://aistudio.google.com/apikey") if e.code == 429: die("Rate limit hit on the free tier.", "Wait a minute and retry, or analyze a shorter clip with --clip.") if e.code == 404: die(f"Model not found. {detail}", "Try: --model gemini-3.8-flash") die(f"HTTP {e.code}: {detail}") except urllib.error.URLError as e: die(f"Could not reach the API: {e.reason}") def upload_large_file(path, key, mime): """Resumable upload for local files over the inline limit.""" size = path.stat().st_size log(f"{size / 1024 / 1024:.1f} MB - uploading to the Files API...") _, headers = request_json( f"{UPLOAD_API}/files?key={key}", payload={"file": {"display_name": path.name}}, headers={ "X-Goog-Upload-Protocol": "resumable", "X-Goog-Upload-Command": "start", "X-Goog-Upload-Header-Content-Length": str(size), "X-Goog-Upload-Header-Content-Type": mime, }, ) upload_url = headers.get("X-Goog-Upload-URL") or headers.get("x-goog-upload-url") if not upload_url: die("The upload did not start. Try again, or use a shorter clip.") result, _ = request_json( upload_url, raw=path.read_bytes(), headers={ "Content-Length": str(size), "X-Goog-Upload-Offset": "0", "X-Goog-Upload-Command": "upload, finalize", }, ) info = result.get("file", {}) name, uri = info.get("name"), info.get("uri") log("uploaded, waiting for processing...") waited = 0 while waited < UPLOAD_TIMEOUT_SEC: state = info.get("state") if state == "ACTIVE": log(f"ready in {waited}s") return uri, info.get("mimeType", mime) if state == "FAILED": die("Gemini could not process that file.", "Try re-exporting it as an .mp4") time.sleep(POLL_SEC) waited += POLL_SEC info, _ = request_json(f"{API}/{name}?key={key}") die(f"Upload still processing after {UPLOAD_TIMEOUT_SEC}s.", "Try a shorter clip with --clip.") def build_video_part(source, key, fps, clip): meta = {} if fps: meta["fps"] = fps if clip: start, end = parse_clip(clip) meta["start_offset"] = start meta["end_offset"] = end if is_youtube(source): log("reading the YouTube link directly, no download needed") part = {"file_data": {"file_uri": source}} else: path = Path(source).expanduser().resolve() if not path.is_file(): die(f"No file at {path}") ext = path.suffix.lower() if ext not in MIME: die(f"'{ext}' is not a supported video format.", f"Supported: {', '.join(sorted(MIME))}") mime = MIME[ext] if path.stat().st_size <= INLINE_LIMIT: log(f"{path.stat().st_size / 1024 / 1024:.1f} MB - sending the file directly") part = {"inline_data": {"mime_type": mime, "data": base64.b64encode(path.read_bytes()).decode()}} else: uri, mime = upload_large_file(path, key, mime) part = {"file_data": {"file_uri": uri, "mime_type": mime}} if meta: part["video_metadata"] = meta return part def main(): p = argparse.ArgumentParser(description="Let your AI watch a video.") p.add_argument("source", help="A YouTube URL or a path to a local video file") p.add_argument("--prompt", default=DEFAULT_PROMPT, help="Ask something specific instead of the full report") p.add_argument("--clip", default=None, help="Analyze only part of it, like 0:00-0:05") p.add_argument("--fps", type=float, default=None, help="Frames sampled per second. Default 1. Use 6-10 for fast cuts.") p.add_argument("--model", default=DEFAULT_MODEL) args = p.parse_args() key = get_key() part = build_video_part(args.source, key, args.fps, args.clip) log(f"analyzing with {args.model}...") result, _ = request_json( f"{API}/models/{args.model}:generateContent?key={key}", payload={"contents": [{"parts": [part, {"text": args.prompt}]}]}, ) try: cand = result["candidates"][0] text = "".join(x.get("text", "") for x in cand["content"]["parts"]) except (KeyError, IndexError): blocked = result.get("promptFeedback", {}).get("blockReason") if blocked: die(f"Gemini declined to analyze this video ({blocked}).") die(f"Unexpected response: {json.dumps(result)[:400]}") usage = result.get("usageMetadata", {}) frames = next((d["tokenCount"] for d in usage.get("promptTokensDetails", []) if d.get("modality") == "VIDEO"), None) if frames: log(f"done - {frames:,} video tokens read") print(text) if __name__ == "__main__": main()

The Rules That Keep It Honest

Both files carry hard rules against making things up. Asked to analyze a video, a model will happily invent a narrator, a brand or a quote, especially on a silent screen recording where it expects narration. These rules make it say "no speech detected" instead. I tested it on a video with a famous speaker, and it correctly refused to name him, because his name is never said or shown.

Set It Up Once

Five minutes, one time. You need a free Gemini key, Python 3 and either Claude Code or Codex. On a Mac, if typing python3 in Terminal asks to install developer tools, say yes.

  1. Get a key at Google AI Studio. Sign in, accept the terms, click Create API key (new accounts may get one made automatically) and copy it.
  2. Unzip so you have a watch-video folder with both files in it. For Claude Code it goes in ~/.claude/skills/. For Codex it goes in ~/.agents/skills/. The folder name must match the name at the top of SKILL.md.
  3. Ask your AI: "Set my GEMINI_API_KEY to [your key] so it’s available in every new terminal." It edits your shell profile for you. Then open a new terminal.
  4. Paste any public YouTube link and say "use the watch-video skill on this."

On The Free Tier

Plenty for one video at a time. Free accounts can read up to 8 hours of YouTube per day, and links only work on public videos. On the free tier, Google says it can use what you send to improve its products, so don’t feed it private client footage. Treat the key like a password.

The Commands I Actually Run

The five I use most. Paste one, swap the bracketed parts, and keep the flags.

1. Tear Apart The Hook
Use the watch-video skill on [VIDEO LINK] with --clip 0:00-0:05 --fps 10 Then tell me: every single cut in those 5 seconds with its timestamp, exactly what is on screen at each one, the words said over it, and the on-screen caption quoted word for word. Then explain why this hook stops the scroll, and rewrite it three ways for [YOUR NICHE].
2. Reverse-Engineer The Edit
Use the watch-video skill on [VIDEO LINK] with --fps 8 Give me the editing formula: how many cuts total, the average seconds per cut, where the pace speeds up or slows down, the caption style and where captions sit on screen, the b-roll, and the transitions. Then write me a shot list I could hand an editor to match this style for [YOUR NICHE].
3. Turn A Long Video Into A Playbook
Use the watch-video skill on [VIDEO LINK] Turn everything in this into a numbered, step-by-step playbook I can act on for [YOUR BUSINESS]. Note which timestamp each step came from so I can jump back to it. Flag anything that would not apply to my situation, and tell me what the video claims without backing up.
4. Steal The Structure, Not The Content
Use the watch-video skill on [VIDEO LINK] Strip out the topic entirely and give me the skeleton underneath: what the first line does, what the second beat does, where the tension goes, where the payoff lands, and how it closes. Write it as a reusable template with blanks. Then fill that template in once for [YOUR TOPIC].
5. Read A Competitor Across Their Whole Account
Use the watch-video skill on each of these one at a time, then compare them: [LINK 1] [LINK 2] [LINK 3] [LINK 4] [LINK 5] What is the same across all five: the hook pattern, the structure, the caption style, the length, and what they ask for at the end. Then tell me what they are clearly testing, what is working, and the one gap none of these five fill that [YOUR BUSINESS] could own.
How I Use It Every Week
When It Breaks

Every error it throws and the fix. The script prints the fix under the error too.

GEMINI_API_KEY is not setThat terminal can’t see the key yet. Open a brand new window. If it still fails, ask your AI to add it to your shell profile.
Your key was rejectedCopied wrong or deleted. Get a fresh one at Google AI Studio, and watch for a trailing space.
Rate limit hitWait a minute, or add --clip. High --fps on a long video burns quota fast, which is why you pair the two.
Model not foundModel names change. Re-run with --model gemini-3.8-flash.
It won’t read the linkOnly public videos work from a URL. For private, unlisted or age-restricted ones, download the file and pass the path.
Not a supported formatmp4, mov, webm, avi, mpeg, mpg, flv, wmv, 3gp. Anything else, re-export as mp4.
It invented a narratorThe rules section is missing. Copy SKILL.md exactly, bottom included.
Vague on a fast-cut videoIt only saw one frame per second. Re-run with --fps 8, or --clip 0:00-0:05 --fps 10 for the hook.