{"id":"4501bb55-9680-4a90-8cf8-ee2f523a0d69","entityType":"agent","slug":"clawhub-heygen-com-music-to-video","name":"music-to-video","canonicalUrl":"https://www.xpersona.co/agent/clawhub-heygen-com-music-to-video","canonicalPath":"/agent/clawhub-heygen-com-music-to-video","generatedAt":"2026-10-10T05:41:10.170Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":null},"description":"Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2.2K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:music-to-video","sourceUrl":"https://clawhub.ai/heygen-com/music-to-video","homepage":"https://clawhub.ai/heygen-com/skills/music-to-video","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/heygen-com/music-to-video","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/heygen-com/skills/music-to-video","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":67,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"music-to-video technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":null},"stars":null,"forks":null,"downloads":2217,"packageName":null,"latestVersion":"1.0.25","tractionLabel":"2.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T17:28:08.435Z","lastCrawledAt":"2026-10-09T17:28:08.435Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T17:28:08.435Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.25","createdAt":"2026-10-07T07:31:07.609Z","changelog":"Synced from 4fcad1e (main)","fileCount":104,"zipByteSize":218585},{"version":"1.0.24","createdAt":"2026-10-04T19:32:47.766Z","changelog":"Synced from 0c3e244 (main)","fileCount":104,"zipByteSize":218534},{"version":"1.0.23","createdAt":"2026-10-04T19:19:52.748Z","changelog":"Synced from 173103d (main)","fileCount":104,"zipByteSize":218522},{"version":"1.0.22","createdAt":"2026-10-04T13:09:22.666Z","changelog":"Synced from 69a2169 (main)","fileCount":104,"zipByteSize":218592},{"version":"1.0.21","createdAt":"2026-10-02T14:32:58.326Z","changelog":"Synced from 9465048 (main)","fileCount":104,"zipByteSize":218554},{"version":"1.0.20","createdAt":"2026-10-02T00:12:47.204Z","changelog":"Synced from 37f30b1 (main)","fileCount":104,"zipByteSize":218505},{"version":"1.0.19","createdAt":"2026-09-27T21:22:31.761Z","changelog":"Synced from ff6e210 (main)","fileCount":104,"zipByteSize":218492},{"version":"1.0.18","createdAt":"2026-09-19T05:09:10.971Z","changelog":"Synced from 1a9668b (main)","fileCount":104,"zipByteSize":218776}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:music-to-video","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:music-to-video` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/heygen-com/music-to-video before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T05:41:10.165Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-music-to-video/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":null},"readme":"Skill: music-to-video\n\nOwner: heygen-com\n\nSummary: Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.\n\nTags: latest:1.0.25\n\nVersion history:\n\nv1.0.25 | 2026-10-07T07:31:07.609Z | user\n\nSynced from 4fcad1e (main)\n\nv1.0.24 | 2026-10-04T19:32:47.766Z | user\n\nSynced from 0c3e244 (main)\n\nv1.0.23 | 2026-10-04T19:19:52.748Z | user\n\nSynced from 173103d (main)\n\nv1.0.22 | 2026-10-04T13:09:22.666Z | user\n\nSynced from 69a2169 (main)\n\nv1.0.21 | 2026-10-02T14:32:58.326Z | user\n\nSynced from 9465048 (main)\n\nv1.0.20 | 2026-10-02T00:12:47.204Z | user\n\nSynced from 37f30b1 (main)\n\nv1.0.19 | 2026-09-27T21:22:31.761Z | user\n\nSynced from ff6e210 (main)\n\nv1.0.18 | 2026-09-19T05:09:10.971Z | user\n\nSynced from 1a9668b (main)\n\nv1.0.17 | 2026-09-19T03:23:06.154Z | user\n\nSynced from 2db126d (main)\n\nv1.0.16 | 2026-09-19T00:59:31.050Z | user\n\nSynced from f297653 (main)\n\nv1.0.15 | 2026-09-14T01:46:03.465Z | user\n\nSynced from f059a6e (main)\n\nv1.0.14 | 2026-09-14T01:26:28.593Z | user\n\nSynced from 95bea16 (main)\n\nv1.0.13 | 2026-09-10T03:25:27.077Z | user\n\nSynced from 0f8eb89 (main)\n\nv1.0.12 | 2026-08-21T03:10:01.246Z | user\n\nSynced from efc2e19 (main)\n\nv1.0.11 | 2026-08-18T01:48:19.626Z | user\n\nSynced from f8a1e2d (main)\n\nv1.0.10 | 2026-07-28T11:29:39.125Z | user\n\nSynced from d287e52 (main)\n\nv1.0.9 | 2026-07-20T15:22:22.670Z | user\n\nSynced from 6ad738b (main)\n\nv1.0.8 | 2026-07-15T13:24:25.956Z | user\n\nSynced from b9be0b2 (main)\n\nv1.0.7 | 2026-07-10T22:50:11.806Z | user\n\nSynced from 00d059b (main)\n\nv1.0.6 | 2026-07-10T05:17:31.018Z | user\n\nSynced from dda09c8 (main)\n\nv1.0.5 | 2026-07-08T18:01:31.363Z | user\n\nSynced from 17b8527 (main)\n\nv1.0.4 | 2026-07-08T17:32:53.498Z | user\n\nSynced from 81884a7 (main)\n\nv1.0.3 | 2026-07-08T08:40:28.203Z | user\n\nSynced from 6192ed4 (main)\n\nv1.0.2 | 2026-07-07T20:28:51.138Z | user\n\nSynced from 7286b00 (main)\n\nv1.0.1 | 2026-07-07T18:58:48.351Z | user\n\nSynced from 5fe9573 (main)\n\nv1.0.0 | 2026-07-01T11:06:51.358Z | user\n\nOfficial HyperFrames skills from heygen-com/hyperframes\n\nArchive index:\n\nArchive v1.0.25: 104 files, 218585 bytes\n\nFiles: references/frame-skeleton.md (6584b), references/montage.md (3269b), references/motion-primitive-catalog.md (9885b), references/motion-primitives/3d-card-flip/index.html (1278b), references/motion-primitives/3d-card-flip/scene.html (3393b), references/motion-primitives/assets/gsap.min.js (72927b), references/motion-primitives/bg-flow-field/index.html (1274b), references/motion-primitives/bg-flow-field/scene.html (11328b), references/motion-primitives/binary-decrypt/index.html (1310b), references/motion-primitives/binary-decrypt/scene.html (2711b), references/motion-primitives/blur-resolve/index.html (1272b), references/motion-primitives/blur-resolve/scene.html (1688b), references/motion-primitives/braam-punch/index.html (1276b), references/motion-primitives/braam-punch/scene.html (2728b), references/motion-primitives/chromatic-split/index.html (1284b), references/motion-primitives/chromatic-split/scene.html (3481b), references/motion-primitives/chrome-sweep/index.html (1278b), references/motion-primitives/chrome-sweep/scene.html (1824b), references/motion-primitives/counting-punch/index.html (1310b), references/motion-primitives/counting-punch/scene.html (3214b), references/motion-primitives/crash-zoom-in/index.html (1280b), references/motion-primitives/crash-zoom-in/scene.html (3386b), references/motion-primitives/datamosh-smear/index.html (1341b), references/motion-primitives/datamosh-smear/scene.html (3503b), references/motion-primitives/directional-fill/index.html (1284b), references/motion-primitives/directional-fill/scene.html (2773b), references/motion-primitives/dolly-zoom/index.html (1274b), references/motion-primitives/dolly-zoom/scene.html (3278b), references/motion-primitives/electric-arc/index.html (1306b), references/motion-primitives/electric-arc/scene.html (3263b), references/motion-primitives/flash-cut/index.html (1270b), references/motion-primitives/flash-cut/scene.html (3146b), references/motion-primitives/gooey-metaball/index.html (1310b), references/motion-primitives/gooey-metaball/scene.html (4918b), references/motion-primitives/hard-cut/index.html (1264b), references/motion-primitives/hard-cut/scene.html (2708b), references/motion-primitives/hypercut-whip/index.html (1280b), references/motion-primitives/hypercut-whip/scene.html (1229b), references/motion-primitives/iris-open/index.html (1272b), references/motion-primitives/iris-open/scene.html (2015b), references/motion-primitives/kinetic-letter-in/index.html (1288b), references/motion-primitives/kinetic-letter-in/scene.html (1764b), references/motion-primitives/liquid-morph/index.html (1278b), references/motion-primitives/liquid-morph/scene.html (2992b), references/motion-primitives/mask-reveal/index.html (1276b), references/motion-primitives/mask-reveal/scene.html (2086b), references/motion-primitives/mosaic-pack/index.html (1274b), references/motion-primitives/mosaic-pack/scene.html (2829b), references/motion-primitives/neon-flicker/index.html (1278b), references/motion-primitives/neon-flicker/scene.html (1979b), references/motion-primitives/outline-to-fill/index.html (1284b), references/motion-primitives/outline-to-fill/scene.html (2282b), references/motion-primitives/palette-flip/index.html (1276b), references/motion-primitives/palette-flip/scene.html (2824b), references/motion-primitives/particle-burst/index.html (1282b), references/motion-primitives/particle-burst/scene.html (3185b), references/motion-primitives/pixel-dissolve/index.html (1282b), references/motion-primitives/pixel-dissolve/scene.html (2645b), references/motion-primitives/radial-burst-lines/index.html (1290b), references/motion-primitives/radial-burst-lines/scene.html (3742b), references/motion-primitives/screen-shake/index.html (1278b), references/motion-primitives/screen-shake/scene.html (2197b), references/motion-primitives/slot-machine-reveal/index.html (1292b), references/motion-primitives/slot-machine-reveal/scene.html (2820b), references/motion-primitives/spotlight-sweep/index.html (1284b), references/motion-primitives/spotlight-sweep/scene.html (3009b), references/motion-primitives/staggered-exit/index.html (1276b), references/motion-primitives/staggered-exit/scene.html (2413b), references/motion-primitives/text-spectral-rays/index.html (1276b), references/motion-primitives/text-spectral-rays/scene.html (11504b), references/motion-primitives/text-spectral-rays/USAGE.md (2401b), references/motion-primitives/text-wave-distort/index.html (1316b), references/motion-primitives/text-wave-distort/scene.html (2999b), references/motion-primitives/tile-mosaic/index.html (1274b), references/motion-primitives/tile-mosaic/scene.html (3063b), references/motion-primitives/typewriter-reveal/index.html (1286b), references/motion-primitives/typewriter-reveal/scene.html (2634b), references/motion-primitives/word-grid-burst/index.html (1282b), references/motion-primitives/word-grid-burst/scene.html (2707b), references/planning.md (6194b)\n\nFile v1.0.25:SKILL.md\n\n---\nname: music-to-video\ndescription: \"Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.\"\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update music-to-video`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n# music-to-video — one music-grounded, beat-synced video workflow\n\nUse this skill to turn a **music track** into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is **no narration and no website capture**. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.\n\nYou are the **orchestrator**. Work in `videos/<project>/`. Run the steps in order and pass each **Gate** before moving on. Two steps need the user: **Step 3** (plan approval) and **Step 6** (render approval) — both are checkpoint gates per `../hyperframes/references/brief-contract.md` (read it before Step 0): in autonomous mode, post the Step 3 summary as a heads-up and proceed; Step 6 render approval is still asked, as the one kept question. Do every step yourself except **Step 4**, where you dispatch **one sub-agent per frame**. Keep design and motion rules out of this file — they live in `references/` and the `frame-worker` sub-agent.\n\n`SKILL_DIR` = this skill directory. `PROJECT_DIR` = `videos/<project-name>/`.\n\nWorkflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze → `audiomap.json`; Step 2 skeleton → `STORYBOARD.md` (frames, groups `TBD`); Step 3 plan → complete `STORYBOARD.md` + `frame.md`; Step 4 build → `compositions/frames/NN-*.html`; Step 5 assemble → `index.html`; Step 6 render → `renders/video.mp4`.\n\n## Two ideas that shape everything\n\n- **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).\n- **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.\n\n---\n\n## Step 0: Setup, BGM, and inputs\n\nGoal: Establish the music source, create the HyperFrames project, and note any user-supplied media.\n\n**The brief starts at the intent layer.** Opening rule, in order: **(1)** `BRIEF.md` exists → read it and ask nothing it answers — its `flow`/`storyboard` derive the mode (brief contract § 1). **(2)** No `BRIEF.md` but the project exists → resume from what's on disk; never re-interrogate. **(3)** A fresh creation request that arrived here directly → read `/hyperframes` and run its intent layer (`references/intent-interview.md`): it confirms this route's must-haves (the music source, destination → aspect — `../hyperframes/references/routes/music-to-video.md`) and announces what stays deferred — brand and genre are chosen at Step 3 by design. Write `BRIEF.md` immediately after init (never before — `init` refuses a non-empty directory) and record the preference-backed answers (`brief-format.md`). Edit requests skip all of this.\n\nThe **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user supplied audio — a music file, or a video to pull audio from — use it. Otherwise choose the mood from the request and generate a track through `/media-use` (`audio/references/bgm.md`); when the host app provides its own music tool, use that. Before the first authenticated provider action, run `npx hyperframes auth status` and relay its output verbatim. If signed out, apply one branch:\n\n- **Collaborative:** wait for sign-in or an explicit choice to continue offline with the local provider.\n- **Autonomous:** state the status and continue through the available local provider.\n\nIf no offline provider can satisfy the required music capability, surface the blocker. Never write keys into a per-repo `.env`. Auth ownership and offline fallbacks live in `/media-use` `references/setup-providers.md` § Providers. The resulting track lands at `assets/bgm.mp3`. Stage supplied images or videos so frames can use them on the beat grid; otherwise typography carries the video.\n\n**Lyric videos:** for lyrics synced to the vocals, get word/line timing by transcribing the track via `/media-use`, or ask the user for the lyrics text and place lines on the beat grid.\n\nInitialize only if `hyperframes.json` is missing. Name `<project>` from the brief in kebab-case, such as `midnight-drive-loop` — never a timestamp. `init` checks the installed skills against the latest on GitHub and updates the global set if any are out of date.\n\n```bash\nnpx hyperframes init \"videos/<project>\" --non-interactive --example=blank --skill=music-to-video\nmkdir -p \"$PROJECT_DIR/assets\" \"$PROJECT_DIR/renders\"\ncp \"<user-music>\" \"$PROJECT_DIR/assets/bgm.mp3\"   # extract from a video first if needed\n# only if the user gave you images/videos:\nnode <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes \"$PROJECT_DIR\" --into public\n```\n\nThe **brand** (font + palette) is chosen at Step 3, not here. Don't pick a genre or a track type up front — assets are just an optional ingredient, and the genre emerges from the per-frame choices.\n\n**Gate:** `hyperframes.json` + `assets/bgm.mp3` exist; aspect / length / fps and (if any) the asset inventory are noted.\n\n---\n\n## Step 1: Analyze the music\n\nGoal: Produce the one canonical timing analysis the whole video is built on.\n\n`analyze-beatgrid.py` is the **only** beat analyzer — never re-measure beats with another tool or by ear. It reads the track once and writes `audiomap.json`: energy phases (level / density / feel), onsets + `onset_rate`, rolls, silences, `hard_stops`, `key_moments`, phrases, tempo / grid, and `audio.duration_sec`. It's deterministic — the same file always gives the same map. Most fields are reliable on any music; `bpm` and `beats_sec` are reliable only when the music is genuinely rhythmic, and judging that is the call you make at Step 2.\n\nPrerequisites: Python 3 with `librosa`, `numpy`, and `soundfile` available. If import fails, install them into the active Python environment before running the analyzer:\n\n```bash\npython3 -m pip install librosa numpy soundfile\n```\n\n```bash\npython3 <SKILL_DIR>/scripts/analyze-beatgrid.py \"$PROJECT_DIR/assets/bgm.mp3\" \\\n  -o \"$PROJECT_DIR/audiomap.json\" --print\n```\n\n**Gate:** `audiomap.json` exists; `audio.duration_sec` is known.\n\n---\n\n## Step 2: Frame skeleton (structure only)\n\nGoal: Read the music and lay out the frames — the skeleton of `STORYBOARD.md`.\n\nRead [`references/frame-skeleton.md`](references/frame-skeleton.md). Turn `audiomap.json` into the **skeleton** of `STORYBOARD.md` yourself — there is no intermediate JSON. Cut the track into **frames** at real musical changes (`hard_stops`, SURGE / DROP `key_moments`, the edges of a roll, a stretch with no onsets, a big energy jump), snapping every boundary to an audiomap anchor. For each frame set `span_sec`, `pacing` (the verdict from Step 1's trust call — `beat_cut` when the grid is real, `phrase_flow` when it's a metronome imposed on calm music), `mood`, and a one-line `feel` (the plain music situation Step 3 matches a template against). Only classify and lay out here: leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank — no templates, copy, color, or fonts. Expect ~1–6 frames.\n\n**Gate:** frames tile the track (first at 0, last at `duration_s`); each carries `span_sec` + `pacing` + `mood` + `feel`; every `### Groups` is `TBD`; no content anywhere.\n\n---\n\n## Step 3: Fill the plan (user-gated)\n\nGoal: Turn the skeleton into an approved, complete `STORYBOARD.md`.\n\nRead [`references/planning.md`](references/planning.md), [`storyboard-format.md`](references/storyboard-format.md), [`template-catalog.md`](references/template-catalog.md), [`motion-primitive-catalog.md`](references/motion-primitive-catalog.md), and [`montage.md`](references/montage.md) (only if the user supplied assets). Editing the same file in place, do two things:\n\n1. **Pick the brand.** Choose one preset from `../hyperframes-creative/frame-presets/` using the table in `../hyperframes-creative/references/design-spec.md` (match the track's mood; **only its fonts and colors matter** — templates own composition). Copy it into `frame.md` **unmodified** and fill the frontmatter `style` (font + a ≤4–6 swatch palette) from it.\n2. **Fill every frame.** Decide its groups and give each a treatment: a matched template from the catalog (with bound params and real audiomap anchors), a free-compose from the primitive catalog, or an asset treatment that **obeys `pacing`**. **Before you free-compose a named look, search the live catalog for it**: for every look, effect, treatment or transition the user asked for — \"CRT scanlines\", \"glitch\", \"film grain\", \"shimmer sweep\" — run `npx hyperframes catalog --query \"<the look, in plain English>\" --json` and read the top results. `template-catalog.md` and `motion-primitive-catalog.md` list only this skill's own local materials; the search ranks the whole hosted registry (~400 blocks and components) and needs **nothing installed** — no project, no prior `add`, no account. Free-compose a look only after a search for it came back with nothing that fits. Write the copy. You own WHAT (template / primitives + content + anchors); the frame-worker owns HOW — **never write millisecond tweens into the storyboard**.\n\n```bash\nnode <SKILL_DIR>/scripts/validate-plan.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --audiomap \"$PROJECT_DIR/audiomap.json\" --templates <SKILL_DIR>/references/templates\n```\n\nFix every `✗` (hard errors: duration mismatch, frames not tiling the track, a missing `src`); warnings are best-effort. Then present the frame-by-frame summary in chat as a proposal (`../hyperframes/references/review-loop.md` § 1) and iterate on the user's replies until they approve; for `storyboard: yes`, also write it as `storyboard.html` (`../hyperframes-creative/references/storyboard-recipe.md` § 3) for them to open. In autonomous mode this is a checkpoint gate: post the summary as a heads-up and proceed (the `validate-plan.mjs` pass is a quality gate and still blocks).\n\n**Gate:** `frame.md` is a verbatim preset copy; `validate-plan.mjs` exits 0; the user approved the plan (autonomous: the summary was posted as a heads-up).\n\n---\n\n## Step 4: Build frames from the plan\n\nGoal: Build every frame as a self-contained composition file.\n\nCreate `compositions/frames/`. Read [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) and `../hyperframes/references/subagent-dispatch.md`. Dispatch **one frame-worker per frame**, in parallel where possible (otherwise in waves). Each worker gets exactly one frame and this context:\n\n```text\nPROJECT_DIR: <abs path>\nframe_id: <NN-frame_id>              # = the frame file stem, e.g. 02-f2; the composition id\nYour block: the `## Frame N — <frame_id>` block in PROJECT_DIR/STORYBOARD.md\naudiomap: PROJECT_DIR/audiomap.json\nframe.md: PROJECT_DIR/frame.md\nMaterials: for each group, <SKILL_DIR>/references/templates/<id>/index.html (templates) and\n           <SKILL_DIR>/references/motion-primitives/<id>/ (free); staged assets/ (asset groups)\nContracts: ../hyperframes-core/references/sub-compositions.md + determinism-rules.md\nCanvas: <w>×<h>   Pacing: <beat_cut|phrase_flow>\nWrite to: PROJECT_DIR/compositions/frames/<frame_id>.html\n```\n\nThe worker forks the cited materials, converts every anchor to frame-local seconds (`local_t = track_t − span_sec[0]`), gates its groups with 0ms cuts, and writes one seek-safe frame file. **The worker never runs the `hyperframes` CLI** — those commands operate on the assembled project, which doesn't exist yet, so they'd report on the wrong files. The worker just writes to the contract and stops; you verify after assembly (Step 6). As each worker returns, you can confirm its file landed on disk.\n\n**Gate:** every frame has its `compositions/frames/NN-*.html` on disk.\n\n---\n\n## Step 5: Assemble\n\nGoal: Wire the built frames + BGM into the playable `index.html`.\n\n`assemble-index.mjs` is deterministic — no subagent, no judgment. It references each frame file at its cumulative `data-start`, mounts `assets/bgm.mp3` on track 11, and hard-cuts frame → frame (frames tile the track with no gaps, so there is **no transition injector**).\n\n```bash\nnode <SKILL_DIR>/scripts/assemble-index.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --hyperframes \"$PROJECT_DIR\" --audiomap \"$PROJECT_DIR/audiomap.json\"\n```\n\nFix any `✗` it reports — a missing or blank frame file means that worker wrote a partial file; re-dispatch it (Step 4) and re-assemble.\n\n**Gate:** `index.html` exists; total duration == `audiomap.audio.duration_sec`.\n\n---\n\n## Step 6: Verify and render\n\nGoal: Verify the assembled video, get user approval, and render the final MP4.\n\nRun the CLI on the **assembled project** — that's the correct unit (the per-frame workers couldn't run it). `check` runs structural lint and the headless-browser runtime, layout, motion, and contrast gate in one pass; `--snapshots` also emits the review frames.\n\n```bash\n( cd \"$PROJECT_DIR\" && npx hyperframes check . --snapshots )\n```\n\nInspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stops[].t`, and the final frame. On failure, make the **cheapest safe fix** yourself: edit the offending `compositions/frames/NN-*.html`. Never change duration or audio timing to hide a sync issue. Once the gates pass, open the final Studio preview (`( cd \"$PROJECT_DIR\" && npx hyperframes preview --background )`) and pause for user review — render now, or what changes? Render only on approval (autonomous mode: the same, as the one kept question), then deliver the MP4 with the contact sheet:\n\n```bash\n( cd \"$PROJECT_DIR\" && npx hyperframes render . --skill=music-to-video -q draft -o renders/video.mp4 --fps 30 )\n```\n\n**Gate:** `check` passed and the snapshots were inspected; the user approved (autonomous: checks passed and the delivery includes the contact sheet); `renders/video.mp4` exists with audio, duration == `audiomap.audio.duration_sec`. The final reply states the MP4 path and duration.\n\n---\n\n## Resume table\n\n| You have                   | Continue from |\n| -------------------------- | ------------- |\n| `assets/bgm.mp3` only      | Step 1        |\n| `audiomap.json`            | Step 2        |\n| `STORYBOARD.md` (skeleton) | Step 3        |\n| `STORYBOARD.md` (complete) | Step 4        |\n| all frame files            | Step 5        |\n| `index.html`               | Step 6        |\n\n## Quick Reference\n\n**Formats:** landscape `1920x1080` by default; portrait `1080x1920`; square `1080x1080`. Set the canvas once in the storyboard frontmatter (`canvas: { w, h, fps }`).\n\n**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.\n\n| Read                                                                                                           | When                                                    |\n| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |\n| [`references/frame-skeleton.md`](references/frame-skeleton.md)                                                 | Step 2: read the music, lay out the frames, set pacing  |\n| [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan |\n| [`references/template-catalog.md`](references/template-catalog.md)                                             | Step 3: pick a template per group                       |\n| [`references/motion-primitive-catalog.md`](references/motion-primitive-catalog.md)                             | Step 3/4: L0 recipes for free-compose                   |\n| [`references/montage.md`](references/montage.md)                                                               | Step 3/4: asset treatments (beat-cut / ken-burns)       |\n| [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md)                                                     | Step 4: dispatch + build one frame                      |\n| `../hyperframes/references/subagent-dispatch.md`                                                               | Step 4: dispatch sub-agents safely                      |\n| `../hyperframes-creative/references/design-spec.md`                                                            | Step 3: pick the preset (the brand)                     |\n\n## Directory layout\n\n```\nmusic-to-video/\n  SKILL.md\n  references/   frame-skeleton.md · planning.md · storyboard-format.md\n                template-catalog.md · motion-primitive-catalog.md · montage.md\n                templates/<id>/          { index.html (+ assets/ · program.json) }  ← L1 catalog impls\n                motion-primitives/<id>/  { index.html (mounts the scene), scene.html (the sub-composition) } (+ ../assets/gsap.min.js shared by recipes) ← L0 catalog impls\n  scripts/      analyze-beatgrid.py · assemble-index.mjs · validate-plan.mjs · stage-assets.mjs · lib/storyboard.mjs\n  sub-agents/   frame-worker.md   ← the one subagent (one per frame)\n```\n\nFile v1.0.25:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"music-to-video\",\n  \"version\": \"1.0.25\",\n  \"publishedAt\": 1791358267609\n}\n\nFile v1.0.25:references/frame-skeleton.md\n\n# Frame skeleton (Step 2) — read the music, lay out the frames\n\nAt Step 2 **you (the orchestrator)** read `audiomap.json` and write the **skeleton** of\n`STORYBOARD.md` directly: cut the track into **frames** (one frame = one composition file =\none scene), and for each frame set its **span**, its **pacing** (does this stretch want hard\nbeat-cuts, or calm phrase/energy flow?), its **mood**, and a one-line **feel** note.\n\nYou **classify and lay out the spine only.** You do **not** pick templates, write copy, choose\ncolors/fonts, or decide a frame's groups — those are Step 3 (the plan fills each frame in\nplace). Leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank.\n\nThere is **no intermediate JSON** — the skeleton _is_ the start of `STORYBOARD.md`. Step 3\nedits the same file.\n\n## The trust boundary (read this first)\n\n`audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are\nreliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:\n\n| Field                                                                                                                                                                                               | Trust                                                                                                                                                                                                                        |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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                                                                                                                                                                                             |\n| `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. |\n\n- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.\n- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.\n\n## How to lay out frames (run in order)\n\n1. **Gestalt.** From `summary` / `tempo` / `audio.duration_sec` + the roll count, write the\n   frontmatter `compositionId`, `duration_s` (== `audio.duration_sec`), `canvas`, and a\n   one-line read of the track's density arc.\n2. **Cut into frames.** Walk `energy_phases[]` and split where the music genuinely changes\n   state — at `hard_stops[]`, `SURGE` / `DROP` `key_moments`, the start/end of a `rolls[]` run,\n   an **onset desert** (a long gap in `events[]`), or a big energy-level jump. Collapse adjacent\n   phases that are one gesture. Expect **~1–6 frames**; a short clip may be one.\n   **Snap every boundary to an audiomap anchor**, then re-snap to the nearest `beats_sec`\n   (tolerance ≤ ½ beat) **only when the grid is reliable**; on calm material snap to\n   `phrases[]` / `energy_phases[]` edges instead. Frames **tile the track** (first at 0, last\n   at `duration_s`, no gaps/overlaps).\n3. **Per frame, set `pacing`** — the trust-boundary call:\n   - **`beat_cut`** — genuinely rhythmic: a roll present, **or** dense, **or** (clearly high `onset_rate` **and** a steady grid). Hard cuts / per-onset reveals may anchor to beats.\n   - **`phrase_flow`** — calm / sparse: `rolls`≈0, mostly sparse, low `onset_rate`. Do **not** anchor hard cuts to the grid; pace by `phrases[]` + the energy envelope (slow crossfades, long holds).\n4. **Per frame, tag `mood`** (1–3 of: `warm` · `dark` · `hype` · `elegant` · `glitch` ·\n   `cinematic` · `playful` · `tense` · `dreamy` · `aggressive`) from `energy_phases[].feel` +\n   energy + any genre cue in the brief/title.\n5. **Per frame, write a one-line `feel`** — the plain-language music situation Step 3 matches a\n   template against (e.g. \"accelerating onset stream into a held downbeat\", \"calm held pad, one\n   onset desert\", \"fast sustained-fill roll, no readable message\"). This is what the planner\n   reads against the catalog's **Reach for it when** — keep it concrete, drawn from the robust\n   fields, never invented.\n\n## What the skeleton looks like\n\nA valid `STORYBOARD.md` with the spine set and every frame's treatment left for Step 3\n(full syntax in [`storyboard-format.md`](storyboard-format.md)):\n\n```markdown\n---\ncompositionId: bgm\nduration_s: 30.0 # == audiomap.audio.duration_sec\ncanvas: { w: 1920, h: 1080, fps: 30 }\nstyle: # blank — Step 3 fills it from the chosen frame.md preset\nbuild_notes: [\"one paused timeline per frame\", \"no remote assets\"]\n---\n\n## Frame 1 — f1\n\n- src: compositions/frames/01-f1.html\n- duration: 7.198s # = span length; assembler sums these for cumulative data-start\n- span_sec: [0.0, 7.198] # track seconds; frames tile the track\n- pacing: beat_cut\n- mood: [hype]\n- feel: accelerating onset stream building into a held downbeat\n\n### Groups\n\n- TBD (Step 3)\n\n## Frame 2 — f2\n\n- src: compositions/frames/02-f2.html\n- duration: 10.4s\n- span_sec: [7.198, 17.598]\n- pacing: phrase_flow\n- mood: [warm, cinematic]\n- feel: calm held pad, one long onset desert\n\n### Groups\n\n- TBD (Step 3)\n```\n\n## Self-check\n\n- `duration_s == audiomap.audio.duration_sec`; frames tile the track gap-free (first at 0, last at `duration_s`).\n- Every frame has `src` + `span_sec` + `duration` + `pacing` + `mood` + a one-line `feel`.\n- `pacing` was set from the **robust** fields (energy / density / rolls / onset_rate), never from `bpm` / `beats_sec` alone.\n- No frame boundary sits inside a `rolls[]` run or leaves a sub-1-bar fragment.\n- Every frame's `### Groups` is `TBD (Step 3)`; `style` is blank. **No template, copy, color, or font anywhere.**\n\nFile v1.0.25:references/montage.md\n\n# Asset treatments — weaving user media onto the beat spine\n\nWhen the user supplies images/videos, a group can be an **asset treatment** instead of a\ntypographic template/free-compose. Assets are an **additive ingredient on the same beat\nspine** — never a separate pipeline. Typography/templates stay the floor: if no asset fits a\ngroup, fall back to a template/free group (a complete video needs zero assets).\n\nThe planner (Step 3) picks the treatment and the clips + anchors (WHAT); the frame-worker\nrealizes it inside the frame file (HOW). **Obey the frame's `pacing`.**\n\n## The three treatments\n\n### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)\n\nThe asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor\n(the frame's beats/onsets from the audiomap). Each clip is a `class=\"clip\"` element\n(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with\n`data-start`/`data-duration`/`data-track-index` per the core clip contract. (Muted on purpose: the music track drives the sound.) Between clips,\ncrossfade the outgoing content to `opacity:0` ending **at** the next anchor.\nCut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.\n\n### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)\n\nFor calm frames: one clip held over the span with a slow scale/translate push (e.g. scale\n1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by\nbeats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment\nwhen the beat grid is unreliable (calm music).\n\n### `bg_under_text` — clip dimmed behind a template/free group\n\nA full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template\nor free-compose typographic treatment. The text rides on the same anchors; the clip is the\nbed. Use when the user wants their footage present but the message must stay readable.\n\n## Rules\n\n- **`pacing` decides the treatment**: `beat_cut` only on a `beat_cut` frame; on a\n  `phrase_flow` frame use `ken_burns` or a slow crossfade — **never** per-onset hard cuts on\n  the (unreliable) calm grid.\n- **Clips are muted; the root owns audio.** Mount each `<video class=\"clip\">` **muted**, as a\n  direct child of the frame root (never nested in another timed element, or the renderer\n  freezes it). The BGM is the only audio in v1.\n- **Crossfades animate `opacity`/`autoAlpha`**, never `visibility`/`display` on a `.clip`\n  (the framework owns clip visibility — that trips `gsap_animates_clip_element`).\n- **Backgrounds dim ~30–50%** so any foreground text stays legible.\n- Anchors are **track seconds from `audiomap.json`**; the worker subtracts the frame start\n  to get frame-local time.\n- Local staged assets only (`assets/` via `stage-assets.mjs`); never remote URLs.\n\n## Deferred hook (not v1)\n\nA clip that should play **its own sound** (interview cut, lyric clip) needs a sibling\n`<audio>` mounted at the **root** by the assembler, with the BGM ducked under it (a\n`data-automation` volume lane on the BGM, see `creator-editing-recipes.md` in `hyperframes-core`). The frame-worker mounts no audio. Keep clips muted in v1; wire clip-audio +\nducking only when the user asks.\n\nFile v1.0.25:references/motion-primitive-catalog.md\n\n# Motion-primitive catalog — the free-compose menu\n\nThe atomic layer: one anchor → one micro-move. When no template fits a group, free-compose by\nnaming primitives from here. Scan **anchor** + **best span** + **what it does**, then pick the\nsmallest set that carries the group.\n\n## Timing & latency (applies to every primitive)\n\n- **Hard hits are 0ms.** Cuts, palette flips, content swaps, freezes are `tl.set(...)` with no duration — the percussion _is_ the motion. Easing a hit kills it.\n- **Lead the anchor.** A move that must _land_ on a beat (a wipe covering the frame, a count-up locking, two blocks colliding) starts **~40–190ms early** so it completes ON the anchor. Reactive entrances (something appearing _because_ of the hit) fire 0–45ms after.\n- **Eased entrances: 300–500ms** (scale punch, slides, camera pushes). **Macro builds: 800–2000ms** spanning a whole roll / silence.\n- **Per-bar caps:** one accumulating element per hit (not a burst); a camera move at most once per phrase, never per beat; a dense flip/strobe system runs ≤2–3s.\n- **Tension-builds lock.** A count-up / sequential build / morph must _resolve on_ a downbeat or hard_stop, never trail off mid-bar.\n- **Best span means active motion.** The catalog's span guidance is not a license to stretch one primitive over a whole frame. If a free-composed group runs longer than the listed span, add a hold / bed / next primitive, or split the frame into another group at the next musical anchor.\n\n## Catalog\n\n| id                    | anchor                           | best span         | what it does                                                              |\n| --------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------- |\n| `hypercut-whip`       | beat / hard_stop                 | 0.18-0.45s        | fast whip-pan hard cut between frames                                     |\n| `kinetic-letter-in`   | downbeat / phrase                | 0.4-1.2s          | per-letter kinetic entrance                                               |\n| `braam-punch`         | drop / surge                     | 0.2-0.9s active   | big impact: scale + weight slam                                           |\n| `chromatic-split`     | snare / glitch / surge           | 0.1-0.6s          | RGB channel split / glitch on a word                                      |\n| `mask-reveal`         | section_start / downbeat         | 0.5-1.2s          | clip-path mask wipe reveal                                                |\n| `screen-shake`        | drop / crash / kick              | 0.1-0.5s          | camera / screen shake jitter                                              |\n| `binary-decrypt`      | roll / build                     | 0.8-2.5s          | scramble→decode text (binary → word)                                      |\n| `dolly-zoom`          | phrase / build                   | 1.2-2.5s          | vertigo dolly-zoom (scale vs perspective)                                 |\n| `iris-open`           | section_start / reveal           | 0.6-1.2s          | circular iris-open reveal                                                 |\n| `electric-arc`        | accent / glitch                  | 0.1-0.6s          | electric arc / lightning accent                                           |\n| `neon-flicker`        | hold / texture                   | 0.5-2.5s          | neon-sign flicker                                                         |\n| `chrome-sweep`        | downbeat / reveal                | 0.6-1.4s          | metallic specular sweep across text                                       |\n| `slot-machine-reveal` | roll → downbeat                  | 0.8-2.0s          | slot-machine spin-to-land character reveal                                |\n| `liquid-morph`        | phrase / transition              | 1.0-2.5s          | liquid / blob morph                                                       |\n| `gooey-metaball`      | build / drop                     | 1.5-3.0s          | gooey metaball merge field                                                |\n| `3d-card-flip`        | downbeat / swap                  | 0.8-1.6s          | 3D card flip (rotateY)                                                    |\n| `crash-zoom-in`       | drop / surge                     | 0.2-0.8s          | violent crash zoom-in                                                     |\n| `spotlight-sweep`     | reveal / hold                    | 0.8-2.0s          | spotlight / gradient sweep over text                                      |\n| `outline-to-fill`     | downbeat / reveal                | 0.8-1.8s          | stroke outline → solid fill                                               |\n| `counting-punch`      | roll → downbeat                  | 1.0-2.5s          | number count-up that punches & locks                                      |\n| `particle-burst`      | drop / crash                     | 0.2-1.2s          | particle explosion burst                                                  |\n| `radial-burst-lines`  | drop / surge                     | 0.2-0.8s          | radial speed-lines burst                                                  |\n| `pixel-dissolve`      | transition / hard_stop           | 0.5-1.5s          | pixelated dissolve                                                        |\n| `datamosh-smear`      | glitch / transition              | 0.4-1.2s          | datamosh / motion smear                                                   |\n| `text-wave-distort`   | hold / texture                   | 1.0-2.5s          | wavy text distortion                                                      |\n| `bg-flow-field`       | energy / whole span (bed)        | 4-12s bed         | generative curl-noise background bed; compose any foreground move over it |\n| `blur-resolve`        | stop / final hold                | 0.7-2.0s          | blur-in to crisp focus, then blur-out on the cut                          |\n| `chromatic-pressure`  | snare / glitch                   | 0.1-0.5s          | RGB split / digital tension on a transient                                |\n| `color-grid-shuffle`  | onset                            | 0ms hits; ≤2s run | grid of cells recolored by a deterministic index per onset                |\n| `content-swap`        | beat                             | 0ms hits; ≤3s run | 0ms swap of stacked nodes: the workhorse percussive move                  |\n| `directional-fill`    | beat / reveal                    | 0.3-1.0s each     | directional wipe-fill (scaleX) sweeping across bars                       |\n| `flash-cut`           | drop / crash                     | 0-0.6s            | full-frame flash masking a word / color state change                      |\n| `freeze-hold`         | hard_stop                        | 0ms in; 0.5-2s    | freeze the moving system and hold it                                      |\n| `hard-cut`            | beat / hard_stop                 | 0ms in; 0.3-2s    | sample-accurate color-block + word cut                                    |\n| `mosaic-pack`         | beat / build                     | 1.5-3.5s          | scattered tiles fly in and pack into a grid                               |\n| `negative-space-hold` | silence / hard_stop / final hold | 1-6s hold         | kill busy layers, hold one readable mark in empty space                   |\n| `overlay-pop`         | accent                           | 0.2-0.6s in       | badge / lower-third overlay pops in over a base                           |\n| `palette-flip`        | section change                   | 0ms flip; 0.5-4s  | same layout re-skins via 0ms palette-variable flips                       |\n| `staggered-exit`      | phrase / transition              | 0.4-1.2s          | ordered cascade-out clearing the frame                                    |\n| `staggered-reveal`    | build                            | 0.8-2.5s          | ordered cascade-in of a stack / list                                      |\n| `system-replace`      | drop / regime change             | 0ms cut           | hard-cut the entire visual system, then boot the new one                  |\n| `text-spectral-rays`  | phrase / sweep (hero text)       | 2.5-5s            | volumetric light-rays cast by a wordmark toward a sweeping light cursor   |\n| `tile-mosaic`         | build / reveal                   | 1.5-3.5s          | grid of tiles revealed in a diagonal sweep, assembling a poster           |\n| `typewriter-reveal`   | roll / build                     | 1.0-3.0s          | character / word type-on with caret                                       |\n| `value-counter`       | roll → downbeat                  | 1.0-2.5s          | count-up that locks on a downbeat / hard_stop                             |\n| `word-grid-burst`     | onsets → downbeat                | 1.8-3.2s          | grid of words revealed per onset, refocus one on a downbeat               |\n\n## How to combine\n\n- One dominant system per group; layer at most one texture primitive over one structural primitive.\n- Structure on strong beats (cuts, camera, `system-replace` → downbeat / phrase / section_start); texture on weak / syncopated hits (`content-swap`, typewriter letters, chromatic accents).\n- A roll is an accumulation container — build during it, hard-cut to a clean layout on the downbeat that ends it.\n- `drop` ≠ `downbeat`: a downbeat is a cut within the regime; a drop is a regime change (`system-replace`, total clear, element-count jump).\n- Let silence remove density (`negative-space-hold`).\n- Background beds are a layer, not a move: one bed at a time, under foreground primitives.\n- `text-spectral-rays` is the hero wordmark treatment; do not stack another visible copy of the same word on top.\n\nFile v1.0.25:references/motion-primitives/text-spectral-rays/USAGE.md\n\n# Using `text-spectral-rays` — it OWNS its wordmark\n\n`text-spectral-rays` is a **self-contained WebGL hero-text renderer**. From ONE rasterized\nglyph mask it draws **both** the solid wordmark **and** the spectral rays that emanate from\nit. Letters and rays share the same mask, so they are always perfectly registered.\n\n## The one trap: never give the word a second source\n\nThe ghost / doubled-wordmark artifact comes from splitting the word across two sources:\n\n- ❌ **Wrong** — use the shader as a \"rays-only background\" and draw the visible letters\n  with a **separate DOM element** (or stack a second text move like `content_swap` /\n  `chromatic_pressure` on the same word). The DOM font (e.g. Inter) and the shader's raster\n  font (Arial Black / Impact fallback) differ in width, shape, and position, so the ray\n  edges never line up with the DOM letters → a misregistered ghost. **Deleting the shader's\n  letter terms does NOT fix it** — the ray mask itself is still the second, misaligned copy\n  of the word.\n\n- ✅ **Right** — let the shader render the wordmark. There is exactly ONE source, so a\n  ghost is structurally impossible.\n\n## Integrate in one pass\n\n1. **It IS the wordmark.** Hide any DOM logo for that word (keep it only as an invisible\n   layout spacer if a tagline/CTA below depends on its box). Never stack a discrete text\n   move on the same word.\n2. **One timeline.** Merge its `progress` / `effectMix` state tweens onto the group's master\n   timeline and repaint via `tl.eventCallback(\"onUpdate\", render)` — no second timeline, no\n   `requestAnimationFrame`.\n3. **Align the cursor to the word.** `mouse.y` must equal the mask's vertical center. If you\n   move the rasterized word off frame-center (e.g. up, to leave room for a tagline), shift\n   the cursor's `y` by the same amount — otherwise the rays cast at the wrong angle.\n4. **Local raster only.** Rasterize the word with a bundled / system font (no CDN font);\n   upload the mask + colour canvases as textures.\n5. **Entrance.** Slam the whole canvas (autoAlpha + a scale punch, `transform-origin` on the\n   word's optical center) on the hit; let `effectMix` bloom the rays just after. The solid\n   letters are present the instant the canvas reveals.\n\n## Pairs with\n\nA background bed (`bg-flow-field`) or **separate** supporting elements (tagline, CTA, rule)\n— never a second treatment of its own word.\n\nFile v1.0.25:references/planning.md\n\n# Planning (Step 3) — pick the brand, fill every frame\n\nAt Step 3 **you (the orchestrator)** turn the Step-2 skeleton into a complete, approved\n`STORYBOARD.md`. You edit the **same file** in place: pick the brand spine, then for each\nframe decide its **groups**, give each group a treatment, bind real beat anchors, and write\nthe copy.\n\nYour mantra: **music is the spine; a template is a head start, not a cage; typography is the\nfloor and assets are an optional ingredient on the same beat grid.**\n\n**You own WHAT, not HOW.** You name the template / primitives, the content, the brand, the\nanchor seconds, and the intent. The frame-worker (Step 4) decides HOW — micro-timing,\nrealization, intra-frame cuts. **Never write millisecond tweens into the storyboard.**\n\n## Inputs\n\n- The Step-2 skeleton already in `STORYBOARD.md` — frames with `span_sec` + `pacing` + `mood` + `feel`.\n- `audiomap.json` — timing truth; read the real anchor seconds inside each frame's span.\n- [`template-catalog.md`](template-catalog.md) — the template selection menu.\n- [`motion-primitive-catalog.md`](motion-primitive-catalog.md) — the free-compose menu (L0 recipes).\n- [`montage.md`](montage.md) — asset treatments (only if the user supplied images/videos).\n- User brief / supplied copy — topic, mood, exact words to keep.\n\n## Step A — pick the brand spine (one preset, unmodified)\n\nThe whole video shares one type family + palette. Pick **one ready-made preset** from\n`../../hyperframes-creative/frame-presets/` using the preset table in\n`../../hyperframes-creative/references/design-spec.md` — choose by the track's mood + the brief,\nand **only its fonts + colors matter** (templates own composition + motion; the preset only\nsets the look). Copy it in **unmodified**:\n\n```bash\ncp ../hyperframes-creative/frame-presets/<preset>/FRAME.md \"$PROJECT_DIR/frame.md\"\n```\n\nThen fill the storyboard frontmatter `style` from it: the `font` from its `typography:` and a\n≤4–6 swatch `palette` from its `colors:`. **Quote the hex / family verbatim — never invent or\nround.** Every group's palette params draw from this one palette; that unity is what makes\ndifferent templates read as one piece.\n\n## Step B — per frame, decide its groups\n\nA frame is usually **one group** (one template or one free composition spanning the frame).\nSubdivide into 2+ groups **only when a single treatment can't cover the frame** — e.g. a busy\nopener plus a closing lockup. When you split, cut at a **real audiomap anchor** inside the\nframe's span (a `key_moment` / `phrase` edge / onset-cluster gap), **never inside a `rolls[]`\nrun**, and keep every group **≥ ~1 bar**. Density does **not** force more groups — a dense\nframe is usually ONE group whose template absorbs the density internally (a meta-template like\n`poster-tile-mosaic`). **Group count tracks distinct treatments, not beats.**\n\n## Step C — per group, pick a treatment (exactly one of three)\n\n### A. Match a template\n\nRead [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing`\nto a template's **Reach for it when**; take the closest fit. Then bind it:\n\n- Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length.\n- Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times).\n- If the template's natural stop and the group's span end disagree, snap to the nearest anchor.\n\n### B. Free-compose (no template fits)\n\nWrite a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md)\n(a dominant system + the named L0 primitives + a density topology) + `anchors` (the real\nbeat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as\ncarefully as a matched group — never a failure.\n\n### C. Asset treatment (only when the user supplied assets and they fit)\n\nMake it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut`\nframe use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use\n`ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none\nfits a group, fall back to template / free (typography is the floor — a complete video needs\nno assets).\n\n## Copy (you own the words)\n\n- Keep exact user words; else invent with taste, on the brief's mood.\n- **Message vs texture:** a readable word holds ≥1 beat (headline 3–8, sentence 4–10), stable + focal; a word held <1 beat is texture (strobe / grid / ticks). Never force a message onto a sub-beat — demote it to texture.\n- Place copy into the template's text params, onto a free group's anchors, or as an asset group's `overlay_copy`. Declare the anchor + accumulate / stagger intent; leave micro-timing to the worker.\n- A closing logo / CTA lands on the final hit / hard stop and holds through trailing silence.\n\n## Transitions (you do not emit them)\n\nEverything is a **0ms hard cut** for now. **frame → frame** is owned by the assembler\n(back-to-back files); adjacent `span_sec` already imply the cut. **group → group inside a\nframe** is owned by the worker on its frame timeline; you only set each group's `span_sec`.\n\n## Write + validate\n\nComplete `STORYBOARD.md` ([`storyboard-format.md`](storyboard-format.md)), then run\n`node scripts/validate-plan.mjs` and fix every `✗`. Present the frame-by-frame summary in chat\nand iterate until approved (Step 3 in `SKILL.md` says how).\n\n## Self-check\n\n- `frame.md` is a verbatim copy of one preset; frontmatter `style.font` / `style.palette` are drawn from it (exact values).\n- Every frame became ≥1 group; groups tile the frame span in order; no group < ~1 bar; no group boundary inside a `rolls[]` run.\n- Each group is exactly one of template / free_design / asset.\n- Template `params` keys match the catalog entry; `role_bindings` / `anchors` use real audiomap seconds.\n- Asset treatments obey `pacing` (no `beat_cut` on a `phrase_flow` frame).\n- Every group's palette draws from the one brand palette.\n- `duration_s == audiomap.audio.duration_sec`; `validate-plan.mjs` passes.\n\nFile v1.0.25:references/storyboard-format.md\n\n# STORYBOARD.md format — frames → groups\n\n`STORYBOARD.md` is the single reviewable plan the user approves at Step 3 and the **manual**\neach frame-worker follows at Step 4. It is **hierarchical**: one block per **frame**, written\nas a `## Frame N — <frame_id>` heading (the parser recognizes `Frame`). **One frame = one\nscene = one composition file.** Inside each frame block are its **groups** (the treatment\nunits). The assembler reads the frame level (`duration` → `data-start`, `src`); the worker\nreads its own frame block. A frame's **composition id = its `src` file stem**\n(`compositions/frames/01-f1.html` → `01-f1`), which the worker uses as `data-composition-id`\nand the `window.__timelines` key.\n\nStep 2 writes the skeleton (frame fields, groups `TBD`); Step 3 fills the groups + brand. It\nis a build spec, not code — the planner writes WHAT, the worker decides HOW. **Never write\nmillisecond tweens here.**\n\n## File shape\n\nYAML frontmatter (the video-wide spine) + one `## Frame N — <frame_id>` block per frame.\n\n```markdown\n---\ncompositionId: bgm\nduration_s: 30.0 # == audiomap.audio.duration_sec, exactly\ncanvas: { w: 1920, h: 1080, fps: 30 }\nstyle: # brand spine — from the chosen frame.md preset (Step 3)\n  font: \"EB Garamond / Inter / JetBrains Mono\" # the preset's typography, verbatim\n  palette: [\"#FAF9F5\", \"#141413\", \"#CC785C\", \"#181715\"] # ≤4–6 swatches from the preset's colors\nassets: false # false, or a note like \"assets/ has 6 user photos\"\nbuild_notes: [\"one paused timeline per frame\", \"no remote assets\"]\navoid: [\"generic slideshow\", \"tiny unreadable hero text\"]\n---\n\n## Frame 1 — f1\n\n- src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id\n- duration: 7.198s # = span length; the assembler reads this for cumulative data-start\n- span_sec: [0.0, 7.198] # track seconds; frames tile the track\n- pacing: beat_cut # beat_cut | phrase_flow (from the skeleton; obey it)\n- mood: [hype]\n- feel: accelerating onset stream into a held downbeat\n\n### Groups\n\n- **g1** — template: `intro-kinetic-cascade`\n  - span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start)\n  - params: { theme: \"light\", icon: \"bolt\", phrases: \"[…]\", climax: \"{…}\" }\n  - role_bindings: { phrase: { times: [0.14, 0.55, 0.87] }, climax: { in: 3.79, iconAt: 4.9 } }\n  - copy: \"GROWTH THROUGH CREATIVITY\"\n- **g2** — free_design\n  - span_sec: [4.017, 7.198]\n  - free_design: { dominant_system: \"per-onset typography\", primitives: [\"content-swap\", \"braam-punch\"], density_topology: \"accumulate\" }\n  - anchors: [4.10, 4.80, 5.50, 6.20] # onset seconds the reveals ride (from audiomap)\n  - copy: [\"BUILD\", \"SHIP\", \"REPEAT\"]\n\n## Frame 2 — f2\n\n…\n```\n\n## Frame block — required fields\n\n| field                             | meaning                                                                                                                        |\n| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index.                                                                    |\n| `src`                             | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. |\n| `duration`                        | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`.         |\n| `span_sec`                        | `[start, end]` track seconds. `duration = end − start`.                                                                        |\n| `pacing`                          | `beat_cut` \\| `phrase_flow` — from the skeleton; the worker must obey (no hard-cut on `phrase_flow`).                          |\n| `mood`, `feel`                    | from the skeleton; tone + the one-line music situation the planner matched against.                                            |\n| `### Groups` list                 | ≥1 group; groups tile the frame span in order.                                                                                 |\n\n## Group entry — exactly one of three kinds\n\nEvery group is **template** OR **free_design** OR **asset** — never two, never none. All three\ncarry `span_sec` (track seconds, tiling the frame) and may carry `copy`.\n\n- **template** — `template: <catalog id>` + `params` (keys from the catalog entry) + `role_bindings` (real audiomap anchor seconds) + `copy`.\n- **free_design** — `free_design: { dominant_system, primitives: [catalog ids], density_topology }` + `anchors` (real beat / onset seconds) + `copy`.\n- **asset** — `asset: { treatment, clips: [public/…], anchors?, overlay_copy? }`. `treatment` ∈ `beat_cut` (one clip per anchor — only on a `beat_cut` frame) | `ken_burns` (slow push — fits `phrase_flow`) | `bg_under_text` (clip dimmed behind a template / free group). See [`montage.md`](montage.md).\n\n## Rules\n\n- Frames tile the track (gap-free, first at 0, last at `duration_s`); a frame's groups tile its span; no group < ~1 bar; no group boundary inside a `rolls[]` run.\n- `params` keys come from the template's [`template-catalog.md`](template-catalog.md) entry.\n- All anchor seconds are **track seconds from `audiomap.json`** — the worker converts to frame-local by subtracting the frame start.\n- A `phrase_flow` frame MUST NOT use `beat_cut` asset treatment or per-onset hard cuts.\n- The brand `style` is set once; every group's palette draws from it.\n- Reviewable prose-plus-data; keep it scannable. No GSAP, no millisecond timing.\n\n## Self-check (the planner runs `validate-plan.mjs`)\n\n- frontmatter has `compositionId`, `duration_s` (== audiomap), `canvas`, `style`.\n- every frame has `span_sec` + `src` + positive `duration` + `pacing` + ≥1 group; frames tile the track.\n- every group is exactly one of template / free_design / asset; template ids exist in the catalog; anchors are real audiomap seconds; `phrase_flow` frames have no `beat_cut`.\n\nFile v1.0.25:references/template-catalog.md\n\n# Template catalog — the selection menu (Step 3)\n\nReverse-engineered, asset-free **group templates**. This file is the **only** thing the\nplanner reads to pick one — you do **not** open the template's `index.html` to choose. One\ntemplate realizes **one group** of a frame (a frame may stack several groups; the brand spine\nunifies them).\n\n**How to pick.** For each group, read its frame's `pacing` + `mood` + the one-line music\nsituation from the skeleton, then scan **Reach for it when** + **Best span** below and take the\nclosest fit. If nothing fits, **free-compose** from\n[`motion-primitive-catalog.md`](motion-primitive-catalog.md) — that is a first-class choice, not a\nfailure.\n\n**Brand comes from `frame.md`, not the template.** Templates ship their own demo palette\n(`theme` / `palette` / color params). Fill those params from the project's `frame.md` (the\nchosen preset's colors + fonts) so every group reads as one piece — the template supplies the\n**motion + layout**, the preset supplies the **look**.\n\n**Params** are listed so you can fill content slots in the storyboard. Exact semantics +\ndefaults live in each template's `index.html` (`data-composition-variables`) — the frame-worker\nreads those at build time; you only name the values.\n\nPacing tag: every template below is **beat_cut** except `held-message-living-field`\n(**phrase_flow**). Never put a beat_cut template on a phrase_flow frame.\n\n## Group duration discipline\n\nTemplate choice is per **group**, not per frame. A frame longer than a template's **Best span**\nshould usually split into multiple groups at real audiomap anchors (`SURGE`, `DROP`, roll edge,\nhard_stop, phrase edge) instead of stretching one template across the whole frame.\n\n- **Best span** below is the active treatment span: the part where the template's system is doing\n  meaningful motion. A short readable hold at the end is fine; a long empty tail means pick another\n  group.\n- **Frames over ~6s** usually need 2+ groups. Beat-cut exceptions are rich programs with real\n  sub-phases (`poster-tile-mosaic`, sometimes `card-flyby`); phrase-flow exceptions use\n  `held-message-living-field`.\n- **Roll templates are one-roll tools.** If a frame has two rolls or a drop between rolls, split it\n  into two groups.\n- Do not extend by slowing every tween. Preserve the template's motion feel, then fill extra time\n  with a hold / palette change / new group.\n\n---\n\n### card-flyby\n\n- **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.\n- **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.\n- **Best span** — **4-6.5s** for 4-7 landings plus a short final hold; split at the next downbeat if it wants to run **>7s**.\n- **Params** — `theme`, `bgColor`, `cards`, `landings`, `yaw`\n\n### held-message-living-field · phrase_flow\n\n- **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes.\n- **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.\n- **Best span** — **6-16s**; this is the long-group exception. Under 4s feels underdeveloped; over ~20s needs a state change or another group.\n- **Params** — `markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration`\n\n### held-text-strobe-burst\n\n- **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.\n- **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/`.)\n- **Best span** — **1.2-3.5s**; strobe fatigue starts fast, so cap at ~4s and cut to a cleaner system.\n- **Params** — `markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration`\n\n### intro-kinetic-cascade\n\n- **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).\n- **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.\n- **Best span** — **3.5-7s** for 2-4 phrase beats; if the statement needs more time, make the next clause a new group.\n- **Params** — `theme`, `icon`, `phrases`, `climax`\n\n### logo-split-lockup-pulse\n\n- **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.\n- **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.\n- **Best span** — **2-4s**; at **>4.5s** it reads like a sting stretched too long. Follow with a separate held-lockup / next idea group.\n- **Params** — `bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4`\n\n### poster-tile-mosaic\n\n- **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.\n- **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.\n- **Best span** — **4-7s**, up to **8s** only when the program has clear sub-phases. If the music changes regime, split even if the tile system could continue.\n- **Params** — `bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program`\n\n### roll-flipbook-word-cycle\n\n- **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.\n- **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.\n- **Best span** — **1.2-3.8s**, one roll into one resolve. Two rolls, or a drop between rolls, means two groups.\n- **Params** — `bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar`\n\n### split-anchor-word-slot\n\n- **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.\n- **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.\n- **Best span** — **3-6s**; above ~6.5s the fixed anchor goes stale unless the right slot enters a new group/program.\n- **Params** — `bgColor`, `anchors`, `theme`, `showText`, `program`\n\n### typewriter-phrase-keyword-shuffle\n\n- **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.\n- **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).\n- **Best span** — **2.5-5s**; if the phrase cannot type and shuffle inside ~5s, reduce words or split the sentence across groups.\n- **Params** — `bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar`\n\nFile v1.0.25:references/templates/card-flyby/program.json\n\n{\n  \"_\": \"Default card-flyby program. `theme` picks a cohesive palette; `cards` is the deck (each card = a big centered title; `color` is OPTIONAL — omit it and the card takes the next color from the theme ramp); `landings` is one onset per card (accelerating cadence; omit to auto-derive). Pass these as the matching composition-variables; empty vars fall back to these values.\",\n  \"theme\": \"aurora\",\n  \"cards\": [\n    { \"title\": \"FADEGLOW\" },\n    { \"title\": \"SAUL BASS\" },\n    { \"title\": \"EULER\" },\n    { \"title\": \"HERMÈS\" },\n    { \"title\": \"DOSSIER\" },\n    { \"title\": \"BLACK HOLES\" }\n  ],\n  \"landings\": [0.4, 1.7, 2.7, 3.45, 4.0, 4.4]\n}\n\nFile v1.0.25:references/templates/intro-kinetic-cascade/program.json\n\n{\n  \"_\": \"Default intro-kinetic-cascade program (reversed from act0-intro-bell). `theme` = palette; `icon` = the ringing climax glyph (MATCH TO SCENE — bell|cursor|sparkle|bolt|play|heart|check|star, an emoji, a raw <svg>, or 'none'); `phrases` = the word-by-word cascade (each `times` = VO word onsets / audio onsets, omit to auto-spread); `climax` = the slide-in finale ({icon} marks the glyph). Pass these as the matching composition-variables; empty vars fall back to these values.\",\n  \"theme\": \"light\",\n  \"icon\": \"bell\",\n  \"phrases\": [\n    {\n      \"out\": 1.36,\n      \"lines\": [\n        { \"text\": \"If you've\", \"size\": 220, \"x\": 140 },\n        { \"text\": \"ever\", \"size\": 520, \"x\": 240, \"hero\": true, \"font\": \"serif\" },\n        { \"text\": \"used AI\", \"size\": 280, \"x\": 780 }\n      ],\n      \"times\": [0.14, 0.3, 0.55, 0.87, 1.06]\n    },\n    {\n      \"out\": 3.06,\n      \"lines\": [\n        { \"text\": \"for\", \"size\": 220, \"x\": 160 },\n        { \"text\": \"video\", \"size\": 500, \"x\": 220, \"hero\": true },\n        { \"text\": \"editing before,\", \"size\": 200, \"x\": 220 }\n      ],\n      \"times\": [1.36, 1.58, 1.92, 2.48]\n    },\n    {\n      \"out\": 3.79,\n      \"lines\": [\n        { \"text\": \"you\", \"size\": 240, \"x\": 200 },\n        { \"text\": \"know\", \"size\": 500, \"x\": 260, \"hero\": true, \"accent\": \"gradient\" },\n        { \"text\": \"that\", \"size\": 340, \"x\": 820 }\n      ],\n      \"times\": [3.06, 3.3, 3.54]\n    }\n  ],\n  \"climax\": {\n    \"text\": \"timing is {icon} tricky.\",\n    \"in\": 3.79,\n    \"iconAt\": 4.9,\n    \"hold\": 1.3,\n    \"size\": 150\n  }\n}\n\nFile v1.0.25:references/templates/poster-tile-mosaic/program.json\n\n[\n  {\n    \"op\": \"staggerInOut\",\n    \"theme\": 0,\n    \"enter\": [0.23, 0.37, 0.46, 0.6, 0.7, 0.84, 0.91, 1.07, 1.14, 1.28],\n    \"hold\": 1.324,\n    \"exit\": [1.37, 1.51, 1.62, 1.74, 1.83, 1.97, 2.07]\n  },\n  {\n    \"op\": \"holdRecolor\",\n    \"on\": 2.35,\n    \"onTheme\": 1,\n    \"recolor\": [2.42, 2.53, 2.81, 2.93],\n    \"themes\": [2, 3, 1, 2],\n    \"off\": 2.98\n  },\n  {\n    \"op\": \"snakeFillOverlay\",\n    \"baseTheme\": 0,\n    \"fill\": [3.04, 3.181, 3.44, 3.65, 3.79, 3.9],\n    \"swap\": 4.0,\n    \"swapTheme\": 4,\n    \"overlay\": [4.11, 4.34, 4.48, 4.81, 4.97, 5.25, 5.48]\n  }\n]\n\nArchive v1.0.24: 104 files, 218534 bytes\n\nFiles: references/frame-skeleton.md (6584b), references/montage.md (3269b), references/motion-primitive-catalog.md (9885b), references/motion-primitives/3d-card-flip/index.html (1278b), references/motion-primitives/3d-card-flip/scene.html (3393b), references/motion-primitives/assets/gsap.min.js (72927b), references/motion-primitives/bg-flow-field/index.html (1274b), references/motion-primitives/bg-flow-field/scene.html (11328b), references/motion-primitives/binary-decrypt/index.html (1310b), references/motion-primitives/binary-decrypt/scene.html (2711b), references/motion-primitives/blur-resolve/index.html (1272b), references/motion-primitives/blur-resolve/scene.html (1688b), references/motion-primitives/braam-punch/index.html (1276b), references/motion-primitives/braam-punch/scene.html (2728b), references/motion-primitives/chromatic-split/index.html (1284b), references/motion-primitives/chromatic-split/scene.html (3481b), references/motion-primitives/chrome-sweep/index.html (1278b), references/motion-primitives/chrome-sweep/scene.html (1824b), references/motion-primitives/counting-punch/index.html (1310b), references/motion-primitives/counting-punch/scene.html (3214b), references/motion-primitives/crash-zoom-in/index.html (1280b), references/motion-primitives/crash-zoom-in/scene.html (3386b), references/motion-primitives/datamosh-smear/index.html (1341b), references/motion-primitives/datamosh-smear/scene.html (3503b), references/motion-primitives/directional-fill/index.html (1284b), references/motion-primitives/directional-fill/scene.html (2773b), references/motion-primitives/dolly-zoom/index.html (1274b), references/motion-primitives/dolly-zoom/scene.html (3278b), references/motion-primitives/electric-arc/index.html (1306b), references/motion-primitives/electric-arc/scene.html (3263b), references/motion-primitives/flash-cut/index.html (1270b), references/motion-primitives/flash-cut/scene.html (3146b), references/motion-primitives/gooey-metaball/index.html (1310b), references/motion-primitives/gooey-metaball/scene.html (4918b), references/motion-primitives/hard-cut/index.html (1264b), references/motion-primitives/hard-cut/scene.html (2708b), references/motion-primitives/hypercut-whip/index.html (1280b), references/motion-primitives/hypercut-whip/scene.html (1229b), references/motion-primitives/iris-open/index.html (1272b), references/motion-primitives/iris-open/scene.html (2015b), references/motion-primitives/kinetic-letter-in/index.html (1288b), references/motion-primitives/kinetic-letter-in/scene.html (1764b), references/motion-primitives/liquid-morph/index.html (1278b), references/motion-primitives/liquid-morph/scene.html (2992b), references/motion-primitives/mask-reveal/index.html (1276b), references/motion-primitives/mask-reveal/scene.html (2086b), references/motion-primitives/mosaic-pack/index.html (1274b), references/motion-primitives/mosaic-pack/scene.html (2829b), references/motion-primitives/neon-flicker/index.html (1278b), references/motion-primitives/neon-flicker/scene.html (1979b), references/motion-primitives/outline-to-fill/index.html (1284b), references/motion-primitives/outline-to-fill/scene.html (2282b), references/motion-primitives/palette-flip/index.html (1276b), references/motion-primitives/palette-flip/scene.html (2824b), references/motion-primitives/particle-burst/index.html (1282b), references/motion-primitives/particle-burst/scene.html (3185b), references/motion-primitives/pixel-dissolve/index.html (1282b), references/motion-primitives/pixel-dissolve/scene.html (2645b), references/motion-primitives/radial-burst-lines/index.html (1290b), references/motion-primitives/radial-burst-lines/scene.html (3742b), references/motion-primitives/screen-shake/index.html (1278b), references/motion-primitives/screen-shake/scene.html (2197b), references/motion-primitives/slot-machine-reveal/index.html (1292b), references/motion-primitives/slot-machine-reveal/scene.html (2820b), references/motion-primitives/spotlight-sweep/index.html (1284b), references/motion-primitives/spotlight-sweep/scene.html (3009b), references/motion-primitives/staggered-exit/index.html (1276b), references/motion-primitives/staggered-exit/scene.html (2413b), references/motion-primitives/text-spectral-rays/index.html (1276b), references/motion-primitives/text-spectral-rays/scene.html (11504b), references/motion-primitives/text-spectral-rays/USAGE.md (2401b), references/motion-primitives/text-wave-distort/index.html (1316b), references/motion-primitives/text-wave-distort/scene.html (2999b), references/motion-primitives/tile-mosaic/index.html (1274b), references/motion-primitives/tile-mosaic/scene.html (3063b), references/motion-primitives/typewriter-reveal/index.html (1286b), references/motion-primitives/typewriter-reveal/scene.html (2634b), references/motion-primitives/word-grid-burst/index.html (1282b), references/motion-primitives/word-grid-burst/scene.html (2707b), references/planning.md (6194b)\n\nFile v1.0.24:SKILL.md\n\n---\nname: music-to-video\ndescription: \"Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.\"\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update music-to-video`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n# music-to-video — one music-grounded, beat-synced video workflow\n\nUse this skill to turn a **music track** into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is **no narration and no website capture**. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.\n\nYou are the **orchestrator**. Work in `videos/<project>/`. Run the steps in order and pass each **Gate** before moving on. Two steps need the user: **Step 3** (plan approval) and **Step 6** (render approval) — both are checkpoint gates per `../hyperframes/references/brief-contract.md` (read it before Step 0): in autonomous mode, post the Step 3 summary as a heads-up and proceed; Step 6 render approval is still asked, as the one kept question. Do every step yourself except **Step 4**, where you dispatch **one sub-agent per frame**. Keep design and motion rules out of this file — they live in `references/` and the `frame-worker` sub-agent.\n\n`SKILL_DIR` = this skill directory. `PROJECT_DIR` = `videos/<project-name>/`.\n\nWorkflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze → `audiomap.json`; Step 2 skeleton → `STORYBOARD.md` (frames, groups `TBD`); Step 3 plan → complete `STORYBOARD.md` + `frame.md`; Step 4 build → `compositions/frames/NN-*.html`; Step 5 assemble → `index.html`; Step 6 render → `renders/video.mp4`.\n\n## Two ideas that shape everything\n\n- **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).\n- **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.\n\n---\n\n## Step 0: Setup, BGM, and inputs\n\nGoal: Establish the music source, create the HyperFrames project, and note any user-supplied media.\n\n**The brief starts at the intent layer.** Opening rule, in order: **(1)** `BRIEF.md` exists → read it and ask nothing it answers — its `flow`/`storyboard` derive the mode (brief contract § 1). **(2)** No `BRIEF.md` but the project exists → resume from what's on disk; never re-interrogate. **(3)** A fresh creation request that arrived here directly → read `/hyperframes` and run its intent layer (`references/intent-interview.md`): it confirms this route's must-haves (the music source, destination → aspect — `../hyperframes/references/routes/music-to-video.md`) and announces what stays deferred — brand and genre are chosen at Step 3 by design. Write `BRIEF.md` immediately after init (never before — `init` refuses a non-empty directory) and record the preference-backed answers (`brief-format.md`). Edit requests skip all of this.\n\nThe **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user supplied audio — a music file, or a video to pull audio from — use it. Otherwise choose the mood from the request and generate a track through `/media-use` (`audio/references/bgm.md`). Before the first authenticated provider action, run `npx hyperframes auth status` and relay its output verbatim. If signed out, apply one branch:\n\n- **Collaborative:** wait for sign-in or an explicit choice to continue offline with the local provider.\n- **Autonomous:** state the status and continue through the available local provider.\n\nIf no offline provider can satisfy the required music capability, surface the blocker. Never write keys into a per-repo `.env`. Auth ownership and offline fallbacks live in `/media-use` `references/setup-providers.md` § Providers. The resulting track lands at `assets/bgm.mp3`. Stage supplied images or videos so frames can use them on the beat grid; otherwise typography carries the video.\n\n**Lyric videos:** for lyrics synced to the vocals, get word/line timing by transcribing the track via `/media-use`, or ask the user for the lyrics text and place lines on the beat grid.\n\nInitialize only if `hyperframes.json` is missing. Name `<project>` from the brief in kebab-case, such as `midnight-drive-loop` — never a timestamp. `init` checks the installed skills against the latest on GitHub and updates the global set if any are out of date.\n\n```bash\nnpx hyperframes init \"videos/<project>\" --non-interactive --example=blank --skill=music-to-video\nmkdir -p \"$PROJECT_DIR/assets\" \"$PROJECT_DIR/renders\"\ncp \"<user-music>\" \"$PROJECT_DIR/assets/bgm.mp3\"   # extract from a video first if needed\n# only if the user gave you images/videos:\nnode <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes \"$PROJECT_DIR\" --into public\n```\n\nThe **brand** (font + palette) is chosen at Step 3, not here. Don't pick a genre or a track type up front — assets are just an optional ingredient, and the genre emerges from the per-frame choices.\n\n**Gate:** `hyperframes.json` + `assets/bgm.mp3` exist; aspect / length / fps and (if any) the asset inventory are noted.\n\n---\n\n## Step 1: Analyze the music\n\nGoal: Produce the one canonical timing analysis the whole video is built on.\n\n`analyze-beatgrid.py` is the **only** beat analyzer — never re-measure beats with another tool or by ear. It reads the track once and writes `audiomap.json`: energy phases (level / density / feel), onsets + `onset_rate`, rolls, silences, `hard_stops`, `key_moments`, phrases, tempo / grid, and `audio.duration_sec`. It's deterministic — the same file always gives the same map. Most fields are reliable on any music; `bpm` and `beats_sec` are reliable only when the music is genuinely rhythmic, and judging that is the call you make at Step 2.\n\nPrerequisites: Python 3 with `librosa`, `numpy`, and `soundfile` available. If import fails, install them into the active Python environment before running the analyzer:\n\n```bash\npython3 -m pip install librosa numpy soundfile\n```\n\n```bash\npython3 <SKILL_DIR>/scripts/analyze-beatgrid.py \"$PROJECT_DIR/assets/bgm.mp3\" \\\n  -o \"$PROJECT_DIR/audiomap.json\" --print\n```\n\n**Gate:** `audiomap.json` exists; `audio.duration_sec` is known.\n\n---\n\n## Step 2: Frame skeleton (structure only)\n\nGoal: Read the music and lay out the frames — the skeleton of `STORYBOARD.md`.\n\nRead [`references/frame-skeleton.md`](references/frame-skeleton.md). Turn `audiomap.json` into the **skeleton** of `STORYBOARD.md` yourself — there is no intermediate JSON. Cut the track into **frames** at real musical changes (`hard_stops`, SURGE / DROP `key_moments`, the edges of a roll, a stretch with no onsets, a big energy jump), snapping every boundary to an audiomap anchor. For each frame set `span_sec`, `pacing` (the verdict from Step 1's trust call — `beat_cut` when the grid is real, `phrase_flow` when it's a metronome imposed on calm music), `mood`, and a one-line `feel` (the plain music situation Step 3 matches a template against). Only classify and lay out here: leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank — no templates, copy, color, or fonts. Expect ~1–6 frames.\n\n**Gate:** frames tile the track (first at 0, last at `duration_s`); each carries `span_sec` + `pacing` + `mood` + `feel`; every `### Groups` is `TBD`; no content anywhere.\n\n---\n\n## Step 3: Fill the plan (user-gated)\n\nGoal: Turn the skeleton into an approved, complete `STORYBOARD.md`.\n\nRead [`references/planning.md`](references/planning.md), [`storyboard-format.md`](references/storyboard-format.md), [`template-catalog.md`](references/template-catalog.md), [`motion-primitive-catalog.md`](references/motion-primitive-catalog.md), and [`montage.md`](references/montage.md) (only if the user supplied assets). Editing the same file in place, do two things:\n\n1. **Pick the brand.** Choose one preset from `../hyperframes-creative/frame-presets/` using the table in `../hyperframes-creative/references/design-spec.md` (match the track's mood; **only its fonts and colors matter** — templates own composition). Copy it into `frame.md` **unmodified** and fill the frontmatter `style` (font + a ≤4–6 swatch palette) from it.\n2. **Fill every frame.** Decide its groups and give each a treatment: a matched template from the catalog (with bound params and real audiomap anchors), a free-compose from the primitive catalog, or an asset treatment that **obeys `pacing`**. **Before you free-compose a named look, search the live catalog for it**: for every look, effect, treatment or transition the user asked for — \"CRT scanlines\", \"glitch\", \"film grain\", \"shimmer sweep\" — run `npx hyperframes catalog --query \"<the look, in plain English>\" --json` and read the top results. `template-catalog.md` and `motion-primitive-catalog.md` list only this skill's own local materials; the search ranks the whole hosted registry (~400 blocks and components) and needs **nothing installed** — no project, no prior `add`, no account. Free-compose a look only after a search for it came back with nothing that fits. Write the copy. You own WHAT (template / primitives + content + anchors); the frame-worker owns HOW — **never write millisecond tweens into the storyboard**.\n\n```bash\nnode <SKILL_DIR>/scripts/validate-plan.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --audiomap \"$PROJECT_DIR/audiomap.json\" --templates <SKILL_DIR>/references/templates\n```\n\nFix every `✗` (hard errors: duration mismatch, frames not tiling the track, a missing `src`); warnings are best-effort. Then present the frame-by-frame summary in chat as a proposal (`../hyperframes/references/review-loop.md` § 1) and iterate on the user's replies until they approve; for `storyboard: yes`, also write it as `storyboard.html` (`../hyperframes-creative/references/storyboard-recipe.md` § 3) for them to open. In autonomous mode this is a checkpoint gate: post the summary as a heads-up and proceed (the `validate-plan.mjs` pass is a quality gate and still blocks).\n\n**Gate:** `frame.md` is a verbatim preset copy; `validate-plan.mjs` exits 0; the user approved the plan (autonomous: the summary was posted as a heads-up).\n\n---\n\n## Step 4: Build frames from the plan\n\nGoal: Build every frame as a self-contained composition file.\n\nCreate `compositions/frames/`. Read [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) and `../hyperframes/references/subagent-dispatch.md`. Dispatch **one frame-worker per frame**, in parallel where possible (otherwise in waves). Each worker gets exactly one frame and this context:\n\n```text\nPROJECT_DIR: <abs path>\nframe_id: <NN-frame_id>              # = the frame file stem, e.g. 02-f2; the composition id\nYour block: the `## Frame N — <frame_id>` block in PROJECT_DIR/STORYBOARD.md\naudiomap: PROJECT_DIR/audiomap.json\nframe.md: PROJECT_DIR/frame.md\nMaterials: for each group, <SKILL_DIR>/references/templates/<id>/index.html (templates) and\n           <SKILL_DIR>/references/motion-primitives/<id>/ (free); staged assets/ (asset groups)\nContracts: ../hyperframes-core/references/sub-compositions.md + determinism-rules.md\nCanvas: <w>×<h>   Pacing: <beat_cut|phrase_flow>\nWrite to: PROJECT_DIR/compositions/frames/<frame_id>.html\n```\n\nThe worker forks the cited materials, converts every anchor to frame-local seconds (`local_t = track_t − span_sec[0]`), gates its groups with 0ms cuts, and writes one seek-safe frame file. **The worker never runs the `hyperframes` CLI** — those commands operate on the assembled project, which doesn't exist yet, so they'd report on the wrong files. The worker just writes to the contract and stops; you verify after assembly (Step 6). As each worker returns, you can confirm its file landed on disk.\n\n**Gate:** every frame has its `compositions/frames/NN-*.html` on disk.\n\n---\n\n## Step 5: Assemble\n\nGoal: Wire the built frames + BGM into the playable `index.html`.\n\n`assemble-index.mjs` is deterministic — no subagent, no judgment. It references each frame file at its cumulative `data-start`, mounts `assets/bgm.mp3` on track 11, and hard-cuts frame → frame (frames tile the track with no gaps, so there is **no transition injector**).\n\n```bash\nnode <SKILL_DIR>/scripts/assemble-index.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --hyperframes \"$PROJECT_DIR\" --audiomap \"$PROJECT_DIR/audiomap.json\"\n```\n\nFix any `✗` it reports — a missing or blank frame file means that worker wrote a partial file; re-dispatch it (Step 4) and re-assemble.\n\n**Gate:** `index.html` exists; total duration == `audiomap.audio.duration_sec`.\n\n---\n\n## Step 6: Verify and render\n\nGoal: Verify the assembled video, get user approval, and render the final MP4.\n\nRun the CLI on the **assembled project** — that's the correct unit (the per-frame workers couldn't run it). `check` runs structural lint and the headless-browser runtime, layout, motion, and contrast gate in one pass; `--snapshots` also emits the review frames.\n\n```bash\n( cd \"$PROJECT_DIR\" && npx hyperframes check . --snapshots )\n```\n\nInspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stops[].t`, and the final frame. On failure, make the **cheapest safe fix** yourself: edit the offending `compositions/frames/NN-*.html`. Never change duration or audio timing to hide a sync issue. Once the gates pass, open the final Studio preview (`( cd \"$PROJECT_DIR\" && npx hyperframes preview --background )`) and pause for user review — render now, or what changes? Render only on approval (autonomous mode: the same, as the one kept question), then deliver the MP4 with the contact sheet:\n\n```bash\n( cd \"$PROJECT_DIR\" && npx hyperframes render . --skill=music-to-video -q draft -o renders/video.mp4 --fps 30 )\n```\n\n**Gate:** `check` passed and the snapshots were inspected; the user approved (autonomous: checks passed and the delivery includes the contact sheet); `renders/video.mp4` exists with audio, duration == `audiomap.audio.duration_sec`. The final reply states the MP4 path and duration.\n\n---\n\n## Resume table\n\n| You have                   | Continue from |\n| -------------------------- | ------------- |\n| `assets/bgm.mp3` only      | Step 1        |\n| `audiomap.json`            | Step 2        |\n| `STORYBOARD.md` (skeleton) | Step 3        |\n| `STORYBOARD.md` (complete) | Step 4        |\n| all frame files            | Step 5        |\n| `index.html`               | Step 6        |\n\n## Quick Reference\n\n**Formats:** landscape `1920x1080` by default; portrait `1080x1920`; square `1080x1080`. Set the canvas once in the storyboard frontmatter (`canvas: { w, h, fps }`).\n\n**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.\n\n| Read                                                                                                           | When                                                    |\n| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |\n| [`references/frame-skeleton.md`](references/frame-skeleton.md)                                                 | Step 2: read the music, lay out the frames, set pacing  |\n| [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan |\n| [`references/template-catalog.md`](references/template-catalog.md)                                             | Step 3: pick a template per group                       |\n| [`references/motion-primitive-catalog.md`](references/motion-primitive-catalog.md)                             | Step 3/4: L0 recipes for free-compose                   |\n| [`references/montage.md`](references/montage.md)                                                               | Step 3/4: asset treatments (beat-cut / ken-burns)       |\n| [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md)                                                     | Step 4: dispatch + build one frame                      |\n| `../hyperframes/references/subagent-dispatch.md`                                                               | Step 4: dispatch sub-agents safely                      |\n| `../hyperframes-creative/references/design-spec.md`                                                            | Step 3: pick the preset (the brand)                     |\n\n## Directory layout\n\n```\nmusic-to-video/\n  SKILL.md\n  references/   frame-skeleton.md · planning.md · storyboard-format.md\n                template-catalog.md · motion-primitive-catalog.md · montage.md\n                templates/<id>/          { index.html (+ assets/ · program.json) }  ← L1 catalog impls\n                motion-primitives/<id>/  { index.html (mounts the scene), scene.html (the sub-composition) } (+ ../assets/gsap.min.js shared by recipes) ← L0 catalog impls\n  scripts/      analyze-beatgrid.py · assemble-index.mjs · validate-plan.mjs · stage-assets.mjs · lib/storyboard.mjs\n  sub-agents/   frame-worker.md   ← the one subagent (one per frame)\n```\n\nFile v1.0.24:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"music-to-video\",\n  \"version\": \"1.0.24\",\n  \"publishedAt\": 1791142367766\n}\n\nFile v1.0.24:references/frame-skeleton.md\n\n# Frame skeleton (Step 2) — read the music, lay out the frames\n\nAt Step 2 **you (the orchestrator)** read `audiomap.json` and write the **skeleton** of\n`STORYBOARD.md` directly: cut the track into **frames** (one frame = one composition file =\none scene), and for each frame set its **span**, its **pacing** (does this stretch want hard\nbeat-cuts, or calm phrase/energy flow?), its **mood**, and a one-line **feel** note.\n\nYou **classify and lay out the spine only.** You do **not** pick templates, write copy, choose\ncolors/fonts, or decide a frame's groups — those are Step 3 (the plan fills each frame in\nplace). Leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank.\n\nThere is **no intermediate JSON** — the skeleton _is_ the start of `STORYBOARD.md`. Step 3\nedits the same file.\n\n## The trust boundary (read this first)\n\n`audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are\nreliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:\n\n| Field                                                                                                                                                                                               | Trust                                                                                                                                                                                                                        |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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                                                                                                                                                                                             |\n| `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. |\n\n- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.\n- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.\n\n## How to lay out frames (run in order)\n\n1. **Gestalt.** From `summary` / `tempo` / `audio.duration_sec` + the roll count, write the\n   frontmatter `compositionId`, `duration_s` (== `audio.duration_sec`), `canvas`, and a\n   one-line read of the track's density arc.\n2. **Cut into frames.** Walk `energy_phases[]` and split where the music genuinely changes\n   state — at `hard_stops[]`, `SURGE` / `DROP` `key_moments`, the start/end of a `rolls[]` run,\n   an **onset desert** (a long gap in `events[]`), or a big energy-level jump. Collapse adjacent\n   phases that are one gesture. Expect **~1–6 frames**; a short clip may be one.\n   **Snap every boundary to an audiomap anchor**, then re-snap to the nearest `beats_sec`\n   (tolerance ≤ ½ beat) **only when the grid is reliable**; on calm material snap to\n   `phrases[]` / `energy_phases[]` edges instead. Frames **tile the track** (first at 0, last\n   at `duration_s`, no gaps/overlaps).\n3. **Per frame, set `pacing`** — the trust-boundary call:\n   - **`beat_cut`** — genuinely rhythmic: a roll present, **or** dense, **or** (clearly high `onset_rate` **and** a steady grid). Hard cuts / per-onset reveals may anchor to beats.\n   - **`phrase_flow`** — calm / sparse: `rolls`≈0, mostly sparse, low `onset_rate`. Do **not** anchor hard cuts to the grid; pace by `phrases[]` + the energy envelope (slow crossfades, long holds).\n4. **Per frame, tag `mood`** (1–3 of: `warm` · `dark` · `hype` · `elegant` · `glitch` ·\n   `cinematic` · `playful` · `tense` · `dreamy` · `aggressive`) from `energy_phases[].feel` +\n   energy + any genre cue in the brief/title.\n5. **Per frame, write a one-line `feel`** — the plain-language music situation Step 3 matches a\n   template against (e.g. \"accelerating onset stream into a held downbeat\", \"calm held pad, one\n   onset desert\", \"fast sustained-fill roll, no readable message\"). This is what the planner\n   reads against the catalog's **Reach for it when** — keep it concrete, drawn from the robust\n   fields, never invented.\n\n## What the skeleton looks like\n\nA valid `STORYBOARD.md` with the spine set and every frame's treatment left for Step 3\n(full syntax in [`storyboard-format.md`](storyboard-format.md)):\n\n```markdown\n---\ncompositionId: bgm\nduration_s: 30.0 # == audiomap.audio.duration_sec\ncanvas: { w: 1920, h: 1080, fps: 30 }\nstyle: # blank — Step 3 fills it from the chosen frame.md preset\nbuild_notes: [\"one paused timeline per frame\", \"no remote assets\"]\n---\n\n## Frame 1 — f1\n\n- src: compositions/frames/01-f1.html\n- duration: 7.198s # = span length; assembler sums these for cumulative data-start\n- span_sec: [0.0, 7.198] # track seconds; frames tile the track\n- pacing: beat_cut\n- mood: [hype]\n- feel: accelerating onset stream building into a held downbeat\n\n### Groups\n\n- TBD (Step 3)\n\n## Frame 2 — f2\n\n- src: compositions/frames/02-f2.html\n- duration: 10.4s\n- span_sec: [7.198, 17.598]\n- pacing: phrase_flow\n- mood: [warm, cinematic]\n- feel: calm held pad, one long onset desert\n\n### Groups\n\n- TBD (Step 3)\n```\n\n## Self-check\n\n- `duration_s == audiomap.audio.duration_sec`; frames tile the track gap-free (first at 0, last at `duration_s`).\n- Every frame has `src` + `span_sec` + `duration` + `pacing` + `mood` + a one-line `feel`.\n- `pacing` was set from the **robust** fields (energy / density / rolls / onset_rate), never from `bpm` / `beats_sec` alone.\n- No frame boundary sits inside a `rolls[]` run or leaves a sub-1-bar fragment.\n- Every frame's `### Groups` is `TBD (Step 3)`; `style` is blank. **No template, copy, color, or font anywhere.**\n\nFile v1.0.24:references/montage.md\n\n# Asset treatments — weaving user media onto the beat spine\n\nWhen the user supplies images/videos, a group can be an **asset treatment** instead of a\ntypographic template/free-compose. Assets are an **additive ingredient on the same beat\nspine** — never a separate pipeline. Typography/templates stay the floor: if no asset fits a\ngroup, fall back to a template/free group (a complete video needs zero assets).\n\nThe planner (Step 3) picks the treatment and the clips + anchors (WHAT); the frame-worker\nrealizes it inside the frame file (HOW). **Obey the frame's `pacing`.**\n\n## The three treatments\n\n### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)\n\nThe asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor\n(the frame's beats/onsets from the audiomap). Each clip is a `class=\"clip\"` element\n(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with\n`data-start`/`data-duration`/`data-track-index` per the core clip contract. (Muted on purpose: the music track drives the sound.) Between clips,\ncrossfade the outgoing content to `opacity:0` ending **at** the next anchor.\nCut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.\n\n### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)\n\nFor calm frames: one clip held over the span with a slow scale/translate push (e.g. scale\n1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by\nbeats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment\nwhen the beat grid is unreliable (calm music).\n\n### `bg_under_text` — clip dimmed behind a template/free group\n\nA full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template\nor free-compose typographic treatment. The text rides on the same anchors; the clip is the\nbed. Use when the user wants their footage present but the message must stay readable.\n\n## Rules\n\n- **`pacing` decides the treatment**: `beat_cut` only on a `beat_cut` frame; on a\n  `phrase_flow` frame use `ken_burns` or a slow crossfade — **never** per-onset hard cuts on\n  the (unreliable) calm grid.\n- **Clips are muted; the root owns audio.** Mount each `<video class=\"clip\">` **muted**, as a\n  direct child of the frame root (never nested in another timed element, or the renderer\n  freezes it). The BGM is the only audio in v1.\n- **Crossfades animate `opacity`/`autoAlpha`**, never `visibility`/`display` on a `.clip`\n  (the framework owns clip visibility — that trips `gsap_animates_clip_element`).\n- **Backgrounds dim ~30–50%** so any foreground text stays legible.\n- Anchors are **track seconds from `audiomap.json`**; the worker subtracts the frame start\n  to get frame-local time.\n- Local staged assets only (`assets/` via `stage-assets.mjs`); never remote URLs.\n\n## Deferred hook (not v1)\n\nA clip that should play **its own sound** (interview cut, lyric clip) needs a sibling\n`<audio>` mounted at the **root** by the assembler, with the BGM ducked under it (a\n`data-automation` volume lane on the BGM, see `creator-editing-recipes.md` in `hyperframes-core`). The frame-worker mounts no audio. Keep clips muted in v1; wire clip-audio +\nducking only when the user asks.\n\nFile v1.0.24:references/motion-primitive-catalog.md\n\n# Motion-primitive catalog — the free-compose menu\n\nThe atomic layer: one anchor → one micro-move. When no template fits a group, free-compose by\nnaming primitives from here. Scan **anchor** + **best span** + **what it does**, then pick the\nsmallest set that carries the group.\n\n## Timing & latency (applies to every primitive)\n\n- **Hard hits are 0ms.** Cuts, palette flips, content swaps, freezes are `tl.set(...)` with no duration — the percussion _is_ the motion. Easing a hit kills it.\n- **Lead the anchor.** A move that must _land_ on a beat (a wipe covering the frame, a count-up locking, two blocks colliding) starts **~40–190ms early** so it completes ON the anchor. Reactive entrances (something appearing _because_ of the hit) fire 0–45ms after.\n- **Eased entrances: 300–500ms** (scale punch, slides, camera pushes). **Macro builds: 800–2000ms** spanning a whole roll / silence.\n- **Per-bar caps:** one accumulating element per hit (not a burst); a camera move at most once per phrase, never per beat; a dense flip/strobe system runs ≤2–3s.\n- **Tension-builds lock.** A count-up / sequential build / morph must _resolve on_ a downbeat or hard_stop, never trail off mid-bar.\n- **Best span means active motion.** The catalog's span guidance is not a license to stretch one primitive over a whole frame. If a free-composed group runs longer than the listed span, add a hold / bed / next primitive, or split the frame into another group at the next musical anchor.\n\n## Catalog\n\n| id                    | anchor                           | best span         | what it does                                                              |\n| --------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------- |\n| `hypercut-whip`       | beat / hard_stop                 | 0.18-0.45s        | fast whip-pan hard cut between frames                                     |\n| `kinetic-letter-in`   | downbeat / phrase                | 0.4-1.2s          | per-letter kinetic entrance                                               |\n| `braam-punch`         | drop / surge                     | 0.2-0.9s active   | big impact: scale + weight slam                                           |\n| `chromatic-split`     | snare / glitch / surge           | 0.1-0.6s          | RGB channel split / glitch on a word                                      |\n| `mask-reveal`         | section_start / downbeat         | 0.5-1.2s          | clip-path mask wipe reveal                                                |\n| `screen-shake`        | drop / crash / kick              | 0.1-0.5s          | camera / screen shake jitter                                              |\n| `binary-decrypt`      | roll / build                     | 0.8-2.5s          | scramble→decode text (binary → word)                                      |\n| `dolly-zoom`          | phrase / build                   | 1.2-2.5s          | vertigo dolly-zoom (scale vs perspective)                                 |\n| `iris-open`           | section_start / reveal           | 0.6-1.2s          | circular iris-open reveal                                                 |\n| `electric-arc`        | accent / glitch                  | 0.1-0.6s          | electric arc / lightning accent                                           |\n| `neon-flicker`        | hold / texture                   | 0.5-2.5s          | neon-sign flicker                                                         |\n| `chrome-sweep`        | downbeat / reveal                | 0.6-1.4s          | metallic specular sweep across text                                       |\n| `slot-machine-reveal` | roll → downbeat                  | 0.8-2.0s          | slot-machine spin-to-land character reveal                                |\n| `liquid-morph`        | phrase / transition              | 1.0-2.5s          | liquid / blob morph                                                       |\n| `gooey-metaball`      | build / drop                     | 1.5-3.0s          | gooey metaball merge field                                                |\n| `3d-card-flip`        | downbeat / swap                  | 0.8-1.6s          | 3D card flip (rotateY)                                                    |\n| `crash-zoom-in`       | drop / surge                     | 0.2-0.8s          | violent crash zoom-in                                                     |\n| `spotlight-sweep`     | reveal / hold                    | 0.8-2.0s          | spotlight / gradient sweep over text                                      |\n| `outline-to-fill`     | downbeat / reveal                | 0.8-1.8s          | stroke outline → solid fill                                               |\n| `counting-punch`      | roll → downbeat                  | 1.0-2.5s          | number count-up that punches & locks                                      |\n| `particle-burst`      | drop / crash                     | 0.2-1.2s          | particle explosion burst                                                  |\n| `radial-burst-lines`  | drop / surge                     | 0.2-0.8s          | radial speed-lines burst                                                  |\n| `pixel-dissolve`      | transition / hard_stop           | 0.5-1.5s          | pixelated dissolve                                                        |\n| `datamosh-smear`      | glitch / transition              | 0.4-1.2s          | datamosh / motion smear                                                   |\n| `text-wave-distort`   | hold / texture                   | 1.0-2.5s          | wavy text distortion                                                      |\n| `bg-flow-field`       | energy / whole span (bed)        | 4-12s bed         | generative curl-noise background bed; compose any foreground move over it |\n| `blur-resolve`        | stop / final hold                | 0.7-2.0s          | blur-in to crisp focus, then blur-out on the cut                          |\n| `chromatic-pressure`  | snare / glitch                   | 0.1-0.5s          | RGB split / digital tension on a transient                                |\n| `color-grid-shuffle`  | onset                            | 0ms hits; ≤2s run | grid of cells recolored by a deterministic index per onset                |\n| `content-swap`        | beat                             | 0ms hits; ≤3s run | 0ms swap of stacked nodes: the workhorse percussive move                  |\n| `directional-fill`    | beat / reveal                    | 0.3-1.0s each     | directional wipe-fill (scaleX) sweeping across bars                       |\n| `flash-cut`           | drop / crash                     | 0-0.6s            | full-frame flash masking a word / color state change                      |\n| `freeze-hold`         | hard_stop                        | 0ms in; 0.5-2s    | freeze the moving system and hold it                                      |\n| `hard-cut`            | beat / hard_stop                 | 0ms in; 0.3-2s    | sample-accurate color-block + word cut                                    |\n| `mosaic-pack`         | beat / build                     | 1.5-3.5s          | scattered tiles fly in and pack into a grid                               |\n| `negative-space-hold` | silence / hard_stop / final hold | 1-6s hold         | kill busy layers, hold one readable mark in empty space                   |\n| `overlay-pop`         | accent                           | 0.2-0.6s in       | badge / lower-third overlay pops in over a base                           |\n| `palette-flip`        | section change                   | 0ms flip; 0.5-4s  | same layout re-skins via 0ms palette-variable flips                       |\n| `staggered-exit`      | phrase / transition              | 0.4-1.2s          | ordered cascade-out clearing the frame                                    |\n| `staggered-reveal`    | build                            | 0.8-2.5s          | ordered cascade-in of a stack / list                                      |\n| `system-replace`      | drop / regime change             | 0ms cut           | hard-cut the entire visual system, then boot the new one                  |\n| `text-spectral-rays`  | phrase / sweep (hero text)       | 2.5-5s            | volumetric light-rays cast by a wordmark toward a sweeping light cursor   |\n| `tile-mosaic`         | build / reveal                   | 1.5-3.5s          | grid of tiles revealed in a diagonal sweep, assembling a poster           |\n| `typewriter-reveal`   | roll / build                     | 1.0-3.0s          | character / word type-on with caret                                       |\n| `value-counter`       | roll → downbeat                  | 1.0-2.5s          | count-up that locks on a downbeat / hard_stop                             |\n| `word-grid-burst`     | onsets → downbeat                | 1.8-3.2s          | grid of words revealed per onset, refocus one on a downbeat               |\n\n## How to combine\n\n- One dominant system per group; layer at most one texture primitive over one structural primitive.\n- Structure on strong beats (cuts, camera, `system-replace` → downbeat / phrase / section_start); texture on weak / syncopated hits (`content-swap`, typewriter letters, chromatic accents).\n- A roll is an accumulation container — build during it, hard-cut to a clean layout on the downbeat that ends it.\n- `drop` ≠ `downbeat`: a downbeat is a cut within the regime; a drop is a regime change (`system-replace`, total clear, element-count jump).\n- Let silence remove density (`negative-space-hold`).\n- Background beds are a layer, not a move: one bed at a time, under foreground primitives.\n- `text-spectral-rays` is the hero wordmark treatment; do not stack another visible copy of the same word on top.\n\nFile v1.0.24:references/motion-primitives/text-spectral-rays/USAGE.md\n\n# Using `text-spectral-rays` — it OWNS its wordmark\n\n`text-spectral-rays` is a **self-contained WebGL hero-text renderer**. From ONE rasterized\nglyph mask it draws **both** the solid wordmark **and** the spectral rays that emanate from\nit. Letters and rays share the same mask, so they are always perfectly registered.\n\n## The one trap: never give the word a second source\n\nThe ghost / doubled-wordmark artifact comes from splitting the word across two sources:\n\n- ❌ **Wrong** — use the shader as a \"rays-only background\" and draw the visible letters\n  with a **separate DOM element** (or stack a second text move like `content_swap` /\n  `chromatic_pressure` on the same word). The DOM font (e.g. Inter) and the shader's raster\n  font (Arial Black / Impact fallback) differ in width, shape, and position, so the ray\n  edges never line up with the DOM letters → a misregistered ghost. **Deleting the shader's\n  letter terms does NOT fix it** — the ray mask itself is still the second, misaligned copy\n  of the word.\n\n- ✅ **Right** — let the shader render the wordmark. There is exactly ONE source, so a\n  ghost is structurally impossible.\n\n## Integrate in one pass\n\n1. **It IS the wordmark.** Hide any DOM logo for that word (keep it only as an invisible\n   layout spacer if a tagline/CTA below depends on its box). Never stack a discrete text\n   move on the same word.\n2. **One timeline.** Merge its `progress` / `effectMix` state tweens onto the group's master\n   timeline and repaint via `tl.eventCallback(\"onUpdate\", render)` — no second timeline, no\n   `requestAnimationFrame`.\n3. **Align the cursor to the word.** `mouse.y` must equal the mask's vertical center. If you\n   move the rasterized word off frame-center (e.g. up, to leave room for a tagline), shift\n   the cursor's `y` by the same amount — otherwise the rays cast at the wrong angle.\n4. **Local raster only.** Rasterize the word with a bundled / system font (no CDN font);\n   upload the mask + colour canvases as textures.\n5. **Entrance.** Slam the whole canvas (autoAlpha + a scale punch, `transform-origin` on the\n   word's optical center) on the hit; let `effectMix` bloom the rays just after. The solid\n   letters are present the instant the canvas reveals.\n\n## Pairs with\n\nA background bed (`bg-flow-field`) or **separate** supporting elements (tagline, CTA, rule)\n— never a second treatment of its own word.\n\nFile v1.0.24:references/planning.md\n\n# Planning (Step 3) — pick the brand, fill every frame\n\nAt Step 3 **you (the orchestrator)** turn the Step-2 skeleton into a complete, approved\n`STORYBOARD.md`. You edit the **same file** in place: pick the brand spine, then for each\nframe decide its **groups**, give each group a treatment, bind real beat anchors, and write\nthe copy.\n\nYour mantra: **music is the spine; a template is a head start, not a cage; typography is the\nfloor and assets are an optional ingredient on the same beat grid.**\n\n**You own WHAT, not HOW.** You name the template / primitives, the content, the brand, the\nanchor seconds, and the intent. The frame-worker (Step 4) decides HOW — micro-timing,\nrealization, intra-frame cuts. **Never write millisecond tweens into the storyboard.**\n\n## Inputs\n\n- The Step-2 skeleton already in `STORYBOARD.md` — frames with `span_sec` + `pacing` + `mood` + `feel`.\n- `audiomap.json` — timing truth; read the real anchor seconds inside each frame's span.\n- [`template-catalog.md`](template-catalog.md) — the template selection menu.\n- [`motion-primitive-catalog.md`](motion-primitive-catalog.md) — the free-compose menu (L0 recipes).\n- [`montage.md`](montage.md) — asset treatments (only if the user supplied images/videos).\n- User brief / supplied copy — topic, mood, exact words to keep.\n\n## Step A — pick the brand spine (one preset, unmodified)\n\nThe whole video shares one type family + palette. Pick **one ready-made preset** from\n`../../hyperframes-creative/frame-presets/` using the preset table in\n`../../hyperframes-creative/references/design-spec.md` — choose by the track's mood + the brief,\nand **only its fonts + colors matter** (templates own composition + motion; the preset only\nsets the look). Copy it in **unmodified**:\n\n```bash\ncp ../hyperframes-creative/frame-presets/<preset>/FRAME.md \"$PROJECT_DIR/frame.md\"\n```\n\nThen fill the storyboard frontmatter `style` from it: the `font` from its `typography:` and a\n≤4–6 swatch `palette` from its `colors:`. **Quote the hex / family verbatim — never invent or\nround.** Every group's palette params draw from this one palette; that unity is what makes\ndifferent templates read as one piece.\n\n## Step B — per frame, decide its groups\n\nA frame is usually **one group** (one template or one free composition spanning the frame).\nSubdivide into 2+ groups **only when a single treatment can't cover the frame** — e.g. a busy\nopener plus a closing lockup. When you split, cut at a **real audiomap anchor** inside the\nframe's span (a `key_moment` / `phrase` edge / onset-cluster gap), **never inside a `rolls[]`\nrun**, and keep every group **≥ ~1 bar**. Density does **not** force more groups — a dense\nframe is usually ONE group whose template absorbs the density internally (a meta-template like\n`poster-tile-mosaic`). **Group count tracks distinct treatments, not beats.**\n\n## Step C — per group, pick a treatment (exactly one of three)\n\n### A. Match a template\n\nRead [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing`\nto a template's **Reach for it when**; take the closest fit. Then bind it:\n\n- Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length.\n- Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times).\n- If the template's natural stop and the group's span end disagree, snap to the nearest anchor.\n\n### B. Free-compose (no template fits)\n\nWrite a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md)\n(a dominant system + the named L0 primitives + a density topology) + `anchors` (the real\nbeat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as\ncarefully as a matched group — never a failure.\n\n### C. Asset treatment (only when the user supplied assets and they fit)\n\nMake it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut`\nframe use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use\n`ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none\nfits a group, fall back to template / free (typography is the floor — a complete video needs\nno assets).\n\n## Copy (you own the words)\n\n- Keep exact user words; else invent with taste, on the brief's mood.\n- **Message vs texture:** a readable word holds ≥1 beat (headline 3–8, sentence 4–10), stable + focal; a word held <1 beat is texture (strobe / grid / ticks). Never force a message onto a sub-beat — demote it to texture.\n- Place copy into the template's text params, onto a free group's anchors, or as an asset group's `overlay_copy`. Declare the anchor + accumulate / stagger intent; leave micro-timing to the worker.\n- A closing logo / CTA lands on the final hit / hard stop and holds through trailing silence.\n\n## Transitions (you do not emit them)\n\nEverything is a **0ms hard cut** for now. **frame → frame** is owned by the assembler\n(back-to-back files); adjacent `span_sec` already imply the cut. **group → group inside a\nframe** is owned by the worker on its frame timeline; you only set each group's `span_sec`.\n\n## Write + validate\n\nComplete `STORYBOARD.md` ([`storyboard-format.md`](storyboard-format.md)), then run\n`node scripts/validate-plan.mjs` and fix every `✗`. Present the frame-by-frame summary in chat\nand iterate until approved (Step 3 in `SKILL.md` says how).\n\n## Self-check\n\n- `frame.md` is a verbatim copy of one preset; frontmatter `style.font` / `style.palette` are drawn from it (exact values).\n- Every frame became ≥1 group; groups tile the frame span in order; no group < ~1 bar; no group boundary inside a `rolls[]` run.\n- Each group is exactly one of template / free_design / asset.\n- Template `params` keys match the catalog entry; `role_bindings` / `anchors` use real audiomap seconds.\n- Asset treatments obey `pacing` (no `beat_cut` on a `phrase_flow` frame).\n- Every group's palette draws from the one brand palette.\n- `duration_s == audiomap.audio.duration_sec`; `validate-plan.mjs` passes.\n\nFile v1.0.24:references/storyboard-format.md\n\n# STORYBOARD.md format — frames → groups\n\n`STORYBOARD.md` is the single reviewable plan the user approves at Step 3 and the **manual**\neach frame-worker follows at Step 4. It is **hierarchical**: one block per **frame**, written\nas a `## Frame N — <frame_id>` heading (the parser recognizes `Frame`). **One frame = one\nscene = one composition file.** Inside each frame block are its **groups** (the treatment\nunits). The assembler reads the frame level (`duration` → `data-start`, `src`); the worker\nreads its own frame block. A frame's **composition id = its `src` file stem**\n(`compositions/frames/01-f1.html` → `01-f1`), which the worker uses as `data-composition-id`\nand the `window.__timelines` key.\n\nStep 2 writes the skeleton (frame fields, groups `TBD`); Step 3 fills the groups + brand. It\nis a build spec, not code — the planner writes WHAT, the worker decides HOW. **Never write\nmillisecond tweens here.**\n\n## File shape\n\nYAML frontmatter (the video-wide spine) + one `## Frame N — <frame_id>` block per frame.\n\n```markdown\n---\ncompositionId: bgm\nduration_s: 30.0 # == audiomap.audio.duration_sec, exactly\ncanvas: { w: 1920, h: 1080, fps: 30 }\nstyle: # brand spine — from the chosen frame.md preset (Step 3)\n  font: \"EB Garamond / Inter / JetBrains Mono\" # the preset's typography, verbatim\n  palette: [\"#FAF9F5\", \"#141413\", \"#CC785C\", \"#181715\"] # ≤4–6 swatches from the preset's colors\nassets: false # false, or a note like \"assets/ has 6 user photos\"\nbuild_notes: [\"one paused timeline per frame\", \"no remote assets\"]\navoid: [\"generic slideshow\", \"tiny unreadable hero text\"]\n---\n\n## Frame 1 — f1\n\n- src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id\n- duration: 7.198s # = span length; the assembler reads this for cumulative data-start\n- span_sec: [0.0, 7.198] # track seconds; frames tile the track\n- pacing: beat_cut # beat_cut | phrase_flow (from the skeleton; obey it)\n- mood: [hype]\n- feel: accelerating onset stream into a held downbeat\n\n### Groups\n\n- **g1** — template: `intro-kinetic-cascade`\n  - span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start)\n  - params: { theme: \"light\", icon: \"bolt\", phrases: \"[…]\", climax: \"{…}\" }\n  - role_bindings: { phrase: { times: [0.14, 0.55, 0.87] }, climax: { in: 3.79, iconAt: 4.9 } }\n  - copy: \"GROWTH THROUGH CREATIVITY\"\n- **g2** — free_design\n  - span_sec: [4.017, 7.198]\n  - free_design: { dominant_system: \"per-onset typography\", primitives: [\"content-swap\", \"braam-punch\"], density_topology: \"accumulate\" }\n  - anchors: [4.10, 4.80, 5.50, 6.20] # onset seconds the reveals ride (from audiomap)\n  - copy: [\"BUILD\", \"SHIP\", \"REPEAT\"]\n\n## Frame 2 — f2\n\n…\n```\n\n## Frame block — required fields\n\n| field                             | meaning                                                                                                                        |\n| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index.                                                                    |\n| `src`                             | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. |\n| `duration`                        | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`.         |\n| `span_sec`                        | `[start, end]` track seconds. `duration = end − start`.                                                                        |\n| `pacing`                          | `beat_cut` \\| `phrase_flow` — from the skeleton; the worker must obey (no hard-cut on `phrase_flow`).                          |\n| `mood`, `feel`                    | from the skeleton; tone + the one-line music situation the planner matched against.                                            |\n| `### Groups` list                 | ≥1 group; groups tile the frame span in order.                                                                                 |\n\n## Group entry — exactly one of three kinds\n\nEvery group is **template** OR **free_design** OR **asset** — never two, never none. All three\ncarry `span_sec` (track seconds, tiling the frame) and may carry `copy`.\n\n- **template** — `template: <catalog id>` + `params` (keys from the catalog entry) + `role_bindings` (real audiomap anchor seconds) + `copy`.\n- **free_design** — `free_design: { dominant_system, primitives: [catalog ids], density_topology }` + `anchors` (real beat / onset seconds) + `copy`.\n- **asset** — `asset: { treatment, clips: [public/…], anchors?, overlay_copy? }`. `treatment` ∈ `beat_cut` (one clip per anchor — only on a `beat_cut` frame) | `ken_burns` (slow push — fits `phrase_flow`) | `bg_under_text` (clip dimmed behind a template / free group). See [`montage.md`](montage.md).\n\n## Rules\n\n- Frames tile the track (gap-free, first at 0, last at `duration_s`); a frame's groups tile its span; no group < ~1 bar; no group boundary inside a `rolls[]` run.\n- `params` keys come from the template's [`template-catalog.md`](template-catalog.md) entry.\n- All anchor seconds are **track seconds from `audiomap.json`** — the worker converts to frame-local by subtracting the frame start.\n- A `phrase_flow` frame MUST NOT use `beat_cut` asset treatment or per-onset hard cuts.\n- The brand `style` is set once; every group's palette draws from it.\n- Reviewable prose-plus-data; keep it scannable. No GSAP, no millisecond timing.\n\n## Self-check (the planner runs `validate-plan.mjs`)\n\n- frontmatter has `compositionId`, `duration_s` (== audiomap), `canvas`, `style`.\n- every frame has `span_sec` + `src` + positive `duration` + `pacing` + ≥1 group; frames tile the track.\n- every group is exactly one of template / free_design / asset; template ids exist in the catalog; anchors are real audiomap seconds; `phrase_flow` frames have no `beat_cut`.\n\nFile v1.0.24:references/template-catalog.md\n\n# Template catalog — the selection menu (Step 3)\n\nReverse-engineered, asset-free **group templates**. This file is the **only** thing the\nplanner reads to pick one — you do **not** open the template's `index.html` to choose. One\ntemplate realizes **one group** of a frame (a frame may stack several groups; the brand spine\nunifies them).\n\n**How to pick.** For each group, read its frame's `pacing` + `mood` + the one-line music\nsituation from the skeleton, then scan **Reach for it when** + **Best span** below and take the\nclosest fit. If nothing fits, **free-compose** from\n[`motion-primitive-catalog.md`](motion-primitive-catalog.md) — that is a first-class choice, not a\nfailure.\n\n**Brand comes from `frame.md`, not the template.** Templates ship their own demo palette\n(`theme` / `palette` / color params). Fill those params from the project's `frame.md` (the\nchosen preset's colors + fonts) so every group reads as one piece — the template supplies the\n**motion + layout**, the preset supplies the **look**.\n\n**Params** are listed so you can fill content slots in the storyboard. Exact semantics +\ndefaults live in each template's `index.html` (`data-composition-variables`) — the frame-worker\nreads those at build time; you only name the values.\n\nPacing tag: every template below is **beat_cut** except `held-message-living-field`\n(**phrase_flow**). Never put a beat_cut template on a phrase_flow frame.\n\n## Group duration discipline\n\nTemplate choice is per **group**, not per frame. A frame longer than a template's **Best span**\nshould usually split into multiple groups at real audiomap anchors (`SURGE`, `DROP`, roll edge,\nhard_stop, phrase edge) instead of stretching one template across the whole frame.\n\n- **Best span** below is the active treatment span: the part where the template's system is doing\n  meaningful motion. A short readable hold at the end is fine; a long empty tail means pick another\n  group.\n- **Frames over ~6s** usually need 2+ groups. Beat-cut exceptions are rich programs with real\n  sub-phases (`poster-tile-mosaic`, sometimes `card-flyby`); phrase-flow exceptions use\n  `held-message-living-field`.\n- **Roll templates are one-roll tools.** If a frame has two rolls or a drop between rolls, split it\n  into two groups.\n- Do not extend by slowing every tween. Preserve the template's motion feel, then fill extra time\n  with a hold / palette change / new group.\n\n---\n\n### card-flyby\n\n- **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.\n- **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.\n- **Best span** — **4-6.5s** for 4-7 landings plus a short final hold; split at the next downbeat if it wants to run **>7s**.\n- **Params** — `theme`, `bgColor`, `cards`, `landings`, `yaw`\n\n### held-message-living-field · phrase_flow\n\n- **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes.\n- **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.\n- **Best span** — **6-16s**; this is the long-group exception. Under 4s feels underdeveloped; over ~20s needs a state change or another group.\n- **Params** — `markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration`\n\n### held-text-strobe-burst\n\n- **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.\n- **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/`.)\n- **Best span** — **1.2-3.5s**; strobe fatigue starts fast, so cap at ~4s and cut to a cleaner system.\n- **Params** — `markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration`\n\n### intro-kinetic-cascade\n\n- **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).\n- **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.\n- **Best span** — **3.5-7s** for 2-4 phrase beats; if the statement needs more time, make the next clause a new group.\n- **Params** — `theme`, `icon`, `phrases`, `climax`\n\n### logo-split-lockup-pulse\n\n- **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.\n- **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.\n- **Best span** — **2-4s**; at **>4.5s** it reads like a sting stretched too long. Follow with a separate held-lockup / next idea group.\n- **Params** — `bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4`\n\n### poster-tile-mosaic\n\n- **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.\n- **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.\n- **Best span** — **4-7s**, up to **8s** only when the program has clear sub-phases. If the music changes regime, split even if the tile system could continue.\n- **Params** — `bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program`\n\n### roll-flipbook-word-cycle\n\n- **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.\n- **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.\n- **Best span** — **1.2-3.8s**, one roll into one resolve. Two rolls, or a drop between rolls, means two groups.\n- **Params** — `bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar`\n\n### split-anchor-word-slot\n\n- **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.\n- **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.\n- **Best span** — **3-6s**; above ~6.5s the fixed anchor goes stale unless the right slot enters a new group/program.\n- **Params** — `bgColor`, `anchors`, `theme`, `showText`, `program`\n\n### typewriter-phrase-keyword-shuffle\n\n- **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.\n- **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).\n- **Best span** — **2.5-5s**; if the phrase cannot type and shuffle inside ~5s, reduce words or split the sentence across groups.\n- **Params** — `bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar`\n\nFile v1.0.24:references/templates/card-flyby/program.json\n\n{\n  \"_\": \"Default card-flyby program. `theme` picks a cohesive palette; `cards` is the deck (each card = a big centered title; `color` is OPTIONAL — omit it and the card takes the next color from the theme ramp); `landings` is one onset per card (accelerating cadence; omit to auto-derive). Pass these as the matching composition-variables; empty vars fall back to these values.\",\n  \"theme\": \"aurora\",\n  \"cards\": [\n    { \"title\": \"FADEGLOW\" },\n    { \"title\": \"SAUL BASS\" },\n    { \"title\": \"EULER\" },\n    { \"title\": \"HERMÈS\" },\n    { \"title\": \"DOSSIER\" },\n    { \"title\": \"BLACK HOLES\" }\n  ],\n  \"landings\": [0.4, 1.7, 2.7, 3.45, 4.0, 4.4]\n}\n\nFile v1.0.24:references/templates/intro-kinetic-cascade/program.json\n\n{\n  \"_\": \"Default intro-kinetic-cascade program (reversed from act0-intro-bell). `theme` = palette; `icon` = the ringing climax glyph (MATCH TO SCENE — bell|cursor|sparkle|bolt|play|heart|check|star, an emoji, a raw <svg>, or 'none'); `phrases` = the word-by-word cascade (each `times` = VO word onsets / audio onsets, omit to auto-spread); `climax` = the slide-in finale ({icon} marks the glyph). Pass these as the matching composition-variables; empty vars fall back to these values.\",\n  \"theme\": \"light\",\n  \"icon\": \"bell\",\n  \"phrases\": [\n    {\n      \"out\": 1.36,\n      \"lines\": [\n        { \"text\": \"If you've\", \"size\": 220, \"x\": 140 },\n        { \"text\": \"ever\", \"size\": 520, \"x\": 240, \"hero\": true, \"font\": \"serif\" },\n        { \"text\": \"used AI\", \"size\": 280, \"x\": 780 }\n      ],\n      \"times\": [0.14, 0.3, 0.55, 0.87, 1.06]\n    },\n    {\n      \"out\": 3.06,\n      \"lines\": [\n        { \"text\": \"for\", \"size\": 220, \"x\": 160 },\n        { \"text\": \"video\", \"size\": 500, \"x\": 220, \"hero\": true },\n        { \"text\": \"editing before,\", \"size\": 200, \"x\": 220 }\n      ],\n      \"times\": [1.36, 1.58, 1.92, 2.48]\n    },\n    {\n      \"out\": 3.79,\n      \"lines\": [\n        { \"text\": \"you\", \"size\": 240, \"x\": 200 },\n        { \"text\": \"know\", \"size\": 500, \"x\": 260, \"hero\": true, \"accent\": \"gradient\" },\n        { \"text\": \"that\", \"size\": 340, \"x\": 820 }\n      ],\n      \"times\": [3.06, 3.3, 3.54]\n    }\n  ],\n  \"climax\": {\n    \"text\": \"timing is {icon} tricky.\",\n    \"in\": 3.79,\n    \"iconAt\": 4.9,\n    \"hold\": 1.3,\n    \"size\": 150\n  }\n}\n\nFile v1.0.24:references/templates/poster-tile-mosaic/program.json\n\n[\n  {\n    \"op\": \"staggerInOut\",\n    \"theme\": 0,\n    \"enter\": [0.23, 0.37, 0.46, 0.6, 0.7, 0.84, 0.91, 1.07, 1.14, 1.28],\n    \"hold\": 1.324,\n    \"exit\": [1.37, 1.51, 1.62, 1.74, 1.83, 1.97, 2.07]\n  },\n  {\n    \"op\": \"holdRecolor\",\n    \"on\": 2.35,\n    \"onTheme\": 1,\n    \"recolor\": [2.42, 2.53, 2.81, 2.93],\n    \"themes\": [2, 3, 1, 2],\n    \"off\": 2.98\n  },\n  {\n    \"op\": \"snakeFillOverlay\",\n    \"baseTheme\": 0,\n    \"fill\": [3.04, 3.181, 3.44, 3.65, 3.79, 3.9],\n    \"swap\": 4.0,\n    \"swapTheme\": 4,\n    \"overlay\": [4.11, 4.34, 4.48, 4.81, 4.97, 5.25, 5.48]\n  }\n]\n\nArchive v1.0.23: 104 files, 218522 bytes\n\nFiles: references/frame-skeleton.md (6584b), references/montage.md (3269b), references/motion-primitive-catalog.md (9885b), references/motion-primitives/3d-card-flip/index.html (1278b), references/motion-primitives/3d-card-flip/scene.html (3393b), references/motion-primitives/assets/gsap.min.js (72927b), references/motion-primitives/bg-flow-field/index.html (1274b), references/motion-primitives/bg-flow-field/scene.html (11328b), references/motion-primitives/binary-decrypt/index.html (1310b), references/motion-primitives/binary-decrypt/scene.html (2711b), references/motion-primitives/blur-resolve/index.html (1272b), references/motion-primitives/blur-resolve/scene.html (1688b), references/motion-primitives/braam-punch/index.html (1276b), references/motion-primitives/braam-punch/scene.html (2728b), references/motion-primitives/chromatic-split/index.html (1284b), references/motion-primitives/chromatic-split/scene.html (3481b), references/motion-primitives/chrome-sweep/index.html (1278b), references/motion-primitives/chrome-sweep/scene.html (1824b), references/motion-primitives/counting-punch/index.html (1310b), references/motion-primitives/counting-punch/scene.html (3214b), references/motion-primitives/crash-zoom-in/index.html (1280b), references/motion-primitives/crash-zoom-in/scene.html (3386b), references/motion-primitives/datamosh-smear/index.html (1341b), references/motion-primitives/datamosh-smear/scene.html (3503b), references/motion-primitives/directional-fill/index.html (1284b), references/motion-primitives/directional-fill/scene.html (2773b), references/motion-primitives/dolly-zoom/index.html (1274b), references/motion-primitives/dolly-zoom/scene.html (3278b), references/motion-primitives/electric-arc/index.html (1306b), references/motion-primitives/electric-arc/scene.html (3263b), references/motion-primitives/flash-cut/index.html (1270b), references/motion-primitives/flash-cut/scene.html (3146b), references/motion-primitives/gooey-metaball/index.html (1310b), references/motion-primitives/gooey-metaball/scene.html (4918b), references/motion-primitives/hard-cut/index.html (1264b), references/motion-primitives/hard-cut/scene.html (2708b), references/motion-primitives/hypercut-whip/index.html (1280b), references/motion-primitives/hypercut-whip/scene.html (1229b), references/motion-primitives/iris-open/index.html (1272b), references/motion-primitives/iris-open/scene.html (2015b), references/motion-primitives/kinetic-letter-in/index.html (1288b), references/motion-primitives/kinetic-letter-in/scene.html (1764b), references/motion-primitives/liquid-morph/index.html (1278b), references/motion-primitives/liquid-morph/scene.html (2992b), references/motion-primitives/mask-reveal/index.html (1276b), references/motion-primitives/mask-reveal/scene.html (2086b), references/motion-primitives/mosaic-pack/index.html (1274b), references/motion-primitives/mosaic-pack/scene.html (2829b), references/motion-primitives/neon-flicker/index.html (1278b), references/motion-primitives/neon-flicker/scene.html (1979b), references/motion-primitives/outline-to-fill/index.html (1284b), references/motion-primitives/outline-to-fill/scene.html (2282b), references/motion-primitives/palette-flip/index.html (1276b), references/motion-primitives/palette-flip/scene.html (2824b), references/motion-primitives/particle-burst/index.html (1282b), references/motion-primitives/particle-burst/scene.html (3185b), references/motion-primitives/pixel-dissolve/index.html (1282b), references/motion-primitives/pixel-dissolve/scene.html (2645b), references/motion-primitives/radial-burst-lines/index.html (1290b), references/motion-primitives/radial-burst-lines/scene.html (3742b), references/motion-primitives/screen-shake/index.html (1278b), references/motion-primitives/screen-shake/scene.html (2197b), references/motion-primitives/slot-machine-reveal/index.html (1292b), references/motion-primitives/slot-machine-reveal/scene.html (2820b), references/motion-primitives/spotlight-sweep/index.html (1284b), references/motion-primitives/spotlight-sweep/scene.html (3009b), references/motion-primitives/staggered-exit/index.html (1276b), references/motion-primitives/staggered-exit/scene.html (2413b), references/motion-primitives/text-spectral-rays/index.html (1276b), references/motion-primitives/text-spectral-rays/scene.html (11504b), references/motion-primitives/text-spectral-rays/USAGE.md (2401b), references/motion-primitives/text-wave-distort/index.html (1316b), references/motion-primitives/text-wave-distort/scene.html (2999b), references/motion-primitives/tile-mosaic/index.html (1274b), references/motion-primitives/tile-mosaic/scene.html (3063b), references/motion-primitives/typewriter-reveal/index.html (1286b), references/motion-primitives/typewriter-reveal/scene.html (2634b), references/motion-primitives/word-grid-burst/index.html (1282b), references/motion-primitives/word-grid-burst/scene.html (2707b), references/planning.md (6194b)\n\nFile v1.0.23:SKILL.md\n\n---\nname: music-to-video\ndescription: \"Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.\"\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update music-to-video`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n# music-to-video — one music-grounded, beat-synced video workflow\n\nUse this skill to turn a **music track** into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is **no narration and no website capture**. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.\n\nYou are the **orchestrator**. Work in `videos/<project>/`. Run the steps in order and pass each **Gate** before moving on. Two steps need the user: **Step 3** (plan approval) and **Step 6** (render approval) — both are checkpoint gates per `../hyperframes/references/brief-contract.md` (read it before Step 0): in autonomous mode, post the Step 3 summary as a heads-up and proceed; Step 6 render approval is still asked, as the one kept question. Do every step yourself except **Step 4**, where you dispatch **one sub-agent per frame**. Keep design and motion rules out of this file — they live in `references/` and the `frame-worker` sub-agent.\n\n`SKILL_DIR` = this skill directory. `PROJECT_DIR` = `videos/<project-name>/`.\n\nWorkflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze → `audiomap.json`; Step 2 skeleton → `STORYBOARD.md` (frames, groups `TBD`); Step 3 plan → complete `STORYBOARD.md` + `frame.md`; Step 4 build → `compositions/frames/NN-*.html`; Step 5 assemble → `index.html`; Step 6 render → `renders/video.mp4`.\n\n## Two ideas that shape everything\n\n- **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).\n- **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.\n\n---\n\n## Step 0: Setup, BGM, and inputs\n\nGoal: Establish the music source, create the HyperFrames project, and note any user-supplied media.\n\n**The brief starts at the intent layer.** Opening rule, in order: **(1)** `BRIEF.md` exists → read it and ask nothing it answers — its `flow`/`storyboard` derive the mode (brief contract § 1). **(2)** No `BRIEF.md` but the project exists → resume from what's on disk; never re-interrogate. **(3)** A fresh creation request that arrived here directly → read `/hyperframes` and run its intent layer (`references/intent-interview.md`): it confirms this route's must-haves (the music source, destination → aspect — `../hyperframes/references/routes/music-to-video.md`) and announces what stays deferred — brand and genre are chosen at Step 3 by design. Write `BRIEF.md` immediately after init (never before — `init` refuses a non-empty directory) and record the preference-backed answers (`brief-format.md`). Edit requests skip all of this.\n\nThe **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user supplied audio — a music file, or a video to pull audio from — use it. Otherwise choose the mood from the request and generate a track through `/media-use` (`references/bgm.md`). Before the first authenticated provider action, run `npx hyperframes auth status` and relay its output verbatim. If signed out, apply one branch:\n\n- **Collaborative:** wait for sign-in or an explicit choice to continue offline with the local provider.\n- **Autonomous:** state the status and continue through the available local provider.\n\nIf no offline provider can satisfy the required music capability, surface the blocker. Never write keys into a per-repo `.env`. Auth ownership and offline fallbacks live in `/media-use` `references/setup-providers.md` § Providers. The resulting track lands at `assets/bgm.mp3`. Stage supplied images or videos so frames can use them on the beat grid; otherwise typography carries the video.\n\n**Lyric videos:** for lyrics synced to the vocals, get word/line timing by transcribing the track via `/media-use`, or ask the user for the lyrics text and place lines on the beat grid.\n\nInitialize only if `hyperframes.json` is missing. Name `<project>` from the brief in kebab-case, such as `midnight-drive-loop` — never a timestamp. `init` checks the installed skills against the latest on GitHub and updates the global set if any are out of date.\n\n```bash\nnpx hyperframes init \"videos/<project>\" --non-interactive --example=blank --skill=music-to-video\nmkdir -p \"$PROJECT_DIR/assets\" \"$PROJECT_DIR/renders\"\ncp \"<user-music>\" \"$PROJECT_DIR/assets/bgm.mp3\"   # extract from a video first if needed\n# only if the user gave you images/videos:\nnode <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes \"$PROJECT_DIR\" --into public\n```\n\nThe **brand** (font + palette) is chosen at Step 3, not here. Don't pick a genre or a track type up front — assets are just an optional ingredient, and the genre emerges from the per-frame choices.\n\n**Gate:** `hyperframes.json` + `assets/bgm.mp3` exist; aspect / length / fps and (if any) the asset inventory are noted.\n\n---\n\n## Step 1: Analyze the music\n\nGoal: Produce the one canonical timing analysis the whole video is built on.\n\n`analyze-beatgrid.py` is the **only** beat analyzer — never re-measure beats with another tool or by ear. It reads the track once and writes `audiomap.json`: energy phases (level / density / feel), onsets + `onset_rate`, rolls, silences, `hard_stops`, `key_moments`, phrases, tempo / grid, and `audio.duration_sec`. It's deterministic — the same file always gives the same map. Most fields are reliable on any music; `bpm` and `beats_sec` are reliable only when the music is genuinely rhythmic, and judging that is the call you make at Step 2.\n\nPrerequisites: Python 3 with `librosa`, `numpy`, and `soundfile` available. If import fails, install them into the active Python environment before running the analyzer:\n\n```bash\npython3 -m pip install librosa numpy soundfile\n```\n\n```bash\npython3 <SKILL_DIR>/scripts/analyze-beatgrid.py \"$PROJECT_DIR/assets/bgm.mp3\" \\\n  -o \"$PROJECT_DIR/audiomap.json\" --print\n```\n\n**Gate:** `audiomap.json` exists; `audio.duration_sec` is known.\n\n---\n\n## Step 2: Frame skeleton (structure only)\n\nGoal: Read the music and lay out the frames — the skeleton of `STORYBOARD.md`.\n\nRead [`references/frame-skeleton.md`](references/frame-skeleton.md). Turn `audiomap.json` into the **skeleton** of `STORYBOARD.md` yourself — there is no intermediate JSON. Cut the track into **frames** at real musical changes (`hard_stops`, SURGE / DROP `key_moments`, the edges of a roll, a stretch with no onsets, a big energy jump), snapping every boundary to an audiomap anchor. For each frame set `span_sec`, `pacing` (the verdict from Step 1's trust call — `beat_cut` when the grid is real, `phrase_flow` when it's a metronome imposed on calm music), `mood`, and a one-line `feel` (the plain music situation Step 3 matches a template against). Only classify and lay out here: leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank — no templates, copy, color, or fonts. Expect ~1–6 frames.\n\n**Gate:** frames tile the track (first at 0, last at `duration_s`); each carries `span_sec` + `pacing` + `mood` + `feel`; every `### Groups` is `TBD`; no content anywhere.\n\n---\n\n## Step 3: Fill the plan (user-gated)\n\nGoal: Turn the skeleton into an approved, complete `STORYBOARD.md`.\n\nRead [`references/planning.md`](references/planning.md), [`storyboard-format.md`](references/storyboard-format.md), [`template-catalog.md`](references/template-catalog.md), [`motion-primitive-catalog.md`](references/motion-primitive-catalog.md), and [`montage.md`](references/montage.md) (only if the user supplied assets). Editing the same file in place, do two things:\n\n1. **Pick the brand.** Choose one preset from `../hyperframes-creative/frame-presets/` using the table in `../hyperframes-creative/references/design-spec.md` (match the track's mood; **only its fonts and colors matter** — templates own composition). Copy it into `frame.md` **unmodified** and fill the frontmatter `style` (font + a ≤4–6 swatch palette) from it.\n2. **Fill every frame.** Decide its groups and give each a treatment: a matched template from the catalog (with bound params and real audiomap anchors), a free-compose from the primitive catalog, or an asset treatment that **obeys `pacing`**. **Before you free-compose a named look, search the live catalog for it**: for every look, effect, treatment or transition the user asked for — \"CRT scanlines\", \"glitch\", \"film grain\", \"shimmer sweep\" — run `npx hyperframes catalog --query \"<the look, in plain English>\" --json` and read the top results. `template-catalog.md` and `motion-primitive-catalog.md` list only this skill's own local materials; the search ranks the whole hosted registry (~400 blocks and components) and needs **nothing installed** — no project, no prior `add`, no account. Free-compose a look only after a search for it came back with nothing that fits. Write the copy. You own WHAT (template / primitives + content + anchors); the frame-worker owns HOW — **never write millisecond tweens into the storyboard**.\n\n```bash\nnode <SKILL_DIR>/scripts/validate-plan.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --audiomap \"$PROJECT_DIR/audiomap.json\" --templates <SKILL_DIR>/references/templates\n```\n\nFix every `✗` (hard errors: duration mismatch, frames not tiling the track, a missing `src`); warnings are best-effort. \n\nArchive v1.0.22: 104 files, 218592 bytes\n\nFiles: references/frame-skeleton.md (6584b), references/montage.md (3269b), references/motion-primitive-catalog.md (9885b), references/motion-primitives/3d-card-flip/index.html (1278b), references/motion-primitives/3d-card-flip/scene.html (3393b), references/motion-primitives/assets/gsap.min.js (72927b), references/motion-primitives/bg-flow-field/index.html (1274b), references/motion-primitives/bg-flow-field/scene.html (11328b), references/motion-primitives/binary-decrypt/index.html (1310b), references/motion-primitives/binary-decrypt/scene.html (2711b), references/motion-primitives/blur-resolve/index.html (1272b), references/motion-primitives/blur-resolve/scene.html (1688b), references/motion-primitives/braam-punch/index.html (1276b), references/motion-primitives/braam-punch/scene.html (2728b), references/motion-primitives/chromatic-split/index.html (1284b), references/motion-primitives/chromatic-split/scene.html (3481b), references/motion-primitives/chrome-sweep/index.html (1278b), references/motion-primitives/chrome-sweep/scene.html (1824b), references/motion-primitives/counting-punch/index.html (1310b), references/motion-primitives/counting-punch/scene.html (3214b), references/motion-primitives/crash-zoom-in/index.html (1280b), references/motion-primitives/crash-zoom-in/scene.html (3386b), referenc...","readmeExcerpt":"Skill: music-to-video Owner: heygen-com Summary: Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). ","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"npx hyperframes init \"videos/<project>\" --non-interactive --example=blank --skill=music-to-video\nmkdir -p \"$PROJECT_DIR/assets\" \"$PROJECT_DIR/renders\"\ncp \"<user-music>\" \"$PROJECT_DIR/assets/bgm.mp3\"   # extract from a video first if needed\n# only if the user gave you images/videos:\nnode <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes \"$PROJECT_DIR\" --into public"},{"language":"bash","snippet":"python3 -m pip install librosa numpy soundfile"},{"language":"bash","snippet":"python3 <SKILL_DIR>/scripts/analyze-beatgrid.py \"$PROJECT_DIR/assets/bgm.mp3\" \\\n  -o \"$PROJECT_DIR/audiomap.json\" --print"},{"language":"bash","snippet":"node <SKILL_DIR>/scripts/validate-plan.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --audiomap \"$PROJECT_DIR/audiomap.json\" --templates <SKILL_DIR>/references/templates"},{"language":"text","snippet":"PROJECT_DIR: <abs path>\nframe_id: <NN-frame_id>              # = the frame file stem, e.g. 02-f2; the composition id\nYour block: the `## Frame N — <frame_id>` block in PROJECT_DIR/STORYBOARD.md\naudiomap: PROJECT_DIR/audiomap.json\nframe.md: PROJECT_DIR/frame.md\nMaterials: for each group, <SKILL_DIR>/references/templates/<id>/index.html (templates) and\n           <SKILL_DIR>/references/motion-primitives/<id>/ (free); staged assets/ (asset groups)\nContracts: ../hyperframes-core/references/sub-compositions.md + determinism-rules.md\nCanvas: <w>×<h>   Pacing: <beat_cut|phrase_flow>\nWrite to: PROJECT_DIR/compositions/frames/<frame_id>.html"},{"language":"bash","snippet":"node <SKILL_DIR>/scripts/assemble-index.mjs --storyboard \"$PROJECT_DIR/STORYBOARD.md\" \\\n  --hyperframes \"$PROJECT_DIR\" --audiomap \"$PROJECT_DIR/audiomap.json\""}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: music-to-video\ndescription: \"Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.\"\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n> **First, keep this skill fresh — confirm with the user before running:** `npx hyperframes skills update music-to-video`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them.\n\n# music-to-video — one music-grounded, beat-synced video workflow\n\nUse this skill to turn a **music track** into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is **no narration and no website capture**. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.\n\nYou are the **orchestrator**. Work in `videos/<project>/`. Run the steps in order and pass each **Gate** before moving on. Two steps need the user: **Step 3** (plan approval) and **Step 6** (render approval) — both are checkpoint gates per `../hyperframes/references/brief-contract.md` (read it before Step 0): in autonomous mode, post the Step 3 summary as a heads-up and proceed; Step 6 render approval is still asked, as the one kept question. Do every step yourself except **Step 4**, where you dispatch **one sub-agent per frame**. Keep design and motion rules out of this file — they live in `references/` and the `frame-worker` sub-agent.\n\n`SKILL_DIR` = this skill directory. `PROJECT_DIR` = `videos/<project-name>/`.\n\nWorkflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze → `audiomap.json`; Step 2 skeleton → `STORYBOARD.md` (frames, groups `TBD`); Step 3 plan → complete `STORYBOARD.md` + `frame.md`; Step 4 build → `compositions/frames/NN-*.html`; Step 5 assemble → `index.html`; Step 6 render → `renders/video.mp4`.\n\n## Two ideas that shape everything\n\n- **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"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"music-to-video\",\n  \"version\": \"1.0.25\",\n  \"publishedAt\": 1791358267609\n}"},{"path":"references/frame-skeleton.md","content":"# Frame skeleton (Step 2) — read the music, lay out the frames\n\nAt Step 2 **you (the orchestrator)** read `audiomap.json` and write the **skeleton** of\n`STORYBOARD.md` directly: cut the track into **frames** (one frame = one composition file =\none scene), and for each frame set its **span**, its **pacing** (does this stretch want hard\nbeat-cuts, or calm phrase/energy flow?), its **mood**, and a one-line **feel** note.\n\nYou **classify and lay out the spine only.** You do **not** pick templates, write copy, choose\ncolors/fonts, or decide a frame's groups — those are Step 3 (the plan fills each frame in\nplace). Leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank.\n\nThere is **no intermediate JSON** — the skeleton _is_ the start of `STORYBOARD.md`. Step 3\nedits the same file.\n\n## The trust boundary (read this first)\n\n`audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are\nreliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:\n\n| Field                                                                                                                                                                                               | Trust                                                                                                                                                                                                                        |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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                                                                                                                                                                                             |\n| `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. |\n\n- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.\n- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.\n\n## How to lay out f"},{"path":"references/montage.md","content":"# Asset treatments — weaving user media onto the beat spine\n\nWhen the user supplies images/videos, a group can be an **asset treatment** instead of a\ntypographic template/free-compose. Assets are an **additive ingredient on the same beat\nspine** — never a separate pipeline. Typography/templates stay the floor: if no asset fits a\ngroup, fall back to a template/free group (a complete video needs zero assets).\n\nThe planner (Step 3) picks the treatment and the clips + anchors (WHAT); the frame-worker\nrealizes it inside the frame file (HOW). **Obey the frame's `pacing`.**\n\n## The three treatments\n\n### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)\n\nThe asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor\n(the frame's beats/onsets from the audiomap). Each clip is a `class=\"clip\"` element\n(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with\n`data-start`/`data-duration`/`data-track-index` per the core clip contract. (Muted on purpose: the music track drives the sound.) Between clips,\ncrossfade the outgoing content to `opacity:0` ending **at** the next anchor.\nCut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.\n\n### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)\n\nFor calm frames: one clip held over the span with a slow scale/translate push (e.g. scale\n1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by\nbeats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment\nwhen the beat grid is unreliable (calm music).\n\n### `bg_under_text` — clip dimmed behind a template/free group\n\nA full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template\nor free-compose typographic treatment. The text rides on the same anchors; the clip is the\nbed. Use when the user wants their footage present but the message must stay readable.\n\n## Rules\n\n- **`pacing` decides the treatment**: `beat_cut` only on a `beat_cut` frame; on a\n  `phrase_flow` frame use `ken_burns` or a slow crossfade — **never** per-onset hard cuts on\n  the (unreliable) calm grid.\n- **Clips are muted; the root owns audio.** Mount each `<video class=\"clip\">` **muted**, as a\n  direct child of the frame root (never nested in another timed element, or the renderer\n  freezes it). The BGM is the only audio in v1.\n- **Crossfades animate `opacity`/`autoAlpha`**, never `visibility`/`display` on a `.clip`\n  (the framework owns clip visibility — that trips `gsap_animates_clip_element`).\n- **Backgrounds dim ~30–50%** so any foreground text stays legible.\n- Anchors are **track seconds from `audiomap.json`**; the worker subtracts the frame start\n  to get frame-local time.\n- Local staged assets only (`assets/` via `stage-assets.mjs`); never remote URLs.\n\n## Deferred hook (not v1)\n\nA clip that should play **its own sound** (interview cut, lyric clip) needs a sibling\n`<audio>` mounted at the **root** by the asse"},{"path":"references/motion-primitive-catalog.md","content":"# Motion-primitive catalog — the free-compose menu\n\nThe atomic layer: one anchor → one micro-move. When no template fits a group, free-compose by\nnaming primitives from here. Scan **anchor** + **best span** + **what it does**, then pick the\nsmallest set that carries the group.\n\n## Timing & latency (applies to every primitive)\n\n- **Hard hits are 0ms.** Cuts, palette flips, content swaps, freezes are `tl.set(...)` with no duration — the percussion _is_ the motion. Easing a hit kills it.\n- **Lead the anchor.** A move that must _land_ on a beat (a wipe covering the frame, a count-up locking, two blocks colliding) starts **~40–190ms early** so it completes ON the anchor. Reactive entrances (something appearing _because_ of the hit) fire 0–45ms after.\n- **Eased entrances: 300–500ms** (scale punch, slides, camera pushes). **Macro builds: 800–2000ms** spanning a whole roll / silence.\n- **Per-bar caps:** one accumulating element per hit (not a burst); a camera move at most once per phrase, never per beat; a dense flip/strobe system runs ≤2–3s.\n- **Tension-builds lock.** A count-up / sequential build / morph must _resolve on_ a downbeat or hard_stop, never trail off mid-bar.\n- **Best span means active motion.** The catalog's span guidance is not a license to stretch one primitive over a whole frame. If a free-composed group runs longer than the listed span, add a hold / bed / next primitive, or split the frame into another group at the next musical anchor.\n\n## Catalog\n\n| id                    | anchor                           | best span         | what it does                                                              |\n| --------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------- |\n| `hypercut-whip`       | beat / hard_stop                 | 0.18-0.45s        | fast whip-pan hard cut between frames                                     |\n| `kinetic-letter-in`   | downbeat / phrase                | 0.4-1.2s          | per-letter kinetic entrance                                               |\n| `braam-punch`         | drop / surge                     | 0.2-0.9s active   | big impact: scale + weight slam                                           |\n| `chromatic-split`     | snare / glitch / surge           | 0.1-0.6s          | RGB channel split / glitch on a word                                      |\n| `mask-reveal`         | section_start / downbeat         | 0.5-1.2s          | clip-path mask wipe reveal                                                |\n| `screen-shake`        | drop / crash / kick              | 0.1-0.5s          | camera / screen shake jitter                                              |\n| `binary-decrypt`      | roll / build                     | 0.8-2.5s          | scramble→decode text (binary → word)                                      |\n| `dolly-zoom`          | phrase / build                   | 1.2-2.5s          | vertigo dolly-zoom (sc"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2006,"uniquenessScore":42,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T17:28:08.435Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:41:10.170Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}