SKILL.md: rewritten following Anthropic best practices - Concise (233 lines, down from 347) - Critical VHS parser limitations section (base64 workaround) - Advanced patterns: self-bootstrap, output filtering, frame verification - Better description for skill triggering New files: - references/advanced_patterns.md: production patterns from dbskill project - assets/templates/self-bootstrap.tape: self-cleaning demo template auto_generate_demo.py: new flags - --bootstrap: hidden setup commands (self-cleaning state) - --filter: regex pattern to filter noisy output - --speed: post-processing speed multiplier (gifsicle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.8 KiB
Advanced VHS Demo Patterns
Battle-tested patterns from production demo recording workflows.
Contents
- Self-bootstrapping tapes
- Output noise filtering with base64 wrapper
- Frame-level verification
- Post-processing with gifsicle
- Auto-detection and template rendering
- Recording script structure
Self-Bootstrapping Tapes
A self-bootstrapping tape cleans its own state before recording, so running it twice produces identical output. Three phases:
# Phase 1: HIDDEN CLEANUP — remove previous state
Hide
Type "my-tool uninstall 2>/dev/null; my-tool reset 2>/dev/null"
Enter
Sleep 3s
# Phase 2: HIDDEN SETUP — create helpers (base64 for special chars)
Type "echo <base64-wrapper> | base64 -d > /tmp/helper.sh && source /tmp/helper.sh"
Enter
Sleep 500ms
# Phase 3: CLEAR + SHOW — wipe buffer before revealing
Type "clear"
Enter
Sleep 500ms
Show
# Phase 4: VISIBLE DEMO — what the viewer sees
Type "my-tool install"
Enter
Sleep 3s
Why clear before Show: VHS's Hide stops recording frames, but the terminal buffer still accumulates text. Without clear, the hidden commands' text appears in the first visible frame.
Output Noise Filtering with Base64
Many CLI tools produce verbose progress output that clutters demos. The solution: a hidden shell wrapper that filters noise lines.
Step 1: Create the wrapper function
# The function you want (can't type directly in VHS due to $/" chars)
my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }
Step 2: Base64 encode it
echo 'my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }' | base64
# Output: bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K
Step 3: Use in tape
Hide
Type "echo bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K | base64 -d > /tmp/w.sh && source /tmp/w.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Now `my_tool` calls the wrapper — clean output
Type "my_tool deploy"
Enter
Sleep 5s
When to filter
- Git operations: filter "Cloning", "Refreshing", cache messages
- Package managers: filter download progress, cache hits
- Build tools: filter intermediate compilation steps
- Any command with
SSH not configured,timeout: 120s, etc.
Frame-Level Verification
After recording, extract and inspect key frames to verify the GIF shows what you expect.
Extract specific frames
# Frame at position N (0-indexed)
ffmpeg -i demo.gif -vf "select=eq(n\,100)" -frames:v 1 /tmp/frame_100.png -y 2>/dev/null
# Multiple frames at once
for n in 50 200 400; do
ffmpeg -i demo.gif -vf "select=eq(n\,$n)" -frames:v 1 "/tmp/frame_$n.png" -y 2>/dev/null
done
Check total frame count and duration
ffmpeg -i demo.gif 2>&1 | grep -E "Duration|fps"
# Duration: 00:00:10.50, ... 25 fps → 262 frames total
What to verify
| Frame | Check |
|---|---|
| First (~frame 5) | No leaked hidden commands |
| Mid (~frame N/2) | Key output visible, no noise |
| Final (~frame N-10) | All commands completed, result shown |
Claude can read frames
Use the Read tool on extracted PNG files — Claude's vision can verify text content in terminal screenshots.
Post-Processing with gifsicle
Speed up or optimize GIFs after recording, avoiding re-recording.
Speed control
# 2x speed — halve frame delay (most common)
gifsicle -d2 input.gif "#0-" > output.gif
# 1.5x speed
gifsicle -d4 input.gif "#0-" > output.gif
# 3x speed
gifsicle -d1 input.gif "#0-" > output.gif
Optimize file size
# Lossless optimization
gifsicle -O3 input.gif > optimized.gif
# Reduce colors (lossy but smaller)
gifsicle --colors 128 input.gif > smaller.gif
Typical recording script pattern
# Record at normal speed
vhs demo.tape
# Speed up 2x for final output
cp demo.gif /tmp/demo_raw.gif
gifsicle -d2 /tmp/demo_raw.gif "#0-" > demo.gif
rm /tmp/demo_raw.gif
Auto-Detection and Template Rendering
For demos that need to adapt to the environment (e.g., different repo URLs, detected tools).
Template placeholders
Use sed to replace placeholders before recording:
# demo.tape (template)
Type "tool marketplace add REPO_PLACEHOLDER"
Enter
# Build script detects the correct repo
REPO=$(detect_repo)
sed "s|REPO_PLACEHOLDER|$REPO|g" demo.tape > /tmp/rendered.tape
vhs /tmp/rendered.tape
Auto-detect pattern (shell function)
detect_repo() {
local upstream origin
upstream=$(git remote get-url upstream 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true
origin=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true
# Check upstream first (canonical), then origin (fork)
if [[ -n "$upstream" ]] && gh api "repos/$upstream/contents/.target-file" &>/dev/null; then
echo "$upstream"
elif [[ -n "$origin" ]]; then
echo "$origin"
else
echo "fallback/default"
fi
}
Recording Script Structure
A complete recording script follows this pattern:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# 1. Check prerequisites
for cmd in vhs gifsicle; do
command -v "$cmd" &>/dev/null || { echo "Missing: $cmd"; exit 1; }
done
# 2. Auto-detect dynamic values
REPO=$(detect_repo)
echo "Using repo: $REPO"
# 3. Render tape template
sed "s|PLACEHOLDER|$REPO|g" "$SCRIPT_DIR/demo.tape" > /tmp/rendered.tape
# 4. Clean previous state
cleanup_state || true
# 5. Record
(cd "$REPO_DIR" && vhs /tmp/rendered.tape)
# 6. Speed up
cp "$REPO_DIR/demo.gif" /tmp/raw.gif
gifsicle -d2 /tmp/raw.gif "#0-" > "$REPO_DIR/demo.gif"
# 7. Clean up
cleanup_state || true
rm -f /tmp/raw.gif /tmp/rendered.tape
# 8. Report
SIZE=$(ls -lh "$REPO_DIR/demo.gif" | awk '{print $5}')
echo "Done: demo.gif ($SIZE)"