Plotbeat Data Videos
Unclecheng-li/AI_Animation
Turns a local CSV or JSON file, or data fetched from an approved source, into an editable data-visualization video by adapting HyperFrames templates.
Lays timed graphic cards such as lower-thirds, data callouts and quotes over an existing talking-head video, synced to the transcript.
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install heygen-com/hyperframes talking-head-recut --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/talking-head-recut .claude/skills/talking-head-recut && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .claude/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recutType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install heygen-com/hyperframes talking-head-recut --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/talking-head-recut .agents/skills/talking-head-recut && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .agents/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install heygen-com/hyperframes talking-head-recut --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/talking-head-recut .cursor/skills/talking-head-recut && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .cursor/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/heygen-com/hyperframes.git --path skills/talking-head-recut--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install heygen-com/hyperframes talking-head-recut --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/talking-head-recut .gemini/skills/talking-head-recut && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .gemini/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install heygen-com/hyperframes talking-head-recutInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/talking-head-recut .github/skills/talking-head-recut && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .github/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add heygen-com/hyperframes --skill talking-head-recut -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install heygen-com/hyperframes talking-head-recut --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/heygen-com/hyperframes.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/talking-head-recut .opencode/skills/talking-head-recut && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "talking-head-recut" agent skill from https://github.com/heygen-com/hyperframes/tree/main/skills/talking-head-recut into .opencode/skills/talking-head-recut/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "talking-head-recut", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
talking-head-recutLays timed graphic cards such as lower-thirds, data callouts and quotes over an existing talking-head video, synced to the transcript.
An existing talking-head, interview or podcast clip plays in full while designed graphic cards appear on top, timed to the transcript. Cards can be kinetic titles, lower-thirds, data callouts, quotes, side panels or picture-in-picture, and the canvas can be 16:9, 9:16 or 4:5. There is no fixed list of card types; the overlays follow what is actually said.
The agent plans the timing and content, writes each card's HTML itself in the conversation, assembles one composition HTML and renders it to MP4 with hyperframes. Aspect, layout, style group and card count are settled late, once the footage and transcript have been probed. Intermediate files such as metadata.json and the extracted audio.mp3 stay in the work directory so you can inspect them.
It is not for plain subtitles, which belong to embedded-captions, and unclear requests go to /hyperframes first. Bundled assets include Inter, Caveat, Virgil and LXGW WenKai fonts, gsap.min.js, and frame and layout templates such as clean, hairline and polaroid. Updating the skill with npx hyperframes skills update talking-head-recut is confirmed with you before it runs.
11 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 8bca7c4. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (JavaScript, from the files we listed), which the agent can run.
Shell commands in SKILL.md call:
npxffmpegffprobeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npx, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Talking Head Recut loads about 16k tokens when it runs, and up to ~42k if it reads all its reference files. Until then it costs about 118 tokens; SKILL.md has 4,854 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from heygen-com/hyperframes at commit 8bca7c4, republished under its Apache-2.0 licence (© heygen-com). 4,854 words, ~16,253 tokens.
.claude/skills/talking-head-recut/SKILL.md (or your agent's skills folder). This skill also uses 28 other files; get the full folder from GitHub.Plugin installs: Before setup or freshness commands, follow plugin execution rules when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.
First, keep this skill fresh — confirm with the user before running:
npx hyperframes skills update talking-head-recut. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.
Talking Head Recut takes a local video that plays in full and layers a sequence of
timed, designed graphic cards onto it — titles, lower-thirds, data callouts,
quotes, side panels, picture-in-picture — synced to what's being said. The agent
designs the cards (timing + content) and writes each card's HTML directly in the
conversation, then assembles a single composition HTML and renders it to MP4 via
hyperframes. There is no fixed archetype list and no prescribed card structure —
the overlays emerge from what the transcript actually says.
The front door is
/hyperframes. This skill packages an existing talking-head clip with designed graphic cards (titles, lower-thirds, data callouts, quotes, side panels, PiP) — not plain captions (the spoken words as text). The clip plays untouched. Any other intent — plain subtitles, a standalone graphic, a from-scratch video — or any uncertainty → read/hyperframesfirst: the intent layer owns every route decision.
Graphic-packaging sibling of
embedded-captions. Captions add the spoken words as a readable subtitle; this adds designed graphics on top of the playing video. Plain subtitles →embedded-captions. Build a video from scratch → the creation workflows (product-launch-video/faceless-explainer/ …).
Routed through /hyperframes, the intent layer confirms only the input (which clip) and announces the render-strategy questions as deferred asks — aspect, layout, style group, and card count stay at Step 7, where the probed footage and transcript ground the recommendations; the layer's run-shape questions don't apply. A BRIEF.md, when present, carries the confirmed input and any user notes — read it first.
Inspectable intermediate files in the work directory:
metadata.json — duration / width / height / fpsaudio.mp3 — extracted audiotranscript.json — a flat word array [{ text, start, end }, …] (Whisper; no segments, no words wrapper)storyboard.json — lightweight card outline (the agent's plan)public/cards/card-XX.html — one HTML fragment per cardpublic/index.html — final assembled compositionoutput.mp4 — rendered video# hyperframes — transcription (local Whisper) + rendering the assembled HTML to MP4
npx hyperframes --helpThis skill runs entirely on the hyperframes CLI plus system ffmpeg / ffprobe.
Transcription is local Whisper via hyperframes transcribe — no third-party
service, API key, or rate-limited proxy.
npx hyperframes doctor # ffmpeg, headless browser, render deps
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"Required:
ffmpeg / ffprobe (system)<SKILL_DIR>/assets/fonts/*.woff2, <SKILL_DIR>/assets/vendor/gsap.min.js (bundled inside this skill, staged to work dir in Step 9)Transcription needs no key — hyperframes transcribe runs Whisper locally (Step 4).
Strongly recommended on macOS for hyperframes render:
export PRODUCER_BROWSER_GPU_MODE=hardwareAll artifacts live under videos/<project-name>/ — the same convention as the other
video workflows (product-launch-video / faceless-explainer / pr-to-video). Keep
the cwd at the workspace root; everything below writes under this one subdirectory.
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"# metadata — duration / width / height / fps
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate \
-show_entries format=duration -of json "$VIDEO_PATH" > "$WORK_DIR/metadata.json"
# audio
ffmpeg -y -i "$VIDEO_PATH" -vn -acodec libmp3lame -q:a 2 "$WORK_DIR/audio.mp3"Outputs: metadata.json (read width/height/duration; fps = the r_frame_rate
fraction evaluated, e.g. 30000/1001 → 29.97) + audio.mp3.
npx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.enLocal Whisper — no API key, no proxy, no rate limit. Writes a word-level
transcript.json into the work dir (word text + start / end timestamps).
Read it for the word / sentence timings that drive card timing in Step 6; group
words into sentences yourself at punctuation / pauses if you need segment-level
chunks.
Clamp to media duration. Whisper can return the final word's end a hair past the
actual clip length — clamp every card endSec and composition.durationSeconds to the
metadata.json duration, or the render will show a black tail past the video.
transcript.json is a flat array of word objects — [{ "text": "...", "start": s, "end": s }, …] (no segments array, no words wrapper; the per-word key is text). Read it and fix obvious ASR errors:
text in place; preserve its start / end timestampssegments array — group words into sentences yourself (split at terminal punctuation / pauses) when you need segment-level chunks for card timingNo CLI involved. Read transcript.json + metadata.json and design
cards directly. storyboard.json is an agent-internal planning artifact
— no CLI command consumes it; it exists so you can think clearly
about timing and content before writing each card's HTML. Keep the
shape consistent with the example below so the same outline can drive
the composition you author in Step 9:
{
"schemaVersion": 3,
"composition": {
"fps": 30,
"width": 1080,
"height": 1920,
"durationSeconds": 121.2,
"layout": "portrait",
"themeId": "noir",
"seed": 42
},
"videoTrack": {
"sourcePath": "input-video.mp4",
"startSec": 0,
"endSec": 121.2,
"bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
},
"subtitles": { "enabled": false },
"cards": [
{
"id": "card-01",
"intent": "Hook with the speaker's anxious midnight question",
"startSec": 0.5,
"endSec": 13.0,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": {
"kicker": "AN HONEST QUESTION",
"title": "The soul-searching question at 11 PM",
"detail": "Client's 60-second voice message: 'If the RMB appreciates, does that mean my USD policy is a terrible loss?'"
}
}
]
}Required Card fields:
| field | type | purpose |
|---|---|---|
id | string | stable id used in card HTML & GSAP selectors |
intent | string | natural-language description; fed to card synthesis |
startSec / endSec | number | times in seconds (endSec > startSec) |
accentIndex | 0 | 1 | 2 | 3 | 4 | which of the 5 theme accent colors this card pulls |
zone | enum (see below) | where on the canvas the card lives |
contentHints | object | free-form bag; agent puts kicker/title/detail/data/quote here |
archetype (optional) | string | free-form label you may attach to remember a card's pattern; absent = free-form, which is the default |
transition (optional) | enum: cut | fade | slide | wipe | declarative card-to-card transition |
Five zone values:
| zone | resolved bounds | when to use |
|---|---|---|
fullscreen | covers whole canvas | hero moments, big numbers, mantras |
whiteboard-area | inset 40px margin (or 45% of portrait height) | dense data / annotated content |
lower-third | bottom 30% band | annotation over visible video |
side-panel | right 42% (landscape) or bottom 40% (portrait) | data side, video other side |
video-overlay | full canvas, expects mostly-transparent card | annotation overlays on full-bleed video |
When you assemble the composition in Step 9, resolve each card's zone
into pixel bounds on the card-host wrapper following the table above.
Video bounds are set once at composition level (videoTrack.bounds);
to make video appear to "move between cards", author GSAP tweens against
#video-wrap in the composition's <script> (see Step 9).
No prescribed card roles, no prescribed narrative arc. Cards emerge from what the video actually says — could be all quotes or all data, could open with a number or with a story. Let the transcript drive the rhythm.
How many takeaways? — auto-infer from duration + density. No fixed upper limit. Pick a base pace from the video duration, then adjust by information density. Only floor is fixed: minimum 5 cards so even short videos have rhythm.
Step 1 — base pace by duration (the natural sec/card for medium density):
| video duration | base pace (sec per card) | rationale |
|---|---|---|
| < 60s (short reel) | 6–8s | viewers expect fast cuts in short-form |
| 60s – 3 min | 8–12s | normal social pace |
| 3 – 10 min | 12–20s | give breathing room; each card carries more |
| 10 – 30 min | 20–35s | long-form lecture / interview rhythm |
| > 30 min | 30–60s | episodic, near-chapter feel |
Step 2 — density multiplier (multiplies the base pace):
| signal in the transcript | multiplier | effect |
|---|---|---|
| High density — many numbers, distinct claims, staccato pacing, list-like enumeration, every 1–2 sentences is a new idea | × 0.7 | cuts faster, more cards |
| Medium density — mixed flow with both data and narrative | × 1.0 | base pace |
| Low density — one extended story, repeated reframing, slow reflective pacing, single argument unfolding | × 1.5 | cuts slower, fewer cards |
Step 3 — compute:
secPerCard = basePace × densityMultiplier
cardCount = max(5, round(videoDurationSec / secPerCard))Examples (notice — no upper clamp; long videos naturally produce more cards):
When a card holds longer than ~15s, plan for a richer card (data block,
multi-step reveal, several sub-points unfolding with staggered
animations) — a static one-liner gets boring past 8s. For long pieces
where many cards exceed 30s, consider chunking the timeline into
sub-compositions (one .html per chapter, mounted with
data-composition-src) so the GSAP timeline per file stays manageable
— see the timeline_track_too_dense HyperFrames lint warning.
contentHints is a free-form object; its keys and nesting can take whatever
shape captures the data. The agent decides the shape per card.
Optional outro. This skill ships no fixed brand outro. If the user wants a closing card, design a neutral one yourself (wordmark + one-line tagline, ~1.5-2s, fade in -> short hold -> fade out), append it to cards[], and extend composition.durationSeconds to its endSec. Otherwise end on the last content card.
Before you start designing cards or deciding bounds, ask the user to pick the output ratio, the layout, the style, and the card-density preset. Frames are auto-selected from the chosen layout × style combination (see "Auto-pick frame" table below). Before sending the question, precompute two things:
recommendedRatio from the source video's aspect ratio
(metadata.json width / height):
sourceAspect = width / heightsourceAspect ≥ 1.5 (≥ ~3:2 wide) → recommend 16:9sourceAspect ≤ 0.7 (≤ ~9:13 tall) → recommend 9:160.7 < sourceAspect < 1.5 (near-square) → recommend 4:5Mark the recommended option's label with " (recommended · matches source video X:Y)" so the user sees why it's recommended.
autoCount from Step 6 (max(5, round(videoSec / (basePace × densityMultiplier)))) so the "auto" option's label can show the
concrete number.
Environment compatibility — pick the best available question channel. Not every runtime exposes the same structured-question tool. Apply this order:
ask_question,
request_user_input, IDE-specific prompt) — use that tool with the
same 4 question texts and option lists. Preserve the recommendation
markers and the precomputed values.Rules that apply to every channel:
../hyperframes/references/brief-contract.md § 1) — skip
the question entirely and use: recommendedRatio, layout="stack"
(safest cross-ratio default), style chosen from transcript tone in
the most neutral group (editorial/data), autoCount. Tell the user
what you picked in one sentence and continue.Channel A — native AskUserQuestion:
// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "Output video aspect ratio (canvas):",
header: "Aspect ratio",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (recommended · matches source video W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) landscape", description: "TV / YouTube / desktop playback. Most natural when the source video is already landscape; widest canvas." },
{ label: "9:16 (1080×1920) portrait", description: "TikTok / Reels / short-form mobile. Most natural for portrait source; native mobile experience." },
{ label: "4:5 (1080×1350) near-portrait", description: "Instagram feed / WeChat Moments. Best when source is near-square or you want to cover both platforms." }
]
},
{
question: "Choose the overall layout: how should the video and cards coexist on the canvas?",
header: "Layout",
multiSelect: false,
options: [
{ label: "side-by-side (split)", description: "Video and card each take half the canvas. Most stable for interview / data side-by-side; clear visual separation." },
{ label: "top-bottom (stack)", description: "Video on top (~52%), card below. Classic combo of speaker face + summary card; works well in portrait too." },
{ label: "picture-in-picture (pip)", description: "Card fills the canvas, video shrinks to a rounded corner window. Use when content is primary and speaker is secondary." },
{ label: "full-screen overlay (overlay)", description: "Video plays full-bleed, card floats as a glass layer on top. Strong cinematic / emotional feel." }
]
},
{
question: "Choose the card visual style (style):",
header: "Style group",
multiSelect: false,
// NOTE: these 3 groups intentionally match the frame auto-pick matrix
// rows below, so picking a group resolves both `style` group AND the
// frame matrix column in one step. Memberships are mutually exclusive.
options: [
{ label: "warm paper (warm-paper)", description: "academic notebook · editorial big-type · whiteboard hand-drawn · xhs social. Best for interview reflections, product launches, lifestyle, emotional stories." },
{ label: "clinical / cold (clinical)", description: "audit magazine · swiss grid · terminal CLI · minimal modern. Best for financial analysis, investigative reports, technical tutorials, serious presentations." },
{ label: "experimental / avant-garde (experimental)", description: "geom color-clash geometry · spotlight dark-background. Best for short-form highlights, product launches, strong emotion, cinematic feel." }
]
},
{
question: "Card count (takeaway pacing): how many cards to cut?",
header: "Card count",
multiSelect: false,
options: [
{ label: "Auto (recommended) · approx N cards", description: "Inferred automatically from video duration and information density (see Step 6 rules). This run estimates approx N cards. Substitute the real N (your autoCount) into the label." },
{ label: "Fewer · approx round(N × 0.6) cards", description: "Sparser cuts, each card holds longer — suits reflective / slow-paced content." },
{ label: "More · approx round(N × 1.5) cards", description: "Tighter cuts, faster rhythm — suits staccato / data-dense / short-form highlight content." }
]
}
]
})About "Other" — AskUserQuestion automatically adds an "Other" option to the card count question. The user can type a number directly (e.g. "8", "20") as the cardCount target. Parse the input as an integer: if parsing succeeds → use that value (minimum 5 as a floor); if parsing fails → fall back to "auto".
Channel B — plain-text fallback (Codex CLI, runtimes without a native question tool). Post this as one normal message, then wait for the reply. Bullet-style 1/2/3/4 keeps the reply parseable:
I need to confirm four visual decisions with you before I start cutting cards:
1) Output aspect ratio (canvas):
A. 16:9 landscape (1920×1080) — TV / YouTube / desktop playback
B. 9:16 portrait (1080×1920) — TikTok / Reels / short-form mobile
C. 4:5 near-portrait (1080×1350) — Instagram feed / works for both platforms
▸ My recommendation: <recommendedRatio> (matches source video W×H = <sourceW>×<sourceH>)
2) Overall layout (how video & card coexist):
A. split side-by-side (50/50)
B. stack top-bottom (video top, card bottom)
C. pip picture-in-picture (card full canvas, video rounded corner window)
D. overlay full-screen glass overlay (video full-bleed, card glass layer)
3) Card style group (maps to frame auto-pick matrix, pick 1 of 3):
A. warm paper (warm-paper) (academic / editorial / whiteboard / xhs)
B. clinical / cold (clinical) (audit / swiss / terminal / minimal)
C. experimental (experimental) (geom / spotlight)
4) Card count (takeaway pacing):
A. Auto (recommended) — approx <autoCount> cards
B. Fewer — approx round(<autoCount> × 0.6) cards
C. More — approx round(<autoCount> × 1.5) cards
D. Give me a specific number (e.g. "8", "20")
Reply format: "1A 2C 3B 4A" or natural language is fine.
If you want all recommended defaults, reply "default" / "auto" / "use all recommendations".Parsing the plain-text reply:
"1A 2C 3B 4A", "A C B A", "16:9 / pip / data / auto", full sentences, or default.After the user answers (any channel):
Resolve the output canvas from the ratio answer — these are the
exact storyboard.composition.width / height values to write:
| user choice | composition.width × height | storyboard.layout field |
|---|---|---|
16:9 | 1920 × 1080 | "landscape" |
9:16 | 1080 × 1920 | "portrait" |
4:5 | 1080 × 1350 | "portrait" (schema treats 4:5 as portrait — height > width) |
For 4:5 bounds inside references/layouts/*.html — those files
only document landscape (1920×1080) and portrait (1080×1920). For
4:5 (1080×1350) derive bounds by proportional scaling from
portrait: keep horizontal values, scale vertical values by
1350/1920 ≈ 0.703. Example: overlay portrait card =
{ x: 24, y: 1280, w: 1032, h: 564 } → 4:5 card =
{ x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }
= { x: 24, y: 900, w: 1032, h: 397 }.
Map the style group to a specific style by looking at the
transcript tone — pick the one that best fits, but stay inside the
user's chosen group. If you're unsure between two specific styles
inside the group, send a second AskUserQuestion with those 2–4
specific style options.
Resolve final cardCount from the density answer:
| user choice | final cardCount |
|---|---|
| Auto (recommended) | the autoCount you already computed |
| Fewer | max(5, round(autoCount × 0.6)) |
| More | round(autoCount × 1.5) (no upper clamp) |
Other = "<n>" (integer) | max(5, parseInt(n)) |
| Other = anything else | fall back to autoCount |
Auto-pick the video frame from this table (frames don't ask the user — they follow from layout × style):
| layout | warm-paper styles (academic / whiteboard / editorial / xhs) | clinical styles (audit / swiss / terminal / minimal) | experimental styles (geom / spotlight) |
|---|---|---|---|
split | polaroid | hairline | clean |
stack | polaroid | hairline | clean |
pip | clean (pip pill already has chrome) | clean | clean |
overlay | clean (full-bleed forbids deco frames) | clean | clean |
Tell the user what you chose in one sentence — ratio (+ canvas size), layout, specific style, frame, and final cardCount — then proceed with the rest of Step 7 (per-card layouts, motion patterns).
Record the five values (ratio / layout / style / frame / cardCount)
in working memory (no schema field needed); you'll reference them
while writing each card's HTML in Step 8 and while reading the
matching references/<dim>/<key>.html for tokens and structure.
If the user picks an answer via "Other" with a free-text style name not in the 10-style library, treat it as a hint to design a fresh card visual yourself, but still anchor on the chosen layout's bounds.
With ratio / layout / style / cardCount / frame locked from Step 7.0, the remaining per-card decisions are:
object-fit: cover and is clipped to #video-wrap's tween bounds.
If you want NO cropping (e.g. portrait source on landscape canvas
shouldn't get its top/bottom chopped), aim the tween at a rect that
matches the source's aspect ratio and let surrounding canvas show
through (or fill with the card / a backdrop).card.zone per card: derive from your chosen composition layout
(split → side-panel, stack → lower-third, pip → fullscreen, overlay
→ video-overlay), OR pick a different zone for one-off variants
(fullscreen for hero / quote, whiteboard-area for dense data).accentIndex per card: each card pulls one of the 5 theme accent
colors. Vary across cards for rhythm; reuse the same index when two
cards belong to the same narrative beat.data-anim kinds (see the table later) and stick to them so the
composition feels coherent.Pick from these themeId palettes (use them as --accent-N /
--bg / --text CSS variables in your composition <style> block):
| themeId | accent palette (5 colors) | board bg | text |
|---|---|---|---|
| classic | #1971c2 #e03131 #2f9e44 #e8590c #9c36b5 | #FFF9E3 (paper) | #1e1e1e |
| noir | #4cc9f0 #f72585 #4ade80 #fb923c #a78bfa | #1a1a1a | #f1f1f1 |
| mint | #0077b6 #d62828 #2d6a4f #e76f51 #7209b7 | #e8faf0 | #1b4332 |
| craft | #bf5700 #d62728 #6c757d #e9b54a #3d5a80 | #f6efe1 | #2d2d2d |
| slate | #0ea5e9 #ef4444 #22c55e #f97316 #a855f7 | #1e293b | #f1f5f9 |
| mono | #000 #555 #888 #aaa #ccc | #fff | #000 |
Available fonts (woff2 in <SKILL_DIR>/assets/fonts/, staged to work dir in Step 9): Caveat (handwriting),
LXGW WenKai TC (Chinese hand-script), Inter (modern sans), Virgil
(geometric hand). Reference via @font-face or font-family directly.
For inspiration on visual patterns, <SKILL_DIR>/references/styles/
ships 10 self-contained reference cards (academic / editorial / minimal
/ spotlight / geom / whiteboard / audit / terminal / swiss / xhs) that
you can copy as starting points — but do not feel constrained to
match any of these. Each card is your own design.
Beyond the composition-level themeId, the skill ships a richer reference
library at <SKILL_DIR>/references/ covering three orthogonal
visual dimensions you can freely mix:
Style × Layout × VideoFrame
(10) (4) (3)| dimension | keys | what it decides |
|---|---|---|
| style | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | the card's visual language — fonts, colors, ornament, layout-within-card |
| layout | split stack pip overlay | how the source video and the card share the canvas |
| frame | clean hairline polaroid | the decorative chrome around the video element |
Read <SKILL_DIR>/references/DESIGN_INDEX.md
for the full matrix and a loose decision guide (interview / product launch / data analysis /
social clip / technical tutorial / emotional story …). When you decide to use a specific
style / layout / frame, Read the corresponding file:
references/styles/<key>.html — self-contained card fragment with that
style's CSS tokens (colors, fonts, padding, ornament) and a placeholder
takeaway. Copy the .card[data-card-id="ref-<key>"] style block, rename
the data-card-id to your card's id, swap the placeholder content for the
real takeaway, and you're done.references/layouts/<key>.html — exact videoBounds + cardBounds for
both landscape and portrait. The storyboard records only card.zone.references/frames/<key>.html — decorative HTML to add as a sibling of
#video-wrap, plus placement instructions for the composition CSS.Within the user's chosen style group you may vary the style per card, and
pick layout × frame per card, as long as the transitions read smoothly.
The 10 styles are skill-side design tokens, not composition-level themes —
they don't need to be declared in storyboard.composition; they live
inside each card's HTML. The themeId field can still pick a
composition-level palette (table above) that controls page-body background
and video border chrome.
Two coordinated decisions per card define how it shares the canvas with the source video:
card.zone (declared in storyboard.json) — one of the 5 schema
values; resolve it into pixel bounds (per the table in Step 6) when
you write the card-host wrapper's inline style in Step 9.#video-wrap bounds at this card's time window (declared
imperatively in the composition's GSAP timeline) — the agent tweens
#video-wrap to a target rect for each layout transition.Schema does NOT store per-card video bounds. videoTrack.bounds is
one-time at composition level (defaults to full canvas). Video
"moving" between cards is purely a GSAP animation authored in
index.html. There is no card.layout field; the schema only has card.zone.
4 composition layouts (from references/layouts/) — each is a
recipe pairing a zone with a #video-wrap tween target:
| composition layout | recommended card.zone | GSAP target for #video-wrap (landscape 1920×1080) | GSAP target for #video-wrap (portrait 1080×1920) | when to use |
|---|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | { left: 0, top: 960, width: 1080, height: 960 } (bottom half) | speaker + data side-by-side / 50:50 weight |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 } (top 52%) | { left: 0, top: 0, width: 1080, height: 844 } (top 44%) | speaker on top + summary card below |
pip | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } + add .framed class | { left: 690, top: 28, width: 360, height: 203 } + add .framed | content-heavy card + corner pip |
overlay | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 } (full-bleed) | { left: 0, top: 0, width: 1080, height: 1920 } | cinematic / dramatic / glass card on full video |
For 4:5 (1080×1350), scale portrait y/h values by 1350/1920 ≈ 0.703
(see Step 7.0 Channel A / Channel B recommendedRatio resolution
table).
Other zone values for one-off variants (still uses card.zone; no
fake "layout" field):
zone | resolved bounds | common use |
|---|---|---|
fullscreen | covers whole canvas | hero card, video tweens to hidden/pip |
whiteboard-area | inset 40px margin (landscape) or bottom 45% (portrait) | dense data card, free margins |
lower-third | bottom 30% band | talking-head annotation |
side-panel | right 42% (landscape) or bottom 40% (portrait) | sidebar / "split" recipe |
video-overlay | full canvas; expect transparent card root | glass overlay on full-bleed video |
You can mix recipes per card — choose card.zone based on what suits
the moment, then write the GSAP tween for #video-wrap between cards.
storyboard.json is an agent-internal planning artifact — no CLI
command parses it. It exists to keep your timing and content decisions
explicit before you write each card's HTML. Stick to the v3-style
shape below so the same outline drives the composition you assemble in
Step 9.
Required structure (see Step 6 for the full example):
schemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed } — note durationSeconds/fps/themeId/layout live inside composition, NOT at top levelvideoTrack: { sourcePath, startSec, endSec, bounds? } — video bounds default to full canvassubtitles: { enabled, ... }cards[] — each card has the 6 required fields: id, intent, startSec, endSec, accentIndex, zone, contentHintsRules:
composition.durationSeconds and should not overlap unless intentional (use data-track-index to control z-order when they do).contentHints. contentHints is your own structured prompt for designing the card; the rendered look is the HTML.storyboard.json — keep them in working memory and use them when authoring card HTML + GSAP tweens.Transparent card backgrounds for cards that share canvas with video.
When the GSAP tween leaves video visible behind/beside the card (overlay
recipe, pip recipe, or any card.zone = 'lower-third' | 'video-overlay'
moment), the card's .root MUST NOT paint a full opaque background —
otherwise it occludes the video. Two patterns:
/* Pattern A: transparent root, page body provides the cream backdrop */
html,
body {
background: var(--bg);
}
.card[data-card-id="card-X"] .root {
background: transparent;
}
/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root {
background: var(--bg);
}
.card[data-card-id="card-overlay"] .root {
background: transparent;
}For side-panel-zone cards (split recipe), the card-host is already
only half the canvas, so an opaque card bg is fine — it only covers its
half.
Create $WORK_DIR/public/cards/{card-id}.html for each card. Each file
contains a single rooted HTML fragment that follows this contract:
<div class="card" data-card-id="{cardId}">
<style>
/* MUST: every rule starts with .card[data-card-id="{cardId}"] */
.card[data-card-id="card-01"] .root {
width: 100%; height: 100%;
display: flex; ...;
font-family: 'Caveat', 'LXGW WenKai TC', serif;
color: var(--text);
background: var(--bg);
}
.card[data-card-id="card-01"] .title { font-size: 84px; ... }
</style>
<div class="root">
<h1
id="card-01-title"
data-anim="kinetic-chars"
data-anim-at="0.3"
data-anim-duration="0.5"
data-anim-stagger="0.04"
data-anim-pattern="pop"
>
<span class="char">S</span>
<span class="char">u</span>
</h1>
<div
id="card-01-line"
data-anim="grow-x"
data-anim-at="0.65"
data-anim-duration="0.5"
data-anim-target-w="420"
style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"
></div>
</div>
</div>Hard rules (hyperframes lint will reject violations):
<div class="card" data-card-id="{cardId}"><style> rules MUST be prefixed with the scope selector above<script> tagssrc= / href= (no CDN, no remote fonts)onclick= etc.)public/ directoryvar(--accent-N) etc. for portability across themesAnimations are declared, not coded. Use data-anim-* attributes
only; never write <script> to animate. You compile every data-anim-*
declaration into the single master GSAP timeline in Step 9.
The 10 references/styles/*.html are sized for a 1920×1080 landscape
preview. When storyboard.layout = "portrait" (1080×1920, the dominant
case for social / mobile), scale every visual size up — phones hold
the screen close, and the same pixel count reads smaller than on a
landscape TV-style canvas.
| token | landscape baseline | portrait target | scale |
|---|---|---|---|
| title (h1/h2 hero) | 64–96px | 88–132px | ×1.35 |
| detail / body | 24–30px | 30–40px | ×1.30 |
| kicker / chip label | 14–16px | 18–22px | ×1.30 |
| timecode / meta | 12–14px | 16–18px | ×1.30 |
| data block primary number | 48–60px | 64–88px | ×1.40 |
| line-height multiplier | 1.05–1.5 | same | (don't scale) |
Rule of thumb: portraitPx = round(landscapePx × 1.3), then floor
to a nearby 4px multiple for visual rhythm. Hero headlines may go up to
×1.4; small meta text stays at ×1.2 to avoid crowding.
Padding shrinks slightly in portrait — the card is narrower so big landscape padding (40–64px) eats too much width. Use 24–36px horizontal padding in portrait.
If you're producing a single card that must work in both layouts,
prefer a @container query on the card root over hard-coding sizes:
.card[data-card-id="X"] .root {
container-type: inline-size;
}
.card[data-card-id="X"] .title {
font-size: clamp(64px, 8.5cqi, 132px);
}
.card[data-card-id="X"] .detail {
font-size: clamp(24px, 3.2cqi, 40px);
}But for most cards, a single layout choice is fine — just pick the size
table column that matches the storyboard's layout field.
data-anim KindsThis list is closed, and deliberately so: a card is an HTML fragment whose motion this
skill compiles into the shared overlay timeline in Step 9 (see the GSAP mapping table
there). That is why this workflow does not search the HyperFrames component registry the
way the composition workflows do — npx hyperframes catalog returns standalone
compositions that carry their own timeline, and a card has no place to mount one. Reach a
look the kinds below cannot express with plain CSS inside the card's scoped <style>.
| kind | use for | key params |
|---|---|---|
fade-in | enter | at, duration, ease? |
fade-out | exit | at, duration, ease? |
slide-in | slide enter | at, duration, from=left|right|top|bottom, distance |
kinetic-chars | per-char pop | at, duration, stagger, pattern=pop|fade — element needs <span class="char"> children |
typewriter | per-char fade | same as kinetic-chars but slower default stagger |
count-up | animate number | at, duration, from, to, format=.0f|.1f|.2f|,d |
draw-path | SVG path reveal | at, duration — element should be a <path> |
grow-y | bar height | at, duration, target-h (px) — element starts height:0 |
grow-x | bar width | at, duration, target-w (px) — element starts width:0 |
scale-pop | pop entrance | at, duration |
blur-in | unfocused → focused | at, duration |
mask-reveal | clip reveal | at, duration, direction=left|right|top|bottom |
morph-to | tween any CSS | at, duration, props='{...JSON...}' |
data-anim-at is seconds relative to the card's startSec — when you
compile each declaration into the GSAP timeline in Step 9, add the
card's startSec to get the absolute time and quantize to 1/fps.
Stage the assets and write $WORK_DIR/public/index.html:
# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"
mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"* "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video — RE-ENCODE with dense keyframes. Sources with a sparse GOP
# (keyframe interval > ~1s) freeze on seek in the renderer (a frozen frame under the
# overlays); -g / -keyint_min set to your composition fps make every frame seekable.
# (Set both to your fps — 30 shown; use 24/25/60 to match.)
ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
-pix_fmt yuv420p -movflags +faststart -c:a aac "$WORK_DIR/public/input-video.mp4"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "LXGW WenKai TC";
src: url("fonts/LXGWWenKaiTC-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "Virgil";
src: url("fonts/Virgil.woff2") format("woff2");
font-display: block;
}
:root {
/* Pick from the themeId palette table in Step 7 — example: classic */
--bg: #fff9e3;
--text: #1e1e1e;
--accent-0: #1971c2;
--accent-1: #e03131;
--accent-2: #2f9e44;
--accent-3: #e8590c;
--accent-4: #9c36b5;
--font-family: "Caveat", "LXGW WenKai TC", serif;
}
* {
box-sizing: border-box;
}
/* Body font-family MUST list concrete font names (not just var(--font-family)) —
the HyperFrames renderer's static analyzer doesn't expand CSS variables when
resolving fonts, so a var-only chain triggers `font_family_without_font_face`
lint and falls back to a generic. Use the concrete chain here; cards that
want the theme font can still reference var(--font-family) internally. */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
font-family: "Inter", "Caveat", "LXGW WenKai TC", ui-sans-serif, system-ui, sans-serif;
}
#stage {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* video-wrapper holds the source video. Its position / size are animated
over time by the master timeline (one tween per layout transition). */
.video-wrapper {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
.video-wrapper video {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-host {
position: absolute;
pointer-events: none;
overflow: hidden;
}
.card-host .card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.card-host .char {
display: inline-block;
visibility: visible;
}
/* Subtle drop shadow + rounded corners for non-fullscreen video framings */
.video-wrapper.framed {
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
</style>
</head>
<body>
<div
id="stage"
data-composition-id="talking-head-recut"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<!-- Layer 1: source video — initial position matches card-01's layout -->
<div class="video-wrapper" id="video-wrap">
<video
id="bg-video"
src="input-video.mp4"
playsinline
data-has-audio="true"
data-start="0"
data-duration="121.2"
data-track-index="1"
></video>
</div>
<!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
<!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
<!-- - "card-host" → our positioning + pointer-events styles -->
<!-- - "clip" → the marker Studio and the linter use to recognise a -->
<!-- clip. Visibility itself comes from data-start / -->
<!-- data-duration, which the runtime honours with or -->
<!-- without this class -->
<!-- (lint: timed_element_missing_clip_class, a warning). -->
<!-- Example: card-01 with zone="fullscreen" → card-host covers (0,0,1920,1080) -->
<div
class="card-host clip"
data-card-id="card-01"
data-start="1.0000"
data-duration="6.5000"
data-track-index="2"
style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;"
>
<!-- paste the contents of public/cards/card-01.html here -->
</div>
<!-- Example: card-02 with zone="side-panel" (split composition layout) → card on left half -->
<div
class="card-host clip"
data-card-id="card-02"
data-start="8.0000"
data-duration="12.0000"
data-track-index="2"
style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;"
>
<!-- card-02 HTML -->
</div>
<!-- ...one "card-host clip" per card with inline bounds matching resolveZoneBounds(card.zone)... -->
<script src="vendor/gsap.min.js"></script>
<script>
(function () {
// count-up formatter helper
window.__fmt = function (v, fmt) {
if (typeof fmt === "string" && /^\.[0-9]+f$/.test(fmt)) {
return Number(v).toFixed(Number(fmt.slice(1, -1)));
}
if (fmt === ",d") return Math.round(v).toLocaleString();
return String(Math.round(v));
};
const tl = window.gsap.timeline({ paused: true });
// ── Card lifecycle (one block per card) ──
// Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:
// Enter (fade in over 0.4s)
tl.set('.card-host[data-card-id="card-01"]', { visibility: "visible" }, 1.0);
tl.fromTo(
'.card-host[data-card-id="card-01"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
1.0,
);
// Card-internal anims (compile each data-anim-* declaration here)
tl.from(
'.card[data-card-id="card-01"] #card-01-title .char',
{ opacity: 0, y: 8, scale: 0.8, duration: 0.5, ease: "power2.out", stagger: 0.04 },
1.3,
);
tl.fromTo(
'.card[data-card-id="card-01"] #card-01-line',
{ width: 0 },
{ width: 420, duration: 0.5, ease: "power2.out" },
1.65,
);
// Exit (fade out over 0.35s, ending at endSec)
tl.to(
'.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.35, ease: "power2.in" },
7.15,
);
tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);
// ── Video framing transitions ──
// When the next card uses a different composition layout, animate the
// video-wrapper to its new bounds. Example: card-01 = fullscreen
// (video hidden behind), card-02 = split composition (zone="side-panel"
// → video on right, card on left).
// Card-02 enters at 8.0s with the split composition. Animate video to
// the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
tl.set("#video-wrap", { className: "video-wrapper framed" }, 7.5);
tl.to(
"#video-wrap",
{ left: 960, top: 0, width: 960, height: 1080, duration: 0.6, ease: "power2.inOut" },
7.5,
);
// Card-02 enter — same pattern as card-01
tl.set('.card-host[data-card-id="card-02"]', { visibility: "visible" }, 8.0);
tl.fromTo(
'.card-host[data-card-id="card-02"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
8.0,
);
// ...card-02 internal anims...
// ── repeat for each card; if the NEXT card's layout differs,
// insert another tl.to('#video-wrap', ...) tween before its enter ──
window.__timelines = window.__timelines || {};
window.__timelines["talking-head-recut"] = tl;
})();
</script>
</div>
</body>
</html>Compile each data-anim attribute into a GSAP statement. Times are
absolute seconds = card.startSec + data-anim-at, quantized to 1/fps.
Selector is .card[data-card-id="X"] #elementId.
| data-anim | GSAP statement template |
|---|---|
fade-in | tl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T); |
fade-out | tl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T); |
slide-in (from=left, dist=80) | tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T); |
kinetic-chars (pop) | tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T); |
count-up | (function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})(); |
draw-path | (function(){const el=document.querySelector(SEL);if(el){const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);}})(); |
grow-x (target-w=W) | tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T); |
grow-y (target-h=H) | tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T); |
scale-pop | tl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T); |
mask-reveal (direction=left) | tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T); |
Quantize: T = Math.round(absSec * fps) / fps. At 30fps the smallest
step is 1/30 ≈ 0.0333s; rounding to 4 decimals (.toFixed(4)) is fine
inside the JS literal.
layout value)The selector for the video container is #video-wrap. Animate its
bounds between cards using tl.to('#video-wrap', { ...bounds }, T).
Initial bounds should be set inline on the element to match card-01's
layout. Pick a transition duration of 0.5–0.7s with ease: 'power2.inOut'.
Decorative frames (clean / hairline / polaroid) sit as a
sibling of #video-wrap and follow it through layout transitions.
See
references/frames/ for each frame's placement
HTML, suggested CSS, and which layouts it pairs with. Quick rule:
overlay layout suppresses decorative frames (the full-bleed video
clashes with chrome); PiP layouts already have their own pill treatment
(border-radius + white ring + shadow), so add a decorative frame only on
top of split / stack.
GSAP target lookup table for #video-wrap per composition layout
(landscape 1920×1080; portrait is in references/layouts/*.html, and 4:5 is
derived from portrait by the proportional scaling described above):
| composition layout | typical card.zone | #video-wrap GSAP target | extra css class |
|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | — |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 } (top 52%) | — |
pip (bottom-right) | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } | pip-pill (border-radius + ring + shadow) |
pip (top-left) | fullscreen | { left: 40, top: 40, width: 400, height: 300 } | pip-pill |
overlay (video full-bleed) | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 } (no change from default) | — |
| hide video (pure-graphic moment) | fullscreen | { opacity: 0 } (or move off-canvas) | — |
To toggle the pip-pill chrome (border-radius + white ring + drop shadow) when entering or leaving a pip moment:
// Enter pip — add chrome
tl.set("#video-wrap", { className: "video-wrapper pip-pill" }, T);
tl.to(
"#video-wrap",
{ left: 1480, top: 760, width: 400, height: 300, duration: 0.6, ease: "power2.inOut" },
T,
);
// Leave pip — back to clean full-bleed
tl.set("#video-wrap", { className: "video-wrapper" }, T_NEXT);
tl.to(
"#video-wrap",
{ left: 0, top: 0, width: 1920, height: 1080, duration: 0.6, ease: "power2.inOut" },
T_NEXT,
);Card-host bounds match the zone. Resolve the card's zone into
pixel bounds using the table at the top of Step 6, then write those
into the card-host's inline style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;...". For video-overlay zone (overlay recipe), the
card-host fills the full canvas — your CSS inside .card .root
decides where the actual visible card sits.
window.__timelines["talking-head-recut"].async, setTimeout, Promises, or media play() calls.Math.random() or Date.now() in render paths.repeat: -1 is allowed only under the finite root data-duration; for loops that must end earlier, calculate finite repeats from the video duration.x, y, scale, rotation, opacity) over layout properties (top, left, width, height) for motion.#video-wrap, not the video element dimensions directly.data-track-index, not data-layer; use data-duration, not data-end.card-host, sub-composition, etc.) should include class="clip" alongside its own classes — e.g. class="card-host clip". Visibility itself is driven by data-start / data-duration: the runtime gates every [data-start] element to its window whether or not this class is present. .clip is the marker Studio and the GSAP clip-ownership rules read to recognise a clip, so leaving it off makes the element harder to edit and to lint (lint: timed_element_missing_clip_class, a warning).font-family, list concrete font names ('Inter', 'Caveat', …) — not a CSS variable like var(--font-family). The HyperFrames font resolver doesn't expand CSS vars during static analysis (lint: font_family_without_font_face). Cards may still use var(--font-family) internally since their @font-face declarations are loaded.cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=talking-head-recut \
-o output.mp4 \
--fps 30hyperframes render <dir> reads <dir>/index.html and produces the MP4.
The source program audio stays on #bg-video, so the rendered MP4 preserves the
talking-head audio without a manual remux.
The flag PRODUCER_BROWSER_GPU_MODE=hardware (or --browser-gpu) is
strongly recommended on macOS — software-only Chrome rendering times out
on most laptops.
For a sanity check before the full render, capture a single frame at a specific timestamp:
npx hyperframes snapshot public --at 5 # → public/snapshots/frame-00-at-5s.png (a single --at ignores --out)Tell the user:
storyboard.json (the card outline you designed)public/cards/*.html (one HTML per card)public/index.html (the assembled composition)output.mp4 (the final video)Optional live preview (on request only). The clip plays unchanged inside public/index.html with the overlays on top, so it previews faithfully. Don't open it during the run. When the user asks, start a long-lived server after render and report the URL:
(cd "$WORK_DIR/public" && npx hyperframes preview --background) # or `npx hyperframes play` for a shareable linkDo not delete the work directory unless the user asks.
© heygen-com, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 28 other files (references, assets) in skills/talking-head-recut of heygen-com/hyperframes.
Open the folder on GitHubat commit 8bca7c4
We found 3 copies of this SKILL.md (exact, near-identical or edited) in other folders, from 3 other GitHub owners. This page covers the copy in heygen-com/hyperframes, which our catalogue first saw on October 7, 2026.
Talking Head Recut next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Talking Head Recut this skillheygen-com/hyperframes | 60k | 3 repos | ~16k | Automated safety check: Pass | Apache-2.0 | |
| Plotbeat Data VideosUnclecheng-li/AI_Animation | 1.5k | — | ~2.6k | Automated safety check: Pass | MIT | |
| Yuv Viral Videohoodini/ai-agents-skills | 282 | — | ~7.5k | Automated safety check: Notes | None | |
| Talking-Head Video Pipelinenaive-kun/naive-video-skill | 132 | — | ~3.3k | Automated safety check: Pass | MIT | |
| Duo Foldable Phone Comparisonheygen-com/hyperframes-community-skills | 187 | — | ~2k | Automated safety check: Pass | Apache-2.0 | |
| X Posting License Videoheygen-com/hyperframes-community-skills | 187 | — | ~1.2k | Automated safety check: Pass | Apache-2.0 |
Unclecheng-li/AI_Animation
Turns a local CSV or JSON file, or data fetched from an approved source, into an editable data-visualization video by adapting HyperFrames templates.
hoodini/ai-agents-skills
Edit any selfie or screen-share footage into a viral short-form video in YUV.AI's signature style — Apple-style liquid-glass cards (real CSS backdrop-filter), dark-mode polish, MrBeast-paced cuts…
naive-kun/naive-video-skill
Turns raw or rough-cut talking-head footage into a captioned, animated final video through a staged, resumable production pipeline.
heygen-com/hyperframes-community-skills
Builds side-by-side comparison and meme videos on a fixed photo of hands holding an open foldable phone, with your own HTML playing in each screen.
heygen-com/hyperframes-community-skills
Builds a short animated posting-license card video from an X profile's name, handle, counters and avatar, using a fixed template that the agent only fills in and renders.
notivn/AIEV
Build a Vietnamese vertical TikTok explainer in the "MỔ XẺ PAPER AI" (AI paper dissection) format with HyperFrames (HTML/CSS/GSAP → MP4), Noti.vn style.
heygen-com/hyperframes
Collects motion rules, scene blueprints, transitions and runtime adapters for HyperFrames video compositions, with GSAP as the default animation runtime.
heygen-com/hyperframes
Adds captions to a single-subject talking-head video without editing the footage, from plain subtitles to cinematic text placed behind the speaker.
heygen-com/hyperframes
Turns a weekly changelog markdown file into a branded HyperFrames video with voiceover, animated mock-UI scenes and captions, using fonts, background and scripts bundled in the skill.
heygen-com/hyperframes
Turns an article, notes or a topic brief into an explainer video whose visuals are invented per scene, built frame by frame in HyperFrames with no footage.
heygen-com/hyperframes
Imports Figma assets, brand tokens, components and motion into a HyperFrames video composition, using the Figma REST API with a connector or native export for shaders.
heygen-com/hyperframes
Finds, generates and edits media for HyperFrames video projects: music, sound effects, images, icons, logos, voiceovers, captions and color grades.
Categories
Lays timed graphic cards such as lower-thirds, data callouts and quotes over an existing talking-head video, synced to the transcript. An existing talking-head, interview or podcast clip plays in full while designed graphic cards appear on top, timed to the transcript. Cards can be kinetic titles, lower-thirds, data callouts, quotes, side panels or picture-in-picture, and the canvas can be 16:9, 9:16 or 4:5.
Talking Head Recut fits situations like: adding lower-thirds and data callouts to an interview recording; packaging a podcast clip with quote cards for a vertical social post; dressing up a talking-head video with kinetic titles synced to what is said.
Run `npx skills add heygen-com/hyperframes --skill talking-head-recut -a claude-code`. Or copy the skill folder (skills/talking-head-recut in heygen-com/hyperframes) into .claude/skills/talking-head-recut in your project. Claude Code loads it when a task matches its description.
Run `npx skills add heygen-com/hyperframes --skill talking-head-recut -a codex`. Or copy the skill folder (skills/talking-head-recut in heygen-com/hyperframes) into .agents/skills/talking-head-recut in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add heygen-com/hyperframes --skill talking-head-recut -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/talking-head-recut, .gemini/skills/talking-head-recut, .github/skills/talking-head-recut and .opencode/skills/talking-head-recut in your project.
Going by SKILL.md and its folder, Talking Head Recut needs JavaScript for the scripts in its folder and the command-line tools its instructions call (npx, ffmpeg and ffprobe). Our summary lists: npx, to run the hyperframes CLI; A local talking-head video file that plays in full.
SKILL.md contains no URLs. Its commands use npx, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Talking Head Recut is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 16k tokens (SKILL.md is roughly 65k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 25k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Talking Head Recut: Plotbeat Data Videos (Unclecheng-li/AI_Animation, 1.5k stars), Yuv Viral Video (hoodini/ai-agents-skills, 282 stars), Talking-Head Video Pipeline (naive-kun/naive-video-skill, 132 stars) and Duo Foldable Phone Comparison (heygen-com/hyperframes-community-skills, 187 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
heygen-com (a GitHub organization) maintains it in heygen-com/hyperframes, which has 60,472 GitHub stars. The repository holds 32 skills in this directory. The repository was last updated on October 11, 2026.
Source: heygen-com/hyperframes on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.