hyperframes 0.8.37 → 0.8.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +118 -142
- package/dist/docs/examples.md +1 -1
- package/dist/fontLocalizeCli.js +3 -92
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/skills/hyperframes/SKILL.md +1 -1
- package/dist/skills/hyperframes/references/brief-contract.md +108 -0
- package/dist/skills/hyperframes/references/brief-format.md +72 -0
- package/dist/skills/hyperframes/references/capability-menu.md +1 -1
- package/dist/skills/hyperframes/references/frame-worker-core.md +80 -0
- package/dist/skills/hyperframes/references/intent-interview.md +8 -8
- package/dist/skills/hyperframes/references/production-loop.md +26 -0
- package/dist/skills/hyperframes/references/review-loop.md +41 -0
- package/dist/skills/hyperframes/references/route-briefs.md +1 -1
- package/dist/skills/hyperframes/references/script-format.md +52 -0
- package/dist/skills/hyperframes/references/storyboard-format.md +130 -0
- package/dist/skills/hyperframes/references/subagent-dispatch.md +35 -0
- package/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +212 -0
- package/dist/skills/hyperframes-cli/SKILL.md +9 -8
- package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +6 -6
- package/dist/skills/hyperframes-cli/references/preview-render.md +6 -3
- package/dist/studio/assets/{hyperframes-player-DrGk-lf5.js → hyperframes-player-otov4oIw.js} +1 -1
- package/dist/studio/assets/{index-xK1YGiPD.js → index-B-FzDhAR.js} +1 -1
- package/dist/studio/assets/{index-BtGF7Kl-.js → index-Bw6cyxOm.js} +1 -1
- package/dist/studio/assets/{index-DMsdtB-j.js → index-DWFEyVJZ.js} +4 -4
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +1 -1
- package/dist/studio/index.js.map +1 -1
- package/dist/templates/blank/index.html +17 -37
- package/dist/templates/from-file/index.html +74 -0
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Storyboard format — `STORYBOARD.md` + parsed manifest
|
|
2
|
+
|
|
3
|
+
Defines the storyboard's **base data format** only: the `STORYBOARD.md` file shape and the `StoryboardManifest` it parses into. How a workflow _generates_ a storyboard lives in that workflow; the optional narration/TTS file (`SCRIPT.md`) is a separate concern owned by the TTS step, not here.
|
|
4
|
+
|
|
5
|
+
A storyboard is the **plan layer** for a video — an ordered set of **frames** (key moments) in one markdown file. HyperFrames Studio renders it as a contact sheet (the Storyboard view, available by default in every Studio session). Parser: `@hyperframes/core/storyboard` → `StoryboardManifest`; read API: `GET /api/projects/<id>/storyboard`.
|
|
6
|
+
|
|
7
|
+
## Frontmatter (global direction)
|
|
8
|
+
|
|
9
|
+
YAML block at the top. Unknown keys are kept under `globals.extra`.
|
|
10
|
+
|
|
11
|
+
| Key | Meaning | Example |
|
|
12
|
+
| ---------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
|
13
|
+
| `format` | Canvas size | `1920x1080` |
|
|
14
|
+
| `duration` | The brief's rough length expectation (advisory, not a hard limit) | `22s` |
|
|
15
|
+
| `message` | One-line thesis | `Ship a launch video in an afternoon` |
|
|
16
|
+
| `arc` | Narrative arc | `Hook → Problem → Solution → Proof → CTA` |
|
|
17
|
+
| `audience` | Who it's for | `indie devs on X` |
|
|
18
|
+
| `mode` | Interaction mode (see `brief-contract.md`; default collaborative) | `autonomous` |
|
|
19
|
+
|
|
20
|
+
Set `duration` from the brief's `length` when the storyboard is first written. It is an expectation, not a gate: assembly reports where the cut actually lands against it and flags a large gap — judge whether the drift serves the piece, and update the value when the intended length genuinely changes.
|
|
21
|
+
|
|
22
|
+
## Per-frame sections
|
|
23
|
+
|
|
24
|
+
One `## Frame N — Title` heading per frame (`Frame` / `Beat` / `Scene` accepted at H2/H3). Metadata as `- key: value` bullets; everything below them until the next heading is the free-form **narrative**.
|
|
25
|
+
|
|
26
|
+
| Key | Meaning |
|
|
27
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `status` | `outline` → `built` → `animated` (defaults `outline`) |
|
|
29
|
+
| `src` | project-relative path to the frame's HTML sub-composition (the tile poster renders from it) |
|
|
30
|
+
| `duration` | e.g. `4s` |
|
|
31
|
+
| `transition_in` | `crossfade` / `cut` / `wipe` … (alias `transition`) |
|
|
32
|
+
| `scene` | one-line contact-sheet caption (aliases `description` / `summary` / `caption`) |
|
|
33
|
+
| `voiceover` | the frame's narration _guide_ (aliases `vo` / `voice_over` / `narration`) |
|
|
34
|
+
| `poster` | seconds to seek for the tile poster (past the intro animation) |
|
|
35
|
+
| _any other key_ | kept verbatim under the frame's `extra` — a workflow carries its own per-frame data (effects, assets, …) here |
|
|
36
|
+
|
|
37
|
+
## Parsed manifest
|
|
38
|
+
|
|
39
|
+
The parser is **lenient**: it never throws and records anything surprising as a `warning`.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
StoryboardManifest {
|
|
43
|
+
globals: { format?, message?, arc?, audience?, extra: {…} }
|
|
44
|
+
frames: Array<{
|
|
45
|
+
index, number?, title?,
|
|
46
|
+
status, // "outline" | "built" | "animated"
|
|
47
|
+
src?, duration? / durationSeconds?, transitionIn?,
|
|
48
|
+
scene?, voiceover?, poster?,
|
|
49
|
+
narrative, // markdown below the metadata
|
|
50
|
+
extra: {…} // unknown keys, preserved
|
|
51
|
+
}>
|
|
52
|
+
warnings: Array<{ message, line?, frameIndex? }>
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The read API also adds `srcExists` per frame and attaches the optional `SCRIPT.md` payload when present.
|
|
57
|
+
|
|
58
|
+
## `SCRIPT.md` (out of scope here)
|
|
59
|
+
|
|
60
|
+
Optional, free-form, **not parsed into the manifest** — the locked-narration file that drives TTS. Its format is defined in `references/script-format.md`, and it is absent for videos with no narration/TTS. The per-frame `voiceover` above is the storyboard's own narration guide.
|
|
61
|
+
|
|
62
|
+
## Frame comments — `.hyperframes/frame-comments.json`
|
|
63
|
+
|
|
64
|
+
The storyboard review's **structured feedback channel** — the file Studio's per-frame comment boxes write on submit (chat feedback follows the same rule — `brief-contract.md` § 1, the comments channel). Like `SCRIPT.md`, it is a sibling of the storyboard, not parsed into the manifest.
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"version": 1,
|
|
69
|
+
"pass": "sketch",
|
|
70
|
+
"submitted_at": "2026-07-09T12:04:00Z",
|
|
71
|
+
"comments": [
|
|
72
|
+
{
|
|
73
|
+
"frame": 3,
|
|
74
|
+
"src": "compositions/frames/03-mechanism.html",
|
|
75
|
+
"title": "Mechanism",
|
|
76
|
+
"text": "Swap the bar chart for a before/after slider."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Field | Meaning |
|
|
83
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `pass` | which review the batch belongs to: `storyboard` (text layer) / `sketch` (static frames) / `final` (assembled video) |
|
|
85
|
+
| `comments[].frame` | the frame's 1-based `index` in the manifest — the key |
|
|
86
|
+
| `comments[].src` `title` | copied from the frame at submit time — if frames get reordered after submit, the mismatch shows |
|
|
87
|
+
| `comments[].text` | the feedback, verbatim |
|
|
88
|
+
|
|
89
|
+
Lifecycle — the whole contract: a workflow finding this file at a checkpoint treats it as the revision feedback — **revise exactly the frames named, delete the file, re-present**. Writers create it only on submit; it never lingers across rounds.
|
|
90
|
+
|
|
91
|
+
## Example
|
|
92
|
+
|
|
93
|
+
```markdown
|
|
94
|
+
---
|
|
95
|
+
format: 1920x1080
|
|
96
|
+
message: "Ship a launch video in an afternoon"
|
|
97
|
+
arc: Hook → Problem → Solution → Proof → CTA
|
|
98
|
+
audience: indie devs on X
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Frame 1 — Hook
|
|
102
|
+
|
|
103
|
+
- scene: Big type punches in on the beat
|
|
104
|
+
- duration: 3s
|
|
105
|
+
- poster: 2s
|
|
106
|
+
- transition_in: cut
|
|
107
|
+
- status: animated
|
|
108
|
+
- voiceover: "Ship a launch video in an afternoon."
|
|
109
|
+
- src: compositions/frames/01-hook.html
|
|
110
|
+
|
|
111
|
+
Open cold on the promise. This is the thesis — everything after pays it off.
|
|
112
|
+
|
|
113
|
+
## Frame 2 — The problem
|
|
114
|
+
|
|
115
|
+
- scene: A 20-minute timer spins on a stack of rejected takes
|
|
116
|
+
- duration: 4s
|
|
117
|
+
- transition_in: crossfade
|
|
118
|
+
- status: built
|
|
119
|
+
- voiceover: "The old way? Prompt, wait twenty minutes, get something that misses."
|
|
120
|
+
- src: compositions/frames/02-problem.html
|
|
121
|
+
|
|
122
|
+
The old way: prompt, wait, get something that misses. Establish the pain we remove.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Notes
|
|
126
|
+
|
|
127
|
+
- A frame with `status: outline` and no built `src` renders as an outline placeholder.
|
|
128
|
+
- `built` is the middle rung: the frame's HTML exists and its **layout is confirmed** (a wireframe sketch or better) — motion not yet added. Studio chips it blue.
|
|
129
|
+
- The process that walks these statuses — plan, sketch, build, each pass reviewed on the board — is `review-loop.md`.
|
|
130
|
+
- Multi-line `voiceover` values collapse to one line on save.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Subagent dispatch — harness adapter
|
|
2
|
+
|
|
3
|
+
The video workflows (`product-launch-video` / `faceless-explainer` / `pr-to-video` / `motion-graphics` / `general-video`) describe subagent dispatch in harness-neutral verbs. This file maps those verbs to the primitives of whatever agent harness you are running on. Read it once per run, before the first dispatch; everything else in the workflows (dispatch packets, file artifacts, exit-code gates, Resume tables) is harness-independent and needs no translation.
|
|
4
|
+
|
|
5
|
+
## The contract (identical on every harness)
|
|
6
|
+
|
|
7
|
+
- **DISPATCH(role_file, dispatch_context)** — start one child agent whose prompt is the **full contents of the named role file** (a builder-assembled payload like `.hyperframes/frame-packets/_role.md`, or a workflow's `sub-agents/<role>.md`) followed by the `## Dispatch context` block from the workflow, copied **verbatim** (never digested or paraphrased). Every harness below accepts arbitrary task text, so this works everywhere; never rely on the child seeing your conversation, memory, or skills — the prompt and the files on disk are its entire world.
|
|
8
|
+
- **Parallel fan-out** — when a step says "start N workers in parallel", the workers are mutually independent (no ordering, no shared state beyond the filesystem). Run as many concurrently as your harness allows.
|
|
9
|
+
- **WAIT** — a step's completion criterion is always **the expected artifact existing on disk** (e.g. `compositions/<scene-id>.html`), never the harness's completion notification (some harnesses deliver results best-effort). After waiting, verify the artifacts; a missing artifact means that child failed — re-dispatch it once with the same prompt before surfacing an error.
|
|
10
|
+
|
|
11
|
+
## Concurrency cap → batching rule (cap never changes scope)
|
|
12
|
+
|
|
13
|
+
A harness concurrency limit **reduces parallelism, not work**: every scene still gets built, one scene per dispatch, with the available slots chewing through the full list.
|
|
14
|
+
|
|
15
|
+
- When the harness queues excess children internally, submit **all N at once** and let the queue drain.
|
|
16
|
+
- Harness hard-caps active children (e.g. OpenClaw `maxChildrenPerAgent`) → dispatch in **waves of the cap size**: start `cap` workers, wait for their artifacts, start the next wave, until all N scenes exist. Example: 9 scenes on a cap-3 harness = 3 waves of 3 — never drop scenes, never merge scenes into one worker to fit the cap.
|
|
17
|
+
|
|
18
|
+
## Harness mapping
|
|
19
|
+
|
|
20
|
+
Use the current harness's native delegation and waiting tools when they are available. The workflow contract stays the same:
|
|
21
|
+
|
|
22
|
+
- **DISPATCH** sends the complete role file and dispatch context to one worker.
|
|
23
|
+
- **Parallel fan-out** starts independent workers concurrently up to the harness limit.
|
|
24
|
+
- **WAIT** verifies the expected artifacts on disk, not only a completion notification.
|
|
25
|
+
- **Re-dispatch** starts a fresh worker with the same context plus the gate failure.
|
|
26
|
+
|
|
27
|
+
When native delegation is unavailable, use the existing fallback ladder: launch headless CLI workers that share the project filesystem, then fall back to inline serial execution.
|
|
28
|
+
|
|
29
|
+
On Codex, native delegation requires the user's explicit permission. Fold a one-line request into the workflow's first existing user pause before dispatch; a standing grant in `AGENTS.md` or the kickoff prompt also counts. Without it, use the fallback ladder rather than silently skipping work.
|
|
30
|
+
|
|
31
|
+
## Vocabulary mapping
|
|
32
|
+
|
|
33
|
+
- A request to work "in the background" means dispatch concurrently when the harness supports it.
|
|
34
|
+
- Load a named skill through the harness's skill mechanism, or read `<skills-root>/<skill>/SKILL.md` directly.
|
|
35
|
+
- Map generic read, write, edit, and shell verbs to the current harness's equivalent tools.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Shared frame-packet builder — the script half of the frame-worker core/delta split.
|
|
2
|
+
//
|
|
3
|
+
// Each narrative workflow ships a thin `scripts/frame-packets.mjs` wrapper that pins
|
|
4
|
+
// its own paths (animation skill, role delta, design-truth resolution, extra packet
|
|
5
|
+
// sections) and delegates everything else here, exactly as the markdown half already
|
|
6
|
+
// does with `references/frame-worker-core.md` + each workflow's delta. One owner for
|
|
7
|
+
// the packet-building logic; the wrappers own only what genuinely differs per workflow.
|
|
8
|
+
//
|
|
9
|
+
// Packet (<frame_id>.md) = project inputs + the frame's exact `## Frame N` block
|
|
10
|
+
// + the blueprint body + every cited rule recipe, inlined — so a worker never opens
|
|
11
|
+
// the shared STORYBOARD.md or any skill document. Cited motions are found
|
|
12
|
+
// mechanically: the explicit `- rules:` field when present, plus every valid rule id
|
|
13
|
+
// (a filename under the animation skill's rules/) mentioned in the block.
|
|
14
|
+
//
|
|
15
|
+
// _role.md = frame-worker-core.md + the workflow's sub-agents/frame-worker.md,
|
|
16
|
+
// concatenated verbatim — the complete worker role, assembled from the two source
|
|
17
|
+
// documents so nothing is hand-maintained twice.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { basename, join, resolve } from "node:path";
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
|
|
30
|
+
export function field(block, name) {
|
|
31
|
+
const match = block.match(new RegExp(`^-\\s+${name}:\\s*(.+)$`, "im"));
|
|
32
|
+
return match?.[1]?.trim() ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function splitFrames(storyboard) {
|
|
36
|
+
const matches = [...storyboard.matchAll(/^## Frame\s+([^\n]+)$/gm)];
|
|
37
|
+
return matches.map((match, index) => {
|
|
38
|
+
const start = match.index;
|
|
39
|
+
const end = matches[index + 1]?.index ?? storyboard.length;
|
|
40
|
+
return {
|
|
41
|
+
heading: match[1].trim(),
|
|
42
|
+
block: storyboard.slice(start, end).trim(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function frameId(frame) {
|
|
48
|
+
const src = field(frame.block, "src");
|
|
49
|
+
if (!src) throw new Error(`${frame.heading}: missing src`);
|
|
50
|
+
return basename(src).replace(/\.html?$/i, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function selectedFile(path, heading) {
|
|
54
|
+
if (!path || !existsSync(path)) return "";
|
|
55
|
+
return `\n## ${heading}\n\n${readFileSync(path, "utf8").trim()}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function escapeRegExp(id) {
|
|
59
|
+
return id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function knownRuleIds(animationDir) {
|
|
63
|
+
const rulesDir = join(animationDir, "rules");
|
|
64
|
+
if (!existsSync(rulesDir)) {
|
|
65
|
+
console.warn(
|
|
66
|
+
`frame-packets: no rules dir at ${rulesDir} — packets will inline no motion recipes`,
|
|
67
|
+
);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return readdirSync(rulesDir)
|
|
71
|
+
.filter((name) => name.endsWith(".md"))
|
|
72
|
+
.map((name) => name.replace(/\.md$/, ""));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function citedRules(block, ruleIds) {
|
|
76
|
+
const explicit = (field(block, "rules") ?? "")
|
|
77
|
+
.split(/[,\s]+/)
|
|
78
|
+
.map((rule) => rule.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
const mentioned = ruleIds.filter((id) =>
|
|
81
|
+
new RegExp(`(?<![\\w-])${escapeRegExp(id)}(?![\\w-])`, "i").test(block),
|
|
82
|
+
);
|
|
83
|
+
return [...new Set([...explicit, ...mentioned])].filter((id) => ruleIds.includes(id));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// visual-design.md tells the author to write the blueprint as `<id> (Reproduce)`
|
|
87
|
+
// or `<id> (Adapt)` — the qualifier is direction for the frame worker, not part of
|
|
88
|
+
// the filename. Parse the field into the id it names (or null for `compose`), so
|
|
89
|
+
// no caller ever resolves a raw field value against the blueprints directory.
|
|
90
|
+
export function blueprintId(block) {
|
|
91
|
+
const raw = field(block, "blueprint");
|
|
92
|
+
if (!raw) return null;
|
|
93
|
+
const id = raw.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
94
|
+
return id && id.toLowerCase() !== "compose" ? id : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resourceSections(block, { animationDir, ruleIds, frameId }) {
|
|
98
|
+
let sections = "";
|
|
99
|
+
const blueprint = blueprintId(block);
|
|
100
|
+
if (blueprint) {
|
|
101
|
+
const blueprintsDir = join(animationDir, "blueprints");
|
|
102
|
+
const path = join(blueprintsDir, `${blueprint}.md`);
|
|
103
|
+
// A blueprint that resolved to nothing used to inline an empty string, so the
|
|
104
|
+
// packet shipped without the one document the frame was designed against and
|
|
105
|
+
// the run still reported success. Name it instead — but only when the library
|
|
106
|
+
// is actually there to be named against. The animation skill installs on
|
|
107
|
+
// demand, so an absent blueprints/ is a missing install, not a bad id, and it
|
|
108
|
+
// degrades with a warning exactly like an absent rules/ (see knownRuleIds).
|
|
109
|
+
if (!existsSync(blueprintsDir)) {
|
|
110
|
+
console.warn(
|
|
111
|
+
`frame-packets: no blueprints dir at ${blueprintsDir} — packets will inline no blueprint`,
|
|
112
|
+
);
|
|
113
|
+
} else if (!existsSync(path)) {
|
|
114
|
+
throw new Error(`${frameId ?? "frame"}: blueprint "${blueprint}" has no file at ${path}`);
|
|
115
|
+
} else {
|
|
116
|
+
sections += selectedFile(path, `Selected blueprint: ${blueprint}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const rule of citedRules(block, ruleIds)) {
|
|
120
|
+
sections += selectedFile(
|
|
121
|
+
join(animationDir, "rules", `${rule}.md`),
|
|
122
|
+
`Selected motion rule: ${rule}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return sections;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildRolePayload({ corePath, deltaPath, outDir }) {
|
|
129
|
+
const core = readFileSync(corePath, "utf8").trim();
|
|
130
|
+
const delta = readFileSync(deltaPath, "utf8").trim();
|
|
131
|
+
const role = `${core}\n\n---\n\n${delta}\n`;
|
|
132
|
+
mkdirSync(outDir, { recursive: true });
|
|
133
|
+
const path = join(outDir, "_role.md");
|
|
134
|
+
writeFileSync(path, role);
|
|
135
|
+
return { path, bytes: Buffer.byteLength(role) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildFramePackets({
|
|
139
|
+
projectDir,
|
|
140
|
+
storyboardPath = join(projectDir, "STORYBOARD.md"),
|
|
141
|
+
outDir = join(projectDir, ".hyperframes", "frame-packets"),
|
|
142
|
+
maxPacketBytes = 48_000,
|
|
143
|
+
animationDir,
|
|
144
|
+
corePath,
|
|
145
|
+
deltaPath,
|
|
146
|
+
// Per-workflow hooks (all optional):
|
|
147
|
+
// designTruthLine(projectDir) -> the packet's design-truth input line
|
|
148
|
+
// validateFrame(frame, id) -> throw to reject a frame before packing
|
|
149
|
+
// extraSections(block) -> extra packet sections appended after the rule recipes
|
|
150
|
+
designTruthLine = (dir) => `- Design tokens: ${join(resolve(dir), "frame.md")}`,
|
|
151
|
+
validateFrame,
|
|
152
|
+
extraSections,
|
|
153
|
+
}) {
|
|
154
|
+
const storyboard = readFileSync(storyboardPath, "utf8");
|
|
155
|
+
const frames = splitFrames(storyboard);
|
|
156
|
+
if (frames.length === 0) throw new Error("STORYBOARD.md has no frame blocks");
|
|
157
|
+
const ruleIds = knownRuleIds(animationDir);
|
|
158
|
+
|
|
159
|
+
const packets = frames.map((frame) => {
|
|
160
|
+
const id = frameId(frame);
|
|
161
|
+
if (validateFrame) validateFrame(frame, id);
|
|
162
|
+
const packet = `# Frame packet: ${id}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n${designTruthLine(projectDir)}\n- RULES_DIR: ${join(animationDir, "rules")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block, { animationDir, ruleIds, frameId: id })}${extraSections ? extraSections(frame.block) : ""}`;
|
|
163
|
+
const bytes = Buffer.byteLength(packet);
|
|
164
|
+
if (bytes > maxPacketBytes) {
|
|
165
|
+
throw new Error(`${id}: frame packet is ${bytes} bytes (limit ${maxPacketBytes})`);
|
|
166
|
+
}
|
|
167
|
+
return { frameId: id, path: join(outDir, `${id}.md`), bytes, packet };
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
mkdirSync(outDir, { recursive: true });
|
|
171
|
+
for (const { path, packet } of packets) writeFileSync(path, packet);
|
|
172
|
+
buildRolePayload({ corePath, deltaPath, outDir });
|
|
173
|
+
return packets.map(({ packet: _packet, ...result }) => result);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function flag(argv, name, fallback) {
|
|
177
|
+
const index = argv.indexOf(`--${name}`);
|
|
178
|
+
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// realpath both sides: on macOS /tmp → /private/tmp, and node resolves the main
|
|
182
|
+
// module's symlinks in import.meta.url while argv[1] keeps the invoked spelling —
|
|
183
|
+
// a raw compare silently skips main() when invoked through any symlinked path.
|
|
184
|
+
export function isMainModule(importMetaUrl) {
|
|
185
|
+
if (!process.argv[1]) return false;
|
|
186
|
+
try {
|
|
187
|
+
return pathToFileURL(realpathSync(process.argv[1])).href === importMetaUrl;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function runCli({ buildFramePackets: build, buildRolePayload: buildRole }) {
|
|
194
|
+
const argv = process.argv.slice(2);
|
|
195
|
+
const projectDir = resolve(flag(argv, "project", "."));
|
|
196
|
+
const outDir = resolve(flag(argv, "out-dir", join(projectDir, ".hyperframes", "frame-packets")));
|
|
197
|
+
try {
|
|
198
|
+
const packets = build({
|
|
199
|
+
projectDir,
|
|
200
|
+
storyboardPath: resolve(flag(argv, "storyboard", join(projectDir, "STORYBOARD.md"))),
|
|
201
|
+
outDir,
|
|
202
|
+
});
|
|
203
|
+
const role = buildRole({ outDir });
|
|
204
|
+
console.log(`✓ frame packets: ${packets.length} bounded packet(s)`);
|
|
205
|
+
for (const packet of packets)
|
|
206
|
+
console.log(` ${packet.frameId}: ${packet.bytes} bytes → ${packet.path}`);
|
|
207
|
+
console.log(` worker role: ${role.bytes} bytes → ${role.path}`);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(`✗ frame packets: ${error.message}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -15,15 +15,15 @@ Run commands as `npx hyperframes ...` unless project instructions provide a wrap
|
|
|
15
15
|
|
|
16
16
|
## Development loop
|
|
17
17
|
|
|
18
|
-
1. **Scaffold:** `npx hyperframes init <project>`
|
|
18
|
+
1. **Scaffold:** `npx hyperframes init <project>` (centered blank). Or capture a site. Pass `--example=<name>` only to start from a named example.
|
|
19
19
|
2. **Find the move:** before authoring motion by hand, search for a primitive that already does it: `npx hyperframes catalog --query "reveal a headline one line at a time"`. Ask for the effect you want rather than the mechanism you have in mind. Install with `npx hyperframes add <name>` (see `/hyperframes-registry`). Author by hand only once nothing fits.
|
|
20
20
|
3. **Author:** write the composition using `/hyperframes-core`.
|
|
21
21
|
4. **Get fast feedback while editing:** run `npx hyperframes lint` after the first HTML pass and after structural changes.
|
|
22
22
|
5. **Run the final gate:** run `npx hyperframes check`; it reruns lint before opening the browser. Do not prepend a redundant standalone lint invocation. Add `--snapshots` for annotated overview frames and finding crops.
|
|
23
23
|
6. **Inspect sub-compositions:** when `index.html` mounts `data-composition-src`, capture midpoint snapshots and inspect each mounted scene.
|
|
24
24
|
7. **Open the final Studio preview:** run `npx hyperframes preview --background`, verify the URL returns HTTP 200, hand the timeline project URL to the user, and ask whether to revise or render. Keep it alive until review ends.
|
|
25
|
-
8. **Render only after approval:** use draft quality for
|
|
26
|
-
9. **Verify the output:** confirm the file exists
|
|
25
|
+
8. **Render only after approval:** use `--quality draft` while iterating, `--quality looks` for the first real encode (the CLI default), and `--quality delivery` for final delivery.
|
|
26
|
+
9. **Verify the output:** confirm the file exists and is non-empty. Read the render summary's second line (`beginframe` vs `screenshot`, GPU, stage timings). `screenshot` + `software gpu` on Linux is the slow path. `ffprobe -v error -show_format -show_streams` and compare duration (and fps if the brief set it) to the root `data-duration`.
|
|
27
27
|
|
|
28
28
|
## Mandatory creator-edit cross-references
|
|
29
29
|
|
|
@@ -46,9 +46,9 @@ npx hyperframes lint
|
|
|
46
46
|
# Required final gate; includes lint.
|
|
47
47
|
npx hyperframes check
|
|
48
48
|
npx hyperframes preview --background
|
|
49
|
-
npx hyperframes render --quality
|
|
49
|
+
npx hyperframes render --quality looks --output out.mp4
|
|
50
50
|
test -s out.mp4
|
|
51
|
-
ffprobe -v error -show_format out.mp4
|
|
51
|
+
ffprobe -v error -show_format -show_streams out.mp4
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
`check` runs lint first, then uses one browser session and one seek pass to audit runtime errors, failed requests, layout, `*.motion.json` assertions, and WCAG contrast. Persistent findings gate the exit code; transient entrance or exit findings are informational. Use `--strict` to gate warnings. `validate`, `inspect`, and `layout` remain aliases for compatibility but must not appear in new instructions or scripts.
|
|
@@ -62,7 +62,7 @@ Do not confuse these states:
|
|
|
62
62
|
| Storyboard board | Before composition checks, only when `storyboard: yes` | Review plan cards and wireframe sketches. Open `?view=storyboard#project/<name>`. |
|
|
63
63
|
| Final composition preview | After `check` passes | Review the assembled timeline before render. Open `#project/<name>`. |
|
|
64
64
|
|
|
65
|
-
The early board is not approval of the final video. Rendering always requires the final approval defined by `hyperframes
|
|
65
|
+
The early board is not approval of the final video. Rendering always requires the final approval defined by `hyperframes/references/review-loop.md`.
|
|
66
66
|
|
|
67
67
|
## Sub-composition smoke test
|
|
68
68
|
|
|
@@ -90,7 +90,7 @@ Treat tiny unstyled content, canvas-sized icons, missing hero elements, or timel
|
|
|
90
90
|
npx hyperframes doctor --json | jq -e '.ok' >/dev/null
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
- Non-TTY mode is automatic.
|
|
93
|
+
- Non-TTY mode is automatic and scaffolds the centered blank. Pass `--example` only to start from a named example. Use `--non-interactive` to force flag-only mode on a TTY.
|
|
94
94
|
- Use one `HYPERFRAMES_RUN_ID` for all commands in the same verification loop.
|
|
95
95
|
- Use `--strict`, `--strict-all`, and `--strict-variables` when the corresponding warnings, variables, or CI conditions must gate the render.
|
|
96
96
|
- JSON paths redact the home directory as `$HOME`; do not try to reverse the redaction.
|
|
@@ -112,7 +112,8 @@ Use `selection.target.hfId` when available, otherwise its selector and source fi
|
|
|
112
112
|
| Need | Command |
|
|
113
113
|
| ---------------------------------------- | ----------------------------------------------------------------------------- |
|
|
114
114
|
| Fast local iteration | `npx hyperframes render --quality draft` |
|
|
115
|
-
|
|
|
115
|
+
| First real encode | `npx hyperframes render --quality looks --output out.mp4` |
|
|
116
|
+
| Final local delivery | `npx hyperframes render --quality delivery --output out.mp4` |
|
|
116
117
|
| Reproducible container render | `npx hyperframes render --docker --strict --output out.mp4` |
|
|
117
118
|
| Local variable-driven batch render | `npx hyperframes render --batch rows.json --output "renders/{name}.mp4"` |
|
|
118
119
|
| HeyGen-hosted zero-infrastructure render | `npx hyperframes cloud render` |
|
|
@@ -7,18 +7,18 @@ Scaffolding commands. Use these instead of creating files by hand — they set u
|
|
|
7
7
|
## init
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx hyperframes init my-video # TTY:
|
|
10
|
+
npx hyperframes init my-video # centered blank (TTY: wizard)
|
|
11
11
|
npx hyperframes init my-video --example warm-grain # pick an example
|
|
12
|
-
npx hyperframes init my-video --
|
|
12
|
+
npx hyperframes init my-video --resolution portrait
|
|
13
13
|
npx hyperframes init my-video --video clip.mp4 # with video file
|
|
14
14
|
npx hyperframes init my-video --audio track.mp3 # with audio file
|
|
15
|
-
npx hyperframes init my-video --
|
|
16
|
-
npx hyperframes init my-video --non-interactive
|
|
15
|
+
npx hyperframes init my-video --tailwind # Tailwind v4 browser runtime
|
|
16
|
+
npx hyperframes init my-video --non-interactive # CI — flag-only, same blank
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
**Default depends on TTY**: in a terminal, the CLI prompts for example/options. Outside a TTY (CI, agents, piped output) it auto-switches to non-interactive and
|
|
19
|
+
**Default depends on TTY**: in a terminal, the CLI prompts for example/options (default: centered blank). Outside a TTY (CI, agents, piped output) it auto-switches to non-interactive and scaffolds that blank. Pass `--example` only to start from a named example. Pass `--non-interactive` to force flag-only mode on a TTY.
|
|
20
20
|
|
|
21
|
-
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`, `vscode-theme-visualizer`. (The closed set of `hyperframes:example` items in `registry/registry.json
|
|
21
|
+
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`, `vscode-theme-visualizer`. (The closed set of `hyperframes:example` items in `registry/registry.json` plus the bundled `blank` template. `hyperframes catalog` does not list examples — its `--type` takes only `block` or `component` — so this list has no live equivalent and is checked by `bun run lint:skills`.)
|
|
22
22
|
|
|
23
23
|
Other useful flags:
|
|
24
24
|
|
|
@@ -109,7 +109,8 @@ npx hyperframes render ./my-video --output ./out.mp4 # render from outside the
|
|
|
109
109
|
npx hyperframes render --output final.mp4 # named output (no timestamp)
|
|
110
110
|
npx hyperframes render -c compositions/intro.html -o intro.mp4 # render a specific sub-composition file
|
|
111
111
|
npx hyperframes render --quality draft # fast iteration
|
|
112
|
-
npx hyperframes render --
|
|
112
|
+
npx hyperframes render --quality looks # first real encode (default)
|
|
113
|
+
npx hyperframes render --fps 60 --quality delivery # final delivery
|
|
113
114
|
npx hyperframes render --format webm # transparent WebM
|
|
114
115
|
npx hyperframes render --docker # byte-identical
|
|
115
116
|
```
|
|
@@ -122,7 +123,7 @@ npx hyperframes render --docker # byte-identical
|
|
|
122
123
|
| `--composition`, `-c` | path to composition file | `index.html` | Render a specific composition file (e.g. `compositions/intro.html`) instead of the project's `index.html`. |
|
|
123
124
|
| `--output`, `-o` | path | `renders/<project>_<ts>.<ext>` | Output path. Default is timestamped (`<project-name>_YYYY-MM-DD_HH-MM-SS.<ext>`). |
|
|
124
125
|
| `--fps` | 24, 30, 60 | 30 | 60fps doubles render time |
|
|
125
|
-
| `--quality` | draft, standard, high
|
|
126
|
+
| `--quality` | draft, looks, delivery, standard, high | looks | `looks` is CRF 16 on the standard preset. `delivery` is `high`. draft for iterating |
|
|
126
127
|
| `--format` | mp4, webm, mov, gif, png-sequence | mp4 | WebM/MOV render with transparency; gif for inline autoplay in GitHub PRs/READMEs/docs (two-pass palette encode, fps capped at 30 — prefer `--fps 15` — no audio, 1-bit transparency only, HDR falls back to SDR); png-sequence writes RGBA frames to a directory (AE/Nuke/Fusion ingest) |
|
|
127
128
|
| `--gif-loop` | 0-65535 | 0 | GIF loop count; `0` loops forever. Only with `--format gif`. |
|
|
128
129
|
| `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (+ aliases `1080p`, `4k`, `uhd`) | — | Supersample via Chrome `deviceScaleFactor`. Aspect ratio must match composition; scale must be an integer. Not with `--hdr`. |
|
|
@@ -142,7 +143,9 @@ npx hyperframes render --docker # byte-identical
|
|
|
142
143
|
| `--variables-file` | path | — | JSON file with variable values (alternative to `--variables`) |
|
|
143
144
|
| `--strict-variables` | flag | off | Fail render on undeclared keys or type mismatches in `--variables` |
|
|
144
145
|
|
|
145
|
-
**Quality guidance:** `draft` while iterating, `
|
|
146
|
+
**Quality guidance:** `draft` while iterating, `looks` (the default) for the first real encode, `delivery` for final delivery. `standard` and `high` still work.
|
|
147
|
+
|
|
148
|
+
**Read the summary's second line.** After the output path and size, `render` prints the capture path, GPU mode, and per-stage timings, e.g. `beginframe capture · software gpu · compile 0.7s · setup 1.1s · capture 8.1s · encode 5.8s`. `capture` is the frame loop and `setup` is calibration plus session init; the capture path reads `a|b` when parallel workers diverged, and `encode (during capture)` means the streaming encoder overlapped the loop, so do not add it to the wall time. Local GPU `auto` (the default) requests BeginFrame. If the line still says `screenshot capture` with `software gpu` on Linux, a hint under it says BeginFrame did not run (needs chrome-headless-shell, no `--resolution` upscale). It only appears when the GPU was auto-probed, never under `--docker` or `--no-browser-gpu`, where software GL is the point. Heavy compositions can stall on software GL. Do not reach for `--debug` just to learn which path ran.
|
|
146
149
|
|
|
147
150
|
**Parametrized renders:** the composition declares its variables on the `<html>` root with **`data-composition-variables`** — a JSON **array of declarations** (`{id, type, label, default}` per entry) that defines the schema. Scripts inside read the resolved values via `window.__hyperframes.getVariables()`. The CLI `--variables '{"title":"Q4 Report"}'` is a JSON **object keyed by id** that overrides those declared defaults for one render; missing keys fall through, so the same composition runs unchanged in dev preview and in production. Sub-comp hosts can also override per-instance with `data-variable-values`. See the `hyperframes-core` skill for the full pattern.
|
|
148
151
|
|
package/dist/studio/assets/{hyperframes-player-DrGk-lf5.js → hyperframes-player-otov4oIw.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var ve=Object.defineProperty;var be=(n,i,e)=>i in n?ve(n,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[i]=e;var u=(n,i,e)=>be(n,typeof i!="symbol"?i+"":i,e);import{r as le,i as we,a as Ae}from"./index-
|
|
1
|
+
var ve=Object.defineProperty;var be=(n,i,e)=>i in n?ve(n,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[i]=e;var u=(n,i,e)=>be(n,typeof i!="symbol"?i+"":i,e);import{r as le,i as we,a as Ae}from"./index-DWFEyVJZ.js";function Ee(n){return n.hasRuntime||n.runtimeInjected?!1:!!(n.hasNestedCompositions||n.hasTimelines&&n.attempts>=5)}function V(n){return typeof n=="object"&&n!==null}function Ce(n){return V(n)&&typeof n.getDuration=="function"}function Te(n){return V(n)&&typeof n.duration=="function"&&typeof n.time=="function"&&typeof n.seek=="function"&&typeof n.play=="function"&&typeof n.pause=="function"}function Se(n){if(!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(n))throw new Error(`Invalid HyperFrames runtime version: ${n}`);return`https://cdn.jsdelivr.net/npm/@hyperframes/core@${n}/dist/hyperframe.runtime.iife.js`}const H=typeof __HYPERFRAMES_RUNTIME_CDN_URL__=="string"?__HYPERFRAMES_RUNTIME_CDN_URL__:Se("0.0.0-dev");function $(n){if(n===null)return null;const i=Number.parseInt(n,10);return Number.isFinite(i)&&i>0?i:null}function Re(n){const i=(n==null?void 0:n.querySelector("[data-composition-id][data-width][data-height]"))??(n==null?void 0:n.querySelector("[data-width][data-height]"));if(!i)return null;const e=$(i.getAttribute("data-width")),t=$(i.getAttribute("data-height"));return e!==null&&t!==null?{width:e,height:t}:null}class xe{constructor(i,e){u(this,"_interval",null);u(this,"_runtimeInjected",!1);this._iframe=i,this._callbacks=e}get runtimeInjected(){return this._runtimeInjected}start(){this.stop(),this._runtimeInjected=!1;let i=0;this._interval=setInterval(()=>{var e;i++;try{const t=this._iframe.contentWindow;if(!t)return;const r=!!(t.__hf||t.__player),s=!!(t.__timelines&&Object.keys(t.__timelines).length>0),o=!!((e=this._iframe.contentDocument)!=null&&e.querySelector("[data-composition-src]"));if(Ee({hasRuntime:r,hasTimelines:s,hasNestedCompositions:o,runtimeInjected:this._runtimeInjected,attempts:i})){this._injectRuntime();return}if(this._runtimeInjected&&!r)return;const d=this._resolvePlaybackDurationAdapter(t);if(d&&d.getDuration()>0){this.stop();const p=Re(this._iframe.contentDocument);this._callbacks.onReady({duration:d.getDuration(),adapter:d,compositionSize:p});return}}catch{}i>=40&&(this.stop(),this._callbacks.onError("Composition timeline not found after 8s"))},200)}stop(){this._interval!==null&&(clearInterval(this._interval),this._interval=null)}resolveDirectTimelineAdapter(){try{const i=this._iframe.contentWindow;return i?this._resolveDirectTimelineAdapterFromWindow(i):null}catch{return null}}resolveDirectTimelineAdapterFromWindow(i){return this._resolveDirectTimelineAdapterFromWindow(i)}hasRuntimeBridge(i){return Reflect.get(i,"__hf")!==void 0||V(Reflect.get(i,"__player"))}_injectRuntime(){var i,e;this._runtimeInjected=!0;try{const t=this._iframe.contentDocument;if(!t)return;const r=t.createElement("script");r.src=H,(t.head||t.documentElement).appendChild(r),(e=(i=this._callbacks).onRuntimeInjected)==null||e.call(i)}catch{}}_resolveDirectTimelineAdapterFromWindow(i){var d,p;if(this.hasRuntimeBridge(i))return null;const e=Reflect.get(i,"__timelines");if(!V(e))return null;const t=Object.keys(e);if(t.length===0)return null;const r=(p=(d=this._iframe.contentDocument)==null?void 0:d.querySelector("[data-composition-id]"))==null?void 0:p.getAttribute("data-composition-id"),s=r&&r in e?r:t[t.length-1],o=e[s];return Te(o)?o:null}_resolvePlaybackDurationAdapter(i){const e=Reflect.get(i,"__player");if(Ce(e))return{kind:"runtime",getDuration:()=>e.getDuration()};const t=this._resolveDirectTimelineAdapterFromWindow(i);return t?{kind:"direct-timeline",timeline:t,getDuration:()=>t.duration()}:null}}const Me=`
|
|
2
2
|
:host {
|
|
3
3
|
display: block;
|
|
4
4
|
position: relative;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{g as P}from"./index-
|
|
1
|
+
import{g as P}from"./index-DWFEyVJZ.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
|