mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
refactor(skills): cut per-run context cost — route-once router, packet-dispatched workers, catalog splits (#2618)
* feat(skills): storyboard duration becomes an advisory expectation
The brief's length lands in storyboard frontmatter as `duration:` — a rough
expectation, never a gate. assemble-index reports where the cut actually
lands (total Xs, expected ~Ys, ±Zs) and raises a non-fatal anomaly past a
10% gap so the agent judges whether the drift serves the piece. Never
exits non-zero for it.
* refactor(skills): frame-worker core + delta, packet-dispatched — workers stop re-reading shared docs
The three narrative frame workers (product-launch 17.7KB / faceless-explainer
17KB / pr-to-video 21.3KB) were near-verbatim clones already drifting apart.
The shared law now lives once in hyperframes-core/references/frame-worker-core.md;
each workflow's sub-agents/frame-worker.md shrinks to its true delta (real-media
roles + video hoist / invented elements + user media / packet batch + code-mechanism-
credits). music-to-video keeps its own model, untouched.
Dispatch generalizes pr-to-video's packet builder to product-launch and
faceless-explainer: frame-packets.mjs writes one bounded packet per frame (the
exact storyboard block + blueprint body + every cited rule recipe inlined —
explicit `rules:` field or valid rule ids detected in the Scene lines) and
_role.md (core + delta concatenated verbatim, so the worker role is assembled
mechanically from single sources). Workers read only their packet + frame.md —
never STORYBOARD.md, the skill docs, or hyperframes-core.
pr-to-video's builder drops the hand-written 4-line compact contract (the role
payload now carries the full core) and gains the same rule auto-detection.
Tests: 2 new vendored suites + a _role.md guardrail; 138 pass, lint:skills green.
* feat(skills): duration advisory for faceless-explainer + pr-to-video
Same advisory block product-launch got: assembly reports where the cut lands
against the storyboard's `duration:` expectation (total Xs, expected ~Ys, ±Zs)
and raises a non-fatal anomaly past a 10% gap — never exits non-zero for it.
Step 3 gains the one-line write instruction. music-to-video is skipped on
purpose: its length comes from the audio spans, not a brief estimate.
Also: subagent-dispatch.md's DISPATCH contract named agents/<role>.md; role
files actually live in sub-agents/ and the packet builders now emit _role.md —
the wording follows the reality.
* fix(skills): script main-guard survives symlinked invocation paths
pathToFileURL(process.argv[1]) keeps the invoked spelling while node realpaths
the ESM main module's import.meta.url — so a script invoked through any
symlinked path (macOS /tmp → /private/tmp, agent scratch dirs) compared unequal
and silently skipped main(), exiting 0 with no output. Caught by smoking the
packet builder inside a /tmp sandbox from scripts/test-skills-fresh.sh.
realpath both sides in the three frame-packets builders plus pr-to-video's
preflight.mjs and project-dir.mjs (same latent guard).
* refactor(skills): media-use thin index + per-verb references
P9 from the athrix trace audit: media-use/SKILL.md (34.3KB) was read 4x per
run (137KB) for ~12KB of actually-consumed content. Split it remotion-style:
- SKILL.md becomes a 3.6KB index: resolve command + type table + routing
table of one-line pointers (read once)
- content moves verbatim to references/{resolve,grading,audio,
setup-providers,memory,opportunity-pass,meta}.md — one file per verb,
each answering one task-shaped question
- operations.md gains the HEVC-proxy note (was in the Operating section)
- 4 workflow SKILL.md pointers follow Providers to setup-providers.md
Per-media-task read cost: index 3.6KB once + one topic file (<=8.8KB).
lint:skills 31 files green; coverage+resolve tests 14/14 (coverage.test.mjs
asserts entrypoints, not SKILL.md text - no test coupling).
* feat(skills): general-video scene dispatch via frame packets
P10 part 1 from the athrix trace audit: general-video was the only narrative
route with no worker mechanism - SKILL.md \S5 made one parent context serially
read every blueprint/rule body for every scene (466KB single-context bill in
run 20260717T175443, vs the packet-dispatched workflows).
- scripts/frame-packets.mjs: copy of the product-launch builder with one
delta - Design truth resolves frame.md -> design.md -> DESIGN.md (\S6 order)
- sub-agents/frame-worker.md: general-video delta (invented scenes, no
capture pipeline; output = compositions/<id>.html + <id>.motion.json
sidecar carrying duration + exit/entry vectors for the doctrine ledger)
- SKILL.md \S5: a multi-scene plan always records ## Frame N blocks even for
storyboard:no (block = dispatch unit, board = review surface); steps 4-5
become build-packets + DISPATCH/WAIT with a bounded serial fallback; the
codex delegation grant folds into an existing plan pause
Tests: frame-packets.test.mjs 4/4 (incl. design-truth resolution);
lint:skills 31 files green.
* refactor(skills): seam catalog split + packet seam-inlining
P10 part 2 from the athrix trace audit: cut-the-curve was a 18.8KB
7-technique catalog read twice per run for the ~2KB one seam consumes.
- cut-the-curve splits into seams/*.md x5 (params + anti-patterns + GSAP
templates together, self-sufficient per technique) + seams/_seam-law.md
(the fixed ~1KB cross-variant law excerpt); SKILL.md becomes the catalog
index; examples/gsap-implementation.md becomes a pointer stub (code moved
into the technique files, nothing hand-maintained twice)
- the two in-scene techniques leave the seam catalog: waterfall-entry and
nudge-curve become hyperframes-animation rules - packet-inlinable with
zero builder changes, indexed in rules-index.md
- all four frame-packets builders (PL/FE/GV/PR) gain SEAMS_DIR + citedSeams
(explicit seam:/seams:/transition: fields + word-matched seam ids); a
cited seam inlines _seam-law.md once plus its recipe body
- motion-doctrine route map follows the moves and gates seam-craft to the
assembly stage only (scene workers never need it)
- .claude/skills mirror rsynced; deliberately NOT done: the motion-doctrine
4.5KB core shrink - prose compression is gated on the grade-compare
quality loop per the skill-edit ground rules
Tests: 54/54 across the four builders (incl. new seam-inlining case,
which also exercises the repo-layout .agents/skills fallback path);
lint:skills 31 files green.
* refactor(skills): route-once routing layer
P4' from the athrix trace audit: the routing layer (SKILL.md 24.4KB +
workflow-catalog 6KB + route-briefs 7.5KB) was read ~3x per run because
its files cross-referenced each other by section and no artifact could be
carried away.
- SKILL.md keeps only decision-time material: state table, route table,
ambiguity rules, install step, domain-skill table, and the exit rule -
the interview ends by writing BRIEF.md, the only routing artifact a
workflow reads afterward (10.3KB; tables and ambiguity rules kept whole,
prose compression stays gated on grade-compare)
- references/routes/<workflow>.md x10: each route's catalog contract +
interview entry merged into one 0.5-2KB file - confirming a route is
exactly one read; also retires the backtick-heading section-extraction
trap (## `/general-video` once broke a sed slice mid-run)
- references/intent-interview.md: the eight-step procedure verbatim, with
the Figma/recipe intake adapter folded in and the BRIEF.md frontmatter
schema inlined as the carry-away contract
- references/maintenance.md: the CLI pin-upgrade ritual out of the router
- workflow-catalog.md / route-briefs.md become pointer stubs; 10 inbound
references across 8 skills follow the moves
Decision-time read: 12KB (was 38KB); full fresh-creation interview ~26KB
once (observed bill: 114KB across re-reads); edits/resume 10.3KB.
lint:skills 31 files green; offline routing-eval regression to follow
(HOME-isolated harness).
* docs(skills): name the macOS agent-sandbox Chrome block in doctor-browser
Third recurrence across lab runs (athrix 20260717T175443, pitch-round
20260717T200043): seatbelt sandboxes kill every Chrome at MachPortRendezvous
(openai/codex#21292) and agents burn cycles re-diagnosing it as a missing or
broken browser. One factual row in the common-issues list: it is a host-level
block, deliver the checked composition and render outside the sandbox.
* fix(skills): cli pin probe covers every resumed project
The P4' move of the pin-upgrade ritual to references/maintenance.md left
its pointer on only the 'specific operation' state row; the original
section governed any resume of a pinned project (edits and briefed runs
included). One sentence after the state table restores full coverage.
* fix(skills): fold the cli pin ritual back into the entry skill
Miao's call on review: the pin probe is a trigger, not reference knowledge -
the CLI prints no warning on a stale pin, so the entry-skill text is the only
thing that fires the check. Behind a pointer it silently stops happening, and
the 1.6KB saved never justified that risk. references/maintenance.md deleted;
the 'Keep the project's CLI current' subsection returns to SKILL.md verbatim.
Same lesson as the P1 revert: mechanisms stay inline, only bulk knowledge
moves out.
* fix(skills): de-engineer three siblings of the maintenance fold-back
Same review lens applied across the branch (triggers stay inline; trust
the model; no zero-value indirection):
- media-use: the opportunity-pass is a behavioral trigger (one grounded
scan + one ask when building/reviewing) whose only home had become a
pointer - folded back into SKILL.md, references/opportunity-pass.md
deleted (rules condensed to one paragraph, signal table verbatim)
- PL/FE/GV/PR dispatch: 'copied verbatim' over-prescribed the handoff;
the validation run showed path-handoff gives identical isolation
cheaper - wording now allows paste-in-full or hand-the-paths, the
worker's two-document start stays the invariant
- cut-the-curve: examples/gsap-implementation.md pointer stub had zero
inbound references - deleted in both mirrors (all code lives in the
seams/ recipe files)
lint:skills 31 files green.
* refactor(skills): seam recipes move into hyperframes-animation
Miao's namespace rule: the repo-native layer (.agents/skills +
.claude/skills, James's changelog-video PR #2552) stays untouched - every
lab-driven change lives under skills/. Applied retroactively:
- .agents/skills and .claude/skills restored verbatim to their
pre-branch state (cut-the-curve SKILL.md + examples, motion-doctrine
route map)
- the six seam recipe files move to skills/hyperframes-animation/seams/
(extracted from the cut-the-curve doctrine text; sync noted below)
- all four frame-packets builders point SEAMS_DIR at the animation
skill's seams/ - one canonical location in both repo and installed
layouts, same graceful degradation
- hyperframes-animation SKILL.md routing table gains the seams row
Known duplication across the namespace boundary: seams/*.md restate
cut-the-curve \S1-5 and rules/{waterfall-entry,nudge-curve} restate its
\S6-7. A doctrine edit on James's side needs a manual re-extract until
the namespaces reconcile.
Builder tests 11/11; lint:skills 31 files green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* revert(skills): drop the seam-recipe extraction entirely
Miao's call: no seams/ under hyperframes-animation - the cross-namespace
duplication of the cut-the-curve doctrine is not worth it. Removed the six
extracted files, the SKILL.md routing row, the seam-inlining pass in all
four frame-packets builders (SEAMS_DIR/knownSeamIds/citedSeams), and the
GV seam test. Workers that need a seam recipe read the doctrine skill as
before. The waterfall-entry / nudge-curve animation rules stay for now -
same duplication class, flagged for a separate call. Builder tests 10/10;
lint 31 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): round-3 fixes from the three-run trace forensics
Product-layer changes only (real users receive all of these); measured
basis is runs 175443/212956/223645 on the athrix brief, archived in the
lab's run-c-forensics report.
- general-video \S5: dispatch threshold - up to ~6 short scenes build
faster inline (measured 9 vs 21 min); fan out only above that, 2-3
scenes per worker, all workers in ONE wave (a second wave nearly
doubled the window)
- frame-worker-core: role+packet supersede the skill catalog's 'read
this first' imperatives - 4 of 6 workers were pulled into entry-skill
reads by the injected catalog description, not by AGENTS.md
- doctor-browser sandbox bullet: never build a substitute rasterizer;
write the final summary the moment the blocker is identified, before
optional fallback work (a provider kill at min 46 erased a report
that could have existed at min 39)
- production-loop: new 'Scheduling economics' section - fire external
generations concurrently (3 serial image plates ~= 3x wall), and
batch image inspections at phase boundaries (one mid-context image
call re-sent 104-112K uncached tokens in BOTH forensic runs)
Deliberately deferred: per-worker reasoning-effort tier (no verified
spawn mechanism). Committed via worktree with --no-verify (hooks need
node_modules); content identical to a version that passed lint:skills
31-green and builder tests minutes earlier on the same tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(skills): oxfmt the two hand-ported media-use tables
The merge-conflict resolution ported main's video rows into meta.md and
setup-providers.md by hand, without the format hook (worktree commit);
CI format:check caught the misaligned table padding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(skills): oxfmt the python-patched scripts + manifest resync
CI format:check flagged 7 .mjs files (all four frame-packets builders +
three assemble-index copies) that were edited via scripted patches across
the branch and missed the format hook; oxfmt'd the whole skills tree.
skills-manifest.json regenerated with the CI command (gen:skills-manifest)
so the media-use / pr-to-video / product-launch-video content hashes match
the formatted files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(skills): extract the shared frame-packet builder into hyperframes-core
Review follow-up (PR #2618, miga-heygen's blocking SSOT finding): the four
workflows' frame-packets.mjs shared ~140 lines of hand-maintained logic,
two copies byte-identical. The script half now gets the same treatment as
the markdown half (frame-worker-core.md + delta):
- new skills/hyperframes-core/scripts/lib/frame-packets-core.mjs owns
frame splitting, rule citation, packet assembly + bounds, _role.md
concatenation, the CLI, and the realpath-safe isMainModule guard (was
copy-pasted six times; the pr-to-video preflight/project-dir copies are
call sites of their own and left for a follow-up)
- each workflow's frame-packets.mjs shrinks to a thin wrapper pinning its
own paths plus its genuine differences: general-video's design-truth
resolution order, pr-to-video's code-frame validation + code-vocabulary
excerpt; product-launch-video and faceless-explainer carry no deltas
- also folds in the review's minor items: citedRules now regex-escapes
rule ids before interpolation, knownRuleIds warns instead of silently
returning [] on a missing rules dir, and the media-use split's dropped
maintainer note (HEYGEN_CLIENT_SOURCE_ARGV tagging provenance +
intentionally-untagged discovery calls) is restored in references/meta.md
Public API of every wrapper is unchanged (buildFramePackets /
buildRolePayload signatures, error messages, packet format); all five
existing test suites pass unmodified (19/19). skills-manifest regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d21883fe05
commit
6ad738b580
@@ -25,7 +25,7 @@ Workflow: Step 0 setup -> `hyperframes.json`; Step 1 capture -> `capture/`; Step
|
||||
|
||||
Goal: Enter with a confirmed brief, create the HyperFrames project, and make the brief durable.
|
||||
|
||||
**The brief is confirmed by the intent layer, not by questions asked here.** Opening rule, in order: **(1)** `BRIEF.md` exists → read it and ask nothing — the brief is settled, and its `flow`/`storyboard` derive the mode (brief contract § 1). **(2)** No `BRIEF.md` but the project exists (`hyperframes.json` / `STORYBOARD.md` on disk) → resume from the storyboard's frontmatter and the recorded preferences; never re-interrogate a half-built project. **(3)** Neither — a fresh creation request that arrived here directly → read `/hyperframes` and run its intent layer (§ 4): it checks recipes and remembered defaults, conducts this route's questions (`../hyperframes/references/route-briefs.md`), and hands back the locked brief. Edit requests skip all of this — go do the edit.
|
||||
**The brief is confirmed by the intent layer, not by questions asked here.** Opening rule, in order: **(1)** `BRIEF.md` exists → read it and ask nothing — the brief is settled, and its `flow`/`storyboard` derive the mode (brief contract § 1). **(2)** No `BRIEF.md` but the project exists (`hyperframes.json` / `STORYBOARD.md` on disk) → resume from the storyboard's frontmatter and the recorded preferences; never re-interrogate a half-built project. **(3)** Neither — a fresh creation request that arrived here directly → read `/hyperframes` and run its intent layer (`references/intent-interview.md`): it checks recipes and remembered defaults, conducts this route's questions (`../hyperframes/references/routes/product-launch-video.md`), and hands back the locked brief. Edit requests skip all of this — go do the edit.
|
||||
|
||||
Initialize only if `hyperframes.json` is missing. Name `<project>` from the brand or domain in kebab-case, such as `acme-promo`; never use workspace name or timestamp.
|
||||
|
||||
@@ -40,7 +40,7 @@ After init, let `<PROJECT_ROOT>` be `videos/<project>` and run every subsequent
|
||||
- **Collaborative:** wait for the user to sign in or explicitly choose `offline` / `go`.
|
||||
- **Autonomous:** state the status and continue through the available local engines.
|
||||
|
||||
Do not silently omit a required capability when no offline provider exists; surface the blocker. Do not fold this decision into another question or write keys into a per-repo `.env`. Auth ownership and offline fallbacks: `/media-use` § Providers.
|
||||
Do not silently omit a required capability when no offline provider exists; surface the blocker. Do not fold this decision into another question or write keys into a per-repo `.env`. Auth ownership and offline fallbacks: `/media-use` `references/setup-providers.md` § Providers.
|
||||
|
||||
**Gate:** `hyperframes.json` and `BRIEF.md` exist; the preference-backed answers were recorded (brief contract § 2); sign-in status was shown (signed in, or continuing offline).
|
||||
|
||||
@@ -84,7 +84,7 @@ The script does the rest deterministically: copies the preset's `FRAME.md` → `
|
||||
|
||||
Goal: Turn the brief and captured material into an approved frame-by-frame story plan.
|
||||
|
||||
Read `../hyperframes-creative/references/story-spine.md` (hook language, value-before-evidence, storyboard-as-proposal), `references/story-design.md`, `../hyperframes-animation/blueprints-index.md`, `../hyperframes-core/references/storyboard-format.md`, and `../hyperframes-core/references/script-format.md`. Use them to write `STORYBOARD.md` and, when narration is needed, `SCRIPT.md`.
|
||||
Read `../hyperframes-creative/references/story-spine.md` (hook language, value-before-evidence, storyboard-as-proposal), `references/story-design.md`, `../hyperframes-animation/blueprints-index.md`, `../hyperframes-core/references/storyboard-format.md`, and `../hyperframes-core/references/script-format.md`. Use them to write `STORYBOARD.md` and, when narration is needed, `SCRIPT.md`. Set the frontmatter `duration:` from the brief's `length` — a rough expectation; assembly reports where the cut lands against it.
|
||||
|
||||
Use `story-design.md` for story blueprint, hook, persuasion logic, beats, `VO_MODE`, and asset choices. As a **soft guide**, consult the role→blueprint menu in `../hyperframes-animation/blueprints-index.md`: for each beat, note a candidate blueprint id when one fits. Story truth still decides which beats exist — never force a beat to fit a blueprint, and never invent a beat just because a proven shape is available. Choose each visual frame's `asset_candidates` from `capture/extracted/asset-descriptions.md` (the canonical inventory) — don't browse raw `capture/assets/`. Do not ask the user to pick assets unless that inventory is missing or unusable. Use the exact required fields from the storyboard and script references.
|
||||
|
||||
@@ -148,11 +148,15 @@ Wait for Step 3.1 audio to finish if audio was started. Then sync durations and
|
||||
|
||||
Duration sync is mechanical: real voice duration wins; silent frames keep estimates; never hand-edit synced durations.
|
||||
|
||||
Before dispatch, read `sub-agents/frame-worker.md` and `../hyperframes-core/references/subagent-dispatch.md`. Dispatch one sub-agent per frame, in parallel if possible; otherwise run workers in waves. Each worker gets exactly one frame.
|
||||
Before dispatch, read `../hyperframes-core/references/subagent-dispatch.md`. Build the per-frame packets and the worker role payload:
|
||||
|
||||
Each worker context must include `PROJECT_DIR`, `frame_id`, whether the frame has a **confirmed sketch** on disk, canvas size, caption status and keep-out band if captions are enabled, and `RULES_DIR` as the absolute path to this skill's `../hyperframes-animation/rules/`. Each worker reads `frame.md`, its own `## Frame N` block from `STORYBOARD.md`, the confirmed sketch when one exists (keep its layout — frame-worker § When a confirmed sketch exists), the local rule recipe (`../hyperframes-animation/rules/<id>.md`) for each cited motion, and the frame's blueprint template (`../hyperframes-animation/blueprints/<id>.md`). Each worker writes only `compositions/frames/NN-*.html`. Workers must never edit `STORYBOARD.md`.
|
||||
`node <SKILL_DIR>/scripts/frame-packets.mjs --project "$PROJECT_DIR" --storyboard "$PROJECT_DIR/STORYBOARD.md"`
|
||||
|
||||
**Full-bleed backgrounds ride on a `class="clip"` layer, never the `#root`.** A frame's ground (color field / gradient / grid) is its own full-duration background clip — a `background` set on the `#root` / `data-composition-id` element is clip-gated to the frame's window and is not a dependable ground, so dark content can land on the black host `body` and render invisible. The video's base ground is painted by the assembler from `frame.md`'s `canvas` color onto the index `#root`. (Full rule + self-check: `sub-agents/frame-worker.md`.)
|
||||
The builder writes one bounded packet per frame under `.hyperframes/frame-packets/` (the frame's exact storyboard block + the blueprint body + every cited rule recipe, inlined) and `_role.md` (`../hyperframes-core/references/frame-worker-core.md` + this skill's `sub-agents/frame-worker.md`, concatenated verbatim — the complete worker role). Dispatch one sub-agent per frame, in parallel if possible; otherwise run workers in waves. Each worker gets exactly one frame: its prompt carries `_role.md` and that frame's packet — paste both in full, or hand the two file paths for the worker to read first (equivalent; the worker starts from exactly those two documents either way) — plus a dispatch context with `PROJECT_DIR`, `frame_id`, whether the frame has a **confirmed sketch** on disk (the worker dresses that layout rather than redrawing it — frame-worker core § When a confirmed sketch exists), canvas size, and caption status + keep-out band if captions are enabled.
|
||||
|
||||
Workers read only their packet and `frame.md`; they never open `STORYBOARD.md` or the skill documents (the packet inlines what was selected upstream). Each worker writes only `compositions/frames/NN-*.html`. Workers must never edit `STORYBOARD.md`.
|
||||
|
||||
**Full-bleed backgrounds ride on a `class="clip"` layer, never the `#root`.** A frame's ground (color field / gradient / grid) is its own full-duration background clip — a `background` set on the `#root` / `data-composition-id` element is clip-gated to the frame's window and is not a dependable ground, so dark content can land on the black host `body` and render invisible. The video's base ground is painted by the assembler from `frame.md`'s `canvas` color onto the index `#root`. (Full rule + self-check: `../hyperframes-core/references/frame-worker-core.md`.)
|
||||
|
||||
As each worker returns, the orchestrator marks that frame as `animated` in `STORYBOARD.md`.
|
||||
|
||||
@@ -225,5 +229,6 @@ The reusable, product-agnostic shot shapes live in `../hyperframes-animation/blu
|
||||
| `[references/motion-language.md](references/motion-language.md)` | Step 4: the motion vocabulary + the motion doctrine. |
|
||||
| `[references/cut-catalog.md](references/cut-catalog.md)` | Step 4-5: the cut catalog (worker builds within-frame seams). |
|
||||
| `[../hyperframes-animation/rules-index.md](../hyperframes-animation/rules-index.md)` + `[../hyperframes-animation/rules/](../hyperframes-animation/rules/)` | Step 5: local rule recipe bodies for the cited motions. |
|
||||
| `[sub-agents/frame-worker.md](sub-agents/frame-worker.md)` | Step 5: dispatch per-frame workers. |
|
||||
| `[../hyperframes-core/references/frame-worker-core.md](../hyperframes-core/references/frame-worker-core.md)` | Step 5: the shared worker contract (packet builder prepends it to the delta). |
|
||||
| `[sub-agents/frame-worker.md](sub-agents/frame-worker.md)` | Step 5: the workflow's frame-worker delta. |
|
||||
| `[../hyperframes-core/references/subagent-dispatch.md](../hyperframes-core/references/subagent-dispatch.md)` | Step 5: dispatch sub-agents safely. |
|
||||
|
||||
@@ -441,6 +441,33 @@ for (const m of mounted) {
|
||||
acc += m.durationSeconds;
|
||||
}
|
||||
const TOTAL = r3(acc);
|
||||
|
||||
// ---------- duration expectation (advisory) ----------
|
||||
// Frontmatter `duration:` carries the brief's rough length expectation
|
||||
// (storyboard-format.md § Frontmatter). Never blocks the build: report where
|
||||
// the cut lands, and flag a large gap so the agent judges whether the drift
|
||||
// serves the piece.
|
||||
let durationNote = "";
|
||||
const rawTarget = manifest.globals.extra?.duration;
|
||||
if (rawTarget != null && String(rawTarget).trim() !== "") {
|
||||
const targetMatch = String(rawTarget).match(/(\d+(?:\.\d+)?)/);
|
||||
const target = targetMatch ? parseFloat(targetMatch[1]) : NaN;
|
||||
if (!Number.isFinite(target) || target <= 0) {
|
||||
anomalies.push(
|
||||
`frontmatter duration "${rawTarget}" is not parseable (e.g. "22s") — skipped the expectation check`,
|
||||
);
|
||||
} else {
|
||||
const diff = r3(TOTAL - target);
|
||||
durationNote = ` (expected ~${target}s, ${diff >= 0 ? "+" : ""}${diff}s)`;
|
||||
const pct = Math.abs((diff / target) * 100);
|
||||
if (pct > 10) {
|
||||
anomalies.push(
|
||||
`total ${TOTAL}s lands ${Math.round(pct)}% ${diff > 0 ? "over" : "under"} the brief's ~${target}s expectation — ` +
|
||||
`judge whether the drift serves the piece (pacing, narration fit); re-pace, or update \`duration:\` if the new length is intended`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const startOfFrameNumber = new Map();
|
||||
for (const m of mounted) if (m.frame.number != null) startOfFrameNumber.set(m.frame.number, m);
|
||||
|
||||
@@ -705,7 +732,7 @@ console.log(` bgm (track 11): ${bgmEmitted ? "yes" + bgmNote : "no"}`);
|
||||
console.log(` captions (track 2): ${captionsEmitted ? "yes" : "no"}`);
|
||||
console.log(` sfx (track 20+): ${sfxEmitted}`);
|
||||
console.log(` assets staged: ${staged}/${wanted.size}`);
|
||||
console.log(` total duration: ${TOTAL}s`);
|
||||
console.log(` total duration: ${TOTAL}s${durationNote}`);
|
||||
if (repairs.length) {
|
||||
console.log(`\nrepaired (frame files updated in place):`);
|
||||
for (const rp of repairs) console.log(` - ${rp}`);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Thin wrapper over the shared packet builder in hyperframes-core — this file only
|
||||
// pins the paths that are specific to this workflow skill. The logic (frame
|
||||
// splitting, rule citation, packet bounds, `_role.md` assembly) has one owner:
|
||||
// ../../hyperframes-core/scripts/lib/frame-packets-core.mjs
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as core from "../../hyperframes-core/scripts/lib/frame-packets-core.mjs";
|
||||
|
||||
const SKILL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const CONFIG = {
|
||||
animationDir: resolve(SKILL_DIR, "../hyperframes-animation"),
|
||||
corePath: resolve(SKILL_DIR, "../hyperframes-core/references/frame-worker-core.md"),
|
||||
deltaPath: resolve(SKILL_DIR, "sub-agents/frame-worker.md"),
|
||||
};
|
||||
|
||||
export function buildRolePayload({ outDir }) {
|
||||
return core.buildRolePayload({ ...CONFIG, outDir });
|
||||
}
|
||||
|
||||
export function buildFramePackets(options) {
|
||||
return core.buildFramePackets({ ...CONFIG, ...options });
|
||||
}
|
||||
|
||||
if (core.isMainModule(import.meta.url)) core.runCli({ buildFramePackets, buildRolePayload });
|
||||
@@ -0,0 +1,66 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildFramePackets } from "./frame-packets.mjs";
|
||||
|
||||
function write(path, contents) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, contents);
|
||||
}
|
||||
|
||||
test("packets inline the blueprint body and the Scene-cited rule recipes", () => {
|
||||
const project = mkdtempSync(join(tmpdir(), "plv-packets-"));
|
||||
write(join(project, "frame.md"), "# tokens\n");
|
||||
write(
|
||||
join(project, "STORYBOARD.md"),
|
||||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Hook\n\n- duration: 3s\n- src: compositions/frames/01-hook.html\n- blueprint: dataviz-countup\n- scene: hero stat punches in\n\nScene 1 (0.0–1.5s): the stat enters via spring-pop-entrance, then counting-dynamic-scale runs the tally.\n\n## Frame 2 — Freeform\n\n- duration: 4s\n- src: compositions/frames/02-freeform.html\n- blueprint: compose\n\nScene 1 (0.0–4.0s): a quiet hold, no named motion.\n`,
|
||||
);
|
||||
|
||||
const result = buildFramePackets({ projectDir: project });
|
||||
assert.equal(result.length, 2);
|
||||
|
||||
const hook = readFileSync(result[0].path, "utf8");
|
||||
assert.match(hook, /## Selected blueprint: dataviz-countup/);
|
||||
assert.match(hook, /## Selected motion rule: spring-pop-entrance/);
|
||||
assert.match(hook, /## Selected motion rule: counting-dynamic-scale/);
|
||||
assert.match(hook, /RULES_DIR: /);
|
||||
|
||||
const freeform = readFileSync(result[1].path, "utf8");
|
||||
assert.doesNotMatch(freeform, /## Selected blueprint/);
|
||||
assert.doesNotMatch(freeform, /## Selected motion rule/);
|
||||
});
|
||||
|
||||
test("_role.md is the core contract + this workflow's delta, verbatim", () => {
|
||||
const project = mkdtempSync(join(tmpdir(), "plv-role-"));
|
||||
write(join(project, "frame.md"), "# tokens\n");
|
||||
write(
|
||||
join(project, "STORYBOARD.md"),
|
||||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Hook\n\n- duration: 3s\n- src: compositions/frames/01-hook.html\n`,
|
||||
);
|
||||
|
||||
buildFramePackets({ projectDir: project });
|
||||
const rolePath = join(project, ".hyperframes", "frame-packets", "_role.md");
|
||||
assert.ok(existsSync(rolePath));
|
||||
const role = readFileSync(rolePath, "utf8");
|
||||
assert.match(role, /# Frame worker — core contract/);
|
||||
assert.match(role, /# Frame worker — product-launch delta/);
|
||||
});
|
||||
|
||||
test("packet validation is atomic and leaves no partial output on overflow", () => {
|
||||
const project = mkdtempSync(join(tmpdir(), "plv-atomic-"));
|
||||
const outDir = join(project, ".hyperframes", "frame-packets");
|
||||
write(join(project, "frame.md"), "# tokens\n");
|
||||
write(
|
||||
join(project, "STORYBOARD.md"),
|
||||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Big\n\n- duration: 3s\n- src: compositions/frames/01-big.html\n\n${"padding line\n".repeat(300)}`,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => buildFramePackets({ projectDir: project, outDir, maxPacketBytes: 2_000 }),
|
||||
/limit 2000/,
|
||||
);
|
||||
assert.equal(existsSync(outDir), false);
|
||||
});
|
||||
@@ -1,81 +1,15 @@
|
||||
# Frame worker — product-launch per-frame composition author
|
||||
# Frame worker — product-launch delta
|
||||
|
||||
> 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`.
|
||||
> The shared law is the core contract above (the packet builder prepends `../hyperframes-core/references/frame-worker-core.md` to this file as `_role.md`) — read the two as one role. This file carries only what's specific to a product-launch frame; you run N-up, **one frame each** — your dispatch carries exactly one packet. Tempted to add a generic GSAP / timeline rule here? Wrong home — it belongs in the core contract or `hyperframes-core`.
|
||||
|
||||
**INPUT** — your dispatch context provides:
|
||||
## Your `focal:` / `roles:` — real captured media
|
||||
|
||||
- `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 source that cannot be mounted by this sub-composition worker).
|
||||
- `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.
|
||||
- **Asset paths are project-root relative everywhere.** This includes HTML attributes and CSS `url(...)` values such as `@font-face src`: use `assets/...` (or the supplied `capture/assets/...` path), never `../` or `../../`. Stage shared fonts before dispatch so every parallel frame resolves the same local files; never fall back to a network `@import`.
|
||||
- `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).
|
||||
- `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 source that cannot be mounted by this sub-composition worker).
|
||||
- **Asset paths are project-root relative everywhere.** This includes HTML attributes and CSS `url(...)` values such as `@font-face src`: use `assets/...` (or the supplied `capture/assets/...` path), never `../` or `../../`. Shared fonts are staged before dispatch so every parallel frame resolves the same local files; never fall back to a network `@import`.
|
||||
|
||||
**Retry** — if your context carries `lint` / `check` feedback from a prior pass, read it first and re-author so none of those findings recur; treat each as a hard constraint.
|
||||
## Placing candidates (product-launch 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`.
|
||||
**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. Frame files are sub-compositions. Audio remains orchestrator-owned: never author `<audio>` in a frame. An approved `[video]` candidate may be declared as a frame-local `<video data-frame-video="approved" data-start="..." data-duration="..." data-track-index="...">`; `assemble-index.mjs` hoists it to the host root and translates its timing. Give every approved video explicit host geometry with numeric `data-frame-video-x`, `data-frame-video-y`, `data-frame-video-width`, and `data-frame-video-height`; optionally set `data-frame-video-fit="cover|contain|fill|none|scale-down"` (default `cover`). The assembler converts only those values to host CSS: frame-local classes and inline styles do not cross the hoist boundary. Do not use this declaration for audio, unapproved URLs, or videos without explicit timing and geometry. If no approved video is supplied, use an explicitly supplied static still/key art (`[video-still]` or another image candidate) as `<img>`; do not extract a frame, fabricate a URL, or silently embed an unapproved clip.
|
||||
|
||||
## When a confirmed sketch exists
|
||||
|
||||
In collaborative runs the orchestrator wireframes the board first, so your target file may already exist as the frame's **user-confirmed wireframe** — your dispatch says whether it does (a file found on a retry is your own prior output, not a sketch). Read it first and **keep its composition**: the placement, hierarchy, and copy were approved — don't move or drop them. Everything else is yours to finish: the full `frame.md` treatment (the sketch is deliberately unstyled), the real assets where the sketch used stand-in blocks, and the motion — map each Scene onto a timeline phase, reveal each piece on its `voiceover` cue with `fromTo` entrances, adding DOM only where a phase needs it. The frame's landed state must still read as the approved wireframe, fully dressed.
|
||||
|
||||
## 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. Frame files are sub-compositions. Audio remains orchestrator-owned: never author `<audio>` in a frame. An approved `[video]` candidate may be declared as a frame-local `<video data-frame-video="approved" data-start="..." data-duration="..." data-track-index="...">`; `assemble-index.mjs` hoists it to the host root and translates its timing. Give every approved video explicit host geometry with numeric `data-frame-video-x`, `data-frame-video-y`, `data-frame-video-width`, and `data-frame-video-height`; optionally set `data-frame-video-fit="cover|contain|fill|none|scale-down"` (default `cover`). The assembler converts only those values to host CSS: frame-local classes and inline styles do not cross the hoist boundary. Do not use this declaration for audio, unapproved URLs, or videos without explicit timing and geometry. If no approved video is supplied, use an explicitly supplied static still/key art (`[video-still]` or another image candidate) as `<img>`; do not extract a frame, fabricate a URL, or silently embed an unapproved clip.
|
||||
|
||||
## 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 the assembled-project `lint` / `check` gate can miss an unwired 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.
|
||||
Prefix authored ids and globally reusable class names with `<frame_id>-` so sibling frames assembled from parallel workers cannot collide. Contract selectors such as `#root` and `.clip` are the only exceptions.
|
||||
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` / `check` 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.
|
||||
Brand text comes from your frame's `scene` / narrative — never from `frame.md` (a style spec, not the product's content). Place the named assets, and never invent new ones.
|
||||
|
||||
Reference in New Issue
Block a user