style(skills): apply oxfmt to music-to-video and router docs

Fixes the Format / Preflight CI checks on the new skill files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Miao Yang
2026-06-23 22:04:39 +08:00
co-authored by Claude Opus 4.8
parent f012b8b846
commit 265b02738e
13 changed files with 166 additions and 113 deletions
@@ -141,8 +141,14 @@ Contrast with **standalone** compositions, which put the root directly in `<body
<!-- ❌ WRONG — class on the root, stylesheet keyed off it --> <!-- ❌ WRONG — class on the root, stylesheet keyed off it -->
<template> <template>
<style> <style>
.frame { position: absolute; inset: 0; background: #faf9f5; } .frame {
.frame .title { font-size: 120px; } position: absolute;
inset: 0;
background: #faf9f5;
}
.frame .title {
font-size: 120px;
}
</style> </style>
<div id="root" class="frame" data-composition-id="03-scene" ...> <div id="root" class="frame" data-composition-id="03-scene" ...>
<div class="title"></div> <div class="title"></div>
@@ -152,8 +158,14 @@ Contrast with **standalone** compositions, which put the root directly in `<body
<!-- ✅ RIGHT — root styled by #root, descendants by plain selectors --> <!-- ✅ RIGHT — root styled by #root, descendants by plain selectors -->
<template> <template>
<style> <style>
#root { position: absolute; inset: 0; background: #faf9f5; } #root {
.title { font-size: 120px; } position: absolute;
inset: 0;
background: #faf9f5;
}
.title {
font-size: 120px;
}
</style> </style>
<div id="root" data-composition-id="03-scene" ...> <div id="root" data-composition-id="03-scene" ...>
<div class="title"></div> <div class="title"></div>
+11 -11
View File
@@ -51,18 +51,18 @@ Routing needs to know **what the video is about** — its input and subject. If
## Workflow cheat-sheet ## Workflow cheat-sheet
| Workflow | Use it for | | Workflow | Use it for |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/product-launch-video` | Marketing / launching / promoting a **product** — from its URL, a brief, or a script (even if the site is only named) | | `/product-launch-video` | Marketing / launching / promoting a **product** — from its URL, a brief, or a script (even if the site is only named) |
| `/website-to-video` | Turning a **general website** into a video — site tour, portfolio / landing-page showcase, social clip from the site's visuals | | `/website-to-video` | Turning a **general website** into a video — site tour, portfolio / landing-page showcase, social clip from the site's visuals |
| `/faceless-explainer` | **Explaining a topic / concept** from text — no product, no URL; every visual is LLM-invented | | `/faceless-explainer` | **Explaining a topic / concept** from text — no product, no URL; every visual is LLM-invented |
| `/pr-to-video` | A **GitHub PR / code change** → changelog / feature-reveal / fix / refactor explainer | | `/pr-to-video` | A **GitHub PR / code change** → changelog / feature-reveal / fix / refactor explainer |
| `/embedded-captions` | Adding **captions / subtitles** to an existing talking-head video (footage untouched) | | `/embedded-captions` | Adding **captions / subtitles** to an existing talking-head video (footage untouched) |
| `/graphic-overlays` | Packaging an existing talking-head video with **designed graphic overlays** — lower-thirds, data callouts, kinetic titles, pull-quotes | | `/graphic-overlays` | Packaging an existing talking-head video with **designed graphic overlays** — lower-thirds, data callouts, kinetic titles, pull-quotes |
| `/motion-graphics` | A short, **unnarrated, design-led motion graphic** — kinetic type, a stat / chart hit, a logo sting, a lower-third overlay | | `/motion-graphics` | A short, **unnarrated, design-led motion graphic** — kinetic type, a stat / chart hit, a logo sting, a lower-third overlay |
| `/music-to-video` | A **music track** → a **beat-synced** video — lyric video, slideshow, or kinetic promo; the music drives pacing (optional user images / videos cut onto the beat grid) | | `/music-to-video` | A **music track** → a **beat-synced** video — lyric video, slideshow, or kinetic promo; the music drives pacing (optional user images / videos cut onto the beat grid) |
| `/general-video` | **Anything else** — longer or multi-scene pieces, a static loop / poster, a custom composition | | `/general-video` | **Anything else** — longer or multi-scene pieces, a static loop / poster, a custom composition |
| `/remotion-to-hyperframes` | **Porting an existing Remotion (React) composition** to HyperFrames (migration, not creation) | | `/remotion-to-hyperframes` | **Porting an existing Remotion (React) composition** to HyperFrames (migration, not creation) |
**Disambiguation (only where confusable):** **Disambiguation (only where confusable):**
+10 -10
View File
@@ -16,7 +16,7 @@ Workflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze
## Two ideas that shape everything ## Two ideas that shape everything
- **One analyzer, and you trust it.** `analyze-beatgrid.py` is the only beat analyzer — never re-measure beats with another tool or by ear. Its energy / density / rolls / onsets / silences are always reliable. Its `bpm` and `beats_sec` are reliable **only when the music is genuinely rhythmic**; on calm music the grid is a metronome the tracker imposed, so pace by phrases and energy instead and never hard-cut to it. Deciding which case you're in is each frame's `pacing` (Step 2). - **One analyzer, and you trust it.** `analyze-beatgrid.py` is the only beat analyzer — never re-measure beats with another tool or by ear. Its energy / density / rolls / onsets / silences are always reliable. Its `bpm` and `beats_sec` are reliable **only when the music is genuinely rhythmic**; on calm music the grid is a metronome the tracker imposed, so pace by phrases and energy instead and never hard-cut to it. Deciding which case you're in is each frame's `pacing` (Step 2).
- **One frame = one file; groups live inside.** Step 2 cuts the track into **frames**, and each frame becomes one composition file `compositions/frames/NN-<frame_id>.html`, built by one frame-worker. A frame can subdivide into **groups** (each a template or a motion-primitives combo). Extra density goes *inside* a group, so **frame count tracks distinct treatments, not beats** — a fast track does not blow up the number of sub-agents. - **One frame = one file; groups live inside.** Step 2 cuts the track into **frames**, and each frame becomes one composition file `compositions/frames/NN-<frame_id>.html`, built by one frame-worker. A frame can subdivide into **groups** (each a template or a motion-primitives combo). Extra density goes _inside_ a group, so **frame count tracks distinct treatments, not beats** — a fast track does not blow up the number of sub-agents.
--- ---
@@ -166,16 +166,16 @@ Inspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stop
**Scripts** under `scripts/`: `analyze-beatgrid.py` (the one analyzer), `validate-plan.mjs` (plan check), `assemble-index.mjs` (index assembly), `stage-assets.mjs` (stage user media), `lib/storyboard.mjs` (vendored parser). Everything else is the `hyperframes` CLI. **Scripts** under `scripts/`: `analyze-beatgrid.py` (the one analyzer), `validate-plan.mjs` (plan check), `assemble-index.mjs` (index assembly), `stage-assets.mjs` (stage user media), `lib/storyboard.mjs` (vendored parser). Everything else is the `hyperframes` CLI.
| Read | When | | Read | When |
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| [`references/frame-skeleton.md`](references/frame-skeleton.md) | Step 2: read the music, lay out the frames, set pacing | | [`references/frame-skeleton.md`](references/frame-skeleton.md) | Step 2: read the music, lay out the frames, set pacing |
| [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan | | [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan |
| [`references/template-catalog.md`](references/template-catalog.md) | Step 3: pick a template per group | | [`references/template-catalog.md`](references/template-catalog.md) | Step 3: pick a template per group |
| [`references/motion-primitive-catalog.md`](references/motion-primitive-catalog.md) | Step 3/4: L0 recipes for free-compose | | [`references/motion-primitive-catalog.md`](references/motion-primitive-catalog.md) | Step 3/4: L0 recipes for free-compose |
| [`references/montage.md`](references/montage.md) | Step 3/4: asset treatments (beat-cut / ken-burns) | | [`references/montage.md`](references/montage.md) | Step 3/4: asset treatments (beat-cut / ken-burns) |
| [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) | Step 4: dispatch + build one frame | | [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) | Step 4: dispatch + build one frame |
| `../hyperframes-core/references/subagent-dispatch.md` | Step 4: dispatch sub-agents safely | | `../hyperframes-core/references/subagent-dispatch.md` | Step 4: dispatch sub-agents safely |
| `../hyperframes-creative/references/design-spec.md` | Step 3: pick the preset (the brand) | | `../hyperframes-creative/references/design-spec.md` | Step 3: pick the preset (the brand) |
## Directory layout ## Directory layout
@@ -17,10 +17,10 @@ edits the same file.
`audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are `audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are
reliable only when the music is **actually rhythmic**. This decides each frame's `pacing`: reliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:
| Field | Trust | | Field | Trust |
| --- | --- | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `energy_phases[]` (level / energy / density / feel), `events[]` + `onset_rate`, `rolls[]` (and their **absence**), `silences[]`, `hard_stops[]`, `key_moments[]`, `phrases[]`, `audio.duration_sec` | **Always** — robust measurements | | `energy_phases[]` (level / energy / density / feel), `events[]` + `onset_rate`, `rolls[]` (and their **absence**), `silences[]`, `hard_stops[]`, `key_moments[]`, `phrases[]`, `audio.duration_sec` | **Always** — robust measurements |
| `tempo.bpm`, `grid.beats_sec` / `downbeats_sec` **precision** | **Only when the music is rhythmic.** On calm / sparse material the beat grid is a metronome the tracker *imposes* (often octave-doubled) — usually **more grid beats than real onsets**. Do **not** anchor cuts to it there. | | `tempo.bpm`, `grid.beats_sec` / `downbeats_sec` **precision** | **Only when the music is rhythmic.** On calm / sparse material the beat grid is a metronome the tracker _imposes_ (often octave-doubled) — usually **more grid beats than real onsets**. Do **not** anchor cuts to it there. |
- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid. - **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.
- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats. - **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.
@@ -58,30 +58,36 @@ A valid `STORYBOARD.md` with the spine set and every frame's treatment left for
```markdown ```markdown
--- ---
compositionId: bgm compositionId: bgm
duration_s: 30.0 # == audiomap.audio.duration_sec duration_s: 30.0 # == audiomap.audio.duration_sec
canvas: { w: 1920, h: 1080, fps: 30 } canvas: { w: 1920, h: 1080, fps: 30 }
style: # blank — Step 3 fills it from the chosen frame.md preset style: # blank — Step 3 fills it from the chosen frame.md preset
build_notes: ["one paused timeline per frame", "no remote assets"] build_notes: ["one paused timeline per frame", "no remote assets"]
--- ---
## Frame 1 — f1 ## Frame 1 — f1
- src: compositions/frames/01-f1.html - src: compositions/frames/01-f1.html
- duration: 7.198s # = span length; assembler sums these for cumulative data-start - duration: 7.198s # = span length; assembler sums these for cumulative data-start
- span_sec: [0.0, 7.198] # track seconds; frames tile the track - span_sec: [0.0, 7.198] # track seconds; frames tile the track
- pacing: beat_cut - pacing: beat_cut
- mood: [hype] - mood: [hype]
- feel: accelerating onset stream building into a held downbeat - feel: accelerating onset stream building into a held downbeat
### Groups ### Groups
- TBD (Step 3) - TBD (Step 3)
## Frame 2 — f2 ## Frame 2 — f2
- src: compositions/frames/02-f2.html - src: compositions/frames/02-f2.html
- duration: 10.4s - duration: 10.4s
- span_sec: [7.198, 17.598] - span_sec: [7.198, 17.598]
- pacing: phrase_flow - pacing: phrase_flow
- mood: [warm, cinematic] - mood: [warm, cinematic]
- feel: calm held pad, one long onset desert - feel: calm held pad, one long onset desert
### Groups ### Groups
- TBD (Step 3) - TBD (Step 3)
``` ```
@@ -11,6 +11,7 @@ realizes it inside the frame file (HOW). **Obey the frame's `pacing`.**
## The three treatments ## The three treatments
### `beat_cut` — one clip per anchor (only on a `beat_cut` frame) ### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)
The asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor The asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor
(the frame's beats/onsets from the audiomap). Each clip is a `class="clip"` element (the frame's beats/onsets from the audiomap). Each clip is a `class="clip"` element
(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with (`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with
@@ -21,12 +22,14 @@ crossfade the outgoing content to `opacity:0` ending **at** the next anchor, the
Cut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat. Cut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.
### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame) ### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)
For calm frames: one clip held over the span with a slow scale/translate push (e.g. scale For calm frames: one clip held over the span with a slow scale/translate push (e.g. scale
1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by 1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by
beats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment beats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment
when the beat grid is unreliable (calm music). when the beat grid is unreliable (calm music).
### `bg_under_text` — clip dimmed behind a template/free group ### `bg_under_text` — clip dimmed behind a template/free group
A full-bleed clip dimmed ~3050% as the background of a group whose foreground is a template A full-bleed clip dimmed ~3050% as the background of a group whose foreground is a template
or free-compose typographic treatment. The text rides on the same anchors; the clip is the or free-compose typographic treatment. The text rides on the same anchors; the clip is the
bed. Use when the user wants their footage present but the message must stay readable. bed. Use when the user wants their footage present but the message must stay readable.
@@ -29,54 +29,54 @@ Quantified from frame-accurate reverse engineering of beat-synced reels:
Each runnable recipe is self-contained and lint-clean: showcase chrome stripped, system-font Each runnable recipe is self-contained and lint-clean: showcase chrome stripped, system-font
fallback, shared `../assets/gsap.min.js`, one paused timeline on `window.__timelines["main"]`. fallback, shared `../assets/gsap.min.js`, one paused timeline on `window.__timelines["main"]`.
| id | anchor | what it does | recipe | | id | anchor | what it does | recipe |
| -------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| `hypercut-whip` | beat / hard_stop | fast whip-pan hard cut between frames | ✓ | | `hypercut-whip` | beat / hard_stop | fast whip-pan hard cut between frames | ✓ |
| `kinetic-letter-in` | downbeat / phrase | per-letter kinetic entrance | ✓ | | `kinetic-letter-in` | downbeat / phrase | per-letter kinetic entrance | ✓ |
| `braam-punch` | drop / surge | big "braam" impact — scale + weight slam | ✓ | | `braam-punch` | drop / surge | big "braam" impact — scale + weight slam | ✓ |
| `chromatic-split` | snare / glitch / surge | RGB channel split / glitch on a word | ✓ | | `chromatic-split` | snare / glitch / surge | RGB channel split / glitch on a word | ✓ |
| `mask-reveal` | section_start / downbeat | clip-path mask wipe reveal | ✓ | | `mask-reveal` | section_start / downbeat | clip-path mask wipe reveal | ✓ |
| `screen-shake` | drop / crash / kick | camera / screen shake jitter | ✓ | | `screen-shake` | drop / crash / kick | camera / screen shake jitter | ✓ |
| `binary-decrypt` | roll / build | scramble→decode text (binary → word) | ✓ | | `binary-decrypt` | roll / build | scramble→decode text (binary → word) | ✓ |
| `dolly-zoom` | phrase / build | vertigo dolly-zoom (scale vs perspective) | ✓ | | `dolly-zoom` | phrase / build | vertigo dolly-zoom (scale vs perspective) | ✓ |
| `iris-open` | section_start / reveal | circular iris-open reveal | ✓ | | `iris-open` | section_start / reveal | circular iris-open reveal | ✓ |
| `electric-arc` | accent / glitch | electric arc / lightning accent | ✓ | | `electric-arc` | accent / glitch | electric arc / lightning accent | ✓ |
| `neon-flicker` | hold / texture | neon-sign flicker | ✓ | | `neon-flicker` | hold / texture | neon-sign flicker | ✓ |
| `chrome-sweep` | downbeat / reveal | metallic specular sweep across text | ✓ | | `chrome-sweep` | downbeat / reveal | metallic specular sweep across text | ✓ |
| `slot-machine-reveal`| roll → downbeat | slot-machine spin-to-land character reveal | ✓ | | `slot-machine-reveal` | roll → downbeat | slot-machine spin-to-land character reveal | ✓ |
| `liquid-morph` | phrase / transition | liquid / blob morph | ✓ | | `liquid-morph` | phrase / transition | liquid / blob morph | ✓ |
| `gooey-metaball` | build / drop | gooey metaball merge field | ✓ | | `gooey-metaball` | build / drop | gooey metaball merge field | ✓ |
| `3d-card-flip` | downbeat / swap | 3D card flip (rotateY) | ✓ | | `3d-card-flip` | downbeat / swap | 3D card flip (rotateY) | ✓ |
| `crash-zoom-in` | drop / surge | violent crash zoom-in | ✓ | | `crash-zoom-in` | drop / surge | violent crash zoom-in | ✓ |
| `spotlight-sweep` | reveal / hold | spotlight / gradient sweep over text | ✓ | | `spotlight-sweep` | reveal / hold | spotlight / gradient sweep over text | ✓ |
| `outline-to-fill` | downbeat / reveal | stroke outline → solid fill | ✓ | | `outline-to-fill` | downbeat / reveal | stroke outline → solid fill | ✓ |
| `counting-punch` | roll → downbeat | number count-up that punches & locks | ✓ | | `counting-punch` | roll → downbeat | number count-up that punches & locks | ✓ |
| `particle-burst` | drop / crash | particle explosion burst | ✓ | | `particle-burst` | drop / crash | particle explosion burst | ✓ |
| `radial-burst-lines` | drop / surge | radial speed-lines burst | ✓ | | `radial-burst-lines` | drop / surge | radial speed-lines burst | ✓ |
| `pixel-dissolve` | transition / hard_stop | pixelated dissolve | ✓ | | `pixel-dissolve` | transition / hard_stop | pixelated dissolve | ✓ |
| `datamosh-smear` | glitch / transition | datamosh / motion smear | ✓ | | `datamosh-smear` | glitch / transition | datamosh / motion smear | ✓ |
| `text-wave-distort` | hold / texture | wavy text distortion | ✓ | | `text-wave-distort` | hold / texture | wavy text distortion | ✓ |
| `bg-flow-field` | energy / whole span (bed) | generative WebGL curl-noise flow-field **background bed** — palette-driven, breathes on the energy envelope; compose any foreground move over it | ✓ | | `bg-flow-field` | energy / whole span (bed) | generative WebGL curl-noise flow-field **background bed** — palette-driven, breathes on the energy envelope; compose any foreground move over it | ✓ |
| `blur-resolve` | stop / final hold | blur-in to crisp focus, then blur-out on the cut | ✓ | | `blur-resolve` | stop / final hold | blur-in to crisp focus, then blur-out on the cut | ✓ |
| `chromatic-pressure` | snare / glitch | RGB split / digital tension on a transient | ≈ `chromatic-split` | | `chromatic-pressure` | snare / glitch | RGB split / digital tension on a transient | ≈ `chromatic-split` |
| `color-grid-shuffle` | onset | grid of cells recoloured by a deterministic index per onset | — | | `color-grid-shuffle` | onset | grid of cells recoloured by a deterministic index per onset | — |
| `content-swap` | beat | 0ms swap of stacked nodes — the workhorse percussive move | ≈ `slot-machine-reveal` | | `content-swap` | beat | 0ms swap of stacked nodes — the workhorse percussive move | ≈ `slot-machine-reveal` |
| `directional-fill` | beat / reveal | directional wipe-fill (scaleX) sweeping across bars | ✓ | | `directional-fill` | beat / reveal | directional wipe-fill (scaleX) sweeping across bars | ✓ |
| `flash-cut` | drop / crash | full-frame flash masking a word / colour state change | ✓ | | `flash-cut` | drop / crash | full-frame flash masking a word / colour state change | ✓ |
| `freeze-hold` | hard_stop | freeze the moving system and hold it (desaturate + vignette) | — | | `freeze-hold` | hard_stop | freeze the moving system and hold it (desaturate + vignette) | — |
| `hard-cut` | beat / hard_stop | sample-accurate 0ms colour-block + word cut | ✓ | | `hard-cut` | beat / hard_stop | sample-accurate 0ms colour-block + word cut | ✓ |
| `mosaic-pack` | beat / build | scattered tiles fly in and pack into a grid | ✓ | | `mosaic-pack` | beat / build | scattered tiles fly in and pack into a grid | ✓ |
| `negative-space-hold`| silence / hard_stop / final hold | kill busy layers, hold one readable mark in empty space | — | | `negative-space-hold` | silence / hard_stop / final hold | kill busy layers, hold one readable mark in empty space | — |
| `overlay-pop` | accent | badge / lower-third overlay pops in over a base | — | | `overlay-pop` | accent | badge / lower-third overlay pops in over a base | — |
| `palette-flip` | section change | same layout re-skins via 0ms palette-variable flips | ✓ | | `palette-flip` | section change | same layout re-skins via 0ms palette-variable flips | ✓ |
| `staggered-exit` | phrase / transition | ordered cascade-out clearing the frame | ✓ | | `staggered-exit` | phrase / transition | ordered cascade-out clearing the frame | ✓ |
| `staggered-reveal` | build | ordered cascade-in of a stack / list | — | | `staggered-reveal` | build | ordered cascade-in of a stack / list | — |
| `system-replace` | drop / regime change | hard-cut the entire visual system, then boot the new one | — | | `system-replace` | drop / regime change | hard-cut the entire visual system, then boot the new one | — |
| `text-spectral-rays` | phrase / sweep (hero text) | volumetric spectral light-rays cast by a wordmark toward a sweeping light cursor — grain + RGB-split; **WebGL hero-text system** | ✓ | | `text-spectral-rays` | phrase / sweep (hero text) | volumetric spectral light-rays cast by a wordmark toward a sweeping light cursor — grain + RGB-split; **WebGL hero-text system** | ✓ |
| `tile-mosaic` | build / reveal | grid of tiles revealed in a diagonal sweep, assembling a poster | ✓ | | `tile-mosaic` | build / reveal | grid of tiles revealed in a diagonal sweep, assembling a poster | ✓ |
| `typewriter-reveal` | roll / build | char / word type-on, explicit per-span set (no `stagger`) + caret | ✓ | | `typewriter-reveal` | roll / build | char / word type-on, explicit per-span set (no `stagger`) + caret | ✓ |
| `value-counter` | roll → downbeat | count-up that locks on a downbeat / hard_stop | ≈ `counting-punch` | | `value-counter` | roll → downbeat | count-up that locks on a downbeat / hard_stop | ≈ `counting-punch` |
| `word-grid-burst` | onsets → downbeat | grid of words revealed per onset, refocus one on a downbeat | ✓ | | `word-grid-burst` | onsets → downbeat | grid of words revealed per onset, refocus one on a downbeat | ✓ |
> **Template-private verbs.** Some templates declare moves realized only inside their own > **Template-private verbs.** Some templates declare moves realized only inside their own
> impl — `held_lockup`, `anchor_pop_in`, `word_slot_cycle`, `per_line_color`, > impl — `held_lockup`, `anchor_pop_in`, `word_slot_cycle`, `per_line_color`,
@@ -51,19 +51,23 @@ frame is usually ONE group whose template absorbs the density internally (a meta
## Step C — per group, pick a treatment (exactly one of three) ## Step C — per group, pick a treatment (exactly one of three)
### A. Match a template ### A. Match a template
Read [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing` Read [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing`
to a template's **Reach for it when**; take the closest fit. Then bind it: to a template's **Reach for it when**; take the closest fit. Then bind it:
- Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length. - Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length.
- Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times). - Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times).
- If the template's natural stop and the group's span end disagree, snap to the nearest anchor. - If the template's natural stop and the group's span end disagree, snap to the nearest anchor.
### B. Free-compose (no template fits) ### B. Free-compose (no template fits)
Write a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md) Write a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md)
(a dominant system + the named L0 primitives + a density topology) + `anchors` (the real (a dominant system + the named L0 primitives + a density topology) + `anchors` (the real
beat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as beat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as
carefully as a matched group — never a failure. carefully as a matched group — never a failure.
### C. Asset treatment (only when the user supplied assets and they fit) ### C. Asset treatment (only when the user supplied assets and they fit)
Make it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut` Make it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut`
frame use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use frame use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use
`ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none `ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none
@@ -20,51 +20,54 @@ YAML frontmatter (the video-wide spine) + one `## Frame N — <frame_id>` block
```markdown ```markdown
--- ---
compositionId: bgm compositionId: bgm
duration_s: 30.0 # == audiomap.audio.duration_sec, exactly duration_s: 30.0 # == audiomap.audio.duration_sec, exactly
canvas: { w: 1920, h: 1080, fps: 30 } canvas: { w: 1920, h: 1080, fps: 30 }
style: # brand spine — from the chosen frame.md preset (Step 3) style: # brand spine — from the chosen frame.md preset (Step 3)
font: "EB Garamond / Inter / JetBrains Mono" # the preset's typography, verbatim font: "EB Garamond / Inter / JetBrains Mono" # the preset's typography, verbatim
palette: ["#FAF9F5", "#141413", "#CC785C", "#181715"] # ≤46 swatches from the preset's colors palette: ["#FAF9F5", "#141413", "#CC785C", "#181715"] # ≤46 swatches from the preset's colors
assets: false # false, or a note like "assets/ has 6 user photos" assets: false # false, or a note like "assets/ has 6 user photos"
build_notes: ["one paused timeline per frame", "no remote assets"] build_notes: ["one paused timeline per frame", "no remote assets"]
avoid: ["generic slideshow", "tiny unreadable hero text"] avoid: ["generic slideshow", "tiny unreadable hero text"]
--- ---
## Frame 1 — f1 ## Frame 1 — f1
- src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id
- duration: 7.198s # = span length; the assembler reads this for cumulative data-start - src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id
- span_sec: [0.0, 7.198] # track seconds; frames tile the track - duration: 7.198s # = span length; the assembler reads this for cumulative data-start
- pacing: beat_cut # beat_cut | phrase_flow (from the skeleton; obey it) - span_sec: [0.0, 7.198] # track seconds; frames tile the track
- pacing: beat_cut # beat_cut | phrase_flow (from the skeleton; obey it)
- mood: [hype] - mood: [hype]
- feel: accelerating onset stream into a held downbeat - feel: accelerating onset stream into a held downbeat
### Groups ### Groups
- **g1** — template: `intro-kinetic-cascade` - **g1** — template: `intro-kinetic-cascade`
- span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start) - span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start)
- params: { theme: "light", icon: "bolt", phrases: "[…]", climax: "{…}" } - params: { theme: "light", icon: "bolt", phrases: "[…]", climax: "{…}" }
- role_bindings: { phrase: { times: [0.14, 0.55, 0.87] }, climax: { in: 3.79, iconAt: 4.9 } } - role_bindings: { phrase: { times: [0.14, 0.55, 0.87] }, climax: { in: 3.79, iconAt: 4.9 } }
- copy: "GROWTH THROUGH CREATIVITY" - copy: "GROWTH THROUGH CREATIVITY"
- **g2** — free_design - **g2** — free_design
- span_sec: [4.017, 7.198] - span_sec: [4.017, 7.198]
- free_design: { dominant_system: "per-onset typography", primitives: ["content-swap", "braam-punch"], density_topology: "accumulate" } - free_design: { dominant_system: "per-onset typography", primitives: ["content-swap", "braam-punch"], density_topology: "accumulate" }
- anchors: [4.10, 4.80, 5.50, 6.20] # onset seconds the reveals ride (from audiomap) - anchors: [4.10, 4.80, 5.50, 6.20] # onset seconds the reveals ride (from audiomap)
- copy: ["BUILD", "SHIP", "REPEAT"] - copy: ["BUILD", "SHIP", "REPEAT"]
## Frame 2 — f2 ## Frame 2 — f2
``` ```
## Frame block — required fields ## Frame block — required fields
| field | meaning | | field | meaning |
| --- | --- | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index. | | heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index. |
| `src` | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. | | `src` | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. |
| `duration` | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`. | | `duration` | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`. |
| `span_sec` | `[start, end]` track seconds. `duration = end start`. | | `span_sec` | `[start, end]` track seconds. `duration = end start`. |
| `pacing` | `beat_cut` \| `phrase_flow` — from the skeleton; the worker must obey (no hard-cut on `phrase_flow`). | | `pacing` | `beat_cut` \| `phrase_flow` — from the skeleton; the worker must obey (no hard-cut on `phrase_flow`). |
| `mood`, `feel` | from the skeleton; tone + the one-line music situation the planner matched against. | | `mood`, `feel` | from the skeleton; tone + the one-line music situation the planner matched against. |
| `### Groups` list | ≥1 group; groups tile the frame span in order. | | `### Groups` list | ≥1 group; groups tile the frame span in order. |
## Group entry — exactly one of three kinds ## Group entry — exactly one of three kinds
@@ -25,46 +25,55 @@ Pacing tag: every template below is **beat_cut** except `held-message-living-fie
--- ---
### card-flyby ### card-flyby
- **What** — a depth column of cards rolls forward through perspective; each landing beat tumbles the next card into the front slot with a solid colored wipe, the old front falls toward camera, dwells shrink card-to-card so the deck accelerates into a held final card. - **What** — a depth column of cards rolls forward through perspective; each landing beat tumbles the next card into the front slot with a solid colored wipe, the old front falls toward camera, dwells shrink card-to-card so the deck accelerates into a held final card.
- **Reach for it when** — a stream of discrete onsets that **accelerate** (gaps shrinking / a build into a downbeat) and you want to flash a **sequence of items** — titles, projects, posters, tiles — one per hit, climaxing on a held card. - **Reach for it when** — a stream of discrete onsets that **accelerate** (gaps shrinking / a build into a downbeat) and you want to flash a **sequence of items** — titles, projects, posters, tiles — one per hit, climaxing on a held card.
- **Params**`theme`, `bgColor`, `cards`, `landings`, `yaw` - **Params**`theme`, `bgColor`, `cards`, `landings`, `yaw`
### held-message-living-field · phrase_flow ### held-message-living-field · phrase_flow
- **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes. - **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes.
- **Reach for it when** — a **calm / sparse** stretch with an onset desert — energy present but few or no onsets (a held pad or riser); you have one word or mark to hold and let breathe. - **Reach for it when** — a **calm / sparse** stretch with an onset desert — energy present but few or no onsets (a held pad or riser); you have one word or mark to hold and let breathe.
- **Params**`markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration` - **Params**`markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration`
### held-text-strobe-burst ### held-text-strobe-burst
- **What** — a dead-still word whose letters flip through texture-filled frames (texture-clipped fill + per-frame tint + bg color) every ~3 frames, in short bursts pinned to a roll. - **What** — a dead-still word whose letters flip through texture-filled frames (texture-clipped fill + per-frame tint + bg color) every ~3 frames, in short bursts pinned to a roll.
- **Reach for it when** — a **dense, hard-hitting roll / fill** and a single word you want to strobe through textures for a few bars. (Ships texture-mask PNGs under `assets/`.) - **Reach for it when** — a **dense, hard-hitting roll / fill** and a single word you want to strobe through textures for a few bars. (Ships texture-mask PNGs under `assets/`.)
- **Params**`markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration` - **Params**`markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration`
### intro-kinetic-cascade ### intro-kinetic-cascade
- **What** — a line laid out as a sequence of big editorial **phrases** (each a stacked poster with one enlarged hero word), revealed word-by-word on its anchors, hard cut between phrases, climaxing on a phrase that slides in with a swappable ringing **icon** (bell / cursor / sparkle / emoji / SVG). - **What** — a line laid out as a sequence of big editorial **phrases** (each a stacked poster with one enlarged hero word), revealed word-by-word on its anchors, hard cut between phrases, climaxing on a phrase that slides in with a swappable ringing **icon** (bell / cursor / sparkle / emoji / SVG).
- **Reach for it when** — an **intro / opening statement**: a short line to land word-by-word as big type, climaxing on one keyword + an icon. Medium-or-more energy, steady grid. - **Reach for it when** — an **intro / opening statement**: a short line to land word-by-word as big type, climaxing on one keyword + an icon. Medium-or-more energy, steady grid.
- **Params**`theme`, `icon`, `phrases`, `climax` - **Params**`theme`, `icon`, `phrases`, `climax`
### logo-split-lockup-pulse ### logo-split-lockup-pulse
- **What** — a two-part mark joined at center splits left↔right to open a gap, grows a center word-lockup one word per onset (key word lands on the downbeat surge), snap-closes on a hit, then pulses with the beat. - **What** — a two-part mark joined at center splits left↔right to open a gap, grows a center word-lockup one word per onset (key word lands on the downbeat surge), snap-closes on a hit, then pulses with the beat.
- **Reach for it when** — a short **logo / brand sting** (not a typed sentence): fast dense onsets + a sustained roll bed to pulse on, with a left/right bracketing mark. - **Reach for it when** — a short **logo / brand sting** (not a typed sentence): fast dense onsets + a sustained roll bed to pulse on, with a left/right bracketing mark.
- **Params**`bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4` - **Params**`bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4`
### poster-tile-mosaic ### poster-tile-mosaic
- **What** — a packed **mosaic** of different-sized colored tiles that tessellate to fill the frame (no overlap), driven by interchangeable beat-synced operations: staggered enter/exit, locked global recolor, snake-fill + overlay. - **What** — a packed **mosaic** of different-sized colored tiles that tessellate to fill the frame (no overlap), driven by interchangeable beat-synced operations: staggered enter/exit, locked global recolor, snake-fill + overlay.
- **Reach for it when** — many discrete, individually-placeable onsets (a hit for every tile move) with distinct sub-phases you want articulated differently (accumulate → recolor-on-roll → fill-then-drop). A dense section best held as **one** rich tile program rather than split. - **Reach for it when** — many discrete, individually-placeable onsets (a hit for every tile move) with distinct sub-phases you want articulated differently (accumulate → recolor-on-roll → fill-then-drop). A dense section best held as **one** rich tile program rather than split.
- **Params**`bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program` - **Params**`bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program`
### roll-flipbook-word-cycle ### roll-flipbook-word-cycle
- **What** — a hi-hat roll drives a centred word that flips every 16th-note through a word list; optionally the flicker resolves and locks into a final phrase. - **What** — a hi-hat roll drives a centred word that flips every 16th-note through a word list; optionally the flicker resolves and locks into a final phrase.
- **Reach for it when** — a **fast sustained-fill roll** (hundreds of hits/min, ~16th-note) with no single readable message — fill the roll with a rapidly-cycling word flipbook. - **Reach for it when** — a **fast sustained-fill roll** (hundreds of hits/min, ~16th-note) with no single readable message — fill the roll with a rapidly-cycling word flipbook.
- **Params**`bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar` - **Params**`bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar`
### split-anchor-word-slot ### split-anchor-word-slot
- **What** — a held left anchor column of fixed-word rows beside a torn-paper word-slot box on the right, driven by beat-synced operators: anchor lock-in, slot word-group cycle (in/out + per-line color), full-scene background flip, per-beat jitter, box-zoom exit wipe. Row count + number of flips are data. - **What** — a held left anchor column of fixed-word rows beside a torn-paper word-slot box on the right, driven by beat-synced operators: anchor lock-in, slot word-group cycle (in/out + per-line color), full-scene background flip, per-beat jitter, box-zoom exit wipe. Row count + number of flips are data.
- **Reach for it when** — a short section with a **held idea** (a brand / name to anchor on the left) **and** a stream of onsets popping separate words on the right, plus a dense run to ride a shake on and a strong downbeat to wipe out into. - **Reach for it when** — a short section with a **held idea** (a brand / name to anchor on the left) **and** a stream of onsets popping separate words on the right, plus a dense run to ride a shake on and a strong downbeat to wipe out into.
- **Params**`bgColor`, `anchors`, `theme`, `showText`, `program` - **Params**`bgColor`, `anchors`, `theme`, `showText`, `program`
### typewriter-phrase-keyword-shuffle ### typewriter-phrase-keyword-shuffle
- **What** — words type in one-per-onset to spell a phrase, then one keyword cycles typefaces on the beat while everything else holds dead still. - **What** — words type in one-per-onset to spell a phrase, then one keyword cycles typefaces on the beat while everything else holds dead still.
- **Reach for it when** — a steady grid with a **continuous onset stream** (no desert): a phrase to type out, then a keyword to shuffle. The inverse of `held-message-living-field` (which wants an onset desert). - **Reach for it when** — a steady grid with a **continuous onset stream** (no desert): a phrase to type out, then a keyword to shuffle. The inverse of `held-message-living-field` (which wants an onset desert).
- **Params**`bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar` - **Params**`bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar`
@@ -61,7 +61,9 @@ if (G.canvas) {
if (Number.isFinite(c.w)) WIDTH = c.w; if (Number.isFinite(c.w)) WIDTH = c.w;
if (Number.isFinite(c.h)) HEIGHT = c.h; if (Number.isFinite(c.h)) HEIGHT = c.h;
} catch { } catch {
anomalies.push(`could not JSON.parse canvas frontmatter: ${G.canvas} — using ${WIDTH}×${HEIGHT}`); anomalies.push(
`could not JSON.parse canvas frontmatter: ${G.canvas} — using ${WIDTH}×${HEIGHT}`,
);
} }
} }
@@ -88,7 +90,9 @@ for (const f of manifest.frames) {
const compId = basename(f.src).replace(/\.html?$/i, ""); const compId = basename(f.src).replace(/\.html?$/i, "");
const inner = readFileSync(compAbs, "utf8"); const inner = readFileSync(compAbs, "utf8");
if (!inner.trim() || !/<\w/.test(inner)) if (!inner.trim() || !/<\w/.test(inner))
die(`${label}: ${f.src} is empty/blank — the frame-worker wrote a partial file. Re-dispatch it.`); die(
`${label}: ${f.src} is empty/blank — the frame-worker wrote a partial file. Re-dispatch it.`,
);
if ( if (
!inner.includes(`data-composition-id="${compId}"`) && !inner.includes(`data-composition-id="${compId}"`) &&
!inner.includes(`data-composition-id='${compId}'`) !inner.includes(`data-composition-id='${compId}'`)
@@ -25,7 +25,8 @@ function die(m) {
const fromDir = flag("from", null); const fromDir = flag("from", null);
if (!fromDir) die("missing --from <srcDir>"); if (!fromDir) die("missing --from <srcDir>");
const fromAbs = resolve(fromDir); const fromAbs = resolve(fromDir);
if (!existsSync(fromAbs) || !statSync(fromAbs).isDirectory()) die(`--from is not a directory: ${fromAbs}`); if (!existsSync(fromAbs) || !statSync(fromAbs).isDirectory())
die(`--from is not a directory: ${fromAbs}`);
const hyperframesDir = resolve(flag("hyperframes", ".")); const hyperframesDir = resolve(flag("hyperframes", "."));
const into = flag("into", ""); const into = flag("into", "");
const destDir = join(hyperframesDir, "assets", into); const destDir = join(hyperframesDir, "assets", into);
@@ -50,6 +51,8 @@ for (const name of readdirSync(fromAbs)) {
landed.push(join("assets", into, basename(name))); landed.push(join("assets", into, basename(name)));
} }
console.log(`✓ stage-assets: ${staged} copied, ${skipped} already present → ${join("assets", into)}/`); console.log(
`✓ stage-assets: ${staged} copied, ${skipped} already present → ${join("assets", into)}/`,
);
for (const l of landed) console.log(` ${l}`); for (const l of landed) console.log(` ${l}`);
if (staged === 0 && skipped === 0) console.log(` (no media files found in ${fromAbs})`); if (staged === 0 && skipped === 0) console.log(` (no media files found in ${fromAbs})`);
@@ -73,7 +73,9 @@ for (const f of frames) {
sum = r3(sum); sum = r3(sum);
const tileTarget = Number.isFinite(declaredDur) ? declaredDur : audioDur; const tileTarget = Number.isFinite(declaredDur) ? declaredDur : audioDur;
if (tileTarget != null && Math.abs(sum - tileTarget) > 0.1) if (tileTarget != null && Math.abs(sum - tileTarget) > 0.1)
errors.push(`frame durations sum to ${sum}s but the track is ${tileTarget}s — frames must tile it gap-free`); errors.push(
`frame durations sum to ${sum}s but the track is ${tileTarget}s — frames must tile it gap-free`,
);
// ---------- group checks (warns) — parse RAW text ---------- // ---------- group checks (warns) — parse RAW text ----------
const FRAME_HEAD = /^##\s+(?:frame|scene|section)\b/i; const FRAME_HEAD = /^##\s+(?:frame|scene|section)\b/i;
@@ -116,7 +118,9 @@ const framesWithGroups = new Set(blocks.map((b) => b.frameLabel));
for (const f of frames) { for (const f of frames) {
const lbl = `Frame ${f.number ?? ""}${f.title ?? f.index}`.replace(/\s+—\s+$/, ""); const lbl = `Frame ${f.number ?? ""}${f.title ?? f.index}`.replace(/\s+—\s+$/, "");
if (![...framesWithGroups].some((s) => s.includes(String(f.title ?? "")))) { if (![...framesWithGroups].some((s) => s.includes(String(f.title ?? "")))) {
warns.push(`${lbl}: no parseable groups (expected \`- **gN** — template|free_design|asset …\`)`); warns.push(
`${lbl}: no parseable groups (expected \`- **gN** — template|free_design|asset …\`)`,
);
} }
} }
@@ -131,7 +135,9 @@ for (const b of blocks) {
warns.push(`${gid}: template \`${id}\` not found at templates/${id}/index.html`); warns.push(`${gid}: template \`${id}\` not found at templates/${id}/index.html`);
} }
if (b.kind === "asset" && b.pacing === "phrase_flow" && /beat_cut/.test(blockText)) if (b.kind === "asset" && b.pacing === "phrase_flow" && /beat_cut/.test(blockText))
warns.push(`${gid}: beat_cut asset treatment on a phrase_flow frame — use ken_burns/crossfade instead`); warns.push(
`${gid}: beat_cut asset treatment on a phrase_flow frame — use ken_burns/crossfade instead`,
);
} }
// ---------- report ---------- // ---------- report ----------
@@ -141,4 +147,6 @@ if (errors.length) {
console.error(`\nvalidate-plan: ${errors.length} error(s), ${warns.length} warning(s)`); console.error(`\nvalidate-plan: ${errors.length} error(s), ${warns.length} warning(s)`);
process.exit(1); process.exit(1);
} }
console.log(`✓ validate-plan: ${frames.length} frames tile ${sum}s; ${blocks.length} groups; ${warns.length} warning(s)`); console.log(
`✓ validate-plan: ${frames.length} frames tile ${sum}s; ${blocks.length} groups; ${warns.length} warning(s)`,
);
@@ -3,8 +3,9 @@
You build one frame's composition file: `compositions/frames/<frame_id>.html`. Siblings build You build one frame's composition file: `compositions/frames/<frame_id>.html`. Siblings build
the other frames in parallel. The generic HyperFrames law — sub-composition shape, timeline the other frames in parallel. The generic HyperFrames law — sub-composition shape, timeline
registration, determinism, layout — lives in `hyperframes-core` (`references/sub-compositions.md` registration, determinism, layout — lives in `hyperframes-core` (`references/sub-compositions.md`
+ `determinism-rules.md` + `data-attributes.md`); read it first. This file covers the
music-specific part. - `determinism-rules.md` + `data-attributes.md`); read it first. This file covers the
music-specific part.
Your job: **follow the manual, fetch the materials, assemble.** The storyboard tells you WHAT Your job: **follow the manual, fetch the materials, assemble.** The storyboard tells you WHAT
(the frame's groups, each group's template / primitives, content, brand, real beat-anchor (the frame's groups, each group's template / primitives, content, brand, real beat-anchor
@@ -47,7 +48,7 @@ If your dispatch carries lint / validate feedback from a prior pass, address eac
1. **Read** your `## Frame` block, `frame.md`, and the body of every template / primitive it 1. **Read** your `## Frame` block, `frame.md`, and the body of every template / primitive it
cites. Reproduce those recipes. cites. Reproduce those recipes.
2. **Work in frame-local time.** Subtract the frame start from every anchor: `local_t = track_t 2. **Work in frame-local time.** Subtract the frame start from every anchor: `local_t = track_t
span_sec[0]`. span_sec[0]`.
3. **Author** `compositions/frames/<frame_id>.html`: a `<template>` wrapping `#stage` 3. **Author** `compositions/frames/<frame_id>.html`: a `<template>` wrapping `#stage`
(`data-composition-id="<frame_id>"`), with all `<style>` / `<script>` inside it, and one (`data-composition-id="<frame_id>"`), with all `<style>` / `<script>` inside it, and one
paused `gsap.timeline({paused:true})` registered at `window.__timelines["<frame_id>"]`, built paused `gsap.timeline({paused:true})` registered at `window.__timelines["<frame_id>"]`, built