Video Bug Analysis
You can't watch video — only still frames ffmpeg extracts. Reconstruct what happened
from those snapshots plus what the user tells you. Despite the name, this also handles
non-bug "read the screen" tasks — inventorying a site's features, transcribing a demo,
or describing on-screen UI from a recording. Work this way:
Reading dense text/UI (not a bug)? Skip the contact sheet — extract full-resolution individual frames (
--fps 1to2, no--contact) and read them one by one. Contact tiles downscale too far for small text, especially on tall portrait phone captures (where the script auto-drops to--cols 2, but individual frames still read best).
1. Get context first
Ask for whatever's missing:
- When the bug appears (a timestamp/range like "~0:12") — the biggest accuracy lever.
- Repro steps and expected vs. actual.
- Console / network / logs — invisible in the video unless devtools are on screen, and often the real cause.
- Relevant code area, if known.
A still screenshot of the bad moment beats hunting inside a clip — prefer it, and ask for one whenever a timestamp is fuzzy or ffmpeg isn't available (see the ffmpeg note below).
2. Extract frames
${CLAUDE_PLUGIN_ROOT}/skills/video-bug-analysis/scripts/extract-frames.sh \
--video <path> [--fps <n>] [--scene <thr>] [--contact] [--timestamps <t1,t2>]
Default workflow:
- Overview:
--fps 2 --contact→ one contact sheet of the whole span; read it to find where the symptom is. Add--textwhenever any part of the answer lives in the chrome — a button label, a toggle's selected state, a status pill, a counter — not just for "text-heavy" UIs. A pixel-art canvas with an 11px☀️ daybutton is exactly the case: the canvas reads fine at any tile size, and the label that proves the bug does not. Cheap insurance; the only cost is a bigger sheet. (Portrait captures additionally drop to--cols 1under--text.) - Zoom:
--timestamps 0:12,0:34 --fps 8→ per moment, a dense burst (catches sub-second transients) plus a before/after strip (tsNN_strip.png) that's the best way to show the user a one-frame change.
For a load / splash / "the intro does X" bug, jump straight to --intro — shorthand for
the first ~2s as a dense, labelled contact sheet (--start 0 --end 2 --fps 12 --contact --label,
portrait-aware); any part yields to an explicit flag.
Tighten the window rather than raising fps across the whole clip. Frames default to
.frames/<video-name>/; --out overrides. Add --dry-run to print the exact ffmpeg
commands without running them (replicate by hand when the plugin isn't loaded this session).
More modes — pick by the question (full detail in reference.md, flags in --help)
Most of these are analysis modes that print a CSV/report and exit (no frames). All honor
--start/--end; CSV modes use --fps for the sample rate.
| The question | Mode |
|---|---|
| Where's the transition? | --scene 0.1 (capture cuts) · --list-scenes (print cut times) |
| Before/after of two frames I have | --strip a.png,b.png |
| Did it move / which direction? | --diff (bright = changed pixels) |
| Is it moving, and how much over time? | --motion → t,motion (mean inter-frame delta) |
| Is it choppy, and where? | --stutter (alias --cadence) — verdict (worst freeze) + per-window unique fps; --freeze-min tunes |
| Does a freeze match the app's own timing marks? | --stutter --marks perf.json — overlays performance.mark entries; verdict names the aligned mark |
| Is it hung — dead canvas / infinite splash loop? | --stall — flags ≥ --stall-min s of no change (reads "smooth" to a jank detector) |
| Blown-white flash / mid-clip black dropout? | --whiteout — mean-luma spans with start/duration/peak (bright-side --blackdetect) |
| Content vanished then came back (words dropped)? | --content-revert — A→B→A flicker events; --crop to the text region |
| Part 2 of a split capture (timestamps off by the split)? | add --t0 <sec> to any mode — relabels all reported times into session clock |
| Uneven frame timing (content changes every frame)? | --pacing → t,interval_ms from real presentation timestamps; median/p95/max + worst hitches |
| Spinning in place vs spiralling inward? | --flow → t,speed,curl,div (swirl vs suck decomposition; --flow-center fx:fy) |
| How much of the frame does the subject fill? | --occupancy → t,coverage_pct,bbox (the "present but too small" case) |
| One region's evolution in a single image | --stack --crop W:H:X:Y → vertical ROI time-stack |
| Is content cut off / off-canvas at an edge? | --stack --edge right:60 — --edge side:px is a coord-free crop of that edge band; feeds any mode --crop feeds |
| How many distinct poses / how fast does a region really animate? | --unique → deduped uniq_*.png (the poses) + a cadence verdict; scope with --crop/--edge |
| Too vivid / "clownish" vs muted? | --saturation → t,saturation (per-frame colour intensity) |
| Read a tiny region (FPS/HUD/label) | --crop W:H:X:Y (crop+zoom; combines with any mode) |
| Black / blank screen | --blackdetect (spans, flags PERMANENT vs transient) |
| A readout number changing (4→5→4) | --ocr-roi W:H:X:Y → t,text (needs tesseract) |
| How big / where is a feature, over time | --measure W:H:X:Y → diameter + center (% of viewport) |
| Is it expanding or just fading? | --measure also emits area_px,mean_luma,peak_luma — area climbing = growing; area flat while luma falls = dimming in place (a static ball reads as "something happening" to the eye) |
| Blown-white flash on a dark scene? | --whiteout flags ≥ --white-rel × the clip's own median luma (default 4), so a washout that never nears the absolute cutoff still trips it |
| Capture size / aspect / orientation | --probe (also: which axis CSS vmin is) |
| Dominant colours (palette) | --palette [--colors n] → hex swatches |
| Colour arc over the clip (art reference) | --palette --over-time [--segments n] → t,[hex…] per window |
| Does this GIF/video loop seamlessly? | --loop-check → first-vs-last-frame seam diff + a strip |
| Two captures of the same thing differ where | --ab other.mov → t,ssim divergence timeline |
| Two clips side-by-side, phase-aligned | --compare-videos a,b → one stacked sheet (row per clip) |
Also: --label burns the source timestamp onto frames (now incl. contact tiles &
--compare-videos); --text/--tile-width tune contact legibility; --window/--frame-width
tune bursts. Every run prints a one-line playback cadence: header (effective vs nominal fps
- a dropped-frame estimate) — the quickest "is it choppy?" read. Read it as a measurement, not a verdict: it describes frame timing only, so a frozen or wrong-state UI can post a perfectly healthy cadence. Never let it lower your prior on a "nothing responds" report.
ffmpeg is not installed for you. If it's missing, the script says so and stops: install it yourself (
sudo apt-get install -y ffmpeg/brew install ffmpeg— these ship ffprobe too, which--probe/--list-scenes/--pacing/--stutterneed), or re-run with the explicit opt-inVBA_ALLOW_INSTALL=1(addVBA_ALLOW_DOWNLOAD=1to permit a checksum-verified static build in a sandbox with no package manager). Nothing is installed, escalated, or downloaded without one of those.
Key steer — frames can't see state. If a tracked value changes (--ocr-roi) or you suspect
a logic/timing bug but nothing near it changes in the frame, the cause is off-screen
logic/state (a counter desynced, a body left the viewport) — say so and point the user at
console logs or a small headless repro instead of extracting more frames. Likewise report
feature sizes as % of viewport (--measure/--probe), since retina (dpr 2) device px
mislead. And for "an animation didn't play", frames confirm absence but not cause (the
element may be in the DOM but paused, the first paint deferred, or JS threw) — pair the video
pass with a DOM/console capture before concluding (see reference.md).
Art-reference steer — captures shift colours. A compressed screen recording is
chroma-subsampled (yuv420): saturated colours drift (hot pink reads salmon), so treat
--palette hexes as approximate and never tune art direction to them — for exact colours ask
for the source asset or a lossless screenshot. For "match this animation" requests, lead
with --unique (the distinct poses + real cadence) rather than a fixed-fps burst that
duplicates or skips poses.
ffmpeg note: ffmpeg is already on PATH in many environments (incl. many web containers).
If it's missing, the script reports that and stops — it does not install anything. Don't keep
retrying: either ask the user to install it (sudo apt-get install -y ffmpeg / brew install ffmpeg — these ship ffprobe too), re-run once with the explicit opt-in VBA_ALLOW_INSTALL=1
(or VBA_ALLOW_DOWNLOAD=1 for a checksum-verified static build where no package manager exists),
or — simplest — ask for a still screenshot of the bad moment.
3. Build a timeline
Read the PNGs in filename order; note what's on screen, what changes between frames, and where the symptom first appears. Cite frames by filename.
4. Confirm in the code
Frames give the symptom and its location; the fix comes from the source. Read the implicated component/handler/state before proposing a change — never patch from pixels alone.
5. Report with confidence + caveats
Label what you saw vs. inferred, and how sure you are. Call out the limits that apply:
- Gaps between samples hide fast flickers / one-frame glitches.
- Timing/race bugs: frames give no real sense of duration.
- Small text / subtle diffs are easy to misread or hallucinate — verify against code.
- Off-screen state (console/network/memory) is invisible.
When unsure, ask for a denser extraction, a tighter timestamp, or a still — don't guess.
See reference.md for the reliability matrix, fps-per-bug-class table, and checklist.
Reporting feedback
Surface the pre-filled feedback link each run prints on stderr (suppress:
VBA_NO_FEEDBACK_HINT=1); for a fuller report, scripts/report-feedback.sh. Skill not loaded
this session (plugins load at session start)? --dry-run prints the raw ffmpeg commands to run
by hand.