755c157466
- watch.py: drop the ⚠️ emoji from the long-video warning; cp1252 consoles on Windows couldn't encode it and the script crashed. - setup.py: add a Windows branch that prints winget / pip install commands, matching what the README already promised. - SKILL.md: note that Windows users must invoke the scripts with `python`, not `python3` (which is the Microsoft Store stub). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
174 lines
12 KiB
Markdown
174 lines
12 KiB
Markdown
---
|
|
name: watch
|
|
description: Watch a video (URL or local path). Downloads with yt-dlp, extracts auto-scaled frames with ffmpeg, pulls the transcript from captions (or Whisper API fallback), and hands the result to Claude so it can answer questions about what's in the video.
|
|
argument-hint: "<video-url-or-path> [question]"
|
|
allowed-tools: Bash, Read, AskUserQuestion
|
|
homepage: https://github.com/bradautomates/claude-video
|
|
repository: https://github.com/bradautomates/claude-video
|
|
author: bradautomates
|
|
license: MIT
|
|
user-invocable: true
|
|
---
|
|
|
|
# /watch — Claude watches a video
|
|
|
|
You don't have a video input; this skill gives you one. A Python script downloads the video, extracts frames as JPEGs, gets a timestamped transcript (native captions first, then Whisper API as fallback), and prints frame paths. You then `Read` each frame path to see the images and combine them with the transcript to answer the user.
|
|
|
|
## Step 0 — Setup preflight (runs every `/watch` invocation, silent on success)
|
|
|
|
**Python interpreter:** every `python3 ...` command in this skill is for macOS/Linux. On **Windows**, substitute `python` — the `python3` command on Windows is the Microsoft Store stub and will not run the script.
|
|
|
|
Before every `/watch` run, verify that dependencies and an API key are in place:
|
|
|
|
```bash
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/setup.py" --check
|
|
```
|
|
|
|
This is a <100ms lookup. On exit 0, the script emits **nothing** — proceed to Step 1 without comment. **Do NOT announce "setup is complete" to the user** — they don't need a status message on every turn. The only acceptable user-visible output from Step 0 is when remediation is required.
|
|
|
|
On non-zero exit, follow the table:
|
|
|
|
| Exit | Meaning | Action |
|
|
|------|---------|--------|
|
|
| `2` | Missing binaries (`ffmpeg` / `ffprobe` / `yt-dlp`) | Run installer |
|
|
| `3` | No Whisper API key | Run installer to scaffold `.env`, then ask user for a key |
|
|
| `4` | Both missing | Run installer, then ask for a key |
|
|
|
|
The installer is idempotent — safe to re-run:
|
|
|
|
```bash
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/setup.py"
|
|
```
|
|
|
|
On macOS with Homebrew, it auto-installs `ffmpeg` and `yt-dlp`. On Linux/Windows, it prints the exact install commands for the user to run. It scaffolds `~/.config/watch/.env` with commented placeholders at `0600` perms, and writes `SETUP_COMPLETE=true` once deps + a key are in place so the next session knows this user has already been through the wizard.
|
|
|
|
**If an API key is still missing after install:** use `AskUserQuestion` to ask the user whether they have a Groq API key (preferred — cheaper, faster) or an OpenAI key. Then write it into `~/.config/watch/.env` — set the matching `GROQ_API_KEY=...` or `OPENAI_API_KEY=...` line. If they don't want to set up Whisper, proceed with `--no-whisper` and tell them videos without native captions will come back frames-only.
|
|
|
|
**Structured mode (optional):** `python3 "${CLAUDE_SKILL_DIR}/scripts/setup.py" --json` emits `{status, first_run, missing_binaries, whisper_backend, has_api_key, config_file, platform}` where `status` is one of `ready | needs_install | needs_key | needs_install_and_key`. Use this when you need to branch on specifics (e.g. "is this the user's very first run?" → `first_run: true`).
|
|
|
|
Within a single session, you can skip Step 0 on follow-up `/watch` calls — once `--check` returned 0, nothing about the environment changes between turns.
|
|
|
|
## When to use
|
|
|
|
- User pastes a video URL (YouTube, Vimeo, X, TikTok, Twitch clip, most yt-dlp-supported sites) and asks about it.
|
|
- User points at a local video file (`.mp4`, `.mov`, `.mkv`, `.webm`, etc.) and asks about it.
|
|
- User types `/watch <url-or-path> [question]`.
|
|
|
|
## Recommended limits
|
|
|
|
- **Best accuracy: videos under 10 minutes.** Frame coverage scales inversely with duration.
|
|
- **Hard caps: 100 frames total and 2 fps.** Token cost grows with frame count, so the script targets a frame budget by duration (and never exceeds 2 fps even when the budget would imply more):
|
|
- ≤30s → ~1-2 fps (up to 30 frames)
|
|
- 30s-1min → ~40 frames
|
|
- 1-3min → ~60 frames
|
|
- 3-10min → ~80 frames
|
|
- \>10min → 100 frames, sparsely spaced (warning printed)
|
|
- If the user hands you a long video, consider asking whether they want a specific section before burning tokens on a sparse scan.
|
|
|
|
## How to invoke
|
|
|
|
**Step 1 — parse the user input.** Separate the video source (URL or path) from any question the user asked. Example: `/watch https://youtu.be/abc what language is this in?` → source = `https://youtu.be/abc`, question = `what language is this in?`.
|
|
|
|
**Step 2 — run the watch script.** Pass the source verbatim. Do not shell-escape it yourself beyond normal quoting:
|
|
|
|
```bash
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/watch.py" "<source>"
|
|
```
|
|
|
|
Optional flags:
|
|
- `--start T` / `--end T` — focus on a section. Accepts `SS`, `MM:SS`, or `HH:MM:SS`. When either is set, fps auto-scales denser (see "Focusing on a section" below).
|
|
- `--max-frames N` — lower the cap for tighter token budget (e.g. `--max-frames 40`)
|
|
- `--resolution W` — change frame width in px (default 512; bump to 1024 only if the user needs to read on-screen text)
|
|
- `--fps F` — override auto-fps (clamped to 2 fps max)
|
|
- `--out-dir DIR` — keep working files somewhere specific (default: an auto-generated tmp dir)
|
|
- `--whisper groq|openai` — force a specific Whisper backend (default: prefer Groq if both keys exist)
|
|
- `--no-whisper` — disable the Whisper fallback entirely (frames-only if no captions)
|
|
|
|
### Focusing on a section (higher frame rate)
|
|
|
|
When the user asks about a specific moment — "what happens at the 2 minute mark?", "zoom into 0:45 to 1:00", "the first 10 seconds" — pass `--start` and/or `--end`. The script switches to focused-mode budgets, which are denser than full-video budgets (still capped at 2 fps):
|
|
|
|
- ≤5s → 2 fps (up to 10 frames)
|
|
- 5-15s → 2 fps (up to 30 frames)
|
|
- 15-30s → ~2 fps (up to 60 frames)
|
|
- 30-60s → ~1.3 fps (up to 80 frames)
|
|
- 60-180s → ~0.6 fps (100 frames, capped)
|
|
|
|
Focused mode is the right call for:
|
|
- Any moment/range the user names explicitly ("around 2:30", "the intro", "the last 30 seconds").
|
|
- Any video longer than ~10 minutes where the user's question is about a specific part — running focused on the relevant section is far more useful than a sparse scan of the whole thing.
|
|
- Re-runs after a full scan didn't have enough detail in some region.
|
|
|
|
Transcript is auto-filtered to the same range. Frame timestamps are absolute (real video timeline, not offset-from-start).
|
|
|
|
Examples:
|
|
```bash
|
|
# Last 10 seconds of a 1 minute video
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/watch.py" video.mp4 --start 50 --end 60
|
|
|
|
# Zoom into 2:15 → 2:45 at 3 fps (90 frames)
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/watch.py" "$URL" --start 2:15 --end 2:45 --fps 3
|
|
|
|
# From 1h12m to the end of the video
|
|
python3 "${CLAUDE_SKILL_DIR}/scripts/watch.py" "$URL" --start 1:12:00
|
|
```
|
|
|
|
**Step 3 — Read every frame path the script lists.** The Read tool renders JPEGs directly as images for you. Read all frames in a single message (parallel tool calls) so you see them together. The frames are in chronological order with a `t=MM:SS` timestamp so you can align them to the transcript.
|
|
|
|
**Step 4 — answer the user.** You now have two streams of evidence:
|
|
- **Frames** — what's on screen at each timestamp
|
|
- **Transcript** — what's said at each timestamp. The report's header shows the source (`captions` = yt-dlp pulled native subs; `whisper (groq)` or `whisper (openai)` = transcribed by API).
|
|
|
|
If the user asked a specific question, answer it directly citing timestamps. If they didn't ask anything, summarize what happens in the video — structure, key moments, notable visuals, spoken content.
|
|
|
|
**Step 5 — clean up.** The script prints a working directory at the end. If the user isn't going to ask follow-ups about this video, delete it with `rm -rf <dir>`. If they might, leave it in place.
|
|
|
|
## Transcription
|
|
|
|
The script gets a timestamped transcript in one of two ways:
|
|
|
|
1. **Native captions (free, preferred).** yt-dlp pulls manual or auto-generated subtitles from the source platform if available.
|
|
2. **Whisper API fallback.** If no captions came back (or the source is a local file), the script extracts audio (`ffmpeg -vn -ac 1 -ar 16000 -b:a 64k`, ~0.5 MB/min) and uploads it to whichever Whisper API has a key configured:
|
|
- **Groq** — `whisper-large-v3`. Preferred default: cheaper, faster. Get a key at console.groq.com/keys.
|
|
- **OpenAI** — `whisper-1`. Fallback. Get a key at platform.openai.com/api-keys.
|
|
|
|
Both keys live in `~/.config/watch/.env`. The script prefers Groq when both are set; override with `--whisper openai` to force OpenAI. Use `--no-whisper` to skip the fallback entirely.
|
|
|
|
## Failure modes and handling
|
|
|
|
- **Setup preflight failed** → run `python3 "${CLAUDE_SKILL_DIR}/scripts/setup.py"` (auto-installs ffmpeg/yt-dlp via brew on macOS, scaffolds the `.env`). For API key, ask the user via `AskUserQuestion` and write it to `~/.config/watch/.env`.
|
|
- **No transcript available** → captions missing AND (no Whisper key OR Whisper API failed). Script prints a hint pointing to setup. Proceed frames-only and tell the user.
|
|
- **Long video warning printed** → acknowledge it in your answer. Offer to re-run focused on a specific section via `--start`/`--end` rather than a sparse full-video scan.
|
|
- **Download fails** → yt-dlp's error goes to stderr. If it's a login-required or region-locked video, tell the user plainly; do not keep retrying.
|
|
- **Whisper request fails** → the error is printed to stderr (likely: invalid key, rate limit, or 25 MB upload limit on a very long video). The report will say "none available" for transcript. You can retry with `--whisper openai` if Groq failed (or vice versa).
|
|
|
|
## Token efficiency
|
|
|
|
This skill burns tokens primarily on frames. Order of magnitude:
|
|
- 80 frames at 512px wide is roughly 50-80k image tokens depending on aspect ratio.
|
|
- The transcript is cheap (a few thousand tokens at most for a 10-minute video).
|
|
- Bumping `--resolution` to 1024 roughly quadruples the image tokens per frame. Only do it when necessary.
|
|
|
|
If you already watched a video this session and the user asks a follow-up, do **not** re-run the script — you already have the frames and transcript in context. Just answer from what you have.
|
|
|
|
## Security & Permissions
|
|
|
|
**What this skill does:**
|
|
- Runs `yt-dlp` locally to download the video and pull native captions when the source supports them (public data; the request goes directly to whatever host the URL points at)
|
|
- Runs `ffmpeg` / `ffprobe` locally to extract frames as JPEGs and, when Whisper is needed, a mono 16 kHz audio clip
|
|
- Sends the extracted audio clip to Groq's Whisper API (`api.groq.com/openai/v1/audio/transcriptions`) when `GROQ_API_KEY` is set (preferred — cheaper, faster)
|
|
- Sends the extracted audio clip to OpenAI's audio transcription API (`api.openai.com/v1/audio/transcriptions`) when `OPENAI_API_KEY` is set and Groq is not, or when `--whisper openai` is forced
|
|
- Writes the downloaded video, frames, audio, and an intermediate transcript to a working directory under the system temp dir (or `--out-dir` if specified) so Claude can `Read` them
|
|
- Reads / creates `~/.config/watch/.env` (mode `0600`) to store the Whisper API key(s) and a `SETUP_COMPLETE` marker. As a fallback, also reads `.env` in the current working directory
|
|
|
|
**What this skill does NOT do:**
|
|
- Does not upload the video itself to any API — only the extracted audio goes out, and only when native captions are missing AND Whisper is not disabled with `--no-whisper`
|
|
- Does not access any platform account (no login, no session cookies, no posting)
|
|
- Does not share API keys between providers (Groq key only goes to `api.groq.com`, OpenAI key only goes to `api.openai.com`)
|
|
- Does not log, cache, or write API keys to stdout, stderr, or output files
|
|
- Does not persist anything outside the working directory and `~/.config/watch/.env` — clean up the working directory when you're done (Step 5)
|
|
|
|
**Bundled scripts:** `scripts/watch.py` (entry point), `scripts/download.py` (yt-dlp wrapper), `scripts/frames.py` (ffmpeg frame extraction), `scripts/transcribe.py` (caption selection + Whisper orchestration), `scripts/whisper.py` (Groq / OpenAI clients), `scripts/setup.py` (preflight + installer)
|
|
|
|
Review scripts before first use to verify behavior.
|