feat: add HyperFrames skills for AI coding tools

Add compose-video, captions, transitions, media, social-media, and
typography-and-assets skills for AI coding assistants. Remove duplicated
typography/assets content from compose-video SKILL.md (now references
typography-and-assets.md). Fix captions skill to reference
--output-json-full flag matching actual whisper CLI usage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-25 22:51:27 -07:00
co-authored by Claude Opus 4.6
parent 31ad8b98d8
commit b7735d609f
9 changed files with 392 additions and 1696 deletions
-291
View File
@@ -1,291 +0,0 @@
---
name: captions
description: Build timed caption compositions from whisper.cpp transcripts in HyperFrames. Use when adding captions, subtitles, or timed text to a composition that has a transcript.json file.
---
# Captions
Build HyperFrames caption compositions from whisper.cpp transcript data.
## When to Use
- Project has a `transcript.json` (output from whisper.cpp `--output-json`)
- User asks to "add captions", "add subtitles", or "show the words"
- Editing an existing captions composition
## Whisper.cpp Transcript Format
The transcript file is whisper.cpp's native JSON output. Word-level timestamps are in the `tokens` array within each segment:
```json
{
"transcription": [
{
"timestamps": { "from": "00:00:00,000", "to": "00:00:05,000" },
"offsets": { "from": 0, "to": 5000 },
"text": " Hello world.",
"tokens": [
{
"text": " Hello",
"offsets": { "from": 0, "to": 1000 },
"p": 0.98
},
{
"text": " world",
"offsets": { "from": 1000, "to": 2000 },
"p": 0.95
}
]
}
]
}
```
Key fields per token:
- `text` — word text (often has leading space for word-initial tokens)
- `offsets.from` / `offsets.to` — milliseconds
- `p` — confidence (0-1)
### Normalizing to a Word Array
Flatten the transcript into a simple word array for grouping:
```js
const words = [];
for (const segment of transcript.transcription) {
for (const token of segment.tokens || []) {
const text = token.text.trim();
if (!text) continue;
words.push({
text: text,
start: token.offsets.from / 1000,
end: token.offsets.to / 1000,
});
}
}
```
## Word Grouping
Group words into caption lines. Never show more than 5 words at once. Break on:
1. **Sentence boundaries** — `.` `?` `!`
2. **Pauses** — gap > 150ms between consecutive words
3. **Max word count** — 3-5 words per group (3 for portrait, 5 for landscape)
```js
function groupTranscript(words, maxWords) {
const groups = [];
let current = [];
words.forEach((word, i) => {
current.push(word);
const next = words[i + 1];
const isPause = next && next.start - word.end > 0.15;
const isSentenceEnd = /[.?!]/.test(word.text);
if (current.length >= maxWords || isPause || isSentenceEnd || !next) {
groups.push({
text: current.map((w) => w.text).join(" "),
start: current[0].start,
end: current[current.length - 1].end,
});
current = [];
}
});
return groups;
}
```
## Composition Structure
Caption compositions follow the standard HyperFrames composition format:
```html
<template id="captions-template">
<div data-composition-id="captions" data-width="1920" data-height="1080" data-duration="30">
<div id="captions-container"></div>
<style>
[data-composition-id="captions"] {
width: 1920px;
height: 1080px;
pointer-events: none;
}
[data-composition-id="captions"] #captions-container {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.caption-group {
position: absolute;
opacity: 0;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
// Word array from transcript — replace with actual data
const WORDS = [
/* ... */
];
const container = document.getElementById("captions-container");
const tl = gsap.timeline({ paused: true });
const groups = groupTranscript(WORDS, 5);
groups.forEach((group, i) => {
const el = document.createElement("div");
el.className = "caption-group";
el.textContent = group.text;
container.appendChild(el);
// Entrance
tl.fromTo(
el,
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.3, ease: "expo.out" },
group.start,
);
// Exit
tl.to(el, { opacity: 0, y: -10, duration: 0.25, ease: "expo.out" }, group.end - 0.25);
});
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
```
## Positioning
- **Landscape (1920x1080):** Bottom third — `bottom: 80-120px`, centered horizontally
- **Portrait (1080x1920):** Lower middle — `bottom: 600-700px`, centered horizontally
- Use `position: absolute` on caption groups, stacked in the same container
- Set `pointer-events: none` on the composition so captions don't block clicks
- **Never use relative positioning for captions.** Relative positioning creates a single long line that overflows off screen. Caption groups must be absolutely positioned so hidden words don't affect layout.
- **Avoid covering the subject's face.** Position captions in the lower third or upper portion, away from the speaker.
- **Maintain adequate padding** from screen edges (80px minimum on sides)
## Animation Patterns
### Fade + slide (clean, modern)
```js
tl.fromTo(el, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.4, ease: "expo.out" }, start);
tl.to(el, { opacity: 0, y: -10, duration: 0.3, ease: "expo.out" }, end - 0.3);
```
### Pop-in bounce (playful)
```js
tl.to(el, { autoAlpha: 1, scale: 1.1, duration: 0.2, ease: "power2.out" }, start);
tl.to(el, { scale: 1, duration: 0.1, ease: "power2.inOut" }, start + 0.2);
tl.to(el, { autoAlpha: 0, scale: 0, duration: 0.15, ease: "power2.in" }, end);
```
### Hard cut (no animation)
```js
tl.set(el, { display: "flex" }, start);
tl.set(el, { display: "none" }, end);
```
## Dynamic Font Sizing
If a caption group is too wide for the viewport, scale the font size down:
```js
const maxWidth = viewportWidth - 160; // 80px padding each side
const el = document.createElement("div");
el.className = "caption-group";
el.textContent = group.text;
el.style.fontSize = baseFontSize + "px";
container.appendChild(el);
// Measure and scale if needed
const measured = el.getBoundingClientRect().width;
if (measured > maxWidth) {
const scale = maxWidth / measured;
el.style.fontSize = Math.floor(baseFontSize * scale) + "px";
}
```
Scale stroke width and spacing proportionally when scaling font size.
## Active Word Highlighting
Highlight the currently spoken word within a caption group by changing its color:
```js
// Create individual word spans within the group
const wordSpans = group.words.map((w) => {
const span = document.createElement("span");
span.textContent = w.text + " ";
span.style.color = defaultColor;
el.appendChild(span);
return { span, start: w.start, end: w.end };
});
// Animate each word's color at its start time
wordSpans.forEach((ws) => {
tl.to(ws.span, { color: accentColor, duration: 0.05 }, ws.start);
tl.to(ws.span, { color: defaultColor, duration: 0.05 }, ws.end);
});
```
## Timing Rules
- **Entrance animation must complete before exit starts.** If a group is shorter than entrance + exit duration, reduce animation durations.
- **Exit before next group enters.** Use `Math.min(group.end, nextGroup.start)` as the hide time.
- **Minimum display time:** 0.3s — skip groups shorter than this.
- **No overlapping caption groups.** Only one group visible at a time.
- **Sync to actual audio.** Captions must match word-level timestamps from the transcript, not estimated timing.
## Constraints
- **Deterministic.** No `Math.random()`, no `Date.now()`.
- **Register timeline.** `window.__timelines["captions"] = tl;`
- **IIFE scope.** Wrap script in `(function() { ... })()` to avoid globals.
- **Scoped CSS.** Prefix all selectors with `[data-composition-id="captions"]`.
- **Set `data-duration`.** Must match or exceed the last caption's end time.
- **Do not animate `visibility` or `display` on timed clips.** Use `opacity` or `autoAlpha`.
- **Do not use `tl.set()` for property changes that need to survive timeline seeking.** Use `tl.to()` with a short duration instead.
- **Keep it simple.** Simpler captions perform better than fancy ones. Avoid excessive effects.
## Common Mistakes
- Captions not synced to actual audio (using estimated timing instead of transcript)
- Multiple caption groups visible simultaneously — only ONE group at a time
- Text too small to read on mobile devices
- Insufficient contrast between text and video background
- Captions covering the subject's face
- Using relative positioning (creates overflowing single line)
- All words the same size and style (no visual hierarchy)
- Overusing animation effects (less is more)
## Loading in the Root Composition
```html
<div
id="el-captions"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="0"
data-duration="30"
data-track-index="5"
data-width="1920"
data-height="1080"
></div>
```
-556
View File
@@ -1,556 +0,0 @@
---
name: compose-video
description: Create HyperFrames HTML video compositions. Use when asked to create a video, build an animation, make a composition, add a title card, or generate any HTML-based video content for HyperFrames.
---
# Compose Video
Create HyperFrames HTML compositions — the HTML files that become videos. This skill contains everything you need to generate valid compositions that work in the studio preview and the producer render pipeline.
## When to Use
- "Create a video about..."
- "Make a composition with..."
- "Build an animation that..."
- "Add a title card / lower third / overlay"
- "Wrap this video in a produced composition"
- Any task that produces an `index.html` for HyperFrames
## How HyperFrames Works
HTML is the source of truth for video. A composition is an HTML file where:
- **HTML elements** with `data-*` attributes define what appears and when
- **CSS** controls positioning and appearance
- **GSAP timeline** drives animations
- **The framework** automatically handles clip visibility, media playback, and timeline sync
You write the HTML. The framework does the rest.
### Framework-Managed Behavior
The framework reads data attributes and automatically manages:
- **Primitive clip timeline entries** — the framework reads `data-start`, `data-duration`, and `data-track-index` from primitive clips and adds them to the composition's GSAP timeline. You do not manually add primitive clips to the timeline in scripts.
- **Media playback** (play, pause, seek) for `<video>` and `<audio>`
- **Clip lifecycle** — clips are **mounted** (made visible on screen) and **unmounted** (removed from screen) based on `data-start` and `data-duration`
- **Timeline synchronization** (keeping media in sync with the GSAP master timeline)
- **Media loading** — the framework waits for all media elements to load before resolving timing and starting playback
Mounting and unmounting controls **presence**, not appearance. A clip that is mounted is on screen; a clip that is unmounted is not. Transitions (fade in, slide in, etc.) are separate — they are animated in scripts and happen _after_ a clip is mounted or _before_ it is unmounted.
The framework does **not** handle transitions, effects, or visual animation — those are driven by GSAP in JavaScript.
Do not manually call `video.play()`, `video.pause()`, set `audio.currentTime`, or mount/unmount clips in scripts. The framework owns media playback and clip lifecycle.
## Viewport
Every composition must include `data-width` and `data-height` so scripts and CSS can reference concrete pixel dimensions for layout.
| Format | Width | Height |
| --------- | ----- | ------ |
| Landscape | 1920 | 1080 |
| Portrait | 1080 | 1920 |
## Compositions
A composition is the fundamental grouping unit. Every clip — video, image, audio — must live inside a composition. The `index.html` file is itself a composition (the top-level one), and it can contain nested compositions. Any composition can be imported into another as a sub-composition — there is no special "root" type.
A composition carries the same core attributes as any other clip (`id`, `data-start`, `data-track-index`, `data-duration`), so it can be placed and timed on a timeline just like a video or image.
**Critical Rule: Every top-level HTML container MUST be a composition (i.e., have a `data-composition-id` attribute). You cannot create a top-level HTML container without an associated composition. All visual content must live inside a composition with data attributes to appear in the timeline.**
### Composition File Structure
Each composition should be defined in its own HTML file. This keeps compositions modular, reusable, and maintainable.
```
project/
├── index.html # Root composition
├── compositions/
│ ├── intro-anim.html # Intro animation composition
│ ├── caption-overlay.html # Caption composition
│ └── outro-title.html # Outro composition
```
**Composition file format** (`compositions/intro-anim.html`):
```html
<template id="intro-anim-template">
<div data-composition-id="intro-anim" data-width="1920" data-height="1080">
<div class="title">Welcome!</div>
<div class="subtitle">Let's get started</div>
<style>
[data-composition-id="intro-anim"] .title {
font-size: 72px;
color: white;
text-align: center;
}
[data-composition-id="intro-anim"] .subtitle {
font-size: 36px;
color: #ccc;
text-align: center;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
const tl = gsap.timeline({ paused: true });
tl.from(".title", { opacity: 0, y: -50, duration: 1 });
tl.from(".subtitle", { opacity: 0, y: 50, duration: 1 }, 0.5);
window.__timelines["intro-anim"] = tl;
</script>
</div>
</template>
```
**GSAP must be included in every composition.** Each composition file must include `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>` before any script that uses GSAP. Compositions are loaded independently, so they cannot rely on a parent document having loaded GSAP.
The composition root inside the template must include `data-width` and `data-height`.
### Loading Compositions
Use `data-composition-src` to load a composition from an external HTML file:
```html
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-track-index="3"
></div>
```
The framework will:
1. Fetch the HTML file specified in `data-composition-src`
2. Extract the `<template>` content
3. Clone and mount it into the composition element
4. Execute any `<script>` tags within the template
5. Register the timeline in `window.__timelines`
### When to Use Separate Files vs Inline
**Use separate HTML files when:**
- The composition is reusable across multiple projects or scenes
- The composition has complex logic, styling, or structure (>20 lines)
- You want to keep the main `index.html` clean and focused on orchestration
**Use inline compositions when:**
- The composition is truly one-off and project-specific
- The composition is very simple (<10 lines total)
- You're prototyping and iterating quickly
## Clip Types
A clip is any discrete block on the timeline, represented as an HTML element with data attributes.
- `<video>` — Video clips, B-roll, A-roll
- `<img>` — Static images, overlays
- `<audio>` — Music, sound effects
- `<div data-composition-id="...">` — Nested compositions (animations, grouped sequences)
## HTML Attributes
### All Clips
| Attribute | Required | Values |
| ------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| `id` | Yes | Unique identifier (e.g., `"el-1"`) |
| `data-start` | Yes | Seconds (e.g., `"0"`, `"5.5"`) or clip ID reference (e.g., `"el-intro"`) |
| `data-duration` | Required for img/div/compositions, optional for video/audio | Seconds. Video/audio defaults to source media duration if omitted. |
| `data-track-index` | Yes | Integer. Clips on the same track **cannot overlap in time**. |
| `data-media-start` | No | Trim offset into source file (seconds) |
| `data-volume` | No | 0-1 (default 1) |
**Important:** `data-track-index` does **not** affect visual layering — use CSS `z-index` to control which elements render in front of others.
### Composition Clips
| Attribute | Required | Values |
| ---------------------------- | -------- | --------------------------------------------------------------------------- |
| `data-composition-id` | Yes | Unique composition ID |
| `data-duration` | Yes | Explicit duration in seconds. Takes precedence over GSAP timeline duration. |
| `data-width` / `data-height` | Yes | Pixel dimensions |
| `data-composition-src` | No | Path to external HTML file containing the composition template |
## Relative Timing
Instead of calculating absolute start times, a clip can reference another clip's `id` in its `data-start` attribute. This means "start when that clip ends."
```html
<video id="intro" data-start="0" data-duration="10" data-track-index="0" src="..."></video>
<video id="main" data-start="intro" data-duration="20" data-track-index="0" src="..."></video>
<video id="outro" data-start="main" data-duration="5" data-track-index="0" src="..."></video>
```
`main` resolves to second 10, `outro` resolves to second 30. If `intro`'s duration changes, downstream clips shift automatically.
### Offsets (gaps and overlaps)
```html
<!-- 2s gap after intro -->
<video
id="scene-a"
data-start="intro + 2"
data-duration="20"
data-track-index="0"
src="..."
></video>
<!-- 0.5s overlap for crossfade (different track since same-track clips can't overlap) -->
<video
id="scene-b"
data-start="intro - 0.5"
data-duration="20"
data-track-index="1"
src="..."
></video>
```
### Rules
- **Same composition only** — references resolve within the clip's parent composition
- **No circular references** — A cannot start after B if B starts after A
- **Referenced clip must have a known duration** — either explicit `data-duration` or inferred from source media
- **Parsing** — if the value is a valid number, it is absolute seconds; otherwise parsed as `<id>`, `<id> + <number>`, or `<id> - <number>`
## Video Clips
```html
<video
id="el-1"
data-start="0"
data-duration="15"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
```
Videos MUST be `muted` and `playsinline`. Audio MUST be a separate `<audio>` element, even if the source is the same file.
## Audio Clips
```html
<audio
id="el-audio"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="0.8"
></audio>
```
Never rely on `<video>` for audio. The framework manages video and audio playback independently.
## Two Layers: Primitives and Scripts
Every composition has the same two layers:
- **HTML** — primitive clips (`video`, `img`, `audio`, nested `div[data-composition-id]`). Declarative structure: what plays, when, and on which track.
- **Script** — effects, transitions, dynamic DOM, canvas, SVG — creative animation and visuals via GSAP. Scripts do **not** control media playback or clip visibility.
### Script Isolation
Each composition's script is scoped to that composition. When loaded via `data-composition-src`, its inline `<script>` and `<style>` tags are automatically included and scoped.
## Timeline Contract
The framework initializes `window.__timelines = {}` before any scripts run. Every composition **must** create a GSAP timeline and register it:
```js
const tl = gsap.timeline({ paused: true });
// ... add tweens, nested timelines, etc.
window.__timelines["<data-composition-id>"] = tl;
```
### Rules
- **Every composition needs a script** — at minimum, to create and register its timeline.
- **All timelines start paused** — create with `{ paused: true }`. The top-level timeline is controlled externally by the player or renderer.
- **Framework auto-nests sub-timelines** — you do **not** need to manually add sub-composition timelines to the master timeline. The framework automatically nests any timeline registered in `window.__timelines` into its parent based on `data-start`.
- **Duration source** — a composition's duration comes from its required `data-duration` attribute. `data-duration` takes precedence over the GSAP timeline duration.
- **NEVER create empty tweens** like `tl.to({}, { duration: N })` or `tl.set({}, {}, N)` just to set duration — use `data-duration` instead.
- **Timelines must be finite** — every composition must have a concrete duration via `data-duration`, and its GSAP timeline must have tweens with explicit durations that sum to > 0.
### What NOT to do
```js
// UNNECESSARY - the framework does this automatically
if (window.__timelines["captions"]) {
masterTL.add(window.__timelines["captions"], 0);
}
// WRONG - use data-duration instead
tl.set({}, {}, VIDEO_DURATION_IN_SECONDS);
```
## Rules (Non-Negotiable)
### Deterministic Output (CRITICAL)
All animations, scripts, and timeline logic MUST be completely deterministic. The renderer executes compositions in a headless server environment and must produce **byte-for-byte identical output** every time.
**NEVER use:**
- `Math.random()` or any randomness
- `Date.now()`, `performance.now()`, or time-based logic
- Probabilistic behavior of any kind
**Use instead:**
- Array indexing with predictable patterns (e.g., `items[index % items.length]`)
- Deterministic functions based on clip index, timeline position, or other known values
- Fixed sequences defined explicitly in arrays or data structures
### GSAP Animation Conflicts
Never animate the same CSS property on an element from multiple timelines simultaneously — this causes flickering/artifacts in headless (Puppeteer) renders even if it looks fine in a browser.
- Animate each property only once per element at any given time
- If a selector (`.item`, tag) matches multiple elements, do NOT also animate those elements individually at the same time
- Ensure animation targets are mutually exclusive sets of elements
```js
// BAD - same elements animated twice simultaneously
tl.to(".item", { textShadow: "..." }, 0);
items.forEach((item, i) => {
tl.fromTo(item, { opacity: 0 }, { opacity: 1 }, i * 0.1);
});
// GOOD - sequential timing
items.forEach((item, i) => {
tl.fromTo(item, { opacity: 0 }, { opacity: 1 }, i * 0.1);
});
tl.to(".item", { textShadow: "..." }, items.length * 0.1 + 0.5);
// GOOD - parent/child separation
tl.to(".container", { scale: 1.1 }, 0);
items.forEach((item, i) => {
tl.fromTo(item, { opacity: 0 }, { opacity: 1 }, i * 0.1);
});
```
### GSAP Animations
Only animate visual properties. The framework owns everything else.
**DO animate:** `opacity`, `x`, `y`, `scale`, `rotation`, `color`, `backgroundColor`, `width`/`height` on non-media elements, `borderRadius`, CSS transforms
**DO NOT animate or control:**
- `visibility` (framework manages this)
- `video.play()` / `video.pause()` / `video.currentTime`
- `audio.play()` / `audio.pause()` / `audio.currentTime`
- `display` property on timed clips
## Never Do
1. **Never forget `window.__timelines` registration** — nothing works without it
2. **Never use video for audio** — always muted video + separate audio element
3. **Never nest video inside a timed div** — video must be a direct stage child (or inside a non-timed wrapper)
4. **Never use `data-layer`** — use `data-track-index`
5. **Never use `data-end` in source** — use `data-duration`
6. **Never animate video element dimensions** — use a non-timed wrapper div
7. **Never call play/pause/seek on media** — framework owns media playback
8. **Never create empty tweens to set duration** — use `data-duration` on the composition
9. **Never use `Math.random()` or any non-deterministic logic** — renderer requires identical output every time
10. **Never create a top-level container without `data-composition-id`** — all visual content must be in a composition
## Wrapping Dynamic Content in Compositions
When you have dynamic or script-animated content (captions, emojis, overlays, text animations), wrap them in a composition element. Children inside can be freely created and animated via JavaScript — they don't need individual data attributes.
### Wrong: Dynamic content outside a composition
```html
<!-- BAD: captions-container is not a composition - won't appear in timeline -->
<div id="ui-layer">
<div id="captions-container"></div>
<div class="emoji" id="emoji-1">🤩</div>
</div>
```
### Correct: Compositions loaded from files (preferred)
```html
<div
id="captions-comp"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="0"
data-track-index="5"
></div>
<div
id="emojis-comp"
data-composition-id="emojis"
data-composition-src="compositions/emojis.html"
data-start="0"
data-track-index="6"
></div>
```
### Correct: Inline composition (for simple one-offs)
```html
<div
id="captions-comp"
data-composition-id="captions"
data-start="0"
data-duration="60"
data-track-index="5"
data-width="1920"
data-height="1080"
>
<div id="captions-container"></div>
<script>
const captionTL = gsap.timeline({ paused: true });
// Dynamically create and animate caption groups...
window.__timelines["captions"] = captionTL;
</script>
</div>
```
## Patterns
### Picture-in-Picture (Video in a Frame)
Animate a wrapper div for position/size. The video fills the wrapper. The wrapper has NO data attributes.
```html
<div
id="pip-frame"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:50;overflow:hidden;"
>
<video
id="el-video"
data-start="0"
data-duration="60"
data-track-index="0"
src="talking-head.mp4"
muted
playsinline
></video>
</div>
```
```js
tl.to(
"#pip-frame",
{ top: 700, left: 1360, width: 500, height: 280, borderRadius: 16, duration: 1 },
10,
);
tl.to("#pip-frame", { left: 40, duration: 0.6 }, 30);
```
### Title Card with Fade
```html
<div
id="title-card"
data-start="0"
data-duration="5"
data-track-index="5"
style="display:flex;align-items:center;justify-content:center;background:#111;z-index:60;"
>
<h1 style="font-size:64px;color:#fff;opacity:0;">My Video Title</h1>
</div>
```
```js
tl.to("#title-card h1", { opacity: 1, duration: 0.6 }, 0.3);
tl.to("#title-card", { opacity: 0, duration: 0.5 }, 4);
```
### Slide Show with Section Headers
Use separate elements on the same track, each with its own time range. Slides auto-mount/unmount based on `data-start`/`data-duration`.
```html
<div class="slide" data-start="0" data-duration="30" data-track-index="3">...</div>
<div class="slide" data-start="30" data-duration="25" data-track-index="3">...</div>
<div class="slide" data-start="55" data-duration="20" data-track-index="3">...</div>
```
## Top-Level Composition Example
```html
<div
id="comp-1"
data-composition-id="my-video"
data-start="0"
data-duration="60"
data-width="1920"
data-height="1080"
>
<!-- Primitive clips -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-track-index="0"
src="..."
muted
playsinline
></video>
<video
id="el-2"
data-start="el-1"
data-duration="8"
data-track-index="0"
src="..."
muted
playsinline
></video>
<img id="el-3" data-start="5" data-duration="4" data-track-index="1" src="..." />
<audio id="el-4" data-start="0" data-duration="30" data-track-index="2" src="..." />
<!-- Sub-compositions loaded from files -->
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-track-index="3"
></div>
<div
id="el-6"
data-composition-id="captions"
data-composition-src="compositions/caption-overlay.html"
data-start="0"
data-track-index="4"
></div>
<script>
// Just register the timeline — framework auto-nests sub-compositions
const tl = gsap.timeline({ paused: true });
window.__timelines["my-video"] = tl;
</script>
</div>
```
## Rendering
Compositions render to MP4 via the producer. The producer loads the HTML in headless Chrome, seeks frame-by-frame, screenshots each frame, and encodes to H.264 with FFmpeg. Preview and render use the same runtime — WYSIWYG.
## Output Checklist
- [ ] Every top-level HTML container is a composition (`data-composition-id` required)
- [ ] Every composition has `data-width` and `data-height` attributes
- [ ] Each composition has a `data-duration` attribute
- [ ] NEVER use empty tweens like `tl.to({}, { duration: N })` — use `data-duration` instead
- [ ] Each reusable composition is in its own HTML file (in `compositions/` directory)
- [ ] Compositions loaded via `data-composition-src` attribute
- [ ] Each composition file uses `<template>` tag to wrap its content
- [ ] `window.__timelines` given all compositions' timelines
- [ ] ALL code is 100% deterministic — no `Math.random()`, `Date.now()`, or any randomness
- [ ] Every composition includes its own GSAP script tag: `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>`
-299
View File
@@ -1,299 +0,0 @@
---
name: media
description: Video, audio, and image handling constraints for HyperFrames. Use when adding media elements to a composition or troubleshooting media playback.
---
# Media
Constraints for working with video, audio, and images in HyperFrames compositions.
## When to Use
- Adding video, audio, or image clips to a composition
- Troubleshooting media that won't play or render
- Working with transparent video, trimming, or volume control
## Video
### Required Attributes
Every `<video>` element must have:
```html
<video
id="el-1"
data-start="0"
data-duration="15"
data-track-index="0"
src="video.mp4"
muted
playsinline
crossorigin="anonymous"
></video>
```
- **`muted`** — required. Video elements must not carry audio. Use a separate `<audio>` element.
- **`playsinline`** — required. Prevents fullscreen on mobile.
- **`crossorigin="anonymous"`** — required for CORS-safe rendering in headless Chrome.
### Supported Codecs
The renderer uses headless Chrome. Only web-compatible codecs work:
| Codec | Container | Works |
| ---------- | --------- | --------------------- |
| H.264 | MP4 | Yes |
| VP8 | WebM | Yes |
| VP9 | WebM | Yes (including alpha) |
| AV1 | MP4/WebM | Yes |
| HEVC/H.265 | MP4 | No |
| ProRes | MOV | No |
If the source video uses an unsupported codec, transcode first:
```bash
ffmpeg -i input.mov -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k output.mp4
```
### Transparent Video
Use WebM VP9 with alpha channel for overlays (talking heads, animated elements):
```html
<video
id="el-overlay"
data-start="0"
data-duration="10"
data-track-index="2"
src="overlay.webm"
muted
playsinline
crossorigin="anonymous"
style="z-index:50;"
></video>
```
Encode transparent video:
```bash
ffmpeg -i input.mov -c:v libvpx-vp9 -pix_fmt yuva420p -b:v 2M output.webm
```
### Trimming
Use `data-media-start` to set an in-point offset into the source file:
```html
<!-- Play from 5s into the source, for 10s -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-media-start="5"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
```
The framework seeks the video to the `data-media-start` position and plays from there.
### Video Duration
If `data-duration` is omitted on a video element, the framework uses the source media's intrinsic duration. For predictable timing, always set `data-duration` explicitly.
## Audio
### Separate Audio Element
Audio must always be a separate `<audio>` element, even if the audio source is the same file as a video:
```html
<!-- Video (muted) -->
<video
id="el-video"
data-start="0"
data-duration="30"
data-track-index="0"
src="interview.mp4"
muted
playsinline
></video>
<!-- Audio from same file -->
<audio
id="el-audio"
data-start="0"
data-duration="30"
data-track-index="2"
src="interview.mp4"
data-volume="1"
></audio>
```
### Volume Control
Use `data-volume` (0-1) to set playback volume:
```html
<audio
id="el-music"
data-start="0"
data-duration="60"
data-track-index="3"
src="bgm.mp3"
data-volume="0.3"
></audio>
```
### Background Music
Background music should run for the entire video duration at a lower volume than the main audio:
- **Volume:** 30-50% relative to voice (`data-volume="0.3"` to `data-volume="0.5"`)
- **Style:** Instrumental only — no vocals or lyrics that compete with speech
- **Timing:** Start at beginning, loop or extend to cover entire video
- **Track:** Use a dedicated track index separate from voice and SFX
```html
<audio
id="el-bgm"
data-start="0"
data-duration="60"
data-track-index="3"
src="bgm.mp3"
data-volume="0.3"
></audio>
```
### Sound Effects
- **Duration:** Short — around 0.5 seconds
- **Volume:** Lower than main audio (30-50% relative to voice)
- **Placement:** At visual transitions (scene changes, zoom cuts, text reveals)
- **Frequency:** 1-2 per video at key moments — don't overuse
```html
<audio
id="el-whoosh"
data-start="3"
data-duration="0.5"
data-track-index="4"
src="whoosh.mp3"
data-volume="0.4"
></audio>
```
### Audio Trimming
Same as video — use `data-media-start`:
```html
<audio
id="el-sfx"
data-start="5"
data-duration="3"
data-media-start="10"
data-track-index="4"
src="effects.mp3"
></audio>
```
## Images
```html
<img
id="el-bg"
data-start="0"
data-duration="30"
data-track-index="1"
src="background.jpg"
style="width:1920px;height:1080px;object-fit:cover;"
/>
```
- `data-duration` is required on images (no intrinsic duration)
- Use `object-fit: cover` for full-bleed backgrounds
- Use `z-index` for layering with video
## Picture-in-Picture
To animate video position/size, wrap it in a non-timed div. Animate the wrapper, not the video:
```html
<div
id="pip-frame"
style="
position:absolute; top:0; left:0;
width:1920px; height:1080px;
z-index:50; overflow:hidden;
"
>
<video
id="el-video"
data-start="0"
data-duration="60"
data-track-index="0"
src="talking-head.mp4"
muted
playsinline
style="width:100%;height:100%;object-fit:cover;"
></video>
</div>
```
```js
// Shrink to corner at 10s
tl.to(
"#pip-frame",
{
top: 700,
left: 1360,
width: 500,
height: 280,
borderRadius: 16,
duration: 1,
ease: "power2.inOut",
},
10,
);
```
**Key rules:**
- The wrapper div has **no** `data-start`, `data-duration`, or `data-track-index`
- The video inside has all the data attributes
- Animate the wrapper's CSS properties (top, left, width, height)
- Never animate the video element's dimensions directly
## Framework Owns Playback
The framework manages all media playback. Never do any of these in scripts:
```js
// ALL OF THESE ARE WRONG
video.play();
video.pause();
video.currentTime = 5;
audio.play();
audio.pause();
audio.volume = 0.5; // use data-volume attribute instead
```
The framework:
- Plays/pauses media in sync with the master timeline
- Seeks media when the timeline is scrubbed
- Waits for media to load before resolving timing
## Constraints
- **Video must be `muted`.** Audio comes from `<audio>` elements.
- **Video must be `playsinline`.** Required for headless rendering.
- **Do not nest video inside a timed div.** Video must be a direct child of the composition, or inside a non-timed wrapper.
- **Do not animate video element dimensions.** Animate a wrapper div instead.
- **Do not call play/pause/seek on media.** Framework owns playback.
- **Set `data-duration` explicitly** on all clips for predictable timing.
- **Use web-compatible codecs.** H.264, VP8, VP9, AV1 only.
- **Separate audio from video.** Always use `<audio>` for sound.
-226
View File
@@ -1,226 +0,0 @@
---
name: social-media
description: Create engaging TikTok/Instagram/YouTube Shorts vertical videos with hooks, captions, emojis, and effects in HyperFrames. Use when building social media video content or optimizing compositions for short-form vertical platforms.
---
# Social Media Video
Guidelines for creating engaging short-form vertical videos (TikTok, Instagram Reels, YouTube Shorts) in HyperFrames.
## When to Use
- Building a vertical (9:16) video for social platforms
- User asks for "TikTok style", "Reels", "Shorts", or "social media video"
- Adding hooks, emojis, or zoom cuts to a composition
## Viewport
Portrait format: `data-width="1080" data-height="1920"`
Account for platform UI safe areas:
- **Top:** 120px (status bar, platform header)
- **Bottom:** 200px (platform controls, description)
- **Right edge:** 80px (like/comment/share buttons on TikTok)
## Hook Section (First 3 Seconds)
The hook grabs attention immediately. It's a separate composition that plays before captions start.
### Text Strategy
- **3-5 words** — short and punchy
- **Highlight 1-2 key words** with accent color or different size
- Extract the key question or provocative phrase from the opening script
### Layout
- Split text across multiple lines with slight rotation and horizontal offset
- Mix small and large text for size contrast and visual hierarchy
- Center the hook as a unit on screen
### Typography
- Bold, heavy fonts (Inter Bold, Montserrat ExtraBold)
- Lowercase for casual, approachable feel
- High contrast colors (white with heavy black stroke, or black on colored background)
- Bright accent colors for emphasis (lime green, yellow, bright pink)
### Styles
**Bubble style:** Rounded background behind each word/phrase
```css
.hook-word {
background: #ffffff;
color: #000000;
padding: 12px 24px;
border-radius: 12px;
font-weight: 800;
}
.hook-word.accent {
background: #c8ff00; /* lime accent */
}
```
**Outlined style:** Heavy stroke, no background
```css
.hook-word {
color: #ffffff;
-webkit-text-stroke: 4px #000000;
paint-order: stroke fill;
font-weight: 900;
}
```
### Animation
- **Display:** Static and fully visible during hook period (no entrance animation)
- **Exit:** Quick exit in the last 250-350ms — slide off, zoom + fade, or scale up
```js
// Hook exit at 2.7s (hook lasts 3s)
tl.to(
".hook-text",
{
x: -1080,
duration: 0.3,
ease: "power3.in",
},
2.7,
);
```
## Captions
See the `captions` skill for full details. Social media specific additions:
- **Start after hook exits** — don't overlap hook and captions
- **Style consistency** — use the same font family and style (bubble or outlined) as the hook
- **2-4 words per group** for mobile readability (smaller than landscape)
- **Active word highlight** — change the current word's color to the accent color
- **Dynamic sizing** — scale font down if caption group is too wide for 1080px minus padding
## Emojis
Triggered by keywords in the transcript. Appear near (but not overlapping) the subject's face.
### Rules
- **One emoji visible at a time**
- **~1 second gap** between emoji appearances
- **3-4 emojis per video** — moderate use
- **Large enough to notice**, consistent size across all emojis
- **Thematically relevant** to the spoken word that triggers them
### Timing
```js
// Trigger emoji when keyword is spoken
const emojiTriggers = [
{ emoji: "🔥", keyword: "amazing", time: 5.2 },
{ emoji: "💡", keyword: "idea", time: 8.7 },
{ emoji: "🚀", keyword: "launch", time: 12.1 },
];
emojiTriggers.forEach(({ emoji, time }, i) => {
const el = document.createElement("div");
el.className = "emoji-overlay";
el.textContent = emoji;
el.style.cssText = `position:absolute;font-size:80px;opacity:0;`;
// Vary positions: upper-right, left-mid, lower-right, upper-left
const positions = [
{ top: "15%", right: "10%" },
{ top: "40%", left: "8%" },
{ top: "65%", right: "12%" },
{ top: "20%", left: "10%" },
];
const pos = positions[i % positions.length];
Object.assign(el.style, pos);
container.appendChild(el);
// Bounce in
tl.fromTo(
el,
{ opacity: 0, scale: 0 },
{ opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" },
time,
);
// Hold for 2-3s then fade out
tl.to(el, { opacity: 0, duration: 0.3 }, time + 2.5);
});
```
## Zoom Cuts
Add visual emphasis during key moments by cutting to a zoomed-in framing.
### Pattern
```js
// Zoom in: instant cut (no gradual zoom)
tl.to(
"#pip-frame",
{
scale: 1.5,
duration: 0.01, // instant
},
keyMomentStart,
);
// Hold zoomed for duration of emphasized phrase
// Zoom out: smooth animated return
tl.to(
"#pip-frame",
{
scale: 1,
duration: 0.5,
ease: "power2.out",
},
keyMomentEnd,
);
```
- **Trigger** at important sentences or phrases
- **Zoom in** is an instant cut, not a gradual zoom
- **Frame** the subject's face with comfortable padding
- **Zoom out** is a smooth animated return
## Z-Index Layering
```
z-index: 1 — base video
z-index: 10 — effects / overlays
z-index: 20 — captions
z-index: 30 — emojis
z-index: 40 — hook text
```
## Workflow
1. Transcribe audio with word-level timestamps
2. Design hook text from opening script (3-5 key words)
3. Group transcript words into caption groups (2-4 words)
4. Choose consistent font family and style (bubble vs outlined)
5. Select background music matching video mood
6. Position captions to avoid covering subject
7. Add zoom cuts at key moments
8. Place emojis triggered by keywords
9. Add transition effects between sections
10. Add sound effects at key transitions
11. Validate all timing syncs with actual audio
## Common Mistakes
- Captions not synced to actual audio
- Multiple caption groups or text elements visible simultaneously
- Text too small to read on mobile
- Insufficient contrast between text and background
- Captions covering subject's face
- Emojis overlapping or too close to face
- No size variation in hook text (all same size lacks impact)
- Background music too loud relative to voice
- Overusing effects and transitions
- Inconsistent styling between hook and captions
-206
View File
@@ -1,206 +0,0 @@
---
name: transitions
description: Scene transition constraints and patterns for HyperFrames. Use when adding crossfades, wipes, or reveals between scenes in a composition.
---
# Transitions
Constraints and patterns for scene-to-scene transitions in HyperFrames compositions.
## When to Use
- Adding a crossfade, wipe, or reveal between video clips or scenes
- User asks for "smooth transition", "crossfade", or "scene change"
- Connecting two sequential clips visually
## Core Constraint: Same-Track Clips Cannot Overlap
Clips on the same `data-track-index` cannot overlap in time. This is enforced by the framework. If two clips need to overlap (for a crossfade), they must be on **different tracks**.
## Pattern: Crossfade
Two clips on separate tracks. The outgoing clip fades out while the incoming clip fades in during the overlap window.
```html
<!-- Outgoing clip: track 0, ends at 10s -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-track-index="0"
src="clip-a.mp4"
muted
playsinline
style="z-index:1;"
></video>
<!-- Incoming clip: track 1, starts at 9s (1s overlap) -->
<video
id="el-2"
data-start="9"
data-duration="15"
data-track-index="1"
src="clip-b.mp4"
muted
playsinline
style="z-index:2;"
></video>
```
```js
// Fade out clip A during overlap
tl.to("#el-1", { opacity: 0, duration: 1, ease: "power2.inOut" }, 9);
// Fade in clip B during overlap
tl.fromTo("#el-2", { opacity: 0 }, { opacity: 1, duration: 1, ease: "power2.inOut" }, 9);
```
**Key rules:**
- The overlap window is `incoming.start` to `outgoing.end`
- Both clips must be on different tracks
- Use `z-index` to control which clip renders on top during the transition
- Animate `opacity` — do not animate `visibility` or `display`
## Pattern: Wipe / Reveal
An overlay composition that covers the frame, hides the cut, then reveals the new content.
```html
<!-- Clip A and Clip B are sequential on track 0 -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-track-index="0"
src="clip-a.mp4"
muted
playsinline
></video>
<video
id="el-2"
data-start="10"
data-duration="15"
data-track-index="0"
src="clip-b.mp4"
muted
playsinline
></video>
<!-- Wipe overlay on track 3, spans the cut point -->
<div
id="wipe-comp"
data-composition-id="wipe"
data-composition-src="compositions/wipe.html"
data-start="9"
data-duration="2"
data-track-index="3"
data-width="1920"
data-height="1080"
style="z-index:100;"
></div>
```
Wipe composition (`compositions/wipe.html`):
```html
<template id="wipe-template">
<div data-composition-id="wipe" data-width="1920" data-height="1080">
<div
id="wipe-bar"
style="
position:absolute; top:0; left:0;
width:1920px; height:1080px;
background:#111; transform:translateX(-100%);
"
></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
const tl = gsap.timeline({ paused: true });
// Sweep in from left, covering the frame
tl.to("#wipe-bar", { x: 0, duration: 0.5, ease: "power3.inOut" }, 0.5);
// Hold for a beat while the cut happens underneath
// Sweep out to the right, revealing new content
tl.to("#wipe-bar", { x: 1920, duration: 0.5, ease: "power3.inOut" }, 1.2);
window.__timelines["wipe"] = tl;
</script>
</div>
</template>
```
**Key rules:**
- The wipe composition spans the cut point (starts before the cut, ends after)
- The wipe must have a high `z-index` to render above both clips
- Clips can be on the same track (no overlap) since the wipe covers the hard cut
- Time the wipe so the frame is fully covered at the exact cut point
## Pattern: Scanline Sweep
A partial-frame element sweeps across, creating a stylized transition.
```js
const tl = gsap.timeline({ paused: true });
// Scanline starts above frame, sweeps down
tl.fromTo("#scanline", { y: -320 }, { y: 1920, duration: 1.5, ease: "power3.inOut" }, 0);
```
Style the scanline with gradients, glow, or accent colors for visual interest.
## Constraints
- **Transitions are compositions.** Wrap transition elements in a composition with `data-composition-id`. Do not use bare divs without data attributes.
- **Do not animate video element dimensions.** If you need to scale video during a transition, animate a wrapper div.
- **Use GSAP, not CSS transitions.** CSS transitions are not deterministic in the renderer.
- **Deterministic.** No `Math.random()`, no `Date.now()`.
- **Transitions must not affect media playback.** Never call `play()`, `pause()`, or set `currentTime`. The framework owns media.
- **Keep transitions short.** 0.3s–1.5s. Longer transitions feel sluggish in rendered video.
## Timing with Relative References
Use relative `data-start` to keep transitions aligned when clip durations change:
```html
<video
id="intro"
data-start="0"
data-duration="10"
data-track-index="0"
src="..."
muted
playsinline
></video>
<!-- Transition starts 1s before intro ends -->
<div
id="wipe-comp"
data-composition-id="wipe"
data-composition-src="compositions/wipe.html"
data-start="intro - 1"
data-duration="2"
data-track-index="3"
data-width="1920"
data-height="1080"
style="z-index:100;"
></div>
<video
id="main"
data-start="intro"
data-duration="20"
data-track-index="0"
src="..."
muted
playsinline
></video>
```
If `intro`'s duration changes, the wipe and `main` shift automatically.
## Anti-Patterns
- **Overlapping clips on the same track.** Framework will reject this. Use different tracks for crossfades.
- **CSS `transition` property.** Not deterministic in headless rendering. Use GSAP.
- **Animating `display` property.** Use `opacity` instead.
- **Transitions longer than 2s.** Feels slow in video.
@@ -1,118 +0,0 @@
# Skills & Transcription Design
## Overview
Three new HyperFrames skills (captions, transitions, media) and a CLI enhancement to add local transcription to `hyperframes init`.
---
## Part 1: CLI — Transcription in `hyperframes init`
### User Flow
```
npx hyperframes init
→ "Project name" → my-video
→ "Got a video?" → Yes → /path/to/video.mp4
→ (probe video: 1920x1080, 45.2s, has audio)
→ "Generate captions from audio?" → Yes
→ (extract audio → run whisper → save transcript)
→ "Pick a template" → warm-grain
→ (scaffold project with transcript.json + captions composition)
→ "Open in studio" / "Render" / "Done"
```
### Whisper Binary Management
Follow the same pattern as the Chrome browser manager (`packages/cli/src/browser/manager.ts`):
- **Cache location:** `~/.cache/hyperframes/whisper/`
- **Binary:** whisper.cpp pre-built binary (download from GitHub releases)
- **Model:** `ggml-base.en.bin` by default (~148MB), downloaded from Hugging Face on first use
- **Resolution priority:** `--whisper-path` flag → `HYPERFRAMES_WHISPER_PATH` env var → cached binary → download
Structure:
```
~/.cache/hyperframes/whisper/
├── bin/
│ └── whisper # whisper.cpp binary
└── models/
└── ggml-base.en.bin # default model
```
### Transcription Pipeline
1. **Extract audio:** `ffmpeg -i video.mp4 -vn -ar 16000 -ac 1 -f wav /tmp/audio.wav`
2. **Run whisper:** `whisper --model ~/.cache/hyperframes/whisper/models/ggml-base.en.bin --output-json --dtw base.en /tmp/audio.wav`
3. **Parse output:** whisper.cpp JSON output → normalize to `transcript.json`
4. **Save:** `transcript.json` in project root
### transcript.json Format
Use whisper.cpp's native JSON output format directly. The captions skill documents whatever format whisper.cpp outputs — no normalization layer.
### Files to Change
- `packages/cli/src/whisper/manager.ts` — new module: download, cache, resolve whisper binary + model (mirrors browser/manager.ts)
- `packages/cli/src/whisper/transcribe.ts` — new module: extract audio, run whisper, return parsed result
- `packages/cli/src/commands/init.ts` — add "Generate captions?" prompt after video probe, call transcribe, save transcript.json, pass to scaffolding
- `packages/cli/src/commands/doctor.ts` — add whisper check
---
## Part 2: Skills
### Skill: `captions`
**File:** `.claude/skills/captions/SKILL.md`
**Purpose:** Given a whisper transcript file in the project, build or modify HyperFrames caption compositions.
**Assumes:** `transcript.json` exists (output from `hyperframes init` or whisper.cpp directly).
**Content covers:**
- Whisper.cpp JSON structure reference (segments, tokens, timestamps)
- Word grouping strategies: by sentence breaks, pause gaps (>150ms), max word count (3-5)
- Composition structure: `<template>` wrapper, `data-composition-id="captions"`, scoped CSS
- GSAP patterns for caption entrance/exit (opacity, y offset, scale)
- Positioning: bottom third centered, safe margins
- Constraints: deterministic, no `Math.random()`, register in `window.__timelines`
- Anti-patterns: don't animate `display`, don't overlap caption groups on same track
### Skill: `transitions`
**File:** `.claude/skills/transitions/SKILL.md`
**Purpose:** Constraints and patterns for scene-to-scene transitions in HyperFrames.
**Content covers:**
- Track overlap rules: same track clips cannot overlap, use different tracks for crossfades
- Crossfade pattern: two clips on separate tracks, opacity tween on outgoing clip
- Wipe/reveal patterns: overlay div with GSAP-animated position
- Timing: transition composition overlaps scene boundaries via `data-start` offset
- Constraints: transitions are compositions (need `data-composition-id`), not bare divs
- Anti-patterns: don't animate video element dimensions, don't use CSS transitions (use GSAP)
### Skill: `media`
**File:** `.claude/skills/media/SKILL.md`
**Purpose:** Constraints for working with video, audio, and images in HyperFrames.
**Content covers:**
- Video: must be `muted` + `playsinline`, audio separate, `crossorigin="anonymous"` for CORS
- Audio: separate `<audio>` element, `data-volume` for level control
- Trimming: `data-media-start` for in-point offset
- Codec requirements: H.264/VP8/VP9/AV1 for video, AAC/MP3/Opus for audio
- Transparent video: WebM VP9 with alpha channel
- Image: `<img>` with `data-start`/`data-duration`/`data-track-index`
- Framework owns playback: never call `play()`, `pause()`, set `currentTime`
- PiP pattern: animate wrapper div, not the video element
- Anti-patterns: don't nest video in timed div, don't animate video dimensions
---
## Prioritization
1. **Skills first** — captions, transitions, media (can ship immediately, no code changes)
2. **CLI transcription** — whisper binary manager + init flow (requires engineering)
+138
View File
@@ -0,0 +1,138 @@
---
name: captions
description: Build tone-adaptive captions from whisper transcripts. Detects script energy (hype, corporate, tutorial, storytelling, social) and applies matching typography, color, and animation. Supports per-word styling for brand names, ALL CAPS, numbers, and CTAs. Use when adding captions or subtitles to a HyperFrames composition.
---
# Captions
Analyze the spoken content to determine caption style. If the user specifies a style, use that. Otherwise, detect tone from the transcript.
## Transcript Source
The project's `transcript.json` contains word-level timestamps from whisper.cpp (`--output-json-full` with `--dtw`):
```json
{
"transcription": [
{
"offsets": { "from": 0, "to": 5000 },
"text": " Hello world.",
"tokens": [
{ "text": " Hello", "offsets": { "from": 0, "to": 1000 }, "p": 0.98 },
{ "text": " world", "offsets": { "from": 1000, "to": 2000 }, "p": 0.95 }
]
}
]
}
```
Normalize tokens into a word array before grouping:
```js
const words = [];
for (const segment of transcript.transcription) {
for (const token of segment.tokens || []) {
const text = token.text.trim();
if (!text) continue;
words.push({
text,
start: token.offsets.from / 1000,
end: token.offsets.to / 1000,
});
}
}
```
If no `transcript.json` exists, check for `.srt` or `.vtt` files. If no transcript is available, ask the user to provide one or run `hyperframes transcribe` (when available).
## Style Detection (Default — When No Style Is Specified)
Read the full transcript before choosing a style. The style comes from the content, not a template.
### Four Dimensions
**1. Visual feel** — the overall aesthetic personality:
- Corporate/professional scripts → clean, minimal, restrained
- Energetic/marketing scripts → bold, punchy, high-impact
- Storytelling/narrative scripts → elegant, warm, cinematic
- Technical/educational scripts → precise, high-contrast, structured
- Social media/casual scripts → playful, dynamic, friendly
**2. Color palette** — driven by the content's mood:
- Dark backgrounds with bright accents for high energy
- Muted/neutral tones for professional or calm content
- High contrast (white on black, black on white) for clarity
- One accent color for emphasis — not multiple
**3. Font mood** — typography character, not specific font names:
- Heavy/condensed for impact and energy
- Clean sans-serif for modern and professional
- Rounded for friendly and approachable
- Serif for elegance and storytelling
**4. Animation character** — how words enter and exit:
- Scale-pop/slam for punchy energy
- Gentle fade/slide for calm or professional
- Word-by-word reveal for emphasis
- Typewriter for technical or narrative pacing
## Per-Word Styling
Scan the script for words that deserve distinct visual treatment. Not every word is equal — some carry the message.
### What to Detect
- **Brand names / product names** — larger size, unique color, distinct entrance
- **ALL CAPS words** — the author emphasized them intentionally. Scale boost, flash, or accent color.
- **Numbers / statistics** — bold weight, accent color. Numbers are the payload in data-driven content.
- **Emotional keywords** — "incredible", "insane", "amazing", "revolutionary" → exaggerated animation (overshoot, bounce)
- **Proper nouns** — names of people, places, events → distinct accent or italic
- **Call-to-action phrases** — "sign up", "get started", "try it now" → highlight, underline, or color pop
### How to Apply
For each detected word, specify:
- Font size multiplier (e.g., 1.3x for emphasis, 1.5x for hero moments)
- Color override (specific hex value)
- Weight/style change (bolder, italic)
- Animation variant (overshoot entrance, glow pulse, scale pop)
## Script-to-Style Mapping
| Script tone | Font mood | Animation | Color | Size |
| -------------------- | ------------------------------------- | --------------------------------------- | -------------------------------------------- | -------------------- |
| Hype/launch | Heavy condensed, 800-900 weight | Scale-pop, back.out(1.7), fast 0.1-0.2s | Bright accent on dark (cyan, yellow, lime) | Large 72-96px |
| Corporate/pitch | Clean sans-serif, 600-700 weight | Fade + slide-up, power3.out, 0.3s | White/neutral on dark, single muted accent | Medium 56-72px |
| Tutorial/educational | Mono or clean sans, 500-600 weight | Typewriter or gentle fade, 0.4-0.5s | High contrast, minimal color | Medium 48-64px |
| Storytelling/brand | Serif or elegant sans, 400-500 weight | Slow fade, power2.out, 0.5-0.6s | Warm muted tones, low opacity (0.85-0.9) | Smaller 44-56px |
| Social/casual | Rounded sans, 700-800 weight | Bounce, elastic.out, word-by-word | Playful colors, colored backgrounds on pills | Medium-large 56-80px |
## Word Grouping by Tone
Group size affects pacing. Fast content needs fast caption turnover.
- **High energy:** 2-3 words per group. Quick turnover matches rapid delivery.
- **Conversational:** 3-5 words per group. Natural phrase length.
- **Measured/calm:** 4-6 words per group. Longer groups match slower pace.
Break groups on sentence boundaries (`.` `?` `!`), pauses (>150ms gap), or max word count — whichever comes first.
## Positioning
- **Landscape (1920x1080):** Bottom 80-120px, centered
- **Portrait (1080x1920):** Lower middle ~600-700px from bottom, centered
- Never cover the subject's face
- Use `position: absolute` — never relative (causes overflow)
- One caption group visible at a time
## Constraints
- **Deterministic.** No `Math.random()`, no `Date.now()`.
- **Sync to transcript timestamps.** Words appear when spoken.
- **One group visible at a time.** No overlapping caption groups.
- **Check project root** for font files before defaulting to Google Fonts.
+136
View File
@@ -0,0 +1,136 @@
---
name: compose-video
description: Create HyperFrames HTML video compositions. Use when asked to create a video, build an animation, make a composition, add a title card, or generate any HTML-based video content for HyperFrames.
---
# Compose Video
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.
## 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. Same-track clips **cannot overlap**. |
| `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`.
### Composition Clips
| Attribute | Required | Values |
| ---------------------------- | -------- | -------------------------------------------- |
| `data-composition-id` | Yes | Unique composition ID |
| `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
Every composition is a `<template>` wrapping a `<div>` with `data-composition-id`. Each must include its own GSAP script and register its timeline:
```html
<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:
```html
<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. The renderer must produce identical output every time.
**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 — causes flickering in headless renders.
**Never do:**
1. Forget `window.__timelines` registration
2. Use video for audio — always muted video + separate `<audio>`
3. Nest video inside a timed div — use a non-timed wrapper
4. Use `data-layer` (use `data-track-index`) or `data-end` (use `data-duration`)
5. Animate video element dimensions — animate a wrapper div
6. Call play/pause/seek on media — framework owns playback
7. Create a top-level container without `data-composition-id`
## Editing Existing Compositions
- Read the full composition first — match existing fonts, colors, animation patterns
- Only change what was requested — don't rewrite untouched sections
- Don't rewrite entire files for small changes
- Preserve timing of unrelated clips
## Typography and Assets
- Every composition loads its own fonts (`@import` or `@font-face` in `<style>`)
- Use `font-display: block` for local fonts — renderer needs fonts loaded before capturing
- Add `crossorigin="anonymous"` to media loaded from external URLs
- Minimum readable text: 20px landscape, 18px portrait
- All files (video, audio, fonts, images) live at the project root alongside `index.html`
- From sub-compositions, use `../` to reference root files
For PiP, title cards, and slide show patterns, see [patterns.md](./patterns.md).
## Output Checklist
- [ ] Every top-level container has `data-composition-id`
- [ ] Every composition has `data-width`, `data-height`, `data-duration`
- [ ] Compositions in own HTML files, loaded via `data-composition-src`
- [ ] `<template>` wrapper on sub-compositions
- [ ] `window.__timelines` registered for every composition
- [ ] 100% deterministic — no randomness
- [ ] Each composition includes GSAP: `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>`
+118
View File
@@ -0,0 +1,118 @@
# Composition Patterns
## Picture-in-Picture (Video in a Frame)
Animate a wrapper div for position/size. The video fills the wrapper. The wrapper has NO data attributes.
```html
<div
id="pip-frame"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:50;overflow:hidden;"
>
<video
id="el-video"
data-start="0"
data-duration="60"
data-track-index="0"
src="talking-head.mp4"
muted
playsinline
></video>
</div>
```
```js
tl.to(
"#pip-frame",
{ top: 700, left: 1360, width: 500, height: 280, borderRadius: 16, duration: 1 },
10,
);
tl.to("#pip-frame", { left: 40, duration: 0.6 }, 30);
```
## Title Card with Fade
```html
<div
id="title-card"
data-start="0"
data-duration="5"
data-track-index="5"
style="display:flex;align-items:center;justify-content:center;background:#111;z-index:60;"
>
<h1 style="font-size:64px;color:#fff;opacity:0;">My Video Title</h1>
</div>
```
```js
tl.to("#title-card h1", { opacity: 1, duration: 0.6 }, 0.3);
tl.to("#title-card", { opacity: 0, duration: 0.5 }, 4);
```
## Slide Show with Section Headers
Use separate elements on the same track, each with its own time range. Slides auto-mount/unmount based on `data-start`/`data-duration`.
```html
<div class="slide" data-start="0" data-duration="30" data-track-index="3">...</div>
<div class="slide" data-start="30" data-duration="25" data-track-index="3">...</div>
<div class="slide" data-start="55" data-duration="20" data-track-index="3">...</div>
```
## Top-Level Composition Example
```html
<div
id="comp-1"
data-composition-id="my-video"
data-start="0"
data-duration="60"
data-width="1920"
data-height="1080"
>
<!-- Primitive clips -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-track-index="0"
src="..."
muted
playsinline
></video>
<video
id="el-2"
data-start="el-1"
data-duration="8"
data-track-index="0"
src="..."
muted
playsinline
></video>
<img id="el-3" data-start="5" data-duration="4" data-track-index="1" src="..." />
<audio id="el-4" data-start="0" data-duration="30" data-track-index="2" src="..." />
<!-- Sub-compositions loaded from files -->
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-track-index="3"
></div>
<div
id="el-6"
data-composition-id="captions"
data-composition-src="compositions/caption-overlay.html"
data-start="0"
data-track-index="4"
></div>
<script>
// Just register the timeline — framework auto-nests sub-compositions
const tl = gsap.timeline({ paused: true });
window.__timelines["my-video"] = tl;
</script>
</div>
```