@vibeframe/cli 0.113.26 → 0.113.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/commands/_shared/build-plan.d.ts +7 -0
  2. package/dist/commands/_shared/compose-beat-prompt.d.ts +47 -0
  3. package/dist/commands/_shared/compose-prompts.d.ts +8 -10
  4. package/dist/commands/_shared/install-skill.d.ts +67 -90
  5. package/dist/commands/_shared/scene-build-mode.d.ts +15 -0
  6. package/dist/commands/_shared/scene-build.d.ts +2 -8
  7. package/dist/commands/_shared/scene-lint.d.ts +23 -0
  8. package/dist/commands/_shared/scene-render.d.ts +12 -3
  9. package/dist/commands/_shared/walkthroughs/walkthroughs.d.ts +1 -1
  10. package/dist/index.js +2196 -3687
  11. package/dist/tools/manifest/agent-only.d.ts +2 -2
  12. package/dist/tools/manifest/audio.d.ts +2 -2
  13. package/dist/tools/manifest/edit.d.ts +12 -12
  14. package/dist/tools/manifest/export.d.ts +2 -2
  15. package/dist/tools/manifest/generate.d.ts +6 -6
  16. package/dist/tools/manifest/index.js +1663 -3236
  17. package/dist/tools/manifest/inspect.d.ts +8 -8
  18. package/dist/tools/manifest/remix.d.ts +10 -10
  19. package/dist/tools/manifest/scene.d.ts +20 -23
  20. package/dist/tools/manifest/storyboard.d.ts +2 -2
  21. package/dist/utils/exec-safe.d.ts +1 -0
  22. package/dist/utils/stdout-guard.d.ts +22 -0
  23. package/package.json +4 -4
  24. package/dist/commands/_shared/compose-scenes-skills.d.ts +0 -334
  25. package/dist/commands/_shared/hf-skill-bundle/bundle-content.d.ts +0 -16
  26. package/dist/commands/_shared/hf-skill-bundle/bundle.d.ts +0 -63
  27. package/dist/commands/_shared/hf-skill-bundle/bundle.test.d.ts +0 -1
  28. /package/dist/{commands/_shared/compose-scenes-skills.test.d.ts → utils/stdout-guard.test.d.ts} +0 -0
