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