video → frames → LLM.
Turn a video, an animated GIF / APNG / WebP, or a live HLS / DASH / RTSP stream into a timeline of still frames plus the audio track and its transcript, so any LLM can watch + listen. Optional OCR, object gating, embeddings, PII blur, speaker diarisation — all opt-in.
Who it's for
LLMs can read images. Your footage is a sequence.
Whatever's in your video — a bug, a break-in, a lecture, an exploit — peepshow turns it into still frames an LLM already knows how to reason about.
A QA engineer screen-records a flicker. A designer sends a 12-second Loom. A user uploads a .mov with the frame that breaks everything. peepshow turns that clip into scene-aware stills so the model sees the bug frame-by-frame.
# drop a repro into Claude — the hook does the rest
peepshow ./bug-repro.mov --strategy scene --max 12- 12 stills across the scene changes
- JSON metadata the LLM can cite — timestamps, scores, tags
- Piped through /peepshow:slides automatically
An hour of overnight camera footage is hours of nothing followed by twelve seconds that matter. peepshow's scene detector flags the motion, a perceptual-hash dedup drops the near-identical static frames, the LLM narrates what changed, and you get a readable incident timeline — not a scrub bar.
# 1 hour of CCTV → a timeline of motion events
peepshow ./cctv-2026-04-23.mp4 --strategy scene --threshold 0.18 --max 60 \
--sink sqlite:./incidents.db --tag camera=front-door- Motion-only keyframes — skip the silent hours
- Searchable SQLite archive tagged by camera / date
- Ask Claude "when did the motion start?" and get a frame back
Lecture captures, fieldwork timelapses, documentary clips, microscopy. The LLM can read a slide, a phase change, a titration colour shift — it just needs a well-chosen frame. peepshow picks frames that change; you get notes the LLM can actually reason about.
# 50-minute lecture -> one still per slide change
peepshow ./lecture-04.mp4 --strategy scene --threshold 0.25 \
--emit markdown --sink obsidian:~/vault/Lectures- Slide-by-slide capture — no manual scrubbing
- Obsidian notes with embedded frames + caption block
- Ask an LLM "summarise slide 7's equation"
CVE repro videos, exploit PoCs, red-team recordings — evidence is only useful if reviewable. peepshow extracts the exact frames where state changes so an analyst, a report, or an LLM can cite them directly. No re-watching at 1x.
# PoC screencast -> pinned evidence + frame-accurate captions
peepshow ./cve-2026-1138-repro.mp4 --strategy scene --max 20 \
--emit xml --sink github-issues:owner/repo --tag severity=high- Frame-exact evidence attached to the issue
- XML emit for threat-intel pipelines
- Reuse across the remediation conversation — nothing re-uploaded
Video content is a wall to users on screen readers. peepshow converts the visual track into per-scene stills so an LLM can describe each moment — alt text that reflects the whole story, not a single thumbnail.
# feed a screen reader / captioning pipeline
peepshow ./training-video.mp4 --strategy scene --emit json \
--sink webhook:https://a11y.example/describe- Scene-by-scene alt text via any LLM
- Webhook fan-out to the captioning service of choice
- Deterministic frames — same video, same stills, same descriptions
Building a desktop AI client (Electron, Tauri, native)? Drop-target a video onto a window, spawn peepshow in the main process, parse the JSON, forward only the distilled context to your bundled LLM. Frames + transcripts stay on the user's disk; just the synthesis crosses the wire.
// main process, Electron
const { spawn } = require("node:child_process");
const ps = spawn("peepshow", [filePath, "--emit", "json"]);
let json = ""; ps.stdout.on("data", b => json += b);
ps.on("close", () => forwardToLLM(JSON.parse(json)));- Local-first context — only the JSON manifest leaves the box
- Token + bandwidth saving vs uploading raw video
- Optional
peepshow servedashboard for the run history
Server-side ingest endpoint — users upload videos, you fan out the JSON manifest to multiple LLMs (Claude, GPT, Gemini, local) for parallel scoring or voting. One extraction, many models. Cuts upload bandwidth + per-token cost vs sending full video to each provider.
# headless service mode — no local index, no report.html
cat upload.mp4 | peepshow - --emit json --no-index --no-report \
--sink webhook:https://llm-router.internal/score- One extract, many models — broadcast manifest via webhook fan-out
- Headless service mode with
--no-index --no-report - Sink interaction log at
~/.peepshow/sink-log.ndjsonfor replay
one slideshow · any LLM · any video
Why peepshow?
Drag & drop — same UX as images
Drop an .mp4, .mov, or .gif into the Claude prompt. A UserPromptSubmit hook detects the path and auto-invokes /peepshow:slides. No slash command required.
Scene-change detection
Defaults to ffmpeg's select='gt(scene,0.3)' filter — catches visually distinct moments, not fixed-fps noise. Falls back to interval sampling for short clips.
Audio extracted too
Second ffmpeg pass writes a compact mono 16 kHz audio.m4a next to the frames, plus loudness peak + silence ratio. GIF / APNG / animated WebP skip cleanly. Opt out with --no-audio.
Local transcription
If whisper.cpp is on PATH, peepshow auto-transcribes the audio — nothing leaves your machine. Swap to openai / groq / deepgram / assemblyai in one flag. Transcript flows to every sink. Setup guide →
95 built-in sinks
Every run can fan out to databases, object stores, vector DBs, issue trackers, chat, and wiki systems. All tested, all shipped on npm. Full list →
Conditional routing
Sinks fire only when the input matches — --when director=Kubrick, --when path=/Volumes/Work/, --when extension=mp4,mov. Turns peepshow into a smart router.
Hardware-accelerated
VideoToolbox on macOS, VAAPI on Linux, D3D11VA on Windows — auto-detected. Prefers native ffmpeg over the bundled static build for extra speed.
Live statusline badge
[PEEPSHOW:decoding:42%] mid-run, [PEEPSHOW:5frm:scene:system] after, [PEEPSHOW|3s] with three auto-sinks armed.
Works with any LLM CLI
Ships native agent manifests for Claude · Cursor · Windsurf · Cline · Codex · Gemini. Integration snippets for aider · llm · Copilot · Continue · Cody · Zed in the docs.
Container metadata
Title, director, producer, show, genre, creation time — every tag inside the video's container flows into the JSON the LLM sees. Ground answers in what the video says it is, not just what's on screen.
The [PEEPSHOW] statusline badge
A one-line live indicator rendered in the Claude Code statusline — shows whether peepshow is idle, probing ffmpeg, decoding, or done. Also reports which sinks fired, how many frames were extracted, which strategy won.
Rich states out of the box: [PEEPSHOW:decoding:42%] mid-run, [PEEPSHOW:6frm:scene:system|3s] after (six frames, scene-detection won, used native ffmpeg, three auto-sinks armed), [PEEPSHOW:sink:slack:posted] when a sink succeeds. All readable at a glance without leaving the editor.
More things peepshow does that aren't obvious
Auto-sinks that survive sessions
peepshow sinks add obsidian once — every future run fires it. Config at ~/.peepshow/sinks.json. The statusline shows how many are armed: [PEEPSHOW|3s].
Conditional routing via --when
--when director=Kubrick, --when extension=mp4,mov, --when path=/Volumes/Work/. Sinks only fire when the input matches. ANDed within a sink, ORed across values.
Container metadata flows through
Title, director, producer, show, genre, creation time — every tag inside the video's container reaches both the LLM and each sink's payload. --when key=value can match any of them.
Four emit formats
--emit paths (default), --emit json (structured), --emit markdown (human-readable), --emit caveman (token-compressed via caveman). Pair with any agent pipeline.
ffmpeg selection heuristic
PEEPSHOW_FFMPEG env → native ffmpeg on PATH → bundled ffmpeg-static. System build wins because brew/choco/apt ship with VideoToolbox, NVENC, QSV, VAAPI, D3D11VA. Static build is the zero-config safety net.
Written-your-own sinks
A sink is any executable that reads the --emit json payload on stdin. Shell, Node, Python, Go, Rust — doesn't matter. Register persistent ones with peepshow sinks add-cmd 'your-cmd'.
Compressor auto-detect
Install caveman on PATH — peepshow picks it up and auto-compresses output unless you've explicitly set --emit. Opt out with PEEPSHOW_AUTO_COMPRESS=0.
GIF, APNG, animated WebP
Same pipeline, different container. Meme-length loops and multi-frame screenshots all flow through the same scene-detect → prune → sink pipeline.
Install
Two steps. Runtime from npm, plugin from GitHub.
1. Runtime from npm
npm i -g peepshow # adds peepshow + all peepshow-sink-* bins to PATH
npx peepshow ./video.mp4 # one-shot, no installOptional native ffmpeg for faster hardware decoding:
brew install ffmpeg # macOS
choco install ffmpeg-full # Windows
sudo apt install ffmpeg # Debian / UbuntuSkip entirely — peepshow bundles ffmpeg-static as a fallback.
Optional whisper.cpp for local audio transcription (no cloud, no API keys). When it's on PATH, peepshow auto-enables transcription — no flag needed:
brew install whisper-cpp # macOS — Homebrew
scoop install whisper-cpp # Windows — Scoop
# Linux / other: prebuilt releases at github.com/ggml-org/whisper.cpp/releasesPrefer cloud? Skip the binary and pass --transcribe openai / groq / deepgram / assemblyai with the matching *_API_KEY env var. Explicit --no-transcribe turns it off entirely.
2. Plugin for Claude Code
claude plugin marketplace add t0mtaylor/peepshow
claude plugin install peepshow@peepshow-marketplaceRestart claude — the /peepshow:slides skill is live, and the drag-and-drop hook fires on every prompt.
Use anywhere
peepshow ./bug.mov # paths + human-readable stats
peepshow ./demo.mp4 --emit json | jq # structured (includes audio + transcript)
peepshow ./loop.gif --emit caveman # token-compressed
peepshow ./keynote.mp4 --sink folder:/shared # fan out
peepshow ./talk.mp4 --transcribe openai # cloud transcribe
peepshow ./clip.mp4 --no-audio --no-transcribe # frames only95 built-in sinks
Every extract can fan out to any number of destinations. A sink reads the JSON payload on stdin, does anything. Conditional matchers (--when) mean each sink only fires when the input fits.
Find the right sink
Pick what you want to do, then where your stack lives. Popular picks re-rank as you choose.
Popular sinks
Browse by category
Auto-sinks — configure once, fire forever
peepshow sinks add folder:/Volumes/Shared/peepshow
peepshow sinks add postgres
peepshow sinks add-cmd 'node ~/scripts/obsidian-sync.js'
peepshow sinks list # see what's active
peepshow ./any-video.mp4 # all three fire
peepshow ./one-off.mp4 --no-auto-sinks # skip for this runConditional routing — --when
peepshow sinks add folder:/Volumes/Family --when extension=mp4,mov
peepshow sinks add postgres --when path=/Volumes/Work/
peepshow sinks add folder:/Cinema/Kubrick --when director=Kubrick --when genre=Thriller
peepshow sinks add-cmd 'node x.js' --when filename='*vacation*'HTML report per run
Every extract writes a self-contained report.html + manifest.json next to the frames. Frames grid · transcript · sink fan-out · LLM analysis. Append-only run history at ~/.peepshow/runs/index.ndjson. The agent that watched the video pipes its synthesis back so the next viewer sees the analysis without re-running the model.
In every report
- Frames grid w/ lightbox + keyboard nav (←/→/J/K/Esc)
- Summary line + full transcript text
- LLM analysis section w/ provider + model badges
- Sink fan-out w/ status filter (✅/❌/⏭)
- Stats grid + raw JSON tree (collapsible)
Opt-out flags
--no-report # skip HTML
--no-manifest # skip both
--no-index # skip ndjson
--report-dir <path> # override location
--report-open # open in browserInspect runs
peepshow runs list
peepshow runs show <id>
peepshow runs prune
peepshow report <dir>Closing the loop — LLM annotates the report
When peepshow runs inside an LLM workflow (Claude Code · Cursor · Windsurf · Cline · Codex · Gemini), the LLM consuming the frames pipes its understanding back. Every supported agent ships with the annotate instruction wired in.
echo '{"summary":"<2-4 sentences>","provider":"claude-code","model":"claude-opus-4-7"}' \
| peepshow report annotate "<outputDir>"Agent support
Native manifests for every major LLM CLI. Install peepshow once; every agent picks it up.
| Agent | Manifest | Install |
|---|---|---|
| Claude Code | .claude-plugin/ + skills/ + hooks/ | claude plugin marketplace add t0mtaylor/peepshow |
| Cursor | .cursor/rules/peepshow.mdc | picked up when peepshow is installed into the project |
| Windsurf | .windsurf/rules/peepshow.md | same |
| Cline | .clinerules/peepshow.md | same |
| Codex CLI | .codex/hooks.json + .codex/config.toml | SessionStart hook announces peepshow; invoke via Bash |
| Gemini CLI | gemini-extension.json + GEMINI.md | point Gemini at the repo as a custom command |
| Codex agents / Zed AI | AGENTS.md | convention-based pickup |
| Generic agents registry | .agents/plugins/marketplace.json | npx skills add t0mtaylor/peepshow (once supported) |
Deep-dive each agent integration →
Plus documented integration snippets for Copilot CLI · aider · llm · Continue · Cody · Zed AI · Perplexity · Ollama in docs/INTEGRATIONS.md.
FAQ
- Does it work without ffmpeg installed?
- Yes —
npm i peepshowpulls in ffmpeg-static as a fallback. Native ffmpeg (via brew / choco / apt) is preferred for hardware decoding but optional. - What happens when I drop a video into Claude Code?
- A
UserPromptSubmithook spots the video path, injects a reminder into Claude's context, and Claude auto-invokes/peepshow:slides <path>. Frames are extracted, read as images, and Claude answers — without you typing the slash command. - Are static images handled too?
- No — static JPG / PNG / WebP are already readable by every LLM. peepshow only runs for things with multiple frames across time: video and animated images.
- Does peepshow extract audio too, or just frames?
- Audio too, as of v0.4.0. When the input carries an audio stream (MP4 / MOV / WebM / MKV), a second ffmpeg pass emits a compact mono 16 kHz AAC
audio.m4anext to the frames and probes loudness peak + silence ratio. Animated GIF, APNG, and animated WebP skip cleanly — those formats can't carry audio. Opt out per-run with--no-audioor globally viaPEEPSHOW_AUDIO_ENCODER=off. - Is the spoken audio transcribed?
- If whisper.cpp is already on your
PATH, yes — transcription runs automatically with thebase.enmodel and the transcript lands in the JSON payload underaudio.transcript. No whisper.cpp binary? peepshow skips silently; frames + audio still emit. Prefer a cloud provider or your own setup? Switch via--transcribe openai|groq|deepgram|assemblyai|custom, or force off with--no-transcribe. - Where do API keys live for the cloud transcribers + sinks?
- On your machine, in your own environment variables. peepshow never forwards credentials to peepshow.dev, the author, or anywhere else — the CLI reads
OPENAI_API_KEY,DEEPGRAM_API_KEY,SLACK_WEBHOOK_URL, etc. locally and talks to those services directly from your terminal. The static site at peepshow.dev is pure documentation; there is no backend to phone home to. - What do all 95 sinks actually do?
- Each sink pipes a peepshow run into a specific downstream system. Browse by category on the sinks hub, or use the use-case finder — pick what you want to do (search / alert / archive / memory / whiteboard / analytics / compliance / LLM pipeline / review / workflow) and where your stack lives (cloud · self-hosted · LLM · local) and it ranks the matching sinks and hands you the CLI command.
- Where does the runtime code come from?
- npm. The public GitHub repo ships only manifests, hooks, and docs — no compiled code. That keeps the source trusted (versioned on npm with integrity hashes) and the GitHub surface clean.
- Can I write my own sink?
- Any executable that reads the
--emit jsonpayload on stdin is a valid sink — bash, Node, Python, Go, Rust, whatever. Register persistent ones viapeepshow sinks add-cmd 'your command'. See the sink spec. - What is
peepshow serve? - A local HTTP dashboard for the run history. Run
peepshow serveand visithttp://127.0.0.1:7331/— the/runspage lists every extract with thumbnails, filter chips (status / sinks / callers / tags / auto-tags), search, and a row/card view toggle. Each run links to a per-run report with the original video preview, frames + per-frame captions, and inline tag editor. Auto-tags (has-analysis,partial-captions,portrait,1080p, etc.) drive the filter chips for free. Loopback-bound by default; remote bind needs--token. Full reference:docs/SERVE.md. - Does peepshow phone home? How do I turn it off?
- By default, yes — an anonymous beacon (just version + OS family + outcome, no paths or payload) goes to Matomo + GA4 after each run, and
peepshow servepages load the same trackers consent-gated. Anonymous uuid lives in~/.peepshow/anon-id. Four ways to disable:peepshow config set telemetry off— persistentPEEPSHOW_TELEMETRY=0— per-invocation envDO_NOT_TRACK=1— honoured globally- "Reject" on the
peepshow serveconsent banner — disables page analytics
~/.peepshow/sink-log.ndjson) and the run history are local only — never beaconed. Full reference + every switch indocs/PRIVACY.md. - What can I do via the
peepshow runssubcommands? peepshow runs listprints recent runs;show <id>dumps a manifest;prune [--keep N]removes dead-outputDir entries (--keepcaps the index to N newest);repairemits a worklist of runs missing LLM analysis (and--applytakes annotations back via stdin);dedup [--all|--runId X] [--dry-run]re-runs the perceptual-hash pass on existing runs that were extracted with--no-dedup. Therepair+dedupflows pair with thepeepshow runsfirst-run nudge frompeepshow serve.- How does it pair with caveman mode?
peepshow ... --emit cavemanprints an ultra-terse one-line summary + paths, designed for token-compressed LLM setups like JuliusBrussee/caveman. Saves ~70% of peepshow's preamble tokens.- Where can I see what's in each release?
- peepshow.dev/releases mirrors
CHANGELOG.mdfrom the repo. User-facing changes only — new sinks, new features, opt-in behaviour — so it's easy to skim before upgrading. The site also shows the version + git short SHA in the footer and in<meta name="peepshow:version">on every page; when a new build lands while you have a tab open, a small banner above the cookie bar offers Reload. - Which LLMs does peepshow work with?
- All multimodal ones. Gemini reads video natively — peepshow caps the token cost and handles animated GIF / APNG / WebP. Claude (Opus 4.7 / Sonnet 4.6 / Haiku 4.5), GPT-4o / GPT-5, and Grok have image-only vision — peepshow is the bridge to video. Pixtral, Qwen2.5-VL, DeepSeek-VL2, and local models (Llama 3.2 Vision via Ollama / LM Studio / llama.cpp) all read the same frame bundle. Per-model guides with token math, install snippets, and frame-strategy presets: peepshow + every LLM →.
- How do I do X with peepshow? (GIF, YouTube, Loom, CCTV, Notion, Slack…)
- The how-to hub has copy-paste workflows for the common tasks: GIF → LLM, YouTube → LLM (yt-dlp + peepshow), Loom → LLM, transcribe locally with whisper.cpp, CCTV analysis, dashcam review, video → Notion, video → Slack, → Obsidian, → SQLite, screen-recording bug repros, plus APNG + animated WebP handling. Each page is a HowTo-schema'd step list with running commands.
- How does peepshow compare to native video on Gemini / hand-rolled ffmpeg?
- Honest side-by-side at peepshow vs. Four comparison pages: vs Gemini native video (token-cost ceiling, animated formats, audit trail), vs hand-rolled ffmpeg (yes you could write it yourself — peepshow already did, plus 71 sinks), vs OpenAI video (OpenAI has no native video — peepshow is the bridge), vs whisper.cpp standalone (whisper alone is fine if you only need a transcript). Each page has a comparison table, "pick peepshow when…" / "pick the alternative when…" bullets, and a verdict.
- Wait — is this connected to the Channel 4 TV show Peep Show?
- No. peepshow LLM is a developer CLI for extracting video frames for large language models. It has no affiliation with the British sitcom Peep Show (© Objective Productions / Channel 4 Television Corporation, created by Sam Bain & Jesse Armstrong) or its cast. Full disclaimer on the Terms page. Looking for the show? Head to channel4.com.