← 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. Both files are 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 is not reading the captions. It looks at the actual frames, the cuts, the on-screen text, the b-roll, and listens to the audio. A transcript is only words, and words are not what makes a video work. That is why this can tell you why a hook landed, not just what was said.

It runs in Claude or Codex, it is free, and you set it up once.

How It Actually Works

Claude and ChatGPT cannot watch video. Google's Gemini can, natively, on a free tier. So the skill lives in Claude or Codex and hands the video to Gemini to watch. Your AI's reasoning, plus eyes and ears on the footage.

Step 1

The link goes straight in

For a public YouTube video, nothing gets downloaded. The URL passes straight through and is read from YouTube itself. No downloader, no folder full of files. Got a saved Reel or a Loom instead? It sends that file.

Step 2

It samples frames and reads the audio

The video becomes still frames plus the audio track, read together. That is how it knows a caption appeared at 00:04 and what was said over it.

Step 3

It answers in a fixed structure

Summary, scene-by-scene with timestamps, transcript, every caption, hook and turn and payoff, pacing, key moments. Then your actual question. That structure is locked into the file, so you get the same shape every run.

The One Thing Almost Everyone Gets Wrong

By default, video is sampled at one frame per second. A Reel cuts every half second or faster. So on the exact videos most people want to study, the default skips most of the cuts, then confidently describes an edit it never saw.

That is why this takes a --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 pay for the part you care about. That one flag is the difference between a vague summary and a real shot-by-shot teardown.

The Skill (Two Files)

Two files, and the split is why this stays reliable. The first tells your AI when to use this and how to make the judgment calls. The second does the work, the same way every time.

Most skills online are one file asking the AI to improvise the technical part on every run, which is why they work once and behave differently the next day. Judgment in the instructions, mechanics in a script, and it stops drifting.

File 1 of 2. Save as SKILL.md.

SKILL.md
--- name: watch-video description: Watch and analyze a YouTube video or a local video file. Reads the actual frames and the audio, not just the transcript, and returns a scene-by-scene breakdown with timestamps, the transcript, the hook/turn/payoff structure, the pacing, and every on-screen caption. Use whenever I paste a video link or a video file and ask what happened, why it worked, or to turn it into a plan. 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, which is already on every Mac. - 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 the current Gemini Flash 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 it: ```bash python3 ~/.claude/skills/watch-video/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 what Python already ships with, so there is nothing to 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 = 100 * 1024 * 1024 UPLOAD_TIMEOUT_SEC = 300 POLL_SEC = 3 MIME = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".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-2.5-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, and they matter more than they sound. Asked to analyze a video, a model will happily invent a narrator, a brand, or a quote that was never there, especially on a silent screen recording where it expects narration. These force it to say "no speech detected" instead. I tested it on a video whose speaker is famous, 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 and either Claude or Codex. Nothing to install, no credit card.

  1. Get a free key at Google AI Studio. Sign in, click Get API key, copy it.
  2. Make a folder called watch-video with both files in it. In Claude Code that is ~/.claude/skills/watch-video/. In Codex, drop it into your project. The folder name must match the name at the top of SKILL.md, which is how your AI finds it.
  3. Give it the key by asking: "Set my GEMINI_API_KEY to [your key] so it is available in every new terminal." Claude edits your shell profile and confirms it worked, so you never touch a config file. 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 generous for one video at a time. Two limits worth knowing: free accounts can read up to 8 hours of YouTube per day, and links only work on public videos. Treat the key like a password.

The Commands I Actually Run

The five I use most. Paste one and swap the bracketed part. The flags at the top are what make the difference, so keep them.

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

Studying the best videos in your space used to mean an hour of rewatching and note-taking. Now it is a link, a question, and about a minute.

When It Breaks

Every error it throws and the fix. The script prints these under the error too, so you are never guessing.

GEMINI_API_KEY is not setThe key is not visible to that terminal. Open a brand new window. If it still fails, ask Claude 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-2.5-flash.
It will not read the linkPrivate, unlisted, and age-restricted videos cannot be read from a URL. 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 narratorYou are missing the rules section. Copy SKILL.md exactly, bottom included. That is the part doing the work.
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.