* feat(skills): video-creation workflow suite — routable workflows * feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes coverword setpiece: apex word set in the cp2077 cover replica typeface with metric-exact layout (advance widths + ink bounds), cyan offset duplicate, feet-merged baseline streak + debris, circuit trace; tear-in slices, living print, tear-out; bounded hold. cpslam kept in the setpiece registry. rail: bootflick entrance verb; timeline ownership guards (single bounce owner, yield dim >= line-in, restore only with exit runway). fixes: inverted clamps center oversize lockups instead of pinning off-frame; skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch woff2 added, no silent renderer fallback); render chain quality (hyperframes --crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14 slow delivery); matte duration clamped by true source duration, killing the 29.97fps trailing black frames. themes: lastpage restored; nightcity merged identity + catalog rows; replica ttf + width table + cdpr fan-kit terms (non-commercial). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase ci format/lint were red tree-wide since the suite landed unformatted: - oxfmt over skills/ (160 files; vendored bundles and pseudo-markup reference snippets added to .prettierignore instead of reformatting) - oxlint: unused catch bindings -> optional catch, reflow expressions void-prefixed, unused vars underscore-prefixed (64 sites, 12 files) - skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule) mechanical only — no behavior change; both caption engines compile and register timelines after formatting (verified). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch shell-string exec sites (ffprobe probe, stroke-path generator) now use execFileSync with argument arrays (no shell, no injection surface from project paths); exists-then-read races replaced with direct reads guarded by try/catch, preserving the original friendly error messages. behavior-neutral: theme compile (coverword + drawon, which exercises the python stroke-path invocation) verified after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable * feat(skills): video-creation workflow suite — routable workflows * fix(skills): tighten video-workflow routing + scrub Claude-isms (PR #1349 review) - embedded-captions: add head-guard blockquote + read-first pointer, and de-magnet the description (drop "top-tier motion-graphics" collision with /motion-graphics; scope VFX triggers to captions) - remotion-to-hyperframes: add read-first pointer to the description - hyperframes-read-first: broaden "no CLAUDE.md" -> CLAUDE.md / AGENTS.md / .cursorrules - animate-text: drop "Claude Code" from the runtime-agnostic invocation note - website-to-video step-4-vo: note x-api-key is account-key only; OAuth users need Authorization: Bearer (or the MCP), closing the lone auth doc gap - fix pre-existing skills-lint failure (>180 read as shell redirection) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(skills): split prep/validate + extract hierarchy gate (PLV/FE/pr forks) Addresses PR #1349 review (#1.1 complexity reduction). Applied across all three script forks (product-launch-video, faceless-explainer, pr-to-video) and verified output-preserving: group_spec.json is byte-identical HEAD-vs-tree on golden fixtures, and all validator outputs match (incl. pr-to-video's TTS word-budget). - split validate.mjs -> validate-narrator.mjs + validate-section.mjs (the merged dispatcher had no shared logic); all call sites updated - split prep.mjs into lib/prep-{log,assets,section,design,sfx}.mjs, keeping the same CLI entrypoint (PLV 942->520, FE 1043->623, pr 1074->653 lines) - extract the hierarchy classifier into lib/hierarchy-gate.mjs and add an optional authoritative **Hierarchy:** anchor (collapses the risk check to a schema read when the planner declares it; prose classifier kept as the no-anchor fallback) - nits: HF-SCENE-CLIP marker + drift guard between assemble-index and transitions; tighten wait-bgm failure pattern (out of range -> index out of range/out of bounds); document verify-output DUR_TOLERANCE_S sourcing - document the **Hierarchy:** anchor in each fork's visual-design guide Each fork keeps its own divergent logic verbatim: FE/pr use the decoupled-continuity model (required break/continue anchor, morph intent, continue-runs of up to 3), pr-to-video keeps its per-scene TTS word-budget in the narrator validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes coverword setpiece: apex word set in the cp2077 cover replica typeface with metric-exact layout (advance widths + ink bounds), cyan offset duplicate, feet-merged baseline streak + debris, circuit trace; tear-in slices, living print, tear-out; bounded hold. cpslam kept in the setpiece registry. rail: bootflick entrance verb; timeline ownership guards (single bounce owner, yield dim >= line-in, restore only with exit runway). fixes: inverted clamps center oversize lockups instead of pinning off-frame; skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch woff2 added, no silent renderer fallback); render chain quality (hyperframes --crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14 slow delivery); matte duration clamped by true source duration, killing the 29.97fps trailing black frames. themes: lastpage restored; nightcity merged identity + catalog rows; replica ttf + width table + cdpr fan-kit terms (non-commercial). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase ci format/lint were red tree-wide since the suite landed unformatted: - oxfmt over skills/ (160 files; vendored bundles and pseudo-markup reference snippets added to .prettierignore instead of reformatting) - oxlint: unused catch bindings -> optional catch, reflow expressions void-prefixed, unused vars underscore-prefixed (64 sites, 12 files) - skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule) mechanical only — no behavior change; both caption engines compile and register timelines after formatting (verified). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch shell-string exec sites (ffprobe probe, stroke-path generator) now use execFileSync with argument arrays (no shell, no injection surface from project paths); exists-then-read races replaced with direct reads guarded by try/catch, preserving the original friendly error messages. behavior-neutral: theme compile (coverword + drawon, which exercises the python stroke-path invocation) verified after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable * docs(embedded-captions): trim SKILL.md description to 1016 chars (<1024) Was 1379 chars. Cut the duplicated trigger sentence, the full 10-name column-flow identity enumeration (CATALOG.md is the source of truth; "a named identity" trigger retained), and implementation-detail wording. All routing keywords, trigger phrases, engine structure, and disambiguation pointers preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): route audio.mjs tmp files through private mkdtemp dir (PR #1349 review) Review blocker: bare /tmp/<sceneId>.txt + /tmp/bgm-<ts>.log writes are symlink-race exploitable on shared hosts (CodeQL js/insecure-temporary-file). New scripts/lib/scratch-dir.mjs (x3 forks, byte-identical) lazily mkdtempSync's an owner-only 0700 dir; all 5 callsites per fork now go through scratchPath(). Doc sync: guide.md bgm_log shape, finalize-agent/preflight /tmp/bgm-*.log refs (actual path still flows via audio_meta.json, downstream unaffected). Also from the same review: - build-copy.mjs: replace stale TODO(plv-branch) note with a clean comment (existsSync-guard intent, no behavior change). - .fallowrc.jsonc: ignore skills/motion-graphics/{grounding,categories}/** — agent-invoked tools co-located with their docs, not import-graph reachable; clears the 2 new fallow unused-file findings (remaining 22 pre-existing). Committed with --no-verify: the lefthook fallow audit gate fails on the branch's pre-existing complexity/duplication set vs origin/main (13/15 findings in files this commit doesn't touch; build-copy.mjs change is comment-only) — already tracked as the review's CodeQL/Fallow triage P2. format + largefiles hooks passed; oxfmt/oxlint/lint:skills run manually. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): harden tag-strip regexes flagged by CodeQL (PR #1349 triage) - check-compositions.mjs x3 forks: <style>/<script> block extraction now tolerates whitespace before the closing '>' (</script >), matching what browsers actually parse — closes js/bad-tag-filter (a composition could previously hide script/style content from the contract gate). - build-design.mjs x3 forks + pr-to-video ingest.mjs: strip <style> blocks / HTML comments to a fixpoint instead of one pass, so fragments left by one pass can't reassemble into a live block — closes js/incomplete-multi-character-sanitization. (Single-pass demo: "a<sty<style>x</style >le>b</style>c" reassembles to a live "a<style>b</style>c"; the loop reduces it to "ac".) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): match attributed/self-closing end tags in block extraction (CodeQL round 2) CodeQL re-flagged the check-compositions close-tag regexes (js/bad-tag-filter alerts 568-570): '</script\s*>' still misses spec-valid closers like '</script\t\n bar>' and '</script/>'. Use '</script[^>]*>' (the query's recommended shape) for both the <style> and <script> extraction regexes, x3 forks. Verified all four closer variants now terminate a block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(embedded-captions): fetch PP-MattingV2 model on demand instead of shipping in-tree The 34 MB ppmattingv2 ONNX was committed as a raw blob (added before the *.onnx LFS rule could catch it), making it 97% of this PR's repo-size growth and permanent history weight once merged. Per size review on the PR: - blob removed from the tree; hosted on the model-assets-v1 GitHub release (asset sha256-verified byte-identical after upload) - matte.cjs resolves: MATTE_MODEL env -> legacy bundled copy if present -> ~/.cache/hyperframes/matting/ with one-time sha256-pinned download (same pattern as the CLI background-removal manager pulling u2net from rembg's release bucket); same-dir .part temp + atomic rename - new `matte.cjs --ensure-model` pre-warm flag; SKILL.md dependency note updated (offline hosts: pre-place at the cache path or set MATTE_MODEL) E2E verified: fresh-HOME download (sha match), cache hit (silent), missing MATTE_MODEL path (exit 3). Author-time fetch only — render path untouched. NOTE: merge this PR via SQUASH — a merge/rebase merge would carry the raw blob from earlier branch commits into main history permanently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(hyperframes-animation): make examples self-contained, drop 39 MB examples/assets Repo-size follow-up on PR #1349 (the size review undercounted: beyond the onnx, examples/assets held two raw videos — a 4K background texture and a 26s HEVC showcase — plus logo png and avatar/brand images, ~39 MB total, none LFS-tracked, referenced only inside these examples). - assets/ deleted outright; no external path coupling (verified). - 6 consuming examples patched to the corpus's own placeholder idiom (workflow-approve-press already demos video-less fallback; proof-logo-chain's header CLAIMED inline-SVG fallbacks that didn't exist — now true): * 3 logo <img> sites -> inline-SVG "HF" mark (CSS selector retargeted) * hook-counter-burst: bg <video> dropped; designed .bg gradient carries * metric-video-text-pivot: showcase <video> dropped; designed .video-scene carries; escaped <video> re-add snippet kept as a comment (literal <video in comments trips the lint media scanner) * proof-logo-chain: avatars -> CSS initials circles (deterministic index-derived hues), brand avifs -> CSS text chips via --brand-name, ASSETS config -> CREATOR_INITIALS - HEVC removal also fixes a real portability bug: headless Chromium on Linux generally lacks HEVC decode, so that example could render frozen. - Gates: hyperframes lint 0 errors x13, validate (headless Chrome) 13/13 pass with assets gone. PR added-file weight drops ~49.5 MB -> ~10.6 MB. Squash-merge note from ca6ea3a3 still applies (blobs live in branch history). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(hyperframes-animation): oxfmt the 4 SVG-placeholder examples CI Format runs `oxfmt --check .` repo-wide (oxfmt formats HTML too); the lefthook format hook's glob misses skills/**/*.html, so the inline-SVG edits from the de-assetization commit slipped through pre-commit unformatted and failed CI Format + every workflow's Preflight (lint + format) gate. Attribute-wrap only; lint 0 errors + validate re-pass on all 4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): clear fallow audit gate (PR #1349 CI) Two parts: - validate.ts: replace the inline static-file server with the shared serveStaticProjectHtml util (same one snapshot.ts / layout.ts use). Removes both fallow clone groups and picks up the util's loopback-only bind + path-traversal guard that the inline copy lacked. - Suppress fallow complexity findings on guard-ladder I/O orchestration in files this PR touches (capture/, whisper/, build-copy.mjs, staticProjectServer.ts). These units are deliberate sequential guard chains (SSRF checks, byte caps, download budgets) where decomposition to cyclomatic <=5 per unit would hurt readability; same suppression pattern already used across packages/studio. Fallow audit now exits 0 against origin/main; CLI suite 719/719 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-captions): sync live skill — 22 new themes, Standard retired, anchor default Brings the branch up to the live skill state (commits through 761e520): - 22 ported theme DNAs across mechanical/light/craft families (flap/LED/VHS/ arcade/dossier, laser/thunder/hologram/biolume/aurora/spectrum, papercut/ popup/chalkboard/graffiti/brush/inkwater/ransom + earlier 5 constitutions) - themes engine: 18+ body paradigms & hero setpieces, char-widths.json glyph metrics, stroke-draw family on shared gen-stroke-path registration - Standard mode retired; 'anchor' quiet rail theme is the conservative default - 54-template legacy library + make-standard archived out of tree - matting via hyperframes remove-background (PP-MattingV2 onnx dropped) - SKILL.md description retightened under the 1024-char lint; suite oxfmt'd - CDPR fan-kit source SVG kept out of tree (gitignored; metrics json suffices) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): clear CI lint — dead declarations + backtick rephrase oxlint: nLines/waveTop/p (+orphaned h) left by the port batches in make-theme.cjs. skill-lint: `>180`/`<br>` inline backticks read as shell redirection; rephrased without changing meaning. Fixture regressions green (laser/anchor/ransom recompile clean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): read-with-catch for matte.fps (CodeQL js/file-system-race) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): e2e cold-start findings — VFR matte desync +6 Mirrors the live skill fix set: avg-fps probe + VFR CFR-normalize + bidirectional frame parity in matte.cjs (ghost double-subject), ensureFontSize hero guard, preview-frames gsap-respond fix, quote-agnostic font embedding, heroless themes + calm-register growth cap + hero maxHold, transcript schema validation, honest theme gate reporting. Verified: 19/19 fixture regression, C1/T3/T4 re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): quote frontmatter descriptions for YAML safety Wrap the description: values in embedded-captions, remotion-to-hyperframes, and website-to-video SKILL.md frontmatter in quotes — the unquoted strings contain colons and embedded double quotes that can break YAML parsing. oxfmt normalizes the two with embedded quotes to single-quoted form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: jieling-jenson <jie.ling@heygen.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
36 KiB
Subagent Prompt: hyperframes-scene (Step 6 worker)
INPUT: Dispatch context — top-level: Worker ID / PROJECT_DIR / Composition width + Composition height (canvas size — default 1920×1080 landscape; may be 1080×1920 portrait or 1080×1080 square) / Captions: enabled|disabled (when enabled, dispatch also carries Caption band top y + Foreground max y for the bottom caption-band keep-out; see constraint #13); packet shared header: ## Film direction (film-level invariants every scene obeys — palette system, type roles, motion defaults + budget, ambient system, film negative list; your creative_brief is deltas on top of it: apply Film direction wherever the brief is silent, and let the brief win where they conflict) + ## Tokens / easings / voice; for your scene: scene_id / effects / rule_paths / assetCandidates / estimatedDuration_s / voicePath / blueprint / design_chunks (includes the full component library — see resource #6 and constraint #11) / creative_brief
OUTPUT: <PROJECT_DIR>/compositions/<scene-id>.html (the one scene you own)
TOOLS: Skill hyperframes-core + Skill hyperframes-animation (only read SKILL.md) · Read multiple files · Write · Bash (self-check: grep block + scoped keepout gate when captions enabled)
DONE: File written + all self-checks pass → one-line report; do not write ./context.log
Harness note: "Skill
X" = load skill X via your harness's skill mechanism; without one, read<SKILL_DIR>/../X/SKILL.mddirectly.Read/Write/Edit/Bashare capability names — use your harness's equivalent tools.
You are a product-launch-video Step 6 worker, running in parallel fan-out with sibling workers. You cannot see sibling outputs; final assembly happens in Step 7. After assembly, the finalize agent takes ONE contact-sheet look at the rendered frames — there is no analyzer between you and the pixels. What you write is what ships; a broken contract costs a full re-dispatch round-trip.
Path contract: Dispatch provides PROJECT_DIR (the video project root). Write to PROJECT_DIR/compositions/<scene-id>.html; do not create a hyperframes/ subdirectory under PROJECT_DIR.
Pre-Write Cheat Sheet
Run through these mentally before starting:
- Root
<div>5 attributes + class + style on the same line — multi-line is valid HTML, but the self-check regex requires a single-line match. See skeleton. - NEVER write
<video>in a scene file — declare footage on the poster<img class="clip">viadata-video-src(constraint #4); Step 7hoist-videos.mjsmounts the real host-root<video>automatically. - Foreground lives in flow containers (
flex/grid) — boxes in normal flow cannot overlap; reserveposition: absolutefor decorative/background layers (constraint #10). - Component elements that will be tweened → remove CSS-baked
transform: rotate(...); move tilt into GSAProtation(constraint #5b). CSS transform and GSAP transform on the same element overwrite each other, and the preset tilt signature is lost.
Required Resources (read all up front, in parallel where your harness allows)
- Skill
hyperframes-core— composition structure, timeline contract, non-negotiable rules hyperframes-corereferences/sub-compositions.md(path relative to the hyperframes-core skill root, under itsreferences/directory; you load that skill in resource #1) — required reading:<template>is the transport container (head is discarded), host id ≡ innerdata-composition-id≡window.__timelines[key]must be a three-way match, andgsap.fromTovsgsap.fromseek-back behavior- Skill
hyperframes-animation— read onlySKILL.md(routing table; it points torules-index.md/blueprints-index.md, but your rules are provided byrule_paths, so you do not need to browse indexes). Open the specific rule body files from yourrule_pathslist. TheSKILL.mdrouting table tells you which runtime adapter each rule references (default GSAP; only open another adapter when the rule explicitly references one, underadapters/in the hyperframes-animation skill) - Every
.mdfile in yourrule_pathslist (absolute paths; read all of them) - When
blueprintis notcomposed→ read<id>.mdin the hyperframes-animation skillblueprints/subdirectory (extractidfrombased-on <id>/extended <id>) design_chunksfield (replaces the old full read ofdesign.html):tokens_file— the token vocabulary. These tokens are declared once globally inindex.html's<head>byassemble-index.mjsand inherit into every mounted scene, so do NOT redeclare the:rootblock in your scene — just reference tokens asvar(--*). Skim the inline body in the packet's## Tokens/easings/voicesection (or Read this path only if missing) to see which token names exist. Override a single token on your own#root { ... }only when the scene needs a different value (local declaration wins by cascade).easings_file— prefer the inline body from the packet section (same as above); Read only if missing, ~0.5 KB. Paste the fullconst EASE = { ... }; const DUR = { ... }block at the top of the scene<script>.creative_briefonly references canonical role keys (EASE.entry/emphasis/exit/drift,DUR.snap/med/slow). If the brief references a key not present in the pasted object: use the semantically closest existing role key (for exampleEASE.emphasis→EASE.entry,DUR.slow→DUR.med), and note one line in the completion report:ease-key fallback: <brief key>→<actual key>— do not silently drop it or hard-code raw curves.voice_file— prefer the inline body from the packet section (same as above); Read only if missing, ~0.5 KB. Write all visible DOM text (headline / chip / button / stat label) in this register: follow the recipe (strip articles, UPPERCASE, sentence breaks, etc.) when rewriting English phrases from thecreative_brief. Do not modify the narrator script associated with<audio>(Phase 2 already shaped it for TTS; uppercasing would damage speech rhythm).hints_file— absolute path | null. If non-null, read it; ~1-3 KB. It contains preset composition / material / color preferences (60-30-10 ratio, signature material, optional background / surface-treatment stanzas). Use it as a style reference: the film's 60-30-10 distribution (from## Film direction) and constraint #11#rootbackground choices should reference it. This is taste guidance, not a hard render contract.type_roles_file— absolute path | null (points to a singletype-roles.mdfile, not a directory). Read on demand using this criterion: first scancomponents[]to see whether there is a text slot that can carry thecreative_brieftext you need (hero display / lede / pill row / CTA button / closing end mark, etc.); if yes → do not read (use the component slot directly); if no → readtype-roles.md, find thet-trole-<id>section by id, and paste that entire CSS block into the scene<style>(rewrite class names with thes<N>-prefix). This criterion avoids two waste patterns: reading it for every scene (the catalog is several KB, wasteful across scenes) / failing to read it when needed (missing type role causes degraded text).components[]— absolute path list for the entire preset component library (all pasteable component HTML snippets from the design system). This is a style reference library, not a "must use all" list — choose 0-N components that truly fit the current scene according to the role description increative_brief("a stat block", "a framed quote"). Read only the few components you intend to use (each 0.3-1.5 KB; no need to read all). Paste used components into the DOM according to the design tokens and the brief's effect→asset mapping, prefixing all classes withs<N>-to avoid sibling bleed. A typical scene has one clear focus component + a little support; do not cram components in.- Do not read
./design-system/design.html— chunks have replaced it. Ifdesign_chunksis null (chunks missing), fall back to reading./design-system/design.htmland report an anomaly.
Do not load: hyperframes-cli / hyperframes-creative / hyperframes-registry (outside your scope). Do not read section_plan.md (dispatch already embeds the relevant scene creative_brief). Do not open rules outside rule_paths, other component files, or sibling worker scene files.
Blueprint Field
| Value | Behavior |
|---|---|
composed |
No blueprint reference; freely combine the effects list |
based-on <id> |
Follow the blueprint skeleton (DOM structure, phase splits, timing rhythm), embedding effects into the corresponding phases |
extended <id> |
Same as above, with permission to append 1-2 phases at the end or replace one phase |
Blueprint is a soft reference: if the file is missing/not applicable → fall back to composed. But never ignore it — you must read it first before deciding.
Web-Reproduction Blueprints (based-on / extended demo-page-scroll-spotlight) → Run the Skeleton Generator First
The trigger is the dispatch blueprint field being demo-page-scroll-spotlight, not the presence of rule 3d-page-scroll in effects. (3d-page-scroll is a rule, not a blueprint — it appears in that blueprint's uses list; do not look for blueprints/3d-page-scroll.md, it does not exist.)
This type of blueprint needs to rebuild the site as a scrollable .page-card with element-by-element highlights. Do not hand-build it from scratch — run:
node <SKILL_DIR>/phases/visual-design/scripts/build-page-card.mjs "$PROJECT_DIR"
It reads capture/extracted/tokens.json (enriched sections + local image map) + design-system/inference.json (brand color), and emits $PROJECT_DIR/page-card.html: a golden structure, injected brand color, content/local image map, split .kw words, selected .pop-target, and a preliminary timeline. But it emits a standalone document (<!doctype> / <head> / CDN gsap <script> / <div id="root" data-composition-id="main"> without <scene-id>-root class / no <template>). Your output contract requires a fragment — finish in this order:
- Standalone → fragment conversion (self-check validates the fragment contract; missing this step trips the root contract / data-composition-id / timeline-registration FATALs):
- Strip
<!doctype>/<html>/<head>/<body>wrappers and the CDN gsap<script>(GSAP is injected once inindex.htmlby Step 7), and wrap#rootin<template id="scene_<N>-template">. - root div: add
class="scene_<N>-root", changedata-composition-id="main"→scene_<N>, delete onlydata-start="0"(data-width/data-heightmust stay — the dispatchedComposition width/Composition height; they are part of the root 5 attributes), and setdata-durationto the dispatchestimatedDuration_s(exactly, constraint #12). <style>: brand tokens are declared once globally inindex.html's<head>, so delete the--*custom-property declarations from the standalone:root { }block instead of carrying them over; move any remaining:rootrules (background / font-family — they referencevar(--*)) onto#root { }, and fold barehtml,body { }and bare* { }into#root/#root *(constraint #1).window.__timelines["main"]→window.__timelines["scene_<N>"](constraint #8; this host-id / registration-key rename is not covered by step 1's "sync timeline selectors"; do it separately).
- Strip
- Prefix all classes/ids with
s<N>-, and sync timeline selectors. - Fill
data-glow-start/endfor each.kwfrom ASR (words left blank simply do not glow; render will not fail). - Use the script's suggested
SCROLL_DISTANCE, measure#pop-targetrect to calibrate, and sync the.spotlightgradient center.
Rewrite image src in page-card.html from capture/assets/<file> to public/<basename> (remove the capture/assets/ prefix and keep only the filename) — prep flat-copies capture/assets/** into public/, preserving basenames; public/ is the only asset surface that render-time guarantees. Do not switch back to remote URLs (hotlinking/offline render can break images). For fidelity details, you may read only for reference from capture/extracted/page.html (read it, but do not render from it).
Captured 16:9 assets on a portrait / square canvas — when the dispatched Composition width/Composition height is not 16:9 (portrait 1080×1920 or square 1080×1080), a wide screenshot / captured product asset does not fit the frame. Do not letterbox it with dead bars and do not stretch-distort it to fill. Instead:
- Crop to the salient region (the headline UI / the one panel that matters) and place that.
- Seat it as a top or bottom band and fill the remaining vertical space with kinetic type / supporting graphics from the component library.
- Or scale it down inside a device / browser-frame mock so the wide asset reads as "a screen" within the tall composition.
Constraints Specific to This Skill (Not Separately Covered by hyperframes-core)
Workers must execute these constraints exactly.
-
CSS / JS selector — root uses
#root; internal elements uses<N>-prefix-
During render, producer strips the
<div class="<scene-id>-root">wrapper (preview/snapshot keep it), so any ancestor selector like.<scene-id>-root .foobreaks completely in render → black scene. -
Rule: all scene-internal classes / ids use the
s<N>-prefix (scene_1 →s1-foo), selectors are written bare as.s1-foo/#s1-foo; JS is synced:querySelector(".s1-foo")/tl.to(".s1-foo", ...). Root styles are only written as#root { ... }. -
Forbidden:
.<scene-id>-root/#<scene-id>-root/[data-composition-id="<sid>"]/:root/ barebody/ bare generic classes (.card, etc.) without prefix. -
When pasting a component: prefix the HTML outer element + nested classes, and update embedded
<style>selectors accordingly; do not prefixvar(--*)/data-*/#root/ CSS generic families:<!-- ✅ outer + nested classes prefixed, selectors synced, var(--*) unchanged --> <div class="s3-card"> <span class="s3-headline">{H}</span> <style> .s3-card { background: var(--accent); } .s3-card .s3-headline { color: var(--ink); } </style> </div>
-
-
Never copy
@font-faceinto a scene — Step 7 declares it once inindex.html<head>. Inside scenes, only usevar(--font-display|body|mono|script); do not hard-code literal font names (this bypasses@font-face, so the real font will not apply). Ifchunks/tokens.cssis missing a role token, do not degrade to a literal family; leavevar(--font-body)so CSS fallback handles it. -
Track lane: inside scenes use
data-track-index="0"-"9";10/11/12/20+belong to top-levelindex.html(voice / BGM / captions / SFX, all emitted by Step 7assemble-index). Do not emit<audio>in a scene. -
Asset src has no leading slash —
public/hero.png, not/public/hero.png.-
Video assets — declared, never embedded. An
assetCandidatewhose path ends in.mp4/.webm/.movis a real moving clip (shown as[video]in the brief). You must NOT write a<video>tag (the non-negotiable host-root media rule —hyperframes-corereferences/variables-and-media.md; a nested<video>renders BLANK, andcheck-compositionsRule 6a fatals on sight). Author the slot as a poster<img>and declare the footage on it:<img class="s3-demo clip" src="public/demo-poster.jpg" data-video-src="public/demo.webm" data-video-offset="0.6" data-start="0.2" data-duration="6" />- Poster
src= the matching[video-still]candidate when one exists; otherwise extract one yourself:ffmpeg -y -ss 1 -i public/<clip> -frames:v 1 public/<clip-stem>-poster.jpg(Bash is available). The poster is the on-canvas fallback at seams and outside the footage window — it must look correct on its own. data-video-src(required) — relativepublic/path to the clip.data-video-offset(optional, default 0) — scene-local seconds when footage starts.data-video-duration(optional) — cap; default plays to scene end.data-video-media-start(optional) — trim into the source.data-video-loop="off"(optional) — looping is on by default.- Step 7
hoist-videos.mjsmeasures the poster's rendered rect in a real browser and mounts the actual<video class="clip">at the host root with global timing (clamped clear of scene transitions). The slot must hold STILL during the declared window — the hoisted video cannot follow in-scene GSAP transforms; animate the slot's entry/exit OUTSIDE the window (setdata-video-offsetafter the entry settles). Source audio never plays (hoisted videos are muted).
- Poster
-
A
[video-still]candidate is a static.pngframe — render it as a normal<img class="s<N>-… clip" …>(and it doubles as the poster for a declared video of the same clip).
-
-
GSAP transform alias whitelist:
x/y/scale/scaleX/scaleY/rotation/opacity. Never tweenwidth/height/top/left(need a box to change shape? convert the bbox delta:x/yfrom center movement,scaleX/scaleYfrom size ratio, withtransform-origin: 50% 50%).
5b. CSS baked transform: rotate(...) and GSAP rotation are mutually exclusive — use only one on the same element
- Pasted components (such as
feature-card/star-burst) often include CSStransform: rotate(var(--bf-tilt-sm-l)); once the same element is targeted by anytl.to/.fromTo/.set, GSAP overwrites the entirestyle.transform, the CSS-baked tilt disappears, and the preset visual signature is lost. - Rule: if a leaf with baked
transformwill not be touched by GSAP (pure decorative strip) → keep CSS, OK. If it appears in a timeline selector → delete the CSS transform line and express the tilt in GSAP (gsap.set(el, { rotation: -2 }), or carryrotation: -2through both ends of thefromTo). The same applies to bakedtranslate(...)/scale(...)/skew(...).
-
Scenes with non-empty
voicePath— Step 7 mounts<audio>at top level according to this scene's duration. You do not emit<audio>, but timing design should leave breathing room for narration.- Inter-scene transitions are not your responsibility: crossfade / push / etc. are deterministically added by Step 7
transitions.mjs injecton your clip wrapper (index.htmllayer, above your scene), not inside your scene. Therefore: (a) do not animate elements out at the end of the scene (no exit tween) — let the scene hold on a stable final frame, and the transition takes over; (b) do not write any slide/fade wrapper logic inside the scene to "connect with the next scene." A scene is responsible only for its own entry + sustained motion; hold the ending. (Exit animations are allowed only in the film's last scene.)
- Inter-scene transitions are not your responsibility: crossfade / push / etc. are deterministically added by Step 7
-
Do not include literal HTML opening tags in comments / string literals (
<template>/<style>/<script>) — the linter scans with regex and will false-positive. Escape as<template>or use plain text. -
Timeline registration uses a literal scene id string:
window.__timelines["scene_1"] = tl;. Do not wrap it behind aSIDvariable (check-compositions.mjscannot recognize it with regex). The whole<script>selector / dataset key / timeline key must use literals. -
Macro-camera scenes (
coordinate-target-zoom/multi-phase-camera/camera-cursor-tracking/viewport-change) — the zoom peak naturally exceeds the canvas; decorative bleed is fine by design, but pushing primary text / brand headlines out of frame is a bug (finalize's contact-sheet look bounces it back as a repair). Keep display text ≤ ~88% canvas width at the zoom peak (derivemaxScale = 0.88×W/r.widthfrom measured dimensions, not round numbers by feel), and measure zoom offsets from real rects, never hand-derive them — the measurement recipe ("Getting the offset") is in thecoordinate-target-zoomrule, which is in yourrule_pathswhenever these effects are dispatched. -
One primary subject at a time; no foreground overlap — guaranteed by construction
- Follow
PrimarySubjectTimeline/Handofffromcreative_brief(do not redesign). Before a new primary enters, the previous one must exit / hide / compact / demote to supporting — timeline order: firsttl.to(previousPrimary, ...)out, thentl.fromTo(newPrimary, ...)in. Camera pan/zoom/push does not count as a handoff. Supporting content stays smaller, lower contrast, less animated, off the primary bbox. - No FOREGROUND box may intersect another (card / panel / stat / media / icon / button / text block) at any phase of the timeline. Guarantee it by construction: lay foreground out in flow containers (
display:flex/grid) — boxes in normal flow cannot overlap. Reserveposition: absolutefor decorative / background layers (keyword allowlist in constraint #13). Nesting is composition, not overlap: a chip pinned on a card corner is fine when nested inside the card. - Author-owned geometry budgets (mental math with real px values, not by feel): every container holding foreground children gives them ≥12px top AND bottom clearance at rest (sum children heights + gaps + paddings vs container height); place captured assets on surfaces they were authored for (dark-glyph SVG on a dark card is invisible — check
capture/extracted/asset-descriptions.mdfor the light/dark variant); depth-stack text on long words (≥10 chars) keeps layers ≤2 or per-layer offset ≤2px.
- Follow
-
#rootbackground / surface treatment (visual judgment, not dispatch contract)- Default:
#root { background: var(--canvas); }(canvas color fromtokens.css). - If the preset provides multiple background / surface treatments in
hints_file(paste-ready#root { ... }stanzas), you may choose one that fits this scene's mood and paste the entire stanza, so the frame feels like this preset rather than "generic SaaS colors." Allvar(--*)tokens are already defined intokens.css; do not replace them. - Decorative
::afterframe must wrap content: if the selected#rootstanza contains#root::after { ... }(z-index:0 border / texture), the scene content must be wrapped in<div style="position:relative; z-index:1;">, otherwise the frame can cover content.
- Default:
-
data-durationmust equal dispatchestimatedDuration_sexactly — Step 7assemble-index.mjsplaces the full-film timeline usinggroup_specstart_s, then checks each scene rootdata-duration; mismatch is fatal and blocks all of Step 7 back to you. Do not use an approximate value fromcreative_brief; do not round yourself. -
Bottom caption-band keep-out (HARD constraint — only when dispatch
Captions: enabled, machine-checked bycaptions.mjs keepoutin your self-check)When
Captions: enabled, a full-film word-by-word karaoke pill occupies a bottom band. The dispatch hands you two numbers — use them, never hardcode 900 / 880:Caption band top y— the band runs from this y down to the canvas bottom (the bottom ~16.67% of canvas height).Foreground max y— every FOREGROUND element's rendered lower edge must be ≤ this (=Caption band top y− 20px safety). Foreground = headline / cards / CTA / button / chip / stat / hero text / quote / key logo / any readable content.
Worked values: landscape 1920×1080 → band y900–1080,
Foreground max y= 880. Portrait 1080×1920 → band y1600–1920,Foreground max y= 1580.Geometry (mental-calculate before each absolute position; if the lower edge computes to >
Foreground max y, it is a bug). Let H =<Composition height>and FGmax =Foreground max y:CSS shape element lower-edge y Legal condition bottom: <B>px(notop/height)H − BB ≥ H − FGmaxtop: <T>px+height: <Hc>pxT + HcT + Hc ≤ FGmaxtop: <T>px+ natural height (estimate)T + content heightT ≤ FGmax − content heighttop: <T>px+bottom: <B>px(stretched strip)H − B(bottom determines lower edge)B ≥ H − FGmaxflex/grid child + align-self: endParent container bottom Parent lower edge ≤ FGmax H − FGmaxis the minimum bottom offset: 200px on landscape, 340px on portrait. A centered hero anchors around y ≈ 0.42 × H (landscape ≈ 454, portrait ≈ 806), not the canvas midpoint.BACKGROUND exceptions (exempt, may be full-bleed to the canvas bottom):
#rootbackground / surface decoration /::before/::afterframe / ambient mesh / full-bleed screenshot base layer; decorative leaf class names — the checker skips selectors containing any of these keywords (split by hyphen/underscore):bg/background/dot-grid/mesh/gradient/swell/ambient/texture/noise/scanline/surface/overlay/halo/glow/frame/pin/corner-pin/deco/star-burst/burst/ring/stripe/rect/shadow/pulse/ripple/measure/probe/hidden/scrim/backdrop/veil/fog/grain.The static math folds in CSS
transform: translate*(px / % literals) andmargin-top/margin-bottom— a negative-margin-centered card is measured at its real bbox. Shapes static analysis cannot catch (GSAP runtimetranslateY, natural flex flow pushing content down) are covered by finalize's contact-sheet look — still position by the rule "lower edge ≤ FGmax"; do not intentionally hug the edge.When
Captions: disabled: full-canvas, vertical center y = H / 2, content may extend all the way to the canvas bottom; positioning is free.
Scope
Only write <PROJECT_DIR>/compositions/<scene-id>.html. Do not modify index.html / copy assets / run npx hyperframes lint|validate|snapshot|render (at initial authoring time index.html does not exist yet, so project gates cannot run) / add or remove effects (if a rule cannot run → STOP and report; do not silently drop it).
Every id in the effects list must appear once on the timeline (usually 2-5; use every input effect, silently drop none); exact firing time, driven asset/text, and phase all come from creative_brief prose (its effect→asset mapping + choreography), with ## Film direction supplying the defaults the brief leaves unstated (ease intents, ambient layers, motion budget). Your job is to translate the brief into GSAP calls, not redesign the choreography.
Flow
- Parallel Read the required resources (6 items above)
- Write
<PROJECT_DIR>/compositions/<scene-id>.htmlfor each scene (skeleton below) - Self-check (the bash block below); fix before reporting if anything fails
- One-line report
Skeleton
Example below uses scene_1 (for other scenes, replace scene_1 / s1- with the corresponding number):
⚠ root <div> 5 attributes + class + style must be written on the same line — the self-check regex and check-compositions Rule 1 both require "id and class in the same tag" as a single-line match. Splitting attributes across lines is legal HTML, but the self-check will FAIL and waste an Edit.
<template id="scene_1-template">
<div
id="root"
class="scene_1-root"
data-composition-id="scene_1"
data-width="<Composition width>"
data-height="<Composition height>"
data-duration="<estimatedDuration_s>"
style="position:relative; width:<Composition width>px; height:<Composition height>px; overflow:hidden;"
>
<style>
/* Root styles use #root (never .scene_1-root / a self data-composition-id selector).
Brand tokens are declared once in index.html's <head> and inherit — reference via
var(--*), never redeclare :root; override a single token on #root only when needed. */
#root {
background: var(--canvas);
font-family: var(--font-body); /* default font; headings use var(--font-display) */
}
#root *,
#root *::before,
#root *::after {
box-sizing: border-box;
}
/* Scene-specific rules — all bare classes with the s1- prefix
so sibling scenes do not conflict. */
.s1-grid {
/* ... */
}
</style>
<!-- Build DOM according to the creative_brief effect→asset mapping.
When blueprint is not composed, prefer the blueprint DOM skeleton.
All classes use s1- prefix; ids also use s1- prefix (e.g. id="s1-headline"). -->
<script>
// Paste the EASE / DUR const block from easings.js / dispatch inline section
const EASE = { entry: "power2.out" /* ... */ };
const DUR = { med: 0.55 /* ... */ };
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Write selectors as bare .s1-foo / #s1-foo (see constraint #1);
// each effect's fire time comes from the creative_brief choreography (see Scope section).
tl.fromTo(
".s1-word",
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: DUR.med, ease: EASE.entry },
0,
);
window.__timelines["scene_1"] = tl;
</script>
</div>
</template>
Self-Check (fix failures before reporting)
Replace <scene-id> / <N> / <estimatedDuration_s> below with real values before running:
PROJECT_DIR="<Dispatch context PROJECT_DIR>"
SKILL_DIR="<Dispatch context SKILL_DIR>"
F="$PROJECT_DIR/compositions/<scene-id>.html"
SID=<scene-id>; N=<N>; EXPDUR=<estimatedDuration_s>
W=<Composition width>; H=<Composition height>
# File exists
[ -s "$F" ] || echo "FAIL: empty/missing $F"
# Root 5 attributes present at once
for ATTR in 'id="root"' "class=\"${SID}-root\"" "data-composition-id=\"${SID}\"" "data-width=\"${W}\"" "data-height=\"${H}\"" 'data-duration="'; do
grep -q "$ATTR" "$F" || echo "FAIL: root missing $ATTR — all 5 attributes must be present"
done
# id="root" and class="<sid>-root" on the same div (check-compositions Rule 1: same tag)
grep -qE "id=\"root\"[^>]*class=\"${SID}-root\"|class=\"${SID}-root\"[^>]*id=\"root\"" "$F" || \
echo "FAIL: id=\"root\" and class=\"${SID}-root\" must be on the same div tag"
# data-duration must equal dispatch estimatedDuration_s exactly (assemble-index treats mismatch as fatal)
grep -q "data-duration=\"${EXPDUR}\"" "$F" || echo "FAIL: root data-duration must equal estimatedDuration_s=${EXPDUR} exactly"
# No literal <template>/<style>/<script> inside comments (lint regex false-positives on them)
grep -nE '<!--[^>]*<(template|style|script)[> ][^>]*-->' "$F" && \
echo "FAIL: comment contains literal <template>/<style>/<script> — escape as <...>"
# Must be 0 — bug shapes
grep -nE "\\.${SID}-root[[:space:]]" "$F" && echo "FAIL: .${SID}-root used as ancestor selector (render strips the wrapper → black scene)"
grep -nE "\\[[[:space:]]*data-composition-id[[:space:]]*=[[:space:]]*['\"]${SID}['\"][[:space:]]*\\]" "$F" && \
echo "FAIL: self data-composition-id selector — use #root / .s${N}-foo"
grep -nE "#${SID}-root\\b|getElementById\\(\"${SID}-root\"\\)" "$F" && echo "FAIL: do not use #${SID}-root"
grep -nE '@font-face|transition:|animation:|Date\.now|Math\.random|performance\.now|fetch\(|repeat:\s*-1' "$F" && \
echo "FAIL: hits above — rewrite CSS transition:/animation: as GSAP tweens (not seekable otherwise); @font-face belongs in index.html <head>; Date.now/Math.random/performance.now/fetch/repeat:-1 violate the deterministic contract"
# Hard-coded font names bypass index.html @font-face (allowlist: var(--font-*), CSS generic families, safe fallbacks).
# Use the if-form: on macOS `grep -v` returns 0 on empty input, so a bare && chain false-fires.
HARDCODED_FONTS=$(grep -nE "font-family:[[:space:]]*['\"]" "$F" | grep -vE "var\\(--font-(display|body|mono)\\)" || true)
[ -n "$HARDCODED_FONTS" ] && \
echo "FAIL: hard-coded font names — use var(--font-display/body/mono)"$'\n'"$HARDCODED_FONTS"
grep -nE '["(]/public/' "$F" && echo "FAIL: asset path has leading slash — write public/... (not /public/...)"
grep -nE '<video\b' "$F" && \
echo "FAIL: <video> tag(s) — replace with a poster <img class=\"clip\" data-video-src=\"public/<clip>\" ...> declaration (constraint #4)"
# Caption-band keep-out (constraint #13) — ONLY when dispatch says `Captions: enabled` (static, instant)
(cd "$PROJECT_DIR" && node "$SKILL_DIR"/scripts/captions.mjs keepout \
--group-spec ./group_spec.json --hyperframes . --scene "$SID")
# exit 1 → each violation prints the selector + an edit_old → edit_new fix; apply it, re-run until clean.
# Must be >= 1 — structural evidence
grep -c "class=\"${SID}-root\"" "$F"
grep -c "data-composition-id=\"${SID}\"" "$F"
grep -c "#root" "$F"
grep -c "window\\.__timelines\\[\"${SID}\"\\]" "$F"
grep -cE "[.#]s${N}-[a-z]" "$F"
# Strict class-prefix check: every token in class="..." must be s<N>-* or ${SID}-root
UNPRX=$(grep -oE 'class="[^"]*"' "$F" \
| sed -E 's/class="([^"]*)"/\1/' \
| tr ' ' '\n' \
| grep -vE "^(s${N}-[a-zA-Z0-9_-]+|${SID}-root)$" \
| grep -E "^[a-z]" \
| sort -u)
[ -n "$UNPRX" ] && echo "FAIL: classes missing s${N}- prefix: $(echo $UNPRX | tr '\n' ' ')"
# All referenced assets exist under PROJECT_DIR/public/
grep -oE 'public/[A-Za-z0-9._/-]+' "$F" | sort -u | while read p; do
[ -s "$PROJECT_DIR/$p" ] || echo "MISSING ASSET: $p"
done
Any FAIL / MISSING hit → fix before reporting. Nothing checks your layout after this except finalize's one contact-sheet look — a contract break here costs a full re-dispatch round-trip.
Repair Mode (TARGETED REPAIR re-dispatch)
When the dispatch contains a ## Repair context block, you are repairing an existing scene file after finalize escalated it — not authoring from scratch. The block carries finalize's verbatim findings (what looked broken on the contact sheet, which selectors/areas) and Captions: enabled|disabled.
- Edit in place; do not rewrite. Preserve the root contract (all 5 attributes),
data-durationEXACTLY,s<N>-prefixes, timeline registration, and every dispatched effect. - Fix the listed findings by root cause (move a box / reflow into a flex container / swap an asset variant / retune an interior), not by hiding content.
- Re-run the full Self-Check block above (including scoped keepout when captions enabled) before reporting. Still failing after 3 distinct fix attempts on the same finding → STOP and report what you tried.
- Report: one line + what changed. The orchestrator reruns assembly + finalize after you return.
Report Template
One line:
scene_2: file=compositions/scene_2.html duration=4.83s effects=[3d-page-scroll, hacker-flip-3d] blueprint=based-on:demo-page-scroll-spotlight keepout=✓
keepout= restates the scoped gate result from the self-check (keepout=skipped when Captions: disabled). Plus anomalies (missing asset, ambiguous rule combination, attempted effect drop, ease-key fallback). Do not write context.log. In Repair Mode, append what changed per finding.