@@ -1,334 +0,0 @@
1
- /**
2
- * @module _shared/compose-scenes-skills
3
- *
4
- * Single-beat Claude composer for the `compose-scenes-with-skills` v0.59
5
- * pipeline action. Per beat:
6
- *
7
- * 1. Compute a sha256 cache key over (skill-bundle hash, DESIGN.md,
8
- * storyboard global direction, beat body, model id, retry-feedback).
9
- * 2. Read the user cache's `compose-scenes/<key>.html` if present.
10
- * 3. Otherwise call Claude Sonnet 4.6 with the skill bundle as system
11
- * prompt and the beat as user prompt; parse HTML out of a fenced
12
- * code block; persist to cache.
13
- *
14
- * Pre-flight (PR #111) validated this exact prompt shape at 5/5 lint pass,
15
- * $0.058/scene, 8.4 s. The diff between runs is high (~33 % line-Jaccard)
16
- * which is precisely *why* we cache on input rather than output: same
17
- * input prompts always return the same cached HTML.
18
- *
19
- * C3 ships:
20
- * - `composeBeatHtml(ctx)` — single shot, no retry
21
- * - Cache I/O (read + write) by sha256 input key
22
- * - Anthropic SDK call with model selection + cost reporting
23
- * - Pure prompt construction helpers (testable without API key)
24
- *
25
- * Lint feedback retry loop comes in C4 (extends this module).
26
- */
27
- import { type LintFinding } from "./scene-lint.js";
28
- /** Resolve the compose fan-out limit (env override, clamped to the beat count). */
29
- export declare function composeConcurrency(beatCount: number): number;
30
- import { type Beat } from "./storyboard-parse.js";
31
- import { type ComposerProvider } from "./composer-resolve.js";
32
- import type { SceneTranscriptWord } from "./scene-html-emit.js";
33
- /** Effort level → token caps. Model is per-provider (see MODEL_SETTINGS_BY_PROVIDER). */
34
- export type ComposeEffort = "low" | "medium" | "high";
35
- export interface ComposeBeatContext {
36
- /** The beat to compose, from `parseStoryboard()`. */
37
- beat: Beat;
38
- /** Project's `DESIGN.md` content (visual identity hard-gate). */
39
- designMd: string;
40
- /**
41
- * Optional `COMPOSITION.md` content — the per-project STRUCTURAL contract
42
- * (layout system, GSAP timeline conventions, element/track rules), parallel
43
- * to DESIGN.md's visual contract. Injected into the system prompt as a
44
- * hard-gate when present.
45
- */
46
- compositionMd?: string;
47
- /** Storyboard's global direction (everything before the first `## Beat`). */
48
- storyboardGlobal: string;
49
- /** Hyperframes skill bundle content + hash, from `loadHyperframesSkillBundle()`. */
50
- skillBundle: {
51
- content: string;
52
- hash: string;
53
- };
54
- /** Effort level — picks model + token caps. Defaults to "medium". */
55
- effort?: ComposeEffort;
56
- /**
57
- * Optional lint feedback to append to the user prompt, used by C4's retry
58
- * loop. When non-empty, the cache key includes it so retries don't return
59
- * the cached first-shot HTML.
60
- */
61
- retryFeedback?: string;
62
- /**
63
- * LLM provider that powers the composer. Picked by `resolveComposer()` at
64
- * the entry point (CLI flag → env auto-resolve). The `apiKey` field below
65
- * is the matching key for this provider.
66
- */
67
- provider: ComposerProvider;
68
- /** API key for {@link ComposeBeatContext.provider}. */
69
- apiKey: string;
70
- /**
71
- * Cache directory override. Defaults to `~/.vibeframe/cache/compose-scenes/`.
72
- * Tests pass a temp dir.
73
- */
74
- cacheDir?: string;
75
- /**
76
- * Narration-synced FINAL beat duration in seconds. The root timeline
77
- * stretches each beat to cover its generated narration
78
- * (see resolveSyncedBeatDuration in root-sync.ts); when set, the prompt
79
- * pins `data-duration` and every timeline anchor to this exact value so the
80
- * scene cannot end early and render a black tail. The storyboard cue
81
- * duration is then only the minimum and is annotated as superseded.
82
- */
83
- finalDurationSec?: number;
84
- /**
85
- * Whisper word-level timings for this beat's narration audio, read from
86
- * `assets/transcript-<id>.json` by the asset stage. When present,
87
- * {@link buildUserPrompt} surfaces a compact timing table so the composer
88
- * can sync caption / kinetic-type word reveals to speech. Folded into the
89
- * user prompt, so the compose cache key invalidates when timings change.
90
- */
91
- transcript?: SceneTranscriptWord[];
92
- }
93
- export interface ComposeBeatResult {
94
- /** Generated HTML (from cache or fresh API call). */
95
- html: string;
96
- /** True when the HTML came from cache (no API call made). */
97
- cached: boolean;
98
- /** Cache key used (sha256 hex). */
99
- cacheKey: string;
100
- /** Provider input tokens (only set on fresh API call). */
101
- inputTokens?: number;
102
- /** Provider output tokens (only set on fresh API call). */
103
- outputTokens?: number;
104
- /** USD cost of this beat (only set on fresh API call). */
105
- costUsd?: number;
106
- /** Wall-clock latency in ms (only set on fresh API call). */
107
- latencyMs?: number;
108
- /** Resolved model id (e.g. "claude-sonnet-4-6"). */
109
- model: string;
110
- /** Composer provider that produced this beat. */
111
- provider: ComposerProvider;
112
- }
113
- /** Build the system prompt — skill bundle + DESIGN.md hard-gate (+ optional COMPOSITION.md). */
114
- export declare function buildSystemPrompt(ctx: Pick<ComposeBeatContext, "skillBundle" | "designMd" | "compositionMd">): string;
115
- /**
116
- * Word-level timing entries above this count are downgraded to coarse,
117
- * phrase-level anchors in the prompt. A token-budget guard: a 60s narration
118
- * can exceed 150 words, and inlining every `[start, "word"]` pair bloats the
119
- * prompt (and the cache key) for little composer benefit past a point.
120
- */
121
- export declare const TRANSCRIPT_PROMPT_MAX_WORDS = 120;
122
- /**
123
- * Render narration word timings as a compact, deterministic prompt section.
124
- * Token-budget guard (the `oversized` case is the reason this is a function,
125
- * not an inline template):
126
- * - no transcript → `""` (section omitted entirely; prompt unchanged).
127
- * - ≤ {@link TRANSCRIPT_PROMPT_MAX_WORDS} → full word-level `[start, "word"]`
128
- * table the composer can map 1:1 to GSAP reveals.
129
- * - more → group into ~`MAX/2` phrase anchors (start time of each phrase),
130
- * tagged "approximate" so the composer paces reveals across the beat
131
- * rather than over-trusting per-word truth.
132
- * Pure and stable for a given input → the compose cache stays consistent.
133
- */
134
- export declare function formatTranscriptSection(transcript: SceneTranscriptWord[] | undefined, maxWords?: number): string;
135
- /** Build the user prompt — instructions + storyboard global + beat body. */
136
- export declare function buildUserPrompt(ctx: Pick<ComposeBeatContext, "beat" | "storyboardGlobal" | "retryFeedback" | "finalDurationSec" | "transcript">): string;
137
- /**
138
- * Compute the sha256 cache key for a (provider, model, system, user) tuple.
139
- * Provider id is folded in so switching `--composer` doesn't serve cached
140
- * HTML produced by a different model.
141
- */
142
- export declare function computeCacheKey(parts: {
143
- provider: ComposerProvider;
144
- systemPrompt: string;
145
- userPrompt: string;
146
- model: string;
147
- }): string;
148
- /** Default cache root: `~/.vibeframe/cache/compose-scenes/`. */
149
- export declare function defaultCacheDir(): string;
150
- /**
151
- * Pull the HTML out of Claude's response. Prefers a fenced ```html block;
152
- * accepts a bare HTML response as fallback (some completions skip the fence
153
- * when the entire reply is the document).
154
- */
155
- export declare function extractHtml(responseText: string): string;
156
- export declare class ComposeBeatError extends Error {
157
- readonly code: string;
158
- readonly cause?: unknown | undefined;
159
- constructor(code: string, message: string, cause?: unknown | undefined);
160
- }
161
- /** Raw text-completion result from any of the three providers. */
162
- export interface LLMCallResult {
163
- text: string;
164
- inputTokens: number;
165
- outputTokens: number;
166
- }
167
- /** Injectable LLM call function — used by tests to mock the SDK calls. */
168
- export type LLMCallFn = (req: {
169
- provider: ComposerProvider;
170
- apiKey: string;
171
- model: string;
172
- maxTokens: number;
173
- systemPrompt: string;
174
- userPrompt: string;
175
- }) => Promise<LLMCallResult>;
176
- /**
177
- * Compose one beat into HTML. Cache-first; on miss, call the configured
178
- * composer provider (Claude / OpenAI / Gemini — picked at the entry point
179
- * via `resolveComposer()` and threaded through `ctx.provider`).
180
- *
181
- * @param overrides allows tests to inject a mocked LLM call. In prod we use
182
- * `defaultCallLLM` which dispatches to the matching SDK.
183
- */
184
- export declare function composeBeatHtml(ctx: ComposeBeatContext, overrides?: {
185
- callLLM?: LLMCallFn;
186
- now?: () => number;
187
- }): Promise<ComposeBeatResult>;
188
- export interface BeatLintResult {
189
- errorCount: number;
190
- warningCount: number;
191
- findings: LintFinding[];
192
- }
193
- /**
194
- * Lint a single beat's HTML in-memory using `@hyperframes/producer`'s
195
- * `runHyperframeLint` directly — no temp project shell, no subprocess.
196
- *
197
- * Sub-composition false-positives are filtered (matches what the
198
- * `vibe scene lint` command does). The result is the same set of
199
- * findings the user would see if they ran `vibe scene lint` after
200
- * the beat HTML landed in a project.
201
- */
202
- export declare function lintBeatHtml(html: string, beatId: string): BeatLintResult;
203
- /**
204
- * Format lint findings into a feedback block for the retry prompt.
205
- * Errors only — warnings don't block, so we don't burn the LLM's
206
- * attention budget on them.
207
- */
208
- export declare function formatLintFeedback(findings: LintFinding[]): string;
209
- export interface ComposeBeatWithRetryResult extends ComposeBeatResult {
210
- /** 1 if first-shot HTML linted clean; 2 if retry succeeded. */
211
- lintAttempts: 1 | 2;
212
- /** Lint result from the *final* (winning) attempt. */
213
- lint: BeatLintResult;
214
- }
215
- /**
216
- * `composeBeatHtml` + lint feedback retry loop, capped at 1 retry.
217
- *
218
- * Pre-flight (PR #111) showed 5/5 first-pass success at $0.058/scene; deeper
219
- * retry loops just burn budget. If the second attempt also fails lint,
220
- * throws `ComposeBeatError("lint-failed-after-retry", ...)` with the final
221
- * findings — caller (C5 pipeline action) decides whether to fall back to
222
- * the 5-preset emit path or surface the failure to the user.
223
- *
224
- * Cache contract preserved: retries use a different `retryFeedback`, which
225
- * is folded into the cache key, so retries don't accidentally serve the
226
- * cached first-shot HTML.
227
- */
228
- export declare function composeBeatWithRetry(ctx: ComposeBeatContext, overrides?: {
229
- callLLM?: LLMCallFn;
230
- now?: () => number;
231
- }): Promise<ComposeBeatWithRetryResult>;
232
- /**
233
- * YAML pipeline action: read DESIGN.md + STORYBOARD.md, compose each beat
234
- * via Claude, write `compositions/scene-<id>.html` per beat. Sequential
235
- * fanout; C6 swaps in `Promise.all` parallelism.
236
- *
237
- * Wired into `pipeline/executor.ts` as the `compose-scenes-with-skills`
238
- * action. YAML shape:
239
- *
240
- * - id: compose
241
- * action: compose-scenes-with-skills
242
- * design: DESIGN.md # required (relative to project)
243
- * storyboard: STORYBOARD.md # required
244
- * project: . # optional, default outputDir
245
- * effort: medium # optional, low|medium|high
246
- *
247
- * Returns the project root as `output` and per-beat metadata in `data`.
248
- */
249
- /** Per-beat lifecycle event emitted to the optional `onProgress` callback. */
250
- export type ComposeProgressEvent = {
251
- type: "beat-start";
252
- beatId: string;
253
- beatIndex: number;
254
- totalBeats: number;
255
- } | {
256
- type: "beat-cached";
257
- beatId: string;
258
- beatIndex: number;
259
- totalBeats: number;
260
- lintAttempts: 1 | 2;
261
- } | {
262
- type: "beat-fresh";
263
- beatId: string;
264
- beatIndex: number;
265
- totalBeats: number;
266
- lintAttempts: 1 | 2;
267
- costUsd?: number;
268
- latencyMs?: number;
269
- } | {
270
- type: "beat-failed";
271
- beatId: string;
272
- beatIndex: number;
273
- totalBeats: number;
274
- error: string;
275
- };
276
- export interface ComposeScenesParams {
277
- /** Path to DESIGN.md, relative to project root. */
278
- design?: string;
279
- /**
280
- * Path to the optional COMPOSITION.md structural contract, relative to project
281
- * root. Defaults to `COMPOSITION.md`; silently ignored when absent.
282
- */
283
- composition?: string;
284
- /** Path to STORYBOARD.md, relative to project root. */
285
- storyboard?: string;
286
- /** Scene project root, relative to outputDir. Defaults to outputDir. */
287
- project?: string;
288
- /** Effort tier — low/medium/high. Defaults to medium. */
289
- effort?: ComposeEffort;
290
- /**
291
- * Composer provider override. When undefined, `resolveComposer()` picks
292
- * one based on env keys (`claude > gemini > openai` order).
293
- */
294
- composer?: ComposerProvider;
295
- /** Override the cache directory (tests). */
296
- cacheDir?: string;
297
- /** Optional per-beat progress callback (CLI spinner / pipeline reporter). */
298
- onProgress?: (event: ComposeProgressEvent) => void;
299
- }
300
- export interface ComposeScenesActionResult {
301
- success: boolean;
302
- /** Project root (where compositions/ lives). */
303
- outputPath?: string;
304
- /** Aggregated metadata for the dry-run / final report. */
305
- data?: {
306
- beats: number;
307
- written: Array<{
308
- beatId: string;
309
- path: string;
310
- cached: boolean;
311
- cacheKey: string;
312
- lintAttempts: 1 | 2;
313
- costUsd?: number;
314
- }>;
315
- totalCostUsd: number;
316
- totalTokensIn: number;
317
- totalTokensOut: number;
318
- cacheHits: number;
319
- };
320
- error?: string;
321
- }
322
- /**
323
- * Execute the action. Per-beat fanout via `Promise.allSettled` — all beats
324
- * compose in parallel, all errors surface together (rather than fail-fast,
325
- * which would still pay for in-flight API calls without surfacing their
326
- * findings). Tests inject `overrides` to mock the SDK + clock.
327
- *
328
- * Wall-clock for an N-beat compose ≈ slowest single beat (~8s @ Sonnet 4.6
329
- * per pre-flight PR #111), regardless of N. Cost still scales with N.
330
- */
331
- export declare function executeComposeScenesWithSkills(params: ComposeScenesParams, outputDir: string, overrides?: {
332
- callLLM?: LLMCallFn;
333
- now?: () => number;
334
- }): Promise<ComposeScenesActionResult>;
@@ -1,16 +0,0 @@
1
- /**
2
- * @module _shared/hf-skill-bundle/bundle-content
3
- *
4
- * Vendored Hyperframes skill content as TS template-literal constants.
5
- * AUTO-GENERATED — regenerate via `scripts/refresh-hf-bundle.sh`. Do not
6
- * hand-edit; modify the sibling .md files (audit reference) and re-run
7
- * the script.
8
- *
9
- * Source: github.com/heygen-com/hyperframes (Apache 2.0). See `./NOTICE`
10
- * for license + provenance, and `/CREDITS.md` for the relationship.
11
- */
12
- export declare const SKILL_MD = "---\nname: hyperframes\ndescription: Create video compositions, animations, title cards, overlays, captions, voiceovers, audio-reactive visuals, and scene transitions in HyperFrames HTML. Use when asked to build any HTML-based video content, add captions or subtitles synced to audio, generate text-to-speech narration, create audio-reactive animation (beat sync, glow, pulse driven by music), add animated text highlighting (marker sweeps, hand-drawn circles, burst lines, scribble, sketchout), or add transitions between scenes (crossfades, wipes, reveals, shader transitions). Covers composition authoring, timing, media, and the full video production workflow. For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill.\n---\n\n# HyperFrames\n\nHTML 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.\n\n## Approach\n\nBefore writing HTML, think at a high level:\n\n1. **What** \u2014 what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.\n2. **Structure** \u2014 how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).\n3. **Timing** \u2014 which clips drive the duration, where do transitions land, what's the pacing.\n4. **Layout** \u2014 build the end-state first. See \"Layout Before Animation\" below.\n5. **Animate** \u2014 then add motion using the rules below.\n\nFor small edits (fix a color, adjust timing, add one element), skip straight to the rules.\n\n### Visual Identity Gate\n\n<HARD-GATE>\nBefore writing ANY composition HTML, you MUST have a visual identity defined. Do NOT write compositions with default or generic colors.\n\nCheck in this order:\n\n1. **DESIGN.md exists in the project?** \u2192 Read it. Use its exact colors, fonts, motion rules, and \"What NOT to Do\" constraints.\n2. **visual-style.md exists?** \u2192 Read it. Apply its `style_prompt_full` and structured fields. (Note: `visual-style.md` is a project-specific file. `visual-styles.md` is the style library with 8 named presets \u2014 different files.)\n3. **User named a style** (e.g., \"Swiss Pulse\", \"dark and techy\", \"luxury brand\")? \u2192 Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Generate a minimal DESIGN.md with: `## Style Prompt` (one paragraph), `## Colors` (3-5 hex values with roles), `## Typography` (1-2 font families), `## What NOT to Do` (3-5 anti-patterns).\n4. **None of the above?** \u2192 Ask 3 questions before writing any HTML:\n - What's the mood? (explosive / cinematic / fluid / technical / chaotic / warm)\n - Light or dark canvas?\n - Any specific brand colors, fonts, or visual references?\n Then generate a minimal DESIGN.md from the answers.\n\nEvery composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for `#333`, `#3b82f6`, or `Roboto` \u2014 you skipped this step.\n</HARD-GATE>\n\nFor motion defaults, sizing, entrance patterns, and easing \u2014 follow [house-style.md](./house-style.md). The house style handles HOW things move. The DESIGN.md handles WHAT things look like.\n\n## Layout Before Animation\n\nPosition every element where it should be at its **most visible moment** \u2014 the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.\n\n**Why this matters:** If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.\n\n### The process\n\n1. **Identify the hero frame** for each scene \u2014 the moment when the most elements are simultaneously visible. This is the layout you build.\n2. **Write static CSS** for that frame. The `.scene-content` container MUST fill the full scene using `width: 100%; height: 100%; padding: Npx;` with `display: flex; flex-direction: column; gap: Npx; box-sizing: border-box`. Use padding to push content inward \u2014 NEVER `position: absolute; top: Npx` on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve `position: absolute` for decoratives only.\n3. **Add entrances with `gsap.from()`** \u2014 animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.\n4. **Add exits with `gsap.to()`** \u2014 animate TO offscreen/invisible FROM the CSS position.\n\n### Example\n\n```css\n/* scene-content fills the scene, padding positions content */\n.scene-content {\n display: flex;\n flex-direction: column;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 120px 160px;\n gap: 24px;\n box-sizing: border-box;\n}\n.title {\n font-size: 120px;\n}\n.subtitle {\n font-size: 42px;\n}\n/* Container fills any scene size (1920x1080, 1080x1920, etc).\n Padding positions content. Flex + gap handles spacing. */\n```\n\n**WRONG \u2014 hardcoded dimensions and absolute positioning:**\n\n```css\n.scene-content {\n position: absolute;\n top: 200px;\n left: 160px;\n width: 1920px;\n height: 1080px;\n display: flex; /* ... */\n}\n```\n\n```js\n// Step 3: Animate INTO those positions\ntl.from(\".title\", { y: 60, opacity: 0, duration: 0.6, ease: \"power3.out\" }, 0);\ntl.from(\".subtitle\", { y: 40, opacity: 0, duration: 0.5, ease: \"power3.out\" }, 0.2);\ntl.from(\".logo\", { scale: 0.8, opacity: 0, duration: 0.4, ease: \"power2.out\" }, 0.3);\n\n// Step 4: Animate OUT from those positions\ntl.to(\".title\", { y: -40, opacity: 0, duration: 0.4, ease: \"power2.in\" }, 3);\ntl.to(\".subtitle\", { y: -30, opacity: 0, duration: 0.3, ease: \"power2.in\" }, 3.1);\ntl.to(\".logo\", { scale: 0.9, opacity: 0, duration: 0.3, ease: \"power2.in\" }, 3.2);\n```\n\n### When elements share space across time\n\nIf element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist \u2014 but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.\n\n### What counts as intentional overlap\n\nLayered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching **unintentional** overlap \u2014 two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.\n\n## Data Attributes\n\n### All Clips\n\n| Attribute | Required | Values |\n| ------------------ | --------------------------------- | ------------------------------------------------------ |\n| `id` | Yes | Unique identifier |\n| `data-start` | Yes | Seconds or clip ID reference (`\"el-1\"`, `\"intro + 2\"`) |\n| `data-duration` | Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |\n| `data-track-index` | Yes | Integer. Same-track clips cannot overlap. |\n| `data-media-start` | No | Trim offset into source (seconds) |\n| `data-volume` | No | 0-1 (default 1) |\n\n`data-track-index` does **not** affect visual layering \u2014 use CSS `z-index`.\n\n### Composition Clips\n\n| Attribute | Required | Values |\n| ---------------------------- | -------- | -------------------------------------------- |\n| `data-composition-id` | Yes | Unique composition ID |\n| `data-start` | Yes | Start time (root composition: use `\"0\"`) |\n| `data-duration` | Yes | Takes precedence over GSAP timeline duration |\n| `data-width` / `data-height` | Yes | Pixel dimensions (1920x1080 or 1080x1920) |\n| `data-composition-src` | No | Path to external HTML file |\n\n## Composition Structure\n\nSub-compositions loaded via `data-composition-src` use a `<template>` wrapper. **Standalone compositions (the main index.html) do NOT use `<template>`** \u2014 they put the `data-composition-id` div directly in `<body>`. Using `<template>` on a standalone file hides all content from the browser and breaks rendering.\n\nSub-composition structure:\n\n```html\n<template id=\"my-comp-template\">\n <div data-composition-id=\"my-comp\" data-width=\"1920\" data-height=\"1080\">\n <!-- content -->\n <style>\n [data-composition-id=\"my-comp\"] {\n /* scoped styles */\n }\n </style>\n <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n <script>\n window.__timelines = window.__timelines || {};\n const tl = gsap.timeline({ paused: true });\n // tweens...\n window.__timelines[\"my-comp\"] = tl;\n </script>\n </div>\n</template>\n```\n\nLoad 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>`\n\n## Video and Audio\n\nVideo must be `muted playsinline`. Audio is always a separate `<audio>` element:\n\n```html\n<video\n id=\"el-v\"\n data-start=\"0\"\n data-duration=\"30\"\n data-track-index=\"0\"\n src=\"video.mp4\"\n muted\n playsinline\n></video>\n<audio\n id=\"el-a\"\n data-start=\"0\"\n data-duration=\"30\"\n data-track-index=\"2\"\n src=\"video.mp4\"\n data-volume=\"1\"\n></audio>\n```\n\n## Timeline Contract\n\n- All timelines start `{ paused: true }` \u2014 the player controls playback\n- Register every timeline: `window.__timelines[\"<composition-id>\"] = tl`\n- Framework auto-nests sub-timelines \u2014 do NOT manually add them\n- Duration comes from `data-duration`, not from GSAP timeline length\n- Never create empty tweens to set duration\n\n## Rules (Non-Negotiable)\n\n**Deterministic:** No `Math.random()`, `Date.now()`, or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).\n\n**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()`.\n\n**Animation conflicts:** Never animate the same property on the same element from multiple timelines simultaneously.\n\n**No `repeat: -1`:** Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: `repeat: Math.ceil(duration / cycleDuration) - 1`.\n\n**Synchronous timeline construction:** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. Fonts are embedded by the compiler, so they're available immediately \u2014 no need to wait for font loading.\n\n**Never do:**\n\n1. Forget `window.__timelines` registration\n2. Use video for audio \u2014 always muted video + separate `<audio>`\n3. Nest video inside a timed div \u2014 use a non-timed wrapper\n4. Use `data-layer` (use `data-track-index`) or `data-end` (use `data-duration`)\n5. Animate video element dimensions \u2014 animate a wrapper div\n6. Call play/pause/seek on media \u2014 framework owns playback\n7. Create a top-level container without `data-composition-id`\n8. Use `repeat: -1` on any timeline or tween \u2014 always finite repeats\n9. Build timelines asynchronously (inside `async`, `setTimeout`, `Promise`)\n10. Use `gsap.set()` on clip elements from later scenes \u2014 they don't exist in the DOM at page load. Use `tl.set(selector, vars, timePosition)` inside the timeline at or after the clip's `data-start` time instead.\n11. Use `<br>` in content text \u2014 forced line breaks don't account for actual rendered font width. Text that wraps naturally + a `<br>` produces an extra unwanted break, causing overlap. Let text wrap via `max-width` instead. Exception: short display titles where each word is deliberately on its own line (e.g., \"THE\\nIMMORTAL\\nGAME\" at 130px).\n\n## Scene Transitions (Non-Negotiable)\n\nEvery multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.\n\n1. **ALWAYS use transitions between scenes.** No jump cuts. No exceptions.\n2. **ALWAYS use entrance animations on every scene.** Every element animates IN via `gsap.from()`. No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.\n3. **NEVER use exit animations** except on the final scene. This means: NO `gsap.to()` that animates opacity to 0, y offscreen, scale to 0, or any other \"out\" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.\n4. **Final scene only:** The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where `gsap.to(..., { opacity: 0 })` is allowed.\n\n**WRONG \u2014 exit animation before transition:**\n\n```js\n// BANNED \u2014 this empties the scene before the transition can use it\ntl.to(\"#s1-title\", { opacity: 0, y: -40, duration: 0.4 }, 6.5);\ntl.to(\"#s1-subtitle\", { opacity: 0, duration: 0.3 }, 6.7);\n// transition fires on empty frame\n```\n\n**RIGHT \u2014 entrance only, transition handles exit:**\n\n```js\n// Scene 1 entrance animations\ntl.from(\"#s1-title\", { y: 50, opacity: 0, duration: 0.7, ease: \"power3.out\" }, 0.3);\ntl.from(\"#s1-subtitle\", { y: 30, opacity: 0, duration: 0.5, ease: \"power2.out\" }, 0.6);\n// NO exit tweens \u2014 transition at 7.2s handles the scene change\n// Scene 2 entrance animations\ntl.from(\"#s2-heading\", { x: -40, opacity: 0, duration: 0.6, ease: \"expo.out\" }, 8.0);\n```\n\n## Animation Guardrails\n\n- Offset first animation 0.1-0.3s (not t=0)\n- Vary eases across entrance tweens \u2014 use at least 3 different eases per scene\n- Don't repeat an entrance pattern within a scene\n- Avoid full-screen linear gradients on dark backgrounds (H.264 banding \u2014 use radial or solid + localized glow)\n- 60px+ headlines, 20px+ body, 16px+ data labels for rendered video\n- `font-variant-numeric: tabular-nums` on number columns\n\nWhen no `visual-style.md` or animation direction is provided, follow [house-style.md](./house-style.md) for aesthetic defaults.\n\n## Typography and Assets\n\n- **Fonts:** Just write the `font-family` you want in CSS \u2014 the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.\n- Add `crossorigin=\"anonymous\"` to external media\n- For dynamic text overflow, use `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })`\n- All files live at the project root alongside `index.html`; sub-compositions use `../`\n\n## Editing Existing Compositions\n\n- Read the full composition first \u2014 match existing fonts, colors, animation patterns\n- Only change what was requested\n- Preserve timing of unrelated clips\n\n## Output Checklist\n\n- [ ] `npx hyperframes lint` and `npx hyperframes validate` both pass\n- [ ] `npx hyperframes inspect` passes, or every reported overflow is intentionally marked\n- [ ] Contrast warnings addressed (see Quality Checks below)\n- [ ] Layout issues addressed (see Quality Checks below)\n- [ ] Animation choreography verified (see Quality Checks below)\n\n## Quality Checks\n\n### Visual Inspect\n\n`hyperframes inspect` runs the composition in headless Chrome, seeks through the timeline, and maps visual layout issues with timestamps, selectors, bounding boxes, and fix hints. Run it after `lint` and `validate`:\n\n```bash\nnpx hyperframes inspect\nnpx hyperframes inspect --json\n```\n\nFailures usually mean text is spilling out of a bubble/card, a fixed-size label is clipping dynamic copy, or text has moved off the canvas. Fix by increasing container size or padding, reducing font size or letter spacing, adding a real `max-width` so text wraps inside the container, or using `window.__hyperframes.fitTextFontSize(...)` for dynamic copy.\n\nUse `--samples 15` for dense videos and `--at 1.5,4,7.25` for specific hero frames. Repeated static issues are collapsed by default to avoid flooding agent context. If overflow is intentional for an entrance/exit animation, mark the element or ancestor with `data-layout-allow-overflow`. If a decorative element should never be audited, mark it with `data-layout-ignore`.\n\n`hyperframes layout` is the compatibility alias for the same check.\n\n### Contrast\n\n`hyperframes validate` runs a WCAG contrast audit by default. It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:\n\n```\n\u26A0 WCAG AA contrast warnings (3):\n \u00B7 .subtitle \"secondary text\" \u2014 2.67:1 (need 4.5:1, t=5.3s)\n```\n\nIf warnings appear:\n\n- On dark backgrounds: brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text, 24px+ or 19px+ bold)\n- On light backgrounds: darken it\n- Stay within the palette family \u2014 don't invent a new color, adjust the existing one\n- Re-run `hyperframes validate` until clean\n\nUse `--no-contrast` to skip if iterating rapidly and you'll check later.\n\n### Animation Map\n\nAfter authoring animations, run the animation map to verify choreography:\n\n```bash\nnode skills/hyperframes/scripts/animation-map.mjs <composition-dir> \\\n --out <composition-dir>/.hyperframes/anim-map\n```\n\nOutputs a single `animation-map.json` with:\n\n- **Per-tween summaries**: `\"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)\"`\n- **ASCII timeline**: Gantt chart of all tweens across the composition duration\n- **Stagger detection**: reports actual intervals (`\"3 elements stagger at 120ms\"`)\n- **Dead zones**: periods over 1s with no animation \u2014 intentional hold or missing entrance?\n- **Element lifecycles**: first/last animation time, final visibility\n- **Scene snapshots**: visible element state at 5 key timestamps\n- **Flags**: `offscreen`, `collision`, `invisible`, `paced-fast` (under 0.2s), `paced-slow` (over 2s)\n\nRead the JSON. Scan summaries for anything unexpected. Check every flag \u2014 fix or justify. Verify the timeline shows the intended choreography rhythm. Re-run after fixes.\n\nSkip on small edits (fixing a color, adjusting one duration). Run on new compositions and significant animation changes.\n\n---\n\n## References (loaded on demand)\n\n- **[references/captions.md](references/captions.md)** \u2014 Captions, subtitles, lyrics, karaoke synced to audio. Tone-adaptive style detection, per-word styling, text overflow prevention, caption exit guarantees, word grouping. Read when adding any text synced to audio timing.\n- **[references/tts.md](references/tts.md)** \u2014 Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.\n- **[references/audio-reactive.md](references/audio-reactive.md)** \u2014 Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.\n- **[references/css-patterns.md](references/css-patterns.md)** \u2014 CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.\n- **[references/typography.md](references/typography.md)** \u2014 Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. **Always read** \u2014 every composition has text.\n- **[references/motion-principles.md](references/motion-principles.md)** \u2014 Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.\n- **[visual-styles.md](visual-styles.md)** \u2014 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.\n- **[house-style.md](house-style.md)** \u2014 Default motion, sizing, and color palettes when no style is specified.\n- **[patterns.md](patterns.md)** \u2014 PiP, title cards, slide show patterns.\n- **[data-in-motion.md](data-in-motion.md)** \u2014 Data, stats, and infographic patterns.\n- **[references/transcript-guide.md](references/transcript-guide.md)** \u2014 Transcription commands, whisper models, external APIs, troubleshooting.\n- **[references/dynamic-techniques.md](references/dynamic-techniques.md)** \u2014 Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).\n\n- **[references/transitions.md](references/transitions.md)** \u2014 Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, CSS vs WebGL guidance. **Always read for multi-scene compositions** \u2014 scenes without transitions feel like jump cuts.\n - [transitions/catalog.md](references/transitions/catalog.md) \u2014 Hard rules, scene template, and routing to per-type implementation code.\n - Shader transitions are in `@hyperframes/shader-transitions` (`packages/shader-transitions/`) \u2014 read package source, not skill files.\n\nGSAP patterns and effects are in the `/gsap` skill.\n";
13
- export declare const HOUSE_STYLE_MD = "# House Style\n\nCreative direction for compositions when no `visual-style.md` is provided. These are starting points \u2014 override anything that doesn't serve the content.\n\n## Before Writing HTML\n\n1. **Interpret the prompt.** Generate real content. A recipe lists real ingredients. A HUD has real readouts.\n2. **Pick a palette.** Light or dark? Declare bg, fg, accent before writing code.\n3. **Pick typefaces.** Run the font discovery script in [references/typography.md](references/typography.md) \u2014 or pick a font you already know that fits the theme. The script broadens your options; it's not the only source.\n\n## Lazy Defaults to Question\n\nThese patterns are AI design tells \u2014 the first thing every LLM reaches for. If you're about to use one, pause and ask: is this a deliberate choice for THIS content, or am I defaulting?\n\n- Gradient text (`background-clip: text` + gradient)\n- Left-edge accent stripes on cards/callouts\n- Cyan-on-dark / purple-to-blue gradients / neon accents\n- Pure `#000` or `#fff` (tint toward your accent hue instead)\n- Identical card grids (same-size cards repeated)\n- Everything centered with equal weight (lead the eye somewhere)\n- Banned fonts (see [references/typography.md](references/typography.md) for full list)\n\nIf the content genuinely calls for one of these \u2014 centered layout for a solemn closing, cards for a real product UI mockup, a banned font because it's the perfect thematic match \u2014 use it. The goal is intentionality, not avoidance.\n\n## Color\n\n- Match light/dark to content: food, wellness, kids \u2192 light. Tech, cinema, finance \u2192 dark.\n- One accent hue. Same background across all scenes.\n- Tint neutrals toward your accent (even subtle warmth/coolness beats dead gray).\n- **Contrast:** enforced by `hyperframes validate` (WCAG AA). Text must be readable with decoratives removed.\n- Declare palette up front. Don't invent colors per-element.\n\n## Background Layer\n\nEvery scene needs visual depth \u2014 persistent decorative elements that stay visible while content animates in. Without these, scenes feel empty during entrance staggering.\n\nIdeas (mix and match, 2-5 per scene):\n\n- Radial glows (accent-tinted, low opacity, breathing scale)\n- Ghost text (theme words at 3-8% opacity, very large, slow drift)\n- Accent lines (hairline rules, subtle pulse)\n- Grain/noise overlay, geometric shapes, grid patterns\n- Thematic decoratives (orbit rings for space, vinyl grooves for music, grid lines for data)\n\nAll decoratives should have slow ambient GSAP animation \u2014 breathing, drift, pulse. Static decoratives feel dead.\n\n## Motion\n\nSee [references/motion-principles.md](references/motion-principles.md) for full rules. Quick: 0.3\u20130.6s, vary eases, combine transforms on entrances, overlap entries.\n\n## Typography\n\nSee [references/typography.md](references/typography.md) for full rules. Quick: 700-900 headlines / 300-400 body, serif + sans (not two sans), 60px+ headlines / 20px+ body.\n\n## Palettes\n\nDeclare one background, one foreground, one accent before writing HTML.\n\n| Category | Use for | File |\n| ----------------- | --------------------------------------------- | ---------------------------------------------------------- |\n| Bold / Energetic | Product launches, social media, announcements | [palettes/bold-energetic.md](palettes/bold-energetic.md) |\n| Warm / Editorial | Storytelling, documentaries, case studies | [palettes/warm-editorial.md](palettes/warm-editorial.md) |\n| Dark / Premium | Tech, finance, luxury, cinematic | [palettes/dark-premium.md](palettes/dark-premium.md) |\n| Clean / Corporate | Explainers, tutorials, presentations | [palettes/clean-corporate.md](palettes/clean-corporate.md) |\n| Nature / Earth | Sustainability, outdoor, organic | [palettes/nature-earth.md](palettes/nature-earth.md) |\n| Neon / Electric | Gaming, tech, nightlife | [palettes/neon-electric.md](palettes/neon-electric.md) |\n| Pastel / Soft | Fashion, beauty, lifestyle, wellness | [palettes/pastel-soft.md](palettes/pastel-soft.md) |\n| Jewel / Rich | Luxury, events, sophisticated | [palettes/jewel-rich.md](palettes/jewel-rich.md) |\n| Monochrome | Dramatic, typography-focused | [palettes/monochrome.md](palettes/monochrome.md) |\n\nOr derive from OKLCH \u2014 pick a hue, build bg/fg/accent at different lightnesses, tint everything toward that hue.\n";
14
- export declare const MOTION_PRINCIPLES_MD = "# Motion Principles\n\n## Guardrails\n\nYou know these rules but you violate them. Stop.\n\n- **Don't use the same ease on every tween.** You default to `power2.out` on everything. Vary eases like you vary font weights \u2014 no more than 2 independent tweens with the same ease in a scene.\n- **Don't use the same speed on everything.** You default to 0.4-0.5s for everything. The slowest scene should be 3\u00D7 slower than the fastest. Vary duration deliberately.\n- **Don't enter everything from the same direction.** You default to `y: 30, opacity: 0` on every element. Vary: from left, from right, from scale, opacity-only, letter-spacing.\n- **Don't use the same stagger on every scene.** Each scene needs its own rhythm.\n- **Don't use ambient zoom on every scene.** Pick different ambient motion per scene: slow pan, subtle rotation, scale push, color shift, or nothing. Stillness after motion is powerful.\n- **Don't start at t=0.** Offset the first animation 0.1-0.3s. Zero-delay feels like a jump cut.\n\n## What You Don't Do Without Being Told\n\n### Easing is emotion, not technique\n\nThe transition is the verb. The easing is the adverb. A slide-in with `expo.out` = confident. With `sine.inOut` = dreamy. With `elastic.out` = playful. Same motion, different meaning. Choose the adverb deliberately.\n\n**Direction rules \u2014 these are not optional:**\n\n- `.out` for elements entering. Starts fast, decelerates. Feels responsive. This is your default.\n- `.in` for elements leaving. Starts slow, accelerates away. Throws them off.\n- `.inOut` for elements moving between positions.\n\nYou get this backwards constantly. Ease-in for entrances feels sluggish. Ease-out for exits feels reluctant.\n\n### Speed communicates weight\n\n- Fast (0.15-0.3s) \u2014 energy, urgency, confidence\n- Medium (0.3-0.5s) \u2014 professional, most content\n- Slow (0.5-0.8s) \u2014 gravity, luxury, contemplation\n- Very slow (0.8-2.0s) \u2014 cinematic, emotional, atmospheric\n\n### Scene structure: build / breathe / resolve\n\nEvery scene has three phases. You dump everything in the build and leave nothing for breathe or resolve.\n\n- **Build (0-30%)** \u2014 elements enter, staggered. Don't dump everything at once.\n- **Breathe (30-70%)** \u2014 content visible, alive with ONE ambient motion.\n- **Resolve (70-100%)** \u2014 exit or decisive end. Exits are faster than entrances.\n\n### Transitions are meaning\n\n- **Crossfade** = \"this continues\"\n- **Hard cut** = \"wake up\" / disruption\n- **Slow dissolve** = \"drift with me\"\n\nYou crossfade everything. Use hard cuts for disruption and register shifts.\n\n### Choreography is hierarchy\n\nThe element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for completion \u2014 overlap entries. Total stagger sequence under 500ms regardless of item count.\n\n### Asymmetry\n\nEntrances need longer than exits. A card takes 0.4s to appear but 0.25s to disappear.\n\n## Visual Composition\n\nYou build for the web. Video frames are not pages.\n\n- **Two focal points minimum per scene.** The eye needs somewhere to travel. Never a single text block floating in empty space.\n- **Fill the frame.** Hero text: 60-80% of width. You will try to use web-sized elements. Don't.\n- **Three layers minimum per scene.** Background treatment (glow, oversized faded type, color panel). Foreground content. Accent elements (dividers, labels, data bars).\n- **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid #000 reads as \"nothing loaded.\"\n- **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern.\n- **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts, not centered stacks.\n- **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (scaleX from 0).\n";
15
- export declare const TYPOGRAPHY_MD = "# Typography\n\nThe compiler embeds supported fonts \u2014 just write `font-family` in CSS.\n\n## Banned\n\nTraining-data defaults that every LLM reaches for. These produce monoculture across compositions.\n\nInter, Roboto, Open Sans, Noto Sans, Arimo, Lato, Source Sans, PT Sans, Nunito, Poppins, Outfit, Sora, Playfair Display, Cormorant Garamond, Bodoni Moda, EB Garamond, Cinzel, Prata, Syne\n\n**Syne in particular** is the most overused \"distinctive\" display font. It is an instant AI design tell.\n\n## Guardrails\n\nYou know these rules but you violate them. Stop.\n\n- **Don't pair two sans-serifs.** You do this constantly \u2014 one for headlines, one for body. Cross the boundary: serif + sans, or sans + mono.\n- **One expressive font per scene.** You pick two interesting fonts trying to make it \"better.\" One performs, one recedes.\n- **Weight contrast must be extreme.** You default to 400 vs 700. Video needs 300 vs 900. The difference must be visible in motion at a glance.\n- **Video sizes, not web sizes.** Body: 20px minimum. Headlines: 60px+. Data labels: 16px. You will try to use 14px. Don't.\n\n## What You Don't Do Without Being Told\n\n- **Tension should mean something.** Don't pattern-match pairings. Ask WHY these two fonts disagree. The pairing should embody the content's contradiction \u2014 mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, it's arbitrary.\n- **Register switching.** Assign different fonts to different communicative modes \u2014 one voice for statements, another for data, another for attribution. Not hierarchy on a page. Voices in a conversation.\n- **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations, not with another font.\n- **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant.\n- **Double personality works.** Two expressive fonts can coexist if they share an attitude (both irreverent, both precise) even when their forms are completely different.\n- **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position.\n- **Motion is typography.** How a word enters carries as much meaning as the font. A 0.1s slam vs a 2s fade \u2014 same font, completely different message.\n- **Fixed reading time.** 3 seconds on screen = must be readable in 2. Fewer words, larger type.\n- **Tracking tighter than web.** -0.03em to -0.05em on display sizes. Video encoding compresses letter detail.\n\n## Finding Fonts\n\nDon't default to what you know. If the content is luxury, a grotesque sans might create more tension than the expected Didone serif. Decide the register first, then search.\n\nSave this script to `/tmp/fontquery.py` and run with `curl -s 'https://fonts.google.com/metadata/fonts' > /tmp/gfonts.json && python3 /tmp/fontquery.py /tmp/gfonts.json`:\n\n```python\nimport json, sys, random\nfrom collections import OrderedDict\n\nrandom.seed() # true random each run\n\nwith open(sys.argv[1]) as f:\n data = json.load(f)\nfonts = data.get(\"familyMetadataList\", [])\n\nban = {\"Inter\",\"Roboto\",\"Open Sans\",\"Noto Sans\",\"Lato\",\"Poppins\",\"Source Sans 3\",\n \"PT Sans\",\"Nunito\",\"Outfit\",\"Sora\",\"Playfair Display\",\"Cormorant Garamond\",\n \"Bodoni Moda\",\"EB Garamond\",\"Cinzel\",\"Prata\",\"Arimo\",\"Source Sans Pro\",\"Syne\"}\nskip_pfx = (\"Roboto\",\"Noto \",\"Google Sans\",\"Bpmf\",\"Playwrite\",\"Anek\",\"BIZ \",\n \"Nanum\",\"Shippori\",\"Sawarabi\",\"Zen \",\"Kaisei\",\"Kiwi \",\"Yuji \",\"Radio \")\n\ndef ok(f):\n if f[\"family\"] in ban: return False\n if any(f[\"family\"].startswith(b) for b in skip_pfx): return False\n if \"latin\" not in (f.get(\"subsets\") or []): return False\n return True\n\nseen = set()\nR = OrderedDict()\n\n# Trending Sans \u2014 recent (2022+), popular (<300)\nR[\"Trending Sans\"] = []\nfor f in fonts:\n if not ok(f) or f[\"family\"] in seen: continue\n if f.get(\"category\") in (\"Sans Serif\",\"Display\") and f.get(\"dateAdded\",\"\") >= \"2022-01-01\" and f.get(\"popularity\",9999) < 300:\n R[\"Trending Sans\"].append(f); seen.add(f[\"family\"])\n\n# Trending Serif \u2014 recent (2018+), popular (<600)\nR[\"Trending Serif\"] = []\nfor f in fonts:\n if not ok(f) or f[\"family\"] in seen: continue\n if f.get(\"category\") == \"Serif\" and f.get(\"dateAdded\",\"\") >= \"2018-01-01\" and f.get(\"popularity\",9999) < 600:\n R[\"Trending Serif\"].append(f); seen.add(f[\"family\"])\n\n# Monospace \u2014 recent (2018+), popular (<600)\nR[\"Monospace\"] = []\nfor f in fonts:\n if not ok(f) or f[\"family\"] in seen: continue\n if f.get(\"category\") == \"Monospace\" and f.get(\"dateAdded\",\"\") >= \"2018-01-01\" and f.get(\"popularity\",9999) < 600:\n R[\"Monospace\"].append(f); seen.add(f[\"family\"])\n\n# Impact & Condensed \u2014 heavy display fonts with 800+ weight\nR[\"Impact & Condensed\"] = []\nfor f in fonts:\n if not ok(f) or f[\"family\"] in seen: continue\n has_heavy = any(k in list(f.get(\"fonts\",{}).keys()) for k in (\"800\",\"900\"))\n is_display = f.get(\"category\") in (\"Sans Serif\",\"Display\")\n if has_heavy and is_display and f.get(\"popularity\",9999) < 400:\n R[\"Impact & Condensed\"].append(f); seen.add(f[\"family\"])\n\n# Script & Handwriting \u2014 popular (<300)\nR[\"Script & Handwriting\"] = []\nfor f in fonts:\n if not ok(f) or f[\"family\"] in seen: continue\n if f.get(\"category\") == \"Handwriting\" and f.get(\"popularity\",9999) < 300:\n R[\"Script & Handwriting\"].append(f); seen.add(f[\"family\"])\n\n\n# Randomize the top 5 in each category so the LLM doesn't always pick the same first result\nfor cat in R:\n R[cat].sort(key=lambda x: x.get(\"popularity\",9999))\n top5 = R[cat][:5]\n rest = R[cat][5:]\n random.shuffle(top5)\n R[cat] = top5 + rest\nlimits = {\"Trending Sans\":15,\"Trending Serif\":12,\"Monospace\":8,\n \"Impact & Condensed\":12,\"Script & Handwriting\":10}\nfor cat in R:\n items = R[cat][:limits.get(cat,10)]\n if not items: continue\n print(f\"--- {cat} ({len(items)}) ---\")\n for ff in items:\n var = \"VAR\" if ff.get(\"axes\") else \" \"\n print(f' {ff.get(\"popularity\"):4d} | {var} | {ff[\"family\"]}')\n print()\n```\n\nFive categories: trending sans, trending serif, monospace, impact/condensed, script/handwriting. All dynamically filtered from Google Fonts metadata \u2014 no hardcoded font names. Cross classification boundaries when pairing.\n\n## Selection Thinking\n\nDon't pick fonts by category reflex (editorial \u2192 serif, tech \u2192 mono, modern \u2192 geometric sans). That's pattern matching, not design.\n\n1. **Name the register.** What voice is the content speaking in? Institutional authority? Personal confession? Technical precision? Casual irreverence? The register narrows the field more than the category.\n2. **Think physically.** Imagine the font as a physical object the brand could ship \u2014 a museum exhibit caption, a hand-painted shop sign, a 1970s mainframe terminal manual, a fabric label inside a coat, a children's book printed on cheap newsprint, a tax form. Whichever physical object fits the register is pointing at the right _kind_ of typeface.\n3. **Reject your first instinct.** The first font that feels right is usually your training-data default for that register. If you picked it last time too, find something else.\n4. **Cross-check the assumption.** An editorial brief does NOT need a serif. A technical brief does NOT need a sans. A children's product does NOT need a rounded display font. The most distinctive choice often contradicts the category expectation.\n\n## Similar-Font Pairing\n\nNever pair two fonts that are similar but not identical \u2014 two geometric sans-serifs, two transitional serifs, two humanist sans. They create visual friction without clear hierarchy. The viewer senses something is \"off\" but can't articulate it. Either use one font at two weights, or pair fonts that contrast on multiple axes: serif + sans, condensed + wide, geometric + humanist.\n\n## Dark Backgrounds\n\nLight text on dark backgrounds creates two optical illusions you need to compensate for:\n\n- **Increased apparent weight.** Light-on-dark reads heavier than dark-on-light at the same `font-weight`. Use 350 instead of 400 for body text. Headlines are less affected because size compensates.\n- **Tighter apparent spacing.** Light halos around letterforms reduce perceived gaps. Increase `line-height` by 0.05-0.1 beyond your light-background value. For display sizes, add 0.01em `letter-spacing` to counteract.\n\n## OpenType Features for Data\n\nMost fonts ship with OpenType features that are off by default. Turn them on for data compositions:\n\n```css\n/* Tabular numbers \u2014 digits align vertically in columns */\n.stat-value,\n.timer,\n.data-column {\n font-variant-numeric: tabular-nums;\n}\n\n/* Diagonal fractions \u2014 renders 1/2 as \u00BD */\n.recipe-amount,\n.ratio {\n font-variant-numeric: diagonal-fractions;\n}\n\n/* Small caps for abbreviations \u2014 less visual shouting */\n.abbreviation,\n.unit {\n font-variant-caps: all-small-caps;\n}\n\n/* Disable ligatures in code \u2014 fi, fl, ffi should stay separate */\ncode,\n.code {\n font-variant-ligatures: none;\n}\n```\n\n`tabular-nums` is essential any time numbers are stacked vertically \u2014 stat callouts, timers, scoreboards, data tables. Without it, digits have proportional widths and columns don't align.\n";
16
- export declare const TRANSITIONS_MD = "# Scene Transitions\n\nA transition tells the viewer how two scenes relate. A crossfade says \"this continues.\" A push slide says \"next point.\" A blur crossfade says \"drift with me.\" Choose transitions that match what the content is doing emotionally, not just technically.\n\n## Animation Rules for Multi-Scene Compositions\n\nThese are non-negotiable for every multi-scene composition:\n\n1. **Every composition uses transitions.** No exceptions. Scenes without transitions feel like jump cuts.\n2. **Every scene uses entrance animations.** Elements animate IN via `gsap.from()` \u2014 opacity, position, scale, etc. No scene should pop fully-formed onto screen.\n3. **Exit animations are BANNED** except on the final scene. Do NOT use `gsap.to()` to animate elements out before a transition fires. The transition IS the exit. Outgoing scene content must be fully visible when the transition starts \u2014 the transition handles the visual handoff.\n4. **Final scene exception:** The last scene MAY fade elements out (e.g., fade to black at the end of the composition). This is the only scene where exit animations are allowed.\n\n## Energy \u2192 Primary Transition\n\n| Energy | CSS Primary | Shader Primary | Accent | Duration | Easing |\n| ---------------------------------------- | ---------------------------- | ------------------------------------ | ------------------------------ | --------- | ---------------------- |\n| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Cross-warp morph, thermal distortion | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |\n| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Whip pan, cinematic zoom | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |\n| **High** (promos, sports, music, launch) | Zoom through, overexposure | Ridged burn, glitch, chromatic split | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |\n\nPick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.\n\n## Mood \u2192 Transition Type\n\nThink about what the transition _communicates_, not just what it looks like.\n\n| Mood | Transitions | Why it works |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |\n| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn \u00B7 **Shader:** thermal distortion, light leak, cross-warp morph | Soft edges, warm color washes. Nothing sharp or mechanical. |\n| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve \u00B7 **Shader:** gravitational lens | Content transforms mechanically \u2014 compressed, shrunk, sliced, gridded. |\n| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter \u00B7 **Shader:** whip pan | Like turning a page or slicing a layout. Clean directional movement. |\n| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration \u00B7 **Shader:** glitch, chromatic split | Grid dissolve is the core \"data\" transition. Shader glitch adds posterization + scan lines. |\n| **Tense / edgy** | Glitch, VHS, chromatic aberration, ripple \u00B7 **Shader:** ridged burn, glitch, domain warp | Instability, distortion, digital breakdown. Ridged burn adds sharp lightning-crack edges. |\n| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe \u00B7 **Shader:** ripple waves, swirl vortex | Overshoot, bounce, rotation, expansion. Swirl vortex adds organic spiral distortion. |\n| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, color dip to black \u00B7 **Shader:** cinematic zoom, gravitational lens, domain warp | Scale, weight, light extremes. Shader transitions add per-pixel depth. |\n| **Premium / luxury** | Focus pull, blur crossfade, color dip to black \u00B7 **Shader:** cross-warp morph, thermal distortion | Restraint. Cross-warp morph flows both scenes into each other organically. |\n| **Retro / analog** | Film burn, light leak, VHS, clock wipe \u00B7 **Shader:** light leak | Organic imperfection. Warm color bleeds, scan line displacement. |\n\n## Narrative Position\n\n| Position | Use | Why |\n| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |\n| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |\n| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract \u2014 the content is continuing. |\n| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals \"new section\" \u2014 the viewer's brain resets. |\n| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff \u2014 spend your best transition here. |\n| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |\n| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |\n\n## Blur Intensity by Energy\n\n| Energy | Blur | Duration | Hold at peak |\n| ---------- | ------- | -------- | ------------ |\n| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |\n| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |\n| **High** | 3-6px | 0.2-0.3s | 0s |\n\n## Presets\n\n| Preset | Duration | Easing |\n| ---------- | -------- | ----------------- |\n| `snappy` | 0.2s | `power4.inOut` |\n| `smooth` | 0.4s | `power2.inOut` |\n| `gentle` | 0.6s | `sine.inOut` |\n| `dramatic` | 0.5s | `power3.in` \u2192 out |\n| `instant` | 0.15s | `expo.inOut` |\n| `luxe` | 0.7s | `power1.inOut` |\n\n## Implementation\n\nRead [transitions/catalog.md](transitions/catalog.md) for GSAP code and hard rules for every transition type.\n\n| Category | CSS | Shader (WebGL) |\n| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |\n| Push/slide | Push slide, vertical push, elastic push, squeeze | Whip pan |\n| Scale/zoom | Zoom through, zoom out, gravity drop, 3D flip | Cinematic zoom, gravitational lens |\n| Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter | SDF iris |\n| Dissolve | Crossfade, blur crossfade, focus pull, color dip | Cross-warp morph, domain warp |\n| Cover | Staggered blocks, horizontal blinds, vertical blinds | \u2014 |\n| Light | Light leak, overexposure burn, film burn | Light leak (shader), thermal distortion |\n| Distortion | Glitch, chromatic aberration, ripple, VHS tape | Glitch (shader), chromatic split, ridged burn, ripple waves, swirl vortex |\n| Pattern | Grid dissolve, morph circle | \u2014 |\n\n## Transitions That Don't Work in CSS\n\nAvoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.\n\n## CSS vs Shader\n\nCSS transitions animate scene containers with opacity, transforms, clip-path, and filters. Shader transitions composite both scene textures per-pixel on a WebGL canvas \u2014 they can warp, dissolve, and morph in ways CSS cannot.\n\n**Both are first-class options.** Shaders are provided by the `@hyperframes/shader-transitions` package \u2014 import from the package instead of writing raw GLSL. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier.\n\nWhen a composition uses shader transitions, ALL transitions in that composition should be shader-based (the WebGL canvas replaces DOM-based scene switching). Don't mix CSS and shader transitions in the same composition.\n\n## Shader-Compatible CSS Rules\n\nShader transitions capture DOM scenes to WebGL textures via html2canvas. The canvas 2D rendering pipeline doesn't match CSS exactly. Follow these rules to avoid visible artifacts at transition boundaries:\n\n1. **No `transparent` keyword in gradients.** Canvas interpolates `transparent` as `rgba(0,0,0,0)` (black at zero alpha), creating dark fringes. Always use the target color at zero alpha: `rgba(200,117,51,0)` not `transparent`.\n2. **No gradient backgrounds on elements thinner than 4px.** Canvas can't match CSS gradient rendering on 1-2px elements. Use solid `background-color` on thin accent lines.\n3. **No CSS variables (`var()`) on elements visible during capture.** html2canvas doesn't reliably resolve custom properties. Use literal color values in inline styles.\n4. **Mark uncapturable decorative elements with `data-no-capture`.** The capture function skips these. They're present on the live DOM but absent from the shader texture. Use for elements that can't follow the rules above.\n5. **No gradient opacity below 0.15.** Gradient elements below 10% opacity render differently in canvas vs CSS. Increase to 0.15+ or use a solid color at equivalent brightness.\n6. **Every `.scene` div must have explicit `background-color`, AND pass the same color as `bgColor` in the `init()` config.** The package captures scene elements via html2canvas. Both the CSS `background-color` on `.scene` and the `bgColor` config must match. Without either, the texture renders as black.\n\nThese rules only apply to shader transition compositions. CSS-only compositions have no restrictions.\n\n## Visual Pattern Warning\n\nAvoid transitions that create visible repeating geometric patterns \u2014 grids of tiles, hexagonal cells, uniform dot arrays, evenly-spaced blob circles. These look cheap and artificial regardless of the math behind them. Organic noise (FBM, domain warping) is good because it's irregular. Geometric repetition is bad because the eye instantly sees the grid.\n";
@@ -1,63 +0,0 @@
1
- /**
2
- * @module _shared/hf-skill-bundle
3
- *
4
- * Loads the vendored Hyperframes skill content as a single concatenated
5
- * system-prompt string. Used by `compose-scenes-with-skills` (v0.59+).
6
- *
7
- * Hyperframes (https://github.com/heygen-com/hyperframes, Apache 2.0)
8
- * publishes this skill content as the agent-loadable definition of its
9
- * composition craft. VibeFrame is not affiliated with HeyGen — see
10
- * `/CREDITS.md` and `./NOTICE` for the relationship + license obligations
11
- * VibeFrame honours.
12
- *
13
- * Sourcing order (highest precedence first):
14
- * 1. User-installed skill — `~/.claude/skills/hyperframes/` (read at runtime
15
- * via readFileSync — kept fresh by the user's `npx skills update` flow)
16
- * 2. Vendored snapshot in this directory (always available, even after
17
- * `npm install -g @vibeframe/cli` because the .md files are inlined
18
- * into the esbuild bundle via the text loader — see `build.js`)
19
- *
20
- * The vendored copy is byte-identical to upstream: no semantic edits. We
21
- * mirror Hyperframes' own treatment of prior art (see their CREDITS.md
22
- * "Prior art" section about Remotion). Refresh the snapshot with
23
- * `scripts/refresh-hf-bundle.sh`, which re-copies from a sibling clone of
24
- * the upstream repo and bumps BUNDLE_VERSION.
25
- *
26
- * The cache for `compose-scenes-with-skills` keys on `BUNDLE_VERSION` (folded
27
- * into the hash) so snapshot upgrades automatically invalidate previously-cached
28
- * HTML.
29
- */
30
- /**
31
- * Snapshot identifier. Bump on every refresh of the vendored MD files.
32
- * Format: `<upstream-git-sha>-<YYYY-MM-DD>` so cache keys auto-invalidate
33
- * even when the upstream sha alone is unchanged but our local copy wasn't
34
- * actually re-fetched.
35
- *
36
- * **Update rule:** any time you change a file under this directory, also
37
- * bump this constant. `scripts/refresh-hf-bundle.sh` does this automatically.
38
- */
39
- export declare const BUNDLE_VERSION = "970367f-2026-04-25";
40
- /**
41
- * Load the Hyperframes skill bundle as a single string. Prefers the
42
- * user-installed skill (kept fresh by `npx skills add heygen-com/hyperframes`
43
- * + `npx skills update`) and falls back to the vendored snapshot inlined
44
- * into this bundle.
45
- *
46
- * Side-effect free except for the optional `~/.claude/skills/hyperframes/`
47
- * filesystem read.
48
- *
49
- * Async because the vendored snapshot content is loaded via a lazy
50
- * `await import("./bundle-content.js")` (see {@link buildVendored}) so paths
51
- * that never need it (agent-mode, template composer) don't pay to evaluate the
52
- * ~52KB string. When an installed skill is present, the vendored content module
53
- * is never imported at all.
54
- */
55
- export declare function loadHyperframesSkillBundle(): Promise<{
56
- content: string;
57
- /** "installed" (user has ~/.claude/skills/hyperframes/) or "vendored" (esbuild-inlined snapshot). */
58
- source: "installed" | "vendored";
59
- /** Human-readable hint for first-load logging. */
60
- hint: string;
61
- /** Stable hash of (BUNDLE_VERSION + content). Used as cache-key input. */
62
- hash: string;
63
- }>;