HyperFrames Best Practices
Adapted from the upstream HyperFrames skill at heygen-com/hyperframes (Apache-2.0). Hebrew and RTL adaptations by skills-il.
HTML is the source of truth for video. A composition is an HTML file with data-* attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.
Problem
Building HTML-based videos with Hebrew text requires the compiler to fetch Hebrew Google Fonts on demand, explicit dir="rtl" on Hebrew containers, mirrored GSAP entrance directions, and Hebrew caption sync via Whisper, none of which HyperFrames documents out of the box. Hebrew voiceover is a separate gap: the local Kokoro fallback does not support Hebrew (its SUPPORTED_LANGS tuple holds 9 locales: en-us, en-gb, es, fr-fr, hi, it, pt-br, ja, zh), so Hebrew narration must come from a cloud TTS provider, either through the media-use audio path, whose voice resolution order is HeyGen Starfish then ElevenLabs then Kokoro, or by generating the file with any external service and importing it as an <audio> element. The hyperframes tts command itself is local-only and has no provider argument.
Hebrew and RTL
For Hebrew and RTL compositions, load references/hebrew-rtl.md. It covers Hebrew font loading (the compiler auto-fetches from Google Fonts), dir="rtl" scoping, GSAP x-axis mirroring, Hebrew caption sync via hyperframes transcribe --language he, Hebrew voiceover via external TTS, and bidirectional text with <bdi>.
Host Capabilities
Every gate in this skill (npx hyperframes check, render, transcribe, normalize-audio, doctor, and the two bundled Node scripts) is a shell command needing a local Node 22+ and FFmpeg install. Split your expectations by host:
| Host tier |
Hosts |
What you get |
| Shell |
claude-code, cursor, windsurf, github-copilot, opencode, codex |
The whole skill, gates included |
| No shell |
chatgpt, claude-ai, claude-desktop, manus |
Authoring guidance only. You can write a correct composition; you cannot lint, contrast-audit, transcribe or render it |
On a no-shell host, say so up front and hand the user the commands to run themselves. That matters most for Hebrew: the blank-render <html dir> failure below is caught only by check, so on those hosts the rule has to be followed by construction rather than verified.
Approach
Before writing HTML, think at a high level:
- What, what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.
- Structure, how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).
- Timing, which clips drive the duration, where do transitions land, what's the pacing.
- Layout, build the end-state first. See "Layout Before Animation" below.
- Animate, then add motion using the rules below.
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
Visual Identity Gate
Check in this order:
- DESIGN.md exists in the project? → Read it. Use its exact colors, fonts, motion rules, and "What NOT to Do" constraints.
- visual-style.md exists? → Read it. Apply its
style_prompt_full and structured fields. (Note: visual-style.md is a project-specific file. visual-styles.md is the style library with 8 named presets, different files.)
- User named a style (e.g., "Swiss Pulse", "dark and techy", "luxury brand")? → Read visual-styles.md for the 8 named presets. Generate a minimal DESIGN.md with:
## Style Prompt (one paragraph), ## Colors (3-5 hex values with roles), ## Typography (1-2 font families), ## What NOT to Do (3-5 anti-patterns).
- None of the above? → Ask 3 questions before writing any HTML:
- What's the mood? (explosive / cinematic / fluid / technical / chaotic / warm)
- Light or dark canvas?
- Any specific brand colors, fonts, or visual references?
Then generate a minimal DESIGN.md from the answers.
Every composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for #333, #3b82f6, or Roboto, you skipped this step.
For motion defaults, sizing, entrance patterns, and easing, follow house-style.md. The house style handles HOW things move. The DESIGN.md handles WHAT things look like.
Layout Before Animation
Position every element where it should be at its most visible moment, the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.
Why this matters: If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.
The process
- Identify the hero frame for each scene, the moment when the most elements are simultaneously visible. This is the layout you build.
- Write static CSS for that frame. The
.scene-content container MUST fill the full scene using width: 100%; height: 100%; padding: Npx; with display: flex; flex-direction: column; gap: Npx; box-sizing: border-box. Use padding to push content inward, NEVER position: absolute; top: Npx on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve position: absolute for decoratives only.
- Add entrances with
gsap.from(), animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.
- Add exits with
gsap.to(), animate TO offscreen/invisible FROM the CSS position.
Example
/* scene-content fills the scene, padding positions content */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* Container fills any scene size (1920x1080, 1080x1920, etc).
Padding positions content. Flex + gap handles spacing. */
WRONG, hardcoded dimensions and absolute positioning:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// Step 3: Animate INTO those positions
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// Step 4: Animate OUT from those positions
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
When elements share space across time
If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist, but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.
What counts as intentional overlap
Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching unintentional overlap, two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.
Data Attributes
All Clips
| Attribute |
Required |
Values |
id |
Yes |
Unique identifier |
data-start |
Yes |
Seconds or clip ID reference ("el-1", "intro + 2") |
data-duration |
Required for img/div/compositions |
Seconds. Video/audio defaults to media duration. |
data-track-index |
Yes |
Integer. A Studio display lane, not a timing constraint. |
data-media-start |
No |
Trim offset into source (seconds) |
data-volume |
No |
0-1 (default 1) |
data-track-index does not affect visual layering (use CSS z-index) and it does not constrain timing. Upstream's linter says of it and its legacy alias data-layer: "Neither name is read by the render." Two clips on the same track index may overlap in time and the render accepts it, so do not restructure tracks to free a lane.
Composition Clips
| Attribute |
Required |
Values |
data-composition-id |
Yes |
Unique composition ID |
data-start |
Yes |
Start time (root composition: use "0") |
data-duration |
Yes |
Takes precedence over GSAP timeline duration |
data-width / data-height |
Yes |
Pixel dimensions (1920x1080 or 1080x1920) |
data-composition-src |
No |
Path to external HTML file |
Composition Structure
Sub-compositions loaded via data-composition-src use a <template> wrapper. Standalone compositions (the main index.html) do NOT use <template>, they put the data-composition-id div directly in <body>. Using <template> on a standalone file hides all content from the browser and breaks rendering.
Sub-composition structure:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
<style>
[data-composition-id="my-comp"] {
/* scoped styles */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tweens...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
Load in root: <div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
Video and Audio
Video must be muted playsinline. Audio is always a separate <audio> element:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
Timeline Contract
- All timelines start
{ paused: true }, the player controls playback
- Register every timeline:
window.__timelines["<composition-id>"] = tl
- Framework auto-nests sub-timelines, do NOT manually add them
- Duration comes from
data-duration, not from GSAP timeline length
- Never create empty tweens to set duration
Rules (Non-Negotiable)
Deterministic: No Math.random(), Date.now(), or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).
GSAP: Only animate visual properties (opacity, x, y, scale, rotation, color, backgroundColor, borderRadius, transforms). Do NOT animate visibility, display, or call video.play()/audio.play().
Animation conflicts: Never animate the same property on the same element from multiple timelines simultaneously.
No repeat: -1: Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration with Math.floor, never Math.ceil: repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1). Upstream lints the Math.ceil(...) - 1 form as gsap_repeat_ceil_overshoot, because ceil runs one cycle past the end and the last cycle is cut mid-motion.
Synchronous timeline construction: Never build timelines inside async/await, setTimeout, or Promises. The capture engine reads window.__timelines synchronously after page load. Fonts are embedded by the compiler, so they're available immediately, no need to wait for font loading.
Never do:
- Forget
window.__timelines registration
- Use video for audio, always muted video + separate
<audio>
- Nest video inside a timed div, use a non-timed wrapper
- Use
data-layer (use data-track-index) or data-end (use data-duration)
- Animate video element dimensions, animate a wrapper div
- Call play/pause/seek on media, framework owns playback
- Create a top-level container without
data-composition-id
- Use
repeat: -1 on any timeline or tween, always finite repeats
- Build timelines asynchronously (inside
async, setTimeout, Promise)
- Use
gsap.set() on clip elements from later scenes, they don't exist in the DOM at page load. Use tl.set(selector, vars, timePosition) inside the timeline at or after the clip's data-start time instead.
- Use
<br> in content text, forced line breaks don't account for actual rendered font width. Text that wraps naturally + a <br> produces an extra unwanted break, causing overlap. Let text wrap via max-width instead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\nIMMORTAL\nGAME" at 130px).
Scene Transitions (Non-Negotiable)
Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.
- ALWAYS use transitions between scenes. No jump cuts. No exceptions.
- ALWAYS use entrance animations on every scene. Every element animates IN via
gsap.from(). No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.
- NEVER use exit animations except on the final scene. This means: NO
gsap.to() that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.
- Final scene only: The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where
gsap.to(..., { opacity: 0 }) is allowed.
WRONG, exit animation before transition:
// BANNED, this empties the scene before the transition can use it
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// transition fires on empty frame
RIGHT, entrance only, transition handles exit:
// Scene 1 entrance animations
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// NO exit tweens, transition at 7.2s handles the scene change
// Scene 2 entrance animations
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
Animation Guardrails
- Offset first animation 0.1-0.3s (not t=0)
- Vary eases across entrance tweens, use at least 3 different eases per scene
- Don't repeat an entrance pattern within a scene
- Avoid full-screen linear gradients on dark backgrounds (H.264 banding, use radial or solid + localized glow)
- 60px+ headlines, 20px+ body, 16px+ data labels for rendered video
font-variant-numeric: tabular-nums on number columns
When no visual-style.md or animation direction is provided, follow house-style.md for aesthetic defaults.
Typography and Assets
- Fonts: Just write the
font-family you want in CSS, the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.
- Add
crossorigin="anonymous" to external media
- For dynamic text overflow, use
window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }). It returns an object { fontSize, fits }, not a number, so read .fontSize. The full option set is maxWidth, baseFontSize, minFontSize, fontWeight, fontFamily, step
- All files live at the project root alongside
index.html; sub-compositions use ../
Editing Existing Compositions
- Read the full composition first, match existing fonts, colors, animation patterns
- Only change what was requested
- Preserve timing of unrelated clips
Output Checklist
Quality Checks
Contrast
hyperframes check runs a WCAG contrast audit by default (--contrast defaults to true, so --no-contrast still turns it off). It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:
⚠ WCAG AA contrast warnings (3):
· .subtitle "secondary text", 2.67:1 (need 4.5:1, t=5.3s)
If warnings appear:
- On dark backgrounds: brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text, 24px+ or 19px+ bold)
- On light backgrounds: darken it
- Stay within the palette family, don't invent a new color, adjust the existing one
- Re-run
hyperframes check until clean
Use --no-contrast to skip if iterating rapidly and you'll check later, and --snapshots to persist the five audited frames as PNGs under snapshots/.
Animation Map
After authoring animations, run the animation map to verify choreography:
# Copy the script into the project first, see Bundled Resources below.
node scripts/animation-map.mjs . --out .hyperframes/anim-map
Outputs a single animation-map.json with:
- Per-tween summaries:
"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)"
- ASCII timeline: Gantt chart of all tweens across the composition duration
- Stagger detection: reports actual intervals (
"3 elements stagger at 120ms")
- Dead zones: periods over 1s with no animation, intentional hold or missing entrance?
- Element lifecycles: first/last animation time, final visibility
- Scene snapshots: visible element state at 5 key timestamps
- Flags:
offscreen, collision, invisible, paced-fast (under 0.2s), paced-slow (over 2s)
Read the JSON. Scan summaries for anything unexpected. Check every flag, fix or justify. Verify the timeline shows the intended choreography rhythm. Re-run after fixes.
Skip on small edits (fixing a color, adjusting one duration). Run on new compositions and significant animation changes.
References (loaded on demand)
references/captions.md, Captions, subtitles, lyrics, karaoke synced to audio. Tone-adaptive style detection, per-word styling, text overflow prevention, caption exit guarantees, word grouping. Read when adding any text synced to audio timing.
references/tts.md, Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.
references/audio-reactive.md, Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.
references/css-patterns.md, CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.
references/typography.md, Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. Always read, every composition has text.
references/motion-principles.md, Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.
visual-styles.md, 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.
house-style.md, Default motion, sizing, and color palettes when no style is specified.
patterns.md, PiP, title cards, slide show patterns.
data-in-motion.md, Data, stats, and infographic patterns.
references/transcript-guide.md, Transcription commands, whisper models, external APIs, troubleshooting.
references/dynamic-techniques.md, Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).
references/hebrew-rtl.md, Hebrew and RTL compositions: dir="rtl" scoping, Google Fonts auto-fetch for Heebo/Rubik/Assistant, GSAP x-axis mirroring, Hebrew captions via hyperframes transcribe --language he, Hebrew voiceover via external TTS, bidirectional text with <bdi>. Read for any composition with Hebrew text.
references/transitions.md, Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, CSS vs WebGL guidance. Always read for multi-scene compositions, scenes without transitions feel like jump cuts.
- transitions/catalog.md, Hard rules, scene template, and routing to per-type implementation code.
- Shader transitions are in
@hyperframes/shader-transitions (packages/shader-transitions/), read package source, not skill files.
For GSAP timeline patterns and easing, follow house-style.md and references/motion-principles.md in this skill, plus the official GSAP docs at https://gsap.com/docs/v3/.
Bundled Scripts
Two Node scripts ship with this skill under scripts/. Both import @hyperframes/producer, and Node resolves that import relative to the script file, not to your working directory. Running them from the skill folder fails with ERR_MODULE_NOT_FOUND no matter what directory you pass as the argument. Copy them into your HyperFrames project first, next to its node_modules, then run them from the project root:
mkdir -p scripts && cp <skill-dir>/scripts/*.mjs scripts/
# Portrait (TikTok / Reels). BOTH scripts default to a 1920x1080 viewport,
# so a portrait composition MUST be given --width/--height or every flag,
# bbox and contrast sample is computed against a frame you will never render.
node scripts/animation-map.mjs . --width 1080 --height 1920 --out .hyperframes/anim-map
node scripts/contrast-report.mjs . --width 1080 --height 1920 --samples 10 --out .hyperframes/contrast
Both also accept --fps (default 30). Match it to your render, and set data-fps on the composition root if you are not rendering at 30.
| Script |
What it does |
scripts/animation-map.mjs |
Enumerates every tween in window.__timelines, samples bounding boxes, and emits animation-map.json with per-tween summaries, an ASCII timeline, stagger detection, dead zones and flags. |
scripts/contrast-report.mjs |
Standalone WCAG audit. Emits contrast-report.json plus a contrast-overlay.png sprite grid (magenta fails AA, yellow passes AA only, green passes AAA) and exits 1 if any element fails AA. Useful when you want the overlay image, which hyperframes check does not produce. |
Both require Node 22+ (the producer package declares "engines": { "node": ">=22" }).
Gotchas
These are agent failure modes specific to Hebrew/RTL HyperFrames work. Generic HyperFrames gotchas (see upstream) still apply.
- Don't add a Google Fonts
<link rel="stylesheet"> tag or a CSS @import url(...) statement for Hebrew fonts. The compiler already fetches Google Fonts server-side via fetchGoogleFont() in packages/producer/src/services/deterministicFonts.ts, caches the WOFF2s at ~/.cache/hyperframes/fonts/<slug>/, and embeds them as base64 data URIs in the compiled HTML. An external stylesheet breaks determinism (network dependency at render time) and duplicates the font loading. Just write font-family: 'Heebo', sans-serif;.
- Don't reach for the built-in
hyperframes tts command for Hebrew narration. It is local-only, described upstream as "Generate speech audio from text using a local AI model (Kokoro-82M)", and its arguments are exactly input, text-file, output, voice, speed, lang, list, json. There is no provider argument, so no environment variable can make this command speak Hebrew. Kokoro maps 9 locales via voice-ID prefix, a=American English, b=British English, e=Spanish, f=French, h=Hindi, i=Italian, j=Japanese, p=Brazilian Portuguese, z=Mandarin. Hebrew is not among them. Two paths that do work: (a) the media-use audio path, whose voice resolution order is HeyGen Starfish, then ElevenLabs (needs ELEVENLABS_API_KEY and the elevenlabs Python module importable), then local Kokoro. Run hyperframes doctor to see which engine your machine would actually pick. (b) Generate the WAV/MP3 with any external service (ElevenLabs, OpenAI TTS, Google Cloud TTS Hebrew) and drop the file into the composition as a normal <audio> clip. Path (b) is the one that works with no account and no Python deps.
- Don't use
.en Whisper models on Hebrew audio. .en variants TRANSLATE non-English audio to English instead of transcribing it. For Hebrew captions use npx hyperframes transcribe audio.wav --model medium --language he (small is weak for Hebrew ASR; step up to large-v3 --language he for noisy audio). references/captions.md is upstream text and prescribes a flat --model small; for Hebrew that rule is superseded by this one, because per-word effects (karaoke, slam, marker sweep) key off word boundaries that small gets wrong. The .en suffix is only correct when the user explicitly says the audio is English. Upstream also added an --engine flag (auto, parakeet, whisper) that prefers Parakeet when it is installed; pass --engine whisper if you need the Whisper multilingual behaviour described here.
- Don't forget
dir="rtl" on Hebrew text containers, even inside a RTL-defaulted composition. HyperFrames sub-compositions set their own direction context. GSAP x: tweens also don't auto-mirror. A title that uses gsap.from({x: -80}) enters from the left in both LTR and RTL, for Hebrew, flip to x: 80 so it enters from the right, matching reading direction.
- Don't "fix" a Hebrew entrance because
animation-map.mjs says it moves the wrong way. The map describes motion from screen-space bounding-box deltas, with no notion of writing direction. In an RTL composition a correct Hebrew entrance, gsap.from(".subtitle", { x: 80 }) so the text flies in from the right, is reported as moves 51px left (measured, 1080x1920 RTL composition). That prose is accurate about pixels and misleading about intent. Read the summaries for choreography and flags, not for direction, and never flip a tween's sign to make the wording read "right".
- Hebrew narration over a music bed now has a first-class API, and a lint rule that can fail you. Upstream added audio groups (
data-audio-group on the member clip) with a summed FX bus (data-fx-chain), plus a lint rule that validates group membership and timing. If you are layering an externally-generated Hebrew voiceover over music, put both in a group rather than hand-balancing data-volume, and use npx hyperframes normalize-audio to match the narration's integrated LUFS to the bed instead of guessing. data-audio-group on a <video> element is ignored, it is audio-clip-only.
- Don't paste English brand names into Hebrew paragraphs without
<bdi> or unicode-bidi: isolate. Without isolation, the Unicode bidi algorithm reorders mixed-direction runs and can place punctuation on the wrong side of the brand name or visually reverse it. Wrap brand names: הצטרפו ל־<bdi>HyperFrames</bdi> עכשיו.
Hebrew Bidi Details
The <bdi> rule above covers brand names. Mixed-direction Hebrew compositions need three more bidi habits:
- Hebrew line-breaking for long headlines. Hebrew does not hyphenate. A long Hebrew headline that overflows must wrap at word boundaries, never mid-word. Set
max-width so it wraps naturally, and add word-break: keep-all (or overflow-wrap: normal) so the compiler does not break inside a Hebrew word. Do NOT use <br> to force breaks (see Rule 11). For deliberate one-word-per-line display titles, give each word its own element instead.
- Digits with adjacent symbols inside RTL runs. A bare digit run is handled correctly by the Unicode bidi algorithm:
2025 next to a Hebrew word stays left-to-right inside an RTL paragraph on its own, it does NOT reverse to 5202, so a plain integer needs no wrapper. The real hazards are a digit touching a symbol, a range, or a Latin token: a percent sign, currency sign, or range dash can detach and land on the wrong side. Wrap those with <bdi> or ... (LTR isolate): <bdi>15%</bdi> הנחה, <bdi>₪199</bdi>, <bdi>10-20</bdi>. This keeps the symbol attached to its number. See references/hebrew-rtl.md.
- Punctuation mirroring. Parentheses, brackets, and quotes are mirrored characters:
( visually becomes ) in an RTL run. In Hebrew captions and paragraphs let the browser mirror them by keeping the text in a proper dir="rtl" container. Do NOT hand-swap ( and ) to "fix" it, and when a parenthetical contains LTR content (a brand, a URL, a number) wrap that inner content in <bdi> so only the inner run is LTR while the parentheses stay correctly mirrored.
Troubleshooting
hyperframes command not found / render fails immediately
HyperFrames requires Node 22+ and FFmpeg on PATH. Confirm node --version is 22 or higher and ffmpeg -version resolves. On macOS install FFmpeg with brew install ffmpeg; on Debian/Ubuntu use apt install ffmpeg. Without FFmpeg the compiler cannot encode the MP4 and aborts before rendering any frames.
Compiler warns "font not supported" or Hebrew renders in a fallback font
The compiler only embeds fonts it can fetch from Google Fonts. Use a Hebrew family that exists on Google Fonts (Heebo, Rubik, Assistant, Alef, Frank Ruhl Libre, Noto Sans Hebrew) and write it plainly in CSS: font-family: 'Heebo', sans-serif;. Do NOT add a <link rel="stylesheet"> or @import (see Gotchas), that breaks determinism without fixing the warning. If a custom non-Google font is required, the upstream docs cover local font embedding.
WCAG contrast audit fails (hyperframes check)
check samples background pixels behind each text element at 5 timestamps and flags ratios under 4.5:1 (normal text) or 3:1 (large text). Fix by adjusting the failing color WITHIN the palette family: brighten it on dark backgrounds, darken it on light backgrounds. Do not invent a new color. Re-run hyperframes check until clean. Use --no-contrast only while iterating, never as the final state.
Composition renders blank or content is invisible
If the composition is Hebrew and it previews fine but the MP4 is black, check <html> for a dir attribute first. <html dir="rtl"> or dir="auto" produces a blank render while preview and snapshot look correct; remove it, keep lang, and scope dir="rtl" to the text-bearing elements instead. npx hyperframes check reports this as html_dir_attribute_breaks_render at severity error. Otherwise, the most common cause is a <template> wrapper on a standalone composition. The main index.html must put the data-composition-id div directly in <body>, not inside <template>. Also check that every timeline is registered via window.__timelines["<composition-id>"] = tl and built synchronously (not inside async, setTimeout, or a Promise), the capture engine reads window.__timelines synchronously after page load.
Hebrew title enters from the wrong side
GSAP x: tweens do not auto-mirror for RTL. A gsap.from({x: -80}) enters from the left in both LTR and RTL. For Hebrew, flip to a positive value (x: 80) so the element enters from the right, matching reading direction. See references/hebrew-rtl.md.
Reference Links
| Source |
URL |
What to Check |
| HyperFrames GitHub |
https://github.com/heygen-com/hyperframes |
Upstream repo, issues, releases |
| HyperFrames docs |
https://hyperframes.heygen.com/quickstart |
CLI, Node 22+, FFmpeg requirement |
| Compiler font logic |
https://github.com/heygen-com/hyperframes/blob/main/packages/producer/src/services/deterministicFonts.ts |
Canonical font list, Google Fonts fallback, cache path |
| Kokoro TTS voices |
https://github.com/heygen-com/hyperframes/blob/main/skills/media-use/audio/references/tts.md |
Kokoro voice prefixes across 9 locales (no Hebrew) |
| Whisper model guide |
https://github.com/skills-il/developer-tools/blob/master/hyperframes-best-practices/references/transcript-guide.md |
.en vs multilingual models, --language flag |
| Google Fonts Hebrew |
https://fonts.google.com/?subset=hebrew |
Heebo, Rubik, Assistant, Alef, Frank Ruhl Libre, Noto Sans Hebrew |
| Unicode bidi spec |
https://developer.mozilla.org/en-US/docs/Web/CSS/unicode-bidi |
isolate, <bdi>, mixed-direction text |
1---2name: hyperframes-best-practices3description: Best practices for programmatic video creation using HyperFrames, plain HTML compositions with GSAP animations rendered to MP4, with full Hebrew and RTL support. Covers composition authoring, data-* timing attributes, GSAP timeline contract, layout-before-animation methodology, visual identity gate, Hebrew fonts via Google Fonts (Heebo, Rubik, Assistant), RTL text rendering with dir="rtl", Hebrew TikTok/Reels-style captions via Whisper, audio-reactive visuals, scene transitions, and bidirectional Hebrew+English text. Use when building HTML-based video content or Hebrew social/marketing videos without React. Do NOT use for Remotion or general React video work, use remotion-best-practices for that.4license: Apache-2.05---67# HyperFrames Best Practices89> Adapted from the upstream HyperFrames skill at [heygen-com/hyperframes](https://github.com/heygen-com/hyperframes) (Apache-2.0). Hebrew and RTL adaptations by [skills-il](https://agentskills.co.il).1011HTML is the source of truth for video. A composition is an HTML file with `data-*` attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.1213## Problem1415Building HTML-based videos with Hebrew text requires the compiler to fetch Hebrew Google Fonts on demand, explicit `dir="rtl"` on Hebrew containers, mirrored GSAP entrance directions, and Hebrew caption sync via Whisper, none of which HyperFrames documents out of the box. Hebrew voiceover is a separate gap: the local Kokoro fallback does not support Hebrew (its `SUPPORTED_LANGS` tuple holds 9 locales: en-us, en-gb, es, fr-fr, hi, it, pt-br, ja, zh), so Hebrew narration must come from a cloud TTS provider, either through the media-use audio path, whose voice resolution order is HeyGen Starfish then ElevenLabs then Kokoro, or by generating the file with any external service and importing it as an `<audio>` element. The `hyperframes tts` command itself is local-only and has no provider argument.1617## Hebrew and RTL1819<HARD-GATE>20**Never put `dir="rtl"` (or `dir="auto"`) on the `<html>` element.** It previews correctly, snapshots correctly, and then renders a fully blank black MP4. The only tell is an output file far smaller than expected. Upstream ships this as a severity-`error` lint rule, `html_dir_attribute_breaks_render`, with the note "a confirmed, silent failure". Keep `lang="he"` on `<html>`, and scope direction to the elements that hold text: `dir="rtl"` on the composition root div, on Hebrew text containers, and on caption word spans. Text still shapes correctly, because the browser's bidi algorithm runs off the element's own direction. This is the single highest-cost mistake available in a Hebrew composition, and it is reachable only from Hebrew.21</HARD-GATE>2223For Hebrew and RTL compositions, load [references/hebrew-rtl.md](./references/hebrew-rtl.md). It covers Hebrew font loading (the compiler auto-fetches from Google Fonts), `dir="rtl"` scoping, GSAP x-axis mirroring, Hebrew caption sync via `hyperframes transcribe --language he`, Hebrew voiceover via external TTS, and bidirectional text with `<bdi>`.2425## Host Capabilities2627Every gate in this skill (`npx hyperframes check`, `render`, `transcribe`, `normalize-audio`, `doctor`, and the two bundled Node scripts) is a shell command needing a local Node 22+ and FFmpeg install. Split your expectations by host:2829| Host tier | Hosts | What you get |30|---|---|---|31| Shell | claude-code, cursor, windsurf, github-copilot, opencode, codex | The whole skill, gates included |32| No shell | chatgpt, claude-ai, claude-desktop, manus | Authoring guidance only. You can write a correct composition; you cannot lint, contrast-audit, transcribe or render it |3334On a no-shell host, say so up front and hand the user the commands to run themselves. That matters most for Hebrew: the blank-render `<html dir>` failure below is caught only by `check`, so on those hosts the rule has to be followed by construction rather than verified.3536## Approach3738Before writing HTML, think at a high level:39401. **What**, what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.412. **Structure**, how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).423. **Timing**, which clips drive the duration, where do transitions land, what's the pacing.434. **Layout**, build the end-state first. See "Layout Before Animation" below.445. **Animate**, then add motion using the rules below.4546For small edits (fix a color, adjust timing, add one element), skip straight to the rules.4748### Visual Identity Gate4950<HARD-GATE>51Before writing ANY composition HTML, you MUST have a visual identity defined. Do NOT write compositions with default or generic colors.5253Check in this order:54551. **DESIGN.md exists in the project?** → Read it. Use its exact colors, fonts, motion rules, and "What NOT to Do" constraints.562. **visual-style.md exists?** → Read it. Apply its `style_prompt_full` and structured fields. (Note: `visual-style.md` is a project-specific file. `visual-styles.md` is the style library with 8 named presets, different files.)573. **User named a style** (e.g., "Swiss Pulse", "dark and techy", "luxury brand")? → Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Generate a minimal DESIGN.md with: `## Style Prompt` (one paragraph), `## Colors` (3-5 hex values with roles), `## Typography` (1-2 font families), `## What NOT to Do` (3-5 anti-patterns).584. **None of the above?** → Ask 3 questions before writing any HTML:59 - What's the mood? (explosive / cinematic / fluid / technical / chaotic / warm)60 - Light or dark canvas?61 - Any specific brand colors, fonts, or visual references?62 Then generate a minimal DESIGN.md from the answers.6364Every composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for `#333`, `#3b82f6`, or `Roboto`, you skipped this step.65</HARD-GATE>6667For motion defaults, sizing, entrance patterns, and easing, follow [house-style.md](./house-style.md). The house style handles HOW things move. The DESIGN.md handles WHAT things look like.6869## Layout Before Animation7071Position every element where it should be at its **most visible moment**, the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.7273**Why this matters:** If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.7475### The process76771. **Identify the hero frame** for each scene, the moment when the most elements are simultaneously visible. This is the layout you build.782. **Write static CSS** for that frame. The `.scene-content` container MUST fill the full scene using `width: 100%; height: 100%; padding: Npx;` with `display: flex; flex-direction: column; gap: Npx; box-sizing: border-box`. Use padding to push content inward, NEVER `position: absolute; top: Npx` on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve `position: absolute` for decoratives only.793. **Add entrances with `gsap.from()`**, animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.804. **Add exits with `gsap.to()`**, animate TO offscreen/invisible FROM the CSS position.8182### Example8384```css85/* scene-content fills the scene, padding positions content */86.scene-content {87 display: flex;88 flex-direction: column;89 justify-content: center;90 width: 100%;91 height: 100%;92 padding: 120px 160px;93 gap: 24px;94 box-sizing: border-box;95}96.title {97 font-size: 120px;98}99.subtitle {100 font-size: 42px;101}102/* Container fills any scene size (1920x1080, 1080x1920, etc).103 Padding positions content. Flex + gap handles spacing. */104```105106**WRONG, hardcoded dimensions and absolute positioning:**107108```css109.scene-content {110 position: absolute;111 top: 200px;112 left: 160px;113 width: 1920px;114 height: 1080px;115 display: flex; /* ... */116}117```118119```js120// Step 3: Animate INTO those positions121tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);122tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);123tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);124125// Step 4: Animate OUT from those positions126tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);127tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);128tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);129```130131### When elements share space across time132133If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist, but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.134135### What counts as intentional overlap136137Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching **unintentional** overlap, two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.138139## Data Attributes140141### All Clips142143| Attribute | Required | Values |144| ------------------ | --------------------------------- | ------------------------------------------------------ |145| `id` | Yes | Unique identifier |146| `data-start` | Yes | Seconds or clip ID reference (`"el-1"`, `"intro + 2"`) |147| `data-duration` | Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |148| `data-track-index` | Yes | Integer. A Studio display lane, not a timing constraint. |149| `data-media-start` | No | Trim offset into source (seconds) |150| `data-volume` | No | 0-1 (default 1) |151152`data-track-index` does **not** affect visual layering (use CSS `z-index`) and it does **not** constrain timing. Upstream's linter says of it and its legacy alias `data-layer`: "Neither name is read by the render." Two clips on the same track index may overlap in time and the render accepts it, so do not restructure tracks to free a lane.153154### Composition Clips155156| Attribute | Required | Values |157| ---------------------------- | -------- | -------------------------------------------- |158| `data-composition-id` | Yes | Unique composition ID |159| `data-start` | Yes | Start time (root composition: use `"0"`) |160| `data-duration` | Yes | Takes precedence over GSAP timeline duration |161| `data-width` / `data-height` | Yes | Pixel dimensions (1920x1080 or 1080x1920) |162| `data-composition-src` | No | Path to external HTML file |163164## Composition Structure165166Sub-compositions loaded via `data-composition-src` use a `<template>` wrapper. **Standalone compositions (the main index.html) do NOT use `<template>`**, they put the `data-composition-id` div directly in `<body>`. Using `<template>` on a standalone file hides all content from the browser and breaks rendering.167168Sub-composition structure:169170```html171<template id="my-comp-template">172 <div data-composition-id="my-comp" data-width="1920" data-height="1080">173 <!-- content -->174 <style>175 [data-composition-id="my-comp"] {176 /* scoped styles */177 }178 </style>179 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>180 <script>181 window.__timelines = window.__timelines || {};182 const tl = gsap.timeline({ paused: true });183 // tweens...184 window.__timelines["my-comp"] = tl;185 </script>186 </div>187</template>188```189190Load in root: `<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>`191192## Video and Audio193194Video must be `muted playsinline`. Audio is always a separate `<audio>` element:195196```html197<video198 id="el-v"199 data-start="0"200 data-duration="30"201 data-track-index="0"202 src="video.mp4"203 muted204 playsinline205></video>206<audio207 id="el-a"208 data-start="0"209 data-duration="30"210 data-track-index="2"211 src="video.mp4"212 data-volume="1"213></audio>214```215216## Timeline Contract217218- All timelines start `{ paused: true }`, the player controls playback219- Register every timeline: `window.__timelines["<composition-id>"] = tl`220- Framework auto-nests sub-timelines, do NOT manually add them221- Duration comes from `data-duration`, not from GSAP timeline length222- Never create empty tweens to set duration223224## Rules (Non-Negotiable)225226**Deterministic:** No `Math.random()`, `Date.now()`, or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).227228**GSAP:** Only animate visual properties (`opacity`, `x`, `y`, `scale`, `rotation`, `color`, `backgroundColor`, `borderRadius`, transforms). Do NOT animate `visibility`, `display`, or call `video.play()`/`audio.play()`.229230**Animation conflicts:** Never animate the same property on the same element from multiple timelines simultaneously.231232**No `repeat: -1`:** Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration with `Math.floor`, never `Math.ceil`: `repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1)`. Upstream lints the `Math.ceil(...) - 1` form as `gsap_repeat_ceil_overshoot`, because ceil runs one cycle past the end and the last cycle is cut mid-motion.233234**Synchronous timeline construction:** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. Fonts are embedded by the compiler, so they're available immediately, no need to wait for font loading.235236**Never do:**2372381. Forget `window.__timelines` registration2392. Use video for audio, always muted video + separate `<audio>`2403. Nest video inside a timed div, use a non-timed wrapper2414. Use `data-layer` (use `data-track-index`) or `data-end` (use `data-duration`)2425. Animate video element dimensions, animate a wrapper div2436. Call play/pause/seek on media, framework owns playback2447. Create a top-level container without `data-composition-id`2458. Use `repeat: -1` on any timeline or tween, always finite repeats2469. Build timelines asynchronously (inside `async`, `setTimeout`, `Promise`)24710. Use `gsap.set()` on clip elements from later scenes, they don't exist in the DOM at page load. Use `tl.set(selector, vars, timePosition)` inside the timeline at or after the clip's `data-start` time instead.24811. Use `<br>` in content text, forced line breaks don't account for actual rendered font width. Text that wraps naturally + a `<br>` produces an extra unwanted break, causing overlap. Let text wrap via `max-width` instead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\nIMMORTAL\nGAME" at 130px).249250## Scene Transitions (Non-Negotiable)251252Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.2532541. **ALWAYS use transitions between scenes.** No jump cuts. No exceptions.2552. **ALWAYS use entrance animations on every scene.** Every element animates IN via `gsap.from()`. No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.2563. **NEVER use exit animations** except on the final scene. This means: NO `gsap.to()` that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.2574. **Final scene only:** The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where `gsap.to(..., { opacity: 0 })` is allowed.258259**WRONG, exit animation before transition:**260261```js262// BANNED, this empties the scene before the transition can use it263tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);264tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);265// transition fires on empty frame266```267268**RIGHT, entrance only, transition handles exit:**269270```js271// Scene 1 entrance animations272tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);273tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);274// NO exit tweens, transition at 7.2s handles the scene change275// Scene 2 entrance animations276tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);277```278279## Animation Guardrails280281- Offset first animation 0.1-0.3s (not t=0)282- Vary eases across entrance tweens, use at least 3 different eases per scene283- Don't repeat an entrance pattern within a scene284- Avoid full-screen linear gradients on dark backgrounds (H.264 banding, use radial or solid + localized glow)285- 60px+ headlines, 20px+ body, 16px+ data labels for rendered video286- `font-variant-numeric: tabular-nums` on number columns287288When no `visual-style.md` or animation direction is provided, follow [house-style.md](./house-style.md) for aesthetic defaults.289290## Typography and Assets291292- **Fonts:** Just write the `font-family` you want in CSS, the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.293- Add `crossorigin="anonymous"` to external media294- For dynamic text overflow, use `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })`. It returns an object `{ fontSize, fits }`, not a number, so read `.fontSize`. The full option set is `maxWidth, baseFontSize, minFontSize, fontWeight, fontFamily, step`295- All files live at the project root alongside `index.html`; sub-compositions use `../`296297## Editing Existing Compositions298299- Read the full composition first, match existing fonts, colors, animation patterns300- Only change what was requested301- Preserve timing of unrelated clips302303## Output Checklist304305- [ ] `npx hyperframes check` passes with 0 findings (it runs lint, runtime, layout, motion and the WCAG contrast pass in one browser session). `npx hyperframes validate` is deprecated upstream in favour of `check`; `lint` is not306- [ ] Contrast warnings addressed (see Quality Checks below)307- [ ] Animation choreography verified (see Quality Checks below)308- [ ] Rendered to MP4 with `npx hyperframes render` (lint and validate run on the HTML; `render` is the step that actually encodes the video, see `/hyperframes-cli` upstream for flags)309310## Quality Checks311312### Contrast313314`hyperframes check` runs a WCAG contrast audit by default (`--contrast` defaults to true, so `--no-contrast` still turns it off). It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:315316```317⚠ WCAG AA contrast warnings (3):318 · .subtitle "secondary text", 2.67:1 (need 4.5:1, t=5.3s)319```320321If warnings appear:322323- On dark backgrounds: brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text, 24px+ or 19px+ bold)324- On light backgrounds: darken it325- Stay within the palette family, don't invent a new color, adjust the existing one326- Re-run `hyperframes check` until clean327328Use `--no-contrast` to skip if iterating rapidly and you'll check later, and `--snapshots` to persist the five audited frames as PNGs under `snapshots/`.329330### Animation Map331332After authoring animations, run the animation map to verify choreography:333334```bash335# Copy the script into the project first, see Bundled Resources below.336node scripts/animation-map.mjs . --out .hyperframes/anim-map337```338339Outputs a single `animation-map.json` with:340341- **Per-tween summaries**: `"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)"`342- **ASCII timeline**: Gantt chart of all tweens across the composition duration343- **Stagger detection**: reports actual intervals (`"3 elements stagger at 120ms"`)344- **Dead zones**: periods over 1s with no animation, intentional hold or missing entrance?345- **Element lifecycles**: first/last animation time, final visibility346- **Scene snapshots**: visible element state at 5 key timestamps347- **Flags**: `offscreen`, `collision`, `invisible`, `paced-fast` (under 0.2s), `paced-slow` (over 2s)348349Read the JSON. Scan summaries for anything unexpected. Check every flag, fix or justify. Verify the timeline shows the intended choreography rhythm. Re-run after fixes.350351Skip on small edits (fixing a color, adjusting one duration). Run on new compositions and significant animation changes.352353---354355## References (loaded on demand)356357- **[references/captions.md](references/captions.md)**, Captions, subtitles, lyrics, karaoke synced to audio. Tone-adaptive style detection, per-word styling, text overflow prevention, caption exit guarantees, word grouping. Read when adding any text synced to audio timing.358- **[references/tts.md](references/tts.md)**, Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.359- **[references/audio-reactive.md](references/audio-reactive.md)**, Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.360- **[references/css-patterns.md](references/css-patterns.md)**, CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.361- **[references/typography.md](references/typography.md)**, Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. **Always read**, every composition has text.362- **[references/motion-principles.md](references/motion-principles.md)**, Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.363- **[visual-styles.md](visual-styles.md)**, 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.364- **[house-style.md](house-style.md)**, Default motion, sizing, and color palettes when no style is specified.365- **[patterns.md](patterns.md)**, PiP, title cards, slide show patterns.366- **[data-in-motion.md](data-in-motion.md)**, Data, stats, and infographic patterns.367- **[references/transcript-guide.md](references/transcript-guide.md)**, Transcription commands, whisper models, external APIs, troubleshooting.368- **[references/dynamic-techniques.md](references/dynamic-techniques.md)**, Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).369- **[references/hebrew-rtl.md](references/hebrew-rtl.md)**, Hebrew and RTL compositions: `dir="rtl"` scoping, Google Fonts auto-fetch for Heebo/Rubik/Assistant, GSAP x-axis mirroring, Hebrew captions via `hyperframes transcribe --language he`, Hebrew voiceover via external TTS, bidirectional text with `<bdi>`. Read for any composition with Hebrew text.370371- **[references/transitions.md](references/transitions.md)**, Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, CSS vs WebGL guidance. **Always read for multi-scene compositions**, scenes without transitions feel like jump cuts.372 - [transitions/catalog.md](references/transitions/catalog.md), Hard rules, scene template, and routing to per-type implementation code.373 - Shader transitions are in `@hyperframes/shader-transitions` (`packages/shader-transitions/`), read package source, not skill files.374375For GSAP timeline patterns and easing, follow [house-style.md](./house-style.md) and [references/motion-principles.md](references/motion-principles.md) in this skill, plus the official GSAP docs at https://gsap.com/docs/v3/.376377## Bundled Scripts378379Two Node scripts ship with this skill under `scripts/`. **Both import `@hyperframes/producer`, and Node resolves that import relative to the script file, not to your working directory.** Running them from the skill folder fails with `ERR_MODULE_NOT_FOUND` no matter what directory you pass as the argument. Copy them into your HyperFrames project first, next to its `node_modules`, then run them from the project root:380381```bash382mkdir -p scripts && cp <skill-dir>/scripts/*.mjs scripts/383# Portrait (TikTok / Reels). BOTH scripts default to a 1920x1080 viewport,384# so a portrait composition MUST be given --width/--height or every flag,385# bbox and contrast sample is computed against a frame you will never render.386node scripts/animation-map.mjs . --width 1080 --height 1920 --out .hyperframes/anim-map387node scripts/contrast-report.mjs . --width 1080 --height 1920 --samples 10 --out .hyperframes/contrast388```389390Both also accept `--fps` (default 30). Match it to your render, and set `data-fps` on the composition root if you are not rendering at 30.391392| Script | What it does |393|---|---|394| `scripts/animation-map.mjs` | Enumerates every tween in `window.__timelines`, samples bounding boxes, and emits `animation-map.json` with per-tween summaries, an ASCII timeline, stagger detection, dead zones and flags. |395| `scripts/contrast-report.mjs` | Standalone WCAG audit. Emits `contrast-report.json` plus a `contrast-overlay.png` sprite grid (magenta fails AA, yellow passes AA only, green passes AAA) and exits 1 if any element fails AA. Useful when you want the overlay image, which `hyperframes check` does not produce. |396397Both require Node 22+ (the producer package declares `"engines": { "node": ">=22" }`).398399## Gotchas400401These are agent failure modes specific to Hebrew/RTL HyperFrames work. Generic HyperFrames gotchas (see upstream) still apply.402403- **Don't add a Google Fonts `<link rel="stylesheet">` tag or a CSS `@import url(...)` statement for Hebrew fonts.** The compiler already fetches Google Fonts server-side via `fetchGoogleFont()` in `packages/producer/src/services/deterministicFonts.ts`, caches the WOFF2s at `~/.cache/hyperframes/fonts/<slug>/`, and embeds them as base64 data URIs in the compiled HTML. An external stylesheet breaks determinism (network dependency at render time) and duplicates the font loading. Just write `font-family: 'Heebo', sans-serif;`.404- **Don't reach for the built-in `hyperframes tts` command for Hebrew narration.** It is local-only, described upstream as "Generate speech audio from text using a local AI model (Kokoro-82M)", and its arguments are exactly `input, text-file, output, voice, speed, lang, list, json`. There is no provider argument, so no environment variable can make this command speak Hebrew. Kokoro maps 9 locales via voice-ID prefix, `a`=American English, `b`=British English, `e`=Spanish, `f`=French, `h`=Hindi, `i`=Italian, `j`=Japanese, `p`=Brazilian Portuguese, `z`=Mandarin. Hebrew is not among them. Two paths that do work: (a) the media-use audio path, whose voice resolution order is HeyGen Starfish, then ElevenLabs (needs `ELEVENLABS_API_KEY` **and** the `elevenlabs` Python module importable), then local Kokoro. Run `hyperframes doctor` to see which engine your machine would actually pick. (b) Generate the WAV/MP3 with any external service (ElevenLabs, OpenAI TTS, Google Cloud TTS Hebrew) and drop the file into the composition as a normal `<audio>` clip. Path (b) is the one that works with no account and no Python deps.405- **Don't use `.en` Whisper models on Hebrew audio.** `.en` variants TRANSLATE non-English audio to English instead of transcribing it. For Hebrew captions use `npx hyperframes transcribe audio.wav --model medium --language he` (`small` is weak for Hebrew ASR; step up to `large-v3 --language he` for noisy audio). **`references/captions.md` is upstream text and prescribes a flat `--model small`; for Hebrew that rule is superseded by this one**, because per-word effects (karaoke, slam, marker sweep) key off word boundaries that `small` gets wrong. The `.en` suffix is only correct when the user explicitly says the audio is English. Upstream also added an `--engine` flag (`auto`, `parakeet`, `whisper`) that prefers Parakeet when it is installed; pass `--engine whisper` if you need the Whisper multilingual behaviour described here.406- **Don't forget `dir="rtl"` on Hebrew text containers, even inside a RTL-defaulted composition.** HyperFrames sub-compositions set their own direction context. GSAP `x:` tweens also don't auto-mirror. A title that uses `gsap.from({x: -80})` enters from the left in both LTR and RTL, for Hebrew, flip to `x: 80` so it enters from the right, matching reading direction.407- **Don't "fix" a Hebrew entrance because `animation-map.mjs` says it moves the wrong way.** The map describes motion from screen-space bounding-box deltas, with no notion of writing direction. In an RTL composition a correct Hebrew entrance, `gsap.from(".subtitle", { x: 80 })` so the text flies in from the right, is reported as `moves 51px left` (measured, 1080x1920 RTL composition). That prose is accurate about pixels and misleading about intent. Read the summaries for choreography and flags, not for direction, and never flip a tween's sign to make the wording read "right".408- **Hebrew narration over a music bed now has a first-class API, and a lint rule that can fail you.** Upstream added audio groups (`data-audio-group` on the member clip) with a summed FX bus (`data-fx-chain`), plus a lint rule that validates group membership and timing. If you are layering an externally-generated Hebrew voiceover over music, put both in a group rather than hand-balancing `data-volume`, and use `npx hyperframes normalize-audio` to match the narration's integrated LUFS to the bed instead of guessing. `data-audio-group` on a `<video>` element is ignored, it is audio-clip-only.409- **Don't paste English brand names into Hebrew paragraphs without `<bdi>` or `unicode-bidi: isolate`.** Without isolation, the Unicode bidi algorithm reorders mixed-direction runs and can place punctuation on the wrong side of the brand name or visually reverse it. Wrap brand names: `הצטרפו ל־<bdi>HyperFrames</bdi> עכשיו`.410411## Hebrew Bidi Details412413The `<bdi>` rule above covers brand names. Mixed-direction Hebrew compositions need three more bidi habits:414415- **Hebrew line-breaking for long headlines.** Hebrew does not hyphenate. A long Hebrew headline that overflows must wrap at word boundaries, never mid-word. Set `max-width` so it wraps naturally, and add `word-break: keep-all` (or `overflow-wrap: normal`) so the compiler does not break inside a Hebrew word. Do NOT use `<br>` to force breaks (see Rule 11). For deliberate one-word-per-line display titles, give each word its own element instead.416- **Digits with adjacent symbols inside RTL runs.** A bare digit run is handled correctly by the Unicode bidi algorithm: `2025` next to a Hebrew word stays left-to-right inside an RTL paragraph on its own, it does NOT reverse to `5202`, so a plain integer needs no wrapper. The real hazards are a digit touching a symbol, a range, or a Latin token: a percent sign, currency sign, or range dash can detach and land on the wrong side. Wrap those with `<bdi>` or `...` (LTR isolate): `<bdi>15%</bdi> הנחה`, `<bdi>₪199</bdi>`, `<bdi>10-20</bdi>`. This keeps the symbol attached to its number. See references/hebrew-rtl.md.417- **Punctuation mirroring.** Parentheses, brackets, and quotes are mirrored characters: `(` visually becomes `)` in an RTL run. In Hebrew captions and paragraphs let the browser mirror them by keeping the text in a proper `dir="rtl"` container. Do NOT hand-swap `(` and `)` to "fix" it, and when a parenthetical contains LTR content (a brand, a URL, a number) wrap that inner content in `<bdi>` so only the inner run is LTR while the parentheses stay correctly mirrored.418419## Troubleshooting420421### `hyperframes` command not found / render fails immediately422HyperFrames requires Node 22+ and FFmpeg on PATH. Confirm `node --version` is 22 or higher and `ffmpeg -version` resolves. On macOS install FFmpeg with `brew install ffmpeg`; on Debian/Ubuntu use `apt install ffmpeg`. Without FFmpeg the compiler cannot encode the MP4 and aborts before rendering any frames.423424### Compiler warns "font not supported" or Hebrew renders in a fallback font425The compiler only embeds fonts it can fetch from Google Fonts. Use a Hebrew family that exists on Google Fonts (Heebo, Rubik, Assistant, Alef, Frank Ruhl Libre, Noto Sans Hebrew) and write it plainly in CSS: `font-family: 'Heebo', sans-serif;`. Do NOT add a `<link rel="stylesheet">` or `@import` (see Gotchas), that breaks determinism without fixing the warning. If a custom non-Google font is required, the upstream docs cover local font embedding.426427### WCAG contrast audit fails (`hyperframes check`)428`check` samples background pixels behind each text element at 5 timestamps and flags ratios under 4.5:1 (normal text) or 3:1 (large text). Fix by adjusting the failing color WITHIN the palette family: brighten it on dark backgrounds, darken it on light backgrounds. Do not invent a new color. Re-run `hyperframes check` until clean. Use `--no-contrast` only while iterating, never as the final state.429430### Composition renders blank or content is invisible431**If the composition is Hebrew and it previews fine but the MP4 is black, check `<html>` for a `dir` attribute first.** `<html dir="rtl">` or `dir="auto"` produces a blank render while preview and snapshot look correct; remove it, keep `lang`, and scope `dir="rtl"` to the text-bearing elements instead. `npx hyperframes check` reports this as `html_dir_attribute_breaks_render` at severity error. Otherwise, the most common cause is a `<template>` wrapper on a standalone composition. The main `index.html` must put the `data-composition-id` div directly in `<body>`, not inside `<template>`. Also check that every timeline is registered via `window.__timelines["<composition-id>"] = tl` and built synchronously (not inside `async`, `setTimeout`, or a Promise), the capture engine reads `window.__timelines` synchronously after page load.432433### Hebrew title enters from the wrong side434GSAP `x:` tweens do not auto-mirror for RTL. A `gsap.from({x: -80})` enters from the left in both LTR and RTL. For Hebrew, flip to a positive value (`x: 80`) so the element enters from the right, matching reading direction. See `references/hebrew-rtl.md`.435436## Reference Links437438| Source | URL | What to Check |439|---|---|---|440| HyperFrames GitHub | https://github.com/heygen-com/hyperframes | Upstream repo, issues, releases |441| HyperFrames docs | https://hyperframes.heygen.com/quickstart | CLI, Node 22+, FFmpeg requirement |442| Compiler font logic | https://github.com/heygen-com/hyperframes/blob/main/packages/producer/src/services/deterministicFonts.ts | Canonical font list, Google Fonts fallback, cache path |443| Kokoro TTS voices | https://github.com/heygen-com/hyperframes/blob/main/skills/media-use/audio/references/tts.md | Kokoro voice prefixes across 9 locales (no Hebrew) |444| Whisper model guide | https://github.com/skills-il/developer-tools/blob/master/hyperframes-best-practices/references/transcript-guide.md | `.en` vs multilingual models, `--language` flag |445| Google Fonts Hebrew | https://fonts.google.com/?subset=hebrew | Heebo, Rubik, Assistant, Alef, Frank Ruhl Libre, Noto Sans Hebrew |446| Unicode bidi spec | https://developer.mozilla.org/en-US/docs/Web/CSS/unicode-bidi | `isolate`, `<bdi>`, mixed-direction text |