| name | vsl-hyperframes |
|---|---|
| description | Generate a complete Video Sales Letter (VSL) from a script or product brief: cloned-voice narration (ElevenLabs), HTML-rendered motion graphics (HyperFrames), muxed into a final MP4 — fully local rendering, no cloud render credits. Triggers on: "make a VSL", "sales video", "promo video", "launch video" from a script or offer/product description. Proven 2026-07-05 across two brands (RDU, AI5x) and two models (Claude, GPT-5.5), including one hardening cycle for overlap prevention and design quality. |
| Requirement | Check | Install |
|---|---|---|
| Node.js ≥ 22 | node --version |
nodejs.org / brew |
| HyperFrames CLI | hyperframes --version (verified v0.7.36) |
npm install -g hyperframes |
| ffmpeg + ffprobe | ffmpeg -version |
brew install ffmpeg / apt |
| Chrome/Chromium | hyperframes doctor |
any recent Chrome |
| ElevenLabs API key | see CONFIG | elevenlabs.io → API Keys |
| Design skill (recommended) | file exists at DESIGN_SKILL path | git clone https://github.com/pbakaus/impeccable |
| HyperFrames official skills | .agents/skills/hyperframes/ in project |
npx skills add heygen-com/hyperframes --yes |
VOICE_ID="dAGU6uZjRbqW8PdD0q3X" # ElevenLabs voice. This default = John Sonmez's clone.
KEY_CMD="/Users/navi/clawd/scripts/secrets get elevenlabs-api-key" # command that prints the API key.
# On other systems: your secret store, or $ELEVENLABS_API_KEY env var.
DESIGN_SKILL="/Users/navi/clawd/skills/impeccable-uxui/SKILL.md" # impeccable UI/UX skill location
PUBLISH_DIR="/Users/navi/clawd/shared/rich-replies/r/<slug>/" # where finished videos are served from
PUBLISH_URL="https://navi.tail2cf39e.ts.net/rich-replies/r/<slug>/" # public/tailnet URL conventionNever print or log the API key. Never commit it. If a target system has no secret store, require the key via environment variable — do not write it into files.
- Beat structure, not freeform. Every VSL is decomposed into the direct-response skeleton: HOOK → PROBLEM → AGITATE → SOLUTION → PROOF → CTA. Each beat gets: VO text (1-3 sentences), a visual treatment spec, and a palette/typography contract. This is the creative guardrail — the model fills slots, it doesn't invent structure.
- Audio-first timing. Generate VO per beat FIRST, measure duration with ffprobe, then size each scene to
audio_duration + 0.3spad. Video always conforms to audio, never the reverse. This is what makes cuts land on VO beats. - One HTML scene per beat. Each beat is an independent HyperFrames composition (deterministic, seekable CSS/JS animation). Independent scenes = surgical re-renders when one beat needs fixing.
- Mux per beat, then concat. Never build one giant composition; per-beat mux + ffmpeg concat is robust and parallelizable.
- QC gate before delivery. Overlap audit per beat (below) + frame extraction across the final video, duration sanity, per-beat A/V length within 0.2s.
<project>/
script/vsl.md # beat script (see template below)
audio/beat-N.mp3 # ElevenLabs VO per beat
scenes/beat-N.html # HyperFrames composition per beat
scenes/vsl-scene.css # shared styles (reference project-root-absolute!)
renders/beat-N.mp4 # silent rendered scenes
renders/beat-N-av.mp4 # muxed per-beat A/V
final/vsl.mp4 # concat result
final/qc/ # extracted QC frames
index.html # REQUIRED at project root (HyperFrames insists)
## Beat N — <HOOK|PROBLEM|AGITATE|SOLUTION|PROOF|CTA> (mm:ss-mm:ss)
VO: "<1-3 sentences, spoken words only>"
VISUAL: <treatment: kinetic type / split-screen / counter / logo reveal / stat cards / CTA card>
Global style block: palette (bg/accent/highlight hex), typography (heavy condensed sans), 1920x1080 30fps, no scene >12s, pacing note, plus a one-line design-context block (audience, tone, brand personality) — the design pass needs it.
Get key via KEY_CMD (never print it).
curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VOICE_ID" \
-H "xi-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"text":"...","model_id":"eleven_turbo_v2_5","voice_settings":{"stability":0.5,"similarity_boost":0.75}}' \
-o audio/beat-N.mp3- Use the brand-correct
VOICE_ID; clone the voice first if needed. - Validate each file:
file(must be audio, not JSON error) +ffprobeduration > 2s. One retry max. - If the read is too fast/slow vs target runtime, prefer regenerating with adjusted text over tempo-stretching; only use
atempowithin 0.93-1.07 (pitch-preserving) as last resort. - NEVER silently substitute a fallback TTS voice. If the key/voice is unavailable, stop and report.
Overlapping graphics are the #1 observed failure (AI5x v1: card grid collided with a phone mockup). Prevent structurally:
- Grid-only layout. Every scene uses one CSS grid with named areas (
header,focal,support,footer). Every visible element is assigned to exactly one area. NOposition:absolutefor content elements — absolute positioning is allowed only for full-bleed backgrounds and particles that render BEHIND content (z-indexbelow,pointer-events:none). - One focal visual per scene. A scene gets EITHER a device mockup OR a card grid OR a big stat/counter OR kinetic type — never two competing focal elements sharing free space. Supporting elements go in their own grid area with a fixed gap.
- Safe margins. 96px minimum from every edge at 1920x1080. Nothing within 140px of the bottom edge (video player chrome overlays it).
- Animation stays in-cell. Animate opacity/scale (≤1.06)/small translates (≤24px) INSIDE the element's grid cell. Never animate elements across the screen through other elements' cells. Entrance slides must start and end within the cell bounds.
- Text must not wrap-collide. Set
max-width+overflow:hiddenguards on every text block; test with the longest line in the script, not a placeholder.
Snapshot each beat at 25%/50%/75% of its timeline (hyperframes snapshot or ffmpeg frame extraction from the rendered mp4) and CHECK EACH IMAGE for: element collisions, text clipped by containers, elements crossing the safe margin. Additionally run a DOM bounding-box audit: seek the timeline to 0.25/0.5/0.75 and evaluate getBoundingClientRect() intersection between all content elements (siblings in different grid areas must not intersect). Any hit = fix layout, re-render that beat. Do not proceed to concat with a known overlap.
Before writing scene HTML, read the design skill at DESIGN_SKILL (typography/spatial/color sections; source: github.com/pbakaus/impeccable) and apply:
- Typography-first scenes. Most VSL beats are best served by dominant kinetic type with strong scale contrast (display headline 120-220px vs. supporting 28-36px), not diagrams. Use a distinctive display font, not boxed labels.
- Kill the box habit. Do not wrap every label in a bordered rectangle. Max ONE card-styled group per scene; other content is naked type, spacing-separated. Uniform grids of same-size bordered boxes = the boxy look to avoid.
- Asymmetry + rhythm. Vary spacing (tight within groups, generous between); off-center focal compositions beat centered-everything.
- Depth without borders. Prefer soft radial glows, layered gradients, subtle noise, and generous negative space over 1px-stroke outlines. Tint neutrals toward the brand hue; never pure #000/#fff.
- Restrained glow. No neon-outline-on-everything; glow is an accent for ONE element per scene.
Install/reference docs once per project: npx skills add heygen-com/hyperframes --yes (21 official skills; read the composition/animation/render ones before writing HTML).
Working CLI invocations (verified v0.7.36):
npx hyperframes lint .hf-check/beat-N --json
npx hyperframes validate .hf-check/beat-N --json --timeout 10000 --no-contrast
npx hyperframes render . --composition "scenes/beat-N.html" \
--output "renders/beat-N.mp4" --fps 30 --quality high --browser-timeout 120 --quietrender --compositionstill requires a rootindex.htmlin the project dir.- Asset paths: scene-relative
./x.cssresolves as/x.cssand 404s. Use project-root-absolute paths (/scenes/vsl-scene.css). lint/validateonly accept dirs containingindex.html— use temp.hf-check/beat-N/dirs with a symlinkedindex.htmlAND a symlinkedscenes/dir (absolute/scenes/*.css|jspaths 404 during validate otherwise).- Lint doesn't follow shared-JS timeline registration; each scene needs an inline timeline registration shim.
- Fonts: declare local
@font-facein each scene to satisfy lint (no network font fetches at render time). - Set scene duration = beat audio duration + 0.3s BEFORE rendering.
render --quietdoes not actually silence output; redirect stdout if log noise matters.- QC frames catch layout collisions (e.g., headline overlapping a visual) — budget one per-beat fix/re-render pass.
- DOM bounding-box audits via Playwright: point it at system Chrome (
channel: 'chrome'or executablePath) — a fresh Playwright install may lack a bundled browser.
# per beat: trim video to new_dur, or extend last frame if audio ran long
ffmpeg -i renders/beat-N.mp4 -i audio/beat-N.mp3 \
-vf "tpad=stop_mode=clone:stop_duration=<diff>" \ # only if audio > video
-t <new_dur> -c:v libx264 -c:a aac renders/beat-N-av.mp4
# concat
printf "file 'renders/beat-%d-av.mp4'\n" 1 2 3 4 5 6 > final/concat.txt
ffmpeg -f concat -safe 0 -i final/concat.txt -c copy -movflags +faststart final/vsl.mp4- ffprobe final: duration sane, h264+aac streams present.
- Extract 3-8 frames (
ffmpeg -ss <t> -frames:v 1); blank scenes show up as tiny files. Fix + re-render only the offending beat. - Deliver per
PUBLISH_DIR/PUBLISH_URLconvention: copy final MP4 + a simple dark HTML page with<video controls>; verify the URL serves 200 before sending the link.
Only three things change; the pipeline is brand-agnostic:
- Script: write a new beat script from the product's positioning (offer, pain, proof points, CTA).
- Style contract: palette, fonts, logo assets for the brand.
- Voice: correct ElevenLabs
VOICE_ID. Everything from Step 1 onward is identical.
For a full VSL, delegate the build to one subagent with this SKILL.md referenced and a self-contained brief (script path, voice ID, palette, output paths). Parent verifies QC frames + duration before delivery. Do NOT allow fallback TTS voices without flagging it loudly in the report.