@koda-sl/baker-cli 0.123.0 → 0.124.0-dev.2ddde71d7
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 +366 -77
- package/dist/{chunk-IWPAXJC3.js → chunk-TKL2CJ6G.js} +930 -431
- package/dist/chunk-TKL2CJ6G.js.map +1 -0
- package/dist/cli.js +3798 -2758
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +137 -1
- package/dist/engine/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-IWPAXJC3.js.map +0 -1
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
__toESM
|
|
4
4
|
} from "./chunk-5WRI5ZAA.js";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
|
|
7
7
|
var require_safe_stable_stringify = __commonJS({
|
|
8
|
-
"
|
|
8
|
+
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
9
9
|
"use strict";
|
|
10
10
|
var { hasOwnProperty } = Object.prototype;
|
|
11
11
|
var stringify = configure2();
|
|
@@ -138,9 +138,9 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
138
138
|
}
|
|
139
139
|
if (value) {
|
|
140
140
|
return (value2) => {
|
|
141
|
-
let
|
|
142
|
-
if (typeof value2 !== "function")
|
|
143
|
-
throw new Error(
|
|
141
|
+
let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
|
|
142
|
+
if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
|
|
143
|
+
throw new Error(message2);
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -652,6 +652,9 @@ var HttpClient = class {
|
|
|
652
652
|
async postJson(path16, body, signal) {
|
|
653
653
|
return await this.requestJson("POST", path16, body, signal);
|
|
654
654
|
}
|
|
655
|
+
async putJson(path16, body, signal) {
|
|
656
|
+
return await this.requestJson("PUT", path16, body, signal);
|
|
657
|
+
}
|
|
655
658
|
async getJson(path16, signal) {
|
|
656
659
|
return await this.requestJson("GET", path16, void 0, signal);
|
|
657
660
|
}
|
|
@@ -679,8 +682,8 @@ var HttpClient = class {
|
|
|
679
682
|
try {
|
|
680
683
|
const res = await this.fetchFn(url, {
|
|
681
684
|
method,
|
|
682
|
-
headers: method === "
|
|
683
|
-
body: method === "
|
|
685
|
+
headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
|
686
|
+
body: method === "GET" ? void 0 : JSON.stringify(body),
|
|
684
687
|
signal: controller.signal
|
|
685
688
|
});
|
|
686
689
|
if (res.ok) return { kind: "value", value: await res.json() };
|
|
@@ -717,33 +720,33 @@ async function parseErrorBody(res) {
|
|
|
717
720
|
const errObj = body.error ?? {};
|
|
718
721
|
return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
|
|
719
722
|
}
|
|
720
|
-
function classifyHttpError(status, errObj,
|
|
723
|
+
function classifyHttpError(status, errObj, message2) {
|
|
721
724
|
if (errObj.code === CONTENT_POLICY_CODE) {
|
|
722
|
-
return { kind: "content_policy", status, provider: errObj.provider, message };
|
|
725
|
+
return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
|
|
723
726
|
}
|
|
724
727
|
if (status === 401 || status === 403) {
|
|
725
|
-
return { kind: "unauthorized", status, message };
|
|
728
|
+
return { kind: "unauthorized", status, message: message2 };
|
|
726
729
|
}
|
|
727
730
|
if (status === 400 || status === 422) {
|
|
728
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
731
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
729
732
|
}
|
|
730
733
|
if (status === 502 || status === 504) {
|
|
731
734
|
if (errObj.code === "provider_timeout" || status === 504) {
|
|
732
|
-
return { kind: "timeout", provider: errObj.provider, message };
|
|
735
|
+
return { kind: "timeout", provider: errObj.provider, message: message2 };
|
|
733
736
|
}
|
|
734
737
|
return {
|
|
735
738
|
kind: "provider",
|
|
736
739
|
status,
|
|
737
740
|
provider: errObj.provider,
|
|
738
741
|
code: errObj.code ?? "provider_error",
|
|
739
|
-
message,
|
|
742
|
+
message: message2,
|
|
740
743
|
retryable: errObj.retryable ?? true
|
|
741
744
|
};
|
|
742
745
|
}
|
|
743
746
|
if (status >= 500 || status === 429) {
|
|
744
|
-
return { kind: "server", status, message };
|
|
747
|
+
return { kind: "server", status, message: message2 };
|
|
745
748
|
}
|
|
746
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
749
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
747
750
|
}
|
|
748
751
|
function backoffMs(attempt) {
|
|
749
752
|
return 1e3 * 2 ** attempt;
|
|
@@ -777,7 +780,9 @@ function failedJobError(error) {
|
|
|
777
780
|
retryable: error.retryable ?? false
|
|
778
781
|
});
|
|
779
782
|
}
|
|
780
|
-
|
|
783
|
+
function pollInterval(attempt) {
|
|
784
|
+
return attempt < 15 ? 1e3 : 3e3;
|
|
785
|
+
}
|
|
781
786
|
var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
|
|
782
787
|
var BackendClient = class {
|
|
783
788
|
http;
|
|
@@ -794,7 +799,7 @@ var BackendClient = class {
|
|
|
794
799
|
async pollJob(jobId, signal) {
|
|
795
800
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
796
801
|
const path16 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
797
|
-
|
|
802
|
+
for (let attempt = 0; ; attempt++) {
|
|
798
803
|
if (signal?.aborted) {
|
|
799
804
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
800
805
|
}
|
|
@@ -804,16 +809,78 @@ var BackendClient = class {
|
|
|
804
809
|
if (Date.now() > deadline) {
|
|
805
810
|
throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
|
|
806
811
|
}
|
|
807
|
-
await sleep(
|
|
812
|
+
await sleep(pollInterval(attempt));
|
|
808
813
|
}
|
|
809
814
|
}
|
|
810
|
-
presignAssetUpload(sha256, mime, signal) {
|
|
815
|
+
presignAssetUpload(sha256, mime, signal, purpose) {
|
|
811
816
|
return this.http.postJson(
|
|
812
817
|
"/api/canvas/assets/presign",
|
|
813
|
-
{ sha256, mime },
|
|
818
|
+
{ sha256, mime, purpose },
|
|
814
819
|
signal
|
|
815
820
|
);
|
|
816
821
|
}
|
|
822
|
+
/** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
|
|
823
|
+
async getCacheEntry(cacheKey, signal) {
|
|
824
|
+
try {
|
|
825
|
+
const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
|
|
826
|
+
return res.entry;
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async putCacheEntry(entry, signal) {
|
|
833
|
+
await this.http.putJson(
|
|
834
|
+
`/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
|
|
835
|
+
entry,
|
|
836
|
+
signal
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
/** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
|
|
840
|
+
async recordRun(payload, signal) {
|
|
841
|
+
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
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
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
878
|
+
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
879
|
+
* Additive on the backend (never archives siblings, never sets definitionPath).
|
|
880
|
+
*/
|
|
881
|
+
async syncCreativeDefinition(payload, signal) {
|
|
882
|
+
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
883
|
+
}
|
|
817
884
|
getArtifact(kind, name, version, signal) {
|
|
818
885
|
const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
819
886
|
return this.http.getJson(path16, signal);
|
|
@@ -837,14 +904,17 @@ function requireCredentialsFromEnv(env = process.env) {
|
|
|
837
904
|
}
|
|
838
905
|
return c;
|
|
839
906
|
}
|
|
907
|
+
function remoteCacheEnabledFromEnv(env = process.env) {
|
|
908
|
+
return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
|
|
909
|
+
}
|
|
840
910
|
|
|
841
911
|
// src/engine/engine/errors.ts
|
|
842
912
|
function isBlocking(issue) {
|
|
843
913
|
return issue.severity !== "warning";
|
|
844
914
|
}
|
|
845
915
|
var CanvasError = class extends Error {
|
|
846
|
-
constructor(
|
|
847
|
-
super(
|
|
916
|
+
constructor(message2) {
|
|
917
|
+
super(message2);
|
|
848
918
|
this.name = "CanvasError";
|
|
849
919
|
}
|
|
850
920
|
};
|
|
@@ -870,6 +940,14 @@ var NodeExecutionError = class extends CanvasError {
|
|
|
870
940
|
this.cause = cause;
|
|
871
941
|
}
|
|
872
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
|
+
};
|
|
873
951
|
var LayerExecutionError = class extends CanvasError {
|
|
874
952
|
failures;
|
|
875
953
|
constructor(failures) {
|
|
@@ -901,7 +979,7 @@ function describeCause(c) {
|
|
|
901
979
|
}
|
|
902
980
|
}
|
|
903
981
|
|
|
904
|
-
//
|
|
982
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
905
983
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
906
984
|
var configure = import__.default.configure;
|
|
907
985
|
var wrapper_default = import__.default;
|
|
@@ -959,10 +1037,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
|
|
|
959
1037
|
var ELEVENLABS_MAX_TEXT_CHARS = 45454;
|
|
960
1038
|
var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
|
|
961
1039
|
var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
962
|
-
var
|
|
963
|
-
var
|
|
1040
|
+
var REPLICATE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
1041
|
+
var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
964
1042
|
var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
965
|
-
var
|
|
1043
|
+
var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
|
|
966
1044
|
var IMAGE_GENERATE_MODELS = [
|
|
967
1045
|
"openai/gpt-5.4-image-2",
|
|
968
1046
|
"google/gemini-3.5-flash",
|
|
@@ -1180,20 +1258,23 @@ var MODEL_REGISTRY = {
|
|
|
1180
1258
|
},
|
|
1181
1259
|
video_generate: {
|
|
1182
1260
|
"bytedance/seedance-2.0": {
|
|
1183
|
-
// Routed via
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1261
|
+
// Routed via Replicate's official `bytedance/seedance-2.0` model. NOTE:
|
|
1262
|
+
// ByteDance's upstream "real person" likeness filter still blocks photoreal
|
|
1263
|
+
// human reference frames on ANY reseller — the escape is a synthetic/AI
|
|
1264
|
+
// presenter face or routing real faces to Veo, not the provider choice.
|
|
1186
1265
|
label: "ByteDance Seedance 2.0",
|
|
1187
1266
|
inputs: [],
|
|
1188
|
-
optional_inputs: [{ kind: "image", mimes:
|
|
1267
|
+
optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
|
|
1189
1268
|
required: ["prompt"],
|
|
1190
1269
|
params: {
|
|
1191
|
-
prompt
|
|
1270
|
+
// Replicate's Seedance wrapper hard-caps the prompt at 4000 chars; gate
|
|
1271
|
+
// it here so an over-length prompt fails validate (free) not the billed call.
|
|
1272
|
+
prompt: { kind: "string", maxLength: 4e3 },
|
|
1192
1273
|
aspect_ratio: {
|
|
1193
1274
|
kind: "string",
|
|
1194
1275
|
enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
|
|
1195
1276
|
},
|
|
1196
|
-
resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
|
|
1277
|
+
resolution: { kind: "string", enum: ["480p", "720p", "1080p", "4k"] },
|
|
1197
1278
|
duration: { kind: "number", enum: SEEDANCE_DURATIONS },
|
|
1198
1279
|
seed: { kind: "number" },
|
|
1199
1280
|
generate_audio: { kind: "boolean" }
|
|
@@ -1214,7 +1295,10 @@ var MODEL_REGISTRY = {
|
|
|
1214
1295
|
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1215
1296
|
seed: { kind: "number" },
|
|
1216
1297
|
generate_audio: { kind: "boolean" },
|
|
1217
|
-
|
|
1298
|
+
// Image-to-video and EU/UK/CH/MENA regions cap this at `allow_adult`;
|
|
1299
|
+
// `allow_all` is text-to-video only. Allow both so an image-conditioned
|
|
1300
|
+
// Veo clip (the real-face fallback) validates.
|
|
1301
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1218
1302
|
enhance_prompt: { kind: "boolean" },
|
|
1219
1303
|
conditioning_scale: { kind: "number" }
|
|
1220
1304
|
}
|
|
@@ -1265,8 +1349,8 @@ var MODEL_REGISTRY = {
|
|
|
1265
1349
|
"fal/veed-lipsync": {
|
|
1266
1350
|
label: "VEED Lipsync (fal.ai)",
|
|
1267
1351
|
inputs: [
|
|
1268
|
-
{ kind: "video", mimes:
|
|
1269
|
-
{ kind: "audio", mimes:
|
|
1352
|
+
{ kind: "video", mimes: REPLICATE_VIDEO_MIMES },
|
|
1353
|
+
{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }
|
|
1270
1354
|
],
|
|
1271
1355
|
required: [],
|
|
1272
1356
|
params: {}
|
|
@@ -1302,7 +1386,7 @@ var MODEL_REGISTRY = {
|
|
|
1302
1386
|
// TARGET voice, preserving timing/prosody. Used to normalize a talking-head
|
|
1303
1387
|
// clip's native (generator-chosen) voice into ONE consistent brand voice.
|
|
1304
1388
|
label: "ElevenLabs Voice Changer (multilingual STS v2)",
|
|
1305
|
-
inputs: [{ kind: "audio", mimes:
|
|
1389
|
+
inputs: [{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }],
|
|
1306
1390
|
required: ["voice"],
|
|
1307
1391
|
params: {
|
|
1308
1392
|
voice: { kind: "string" },
|
|
@@ -1329,7 +1413,7 @@ var MODEL_REGISTRY = {
|
|
|
1329
1413
|
},
|
|
1330
1414
|
"elevenlabs/video-background-music-v1": {
|
|
1331
1415
|
label: "ElevenLabs Video Background Music v1",
|
|
1332
|
-
inputs: [{ kind: "video", mimes:
|
|
1416
|
+
inputs: [{ kind: "video", mimes: REPLICATE_VIDEO_MIMES }],
|
|
1333
1417
|
required: [],
|
|
1334
1418
|
params: {
|
|
1335
1419
|
description: { kind: "string" },
|
|
@@ -1500,7 +1584,7 @@ function validateValue(key, value, schema, model) {
|
|
|
1500
1584
|
}
|
|
1501
1585
|
|
|
1502
1586
|
// src/engine/lib/concurrency.ts
|
|
1503
|
-
var DEFAULT_CONCURRENCY =
|
|
1587
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
1504
1588
|
function resolveConcurrency(...candidates) {
|
|
1505
1589
|
for (const candidate of candidates) {
|
|
1506
1590
|
if (candidate === void 0 || candidate === "") continue;
|
|
@@ -1562,6 +1646,185 @@ function encodeRandom() {
|
|
|
1562
1646
|
return out;
|
|
1563
1647
|
}
|
|
1564
1648
|
|
|
1649
|
+
// src/engine/storage/remote-cache-store.ts
|
|
1650
|
+
var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
|
|
1651
|
+
function isPersistedAssetRef(ref) {
|
|
1652
|
+
const { url, sha256 } = ref;
|
|
1653
|
+
if (typeof url !== "string" || typeof sha256 !== "string") return false;
|
|
1654
|
+
return url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256.slice(0, 2)}/${sha256}`) || url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256}`);
|
|
1655
|
+
}
|
|
1656
|
+
function isAssetRefLike(value) {
|
|
1657
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
|
|
1658
|
+
}
|
|
1659
|
+
function collectAssetRefLikes(value, out = []) {
|
|
1660
|
+
if (Array.isArray(value)) {
|
|
1661
|
+
for (const item of value) collectAssetRefLikes(item, out);
|
|
1662
|
+
return out;
|
|
1663
|
+
}
|
|
1664
|
+
if (typeof value !== "object" || value === null) return out;
|
|
1665
|
+
if (isAssetRefLike(value)) {
|
|
1666
|
+
out.push(value);
|
|
1667
|
+
}
|
|
1668
|
+
for (const item of Object.values(value)) collectAssetRefLikes(item, out);
|
|
1669
|
+
return out;
|
|
1670
|
+
}
|
|
1671
|
+
function entryFullyPersisted(entry) {
|
|
1672
|
+
return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
|
|
1673
|
+
}
|
|
1674
|
+
function stripLocalFields(entry) {
|
|
1675
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1676
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1677
|
+
delete ref.path;
|
|
1678
|
+
delete ref.bytes;
|
|
1679
|
+
}
|
|
1680
|
+
return clone;
|
|
1681
|
+
}
|
|
1682
|
+
var RemoteCacheStore = class {
|
|
1683
|
+
client;
|
|
1684
|
+
log;
|
|
1685
|
+
constructor(client, log) {
|
|
1686
|
+
this.client = client;
|
|
1687
|
+
this.log = log ?? (() => void 0);
|
|
1688
|
+
}
|
|
1689
|
+
async get(cacheKey) {
|
|
1690
|
+
return await this.client.getCacheEntry(cacheKey);
|
|
1691
|
+
}
|
|
1692
|
+
async put(entry) {
|
|
1693
|
+
if (!entryFullyPersisted(entry)) {
|
|
1694
|
+
this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const stripped = stripLocalFields(entry);
|
|
1698
|
+
if (stripped.refs.length > MAX_REMOTE_REFS) {
|
|
1699
|
+
stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
|
|
1700
|
+
}
|
|
1701
|
+
await this.client.putCacheEntry(stripped);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
var MAX_REMOTE_REFS = 512;
|
|
1705
|
+
var LayeredCacheStore = class {
|
|
1706
|
+
rootDir;
|
|
1707
|
+
local;
|
|
1708
|
+
remote;
|
|
1709
|
+
assets;
|
|
1710
|
+
log;
|
|
1711
|
+
constructor(opts) {
|
|
1712
|
+
this.local = opts.local;
|
|
1713
|
+
this.remote = opts.remote;
|
|
1714
|
+
this.assets = opts.assets;
|
|
1715
|
+
this.rootDir = opts.local.rootDir;
|
|
1716
|
+
this.log = opts.log ?? (() => void 0);
|
|
1717
|
+
}
|
|
1718
|
+
async get(cacheKey) {
|
|
1719
|
+
const localHit = await this.local.get(cacheKey);
|
|
1720
|
+
if (localHit) return localHit;
|
|
1721
|
+
let remoteEntry;
|
|
1722
|
+
try {
|
|
1723
|
+
remoteEntry = await this.remote.get(cacheKey);
|
|
1724
|
+
} catch (e) {
|
|
1725
|
+
this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
if (!remoteEntry) return null;
|
|
1729
|
+
let rehydrated;
|
|
1730
|
+
try {
|
|
1731
|
+
rehydrated = await this.rehydrate(remoteEntry);
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
await this.local.put(rehydrated);
|
|
1737
|
+
return rehydrated;
|
|
1738
|
+
}
|
|
1739
|
+
async put(entry) {
|
|
1740
|
+
await this.local.put(entry);
|
|
1741
|
+
try {
|
|
1742
|
+
await this.remote.put(entry);
|
|
1743
|
+
} catch (e) {
|
|
1744
|
+
this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Download every referenced asset into the local content-addressed store
|
|
1749
|
+
* (sha-verified) and stamp fresh local paths. Any ref that cannot be
|
|
1750
|
+
* rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
|
|
1751
|
+
* crash materialization later with a far less actionable error.
|
|
1752
|
+
*/
|
|
1753
|
+
async rehydrate(entry) {
|
|
1754
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1755
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1756
|
+
if (!isPersistedAssetRef(ref)) {
|
|
1757
|
+
throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
|
|
1758
|
+
}
|
|
1759
|
+
const ingested = await this.assets.ingestRemote({
|
|
1760
|
+
kind: typeof ref.kind === "string" ? ref.kind : "json",
|
|
1761
|
+
url: ref.url,
|
|
1762
|
+
sha256: ref.sha256,
|
|
1763
|
+
mime: ref.mime,
|
|
1764
|
+
metadata: ref.metadata ?? void 0
|
|
1765
|
+
});
|
|
1766
|
+
ref.path = ingested.path;
|
|
1767
|
+
}
|
|
1768
|
+
return clone;
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
function message(e) {
|
|
1772
|
+
return e instanceof Error ? e.message : String(e);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/engine/nodes/remote/upload.ts
|
|
1776
|
+
var PUT_MAX_ATTEMPTS = 4;
|
|
1777
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1778
|
+
async function presignAndPut(args) {
|
|
1779
|
+
let lastFailure = null;
|
|
1780
|
+
for (let attempt = 0; attempt < PUT_MAX_ATTEMPTS; attempt++) {
|
|
1781
|
+
if (args.ctx.signal?.aborted) break;
|
|
1782
|
+
if (attempt > 0) await sleep2(500 * 2 ** (attempt - 1) * (1 + Math.random() * 0.25));
|
|
1783
|
+
const result = await attemptPresignedPut(args);
|
|
1784
|
+
if (result.ok) return result.url;
|
|
1785
|
+
lastFailure = result.failure;
|
|
1786
|
+
if (!result.retryable) break;
|
|
1787
|
+
}
|
|
1788
|
+
throw lastFailure ?? new Error("upload: aborted before the PUT could start");
|
|
1789
|
+
}
|
|
1790
|
+
async function attemptPresignedPut(args) {
|
|
1791
|
+
try {
|
|
1792
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1793
|
+
const putRes = await fetch(putUrl, {
|
|
1794
|
+
method: "PUT",
|
|
1795
|
+
body: new Uint8Array(args.bytes),
|
|
1796
|
+
headers: { "Content-Type": args.mime },
|
|
1797
|
+
signal: args.ctx.signal
|
|
1798
|
+
});
|
|
1799
|
+
if (putRes.ok) return { ok: true, url: publicUrl };
|
|
1800
|
+
return {
|
|
1801
|
+
ok: false,
|
|
1802
|
+
failure: new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`),
|
|
1803
|
+
// Only transient statuses warrant a replay — a 400/403 fails identically every attempt.
|
|
1804
|
+
retryable: putRes.status >= 500 || putRes.status === 429 || putRes.status === 408
|
|
1805
|
+
};
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
return {
|
|
1808
|
+
ok: false,
|
|
1809
|
+
failure: e instanceof Error ? e : new Error(String(e)),
|
|
1810
|
+
retryable: args.ctx.signal?.aborted !== true
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
async function ensureUploaded(ref, ctx) {
|
|
1815
|
+
if (ref.url) return ref;
|
|
1816
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1817
|
+
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1818
|
+
return { ...ref, url };
|
|
1819
|
+
}
|
|
1820
|
+
async function persistOutputAssetUrls(outputs, ctx) {
|
|
1821
|
+
for (const ref of collectAssetRefLikes(outputs)) {
|
|
1822
|
+
if (isPersistedAssetRef(ref)) continue;
|
|
1823
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1824
|
+
ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1565
1828
|
// src/engine/schema/canvas.ts
|
|
1566
1829
|
import { z } from "zod";
|
|
1567
1830
|
var REF_PREFIX = "$ref:";
|
|
@@ -1591,7 +1854,14 @@ var NodeDecl = z.object({
|
|
|
1591
1854
|
version: z.string().min(1).optional(),
|
|
1592
1855
|
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
1593
1856
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
1594
|
-
when: z.unknown().optional()
|
|
1857
|
+
when: z.unknown().optional(),
|
|
1858
|
+
// Regenerate knob. The engine is content-addressed: identical params + inputs
|
|
1859
|
+
// return the cached render, so re-running an unchanged node NEVER re-bills or
|
|
1860
|
+
// produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
|
|
1861
|
+
// a note) and re-run to force THIS node to render fresh; because its new output
|
|
1862
|
+
// changes downstream input hashes, everything depending on it regenerates too.
|
|
1863
|
+
// This is the declarative "change a value, re-run, get a new render" affordance.
|
|
1864
|
+
regenerate: z.union([z.string(), z.number()]).optional()
|
|
1595
1865
|
}).strict();
|
|
1596
1866
|
var OutputRef = z.object({
|
|
1597
1867
|
node: z.string(),
|
|
@@ -2841,6 +3111,7 @@ var Engine = class {
|
|
|
2841
3111
|
cache;
|
|
2842
3112
|
outputsDir;
|
|
2843
3113
|
log;
|
|
3114
|
+
persistAssets;
|
|
2844
3115
|
constructor(opts) {
|
|
2845
3116
|
this.registry = opts.registry;
|
|
2846
3117
|
this.client = opts.client;
|
|
@@ -2848,6 +3119,7 @@ var Engine = class {
|
|
|
2848
3119
|
this.cache = opts.cache;
|
|
2849
3120
|
this.outputsDir = opts.outputsDir;
|
|
2850
3121
|
this.log = opts.log ?? (() => void 0);
|
|
3122
|
+
this.persistAssets = opts.persistAssets ?? false;
|
|
2851
3123
|
}
|
|
2852
3124
|
validate(canvas) {
|
|
2853
3125
|
return validateCanvas(canvas, this.registry);
|
|
@@ -2859,6 +3131,12 @@ var Engine = class {
|
|
|
2859
3131
|
async run(input, opts = {}) {
|
|
2860
3132
|
const validation = await this.validateDeep(input);
|
|
2861
3133
|
if (!validation.ok) throw new ValidationError(validation.issues);
|
|
3134
|
+
if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
|
|
3135
|
+
throw new RunAbortedError(
|
|
3136
|
+
"cost_cap",
|
|
3137
|
+
`estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
2862
3140
|
const canvas = validation.canvas;
|
|
2863
3141
|
const runId = opts.run_id ?? `r_${ulid()}`;
|
|
2864
3142
|
const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
|
|
@@ -2871,7 +3149,16 @@ var Engine = class {
|
|
|
2871
3149
|
const outputs = {};
|
|
2872
3150
|
const counters = { cachedNodes: 0, totalCredits: 0 };
|
|
2873
3151
|
const nodeRuns = [];
|
|
2874
|
-
|
|
3152
|
+
const graph = this.pruneToOutput(canvas, buildGraph(canvas));
|
|
3153
|
+
const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
|
|
3154
|
+
this.emitProgress(opts, {
|
|
3155
|
+
kind: "plan",
|
|
3156
|
+
nodes: [...graph.entries()].map(([id, deps]) => {
|
|
3157
|
+
const node = canvas.nodes.find((n) => n.id === id);
|
|
3158
|
+
return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
|
|
3159
|
+
})
|
|
3160
|
+
});
|
|
3161
|
+
await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes);
|
|
2875
3162
|
const output = pickFinalOutput(canvas, outputs);
|
|
2876
3163
|
const stats = {
|
|
2877
3164
|
total_nodes: canvas.nodes.length,
|
|
@@ -2894,39 +3181,63 @@ var Engine = class {
|
|
|
2894
3181
|
`[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
|
|
2895
3182
|
);
|
|
2896
3183
|
this.log(`outputs in: ${writer.runDir}`);
|
|
2897
|
-
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
|
|
3184
|
+
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
2898
3185
|
}
|
|
2899
|
-
async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
2900
|
-
const layers = topologicalLayers(
|
|
3186
|
+
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
3187
|
+
const layers = topologicalLayers(graph);
|
|
2901
3188
|
const limit = resolveConcurrency(opts.concurrency);
|
|
2902
3189
|
for (const layer of layers) {
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3190
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
|
|
3191
|
+
if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
|
|
3192
|
+
throw new RunAbortedError(
|
|
3193
|
+
"cost_cap",
|
|
3194
|
+
`spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3198
|
+
if (opts.signal?.aborted) {
|
|
3199
|
+
return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
|
|
3200
|
+
}
|
|
3201
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3202
|
+
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
|
|
2907
3203
|
if (r.cached) counters.cachedNodes++;
|
|
2908
3204
|
counters.totalCredits += r.credits;
|
|
2909
3205
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2910
3206
|
if (node) {
|
|
2911
|
-
|
|
3207
|
+
const run = {
|
|
2912
3208
|
node_id: nodeId,
|
|
2913
3209
|
node_type: node.type,
|
|
2914
3210
|
cached: r.cached,
|
|
2915
3211
|
duration_ms: r.durationMs,
|
|
2916
3212
|
credits: r.credits
|
|
2917
|
-
}
|
|
3213
|
+
};
|
|
3214
|
+
nodeRuns.push(run);
|
|
3215
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
2918
3216
|
}
|
|
2919
|
-
})
|
|
2920
|
-
);
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
2921
3219
|
const failures = [];
|
|
2922
3220
|
settled.forEach((result, i) => {
|
|
2923
3221
|
const nodeId = layer[i];
|
|
2924
|
-
if (result.status === "rejected" && nodeId)
|
|
3222
|
+
if (result.status === "rejected" && nodeId) {
|
|
3223
|
+
if (result.reason instanceof RunAbortedError) return;
|
|
3224
|
+
failures.push({ nodeId, reason: result.reason });
|
|
3225
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
|
|
3226
|
+
}
|
|
2925
3227
|
});
|
|
3228
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
2926
3229
|
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
2927
3230
|
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
2928
3231
|
}
|
|
2929
3232
|
}
|
|
3233
|
+
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
3234
|
+
emitProgress(opts, event) {
|
|
3235
|
+
if (!opts.onProgress) return;
|
|
3236
|
+
try {
|
|
3237
|
+
opts.onProgress(event);
|
|
3238
|
+
} catch {
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
2930
3241
|
/**
|
|
2931
3242
|
* Dead-node elimination: when the canvas declares an `output`, execute only the
|
|
2932
3243
|
* nodes that output transitively depends on. Orphaned nodes (left by an edit or
|
|
@@ -2957,12 +3268,13 @@ var Engine = class {
|
|
|
2957
3268
|
}
|
|
2958
3269
|
await writer.writeManifest("_final", output);
|
|
2959
3270
|
}
|
|
2960
|
-
async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
|
|
3271
|
+
async executeOne(canvas, nodeId, outputs, runId, writer, opts, downloadOutputs) {
|
|
2961
3272
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2962
3273
|
if (!node) throw new Error(`executor: missing node ${nodeId}`);
|
|
2963
3274
|
const def = this.registry.get(node.type);
|
|
2964
3275
|
if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
|
|
2965
|
-
const
|
|
3276
|
+
const regenerateToken = resolveRegenerateToken(node, opts.regenerate, runId);
|
|
3277
|
+
const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
|
|
2966
3278
|
const policy = opts.cache_policy ?? "read_write";
|
|
2967
3279
|
if (policy !== "bypass") {
|
|
2968
3280
|
const cacheT0 = Date.now();
|
|
@@ -2980,17 +3292,27 @@ var Engine = class {
|
|
|
2980
3292
|
nodeId: node.id,
|
|
2981
3293
|
nodeType: node.type,
|
|
2982
3294
|
cacheKey: prepared.cacheKey,
|
|
3295
|
+
downloadOutputs,
|
|
2983
3296
|
client: this.client,
|
|
2984
3297
|
assets: this.assets,
|
|
2985
3298
|
log: this.log,
|
|
2986
3299
|
signal: opts.signal
|
|
2987
3300
|
};
|
|
2988
|
-
const
|
|
3301
|
+
const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
|
|
3302
|
+
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
2989
3303
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
2990
3304
|
const elapsed = Date.now() - t0;
|
|
2991
3305
|
const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
|
|
2992
3306
|
const outputsObj = result;
|
|
2993
3307
|
outputs[node.id] = outputsObj;
|
|
3308
|
+
if (this.persistAssets) {
|
|
3309
|
+
try {
|
|
3310
|
+
await persistOutputAssetUrls(outputsObj, ctx);
|
|
3311
|
+
} catch (e) {
|
|
3312
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3313
|
+
this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
2994
3316
|
if (policy === "read_write") {
|
|
2995
3317
|
await this.cache.put({
|
|
2996
3318
|
cacheKey: prepared.cacheKey,
|
|
@@ -3019,8 +3341,42 @@ var Engine = class {
|
|
|
3019
3341
|
}
|
|
3020
3342
|
}
|
|
3021
3343
|
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Download any URL-only asset ref reachable in a local node's inputs so the
|
|
3346
|
+
* bytes are on disk before the local runner stages them. Returns a copy —
|
|
3347
|
+
* refs are replaced, never mutated in place, so the producer's cached output
|
|
3348
|
+
* (shared object) keeps its URL-only shape.
|
|
3349
|
+
*/
|
|
3350
|
+
async materializeLocalInputs(inputs) {
|
|
3351
|
+
const fix = async (value) => {
|
|
3352
|
+
if (Array.isArray(value)) return Promise.all(value.map(fix));
|
|
3353
|
+
if (value && typeof value === "object") {
|
|
3354
|
+
const v = value;
|
|
3355
|
+
if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
|
|
3356
|
+
this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
|
|
3357
|
+
return this.assets.ingestRemote({
|
|
3358
|
+
kind: v.kind,
|
|
3359
|
+
url: v.url,
|
|
3360
|
+
sha256: v.sha256,
|
|
3361
|
+
mime: v.mime,
|
|
3362
|
+
metadata: v.metadata
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
const out = {};
|
|
3366
|
+
for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
|
|
3367
|
+
return out;
|
|
3368
|
+
}
|
|
3369
|
+
return value;
|
|
3370
|
+
};
|
|
3371
|
+
return await fix(inputs);
|
|
3372
|
+
}
|
|
3022
3373
|
};
|
|
3023
|
-
|
|
3374
|
+
function resolveRegenerateToken(node, forced, runId) {
|
|
3375
|
+
if (forced?.has(node.id)) return `run:${runId}`;
|
|
3376
|
+
if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
|
|
3377
|
+
return void 0;
|
|
3378
|
+
}
|
|
3379
|
+
async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
|
|
3024
3380
|
const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
|
|
3025
3381
|
const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
|
|
3026
3382
|
const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
|
|
@@ -3033,6 +3389,9 @@ async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
|
|
|
3033
3389
|
throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
|
|
3034
3390
|
}
|
|
3035
3391
|
}
|
|
3392
|
+
if (regenerateToken !== void 0) {
|
|
3393
|
+
extras = { ...extras ?? {}, __regenerate__: regenerateToken };
|
|
3394
|
+
}
|
|
3036
3395
|
const cacheKey = computeCacheKey({
|
|
3037
3396
|
node_id: node.type,
|
|
3038
3397
|
node_version: def.version,
|
|
@@ -3061,6 +3420,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
|
|
|
3061
3420
|
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3062
3421
|
}
|
|
3063
3422
|
}
|
|
3423
|
+
function needsLocalMaterialization(def) {
|
|
3424
|
+
return def.location === "local" && !def.passthroughRefs;
|
|
3425
|
+
}
|
|
3064
3426
|
function pickFinalOutput(canvas, outputs) {
|
|
3065
3427
|
if (canvas.output) {
|
|
3066
3428
|
const node = outputs[canvas.output.node];
|
|
@@ -3071,6 +3433,16 @@ function pickFinalOutput(canvas, outputs) {
|
|
|
3071
3433
|
const lastOut = outputs[last.id];
|
|
3072
3434
|
return lastOut ? Object.values(lastOut)[0] : void 0;
|
|
3073
3435
|
}
|
|
3436
|
+
function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
3437
|
+
const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
|
|
3438
|
+
const needs = /* @__PURE__ */ new Set();
|
|
3439
|
+
for (const [consumerId, deps] of graph) {
|
|
3440
|
+
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
3441
|
+
if (def?.location !== "local" || def.passthroughRefs) continue;
|
|
3442
|
+
for (const dep of deps) needs.add(dep);
|
|
3443
|
+
}
|
|
3444
|
+
return needs;
|
|
3445
|
+
}
|
|
3074
3446
|
function buildGraph(canvas) {
|
|
3075
3447
|
const graph = /* @__PURE__ */ new Map();
|
|
3076
3448
|
for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
|
|
@@ -3201,7 +3573,16 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
|
|
|
3201
3573
|
try {
|
|
3202
3574
|
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3203
3575
|
} catch (e) {
|
|
3204
|
-
|
|
3576
|
+
if (value.url) {
|
|
3577
|
+
try {
|
|
3578
|
+
await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
|
|
3579
|
+
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3580
|
+
} catch (e2) {
|
|
3581
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
|
|
3582
|
+
}
|
|
3583
|
+
} else {
|
|
3584
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3585
|
+
}
|
|
3205
3586
|
}
|
|
3206
3587
|
if (bytes.length > MAX_INLINE_TEXT_BYTES) {
|
|
3207
3588
|
throw new NodeExecutionError(nodeId, nodeType, {
|
|
@@ -3309,27 +3690,6 @@ var FontRef = BaseAssetRef.extend({
|
|
|
3309
3690
|
});
|
|
3310
3691
|
var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
|
|
3311
3692
|
|
|
3312
|
-
// src/engine/nodes/remote/upload.ts
|
|
3313
|
-
async function presignAndPut(args) {
|
|
3314
|
-
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
3315
|
-
const putRes = await fetch(putUrl, {
|
|
3316
|
-
method: "PUT",
|
|
3317
|
-
body: new Uint8Array(args.bytes),
|
|
3318
|
-
headers: { "Content-Type": args.mime },
|
|
3319
|
-
signal: args.ctx.signal
|
|
3320
|
-
});
|
|
3321
|
-
if (!putRes.ok) {
|
|
3322
|
-
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
3323
|
-
}
|
|
3324
|
-
return publicUrl;
|
|
3325
|
-
}
|
|
3326
|
-
async function ensureUploaded(ref, ctx) {
|
|
3327
|
-
if (ref.url) return ref;
|
|
3328
|
-
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
3329
|
-
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
3330
|
-
return { ...ref, url };
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
3693
|
// src/engine/nodes/remote/delegate.ts
|
|
3334
3694
|
function delegated(spec) {
|
|
3335
3695
|
return {
|
|
@@ -3357,7 +3717,9 @@ async function callBackendExec(args) {
|
|
|
3357
3717
|
nodeVersion: args.nodeVersion,
|
|
3358
3718
|
params: args.params,
|
|
3359
3719
|
inputs: serialized,
|
|
3360
|
-
idempotency_key: idempotencyKey
|
|
3720
|
+
idempotency_key: idempotencyKey,
|
|
3721
|
+
canvas_run_id: args.ctx.canvasRunId,
|
|
3722
|
+
node_id: args.ctx.nodeId
|
|
3361
3723
|
},
|
|
3362
3724
|
args.ctx.signal
|
|
3363
3725
|
);
|
|
@@ -3409,6 +3771,9 @@ async function ingestValue(value, ctx, declaredKind) {
|
|
|
3409
3771
|
}
|
|
3410
3772
|
if (isRawAsset(value)) {
|
|
3411
3773
|
const kind = value.kind ?? declaredKind ?? "json";
|
|
3774
|
+
if (ctx.downloadOutputs === false) {
|
|
3775
|
+
return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
|
|
3776
|
+
}
|
|
3412
3777
|
return ctx.assets.ingestRemote({
|
|
3413
3778
|
kind,
|
|
3414
3779
|
url: value.url,
|
|
@@ -3523,7 +3888,7 @@ function safePathname(rawUrl) {
|
|
|
3523
3888
|
}
|
|
3524
3889
|
var ingestNode = defineNode({
|
|
3525
3890
|
id: "ingest",
|
|
3526
|
-
version: "1.
|
|
3891
|
+
version: "1.2.0",
|
|
3527
3892
|
category: "io",
|
|
3528
3893
|
location: "local",
|
|
3529
3894
|
summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Handinger for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
|
|
@@ -3566,6 +3931,9 @@ function runStrategy(strategy, params, ctx) {
|
|
|
3566
3931
|
}
|
|
3567
3932
|
}
|
|
3568
3933
|
async function execDirectFetch(params, ctx) {
|
|
3934
|
+
if (params.expect === "image") {
|
|
3935
|
+
return ingestImageUrl(params.url, ctx);
|
|
3936
|
+
}
|
|
3569
3937
|
const result = await callBackendExec({
|
|
3570
3938
|
nodeType: "ingest",
|
|
3571
3939
|
nodeVersion: ingestNode.version,
|
|
@@ -3576,6 +3944,37 @@ async function execDirectFetch(params, ctx) {
|
|
|
3576
3944
|
});
|
|
3577
3945
|
return assertAssetOutput(result, params.expect);
|
|
3578
3946
|
}
|
|
3947
|
+
async function ingestImageUrl(url, ctx) {
|
|
3948
|
+
const res = await fetch(url);
|
|
3949
|
+
if (!res.ok) {
|
|
3950
|
+
throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
|
|
3951
|
+
}
|
|
3952
|
+
const ab = await res.arrayBuffer();
|
|
3953
|
+
if (ab.byteLength > MAX_ASSET_BYTES) {
|
|
3954
|
+
throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
|
|
3955
|
+
}
|
|
3956
|
+
let normalized;
|
|
3957
|
+
try {
|
|
3958
|
+
normalized = await toModelSafeImage(Buffer.from(ab));
|
|
3959
|
+
} catch (e) {
|
|
3960
|
+
throw localExecError(ctx, `${url}: ${e.message}`);
|
|
3961
|
+
}
|
|
3962
|
+
if (normalized.rasterizedFrom) {
|
|
3963
|
+
ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
|
|
3964
|
+
}
|
|
3965
|
+
return uploadAndIngest({
|
|
3966
|
+
bytes: normalized.bytes,
|
|
3967
|
+
kind: "image",
|
|
3968
|
+
mime: normalized.mime,
|
|
3969
|
+
metadata: {
|
|
3970
|
+
source_url: url,
|
|
3971
|
+
strategy: "direct_fetch",
|
|
3972
|
+
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3973
|
+
...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
|
|
3974
|
+
},
|
|
3975
|
+
ctx
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3579
3978
|
async function execHandinger(params, ctx) {
|
|
3580
3979
|
const result = await callBackendExec({
|
|
3581
3980
|
nodeType: "ingest",
|
|
@@ -3612,7 +4011,15 @@ var EXT_TO_MIME = {
|
|
|
3612
4011
|
jpeg: "image/jpeg",
|
|
3613
4012
|
webp: "image/webp",
|
|
3614
4013
|
gif: "image/gif",
|
|
4014
|
+
// Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
|
|
4015
|
+
// must resolve to an image mime here or the kind-check rejects the local file
|
|
4016
|
+
// before normalization ever runs.
|
|
3615
4017
|
avif: "image/avif",
|
|
4018
|
+
heic: "image/heic",
|
|
4019
|
+
heif: "image/heif",
|
|
4020
|
+
tif: "image/tiff",
|
|
4021
|
+
tiff: "image/tiff",
|
|
4022
|
+
bmp: "image/bmp",
|
|
3616
4023
|
mp4: "video/mp4",
|
|
3617
4024
|
webm: "video/webm",
|
|
3618
4025
|
mov: "video/quicktime",
|
|
@@ -3663,15 +4070,45 @@ async function rasterizeSvgToPng(bytes) {
|
|
|
3663
4070
|
}
|
|
3664
4071
|
return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
|
|
3665
4072
|
}
|
|
4073
|
+
var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
4074
|
+
async function toModelSafeImage(bytes) {
|
|
4075
|
+
const safe = sniffImageMime(bytes);
|
|
4076
|
+
if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
|
|
4077
|
+
return { bytes, mime: safe };
|
|
4078
|
+
}
|
|
4079
|
+
if (sniffSvg(bytes)) {
|
|
4080
|
+
return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
|
|
4081
|
+
}
|
|
4082
|
+
const { default: sharp } = await import("sharp");
|
|
4083
|
+
try {
|
|
4084
|
+
const img = sharp(bytes);
|
|
4085
|
+
const format = (await img.metadata()).format;
|
|
4086
|
+
const png = await img.png({ force: true }).toBuffer();
|
|
4087
|
+
return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
|
|
4088
|
+
} catch (e) {
|
|
4089
|
+
throw new Error(`bytes are not a decodable image (${e.message})`);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
function hasAscii(buf, offset, sig) {
|
|
4093
|
+
return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
|
|
4094
|
+
}
|
|
4095
|
+
var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
|
|
4096
|
+
function sniffIsoBmff(buf) {
|
|
4097
|
+
if (!hasAscii(buf, 4, "ftyp")) return null;
|
|
4098
|
+
const brand = buf.subarray(8, 12).toString("ascii");
|
|
4099
|
+
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
4100
|
+
if (HEIC_BRANDS.has(brand)) return "image/heic";
|
|
4101
|
+
return null;
|
|
4102
|
+
}
|
|
3666
4103
|
function sniffImageMime(buf) {
|
|
3667
4104
|
if (buf.length < 4) return null;
|
|
3668
|
-
if (buf[0] === 137 && buf
|
|
4105
|
+
if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
|
|
3669
4106
|
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
3670
|
-
if (buf
|
|
3671
|
-
if (buf
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
return
|
|
4107
|
+
if (hasAscii(buf, 0, "GIF")) return "image/gif";
|
|
4108
|
+
if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
|
|
4109
|
+
if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
|
|
4110
|
+
if (hasAscii(buf, 0, "BM")) return "image/bmp";
|
|
4111
|
+
return sniffIsoBmff(buf);
|
|
3675
4112
|
}
|
|
3676
4113
|
function findBoxPayload(buf, start, end, type) {
|
|
3677
4114
|
let offset = start;
|
|
@@ -3724,10 +4161,10 @@ function inferKindFromMime(mime) {
|
|
|
3724
4161
|
if (mime.startsWith("font/")) return "font";
|
|
3725
4162
|
return null;
|
|
3726
4163
|
}
|
|
3727
|
-
function localExecError(ctx,
|
|
4164
|
+
function localExecError(ctx, message2) {
|
|
3728
4165
|
return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
|
|
3729
4166
|
kind: "local",
|
|
3730
|
-
cause: new Error(`ingest: ${
|
|
4167
|
+
cause: new Error(`ingest: ${message2}`)
|
|
3731
4168
|
});
|
|
3732
4169
|
}
|
|
3733
4170
|
async function execLocalFile(params, ctx) {
|
|
@@ -3774,17 +4211,20 @@ async function execLocalFile(params, ctx) {
|
|
|
3774
4211
|
ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
|
|
3775
4212
|
let outBytes = bytes;
|
|
3776
4213
|
let outMime = mime;
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4214
|
+
let rasterizedFrom;
|
|
4215
|
+
if (kind === "image") {
|
|
4216
|
+
const normalized = await toModelSafeImage(bytes);
|
|
4217
|
+
outBytes = normalized.bytes;
|
|
4218
|
+
outMime = normalized.mime;
|
|
4219
|
+
rasterizedFrom = normalized.rasterizedFrom;
|
|
4220
|
+
if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
|
|
3781
4221
|
}
|
|
3782
4222
|
const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
|
|
3783
4223
|
const ref = await uploadAndIngest({
|
|
3784
4224
|
bytes: outBytes,
|
|
3785
4225
|
kind: params.expect,
|
|
3786
4226
|
mime: outMime,
|
|
3787
|
-
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
|
|
4227
|
+
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
|
|
3788
4228
|
ctx
|
|
3789
4229
|
});
|
|
3790
4230
|
return withProbedDuration(ref, durationMs);
|
|
@@ -3802,7 +4242,7 @@ function localFileMetadata(args) {
|
|
|
3802
4242
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3803
4243
|
file_size: args.fileSize,
|
|
3804
4244
|
original_filename: path3.basename(args.absPath),
|
|
3805
|
-
...args.
|
|
4245
|
+
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
3806
4246
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
3807
4247
|
};
|
|
3808
4248
|
}
|
|
@@ -4329,19 +4769,56 @@ var audioTimelineNode = defineNode({
|
|
|
4329
4769
|
}
|
|
4330
4770
|
});
|
|
4331
4771
|
|
|
4332
|
-
// src/engine/nodes/local/
|
|
4772
|
+
// src/engine/nodes/local/collect.ts
|
|
4333
4773
|
import { z as z7 } from "zod";
|
|
4774
|
+
var collectNode = defineNode({
|
|
4775
|
+
id: "collect",
|
|
4776
|
+
version: "1.0.0",
|
|
4777
|
+
category: "data",
|
|
4778
|
+
location: "local",
|
|
4779
|
+
passthroughRefs: true,
|
|
4780
|
+
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.",
|
|
4781
|
+
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.",
|
|
4782
|
+
inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
4783
|
+
params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
|
|
4784
|
+
outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
4785
|
+
outputKinds: { images: "image" },
|
|
4786
|
+
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
4787
|
+
// Arity is only knowable at validate time when `images` is a literal array;
|
|
4788
|
+
// a single `$ref:` string to an upstream array output defers to runtime.
|
|
4789
|
+
validateExtra: ({ rawParams, rawInputs }) => {
|
|
4790
|
+
const labels = rawParams?.labels;
|
|
4791
|
+
if (!Array.isArray(labels)) return [];
|
|
4792
|
+
if (new Set(labels).size !== labels.length) {
|
|
4793
|
+
return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
|
|
4794
|
+
}
|
|
4795
|
+
const images = rawInputs?.images;
|
|
4796
|
+
if (Array.isArray(images) && labels.length !== images.length) {
|
|
4797
|
+
return [
|
|
4798
|
+
{
|
|
4799
|
+
path: "params.labels",
|
|
4800
|
+
message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
|
|
4801
|
+
}
|
|
4802
|
+
];
|
|
4803
|
+
}
|
|
4804
|
+
return [];
|
|
4805
|
+
},
|
|
4806
|
+
execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
|
|
4807
|
+
});
|
|
4808
|
+
|
|
4809
|
+
// src/engine/nodes/local/ffmpeg.ts
|
|
4810
|
+
import { z as z8 } from "zod";
|
|
4334
4811
|
var FFMPEG_BIN2 = "ffmpeg";
|
|
4335
|
-
var OutputDecl =
|
|
4336
|
-
kind:
|
|
4337
|
-
ext:
|
|
4812
|
+
var OutputDecl = z8.object({
|
|
4813
|
+
kind: z8.enum(["image", "video", "audio"]),
|
|
4814
|
+
ext: z8.string().min(1).max(8)
|
|
4338
4815
|
}).strict();
|
|
4339
|
-
var FfmpegParams =
|
|
4340
|
-
args:
|
|
4341
|
-
outputs:
|
|
4816
|
+
var FfmpegParams = z8.object({
|
|
4817
|
+
args: z8.array(z8.string()).min(1),
|
|
4818
|
+
outputs: z8.record(z8.string(), OutputDecl).default({})
|
|
4342
4819
|
}).strict();
|
|
4343
|
-
var FfmpegInputs =
|
|
4344
|
-
var FfmpegOutputs =
|
|
4820
|
+
var FfmpegInputs = z8.record(z8.string(), z8.unknown());
|
|
4821
|
+
var FfmpegOutputs = z8.record(z8.string(), z8.custom());
|
|
4345
4822
|
var ffmpegNode = defineNode({
|
|
4346
4823
|
id: "ffmpeg",
|
|
4347
4824
|
version: "2.0.0",
|
|
@@ -4372,7 +4849,7 @@ import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/prom
|
|
|
4372
4849
|
import { createRequire } from "module";
|
|
4373
4850
|
import { tmpdir as tmpdir3 } from "os";
|
|
4374
4851
|
import path6 from "path";
|
|
4375
|
-
import { z as
|
|
4852
|
+
import { z as z9 } from "zod";
|
|
4376
4853
|
|
|
4377
4854
|
// src/engine/nodes/local/lib/assets.ts
|
|
4378
4855
|
import { copyFile as copyFile3, readFile as readFile4 } from "fs/promises";
|
|
@@ -4394,7 +4871,7 @@ async function refToUrl(ref) {
|
|
|
4394
4871
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4395
4872
|
}
|
|
4396
4873
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
4397
|
-
function
|
|
4874
|
+
function isAssetRefLike2(value) {
|
|
4398
4875
|
if (!value || typeof value !== "object") return false;
|
|
4399
4876
|
const v = value;
|
|
4400
4877
|
return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
|
|
@@ -4408,15 +4885,15 @@ var DEFAULT_SPECIMEN = [
|
|
|
4408
4885
|
"abcdefghijklmnopqrstuvwxyz",
|
|
4409
4886
|
`0123456789 !?&@#$%().,:;'"-`
|
|
4410
4887
|
].join("\n");
|
|
4411
|
-
var FontSpecimenParams =
|
|
4412
|
-
text:
|
|
4413
|
-
font_size:
|
|
4414
|
-
padding:
|
|
4415
|
-
line_height:
|
|
4416
|
-
max_width:
|
|
4888
|
+
var FontSpecimenParams = z9.object({
|
|
4889
|
+
text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
|
|
4890
|
+
font_size: z9.number().int().min(8).max(512).optional().default(72),
|
|
4891
|
+
padding: z9.number().int().min(0).max(512).optional().default(64),
|
|
4892
|
+
line_height: z9.number().min(0.8).max(3).optional().default(1.35),
|
|
4893
|
+
max_width: z9.number().int().min(256).max(4096).optional()
|
|
4417
4894
|
}).strict();
|
|
4418
|
-
var FontSpecimenInputs =
|
|
4419
|
-
var FontSpecimenOutputs =
|
|
4895
|
+
var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
|
|
4896
|
+
var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
|
|
4420
4897
|
var DEVICE_SCALE_FACTOR = 2;
|
|
4421
4898
|
var PAGE_TIMEOUT_MS = 3e4;
|
|
4422
4899
|
function escapeHtml(text) {
|
|
@@ -4557,7 +5034,7 @@ import { createRequire as createRequire2 } from "module";
|
|
|
4557
5034
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
4558
5035
|
import path11 from "path";
|
|
4559
5036
|
import { promisify as promisify4 } from "util";
|
|
4560
|
-
import { z as
|
|
5037
|
+
import { z as z11 } from "zod";
|
|
4561
5038
|
|
|
4562
5039
|
// src/engine/engine/composition-hash.ts
|
|
4563
5040
|
import { readdir as readdir2, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
@@ -4595,62 +5072,62 @@ async function collectFiles(root, current) {
|
|
|
4595
5072
|
// src/engine/engine/composition-meta.ts
|
|
4596
5073
|
import { readFile as readFile6 } from "fs/promises";
|
|
4597
5074
|
import path8 from "path";
|
|
4598
|
-
import { z as
|
|
4599
|
-
var InputKind =
|
|
4600
|
-
var InputSpec =
|
|
5075
|
+
import { z as z10 } from "zod";
|
|
5076
|
+
var InputKind = z10.enum(["video", "image", "audio", "json"]);
|
|
5077
|
+
var InputSpec = z10.object({
|
|
4601
5078
|
kind: InputKind,
|
|
4602
|
-
required:
|
|
5079
|
+
required: z10.boolean().optional().default(false),
|
|
4603
5080
|
// Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
|
|
4604
5081
|
// Defaults to `<key><ext>` derived from the kind.
|
|
4605
|
-
staged_as:
|
|
4606
|
-
description:
|
|
5082
|
+
staged_as: z10.string().min(1).optional(),
|
|
5083
|
+
description: z10.string().optional()
|
|
4607
5084
|
}).strict();
|
|
4608
5085
|
var ParamSpecBase = {
|
|
4609
|
-
required:
|
|
4610
|
-
description:
|
|
5086
|
+
required: z10.boolean().optional().default(false),
|
|
5087
|
+
description: z10.string().optional()
|
|
4611
5088
|
};
|
|
4612
|
-
var StringParam =
|
|
5089
|
+
var StringParam = z10.object({
|
|
4613
5090
|
...ParamSpecBase,
|
|
4614
|
-
kind:
|
|
4615
|
-
default:
|
|
4616
|
-
enum:
|
|
5091
|
+
kind: z10.literal("string"),
|
|
5092
|
+
default: z10.string().optional(),
|
|
5093
|
+
enum: z10.array(z10.string()).optional()
|
|
4617
5094
|
}).strict();
|
|
4618
|
-
var IntegerParam =
|
|
5095
|
+
var IntegerParam = z10.object({
|
|
4619
5096
|
...ParamSpecBase,
|
|
4620
|
-
kind:
|
|
4621
|
-
default:
|
|
4622
|
-
min:
|
|
4623
|
-
max:
|
|
5097
|
+
kind: z10.literal("integer"),
|
|
5098
|
+
default: z10.number().int().optional(),
|
|
5099
|
+
min: z10.number().int().optional(),
|
|
5100
|
+
max: z10.number().int().optional()
|
|
4624
5101
|
}).strict();
|
|
4625
|
-
var NumberParam =
|
|
5102
|
+
var NumberParam = z10.object({
|
|
4626
5103
|
...ParamSpecBase,
|
|
4627
|
-
kind:
|
|
4628
|
-
default:
|
|
4629
|
-
min:
|
|
4630
|
-
max:
|
|
5104
|
+
kind: z10.literal("number"),
|
|
5105
|
+
default: z10.number().optional(),
|
|
5106
|
+
min: z10.number().optional(),
|
|
5107
|
+
max: z10.number().optional()
|
|
4631
5108
|
}).strict();
|
|
4632
|
-
var BooleanParam =
|
|
5109
|
+
var BooleanParam = z10.object({
|
|
4633
5110
|
...ParamSpecBase,
|
|
4634
|
-
kind:
|
|
4635
|
-
default:
|
|
5111
|
+
kind: z10.literal("boolean"),
|
|
5112
|
+
default: z10.boolean().optional()
|
|
4636
5113
|
}).strict();
|
|
4637
|
-
var ColorParam =
|
|
5114
|
+
var ColorParam = z10.object({
|
|
4638
5115
|
...ParamSpecBase,
|
|
4639
|
-
kind:
|
|
4640
|
-
default:
|
|
5116
|
+
kind: z10.literal("color"),
|
|
5117
|
+
default: z10.string().optional()
|
|
4641
5118
|
}).strict();
|
|
4642
|
-
var ImageParam =
|
|
5119
|
+
var ImageParam = z10.object({
|
|
4643
5120
|
...ParamSpecBase,
|
|
4644
|
-
kind:
|
|
4645
|
-
default:
|
|
5121
|
+
kind: z10.literal("image"),
|
|
5122
|
+
default: z10.string().optional()
|
|
4646
5123
|
}).strict();
|
|
4647
|
-
var JsonParam =
|
|
5124
|
+
var JsonParam = z10.object({
|
|
4648
5125
|
...ParamSpecBase,
|
|
4649
|
-
kind:
|
|
4650
|
-
schema:
|
|
4651
|
-
default:
|
|
5126
|
+
kind: z10.literal("json"),
|
|
5127
|
+
schema: z10.unknown().optional(),
|
|
5128
|
+
default: z10.unknown().optional()
|
|
4652
5129
|
}).strict();
|
|
4653
|
-
var ParamSpec =
|
|
5130
|
+
var ParamSpec = z10.discriminatedUnion("kind", [
|
|
4654
5131
|
StringParam,
|
|
4655
5132
|
IntegerParam,
|
|
4656
5133
|
NumberParam,
|
|
@@ -4659,16 +5136,16 @@ var ParamSpec = z9.discriminatedUnion("kind", [
|
|
|
4659
5136
|
ImageParam,
|
|
4660
5137
|
JsonParam
|
|
4661
5138
|
]);
|
|
4662
|
-
var CompositionMetaSchema =
|
|
4663
|
-
id:
|
|
4664
|
-
title:
|
|
4665
|
-
description:
|
|
4666
|
-
width:
|
|
4667
|
-
height:
|
|
4668
|
-
fps:
|
|
4669
|
-
default_duration:
|
|
4670
|
-
inputs:
|
|
4671
|
-
params:
|
|
5139
|
+
var CompositionMetaSchema = z10.object({
|
|
5140
|
+
id: z10.string().min(1),
|
|
5141
|
+
title: z10.string().min(1),
|
|
5142
|
+
description: z10.string().optional(),
|
|
5143
|
+
width: z10.number().int().positive(),
|
|
5144
|
+
height: z10.number().int().positive(),
|
|
5145
|
+
fps: z10.number().int().positive().default(30),
|
|
5146
|
+
default_duration: z10.number().positive().default(10),
|
|
5147
|
+
inputs: z10.record(z10.string(), InputSpec).default({}),
|
|
5148
|
+
params: z10.record(z10.string(), ParamSpec).default({})
|
|
4672
5149
|
}).strict();
|
|
4673
5150
|
async function loadCompositionMeta(compositionDir) {
|
|
4674
5151
|
const metaPath = path8.join(compositionDir, "meta.json");
|
|
@@ -4696,39 +5173,39 @@ function buildParamsSchema(meta) {
|
|
|
4696
5173
|
for (const [name, spec] of Object.entries(meta.params)) {
|
|
4697
5174
|
shape[name] = buildParamFieldSchema(name, spec);
|
|
4698
5175
|
}
|
|
4699
|
-
return
|
|
5176
|
+
return z10.object(shape).strict();
|
|
4700
5177
|
}
|
|
4701
5178
|
function buildParamFieldSchema(name, spec) {
|
|
4702
5179
|
switch (spec.kind) {
|
|
4703
5180
|
case "string": {
|
|
4704
|
-
const s = spec.enum && spec.enum.length > 0 ?
|
|
5181
|
+
const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
|
|
4705
5182
|
return finalize(s, spec.default, spec.required);
|
|
4706
5183
|
}
|
|
4707
5184
|
case "integer": {
|
|
4708
|
-
let s =
|
|
5185
|
+
let s = z10.number().int();
|
|
4709
5186
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
4710
5187
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
4711
5188
|
return finalize(s, spec.default, spec.required);
|
|
4712
5189
|
}
|
|
4713
5190
|
case "number": {
|
|
4714
|
-
let s =
|
|
5191
|
+
let s = z10.number();
|
|
4715
5192
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
4716
5193
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
4717
5194
|
return finalize(s, spec.default, spec.required);
|
|
4718
5195
|
}
|
|
4719
5196
|
case "boolean":
|
|
4720
|
-
return finalize(
|
|
5197
|
+
return finalize(z10.boolean(), spec.default, spec.required);
|
|
4721
5198
|
case "color": {
|
|
4722
|
-
const s =
|
|
5199
|
+
const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
4723
5200
|
message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
|
|
4724
5201
|
});
|
|
4725
5202
|
return finalize(s, spec.default, spec.required);
|
|
4726
5203
|
}
|
|
4727
5204
|
case "image":
|
|
4728
|
-
return finalize(
|
|
5205
|
+
return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
|
|
4729
5206
|
case "json":
|
|
4730
5207
|
return finalize(
|
|
4731
|
-
|
|
5208
|
+
z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
|
|
4732
5209
|
spec.default,
|
|
4733
5210
|
spec.required
|
|
4734
5211
|
);
|
|
@@ -4766,8 +5243,8 @@ var NEVER_BLOCK = [
|
|
|
4766
5243
|
/text[_-]?occluded/i
|
|
4767
5244
|
];
|
|
4768
5245
|
var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
|
|
4769
|
-
function isAdvisory(code,
|
|
4770
|
-
const hay = `${code} ${
|
|
5246
|
+
function isAdvisory(code, message2) {
|
|
5247
|
+
const hay = `${code} ${message2}`;
|
|
4771
5248
|
return NEVER_BLOCK.some((re) => re.test(hay));
|
|
4772
5249
|
}
|
|
4773
5250
|
function parseCheckJson(raw) {
|
|
@@ -4795,10 +5272,10 @@ function classifyLint(json) {
|
|
|
4795
5272
|
for (const f of findings) {
|
|
4796
5273
|
const rec = f;
|
|
4797
5274
|
const code = String(rec?.code ?? "");
|
|
4798
|
-
const
|
|
5275
|
+
const message2 = String(rec?.message ?? "");
|
|
4799
5276
|
const severity = String(rec?.severity ?? "info");
|
|
4800
|
-
const blocking = severity === "error" && !isAdvisory(code,
|
|
4801
|
-
out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
|
|
5277
|
+
const blocking = severity === "error" && !isAdvisory(code, message2);
|
|
5278
|
+
out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
|
|
4802
5279
|
}
|
|
4803
5280
|
return out;
|
|
4804
5281
|
}
|
|
@@ -4810,9 +5287,9 @@ function classifyInspect(json) {
|
|
|
4810
5287
|
for (const iss of issues) {
|
|
4811
5288
|
const rec = iss;
|
|
4812
5289
|
const code = String(rec?.code ?? rec?.type ?? "overflow");
|
|
4813
|
-
const
|
|
5290
|
+
const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
|
|
4814
5291
|
const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
|
|
4815
|
-
out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
|
|
5292
|
+
out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
|
|
4816
5293
|
}
|
|
4817
5294
|
return out;
|
|
4818
5295
|
}
|
|
@@ -5028,17 +5505,17 @@ function literalize(value) {
|
|
|
5028
5505
|
// src/engine/nodes/local/hyperframe.ts
|
|
5029
5506
|
var execFileAsync2 = promisify4(execFile4);
|
|
5030
5507
|
var require_2 = createRequire2(import.meta.url);
|
|
5031
|
-
var HyperframeParams =
|
|
5032
|
-
composition:
|
|
5508
|
+
var HyperframeParams = z11.object({
|
|
5509
|
+
composition: z11.string().min(1),
|
|
5033
5510
|
// Output container. mp4 (default) for delivery; webm/mov render WITH
|
|
5034
5511
|
// transparency (alpha) when the composition background is transparent —
|
|
5035
5512
|
// use for motion-graphic overlays dropped into Premiere/AE/Nuke.
|
|
5036
|
-
format:
|
|
5037
|
-
timeout_ms:
|
|
5038
|
-
}).catchall(
|
|
5039
|
-
var HyperframeInputs =
|
|
5040
|
-
var HyperframeOutputs =
|
|
5041
|
-
video:
|
|
5513
|
+
format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
|
|
5514
|
+
timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
|
|
5515
|
+
}).catchall(z11.unknown());
|
|
5516
|
+
var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
|
|
5517
|
+
var HyperframeOutputs = z11.object({
|
|
5518
|
+
video: z11.custom()
|
|
5042
5519
|
}).strict();
|
|
5043
5520
|
var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
|
|
5044
5521
|
var MIME_BY_FORMAT = {
|
|
@@ -5257,7 +5734,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
|
|
|
5257
5734
|
}
|
|
5258
5735
|
function coerceImageParam(value) {
|
|
5259
5736
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5260
|
-
if (
|
|
5737
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5261
5738
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5262
5739
|
}
|
|
5263
5740
|
async function substituteCompositionFiles(tmp, values) {
|
|
@@ -5327,23 +5804,23 @@ import { createRequire as createRequire3 } from "module";
|
|
|
5327
5804
|
import { tmpdir as tmpdir5 } from "os";
|
|
5328
5805
|
import path12 from "path";
|
|
5329
5806
|
import { promisify as promisify5 } from "util";
|
|
5330
|
-
import { z as
|
|
5807
|
+
import { z as z12 } from "zod";
|
|
5331
5808
|
var _execFileAsync = promisify5(execFile5);
|
|
5332
5809
|
var require_3 = createRequire3(import.meta.url);
|
|
5333
|
-
var WaitForSpec =
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5810
|
+
var WaitForSpec = z12.discriminatedUnion("kind", [
|
|
5811
|
+
z12.object({ kind: z12.literal("auto") }),
|
|
5812
|
+
z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
|
|
5813
|
+
z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
|
|
5814
|
+
z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
|
|
5338
5815
|
]);
|
|
5339
|
-
var HyperframeSnapshotParams =
|
|
5340
|
-
composition:
|
|
5816
|
+
var HyperframeSnapshotParams = z12.object({
|
|
5817
|
+
composition: z12.string().min(1),
|
|
5341
5818
|
wait_for: WaitForSpec.optional().default({ kind: "auto" }),
|
|
5342
|
-
timeout_ms:
|
|
5343
|
-
}).catchall(
|
|
5344
|
-
var HyperframeSnapshotInputs =
|
|
5345
|
-
var HyperframeSnapshotOutputs =
|
|
5346
|
-
image:
|
|
5819
|
+
timeout_ms: z12.number().int().positive().optional().default(6e4)
|
|
5820
|
+
}).catchall(z12.unknown());
|
|
5821
|
+
var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
|
|
5822
|
+
var HyperframeSnapshotOutputs = z12.object({
|
|
5823
|
+
image: z12.custom()
|
|
5347
5824
|
}).strict();
|
|
5348
5825
|
var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
|
|
5349
5826
|
var DEVICE_SCALE_FACTOR2 = 2;
|
|
@@ -5487,7 +5964,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
|
|
|
5487
5964
|
}
|
|
5488
5965
|
function coerceImageParam2(value) {
|
|
5489
5966
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5490
|
-
if (
|
|
5967
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5491
5968
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5492
5969
|
}
|
|
5493
5970
|
async function substituteCompositionFiles2(tmp, values) {
|
|
@@ -5535,18 +6012,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
|
|
|
5535
6012
|
// src/engine/nodes/local/imagemagick.ts
|
|
5536
6013
|
import { execFile as execFile6 } from "child_process";
|
|
5537
6014
|
import { promisify as promisify6 } from "util";
|
|
5538
|
-
import { z as
|
|
6015
|
+
import { z as z13 } from "zod";
|
|
5539
6016
|
var execFileAsync3 = promisify6(execFile6);
|
|
5540
|
-
var OutputDecl2 =
|
|
5541
|
-
kind:
|
|
5542
|
-
ext:
|
|
6017
|
+
var OutputDecl2 = z13.object({
|
|
6018
|
+
kind: z13.enum(["image", "video", "audio"]),
|
|
6019
|
+
ext: z13.string().min(1).max(8)
|
|
5543
6020
|
}).strict();
|
|
5544
|
-
var ImageMagickParams =
|
|
5545
|
-
args:
|
|
5546
|
-
outputs:
|
|
6021
|
+
var ImageMagickParams = z13.object({
|
|
6022
|
+
args: z13.array(z13.string()).min(1),
|
|
6023
|
+
outputs: z13.record(z13.string(), OutputDecl2).default({})
|
|
5547
6024
|
}).strict();
|
|
5548
|
-
var ImageMagickInputs =
|
|
5549
|
-
var ImageMagickOutputs =
|
|
6025
|
+
var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
|
|
6026
|
+
var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
|
|
5550
6027
|
var resolvedBin;
|
|
5551
6028
|
async function resolveBin() {
|
|
5552
6029
|
if (resolvedBin) return resolvedBin;
|
|
@@ -5588,29 +6065,29 @@ var imagemagickNode = defineNode({
|
|
|
5588
6065
|
});
|
|
5589
6066
|
|
|
5590
6067
|
// src/engine/nodes/local/text.ts
|
|
5591
|
-
import { z as
|
|
6068
|
+
import { z as z14 } from "zod";
|
|
5592
6069
|
var textNode = defineNode({
|
|
5593
6070
|
id: "text",
|
|
5594
6071
|
version: "1.0.0",
|
|
5595
6072
|
category: "data",
|
|
5596
6073
|
location: "local",
|
|
5597
6074
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
5598
|
-
inputs:
|
|
5599
|
-
params:
|
|
5600
|
-
outputs:
|
|
6075
|
+
inputs: z14.object({}).strict(),
|
|
6076
|
+
params: z14.object({ value: z14.string() }).strict(),
|
|
6077
|
+
outputs: z14.object({ text: z14.string() }).strict(),
|
|
5601
6078
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5602
6079
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
5603
6080
|
});
|
|
5604
6081
|
|
|
5605
6082
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
5606
|
-
import { z as
|
|
5607
|
-
var AudioVoiceConvertParams =
|
|
5608
|
-
model:
|
|
6083
|
+
import { z as z15 } from "zod";
|
|
6084
|
+
var AudioVoiceConvertParams = z15.object({
|
|
6085
|
+
model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
5609
6086
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
5610
|
-
voice:
|
|
5611
|
-
output_format:
|
|
6087
|
+
voice: z15.string().min(1),
|
|
6088
|
+
output_format: z15.string().optional(),
|
|
5612
6089
|
/** Strip the source clip's background noise before re-voicing. */
|
|
5613
|
-
remove_background_noise:
|
|
6090
|
+
remove_background_noise: z15.boolean().optional()
|
|
5614
6091
|
}).strict();
|
|
5615
6092
|
var audioVoiceConvertNode = delegated({
|
|
5616
6093
|
id: "audio_voice_convert",
|
|
@@ -5618,44 +6095,44 @@ var audioVoiceConvertNode = delegated({
|
|
|
5618
6095
|
category: "audio",
|
|
5619
6096
|
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.",
|
|
5620
6097
|
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}}"`.',
|
|
5621
|
-
inputs:
|
|
6098
|
+
inputs: z15.object({
|
|
5622
6099
|
audio: AudioRef,
|
|
5623
6100
|
voice_ref: TextRef.optional()
|
|
5624
6101
|
}).strict(),
|
|
5625
6102
|
params: AudioVoiceConvertParams,
|
|
5626
|
-
outputs:
|
|
6103
|
+
outputs: z15.object({ audio: AudioRef }).strict(),
|
|
5627
6104
|
outputKinds: { audio: "audio" },
|
|
5628
6105
|
cost: () => ({ credits: 1, seconds_estimate: 20 })
|
|
5629
6106
|
});
|
|
5630
6107
|
|
|
5631
6108
|
// src/engine/nodes/remote/dialogue.ts
|
|
5632
|
-
import { z as
|
|
5633
|
-
var DialogueInput =
|
|
5634
|
-
text:
|
|
5635
|
-
voice_id:
|
|
6109
|
+
import { z as z16 } from "zod";
|
|
6110
|
+
var DialogueInput = z16.object({
|
|
6111
|
+
text: z16.string().min(1),
|
|
6112
|
+
voice_id: z16.string().min(1)
|
|
5636
6113
|
});
|
|
5637
6114
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
5638
|
-
var DialogueParams =
|
|
5639
|
-
model:
|
|
6115
|
+
var DialogueParams = z16.object({
|
|
6116
|
+
model: z16.enum(DIALOGUE_MODELS),
|
|
5640
6117
|
/**
|
|
5641
6118
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
5642
6119
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
5643
6120
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
5644
6121
|
*/
|
|
5645
|
-
inputs:
|
|
5646
|
-
language_code:
|
|
6122
|
+
inputs: z16.array(DialogueInput).min(1).max(50),
|
|
6123
|
+
language_code: z16.string().optional(),
|
|
5647
6124
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
5648
|
-
settings:
|
|
5649
|
-
seed:
|
|
5650
|
-
apply_text_normalization:
|
|
6125
|
+
settings: z16.record(z16.string(), z16.unknown()).optional(),
|
|
6126
|
+
seed: z16.number().int().min(0).max(4294967295).optional(),
|
|
6127
|
+
apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
|
|
5651
6128
|
/**
|
|
5652
6129
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
5653
6130
|
* separate `timestamps` output — character-level alignment plus
|
|
5654
6131
|
* per-voice segment markers usable for captions, lipsync, or
|
|
5655
6132
|
* beat-matched cuts in ad creatives.
|
|
5656
6133
|
*/
|
|
5657
|
-
with_timestamps:
|
|
5658
|
-
output_format:
|
|
6134
|
+
with_timestamps: z16.boolean().optional(),
|
|
6135
|
+
output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5659
6136
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
5660
6137
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
5661
6138
|
path: ["inputs"]
|
|
@@ -5666,9 +6143,9 @@ var dialogueNode = delegated({
|
|
|
5666
6143
|
category: "audio",
|
|
5667
6144
|
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.",
|
|
5668
6145
|
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.",
|
|
5669
|
-
inputs:
|
|
6146
|
+
inputs: z16.object({}).loose(),
|
|
5670
6147
|
params: DialogueParams,
|
|
5671
|
-
outputs:
|
|
6148
|
+
outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
5672
6149
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
5673
6150
|
cost: ({ params }) => {
|
|
5674
6151
|
const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
|
|
@@ -5677,7 +6154,7 @@ var dialogueNode = delegated({
|
|
|
5677
6154
|
});
|
|
5678
6155
|
|
|
5679
6156
|
// src/engine/nodes/remote/image.ts
|
|
5680
|
-
import { z as
|
|
6157
|
+
import { z as z17 } from "zod";
|
|
5681
6158
|
var IMAGE_GENERATE_MODELS2 = [
|
|
5682
6159
|
"openai/gpt-5.4-image-2",
|
|
5683
6160
|
"google/gemini-3.5-flash",
|
|
@@ -5685,16 +6162,16 @@ var IMAGE_GENERATE_MODELS2 = [
|
|
|
5685
6162
|
"google/gemini-3-pro-image-preview",
|
|
5686
6163
|
"recraft/recraft-v4.1-pro-vector"
|
|
5687
6164
|
];
|
|
5688
|
-
var ImageGenerateParams =
|
|
5689
|
-
model:
|
|
5690
|
-
prompt:
|
|
5691
|
-
aspect_ratio:
|
|
5692
|
-
image_size:
|
|
6165
|
+
var ImageGenerateParams = z17.object({
|
|
6166
|
+
model: z17.enum(IMAGE_GENERATE_MODELS2),
|
|
6167
|
+
prompt: z17.string().min(1),
|
|
6168
|
+
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(),
|
|
6169
|
+
image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
|
|
5693
6170
|
// Recraft v4 vector controls — forwarded into `image_config`. Registry
|
|
5694
6171
|
// rejects them on non-Recraft models.
|
|
5695
|
-
strength:
|
|
5696
|
-
rgb_colors:
|
|
5697
|
-
background_rgb_color:
|
|
6172
|
+
strength: z17.number().min(0).max(1).optional(),
|
|
6173
|
+
rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
|
|
6174
|
+
background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
|
|
5698
6175
|
}).strict();
|
|
5699
6176
|
var imageGenerateNode = delegated({
|
|
5700
6177
|
id: "image_generate",
|
|
@@ -5704,22 +6181,22 @@ var imageGenerateNode = delegated({
|
|
|
5704
6181
|
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.",
|
|
5705
6182
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
5706
6183
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
5707
|
-
inputs:
|
|
6184
|
+
inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
|
|
5708
6185
|
params: ImageGenerateParams,
|
|
5709
|
-
outputs:
|
|
6186
|
+
outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
|
|
5710
6187
|
outputKinds: { images: "image" },
|
|
5711
6188
|
cost: () => ({ credits: 5, seconds_estimate: 10 })
|
|
5712
6189
|
});
|
|
5713
6190
|
|
|
5714
6191
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
5715
|
-
import { z as
|
|
6192
|
+
import { z as z18 } from "zod";
|
|
5716
6193
|
var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
5717
6194
|
var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
5718
|
-
var ImageAspectAdaptParams =
|
|
5719
|
-
model:
|
|
5720
|
-
formats:
|
|
5721
|
-
guidance:
|
|
5722
|
-
image_size:
|
|
6195
|
+
var ImageAspectAdaptParams = z18.object({
|
|
6196
|
+
model: z18.enum(ASPECT_ADAPT_MODELS),
|
|
6197
|
+
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" }),
|
|
6198
|
+
guidance: z18.string().min(1).optional(),
|
|
6199
|
+
image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
5723
6200
|
}).strict();
|
|
5724
6201
|
var imageAspectAdaptNode = delegated({
|
|
5725
6202
|
id: "image_aspect_adapt",
|
|
@@ -5727,9 +6204,9 @@ var imageAspectAdaptNode = delegated({
|
|
|
5727
6204
|
category: "image",
|
|
5728
6205
|
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`.",
|
|
5729
6206
|
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.",
|
|
5730
|
-
inputs:
|
|
6207
|
+
inputs: z18.object({ source: ImageRef }).loose(),
|
|
5731
6208
|
params: ImageAspectAdaptParams,
|
|
5732
|
-
outputs:
|
|
6209
|
+
outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
|
|
5733
6210
|
outputKinds: { images: "image" },
|
|
5734
6211
|
cost: ({ params }) => {
|
|
5735
6212
|
const p = params;
|
|
@@ -5742,12 +6219,12 @@ var imageAspectAdaptNode = delegated({
|
|
|
5742
6219
|
});
|
|
5743
6220
|
|
|
5744
6221
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
5745
|
-
import { z as
|
|
5746
|
-
var ImageBackgroundRemoveParams =
|
|
5747
|
-
model:
|
|
5748
|
-
model_variant:
|
|
5749
|
-
operating_resolution:
|
|
5750
|
-
mask_only:
|
|
6222
|
+
import { z as z19 } from "zod";
|
|
6223
|
+
var ImageBackgroundRemoveParams = z19.object({
|
|
6224
|
+
model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
|
|
6225
|
+
model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
|
|
6226
|
+
operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
6227
|
+
mask_only: z19.boolean().optional().default(false)
|
|
5751
6228
|
}).strict();
|
|
5752
6229
|
var imageBackgroundRemoveNode = delegated({
|
|
5753
6230
|
id: "image_background_remove",
|
|
@@ -5755,11 +6232,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
5755
6232
|
category: "image",
|
|
5756
6233
|
summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
|
|
5757
6234
|
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.",
|
|
5758
|
-
inputs:
|
|
6235
|
+
inputs: z19.object({
|
|
5759
6236
|
image: ImageRef
|
|
5760
6237
|
}).strict(),
|
|
5761
6238
|
params: ImageBackgroundRemoveParams,
|
|
5762
|
-
outputs:
|
|
6239
|
+
outputs: z19.object({
|
|
5763
6240
|
image: ImageRef,
|
|
5764
6241
|
mask: ImageRef.optional()
|
|
5765
6242
|
}).strict(),
|
|
@@ -5768,7 +6245,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
5768
6245
|
});
|
|
5769
6246
|
|
|
5770
6247
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
5771
|
-
import { z as
|
|
6248
|
+
import { z as z20 } from "zod";
|
|
5772
6249
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
5773
6250
|
var imageDescribeNode = delegated({
|
|
5774
6251
|
id: "image_describe",
|
|
@@ -5776,33 +6253,33 @@ var imageDescribeNode = delegated({
|
|
|
5776
6253
|
category: "vision",
|
|
5777
6254
|
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.",
|
|
5778
6255
|
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.',
|
|
5779
|
-
inputs:
|
|
5780
|
-
params:
|
|
5781
|
-
model:
|
|
5782
|
-
focus:
|
|
5783
|
-
context:
|
|
5784
|
-
temperature:
|
|
5785
|
-
max_tokens:
|
|
6256
|
+
inputs: z20.object({ image: ImageRef }).loose(),
|
|
6257
|
+
params: z20.object({
|
|
6258
|
+
model: z20.enum(IMAGE_DESCRIBE_MODELS),
|
|
6259
|
+
focus: z20.string().optional(),
|
|
6260
|
+
context: z20.string().optional(),
|
|
6261
|
+
temperature: z20.number().min(0).max(2).optional(),
|
|
6262
|
+
max_tokens: z20.number().int().positive().optional()
|
|
5786
6263
|
}).strict(),
|
|
5787
|
-
outputs:
|
|
6264
|
+
outputs: z20.object({ description: JsonRef }).strict(),
|
|
5788
6265
|
outputKinds: { description: "json" },
|
|
5789
6266
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
5790
6267
|
});
|
|
5791
6268
|
|
|
5792
6269
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
5793
|
-
import { z as
|
|
6270
|
+
import { z as z21 } from "zod";
|
|
5794
6271
|
var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
5795
|
-
var ImageReferenceSheetParams =
|
|
5796
|
-
model:
|
|
5797
|
-
subject_description:
|
|
6272
|
+
var ImageReferenceSheetParams = z21.object({
|
|
6273
|
+
model: z21.enum(REFERENCE_SHEET_MODELS),
|
|
6274
|
+
subject_description: z21.string().min(1),
|
|
5798
6275
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
5799
6276
|
// so a multi-scene shoot keeps one consistent set.
|
|
5800
|
-
subject_type:
|
|
5801
|
-
views:
|
|
5802
|
-
style:
|
|
5803
|
-
prompt_override:
|
|
5804
|
-
aspect_ratio:
|
|
5805
|
-
image_size:
|
|
6277
|
+
subject_type: z21.enum(["character", "person", "product", "location"]),
|
|
6278
|
+
views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
|
|
6279
|
+
style: z21.string().optional(),
|
|
6280
|
+
prompt_override: z21.string().min(1).optional(),
|
|
6281
|
+
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(),
|
|
6282
|
+
image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
5806
6283
|
}).strict();
|
|
5807
6284
|
var imageReferenceSheetNode = delegated({
|
|
5808
6285
|
id: "image_reference_sheet",
|
|
@@ -5810,9 +6287,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
5810
6287
|
category: "image",
|
|
5811
6288
|
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).",
|
|
5812
6289
|
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.",
|
|
5813
|
-
inputs:
|
|
6290
|
+
inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
|
|
5814
6291
|
params: ImageReferenceSheetParams,
|
|
5815
|
-
outputs:
|
|
6292
|
+
outputs: z21.object({ sheet: ImageRef }).strict(),
|
|
5816
6293
|
outputKinds: { sheet: "image" },
|
|
5817
6294
|
cost: ({ params }) => ({
|
|
5818
6295
|
credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
|
|
@@ -5821,10 +6298,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
5821
6298
|
});
|
|
5822
6299
|
|
|
5823
6300
|
// src/engine/nodes/remote/imageSearch.ts
|
|
5824
|
-
import { z as
|
|
5825
|
-
var ImageSearchParams =
|
|
5826
|
-
prompt:
|
|
5827
|
-
count:
|
|
6301
|
+
import { z as z22 } from "zod";
|
|
6302
|
+
var ImageSearchParams = z22.object({
|
|
6303
|
+
prompt: z22.string().min(1),
|
|
6304
|
+
count: z22.number().int().min(1).max(20).default(5)
|
|
5828
6305
|
}).strict();
|
|
5829
6306
|
var imageSearchNode = delegated({
|
|
5830
6307
|
id: "image_search",
|
|
@@ -5832,15 +6309,15 @@ var imageSearchNode = delegated({
|
|
|
5832
6309
|
category: "image",
|
|
5833
6310
|
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.",
|
|
5834
6311
|
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.",
|
|
5835
|
-
inputs:
|
|
6312
|
+
inputs: z22.object({}).loose(),
|
|
5836
6313
|
params: ImageSearchParams,
|
|
5837
|
-
outputs:
|
|
6314
|
+
outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
|
|
5838
6315
|
outputKinds: { images: "image" },
|
|
5839
6316
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
5840
6317
|
});
|
|
5841
6318
|
|
|
5842
6319
|
// src/engine/nodes/remote/imageSelect.ts
|
|
5843
|
-
import { z as
|
|
6320
|
+
import { z as z23 } from "zod";
|
|
5844
6321
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
5845
6322
|
var imageSelectNode = delegated({
|
|
5846
6323
|
id: "image_select",
|
|
@@ -5848,15 +6325,15 @@ var imageSelectNode = delegated({
|
|
|
5848
6325
|
category: "vision",
|
|
5849
6326
|
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.",
|
|
5850
6327
|
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.",
|
|
5851
|
-
inputs:
|
|
5852
|
-
params:
|
|
5853
|
-
model:
|
|
5854
|
-
prompt:
|
|
5855
|
-
count:
|
|
5856
|
-
temperature:
|
|
5857
|
-
max_tokens:
|
|
6328
|
+
inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
|
|
6329
|
+
params: z23.object({
|
|
6330
|
+
model: z23.enum(IMAGE_SELECT_MODELS),
|
|
6331
|
+
prompt: z23.string().min(1),
|
|
6332
|
+
count: z23.number().int().min(1).default(1),
|
|
6333
|
+
temperature: z23.number().min(0).max(2).optional(),
|
|
6334
|
+
max_tokens: z23.number().int().positive().optional()
|
|
5858
6335
|
}).strict(),
|
|
5859
|
-
outputs:
|
|
6336
|
+
outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
|
|
5860
6337
|
outputKinds: { images: "image", reasoning: "text" },
|
|
5861
6338
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
5862
6339
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -5881,34 +6358,34 @@ var imageSelectNode = delegated({
|
|
|
5881
6358
|
});
|
|
5882
6359
|
|
|
5883
6360
|
// src/engine/nodes/remote/music.ts
|
|
5884
|
-
import { z as
|
|
6361
|
+
import { z as z24 } from "zod";
|
|
5885
6362
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
5886
|
-
var MusicParams =
|
|
5887
|
-
model:
|
|
6363
|
+
var MusicParams = z24.object({
|
|
6364
|
+
model: z24.enum(MUSIC_MODELS),
|
|
5888
6365
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
5889
|
-
prompt:
|
|
6366
|
+
prompt: z24.string().optional(),
|
|
5890
6367
|
/**
|
|
5891
6368
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
5892
6369
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
5893
6370
|
*/
|
|
5894
|
-
composition_plan:
|
|
6371
|
+
composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
|
|
5895
6372
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
5896
|
-
music_length_ms:
|
|
5897
|
-
seed:
|
|
6373
|
+
music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
6374
|
+
seed: z24.number().int().optional(),
|
|
5898
6375
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
5899
|
-
force_instrumental:
|
|
6376
|
+
force_instrumental: z24.boolean().optional(),
|
|
5900
6377
|
/** composition_plan only — honor exact section durations. */
|
|
5901
|
-
respect_sections_durations:
|
|
6378
|
+
respect_sections_durations: z24.boolean().optional(),
|
|
5902
6379
|
/** Emit word-level timestamps alongside the audio. */
|
|
5903
|
-
with_timestamps:
|
|
6380
|
+
with_timestamps: z24.boolean().optional(),
|
|
5904
6381
|
/**
|
|
5905
6382
|
* video-to-music only — short description of the desired score
|
|
5906
6383
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
5907
6384
|
*/
|
|
5908
|
-
description:
|
|
6385
|
+
description: z24.string().max(1e3).optional(),
|
|
5909
6386
|
/** video-to-music only — up to 10 style tags. */
|
|
5910
|
-
tags:
|
|
5911
|
-
output_format:
|
|
6387
|
+
tags: z24.array(z24.string()).max(10).optional(),
|
|
6388
|
+
output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5912
6389
|
}).strict();
|
|
5913
6390
|
var musicNode = delegated({
|
|
5914
6391
|
id: "music",
|
|
@@ -5916,9 +6393,9 @@ var musicNode = delegated({
|
|
|
5916
6393
|
category: "audio",
|
|
5917
6394
|
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`.",
|
|
5918
6395
|
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.",
|
|
5919
|
-
inputs:
|
|
6396
|
+
inputs: z24.object({ video: VideoRef.optional() }).loose(),
|
|
5920
6397
|
params: MusicParams,
|
|
5921
|
-
outputs:
|
|
6398
|
+
outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
5922
6399
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
5923
6400
|
cost: ({ params }) => {
|
|
5924
6401
|
const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
|
|
@@ -5949,25 +6426,25 @@ var musicNode = delegated({
|
|
|
5949
6426
|
});
|
|
5950
6427
|
|
|
5951
6428
|
// src/engine/nodes/remote/soundEffect.ts
|
|
5952
|
-
import { z as
|
|
6429
|
+
import { z as z25 } from "zod";
|
|
5953
6430
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
5954
|
-
var SoundEffectParams =
|
|
5955
|
-
model:
|
|
6431
|
+
var SoundEffectParams = z25.object({
|
|
6432
|
+
model: z25.enum(SOUND_EFFECT_MODELS),
|
|
5956
6433
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
5957
|
-
text:
|
|
6434
|
+
text: z25.string().min(1),
|
|
5958
6435
|
/**
|
|
5959
6436
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
5960
6437
|
* natural length for the described effect.
|
|
5961
6438
|
*/
|
|
5962
|
-
duration_seconds:
|
|
6439
|
+
duration_seconds: z25.number().min(0.5).max(30).optional(),
|
|
5963
6440
|
/**
|
|
5964
6441
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
5965
6442
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
5966
6443
|
*/
|
|
5967
|
-
prompt_influence:
|
|
6444
|
+
prompt_influence: z25.number().min(0).max(1).optional(),
|
|
5968
6445
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
5969
|
-
loop:
|
|
5970
|
-
output_format:
|
|
6446
|
+
loop: z25.boolean().optional(),
|
|
6447
|
+
output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5971
6448
|
}).strict();
|
|
5972
6449
|
var soundEffectNode = delegated({
|
|
5973
6450
|
id: "sound_effect",
|
|
@@ -5975,9 +6452,9 @@ var soundEffectNode = delegated({
|
|
|
5975
6452
|
category: "audio",
|
|
5976
6453
|
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.",
|
|
5977
6454
|
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.",
|
|
5978
|
-
inputs:
|
|
6455
|
+
inputs: z25.object({}).loose(),
|
|
5979
6456
|
params: SoundEffectParams,
|
|
5980
|
-
outputs:
|
|
6457
|
+
outputs: z25.object({ audio: AudioRef }).strict(),
|
|
5981
6458
|
outputKinds: { audio: "audio" },
|
|
5982
6459
|
cost: ({ params }) => {
|
|
5983
6460
|
const seconds = params.duration_seconds ?? 5;
|
|
@@ -5986,7 +6463,7 @@ var soundEffectNode = delegated({
|
|
|
5986
6463
|
});
|
|
5987
6464
|
|
|
5988
6465
|
// src/engine/nodes/remote/textGenerate.ts
|
|
5989
|
-
import { z as
|
|
6466
|
+
import { z as z26 } from "zod";
|
|
5990
6467
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
5991
6468
|
var textGenerateNode = delegated({
|
|
5992
6469
|
id: "text_generate",
|
|
@@ -5994,58 +6471,58 @@ var textGenerateNode = delegated({
|
|
|
5994
6471
|
category: "language",
|
|
5995
6472
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
5996
6473
|
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.',
|
|
5997
|
-
inputs:
|
|
5998
|
-
params:
|
|
5999
|
-
model:
|
|
6000
|
-
prompt:
|
|
6001
|
-
system:
|
|
6002
|
-
response_format:
|
|
6003
|
-
web_search:
|
|
6004
|
-
temperature:
|
|
6005
|
-
max_tokens:
|
|
6474
|
+
inputs: z26.object({}).loose(),
|
|
6475
|
+
params: z26.object({
|
|
6476
|
+
model: z26.enum(TEXT_GENERATE_MODELS),
|
|
6477
|
+
prompt: z26.string().min(1),
|
|
6478
|
+
system: z26.string().optional(),
|
|
6479
|
+
response_format: z26.enum(["text", "json_object"]).optional(),
|
|
6480
|
+
web_search: z26.boolean().optional(),
|
|
6481
|
+
temperature: z26.number().min(0).max(2).optional(),
|
|
6482
|
+
max_tokens: z26.number().int().positive().optional()
|
|
6006
6483
|
}).strict(),
|
|
6007
|
-
outputs:
|
|
6484
|
+
outputs: z26.object({ text: TextRef }).strict(),
|
|
6008
6485
|
outputKinds: { text: "text" },
|
|
6009
6486
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
6010
6487
|
});
|
|
6011
6488
|
|
|
6012
6489
|
// src/engine/nodes/remote/tts.ts
|
|
6013
|
-
import { z as
|
|
6490
|
+
import { z as z27 } from "zod";
|
|
6014
6491
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
6015
|
-
var TtsVoiceSettings =
|
|
6016
|
-
stability:
|
|
6017
|
-
similarity_boost:
|
|
6018
|
-
style:
|
|
6019
|
-
use_speaker_boost:
|
|
6020
|
-
speed:
|
|
6492
|
+
var TtsVoiceSettings = z27.object({
|
|
6493
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
6494
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
6495
|
+
style: z27.number().min(0).max(1).optional(),
|
|
6496
|
+
use_speaker_boost: z27.boolean().optional(),
|
|
6497
|
+
speed: z27.number().min(0.25).max(4).optional()
|
|
6021
6498
|
}).strict();
|
|
6022
|
-
var TtsPronunciationLocator =
|
|
6023
|
-
pronunciation_dictionary_id:
|
|
6024
|
-
version_id:
|
|
6499
|
+
var TtsPronunciationLocator = z27.object({
|
|
6500
|
+
pronunciation_dictionary_id: z27.string().min(1),
|
|
6501
|
+
version_id: z27.string().nullable().optional()
|
|
6025
6502
|
}).strict();
|
|
6026
|
-
var TtsParams =
|
|
6027
|
-
model:
|
|
6028
|
-
text:
|
|
6029
|
-
voice:
|
|
6503
|
+
var TtsParams = z27.object({
|
|
6504
|
+
model: z27.enum(TTS_MODELS),
|
|
6505
|
+
text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
6506
|
+
voice: z27.string().min(1),
|
|
6030
6507
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
6031
|
-
output_format:
|
|
6032
|
-
seed:
|
|
6508
|
+
output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
6509
|
+
seed: z27.number().int().min(0).max(4294967295).optional(),
|
|
6033
6510
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
6034
|
-
stability:
|
|
6035
|
-
similarity_boost:
|
|
6511
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
6512
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
6036
6513
|
voice_settings: TtsVoiceSettings.optional(),
|
|
6037
6514
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
6038
|
-
language_code:
|
|
6039
|
-
pronunciation_dictionary_locators:
|
|
6040
|
-
apply_text_normalization:
|
|
6515
|
+
language_code: z27.string().optional(),
|
|
6516
|
+
pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
|
|
6517
|
+
apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
|
|
6041
6518
|
/** Currently Japanese-only. Adds latency. */
|
|
6042
|
-
apply_language_text_normalization:
|
|
6519
|
+
apply_language_text_normalization: z27.boolean().optional(),
|
|
6043
6520
|
/**
|
|
6044
6521
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
6045
6522
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
6046
6523
|
* rendering, lipsync, and beat-matched cuts.
|
|
6047
6524
|
*/
|
|
6048
|
-
with_timestamps:
|
|
6525
|
+
with_timestamps: z27.boolean().optional()
|
|
6049
6526
|
}).strict();
|
|
6050
6527
|
var ttsNode = delegated({
|
|
6051
6528
|
id: "tts",
|
|
@@ -6053,9 +6530,9 @@ var ttsNode = delegated({
|
|
|
6053
6530
|
category: "audio",
|
|
6054
6531
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
6055
6532
|
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).",
|
|
6056
|
-
inputs:
|
|
6533
|
+
inputs: z27.object({}).loose(),
|
|
6057
6534
|
params: TtsParams,
|
|
6058
|
-
outputs:
|
|
6535
|
+
outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6059
6536
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6060
6537
|
cost: ({ params }) => ({
|
|
6061
6538
|
credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
|
|
@@ -6064,23 +6541,23 @@ var ttsNode = delegated({
|
|
|
6064
6541
|
});
|
|
6065
6542
|
|
|
6066
6543
|
// src/engine/nodes/remote/video.ts
|
|
6067
|
-
import { z as
|
|
6544
|
+
import { z as z28 } from "zod";
|
|
6068
6545
|
var VIDEO_GENERATE_MODELS = ["bytedance/seedance-2.0", "google/veo-3.1-fast"];
|
|
6069
|
-
var VideoGenerateParams =
|
|
6070
|
-
model:
|
|
6071
|
-
prompt:
|
|
6072
|
-
duration:
|
|
6073
|
-
resolution:
|
|
6546
|
+
var VideoGenerateParams = z28.object({
|
|
6547
|
+
model: z28.enum(VIDEO_GENERATE_MODELS),
|
|
6548
|
+
prompt: z28.string().min(1),
|
|
6549
|
+
duration: z28.number().int().positive().optional(),
|
|
6550
|
+
resolution: z28.string().optional(),
|
|
6074
6551
|
// Union of ratios accepted by at least one curated model (registry gates
|
|
6075
6552
|
// per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
|
|
6076
|
-
aspect_ratio:
|
|
6077
|
-
generate_audio:
|
|
6078
|
-
seed:
|
|
6553
|
+
aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
|
|
6554
|
+
generate_audio: z28.boolean().optional(),
|
|
6555
|
+
seed: z28.number().int().nonnegative().optional(),
|
|
6079
6556
|
// Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
|
|
6080
|
-
negative_prompt:
|
|
6081
|
-
person_generation:
|
|
6082
|
-
enhance_prompt:
|
|
6083
|
-
conditioning_scale:
|
|
6557
|
+
negative_prompt: z28.string().optional(),
|
|
6558
|
+
person_generation: z28.string().optional(),
|
|
6559
|
+
enhance_prompt: z28.boolean().optional(),
|
|
6560
|
+
conditioning_scale: z28.number().optional()
|
|
6084
6561
|
}).strict();
|
|
6085
6562
|
var videoGenerateNode = delegated({
|
|
6086
6563
|
id: "video_generate",
|
|
@@ -6088,23 +6565,23 @@ var videoGenerateNode = delegated({
|
|
|
6088
6565
|
category: "video",
|
|
6089
6566
|
summary: "Generate video for ad creatives. Two curated models: `bytedance/seedance-2.0` (production quality, photorealistic humans via fal.ai) and `google/veo-3.1-fast` (cheap/fast for iteration and tests). Async with polling.",
|
|
6090
6567
|
when_to_use: "Use `bytedance/seedance-2.0` for final ad output (photoreal subjects, image-to-video with first/last frames). Use `google/veo-3.1-fast` while iterating to keep cost low. Each model has different supported durations, resolutions, and aspect ratios \u2014 see the README per-model section.",
|
|
6091
|
-
inputs:
|
|
6568
|
+
inputs: z28.object({
|
|
6092
6569
|
first_frame: ImageRef.optional(),
|
|
6093
6570
|
last_frame: ImageRef.optional(),
|
|
6094
6571
|
reference: ImageRef.optional()
|
|
6095
6572
|
}).loose(),
|
|
6096
6573
|
params: VideoGenerateParams,
|
|
6097
|
-
outputs:
|
|
6574
|
+
outputs: z28.object({ video: VideoRef }).strict(),
|
|
6098
6575
|
outputKinds: { video: "video" },
|
|
6099
6576
|
cost: () => ({ credits: 50, seconds_estimate: 120 })
|
|
6100
6577
|
});
|
|
6101
6578
|
|
|
6102
6579
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
6103
|
-
import { z as
|
|
6104
|
-
var VideoBackgroundRemoveParams =
|
|
6105
|
-
model:
|
|
6106
|
-
edge_refinement:
|
|
6107
|
-
output_codec:
|
|
6580
|
+
import { z as z29 } from "zod";
|
|
6581
|
+
var VideoBackgroundRemoveParams = z29.object({
|
|
6582
|
+
model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
|
|
6583
|
+
edge_refinement: z29.boolean().optional().default(true),
|
|
6584
|
+
output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
|
|
6108
6585
|
}).strict();
|
|
6109
6586
|
var videoBackgroundRemoveNode = delegated({
|
|
6110
6587
|
id: "video_background_remove",
|
|
@@ -6112,18 +6589,18 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
6112
6589
|
category: "video",
|
|
6113
6590
|
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`.",
|
|
6114
6591
|
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.",
|
|
6115
|
-
inputs:
|
|
6592
|
+
inputs: z29.object({
|
|
6116
6593
|
video: VideoRef
|
|
6117
6594
|
}).strict(),
|
|
6118
6595
|
params: VideoBackgroundRemoveParams,
|
|
6119
|
-
outputs:
|
|
6596
|
+
outputs: z29.object({ video: VideoRef }).strict(),
|
|
6120
6597
|
outputKinds: { video: "video" },
|
|
6121
6598
|
// $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
|
|
6122
6599
|
cost: () => ({ credits: 50, seconds_estimate: 60 })
|
|
6123
6600
|
});
|
|
6124
6601
|
|
|
6125
6602
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
6126
|
-
import { z as
|
|
6603
|
+
import { z as z30 } from "zod";
|
|
6127
6604
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6128
6605
|
var videoDeconstructNode = delegated({
|
|
6129
6606
|
id: "video_deconstruct",
|
|
@@ -6131,34 +6608,34 @@ var videoDeconstructNode = delegated({
|
|
|
6131
6608
|
category: "video",
|
|
6132
6609
|
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).',
|
|
6133
6610
|
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.',
|
|
6134
|
-
inputs:
|
|
6135
|
-
params:
|
|
6136
|
-
model:
|
|
6137
|
-
mode:
|
|
6138
|
-
language:
|
|
6139
|
-
max_scenes:
|
|
6140
|
-
focus:
|
|
6141
|
-
start_s:
|
|
6142
|
-
end_s:
|
|
6611
|
+
inputs: z30.object({ video: VideoRef }).loose(),
|
|
6612
|
+
params: z30.object({
|
|
6613
|
+
model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
6614
|
+
mode: z30.enum(["full", "index"]).optional(),
|
|
6615
|
+
language: z30.string().min(2).max(8).optional(),
|
|
6616
|
+
max_scenes: z30.number().int().min(1).max(60).optional(),
|
|
6617
|
+
focus: z30.string().optional(),
|
|
6618
|
+
start_s: z30.number().min(0).optional(),
|
|
6619
|
+
end_s: z30.number().positive().optional(),
|
|
6143
6620
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
6144
6621
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
6145
6622
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
6146
6623
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
6147
|
-
shot_cuts:
|
|
6624
|
+
shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
|
|
6148
6625
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
6149
6626
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
6150
6627
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
6151
6628
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
6152
|
-
max_clip_s:
|
|
6629
|
+
max_clip_s: z30.number().positive().max(60).optional(),
|
|
6153
6630
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
6154
6631
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
6155
|
-
transcriber:
|
|
6632
|
+
transcriber: z30.enum(["groq", "deepgram"]).optional()
|
|
6156
6633
|
}).strict(),
|
|
6157
|
-
outputs:
|
|
6634
|
+
outputs: z30.object({
|
|
6158
6635
|
analysis: JsonRef,
|
|
6159
6636
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
6160
|
-
start_frames:
|
|
6161
|
-
end_frames:
|
|
6637
|
+
start_frames: z30.array(ImageRef).min(1).optional(),
|
|
6638
|
+
end_frames: z30.array(ImageRef).min(1).optional(),
|
|
6162
6639
|
transcript: JsonRef
|
|
6163
6640
|
}).strict(),
|
|
6164
6641
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -6166,22 +6643,22 @@ var videoDeconstructNode = delegated({
|
|
|
6166
6643
|
});
|
|
6167
6644
|
|
|
6168
6645
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
6169
|
-
import { z as
|
|
6170
|
-
var FalLipsyncParams =
|
|
6171
|
-
model:
|
|
6646
|
+
import { z as z31 } from "zod";
|
|
6647
|
+
var FalLipsyncParams = z31.object({
|
|
6648
|
+
model: z31.literal("fal/veed-lipsync")
|
|
6172
6649
|
}).strict();
|
|
6173
|
-
var VideoLipsyncParams =
|
|
6650
|
+
var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
|
|
6174
6651
|
var videoLipsyncNode = delegated({
|
|
6175
6652
|
id: "video_lipsync",
|
|
6176
6653
|
version: "1.0.0",
|
|
6177
6654
|
category: "video",
|
|
6178
6655
|
summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
|
|
6179
|
-
inputs:
|
|
6656
|
+
inputs: z31.object({
|
|
6180
6657
|
video: VideoRef,
|
|
6181
6658
|
audio: AudioRef
|
|
6182
6659
|
}).strict(),
|
|
6183
6660
|
params: VideoLipsyncParams,
|
|
6184
|
-
outputs:
|
|
6661
|
+
outputs: z31.object({ video: VideoRef }).strict(),
|
|
6185
6662
|
outputKinds: { video: "video" },
|
|
6186
6663
|
cost: () => ({ credits: 20, seconds_estimate: 120 })
|
|
6187
6664
|
});
|
|
@@ -6190,7 +6667,7 @@ var videoLipsyncNode = delegated({
|
|
|
6190
6667
|
import { mkdtemp as mkdtemp6, readFile as readFile10, rm as rm6 } from "fs/promises";
|
|
6191
6668
|
import { tmpdir as tmpdir6 } from "os";
|
|
6192
6669
|
import path13 from "path";
|
|
6193
|
-
import { z as
|
|
6670
|
+
import { z as z32 } from "zod";
|
|
6194
6671
|
|
|
6195
6672
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
6196
6673
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -6269,21 +6746,21 @@ ${detail.slice(-4e3)}`);
|
|
|
6269
6746
|
}
|
|
6270
6747
|
|
|
6271
6748
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6272
|
-
var VideoTranscribeParams =
|
|
6273
|
-
language:
|
|
6749
|
+
var VideoTranscribeParams = z32.object({
|
|
6750
|
+
language: z32.string().min(2).max(8).optional(),
|
|
6274
6751
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
6275
6752
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
6276
6753
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
6277
|
-
transcriber:
|
|
6754
|
+
transcriber: z32.enum(["groq", "deepgram"]).optional()
|
|
6278
6755
|
}).strict();
|
|
6279
|
-
var VideoTranscribeInputs =
|
|
6756
|
+
var VideoTranscribeInputs = z32.object({
|
|
6280
6757
|
video: VideoRef
|
|
6281
6758
|
}).strict();
|
|
6282
|
-
var VideoTranscribeOutputs =
|
|
6283
|
-
transcript:
|
|
6759
|
+
var VideoTranscribeOutputs = z32.object({
|
|
6760
|
+
transcript: z32.custom(),
|
|
6284
6761
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
6285
6762
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
6286
|
-
rich:
|
|
6763
|
+
rich: z32.custom().optional()
|
|
6287
6764
|
}).strict();
|
|
6288
6765
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
6289
6766
|
var videoTranscribeNode = defineNode({
|
|
@@ -6368,29 +6845,29 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6368
6845
|
}
|
|
6369
6846
|
|
|
6370
6847
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
6371
|
-
import { z as
|
|
6848
|
+
import { z as z33 } from "zod";
|
|
6372
6849
|
var voiceSelectNode = delegated({
|
|
6373
6850
|
id: "voice_select",
|
|
6374
6851
|
version: "1.0.0",
|
|
6375
6852
|
category: "audio",
|
|
6376
6853
|
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.',
|
|
6377
6854
|
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.',
|
|
6378
|
-
inputs:
|
|
6379
|
-
params:
|
|
6380
|
-
description:
|
|
6381
|
-
gender:
|
|
6382
|
-
age:
|
|
6383
|
-
accent:
|
|
6384
|
-
language:
|
|
6385
|
-
limit:
|
|
6855
|
+
inputs: z33.object({}).loose(),
|
|
6856
|
+
params: z33.object({
|
|
6857
|
+
description: z33.string().min(1),
|
|
6858
|
+
gender: z33.string().optional(),
|
|
6859
|
+
age: z33.string().optional(),
|
|
6860
|
+
accent: z33.string().optional(),
|
|
6861
|
+
language: z33.string().optional(),
|
|
6862
|
+
limit: z33.number().int().min(1).max(20).optional()
|
|
6386
6863
|
}).strict(),
|
|
6387
|
-
outputs:
|
|
6864
|
+
outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
6388
6865
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
6389
6866
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
6390
6867
|
});
|
|
6391
6868
|
|
|
6392
6869
|
// src/engine/schema/catalog.ts
|
|
6393
|
-
import { z as
|
|
6870
|
+
import { z as z34 } from "zod";
|
|
6394
6871
|
function generateCatalog(registry, opts = {}) {
|
|
6395
6872
|
const entries = registry.all().map((def) => {
|
|
6396
6873
|
const cost = def.cost ? safeCost(def) : void 0;
|
|
@@ -6401,9 +6878,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
6401
6878
|
summary: def.summary,
|
|
6402
6879
|
when_to_use: def.when_to_use,
|
|
6403
6880
|
location: def.location,
|
|
6404
|
-
inputs:
|
|
6405
|
-
params:
|
|
6406
|
-
outputs:
|
|
6881
|
+
inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
6882
|
+
params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
6883
|
+
outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
6407
6884
|
cost_estimate_credits: cost?.credits,
|
|
6408
6885
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
6409
6886
|
};
|
|
@@ -6480,7 +6957,8 @@ var LOCAL_NODES = [
|
|
|
6480
6957
|
imagemagickNode,
|
|
6481
6958
|
videoTranscribeNode,
|
|
6482
6959
|
fontSpecimenNode,
|
|
6483
|
-
audioTimelineNode
|
|
6960
|
+
audioTimelineNode,
|
|
6961
|
+
collectNode
|
|
6484
6962
|
];
|
|
6485
6963
|
var REMOTE_NODES = [
|
|
6486
6964
|
textGenerateNode,
|
|
@@ -6513,17 +6991,31 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6513
6991
|
const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
|
|
6514
6992
|
const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
|
|
6515
6993
|
const creds = requireCredentialsFromEnv();
|
|
6994
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
6995
|
+
const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
|
|
6996
|
+
const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
|
|
6997
|
+
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
6998
|
+
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
6999
|
+
local: localCache,
|
|
7000
|
+
remote: new RemoteCacheStore(client, opts.log),
|
|
7001
|
+
assets,
|
|
7002
|
+
log: opts.log
|
|
7003
|
+
}) : localCache;
|
|
6516
7004
|
return new Engine({
|
|
6517
7005
|
registry: defaultRegistry(),
|
|
6518
|
-
client
|
|
6519
|
-
assets
|
|
6520
|
-
cache
|
|
7006
|
+
client,
|
|
7007
|
+
assets,
|
|
7008
|
+
cache,
|
|
6521
7009
|
outputsDir,
|
|
6522
|
-
log: opts.log
|
|
7010
|
+
log: opts.log,
|
|
7011
|
+
persistAssets: remoteCacheEnabled
|
|
6523
7012
|
});
|
|
6524
7013
|
}
|
|
6525
7014
|
|
|
6526
7015
|
export {
|
|
7016
|
+
BackendClient,
|
|
7017
|
+
requireCredentialsFromEnv,
|
|
7018
|
+
RunAbortedError,
|
|
6527
7019
|
LayerExecutionError,
|
|
6528
7020
|
describeFailureReason,
|
|
6529
7021
|
SEEDANCE_DURATIONS,
|
|
@@ -6531,8 +7023,15 @@ export {
|
|
|
6531
7023
|
IMAGE_GENERATE_MODELS,
|
|
6532
7024
|
MODEL_REGISTRY,
|
|
6533
7025
|
resolveConcurrency,
|
|
7026
|
+
ulid,
|
|
7027
|
+
isPersistedAssetRef,
|
|
7028
|
+
collectAssetRefLikes,
|
|
7029
|
+
REF_PREFIX,
|
|
7030
|
+
parseRefExpr,
|
|
7031
|
+
sha256Hex,
|
|
6534
7032
|
elementMentionKeywords,
|
|
6535
|
-
|
|
7033
|
+
toModelSafeImage,
|
|
7034
|
+
BackendClient2,
|
|
6536
7035
|
Engine2 as Engine,
|
|
6537
7036
|
LocalAssetStore2 as LocalAssetStore,
|
|
6538
7037
|
LocalCacheStore2 as LocalCacheStore,
|
|
@@ -6542,4 +7041,4 @@ export {
|
|
|
6542
7041
|
defaultRegistry,
|
|
6543
7042
|
createEngineFromEnv
|
|
6544
7043
|
};
|
|
6545
|
-
//# sourceMappingURL=chunk-
|
|
7044
|
+
//# sourceMappingURL=chunk-TKL2CJ6G.js.map
|