@koda-sl/baker-cli 0.128.0-dev.70bf43ce4 → 0.129.1-dev.972f3c9aa
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 +170 -19
- package/canvas/tiktok-captions-composition/index.html +1 -1
- package/canvas/video-overlay-composition/index.html +78 -6
- package/canvas/video-overlay-composition/meta.json +30 -2
- package/dist/{chunk-43KBQLP5.js → chunk-J2LYFDVC.js} +1168 -460
- package/dist/chunk-J2LYFDVC.js.map +1 -0
- package/dist/cli.js +7102 -5228
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +52 -2
- package/dist/engine/index.js +2 -2
- package/package.json +4 -2
- package/dist/chunk-43KBQLP5.js.map +0 -1
|
@@ -600,7 +600,7 @@ ${originalIndentation}`;
|
|
|
600
600
|
});
|
|
601
601
|
|
|
602
602
|
// src/engine/index.ts
|
|
603
|
-
import
|
|
603
|
+
import path16 from "path";
|
|
604
604
|
|
|
605
605
|
// src/engine/client/http.ts
|
|
606
606
|
var CONTENT_POLICY_CODE = "content_policy_blocked";
|
|
@@ -649,17 +649,17 @@ var HttpClient = class {
|
|
|
649
649
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
650
650
|
this.sleepFn = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
651
651
|
}
|
|
652
|
-
async postJson(
|
|
653
|
-
return await this.requestJson("POST",
|
|
652
|
+
async postJson(path17, body, signal) {
|
|
653
|
+
return await this.requestJson("POST", path17, body, signal);
|
|
654
654
|
}
|
|
655
|
-
async putJson(
|
|
656
|
-
return await this.requestJson("PUT",
|
|
655
|
+
async putJson(path17, body, signal) {
|
|
656
|
+
return await this.requestJson("PUT", path17, body, signal);
|
|
657
657
|
}
|
|
658
|
-
async getJson(
|
|
659
|
-
return await this.requestJson("GET",
|
|
658
|
+
async getJson(path17, signal) {
|
|
659
|
+
return await this.requestJson("GET", path17, void 0, signal);
|
|
660
660
|
}
|
|
661
|
-
async requestJson(method,
|
|
662
|
-
const url = `${this.baseUrl}${
|
|
661
|
+
async requestJson(method, path17, body, signal) {
|
|
662
|
+
const url = `${this.baseUrl}${path17.startsWith("/") ? path17 : `/${path17}`}`;
|
|
663
663
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
664
664
|
const outcome = await this.attempt(method, url, body, attempt, signal);
|
|
665
665
|
if (outcome.kind === "value") return outcome.value;
|
|
@@ -798,12 +798,12 @@ var BackendClient = class {
|
|
|
798
798
|
}
|
|
799
799
|
async pollJob(jobId, signal) {
|
|
800
800
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
801
|
-
const
|
|
801
|
+
const path17 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
802
802
|
for (let attempt = 0; ; attempt++) {
|
|
803
803
|
if (signal?.aborted) {
|
|
804
804
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
805
805
|
}
|
|
806
|
-
const job = await this.http.getJson(
|
|
806
|
+
const job = await this.http.getJson(path17, signal);
|
|
807
807
|
if (job.status === "completed") return job.result;
|
|
808
808
|
if (job.status === "failed") throw failedJobError(job.error);
|
|
809
809
|
if (Date.now() > deadline) {
|
|
@@ -812,10 +812,10 @@ var BackendClient = class {
|
|
|
812
812
|
await sleep(pollInterval(attempt));
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
|
-
presignAssetUpload(sha256, mime, signal) {
|
|
815
|
+
presignAssetUpload(sha256, mime, signal, purpose) {
|
|
816
816
|
return this.http.postJson(
|
|
817
817
|
"/api/canvas/assets/presign",
|
|
818
|
-
{ sha256, mime },
|
|
818
|
+
{ sha256, mime, purpose },
|
|
819
819
|
signal
|
|
820
820
|
);
|
|
821
821
|
}
|
|
@@ -840,6 +840,39 @@ var BackendClient = class {
|
|
|
840
840
|
async recordRun(payload, signal) {
|
|
841
841
|
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
842
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Resumable-run lookup — GET /api/canvas/runs/active. Returns the newest
|
|
845
|
+
* still-running/interrupted run for the creative (pinned to the exact canvas
|
|
846
|
+
* sha), so a fresh sandbox can adopt its run id and re-attach billed
|
|
847
|
+
* in-flight jobs. Null on no active run — or an older backend without the
|
|
848
|
+
* route (both 404).
|
|
849
|
+
*/
|
|
850
|
+
async getActiveRun(creativeSlug, canvasSha, signal) {
|
|
851
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
852
|
+
if (canvasSha) params.set("sha", canvasSha);
|
|
853
|
+
try {
|
|
854
|
+
const res = await this.http.getJson(`/api/canvas/runs/active?${params}`, signal);
|
|
855
|
+
return res.run;
|
|
856
|
+
} catch (e) {
|
|
857
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
858
|
+
throw e;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Portable-rerun lookup — GET /api/canvas/runs/latest. The newest run for
|
|
863
|
+
* the creative that recorded a canvas snapshot manifest; null when none
|
|
864
|
+
* exists (or the backend predates the route — both 404).
|
|
865
|
+
*/
|
|
866
|
+
async getLatestSnapshotRun(creativeSlug, signal) {
|
|
867
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
868
|
+
try {
|
|
869
|
+
const res = await this.http.getJson(`/api/canvas/runs/latest?${params}`, signal);
|
|
870
|
+
return res.run;
|
|
871
|
+
} catch (e) {
|
|
872
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
873
|
+
throw e;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
843
876
|
/**
|
|
844
877
|
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
845
878
|
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
@@ -849,8 +882,8 @@ var BackendClient = class {
|
|
|
849
882
|
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
850
883
|
}
|
|
851
884
|
getArtifact(kind, name, version, signal) {
|
|
852
|
-
const
|
|
853
|
-
return this.http.getJson(
|
|
885
|
+
const path17 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
886
|
+
return this.http.getJson(path17, signal);
|
|
854
887
|
}
|
|
855
888
|
};
|
|
856
889
|
|
|
@@ -907,6 +940,14 @@ var NodeExecutionError = class extends CanvasError {
|
|
|
907
940
|
this.cause = cause;
|
|
908
941
|
}
|
|
909
942
|
};
|
|
943
|
+
var RunAbortedError = class extends CanvasError {
|
|
944
|
+
reason;
|
|
945
|
+
constructor(reason, message2) {
|
|
946
|
+
super(message2);
|
|
947
|
+
this.name = "RunAbortedError";
|
|
948
|
+
this.reason = reason;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
910
951
|
var LayerExecutionError = class extends CanvasError {
|
|
911
952
|
failures;
|
|
912
953
|
constructor(failures) {
|
|
@@ -984,6 +1025,7 @@ var OPENROUTER_IMAGE_AR = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:1
|
|
|
984
1025
|
var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8", "8:1"];
|
|
985
1026
|
var OPENROUTER_IMAGE_SIZE = ["1K", "2K", "4K"];
|
|
986
1027
|
var OPENROUTER_IMAGE_SIZE_EXTENDED = ["0.5K", ...OPENROUTER_IMAGE_SIZE];
|
|
1028
|
+
var OPENROUTER_IMAGE_QUALITY = ["auto", "low", "medium", "high"];
|
|
987
1029
|
var SEEDANCE_DURATIONS = [4, 5, 6, 8, 10, 12, 15];
|
|
988
1030
|
var ELEVENLABS_OUTPUT_FORMATS = [
|
|
989
1031
|
"mp3_22050_32",
|
|
@@ -1007,6 +1049,13 @@ var IMAGE_GENERATE_MODELS = [
|
|
|
1007
1049
|
"google/gemini-3-pro-image-preview",
|
|
1008
1050
|
"recraft/recraft-v4.1-pro-vector"
|
|
1009
1051
|
];
|
|
1052
|
+
var VIDEO_GENERATE_MODELS = [
|
|
1053
|
+
"bytedance/seedance-2.0",
|
|
1054
|
+
"google/veo-3.1",
|
|
1055
|
+
"google/veo-3.1-fast",
|
|
1056
|
+
"kwaivgi/kling-v3.0-pro"
|
|
1057
|
+
];
|
|
1058
|
+
var DEFAULT_VIDEO_GENERATE_MODEL = "bytedance/seedance-2.0";
|
|
1010
1059
|
var MODEL_REGISTRY = {
|
|
1011
1060
|
text_generate: {
|
|
1012
1061
|
"~google/gemini-flash-latest": {
|
|
@@ -1102,7 +1151,8 @@ var MODEL_REGISTRY = {
|
|
|
1102
1151
|
params: {
|
|
1103
1152
|
prompt: { kind: "string" },
|
|
1104
1153
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1105
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1154
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1155
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1106
1156
|
}
|
|
1107
1157
|
},
|
|
1108
1158
|
"google/gemini-3.5-flash": {
|
|
@@ -1114,7 +1164,8 @@ var MODEL_REGISTRY = {
|
|
|
1114
1164
|
params: {
|
|
1115
1165
|
prompt: { kind: "string" },
|
|
1116
1166
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
|
|
1117
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
|
|
1167
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED },
|
|
1168
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1118
1169
|
}
|
|
1119
1170
|
},
|
|
1120
1171
|
"google/gemini-3.1-flash-image-preview": {
|
|
@@ -1136,7 +1187,8 @@ var MODEL_REGISTRY = {
|
|
|
1136
1187
|
params: {
|
|
1137
1188
|
prompt: { kind: "string" },
|
|
1138
1189
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1139
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1190
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1191
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1140
1192
|
}
|
|
1141
1193
|
},
|
|
1142
1194
|
"recraft/recraft-v4.1-pro-vector": {
|
|
@@ -1239,6 +1291,53 @@ var MODEL_REGISTRY = {
|
|
|
1239
1291
|
generate_audio: { kind: "boolean" }
|
|
1240
1292
|
}
|
|
1241
1293
|
},
|
|
1294
|
+
"google/veo-3.1": {
|
|
1295
|
+
// Photoreal CINE CEILING + the real-face fallback (Veo generates adult
|
|
1296
|
+
// humans from a keyframe, dodging ByteDance's real-person filter). Same
|
|
1297
|
+
// OpenRouter google-vertex routing as the fast tier; the quality dial is
|
|
1298
|
+
// `resolution: 1080p` + `generate_audio` + a real `negative_prompt`, not a
|
|
1299
|
+
// separate provider knob. Reach for this for hero beats and any clip that
|
|
1300
|
+
// must carry a real human likeness.
|
|
1301
|
+
label: "Google Veo 3.1",
|
|
1302
|
+
inputs: [],
|
|
1303
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1304
|
+
required: ["prompt"],
|
|
1305
|
+
params: {
|
|
1306
|
+
prompt: { kind: "string" },
|
|
1307
|
+
negative_prompt: { kind: "string" },
|
|
1308
|
+
aspect_ratio: { kind: "string", enum: ["16:9", "9:16"] },
|
|
1309
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1310
|
+
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1311
|
+
seed: { kind: "number" },
|
|
1312
|
+
generate_audio: { kind: "boolean" },
|
|
1313
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1314
|
+
enhance_prompt: { kind: "boolean" },
|
|
1315
|
+
conditioning_scale: { kind: "number" }
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
"kwaivgi/kling-v3.0-pro": {
|
|
1319
|
+
// Motion-transfer / dynamic multi-shot beats. Reachable through the default
|
|
1320
|
+
// OpenRouter gateway (generic video body — no google-vertex block), so it
|
|
1321
|
+
// needs no direct-provider exception. Cost is usage-based (known from the
|
|
1322
|
+
// provider response), so it has no pre-flight cost estimate. `cfg_scale`
|
|
1323
|
+
// trades prompt adherence vs motion freedom; higher = closer to prompt.
|
|
1324
|
+
label: "Kling 3.0",
|
|
1325
|
+
inputs: [],
|
|
1326
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1327
|
+
required: ["prompt"],
|
|
1328
|
+
params: {
|
|
1329
|
+
// Kling caps the prompt shorter than Seedance; gate it here so an
|
|
1330
|
+
// over-length prompt fails validate (free) not the billed call.
|
|
1331
|
+
prompt: { kind: "string", maxLength: 2500 },
|
|
1332
|
+
negative_prompt: { kind: "string" },
|
|
1333
|
+
aspect_ratio: { kind: "string", enum: ["1:1", "16:9", "9:16"] },
|
|
1334
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1335
|
+
duration: { kind: "number", enum: [5, 10] },
|
|
1336
|
+
seed: { kind: "number" },
|
|
1337
|
+
generate_audio: { kind: "boolean" },
|
|
1338
|
+
cfg_scale: { kind: "number", min: 0, max: 1 }
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1242
1341
|
"google/veo-3.1-fast": {
|
|
1243
1342
|
// Cheap test/iteration model. Forwarded by the backend via
|
|
1244
1343
|
// `provider.options.google-vertex.parameters` (camelCased on the wire).
|
|
@@ -1732,18 +1831,43 @@ function message(e) {
|
|
|
1732
1831
|
}
|
|
1733
1832
|
|
|
1734
1833
|
// src/engine/nodes/remote/upload.ts
|
|
1834
|
+
var PUT_MAX_ATTEMPTS = 4;
|
|
1835
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1735
1836
|
async function presignAndPut(args) {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1837
|
+
let lastFailure = null;
|
|
1838
|
+
for (let attempt = 0; attempt < PUT_MAX_ATTEMPTS; attempt++) {
|
|
1839
|
+
if (args.ctx.signal?.aborted) break;
|
|
1840
|
+
if (attempt > 0) await sleep2(500 * 2 ** (attempt - 1) * (1 + Math.random() * 0.25));
|
|
1841
|
+
const result = await attemptPresignedPut(args);
|
|
1842
|
+
if (result.ok) return result.url;
|
|
1843
|
+
lastFailure = result.failure;
|
|
1844
|
+
if (!result.retryable) break;
|
|
1845
|
+
}
|
|
1846
|
+
throw lastFailure ?? new Error("upload: aborted before the PUT could start");
|
|
1847
|
+
}
|
|
1848
|
+
async function attemptPresignedPut(args) {
|
|
1849
|
+
try {
|
|
1850
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1851
|
+
const putRes = await fetch(putUrl, {
|
|
1852
|
+
method: "PUT",
|
|
1853
|
+
body: new Uint8Array(args.bytes),
|
|
1854
|
+
headers: { "Content-Type": args.mime },
|
|
1855
|
+
signal: args.ctx.signal
|
|
1856
|
+
});
|
|
1857
|
+
if (putRes.ok) return { ok: true, url: publicUrl };
|
|
1858
|
+
return {
|
|
1859
|
+
ok: false,
|
|
1860
|
+
failure: new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`),
|
|
1861
|
+
// Only transient statuses warrant a replay — a 400/403 fails identically every attempt.
|
|
1862
|
+
retryable: putRes.status >= 500 || putRes.status === 429 || putRes.status === 408
|
|
1863
|
+
};
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
return {
|
|
1866
|
+
ok: false,
|
|
1867
|
+
failure: e instanceof Error ? e : new Error(String(e)),
|
|
1868
|
+
retryable: args.ctx.signal?.aborted !== true
|
|
1869
|
+
};
|
|
1745
1870
|
}
|
|
1746
|
-
return publicUrl;
|
|
1747
1871
|
}
|
|
1748
1872
|
async function ensureUploaded(ref, ctx) {
|
|
1749
1873
|
if (ref.url) return ref;
|
|
@@ -1803,6 +1927,16 @@ var OutputRef = z.object({
|
|
|
1803
1927
|
}).strict();
|
|
1804
1928
|
var VideoMeta = z.object({
|
|
1805
1929
|
duration_s: z.number(),
|
|
1930
|
+
// The one-clock contract: the spine's summed clip lengths and every audio
|
|
1931
|
+
// timeline reachable from the final mux must equal `expected_total_s` — the
|
|
1932
|
+
// validator recomputes both from the LIVE graph (`-shortest` at the final mux
|
|
1933
|
+
// silently truncates the longer stream on any mismatch). Emitted by
|
|
1934
|
+
// scaffold-video; optional so hand-authored canvases still parse.
|
|
1935
|
+
timeline: z.object({
|
|
1936
|
+
spine_node: z.string(),
|
|
1937
|
+
expected_total_s: z.number(),
|
|
1938
|
+
audio_mix_node: z.string().optional()
|
|
1939
|
+
}).strict().optional(),
|
|
1806
1940
|
// Each sequenced voiceover turn on the absolute timeline.
|
|
1807
1941
|
vo_segments: z.array(
|
|
1808
1942
|
z.object({
|
|
@@ -2280,8 +2414,111 @@ function dfsCycle(u, color, stack, reverseAdj) {
|
|
|
2280
2414
|
}
|
|
2281
2415
|
|
|
2282
2416
|
// src/engine/engine/validator.ts
|
|
2417
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2418
|
+
import path3 from "path";
|
|
2283
2419
|
import { z as z2 } from "zod";
|
|
2284
2420
|
|
|
2421
|
+
// src/engine/scaffold/lib/prompt-profiles.ts
|
|
2422
|
+
var VEO_PERSON_GENERATION = "allow_adult";
|
|
2423
|
+
var VEO_NEGATIVE_PROMPT = "subtitles, captions, on-screen text, watermark, logo, warped face, distorted hands, extra fingers, low quality";
|
|
2424
|
+
var VEO_DURATIONS = [4, 6, 8];
|
|
2425
|
+
var KLING_DURATIONS = [5, 10];
|
|
2426
|
+
var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
|
|
2427
|
+
var KLING_CFG_SCALE = 0.7;
|
|
2428
|
+
var SPEAKS_PROSE = (line) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues). Dialogue: "${line}"`;
|
|
2429
|
+
var SEEDANCE_PROFILE = {
|
|
2430
|
+
id: "seedance",
|
|
2431
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2432
|
+
extraDirectives: [],
|
|
2433
|
+
// Seedance has no negative_prompt field. Phrase the anti-artifact intent as the POSITIVE
|
|
2434
|
+
// state we want (crisp hands, one stable face, smooth coherent motion, locked identity) —
|
|
2435
|
+
// an "Avoid: warped fingers, morphing faces" list tends to plant those very artifacts.
|
|
2436
|
+
stabilityDirectives: [
|
|
2437
|
+
"hands and fingers crisp and anatomically correct",
|
|
2438
|
+
"one consistent face and identity across every frame",
|
|
2439
|
+
"smooth, temporally coherent motion that holds steady frame to frame"
|
|
2440
|
+
],
|
|
2441
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; change only the motion described.",
|
|
2442
|
+
// ~120 words is the Seedance quality sweet spot: past it the model starts dropping beats and
|
|
2443
|
+
// the attention budget dilutes. The validator warns above this; composite scenes that stack
|
|
2444
|
+
// many brief `parts` are the usual offenders.
|
|
2445
|
+
wordBudget: 120,
|
|
2446
|
+
durationSet: SEEDANCE_DURATIONS,
|
|
2447
|
+
paramDefaults: {}
|
|
2448
|
+
};
|
|
2449
|
+
var VEO_PROFILE = {
|
|
2450
|
+
id: "veo",
|
|
2451
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2452
|
+
// Veo's known failure mode: quoted dialogue triggers burned-in subtitles. Belt
|
|
2453
|
+
// (inline) and braces (the negative_prompt param, below) — the param is the real lever.
|
|
2454
|
+
extraDirectives: ["No subtitles, no captions, no on-screen text of any kind."],
|
|
2455
|
+
stabilityDirectives: [],
|
|
2456
|
+
// Veo takes a negative_prompt PARAM instead (paramDefaults).
|
|
2457
|
+
keyframeInstruction: "Describe the transition and camera move between the frames; the frames fix the content.",
|
|
2458
|
+
wordBudget: 200,
|
|
2459
|
+
durationSet: VEO_DURATIONS,
|
|
2460
|
+
paramDefaults: { person_generation: VEO_PERSON_GENERATION, negative_prompt: VEO_NEGATIVE_PROMPT }
|
|
2461
|
+
};
|
|
2462
|
+
var KLING_PROFILE = {
|
|
2463
|
+
id: "kling",
|
|
2464
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2465
|
+
// Belt (inline) and braces (the negative_prompt param) — the param is the real lever.
|
|
2466
|
+
extraDirectives: ["Single, deliberate camera move \u2014 no whip pans or rapid cuts within the clip."],
|
|
2467
|
+
stabilityDirectives: [],
|
|
2468
|
+
// Kling takes a negative_prompt PARAM instead (paramDefaults).
|
|
2469
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; animate the motion described.",
|
|
2470
|
+
wordBudget: 200,
|
|
2471
|
+
durationSet: KLING_DURATIONS,
|
|
2472
|
+
paramDefaults: { negative_prompt: KLING_NEGATIVE_PROMPT, cfg_scale: KLING_CFG_SCALE }
|
|
2473
|
+
};
|
|
2474
|
+
function clipProfileFor(modelId) {
|
|
2475
|
+
if (/^bytedance\/seedance/.test(modelId)) return SEEDANCE_PROFILE;
|
|
2476
|
+
if (/^google\/veo/.test(modelId)) return VEO_PROFILE;
|
|
2477
|
+
if (/^kwaivgi\/kling|^kling\//.test(modelId)) return KLING_PROFILE;
|
|
2478
|
+
return void 0;
|
|
2479
|
+
}
|
|
2480
|
+
function clipParamRecipe(profile, intent) {
|
|
2481
|
+
const out = {};
|
|
2482
|
+
if (intent === "hero") {
|
|
2483
|
+
out.resolution = "1080p";
|
|
2484
|
+
}
|
|
2485
|
+
if (intent === "hook" && profile.id === "kling") {
|
|
2486
|
+
out.cfg_scale = 0.85;
|
|
2487
|
+
}
|
|
2488
|
+
return out;
|
|
2489
|
+
}
|
|
2490
|
+
function nativeDialogueOf(prompt) {
|
|
2491
|
+
if (typeof prompt !== "string") return void 0;
|
|
2492
|
+
const m = prompt.match(/Dialogue: "(.*)"/);
|
|
2493
|
+
return m?.[1]?.trim() || void 0;
|
|
2494
|
+
}
|
|
2495
|
+
var GPT_IMAGE_PROFILE = {
|
|
2496
|
+
id: "gpt-image",
|
|
2497
|
+
constraintPlacement: "last",
|
|
2498
|
+
photorealCue: true,
|
|
2499
|
+
// OpenRouter forwards `quality`; gpt-image-2 already processes inputs at high
|
|
2500
|
+
// fidelity automatically, so we deliberately do NOT send `input_fidelity`.
|
|
2501
|
+
paramDefaults: { quality: "high" }
|
|
2502
|
+
};
|
|
2503
|
+
var GEMINI_IMAGE_PROFILE = {
|
|
2504
|
+
id: "gemini",
|
|
2505
|
+
constraintPlacement: "inline",
|
|
2506
|
+
photorealCue: true,
|
|
2507
|
+
paramDefaults: { quality: "high" }
|
|
2508
|
+
};
|
|
2509
|
+
var RECRAFT_IMAGE_PROFILE = {
|
|
2510
|
+
id: "recraft",
|
|
2511
|
+
constraintPlacement: "inline",
|
|
2512
|
+
photorealCue: false,
|
|
2513
|
+
paramDefaults: {}
|
|
2514
|
+
};
|
|
2515
|
+
function imageProfileFor(modelId) {
|
|
2516
|
+
if (/^openai\/gpt-.*image/.test(modelId)) return GPT_IMAGE_PROFILE;
|
|
2517
|
+
if (/^google\/gemini/.test(modelId)) return GEMINI_IMAGE_PROFILE;
|
|
2518
|
+
if (/^recraft\//.test(modelId)) return RECRAFT_IMAGE_PROFILE;
|
|
2519
|
+
return void 0;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2285
2522
|
// src/engine/engine/define.ts
|
|
2286
2523
|
function resolveOutputKinds(spec, params) {
|
|
2287
2524
|
if (!spec) return {};
|
|
@@ -2363,16 +2600,62 @@ var STAGE_CODES = {
|
|
|
2363
2600
|
REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
|
|
2364
2601
|
SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL",
|
|
2365
2602
|
UI_IN_PROMPT: "VIDEO_UI_IN_PROMPT",
|
|
2366
|
-
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT"
|
|
2603
|
+
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT",
|
|
2604
|
+
SPEECH_EXCEEDS_EXTRACT: "VIDEO_SPEECH_EXCEEDS_EXTRACT",
|
|
2605
|
+
PROMPT_PROFILE_MISSING: "VIDEO_PROMPT_PROFILE_MISSING",
|
|
2606
|
+
PROMPT_DECISION_MISSING: "VIDEO_PROMPT_DECISION_MISSING",
|
|
2607
|
+
HOOK_LAYER_MISSING: "VIDEO_HOOK_LAYER_MISSING",
|
|
2608
|
+
IMAGE_PROFILE_MISSING: "IMAGE_PROMPT_PROFILE_MISSING",
|
|
2609
|
+
PROMPT_OVER_BUDGET: "VIDEO_PROMPT_OVER_BUDGET",
|
|
2610
|
+
PERSON_GENERATION_MISSING: "VIDEO_PERSON_GENERATION_MISSING",
|
|
2611
|
+
RAW_FACE_KEYFRAME: "VIDEO_RAW_FACE_KEYFRAME",
|
|
2612
|
+
TIMELINE_TOTAL: "VIDEO_TIMELINE_TOTAL_MISMATCH",
|
|
2613
|
+
NATIVE_SEG_OVERLAP: "VIDEO_NATIVE_SEG_OVERLAP",
|
|
2614
|
+
SPINE_UNNORMALIZED: "VIDEO_SPINE_UNNORMALIZED",
|
|
2615
|
+
ODD_DIMENSIONS: "VIDEO_ODD_DIMENSIONS",
|
|
2616
|
+
REGION_DROPPED: "VIDEO_REGION_DROPPED",
|
|
2617
|
+
OVERLAY_OUT_OF_BOUNDS: "VIDEO_OVERLAY_OUT_OF_BOUNDS"
|
|
2367
2618
|
};
|
|
2368
2619
|
var SPAN_MODEL_SLACK_S = 0.25;
|
|
2369
2620
|
var VIDEO_TIME_SLACK_S = 0.75;
|
|
2370
2621
|
var SPEECH_WORDS_PER_SECOND = 2.5;
|
|
2371
2622
|
var SPEECH_OVERRUN_RATIO = 1.6;
|
|
2372
|
-
function
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2623
|
+
function ffmpegTrimSeconds(node) {
|
|
2624
|
+
const args = node.params?.args;
|
|
2625
|
+
if (!Array.isArray(args)) return null;
|
|
2626
|
+
let t = null;
|
|
2627
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
2628
|
+
if (args[i] === "-t") {
|
|
2629
|
+
const v = Number(args[i + 1]);
|
|
2630
|
+
if (Number.isFinite(v)) t = v;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return t;
|
|
2634
|
+
}
|
|
2635
|
+
function refNodeOf(ctx, value) {
|
|
2636
|
+
if (typeof value !== "string" || !value.startsWith(REF_PREFIX)) return null;
|
|
2637
|
+
const parsed = parseRefExpr(value);
|
|
2638
|
+
if (!parsed) return null;
|
|
2639
|
+
const idx = ctx.idToIndex.get(parsed.nodeId);
|
|
2640
|
+
return idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
2641
|
+
}
|
|
2642
|
+
function liveNodeDurationS(ctx, node, depth = 0) {
|
|
2643
|
+
if (!node || depth > 3) return null;
|
|
2644
|
+
if (node.type === "video_generate") {
|
|
2645
|
+
const d = node.params?.duration;
|
|
2646
|
+
return typeof d === "number" ? d : null;
|
|
2647
|
+
}
|
|
2648
|
+
if (node.type === "ffmpeg") {
|
|
2649
|
+
const t = ffmpegTrimSeconds(node);
|
|
2650
|
+
if (t !== null) return t;
|
|
2651
|
+
for (const v of Object.values(node.inputs ?? {})) {
|
|
2652
|
+
const upstream = refNodeOf(ctx, v);
|
|
2653
|
+
const d = liveNodeDurationS(ctx, upstream, depth + 1);
|
|
2654
|
+
if (d !== null) return d;
|
|
2655
|
+
}
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
return null;
|
|
2376
2659
|
}
|
|
2377
2660
|
function validateCanvas(input, registry) {
|
|
2378
2661
|
const issues = [];
|
|
@@ -2434,6 +2717,7 @@ async function validateCanvasDeep(input, registry) {
|
|
|
2434
2717
|
});
|
|
2435
2718
|
}
|
|
2436
2719
|
}
|
|
2720
|
+
await checkOverlayTimingBounds(canvas, issues);
|
|
2437
2721
|
const hasBlocking = issues.some(isBlocking);
|
|
2438
2722
|
if (hasBlocking) return { ok: false, issues };
|
|
2439
2723
|
const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
|
|
@@ -2675,6 +2959,38 @@ function estimateCredits(ctx) {
|
|
|
2675
2959
|
}
|
|
2676
2960
|
return total;
|
|
2677
2961
|
}
|
|
2962
|
+
function overlayTimingIssues(html, durationS) {
|
|
2963
|
+
const out = [];
|
|
2964
|
+
for (const m of html.matchAll(/<[^>]*\bdata-start="([\d.]+)"[^>]*\bdata-dur="([\d.]+)"[^>]*>/g)) {
|
|
2965
|
+
const start = Number(m[1]);
|
|
2966
|
+
const dur = Number(m[2]);
|
|
2967
|
+
if (!Number.isFinite(start) || !Number.isFinite(dur)) continue;
|
|
2968
|
+
if (start >= durationS || start + dur > durationS + 0.25) out.push({ start, dur });
|
|
2969
|
+
}
|
|
2970
|
+
return out;
|
|
2971
|
+
}
|
|
2972
|
+
async function checkOverlayTimingBounds(canvas, issues) {
|
|
2973
|
+
const durationS = canvas.metadata?.video?.duration_s;
|
|
2974
|
+
if (typeof durationS !== "number") return;
|
|
2975
|
+
for (let i = 0; i < canvas.nodes.length; i++) {
|
|
2976
|
+
const n = canvas.nodes[i];
|
|
2977
|
+
if (n?.type !== "hyperframe_render") continue;
|
|
2978
|
+
const composition = n.params?.composition;
|
|
2979
|
+
if (typeof composition !== "string" || !path3.isAbsolute(composition)) continue;
|
|
2980
|
+
const html = await readFile2(path3.join(composition, "index.html"), "utf8").catch(() => null);
|
|
2981
|
+
if (!html) continue;
|
|
2982
|
+
for (const w of overlayTimingIssues(html, durationS)) {
|
|
2983
|
+
issues.push({
|
|
2984
|
+
path: `nodes[${i}].params.composition`,
|
|
2985
|
+
code: STAGE_CODES.OVERLAY_OUT_OF_BOUNDS,
|
|
2986
|
+
severity: "warning",
|
|
2987
|
+
node_id: n.id,
|
|
2988
|
+
node_type: n.type,
|
|
2989
|
+
message: `an overlay in ${path3.basename(composition)}/index.html runs [${w.start}s, ${w.start + w.dur}s] but the video ends at ${durationS}s \u2014 it never hides on screen. Re-time its data-start/data-dur to fit the video`
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2678
2994
|
function nativeAudioReachesMix(ctx, scene) {
|
|
2679
2995
|
const wanted = [
|
|
2680
2996
|
`$ref:s${scene}_voextract.audio`,
|
|
@@ -2707,6 +3023,13 @@ function talkingSceneSatisfied(ctx, entry, scene) {
|
|
|
2707
3023
|
});
|
|
2708
3024
|
}
|
|
2709
3025
|
function checkVideoInvariants(ctx) {
|
|
3026
|
+
checkPromptProfile(ctx);
|
|
3027
|
+
checkImageProfile(ctx);
|
|
3028
|
+
checkPersonGeneration(ctx);
|
|
3029
|
+
checkPromptBudget(ctx);
|
|
3030
|
+
checkRawFaceKeyframe(ctx);
|
|
3031
|
+
checkPromptDecisions(ctx);
|
|
3032
|
+
checkHookLayers(ctx);
|
|
2710
3033
|
const meta = ctx.canvas.metadata?.video;
|
|
2711
3034
|
if (!meta) return;
|
|
2712
3035
|
const segments = [...meta.vo_segments].sort((a, b) => a.start_s - b.start_s);
|
|
@@ -2746,6 +3069,286 @@ function checkVideoInvariants(ctx) {
|
|
|
2746
3069
|
checkBrandmarkInPrompt(ctx);
|
|
2747
3070
|
checkReferenceCompleteness(ctx, meta);
|
|
2748
3071
|
checkClipSpanFitsModel(ctx, meta);
|
|
3072
|
+
checkTimelineContract(ctx, meta);
|
|
3073
|
+
checkNativeSegOverlap(ctx);
|
|
3074
|
+
checkSpineNormalized(ctx, meta);
|
|
3075
|
+
checkOddDimensions(ctx);
|
|
3076
|
+
checkRegionDropped(ctx);
|
|
3077
|
+
}
|
|
3078
|
+
function checkPromptProfile(ctx) {
|
|
3079
|
+
for (const n of ctx.canvas.nodes) {
|
|
3080
|
+
if (n.type !== "video_generate") continue;
|
|
3081
|
+
const model = n.params?.model;
|
|
3082
|
+
if (typeof model !== "string" || clipProfileFor(model)) continue;
|
|
3083
|
+
ctx.issues.push({
|
|
3084
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3085
|
+
code: STAGE_CODES.PROMPT_PROFILE_MISSING,
|
|
3086
|
+
severity: "warning",
|
|
3087
|
+
node_id: n.id,
|
|
3088
|
+
message: `"${n.id}" runs on "${model}", a model with no clip-prompt profile \u2014 its prompt was authored with the default Seedance syntax. Review the dialogue/emotion markup for this model before billing`
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
var TECHNIQUE_CUE = /\b(camera|push[- ]?in|pull[- ]?out|pan|dolly|zoom|tilt|track|handheld|locked|orbit|crane|whip|motion|move)\b/i;
|
|
3093
|
+
var NEGATIVES_CUE = /\bavoid:|\bno subtitles|\bnegative\b|keep it clean and stable/i;
|
|
3094
|
+
var VIBE_CUE = /\b(light|lighting|golden|moody|warm|cool|tone|grade|contrast|shadow|glow|rim|backlit|neon|soft|harsh)\b/i;
|
|
3095
|
+
function checkPromptDecisions(ctx) {
|
|
3096
|
+
for (const n of ctx.canvas.nodes) {
|
|
3097
|
+
if (n.type !== "video_generate") continue;
|
|
3098
|
+
const params = n.params;
|
|
3099
|
+
const prompt = typeof params?.prompt === "string" ? params.prompt : "";
|
|
3100
|
+
if (!prompt) continue;
|
|
3101
|
+
const hasTechnique = TECHNIQUE_CUE.test(prompt);
|
|
3102
|
+
const hasNegatives = NEGATIVES_CUE.test(prompt) || typeof params?.negative_prompt === "string";
|
|
3103
|
+
if (hasTechnique || hasNegatives) continue;
|
|
3104
|
+
ctx.issues.push({
|
|
3105
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3106
|
+
code: STAGE_CODES.PROMPT_DECISION_MISSING,
|
|
3107
|
+
severity: "warning",
|
|
3108
|
+
node_id: n.id,
|
|
3109
|
+
message: `"${n.id}" is a thin clip prompt \u2014 it names no camera/motion move (TECHNIQUE) and no constraints (NEGATIVES: an affirmative "Keep it clean and stable" line, a negative_prompt param, or an "Avoid:" tail). Make the six decisions (route / spec / beats / copy / technique / negatives) so the model doesn't fill the gaps with drift. See prompt-anatomy.md`
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function checkHookLayers(ctx) {
|
|
3114
|
+
const scene0 = ctx.canvas.nodes.filter((n) => /^s0(?:[_a-z0-9])*_/.test(n.id) || n.id === "s0");
|
|
3115
|
+
const hookClip = scene0.find((n) => n.type === "video_generate");
|
|
3116
|
+
if (!hookClip) return;
|
|
3117
|
+
const scene0Text = scene0.map((n) => {
|
|
3118
|
+
const p = n.params?.prompt;
|
|
3119
|
+
return typeof p === "string" ? p : "";
|
|
3120
|
+
}).join("\n");
|
|
3121
|
+
const hookParams = hookClip.params;
|
|
3122
|
+
const hasOverlayLayer = ctx.canvas.nodes.some((n) => /hyperframe/.test(n.type));
|
|
3123
|
+
const hasSound = hookParams?.generate_audio === true || /Dialogue:|Audio:/.test(scene0Text) || ctx.canvas.nodes.some((n) => n.type === "tts" || n.type === "dialogue" || n.type === "music");
|
|
3124
|
+
const hasVisual = scene0Text.trim().length > 40;
|
|
3125
|
+
const hasVibe = VIBE_CUE.test(scene0Text);
|
|
3126
|
+
const missing = [];
|
|
3127
|
+
if (!hasOverlayLayer) missing.push("text (overlay)");
|
|
3128
|
+
if (!hasSound) missing.push("sound (line/SFX)");
|
|
3129
|
+
if (!hasVisual) missing.push("visual (the frame)");
|
|
3130
|
+
if (!hasVibe) missing.push("vibe (lighting/tone)");
|
|
3131
|
+
if (missing.length < 2) return;
|
|
3132
|
+
ctx.issues.push({
|
|
3133
|
+
path: `nodes[${ctx.idToIndex.get(hookClip.id) ?? -1}]`,
|
|
3134
|
+
code: STAGE_CODES.HOOK_LAYER_MISSING,
|
|
3135
|
+
severity: "warning",
|
|
3136
|
+
node_id: hookClip.id,
|
|
3137
|
+
message: `the hook (scene 0) is thin across layers \u2014 missing ${missing.join(", ")}. A scroll-stopping hook works on all four layers (text / sound / visual / vibe); strengthen the missing ones. See hook-craft.md`
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
function checkImageProfile(ctx) {
|
|
3141
|
+
for (const n of ctx.canvas.nodes) {
|
|
3142
|
+
if (n.type !== "image_generate") continue;
|
|
3143
|
+
const model = n.params?.model;
|
|
3144
|
+
if (typeof model !== "string" || imageProfileFor(model)) continue;
|
|
3145
|
+
ctx.issues.push({
|
|
3146
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3147
|
+
code: STAGE_CODES.IMAGE_PROFILE_MISSING,
|
|
3148
|
+
severity: "warning",
|
|
3149
|
+
node_id: n.id,
|
|
3150
|
+
message: `"${n.id}" renders on "${model}", an image model with no frame-prompt profile \u2014 it got the generic template (constraint placement / photoreal cue not tuned). Add an ImageModelProfile for this model`
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
function checkPersonGeneration(ctx) {
|
|
3155
|
+
for (const n of ctx.canvas.nodes) {
|
|
3156
|
+
if (n.type !== "video_generate") continue;
|
|
3157
|
+
const params = n.params;
|
|
3158
|
+
const model = params?.model;
|
|
3159
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "veo") continue;
|
|
3160
|
+
const hasKeyframe = Boolean(n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3161
|
+
if (!hasKeyframe || params?.person_generation) continue;
|
|
3162
|
+
ctx.issues.push({
|
|
3163
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params`,
|
|
3164
|
+
code: STAGE_CODES.PERSON_GENERATION_MISSING,
|
|
3165
|
+
severity: "warning",
|
|
3166
|
+
node_id: n.id,
|
|
3167
|
+
message: `Veo clip "${n.id}" drives from a keyframe but sets no person_generation \u2014 set "allow_adult" (the only legal value for image-to-video, and the only one allowed in the EU/UK)`
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function checkPromptBudget(ctx) {
|
|
3172
|
+
for (const n of ctx.canvas.nodes) {
|
|
3173
|
+
if (n.type !== "video_generate") continue;
|
|
3174
|
+
const params = n.params;
|
|
3175
|
+
const model = params?.model;
|
|
3176
|
+
const prompt = params?.prompt;
|
|
3177
|
+
const profile = typeof model === "string" ? clipProfileFor(model) : void 0;
|
|
3178
|
+
if (!profile || typeof prompt !== "string") continue;
|
|
3179
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean).length;
|
|
3180
|
+
if (words <= profile.wordBudget) continue;
|
|
3181
|
+
ctx.issues.push({
|
|
3182
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3183
|
+
code: STAGE_CODES.PROMPT_OVER_BUDGET,
|
|
3184
|
+
severity: "warning",
|
|
3185
|
+
node_id: n.id,
|
|
3186
|
+
message: `"${n.id}" prompt is ${words} words \u2014 over ${model}'s ~${profile.wordBudget}-word budget; trim it (the frames carry the content, so keep the clip prompt to motion + audio)`
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function checkRawFaceKeyframe(ctx) {
|
|
3191
|
+
for (const n of ctx.canvas.nodes) {
|
|
3192
|
+
if (n.type !== "video_generate") continue;
|
|
3193
|
+
const model = n.params?.model;
|
|
3194
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "seedance") continue;
|
|
3195
|
+
const src = refNodeOf(ctx, n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3196
|
+
if (!src || src.type !== "ingest") continue;
|
|
3197
|
+
ctx.issues.push({
|
|
3198
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].inputs.first_frame`,
|
|
3199
|
+
code: STAGE_CODES.RAW_FACE_KEYFRAME,
|
|
3200
|
+
severity: "warning",
|
|
3201
|
+
node_id: n.id,
|
|
3202
|
+
message: `Seedance clip "${n.id}" animates a RAW ingested image ("${src.id}"), not a generated frame \u2014 if it shows a real human face, ByteDance's real-person filter rejects it (422). Anchor on an AI-generated portrait (recast), or route this clip to Veo`
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function spineTotalS(ctx, spine) {
|
|
3207
|
+
let total = 0;
|
|
3208
|
+
const inputs = Object.entries(spine.inputs ?? {}).filter(([k]) => /^c\d+$/.test(k));
|
|
3209
|
+
if (inputs.length === 0) return null;
|
|
3210
|
+
for (const [, v] of inputs) {
|
|
3211
|
+
const d = liveNodeDurationS(ctx, refNodeOf(ctx, v));
|
|
3212
|
+
if (d === null) return null;
|
|
3213
|
+
total += d;
|
|
3214
|
+
}
|
|
3215
|
+
const graph = spine.params.args?.join?.(" ") ?? "";
|
|
3216
|
+
for (const m of String(graph).matchAll(/xfade=transition=[^:]+:duration=([\d.]+)/g)) {
|
|
3217
|
+
total -= Number(m[1]);
|
|
3218
|
+
}
|
|
3219
|
+
return total;
|
|
3220
|
+
}
|
|
3221
|
+
function checkTimelineContract(ctx, meta) {
|
|
3222
|
+
const stamp = meta.timeline;
|
|
3223
|
+
const spineId = stamp?.spine_node ?? (ctx.idToIndex.has("spine") ? "spine" : null);
|
|
3224
|
+
if (!spineId) return;
|
|
3225
|
+
const expected = stamp?.expected_total_s ?? meta.duration_s;
|
|
3226
|
+
checkSpineTotal(ctx, spineId, expected);
|
|
3227
|
+
checkAudioTimelineTotals(ctx, stamp?.audio_mix_node, expected);
|
|
3228
|
+
}
|
|
3229
|
+
function checkSpineTotal(ctx, spineId, expected) {
|
|
3230
|
+
const spineIdx = ctx.idToIndex.get(spineId);
|
|
3231
|
+
const spine = spineIdx === void 0 ? null : ctx.canvas.nodes[spineIdx];
|
|
3232
|
+
const spineLen = spine ? spineTotalS(ctx, spine) : null;
|
|
3233
|
+
if (spineLen === null || Math.abs(spineLen - expected) <= VIDEO_TIME_SLACK_S) return;
|
|
3234
|
+
ctx.issues.push({
|
|
3235
|
+
path: `nodes[${spineIdx}].params.args`,
|
|
3236
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3237
|
+
node_id: spineId,
|
|
3238
|
+
message: `the picture sums to ${round2(spineLen)}s but the timeline is pinned to ${expected}s \u2014 \`-shortest\` at the final mux will silently cut the longer stream. Re-time the seg trims (or the audio total_ms) so both match`
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
function checkAudioTimelineTotals(ctx, audioMixNode, expected) {
|
|
3242
|
+
const mustBeFullLength = /* @__PURE__ */ new Set();
|
|
3243
|
+
if (audioMixNode) mustBeFullLength.add(audioMixNode);
|
|
3244
|
+
ctx.canvas.nodes.forEach((n) => {
|
|
3245
|
+
if (n.type !== "audio_voice_convert") return;
|
|
3246
|
+
const track = refNodeOf(ctx, (n.inputs ?? {}).audio);
|
|
3247
|
+
if (track?.type === "audio_timeline") mustBeFullLength.add(track.id);
|
|
3248
|
+
});
|
|
3249
|
+
for (const id of mustBeFullLength) {
|
|
3250
|
+
const idx = ctx.idToIndex.get(id);
|
|
3251
|
+
const node = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3252
|
+
const totalMs = node?.params?.total_ms;
|
|
3253
|
+
if (typeof totalMs !== "number") continue;
|
|
3254
|
+
if (Math.abs(totalMs / 1e3 - expected) > VIDEO_TIME_SLACK_S) {
|
|
3255
|
+
ctx.issues.push({
|
|
3256
|
+
path: `nodes[${idx}].params.total_ms`,
|
|
3257
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3258
|
+
node_id: id,
|
|
3259
|
+
message: `audio timeline "${id}" is pinned to ${totalMs}ms but the picture timeline is ${expected}s \u2014 \`-shortest\` at the final mux will silently cut the longer stream. Pin total_ms to ${Math.round(expected * 1e3)}`
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function checkNativeSegOverlap(ctx) {
|
|
3265
|
+
for (const conv of ctx.canvas.nodes) {
|
|
3266
|
+
if (conv.type !== "audio_voice_convert") continue;
|
|
3267
|
+
const track = refNodeOf(ctx, (conv.inputs ?? {}).audio);
|
|
3268
|
+
if (track?.type !== "audio_timeline") continue;
|
|
3269
|
+
const params = track.params;
|
|
3270
|
+
const windows = (params.tracks ?? []).map((t) => {
|
|
3271
|
+
const extract = refNodeOf(ctx, (track.inputs ?? {})[t.slot]);
|
|
3272
|
+
const len = t.duration_s ?? (extract ? ffmpegTrimSeconds(extract) : null);
|
|
3273
|
+
return len === null ? null : { slot: t.slot, start: t.start_s, end: t.start_s + len };
|
|
3274
|
+
}).filter((w) => w !== null).sort((a, b) => a.start - b.start);
|
|
3275
|
+
for (let i = 1; i < windows.length; i++) {
|
|
3276
|
+
const prev = windows[i - 1];
|
|
3277
|
+
const cur = windows[i];
|
|
3278
|
+
if (!prev || !cur || cur.start >= prev.end - 0.01) continue;
|
|
3279
|
+
const trackIdx = ctx.idToIndex.get(track.id);
|
|
3280
|
+
ctx.issues.push({
|
|
3281
|
+
path: `nodes[${trackIdx}].params.tracks`,
|
|
3282
|
+
code: STAGE_CODES.NATIVE_SEG_OVERLAP,
|
|
3283
|
+
node_id: track.id,
|
|
3284
|
+
message: `voice track "${track.id}": "${prev.slot}" runs to ${round2(prev.end)}s but "${cur.slot}" starts at ${cur.start}s \u2014 both play at once (echo). Cap the first with duration_s: ${round2(cur.start - prev.start)} or re-time the windows`
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function checkSpineNormalized(ctx, meta) {
|
|
3290
|
+
const spineId = meta.timeline?.spine_node ?? "spine";
|
|
3291
|
+
const idx = ctx.idToIndex.get(spineId);
|
|
3292
|
+
const spine = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3293
|
+
if (!spine || spine.type !== "ffmpeg") return;
|
|
3294
|
+
const args = spine.params.args;
|
|
3295
|
+
const graph = Array.isArray(args) ? args.join(" ") : "";
|
|
3296
|
+
if (!graph.includes("concat=n=")) return;
|
|
3297
|
+
if (/\[\d+:v\](?:\[|concat)/.test(graph)) {
|
|
3298
|
+
ctx.issues.push({
|
|
3299
|
+
path: `nodes[${idx}].params.args`,
|
|
3300
|
+
code: STAGE_CODES.SPINE_UNNORMALIZED,
|
|
3301
|
+
severity: "warning",
|
|
3302
|
+
node_id: spineId,
|
|
3303
|
+
message: `the spine concat feeds raw input labels \u2014 generated clips carry no fps/SAR guarantee, and one 24fps clip silently stretches the picture off the audio/overlay clock. Chain \`format=yuv420p,fps=30,setsar=1,settb=AVTB\` on every input before the concat`
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function checkOddDimensions(ctx) {
|
|
3308
|
+
ctx.canvas.nodes.forEach((node, idx) => {
|
|
3309
|
+
if (node.type !== "ffmpeg") return;
|
|
3310
|
+
const args = node.params.args;
|
|
3311
|
+
const blob = Array.isArray(args) ? args.join(" ") : "";
|
|
3312
|
+
for (const m of blob.matchAll(/(?:scale|crop|pad)=(\d+):(\d+)/g)) {
|
|
3313
|
+
const w = Number(m[1]);
|
|
3314
|
+
const h = Number(m[2]);
|
|
3315
|
+
if (w % 2 === 0 && h % 2 === 0) continue;
|
|
3316
|
+
ctx.issues.push({
|
|
3317
|
+
path: `nodes[${idx}].params.args`,
|
|
3318
|
+
code: STAGE_CODES.ODD_DIMENSIONS,
|
|
3319
|
+
severity: "warning",
|
|
3320
|
+
node_id: node.id,
|
|
3321
|
+
message: `"${m[0]}" produces an odd dimension \u2014 libx264 yuv420p rejects odd sizes, so this node fails at render time. Round both to even numbers`
|
|
3322
|
+
});
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
function checkRegionDropped(ctx) {
|
|
3328
|
+
const regionClips = /* @__PURE__ */ new Map();
|
|
3329
|
+
for (const node of ctx.canvas.nodes) {
|
|
3330
|
+
const m = /^s(\d+)_r\d+_clip$/.exec(node.id);
|
|
3331
|
+
if (m && node.type === "video_generate")
|
|
3332
|
+
regionClips.set(m[1], (regionClips.get(m[1]) ?? 0) + 1);
|
|
3333
|
+
}
|
|
3334
|
+
for (const [scene, billed] of regionClips) {
|
|
3335
|
+
const idx = ctx.idToIndex.get(`s${scene}_composite`);
|
|
3336
|
+
if (idx === void 0) continue;
|
|
3337
|
+
const composite = ctx.canvas.nodes[idx];
|
|
3338
|
+
const consumed = Object.keys(composite.inputs ?? {}).filter((k) => /^c\d+$/.test(k)).length;
|
|
3339
|
+
if (billed > consumed) {
|
|
3340
|
+
ctx.issues.push({
|
|
3341
|
+
path: `nodes[${idx}].inputs`,
|
|
3342
|
+
code: STAGE_CODES.REGION_DROPPED,
|
|
3343
|
+
severity: "warning",
|
|
3344
|
+
node_id: composite.id,
|
|
3345
|
+
message: `scene s${scene} bills ${billed} region clips but its composite consumes ${consumed} \u2014 the extra generation is paid for and never reaches the frame. Wire every region into the composite or delete the unused clip node`
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function round2(n) {
|
|
3351
|
+
return Math.round(n * 100) / 100;
|
|
2749
3352
|
}
|
|
2750
3353
|
var ELEMENT_TYPE_KEYWORDS = {
|
|
2751
3354
|
animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
|
|
@@ -2873,15 +3476,37 @@ function checkSpeechOverrun(ctx, talkingScenes) {
|
|
|
2873
3476
|
for (const n of ctx.canvas.nodes) {
|
|
2874
3477
|
if (!nativeClipRe.test(n.id) || n.type !== "video_generate") continue;
|
|
2875
3478
|
const overrun = speechOverrunOf(n, secondsPerWord(entry));
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3479
|
+
if (overrun) {
|
|
3480
|
+
ctx.issues.push({
|
|
3481
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3482
|
+
code: STAGE_CODES.SPEECH_OVERRUN,
|
|
3483
|
+
message: `"${n.id}" asks Seedance to speak ~${Math.round(overrun.estSpeechS * 10) / 10}s of dialogue inside a ${overrun.duration}s clip \u2014 the line cannot fit (>${SPEECH_OVERRUN_RATIO}\xD7 the clip). Shorten the line, split the scene, or lengthen the clip duration`
|
|
3484
|
+
});
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
checkSpeechExceedsExtract(ctx, n, entry);
|
|
2882
3488
|
}
|
|
2883
3489
|
}
|
|
2884
3490
|
}
|
|
3491
|
+
function checkSpeechExceedsExtract(ctx, clip, entry) {
|
|
3492
|
+
const params = clip.params;
|
|
3493
|
+
if (params?.generate_audio !== true) return;
|
|
3494
|
+
const line = nativeDialogueOf(params.prompt);
|
|
3495
|
+
if (!line) return;
|
|
3496
|
+
const regionTag = /^s\d+(_r\d+)?_clip$/.exec(clip.id)?.[1] ?? "";
|
|
3497
|
+
const extractIdx = ctx.idToIndex.get(`s${entry.scene}${regionTag}_voextract`) ?? ctx.idToIndex.get(`s${entry.scene}_voextract`);
|
|
3498
|
+
const extract = extractIdx === void 0 ? null : ctx.canvas.nodes[extractIdx];
|
|
3499
|
+
const windowS = extract ? ffmpegTrimSeconds(extract) : null;
|
|
3500
|
+
if (windowS === null) return;
|
|
3501
|
+
const estSpeechS = line.split(/\s+/).filter(Boolean).length * secondsPerWord(entry);
|
|
3502
|
+
if (estSpeechS <= windowS * SPEECH_OVERRUN_RATIO) return;
|
|
3503
|
+
ctx.issues.push({
|
|
3504
|
+
path: `nodes[${ctx.idToIndex.get(clip.id) ?? -1}].params.prompt`,
|
|
3505
|
+
code: STAGE_CODES.SPEECH_EXCEEDS_EXTRACT,
|
|
3506
|
+
node_id: clip.id,
|
|
3507
|
+
message: `"${clip.id}"'s line is ~${Math.round(estSpeechS * 10) / 10}s of speech but its extract window (s${entry.scene}_voextract) is ${windowS}s \u2014 the read gets cut mid-word on the spine. Shorten the Dialogue line, lengthen the scene in prompt.json and re-scaffold, or raise the voextract \`-t\` when the read should carry over the next cutaway`
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
2885
3510
|
var UI_IN_PROMPT_RE = /\bscreen[- ]?(?:recording|capture|grab|share)\b|\bapp (?:interface|screen)\b|\bphone screen overlay\b/i;
|
|
2886
3511
|
function checkUiInPrompt(ctx) {
|
|
2887
3512
|
for (const n of ctx.canvas.nodes) {
|
|
@@ -2954,9 +3579,9 @@ function checkOutputRef(ctx) {
|
|
|
2954
3579
|
function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
2955
3580
|
for (const issue of err.issues) {
|
|
2956
3581
|
const tail2 = pathToString(issue.path);
|
|
2957
|
-
const
|
|
3582
|
+
const path17 = pathPrefix ? tail2 ? `${pathPrefix}.${tail2}` : pathPrefix : tail2;
|
|
2958
3583
|
issues.push({
|
|
2959
|
-
path:
|
|
3584
|
+
path: path17,
|
|
2960
3585
|
code,
|
|
2961
3586
|
message: issue.message,
|
|
2962
3587
|
received: issue.code === "invalid_type" ? issue.received : void 0,
|
|
@@ -2965,8 +3590,8 @@ function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
|
2965
3590
|
});
|
|
2966
3591
|
}
|
|
2967
3592
|
}
|
|
2968
|
-
function pathToString(
|
|
2969
|
-
return
|
|
3593
|
+
function pathToString(path17) {
|
|
3594
|
+
return path17.map((p) => typeof p === "number" ? `[${p}]` : `.${String(p)}`).join("").replace(/^\./, "");
|
|
2970
3595
|
}
|
|
2971
3596
|
function buildDepGraph(canvas) {
|
|
2972
3597
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -3065,6 +3690,12 @@ var Engine = class {
|
|
|
3065
3690
|
async run(input, opts = {}) {
|
|
3066
3691
|
const validation = await this.validateDeep(input);
|
|
3067
3692
|
if (!validation.ok) throw new ValidationError(validation.issues);
|
|
3693
|
+
if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
|
|
3694
|
+
throw new RunAbortedError(
|
|
3695
|
+
"cost_cap",
|
|
3696
|
+
`estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3068
3699
|
const canvas = validation.canvas;
|
|
3069
3700
|
const runId = opts.run_id ?? `r_${ulid()}`;
|
|
3070
3701
|
const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
|
|
@@ -3115,7 +3746,17 @@ var Engine = class {
|
|
|
3115
3746
|
const layers = topologicalLayers(graph);
|
|
3116
3747
|
const limit = resolveConcurrency(opts.concurrency);
|
|
3117
3748
|
for (const layer of layers) {
|
|
3749
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
|
|
3750
|
+
if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
|
|
3751
|
+
throw new RunAbortedError(
|
|
3752
|
+
"cost_cap",
|
|
3753
|
+
`spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3118
3756
|
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3757
|
+
if (opts.signal?.aborted) {
|
|
3758
|
+
return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
|
|
3759
|
+
}
|
|
3119
3760
|
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3120
3761
|
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
|
|
3121
3762
|
if (r.cached) counters.cachedNodes++;
|
|
@@ -3138,10 +3779,12 @@ var Engine = class {
|
|
|
3138
3779
|
settled.forEach((result, i) => {
|
|
3139
3780
|
const nodeId = layer[i];
|
|
3140
3781
|
if (result.status === "rejected" && nodeId) {
|
|
3782
|
+
if (result.reason instanceof RunAbortedError) return;
|
|
3141
3783
|
failures.push({ nodeId, reason: result.reason });
|
|
3142
3784
|
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
|
|
3143
3785
|
}
|
|
3144
3786
|
});
|
|
3787
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
3145
3788
|
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
3146
3789
|
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
3147
3790
|
}
|
|
@@ -3214,7 +3857,7 @@ var Engine = class {
|
|
|
3214
3857
|
log: this.log,
|
|
3215
3858
|
signal: opts.signal
|
|
3216
3859
|
};
|
|
3217
|
-
const preparedForExec = def
|
|
3860
|
+
const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
|
|
3218
3861
|
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
3219
3862
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
3220
3863
|
const elapsed = Date.now() - t0;
|
|
@@ -3336,6 +3979,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
|
|
|
3336
3979
|
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3337
3980
|
}
|
|
3338
3981
|
}
|
|
3982
|
+
function needsLocalMaterialization(def) {
|
|
3983
|
+
return def.location === "local" && !def.passthroughRefs;
|
|
3984
|
+
}
|
|
3339
3985
|
function pickFinalOutput(canvas, outputs) {
|
|
3340
3986
|
if (canvas.output) {
|
|
3341
3987
|
const node = outputs[canvas.output.node];
|
|
@@ -3351,7 +3997,7 @@ function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
|
3351
3997
|
const needs = /* @__PURE__ */ new Set();
|
|
3352
3998
|
for (const [consumerId, deps] of graph) {
|
|
3353
3999
|
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
3354
|
-
if (def?.location !== "local") continue;
|
|
4000
|
+
if (def?.location !== "local" || def.passthroughRefs) continue;
|
|
3355
4001
|
for (const dep of deps) needs.add(dep);
|
|
3356
4002
|
}
|
|
3357
4003
|
return needs;
|
|
@@ -3390,11 +4036,11 @@ function hashInputs(inputs) {
|
|
|
3390
4036
|
const out = {};
|
|
3391
4037
|
for (const [k, v] of Object.entries(inputs)) {
|
|
3392
4038
|
if (Array.isArray(v)) {
|
|
3393
|
-
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(el));
|
|
4039
|
+
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(normalizeParamsForCacheKey(el)));
|
|
3394
4040
|
} else {
|
|
3395
4041
|
const s = extractSha(v);
|
|
3396
4042
|
if (s !== null) out[k] = s;
|
|
3397
|
-
else out[k] = canonicalLiteral(v);
|
|
4043
|
+
else out[k] = canonicalLiteral(normalizeParamsForCacheKey(v));
|
|
3398
4044
|
}
|
|
3399
4045
|
}
|
|
3400
4046
|
return out;
|
|
@@ -3555,9 +4201,9 @@ var NodeRegistry = class {
|
|
|
3555
4201
|
|
|
3556
4202
|
// src/engine/nodes/ingest.ts
|
|
3557
4203
|
import { execFile as execFileCb, spawn } from "child_process";
|
|
3558
|
-
import { mkdtemp, readdir, readFile as
|
|
4204
|
+
import { mkdtemp, readdir, readFile as readFile3, rm, stat as stat2 } from "fs/promises";
|
|
3559
4205
|
import { tmpdir } from "os";
|
|
3560
|
-
import
|
|
4206
|
+
import path4 from "path";
|
|
3561
4207
|
import { promisify } from "util";
|
|
3562
4208
|
import { z as z5 } from "zod";
|
|
3563
4209
|
|
|
@@ -3915,8 +4561,8 @@ function resolveLocalPath(input) {
|
|
|
3915
4561
|
`ingest: ~ path expansion is not supported (got "${input}"). Use an absolute path or a cwd-relative path.`
|
|
3916
4562
|
);
|
|
3917
4563
|
}
|
|
3918
|
-
if (
|
|
3919
|
-
return
|
|
4564
|
+
if (path4.isAbsolute(input)) return input;
|
|
4565
|
+
return path4.resolve(process.cwd(), input);
|
|
3920
4566
|
}
|
|
3921
4567
|
var EXT_TO_MIME = {
|
|
3922
4568
|
png: "image/png",
|
|
@@ -3964,7 +4610,7 @@ function sniffSvg(buf) {
|
|
|
3964
4610
|
return head.startsWith("<?xml") ? head.includes("<svg") : head.startsWith("<svg");
|
|
3965
4611
|
}
|
|
3966
4612
|
function inferMimeFromPath(absPath, sniffBytes) {
|
|
3967
|
-
const ext =
|
|
4613
|
+
const ext = path4.extname(absPath).slice(1).toLowerCase();
|
|
3968
4614
|
const fromExt = EXT_TO_MIME[ext];
|
|
3969
4615
|
if (fromExt) return fromExt;
|
|
3970
4616
|
const fromBytes = sniffImageMime(sniffBytes);
|
|
@@ -4103,7 +4749,7 @@ async function execLocalFile(params, ctx) {
|
|
|
4103
4749
|
}
|
|
4104
4750
|
let bytes;
|
|
4105
4751
|
try {
|
|
4106
|
-
bytes = await
|
|
4752
|
+
bytes = await readFile3(absPath);
|
|
4107
4753
|
} catch (e) {
|
|
4108
4754
|
if (e.code === "EACCES") {
|
|
4109
4755
|
throw localExecError(ctx, `permission_denied: ${absPath}`);
|
|
@@ -4154,7 +4800,7 @@ function localFileMetadata(args) {
|
|
|
4154
4800
|
strategy: "local_file",
|
|
4155
4801
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4156
4802
|
file_size: args.fileSize,
|
|
4157
|
-
original_filename:
|
|
4803
|
+
original_filename: path4.basename(args.absPath),
|
|
4158
4804
|
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
4159
4805
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
4160
4806
|
};
|
|
@@ -4178,7 +4824,7 @@ async function execYtDlp(params, ctx) {
|
|
|
4178
4824
|
if (params.expect !== "video" && params.expect !== "audio") {
|
|
4179
4825
|
throw new Error(`ingest: yt_dlp only handles video/audio, got expect=${params.expect}`);
|
|
4180
4826
|
}
|
|
4181
|
-
const workDir = await mkdtemp(
|
|
4827
|
+
const workDir = await mkdtemp(path4.join(tmpdir(), "ingest-yt-"));
|
|
4182
4828
|
try {
|
|
4183
4829
|
const { filePath, info } = await runYtDlp({
|
|
4184
4830
|
url: params.url,
|
|
@@ -4193,7 +4839,7 @@ async function execYtDlp(params, ctx) {
|
|
|
4193
4839
|
`file_too_large: yt-dlp output for ${params.url} is ${downloadedStats.size} bytes (limit ${MAX_ASSET_BYTES})`
|
|
4194
4840
|
);
|
|
4195
4841
|
}
|
|
4196
|
-
const bytes = await
|
|
4842
|
+
const bytes = await readFile3(filePath);
|
|
4197
4843
|
const kind = params.expect;
|
|
4198
4844
|
const mime = YT_DLP_MIME[kind];
|
|
4199
4845
|
const metadata = buildYtDlpMetadata(params.url, info);
|
|
@@ -4205,8 +4851,8 @@ async function execYtDlp(params, ctx) {
|
|
|
4205
4851
|
}
|
|
4206
4852
|
}
|
|
4207
4853
|
async function runYtDlp(args) {
|
|
4208
|
-
const outTemplate =
|
|
4209
|
-
const infoPath =
|
|
4854
|
+
const outTemplate = path4.join(args.workDir, "out.%(ext)s");
|
|
4855
|
+
const infoPath = path4.join(args.workDir, "out.info.json");
|
|
4210
4856
|
const argv = [
|
|
4211
4857
|
args.url,
|
|
4212
4858
|
"--no-playlist",
|
|
@@ -4239,11 +4885,11 @@ ${tail(stderr, 40)}`)
|
|
|
4239
4885
|
}
|
|
4240
4886
|
let info = {};
|
|
4241
4887
|
try {
|
|
4242
|
-
const raw = await
|
|
4888
|
+
const raw = await readFile3(infoPath, "utf-8");
|
|
4243
4889
|
info = JSON.parse(raw);
|
|
4244
4890
|
} catch {
|
|
4245
4891
|
}
|
|
4246
|
-
return { filePath:
|
|
4892
|
+
return { filePath: path4.join(args.workDir, downloaded), info };
|
|
4247
4893
|
}
|
|
4248
4894
|
function buildYtDlpMetadata(sourceUrl, info) {
|
|
4249
4895
|
const out = {
|
|
@@ -4332,9 +4978,9 @@ import { z as z6 } from "zod";
|
|
|
4332
4978
|
|
|
4333
4979
|
// src/engine/nodes/local/lib/cli-runner.ts
|
|
4334
4980
|
import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
|
|
4335
|
-
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as
|
|
4981
|
+
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, stat as stat3 } from "fs/promises";
|
|
4336
4982
|
import { tmpdir as tmpdir2 } from "os";
|
|
4337
|
-
import
|
|
4983
|
+
import path5 from "path";
|
|
4338
4984
|
import { promisify as promisify2 } from "util";
|
|
4339
4985
|
var execFile2 = promisify2(execFileCb2);
|
|
4340
4986
|
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -4361,7 +5007,7 @@ function mimeForExt(ext) {
|
|
|
4361
5007
|
}
|
|
4362
5008
|
function extForAssetRef(ref) {
|
|
4363
5009
|
if (ref.path) {
|
|
4364
|
-
const e =
|
|
5010
|
+
const e = path5.extname(ref.path);
|
|
4365
5011
|
if (e) return e;
|
|
4366
5012
|
}
|
|
4367
5013
|
const reverse = {
|
|
@@ -4383,14 +5029,14 @@ function planArrayInputSlot(tmpDir, slot, values, lookup, stagedInputs) {
|
|
|
4383
5029
|
const ref = values[i];
|
|
4384
5030
|
if (!ref) continue;
|
|
4385
5031
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot}[${i}] has no local path`);
|
|
4386
|
-
const dest =
|
|
5032
|
+
const dest = path5.join(tmpDir, `in_${slot}_${i}${extForAssetRef(ref)}`);
|
|
4387
5033
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4388
5034
|
lookup.set(`in.${slot}.${i}`, dest);
|
|
4389
5035
|
}
|
|
4390
5036
|
}
|
|
4391
5037
|
function planSingleInputSlot(tmpDir, slot, ref, lookup, stagedInputs) {
|
|
4392
5038
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot} has no local path`);
|
|
4393
|
-
const dest =
|
|
5039
|
+
const dest = path5.join(tmpDir, `in_${slot}${extForAssetRef(ref)}`);
|
|
4394
5040
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4395
5041
|
lookup.set(`in.${slot}`, dest);
|
|
4396
5042
|
}
|
|
@@ -4398,7 +5044,7 @@ function planOutputs(tmpDir, outputs, lookup) {
|
|
|
4398
5044
|
const outputPaths = [];
|
|
4399
5045
|
for (const [name, spec] of Object.entries(outputs)) {
|
|
4400
5046
|
const ext = spec.ext.startsWith(".") ? spec.ext : `.${spec.ext}`;
|
|
4401
|
-
const absPath =
|
|
5047
|
+
const absPath = path5.join(tmpDir, `out_${name}${ext}`);
|
|
4402
5048
|
outputPaths.push({ name, absPath, spec });
|
|
4403
5049
|
lookup.set(`out.${name}`, absPath);
|
|
4404
5050
|
}
|
|
@@ -4439,8 +5085,8 @@ function rejectRawPaths(substituted, original, stagingDir) {
|
|
|
4439
5085
|
throw new Error(`cli-runner: home-relative path "${original}" not allowed in args.`);
|
|
4440
5086
|
}
|
|
4441
5087
|
if (substituted.startsWith("/")) {
|
|
4442
|
-
const resolved =
|
|
4443
|
-
if (!resolved.startsWith(`${stagingDir}${
|
|
5088
|
+
const resolved = path5.resolve(substituted);
|
|
5089
|
+
if (!resolved.startsWith(`${stagingDir}${path5.sep}`) && resolved !== stagingDir) {
|
|
4444
5090
|
throw new Error(
|
|
4445
5091
|
`cli-runner: raw filesystem path "${original}" not allowed \u2014 declare an input slot and use {{in.<slot>}} instead.`
|
|
4446
5092
|
);
|
|
@@ -4490,7 +5136,7 @@ function tailLines(text, maxLines) {
|
|
|
4490
5136
|
}
|
|
4491
5137
|
async function runCli(opts) {
|
|
4492
5138
|
const { bin, args, inputs, outputs, ctx, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
4493
|
-
const tmpDir = await mkdtemp2(
|
|
5139
|
+
const tmpDir = await mkdtemp2(path5.join(tmpdir2(), `cli-${bin.replace(/[^a-z0-9]/gi, "")}-`));
|
|
4494
5140
|
try {
|
|
4495
5141
|
const { lookup, stagedInputs, outputPaths } = planPlaceholders(tmpDir, inputs, outputs);
|
|
4496
5142
|
await stageInputs(stagedInputs);
|
|
@@ -4512,7 +5158,7 @@ ${tailLines(stderr, 40)}`);
|
|
|
4512
5158
|
if (!s?.isFile() || s.size === 0) {
|
|
4513
5159
|
throw new Error(`cli-runner: declared output "${name}" missing or empty at ${absPath}`);
|
|
4514
5160
|
}
|
|
4515
|
-
const bytes = await
|
|
5161
|
+
const bytes = await readFile4(absPath);
|
|
4516
5162
|
const ref = await ctx.assets.ingestBytes({
|
|
4517
5163
|
bytes: Buffer.from(bytes),
|
|
4518
5164
|
kind: spec.kind,
|
|
@@ -4552,6 +5198,12 @@ var Track = z6.object({
|
|
|
4552
5198
|
slot: z6.string().min(1),
|
|
4553
5199
|
/** When this track starts on the timeline, seconds from 0. */
|
|
4554
5200
|
start_s: z6.number().min(0),
|
|
5201
|
+
/**
|
|
5202
|
+
* Optional hard cap on this track's length, seconds. The clip is trimmed BEFORE
|
|
5203
|
+
* placement, so a source that runs long (an over-long voice extract, a converted
|
|
5204
|
+
* track that came back oversized) cannot bleed into the next track's window.
|
|
5205
|
+
*/
|
|
5206
|
+
duration_s: z6.number().positive().optional(),
|
|
4555
5207
|
/** Optional level adjustment in dB (negative ducks, e.g. a music bed at -12). */
|
|
4556
5208
|
gain_db: z6.number().optional()
|
|
4557
5209
|
}).strict();
|
|
@@ -4608,7 +5260,10 @@ function buildAudioTimelineArgs(params) {
|
|
|
4608
5260
|
params.tracks.forEach((track, i) => {
|
|
4609
5261
|
inputArgs.push("-i", `{{in.${track.slot}}}`);
|
|
4610
5262
|
const delayMs = Math.round(track.start_s * 1e3);
|
|
4611
|
-
const steps = [
|
|
5263
|
+
const steps = [
|
|
5264
|
+
...track.duration_s !== void 0 ? [`atrim=0:${track.duration_s}`] : [],
|
|
5265
|
+
`adelay=${delayMs}:all=1`
|
|
5266
|
+
];
|
|
4612
5267
|
if (track.gain_db !== void 0) steps.push(`volume=${track.gain_db}dB`);
|
|
4613
5268
|
const label = `a${i}`;
|
|
4614
5269
|
filterChains.push(`[${i}:a]${steps.join(",")}[${label}]`);
|
|
@@ -4626,11 +5281,11 @@ function buildAudioTimelineArgs(params) {
|
|
|
4626
5281
|
}
|
|
4627
5282
|
var audioTimelineNode = defineNode({
|
|
4628
5283
|
id: "audio_timeline",
|
|
4629
|
-
version: "1.
|
|
5284
|
+
version: "1.2.0",
|
|
4630
5285
|
category: "audio",
|
|
4631
5286
|
location: "local",
|
|
4632
5287
|
summary: "Place and mix several audio clips onto one timeline: each track starts at a given second (optionally level-adjusted in dB), then they're combined into a single track. Built for laying a music bed plus timed voiceover lines and sound effects under a video.",
|
|
4633
|
-
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, gain_db?}
|
|
5288
|
+
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, duration_s?, gain_db?}` (`duration_s` hard-caps a clip so it can't bleed into the next track's window). Set `total_ms` to pin the final length to the video. Requires `ffmpeg` on PATH.",
|
|
4634
5289
|
inputs: AudioTimelineInputs,
|
|
4635
5290
|
params: AudioTimelineParams,
|
|
4636
5291
|
outputs: AudioTimelineOutputs,
|
|
@@ -4682,19 +5337,56 @@ var audioTimelineNode = defineNode({
|
|
|
4682
5337
|
}
|
|
4683
5338
|
});
|
|
4684
5339
|
|
|
4685
|
-
// src/engine/nodes/local/
|
|
5340
|
+
// src/engine/nodes/local/collect.ts
|
|
4686
5341
|
import { z as z7 } from "zod";
|
|
5342
|
+
var collectNode = defineNode({
|
|
5343
|
+
id: "collect",
|
|
5344
|
+
version: "1.0.0",
|
|
5345
|
+
category: "data",
|
|
5346
|
+
location: "local",
|
|
5347
|
+
passthroughRefs: true,
|
|
5348
|
+
summary: "Gather images from multiple upstream nodes into one ordered array \u2014 the standard terminal for multi-variant canvases whose final output is several images.",
|
|
5349
|
+
when_to_use: "Point the canvas `output` at this node when several independent branches (e.g. one image_generate per scene/variant) must ALL be finals. Wire `inputs.images` as an array of refs like `$ref:gen_billboard_03.images#0` \u2014 each final inherits its producer node id as its label (or set `params.labels` to override), so variants stay identifiable in the dashboard and selection.",
|
|
5350
|
+
inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5351
|
+
params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
|
|
5352
|
+
outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5353
|
+
outputKinds: { images: "image" },
|
|
5354
|
+
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5355
|
+
// Arity is only knowable at validate time when `images` is a literal array;
|
|
5356
|
+
// a single `$ref:` string to an upstream array output defers to runtime.
|
|
5357
|
+
validateExtra: ({ rawParams, rawInputs }) => {
|
|
5358
|
+
const labels = rawParams?.labels;
|
|
5359
|
+
if (!Array.isArray(labels)) return [];
|
|
5360
|
+
if (new Set(labels).size !== labels.length) {
|
|
5361
|
+
return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
|
|
5362
|
+
}
|
|
5363
|
+
const images = rawInputs?.images;
|
|
5364
|
+
if (Array.isArray(images) && labels.length !== images.length) {
|
|
5365
|
+
return [
|
|
5366
|
+
{
|
|
5367
|
+
path: "params.labels",
|
|
5368
|
+
message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
|
|
5369
|
+
}
|
|
5370
|
+
];
|
|
5371
|
+
}
|
|
5372
|
+
return [];
|
|
5373
|
+
},
|
|
5374
|
+
execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
|
|
5375
|
+
});
|
|
5376
|
+
|
|
5377
|
+
// src/engine/nodes/local/ffmpeg.ts
|
|
5378
|
+
import { z as z8 } from "zod";
|
|
4687
5379
|
var FFMPEG_BIN2 = "ffmpeg";
|
|
4688
|
-
var OutputDecl =
|
|
4689
|
-
kind:
|
|
4690
|
-
ext:
|
|
5380
|
+
var OutputDecl = z8.object({
|
|
5381
|
+
kind: z8.enum(["image", "video", "audio"]),
|
|
5382
|
+
ext: z8.string().min(1).max(8)
|
|
4691
5383
|
}).strict();
|
|
4692
|
-
var FfmpegParams =
|
|
4693
|
-
args:
|
|
4694
|
-
outputs:
|
|
5384
|
+
var FfmpegParams = z8.object({
|
|
5385
|
+
args: z8.array(z8.string()).min(1),
|
|
5386
|
+
outputs: z8.record(z8.string(), OutputDecl).default({})
|
|
4695
5387
|
}).strict();
|
|
4696
|
-
var FfmpegInputs =
|
|
4697
|
-
var FfmpegOutputs =
|
|
5388
|
+
var FfmpegInputs = z8.record(z8.string(), z8.unknown());
|
|
5389
|
+
var FfmpegOutputs = z8.record(z8.string(), z8.custom());
|
|
4698
5390
|
var ffmpegNode = defineNode({
|
|
4699
5391
|
id: "ffmpeg",
|
|
4700
5392
|
version: "2.0.0",
|
|
@@ -4724,17 +5416,17 @@ var ffmpegNode = defineNode({
|
|
|
4724
5416
|
import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4725
5417
|
import { createRequire } from "module";
|
|
4726
5418
|
import { tmpdir as tmpdir3 } from "os";
|
|
4727
|
-
import
|
|
4728
|
-
import { z as
|
|
5419
|
+
import path7 from "path";
|
|
5420
|
+
import { z as z9 } from "zod";
|
|
4729
5421
|
|
|
4730
5422
|
// src/engine/nodes/local/lib/assets.ts
|
|
4731
|
-
import { copyFile as copyFile3, readFile as
|
|
4732
|
-
import
|
|
5423
|
+
import { copyFile as copyFile3, readFile as readFile5 } from "fs/promises";
|
|
5424
|
+
import path6 from "path";
|
|
4733
5425
|
async function stageAsset(ref, destDir, filename) {
|
|
4734
5426
|
if (!ref.path) {
|
|
4735
5427
|
throw new Error(`stageAsset: ref (${ref.kind}/${ref.mime}) has no local path`);
|
|
4736
5428
|
}
|
|
4737
|
-
const dest =
|
|
5429
|
+
const dest = path6.join(destDir, filename);
|
|
4738
5430
|
await copyFile3(ref.path, dest);
|
|
4739
5431
|
return dest;
|
|
4740
5432
|
}
|
|
@@ -4743,7 +5435,7 @@ async function refToUrl(ref) {
|
|
|
4743
5435
|
if (!ref.path) {
|
|
4744
5436
|
throw new Error("refToUrl: AssetRef has neither url nor path");
|
|
4745
5437
|
}
|
|
4746
|
-
const bytes = await
|
|
5438
|
+
const bytes = await readFile5(ref.path);
|
|
4747
5439
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4748
5440
|
}
|
|
4749
5441
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
@@ -4761,15 +5453,15 @@ var DEFAULT_SPECIMEN = [
|
|
|
4761
5453
|
"abcdefghijklmnopqrstuvwxyz",
|
|
4762
5454
|
`0123456789 !?&@#$%().,:;'"-`
|
|
4763
5455
|
].join("\n");
|
|
4764
|
-
var FontSpecimenParams =
|
|
4765
|
-
text:
|
|
4766
|
-
font_size:
|
|
4767
|
-
padding:
|
|
4768
|
-
line_height:
|
|
4769
|
-
max_width:
|
|
5456
|
+
var FontSpecimenParams = z9.object({
|
|
5457
|
+
text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
|
|
5458
|
+
font_size: z9.number().int().min(8).max(512).optional().default(72),
|
|
5459
|
+
padding: z9.number().int().min(0).max(512).optional().default(64),
|
|
5460
|
+
line_height: z9.number().min(0.8).max(3).optional().default(1.35),
|
|
5461
|
+
max_width: z9.number().int().min(256).max(4096).optional()
|
|
4770
5462
|
}).strict();
|
|
4771
|
-
var FontSpecimenInputs =
|
|
4772
|
-
var FontSpecimenOutputs =
|
|
5463
|
+
var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
|
|
5464
|
+
var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
|
|
4773
5465
|
var DEVICE_SCALE_FACTOR = 2;
|
|
4774
5466
|
var PAGE_TIMEOUT_MS = 3e4;
|
|
4775
5467
|
function escapeHtml(text) {
|
|
@@ -4817,11 +5509,11 @@ var fontSpecimenNode = defineNode({
|
|
|
4817
5509
|
outputKinds: { image: "image" },
|
|
4818
5510
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
4819
5511
|
async execute({ inputs, params, ctx }) {
|
|
4820
|
-
const tmp = await mkdtemp3(
|
|
5512
|
+
const tmp = await mkdtemp3(path7.join(tmpdir3(), "font-specimen-"));
|
|
4821
5513
|
try {
|
|
4822
5514
|
const fontFilename = `font.${extForMime(inputs.font.mime)}`;
|
|
4823
5515
|
await stageAsset(inputs.font, tmp, fontFilename);
|
|
4824
|
-
const entryPath =
|
|
5516
|
+
const entryPath = path7.join(tmp, "index.html");
|
|
4825
5517
|
await writeFile3(entryPath, buildSpecimenHtml(params, fontFilename), "utf-8");
|
|
4826
5518
|
ctx.log(`rendering specimen (${params.font_size}px, ${params.text.split("\n").length} lines)`);
|
|
4827
5519
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -4905,16 +5597,16 @@ var fontSpecimenNode = defineNode({
|
|
|
4905
5597
|
|
|
4906
5598
|
// src/engine/nodes/local/hyperframe.ts
|
|
4907
5599
|
import { execFile as execFile4 } from "child_process";
|
|
4908
|
-
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as
|
|
5600
|
+
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as readFile9, rm as rm4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
4909
5601
|
import { createRequire as createRequire2 } from "module";
|
|
4910
5602
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
4911
|
-
import
|
|
5603
|
+
import path12 from "path";
|
|
4912
5604
|
import { promisify as promisify4 } from "util";
|
|
4913
|
-
import { z as
|
|
5605
|
+
import { z as z11 } from "zod";
|
|
4914
5606
|
|
|
4915
5607
|
// src/engine/engine/composition-hash.ts
|
|
4916
|
-
import { readdir as readdir2, readFile as
|
|
4917
|
-
import
|
|
5608
|
+
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
5609
|
+
import path8 from "path";
|
|
4918
5610
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".cache", ".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
4919
5611
|
function isSkippedName(name) {
|
|
4920
5612
|
if (name.startsWith(".")) return true;
|
|
@@ -4931,79 +5623,79 @@ async function collectFiles(root, current) {
|
|
|
4931
5623
|
const names = await readdir2(current);
|
|
4932
5624
|
for (const name of names) {
|
|
4933
5625
|
if (isSkippedName(name)) continue;
|
|
4934
|
-
const abs =
|
|
5626
|
+
const abs = path8.join(current, name);
|
|
4935
5627
|
const s = await stat4(abs);
|
|
4936
5628
|
if (s.isDirectory()) {
|
|
4937
5629
|
out.push(...await collectFiles(root, abs));
|
|
4938
5630
|
continue;
|
|
4939
5631
|
}
|
|
4940
5632
|
if (!s.isFile()) continue;
|
|
4941
|
-
const bytes = await
|
|
4942
|
-
const relPath =
|
|
5633
|
+
const bytes = await readFile6(abs);
|
|
5634
|
+
const relPath = path8.relative(root, abs).split(path8.sep).join("/");
|
|
4943
5635
|
out.push({ relPath, contentSha: sha256Hex(bytes) });
|
|
4944
5636
|
}
|
|
4945
5637
|
return out;
|
|
4946
5638
|
}
|
|
4947
5639
|
|
|
4948
5640
|
// src/engine/engine/composition-meta.ts
|
|
4949
|
-
import { readFile as
|
|
4950
|
-
import
|
|
4951
|
-
import { z as
|
|
4952
|
-
var InputKind =
|
|
4953
|
-
var InputSpec =
|
|
5641
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5642
|
+
import path9 from "path";
|
|
5643
|
+
import { z as z10 } from "zod";
|
|
5644
|
+
var InputKind = z10.enum(["video", "image", "audio", "json"]);
|
|
5645
|
+
var InputSpec = z10.object({
|
|
4954
5646
|
kind: InputKind,
|
|
4955
|
-
required:
|
|
5647
|
+
required: z10.boolean().optional().default(false),
|
|
4956
5648
|
// Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
|
|
4957
5649
|
// Defaults to `<key><ext>` derived from the kind.
|
|
4958
|
-
staged_as:
|
|
4959
|
-
description:
|
|
5650
|
+
staged_as: z10.string().min(1).optional(),
|
|
5651
|
+
description: z10.string().optional()
|
|
4960
5652
|
}).strict();
|
|
4961
5653
|
var ParamSpecBase = {
|
|
4962
|
-
required:
|
|
4963
|
-
description:
|
|
5654
|
+
required: z10.boolean().optional().default(false),
|
|
5655
|
+
description: z10.string().optional()
|
|
4964
5656
|
};
|
|
4965
|
-
var StringParam =
|
|
5657
|
+
var StringParam = z10.object({
|
|
4966
5658
|
...ParamSpecBase,
|
|
4967
|
-
kind:
|
|
4968
|
-
default:
|
|
4969
|
-
enum:
|
|
5659
|
+
kind: z10.literal("string"),
|
|
5660
|
+
default: z10.string().optional(),
|
|
5661
|
+
enum: z10.array(z10.string()).optional()
|
|
4970
5662
|
}).strict();
|
|
4971
|
-
var IntegerParam =
|
|
5663
|
+
var IntegerParam = z10.object({
|
|
4972
5664
|
...ParamSpecBase,
|
|
4973
|
-
kind:
|
|
4974
|
-
default:
|
|
4975
|
-
min:
|
|
4976
|
-
max:
|
|
5665
|
+
kind: z10.literal("integer"),
|
|
5666
|
+
default: z10.number().int().optional(),
|
|
5667
|
+
min: z10.number().int().optional(),
|
|
5668
|
+
max: z10.number().int().optional()
|
|
4977
5669
|
}).strict();
|
|
4978
|
-
var NumberParam =
|
|
5670
|
+
var NumberParam = z10.object({
|
|
4979
5671
|
...ParamSpecBase,
|
|
4980
|
-
kind:
|
|
4981
|
-
default:
|
|
4982
|
-
min:
|
|
4983
|
-
max:
|
|
5672
|
+
kind: z10.literal("number"),
|
|
5673
|
+
default: z10.number().optional(),
|
|
5674
|
+
min: z10.number().optional(),
|
|
5675
|
+
max: z10.number().optional()
|
|
4984
5676
|
}).strict();
|
|
4985
|
-
var BooleanParam =
|
|
5677
|
+
var BooleanParam = z10.object({
|
|
4986
5678
|
...ParamSpecBase,
|
|
4987
|
-
kind:
|
|
4988
|
-
default:
|
|
5679
|
+
kind: z10.literal("boolean"),
|
|
5680
|
+
default: z10.boolean().optional()
|
|
4989
5681
|
}).strict();
|
|
4990
|
-
var ColorParam =
|
|
5682
|
+
var ColorParam = z10.object({
|
|
4991
5683
|
...ParamSpecBase,
|
|
4992
|
-
kind:
|
|
4993
|
-
default:
|
|
5684
|
+
kind: z10.literal("color"),
|
|
5685
|
+
default: z10.string().optional()
|
|
4994
5686
|
}).strict();
|
|
4995
|
-
var ImageParam =
|
|
5687
|
+
var ImageParam = z10.object({
|
|
4996
5688
|
...ParamSpecBase,
|
|
4997
|
-
kind:
|
|
4998
|
-
default:
|
|
5689
|
+
kind: z10.literal("image"),
|
|
5690
|
+
default: z10.string().optional()
|
|
4999
5691
|
}).strict();
|
|
5000
|
-
var JsonParam =
|
|
5692
|
+
var JsonParam = z10.object({
|
|
5001
5693
|
...ParamSpecBase,
|
|
5002
|
-
kind:
|
|
5003
|
-
schema:
|
|
5004
|
-
default:
|
|
5694
|
+
kind: z10.literal("json"),
|
|
5695
|
+
schema: z10.unknown().optional(),
|
|
5696
|
+
default: z10.unknown().optional()
|
|
5005
5697
|
}).strict();
|
|
5006
|
-
var ParamSpec =
|
|
5698
|
+
var ParamSpec = z10.discriminatedUnion("kind", [
|
|
5007
5699
|
StringParam,
|
|
5008
5700
|
IntegerParam,
|
|
5009
5701
|
NumberParam,
|
|
@@ -5012,22 +5704,22 @@ var ParamSpec = z9.discriminatedUnion("kind", [
|
|
|
5012
5704
|
ImageParam,
|
|
5013
5705
|
JsonParam
|
|
5014
5706
|
]);
|
|
5015
|
-
var CompositionMetaSchema =
|
|
5016
|
-
id:
|
|
5017
|
-
title:
|
|
5018
|
-
description:
|
|
5019
|
-
width:
|
|
5020
|
-
height:
|
|
5021
|
-
fps:
|
|
5022
|
-
default_duration:
|
|
5023
|
-
inputs:
|
|
5024
|
-
params:
|
|
5707
|
+
var CompositionMetaSchema = z10.object({
|
|
5708
|
+
id: z10.string().min(1),
|
|
5709
|
+
title: z10.string().min(1),
|
|
5710
|
+
description: z10.string().optional(),
|
|
5711
|
+
width: z10.number().int().positive(),
|
|
5712
|
+
height: z10.number().int().positive(),
|
|
5713
|
+
fps: z10.number().int().positive().default(30),
|
|
5714
|
+
default_duration: z10.number().positive().default(10),
|
|
5715
|
+
inputs: z10.record(z10.string(), InputSpec).default({}),
|
|
5716
|
+
params: z10.record(z10.string(), ParamSpec).default({})
|
|
5025
5717
|
}).strict();
|
|
5026
5718
|
async function loadCompositionMeta(compositionDir) {
|
|
5027
|
-
const metaPath =
|
|
5719
|
+
const metaPath = path9.join(compositionDir, "meta.json");
|
|
5028
5720
|
let raw;
|
|
5029
5721
|
try {
|
|
5030
|
-
raw = await
|
|
5722
|
+
raw = await readFile7(metaPath, "utf-8");
|
|
5031
5723
|
} catch (e) {
|
|
5032
5724
|
throw new Error(`composition meta: cannot read ${metaPath} (${e.message})`);
|
|
5033
5725
|
}
|
|
@@ -5049,39 +5741,39 @@ function buildParamsSchema(meta) {
|
|
|
5049
5741
|
for (const [name, spec] of Object.entries(meta.params)) {
|
|
5050
5742
|
shape[name] = buildParamFieldSchema(name, spec);
|
|
5051
5743
|
}
|
|
5052
|
-
return
|
|
5744
|
+
return z10.object(shape).strict();
|
|
5053
5745
|
}
|
|
5054
5746
|
function buildParamFieldSchema(name, spec) {
|
|
5055
5747
|
switch (spec.kind) {
|
|
5056
5748
|
case "string": {
|
|
5057
|
-
const s = spec.enum && spec.enum.length > 0 ?
|
|
5749
|
+
const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
|
|
5058
5750
|
return finalize(s, spec.default, spec.required);
|
|
5059
5751
|
}
|
|
5060
5752
|
case "integer": {
|
|
5061
|
-
let s =
|
|
5753
|
+
let s = z10.number().int();
|
|
5062
5754
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
5063
5755
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
5064
5756
|
return finalize(s, spec.default, spec.required);
|
|
5065
5757
|
}
|
|
5066
5758
|
case "number": {
|
|
5067
|
-
let s =
|
|
5759
|
+
let s = z10.number();
|
|
5068
5760
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
5069
5761
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
5070
5762
|
return finalize(s, spec.default, spec.required);
|
|
5071
5763
|
}
|
|
5072
5764
|
case "boolean":
|
|
5073
|
-
return finalize(
|
|
5765
|
+
return finalize(z10.boolean(), spec.default, spec.required);
|
|
5074
5766
|
case "color": {
|
|
5075
|
-
const s =
|
|
5767
|
+
const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
5076
5768
|
message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
|
|
5077
5769
|
});
|
|
5078
5770
|
return finalize(s, spec.default, spec.required);
|
|
5079
5771
|
}
|
|
5080
5772
|
case "image":
|
|
5081
|
-
return finalize(
|
|
5773
|
+
return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
|
|
5082
5774
|
case "json":
|
|
5083
5775
|
return finalize(
|
|
5084
|
-
|
|
5776
|
+
z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
|
|
5085
5777
|
spec.default,
|
|
5086
5778
|
spec.required
|
|
5087
5779
|
);
|
|
@@ -5105,8 +5797,8 @@ function defaultFilenameForInput(key, kind) {
|
|
|
5105
5797
|
|
|
5106
5798
|
// src/engine/nodes/local/lib/hyperframe-check.ts
|
|
5107
5799
|
import { execFile as execFile3 } from "child_process";
|
|
5108
|
-
import { readFile as
|
|
5109
|
-
import
|
|
5800
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5801
|
+
import path10 from "path";
|
|
5110
5802
|
import { promisify as promisify3 } from "util";
|
|
5111
5803
|
var execFileAsync = promisify3(execFile3);
|
|
5112
5804
|
var NEVER_BLOCK = [
|
|
@@ -5258,7 +5950,7 @@ ${detail}`);
|
|
|
5258
5950
|
}
|
|
5259
5951
|
let indexHtml = "";
|
|
5260
5952
|
try {
|
|
5261
|
-
indexHtml = await
|
|
5953
|
+
indexHtml = await readFile8(path10.join(dir, "index.html"), "utf-8");
|
|
5262
5954
|
} catch {
|
|
5263
5955
|
indexHtml = "";
|
|
5264
5956
|
}
|
|
@@ -5323,9 +6015,9 @@ ${stderr.slice(0, 1500)}`;
|
|
|
5323
6015
|
|
|
5324
6016
|
// src/engine/nodes/local/lib/hyperframe-meta.ts
|
|
5325
6017
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
5326
|
-
import
|
|
6018
|
+
import path11 from "path";
|
|
5327
6019
|
async function ensureHyperframesMetaJson(tmp, nodeId, meta, duration) {
|
|
5328
|
-
const metaPath =
|
|
6020
|
+
const metaPath = path11.join(tmp, "meta.json");
|
|
5329
6021
|
await writeFile4(
|
|
5330
6022
|
metaPath,
|
|
5331
6023
|
JSON.stringify(
|
|
@@ -5381,17 +6073,17 @@ function literalize(value) {
|
|
|
5381
6073
|
// src/engine/nodes/local/hyperframe.ts
|
|
5382
6074
|
var execFileAsync2 = promisify4(execFile4);
|
|
5383
6075
|
var require_2 = createRequire2(import.meta.url);
|
|
5384
|
-
var HyperframeParams =
|
|
5385
|
-
composition:
|
|
6076
|
+
var HyperframeParams = z11.object({
|
|
6077
|
+
composition: z11.string().min(1),
|
|
5386
6078
|
// Output container. mp4 (default) for delivery; webm/mov render WITH
|
|
5387
6079
|
// transparency (alpha) when the composition background is transparent —
|
|
5388
6080
|
// use for motion-graphic overlays dropped into Premiere/AE/Nuke.
|
|
5389
|
-
format:
|
|
5390
|
-
timeout_ms:
|
|
5391
|
-
}).catchall(
|
|
5392
|
-
var HyperframeInputs =
|
|
5393
|
-
var HyperframeOutputs =
|
|
5394
|
-
video:
|
|
6081
|
+
format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
|
|
6082
|
+
timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
|
|
6083
|
+
}).catchall(z11.unknown());
|
|
6084
|
+
var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
|
|
6085
|
+
var HyperframeOutputs = z11.object({
|
|
6086
|
+
video: z11.custom()
|
|
5395
6087
|
}).strict();
|
|
5396
6088
|
var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
|
|
5397
6089
|
var MIME_BY_FORMAT = {
|
|
@@ -5425,7 +6117,7 @@ var hyperframeRenderNode = defineNode({
|
|
|
5425
6117
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5426
6118
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5427
6119
|
const compositionParams = validateAndParseDynamicParams(meta, params);
|
|
5428
|
-
const tmp = await mkdtemp4(
|
|
6120
|
+
const tmp = await mkdtemp4(path12.join(tmpdir4(), "hf-render-"));
|
|
5429
6121
|
try {
|
|
5430
6122
|
await copyComposition(compositionDir, tmp);
|
|
5431
6123
|
await vendorGsap(tmp, ctx);
|
|
@@ -5435,9 +6127,9 @@ var hyperframeRenderNode = defineNode({
|
|
|
5435
6127
|
await substituteCompositionFiles(tmp, substitutionValues);
|
|
5436
6128
|
await ensureHyperframesMetaJson(tmp, ctx.nodeId, meta, duration);
|
|
5437
6129
|
await runHyperframesCheck({ dir: tmp, nodeId: "hyperframe_render", ctx, timeoutMs: params.timeout_ms });
|
|
5438
|
-
const outputPath =
|
|
6130
|
+
const outputPath = path12.join(tmp, `output.${params.format}`);
|
|
5439
6131
|
await runRender({ tmp, outputPath, params, meta, ctx });
|
|
5440
|
-
const bytes = await
|
|
6132
|
+
const bytes = await readFile9(outputPath);
|
|
5441
6133
|
ctx.log(`rendered ${bytes.length} bytes`);
|
|
5442
6134
|
const ref = await ctx.assets.ingestBytes({
|
|
5443
6135
|
bytes: Buffer.from(bytes),
|
|
@@ -5459,10 +6151,10 @@ var hyperframeRenderNode = defineNode({
|
|
|
5459
6151
|
}
|
|
5460
6152
|
});
|
|
5461
6153
|
async function resolveCompositionDir(composition) {
|
|
5462
|
-
const compositionPath =
|
|
6154
|
+
const compositionPath = path12.isAbsolute(composition) ? composition : path12.resolve(process.cwd(), composition);
|
|
5463
6155
|
const s = await stat5(compositionPath);
|
|
5464
6156
|
if (s.isDirectory()) return compositionPath;
|
|
5465
|
-
return
|
|
6157
|
+
return path12.dirname(compositionPath);
|
|
5466
6158
|
}
|
|
5467
6159
|
async function validateComposition(rawParams) {
|
|
5468
6160
|
const issues = await validateCompositionParams(rawParams);
|
|
@@ -5544,7 +6236,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5544
6236
|
await cp(srcDir, destDir, {
|
|
5545
6237
|
recursive: true,
|
|
5546
6238
|
filter: (src) => {
|
|
5547
|
-
const name =
|
|
6239
|
+
const name = path12.basename(src);
|
|
5548
6240
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5549
6241
|
return true;
|
|
5550
6242
|
}
|
|
@@ -5553,7 +6245,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5553
6245
|
async function vendorGsap(tmp, ctx) {
|
|
5554
6246
|
try {
|
|
5555
6247
|
const gsapMin = require_2.resolve("gsap/dist/gsap.min.js");
|
|
5556
|
-
await copyFile4(gsapMin,
|
|
6248
|
+
await copyFile4(gsapMin, path12.join(tmp, "gsap.min.js"));
|
|
5557
6249
|
} catch (e) {
|
|
5558
6250
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5559
6251
|
}
|
|
@@ -5568,7 +6260,7 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
|
|
|
5568
6260
|
await stageAsset(ref, tmp, filename);
|
|
5569
6261
|
ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
|
|
5570
6262
|
if (spec.kind === "video" && primaryDuration === null) {
|
|
5571
|
-
primaryDuration = await probeDurationSeconds(
|
|
6263
|
+
primaryDuration = await probeDurationSeconds(path12.join(tmp, filename));
|
|
5572
6264
|
}
|
|
5573
6265
|
}
|
|
5574
6266
|
return primaryDuration;
|
|
@@ -5614,8 +6306,8 @@ function coerceImageParam(value) {
|
|
|
5614
6306
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5615
6307
|
}
|
|
5616
6308
|
async function substituteCompositionFiles(tmp, values) {
|
|
5617
|
-
const entryPath =
|
|
5618
|
-
const original = await
|
|
6309
|
+
const entryPath = path12.join(tmp, "index.html");
|
|
6310
|
+
const original = await readFile9(entryPath, "utf-8");
|
|
5619
6311
|
const { output, missing } = substituteVariables(original, values);
|
|
5620
6312
|
if (missing.length > 0) {
|
|
5621
6313
|
throw new Error(
|
|
@@ -5631,7 +6323,7 @@ function workerCount() {
|
|
|
5631
6323
|
async function runRender(opts) {
|
|
5632
6324
|
const { tmp, outputPath, params, meta, ctx } = opts;
|
|
5633
6325
|
const args = buildRenderArgs(tmp, outputPath, meta, params.format);
|
|
5634
|
-
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${
|
|
6326
|
+
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${path12.basename(tmp)}`);
|
|
5635
6327
|
try {
|
|
5636
6328
|
await execFileAsync2("npx", args, { timeout: params.timeout_ms, maxBuffer: 64 * 1024 * 1024 });
|
|
5637
6329
|
} catch (e) {
|
|
@@ -5675,28 +6367,28 @@ async function probeDurationSeconds(filePath) {
|
|
|
5675
6367
|
|
|
5676
6368
|
// src/engine/nodes/local/hyperframe-snapshot.ts
|
|
5677
6369
|
import { execFile as execFile5 } from "child_process";
|
|
5678
|
-
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as
|
|
6370
|
+
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as readFile10, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
5679
6371
|
import { createRequire as createRequire3 } from "module";
|
|
5680
6372
|
import { tmpdir as tmpdir5 } from "os";
|
|
5681
|
-
import
|
|
6373
|
+
import path13 from "path";
|
|
5682
6374
|
import { promisify as promisify5 } from "util";
|
|
5683
|
-
import { z as
|
|
6375
|
+
import { z as z12 } from "zod";
|
|
5684
6376
|
var _execFileAsync = promisify5(execFile5);
|
|
5685
6377
|
var require_3 = createRequire3(import.meta.url);
|
|
5686
|
-
var WaitForSpec =
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6378
|
+
var WaitForSpec = z12.discriminatedUnion("kind", [
|
|
6379
|
+
z12.object({ kind: z12.literal("auto") }),
|
|
6380
|
+
z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
|
|
6381
|
+
z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
|
|
6382
|
+
z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
|
|
5691
6383
|
]);
|
|
5692
|
-
var HyperframeSnapshotParams =
|
|
5693
|
-
composition:
|
|
6384
|
+
var HyperframeSnapshotParams = z12.object({
|
|
6385
|
+
composition: z12.string().min(1),
|
|
5694
6386
|
wait_for: WaitForSpec.optional().default({ kind: "auto" }),
|
|
5695
|
-
timeout_ms:
|
|
5696
|
-
}).catchall(
|
|
5697
|
-
var HyperframeSnapshotInputs =
|
|
5698
|
-
var HyperframeSnapshotOutputs =
|
|
5699
|
-
image:
|
|
6387
|
+
timeout_ms: z12.number().int().positive().optional().default(6e4)
|
|
6388
|
+
}).catchall(z12.unknown());
|
|
6389
|
+
var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
|
|
6390
|
+
var HyperframeSnapshotOutputs = z12.object({
|
|
6391
|
+
image: z12.custom()
|
|
5700
6392
|
}).strict();
|
|
5701
6393
|
var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
|
|
5702
6394
|
var DEVICE_SCALE_FACTOR2 = 2;
|
|
@@ -5725,7 +6417,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5725
6417
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5726
6418
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5727
6419
|
const compositionParams = validateAndParseDynamicParams2(meta, params);
|
|
5728
|
-
const tmp = await mkdtemp5(
|
|
6420
|
+
const tmp = await mkdtemp5(path13.join(tmpdir5(), "hf-snap-"));
|
|
5729
6421
|
try {
|
|
5730
6422
|
await copyComposition2(compositionDir, tmp);
|
|
5731
6423
|
await vendorGsap2(tmp, ctx);
|
|
@@ -5740,7 +6432,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5740
6432
|
timeoutMs: params.timeout_ms,
|
|
5741
6433
|
samples: 1
|
|
5742
6434
|
});
|
|
5743
|
-
const entryPath =
|
|
6435
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
5744
6436
|
const entryUrl = `file://${entryPath}`;
|
|
5745
6437
|
ctx.log(`snapshotting ${meta.width}x${meta.height}@${DEVICE_SCALE_FACTOR2}x wait=${params.wait_for.kind}`);
|
|
5746
6438
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -5801,7 +6493,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5801
6493
|
await cp(srcDir, destDir, {
|
|
5802
6494
|
recursive: true,
|
|
5803
6495
|
filter: (src) => {
|
|
5804
|
-
const name =
|
|
6496
|
+
const name = path13.basename(src);
|
|
5805
6497
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5806
6498
|
return true;
|
|
5807
6499
|
}
|
|
@@ -5810,7 +6502,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5810
6502
|
async function vendorGsap2(tmp, ctx) {
|
|
5811
6503
|
try {
|
|
5812
6504
|
const gsapMin = require_3.resolve("gsap/dist/gsap.min.js");
|
|
5813
|
-
await copyFile5(gsapMin,
|
|
6505
|
+
await copyFile5(gsapMin, path13.join(tmp, "gsap.min.js"));
|
|
5814
6506
|
} catch (e) {
|
|
5815
6507
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5816
6508
|
}
|
|
@@ -5844,8 +6536,8 @@ function coerceImageParam2(value) {
|
|
|
5844
6536
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5845
6537
|
}
|
|
5846
6538
|
async function substituteCompositionFiles2(tmp, values) {
|
|
5847
|
-
const entryPath =
|
|
5848
|
-
const original = await
|
|
6539
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
6540
|
+
const original = await readFile10(entryPath, "utf-8");
|
|
5849
6541
|
const { output, missing } = substituteVariables(original, values);
|
|
5850
6542
|
if (missing.length > 0) {
|
|
5851
6543
|
throw new Error(
|
|
@@ -5888,18 +6580,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
|
|
|
5888
6580
|
// src/engine/nodes/local/imagemagick.ts
|
|
5889
6581
|
import { execFile as execFile6 } from "child_process";
|
|
5890
6582
|
import { promisify as promisify6 } from "util";
|
|
5891
|
-
import { z as
|
|
6583
|
+
import { z as z13 } from "zod";
|
|
5892
6584
|
var execFileAsync3 = promisify6(execFile6);
|
|
5893
|
-
var OutputDecl2 =
|
|
5894
|
-
kind:
|
|
5895
|
-
ext:
|
|
6585
|
+
var OutputDecl2 = z13.object({
|
|
6586
|
+
kind: z13.enum(["image", "video", "audio"]),
|
|
6587
|
+
ext: z13.string().min(1).max(8)
|
|
5896
6588
|
}).strict();
|
|
5897
|
-
var ImageMagickParams =
|
|
5898
|
-
args:
|
|
5899
|
-
outputs:
|
|
6589
|
+
var ImageMagickParams = z13.object({
|
|
6590
|
+
args: z13.array(z13.string()).min(1),
|
|
6591
|
+
outputs: z13.record(z13.string(), OutputDecl2).default({})
|
|
5900
6592
|
}).strict();
|
|
5901
|
-
var ImageMagickInputs =
|
|
5902
|
-
var ImageMagickOutputs =
|
|
6593
|
+
var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
|
|
6594
|
+
var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
|
|
5903
6595
|
var resolvedBin;
|
|
5904
6596
|
async function resolveBin() {
|
|
5905
6597
|
if (resolvedBin) return resolvedBin;
|
|
@@ -5941,29 +6633,29 @@ var imagemagickNode = defineNode({
|
|
|
5941
6633
|
});
|
|
5942
6634
|
|
|
5943
6635
|
// src/engine/nodes/local/text.ts
|
|
5944
|
-
import { z as
|
|
6636
|
+
import { z as z14 } from "zod";
|
|
5945
6637
|
var textNode = defineNode({
|
|
5946
6638
|
id: "text",
|
|
5947
6639
|
version: "1.0.0",
|
|
5948
6640
|
category: "data",
|
|
5949
6641
|
location: "local",
|
|
5950
6642
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
5951
|
-
inputs:
|
|
5952
|
-
params:
|
|
5953
|
-
outputs:
|
|
6643
|
+
inputs: z14.object({}).strict(),
|
|
6644
|
+
params: z14.object({ value: z14.string() }).strict(),
|
|
6645
|
+
outputs: z14.object({ text: z14.string() }).strict(),
|
|
5954
6646
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5955
6647
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
5956
6648
|
});
|
|
5957
6649
|
|
|
5958
6650
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
5959
|
-
import { z as
|
|
5960
|
-
var AudioVoiceConvertParams =
|
|
5961
|
-
model:
|
|
6651
|
+
import { z as z15 } from "zod";
|
|
6652
|
+
var AudioVoiceConvertParams = z15.object({
|
|
6653
|
+
model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
5962
6654
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
5963
|
-
voice:
|
|
5964
|
-
output_format:
|
|
6655
|
+
voice: z15.string().min(1),
|
|
6656
|
+
output_format: z15.string().optional(),
|
|
5965
6657
|
/** Strip the source clip's background noise before re-voicing. */
|
|
5966
|
-
remove_background_noise:
|
|
6658
|
+
remove_background_noise: z15.boolean().optional()
|
|
5967
6659
|
}).strict();
|
|
5968
6660
|
var audioVoiceConvertNode = delegated({
|
|
5969
6661
|
id: "audio_voice_convert",
|
|
@@ -5971,44 +6663,44 @@ var audioVoiceConvertNode = delegated({
|
|
|
5971
6663
|
category: "audio",
|
|
5972
6664
|
summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
|
|
5973
6665
|
when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
|
|
5974
|
-
inputs:
|
|
6666
|
+
inputs: z15.object({
|
|
5975
6667
|
audio: AudioRef,
|
|
5976
6668
|
voice_ref: TextRef.optional()
|
|
5977
6669
|
}).strict(),
|
|
5978
6670
|
params: AudioVoiceConvertParams,
|
|
5979
|
-
outputs:
|
|
6671
|
+
outputs: z15.object({ audio: AudioRef }).strict(),
|
|
5980
6672
|
outputKinds: { audio: "audio" },
|
|
5981
6673
|
cost: () => ({ credits: 1, seconds_estimate: 20 })
|
|
5982
6674
|
});
|
|
5983
6675
|
|
|
5984
6676
|
// src/engine/nodes/remote/dialogue.ts
|
|
5985
|
-
import { z as
|
|
5986
|
-
var DialogueInput =
|
|
5987
|
-
text:
|
|
5988
|
-
voice_id:
|
|
6677
|
+
import { z as z16 } from "zod";
|
|
6678
|
+
var DialogueInput = z16.object({
|
|
6679
|
+
text: z16.string().min(1),
|
|
6680
|
+
voice_id: z16.string().min(1)
|
|
5989
6681
|
});
|
|
5990
6682
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
5991
|
-
var DialogueParams =
|
|
5992
|
-
model:
|
|
6683
|
+
var DialogueParams = z16.object({
|
|
6684
|
+
model: z16.enum(DIALOGUE_MODELS),
|
|
5993
6685
|
/**
|
|
5994
6686
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
5995
6687
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
5996
6688
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
5997
6689
|
*/
|
|
5998
|
-
inputs:
|
|
5999
|
-
language_code:
|
|
6690
|
+
inputs: z16.array(DialogueInput).min(1).max(50),
|
|
6691
|
+
language_code: z16.string().optional(),
|
|
6000
6692
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
6001
|
-
settings:
|
|
6002
|
-
seed:
|
|
6003
|
-
apply_text_normalization:
|
|
6693
|
+
settings: z16.record(z16.string(), z16.unknown()).optional(),
|
|
6694
|
+
seed: z16.number().int().min(0).max(4294967295).optional(),
|
|
6695
|
+
apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
|
|
6004
6696
|
/**
|
|
6005
6697
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
6006
6698
|
* separate `timestamps` output — character-level alignment plus
|
|
6007
6699
|
* per-voice segment markers usable for captions, lipsync, or
|
|
6008
6700
|
* beat-matched cuts in ad creatives.
|
|
6009
6701
|
*/
|
|
6010
|
-
with_timestamps:
|
|
6011
|
-
output_format:
|
|
6702
|
+
with_timestamps: z16.boolean().optional(),
|
|
6703
|
+
output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6012
6704
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
6013
6705
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
6014
6706
|
path: ["inputs"]
|
|
@@ -6019,9 +6711,9 @@ var dialogueNode = delegated({
|
|
|
6019
6711
|
category: "audio",
|
|
6020
6712
|
summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
|
|
6021
6713
|
when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
|
|
6022
|
-
inputs:
|
|
6714
|
+
inputs: z16.object({}).loose(),
|
|
6023
6715
|
params: DialogueParams,
|
|
6024
|
-
outputs:
|
|
6716
|
+
outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6025
6717
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6026
6718
|
cost: ({ params }) => {
|
|
6027
6719
|
const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
|
|
@@ -6030,7 +6722,7 @@ var dialogueNode = delegated({
|
|
|
6030
6722
|
});
|
|
6031
6723
|
|
|
6032
6724
|
// src/engine/nodes/remote/image.ts
|
|
6033
|
-
import { z as
|
|
6725
|
+
import { z as z17 } from "zod";
|
|
6034
6726
|
var IMAGE_GENERATE_MODELS2 = [
|
|
6035
6727
|
"openai/gpt-5.4-image-2",
|
|
6036
6728
|
"google/gemini-3.5-flash",
|
|
@@ -6038,41 +6730,44 @@ var IMAGE_GENERATE_MODELS2 = [
|
|
|
6038
6730
|
"google/gemini-3-pro-image-preview",
|
|
6039
6731
|
"recraft/recraft-v4.1-pro-vector"
|
|
6040
6732
|
];
|
|
6041
|
-
var ImageGenerateParams =
|
|
6042
|
-
model:
|
|
6043
|
-
prompt:
|
|
6044
|
-
aspect_ratio:
|
|
6045
|
-
image_size:
|
|
6733
|
+
var ImageGenerateParams = z17.object({
|
|
6734
|
+
model: z17.enum(IMAGE_GENERATE_MODELS2),
|
|
6735
|
+
prompt: z17.string().min(1),
|
|
6736
|
+
aspect_ratio: z17.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
|
|
6737
|
+
image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
|
|
6738
|
+
// Rendering quality — forwarded into `image_config`. OpenRouter models without a
|
|
6739
|
+
// quality knob ignore it; the registry gates which models accept it (gpt-image, Gemini).
|
|
6740
|
+
quality: z17.enum(["auto", "low", "medium", "high"]).optional(),
|
|
6046
6741
|
// Recraft v4 vector controls — forwarded into `image_config`. Registry
|
|
6047
6742
|
// rejects them on non-Recraft models.
|
|
6048
|
-
strength:
|
|
6049
|
-
rgb_colors:
|
|
6050
|
-
background_rgb_color:
|
|
6743
|
+
strength: z17.number().min(0).max(1).optional(),
|
|
6744
|
+
rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
|
|
6745
|
+
background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
|
|
6051
6746
|
}).strict();
|
|
6052
6747
|
var imageGenerateNode = delegated({
|
|
6053
6748
|
id: "image_generate",
|
|
6054
|
-
version: "2.
|
|
6749
|
+
version: "2.2.0",
|
|
6055
6750
|
category: "image",
|
|
6056
6751
|
summary: "Generate images for ad creatives. Curated model set: GPT-5.4 Image, Gemini 3.5 Flash, Gemini 3.1 Flash Image Preview, Gemini 3 Pro Image, Recraft v4.1 Pro Vector. Per-model param support comes from the canvas-engine model registry.",
|
|
6057
6752
|
when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-5.4-image-2` for photorealistic; Gemini variants for fast iteration and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order.",
|
|
6058
6753
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
6059
6754
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
6060
|
-
inputs:
|
|
6755
|
+
inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
|
|
6061
6756
|
params: ImageGenerateParams,
|
|
6062
|
-
outputs:
|
|
6757
|
+
outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
|
|
6063
6758
|
outputKinds: { images: "image" },
|
|
6064
6759
|
cost: () => ({ credits: 5, seconds_estimate: 10 })
|
|
6065
6760
|
});
|
|
6066
6761
|
|
|
6067
6762
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
6068
|
-
import { z as
|
|
6763
|
+
import { z as z18 } from "zod";
|
|
6069
6764
|
var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
6070
6765
|
var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
6071
|
-
var ImageAspectAdaptParams =
|
|
6072
|
-
model:
|
|
6073
|
-
formats:
|
|
6074
|
-
guidance:
|
|
6075
|
-
image_size:
|
|
6766
|
+
var ImageAspectAdaptParams = z18.object({
|
|
6767
|
+
model: z18.enum(ASPECT_ADAPT_MODELS),
|
|
6768
|
+
formats: z18.array(z18.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }),
|
|
6769
|
+
guidance: z18.string().min(1).optional(),
|
|
6770
|
+
image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
6076
6771
|
}).strict();
|
|
6077
6772
|
var imageAspectAdaptNode = delegated({
|
|
6078
6773
|
id: "image_aspect_adapt",
|
|
@@ -6080,9 +6775,9 @@ var imageAspectAdaptNode = delegated({
|
|
|
6080
6775
|
category: "image",
|
|
6081
6776
|
summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as `formats`.",
|
|
6082
6777
|
when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and list the target ratios in `formats`. Cost is estimated per format; formats matching the source ratio are free pass-throughs. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
|
|
6083
|
-
inputs:
|
|
6778
|
+
inputs: z18.object({ source: ImageRef }).loose(),
|
|
6084
6779
|
params: ImageAspectAdaptParams,
|
|
6085
|
-
outputs:
|
|
6780
|
+
outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
|
|
6086
6781
|
outputKinds: { images: "image" },
|
|
6087
6782
|
cost: ({ params }) => {
|
|
6088
6783
|
const p = params;
|
|
@@ -6095,12 +6790,12 @@ var imageAspectAdaptNode = delegated({
|
|
|
6095
6790
|
});
|
|
6096
6791
|
|
|
6097
6792
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
6098
|
-
import { z as
|
|
6099
|
-
var ImageBackgroundRemoveParams =
|
|
6100
|
-
model:
|
|
6101
|
-
model_variant:
|
|
6102
|
-
operating_resolution:
|
|
6103
|
-
mask_only:
|
|
6793
|
+
import { z as z19 } from "zod";
|
|
6794
|
+
var ImageBackgroundRemoveParams = z19.object({
|
|
6795
|
+
model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
|
|
6796
|
+
model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
|
|
6797
|
+
operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
6798
|
+
mask_only: z19.boolean().optional().default(false)
|
|
6104
6799
|
}).strict();
|
|
6105
6800
|
var imageBackgroundRemoveNode = delegated({
|
|
6106
6801
|
id: "image_background_remove",
|
|
@@ -6108,11 +6803,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
6108
6803
|
category: "image",
|
|
6109
6804
|
summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
|
|
6110
6805
|
when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. Set `mask_only:true` to return the binary mask instead of the alpha-cut image.",
|
|
6111
|
-
inputs:
|
|
6806
|
+
inputs: z19.object({
|
|
6112
6807
|
image: ImageRef
|
|
6113
6808
|
}).strict(),
|
|
6114
6809
|
params: ImageBackgroundRemoveParams,
|
|
6115
|
-
outputs:
|
|
6810
|
+
outputs: z19.object({
|
|
6116
6811
|
image: ImageRef,
|
|
6117
6812
|
mask: ImageRef.optional()
|
|
6118
6813
|
}).strict(),
|
|
@@ -6121,7 +6816,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
6121
6816
|
});
|
|
6122
6817
|
|
|
6123
6818
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
6124
|
-
import { z as
|
|
6819
|
+
import { z as z20 } from "zod";
|
|
6125
6820
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
6126
6821
|
var imageDescribeNode = delegated({
|
|
6127
6822
|
id: "image_describe",
|
|
@@ -6129,33 +6824,33 @@ var imageDescribeNode = delegated({
|
|
|
6129
6824
|
category: "vision",
|
|
6130
6825
|
summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
|
|
6131
6826
|
when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
|
|
6132
|
-
inputs:
|
|
6133
|
-
params:
|
|
6134
|
-
model:
|
|
6135
|
-
focus:
|
|
6136
|
-
context:
|
|
6137
|
-
temperature:
|
|
6138
|
-
max_tokens:
|
|
6827
|
+
inputs: z20.object({ image: ImageRef }).loose(),
|
|
6828
|
+
params: z20.object({
|
|
6829
|
+
model: z20.enum(IMAGE_DESCRIBE_MODELS),
|
|
6830
|
+
focus: z20.string().optional(),
|
|
6831
|
+
context: z20.string().optional(),
|
|
6832
|
+
temperature: z20.number().min(0).max(2).optional(),
|
|
6833
|
+
max_tokens: z20.number().int().positive().optional()
|
|
6139
6834
|
}).strict(),
|
|
6140
|
-
outputs:
|
|
6835
|
+
outputs: z20.object({ description: JsonRef }).strict(),
|
|
6141
6836
|
outputKinds: { description: "json" },
|
|
6142
6837
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
6143
6838
|
});
|
|
6144
6839
|
|
|
6145
6840
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
6146
|
-
import { z as
|
|
6841
|
+
import { z as z21 } from "zod";
|
|
6147
6842
|
var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
6148
|
-
var ImageReferenceSheetParams =
|
|
6149
|
-
model:
|
|
6150
|
-
subject_description:
|
|
6843
|
+
var ImageReferenceSheetParams = z21.object({
|
|
6844
|
+
model: z21.enum(REFERENCE_SHEET_MODELS),
|
|
6845
|
+
subject_description: z21.string().min(1),
|
|
6151
6846
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
6152
6847
|
// so a multi-scene shoot keeps one consistent set.
|
|
6153
|
-
subject_type:
|
|
6154
|
-
views:
|
|
6155
|
-
style:
|
|
6156
|
-
prompt_override:
|
|
6157
|
-
aspect_ratio:
|
|
6158
|
-
image_size:
|
|
6848
|
+
subject_type: z21.enum(["character", "person", "product", "location"]),
|
|
6849
|
+
views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
|
|
6850
|
+
style: z21.string().optional(),
|
|
6851
|
+
prompt_override: z21.string().min(1).optional(),
|
|
6852
|
+
aspect_ratio: z21.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
|
|
6853
|
+
image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
6159
6854
|
}).strict();
|
|
6160
6855
|
var imageReferenceSheetNode = delegated({
|
|
6161
6856
|
id: "image_reference_sheet",
|
|
@@ -6163,9 +6858,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
6163
6858
|
category: "image",
|
|
6164
6859
|
summary: "Fuse 1\u20136 images of a single subject (person, character, product, or location/set) into ONE multi-view reference sheet \u2014 a labeled grid in consistent style and lighting: a turnaround (FRONT / SIDE / BACK\u2026) for a person/character/product, or several camera angles of the same room (WIDE / REVERSE / DETAIL\u2026) for a location. Curated models: Gemini 3 Pro Image (best fusion + labels), Gemini 3.1 Flash Image (cheap iteration).",
|
|
6165
6860
|
when_to_use: "Use before image_generate / video_generate when a subject must stay consistent across many creatives \u2014 wire the `sheet` output into their `reference` input instead of re-describing the subject per prompt. `subject_description` should be the exact wording you reuse downstream. Pick `google/gemini-3-pro-image-preview` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image-preview` while iterating.",
|
|
6166
|
-
inputs:
|
|
6861
|
+
inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
|
|
6167
6862
|
params: ImageReferenceSheetParams,
|
|
6168
|
-
outputs:
|
|
6863
|
+
outputs: z21.object({ sheet: ImageRef }).strict(),
|
|
6169
6864
|
outputKinds: { sheet: "image" },
|
|
6170
6865
|
cost: ({ params }) => ({
|
|
6171
6866
|
credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
|
|
@@ -6174,10 +6869,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
6174
6869
|
});
|
|
6175
6870
|
|
|
6176
6871
|
// src/engine/nodes/remote/imageSearch.ts
|
|
6177
|
-
import { z as
|
|
6178
|
-
var ImageSearchParams =
|
|
6179
|
-
prompt:
|
|
6180
|
-
count:
|
|
6872
|
+
import { z as z22 } from "zod";
|
|
6873
|
+
var ImageSearchParams = z22.object({
|
|
6874
|
+
prompt: z22.string().min(1),
|
|
6875
|
+
count: z22.number().int().min(1).max(20).default(5)
|
|
6181
6876
|
}).strict();
|
|
6182
6877
|
var imageSearchNode = delegated({
|
|
6183
6878
|
id: "image_search",
|
|
@@ -6185,15 +6880,15 @@ var imageSearchNode = delegated({
|
|
|
6185
6880
|
category: "image",
|
|
6186
6881
|
summary: "Agentic image search across Google Images, stock photography (Freepik), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
|
|
6187
6882
|
when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
|
|
6188
|
-
inputs:
|
|
6883
|
+
inputs: z22.object({}).loose(),
|
|
6189
6884
|
params: ImageSearchParams,
|
|
6190
|
-
outputs:
|
|
6885
|
+
outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
|
|
6191
6886
|
outputKinds: { images: "image" },
|
|
6192
6887
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
6193
6888
|
});
|
|
6194
6889
|
|
|
6195
6890
|
// src/engine/nodes/remote/imageSelect.ts
|
|
6196
|
-
import { z as
|
|
6891
|
+
import { z as z23 } from "zod";
|
|
6197
6892
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6198
6893
|
var imageSelectNode = delegated({
|
|
6199
6894
|
id: "image_select",
|
|
@@ -6201,15 +6896,15 @@ var imageSelectNode = delegated({
|
|
|
6201
6896
|
category: "vision",
|
|
6202
6897
|
summary: "Pick the best `count` images out of 2+ candidates with a vision LLM, judged against a prompt. Outputs a passthrough subset of the input refs (no new pixels) plus the model's comparative reasoning.",
|
|
6203
6898
|
when_to_use: "Use after fanning out several image_generate variants (or any pool of 2+ images) to keep only the strongest before expensive downstream steps \u2014 video generation, reference sheets, final delivery. `count` fixes the output size, so `images#0`\u2026`images#count-1` are always safe to wire. Pick `~google/gemini-flash-latest` for cheap/fast picks and `~google/gemini-pro-latest` for harder aesthetic judgement.",
|
|
6204
|
-
inputs:
|
|
6205
|
-
params:
|
|
6206
|
-
model:
|
|
6207
|
-
prompt:
|
|
6208
|
-
count:
|
|
6209
|
-
temperature:
|
|
6210
|
-
max_tokens:
|
|
6899
|
+
inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
|
|
6900
|
+
params: z23.object({
|
|
6901
|
+
model: z23.enum(IMAGE_SELECT_MODELS),
|
|
6902
|
+
prompt: z23.string().min(1),
|
|
6903
|
+
count: z23.number().int().min(1).default(1),
|
|
6904
|
+
temperature: z23.number().min(0).max(2).optional(),
|
|
6905
|
+
max_tokens: z23.number().int().positive().optional()
|
|
6211
6906
|
}).strict(),
|
|
6212
|
-
outputs:
|
|
6907
|
+
outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
|
|
6213
6908
|
outputKinds: { images: "image", reasoning: "text" },
|
|
6214
6909
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
6215
6910
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -6234,34 +6929,34 @@ var imageSelectNode = delegated({
|
|
|
6234
6929
|
});
|
|
6235
6930
|
|
|
6236
6931
|
// src/engine/nodes/remote/music.ts
|
|
6237
|
-
import { z as
|
|
6932
|
+
import { z as z24 } from "zod";
|
|
6238
6933
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
6239
|
-
var MusicParams =
|
|
6240
|
-
model:
|
|
6934
|
+
var MusicParams = z24.object({
|
|
6935
|
+
model: z24.enum(MUSIC_MODELS),
|
|
6241
6936
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
6242
|
-
prompt:
|
|
6937
|
+
prompt: z24.string().optional(),
|
|
6243
6938
|
/**
|
|
6244
6939
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
6245
6940
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
6246
6941
|
*/
|
|
6247
|
-
composition_plan:
|
|
6942
|
+
composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
|
|
6248
6943
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
6249
|
-
music_length_ms:
|
|
6250
|
-
seed:
|
|
6944
|
+
music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
6945
|
+
seed: z24.number().int().optional(),
|
|
6251
6946
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
6252
|
-
force_instrumental:
|
|
6947
|
+
force_instrumental: z24.boolean().optional(),
|
|
6253
6948
|
/** composition_plan only — honor exact section durations. */
|
|
6254
|
-
respect_sections_durations:
|
|
6949
|
+
respect_sections_durations: z24.boolean().optional(),
|
|
6255
6950
|
/** Emit word-level timestamps alongside the audio. */
|
|
6256
|
-
with_timestamps:
|
|
6951
|
+
with_timestamps: z24.boolean().optional(),
|
|
6257
6952
|
/**
|
|
6258
6953
|
* video-to-music only — short description of the desired score
|
|
6259
6954
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
6260
6955
|
*/
|
|
6261
|
-
description:
|
|
6956
|
+
description: z24.string().max(1e3).optional(),
|
|
6262
6957
|
/** video-to-music only — up to 10 style tags. */
|
|
6263
|
-
tags:
|
|
6264
|
-
output_format:
|
|
6958
|
+
tags: z24.array(z24.string()).max(10).optional(),
|
|
6959
|
+
output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6265
6960
|
}).strict();
|
|
6266
6961
|
var musicNode = delegated({
|
|
6267
6962
|
id: "music",
|
|
@@ -6269,9 +6964,9 @@ var musicNode = delegated({
|
|
|
6269
6964
|
category: "audio",
|
|
6270
6965
|
summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
|
|
6271
6966
|
when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
|
|
6272
|
-
inputs:
|
|
6967
|
+
inputs: z24.object({ video: VideoRef.optional() }).loose(),
|
|
6273
6968
|
params: MusicParams,
|
|
6274
|
-
outputs:
|
|
6969
|
+
outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6275
6970
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6276
6971
|
cost: ({ params }) => {
|
|
6277
6972
|
const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
|
|
@@ -6302,25 +6997,25 @@ var musicNode = delegated({
|
|
|
6302
6997
|
});
|
|
6303
6998
|
|
|
6304
6999
|
// src/engine/nodes/remote/soundEffect.ts
|
|
6305
|
-
import { z as
|
|
7000
|
+
import { z as z25 } from "zod";
|
|
6306
7001
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
6307
|
-
var SoundEffectParams =
|
|
6308
|
-
model:
|
|
7002
|
+
var SoundEffectParams = z25.object({
|
|
7003
|
+
model: z25.enum(SOUND_EFFECT_MODELS),
|
|
6309
7004
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
6310
|
-
text:
|
|
7005
|
+
text: z25.string().min(1),
|
|
6311
7006
|
/**
|
|
6312
7007
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
6313
7008
|
* natural length for the described effect.
|
|
6314
7009
|
*/
|
|
6315
|
-
duration_seconds:
|
|
7010
|
+
duration_seconds: z25.number().min(0.5).max(30).optional(),
|
|
6316
7011
|
/**
|
|
6317
7012
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
6318
7013
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
6319
7014
|
*/
|
|
6320
|
-
prompt_influence:
|
|
7015
|
+
prompt_influence: z25.number().min(0).max(1).optional(),
|
|
6321
7016
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
6322
|
-
loop:
|
|
6323
|
-
output_format:
|
|
7017
|
+
loop: z25.boolean().optional(),
|
|
7018
|
+
output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6324
7019
|
}).strict();
|
|
6325
7020
|
var soundEffectNode = delegated({
|
|
6326
7021
|
id: "sound_effect",
|
|
@@ -6328,9 +7023,9 @@ var soundEffectNode = delegated({
|
|
|
6328
7023
|
category: "audio",
|
|
6329
7024
|
summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
|
|
6330
7025
|
when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
|
|
6331
|
-
inputs:
|
|
7026
|
+
inputs: z25.object({}).loose(),
|
|
6332
7027
|
params: SoundEffectParams,
|
|
6333
|
-
outputs:
|
|
7028
|
+
outputs: z25.object({ audio: AudioRef }).strict(),
|
|
6334
7029
|
outputKinds: { audio: "audio" },
|
|
6335
7030
|
cost: ({ params }) => {
|
|
6336
7031
|
const seconds = params.duration_seconds ?? 5;
|
|
@@ -6339,7 +7034,7 @@ var soundEffectNode = delegated({
|
|
|
6339
7034
|
});
|
|
6340
7035
|
|
|
6341
7036
|
// src/engine/nodes/remote/textGenerate.ts
|
|
6342
|
-
import { z as
|
|
7037
|
+
import { z as z26 } from "zod";
|
|
6343
7038
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6344
7039
|
var textGenerateNode = delegated({
|
|
6345
7040
|
id: "text_generate",
|
|
@@ -6347,58 +7042,58 @@ var textGenerateNode = delegated({
|
|
|
6347
7042
|
category: "language",
|
|
6348
7043
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
6349
7044
|
when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
|
|
6350
|
-
inputs:
|
|
6351
|
-
params:
|
|
6352
|
-
model:
|
|
6353
|
-
prompt:
|
|
6354
|
-
system:
|
|
6355
|
-
response_format:
|
|
6356
|
-
web_search:
|
|
6357
|
-
temperature:
|
|
6358
|
-
max_tokens:
|
|
7045
|
+
inputs: z26.object({}).loose(),
|
|
7046
|
+
params: z26.object({
|
|
7047
|
+
model: z26.enum(TEXT_GENERATE_MODELS),
|
|
7048
|
+
prompt: z26.string().min(1),
|
|
7049
|
+
system: z26.string().optional(),
|
|
7050
|
+
response_format: z26.enum(["text", "json_object"]).optional(),
|
|
7051
|
+
web_search: z26.boolean().optional(),
|
|
7052
|
+
temperature: z26.number().min(0).max(2).optional(),
|
|
7053
|
+
max_tokens: z26.number().int().positive().optional()
|
|
6359
7054
|
}).strict(),
|
|
6360
|
-
outputs:
|
|
7055
|
+
outputs: z26.object({ text: TextRef }).strict(),
|
|
6361
7056
|
outputKinds: { text: "text" },
|
|
6362
7057
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
6363
7058
|
});
|
|
6364
7059
|
|
|
6365
7060
|
// src/engine/nodes/remote/tts.ts
|
|
6366
|
-
import { z as
|
|
7061
|
+
import { z as z27 } from "zod";
|
|
6367
7062
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
6368
|
-
var TtsVoiceSettings =
|
|
6369
|
-
stability:
|
|
6370
|
-
similarity_boost:
|
|
6371
|
-
style:
|
|
6372
|
-
use_speaker_boost:
|
|
6373
|
-
speed:
|
|
7063
|
+
var TtsVoiceSettings = z27.object({
|
|
7064
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7065
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
7066
|
+
style: z27.number().min(0).max(1).optional(),
|
|
7067
|
+
use_speaker_boost: z27.boolean().optional(),
|
|
7068
|
+
speed: z27.number().min(0.25).max(4).optional()
|
|
6374
7069
|
}).strict();
|
|
6375
|
-
var TtsPronunciationLocator =
|
|
6376
|
-
pronunciation_dictionary_id:
|
|
6377
|
-
version_id:
|
|
7070
|
+
var TtsPronunciationLocator = z27.object({
|
|
7071
|
+
pronunciation_dictionary_id: z27.string().min(1),
|
|
7072
|
+
version_id: z27.string().nullable().optional()
|
|
6378
7073
|
}).strict();
|
|
6379
|
-
var TtsParams =
|
|
6380
|
-
model:
|
|
6381
|
-
text:
|
|
6382
|
-
voice:
|
|
7074
|
+
var TtsParams = z27.object({
|
|
7075
|
+
model: z27.enum(TTS_MODELS),
|
|
7076
|
+
text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
7077
|
+
voice: z27.string().min(1),
|
|
6383
7078
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
6384
|
-
output_format:
|
|
6385
|
-
seed:
|
|
7079
|
+
output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
7080
|
+
seed: z27.number().int().min(0).max(4294967295).optional(),
|
|
6386
7081
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
6387
|
-
stability:
|
|
6388
|
-
similarity_boost:
|
|
7082
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7083
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
6389
7084
|
voice_settings: TtsVoiceSettings.optional(),
|
|
6390
7085
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
6391
|
-
language_code:
|
|
6392
|
-
pronunciation_dictionary_locators:
|
|
6393
|
-
apply_text_normalization:
|
|
7086
|
+
language_code: z27.string().optional(),
|
|
7087
|
+
pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
|
|
7088
|
+
apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
|
|
6394
7089
|
/** Currently Japanese-only. Adds latency. */
|
|
6395
|
-
apply_language_text_normalization:
|
|
7090
|
+
apply_language_text_normalization: z27.boolean().optional(),
|
|
6396
7091
|
/**
|
|
6397
7092
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
6398
7093
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
6399
7094
|
* rendering, lipsync, and beat-matched cuts.
|
|
6400
7095
|
*/
|
|
6401
|
-
with_timestamps:
|
|
7096
|
+
with_timestamps: z27.boolean().optional()
|
|
6402
7097
|
}).strict();
|
|
6403
7098
|
var ttsNode = delegated({
|
|
6404
7099
|
id: "tts",
|
|
@@ -6406,9 +7101,9 @@ var ttsNode = delegated({
|
|
|
6406
7101
|
category: "audio",
|
|
6407
7102
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
6408
7103
|
when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
|
|
6409
|
-
inputs:
|
|
7104
|
+
inputs: z27.object({}).loose(),
|
|
6410
7105
|
params: TtsParams,
|
|
6411
|
-
outputs:
|
|
7106
|
+
outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6412
7107
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6413
7108
|
cost: ({ params }) => ({
|
|
6414
7109
|
credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
|
|
@@ -6417,47 +7112,49 @@ var ttsNode = delegated({
|
|
|
6417
7112
|
});
|
|
6418
7113
|
|
|
6419
7114
|
// src/engine/nodes/remote/video.ts
|
|
6420
|
-
import { z as
|
|
6421
|
-
var
|
|
6422
|
-
var VideoGenerateParams =
|
|
6423
|
-
model:
|
|
6424
|
-
prompt:
|
|
6425
|
-
duration:
|
|
6426
|
-
resolution:
|
|
7115
|
+
import { z as z28 } from "zod";
|
|
7116
|
+
var videoModelEnum = z28.enum(VIDEO_GENERATE_MODELS);
|
|
7117
|
+
var VideoGenerateParams = z28.object({
|
|
7118
|
+
model: videoModelEnum,
|
|
7119
|
+
prompt: z28.string().min(1),
|
|
7120
|
+
duration: z28.number().int().positive().optional(),
|
|
7121
|
+
resolution: z28.string().optional(),
|
|
6427
7122
|
// Union of ratios accepted by at least one curated model (registry gates
|
|
6428
7123
|
// per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
|
|
6429
|
-
aspect_ratio:
|
|
6430
|
-
generate_audio:
|
|
6431
|
-
seed:
|
|
7124
|
+
aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
|
|
7125
|
+
generate_audio: z28.boolean().optional(),
|
|
7126
|
+
seed: z28.number().int().nonnegative().optional(),
|
|
6432
7127
|
// Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
|
|
6433
|
-
negative_prompt:
|
|
6434
|
-
person_generation:
|
|
6435
|
-
enhance_prompt:
|
|
6436
|
-
conditioning_scale:
|
|
7128
|
+
negative_prompt: z28.string().optional(),
|
|
7129
|
+
person_generation: z28.string().optional(),
|
|
7130
|
+
enhance_prompt: z28.boolean().optional(),
|
|
7131
|
+
conditioning_scale: z28.number().optional(),
|
|
7132
|
+
// Kling-only passthrough (prompt-adherence dial, sent top-level).
|
|
7133
|
+
cfg_scale: z28.number().optional()
|
|
6437
7134
|
}).strict();
|
|
6438
7135
|
var videoGenerateNode = delegated({
|
|
6439
7136
|
id: "video_generate",
|
|
6440
7137
|
version: "2.0.0",
|
|
6441
7138
|
category: "video",
|
|
6442
|
-
summary: "Generate video for ad creatives.
|
|
6443
|
-
when_to_use: "Use `bytedance/seedance-2.0` for
|
|
6444
|
-
inputs:
|
|
7139
|
+
summary: "Generate video for ad creatives. Curated roster: `bytedance/seedance-2.0` (identity/product workhorse), `google/veo-3.1` (photoreal cine ceiling + real-face fallback), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-3.0` (motion-transfer/dynamic). Async with polling.",
|
|
7140
|
+
when_to_use: "Use `bytedance/seedance-2.0` for identity/product output. Route real human likenesses to `google/veo-3.1` (dodges the ByteDance real-person filter); use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-3.0` for motion-transfer/hyper-dynamic beats. The scaffolder's scored router picks for you. Each model gates its own durations/resolutions/aspect ratios in the registry \u2014 see the README per-model section.",
|
|
7141
|
+
inputs: z28.object({
|
|
6445
7142
|
first_frame: ImageRef.optional(),
|
|
6446
7143
|
last_frame: ImageRef.optional(),
|
|
6447
7144
|
reference: ImageRef.optional()
|
|
6448
7145
|
}).loose(),
|
|
6449
7146
|
params: VideoGenerateParams,
|
|
6450
|
-
outputs:
|
|
7147
|
+
outputs: z28.object({ video: VideoRef }).strict(),
|
|
6451
7148
|
outputKinds: { video: "video" },
|
|
6452
7149
|
cost: () => ({ credits: 50, seconds_estimate: 120 })
|
|
6453
7150
|
});
|
|
6454
7151
|
|
|
6455
7152
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
6456
|
-
import { z as
|
|
6457
|
-
var VideoBackgroundRemoveParams =
|
|
6458
|
-
model:
|
|
6459
|
-
edge_refinement:
|
|
6460
|
-
output_codec:
|
|
7153
|
+
import { z as z29 } from "zod";
|
|
7154
|
+
var VideoBackgroundRemoveParams = z29.object({
|
|
7155
|
+
model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
|
|
7156
|
+
edge_refinement: z29.boolean().optional().default(true),
|
|
7157
|
+
output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
|
|
6461
7158
|
}).strict();
|
|
6462
7159
|
var videoBackgroundRemoveNode = delegated({
|
|
6463
7160
|
id: "video_background_remove",
|
|
@@ -6465,18 +7162,18 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
6465
7162
|
category: "video",
|
|
6466
7163
|
summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by fal.ai `veed/video-background-removal/fast`.",
|
|
6467
7164
|
when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
|
|
6468
|
-
inputs:
|
|
7165
|
+
inputs: z29.object({
|
|
6469
7166
|
video: VideoRef
|
|
6470
7167
|
}).strict(),
|
|
6471
7168
|
params: VideoBackgroundRemoveParams,
|
|
6472
|
-
outputs:
|
|
7169
|
+
outputs: z29.object({ video: VideoRef }).strict(),
|
|
6473
7170
|
outputKinds: { video: "video" },
|
|
6474
7171
|
// $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
|
|
6475
7172
|
cost: () => ({ credits: 50, seconds_estimate: 60 })
|
|
6476
7173
|
});
|
|
6477
7174
|
|
|
6478
7175
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
6479
|
-
import { z as
|
|
7176
|
+
import { z as z30 } from "zod";
|
|
6480
7177
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6481
7178
|
var videoDeconstructNode = delegated({
|
|
6482
7179
|
id: "video_deconstruct",
|
|
@@ -6484,34 +7181,34 @@ var videoDeconstructNode = delegated({
|
|
|
6484
7181
|
category: "video",
|
|
6485
7182
|
summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
|
|
6486
7183
|
when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
|
|
6487
|
-
inputs:
|
|
6488
|
-
params:
|
|
6489
|
-
model:
|
|
6490
|
-
mode:
|
|
6491
|
-
language:
|
|
6492
|
-
max_scenes:
|
|
6493
|
-
focus:
|
|
6494
|
-
start_s:
|
|
6495
|
-
end_s:
|
|
7184
|
+
inputs: z30.object({ video: VideoRef }).loose(),
|
|
7185
|
+
params: z30.object({
|
|
7186
|
+
model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
7187
|
+
mode: z30.enum(["full", "index"]).optional(),
|
|
7188
|
+
language: z30.string().min(2).max(8).optional(),
|
|
7189
|
+
max_scenes: z30.number().int().min(1).max(60).optional(),
|
|
7190
|
+
focus: z30.string().optional(),
|
|
7191
|
+
start_s: z30.number().min(0).optional(),
|
|
7192
|
+
end_s: z30.number().positive().optional(),
|
|
6496
7193
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
6497
7194
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
6498
7195
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
6499
7196
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
6500
|
-
shot_cuts:
|
|
7197
|
+
shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
|
|
6501
7198
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
6502
7199
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
6503
7200
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
6504
7201
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
6505
|
-
max_clip_s:
|
|
7202
|
+
max_clip_s: z30.number().positive().max(60).optional(),
|
|
6506
7203
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
6507
7204
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
6508
|
-
transcriber:
|
|
7205
|
+
transcriber: z30.enum(["groq", "deepgram"]).optional()
|
|
6509
7206
|
}).strict(),
|
|
6510
|
-
outputs:
|
|
7207
|
+
outputs: z30.object({
|
|
6511
7208
|
analysis: JsonRef,
|
|
6512
7209
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
6513
|
-
start_frames:
|
|
6514
|
-
end_frames:
|
|
7210
|
+
start_frames: z30.array(ImageRef).min(1).optional(),
|
|
7211
|
+
end_frames: z30.array(ImageRef).min(1).optional(),
|
|
6515
7212
|
transcript: JsonRef
|
|
6516
7213
|
}).strict(),
|
|
6517
7214
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -6519,31 +7216,31 @@ var videoDeconstructNode = delegated({
|
|
|
6519
7216
|
});
|
|
6520
7217
|
|
|
6521
7218
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
6522
|
-
import { z as
|
|
6523
|
-
var FalLipsyncParams =
|
|
6524
|
-
model:
|
|
7219
|
+
import { z as z31 } from "zod";
|
|
7220
|
+
var FalLipsyncParams = z31.object({
|
|
7221
|
+
model: z31.literal("fal/veed-lipsync")
|
|
6525
7222
|
}).strict();
|
|
6526
|
-
var VideoLipsyncParams =
|
|
7223
|
+
var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
|
|
6527
7224
|
var videoLipsyncNode = delegated({
|
|
6528
7225
|
id: "video_lipsync",
|
|
6529
7226
|
version: "1.0.0",
|
|
6530
7227
|
category: "video",
|
|
6531
7228
|
summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
|
|
6532
|
-
inputs:
|
|
7229
|
+
inputs: z31.object({
|
|
6533
7230
|
video: VideoRef,
|
|
6534
7231
|
audio: AudioRef
|
|
6535
7232
|
}).strict(),
|
|
6536
7233
|
params: VideoLipsyncParams,
|
|
6537
|
-
outputs:
|
|
7234
|
+
outputs: z31.object({ video: VideoRef }).strict(),
|
|
6538
7235
|
outputKinds: { video: "video" },
|
|
6539
7236
|
cost: () => ({ credits: 20, seconds_estimate: 120 })
|
|
6540
7237
|
});
|
|
6541
7238
|
|
|
6542
7239
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6543
|
-
import { mkdtemp as mkdtemp6, readFile as
|
|
7240
|
+
import { mkdtemp as mkdtemp6, readFile as readFile11, rm as rm6 } from "fs/promises";
|
|
6544
7241
|
import { tmpdir as tmpdir6 } from "os";
|
|
6545
|
-
import
|
|
6546
|
-
import { z as
|
|
7242
|
+
import path14 from "path";
|
|
7243
|
+
import { z as z32 } from "zod";
|
|
6547
7244
|
|
|
6548
7245
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
6549
7246
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -6622,29 +7319,32 @@ ${detail.slice(-4e3)}`);
|
|
|
6622
7319
|
}
|
|
6623
7320
|
|
|
6624
7321
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6625
|
-
var VideoTranscribeParams =
|
|
6626
|
-
language:
|
|
7322
|
+
var VideoTranscribeParams = z32.object({
|
|
7323
|
+
language: z32.string().min(2).max(8).optional(),
|
|
6627
7324
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
6628
7325
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
6629
7326
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
6630
|
-
transcriber:
|
|
7327
|
+
transcriber: z32.enum(["groq", "deepgram"]).optional()
|
|
6631
7328
|
}).strict();
|
|
6632
|
-
var VideoTranscribeInputs =
|
|
6633
|
-
video
|
|
7329
|
+
var VideoTranscribeInputs = z32.object({
|
|
7330
|
+
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
7331
|
+
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
7332
|
+
// the local extraction path has been shipping one for every video input.
|
|
7333
|
+
video: z32.union([VideoRef, AudioRef])
|
|
6634
7334
|
}).strict();
|
|
6635
|
-
var VideoTranscribeOutputs =
|
|
6636
|
-
transcript:
|
|
7335
|
+
var VideoTranscribeOutputs = z32.object({
|
|
7336
|
+
transcript: z32.custom(),
|
|
6637
7337
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
6638
7338
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
6639
|
-
rich:
|
|
7339
|
+
rich: z32.custom().optional()
|
|
6640
7340
|
}).strict();
|
|
6641
7341
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
6642
7342
|
var videoTranscribeNode = defineNode({
|
|
6643
7343
|
id: "video_transcribe",
|
|
6644
|
-
version: "2.
|
|
7344
|
+
version: "2.3.0",
|
|
6645
7345
|
category: "language",
|
|
6646
7346
|
location: "local",
|
|
6647
|
-
summary: 'Transcribe a video
|
|
7347
|
+
summary: 'Transcribe a video or audio track to a word-level JSON transcript. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo ($0.04/hr, 10s min); `transcriber:"deepgram"` uses Deepgram Nova-3 ($0.0043/min) and additionally emits a `rich` JSON output with punctuated words + paragraph/sentence grouping (and speaker indices). Automatically extracts audio locally (mono 16 kHz MP3) before uploading \u2014 reduces payload ~100\xD7 and lifts the effective duration limit well beyond Groq\'s 100 MB file cap. The `transcript` output is always an array of {text, start, end} entries ready to feed Hyperframes caption compositions (Deepgram prefers the punctuated word form).',
|
|
6648
7348
|
when_to_use: 'Use to generate burned-in captions for a stitched video. Pair with `hyperframe_render` and a captions composition (e.g. `tiktok-captions`) by passing the transcript JSON through `params.variables.transcript`. Pick `params.language` to filter out non-target speech (e.g. "es" for Spanish). Pick `transcriber:"deepgram"` when you want punctuation/paragraphs (read them from the `rich` output) or speaker grouping. Requires ffmpeg on PATH for audio extraction (falls back to full video upload if unavailable).',
|
|
6649
7349
|
inputs: VideoTranscribeInputs,
|
|
6650
7350
|
params: VideoTranscribeParams,
|
|
@@ -6656,7 +7356,7 @@ var videoTranscribeNode = defineNode({
|
|
|
6656
7356
|
const effectiveInputs = audioInput ?? inputs;
|
|
6657
7357
|
return await callBackendExec({
|
|
6658
7358
|
nodeType: "video_transcribe",
|
|
6659
|
-
nodeVersion: "2.
|
|
7359
|
+
nodeVersion: "2.3.0",
|
|
6660
7360
|
params,
|
|
6661
7361
|
inputs: effectiveInputs,
|
|
6662
7362
|
outputKinds: { transcript: "json", rich: "json" },
|
|
@@ -6674,14 +7374,14 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6674
7374
|
ctx.log("video_transcribe: no audio track detected, sending full video");
|
|
6675
7375
|
return null;
|
|
6676
7376
|
}
|
|
6677
|
-
tmpDir = await mkdtemp6(
|
|
6678
|
-
const audioPath =
|
|
7377
|
+
tmpDir = await mkdtemp6(path14.join(tmpdir6(), "vtx-"));
|
|
7378
|
+
const audioPath = path14.join(tmpDir, "audio.mp3");
|
|
6679
7379
|
ctx.log("video_transcribe: extracting audio (mono 16kHz mp3)");
|
|
6680
7380
|
await runFfmpeg(
|
|
6681
7381
|
["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
|
|
6682
7382
|
{ timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
|
|
6683
7383
|
);
|
|
6684
|
-
const bytes = await
|
|
7384
|
+
const bytes = await readFile11(audioPath);
|
|
6685
7385
|
if (bytes.byteLength === 0) {
|
|
6686
7386
|
ctx.log("video_transcribe: extracted audio is empty, sending full video");
|
|
6687
7387
|
return null;
|
|
@@ -6721,29 +7421,29 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6721
7421
|
}
|
|
6722
7422
|
|
|
6723
7423
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
6724
|
-
import { z as
|
|
7424
|
+
import { z as z33 } from "zod";
|
|
6725
7425
|
var voiceSelectNode = delegated({
|
|
6726
7426
|
id: "voice_select",
|
|
6727
7427
|
version: "1.0.0",
|
|
6728
7428
|
category: "audio",
|
|
6729
7429
|
summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
|
|
6730
7430
|
when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
|
|
6731
|
-
inputs:
|
|
6732
|
-
params:
|
|
6733
|
-
description:
|
|
6734
|
-
gender:
|
|
6735
|
-
age:
|
|
6736
|
-
accent:
|
|
6737
|
-
language:
|
|
6738
|
-
limit:
|
|
7431
|
+
inputs: z33.object({}).loose(),
|
|
7432
|
+
params: z33.object({
|
|
7433
|
+
description: z33.string().min(1),
|
|
7434
|
+
gender: z33.string().optional(),
|
|
7435
|
+
age: z33.string().optional(),
|
|
7436
|
+
accent: z33.string().optional(),
|
|
7437
|
+
language: z33.string().optional(),
|
|
7438
|
+
limit: z33.number().int().min(1).max(20).optional()
|
|
6739
7439
|
}).strict(),
|
|
6740
|
-
outputs:
|
|
7440
|
+
outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
6741
7441
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
6742
7442
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
6743
7443
|
});
|
|
6744
7444
|
|
|
6745
7445
|
// src/engine/schema/catalog.ts
|
|
6746
|
-
import { z as
|
|
7446
|
+
import { z as z34 } from "zod";
|
|
6747
7447
|
function generateCatalog(registry, opts = {}) {
|
|
6748
7448
|
const entries = registry.all().map((def) => {
|
|
6749
7449
|
const cost = def.cost ? safeCost(def) : void 0;
|
|
@@ -6754,9 +7454,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
6754
7454
|
summary: def.summary,
|
|
6755
7455
|
when_to_use: def.when_to_use,
|
|
6756
7456
|
location: def.location,
|
|
6757
|
-
inputs:
|
|
6758
|
-
params:
|
|
6759
|
-
outputs:
|
|
7457
|
+
inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
7458
|
+
params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
7459
|
+
outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
6760
7460
|
cost_estimate_credits: cost?.credits,
|
|
6761
7461
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
6762
7462
|
};
|
|
@@ -6788,19 +7488,19 @@ function safeCost(def) {
|
|
|
6788
7488
|
|
|
6789
7489
|
// src/engine/storage/cache-store.ts
|
|
6790
7490
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6791
|
-
import { mkdir as mkdir3, readFile as
|
|
6792
|
-
import
|
|
7491
|
+
import { mkdir as mkdir3, readFile as readFile12, rename as rename2, writeFile as writeFile7 } from "fs/promises";
|
|
7492
|
+
import path15 from "path";
|
|
6793
7493
|
var LocalCacheStore = class {
|
|
6794
7494
|
rootDir;
|
|
6795
7495
|
constructor(rootDir) {
|
|
6796
7496
|
this.rootDir = rootDir;
|
|
6797
7497
|
}
|
|
6798
7498
|
filePath(cacheKey) {
|
|
6799
|
-
return
|
|
7499
|
+
return path15.join(this.rootDir, `${cacheKey}.json`);
|
|
6800
7500
|
}
|
|
6801
7501
|
async get(cacheKey) {
|
|
6802
7502
|
try {
|
|
6803
|
-
const buf = await
|
|
7503
|
+
const buf = await readFile12(this.filePath(cacheKey), "utf8");
|
|
6804
7504
|
return JSON.parse(buf);
|
|
6805
7505
|
} catch (e) {
|
|
6806
7506
|
if (e.code === "ENOENT") return null;
|
|
@@ -6809,7 +7509,7 @@ var LocalCacheStore = class {
|
|
|
6809
7509
|
}
|
|
6810
7510
|
async put(entry) {
|
|
6811
7511
|
const dest = this.filePath(entry.cacheKey);
|
|
6812
|
-
await mkdir3(
|
|
7512
|
+
await mkdir3(path15.dirname(dest), { recursive: true });
|
|
6813
7513
|
const tmp = `${dest}.tmp-${process.pid}-${randomUUID2()}`;
|
|
6814
7514
|
await writeFile7(tmp, JSON.stringify(entry, null, 0));
|
|
6815
7515
|
await rename2(tmp, dest);
|
|
@@ -6833,7 +7533,8 @@ var LOCAL_NODES = [
|
|
|
6833
7533
|
imagemagickNode,
|
|
6834
7534
|
videoTranscribeNode,
|
|
6835
7535
|
fontSpecimenNode,
|
|
6836
|
-
audioTimelineNode
|
|
7536
|
+
audioTimelineNode,
|
|
7537
|
+
collectNode
|
|
6837
7538
|
];
|
|
6838
7539
|
var REMOTE_NODES = [
|
|
6839
7540
|
textGenerateNode,
|
|
@@ -6863,12 +7564,12 @@ function defaultRegistry() {
|
|
|
6863
7564
|
}
|
|
6864
7565
|
function createEngineFromEnv(opts = {}) {
|
|
6865
7566
|
const cwd = opts.cwd ?? process.cwd();
|
|
6866
|
-
const cacheDir = opts.cacheDir ??
|
|
6867
|
-
const outputsDir = opts.outputsDir ??
|
|
7567
|
+
const cacheDir = opts.cacheDir ?? path16.join(cwd, "canvas", ".cache");
|
|
7568
|
+
const outputsDir = opts.outputsDir ?? path16.join(cwd, "canvas");
|
|
6868
7569
|
const creds = requireCredentialsFromEnv();
|
|
6869
7570
|
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
6870
|
-
const assets = new LocalAssetStore(
|
|
6871
|
-
const localCache = new LocalCacheStore(
|
|
7571
|
+
const assets = new LocalAssetStore(path16.join(cacheDir, "assets"));
|
|
7572
|
+
const localCache = new LocalCacheStore(path16.join(cacheDir, "index"));
|
|
6872
7573
|
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
6873
7574
|
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
6874
7575
|
local: localCache,
|
|
@@ -6888,12 +7589,15 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6888
7589
|
}
|
|
6889
7590
|
|
|
6890
7591
|
export {
|
|
7592
|
+
BackendClient,
|
|
6891
7593
|
requireCredentialsFromEnv,
|
|
7594
|
+
RunAbortedError,
|
|
6892
7595
|
LayerExecutionError,
|
|
6893
7596
|
describeFailureReason,
|
|
6894
7597
|
SEEDANCE_DURATIONS,
|
|
6895
7598
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
6896
7599
|
IMAGE_GENERATE_MODELS,
|
|
7600
|
+
DEFAULT_VIDEO_GENERATE_MODEL,
|
|
6897
7601
|
MODEL_REGISTRY,
|
|
6898
7602
|
resolveConcurrency,
|
|
6899
7603
|
ulid,
|
|
@@ -6902,9 +7606,13 @@ export {
|
|
|
6902
7606
|
REF_PREFIX,
|
|
6903
7607
|
parseRefExpr,
|
|
6904
7608
|
sha256Hex,
|
|
7609
|
+
SEEDANCE_PROFILE,
|
|
7610
|
+
clipProfileFor,
|
|
7611
|
+
clipParamRecipe,
|
|
7612
|
+
imageProfileFor,
|
|
6905
7613
|
elementMentionKeywords,
|
|
6906
7614
|
toModelSafeImage,
|
|
6907
|
-
BackendClient2
|
|
7615
|
+
BackendClient2,
|
|
6908
7616
|
Engine2 as Engine,
|
|
6909
7617
|
LocalAssetStore2 as LocalAssetStore,
|
|
6910
7618
|
LocalCacheStore2 as LocalCacheStore,
|
|
@@ -6914,4 +7622,4 @@ export {
|
|
|
6914
7622
|
defaultRegistry,
|
|
6915
7623
|
createEngineFromEnv
|
|
6916
7624
|
};
|
|
6917
|
-
//# sourceMappingURL=chunk-
|
|
7625
|
+
//# sourceMappingURL=chunk-J2LYFDVC.js.map
|