mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +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
@@ -23,7 +23,7 @@ Workflow: Step 0 setup → `hyperframes.json`; Step 1 ingest → `capture/extrac
|
||||
|
||||
Goal: Enter with a confirmed brief — including the **PR reference** (a full URL, an `<owner>/<repo>#<N>` ref, or "this PR" in a checked-out repo) — create the HyperFrames project, and make the brief durable. The style is always **claude** (fixed at Step 2, never asked).
|
||||
|
||||
**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, and conducts this route's questions — including the PR-size → length doctrine, which lives whole in `../hyperframes/references/route-briefs.md` § /pr-to-video — then 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, and conducts this route's questions — including the PR-size → length doctrine, which lives whole in `../hyperframes/references/routes/pr-to-video.md` — then hands back the locked brief. Edit requests skip all of this — go do the edit.
|
||||
|
||||
Resolve the project directory before doing any other work. Preserve a user-supplied project directory; otherwise use the durable external cache location printed by the resolver. Never create `videos/` in the caller repository:
|
||||
|
||||
@@ -53,7 +53,7 @@ Every relative-path command below runs with `$PROJECT_DIR` as its working direct
|
||||
- **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 PR ref is captured in the brief; the preference-backed answers were recorded (brief contract § 2); sign-in status was shown (signed in, or continuing offline).
|
||||
|
||||
@@ -111,7 +111,7 @@ The script copies the claude preset's `FRAME.md` → `frame.md`, remixes it onto
|
||||
|
||||
Goal: Turn the PR into an approved frame-by-frame explanation 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 the PR archetype (changelog / feature-reveal / fix-explainer / refactor-walkthrough), the PR-native frame types, hook, persuasion, beats, the per-frame word budget, and the credits close. The sequence comes from **narrative design, not the diff's file order** — explain the change, don't read the diff aloud. As a **soft guide**, consult the role→blueprint menu in `../hyperframes-animation/blueprints-index.md`: for each beat, write the voiceover in the shape its candidate blueprint implies and tag that candidate `blueprint:` id when one fits (story truth still decides which beats exist — never force a beat to fit a shape). Feature 2–4 real diff hunks (from `capture/diff.patch`), each a small legible snippet; name the `code-*` block each wants in the frame's `scene`. Frames carry no `asset_candidates` except the `credits` close (1–6 `assets/<login>.png` avatars). Use the exact required fields from the storyboard and script references.
|
||||
|
||||
@@ -175,17 +175,17 @@ Duration sync is mechanical: real voice duration wins; silent frames keep estima
|
||||
|
||||
`for b in <each registry block named in the storyboard>; do npx hyperframes add "$b"; done`
|
||||
|
||||
Before dispatch, read `sub-agents/frame-worker.md` and `../hyperframes-core/references/subagent-dispatch.md`. Build bounded packets:
|
||||
Before dispatch, read `../hyperframes-core/references/subagent-dispatch.md`. Build bounded packets and the worker role payload:
|
||||
|
||||
```bash
|
||||
node <SKILL_DIR>/scripts/frame-packets.mjs --project "$PROJECT_DIR" --storyboard "$PROJECT_DIR/STORYBOARD.md"
|
||||
```
|
||||
|
||||
The packet builder hard-fails a code frame without the upstream-selected `### Source excerpt`, and hard-caps packet bytes. Dispatch **at most three workers total**, balanced across the packet paths; each worker may build multiple assigned frames sequentially and reads shared instructions once. Workers read only their packet(s) and `frame.md`. They never open the full `STORYBOARD.md`, `capture/diff.patch`, or `capture/extracted/visible-text.txt`. Each worker writes only its assigned `compositions/frames/NN-*.html`; workers never edit `STORYBOARD.md`. When a frame has a **confirmed sketch** on disk (collaborative runs — review loop § 3), say so in that worker's dispatch context: the sketch is the existing `compositions/frames/NN-*.html`, and the worker dresses that layout rather than redrawing it (frame-worker § When a confirmed sketch exists).
|
||||
The packet builder hard-fails a code frame without the upstream-selected `### Source excerpt`, and hard-caps packet bytes. It also writes `_role.md` (`../hyperframes-core/references/frame-worker-core.md` + this skill's `sub-agents/frame-worker.md`, concatenated verbatim — the complete worker role). Dispatch **at most three workers total**, balanced across the packet paths; each worker's prompt carries `_role.md` and its assigned packet paths — paste the role in full or hand its path (equivalent; the worker starts from exactly those documents) — and each worker may build multiple assigned frames sequentially, reading the role once. Workers read only their packet(s) and `frame.md`. They never open the full `STORYBOARD.md`, `capture/diff.patch`, or `capture/extracted/visible-text.txt`. Each worker writes only its assigned `compositions/frames/NN-*.html`; workers never edit `STORYBOARD.md`. When a frame has a **confirmed sketch** on disk (collaborative runs — review loop § 3), say so in that worker's dispatch context: the sketch is the existing `compositions/frames/NN-*.html`, and the worker dresses that layout rather than redrawing it (frame-worker core § When a confirmed sketch exists).
|
||||
|
||||
On a failed frame, re-dispatch **that frame only**, with its existing packet plus the exact validator/lint finding. One retry maximum. Do not replay a whole batch and do not retry without a concrete finding.
|
||||
|
||||
**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`.)
|
||||
**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, mark that frame `animated` in `STORYBOARD.md`.
|
||||
|
||||
@@ -263,6 +263,7 @@ The reusable, domain-agnostic shot shapes live in `../hyperframes-animation/blue
|
||||
| `[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. |
|
||||
| `[../hyperframes-creative/frame-presets/claude/FRAME.md](../hyperframes-creative/frame-presets/claude/FRAME.md)` | Step 2: the claude preset (fixed style). |
|
||||
|
||||
@@ -324,6 +324,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);
|
||||
|
||||
@@ -568,7 +595,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}`);
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
// Thin wrapper over the shared packet builder in hyperframes-core — this file pins
|
||||
// this workflow's paths plus its two behavioral differences: a code frame must carry
|
||||
// an upstream-selected `### Source excerpt`, and code frames get a code-vocabulary
|
||||
// excerpt appended to their packet. Everything else has one owner:
|
||||
// ../../hyperframes-core/scripts/lib/frame-packets-core.mjs
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const SKILL_DIR = resolve(SCRIPT_DIR, "..");
|
||||
const ANIMATION_DIR = resolve(SKILL_DIR, "../hyperframes-animation");
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as core from "../../hyperframes-core/scripts/lib/frame-packets-core.mjs";
|
||||
|
||||
function field(block, name) {
|
||||
const match = block.match(new RegExp(`^-\\s+${name}:\\s*(.+)$`, "im"));
|
||||
return match?.[1]?.trim() ?? null;
|
||||
}
|
||||
|
||||
function splitFrames(storyboard) {
|
||||
const matches = [...storyboard.matchAll(/^## Frame\s+([^\n]+)$/gm)];
|
||||
return matches.map((match, index) => {
|
||||
const start = match.index;
|
||||
const end = matches[index + 1]?.index ?? storyboard.length;
|
||||
return {
|
||||
heading: match[1].trim(),
|
||||
block: storyboard.slice(start, end).trim(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function frameId(frame) {
|
||||
const src = field(frame.block, "src");
|
||||
if (!src) throw new Error(`${frame.heading}: missing src`);
|
||||
return basename(src).replace(/\.html?$/i, "");
|
||||
}
|
||||
const SKILL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
function sourceExcerpt(block) {
|
||||
const match = block.match(/^### Source excerpt\s*\n+(```[^\n]*\n[\s\S]*?\n```)/im);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function selectedFile(path, heading) {
|
||||
if (!path || !existsSync(path)) return "";
|
||||
return `\n## ${heading}\n\n${readFileSync(path, "utf8").trim()}\n`;
|
||||
function validateFrame(frame) {
|
||||
const codeFrame = /\bcode-[a-z0-9-]+\b/i.test(core.field(frame.block, "focal") ?? "");
|
||||
if (codeFrame && !sourceExcerpt(frame.block)) {
|
||||
throw new Error(`${frame.heading}: code frame requires an upstream-selected Source excerpt`);
|
||||
}
|
||||
}
|
||||
|
||||
function codeVocabularySection(block) {
|
||||
const focal = field(block, "focal") ?? "";
|
||||
const focal = core.field(block, "focal") ?? "";
|
||||
const codeId = focal.match(/\b(code-[a-z0-9-]+)\b/i)?.[1];
|
||||
if (!codeId) return "";
|
||||
const vocabPath = join(SKILL_DIR, "references", "code-vocabulary.md");
|
||||
@@ -59,84 +43,20 @@ function codeVocabularySection(block) {
|
||||
return `\n## Code block excerpt (${codeId})\n\n${matchingLines.join("\n").trim()}\n`;
|
||||
}
|
||||
|
||||
function resourceSections(block) {
|
||||
let sections = "";
|
||||
const blueprint = field(block, "blueprint");
|
||||
if (blueprint && blueprint.toLowerCase() !== "compose") {
|
||||
sections += selectedFile(
|
||||
join(ANIMATION_DIR, "blueprints", `${blueprint}.md`),
|
||||
`Selected blueprint: ${blueprint}`,
|
||||
);
|
||||
}
|
||||
const rules = (field(block, "rules") ?? "")
|
||||
.split(/[,\s]+/)
|
||||
.map((rule) => rule.trim())
|
||||
.filter(Boolean);
|
||||
for (const rule of rules) {
|
||||
sections += selectedFile(
|
||||
join(ANIMATION_DIR, "rules", `${rule}.md`),
|
||||
`Selected motion rule: ${rule}`,
|
||||
);
|
||||
}
|
||||
return sections;
|
||||
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"),
|
||||
validateFrame,
|
||||
extraSections: codeVocabularySection,
|
||||
};
|
||||
|
||||
export function buildRolePayload({ outDir }) {
|
||||
return core.buildRolePayload({ ...CONFIG, outDir });
|
||||
}
|
||||
|
||||
const COMPACT_CONTRACT = `- Output exactly one bare \`<template>…</template>\` fragment; never emit DOCTYPE, html, head, or body.
|
||||
- The first composition root must carry the exact frame id, positive duration, width, and height.
|
||||
- Register exactly one paused GSAP timeline under the exact frame id.
|
||||
- Write only the requested frame file. Do not read the full PR diff or the full storyboard.`;
|
||||
|
||||
export function buildFramePackets({
|
||||
projectDir,
|
||||
storyboardPath = join(projectDir, "STORYBOARD.md"),
|
||||
outDir = join(projectDir, ".hyperframes", "frame-packets"),
|
||||
maxPacketBytes = 48_000,
|
||||
}) {
|
||||
const storyboard = readFileSync(storyboardPath, "utf8");
|
||||
const frames = splitFrames(storyboard);
|
||||
if (frames.length === 0) throw new Error("STORYBOARD.md has no frame blocks");
|
||||
|
||||
const packets = frames.map((frame) => {
|
||||
const id = frameId(frame);
|
||||
const codeFrame = /\bcode-[a-z0-9-]+\b/i.test(field(frame.block, "focal") ?? "");
|
||||
const excerpt = sourceExcerpt(frame.block);
|
||||
if (codeFrame && !excerpt) {
|
||||
throw new Error(`${frame.heading}: code frame requires an upstream-selected Source excerpt`);
|
||||
}
|
||||
const packet = `# Frame packet: ${id}\n\n## Structural contract\n\n${COMPACT_CONTRACT}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n- Design tokens: ${join(resolve(projectDir), "frame.md")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block)}${codeVocabularySection(frame.block)}`;
|
||||
const bytes = Buffer.byteLength(packet);
|
||||
if (bytes > maxPacketBytes) {
|
||||
throw new Error(`${id}: frame packet is ${bytes} bytes (limit ${maxPacketBytes})`);
|
||||
}
|
||||
return { frameId: id, path: join(outDir, `${id}.md`), bytes, packet };
|
||||
});
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
for (const { path, packet } of packets) writeFileSync(path, packet);
|
||||
return packets.map(({ packet: _packet, ...result }) => result);
|
||||
export function buildFramePackets(options) {
|
||||
return core.buildFramePackets({ ...CONFIG, ...options });
|
||||
}
|
||||
|
||||
function flag(argv, name, fallback) {
|
||||
const index = argv.indexOf(`--${name}`);
|
||||
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
const projectDir = resolve(flag(argv, "project", "."));
|
||||
try {
|
||||
const packets = buildFramePackets({
|
||||
projectDir,
|
||||
storyboardPath: resolve(flag(argv, "storyboard", join(projectDir, "STORYBOARD.md"))),
|
||||
outDir: resolve(flag(argv, "out-dir", join(projectDir, ".hyperframes", "frame-packets"))),
|
||||
});
|
||||
console.log(`✓ frame packets: ${packets.length} bounded packet(s)`);
|
||||
for (const packet of packets)
|
||||
console.log(` ${packet.frameId}: ${packet.bytes} bytes → ${packet.path}`);
|
||||
} catch (error) {
|
||||
console.error(`✗ frame packets: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathToFileURL(process.argv[1] ?? "").href === import.meta.url) main();
|
||||
if (core.isMainModule(import.meta.url)) core.runCli({ buildFramePackets, buildRolePayload });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { realpathSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export function hasCliCommand(helpText, command) {
|
||||
@@ -35,4 +36,16 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (pathToFileURL(process.argv[1] ?? "").href === import.meta.url) main();
|
||||
// realpath both sides: on macOS /tmp → /private/tmp, and node resolves the main
|
||||
// module's symlinks in import.meta.url while argv[1] keeps the invoked spelling —
|
||||
// a raw compare silently skips main() when invoked through any symlinked path.
|
||||
function isMainModule() {
|
||||
if (!process.argv[1]) return false;
|
||||
try {
|
||||
return pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMainModule()) main();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { realpathSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function safeSegment(value) {
|
||||
@@ -76,4 +77,16 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (pathToFileURL(process.argv[1] ?? "").href === import.meta.url) main();
|
||||
// realpath both sides: on macOS /tmp → /private/tmp, and node resolves the main
|
||||
// module's symlinks in import.meta.url while argv[1] keeps the invoked spelling —
|
||||
// a raw compare silently skips main() when invoked through any symlinked path.
|
||||
function isMainModule() {
|
||||
if (!process.argv[1]) return false;
|
||||
try {
|
||||
return pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMainModule()) main();
|
||||
|
||||
@@ -100,6 +100,10 @@ test("#1092 packets contain selected excerpts but never the full diff", () => {
|
||||
assert.doesNotMatch(codePacket, /code-scroll/);
|
||||
assert.ok(Buffer.byteLength(codePacket) < 32_000);
|
||||
assert.ok(result.every((packet) => packet.path.endsWith(".md")));
|
||||
|
||||
const role = readFileSync(join(project, ".hyperframes", "frame-packets", "_role.md"), "utf8");
|
||||
assert.match(role, /# Frame worker — core contract/);
|
||||
assert.match(role, /# Frame worker — PR-to-video delta/);
|
||||
});
|
||||
|
||||
test("packet validation is atomic and leaves no partial output on overflow", () => {
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
# Frame worker — PR-to-video per-frame composition author
|
||||
# Frame worker — PR-to-video delta
|
||||
|
||||
> You build the small batch of frame composition files assigned to you and nothing else. At most three workers run; each reads shared context once, then builds its packet paths sequentially. The **structural composition contract** is compacted into each packet. This file carries only what's specific to a PR-to-video frame.
|
||||
> 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 PR-to-video frame.
|
||||
|
||||
**INPUT** — your dispatch context provides `PROJECT_DIR` plus one or more bounded packet paths under `.hyperframes/frame-packets/`. Read shared `frame.md` once, then process the packets in order. Never open the full `STORYBOARD.md`, `capture/diff.patch`, or `capture/extracted/visible-text.txt`; the orchestrator already selected the exact source excerpt and put it in each code frame's packet.
|
||||
## Batch dispatch — you build a small packet batch
|
||||
|
||||
Each packet provides:
|
||||
At most three workers run; your dispatch assigns **one or more** bounded packet paths under `.hyperframes/frame-packets/`. Read this role and shared `frame.md` **once**, then process the packets in order — for each, use its exact frame block, inlined blueprint / rule excerpts, and (for a code beat) the selected code-block / source excerpts. Never open the full `STORYBOARD.md`, `capture/diff.patch`, or `capture/extracted/visible-text.txt`; the orchestrator already selected the exact source excerpt and put it in each code frame's packet. After the last assigned file passes the self-check, stop.
|
||||
|
||||
- `PROJECT_DIR` — the project root; all paths are relative to it.
|
||||
- `frame_id` — e.g. `04-the-fix`. Use it **verbatim** as the composition id, the `window.__timelines` key, and the file name (`compositions/frames/04-the-fix.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 exact **`## Frame N` block** (already extracted from `STORYBOARD.md`; 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 **domain-agnostic shot template** this frame instantiates — the overall shape + its signature move. Read it for the shape; `compose` means there's no template (common for a code beat — the `code-*` block is the shape), sequence the shot from the Scene lines directly.
|
||||
- `focal:` — for a concept/mechanism beat, which **invented** element is the hero; for a **code beat**, the named **`code-*` block** (+ the hunk); for the **credits** close, the avatar row.
|
||||
- `roles:` — each element's role: `foreground subject` / `background` full-bleed / `supporting`. Most are invented elements you design; the only real assets are the credits `assets/<login>.png` avatars.
|
||||
- `sfx:` — the orchestrator's; you mount no audio.
|
||||
- `frame.md` (project root) — the **design-truth**: palette (claude), 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. The blueprint templates are the sibling `../hyperframes-animation/blueprints/<id>.md`; an optional runnable demo is `../hyperframes-animation/examples/<id>.html`.
|
||||
- `code-vocabulary.md` — absolute path provided in your dispatch. For a **code beat**, read it for the named `code-*` block's exact inputs (`window.__TOKENS`, `window.__BLOCK`, line indexing).
|
||||
- `../references/cut-catalog.md` — the **cut catalog** (zoom-through / inverse / cut-the-curve / waterfall) for a within-frame seam. 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).
|
||||
Extra inputs beyond the core contract:
|
||||
|
||||
**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.
|
||||
|
||||
**OUTPUT** — one `compositions/frames/<frame_id>.html` per assigned packet, each a bare template fragment. The first non-whitespace bytes are `<template`; the last are `</template>`. Never emit `<!doctype>`, `<html>`, `<head>`, `<body>`, or any markup outside that single template. After the last assigned file passes the self-check, stop — you do not edit `STORYBOARD.md`, mint audio, assemble the index, run the CLI, or report back. The orchestrator validates the files before assembly and marks their `status`.
|
||||
|
||||
## 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 `code-*` block where the sketch held plain code, the finished invented visuals where it used plain 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.
|
||||
- `code-vocabulary.md` — absolute path provided in your dispatch. For a **code beat**, read it for the named `code-*` block's exact inputs (`window.__TOKENS`, `window.__BLOCK`, line indexing); your packet carries the matching excerpt.
|
||||
- `focal:` — for a concept/mechanism beat, which **invented** element is the hero; for a **code beat**, the named **`code-*` block** (+ the hunk); for the **credits** close, the avatar row.
|
||||
- `roles:` — each element's role: `foreground subject` / `background` full-bleed / `supporting`. Most are invented elements you design; the only real assets are the credits `assets/<login>.png` avatars.
|
||||
|
||||
## Mostly invented — you build the visual (except code blocks + the credits avatars)
|
||||
|
||||
@@ -38,64 +18,21 @@ A PR video is **mostly invented**: there are **no screenshots and no captured UI
|
||||
|
||||
## PR code beats, mechanism beats + the credits close
|
||||
|
||||
- **Code beats (`diff` / `before_after` / a new-code reveal) — use the named `code-*` block, don't hand-build code motion.** Your `## Frame N` `scene` / `focal` names which block (e.g. `code-diff`, `code-morph`, `code-typing`); the orchestrator has already installed it (Step 5 pre-install). Read **`code-vocabulary.md`** (path in your dispatch) for that block's exact inputs, then:
|
||||
- **Code beats (`diff` / `before_after` / a new-code reveal) — use the named `code-*` block, don't hand-build code motion.** Your `## Frame N` `scene` / `focal` names which block (e.g. `code-diff`, `code-morph`, `code-typing`); the orchestrator has already installed it (pre-install step). Read the `code-vocabulary.md` excerpt in your packet for that block's exact inputs, then:
|
||||
- Use only the packet's `### Source excerpt`. It is the real before/after hunk selected upstream. Never reopen the full diff or brief.
|
||||
- Fill the block's `window.__TOKENS` with that real code (the baked Shiki tokens) and set `window.__BLOCK` (effect, `line`, `duration`) **so the full block completes within the frame's `data-duration`** — a long snippet at the block's default per-character cadence overruns a short frame (the code never finishes typing). `code-diff` / `code-morph` need **2 states** (before, after); the others take one. **Line indexing differs — `code-highlight` is 0-based, `code-scroll` 1-based** — don't off-by-one.
|
||||
- Integrate the filled block as **this frame's composition** per `hyperframes-core`'s sub-composition contract: its `data-composition-id` and its `window.__timelines[...]` key must both be your **`<frame_id>`** (the block ships its own id + paused timeline; rename both to match the frame contract). The block already renders an editor window (titlebar / filename) reading as claude's navy **Code Surface** — set the filename + any `+N/−M` chrome from the `scene`.
|
||||
- Integrate the filled block as **this frame's composition** per the core sub-composition contract: its `data-composition-id` and its `window.__timelines[...]` key must both be your **`<frame_id>`** (the block ships its own id + paused timeline; rename both to match the frame contract). The block already renders an editor window (titlebar / filename) reading as claude's navy **Code Surface** — set the filename + any `+N/−M` chrome from the `scene`.
|
||||
- **The block owns the code animation; your Scene windows choreograph the surrounding Code Surface** — the navy window seating in, the file header typing on, the camera settling onto the hunk, a coral underline on the landed line. **Do not re-specify the code motion** (the block is the development beat). A code beat is usually `blueprint: compose`.
|
||||
- **The block has no caption-safe band.** When `Captions: enabled`, inset/scale the code panel into the top ~83% so it clears the keep-out band; never let code run under the caption pill.
|
||||
- **Mechanism beats (`mechanism`) — build an invented animated diagram of the behavior; the build _is_ the shot.** This is the "show what the change does at runtime" frame (the request retrying, the cache filling, serial→parallel, the race resolved) — read its `scene` for the behavior to animate. Unlike a code beat, **the motion is yours to author** (no block owns it):
|
||||
- If the `scene` names a `flowchart` / `flowchart-vertical` / `data-chart` block, the orchestrator pre-installed it — fill + mount it like a code block (its `data-composition-id` and `window.__timelines[...]` key both become your `<frame_id>`). Otherwise **hand-build the diagram in SVG / HTML / GSAP** from `frame.md`'s atoms.
|
||||
- **Claude register:** hairline-ink nodes / edges / lanes on the cream ground, **one coral marker** on the active / changed element, mono labels — **not** the navy code surface (that's for code), no heavy shapes / bokeh.
|
||||
- **Choreograph the Scene windows:** the nodes / lanes draw on (Scene 1); **the flow runs** as the VO names each step (middle Scenes — the request hops, the lane splits, the front advances, the bars race) — this _is_ the teaching, so it must play across the shot, never enter-then-freeze; the resolved state + the one coral emphasis lands (final Scene). Keep it in the top ~83% (caption keep-out).
|
||||
- **The `credits` close — the one frame with real assets.** Its `asset_candidates` names 2–6 `assets/<login>.png` avatars (downloaded in Step 1). Render them as `<img>` in hairline-ringed chips — an avatar row with each contributor's name + role in mono (an "approved" mark if the close calls for it), staggered in across the Scene windows. Avatars appear **only** here, never decorating a code frame.
|
||||
- **The `credits` close — the one frame with real assets.** Its `asset_candidates` names 2–6 `assets/<login>.png` avatars (downloaded upstream). Render them as `<img>` in hairline-ringed chips — an avatar row with each contributor's name + role in mono (an "approved" mark if the close calls for it), staggered in across the Scene windows. Avatars appear **only** here, never decorating a code frame.
|
||||
|
||||
## You do NOT decide
|
||||
## PR-specific self-check additions
|
||||
|
||||
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 shot 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 content. Visible text comes from your frame's `scene` / narrative (and the real code, for a code beat).
|
||||
- **Which motions exist** — named upstream in your block (the shot sequence's motion verbs + `blueprint:` + the `code-*` block). Implement them; don't invent new ones.
|
||||
- **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 **PR-to-video 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, code panel, diagram, labels) 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 the hero / code panel / diagram toward full-bleed. (Landscape's region is short, so vertical centering near 0.42 × height is fine.)
|
||||
- **Visible text is short motion-graphics copy** — a hero word, a stat (`"+1,204"`, `"2× faster"`), a file/label — never a sentence from the narration. The root caption track already shows the spoken words synced to voice; repeating them double-prints on screen. (Real code inside a `code-*` block is the exception — that is the content, not narration.)
|
||||
- **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 file chip, the hunk, a node, a stat — **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 (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.
|
||||
- **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. `compose` → no template; sequence the shot straight from the Scene lines (a code beat composes the surround around the block). Whichever, never front-load the whole sequence at `t=0`.
|
||||
- **Realize each element by its `roles`** (the `focal` is the hero): a `foreground subject` is the thing the eye lands on — respect the 83% keep-out and lay text around it; a `background` is a full-bleed field / gradient / grid dimmed ~30–50% so foreground content stays legible; `supporting` elements are labels, secondary shapes, ambient layers. Invented elements are HTML/CSS/SVG you build; a `code-*` block / `flowchart` / `data-chart` is filled + mounted per the section above; the credits avatars render as `<img>`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read once, then build sequentially** — read the compact structural contract and `frame.md` once. For each assigned packet, use its exact frame block, selected blueprint/rule excerpts, and (for code) selected code-block/source excerpts. Do not reopen the source documents those excerpts came from. 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>`.
|
||||
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. Build the invented hero (diagram / type / number-lockup), or fill + mount the `code-*` block and choreograph the surround.
|
||||
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 continue/finish** — re-read that frame file against the checklist below and fix in place. Continue to the next assigned packet; after the final file, stop. 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` — the entire file is exactly one bare `<template>…</template>` fragment (no DOCTYPE/full document); its first element carries `data-composition-id="<frame_id>"` and a positive `data-duration` matching the packet.
|
||||
- **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`).
|
||||
- The composition root also carries a **positive `data-duration` matching the packet**.
|
||||
- **Code-block cadence fits `data-duration`** — for a code beat, the `code-*` block's internal cadence is set so the full block completes within the frame's `data-duration` (a long snippet at the default per-character speed overruns — the code never finishes and the chrome beats never play; see `code-vocabulary.md`).
|
||||
- `font_family_without_font_face` — every font you name has a matching local `@font-face` **inside this template**. Copy the auto-generated block from `frame.md`; the Claude preset's EB Garamond, Inter, and JetBrains Mono faces live in `assets/fonts/`. Never link Google Fonts and never name a family without a project file, including system CJK / Japanese / Devanagari families (`Hiragino Sans`, `Yu Gothic`, `Noto Sans CJK`, `Noto Sans Devanagari`, …): the render machine is clean headless Chrome, so absent faces silently fall back. For non-Latin visible text, use a shipped face that covers the script or romanize/transliterate it; otherwise it is out of scope.
|
||||
- **Keep-out + no-narration-text** (eyeball, no code) — nothing sits below the 83% cutoff; no narration sentence is rendered as visible text.
|
||||
- Fonts: copy the auto-generated `@font-face` block from `frame.md`; the Claude preset's EB Garamond, Inter, and JetBrains Mono faces live in `assets/fonts/`. Never link Google Fonts.
|
||||
- Visible-text exception: real code inside a `code-*` block is the content, not narration.
|
||||
|
||||
Reference in New Issue
Block a user