Slides
SkillDocs & knowledgeExtract relevant frames from a video or animated image (GIF, APNG, animated WebP) so the model can view them as a timeline. Use when the user shares a video or an animated image. DO NOT use for static images, the model can already read those natively.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Slides skill
What this skill tells your AI
The instructions your AI receives, as published by t0mtaylor/peepshow in skills/slides/SKILL.md and read by ahel’s review.
Static images (JPG/PNG/static WebP) can be read natively. This skill is only for video and animated images — anything that has multiple frames across time. It uses peepshow (ffmpeg under the hood) to extract a sequence of relevant still frames so they can be viewed as a timeline.
Input
The user may provide the video (or animated image — GIF, APNG, animated WebP) in any of these forms:
- A local file path (absolute, relative, or a network mount like
/Volumes/share/...) - An
http://orhttps://URL - A
data:video/...;base64,...ordata:image/(gif|apng|webp|png);base64,...URI pasted into the prompt - The literal
-to read bytes from stdin (you would pipe viaBash)
If $ARGUMENTS contains the video reference, use it directly. Otherwise ask the user to share one of the above.
Steps
-
Run the CLI with the
Bashtool. JSON is the most reliable output for parsing:PEEPSHOW_CLIENT=claude-code PEEPSHOW_SESSION="${CLAUDE_SESSION_ID:-}" peepshow "$ARGUMENTS" --emit jsonThe
PEEPSHOW_CLIENT+PEEPSHOW_SESSIONenv vars tag the run in the manifest and the access log so a sharedpeepshow serveinstance can attribute every run + HTTP call back to the right Claude Code session. Both are optional — peepshow runs fine without them — but setting them costs nothing.(If the path contains spaces, quote it. Use
--emit pathsif you prefer reading the human-readable list.) -
Parse the output. In JSON mode,
frames[].pathis the ordered list of absolute paths. Thevideoobject gives you container, codec, resolution, fps, duration, and file size — useful context for the user's question without extra prompting. Thevideo.tagsobject carries container-level metadata embedded in the file (title, artist, album_artist, director, producer, publisher, copyright, genre, description, creation_time, show, episode_id, season_number, etc.) — use it to ground your answer in what the video says it is before describing what you see. Theextractionobject tells you which strategy (scenevsfps) was used, how many frames were pruned, how many were dropped by the perceptual-hash dedup pass (framesDeduped+dedupDistance), and a coarse motion signal across the kept frames (motionSignalAvgnumeric +motionSignalLevellow/medium/high). Use the motion signal to colour your narration (e.g. "rapid action segment" vs "near-static timelapse"); combine withframesDeduped == 0+motionSignalLevel == "high"to recognise a high-information clip where the LLM should pay close attention to every frame. -
Read the audio transcript, if present. When the input had an audio track, the JSON payload's
audioobject has:audio.path— the extractedaudio.m4aon disk.audio.durationSeconds,audio.codec,audio.peakDbfs,audio.silenceRatio— audio metadata.audio.transcript— when transcription ran (whisper.cpp or a cloud provider), this containstext(full concatenated transcript) andsegments[](each withstart/end/textin seconds). Use the transcript to understand what was said in addition to what was shown. For conversational or narration-heavy videos the transcript is often more informative than the frames alone. Cross-reference segment timestamps against frame indices so you can quote who said what when.
If
audio.path === nullthe input had no audio track (GIF, APNG, animated WebP, silent video). Skip the audio step cleanly.If
audio.transcript === nulloraudio.transcript.skippedReasonis set, transcription didn't run — just work with the frames. -
Read each frame as an image with the
Readtool, in order. They are namedframe_0001.jpg,frame_0002.jpg, etc. and represent the timeline from earliest to latest. -
Answer the user's question using both the frames and the transcript together. Reference timestamps when helpful (derive from
video.durationSeconds, frame ordering, and transcriptsegments[].start). A useful pattern for longer clips: summarise the visual timeline in 2-3 beats, then weave in direct quotes from the transcript to ground what was said at those moments. -
Annotate the report — MANDATORY. The JSON payload from step 1 includes an
annotateblock with the exact command. Without this step the manifest stays empty and the run shows up asno-analysison the /runs page. Pipe a JSON object with yoursummaryandperFramecovering every frame back into peepshow:echo '{"summary":"<2-4 sentences describing the timeline>","perFrame":[{"idx":0,"text":"<frame 0 caption>"},{"idx":1,"text":"<frame 1 caption>"},{"idx":2,"text":"<frame 2 caption>"},…,{"idx":N-1,"text":"<frame N-1 caption>"}],"provider":"claude-code","model":"claude-opus-4-7"}' \ | peepshow report annotate "$OUTPUT_DIR"$OUTPUT_DIRis the run'soutputDir(the JSON payload'soutputDirfield — typically a/tmp/peepshow-...path). The annotate step rewritesmanifest.jsonandreport.htmlatomically; users opening the HTML now see your synthesis under the "LLM analysis" section.perFrame.lengthMUST equalframes.length. Every extracted frame gets its own caption — no skipping, no key-beats-only summaries. The /runs page surfaces sparse coverage aspartial-captions; agents that ship sparse perFrame are doing it wrong. peepshow logs a warning to stderr when sparse uploads are detected (and rejects them with--strict).Run this for every invocation, including ones that didn't end with a question — the manifest is the durable record of what you understood. Only skip when the user explicitly asks for raw frames with no synthesis.
Useful flags
Pass these after the input when the defaults are not right:
--max 20— cap the number of frames returned (default 40)--min 6— ensure at least this many (falls back from scene detection to fps sampling)--threshold 0.2— more sensitive scene detection (default 0.3; lower = more frames)--fps 0.5— skip scene detection and sample at a fixed rate--width 960— max output width in pixels (default 1280)--format png— output PNG instead of JPG--stats full— include the full stats block (video + extraction details)--stats off— suppress stats entirely if context is tight
Troubleshooting
ffmpeg not found: runnpm installinside the plugin directory once to fetch the bundled ffmpeg binary.- Zero frames returned: lower
--thresholdor force--fps 1. - Too many frames: raise
--thresholdor lower--max.
Signals
- GitHub stars
- 67
- Forks
- 13
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
slides-t0mtaylor- Source
- github.com/t0mtaylor/peepshow