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>
+1 -1
View File
@@ -52,7 +52,7 @@ 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 |
+2 -2
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.
--- ---
@@ -167,7 +167,7 @@ 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 |
@@ -18,9 +18,9 @@ edits the same file.
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.
@@ -65,23 +65,29 @@ 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.
@@ -30,7 +30,7 @@ Each runnable recipe is self-contained and lint-clean: showcase chrome stripped,
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 | ✓ |
@@ -43,7 +43,7 @@ fallback, shared `../assets/gsap.min.js`, one paused timeline on `window.__timel
| `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) | ✓ |
@@ -66,7 +66,7 @@ fallback, shared `../assets/gsap.min.js`, one paused timeline on `window.__timel
| `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 | ✓ |
@@ -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
@@ -31,6 +31,7 @@ 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 - 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 - duration: 7.198s # = span length; the assembler reads this 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
@@ -39,6 +40,7 @@ avoid: ["generic slideshow", "tiny unreadable hero text"]
- 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: "{…}" }
@@ -51,13 +53,14 @@ avoid: ["generic slideshow", "tiny unreadable hero text"]
- 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`. |
@@ -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