mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-05 05:37:09 +00:00
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:
co-authored by
Claude Opus 4.6
parent
31ad8b98d8
commit
b7735d609f
@@ -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>
|
||||
```
|
||||
@@ -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>`
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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>`
|
||||
@@ -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>
|
||||
```
|
||||
Reference in New Issue
Block a user