@koda-sl/baker-cli 0.79.0 → 0.80.0
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/README.md +1 -1
- package/canvas/video-call-composition/index.html +136 -0
- package/canvas/video-call-composition/meta.json +26 -0
- package/canvas/video-overlay-composition/index.html +39 -2
- package/dist/{chunk-CCO34ACK.js → chunk-NBNUNCY7.js} +90 -51
- package/dist/{chunk-CCO34ACK.js.map → chunk-NBNUNCY7.js.map} +1 -1
- package/dist/cli.js +19 -8
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2502,7 +2502,7 @@ Local ImageMagick passthrough. Identical schema and safety contract to `ffmpeg`;
|
|
|
2502
2502
|
|
|
2503
2503
|
Render an HTML/CSS/GSAP composition to **mp4** (default), or to a transparent **webm**/**mov** alpha overlay. Quality and worker count are fixed by the engine for ad-creative delivery; the canvas owns the composition path, the output format, and the composition vars.
|
|
2504
2504
|
|
|
2505
|
-
Before each render the engine runs a **pre-render quality gate** on the *substituted* composition — Hyperframes' own `lint` + `inspect`. Layout **overflow** (clipped text) and structural lint **errors block** the render; **low-contrast** text and missing brand fonts/assets only **warn** (logged as `hyperframe check warning …`). If the `hyperframes` binary or its subcommands are unavailable the gate degrades gracefully and the render proceeds.
|
|
2505
|
+
Before each render the engine runs a **pre-render quality gate** on the *substituted* composition — Hyperframes' own `lint` + `inspect`. Layout **overflow** (clipped text) and structural lint **errors block** the render; **low-contrast** text and missing brand fonts/assets only **warn** (logged as `hyperframe check warning …`). When the composition embeds another via a live `data-composition-src` (a nested catalog block), the gate additionally runs `snapshot` as a render-path smoke test — the only check that catches a broken embed (id mismatch, `<style>`/`<script>` outside the sub-comp's `<template>`, or a class-styled sub-comp root) that `lint`/`inspect`/preview all miss. If the `hyperframes` binary or its subcommands are unavailable the gate degrades gracefully and the render proceeds.
|
|
2506
2506
|
|
|
2507
2507
|
**Inputs**
|
|
2508
2508
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
/* The "talking head on a video call" pattern — see the creative-canvas-ads
|
|
9
|
+
skill's references/hyperframes/talking-head-and-overlays.md. Presenter
|
|
10
|
+
(the `background` video) starts full-bleed, then shrinks to a corner PIP
|
|
11
|
+
pill while #content takes the stage. Replace #content with a real
|
|
12
|
+
screenshot <img> you drop in this dir, or nest a catalog block. Drop
|
|
13
|
+
brand-bold.otf / brand-regular.otf for on-brand type. */
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'BrandFont';
|
|
16
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
17
|
+
font-weight: 700 900;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
@font-face {
|
|
21
|
+
font-family: 'BrandFont';
|
|
22
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
23
|
+
font-weight: 100 600;
|
|
24
|
+
font-display: swap;
|
|
25
|
+
}
|
|
26
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
27
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: #000; }
|
|
28
|
+
#root { position: absolute; inset: 0; font-family: 'BrandFont', 'Helvetica Neue', Arial, sans-serif; }
|
|
29
|
+
|
|
30
|
+
/* Content stage — sits BEHIND the presenter (DOM order: #content first, then
|
|
31
|
+
#video-wrap → the pill paints on top). Swap this panel for a screenshot. */
|
|
32
|
+
#content {
|
|
33
|
+
position: absolute; inset: 0; background: {{bg_color}}; color: {{text_color}};
|
|
34
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
35
|
+
padding: 160px 96px; opacity: 0;
|
|
36
|
+
}
|
|
37
|
+
#content .kicker { width: 120px; height: 8px; background: {{accent_color}}; border-radius: 4px; transform: scaleX(0); transform-origin: left center; margin-bottom: 40px; }
|
|
38
|
+
#content .headline { font-size: 104px; font-weight: 900; line-height: 1.08; letter-spacing: -1px; text-wrap: balance; }
|
|
39
|
+
|
|
40
|
+
/* Presenter video — full-bleed at first, then TRANSFORM-scaled into a corner
|
|
41
|
+
pill. Transforms only: never animate left/top/width/height — they trigger
|
|
42
|
+
layout, and the renderer's parallel per-frame seeks can desync layout-driven
|
|
43
|
+
geometry (see references/hyperframes/composition-engine.md). The pill chrome
|
|
44
|
+
(radius/ring) is applied at PIP time and pre-multiplied for the scale. */
|
|
45
|
+
#video-wrap { position: absolute; top: 0; left: 0; width: 1080px; height: 1920px; overflow: hidden; transform-origin: 0 0; }
|
|
46
|
+
#bg { width: 100%; height: 100%; object-fit: cover; }
|
|
47
|
+
|
|
48
|
+
/* Floating glass claim card. */
|
|
49
|
+
#claim {
|
|
50
|
+
position: absolute; left: 64px; bottom: 360px; max-width: 720px;
|
|
51
|
+
padding: 36px 44px; border-radius: 24px;
|
|
52
|
+
background: rgba(255,255,255,0.10); backdrop-filter: blur(18px) saturate(140%);
|
|
53
|
+
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
54
|
+
border: 1px solid rgba(255,255,255,0.22);
|
|
55
|
+
color: {{text_color}}; font-size: 46px; font-weight: 700; line-height: 1.2;
|
|
56
|
+
opacity: 0; transform: translateY(24px);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Caption band. */
|
|
60
|
+
#caption {
|
|
61
|
+
position: absolute; left: 50%; bottom: 150px; transform: translateX(-50%);
|
|
62
|
+
width: 88%; text-align: center; color: {{text_color}};
|
|
63
|
+
font-size: 56px; font-weight: 800; line-height: 1.15;
|
|
64
|
+
text-shadow: 0 2px 8px rgba(0,0,0,0.6), 0 0 2px rgba(0,0,0,0.9);
|
|
65
|
+
opacity: 0;
|
|
66
|
+
}
|
|
67
|
+
#caption .u { display: inline-block; height: 6px; width: 120px; background: {{accent_color}}; border-radius: 3px; vertical-align: middle; margin-left: 16px; transform: scaleX(0); transform-origin: left center; }
|
|
68
|
+
</style>
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
<div id="root" data-composition-id="video-call" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
72
|
+
<div id="content">
|
|
73
|
+
<div class="kicker" id="kicker"></div>
|
|
74
|
+
<div class="headline" id="headline">{{headline}}</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div id="video-wrap">
|
|
77
|
+
<video id="bg" src="background.mp4" muted></video>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="claim">{{claim}}</div>
|
|
80
|
+
<div id="caption"><span id="caption-text">{{caption}}</span><span class="u" id="caption-u"></span></div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<script>
|
|
84
|
+
(() => {
|
|
85
|
+
var DURATION = parseFloat('{{duration}}') || 8;
|
|
86
|
+
var PIP_AT = parseFloat('{{pip_at_s}}'); if (!(PIP_AT >= 0)) PIP_AT = 2;
|
|
87
|
+
var hasHeadline = ((document.getElementById('headline').textContent) || '').trim().length > 0;
|
|
88
|
+
var hasClaim = ((document.getElementById('claim').textContent) || '').trim().length > 0;
|
|
89
|
+
var hasCaption = ((document.getElementById('caption-text').textContent) || '').trim().length > 0;
|
|
90
|
+
if (!hasClaim) document.getElementById('claim').style.display = 'none';
|
|
91
|
+
if (!hasCaption) document.getElementById('caption').style.display = 'none';
|
|
92
|
+
// Kicker sits at scaleX(0) but still holds its layout box + 40px bottom margin,
|
|
93
|
+
// so hide it outright when there's no headline (the default) — no dead space.
|
|
94
|
+
if (!hasHeadline) document.getElementById('kicker').style.display = 'none';
|
|
95
|
+
|
|
96
|
+
// Top-right PIP pill, 9:16 to MATCH the full frame so a single UNIFORM scale
|
|
97
|
+
// shrinks the presenter without distorting the video (non-uniform scaleX/scaleY
|
|
98
|
+
// would squish it). Edit x/y for another corner. Transforms only.
|
|
99
|
+
var PILL = { x: 672, y: 56, w: 360, h: 640 };
|
|
100
|
+
var s = PILL.w / 1080; // === PILL.h / 1920 (uniform)
|
|
101
|
+
var k = 1 / s; // pre-multiplier so the chrome lands crisp at the target size after scaling
|
|
102
|
+
|
|
103
|
+
var tl = gsap.timeline({ paused: true });
|
|
104
|
+
|
|
105
|
+
// Presenter shrinks into the pill; content stage fades up underneath. The pill
|
|
106
|
+
// chrome (radius + white ring) is set at PIP time and pre-multiplied by k so,
|
|
107
|
+
// once the element is scaled by s, it reads as ~28px radius + a crisp ring.
|
|
108
|
+
tl.set('#video-wrap', {
|
|
109
|
+
borderRadius: 28 * k,
|
|
110
|
+
boxShadow: '0 ' + 24 * k + 'px ' + 60 * k + 'px ' + (-20 * k) + 'px rgba(0,0,0,0.45), 0 0 0 ' + 6 * k + 'px rgba(255,255,255,0.7), 0 0 0 ' + 8 * k + 'px rgba(11,16,32,0.18)',
|
|
111
|
+
}, PIP_AT);
|
|
112
|
+
tl.fromTo('#video-wrap',
|
|
113
|
+
{ x: 0, y: 0, scale: 1 },
|
|
114
|
+
{ x: PILL.x, y: PILL.y, scale: s, duration: 0.6, ease: 'power2.inOut' },
|
|
115
|
+
PIP_AT);
|
|
116
|
+
tl.fromTo('#content', { opacity: 0 }, { opacity: 1, duration: 0.4, ease: 'power2.out' }, PIP_AT + 0.1);
|
|
117
|
+
if (hasHeadline) tl.fromTo('#kicker', { scaleX: 0 }, { scaleX: 1, duration: 0.4, ease: 'power3.out' }, PIP_AT + 0.25);
|
|
118
|
+
|
|
119
|
+
// Glass claim card rises in.
|
|
120
|
+
if (hasClaim) tl.fromTo('#claim', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, PIP_AT + 0.45);
|
|
121
|
+
|
|
122
|
+
// Caption fades in with an accent underline wipe.
|
|
123
|
+
if (hasCaption) {
|
|
124
|
+
tl.fromTo('#caption', { opacity: 0 }, { opacity: 1, duration: 0.35, ease: 'power1.out' }, PIP_AT + 0.6);
|
|
125
|
+
tl.fromTo('#caption-u', { scaleX: 0 }, { scaleX: 1, duration: 0.4, ease: 'power3.out' }, PIP_AT + 0.7);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Hold to the full duration.
|
|
129
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
130
|
+
|
|
131
|
+
window.__timelines = window.__timelines || {};
|
|
132
|
+
window.__timelines["video-call"] = tl;
|
|
133
|
+
})();
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "video-call",
|
|
3
|
+
"title": "Talking head on a video call — presenter shrinks to a PIP pill over content",
|
|
4
|
+
"description": "The 'AI person showing you something on a call' pattern. The presenter video starts full-bleed, then at pip_at_s shrinks into a rounded corner PIP pill while a content card (a claim, or your screen-share/app-UI image) takes the stage; a glass claim card and a caption band animate in. The presenter clip is the `background` input. Replace the #content panel with a real screenshot <img> you drop in this dir, or nest a catalog block (npx hyperframes add …) — see the creative-canvas-ads skill's references/hyperframes/talking-head-and-overlays.md. Drop brand-bold.otf / brand-regular.otf for on-brand type. Fixed 1080x1920; copy + edit width/height for other ratios.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 8,
|
|
9
|
+
"inputs": {
|
|
10
|
+
"background": {
|
|
11
|
+
"kind": "video",
|
|
12
|
+
"required": true,
|
|
13
|
+
"staged_as": "background.mp4",
|
|
14
|
+
"description": "The presenter / talking-head clip (e.g. a Seedance native-voiced scene)."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"params": {
|
|
18
|
+
"headline": { "kind": "string", "default": "", "description": "Big line on the content stage (what the presenter is showing)." },
|
|
19
|
+
"claim": { "kind": "string", "default": "", "description": "Short proof/claim on the floating glass card (optional)." },
|
|
20
|
+
"caption": { "kind": "string", "default": "", "description": "A caption line in the lower band (optional)." },
|
|
21
|
+
"pip_at_s": { "kind": "number", "default": 2, "min": 0, "max": 30, "description": "When the presenter shrinks to the corner pill." },
|
|
22
|
+
"bg_color": { "kind": "color", "default": "#0b1020", "description": "Content-stage background (behind the presenter)." },
|
|
23
|
+
"text_color": { "kind": "color", "default": "#ffffff" },
|
|
24
|
+
"accent_color": { "kind": "color", "default": "#5b8cff", "description": "Headline accent + caption underline + pill ring tint." }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -8,17 +8,33 @@
|
|
|
8
8
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
9
9
|
OVERLAY LAYER — PAINT THIS YOURSELF.
|
|
10
10
|
|
|
11
|
+
This is a REAL Hyperframes composition (hyperframe_render runs the genuine
|
|
12
|
+
`npx hyperframes` renderer), so it can do FAR more than the seeded text.
|
|
13
|
+
|
|
11
14
|
The scaffold seeds #overlay-root (below) with the reference ad's overlays
|
|
12
15
|
as plain HTML elements — real text, a position class, and data-start/
|
|
13
16
|
data-dur timing. This file is YOURS: restyle every rule here, add classes,
|
|
14
17
|
build a lower-third bar or a scrolling ticker, drop a logo image into this
|
|
15
|
-
dir and reference it with <img>. The
|
|
18
|
+
dir and reference it with <img>. The simple contract for a seeded element:
|
|
16
19
|
|
|
17
20
|
• keep each overlay element inside #overlay-root
|
|
18
21
|
• keep its data-start / data-dur (seconds) — the runtime shows/hides by them
|
|
19
22
|
• (optional) data-anim="fade|slide_up|slide_down|pop" for a canned entrance
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
…and the richer capabilities (see the two commented slots in #overlay-root,
|
|
25
|
+
and the full docs under the creative-canvas-ads skill's references/hyperframes/):
|
|
26
|
+
|
|
27
|
+
• PULL A READY-MADE BLOCK — `npx hyperframes add <id>` (run in THIS dir;
|
|
28
|
+
lands in compositions/) then nest it with a <div data-composition-src>
|
|
29
|
+
clip. ~97 blocks: lower-thirds, social-proof cards, stat counters, charts,
|
|
30
|
+
code, logo stings. `npx hyperframes catalog --type block` lists them.
|
|
31
|
+
• CAPTIONS — drive a karaoke/themed/kinetic caption track off the
|
|
32
|
+
transcript (sound-off feed). `npx hyperframes add caption-highlight`.
|
|
33
|
+
• MOTION — animate with GSAP using the named motion-rule vocabulary, not
|
|
34
|
+
just the four canned entrances.
|
|
35
|
+
|
|
36
|
+
The seeded runtime decides only WHEN a plain element shows; everything
|
|
37
|
+
else — look, motion, nested blocks — is your CSS/markup/GSAP.
|
|
22
38
|
For on-brand type, drop brand-bold.otf / brand-regular.otf into this dir.
|
|
23
39
|
═══════════════════════════════════════════════════════════════════════ */
|
|
24
40
|
@font-face {
|
|
@@ -68,6 +84,27 @@
|
|
|
68
84
|
<video id="bg" src="background.mp4" muted></video>
|
|
69
85
|
<div id="overlay-root">
|
|
70
86
|
<!--OVERLAYS-->
|
|
87
|
+
|
|
88
|
+
<!-- ── NESTED BLOCK SLOT (optional) ───────────────────────────────────
|
|
89
|
+
For an ANIMATED graphic (lower-third, social-proof/review card, stat
|
|
90
|
+
counter, chart, logo sting), pull a ready-made block instead of a
|
|
91
|
+
static <img>. From this dir:
|
|
92
|
+
npx hyperframes catalog --type block # find one by tag/name
|
|
93
|
+
npx hyperframes add yt-lower-third # writes compositions/yt-lower-third.html
|
|
94
|
+
then uncomment + retime the nested clip (add data-start/data-track-index;
|
|
95
|
+
data-composition-id is optional but if set must match the block's id):
|
|
96
|
+
<div data-composition-src="compositions/yt-lower-third.html"
|
|
97
|
+
data-start="3" data-duration="4.5" data-track-index="2"
|
|
98
|
+
data-width="1920" data-height="1080"></div>
|
|
99
|
+
──────────────────────────────────────────────────────────────────── -->
|
|
100
|
+
|
|
101
|
+
<!-- ── CAPTION TRACK SLOT (optional, recommended for sound-off feeds) ──
|
|
102
|
+
The feed plays muted — burn in captions from the word-level transcript
|
|
103
|
+
the deconstruct produced. Floor: `npx hyperframes add caption-highlight`
|
|
104
|
+
(TikTok karaoke); escalate by register (caption-kinetic-slam, etc.).
|
|
105
|
+
Wiring + the {id,text,start,end} sync contract:
|
|
106
|
+
references/hyperframes/captions-and-audio.md.
|
|
107
|
+
──────────────────────────────────────────────────────────────────── -->
|
|
71
108
|
</div>
|
|
72
109
|
</div>
|
|
73
110
|
|
|
@@ -621,7 +621,7 @@ ${originalIndentation}`;
|
|
|
621
621
|
});
|
|
622
622
|
|
|
623
623
|
// src/engine/index.ts
|
|
624
|
-
import
|
|
624
|
+
import path15 from "path";
|
|
625
625
|
|
|
626
626
|
// src/engine/client/http.ts
|
|
627
627
|
var BackendHttpError = class extends Error {
|
|
@@ -667,14 +667,14 @@ var HttpClient = class {
|
|
|
667
667
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
668
668
|
this.sleepFn = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
669
669
|
}
|
|
670
|
-
async postJson(
|
|
671
|
-
return await this.requestJson("POST",
|
|
670
|
+
async postJson(path16, body, signal) {
|
|
671
|
+
return await this.requestJson("POST", path16, body, signal);
|
|
672
672
|
}
|
|
673
|
-
async getJson(
|
|
674
|
-
return await this.requestJson("GET",
|
|
673
|
+
async getJson(path16, signal) {
|
|
674
|
+
return await this.requestJson("GET", path16, void 0, signal);
|
|
675
675
|
}
|
|
676
|
-
async requestJson(method,
|
|
677
|
-
const url = `${this.baseUrl}${
|
|
676
|
+
async requestJson(method, path16, body, signal) {
|
|
677
|
+
const url = `${this.baseUrl}${path16.startsWith("/") ? path16 : `/${path16}`}`;
|
|
678
678
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
679
679
|
const outcome = await this.attempt(method, url, body, attempt, signal);
|
|
680
680
|
if (outcome.kind === "value") return outcome.value;
|
|
@@ -786,8 +786,8 @@ var BackendClient = class {
|
|
|
786
786
|
);
|
|
787
787
|
}
|
|
788
788
|
getArtifact(kind, name, version, signal) {
|
|
789
|
-
const
|
|
790
|
-
return this.http.getJson(
|
|
789
|
+
const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
790
|
+
return this.http.getJson(path16, signal);
|
|
791
791
|
}
|
|
792
792
|
};
|
|
793
793
|
|
|
@@ -2403,9 +2403,9 @@ function checkOutputRef(ctx) {
|
|
|
2403
2403
|
function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
2404
2404
|
for (const issue of err.issues) {
|
|
2405
2405
|
const tail2 = pathToString(issue.path);
|
|
2406
|
-
const
|
|
2406
|
+
const path16 = pathPrefix ? tail2 ? `${pathPrefix}.${tail2}` : pathPrefix : tail2;
|
|
2407
2407
|
issues.push({
|
|
2408
|
-
path:
|
|
2408
|
+
path: path16,
|
|
2409
2409
|
code,
|
|
2410
2410
|
message: issue.message,
|
|
2411
2411
|
received: issue.code === "invalid_type" ? issue.received : void 0,
|
|
@@ -2414,8 +2414,8 @@ function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
|
2414
2414
|
});
|
|
2415
2415
|
}
|
|
2416
2416
|
}
|
|
2417
|
-
function pathToString(
|
|
2418
|
-
return
|
|
2417
|
+
function pathToString(path16) {
|
|
2418
|
+
return path16.map((p) => typeof p === "number" ? `[${p}]` : `.${String(p)}`).join("").replace(/^\./, "");
|
|
2419
2419
|
}
|
|
2420
2420
|
function buildDepGraph(canvas) {
|
|
2421
2421
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -4044,10 +4044,10 @@ var fontSpecimenNode = defineNode({
|
|
|
4044
4044
|
|
|
4045
4045
|
// src/engine/nodes/local/hyperframe.ts
|
|
4046
4046
|
import { execFile as execFile4 } from "child_process";
|
|
4047
|
-
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as
|
|
4047
|
+
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as readFile8, rm as rm4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
4048
4048
|
import { createRequire as createRequire2 } from "module";
|
|
4049
4049
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
4050
|
-
import
|
|
4050
|
+
import path11 from "path";
|
|
4051
4051
|
import { promisify as promisify4 } from "util";
|
|
4052
4052
|
import { z as z10 } from "zod";
|
|
4053
4053
|
|
|
@@ -4244,6 +4244,8 @@ function defaultFilenameForInput(key, kind) {
|
|
|
4244
4244
|
|
|
4245
4245
|
// src/engine/nodes/local/lib/hyperframe-check.ts
|
|
4246
4246
|
import { execFile as execFile3 } from "child_process";
|
|
4247
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
4248
|
+
import path9 from "path";
|
|
4247
4249
|
import { promisify as promisify3 } from "util";
|
|
4248
4250
|
var execFileAsync = promisify3(execFile3);
|
|
4249
4251
|
var NEVER_BLOCK = [/contrast/i, /\bwcag\b/i, /missing_local_asset/i, /font[_-]?family/i, /font[_-]?face/i];
|
|
@@ -4311,6 +4313,13 @@ function buildLintArgs(dir) {
|
|
|
4311
4313
|
function buildInspectArgs(dir, samples) {
|
|
4312
4314
|
return ["hyperframes", "inspect", dir, "--json", "--samples", String(samples)];
|
|
4313
4315
|
}
|
|
4316
|
+
function buildSnapshotArgs(dir, frames) {
|
|
4317
|
+
return ["hyperframes", "snapshot", dir, "--frames", String(frames), "--describe", "false"];
|
|
4318
|
+
}
|
|
4319
|
+
function usesNestedCompositions(indexHtml) {
|
|
4320
|
+
const withoutComments = indexHtml.replace(/<!--[\s\S]*?-->/g, "");
|
|
4321
|
+
return /data-composition-src\s*=/.test(withoutComments);
|
|
4322
|
+
}
|
|
4314
4323
|
async function runOne(args, timeoutMs) {
|
|
4315
4324
|
try {
|
|
4316
4325
|
const { stdout } = await execFileAsync("npx", args, { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
@@ -4323,6 +4332,17 @@ async function runOne(args, timeoutMs) {
|
|
|
4323
4332
|
return null;
|
|
4324
4333
|
}
|
|
4325
4334
|
}
|
|
4335
|
+
async function runSnapshotSmoke(args, timeoutMs) {
|
|
4336
|
+
try {
|
|
4337
|
+
await execFileAsync("npx", args, { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
4338
|
+
return { ok: true, unavailable: false, message: "" };
|
|
4339
|
+
} catch (e) {
|
|
4340
|
+
const err = e;
|
|
4341
|
+
const blob = `${err.stderr ?? ""} ${err.message ?? ""}`;
|
|
4342
|
+
if (UNAVAILABLE.test(blob)) return { ok: false, unavailable: true, message: blob };
|
|
4343
|
+
return { ok: false, unavailable: false, message: (err.stderr || err.message || "snapshot failed").slice(0, 800) };
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4326
4346
|
async function runHyperframesCheck(opts) {
|
|
4327
4347
|
const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
|
|
4328
4348
|
const [lintRaw, inspectRaw] = await Promise.all([
|
|
@@ -4342,6 +4362,25 @@ async function runHyperframesCheck(opts) {
|
|
|
4342
4362
|
throw new Error(`${nodeId}: pre-render check failed (${blocking.length} blocking)
|
|
4343
4363
|
${detail}`);
|
|
4344
4364
|
}
|
|
4365
|
+
let indexHtml = "";
|
|
4366
|
+
try {
|
|
4367
|
+
indexHtml = await readFile7(path9.join(dir, "index.html"), "utf-8");
|
|
4368
|
+
} catch {
|
|
4369
|
+
indexHtml = "";
|
|
4370
|
+
}
|
|
4371
|
+
if (indexHtml && usesNestedCompositions(indexHtml)) {
|
|
4372
|
+
const snap = await runSnapshotSmoke(buildSnapshotArgs(dir, Math.min(samples, 3)), Math.max(timeoutMs, 12e4));
|
|
4373
|
+
if (snap.unavailable) {
|
|
4374
|
+
ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
|
|
4375
|
+
} else if (!snap.ok) {
|
|
4376
|
+
throw new Error(
|
|
4377
|
+
`${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
|
|
4378
|
+
${snap.message}`
|
|
4379
|
+
);
|
|
4380
|
+
} else {
|
|
4381
|
+
ctx.log(`${nodeId}: nested-composition smoke test passed`);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4345
4384
|
ctx.log(`${nodeId}: pre-render check passed (${warnings.length} warning${warnings.length === 1 ? "" : "s"})`);
|
|
4346
4385
|
}
|
|
4347
4386
|
|
|
@@ -4390,9 +4429,9 @@ ${stderr.slice(0, 1500)}`;
|
|
|
4390
4429
|
|
|
4391
4430
|
// src/engine/nodes/local/lib/hyperframe-meta.ts
|
|
4392
4431
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
4393
|
-
import
|
|
4432
|
+
import path10 from "path";
|
|
4394
4433
|
async function ensureHyperframesMetaJson(tmp, nodeId, meta, duration) {
|
|
4395
|
-
const metaPath =
|
|
4434
|
+
const metaPath = path10.join(tmp, "meta.json");
|
|
4396
4435
|
await writeFile4(
|
|
4397
4436
|
metaPath,
|
|
4398
4437
|
JSON.stringify(
|
|
@@ -4492,7 +4531,7 @@ var hyperframeRenderNode = defineNode({
|
|
|
4492
4531
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
4493
4532
|
const meta = await loadCompositionMeta(compositionDir);
|
|
4494
4533
|
const compositionParams = validateAndParseDynamicParams(meta, params);
|
|
4495
|
-
const tmp = await mkdtemp4(
|
|
4534
|
+
const tmp = await mkdtemp4(path11.join(tmpdir4(), "hf-render-"));
|
|
4496
4535
|
try {
|
|
4497
4536
|
await copyComposition(compositionDir, tmp);
|
|
4498
4537
|
await vendorGsap(tmp, ctx);
|
|
@@ -4502,9 +4541,9 @@ var hyperframeRenderNode = defineNode({
|
|
|
4502
4541
|
await substituteCompositionFiles(tmp, substitutionValues);
|
|
4503
4542
|
await ensureHyperframesMetaJson(tmp, ctx.nodeId, meta, duration);
|
|
4504
4543
|
await runHyperframesCheck({ dir: tmp, nodeId: "hyperframe_render", ctx, timeoutMs: params.timeout_ms });
|
|
4505
|
-
const outputPath =
|
|
4544
|
+
const outputPath = path11.join(tmp, `output.${params.format}`);
|
|
4506
4545
|
await runRender({ tmp, outputPath, params, meta, ctx });
|
|
4507
|
-
const bytes = await
|
|
4546
|
+
const bytes = await readFile8(outputPath);
|
|
4508
4547
|
ctx.log(`rendered ${bytes.length} bytes`);
|
|
4509
4548
|
const ref = await ctx.assets.ingestBytes({
|
|
4510
4549
|
bytes: Buffer.from(bytes),
|
|
@@ -4526,10 +4565,10 @@ var hyperframeRenderNode = defineNode({
|
|
|
4526
4565
|
}
|
|
4527
4566
|
});
|
|
4528
4567
|
async function resolveCompositionDir(composition) {
|
|
4529
|
-
const compositionPath =
|
|
4568
|
+
const compositionPath = path11.isAbsolute(composition) ? composition : path11.resolve(process.cwd(), composition);
|
|
4530
4569
|
const s = await stat5(compositionPath);
|
|
4531
4570
|
if (s.isDirectory()) return compositionPath;
|
|
4532
|
-
return
|
|
4571
|
+
return path11.dirname(compositionPath);
|
|
4533
4572
|
}
|
|
4534
4573
|
async function validateCompositionParams(rawParams) {
|
|
4535
4574
|
const issues = [];
|
|
@@ -4596,7 +4635,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
4596
4635
|
await cp(srcDir, destDir, {
|
|
4597
4636
|
recursive: true,
|
|
4598
4637
|
filter: (src) => {
|
|
4599
|
-
const name =
|
|
4638
|
+
const name = path11.basename(src);
|
|
4600
4639
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
4601
4640
|
return true;
|
|
4602
4641
|
}
|
|
@@ -4605,7 +4644,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
4605
4644
|
async function vendorGsap(tmp, ctx) {
|
|
4606
4645
|
try {
|
|
4607
4646
|
const gsapMin = require_2.resolve("gsap/dist/gsap.min.js");
|
|
4608
|
-
await copyFile4(gsapMin,
|
|
4647
|
+
await copyFile4(gsapMin, path11.join(tmp, "gsap.min.js"));
|
|
4609
4648
|
} catch (e) {
|
|
4610
4649
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
4611
4650
|
}
|
|
@@ -4620,7 +4659,7 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
|
|
|
4620
4659
|
await stageAsset(ref, tmp, filename);
|
|
4621
4660
|
ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
|
|
4622
4661
|
if (spec.kind === "video" && primaryDuration === null) {
|
|
4623
|
-
primaryDuration = await probeDurationSeconds(
|
|
4662
|
+
primaryDuration = await probeDurationSeconds(path11.join(tmp, filename));
|
|
4624
4663
|
}
|
|
4625
4664
|
}
|
|
4626
4665
|
return primaryDuration;
|
|
@@ -4666,8 +4705,8 @@ function coerceImageParam(value) {
|
|
|
4666
4705
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
4667
4706
|
}
|
|
4668
4707
|
async function substituteCompositionFiles(tmp, values) {
|
|
4669
|
-
const entryPath =
|
|
4670
|
-
const original = await
|
|
4708
|
+
const entryPath = path11.join(tmp, "index.html");
|
|
4709
|
+
const original = await readFile8(entryPath, "utf-8");
|
|
4671
4710
|
const { output, missing } = substituteVariables(original, values);
|
|
4672
4711
|
if (missing.length > 0) {
|
|
4673
4712
|
throw new Error(
|
|
@@ -4683,7 +4722,7 @@ function workerCount() {
|
|
|
4683
4722
|
async function runRender(opts) {
|
|
4684
4723
|
const { tmp, outputPath, params, meta, ctx } = opts;
|
|
4685
4724
|
const args = buildRenderArgs(tmp, outputPath, meta, params.format);
|
|
4686
|
-
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${
|
|
4725
|
+
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${path11.basename(tmp)}`);
|
|
4687
4726
|
try {
|
|
4688
4727
|
await execFileAsync2("npx", args, { timeout: params.timeout_ms, maxBuffer: 64 * 1024 * 1024 });
|
|
4689
4728
|
} catch (e) {
|
|
@@ -4727,10 +4766,10 @@ async function probeDurationSeconds(filePath) {
|
|
|
4727
4766
|
|
|
4728
4767
|
// src/engine/nodes/local/hyperframe-snapshot.ts
|
|
4729
4768
|
import { execFile as execFile5 } from "child_process";
|
|
4730
|
-
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as
|
|
4769
|
+
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
4731
4770
|
import { createRequire as createRequire3 } from "module";
|
|
4732
4771
|
import { tmpdir as tmpdir5 } from "os";
|
|
4733
|
-
import
|
|
4772
|
+
import path12 from "path";
|
|
4734
4773
|
import { promisify as promisify5 } from "util";
|
|
4735
4774
|
import { z as z11 } from "zod";
|
|
4736
4775
|
var _execFileAsync = promisify5(execFile5);
|
|
@@ -4777,7 +4816,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
4777
4816
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
4778
4817
|
const meta = await loadCompositionMeta(compositionDir);
|
|
4779
4818
|
const compositionParams = validateAndParseDynamicParams2(meta, params);
|
|
4780
|
-
const tmp = await mkdtemp5(
|
|
4819
|
+
const tmp = await mkdtemp5(path12.join(tmpdir5(), "hf-snap-"));
|
|
4781
4820
|
try {
|
|
4782
4821
|
await copyComposition2(compositionDir, tmp);
|
|
4783
4822
|
await vendorGsap2(tmp, ctx);
|
|
@@ -4792,7 +4831,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
4792
4831
|
timeoutMs: params.timeout_ms,
|
|
4793
4832
|
samples: 1
|
|
4794
4833
|
});
|
|
4795
|
-
const entryPath =
|
|
4834
|
+
const entryPath = path12.join(tmp, "index.html");
|
|
4796
4835
|
const entryUrl = `file://${entryPath}`;
|
|
4797
4836
|
ctx.log(`snapshotting ${meta.width}x${meta.height}@${DEVICE_SCALE_FACTOR2}x wait=${params.wait_for.kind}`);
|
|
4798
4837
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -4853,7 +4892,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
4853
4892
|
await cp(srcDir, destDir, {
|
|
4854
4893
|
recursive: true,
|
|
4855
4894
|
filter: (src) => {
|
|
4856
|
-
const name =
|
|
4895
|
+
const name = path12.basename(src);
|
|
4857
4896
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
4858
4897
|
return true;
|
|
4859
4898
|
}
|
|
@@ -4862,7 +4901,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
4862
4901
|
async function vendorGsap2(tmp, ctx) {
|
|
4863
4902
|
try {
|
|
4864
4903
|
const gsapMin = require_3.resolve("gsap/dist/gsap.min.js");
|
|
4865
|
-
await copyFile5(gsapMin,
|
|
4904
|
+
await copyFile5(gsapMin, path12.join(tmp, "gsap.min.js"));
|
|
4866
4905
|
} catch (e) {
|
|
4867
4906
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
4868
4907
|
}
|
|
@@ -4896,8 +4935,8 @@ function coerceImageParam2(value) {
|
|
|
4896
4935
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
4897
4936
|
}
|
|
4898
4937
|
async function substituteCompositionFiles2(tmp, values) {
|
|
4899
|
-
const entryPath =
|
|
4900
|
-
const original = await
|
|
4938
|
+
const entryPath = path12.join(tmp, "index.html");
|
|
4939
|
+
const original = await readFile9(entryPath, "utf-8");
|
|
4901
4940
|
const { output, missing } = substituteVariables(original, values);
|
|
4902
4941
|
if (missing.length > 0) {
|
|
4903
4942
|
throw new Error(
|
|
@@ -5580,9 +5619,9 @@ var videoLipsyncNode = delegated({
|
|
|
5580
5619
|
});
|
|
5581
5620
|
|
|
5582
5621
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
5583
|
-
import { mkdtemp as mkdtemp6, readFile as
|
|
5622
|
+
import { mkdtemp as mkdtemp6, readFile as readFile10, rm as rm6 } from "fs/promises";
|
|
5584
5623
|
import { tmpdir as tmpdir6 } from "os";
|
|
5585
|
-
import
|
|
5624
|
+
import path13 from "path";
|
|
5586
5625
|
import { z as z31 } from "zod";
|
|
5587
5626
|
|
|
5588
5627
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
@@ -5714,14 +5753,14 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
5714
5753
|
ctx.log("video_transcribe: no audio track detected, sending full video");
|
|
5715
5754
|
return null;
|
|
5716
5755
|
}
|
|
5717
|
-
tmpDir = await mkdtemp6(
|
|
5718
|
-
const audioPath =
|
|
5756
|
+
tmpDir = await mkdtemp6(path13.join(tmpdir6(), "vtx-"));
|
|
5757
|
+
const audioPath = path13.join(tmpDir, "audio.mp3");
|
|
5719
5758
|
ctx.log("video_transcribe: extracting audio (mono 16kHz mp3)");
|
|
5720
5759
|
await runFfmpeg(
|
|
5721
5760
|
["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
|
|
5722
5761
|
{ timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
|
|
5723
5762
|
);
|
|
5724
|
-
const bytes = await
|
|
5763
|
+
const bytes = await readFile10(audioPath);
|
|
5725
5764
|
if (bytes.byteLength === 0) {
|
|
5726
5765
|
ctx.log("video_transcribe: extracted audio is empty, sending full video");
|
|
5727
5766
|
return null;
|
|
@@ -5828,19 +5867,19 @@ function safeCost(def) {
|
|
|
5828
5867
|
|
|
5829
5868
|
// src/engine/storage/cache-store.ts
|
|
5830
5869
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5831
|
-
import { mkdir as mkdir3, readFile as
|
|
5832
|
-
import
|
|
5870
|
+
import { mkdir as mkdir3, readFile as readFile11, rename as rename2, writeFile as writeFile7 } from "fs/promises";
|
|
5871
|
+
import path14 from "path";
|
|
5833
5872
|
var LocalCacheStore = class {
|
|
5834
5873
|
rootDir;
|
|
5835
5874
|
constructor(rootDir) {
|
|
5836
5875
|
this.rootDir = rootDir;
|
|
5837
5876
|
}
|
|
5838
5877
|
filePath(cacheKey) {
|
|
5839
|
-
return
|
|
5878
|
+
return path14.join(this.rootDir, `${cacheKey}.json`);
|
|
5840
5879
|
}
|
|
5841
5880
|
async get(cacheKey) {
|
|
5842
5881
|
try {
|
|
5843
|
-
const buf = await
|
|
5882
|
+
const buf = await readFile11(this.filePath(cacheKey), "utf8");
|
|
5844
5883
|
return JSON.parse(buf);
|
|
5845
5884
|
} catch (e) {
|
|
5846
5885
|
if (e.code === "ENOENT") return null;
|
|
@@ -5849,7 +5888,7 @@ var LocalCacheStore = class {
|
|
|
5849
5888
|
}
|
|
5850
5889
|
async put(entry) {
|
|
5851
5890
|
const dest = this.filePath(entry.cacheKey);
|
|
5852
|
-
await mkdir3(
|
|
5891
|
+
await mkdir3(path14.dirname(dest), { recursive: true });
|
|
5853
5892
|
const tmp = `${dest}.tmp-${process.pid}-${randomUUID2()}`;
|
|
5854
5893
|
await writeFile7(tmp, JSON.stringify(entry, null, 0));
|
|
5855
5894
|
await rename2(tmp, dest);
|
|
@@ -5903,14 +5942,14 @@ function defaultRegistry() {
|
|
|
5903
5942
|
}
|
|
5904
5943
|
function createEngineFromEnv(opts = {}) {
|
|
5905
5944
|
const cwd = opts.cwd ?? process.cwd();
|
|
5906
|
-
const cacheDir = opts.cacheDir ??
|
|
5907
|
-
const outputsDir = opts.outputsDir ??
|
|
5945
|
+
const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
|
|
5946
|
+
const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
|
|
5908
5947
|
const creds = requireCredentialsFromEnv();
|
|
5909
5948
|
return new Engine({
|
|
5910
5949
|
registry: defaultRegistry(),
|
|
5911
5950
|
client: new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey }),
|
|
5912
|
-
assets: new LocalAssetStore(
|
|
5913
|
-
cache: new LocalCacheStore(
|
|
5951
|
+
assets: new LocalAssetStore(path15.join(cacheDir, "assets")),
|
|
5952
|
+
cache: new LocalCacheStore(path15.join(cacheDir, "index")),
|
|
5914
5953
|
outputsDir,
|
|
5915
5954
|
log: opts.log
|
|
5916
5955
|
});
|
|
@@ -5931,4 +5970,4 @@ export {
|
|
|
5931
5970
|
defaultRegistry,
|
|
5932
5971
|
createEngineFromEnv
|
|
5933
5972
|
};
|
|
5934
|
-
//# sourceMappingURL=chunk-
|
|
5973
|
+
//# sourceMappingURL=chunk-NBNUNCY7.js.map
|