peepshow — плагин для Claude Code: что делает и как установить
Разбирает видеоролики и анимации на таймлайн кадров со звуковой дорожкой и транскриптом. Позволяет скормить скринкаст или гифку модели прямо в Claude Code.
/plugin marketplace add anthropics/claude-plugins-community
/plugin install peepshow@claude-communityЧто делает peepshow
Плагин перехватывает добавление видеофайлов и анимаций форматов MP4, MOV, GIF, APNG и WebP в диалог. Пользователю не нужно нарезать ролики вручную. Хук UserPromptSubmit запускает команду /peepshow:slides, генерирует раскадровку и передаёт её агенту вместе с контекстом.
Для файлов со звуком утилита извлекает аудиодорожку в формате AAC и может сразу расшифровать речь. Если в PATH обнаружен whisper.cpp, локальная транскрипция с моделью base.en запустится автоматически. Для внешних сервисов поддерживаются провайдеры вроде Groq, Deepgram или OpenAI. Извлечённые данные можно передавать дальше через 95 готовых приёмников sink во внешние базы или трекеры задач.
Системный ffmpeg ставить необязательно. Пакет включает запасной бинарник ffmpeg-static, однако для работы самого инструмента требуется Node.js версии не ниже 22.
Какие задачи решает
- разобрать скринкаст с багом на стоп-кадры для отладки
- извлечь аудиодорожку из видео и перевести речь в текст
- проанализировать анимированный GIF прямо в диалоге с моделью
- отправить раскадровку и метаданные ролика во внешнее хранилище
Примеры запросов агенту
- /peepshow:slides ~/Desktop/bug.mov
- Что происходит в этом скринкасте
./demo.mp4? - Посмотри
./error.gifи опиши ошибку в интерфейсе
Как установить peepshow
Через маркетплейс плагинов
/plugin marketplace add anthropics/claude-plugins-community/plugin install peepshow@claude-communityПо шагам, с командами для каждой платформы: Плагины Claude Code: маркетплейсы, установка и как собрать свой
Вопросы о peepshow
Как установить peepshow в Claude Code?
Выполните в чате платформы две команды по очереди: сначала /plugin marketplace add anthropics/claude-plugins-community, затем /plugin install peepshow@claude-community.
Что делает peepshow?
Плагин нарезает видеозаписи и анимированные картинки на кадры, вытаскивает звук с расшифровкой и отдаёт полученные материалы модели для анализа.
Обязательно ли устанавливать системный ffmpeg?
Нет, плагин автоматически обращается к встроенному пакету ffmpeg-static, хотя системная сборка ffmpeg работает быстрее и распознаёт больше кодеков.
Исходный текст
SKILL.md · MIT
<p align="center">
<img src="./docs/logo.svg" alt="peepshow" width="520">
</p>
<p align="center">
<a href="https://www.peepshow.dev/"><img src="https://img.shields.io/badge/site-peepshow.dev-a78bfa" alt="peepshow.dev"></a>
<a href="https://www.npmjs.com/package/peepshow"><img src="https://img.shields.io/npm/v/peepshow.svg" alt="npm"></a>
<img src="https://img.shields.io/badge/node-%E2%89%A522-43853d" alt="Node ≥ 22">
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT">
<a href="https://x.com/tom_taylor"><img src="https://img.shields.io/badge/x-%40tom__taylor-000000" alt="@tom_taylor on X"></a>
</p>
<p align="center">
<a href="https://www.peepshow.dev/">peepshow.dev</a> ·
<a href="https://www.peepshow.dev/sinks/">Sinks</a> ·
<a href="https://www.peepshow.dev/agents/">Agents</a> ·
<a href="https://x.com/tom_taylor">@tom_taylor</a>
</p>
# peepshow
> **video → frames → LLM.** Any LLM CLI. Any storage backend. Zero glue code.
Turn a video — or an animated GIF, APNG, or WebP — into a timeline of still frames plus an extracted audio track and its transcript, so an LLM can "watch" and "listen" to what's inside.
<p align="center">
<img src="docs/demo.gif" alt="peepshow demo — drop a clip, extract frames and transcript, LLM answers" width="720">
</p>
Static images (JPG, PNG, static WebP) are already handled natively by most LLMs — drag, drop, done. **`peepshow` only steps in for things that have multiple frames across time**: videos and animated images, which today's LLMs can't consume directly.
**Same drag-and-drop UX as images.** Drop a `.mp4` / `.mov` / `.gif` into the Claude Code prompt and a `UserPromptSubmit` hook auto-invokes `/peepshow:slides <path>` behind the scenes — Claude extracts frames, reads them, and answers. No slash command, no Bash call, no copy-paste. Works with natural-language prompts too ("what's in ~/Desktop/bug.mov?") and with every other CLI entry point (explicit `/peepshow:slides`, shell `peepshow ...`, pipes into `--sink`).
**Pluggable sink backends — write your own, or use the built-ins.** Every run can fan out to downstream systems via `--sink <name>` or `--sink-cmd <shell>`. A sink is any executable that reads a JSON payload on stdin — write one in bash, Node, Python, Go, whatever. The full `video` + `tags` + `frames` + `audio` + `extraction` payload (same as `--emit json`) flows to each sink, so they receive the full context, not just the frame paths. **95 sinks ship built-in** across SQL (SQLite · Postgres · MongoDB), vector + AI memory (Chroma · Qdrant · Pinecone · pgvector · Weaviate · Milvus · MemPalace · Zep · Mem0 · Letta), object + file storage (S3-compatible · GCS · Firebase Storage · Azure Blob · Supabase · Dropbox · Google Drive · Box · OpenAI Files), chat (Slack · Discord · MS Teams · Telegram · WhatsApp · iMessage · Mattermost · Rocket.Chat · Zulip · Matrix), issue trackers (Linear · Height · GitHub Issues · Jira · Asana · ClickUp · Shortcut · Trello), whiteboards (Miro · Figma), wiki/notes (Notion · Obsidian · Logseq · Outline · Confluence · Apple Notes · Bear), product analytics (PostHog · Plausible · Mixpanel/Amplitude/Segment), observability (Sentry · Datadog · New Relic · Honeycomb · PagerDuty · Opsgenie · Grafana Oncall), and workflow glue (Webhook · GraphQL · IDE · Aider · Continue · Cody · Raycast · Pipedream · Zapier · Shortcuts · Apple Reminders · Things 3). Browse [`docs/sinks/`](./docs/sinks/) or use the [use-case finder](https://www.peepshow.dev/sinks/find/). Full spec: [`docs/PLUGINS.md`](./docs/PLUGINS.md).
**Audio is extracted too, not just frames.** When the input has an audio track (MP4 / MOV / WebM / MKV), `peepshow` runs a second ffmpeg pass that emits a compact mono 16 kHz AAC track next to the frames and probes loudness peak + silence ratio. Animated GIF, APNG, and animated WebP inputs skip this cleanly — they can't carry audio at the format level. The extracted `audio.m4a` + its analysis fields land in the JSON payload alongside `video` + `frames`, so any downstream sink sees the full picture. Opt out with `--no-audio`.
## Audio transcription
When audio is extracted, `peepshow` can also transcribe it. **If [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp) is on your `PATH`, transcription auto-enables with the `base.en` model — no flag, no API key, nothing leaves the machine.** The model file downloads to `~/.peepshow/whisper-models/ggml-<model>.bin` on first use (override with `PEEPSHOW_WHISPER_MODEL_DIR`). Skip entirely with `--no-transcribe` or `PEEPSHOW_TRANSCRIBE=off`.
Prefer a cloud provider? Pick one in a single flag and set the matching API key — peepshow uploads the extracted `audio.m4a` and stores the result back on the same payload:
| Provider | Install / key | CLI |
| :------- | :------------ | :-- |
| `whisper-cpp` (default when on PATH) | `brew install whisper-cpp` (macOS) · `scoop install whisper-cpp` (Windows) · [prebuilt Linux releases](https://github.com/ggml-org/whisper.cpp/releases) | `--transcribe whisper-cpp` |
| `openai` | `OPENAI_API_KEY=…` | `--transcribe openai` |
| `groq` | `GROQ_API_KEY=…` | `--transcribe groq` |
| `deepgram` | `DEEPGRAM_API_KEY=…` | `--transcribe deepgram` |
| `assemblyai` | `ASSEMBLYAI_API_KEY=…` | `--transcribe assemblyai` |
| `custom` | any shell command — reads audio on stdin, writes `{segments,text}` JSON on stdout | `PEEPSHOW_TRANSCRIBE_CMD='my-asr'` + `--transcribe custom` |
The transcript lands on the JSON payload as `audio.transcript` (`{provider, model, language, durationSeconds, segments: [{start, end, text}], text}`). Because it rides on the same payload as frames and `video.tags`, **every sink gets the transcript for free** — search SQLite by spoken phrase, embed segments into pgvector/Chroma/Pinecone, drop it into Obsidian next to the frames, pipe it to Slack.
Full reference + provider caveats: **[peepshow.dev/transcription/](https://www.peepshow.dev/transcription/)**.
## TL;DR
```bash
# 1. Install peepshow from npm — https://www.npmjs.com/package/peepshow
npm i -g peepshow # global bin: `peepshow` + sink bins
# or: npm i peepshow # local dep
# or: npx peepshow ./video.mp4 # one-shot
# 2. (Optional) install ffmpeg natively — faster, more codecs
brew install ffmpeg # macOS
choco install ffmpeg-full # Windows
sudo apt install ffmpeg # Debian/Ubuntu
# Skip the above entirely — peepshow falls back to the bundled
# ffmpeg-static that `npm i peepshow` pulls in automatically.
# 3. Use it anywhere
peepshow ./bug.mov # stdout: human-readable summary + frame paths
peepshow ./demo.mp4 --emit json | jq # structured output
peepshow ./loop.gif --emit caveman # token-compressed output for LLMs
peepshow ./keynote.mp4 --sink folder:/shared # fan out to folder/mysql/webhook/...
```
Inside Claude Code — **just drag the video into the prompt**. A `UserPromptSubmit` hook detects the path and auto-invokes `/peepshow:slides`, so dropping a `.mp4` / `.mov` / `.gif` works the same as dropping an image:
<p align="center">
<img src="./docs/demo-drop.png" alt="Dragging a video into Claude Code triggers peepshow automatically" width="900">
</p>
Explicit invocation works too:
```
/peepshow:slides ./bug.mov
```
Or describe the task in natural language — the skill auto-invokes:
> "Watch ~/Desktop/demo.mp4 and tell me what happens in the first 10 seconds."
## Why
- **Drag-and-drop into Claude.** UserPromptSubmit hook spots video/animated-image paths in your prompt and auto-invokes the skill — same UX as dropping an image.
- **Scene-change detection by default** — ffmpeg's `select='gt(scene,…)'` filter catches visually distinct moments, no fixed-fps noise.
- **Perceptual frame dedup** — every extraction runs an 8×8 dHash post-pass and drops near-identical frames (default hamming threshold 5). Means a static talking-head, a security-cam loop, or a slideshow recording emits a handful of frames covering the actual visual changes — not 600 thumbnails of the same face. Survives `--fps` fallback (where fixed-rate sampling normally wastes frames). Off via `--no-dedup`. Real numbers across the hero reels: `earth-at-night` 6→2 frames (−67%), `UI demo` 18→10 (−44%), `jellyfish` 6→6 (0% — high-motion content keeps every distinct frame, zero false drops).
- **Motion signal + adaptive density** — the dHash pass also computes the average pairwise hamming distance between kept frames, classified as `low`/`medium`/`high` and surfaced on the manifest as `extraction.motionSignalAvg` + `extraction.motionSignalLevel`. When the signal reads `high` AND dedup dropped 0 frames AND there's still budget under `--max`, peepshow auto re-extracts at ~80% of `--max` so high-motion clips get proportionally more frames in the same budget (verified live: `big-buck-bunny.mp4` 6→14 frames). Disable with `--no-adaptive` when you need a deterministic fps every run. See [`/optimised/`](https://www.peepshow.dev/optimised/) for the full benchmark.
- **Ships native agent manifests for 8+ tools.** Claude Code plugin (`.claude-plugin/`), Cursor (`.cursor/rules/`), Windsurf (`.windsurf/rules/`), Cline (`.clinerules/`), Codex CLI (`.codex/hooks.json`), Gemini CLI (`gemini-extension.json` + `GEMINI.md`), generic `AGENTS.md` convention, `.agents/plugins/` marketplace. Plus integration snippets for Copilot CLI, aider, `llm`, Continue, Cody, Zed AI, Perplexity, Ollama in [`docs/INTEGRATIONS.md`](./docs/INTEGRATIONS.md).
- **Hardware-accelerated decoding** by default — VideoToolbox on macOS, VAAPI on Linux, D3D11VA on Windows. `--no-gpu` to force CPU.
- **Pluggable sink backends.** Fan out each run to folders, SQL, vector DBs, object storage, chat, wikis, issue trackers, observability, workflow glue — 71 ship built-in across 10 categories. Contract is one JSON stream on stdin. See [`docs/PLUGINS.md`](./docs/PLUGINS.md) and the [sinks hub](https://www.peepshow.dev/sinks/).
- **Pairs with [JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)** for token-aware LLM setups (`--emit caveman`).
- **Live statusline badge.** `[PEEPSHOW:decoding:42%]` mid-run, `[PEEPSHOW:5frm:scene:system]` after, `[PEEPSHOW]` idle.
- **Context-aware hints on stderr.** Missing ffmpeg → OS-specific install command. Bundled ffmpeg → one-time nudge toward native. Zero frames / short clip / heavy pruning → actionable flag suggestions. `PEEPSHOW_NO_HINTS=1` to silence.
## Where peepshow fits
`peepshow` is a CLI first — anything that can spawn a child process can use it. The intended use cases break down like this:
- **Claude Code plugin** — drag-and-drop UX via the `UserPromptSubmit` hook. Native skills `/peepshow:slides` and `/peepshow:sink`. Already covered above.
- **Cursor / Windsurf / Cline rules** — the per-agent rule files in `.cursor/rules/`, `.windsurf/rules/`, `.clinerules/` teach each agent to call `peepshow` when the user mentions a video path.
- **Gemini CLI extension** — a real extension, not just a manifest. `gemini extensions install https://github.com/t0mtaylor/peepshow` registers `/peepshow:slides` and `/peepshow:sink` as native slash commands, with `GEMINI.md` supplying the context. Details below.
- **Codex CLI** — `.codex/hooks.json` ships a native `SessionStart` hook manifest.
- **Aider / Continue / Cody / Zed AI / Copilot CLI / `llm`** — copy-paste snippets in [`docs/INTEGRATIONS.md`](./docs/INTEGRATIONS.md). Generic `AGENTS.md` covers anything that follows that convention.
- **Standalone shell** — pipe video bytes in on stdin, pipe JSON out, fan out to sinks. Works inside any shell pipeline, CI job, cron task, Makefile target, or sandbox.
- **Electron desktop AI client** — pre-process video in the main process before forwarding context to a cloud LLM. See below.
- **Server-side AI portal** — extract once, fan a single JSON manifest out to multiple LLMs. See below.
- **`peepshow serve` dashboard** — local HTTP server that browses run history, streams frames, manages auto-sinks. Loopback by default. Full reference: [`docs/SERVE.md`](./docs/SERVE.md).
### Electron desktop AI client
Drop-target a video onto a `BrowserWindow`, pre-process locally in the main process, only forward the distilled JSON to your cloud model. Frames + transcript stay on disk; the LLM sees a compact manifest.
```js
// main.js — Electron main process
import { spawn } from 'node:child_process';
ipcMain.handle('peepshow:run', async (_evt, videoPath) => {
return new Promise((resolve, reject) => {
const child = spawn('peepshow', [videoPath, '--emit', 'json', '--quiet']);
let out = '';
child.stdout.on('data', (b) => { out += b; });
child.on('close', (code) => code === 0 ? resolve(JSON.parse(out)) : reject(new Error(`peepshow exit ${code}`)));
});
});
```
Sandbox the renderer (`contextIsolation: true`, `nodeIntegration: false`); only the main process gets `child_process`. If you expose `peepshow serve` to the renderer over HTTP, bind it loopback or pass `--token`. Pattern: pre-process locally → keep frames + transcript on disk → only forward distilled context to the cloud LLM.
### Server-side AI portal / multi-LLM pre-processor
A Node service that ingests user uploads, runs `peepshow` once, then fans the JSON manifest out to multiple LLMs. Cuts upload bandwidth and per-token cost vs sending the full video to each provider.
```js
// portal.js — fan one extraction out to N models
import { spawn } from 'node:child_process';
import express from 'express';
const app = express();
app.post('/analyse', express.raw({ type: '*/*', limit: '500mb' }), async (req, res) => {
const ps = spawn('peepshow', ['-', '--emit', 'json', '--no-index', '--no-report', '--quiet']);
ps.stdin.end(req.body); // upload bytes → stdin
let manifest = '';
ps.stdout.on('data', (b) => { manifest += b; });
ps.on('close', async (code) => {
if (code !== 0) return res.status(500).json({ error: 'extract failed' });
const payload = JSON.parse(manifest);
const replies = await Promise.all([
callClaude(payload), callGPT(payload), callGemini(payload), callLocal(payload),
]);
res.json({ replies });
});
});
```
`--no-index --no-report` keeps the service stateless. Pattern: one extract, many models. Cuts upload bandwidth + cost vs sending the full video to each LLM.
### Telemetry + privacy
`peepshow` sends an anonymous run beacon by default (version + OS family + outcome — no paths, no payload). Opt out with `peepshow config set telemetry off`, `PEEPSHOW_TELEMETRY=0`, or `DO_NOT_TRACK=1`. Full details + every switch in [`docs/PRIVACY.md`](./docs/PRIVACY.md).
## Requirements
- **Node.js ≥ 22** — enforced by `npm i peepshow` via the `engines` field. Node is the only interpreter the launchers need: every command in `bin/` is a `#!/usr/bin/env node` script, so `npm` and `pnpm` generate a Windows `.CMD` shim that calls `node` directly. **No POSIX shell, MSYS, or Git Bash is required on Windows.**
- **ffmpeg** — `npm i peepshow` pulls in `ffmpeg-static` automatically, so it works out of the box on linux-x64/arm64, darwin-x64/arm64, win32-x64. Install a native build for faster hardware decoding (see matrix below).
## ffmpeg install matrix
`peepshow` auto-discovers ffmpeg in this order: **`PEEPSHOW_FFMPEG` env var → native `ffmpeg` on PATH → bundled `ffmpeg-static`**. Native builds are preferred because distro packages are typically linked with full hardware-acceleration support (VideoToolbox on macOS, NVENC/QSV/VAAPI on Linux, D3D11VA on Windows) while the `ffmpeg-static` binary is a conservative generic build.
| OS | Recommended install | Notes |
| :-- | :------------------ | :---- |
| macOS (Apple Silicon & Intel) | `brew install ffmpeg` | Bottle includes VideoToolbox + most codecs. |
| Windows 10/11 | `choco install ffmpeg-full` or `winget install Gyan.FFmpeg` | Full build has hardware accel and extra codecs. |
| Windows (Scoop) | `scoop install ffmpeg` | Smaller install, still usable. |
| Debian / Ubuntu | `sudo apt install ffmpeg` | Needs `universe` repo enabled on some Ubuntus. |
| Fedora / RHEL | `sudo dnf install ffmpeg-free` (Fedora 41+) or enable RPM Fusion for `ffmpeg` | `ffmpeg-free` omits some patented codecs. |
| Arch / Manjaro | `sudo pacman -S ffmpeg` | |
| Alpine | `sudo apk add ffmpeg` | |
| openSUSE | `sudo zypper install ffmpeg-7` | |
| NixOS | `nix-env -iA nixpkgs.ffmpeg-full` | |
| Any OS, no admin rights | nothing to do — `npm install` inside the plugin pulls `ffmpeg-static` | Works on linux-x64/arm64, darwin-x64/arm64, win32-x64. |
Override the selection explicitly:
```bash
export PEEPSHOW_FFMPEG=/opt/homebrew/bin/ffmpeg # any absolute path
peepshow ./video.mp4
```
Combine with `--gpu` to pick a hardware-accel backend (`videotoolbox`, `cuda`, `qsv`, `vaapi`, `amf`, `d3d11va`), or `--no-gpu` to force CPU-only decoding.
## Install
Two steps. Both required — **install the runtime from npm, register the Claude plugin from GitHub.**
### 1. Install the runtime from npm
Published on npm as [`peepshow`](https://www.npmjs.com/package/peepshow):
```bash
npm i -g peepshow # global — adds peepshow + all peepshow-sink-* bins to PATH
# or
npx peepshow ./video.mp4 # one-shot, no install
```
Every sink bin (`peepshow-sink-sqlite`, `peepshow-sink-postgres`, `peepshow-sink-s3`, `peepshow-sink-webhook`, `peepshow-sink-slack`, `peepshow-sink-discord`, `peepshow-sink-graphql`, `peepshow-sink-notion`, `peepshow-sink-obsidian`, `peepshow-sink-ide`, `peepshow-sink-linear`, `peepshow-sink-github-issues`, `peepshow-sink-sentry`, `peepshow-sink-chroma`, `peepshow-sink-qdrant`, `peepshow-sink-pinecone`, `peepshow-sink-pgvector`, `peepshow-sink-mongodb`, `peepshow-sink-mempalace`) installs alongside. Heavy driver libraries (`better-sqlite3`, `pg`, `@aws-sdk/client-s3`, `mongodb`) are **optional deps** — if one fails to build, the rest of peepshow still works; the missing sink prints a one-line install hint when invoked.
Verify:
```bash
peepshow --help
```
### 2. Register the Claude Code plugin
Two commands pull the skill, hooks, and agent manifests from the public GitHub repo:
```bash
claude plugin marketplace add t0mtaylor/peepshow
claude plugin install peepshow@peepshow-marketplace
```
After that, `claude` launches normally with peepshow enabled. The skill is available as `/peepshow:slides` — and the drag-and-drop hook fires automatically on every prompt.
> **Why two steps?** This GitHub repo ships only manifests, hooks, and docs — no compiled code. The runtime lives on npm so everyone gets minified, versioned binaries. Step 1 supplies the `peepshow` command; step 2 lets Claude Code know about it.
### 3. Register the Gemini CLI extension
peepshow ships as a first-class [Gemini CLI extension](https://geminicli.com/extensions) — slash commands included, no MCP server to run:
```bash
gemini extensions install https://github.com/t0mtaylor/peepshow
```
That clones the public repo into `~/.gemini/extensions/peepshow` and registers two namespaced commands. Restart the CLI (or `/commands reload`) and they appear in `/help`:
| Command | Does |
| :------ | :--- |
| `/peepshow:slides <video>` | Extract the timeline, read every frame in order, read the transcript, answer, then write the analysis back into the run's report. |
| `/peepshow:sink <intent>` | Manage the persistent auto-sink list — list, add (including `--when` conditions), remove, clear. |
The commands shell out to the `peepshow` binary from step 1, so npm supplies the runtime and the extension supplies the workflow — same split as the Claude Code plugin. `GEMINI.md` imports `AGENTS.md`, so Gemini also gets the full tool description in context even when you invoke the CLI by hand instead of through a command.
Verify with `gemini extensions list`; update later with `gemini extensions update peepshow`.
## Drag-and-drop auto-invoke
Claude Code attaches dropped images inline but inserts a plain path for videos. peepshow's `UserPromptSubmit` hook closes that gap:
1. Drag `clip.mp4` (or any of mov/m4v/mkv/webm/avi/flv/wmv/ts/mts/m2ts/3gp/3g2/ogv/mpg/mpeg/gif/apng) into the Claude prompt.
2. The path text gets inserted. Hit Enter.
3. Hook scans the prompt, spots the video extension, injects a reminder: _"run `/peepshow:slides <path>`"_.
4. Claude auto-runs the skill, extracts frames, reads each one, and answers your question.
No explicit slash command required. If you just drop the path with no message, Claude describes the timeline. If you add a short question (`what's in ./bug.mov?`), that gets answered against the frames.
**Silent when:** the path points to a static image (JPG/PNG/static WebP — Claude handles those natively), the prompt is plain text with no media path, or the prompt is a long narrative that merely mentions a filename.
## Usage
Inside Claude Code:
```
/peepshow:slides ./demo.mp4
/peepshow:slides https://example.com/keynote.mp4
/peepshow:slides /Volumes/share/meeting.mov --max 20
```
Or just describe the task — Claude will auto-invoke the skill based on its description:
> "Here's a video of the bug: ./bug.mov — what do you see?"
The skill runs the CLI, reads each frame back as an image, and answers your question.
## Standalone CLI
You can also use `peepshow` directly from a shell:
```bash
./bin/peepshow.mjs ./video.mp4
./bin/peepshow.mjs https://example.com/clip.webm --threshold 0.2
cat video.mp4 | ./bin/peepshow.mjs - # bytes on stdin
./bin/peepshow.mjs ./v.mp4 --fps 1 --max 30 # fixed-fps sampling, cap at 30 frames
```
Default output (paths + one-line stats):
```
peepshow: extracted 12 frames via scene detection to /tmp/peepshow-abc123
peepshow stats: 0:42 1920×1080 h264 30fps 12.4MB → 12 via scene, 410KB, 187.24ms
/tmp/peepshow-abc123/frame_0001.jpg
/tmp/peepshow-abc123/frame_0002.jpg
...
```
Machine-readable output (`--emit json`):
```json
{
"outputDir": "/tmp/peepshow-abc123",
"strategy": "scene",
"frames": [{ "path": "/tmp/peepshow-abc123/frame_0001.jpg", "bytes": 34201 }, ...],
"video": { "container": "mov", "codec": "h264", "width": 1920, "height": 1080, "fps": 30, "durationSeconds": 42.0, "sizeBytes": 12994816, ... },
"extraction": { "strategy": "scene", "framesEmitted": 12, "elapsedMs": 187.24, ... }
}
```
LLM-friendly markdown output (`--emit markdown`) emits one `` link per frame plus an optional stats table.
### Options
| Flag | Default | Purpose |
| :--- | :------ | :------ |
| `--threshold <0-1>` | `0.3` | Scene-change sensitivity. Lower = more frames. |
| `--max <n>` | `40` | Maximum frames to keep (evenly pruned if exceeded). |
| `--min <n>` | `4` | Minimum frames; falls back from scene detection to fps sampling when scene yields fewer. |
| `--fps <n>` | — | Force fixed fps sampling. Skips scene detection. |
| `--width <px>` | `1280` | Max output width. Aspect ratio preserved. |
| `--format jpg\|png` | `jpg` | Output format. |
| `--output <dir>` | temp dir | Where to write frames. |
| `--emit paths\|json\|markdown\|caveman` | `paths` | Output format — see [`docs/INTEGRATIONS.md`](./docs/INTEGRATIONS.md) for when each one shines. |
| `--stats off\|short\|full` | `short` | Verbosity of the stats block. `--no-stats` / `--full-stats` are shortcuts. |
| `--quiet`, `-q` | off | Alias for `--stats off`. |
| `--gpu auto\|off\|videotoolbox\|cuda\|qsv\|vaapi\|amf\|d3d11va` | `auto` | ffmpeg hardware-decode backend. **`auto` is duration + resolution aware**: clips <30s (or 30–60s at <1080p) decode on CPU because GPU init + memory-copy overhead exceeds per-frame savings — measured up to ~3× faster than forced GPU on short H.264 clips. 1080p+ longer than 30s uses the platform default (VideoToolbox/macOS, VAAPI/Linux, D3D11VA/Windows). Override via `PEEPSHOW_GPU_MIN_SECONDS=<n>` (default 30) or `PEEPSHOW_GPU_MIN_HEIGHT=<px>` (default 1080). Pass an explicit backend (`--gpu videotoolbox` / `cuda` / `vaapi` / `amf`) to force GPU regardless. `--no-gpu` shortcuts `--gpu off`. |
| `--dedup on\|auto\|off` | `on` | Perceptual frame dedup post-pass via 8×8 dHash. `on` runs in both scene + fps modes; `auto` only fps fallback (scene already filters); `off` disables. `--no-dedup` is a shortcut. Adds ~50–100ms for 40 frames; drops near-identical thumbnails so static talking-heads don't waste tokens. |
| `--dedup-distance <0-64>` | `5` | Hamming-distance threshold. Lower = stricter (drops fewer near-duplicates); higher = looser (drops more). 0 means only bit-identical hashes drop. |
| `--adaptive on\|off` | `on` | Adaptive density second pass. When dedup drops 0 frames AND motion is high AND there's room under `--max`, peepshow re-extracts at higher fps to amortise the headroom. Targets ~80% of `--max`. Recursion-guarded (single retry only). `--no-adaptive` shortcuts disable. |
| `--version`, `-V` | — | Print the peepshow version and exit 0. |
| `--help`, `-h` | — | Show usage. |
### Input sources
Auto-detected from the first argument. Any ffmpeg-readable format works; animated image formats are treated as videos.
| Form | Example |
| :--- | :------ |
| Local path | `./video.mp4`, `/Volumes/share/clip.mov`, `./loop.gif` |
| HTTP(S) URL | `https://example.com/v.mp4` |
| Video data URI | `data:video/mp4;base64,AAAA...` |
| Animated image data URI | `data:image/gif;base64,...` (plus `apng`, `webp`, `png`) |
| Stdin bytes | `cat v.mp4 \| peepshow -` |
### Container metadata (tags)
peepshow reads the video's top-level `Metadata:` block from ffmpeg and exposes it as `video.tags` in the JSON emit (plus in the full-stats block and markdown table). Every narrative tag ffmpeg surfaces flows through — common keys include:
`title`, `artist`, `album_artist`, `album`, `composer`, `performer`, `director`, `producer`, `publisher`, `network`, `show`, `episode_id`, `season_number`, `genre`, `description`, `synopsis`, `comment`, `copyright`, `encoder`, `creation_time`, `date`.
Container-boilerplate keys (`major_brand`, `minor_version`, `compatible_brands`, `handler_name`, `vendor_id`, `timecode`) are dropped — they add noise without LLM value. Unknown custom tags pass through verbatim after the curated set.
The short-stats line surfaces the title (if present) so a quick run on a tagged movie looks like:
```
peepshow stats: "The Heist" 1:32:18 1920×1080 h264 24fps 2.8GB → 40 via scene, …
```
Because the tags ride inside the JSON contract, every **sink** (folder archive, Postgres, webhook, S3, Obsidian, Chroma, …) receives them too — store them next to the frames, index by director, group by show, whatever.
### Runtime hints
peepshow prints short, context-aware tips to stderr when it can nudge you in a useful direction. Hints never block or fail a run — they're just hints.
| Situation | Hint |
| :-------- | :--- |
| `ffmpeg` not found (exit 3) | OS-specific install command (`brew install ffmpeg` on macOS, `choco install ffmpeg-full` on Windows, `sudo apt install ffmpeg` on Ubuntu, etc.) |
| Using bundled `ffmpeg-static` | One-off tip that native ffmpeg is faster + the install command for your OS. Shown once per machine. |
| 0 frames extracted with scene detection | Suggests lowering `--threshold` or forcing `--fps 1`. |
| Short clip (<3s) where scene detection missed | Suggests `--fps 2` for tiny clips/loops. |
| Many frames pruned to meet `--max` | Suggests raising `--max` or tightening `--threshold`. |
Opt-out per-invocation: `PEEPSHOW_NO_HINTS=1 peepshow ...`
"Shown-once" hints are tracked in `~/.peepshow/hints-shown` (override path with `PEEPSHOW_HINTS_FILE`). Delete the file to re-show them.
Inside Claude Code, hints flow naturally — Claude reads the stderr alongside the stdout output, so if ffmpeg is missing or a setting is off, it can act on the tip without you re-typing anything.
### Auto-compression
If peepshow detects a known token compressor on your `$PATH`, it will automatically route its output through it — no flag required. Preference order:
1. **`boom-llm`** (future-facing binary — not yet released)
2. **`caveman`** — [`JuliusBrussee/caveman`](https://github.com/JuliusBrussee/caveman)
When auto-detection fires, peepshow prints a one-line note to stderr:
```
peepshow: auto-compressor detected → caveman
```
Under the hood, peepshow emits its standard JSON payload to the compressor's stdin and uses the compressor's stdout as the final output. Any compressor failure falls back silently to the default `paths` emit (the error is logged to stderr).
**Explicit `--emit` always wins.** Pass `--emit json` (or `paths`/`markdown`/`caveman`) and auto-detection is skipped entirely.
**Opt out globally** with an env var:
```bash
PEEPSHOW_AUTO_COMPRESS=0 peepshow ./video.mp4
# also accepted: PEEPSHOW_AUTO_COMPRESS=false
```
The internal `--emit caveman` format (ultra-terse, no external binary required) remains available whether or not the `caveman` binary is installed.
### Exit codes
| Code | Meaning |
| :--- | :------ |
| 0 | Success, at least one frame written |
| 2 | Argument parsing error |
| 3 | `ffmpeg` not found |
| 4 | Input could not be resolved |
| 5 | ffmpeg produced zero frames |
| 6 | ffmpeg exited non-zero |
## Sink plugins
peepshow can fan out extracted frames + metadata to any downstream system. A **sink** is just an executable that reads the run's JSON payload on stdin and forwards it somewhere — written in any language.
**95 sinks ship built-in** across 10 categories. Browse them on the [sinks hub](https://www.peepshow.dev/sinks/), pick by use case at the [finder](https://www.peepshow.dev/sinks/find/), or read the per-sink docs in [`docs/sinks/`](./docs/sinks/). A short cross-section:
| Category | Examples |
| :------- | :------- |
| **SQL & document** | sqlite · postgres · mongodb |
| **Vector + AI memory** | chroma · qdrant · pinecone · pgvector · weaviate · milvus · zep · mem0 · letta · mempalace |
| **Object + cloud storage** | s3-compatible · gcs · fireba...
© t0mtaylor · MIT · t0mtaylor/peepshow