Skip to content

Instantly share code, notes, and snippets.

@jsonmez
Created July 6, 2026 06:07
Show Gist options
  • Select an option

  • Save jsonmez/15e5182b042671c6751195ca019078a2 to your computer and use it in GitHub Desktop.

Select an option

Save jsonmez/15e5182b042671c6751195ca019078a2 to your computer and use it in GitHub Desktop.
vsl-hyperframes — VSL Generator skill (HyperFrames + ElevenLabs) for AI agents
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.

vsl-hyperframes — VSL Generator (HyperFrames + ElevenLabs)

Prerequisites (portable setup — verify before first run)

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

CONFIG (adapt these per system/brand — everything else is universal)

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 convention

Never 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.

Core strategy (why this works regardless of model)

  1. 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.
  2. Audio-first timing. Generate VO per beat FIRST, measure duration with ffprobe, then size each scene to audio_duration + 0.3s pad. Video always conforms to audio, never the reverse. This is what makes cuts land on VO beats.
  3. 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.
  4. Mux per beat, then concat. Never build one giant composition; per-beat mux + ffmpeg concat is robust and parallelizable.
  5. 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.

Directory layout (per project)

<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)

Script template (per beat)

## 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.

Step 1 — VO generation (ElevenLabs)

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) + ffprobe duration > 2s. One retry max.
  • If the read is too fast/slow vs target runtime, prefer regenerating with adjusted text over tempo-stretching; only use atempo within 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.

Step 2 — Scenes (HyperFrames)

Layout system (MANDATORY — prevents element overlap)

Overlapping graphics are the #1 observed failure (AI5x v1: card grid collided with a phone mockup). Prevent structurally:

  1. 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. NO position:absolute for content elements — absolute positioning is allowed only for full-bleed backgrounds and particles that render BEHIND content (z-index below, pointer-events:none).
  2. 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.
  3. Safe margins. 96px minimum from every edge at 1920x1080. Nothing within 140px of the bottom edge (video player chrome overlays it).
  4. 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.
  5. Text must not wrap-collide. Set max-width + overflow:hidden guards on every text block; test with the longest line in the script, not a placeholder.

Overlap audit (MANDATORY QC gate, per beat, before mux)

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.

Visual design (avoid "boxy AI slop")

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 --quiet

Gotchas (all hit in real builds — do not rediscover)

  • render --composition still requires a root index.html in the project dir.
  • Asset paths: scene-relative ./x.css resolves as /x.css and 404s. Use project-root-absolute paths (/scenes/vsl-scene.css).
  • lint/validate only accept dirs containing index.html — use temp .hf-check/beat-N/ dirs with a symlinked index.html AND a symlinked scenes/ dir (absolute /scenes/*.css|js paths 404 during validate otherwise).
  • Lint doesn't follow shared-JS timeline registration; each scene needs an inline timeline registration shim.
  • Fonts: declare local @font-face in each scene to satisfy lint (no network font fetches at render time).
  • Set scene duration = beat audio duration + 0.3s BEFORE rendering.
  • render --quiet does 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.

Step 3 — Mux + concat (ffmpeg)

# 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

Step 4 — QC + delivery

  • 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_URL convention: copy final MP4 + a simple dark HTML page with <video controls>; verify the URL serves 200 before sending the link.

Adapting to a new brand

Only three things change; the pipeline is brand-agnostic:

  1. Script: write a new beat script from the product's positioning (offer, pain, proof points, CTA).
  2. Style contract: palette, fonts, logo assets for the brand.
  3. Voice: correct ElevenLabs VOICE_ID. Everything from Step 1 onward is identical.

Orchestration note

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment