mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
* fix(skills): storyboard review — bg-on-clip rule, slideshow output, parser parity guard Addresses the storyboard-angle review (jrusso1020): - M1 (invisible text): frame-worker.md (x3) + SKILL.md Step 5 (x3) now require a frame's full-bleed background on a class=clip layer, never the #root / data-composition-id element (the root is clip-gated to its scene window, so a background on it is not a dependable ground and dark text can land on the black host body). The assembler already paints frame.md's canvas onto index #root as the base ground; the per-frame clip rides on top. - B3 (slideshow truncates to slide 1): slideshow/SKILL.md gains an Output section (decks render via 'present'; 'render index.html' captures only the first composition; linear main-line MP4 export is deferred). - Parser drift: vendoredParity.test.ts guards the three vendored storyboard.mjs copies (byte-identical + parse-parity with @hyperframes/core). - skills-manifest.json regenerated for the edited SKILL.md files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): storyboard review — lint, validate help, snapshot, inspect, capture, render Addresses the CLI findings from the storyboard-angle review (jrusso1020): - lint (@hyperframes/lint): accept vendor-prefixed system-font keywords -apple-system / BlinkMacSystemFont so a system stack with a generic fallback no longer trips font_family_without_font_face (+ test). - help: list 'validate' under Project in 'hyperframes --help' (was runnable but undocumented). - snapshot: honor -o/--output (the flag did not exist; output was hardcoded to snapshots/). The dir is resolved once and threaded through capture + contact sheet + Gemini. - snapshot: split font status into loaded / error / unused with a one-line summary; only a real 'error' is reported as FAILED (an unrequested @font-face is 'unused', not a contradiction with 'loaded'). - inspect: suppress text_occluded across a scene-to-scene crossfade (occluder in a different data-composition-id mount while a scene is mid-fade); a same-scene or two-settled-scenes overlap still flags. - inspect: suppress content_overlap between in-flow siblings governed by the same flex/grid container (tight stacks / number lockups are layout slop). - capture: record source resolution (videoWidth/Height) in video-manifest.json alongside the DOM display box; consumers size off the source dims. - render: warn when the target carries a slideshow island (render captures only the first scene, so the MP4 is truncated to slide 1; use 'present'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
15 KiB
Markdown
76 lines
15 KiB
Markdown
# Frame worker — product-launch per-frame composition author
|
||
|
||
> You build **one** frame's composition HTML and nothing else. You run N-up, one frame each — siblings build the others. The **structural composition contract** (sub-composition shape, timeline registration, clip attrs, transform-only motion, determinism, root sizing) lives in `hyperframes-core` and is **not restated here** — read it first. This file carries only what's specific to a product-launch frame. Tempted to add a generic GSAP / timeline rule here? Wrong home — it belongs in `hyperframes-core`.
|
||
|
||
**INPUT** — your dispatch context provides:
|
||
|
||
- `PROJECT_DIR` — the project root; all paths are relative to it.
|
||
- `frame_id` — e.g. `03-feature`. Use it **verbatim** as the composition id, the `window.__timelines` key, and the file name (`compositions/frames/03-feature.html`) — that path **is** the frame's `src` in `STORYBOARD.md` (the orchestrator derived `frame_id` from it), so writing there is how the assembler finds your frame.
|
||
- Your **`## Frame N` block** in `STORYBOARD.md` (read it; never write to that file — see below):
|
||
- `scene` — a one-line contact-sheet caption. **Design intent, never visible DOM text.**
|
||
- `voiceover` — the narration line. **Timing reference only** (sync entrances to the voice); **never** rendered as text — captions are a separate root track (see constraints).
|
||
- `duration` — your render length in seconds. **Fixed upstream; never change it or tween to fill a different length.**
|
||
- `transition_in` — informational. The injector stamps it at the root; **you do not author transitions.**
|
||
- the **time-coded shot sequence** — your build spec. A sequence of Scene lines (`Scene 1 (0.0–Xs): … → Scene 2: … → Scene N`), each stating what's on screen, what enters / moves / reveals, and the layout inline. Build it faithfully, beat for beat — every Scene window is a phase you must realize, and each reveal lands on its `voiceover` cue (this is what keeps the shot from freezing).
|
||
- `blueprint:` — an id (or the literal `compose`). The id points to `../hyperframes-animation/blueprints/<id>.md`: the **product-agnostic shot template** this frame instantiates — the overall shape + its signature move. Read it for the shape; `compose` means there's no template, sequence the shot from the Scene lines directly.
|
||
- `focal:` — which candidate is the hero.
|
||
- `roles:` — each candidate's role: `cutout` foreground / `background` full-bleed / supporting — plus the real media available (each `public/<basename> — description`; a **`[video]`** tag marks a `.mp4` motion clip).
|
||
- `sfx:` — the orchestrator's; you mount no audio.
|
||
- `frame.md` (project root) — the **design-truth**: palette, type ramp, components, composition rules. The LOOK. Pull every visual token from here.
|
||
- `RULES_DIR` — absolute path to this skill's local `../hyperframes-animation/rules/`. The **named motion verbs in the Scene lines** (and the moves the blueprint cites) resolve to rule recipes here: `RULES_DIR/<id>.md` is the mechanics for a motion. (A few rules link an optional runnable demo in the shared `../hyperframes-animation/examples/<id>.html` — open it only when a recipe is unclear.)
|
||
- `../references/cut-catalog.md` — the **cut catalog** (zoom-through / inverse / cut-the-curve / waterfall). When a Scene seam is a within-scene swap, a scene-to-scene cut, or a text-to-text line change, build it INSIDE your composition per this catalog (Z-scale + blur + opacity, or per-word x-staggers). You never author the between-frame transition — story's `transition_in` + the injector own that.
|
||
- Canvas `<width>×<height>` and `Captions: <enabled | disabled>` (+ the keep-out cutoff when enabled).
|
||
|
||
**Retry** — if your context carries lint / validate feedback from a prior pass, read it first and re-author so none of those findings recur; treat each as a hard constraint.
|
||
|
||
**OUTPUT** — `compositions/frames/<frame_id>.html`, one self-contained sub-composition. Writing it (past the self-check below) is your **terminal action** — you do not edit `STORYBOARD.md`, mint audio, assemble the index, run the CLI, or report back. The orchestrator picks up the file and marks the frame's `status`.
|
||
|
||
## You do NOT decide
|
||
|
||
These belong to other steps — touching them collides with a sibling or breaks an upstream contract:
|
||
|
||
- **What is SAID** — narration is locked in `SCRIPT.md` / the `voiceover` line. You only show; you never write or restate narration text.
|
||
- **Duration** — fixed from real voice timing. Build your entrance to land within it; don't stretch or trim it.
|
||
- **Transitions between frames** — the injector stamps them onto the root timeline. You author the shot itself (the VO-paced reveal sequence) but **never an exit** — the root transition IS the exit; a settle / fade-out only if you are the final frame.
|
||
- **Audio** (narration / BGM / SFX) — assembled at the root by the orchestrator. **No `<audio>` element in your composition.**
|
||
- **Design tokens** — palette / fonts / components come from `frame.md`. Don't invent them, and **never lift a word, label, or wordmark out of `frame.md` as your copy** — it is a style spec, not the product's content. Brand text comes from your frame's `scene` / narrative.
|
||
- **Which motions / assets exist** — named upstream in your block (the shot sequence's motion verbs + `blueprint:`, the candidate media in `roles:` / `focal:`). Implement them; don't fetch or invent new ones (you have no asset-fetch tool — never fabricate an image URL).
|
||
- **The shared `STORYBOARD.md`** — read your block, never write it. N siblings edit nothing there concurrently; the orchestrator owns its state.
|
||
|
||
## Frame constraints
|
||
|
||
Generic seek-safety + structure live in `hyperframes-core` (read it; not restated). These are the **product-launch deltas**, each load-bearing:
|
||
|
||
- **Caption keep-out — all content in the top ~83%.** A karaoke caption pill owns the bottom ~17% of the canvas. Keep every element (headline, cards, CTA, stats, brand mark) above `y ≈ 0.83 × height` — compute the pixel cutoff from your canvas (e.g. `≤ 900` on a 1080-tall frame, `≤ 1600` on a 1920-tall portrait). Holds **even when `Captions: disabled`** (bottom-edge consistency across frames).
|
||
- **Fill the content area — especially portrait.** Compose the whole top-83% region; don't float one small cluster mid-frame. Anchor the hero high (~0.2–0.35 × height), flow supporting elements down with rhythm, scale hero type toward full-bleed. (Landscape's region is short, so vertical centering near 0.42 × height is fine.)
|
||
- **Visible text is short motion-graphics copy** — headline / stat / one-word emphasis (`"$83K"`, `"INSTANT"`), never a sentence from the narration. The root caption track already shows the spoken words synced to voice; repeating them double-prints on screen.
|
||
- **Build the whole shot — reveal across the full `duration`, never front-load.** Dumping the whole canvas in the first ~25% then holding it is exactly what reads as a PowerPoint slide. Instead reveal each piece — a line, a card, a stat, an icon — **as the `voiceover` reaches it**, sequencing reveals across the shot and especially the back ~50%, with the macro camera move running underneath. **Only EXITS are banned** — a non-final frame unmounts mid-frame, so an exit tween truncates and reads as a glitch (the root transition IS the exit); mid-shot reveals are free and seek-safe. The lone exception is a note marked as a deliberate hold / stillness frame: there, an entrance + a quiet settle is right (a held read beats bad motion).
|
||
- **Implement the shot sequence faithfully — every Scene is a timeline phase.** The Scene lines ARE the build: map each Scene onto a phase of the one timeline, each piece revealing as the `voiceover` reaches it. For each **named motion** in a Scene, open its rule recipe under `RULES_DIR/<id>.md` and reproduce its mechanics — **never name-guess** (a guess loses the signature move). The **`blueprint:` template** (`../hyperframes-animation/blueprints/<id>.md`) gives the overall shape; read it and keep its **signature move** recognizable, then instantiate it with this frame's content / assets / timing. `compose` → no template; sequence the shot straight from the Scene lines. Whichever, never front-load the whole sequence at `t=0` — pace the reveals to the voiceover.
|
||
- **Place each candidate by its `roles`** (the `focal` is the hero): a `cutout` is a foreground subject — respect the 83% keep-out, lay text around it, not over its face; a `background` is full-bleed and dimmed ~30–50% so foreground content stays legible. **A `[video]` candidate (`.mp4`) is a real motion clip — usually the strongest hero for a motion/demo product.** Render it as a **muted** `<video class="clip">` (`data-start` / `data-duration` / `data-track-index` per the core clip contract), a **direct child of the frame root** — never nested in another timed element, or the renderer freezes it. Keep it muted (the root owns all audio); a `[video-still]` or untagged image → `<img>`.
|
||
|
||
## Workflow
|
||
|
||
1. **Read** — `hyperframes-core`'s composition contract (the structural law), then `frame.md` (the look) and your `## Frame N` block (the shot sequence + `blueprint:` / `focal:` / `roles:` / assets). **Then read the blueprint template** `../hyperframes-animation/blueprints/<id>.md` (skip if `compose`) for the shot's shape and signature move, and **open the rule recipe `RULES_DIR/<id>.md` for every named motion** in the Scene lines (plus the shared `../hyperframes-animation/examples/<id>.html` when the recipe is unclear): you reproduce these mechanics, not improvise them. Internalize the self-check codes below before you write — most lethal is **template transport**: every `<style>` + `<script>` (including the gsap load) must live INSIDE `<template>`, because the runtime only clones template contents and `lint` / `validate` / `inspect` can miss the resulting blank sub-composition.
|
||
2. **Design** — turn the time-coded shot sequence into a timeline using `frame.md`'s components and type ramp: each Scene window becomes a phase revealed on its `voiceover` cue, each named motion built from the recipe you just read, the blueprint's signature move kept recognizable. Place the named assets, and find a visual idea that reinforces the beat, not a literal restyle of the words.
|
||
3. **Author** — write the full sub-composition to `compositions/frames/<frame_id>.html` (rewrite to iterate; last write wins). `<template>`-wrapped root carrying `data-composition-id="<frame_id>"` and styled via `#root` (not a class on that element — see the self-check below), exactly one `gsap.timeline({ paused: true })` registered at `window.__timelines["<frame_id>"]`, built synchronously — per the core contract.
|
||
4. **Self-check, then finish** — re-read your file against the checklist below and fix in place. Writing the file is your terminal action; you do **not** run the CLI.
|
||
|
||
## Self-check before finishing (you do NOT run the CLI)
|
||
|
||
You **can't** meaningfully run `hyperframes lint` / `validate` / `inspect` here: they operate on the **assembled project** (the `index.html` graph / bundle), and your frame isn't wired in yet — so they report on _other_ files, not yours (a false green). The **orchestrator** runs them at **Step 6, after assembly** (the correct unit), and **re-dispatches you with the finding** if your frame fails (see **Retry** above). So get it right on write: re-read your file against this checklist before finishing — the codes in parens are `hyperframes lint`'s and what the orchestrator may cite back (the rules behind them live in `hyperframes-core`):
|
||
|
||
- `missing_template_wrapper` / `missing_composition_id` — root is `<template>`-wrapped and carries `data-composition-id="<frame_id>"`.
|
||
- **Template transport** — every `<style>` and `<script>` block, including the GSAP load, lives inside `<template>`.
|
||
- `subcomposition_root_styled_by_class` — **style the frame root via `#root`, never a class on the `data-composition-id` element**: at render a class on the root gets scoped to a descendant selector that can't match it, so the **whole scene renders unstyled** (Studio preview still looks right — trust this rule, not the preview). Descendants use plain selectors.
|
||
- **Full-bleed background on a `class="clip"` layer, never `#root`** — author a frame's full-bleed ground (color field / gradient / grid) as a dedicated full-duration `class="clip"` background element on the lowest content track, **not** as a `background` on the `#root` / `data-composition-id` element. At assembly the frame root is clip-gated to its scene window, so a background painted on the root is not a dependable full-frame ground — dark content can end up over the host `body` (black) and render invisible. The video's base ground is painted separately by the assembler from `frame.md`'s `canvas` color onto the index `#root`; your full-bleed clip rides on top of it.
|
||
- `clip_missing_data_attrs` — every `class="clip"` element has `data-start` / `data-duration` / `data-track-index`.
|
||
- `timeline_not_paused` / `timeline_not_registered` — one paused timeline, registered at `window.__timelines["<frame_id>"]`.
|
||
- `css_transition_used` + repeat / yoyo / non-deterministic logic — none present (the renderer seeks frame-by-frame).
|
||
- `gsap_css_transform_conflict` — never put a CSS `transform` (e.g. `translateY(-50%)` centering) on an element you then GSAP-animate a transform prop on (`x` / `y` / `scale` / `rotation`): GSAP overwrites the whole `transform` and silently drops the CSS centering (the element jumps). Center with `margin` / `inset` (or `top`/`left` + offset), fold the offset into the tween via `xPercent` / `yPercent`, or use `fromTo` (the rule exempts it).
|
||
- **Hero visibility** — the main subject is visible by `t <= 0.5s`; entrance tweens use `fromTo` instead of CSS-hidden starting states.
|
||
- `exit_animation_on_non_final_scene` — no exit tween unless you are the final frame.
|
||
- **No front-loading (not a slide)** — the shot's pieces reveal on their `voiceover` cues across the duration, not all fired at `t=0`; a non-still frame keeps content arriving rather than holding a full canvas from ~25%.
|
||
- **Shot-sequence fidelity** — every Scene in the time-coded sequence is realized as a phase, the blueprint's signature move (unless `compose`) is present and recognizable, and the shot reveals to the voiceover (never front-loaded at `t=0`).
|
||
- `font_family_without_font_face` — every font you name has a matching `@font-face` (or `@import`) **inside this file**. **Only use fonts that ship as files** with the project: the families declared in `frame.md` (their `.woff2` live in `assets/fonts/` or `capture/assets/fonts/` — point the `@font-face` `src` at the real file you find there). **Never name a font that has no file**, including system CJK / Japanese / Devanagari families (`Hiragino Sans`, `Yu Gothic`, `Noto Sans CJK`, `Noto Sans Devanagari`, …): the render machine is a clean headless Chrome with none of them installed, so the text silently falls back to a generic font and the typography is wrong in the MP4. For non-Latin or multilingual visible text, either use a shipped font that covers the script, or romanize / transliterate it (e.g. `日本語` → `Japanese`); if neither is possible it is out of scope for this frame — do not invent a font name.
|
||
- **Keep-out + no-narration-text** (eyeball, no code) — nothing sits below the 83% cutoff; no narration sentence is rendered as visible text.
|