Install Watch Video
Pick your agent. Every command is ready to paste.
npx skills add coreyhaines31/makerskills --skill watch-videoThe open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.
01
Install it
Run the command above, or upload the folder in Claude.
02
Ask naturally
Describe the task. Your agent loads watch-video when the request matches its description.
03
Name it if needed
If it does not trigger, say “use the watch-video skill”. In Codex, type $watch-video.
/watch-video — Transcribe and analyze any video at the depth you choose
Replaces and broadens the prior youtube-transcript skill. YouTube is now one of many sources; depth is user-controlled.
Step 1 — Parse input
Accept:
- YouTube: full URL,
youtu.be/<id>,youtube.com/shorts/<id>, raw 11-char ID - Loom:
loom.com/share/<id>orloom.com/embed/<id> - Vimeo:
vimeo.com/<id> - Riverside: download URL or local file
- Zoom: local
.mp4from a downloaded recording - X / IG / TikTok video: URL — defers to
social-fetchfor metadata, uses yt-dlp for the file - Local file: any path to an
.mp4/.mov/.webm/.mkv
Detect source from URL pattern or file extension. If ambiguous, ask.
Step 2 — Parse depth mode
| Invocation | Mode | What you get |
|---|---|---|
/watch-video <url> |
transcript (default) | Clean text, metadata, optional chapters |
/watch-video <url> transcript |
transcript | Same as default |
/watch-video <url> visual |
visual | Transcript + frames at intervals + Claude vision pass identifying key moments |
/watch-video <url> multimodal |
multimodal | Native video to Gemini (if $GEMINI_API_KEY), else dense Claude vision frame-by-frame |
If the depth isn't specified and the video is >10 minutes, ask before defaulting (visual/multimodal cost real money on long videos).
Step 3 — Pull metadata
For URL sources, use yt-dlp:
yt-dlp --skip-download --print "%(.{title,uploader,duration_string,upload_date,webpage_url,chapters,description})j" "<url>"
One JSON object comes back. Descriptions run multi-line, so don't parse the output with | or line splits. chapters is absent when the video has none. upload_date is YYYYMMDD.
Capture: title, uploader/channel, duration, upload date, description (first paragraph), chapters (list or absent). Save the object as <workdir>/metadata.json once Step 4 creates the workdir.
For local files, use ffprobe:
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "<file>"
Step 4 — Build workdir
~/Documents/videos/<source>-<slug>-<date>/
Where:
source:youtube/loom/vimeo/riverside/zoom/localslug: kebab-case of title (first 4–6 words, max 50 chars)date:YYYY-MM-DD
Step 5 — Get the transcript
Backend selection (in order):
Platform-provided transcript if it exists and looks complete:
- YouTube:
yt-dlp --write-sub --write-auto-sub --skip-download --sub-langs "en,en-US,en-GB" --sub-format vtt -o "<workdir>/transcript" "<url>"— writes<workdir>/transcript.en.vtt. For a non-English video, runyt-dlp --list-subs "<url>"and swap in its code. Don't useen.*: it also matches auto-translated tracks (en-sq, …) and trips HTTP 429. - Loom: fetch via
https://www.loom.com/share/<id>page metadata or Loom API if$LOOM_API_KEYset - Riverside: built-in transcripts available on the recording's share page
- If platform transcript exists and has timestamps, use it. Skip Whisper.
- YouTube:
MLX-Whisper local (default on Apple Silicon Macs):
# Install once: pip install mlx-whisper
python3 -I - "<file>" "<workdir>/transcript" <<'PY'
import sys, mlx_whisper
r = mlx_whisper.transcribe(sys.argv[1], path_or_hf_repo="mlx-community/whisper-large-v3-turbo", condition_on_previous_text=False)
def ts(x):
h, m, s = int(x // 3600), int(x % 3600 // 60), x % 60
return f"{h:02}:{m:02}:{s:06.3f}".replace(".", ",")
segs = r["segments"]
open(sys.argv[2] + ".txt", "w", encoding="utf-8").write("\n".join(s["text"].strip() for s in segs) + "\n")
with open(sys.argv[2] + ".srt", "w", encoding="utf-8") as f:
for i, s in enumerate(segs, 1):
f.write(f"{i}\n{ts(s['start'])} --> {ts(s['end'])}\n{s['text'].strip()}\n\n")
PY
Both snippets write transcript.txt and a timestamped transcript.srt, which the loop check in Step 5 reads.
Or via the CLI: mlx_whisper <file> --model mlx-community/whisper-large-v3-turbo --output-dir <workdir>
- Not on Apple Silicon (Linux, Intel Mac, Windows, cloud agents):
faster-whisper(CPU or CUDA) orwhisper.cpp. Same model size.
# Install once: pip install faster-whisper "av>=17,<18"
python3 -I - "<file>" "<workdir>/transcript" <<'PY'
import sys
from faster_whisper import WhisperModel
model = WhisperModel("large-v3-turbo", compute_type="int8") # add device="cuda" on an NVIDIA GPU
segments, info = model.transcribe(sys.argv[1], vad_filter=True, condition_on_previous_text=False)
def ts(x):
h, m, s = int(x // 3600), int(x % 3600 // 60), x % 60
return f"{h:02}:{m:02}:{s:06.3f}".replace(".", ",")
with open(sys.argv[2] + ".txt", "w", encoding="utf-8") as txt, open(sys.argv[2] + ".srt", "w", encoding="utf-8") as srt:
for i, s in enumerate(segments, 1):
txt.write(s.text.strip() + "\n")
srt.write(f"{i}\n{ts(s.start)} --> {ts(s.end)}\n{s.text.strip()}\n\n")
PY
Run it with whichever python3 has the package (a venv's bin/python works). The av pin matters: faster-whisper 1.2.x passes an argument that PyAV 18+ removed, and transcription then dies with TypeError: open() got an unexpected keyword argument 'metadata_errors'.
Download the video file first if it's a URL (use yt-dlp; Loom/Vimeo/YT all supported):
yt-dlp -f "bv*[height<=720]+ba/b[height<=720]" --merge-output-format mp4 -o "<workdir>/video.%(ext)s" "<url>"
720p is plenty for transcription and frame analysis (smaller download, faster processing). Without --merge-output-format mp4, YouTube usually lands as video.webm and every video.mp4 below misses.
Clean the transcript (YouTube VTT — de-dups rolling captions, strips tags, decodes >/&, starts a new paragraph after a >2s gap between cues; Whisper output is already clean):
python3 -I - "<workdir>/transcript.en.vtt" "<workdir>/transcript.txt" <<'PY'
import html, re, sys
def secs(t):
return sum(float(p) * m for p, m in zip(reversed(t.replace(",", ".").split(":")), (1, 60, 3600)))
out, last, prev_end = [], None, None
for block in re.split(r"\n{2,}", open(sys.argv[1], encoding="utf-8").read().replace("\r\n", "\n")):
lines = block.strip().splitlines()
cue = next((i for i, l in enumerate(lines) if "-->" in l), None)
if cue is None:
continue
start, end = (secs(t.split()[0]) for t in lines[cue].split("-->"))
first = True
for raw in lines[cue + 1:]:
text = html.unescape(re.sub(r"<[^>]+>", "", raw)).strip()
if not text or text == last: # rolling captions repeat the previous line
continue
if first and prev_end is not None and start - prev_end > 2:
out.append("")
first = False
out.append(text)
last = text
prev_end = end
open(sys.argv[2], "w", encoding="utf-8").write("\n".join(out) + "\n")
PY
Verify the transcript for Whisper hallucination loops before using it. On long recordings (especially multi-speaker calls with silence/crosstalk), Whisper can fall into a repetition loop and emit the same caption for huge stretches — a real failure produced "Yep." for 95 of 105 minutes while looking like a successful run. Detect it by scanning for runs of ≥8 consecutive identical captions:
The check needs timestamped captions, so have Whisper write SRT (--output-format srt on the mlx_whisper CLI).
# entries = [(start_sec, text), ...] parsed from the SRT
i = 0
while i < len(entries):
j = i
while j+1 < len(entries) and entries[j+1][1] == entries[i][1]:
j += 1
if j - i >= 8:
print("hallucination run:", entries[i][0], "->", entries[j][0], repr(entries[i][1][:30]))
i = j + 1
If long runs appear, re-transcribe with conditioning disabled — this breaks the loop propagation:
mlx_whisper <file> --model mlx-community/whisper-large-v3-turbo --output-dir <workdir> --output-format srt --condition-on-previous-text False
On faster-whisper, pass condition_on_previous_text=False to transcribe(); on whisper.cpp, --max-context 0.
Run this check BEFORE building any analysis on the transcript; a looped transcript silently poisons keyword scans and any vision pass that pairs frames with audio.
Save final to <workdir>/transcript.txt.
Step 6 — If transcript mode: stop here
Output:
transcript.txtmetadata.json- One-line summary in chat: title, source, duration, word count
- Path to workdir
- (Optional) Step 9 — offer to capture to second-brain
Step 7 — If visual mode: extract frames + vision pass
Frame extraction (ffmpeg)
Cadence by source heuristic:
| Source type | Frame cadence |
|---|---|
| Screen-share / Loom / demo | 1 frame per 5s (UI changes fast) |
| Talking head / podcast | 1 frame per 30s (slow change) |
| Slide presentation | 1 frame per 10s + force a frame on each detected scene change |
| Default if unsure | 1 frame per 15s |
mkdir -p "<workdir>/frames"
ffmpeg -i "<workdir>/video.mp4" -vf "fps=1/15" "<workdir>/frames/frame-%04d.png" -y
For scene-change detection (slide decks especially):
ffmpeg -i "<workdir>/video.mp4" -vf "select='gt(scene,0.3)',showinfo" -fps_mode vfr "<workdir>/frames/scene-%04d.png" 2> "<workdir>/scene-detection.log"
Dense second pass on hot windows (bugs / changes being discussed)
For working sessions, bug reviews, and product walkthroughs, a single uniform cadence misses the detail that matters. Do a two-pass analysis:
- First pass at the normal cadence identifies findings (bugs hit on screen, improvements discussed) with timestamps.
- Build hot windows from (a) the first pass's finding timestamps and (b) a transcript keyword scan (
bug|broken|doesn't work|error|glitch|stuck|failing|should change|fix|confusing|janky), pad each ±20s, and merge overlaps. Keep windows tight — over-padding merges the whole video back into one window. - Re-extract those windows at 1 frame per 3s and run a second, detail-focused vision pass over them, pairing with the transcript:
mkdir -p "<workdir>/dense"
# per hot window [A, B] seconds:
ffmpeg -ss <A> -to <B> -i "<workdir>/video.mp4" -vf "fps=1/3,scale=1280:-2" "<workdir>/dense/w<NN>-%03d.png" -y
The dense pass is where exact repro sequences, on-screen error text, and short-lived UI states (a modal that flashes, a wrong redirect, a state mismatch) get captured — several real findings only exist at 3s cadence. Long videos: parallelize both passes with subagents, each taking a frame chunk + its transcript slice, then dedupe findings across passes at synthesis.
Vision pass
Pair each frame with the transcript chunk for the same timestamp window. Then batch-send to Claude vision for synthesis.
Per-frame batch prompt (up to ~10 frames per call):
Here are N frames from a video at timestamps T1..TN. For each frame, describe what's on screen in 1–2 sentences. Flag: (a) UI changes from previous frame, (b) text visible on screen, (c) any moment that looks like a decision, action, or notable event. Also note the transcript text spoken during this window.
Save the output as <workdir>/moments.md:
# Key moments — <title>
## 00:00:15 (frame-001.png)
**On screen**: Login form, email field focused
**Transcript**: "So you just open it up and..."
**Note**: Beginning of UI demo
## 00:00:45 (frame-002.png)
**On screen**: Dashboard with 4 cards
**Transcript**: "And here's where you see all your projects."
**Note**: Major view change — first time the dashboard appears
Generate summary
After moments are identified, synthesize the whole video into <workdir>/summary.md:
# Summary — <title>
**Source:** <source URL / file>
**Duration:** <hh:mm:ss>
**Watched at:** <date>
**Mode:** visual
## TL;DR
<2–4 sentences>
## Key moments
- 00:00:15 — <one-line>
- 00:00:45 — <one-line>
## Action items flagged
- <item> [timestamp]
## Decisions flagged
- <decision> [timestamp] — consider routing to /decide
## Quotes worth keeping
- "..." [timestamp]
## Open questions
- <question raised but not answered>
Step 8 — If multimodal mode
Backend selection
Gemini native if
$GEMINI_API_KEYis set (much cheaper + faster than per-frame for long videos):Default model:
gemini-3.5-flash(released May 2026, ~USD 1.50 input / USD 9 output per 1M tokens; ~USD 0.15/sec of video; beats 3.1 Pro on coding/agentic benchmarks at 4× the speed). Override togemini-3.1-profor brand audits / high-stakes analysis where details matter;gemini-2.5-flash-litefor bulk cheap processing.# Step 1: Upload video via Files API FILE_URI=$(curl -s -X POST "https://generativelanguage.googleapis.com/upload/v1beta/files?key=$GEMINI_API_KEY" \ -H "X-Goog-Upload-Command: start, upload, finalize" \ -H "Content-Type: video/mp4" \ --data-binary "@<workdir>/video.mp4" | jq -r '.file.uri') # Wait until file is ACTIVE (Gemini processes the video first) while true; do STATE=$(curl -s "$FILE_URI?key=$GEMINI_API_KEY" | jq -r '.state') [ "$STATE" = "ACTIVE" ] && break sleep 3 done # Step 2: Generate content with the file + multimodal-analysis prompt curl -s -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=$GEMINI_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"contents\":[{ \"parts\":[ {\"file_data\":{\"mime_type\":\"video/mp4\",\"file_uri\":\"$FILE_URI\"}}, {\"text\":\"<multimodal analysis prompt — see Step 7's summary template + use-case extensions>\"} ] }] }"Files persist in Gemini Files API for ~48 hours — useful for re-querying the same video with different prompts.
Dense Claude vision fallback if no Gemini key:
- Frame cadence: 1 frame per 3s (much denser than visual mode)
- Batch through Claude vision with the multimodal-analysis prompt
- Slower and more expensive than Gemini for long videos — warn the user before running on >10min content
Multimodal output
Same summary.md template as Step 7 + an extended section:
## Multimodal observations
- **Body language / delivery**: <observations on talking-head video>
- **Pacing**: <fast/slow/uneven>
- **Visual style**: <brand audit, ad review, design observations>
- **Audio quality / atmosphere**: <music, silence, background>
Exact extra sections depend on the use case (brand audit, ad review, talk delivery review, client-call read). Use case is inferred from the source + the user's verbal framing when invoking.
Step 9 — Optional: capture to second-brain
After any mode completes, offer:
"Want to capture this to second-brain? I'll write a
call-<slug>.md(ormeeting-/note-/resource-) to${SECOND_BRAIN_VAULT:-$HOME/Documents/SecondBrain}/raw/with the summary, source URL, and transcript link."
Type prefix by source:
| Source | Prefix |
|---|---|
| Loom / Zoom / Riverside / Otter / call recording | call- |
| Meeting (own notes, not a transcript) | meeting- |
| Talk / keynote / conference | note- |
| Ad / landing-page video / marketing reference / competitor video | resource- |
File body: 1-line source, the summary, link to full workdir.
Step 10 — Report
In chat:
- One-line headline:
<source> · <title> · <duration> · <mode> · <word count> words - Workdir path
- For
visual/multimodal: brief list of top 3 key moments - For all modes: any action items / decisions flagged for triage
- If captured to second-brain: that path too
Sources reference
| Source | Download | Built-in transcript | Notes |
|---|---|---|---|
| YouTube | yt-dlp |
Auto-subs (--write-auto-sub) |
Same as the prior youtube-transcript skill |
| Loom | yt-dlp (Loom supported) |
Yes — fetch via embed metadata or Loom API | Async screenshare focus — prime use case |
| Vimeo | yt-dlp |
Sometimes | Marketing/embed videos |
| Riverside | Direct URL from export, or local file | Yes — Riverside generates them | Podcast episodes |
| Zoom | Local .mp4 (downloaded recordings) |
Sometimes (Zoom audio transcript file) | Client calls |
| X / IG / TikTok | Defer to social-fetch for metadata, yt-dlp for file |
No | Short-form |
| Local file | n/a | n/a | Drop a path |
Composes with
social-fetch— for X/IG/TikTok URL metadata (engagement, author, replies) before video processingsecond-brain— capture summary asraw/call-<slug>.md,meeting-,note-, orresource-per source typedecide— when a video contains a flagged decision, route to/decidefor structured captureproject-manager— action items flagged in summary can be triaged to project boardsslide-deck— talk recordings → outline extraction → deck draft (loop)jab-hook— quotes + clip-worthy moments from podcast/talk videos feed BIP/promo postsskillify from-video— primary use case forvisualmode on process recordings. the user records themselves doing a workflow (Loom/screen-share), this skill extracts transcript + key visual moments, thenskillifysynthesizes the workflow into a SKILL.md. "Record once, AI converts to skill."
Error handling
| Failure | Response |
|---|---|
| Video unavailable / private / region-locked | Report and stop |
| No subtitles + Whisper not installed | Tell the user: pip install mlx-whisper (Apple Silicon) or pip install faster-whisper (anything else) |
| ffmpeg missing (for visual/multimodal) | Tell the user: brew install ffmpeg |
| Vision pass returns empty / unclear | Lower the frame count, retry, or fall back to transcript-only with a note |
Multimodal requested but no $GEMINI_API_KEY and >30min video |
Warn cost, offer to fall back to visual mode |
yt-dlp binary missing |
brew install yt-dlp |
Notes on quality
- Never put a dollar sign directly before a digit in this file. Agent hosts replace those tokens with the invocation arguments when they load a skill: an awk field reference turns into the URL and a price like USD 1.50 written with a dollar sign turns into a word from the command line. Use a Python snippet, and spell money as
USD 1.50. - User picks depth, not the skill. Transcript / visual / multimodal are 3 different cost + latency profiles. Long videos (>10 min) always confirm before spending on visual/multimodal.
- Platform transcript first, Whisper second. YouTube auto-subs, Loom transcripts, Riverside built-in transcripts — all free + instant when they exist. Fall back to local Whisper only when nothing platform-provided works.
- Always verify Whisper output for repetition loops. A hallucination loop can silently replace 90%+ of a long recording with one repeated caption while the run exits 0. Scan for runs of identical captions (Step 5); re-run with conditioning on previous text turned off if found.
- Densify around bugs and change discussions. When the video is a working session or bug review, run the two-pass flow (Step 7): normal cadence to find the moments, then 1 frame/3s over ±20s hot windows around every bug/change timestamp + transcript keyword hit. Uniform cadence alone misses flash-frame UI states and exact repro sequences.
- Local Whisper is the fast path. MLX on M-series Macs transcribes faster than real-time; faster-whisper is the equivalent elsewhere. Cloud Whisper is a distant second choice — costs money, network dependency, worse latency on typical durations.
- Frame cadence by source type. Screen-share / demos need 1 frame per 5s (UI changes fast); talking-head podcasts need 1 per 30s (slow change). Default 15s if unsure. Wrong cadence = missed key moments OR wasted vision-pass cost.
- 720p is plenty. Downloading 1080p / 4K for transcription + frame analysis wastes bandwidth + storage.
yt-dlp -f "bv*[height<=720]+ba/b[height<=720]"is the default. - Scene-change detection catches slide transitions. When the video is a slide presentation, add
ffmpeg -vf "select='gt(scene,0.3)'"to force a frame on each detected slide change — more reliable than pure time-based sampling. - Multimodal cost warning is non-optional. Gemini multimodal on a 60-min video is meaningfully expensive. Warn before running; offer transcript-only as fallback if the user isn't sure.
- Summary format includes routing hints.
## Decisions flagged+## Action items flaggedsections signal/decideand/project-managerfollow-ups. Downstream composability lives in the summary structure.
Tips for Watch Video
- • Start a fresh session after installing so your agent picks up the new skill.
- • It is part of a pack with 25 skills. Install only the ones you need with --skill to keep triggers precise.
- • If two skills overlap, mention the one you want by name.
Questions about Watch Video
What does the Watch Video skill do?
When you want to extract content from a video — YouTube, Loom, Vimeo, Riverside, Zoom recording, local MP4, X/IG video, anything yt-dlp supports. Three depth modes user picks per invocation — transcript (just words, fast/free), visual (transcript + ffmpeg frame extraction + Claude vision pass on key moments), multimodal (Gemini native video ingestion if $GEMINI_API_KEY set, else dense Claude vision). Uses local Whisp
How do I install Watch Video?
Run "npx skills add coreyhaines31/makerskills --skill watch-video" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.
Does Watch Video work in Cursor and Codex?
Yes. Watch Video uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.
Is Watch Video free?
Watch Video is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.
Who made Watch Video?
Watch Video is published by Corey Haines in the coreyhaines31/makerskills repository, together with 24 other skills.




