/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> or loom.com/embed/<id>
- Vimeo:
vimeo.com/<id>
- Riverside: download URL or local file
- Zoom: local
.mp4 from a downloaded recording
- X / IG / TikTok video: URL — defers to
social-fetch for 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 $GEMINIAPIKEY), 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 --print "%(title)s|%(uploader)s|%(duration_string)s|%(upload_date>%Y-%m-%d)s|%(description)s" \
--print "%(chapters)j" --skip-download "<url>"
Capture: title, uploader/channel, duration, upload date, description (first paragraph), chapters (JSON or null).
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 / local
slug: 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-lang en --sub-format vtt - Loom: fetch via https://www.loom.com/share/<id> page metadata or Loom API if $LOOMAPIKEY set - Riverside: built-in transcripts available on the recording's share page - If platform transcript exists and has timestamps, use it. Skip Whisper.
- MLX-Whisper local (default fallback — fast on Mac M-series):
``bash # Install once: pip install mlx-whisper python3 -c "import mlxwhisper; mlxwhisper.transcribe('<file>', pathorhfrepo='mlx-community/whisper-large-v3-turbo')" \ > "<workdir>/transcript-raw.json" ` Or via the CLI: mlxwhisper <file> --model mlx-community/whisper-large-v3-turbo --output-dir <workdir>`
- whisper.cpp (further fallback if MLX unavailable)
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]" -o "<workdir>/video.%(ext)s" "<url>"
720p is plenty for transcription and frame analysis (smaller download, faster processing).
Clean the transcript (only needed for YouTube auto-subs which have rolling captions; Whisper output is already clean):
# YouTube VTT cleanup — de-dup rolling captions, strip tags, paragraph-break on cue gaps >2s
awk '
/^WEBVTT/ || /^Kind:/ || /^Language:/ || /^NOTE/ { next }
/-->/ { in_cue = 1; last = ""; next }
/^$/ { if (last) print last; in_cue = 0; last = ""; next }
in_cue { gsub(/<[^>]+>/, "", $0); last = $0 }
END { if (last) print last }
' "<workdir>/transcript.en.vtt" | awk '!seen[$0]++' > "<workdir>/transcript.txt"
Save final to <workdir>/transcript.txt.
Step 6 — If transcript mode: stop here
Output:
transcript.txt
metadata.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" -vsync vfr "<workdir>/frames/scene-%04d.png" 2> "<workdir>/scene-detection.log"
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
$GEMINIAPIKEY is set (much cheaper + faster than per-frame for long videos):
Default model: gemini-3.5-flash (released May 2026, ~$1.50 input / $9 output per 1M tokens; ~$0.15/sec of video; beats 3.1 Pro on coding/agentic benchmarks at 4× the speed). Override to gemini-3.1-pro for brand audits / high-stakes analysis where details matter; gemini-2.5-flash-lite for bulk cheap processing.
```bash # Step 1: Upload video via Files API FILEURI=$(curl -s -X POST "https://generativelanguage.googleapis.com/upload/v1beta/files?key=$GEMINIAPI_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 "$FILEURI?key=$GEMINIAPI_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=$GEMINIAPIKEY" \ -H "Content-Type: application/json" \ -d "{ \"contents\":[{ \"parts\":[ {\"filedata\":{\"mimetype\":\"video/mp4\",\"fileuri\":\"$FILEURI\"}}, {\"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 (or meeting- / note- / resource-) to ${SECONDBRAINVAULT:-$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 processing
second-brain — capture summary as raw/call-<slug>.md, meeting-, note-, or resource- per source type
decide — when a video contains a flagged decision, route to /decide for structured capture
pm — action items flagged in summary can be triaged to project boards
slide-deck — talk recordings → outline extraction → deck draft (loop)
jab-hook — quotes + clip-worthy moments from podcast/talk videos feed BIP/promo posts
skillify from-video — primary use case for visual mode on process recordings. the user records themselves doing a workflow (Loom/screen-share), this skill extracts transcript + key visual moments, then skillify synthesizes 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 (Mac) |
| 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 $GEMINIAPIKEY and >30min video |
Warn cost, offer to fall back to visual mode |
yt-dlp binary missing |
brew install yt-dlp |
Notes on quality
- 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 MLX-Whisper local only when nothing platform-provided works.
- MLX-Whisper local is the fast path on Mac. M-series machines transcribe faster than real-time. 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 flagged sections signal /decide and /pm follow-ups. Downstream composability lives in the summary structure.