@koda-sl/baker-cli 0.125.0 → 0.129.1-dev.972f3c9aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +447 -89
- package/canvas/tiktok-captions-composition/index.html +1 -1
- package/canvas/video-overlay-composition/index.html +78 -6
- package/canvas/video-overlay-composition/meta.json +30 -2
- package/dist/{chunk-4EPKHOTF.js → chunk-J2LYFDVC.js} +1630 -550
- package/dist/chunk-J2LYFDVC.js.map +1 -0
- package/dist/cli.js +6145 -4514
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +143 -2
- package/dist/engine/index.js +2 -2
- package/package.json +4 -2
- package/dist/chunk-4EPKHOTF.js.map +0 -1
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
__toESM
|
|
4
4
|
} from "./chunk-5WRI5ZAA.js";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
|
|
7
7
|
var require_safe_stable_stringify = __commonJS({
|
|
8
|
-
"
|
|
8
|
+
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
9
9
|
"use strict";
|
|
10
10
|
var { hasOwnProperty } = Object.prototype;
|
|
11
11
|
var stringify = configure2();
|
|
@@ -138,9 +138,9 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
138
138
|
}
|
|
139
139
|
if (value) {
|
|
140
140
|
return (value2) => {
|
|
141
|
-
let
|
|
142
|
-
if (typeof value2 !== "function")
|
|
143
|
-
throw new Error(
|
|
141
|
+
let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
|
|
142
|
+
if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
|
|
143
|
+
throw new Error(message2);
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -600,7 +600,7 @@ ${originalIndentation}`;
|
|
|
600
600
|
});
|
|
601
601
|
|
|
602
602
|
// src/engine/index.ts
|
|
603
|
-
import
|
|
603
|
+
import path16 from "path";
|
|
604
604
|
|
|
605
605
|
// src/engine/client/http.ts
|
|
606
606
|
var CONTENT_POLICY_CODE = "content_policy_blocked";
|
|
@@ -649,14 +649,17 @@ var HttpClient = class {
|
|
|
649
649
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
650
650
|
this.sleepFn = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
651
651
|
}
|
|
652
|
-
async postJson(
|
|
653
|
-
return await this.requestJson("POST",
|
|
652
|
+
async postJson(path17, body, signal) {
|
|
653
|
+
return await this.requestJson("POST", path17, body, signal);
|
|
654
|
+
}
|
|
655
|
+
async putJson(path17, body, signal) {
|
|
656
|
+
return await this.requestJson("PUT", path17, body, signal);
|
|
654
657
|
}
|
|
655
|
-
async getJson(
|
|
656
|
-
return await this.requestJson("GET",
|
|
658
|
+
async getJson(path17, signal) {
|
|
659
|
+
return await this.requestJson("GET", path17, void 0, signal);
|
|
657
660
|
}
|
|
658
|
-
async requestJson(method,
|
|
659
|
-
const url = `${this.baseUrl}${
|
|
661
|
+
async requestJson(method, path17, body, signal) {
|
|
662
|
+
const url = `${this.baseUrl}${path17.startsWith("/") ? path17 : `/${path17}`}`;
|
|
660
663
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
661
664
|
const outcome = await this.attempt(method, url, body, attempt, signal);
|
|
662
665
|
if (outcome.kind === "value") return outcome.value;
|
|
@@ -679,8 +682,8 @@ var HttpClient = class {
|
|
|
679
682
|
try {
|
|
680
683
|
const res = await this.fetchFn(url, {
|
|
681
684
|
method,
|
|
682
|
-
headers: method === "
|
|
683
|
-
body: method === "
|
|
685
|
+
headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
|
686
|
+
body: method === "GET" ? void 0 : JSON.stringify(body),
|
|
684
687
|
signal: controller.signal
|
|
685
688
|
});
|
|
686
689
|
if (res.ok) return { kind: "value", value: await res.json() };
|
|
@@ -717,33 +720,33 @@ async function parseErrorBody(res) {
|
|
|
717
720
|
const errObj = body.error ?? {};
|
|
718
721
|
return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
|
|
719
722
|
}
|
|
720
|
-
function classifyHttpError(status, errObj,
|
|
723
|
+
function classifyHttpError(status, errObj, message2) {
|
|
721
724
|
if (errObj.code === CONTENT_POLICY_CODE) {
|
|
722
|
-
return { kind: "content_policy", status, provider: errObj.provider, message };
|
|
725
|
+
return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
|
|
723
726
|
}
|
|
724
727
|
if (status === 401 || status === 403) {
|
|
725
|
-
return { kind: "unauthorized", status, message };
|
|
728
|
+
return { kind: "unauthorized", status, message: message2 };
|
|
726
729
|
}
|
|
727
730
|
if (status === 400 || status === 422) {
|
|
728
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
731
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
729
732
|
}
|
|
730
733
|
if (status === 502 || status === 504) {
|
|
731
734
|
if (errObj.code === "provider_timeout" || status === 504) {
|
|
732
|
-
return { kind: "timeout", provider: errObj.provider, message };
|
|
735
|
+
return { kind: "timeout", provider: errObj.provider, message: message2 };
|
|
733
736
|
}
|
|
734
737
|
return {
|
|
735
738
|
kind: "provider",
|
|
736
739
|
status,
|
|
737
740
|
provider: errObj.provider,
|
|
738
741
|
code: errObj.code ?? "provider_error",
|
|
739
|
-
message,
|
|
742
|
+
message: message2,
|
|
740
743
|
retryable: errObj.retryable ?? true
|
|
741
744
|
};
|
|
742
745
|
}
|
|
743
746
|
if (status >= 500 || status === 429) {
|
|
744
|
-
return { kind: "server", status, message };
|
|
747
|
+
return { kind: "server", status, message: message2 };
|
|
745
748
|
}
|
|
746
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
749
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
747
750
|
}
|
|
748
751
|
function backoffMs(attempt) {
|
|
749
752
|
return 1e3 * 2 ** attempt;
|
|
@@ -777,7 +780,9 @@ function failedJobError(error) {
|
|
|
777
780
|
retryable: error.retryable ?? false
|
|
778
781
|
});
|
|
779
782
|
}
|
|
780
|
-
|
|
783
|
+
function pollInterval(attempt) {
|
|
784
|
+
return attempt < 15 ? 1e3 : 3e3;
|
|
785
|
+
}
|
|
781
786
|
var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
|
|
782
787
|
var BackendClient = class {
|
|
783
788
|
http;
|
|
@@ -793,30 +798,92 @@ var BackendClient = class {
|
|
|
793
798
|
}
|
|
794
799
|
async pollJob(jobId, signal) {
|
|
795
800
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
796
|
-
const
|
|
797
|
-
|
|
801
|
+
const path17 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
802
|
+
for (let attempt = 0; ; attempt++) {
|
|
798
803
|
if (signal?.aborted) {
|
|
799
804
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
800
805
|
}
|
|
801
|
-
const job = await this.http.getJson(
|
|
806
|
+
const job = await this.http.getJson(path17, signal);
|
|
802
807
|
if (job.status === "completed") return job.result;
|
|
803
808
|
if (job.status === "failed") throw failedJobError(job.error);
|
|
804
809
|
if (Date.now() > deadline) {
|
|
805
810
|
throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
|
|
806
811
|
}
|
|
807
|
-
await sleep(
|
|
812
|
+
await sleep(pollInterval(attempt));
|
|
808
813
|
}
|
|
809
814
|
}
|
|
810
|
-
presignAssetUpload(sha256, mime, signal) {
|
|
815
|
+
presignAssetUpload(sha256, mime, signal, purpose) {
|
|
811
816
|
return this.http.postJson(
|
|
812
817
|
"/api/canvas/assets/presign",
|
|
813
|
-
{ sha256, mime },
|
|
818
|
+
{ sha256, mime, purpose },
|
|
814
819
|
signal
|
|
815
820
|
);
|
|
816
821
|
}
|
|
822
|
+
/** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
|
|
823
|
+
async getCacheEntry(cacheKey, signal) {
|
|
824
|
+
try {
|
|
825
|
+
const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
|
|
826
|
+
return res.entry;
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async putCacheEntry(entry, signal) {
|
|
833
|
+
await this.http.putJson(
|
|
834
|
+
`/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
|
|
835
|
+
entry,
|
|
836
|
+
signal
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
/** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
|
|
840
|
+
async recordRun(payload, signal) {
|
|
841
|
+
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Resumable-run lookup — GET /api/canvas/runs/active. Returns the newest
|
|
845
|
+
* still-running/interrupted run for the creative (pinned to the exact canvas
|
|
846
|
+
* sha), so a fresh sandbox can adopt its run id and re-attach billed
|
|
847
|
+
* in-flight jobs. Null on no active run — or an older backend without the
|
|
848
|
+
* route (both 404).
|
|
849
|
+
*/
|
|
850
|
+
async getActiveRun(creativeSlug, canvasSha, signal) {
|
|
851
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
852
|
+
if (canvasSha) params.set("sha", canvasSha);
|
|
853
|
+
try {
|
|
854
|
+
const res = await this.http.getJson(`/api/canvas/runs/active?${params}`, signal);
|
|
855
|
+
return res.run;
|
|
856
|
+
} catch (e) {
|
|
857
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
858
|
+
throw e;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Portable-rerun lookup — GET /api/canvas/runs/latest. The newest run for
|
|
863
|
+
* the creative that recorded a canvas snapshot manifest; null when none
|
|
864
|
+
* exists (or the backend predates the route — both 404).
|
|
865
|
+
*/
|
|
866
|
+
async getLatestSnapshotRun(creativeSlug, signal) {
|
|
867
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
868
|
+
try {
|
|
869
|
+
const res = await this.http.getJson(`/api/canvas/runs/latest?${params}`, signal);
|
|
870
|
+
return res.run;
|
|
871
|
+
} catch (e) {
|
|
872
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
873
|
+
throw e;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
878
|
+
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
879
|
+
* Additive on the backend (never archives siblings, never sets definitionPath).
|
|
880
|
+
*/
|
|
881
|
+
async syncCreativeDefinition(payload, signal) {
|
|
882
|
+
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
883
|
+
}
|
|
817
884
|
getArtifact(kind, name, version, signal) {
|
|
818
|
-
const
|
|
819
|
-
return this.http.getJson(
|
|
885
|
+
const path17 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
886
|
+
return this.http.getJson(path17, signal);
|
|
820
887
|
}
|
|
821
888
|
};
|
|
822
889
|
|
|
@@ -837,14 +904,17 @@ function requireCredentialsFromEnv(env = process.env) {
|
|
|
837
904
|
}
|
|
838
905
|
return c;
|
|
839
906
|
}
|
|
907
|
+
function remoteCacheEnabledFromEnv(env = process.env) {
|
|
908
|
+
return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
|
|
909
|
+
}
|
|
840
910
|
|
|
841
911
|
// src/engine/engine/errors.ts
|
|
842
912
|
function isBlocking(issue) {
|
|
843
913
|
return issue.severity !== "warning";
|
|
844
914
|
}
|
|
845
915
|
var CanvasError = class extends Error {
|
|
846
|
-
constructor(
|
|
847
|
-
super(
|
|
916
|
+
constructor(message2) {
|
|
917
|
+
super(message2);
|
|
848
918
|
this.name = "CanvasError";
|
|
849
919
|
}
|
|
850
920
|
};
|
|
@@ -870,6 +940,14 @@ var NodeExecutionError = class extends CanvasError {
|
|
|
870
940
|
this.cause = cause;
|
|
871
941
|
}
|
|
872
942
|
};
|
|
943
|
+
var RunAbortedError = class extends CanvasError {
|
|
944
|
+
reason;
|
|
945
|
+
constructor(reason, message2) {
|
|
946
|
+
super(message2);
|
|
947
|
+
this.name = "RunAbortedError";
|
|
948
|
+
this.reason = reason;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
873
951
|
var LayerExecutionError = class extends CanvasError {
|
|
874
952
|
failures;
|
|
875
953
|
constructor(failures) {
|
|
@@ -901,7 +979,7 @@ function describeCause(c) {
|
|
|
901
979
|
}
|
|
902
980
|
}
|
|
903
981
|
|
|
904
|
-
//
|
|
982
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
905
983
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
906
984
|
var configure = import__.default.configure;
|
|
907
985
|
var wrapper_default = import__.default;
|
|
@@ -947,6 +1025,7 @@ var OPENROUTER_IMAGE_AR = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:1
|
|
|
947
1025
|
var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8", "8:1"];
|
|
948
1026
|
var OPENROUTER_IMAGE_SIZE = ["1K", "2K", "4K"];
|
|
949
1027
|
var OPENROUTER_IMAGE_SIZE_EXTENDED = ["0.5K", ...OPENROUTER_IMAGE_SIZE];
|
|
1028
|
+
var OPENROUTER_IMAGE_QUALITY = ["auto", "low", "medium", "high"];
|
|
950
1029
|
var SEEDANCE_DURATIONS = [4, 5, 6, 8, 10, 12, 15];
|
|
951
1030
|
var ELEVENLABS_OUTPUT_FORMATS = [
|
|
952
1031
|
"mp3_22050_32",
|
|
@@ -959,10 +1038,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
|
|
|
959
1038
|
var ELEVENLABS_MAX_TEXT_CHARS = 45454;
|
|
960
1039
|
var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
|
|
961
1040
|
var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
962
|
-
var
|
|
963
|
-
var
|
|
1041
|
+
var REPLICATE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
1042
|
+
var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
964
1043
|
var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
965
|
-
var
|
|
1044
|
+
var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
|
|
966
1045
|
var IMAGE_GENERATE_MODELS = [
|
|
967
1046
|
"openai/gpt-5.4-image-2",
|
|
968
1047
|
"google/gemini-3.5-flash",
|
|
@@ -970,6 +1049,13 @@ var IMAGE_GENERATE_MODELS = [
|
|
|
970
1049
|
"google/gemini-3-pro-image-preview",
|
|
971
1050
|
"recraft/recraft-v4.1-pro-vector"
|
|
972
1051
|
];
|
|
1052
|
+
var VIDEO_GENERATE_MODELS = [
|
|
1053
|
+
"bytedance/seedance-2.0",
|
|
1054
|
+
"google/veo-3.1",
|
|
1055
|
+
"google/veo-3.1-fast",
|
|
1056
|
+
"kwaivgi/kling-v3.0-pro"
|
|
1057
|
+
];
|
|
1058
|
+
var DEFAULT_VIDEO_GENERATE_MODEL = "bytedance/seedance-2.0";
|
|
973
1059
|
var MODEL_REGISTRY = {
|
|
974
1060
|
text_generate: {
|
|
975
1061
|
"~google/gemini-flash-latest": {
|
|
@@ -1065,7 +1151,8 @@ var MODEL_REGISTRY = {
|
|
|
1065
1151
|
params: {
|
|
1066
1152
|
prompt: { kind: "string" },
|
|
1067
1153
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1068
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1154
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1155
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1069
1156
|
}
|
|
1070
1157
|
},
|
|
1071
1158
|
"google/gemini-3.5-flash": {
|
|
@@ -1077,7 +1164,8 @@ var MODEL_REGISTRY = {
|
|
|
1077
1164
|
params: {
|
|
1078
1165
|
prompt: { kind: "string" },
|
|
1079
1166
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
|
|
1080
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
|
|
1167
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED },
|
|
1168
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1081
1169
|
}
|
|
1082
1170
|
},
|
|
1083
1171
|
"google/gemini-3.1-flash-image-preview": {
|
|
@@ -1099,7 +1187,8 @@ var MODEL_REGISTRY = {
|
|
|
1099
1187
|
params: {
|
|
1100
1188
|
prompt: { kind: "string" },
|
|
1101
1189
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1102
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1190
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1191
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1103
1192
|
}
|
|
1104
1193
|
},
|
|
1105
1194
|
"recraft/recraft-v4.1-pro-vector": {
|
|
@@ -1180,25 +1269,75 @@ var MODEL_REGISTRY = {
|
|
|
1180
1269
|
},
|
|
1181
1270
|
video_generate: {
|
|
1182
1271
|
"bytedance/seedance-2.0": {
|
|
1183
|
-
// Routed via
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1272
|
+
// Routed via Replicate's official `bytedance/seedance-2.0` model. NOTE:
|
|
1273
|
+
// ByteDance's upstream "real person" likeness filter still blocks photoreal
|
|
1274
|
+
// human reference frames on ANY reseller — the escape is a synthetic/AI
|
|
1275
|
+
// presenter face or routing real faces to Veo, not the provider choice.
|
|
1186
1276
|
label: "ByteDance Seedance 2.0",
|
|
1187
1277
|
inputs: [],
|
|
1188
|
-
optional_inputs: [{ kind: "image", mimes:
|
|
1278
|
+
optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
|
|
1189
1279
|
required: ["prompt"],
|
|
1190
1280
|
params: {
|
|
1191
|
-
prompt
|
|
1281
|
+
// Replicate's Seedance wrapper hard-caps the prompt at 4000 chars; gate
|
|
1282
|
+
// it here so an over-length prompt fails validate (free) not the billed call.
|
|
1283
|
+
prompt: { kind: "string", maxLength: 4e3 },
|
|
1192
1284
|
aspect_ratio: {
|
|
1193
1285
|
kind: "string",
|
|
1194
1286
|
enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
|
|
1195
1287
|
},
|
|
1196
|
-
resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
|
|
1288
|
+
resolution: { kind: "string", enum: ["480p", "720p", "1080p", "4k"] },
|
|
1197
1289
|
duration: { kind: "number", enum: SEEDANCE_DURATIONS },
|
|
1198
1290
|
seed: { kind: "number" },
|
|
1199
1291
|
generate_audio: { kind: "boolean" }
|
|
1200
1292
|
}
|
|
1201
1293
|
},
|
|
1294
|
+
"google/veo-3.1": {
|
|
1295
|
+
// Photoreal CINE CEILING + the real-face fallback (Veo generates adult
|
|
1296
|
+
// humans from a keyframe, dodging ByteDance's real-person filter). Same
|
|
1297
|
+
// OpenRouter google-vertex routing as the fast tier; the quality dial is
|
|
1298
|
+
// `resolution: 1080p` + `generate_audio` + a real `negative_prompt`, not a
|
|
1299
|
+
// separate provider knob. Reach for this for hero beats and any clip that
|
|
1300
|
+
// must carry a real human likeness.
|
|
1301
|
+
label: "Google Veo 3.1",
|
|
1302
|
+
inputs: [],
|
|
1303
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1304
|
+
required: ["prompt"],
|
|
1305
|
+
params: {
|
|
1306
|
+
prompt: { kind: "string" },
|
|
1307
|
+
negative_prompt: { kind: "string" },
|
|
1308
|
+
aspect_ratio: { kind: "string", enum: ["16:9", "9:16"] },
|
|
1309
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1310
|
+
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1311
|
+
seed: { kind: "number" },
|
|
1312
|
+
generate_audio: { kind: "boolean" },
|
|
1313
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1314
|
+
enhance_prompt: { kind: "boolean" },
|
|
1315
|
+
conditioning_scale: { kind: "number" }
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
"kwaivgi/kling-v3.0-pro": {
|
|
1319
|
+
// Motion-transfer / dynamic multi-shot beats. Reachable through the default
|
|
1320
|
+
// OpenRouter gateway (generic video body — no google-vertex block), so it
|
|
1321
|
+
// needs no direct-provider exception. Cost is usage-based (known from the
|
|
1322
|
+
// provider response), so it has no pre-flight cost estimate. `cfg_scale`
|
|
1323
|
+
// trades prompt adherence vs motion freedom; higher = closer to prompt.
|
|
1324
|
+
label: "Kling 3.0",
|
|
1325
|
+
inputs: [],
|
|
1326
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1327
|
+
required: ["prompt"],
|
|
1328
|
+
params: {
|
|
1329
|
+
// Kling caps the prompt shorter than Seedance; gate it here so an
|
|
1330
|
+
// over-length prompt fails validate (free) not the billed call.
|
|
1331
|
+
prompt: { kind: "string", maxLength: 2500 },
|
|
1332
|
+
negative_prompt: { kind: "string" },
|
|
1333
|
+
aspect_ratio: { kind: "string", enum: ["1:1", "16:9", "9:16"] },
|
|
1334
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1335
|
+
duration: { kind: "number", enum: [5, 10] },
|
|
1336
|
+
seed: { kind: "number" },
|
|
1337
|
+
generate_audio: { kind: "boolean" },
|
|
1338
|
+
cfg_scale: { kind: "number", min: 0, max: 1 }
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1202
1341
|
"google/veo-3.1-fast": {
|
|
1203
1342
|
// Cheap test/iteration model. Forwarded by the backend via
|
|
1204
1343
|
// `provider.options.google-vertex.parameters` (camelCased on the wire).
|
|
@@ -1214,7 +1353,10 @@ var MODEL_REGISTRY = {
|
|
|
1214
1353
|
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1215
1354
|
seed: { kind: "number" },
|
|
1216
1355
|
generate_audio: { kind: "boolean" },
|
|
1217
|
-
|
|
1356
|
+
// Image-to-video and EU/UK/CH/MENA regions cap this at `allow_adult`;
|
|
1357
|
+
// `allow_all` is text-to-video only. Allow both so an image-conditioned
|
|
1358
|
+
// Veo clip (the real-face fallback) validates.
|
|
1359
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1218
1360
|
enhance_prompt: { kind: "boolean" },
|
|
1219
1361
|
conditioning_scale: { kind: "number" }
|
|
1220
1362
|
}
|
|
@@ -1265,8 +1407,8 @@ var MODEL_REGISTRY = {
|
|
|
1265
1407
|
"fal/veed-lipsync": {
|
|
1266
1408
|
label: "VEED Lipsync (fal.ai)",
|
|
1267
1409
|
inputs: [
|
|
1268
|
-
{ kind: "video", mimes:
|
|
1269
|
-
{ kind: "audio", mimes:
|
|
1410
|
+
{ kind: "video", mimes: REPLICATE_VIDEO_MIMES },
|
|
1411
|
+
{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }
|
|
1270
1412
|
],
|
|
1271
1413
|
required: [],
|
|
1272
1414
|
params: {}
|
|
@@ -1302,7 +1444,7 @@ var MODEL_REGISTRY = {
|
|
|
1302
1444
|
// TARGET voice, preserving timing/prosody. Used to normalize a talking-head
|
|
1303
1445
|
// clip's native (generator-chosen) voice into ONE consistent brand voice.
|
|
1304
1446
|
label: "ElevenLabs Voice Changer (multilingual STS v2)",
|
|
1305
|
-
inputs: [{ kind: "audio", mimes:
|
|
1447
|
+
inputs: [{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }],
|
|
1306
1448
|
required: ["voice"],
|
|
1307
1449
|
params: {
|
|
1308
1450
|
voice: { kind: "string" },
|
|
@@ -1329,7 +1471,7 @@ var MODEL_REGISTRY = {
|
|
|
1329
1471
|
},
|
|
1330
1472
|
"elevenlabs/video-background-music-v1": {
|
|
1331
1473
|
label: "ElevenLabs Video Background Music v1",
|
|
1332
|
-
inputs: [{ kind: "video", mimes:
|
|
1474
|
+
inputs: [{ kind: "video", mimes: REPLICATE_VIDEO_MIMES }],
|
|
1333
1475
|
required: [],
|
|
1334
1476
|
params: {
|
|
1335
1477
|
description: { kind: "string" },
|
|
@@ -1500,7 +1642,7 @@ function validateValue(key, value, schema, model) {
|
|
|
1500
1642
|
}
|
|
1501
1643
|
|
|
1502
1644
|
// src/engine/lib/concurrency.ts
|
|
1503
|
-
var DEFAULT_CONCURRENCY =
|
|
1645
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
1504
1646
|
function resolveConcurrency(...candidates) {
|
|
1505
1647
|
for (const candidate of candidates) {
|
|
1506
1648
|
if (candidate === void 0 || candidate === "") continue;
|
|
@@ -1562,6 +1704,185 @@ function encodeRandom() {
|
|
|
1562
1704
|
return out;
|
|
1563
1705
|
}
|
|
1564
1706
|
|
|
1707
|
+
// src/engine/storage/remote-cache-store.ts
|
|
1708
|
+
var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
|
|
1709
|
+
function isPersistedAssetRef(ref) {
|
|
1710
|
+
const { url, sha256 } = ref;
|
|
1711
|
+
if (typeof url !== "string" || typeof sha256 !== "string") return false;
|
|
1712
|
+
return url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256.slice(0, 2)}/${sha256}`) || url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256}`);
|
|
1713
|
+
}
|
|
1714
|
+
function isAssetRefLike(value) {
|
|
1715
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
|
|
1716
|
+
}
|
|
1717
|
+
function collectAssetRefLikes(value, out = []) {
|
|
1718
|
+
if (Array.isArray(value)) {
|
|
1719
|
+
for (const item of value) collectAssetRefLikes(item, out);
|
|
1720
|
+
return out;
|
|
1721
|
+
}
|
|
1722
|
+
if (typeof value !== "object" || value === null) return out;
|
|
1723
|
+
if (isAssetRefLike(value)) {
|
|
1724
|
+
out.push(value);
|
|
1725
|
+
}
|
|
1726
|
+
for (const item of Object.values(value)) collectAssetRefLikes(item, out);
|
|
1727
|
+
return out;
|
|
1728
|
+
}
|
|
1729
|
+
function entryFullyPersisted(entry) {
|
|
1730
|
+
return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
|
|
1731
|
+
}
|
|
1732
|
+
function stripLocalFields(entry) {
|
|
1733
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1734
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1735
|
+
delete ref.path;
|
|
1736
|
+
delete ref.bytes;
|
|
1737
|
+
}
|
|
1738
|
+
return clone;
|
|
1739
|
+
}
|
|
1740
|
+
var RemoteCacheStore = class {
|
|
1741
|
+
client;
|
|
1742
|
+
log;
|
|
1743
|
+
constructor(client, log) {
|
|
1744
|
+
this.client = client;
|
|
1745
|
+
this.log = log ?? (() => void 0);
|
|
1746
|
+
}
|
|
1747
|
+
async get(cacheKey) {
|
|
1748
|
+
return await this.client.getCacheEntry(cacheKey);
|
|
1749
|
+
}
|
|
1750
|
+
async put(entry) {
|
|
1751
|
+
if (!entryFullyPersisted(entry)) {
|
|
1752
|
+
this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const stripped = stripLocalFields(entry);
|
|
1756
|
+
if (stripped.refs.length > MAX_REMOTE_REFS) {
|
|
1757
|
+
stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
|
|
1758
|
+
}
|
|
1759
|
+
await this.client.putCacheEntry(stripped);
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
var MAX_REMOTE_REFS = 512;
|
|
1763
|
+
var LayeredCacheStore = class {
|
|
1764
|
+
rootDir;
|
|
1765
|
+
local;
|
|
1766
|
+
remote;
|
|
1767
|
+
assets;
|
|
1768
|
+
log;
|
|
1769
|
+
constructor(opts) {
|
|
1770
|
+
this.local = opts.local;
|
|
1771
|
+
this.remote = opts.remote;
|
|
1772
|
+
this.assets = opts.assets;
|
|
1773
|
+
this.rootDir = opts.local.rootDir;
|
|
1774
|
+
this.log = opts.log ?? (() => void 0);
|
|
1775
|
+
}
|
|
1776
|
+
async get(cacheKey) {
|
|
1777
|
+
const localHit = await this.local.get(cacheKey);
|
|
1778
|
+
if (localHit) return localHit;
|
|
1779
|
+
let remoteEntry;
|
|
1780
|
+
try {
|
|
1781
|
+
remoteEntry = await this.remote.get(cacheKey);
|
|
1782
|
+
} catch (e) {
|
|
1783
|
+
this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
if (!remoteEntry) return null;
|
|
1787
|
+
let rehydrated;
|
|
1788
|
+
try {
|
|
1789
|
+
rehydrated = await this.rehydrate(remoteEntry);
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
await this.local.put(rehydrated);
|
|
1795
|
+
return rehydrated;
|
|
1796
|
+
}
|
|
1797
|
+
async put(entry) {
|
|
1798
|
+
await this.local.put(entry);
|
|
1799
|
+
try {
|
|
1800
|
+
await this.remote.put(entry);
|
|
1801
|
+
} catch (e) {
|
|
1802
|
+
this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Download every referenced asset into the local content-addressed store
|
|
1807
|
+
* (sha-verified) and stamp fresh local paths. Any ref that cannot be
|
|
1808
|
+
* rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
|
|
1809
|
+
* crash materialization later with a far less actionable error.
|
|
1810
|
+
*/
|
|
1811
|
+
async rehydrate(entry) {
|
|
1812
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1813
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1814
|
+
if (!isPersistedAssetRef(ref)) {
|
|
1815
|
+
throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
|
|
1816
|
+
}
|
|
1817
|
+
const ingested = await this.assets.ingestRemote({
|
|
1818
|
+
kind: typeof ref.kind === "string" ? ref.kind : "json",
|
|
1819
|
+
url: ref.url,
|
|
1820
|
+
sha256: ref.sha256,
|
|
1821
|
+
mime: ref.mime,
|
|
1822
|
+
metadata: ref.metadata ?? void 0
|
|
1823
|
+
});
|
|
1824
|
+
ref.path = ingested.path;
|
|
1825
|
+
}
|
|
1826
|
+
return clone;
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
function message(e) {
|
|
1830
|
+
return e instanceof Error ? e.message : String(e);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/engine/nodes/remote/upload.ts
|
|
1834
|
+
var PUT_MAX_ATTEMPTS = 4;
|
|
1835
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1836
|
+
async function presignAndPut(args) {
|
|
1837
|
+
let lastFailure = null;
|
|
1838
|
+
for (let attempt = 0; attempt < PUT_MAX_ATTEMPTS; attempt++) {
|
|
1839
|
+
if (args.ctx.signal?.aborted) break;
|
|
1840
|
+
if (attempt > 0) await sleep2(500 * 2 ** (attempt - 1) * (1 + Math.random() * 0.25));
|
|
1841
|
+
const result = await attemptPresignedPut(args);
|
|
1842
|
+
if (result.ok) return result.url;
|
|
1843
|
+
lastFailure = result.failure;
|
|
1844
|
+
if (!result.retryable) break;
|
|
1845
|
+
}
|
|
1846
|
+
throw lastFailure ?? new Error("upload: aborted before the PUT could start");
|
|
1847
|
+
}
|
|
1848
|
+
async function attemptPresignedPut(args) {
|
|
1849
|
+
try {
|
|
1850
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1851
|
+
const putRes = await fetch(putUrl, {
|
|
1852
|
+
method: "PUT",
|
|
1853
|
+
body: new Uint8Array(args.bytes),
|
|
1854
|
+
headers: { "Content-Type": args.mime },
|
|
1855
|
+
signal: args.ctx.signal
|
|
1856
|
+
});
|
|
1857
|
+
if (putRes.ok) return { ok: true, url: publicUrl };
|
|
1858
|
+
return {
|
|
1859
|
+
ok: false,
|
|
1860
|
+
failure: new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`),
|
|
1861
|
+
// Only transient statuses warrant a replay — a 400/403 fails identically every attempt.
|
|
1862
|
+
retryable: putRes.status >= 500 || putRes.status === 429 || putRes.status === 408
|
|
1863
|
+
};
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
return {
|
|
1866
|
+
ok: false,
|
|
1867
|
+
failure: e instanceof Error ? e : new Error(String(e)),
|
|
1868
|
+
retryable: args.ctx.signal?.aborted !== true
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function ensureUploaded(ref, ctx) {
|
|
1873
|
+
if (ref.url) return ref;
|
|
1874
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1875
|
+
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1876
|
+
return { ...ref, url };
|
|
1877
|
+
}
|
|
1878
|
+
async function persistOutputAssetUrls(outputs, ctx) {
|
|
1879
|
+
for (const ref of collectAssetRefLikes(outputs)) {
|
|
1880
|
+
if (isPersistedAssetRef(ref)) continue;
|
|
1881
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1882
|
+
ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1565
1886
|
// src/engine/schema/canvas.ts
|
|
1566
1887
|
import { z } from "zod";
|
|
1567
1888
|
var REF_PREFIX = "$ref:";
|
|
@@ -1591,7 +1912,14 @@ var NodeDecl = z.object({
|
|
|
1591
1912
|
version: z.string().min(1).optional(),
|
|
1592
1913
|
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
1593
1914
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
1594
|
-
when: z.unknown().optional()
|
|
1915
|
+
when: z.unknown().optional(),
|
|
1916
|
+
// Regenerate knob. The engine is content-addressed: identical params + inputs
|
|
1917
|
+
// return the cached render, so re-running an unchanged node NEVER re-bills or
|
|
1918
|
+
// produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
|
|
1919
|
+
// a note) and re-run to force THIS node to render fresh; because its new output
|
|
1920
|
+
// changes downstream input hashes, everything depending on it regenerates too.
|
|
1921
|
+
// This is the declarative "change a value, re-run, get a new render" affordance.
|
|
1922
|
+
regenerate: z.union([z.string(), z.number()]).optional()
|
|
1595
1923
|
}).strict();
|
|
1596
1924
|
var OutputRef = z.object({
|
|
1597
1925
|
node: z.string(),
|
|
@@ -1599,6 +1927,16 @@ var OutputRef = z.object({
|
|
|
1599
1927
|
}).strict();
|
|
1600
1928
|
var VideoMeta = z.object({
|
|
1601
1929
|
duration_s: z.number(),
|
|
1930
|
+
// The one-clock contract: the spine's summed clip lengths and every audio
|
|
1931
|
+
// timeline reachable from the final mux must equal `expected_total_s` — the
|
|
1932
|
+
// validator recomputes both from the LIVE graph (`-shortest` at the final mux
|
|
1933
|
+
// silently truncates the longer stream on any mismatch). Emitted by
|
|
1934
|
+
// scaffold-video; optional so hand-authored canvases still parse.
|
|
1935
|
+
timeline: z.object({
|
|
1936
|
+
spine_node: z.string(),
|
|
1937
|
+
expected_total_s: z.number(),
|
|
1938
|
+
audio_mix_node: z.string().optional()
|
|
1939
|
+
}).strict().optional(),
|
|
1602
1940
|
// Each sequenced voiceover turn on the absolute timeline.
|
|
1603
1941
|
vo_segments: z.array(
|
|
1604
1942
|
z.object({
|
|
@@ -2076,8 +2414,111 @@ function dfsCycle(u, color, stack, reverseAdj) {
|
|
|
2076
2414
|
}
|
|
2077
2415
|
|
|
2078
2416
|
// src/engine/engine/validator.ts
|
|
2417
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2418
|
+
import path3 from "path";
|
|
2079
2419
|
import { z as z2 } from "zod";
|
|
2080
2420
|
|
|
2421
|
+
// src/engine/scaffold/lib/prompt-profiles.ts
|
|
2422
|
+
var VEO_PERSON_GENERATION = "allow_adult";
|
|
2423
|
+
var VEO_NEGATIVE_PROMPT = "subtitles, captions, on-screen text, watermark, logo, warped face, distorted hands, extra fingers, low quality";
|
|
2424
|
+
var VEO_DURATIONS = [4, 6, 8];
|
|
2425
|
+
var KLING_DURATIONS = [5, 10];
|
|
2426
|
+
var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
|
|
2427
|
+
var KLING_CFG_SCALE = 0.7;
|
|
2428
|
+
var SPEAKS_PROSE = (line) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues). Dialogue: "${line}"`;
|
|
2429
|
+
var SEEDANCE_PROFILE = {
|
|
2430
|
+
id: "seedance",
|
|
2431
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2432
|
+
extraDirectives: [],
|
|
2433
|
+
// Seedance has no negative_prompt field. Phrase the anti-artifact intent as the POSITIVE
|
|
2434
|
+
// state we want (crisp hands, one stable face, smooth coherent motion, locked identity) —
|
|
2435
|
+
// an "Avoid: warped fingers, morphing faces" list tends to plant those very artifacts.
|
|
2436
|
+
stabilityDirectives: [
|
|
2437
|
+
"hands and fingers crisp and anatomically correct",
|
|
2438
|
+
"one consistent face and identity across every frame",
|
|
2439
|
+
"smooth, temporally coherent motion that holds steady frame to frame"
|
|
2440
|
+
],
|
|
2441
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; change only the motion described.",
|
|
2442
|
+
// ~120 words is the Seedance quality sweet spot: past it the model starts dropping beats and
|
|
2443
|
+
// the attention budget dilutes. The validator warns above this; composite scenes that stack
|
|
2444
|
+
// many brief `parts` are the usual offenders.
|
|
2445
|
+
wordBudget: 120,
|
|
2446
|
+
durationSet: SEEDANCE_DURATIONS,
|
|
2447
|
+
paramDefaults: {}
|
|
2448
|
+
};
|
|
2449
|
+
var VEO_PROFILE = {
|
|
2450
|
+
id: "veo",
|
|
2451
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2452
|
+
// Veo's known failure mode: quoted dialogue triggers burned-in subtitles. Belt
|
|
2453
|
+
// (inline) and braces (the negative_prompt param, below) — the param is the real lever.
|
|
2454
|
+
extraDirectives: ["No subtitles, no captions, no on-screen text of any kind."],
|
|
2455
|
+
stabilityDirectives: [],
|
|
2456
|
+
// Veo takes a negative_prompt PARAM instead (paramDefaults).
|
|
2457
|
+
keyframeInstruction: "Describe the transition and camera move between the frames; the frames fix the content.",
|
|
2458
|
+
wordBudget: 200,
|
|
2459
|
+
durationSet: VEO_DURATIONS,
|
|
2460
|
+
paramDefaults: { person_generation: VEO_PERSON_GENERATION, negative_prompt: VEO_NEGATIVE_PROMPT }
|
|
2461
|
+
};
|
|
2462
|
+
var KLING_PROFILE = {
|
|
2463
|
+
id: "kling",
|
|
2464
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2465
|
+
// Belt (inline) and braces (the negative_prompt param) — the param is the real lever.
|
|
2466
|
+
extraDirectives: ["Single, deliberate camera move \u2014 no whip pans or rapid cuts within the clip."],
|
|
2467
|
+
stabilityDirectives: [],
|
|
2468
|
+
// Kling takes a negative_prompt PARAM instead (paramDefaults).
|
|
2469
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; animate the motion described.",
|
|
2470
|
+
wordBudget: 200,
|
|
2471
|
+
durationSet: KLING_DURATIONS,
|
|
2472
|
+
paramDefaults: { negative_prompt: KLING_NEGATIVE_PROMPT, cfg_scale: KLING_CFG_SCALE }
|
|
2473
|
+
};
|
|
2474
|
+
function clipProfileFor(modelId) {
|
|
2475
|
+
if (/^bytedance\/seedance/.test(modelId)) return SEEDANCE_PROFILE;
|
|
2476
|
+
if (/^google\/veo/.test(modelId)) return VEO_PROFILE;
|
|
2477
|
+
if (/^kwaivgi\/kling|^kling\//.test(modelId)) return KLING_PROFILE;
|
|
2478
|
+
return void 0;
|
|
2479
|
+
}
|
|
2480
|
+
function clipParamRecipe(profile, intent) {
|
|
2481
|
+
const out = {};
|
|
2482
|
+
if (intent === "hero") {
|
|
2483
|
+
out.resolution = "1080p";
|
|
2484
|
+
}
|
|
2485
|
+
if (intent === "hook" && profile.id === "kling") {
|
|
2486
|
+
out.cfg_scale = 0.85;
|
|
2487
|
+
}
|
|
2488
|
+
return out;
|
|
2489
|
+
}
|
|
2490
|
+
function nativeDialogueOf(prompt) {
|
|
2491
|
+
if (typeof prompt !== "string") return void 0;
|
|
2492
|
+
const m = prompt.match(/Dialogue: "(.*)"/);
|
|
2493
|
+
return m?.[1]?.trim() || void 0;
|
|
2494
|
+
}
|
|
2495
|
+
var GPT_IMAGE_PROFILE = {
|
|
2496
|
+
id: "gpt-image",
|
|
2497
|
+
constraintPlacement: "last",
|
|
2498
|
+
photorealCue: true,
|
|
2499
|
+
// OpenRouter forwards `quality`; gpt-image-2 already processes inputs at high
|
|
2500
|
+
// fidelity automatically, so we deliberately do NOT send `input_fidelity`.
|
|
2501
|
+
paramDefaults: { quality: "high" }
|
|
2502
|
+
};
|
|
2503
|
+
var GEMINI_IMAGE_PROFILE = {
|
|
2504
|
+
id: "gemini",
|
|
2505
|
+
constraintPlacement: "inline",
|
|
2506
|
+
photorealCue: true,
|
|
2507
|
+
paramDefaults: { quality: "high" }
|
|
2508
|
+
};
|
|
2509
|
+
var RECRAFT_IMAGE_PROFILE = {
|
|
2510
|
+
id: "recraft",
|
|
2511
|
+
constraintPlacement: "inline",
|
|
2512
|
+
photorealCue: false,
|
|
2513
|
+
paramDefaults: {}
|
|
2514
|
+
};
|
|
2515
|
+
function imageProfileFor(modelId) {
|
|
2516
|
+
if (/^openai\/gpt-.*image/.test(modelId)) return GPT_IMAGE_PROFILE;
|
|
2517
|
+
if (/^google\/gemini/.test(modelId)) return GEMINI_IMAGE_PROFILE;
|
|
2518
|
+
if (/^recraft\//.test(modelId)) return RECRAFT_IMAGE_PROFILE;
|
|
2519
|
+
return void 0;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2081
2522
|
// src/engine/engine/define.ts
|
|
2082
2523
|
function resolveOutputKinds(spec, params) {
|
|
2083
2524
|
if (!spec) return {};
|
|
@@ -2159,16 +2600,62 @@ var STAGE_CODES = {
|
|
|
2159
2600
|
REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
|
|
2160
2601
|
SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL",
|
|
2161
2602
|
UI_IN_PROMPT: "VIDEO_UI_IN_PROMPT",
|
|
2162
|
-
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT"
|
|
2603
|
+
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT",
|
|
2604
|
+
SPEECH_EXCEEDS_EXTRACT: "VIDEO_SPEECH_EXCEEDS_EXTRACT",
|
|
2605
|
+
PROMPT_PROFILE_MISSING: "VIDEO_PROMPT_PROFILE_MISSING",
|
|
2606
|
+
PROMPT_DECISION_MISSING: "VIDEO_PROMPT_DECISION_MISSING",
|
|
2607
|
+
HOOK_LAYER_MISSING: "VIDEO_HOOK_LAYER_MISSING",
|
|
2608
|
+
IMAGE_PROFILE_MISSING: "IMAGE_PROMPT_PROFILE_MISSING",
|
|
2609
|
+
PROMPT_OVER_BUDGET: "VIDEO_PROMPT_OVER_BUDGET",
|
|
2610
|
+
PERSON_GENERATION_MISSING: "VIDEO_PERSON_GENERATION_MISSING",
|
|
2611
|
+
RAW_FACE_KEYFRAME: "VIDEO_RAW_FACE_KEYFRAME",
|
|
2612
|
+
TIMELINE_TOTAL: "VIDEO_TIMELINE_TOTAL_MISMATCH",
|
|
2613
|
+
NATIVE_SEG_OVERLAP: "VIDEO_NATIVE_SEG_OVERLAP",
|
|
2614
|
+
SPINE_UNNORMALIZED: "VIDEO_SPINE_UNNORMALIZED",
|
|
2615
|
+
ODD_DIMENSIONS: "VIDEO_ODD_DIMENSIONS",
|
|
2616
|
+
REGION_DROPPED: "VIDEO_REGION_DROPPED",
|
|
2617
|
+
OVERLAY_OUT_OF_BOUNDS: "VIDEO_OVERLAY_OUT_OF_BOUNDS"
|
|
2163
2618
|
};
|
|
2164
2619
|
var SPAN_MODEL_SLACK_S = 0.25;
|
|
2165
2620
|
var VIDEO_TIME_SLACK_S = 0.75;
|
|
2166
2621
|
var SPEECH_WORDS_PER_SECOND = 2.5;
|
|
2167
2622
|
var SPEECH_OVERRUN_RATIO = 1.6;
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2623
|
+
function ffmpegTrimSeconds(node) {
|
|
2624
|
+
const args = node.params?.args;
|
|
2625
|
+
if (!Array.isArray(args)) return null;
|
|
2626
|
+
let t = null;
|
|
2627
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
2628
|
+
if (args[i] === "-t") {
|
|
2629
|
+
const v = Number(args[i + 1]);
|
|
2630
|
+
if (Number.isFinite(v)) t = v;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return t;
|
|
2634
|
+
}
|
|
2635
|
+
function refNodeOf(ctx, value) {
|
|
2636
|
+
if (typeof value !== "string" || !value.startsWith(REF_PREFIX)) return null;
|
|
2637
|
+
const parsed = parseRefExpr(value);
|
|
2638
|
+
if (!parsed) return null;
|
|
2639
|
+
const idx = ctx.idToIndex.get(parsed.nodeId);
|
|
2640
|
+
return idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
2641
|
+
}
|
|
2642
|
+
function liveNodeDurationS(ctx, node, depth = 0) {
|
|
2643
|
+
if (!node || depth > 3) return null;
|
|
2644
|
+
if (node.type === "video_generate") {
|
|
2645
|
+
const d = node.params?.duration;
|
|
2646
|
+
return typeof d === "number" ? d : null;
|
|
2647
|
+
}
|
|
2648
|
+
if (node.type === "ffmpeg") {
|
|
2649
|
+
const t = ffmpegTrimSeconds(node);
|
|
2650
|
+
if (t !== null) return t;
|
|
2651
|
+
for (const v of Object.values(node.inputs ?? {})) {
|
|
2652
|
+
const upstream = refNodeOf(ctx, v);
|
|
2653
|
+
const d = liveNodeDurationS(ctx, upstream, depth + 1);
|
|
2654
|
+
if (d !== null) return d;
|
|
2655
|
+
}
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
return null;
|
|
2172
2659
|
}
|
|
2173
2660
|
function validateCanvas(input, registry) {
|
|
2174
2661
|
const issues = [];
|
|
@@ -2230,6 +2717,7 @@ async function validateCanvasDeep(input, registry) {
|
|
|
2230
2717
|
});
|
|
2231
2718
|
}
|
|
2232
2719
|
}
|
|
2720
|
+
await checkOverlayTimingBounds(canvas, issues);
|
|
2233
2721
|
const hasBlocking = issues.some(isBlocking);
|
|
2234
2722
|
if (hasBlocking) return { ok: false, issues };
|
|
2235
2723
|
const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
|
|
@@ -2471,6 +2959,38 @@ function estimateCredits(ctx) {
|
|
|
2471
2959
|
}
|
|
2472
2960
|
return total;
|
|
2473
2961
|
}
|
|
2962
|
+
function overlayTimingIssues(html, durationS) {
|
|
2963
|
+
const out = [];
|
|
2964
|
+
for (const m of html.matchAll(/<[^>]*\bdata-start="([\d.]+)"[^>]*\bdata-dur="([\d.]+)"[^>]*>/g)) {
|
|
2965
|
+
const start = Number(m[1]);
|
|
2966
|
+
const dur = Number(m[2]);
|
|
2967
|
+
if (!Number.isFinite(start) || !Number.isFinite(dur)) continue;
|
|
2968
|
+
if (start >= durationS || start + dur > durationS + 0.25) out.push({ start, dur });
|
|
2969
|
+
}
|
|
2970
|
+
return out;
|
|
2971
|
+
}
|
|
2972
|
+
async function checkOverlayTimingBounds(canvas, issues) {
|
|
2973
|
+
const durationS = canvas.metadata?.video?.duration_s;
|
|
2974
|
+
if (typeof durationS !== "number") return;
|
|
2975
|
+
for (let i = 0; i < canvas.nodes.length; i++) {
|
|
2976
|
+
const n = canvas.nodes[i];
|
|
2977
|
+
if (n?.type !== "hyperframe_render") continue;
|
|
2978
|
+
const composition = n.params?.composition;
|
|
2979
|
+
if (typeof composition !== "string" || !path3.isAbsolute(composition)) continue;
|
|
2980
|
+
const html = await readFile2(path3.join(composition, "index.html"), "utf8").catch(() => null);
|
|
2981
|
+
if (!html) continue;
|
|
2982
|
+
for (const w of overlayTimingIssues(html, durationS)) {
|
|
2983
|
+
issues.push({
|
|
2984
|
+
path: `nodes[${i}].params.composition`,
|
|
2985
|
+
code: STAGE_CODES.OVERLAY_OUT_OF_BOUNDS,
|
|
2986
|
+
severity: "warning",
|
|
2987
|
+
node_id: n.id,
|
|
2988
|
+
node_type: n.type,
|
|
2989
|
+
message: `an overlay in ${path3.basename(composition)}/index.html runs [${w.start}s, ${w.start + w.dur}s] but the video ends at ${durationS}s \u2014 it never hides on screen. Re-time its data-start/data-dur to fit the video`
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2474
2994
|
function nativeAudioReachesMix(ctx, scene) {
|
|
2475
2995
|
const wanted = [
|
|
2476
2996
|
`$ref:s${scene}_voextract.audio`,
|
|
@@ -2503,6 +3023,13 @@ function talkingSceneSatisfied(ctx, entry, scene) {
|
|
|
2503
3023
|
});
|
|
2504
3024
|
}
|
|
2505
3025
|
function checkVideoInvariants(ctx) {
|
|
3026
|
+
checkPromptProfile(ctx);
|
|
3027
|
+
checkImageProfile(ctx);
|
|
3028
|
+
checkPersonGeneration(ctx);
|
|
3029
|
+
checkPromptBudget(ctx);
|
|
3030
|
+
checkRawFaceKeyframe(ctx);
|
|
3031
|
+
checkPromptDecisions(ctx);
|
|
3032
|
+
checkHookLayers(ctx);
|
|
2506
3033
|
const meta = ctx.canvas.metadata?.video;
|
|
2507
3034
|
if (!meta) return;
|
|
2508
3035
|
const segments = [...meta.vo_segments].sort((a, b) => a.start_s - b.start_s);
|
|
@@ -2542,6 +3069,286 @@ function checkVideoInvariants(ctx) {
|
|
|
2542
3069
|
checkBrandmarkInPrompt(ctx);
|
|
2543
3070
|
checkReferenceCompleteness(ctx, meta);
|
|
2544
3071
|
checkClipSpanFitsModel(ctx, meta);
|
|
3072
|
+
checkTimelineContract(ctx, meta);
|
|
3073
|
+
checkNativeSegOverlap(ctx);
|
|
3074
|
+
checkSpineNormalized(ctx, meta);
|
|
3075
|
+
checkOddDimensions(ctx);
|
|
3076
|
+
checkRegionDropped(ctx);
|
|
3077
|
+
}
|
|
3078
|
+
function checkPromptProfile(ctx) {
|
|
3079
|
+
for (const n of ctx.canvas.nodes) {
|
|
3080
|
+
if (n.type !== "video_generate") continue;
|
|
3081
|
+
const model = n.params?.model;
|
|
3082
|
+
if (typeof model !== "string" || clipProfileFor(model)) continue;
|
|
3083
|
+
ctx.issues.push({
|
|
3084
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3085
|
+
code: STAGE_CODES.PROMPT_PROFILE_MISSING,
|
|
3086
|
+
severity: "warning",
|
|
3087
|
+
node_id: n.id,
|
|
3088
|
+
message: `"${n.id}" runs on "${model}", a model with no clip-prompt profile \u2014 its prompt was authored with the default Seedance syntax. Review the dialogue/emotion markup for this model before billing`
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
var TECHNIQUE_CUE = /\b(camera|push[- ]?in|pull[- ]?out|pan|dolly|zoom|tilt|track|handheld|locked|orbit|crane|whip|motion|move)\b/i;
|
|
3093
|
+
var NEGATIVES_CUE = /\bavoid:|\bno subtitles|\bnegative\b|keep it clean and stable/i;
|
|
3094
|
+
var VIBE_CUE = /\b(light|lighting|golden|moody|warm|cool|tone|grade|contrast|shadow|glow|rim|backlit|neon|soft|harsh)\b/i;
|
|
3095
|
+
function checkPromptDecisions(ctx) {
|
|
3096
|
+
for (const n of ctx.canvas.nodes) {
|
|
3097
|
+
if (n.type !== "video_generate") continue;
|
|
3098
|
+
const params = n.params;
|
|
3099
|
+
const prompt = typeof params?.prompt === "string" ? params.prompt : "";
|
|
3100
|
+
if (!prompt) continue;
|
|
3101
|
+
const hasTechnique = TECHNIQUE_CUE.test(prompt);
|
|
3102
|
+
const hasNegatives = NEGATIVES_CUE.test(prompt) || typeof params?.negative_prompt === "string";
|
|
3103
|
+
if (hasTechnique || hasNegatives) continue;
|
|
3104
|
+
ctx.issues.push({
|
|
3105
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3106
|
+
code: STAGE_CODES.PROMPT_DECISION_MISSING,
|
|
3107
|
+
severity: "warning",
|
|
3108
|
+
node_id: n.id,
|
|
3109
|
+
message: `"${n.id}" is a thin clip prompt \u2014 it names no camera/motion move (TECHNIQUE) and no constraints (NEGATIVES: an affirmative "Keep it clean and stable" line, a negative_prompt param, or an "Avoid:" tail). Make the six decisions (route / spec / beats / copy / technique / negatives) so the model doesn't fill the gaps with drift. See prompt-anatomy.md`
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function checkHookLayers(ctx) {
|
|
3114
|
+
const scene0 = ctx.canvas.nodes.filter((n) => /^s0(?:[_a-z0-9])*_/.test(n.id) || n.id === "s0");
|
|
3115
|
+
const hookClip = scene0.find((n) => n.type === "video_generate");
|
|
3116
|
+
if (!hookClip) return;
|
|
3117
|
+
const scene0Text = scene0.map((n) => {
|
|
3118
|
+
const p = n.params?.prompt;
|
|
3119
|
+
return typeof p === "string" ? p : "";
|
|
3120
|
+
}).join("\n");
|
|
3121
|
+
const hookParams = hookClip.params;
|
|
3122
|
+
const hasOverlayLayer = ctx.canvas.nodes.some((n) => /hyperframe/.test(n.type));
|
|
3123
|
+
const hasSound = hookParams?.generate_audio === true || /Dialogue:|Audio:/.test(scene0Text) || ctx.canvas.nodes.some((n) => n.type === "tts" || n.type === "dialogue" || n.type === "music");
|
|
3124
|
+
const hasVisual = scene0Text.trim().length > 40;
|
|
3125
|
+
const hasVibe = VIBE_CUE.test(scene0Text);
|
|
3126
|
+
const missing = [];
|
|
3127
|
+
if (!hasOverlayLayer) missing.push("text (overlay)");
|
|
3128
|
+
if (!hasSound) missing.push("sound (line/SFX)");
|
|
3129
|
+
if (!hasVisual) missing.push("visual (the frame)");
|
|
3130
|
+
if (!hasVibe) missing.push("vibe (lighting/tone)");
|
|
3131
|
+
if (missing.length < 2) return;
|
|
3132
|
+
ctx.issues.push({
|
|
3133
|
+
path: `nodes[${ctx.idToIndex.get(hookClip.id) ?? -1}]`,
|
|
3134
|
+
code: STAGE_CODES.HOOK_LAYER_MISSING,
|
|
3135
|
+
severity: "warning",
|
|
3136
|
+
node_id: hookClip.id,
|
|
3137
|
+
message: `the hook (scene 0) is thin across layers \u2014 missing ${missing.join(", ")}. A scroll-stopping hook works on all four layers (text / sound / visual / vibe); strengthen the missing ones. See hook-craft.md`
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
function checkImageProfile(ctx) {
|
|
3141
|
+
for (const n of ctx.canvas.nodes) {
|
|
3142
|
+
if (n.type !== "image_generate") continue;
|
|
3143
|
+
const model = n.params?.model;
|
|
3144
|
+
if (typeof model !== "string" || imageProfileFor(model)) continue;
|
|
3145
|
+
ctx.issues.push({
|
|
3146
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3147
|
+
code: STAGE_CODES.IMAGE_PROFILE_MISSING,
|
|
3148
|
+
severity: "warning",
|
|
3149
|
+
node_id: n.id,
|
|
3150
|
+
message: `"${n.id}" renders on "${model}", an image model with no frame-prompt profile \u2014 it got the generic template (constraint placement / photoreal cue not tuned). Add an ImageModelProfile for this model`
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
function checkPersonGeneration(ctx) {
|
|
3155
|
+
for (const n of ctx.canvas.nodes) {
|
|
3156
|
+
if (n.type !== "video_generate") continue;
|
|
3157
|
+
const params = n.params;
|
|
3158
|
+
const model = params?.model;
|
|
3159
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "veo") continue;
|
|
3160
|
+
const hasKeyframe = Boolean(n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3161
|
+
if (!hasKeyframe || params?.person_generation) continue;
|
|
3162
|
+
ctx.issues.push({
|
|
3163
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params`,
|
|
3164
|
+
code: STAGE_CODES.PERSON_GENERATION_MISSING,
|
|
3165
|
+
severity: "warning",
|
|
3166
|
+
node_id: n.id,
|
|
3167
|
+
message: `Veo clip "${n.id}" drives from a keyframe but sets no person_generation \u2014 set "allow_adult" (the only legal value for image-to-video, and the only one allowed in the EU/UK)`
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function checkPromptBudget(ctx) {
|
|
3172
|
+
for (const n of ctx.canvas.nodes) {
|
|
3173
|
+
if (n.type !== "video_generate") continue;
|
|
3174
|
+
const params = n.params;
|
|
3175
|
+
const model = params?.model;
|
|
3176
|
+
const prompt = params?.prompt;
|
|
3177
|
+
const profile = typeof model === "string" ? clipProfileFor(model) : void 0;
|
|
3178
|
+
if (!profile || typeof prompt !== "string") continue;
|
|
3179
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean).length;
|
|
3180
|
+
if (words <= profile.wordBudget) continue;
|
|
3181
|
+
ctx.issues.push({
|
|
3182
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3183
|
+
code: STAGE_CODES.PROMPT_OVER_BUDGET,
|
|
3184
|
+
severity: "warning",
|
|
3185
|
+
node_id: n.id,
|
|
3186
|
+
message: `"${n.id}" prompt is ${words} words \u2014 over ${model}'s ~${profile.wordBudget}-word budget; trim it (the frames carry the content, so keep the clip prompt to motion + audio)`
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function checkRawFaceKeyframe(ctx) {
|
|
3191
|
+
for (const n of ctx.canvas.nodes) {
|
|
3192
|
+
if (n.type !== "video_generate") continue;
|
|
3193
|
+
const model = n.params?.model;
|
|
3194
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "seedance") continue;
|
|
3195
|
+
const src = refNodeOf(ctx, n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3196
|
+
if (!src || src.type !== "ingest") continue;
|
|
3197
|
+
ctx.issues.push({
|
|
3198
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].inputs.first_frame`,
|
|
3199
|
+
code: STAGE_CODES.RAW_FACE_KEYFRAME,
|
|
3200
|
+
severity: "warning",
|
|
3201
|
+
node_id: n.id,
|
|
3202
|
+
message: `Seedance clip "${n.id}" animates a RAW ingested image ("${src.id}"), not a generated frame \u2014 if it shows a real human face, ByteDance's real-person filter rejects it (422). Anchor on an AI-generated portrait (recast), or route this clip to Veo`
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function spineTotalS(ctx, spine) {
|
|
3207
|
+
let total = 0;
|
|
3208
|
+
const inputs = Object.entries(spine.inputs ?? {}).filter(([k]) => /^c\d+$/.test(k));
|
|
3209
|
+
if (inputs.length === 0) return null;
|
|
3210
|
+
for (const [, v] of inputs) {
|
|
3211
|
+
const d = liveNodeDurationS(ctx, refNodeOf(ctx, v));
|
|
3212
|
+
if (d === null) return null;
|
|
3213
|
+
total += d;
|
|
3214
|
+
}
|
|
3215
|
+
const graph = spine.params.args?.join?.(" ") ?? "";
|
|
3216
|
+
for (const m of String(graph).matchAll(/xfade=transition=[^:]+:duration=([\d.]+)/g)) {
|
|
3217
|
+
total -= Number(m[1]);
|
|
3218
|
+
}
|
|
3219
|
+
return total;
|
|
3220
|
+
}
|
|
3221
|
+
function checkTimelineContract(ctx, meta) {
|
|
3222
|
+
const stamp = meta.timeline;
|
|
3223
|
+
const spineId = stamp?.spine_node ?? (ctx.idToIndex.has("spine") ? "spine" : null);
|
|
3224
|
+
if (!spineId) return;
|
|
3225
|
+
const expected = stamp?.expected_total_s ?? meta.duration_s;
|
|
3226
|
+
checkSpineTotal(ctx, spineId, expected);
|
|
3227
|
+
checkAudioTimelineTotals(ctx, stamp?.audio_mix_node, expected);
|
|
3228
|
+
}
|
|
3229
|
+
function checkSpineTotal(ctx, spineId, expected) {
|
|
3230
|
+
const spineIdx = ctx.idToIndex.get(spineId);
|
|
3231
|
+
const spine = spineIdx === void 0 ? null : ctx.canvas.nodes[spineIdx];
|
|
3232
|
+
const spineLen = spine ? spineTotalS(ctx, spine) : null;
|
|
3233
|
+
if (spineLen === null || Math.abs(spineLen - expected) <= VIDEO_TIME_SLACK_S) return;
|
|
3234
|
+
ctx.issues.push({
|
|
3235
|
+
path: `nodes[${spineIdx}].params.args`,
|
|
3236
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3237
|
+
node_id: spineId,
|
|
3238
|
+
message: `the picture sums to ${round2(spineLen)}s but the timeline is pinned to ${expected}s \u2014 \`-shortest\` at the final mux will silently cut the longer stream. Re-time the seg trims (or the audio total_ms) so both match`
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
function checkAudioTimelineTotals(ctx, audioMixNode, expected) {
|
|
3242
|
+
const mustBeFullLength = /* @__PURE__ */ new Set();
|
|
3243
|
+
if (audioMixNode) mustBeFullLength.add(audioMixNode);
|
|
3244
|
+
ctx.canvas.nodes.forEach((n) => {
|
|
3245
|
+
if (n.type !== "audio_voice_convert") return;
|
|
3246
|
+
const track = refNodeOf(ctx, (n.inputs ?? {}).audio);
|
|
3247
|
+
if (track?.type === "audio_timeline") mustBeFullLength.add(track.id);
|
|
3248
|
+
});
|
|
3249
|
+
for (const id of mustBeFullLength) {
|
|
3250
|
+
const idx = ctx.idToIndex.get(id);
|
|
3251
|
+
const node = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3252
|
+
const totalMs = node?.params?.total_ms;
|
|
3253
|
+
if (typeof totalMs !== "number") continue;
|
|
3254
|
+
if (Math.abs(totalMs / 1e3 - expected) > VIDEO_TIME_SLACK_S) {
|
|
3255
|
+
ctx.issues.push({
|
|
3256
|
+
path: `nodes[${idx}].params.total_ms`,
|
|
3257
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3258
|
+
node_id: id,
|
|
3259
|
+
message: `audio timeline "${id}" is pinned to ${totalMs}ms but the picture timeline is ${expected}s \u2014 \`-shortest\` at the final mux will silently cut the longer stream. Pin total_ms to ${Math.round(expected * 1e3)}`
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function checkNativeSegOverlap(ctx) {
|
|
3265
|
+
for (const conv of ctx.canvas.nodes) {
|
|
3266
|
+
if (conv.type !== "audio_voice_convert") continue;
|
|
3267
|
+
const track = refNodeOf(ctx, (conv.inputs ?? {}).audio);
|
|
3268
|
+
if (track?.type !== "audio_timeline") continue;
|
|
3269
|
+
const params = track.params;
|
|
3270
|
+
const windows = (params.tracks ?? []).map((t) => {
|
|
3271
|
+
const extract = refNodeOf(ctx, (track.inputs ?? {})[t.slot]);
|
|
3272
|
+
const len = t.duration_s ?? (extract ? ffmpegTrimSeconds(extract) : null);
|
|
3273
|
+
return len === null ? null : { slot: t.slot, start: t.start_s, end: t.start_s + len };
|
|
3274
|
+
}).filter((w) => w !== null).sort((a, b) => a.start - b.start);
|
|
3275
|
+
for (let i = 1; i < windows.length; i++) {
|
|
3276
|
+
const prev = windows[i - 1];
|
|
3277
|
+
const cur = windows[i];
|
|
3278
|
+
if (!prev || !cur || cur.start >= prev.end - 0.01) continue;
|
|
3279
|
+
const trackIdx = ctx.idToIndex.get(track.id);
|
|
3280
|
+
ctx.issues.push({
|
|
3281
|
+
path: `nodes[${trackIdx}].params.tracks`,
|
|
3282
|
+
code: STAGE_CODES.NATIVE_SEG_OVERLAP,
|
|
3283
|
+
node_id: track.id,
|
|
3284
|
+
message: `voice track "${track.id}": "${prev.slot}" runs to ${round2(prev.end)}s but "${cur.slot}" starts at ${cur.start}s \u2014 both play at once (echo). Cap the first with duration_s: ${round2(cur.start - prev.start)} or re-time the windows`
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function checkSpineNormalized(ctx, meta) {
|
|
3290
|
+
const spineId = meta.timeline?.spine_node ?? "spine";
|
|
3291
|
+
const idx = ctx.idToIndex.get(spineId);
|
|
3292
|
+
const spine = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3293
|
+
if (!spine || spine.type !== "ffmpeg") return;
|
|
3294
|
+
const args = spine.params.args;
|
|
3295
|
+
const graph = Array.isArray(args) ? args.join(" ") : "";
|
|
3296
|
+
if (!graph.includes("concat=n=")) return;
|
|
3297
|
+
if (/\[\d+:v\](?:\[|concat)/.test(graph)) {
|
|
3298
|
+
ctx.issues.push({
|
|
3299
|
+
path: `nodes[${idx}].params.args`,
|
|
3300
|
+
code: STAGE_CODES.SPINE_UNNORMALIZED,
|
|
3301
|
+
severity: "warning",
|
|
3302
|
+
node_id: spineId,
|
|
3303
|
+
message: `the spine concat feeds raw input labels \u2014 generated clips carry no fps/SAR guarantee, and one 24fps clip silently stretches the picture off the audio/overlay clock. Chain \`format=yuv420p,fps=30,setsar=1,settb=AVTB\` on every input before the concat`
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function checkOddDimensions(ctx) {
|
|
3308
|
+
ctx.canvas.nodes.forEach((node, idx) => {
|
|
3309
|
+
if (node.type !== "ffmpeg") return;
|
|
3310
|
+
const args = node.params.args;
|
|
3311
|
+
const blob = Array.isArray(args) ? args.join(" ") : "";
|
|
3312
|
+
for (const m of blob.matchAll(/(?:scale|crop|pad)=(\d+):(\d+)/g)) {
|
|
3313
|
+
const w = Number(m[1]);
|
|
3314
|
+
const h = Number(m[2]);
|
|
3315
|
+
if (w % 2 === 0 && h % 2 === 0) continue;
|
|
3316
|
+
ctx.issues.push({
|
|
3317
|
+
path: `nodes[${idx}].params.args`,
|
|
3318
|
+
code: STAGE_CODES.ODD_DIMENSIONS,
|
|
3319
|
+
severity: "warning",
|
|
3320
|
+
node_id: node.id,
|
|
3321
|
+
message: `"${m[0]}" produces an odd dimension \u2014 libx264 yuv420p rejects odd sizes, so this node fails at render time. Round both to even numbers`
|
|
3322
|
+
});
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
function checkRegionDropped(ctx) {
|
|
3328
|
+
const regionClips = /* @__PURE__ */ new Map();
|
|
3329
|
+
for (const node of ctx.canvas.nodes) {
|
|
3330
|
+
const m = /^s(\d+)_r\d+_clip$/.exec(node.id);
|
|
3331
|
+
if (m && node.type === "video_generate")
|
|
3332
|
+
regionClips.set(m[1], (regionClips.get(m[1]) ?? 0) + 1);
|
|
3333
|
+
}
|
|
3334
|
+
for (const [scene, billed] of regionClips) {
|
|
3335
|
+
const idx = ctx.idToIndex.get(`s${scene}_composite`);
|
|
3336
|
+
if (idx === void 0) continue;
|
|
3337
|
+
const composite = ctx.canvas.nodes[idx];
|
|
3338
|
+
const consumed = Object.keys(composite.inputs ?? {}).filter((k) => /^c\d+$/.test(k)).length;
|
|
3339
|
+
if (billed > consumed) {
|
|
3340
|
+
ctx.issues.push({
|
|
3341
|
+
path: `nodes[${idx}].inputs`,
|
|
3342
|
+
code: STAGE_CODES.REGION_DROPPED,
|
|
3343
|
+
severity: "warning",
|
|
3344
|
+
node_id: composite.id,
|
|
3345
|
+
message: `scene s${scene} bills ${billed} region clips but its composite consumes ${consumed} \u2014 the extra generation is paid for and never reaches the frame. Wire every region into the composite or delete the unused clip node`
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function round2(n) {
|
|
3351
|
+
return Math.round(n * 100) / 100;
|
|
2545
3352
|
}
|
|
2546
3353
|
var ELEMENT_TYPE_KEYWORDS = {
|
|
2547
3354
|
animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
|
|
@@ -2669,15 +3476,37 @@ function checkSpeechOverrun(ctx, talkingScenes) {
|
|
|
2669
3476
|
for (const n of ctx.canvas.nodes) {
|
|
2670
3477
|
if (!nativeClipRe.test(n.id) || n.type !== "video_generate") continue;
|
|
2671
3478
|
const overrun = speechOverrunOf(n, secondsPerWord(entry));
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3479
|
+
if (overrun) {
|
|
3480
|
+
ctx.issues.push({
|
|
3481
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3482
|
+
code: STAGE_CODES.SPEECH_OVERRUN,
|
|
3483
|
+
message: `"${n.id}" asks Seedance to speak ~${Math.round(overrun.estSpeechS * 10) / 10}s of dialogue inside a ${overrun.duration}s clip \u2014 the line cannot fit (>${SPEECH_OVERRUN_RATIO}\xD7 the clip). Shorten the line, split the scene, or lengthen the clip duration`
|
|
3484
|
+
});
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
checkSpeechExceedsExtract(ctx, n, entry);
|
|
2678
3488
|
}
|
|
2679
3489
|
}
|
|
2680
3490
|
}
|
|
3491
|
+
function checkSpeechExceedsExtract(ctx, clip, entry) {
|
|
3492
|
+
const params = clip.params;
|
|
3493
|
+
if (params?.generate_audio !== true) return;
|
|
3494
|
+
const line = nativeDialogueOf(params.prompt);
|
|
3495
|
+
if (!line) return;
|
|
3496
|
+
const regionTag = /^s\d+(_r\d+)?_clip$/.exec(clip.id)?.[1] ?? "";
|
|
3497
|
+
const extractIdx = ctx.idToIndex.get(`s${entry.scene}${regionTag}_voextract`) ?? ctx.idToIndex.get(`s${entry.scene}_voextract`);
|
|
3498
|
+
const extract = extractIdx === void 0 ? null : ctx.canvas.nodes[extractIdx];
|
|
3499
|
+
const windowS = extract ? ffmpegTrimSeconds(extract) : null;
|
|
3500
|
+
if (windowS === null) return;
|
|
3501
|
+
const estSpeechS = line.split(/\s+/).filter(Boolean).length * secondsPerWord(entry);
|
|
3502
|
+
if (estSpeechS <= windowS * SPEECH_OVERRUN_RATIO) return;
|
|
3503
|
+
ctx.issues.push({
|
|
3504
|
+
path: `nodes[${ctx.idToIndex.get(clip.id) ?? -1}].params.prompt`,
|
|
3505
|
+
code: STAGE_CODES.SPEECH_EXCEEDS_EXTRACT,
|
|
3506
|
+
node_id: clip.id,
|
|
3507
|
+
message: `"${clip.id}"'s line is ~${Math.round(estSpeechS * 10) / 10}s of speech but its extract window (s${entry.scene}_voextract) is ${windowS}s \u2014 the read gets cut mid-word on the spine. Shorten the Dialogue line, lengthen the scene in prompt.json and re-scaffold, or raise the voextract \`-t\` when the read should carry over the next cutaway`
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
2681
3510
|
var UI_IN_PROMPT_RE = /\bscreen[- ]?(?:recording|capture|grab|share)\b|\bapp (?:interface|screen)\b|\bphone screen overlay\b/i;
|
|
2682
3511
|
function checkUiInPrompt(ctx) {
|
|
2683
3512
|
for (const n of ctx.canvas.nodes) {
|
|
@@ -2750,9 +3579,9 @@ function checkOutputRef(ctx) {
|
|
|
2750
3579
|
function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
2751
3580
|
for (const issue of err.issues) {
|
|
2752
3581
|
const tail2 = pathToString(issue.path);
|
|
2753
|
-
const
|
|
3582
|
+
const path17 = pathPrefix ? tail2 ? `${pathPrefix}.${tail2}` : pathPrefix : tail2;
|
|
2754
3583
|
issues.push({
|
|
2755
|
-
path:
|
|
3584
|
+
path: path17,
|
|
2756
3585
|
code,
|
|
2757
3586
|
message: issue.message,
|
|
2758
3587
|
received: issue.code === "invalid_type" ? issue.received : void 0,
|
|
@@ -2761,8 +3590,8 @@ function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
|
2761
3590
|
});
|
|
2762
3591
|
}
|
|
2763
3592
|
}
|
|
2764
|
-
function pathToString(
|
|
2765
|
-
return
|
|
3593
|
+
function pathToString(path17) {
|
|
3594
|
+
return path17.map((p) => typeof p === "number" ? `[${p}]` : `.${String(p)}`).join("").replace(/^\./, "");
|
|
2766
3595
|
}
|
|
2767
3596
|
function buildDepGraph(canvas) {
|
|
2768
3597
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -2841,6 +3670,7 @@ var Engine = class {
|
|
|
2841
3670
|
cache;
|
|
2842
3671
|
outputsDir;
|
|
2843
3672
|
log;
|
|
3673
|
+
persistAssets;
|
|
2844
3674
|
constructor(opts) {
|
|
2845
3675
|
this.registry = opts.registry;
|
|
2846
3676
|
this.client = opts.client;
|
|
@@ -2848,6 +3678,7 @@ var Engine = class {
|
|
|
2848
3678
|
this.cache = opts.cache;
|
|
2849
3679
|
this.outputsDir = opts.outputsDir;
|
|
2850
3680
|
this.log = opts.log ?? (() => void 0);
|
|
3681
|
+
this.persistAssets = opts.persistAssets ?? false;
|
|
2851
3682
|
}
|
|
2852
3683
|
validate(canvas) {
|
|
2853
3684
|
return validateCanvas(canvas, this.registry);
|
|
@@ -2859,6 +3690,12 @@ var Engine = class {
|
|
|
2859
3690
|
async run(input, opts = {}) {
|
|
2860
3691
|
const validation = await this.validateDeep(input);
|
|
2861
3692
|
if (!validation.ok) throw new ValidationError(validation.issues);
|
|
3693
|
+
if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
|
|
3694
|
+
throw new RunAbortedError(
|
|
3695
|
+
"cost_cap",
|
|
3696
|
+
`estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
2862
3699
|
const canvas = validation.canvas;
|
|
2863
3700
|
const runId = opts.run_id ?? `r_${ulid()}`;
|
|
2864
3701
|
const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
|
|
@@ -2871,7 +3708,16 @@ var Engine = class {
|
|
|
2871
3708
|
const outputs = {};
|
|
2872
3709
|
const counters = { cachedNodes: 0, totalCredits: 0 };
|
|
2873
3710
|
const nodeRuns = [];
|
|
2874
|
-
|
|
3711
|
+
const graph = this.pruneToOutput(canvas, buildGraph(canvas));
|
|
3712
|
+
const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
|
|
3713
|
+
this.emitProgress(opts, {
|
|
3714
|
+
kind: "plan",
|
|
3715
|
+
nodes: [...graph.entries()].map(([id, deps]) => {
|
|
3716
|
+
const node = canvas.nodes.find((n) => n.id === id);
|
|
3717
|
+
return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
|
|
3718
|
+
})
|
|
3719
|
+
});
|
|
3720
|
+
await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes);
|
|
2875
3721
|
const output = pickFinalOutput(canvas, outputs);
|
|
2876
3722
|
const stats = {
|
|
2877
3723
|
total_nodes: canvas.nodes.length,
|
|
@@ -2894,39 +3740,63 @@ var Engine = class {
|
|
|
2894
3740
|
`[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
|
|
2895
3741
|
);
|
|
2896
3742
|
this.log(`outputs in: ${writer.runDir}`);
|
|
2897
|
-
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
|
|
3743
|
+
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
2898
3744
|
}
|
|
2899
|
-
async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
2900
|
-
const layers = topologicalLayers(
|
|
3745
|
+
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
3746
|
+
const layers = topologicalLayers(graph);
|
|
2901
3747
|
const limit = resolveConcurrency(opts.concurrency);
|
|
2902
3748
|
for (const layer of layers) {
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3749
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
|
|
3750
|
+
if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
|
|
3751
|
+
throw new RunAbortedError(
|
|
3752
|
+
"cost_cap",
|
|
3753
|
+
`spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3757
|
+
if (opts.signal?.aborted) {
|
|
3758
|
+
return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
|
|
3759
|
+
}
|
|
3760
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3761
|
+
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
|
|
2907
3762
|
if (r.cached) counters.cachedNodes++;
|
|
2908
3763
|
counters.totalCredits += r.credits;
|
|
2909
3764
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2910
3765
|
if (node) {
|
|
2911
|
-
|
|
3766
|
+
const run = {
|
|
2912
3767
|
node_id: nodeId,
|
|
2913
3768
|
node_type: node.type,
|
|
2914
3769
|
cached: r.cached,
|
|
2915
3770
|
duration_ms: r.durationMs,
|
|
2916
3771
|
credits: r.credits
|
|
2917
|
-
}
|
|
3772
|
+
};
|
|
3773
|
+
nodeRuns.push(run);
|
|
3774
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
2918
3775
|
}
|
|
2919
|
-
})
|
|
2920
|
-
);
|
|
3776
|
+
});
|
|
3777
|
+
});
|
|
2921
3778
|
const failures = [];
|
|
2922
3779
|
settled.forEach((result, i) => {
|
|
2923
3780
|
const nodeId = layer[i];
|
|
2924
|
-
if (result.status === "rejected" && nodeId)
|
|
3781
|
+
if (result.status === "rejected" && nodeId) {
|
|
3782
|
+
if (result.reason instanceof RunAbortedError) return;
|
|
3783
|
+
failures.push({ nodeId, reason: result.reason });
|
|
3784
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
|
|
3785
|
+
}
|
|
2925
3786
|
});
|
|
3787
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
2926
3788
|
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
2927
3789
|
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
2928
3790
|
}
|
|
2929
3791
|
}
|
|
3792
|
+
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
3793
|
+
emitProgress(opts, event) {
|
|
3794
|
+
if (!opts.onProgress) return;
|
|
3795
|
+
try {
|
|
3796
|
+
opts.onProgress(event);
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
2930
3800
|
/**
|
|
2931
3801
|
* Dead-node elimination: when the canvas declares an `output`, execute only the
|
|
2932
3802
|
* nodes that output transitively depends on. Orphaned nodes (left by an edit or
|
|
@@ -2957,12 +3827,13 @@ var Engine = class {
|
|
|
2957
3827
|
}
|
|
2958
3828
|
await writer.writeManifest("_final", output);
|
|
2959
3829
|
}
|
|
2960
|
-
async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
|
|
3830
|
+
async executeOne(canvas, nodeId, outputs, runId, writer, opts, downloadOutputs) {
|
|
2961
3831
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2962
3832
|
if (!node) throw new Error(`executor: missing node ${nodeId}`);
|
|
2963
3833
|
const def = this.registry.get(node.type);
|
|
2964
3834
|
if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
|
|
2965
|
-
const
|
|
3835
|
+
const regenerateToken = resolveRegenerateToken(node, opts.regenerate, runId);
|
|
3836
|
+
const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
|
|
2966
3837
|
const policy = opts.cache_policy ?? "read_write";
|
|
2967
3838
|
if (policy !== "bypass") {
|
|
2968
3839
|
const cacheT0 = Date.now();
|
|
@@ -2980,17 +3851,27 @@ var Engine = class {
|
|
|
2980
3851
|
nodeId: node.id,
|
|
2981
3852
|
nodeType: node.type,
|
|
2982
3853
|
cacheKey: prepared.cacheKey,
|
|
3854
|
+
downloadOutputs,
|
|
2983
3855
|
client: this.client,
|
|
2984
3856
|
assets: this.assets,
|
|
2985
3857
|
log: this.log,
|
|
2986
3858
|
signal: opts.signal
|
|
2987
3859
|
};
|
|
2988
|
-
const
|
|
3860
|
+
const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
|
|
3861
|
+
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
2989
3862
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
2990
3863
|
const elapsed = Date.now() - t0;
|
|
2991
3864
|
const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
|
|
2992
3865
|
const outputsObj = result;
|
|
2993
3866
|
outputs[node.id] = outputsObj;
|
|
3867
|
+
if (this.persistAssets) {
|
|
3868
|
+
try {
|
|
3869
|
+
await persistOutputAssetUrls(outputsObj, ctx);
|
|
3870
|
+
} catch (e) {
|
|
3871
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3872
|
+
this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
2994
3875
|
if (policy === "read_write") {
|
|
2995
3876
|
await this.cache.put({
|
|
2996
3877
|
cacheKey: prepared.cacheKey,
|
|
@@ -3019,8 +3900,42 @@ var Engine = class {
|
|
|
3019
3900
|
}
|
|
3020
3901
|
}
|
|
3021
3902
|
}
|
|
3903
|
+
/**
|
|
3904
|
+
* Download any URL-only asset ref reachable in a local node's inputs so the
|
|
3905
|
+
* bytes are on disk before the local runner stages them. Returns a copy —
|
|
3906
|
+
* refs are replaced, never mutated in place, so the producer's cached output
|
|
3907
|
+
* (shared object) keeps its URL-only shape.
|
|
3908
|
+
*/
|
|
3909
|
+
async materializeLocalInputs(inputs) {
|
|
3910
|
+
const fix = async (value) => {
|
|
3911
|
+
if (Array.isArray(value)) return Promise.all(value.map(fix));
|
|
3912
|
+
if (value && typeof value === "object") {
|
|
3913
|
+
const v = value;
|
|
3914
|
+
if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
|
|
3915
|
+
this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
|
|
3916
|
+
return this.assets.ingestRemote({
|
|
3917
|
+
kind: v.kind,
|
|
3918
|
+
url: v.url,
|
|
3919
|
+
sha256: v.sha256,
|
|
3920
|
+
mime: v.mime,
|
|
3921
|
+
metadata: v.metadata
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
const out = {};
|
|
3925
|
+
for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
|
|
3926
|
+
return out;
|
|
3927
|
+
}
|
|
3928
|
+
return value;
|
|
3929
|
+
};
|
|
3930
|
+
return await fix(inputs);
|
|
3931
|
+
}
|
|
3022
3932
|
};
|
|
3023
|
-
|
|
3933
|
+
function resolveRegenerateToken(node, forced, runId) {
|
|
3934
|
+
if (forced?.has(node.id)) return `run:${runId}`;
|
|
3935
|
+
if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
|
|
3936
|
+
return void 0;
|
|
3937
|
+
}
|
|
3938
|
+
async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
|
|
3024
3939
|
const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
|
|
3025
3940
|
const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
|
|
3026
3941
|
const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
|
|
@@ -3033,6 +3948,9 @@ async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
|
|
|
3033
3948
|
throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
|
|
3034
3949
|
}
|
|
3035
3950
|
}
|
|
3951
|
+
if (regenerateToken !== void 0) {
|
|
3952
|
+
extras = { ...extras ?? {}, __regenerate__: regenerateToken };
|
|
3953
|
+
}
|
|
3036
3954
|
const cacheKey = computeCacheKey({
|
|
3037
3955
|
node_id: node.type,
|
|
3038
3956
|
node_version: def.version,
|
|
@@ -3061,6 +3979,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
|
|
|
3061
3979
|
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3062
3980
|
}
|
|
3063
3981
|
}
|
|
3982
|
+
function needsLocalMaterialization(def) {
|
|
3983
|
+
return def.location === "local" && !def.passthroughRefs;
|
|
3984
|
+
}
|
|
3064
3985
|
function pickFinalOutput(canvas, outputs) {
|
|
3065
3986
|
if (canvas.output) {
|
|
3066
3987
|
const node = outputs[canvas.output.node];
|
|
@@ -3071,6 +3992,16 @@ function pickFinalOutput(canvas, outputs) {
|
|
|
3071
3992
|
const lastOut = outputs[last.id];
|
|
3072
3993
|
return lastOut ? Object.values(lastOut)[0] : void 0;
|
|
3073
3994
|
}
|
|
3995
|
+
function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
3996
|
+
const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
|
|
3997
|
+
const needs = /* @__PURE__ */ new Set();
|
|
3998
|
+
for (const [consumerId, deps] of graph) {
|
|
3999
|
+
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
4000
|
+
if (def?.location !== "local" || def.passthroughRefs) continue;
|
|
4001
|
+
for (const dep of deps) needs.add(dep);
|
|
4002
|
+
}
|
|
4003
|
+
return needs;
|
|
4004
|
+
}
|
|
3074
4005
|
function buildGraph(canvas) {
|
|
3075
4006
|
const graph = /* @__PURE__ */ new Map();
|
|
3076
4007
|
for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
|
|
@@ -3105,11 +4036,11 @@ function hashInputs(inputs) {
|
|
|
3105
4036
|
const out = {};
|
|
3106
4037
|
for (const [k, v] of Object.entries(inputs)) {
|
|
3107
4038
|
if (Array.isArray(v)) {
|
|
3108
|
-
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(el));
|
|
4039
|
+
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(normalizeParamsForCacheKey(el)));
|
|
3109
4040
|
} else {
|
|
3110
4041
|
const s = extractSha(v);
|
|
3111
4042
|
if (s !== null) out[k] = s;
|
|
3112
|
-
else out[k] = canonicalLiteral(v);
|
|
4043
|
+
else out[k] = canonicalLiteral(normalizeParamsForCacheKey(v));
|
|
3113
4044
|
}
|
|
3114
4045
|
}
|
|
3115
4046
|
return out;
|
|
@@ -3201,7 +4132,16 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
|
|
|
3201
4132
|
try {
|
|
3202
4133
|
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3203
4134
|
} catch (e) {
|
|
3204
|
-
|
|
4135
|
+
if (value.url) {
|
|
4136
|
+
try {
|
|
4137
|
+
await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
|
|
4138
|
+
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
4139
|
+
} catch (e2) {
|
|
4140
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
|
|
4141
|
+
}
|
|
4142
|
+
} else {
|
|
4143
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
4144
|
+
}
|
|
3205
4145
|
}
|
|
3206
4146
|
if (bytes.length > MAX_INLINE_TEXT_BYTES) {
|
|
3207
4147
|
throw new NodeExecutionError(nodeId, nodeType, {
|
|
@@ -3261,9 +4201,9 @@ var NodeRegistry = class {
|
|
|
3261
4201
|
|
|
3262
4202
|
// src/engine/nodes/ingest.ts
|
|
3263
4203
|
import { execFile as execFileCb, spawn } from "child_process";
|
|
3264
|
-
import { mkdtemp, readdir, readFile as
|
|
4204
|
+
import { mkdtemp, readdir, readFile as readFile3, rm, stat as stat2 } from "fs/promises";
|
|
3265
4205
|
import { tmpdir } from "os";
|
|
3266
|
-
import
|
|
4206
|
+
import path4 from "path";
|
|
3267
4207
|
import { promisify } from "util";
|
|
3268
4208
|
import { z as z5 } from "zod";
|
|
3269
4209
|
|
|
@@ -3309,27 +4249,6 @@ var FontRef = BaseAssetRef.extend({
|
|
|
3309
4249
|
});
|
|
3310
4250
|
var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
|
|
3311
4251
|
|
|
3312
|
-
// src/engine/nodes/remote/upload.ts
|
|
3313
|
-
async function presignAndPut(args) {
|
|
3314
|
-
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
3315
|
-
const putRes = await fetch(putUrl, {
|
|
3316
|
-
method: "PUT",
|
|
3317
|
-
body: new Uint8Array(args.bytes),
|
|
3318
|
-
headers: { "Content-Type": args.mime },
|
|
3319
|
-
signal: args.ctx.signal
|
|
3320
|
-
});
|
|
3321
|
-
if (!putRes.ok) {
|
|
3322
|
-
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
3323
|
-
}
|
|
3324
|
-
return publicUrl;
|
|
3325
|
-
}
|
|
3326
|
-
async function ensureUploaded(ref, ctx) {
|
|
3327
|
-
if (ref.url) return ref;
|
|
3328
|
-
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
3329
|
-
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
3330
|
-
return { ...ref, url };
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
4252
|
// src/engine/nodes/remote/delegate.ts
|
|
3334
4253
|
function delegated(spec) {
|
|
3335
4254
|
return {
|
|
@@ -3357,7 +4276,9 @@ async function callBackendExec(args) {
|
|
|
3357
4276
|
nodeVersion: args.nodeVersion,
|
|
3358
4277
|
params: args.params,
|
|
3359
4278
|
inputs: serialized,
|
|
3360
|
-
idempotency_key: idempotencyKey
|
|
4279
|
+
idempotency_key: idempotencyKey,
|
|
4280
|
+
canvas_run_id: args.ctx.canvasRunId,
|
|
4281
|
+
node_id: args.ctx.nodeId
|
|
3361
4282
|
},
|
|
3362
4283
|
args.ctx.signal
|
|
3363
4284
|
);
|
|
@@ -3409,6 +4330,9 @@ async function ingestValue(value, ctx, declaredKind) {
|
|
|
3409
4330
|
}
|
|
3410
4331
|
if (isRawAsset(value)) {
|
|
3411
4332
|
const kind = value.kind ?? declaredKind ?? "json";
|
|
4333
|
+
if (ctx.downloadOutputs === false) {
|
|
4334
|
+
return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
|
|
4335
|
+
}
|
|
3412
4336
|
return ctx.assets.ingestRemote({
|
|
3413
4337
|
kind,
|
|
3414
4338
|
url: value.url,
|
|
@@ -3523,7 +4447,7 @@ function safePathname(rawUrl) {
|
|
|
3523
4447
|
}
|
|
3524
4448
|
var ingestNode = defineNode({
|
|
3525
4449
|
id: "ingest",
|
|
3526
|
-
version: "1.
|
|
4450
|
+
version: "1.2.0",
|
|
3527
4451
|
category: "io",
|
|
3528
4452
|
location: "local",
|
|
3529
4453
|
summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Handinger for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
|
|
@@ -3566,6 +4490,9 @@ function runStrategy(strategy, params, ctx) {
|
|
|
3566
4490
|
}
|
|
3567
4491
|
}
|
|
3568
4492
|
async function execDirectFetch(params, ctx) {
|
|
4493
|
+
if (params.expect === "image") {
|
|
4494
|
+
return ingestImageUrl(params.url, ctx);
|
|
4495
|
+
}
|
|
3569
4496
|
const result = await callBackendExec({
|
|
3570
4497
|
nodeType: "ingest",
|
|
3571
4498
|
nodeVersion: ingestNode.version,
|
|
@@ -3576,6 +4503,37 @@ async function execDirectFetch(params, ctx) {
|
|
|
3576
4503
|
});
|
|
3577
4504
|
return assertAssetOutput(result, params.expect);
|
|
3578
4505
|
}
|
|
4506
|
+
async function ingestImageUrl(url, ctx) {
|
|
4507
|
+
const res = await fetch(url);
|
|
4508
|
+
if (!res.ok) {
|
|
4509
|
+
throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
|
|
4510
|
+
}
|
|
4511
|
+
const ab = await res.arrayBuffer();
|
|
4512
|
+
if (ab.byteLength > MAX_ASSET_BYTES) {
|
|
4513
|
+
throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
|
|
4514
|
+
}
|
|
4515
|
+
let normalized;
|
|
4516
|
+
try {
|
|
4517
|
+
normalized = await toModelSafeImage(Buffer.from(ab));
|
|
4518
|
+
} catch (e) {
|
|
4519
|
+
throw localExecError(ctx, `${url}: ${e.message}`);
|
|
4520
|
+
}
|
|
4521
|
+
if (normalized.rasterizedFrom) {
|
|
4522
|
+
ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
|
|
4523
|
+
}
|
|
4524
|
+
return uploadAndIngest({
|
|
4525
|
+
bytes: normalized.bytes,
|
|
4526
|
+
kind: "image",
|
|
4527
|
+
mime: normalized.mime,
|
|
4528
|
+
metadata: {
|
|
4529
|
+
source_url: url,
|
|
4530
|
+
strategy: "direct_fetch",
|
|
4531
|
+
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4532
|
+
...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
|
|
4533
|
+
},
|
|
4534
|
+
ctx
|
|
4535
|
+
});
|
|
4536
|
+
}
|
|
3579
4537
|
async function execHandinger(params, ctx) {
|
|
3580
4538
|
const result = await callBackendExec({
|
|
3581
4539
|
nodeType: "ingest",
|
|
@@ -3603,8 +4561,8 @@ function resolveLocalPath(input) {
|
|
|
3603
4561
|
`ingest: ~ path expansion is not supported (got "${input}"). Use an absolute path or a cwd-relative path.`
|
|
3604
4562
|
);
|
|
3605
4563
|
}
|
|
3606
|
-
if (
|
|
3607
|
-
return
|
|
4564
|
+
if (path4.isAbsolute(input)) return input;
|
|
4565
|
+
return path4.resolve(process.cwd(), input);
|
|
3608
4566
|
}
|
|
3609
4567
|
var EXT_TO_MIME = {
|
|
3610
4568
|
png: "image/png",
|
|
@@ -3612,7 +4570,15 @@ var EXT_TO_MIME = {
|
|
|
3612
4570
|
jpeg: "image/jpeg",
|
|
3613
4571
|
webp: "image/webp",
|
|
3614
4572
|
gif: "image/gif",
|
|
4573
|
+
// Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
|
|
4574
|
+
// must resolve to an image mime here or the kind-check rejects the local file
|
|
4575
|
+
// before normalization ever runs.
|
|
3615
4576
|
avif: "image/avif",
|
|
4577
|
+
heic: "image/heic",
|
|
4578
|
+
heif: "image/heif",
|
|
4579
|
+
tif: "image/tiff",
|
|
4580
|
+
tiff: "image/tiff",
|
|
4581
|
+
bmp: "image/bmp",
|
|
3616
4582
|
mp4: "video/mp4",
|
|
3617
4583
|
webm: "video/webm",
|
|
3618
4584
|
mov: "video/quicktime",
|
|
@@ -3644,7 +4610,7 @@ function sniffSvg(buf) {
|
|
|
3644
4610
|
return head.startsWith("<?xml") ? head.includes("<svg") : head.startsWith("<svg");
|
|
3645
4611
|
}
|
|
3646
4612
|
function inferMimeFromPath(absPath, sniffBytes) {
|
|
3647
|
-
const ext =
|
|
4613
|
+
const ext = path4.extname(absPath).slice(1).toLowerCase();
|
|
3648
4614
|
const fromExt = EXT_TO_MIME[ext];
|
|
3649
4615
|
if (fromExt) return fromExt;
|
|
3650
4616
|
const fromBytes = sniffImageMime(sniffBytes);
|
|
@@ -3663,15 +4629,45 @@ async function rasterizeSvgToPng(bytes) {
|
|
|
3663
4629
|
}
|
|
3664
4630
|
return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
|
|
3665
4631
|
}
|
|
4632
|
+
var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
4633
|
+
async function toModelSafeImage(bytes) {
|
|
4634
|
+
const safe = sniffImageMime(bytes);
|
|
4635
|
+
if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
|
|
4636
|
+
return { bytes, mime: safe };
|
|
4637
|
+
}
|
|
4638
|
+
if (sniffSvg(bytes)) {
|
|
4639
|
+
return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
|
|
4640
|
+
}
|
|
4641
|
+
const { default: sharp } = await import("sharp");
|
|
4642
|
+
try {
|
|
4643
|
+
const img = sharp(bytes);
|
|
4644
|
+
const format = (await img.metadata()).format;
|
|
4645
|
+
const png = await img.png({ force: true }).toBuffer();
|
|
4646
|
+
return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
|
|
4647
|
+
} catch (e) {
|
|
4648
|
+
throw new Error(`bytes are not a decodable image (${e.message})`);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
function hasAscii(buf, offset, sig) {
|
|
4652
|
+
return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
|
|
4653
|
+
}
|
|
4654
|
+
var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
|
|
4655
|
+
function sniffIsoBmff(buf) {
|
|
4656
|
+
if (!hasAscii(buf, 4, "ftyp")) return null;
|
|
4657
|
+
const brand = buf.subarray(8, 12).toString("ascii");
|
|
4658
|
+
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
4659
|
+
if (HEIC_BRANDS.has(brand)) return "image/heic";
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
3666
4662
|
function sniffImageMime(buf) {
|
|
3667
4663
|
if (buf.length < 4) return null;
|
|
3668
|
-
if (buf[0] === 137 && buf
|
|
4664
|
+
if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
|
|
3669
4665
|
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
3670
|
-
if (buf
|
|
3671
|
-
if (buf
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
return
|
|
4666
|
+
if (hasAscii(buf, 0, "GIF")) return "image/gif";
|
|
4667
|
+
if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
|
|
4668
|
+
if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
|
|
4669
|
+
if (hasAscii(buf, 0, "BM")) return "image/bmp";
|
|
4670
|
+
return sniffIsoBmff(buf);
|
|
3675
4671
|
}
|
|
3676
4672
|
function findBoxPayload(buf, start, end, type) {
|
|
3677
4673
|
let offset = start;
|
|
@@ -3724,10 +4720,10 @@ function inferKindFromMime(mime) {
|
|
|
3724
4720
|
if (mime.startsWith("font/")) return "font";
|
|
3725
4721
|
return null;
|
|
3726
4722
|
}
|
|
3727
|
-
function localExecError(ctx,
|
|
4723
|
+
function localExecError(ctx, message2) {
|
|
3728
4724
|
return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
|
|
3729
4725
|
kind: "local",
|
|
3730
|
-
cause: new Error(`ingest: ${
|
|
4726
|
+
cause: new Error(`ingest: ${message2}`)
|
|
3731
4727
|
});
|
|
3732
4728
|
}
|
|
3733
4729
|
async function execLocalFile(params, ctx) {
|
|
@@ -3753,7 +4749,7 @@ async function execLocalFile(params, ctx) {
|
|
|
3753
4749
|
}
|
|
3754
4750
|
let bytes;
|
|
3755
4751
|
try {
|
|
3756
|
-
bytes = await
|
|
4752
|
+
bytes = await readFile3(absPath);
|
|
3757
4753
|
} catch (e) {
|
|
3758
4754
|
if (e.code === "EACCES") {
|
|
3759
4755
|
throw localExecError(ctx, `permission_denied: ${absPath}`);
|
|
@@ -3774,17 +4770,20 @@ async function execLocalFile(params, ctx) {
|
|
|
3774
4770
|
ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
|
|
3775
4771
|
let outBytes = bytes;
|
|
3776
4772
|
let outMime = mime;
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4773
|
+
let rasterizedFrom;
|
|
4774
|
+
if (kind === "image") {
|
|
4775
|
+
const normalized = await toModelSafeImage(bytes);
|
|
4776
|
+
outBytes = normalized.bytes;
|
|
4777
|
+
outMime = normalized.mime;
|
|
4778
|
+
rasterizedFrom = normalized.rasterizedFrom;
|
|
4779
|
+
if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
|
|
3781
4780
|
}
|
|
3782
4781
|
const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
|
|
3783
4782
|
const ref = await uploadAndIngest({
|
|
3784
4783
|
bytes: outBytes,
|
|
3785
4784
|
kind: params.expect,
|
|
3786
4785
|
mime: outMime,
|
|
3787
|
-
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
|
|
4786
|
+
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
|
|
3788
4787
|
ctx
|
|
3789
4788
|
});
|
|
3790
4789
|
return withProbedDuration(ref, durationMs);
|
|
@@ -3801,8 +4800,8 @@ function localFileMetadata(args) {
|
|
|
3801
4800
|
strategy: "local_file",
|
|
3802
4801
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3803
4802
|
file_size: args.fileSize,
|
|
3804
|
-
original_filename:
|
|
3805
|
-
...args.
|
|
4803
|
+
original_filename: path4.basename(args.absPath),
|
|
4804
|
+
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
3806
4805
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
3807
4806
|
};
|
|
3808
4807
|
}
|
|
@@ -3825,7 +4824,7 @@ async function execYtDlp(params, ctx) {
|
|
|
3825
4824
|
if (params.expect !== "video" && params.expect !== "audio") {
|
|
3826
4825
|
throw new Error(`ingest: yt_dlp only handles video/audio, got expect=${params.expect}`);
|
|
3827
4826
|
}
|
|
3828
|
-
const workDir = await mkdtemp(
|
|
4827
|
+
const workDir = await mkdtemp(path4.join(tmpdir(), "ingest-yt-"));
|
|
3829
4828
|
try {
|
|
3830
4829
|
const { filePath, info } = await runYtDlp({
|
|
3831
4830
|
url: params.url,
|
|
@@ -3840,7 +4839,7 @@ async function execYtDlp(params, ctx) {
|
|
|
3840
4839
|
`file_too_large: yt-dlp output for ${params.url} is ${downloadedStats.size} bytes (limit ${MAX_ASSET_BYTES})`
|
|
3841
4840
|
);
|
|
3842
4841
|
}
|
|
3843
|
-
const bytes = await
|
|
4842
|
+
const bytes = await readFile3(filePath);
|
|
3844
4843
|
const kind = params.expect;
|
|
3845
4844
|
const mime = YT_DLP_MIME[kind];
|
|
3846
4845
|
const metadata = buildYtDlpMetadata(params.url, info);
|
|
@@ -3852,8 +4851,8 @@ async function execYtDlp(params, ctx) {
|
|
|
3852
4851
|
}
|
|
3853
4852
|
}
|
|
3854
4853
|
async function runYtDlp(args) {
|
|
3855
|
-
const outTemplate =
|
|
3856
|
-
const infoPath =
|
|
4854
|
+
const outTemplate = path4.join(args.workDir, "out.%(ext)s");
|
|
4855
|
+
const infoPath = path4.join(args.workDir, "out.info.json");
|
|
3857
4856
|
const argv = [
|
|
3858
4857
|
args.url,
|
|
3859
4858
|
"--no-playlist",
|
|
@@ -3886,11 +4885,11 @@ ${tail(stderr, 40)}`)
|
|
|
3886
4885
|
}
|
|
3887
4886
|
let info = {};
|
|
3888
4887
|
try {
|
|
3889
|
-
const raw = await
|
|
4888
|
+
const raw = await readFile3(infoPath, "utf-8");
|
|
3890
4889
|
info = JSON.parse(raw);
|
|
3891
4890
|
} catch {
|
|
3892
4891
|
}
|
|
3893
|
-
return { filePath:
|
|
4892
|
+
return { filePath: path4.join(args.workDir, downloaded), info };
|
|
3894
4893
|
}
|
|
3895
4894
|
function buildYtDlpMetadata(sourceUrl, info) {
|
|
3896
4895
|
const out = {
|
|
@@ -3979,9 +4978,9 @@ import { z as z6 } from "zod";
|
|
|
3979
4978
|
|
|
3980
4979
|
// src/engine/nodes/local/lib/cli-runner.ts
|
|
3981
4980
|
import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
|
|
3982
|
-
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as
|
|
4981
|
+
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, stat as stat3 } from "fs/promises";
|
|
3983
4982
|
import { tmpdir as tmpdir2 } from "os";
|
|
3984
|
-
import
|
|
4983
|
+
import path5 from "path";
|
|
3985
4984
|
import { promisify as promisify2 } from "util";
|
|
3986
4985
|
var execFile2 = promisify2(execFileCb2);
|
|
3987
4986
|
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -4008,7 +5007,7 @@ function mimeForExt(ext) {
|
|
|
4008
5007
|
}
|
|
4009
5008
|
function extForAssetRef(ref) {
|
|
4010
5009
|
if (ref.path) {
|
|
4011
|
-
const e =
|
|
5010
|
+
const e = path5.extname(ref.path);
|
|
4012
5011
|
if (e) return e;
|
|
4013
5012
|
}
|
|
4014
5013
|
const reverse = {
|
|
@@ -4030,14 +5029,14 @@ function planArrayInputSlot(tmpDir, slot, values, lookup, stagedInputs) {
|
|
|
4030
5029
|
const ref = values[i];
|
|
4031
5030
|
if (!ref) continue;
|
|
4032
5031
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot}[${i}] has no local path`);
|
|
4033
|
-
const dest =
|
|
5032
|
+
const dest = path5.join(tmpDir, `in_${slot}_${i}${extForAssetRef(ref)}`);
|
|
4034
5033
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4035
5034
|
lookup.set(`in.${slot}.${i}`, dest);
|
|
4036
5035
|
}
|
|
4037
5036
|
}
|
|
4038
5037
|
function planSingleInputSlot(tmpDir, slot, ref, lookup, stagedInputs) {
|
|
4039
5038
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot} has no local path`);
|
|
4040
|
-
const dest =
|
|
5039
|
+
const dest = path5.join(tmpDir, `in_${slot}${extForAssetRef(ref)}`);
|
|
4041
5040
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4042
5041
|
lookup.set(`in.${slot}`, dest);
|
|
4043
5042
|
}
|
|
@@ -4045,7 +5044,7 @@ function planOutputs(tmpDir, outputs, lookup) {
|
|
|
4045
5044
|
const outputPaths = [];
|
|
4046
5045
|
for (const [name, spec] of Object.entries(outputs)) {
|
|
4047
5046
|
const ext = spec.ext.startsWith(".") ? spec.ext : `.${spec.ext}`;
|
|
4048
|
-
const absPath =
|
|
5047
|
+
const absPath = path5.join(tmpDir, `out_${name}${ext}`);
|
|
4049
5048
|
outputPaths.push({ name, absPath, spec });
|
|
4050
5049
|
lookup.set(`out.${name}`, absPath);
|
|
4051
5050
|
}
|
|
@@ -4086,8 +5085,8 @@ function rejectRawPaths(substituted, original, stagingDir) {
|
|
|
4086
5085
|
throw new Error(`cli-runner: home-relative path "${original}" not allowed in args.`);
|
|
4087
5086
|
}
|
|
4088
5087
|
if (substituted.startsWith("/")) {
|
|
4089
|
-
const resolved =
|
|
4090
|
-
if (!resolved.startsWith(`${stagingDir}${
|
|
5088
|
+
const resolved = path5.resolve(substituted);
|
|
5089
|
+
if (!resolved.startsWith(`${stagingDir}${path5.sep}`) && resolved !== stagingDir) {
|
|
4091
5090
|
throw new Error(
|
|
4092
5091
|
`cli-runner: raw filesystem path "${original}" not allowed \u2014 declare an input slot and use {{in.<slot>}} instead.`
|
|
4093
5092
|
);
|
|
@@ -4137,7 +5136,7 @@ function tailLines(text, maxLines) {
|
|
|
4137
5136
|
}
|
|
4138
5137
|
async function runCli(opts) {
|
|
4139
5138
|
const { bin, args, inputs, outputs, ctx, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
4140
|
-
const tmpDir = await mkdtemp2(
|
|
5139
|
+
const tmpDir = await mkdtemp2(path5.join(tmpdir2(), `cli-${bin.replace(/[^a-z0-9]/gi, "")}-`));
|
|
4141
5140
|
try {
|
|
4142
5141
|
const { lookup, stagedInputs, outputPaths } = planPlaceholders(tmpDir, inputs, outputs);
|
|
4143
5142
|
await stageInputs(stagedInputs);
|
|
@@ -4159,7 +5158,7 @@ ${tailLines(stderr, 40)}`);
|
|
|
4159
5158
|
if (!s?.isFile() || s.size === 0) {
|
|
4160
5159
|
throw new Error(`cli-runner: declared output "${name}" missing or empty at ${absPath}`);
|
|
4161
5160
|
}
|
|
4162
|
-
const bytes = await
|
|
5161
|
+
const bytes = await readFile4(absPath);
|
|
4163
5162
|
const ref = await ctx.assets.ingestBytes({
|
|
4164
5163
|
bytes: Buffer.from(bytes),
|
|
4165
5164
|
kind: spec.kind,
|
|
@@ -4199,6 +5198,12 @@ var Track = z6.object({
|
|
|
4199
5198
|
slot: z6.string().min(1),
|
|
4200
5199
|
/** When this track starts on the timeline, seconds from 0. */
|
|
4201
5200
|
start_s: z6.number().min(0),
|
|
5201
|
+
/**
|
|
5202
|
+
* Optional hard cap on this track's length, seconds. The clip is trimmed BEFORE
|
|
5203
|
+
* placement, so a source that runs long (an over-long voice extract, a converted
|
|
5204
|
+
* track that came back oversized) cannot bleed into the next track's window.
|
|
5205
|
+
*/
|
|
5206
|
+
duration_s: z6.number().positive().optional(),
|
|
4202
5207
|
/** Optional level adjustment in dB (negative ducks, e.g. a music bed at -12). */
|
|
4203
5208
|
gain_db: z6.number().optional()
|
|
4204
5209
|
}).strict();
|
|
@@ -4255,7 +5260,10 @@ function buildAudioTimelineArgs(params) {
|
|
|
4255
5260
|
params.tracks.forEach((track, i) => {
|
|
4256
5261
|
inputArgs.push("-i", `{{in.${track.slot}}}`);
|
|
4257
5262
|
const delayMs = Math.round(track.start_s * 1e3);
|
|
4258
|
-
const steps = [
|
|
5263
|
+
const steps = [
|
|
5264
|
+
...track.duration_s !== void 0 ? [`atrim=0:${track.duration_s}`] : [],
|
|
5265
|
+
`adelay=${delayMs}:all=1`
|
|
5266
|
+
];
|
|
4259
5267
|
if (track.gain_db !== void 0) steps.push(`volume=${track.gain_db}dB`);
|
|
4260
5268
|
const label = `a${i}`;
|
|
4261
5269
|
filterChains.push(`[${i}:a]${steps.join(",")}[${label}]`);
|
|
@@ -4273,11 +5281,11 @@ function buildAudioTimelineArgs(params) {
|
|
|
4273
5281
|
}
|
|
4274
5282
|
var audioTimelineNode = defineNode({
|
|
4275
5283
|
id: "audio_timeline",
|
|
4276
|
-
version: "1.
|
|
5284
|
+
version: "1.2.0",
|
|
4277
5285
|
category: "audio",
|
|
4278
5286
|
location: "local",
|
|
4279
5287
|
summary: "Place and mix several audio clips onto one timeline: each track starts at a given second (optionally level-adjusted in dB), then they're combined into a single track. Built for laying a music bed plus timed voiceover lines and sound effects under a video.",
|
|
4280
|
-
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, gain_db?}
|
|
5288
|
+
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, duration_s?, gain_db?}` (`duration_s` hard-caps a clip so it can't bleed into the next track's window). Set `total_ms` to pin the final length to the video. Requires `ffmpeg` on PATH.",
|
|
4281
5289
|
inputs: AudioTimelineInputs,
|
|
4282
5290
|
params: AudioTimelineParams,
|
|
4283
5291
|
outputs: AudioTimelineOutputs,
|
|
@@ -4329,19 +5337,56 @@ var audioTimelineNode = defineNode({
|
|
|
4329
5337
|
}
|
|
4330
5338
|
});
|
|
4331
5339
|
|
|
4332
|
-
// src/engine/nodes/local/
|
|
5340
|
+
// src/engine/nodes/local/collect.ts
|
|
4333
5341
|
import { z as z7 } from "zod";
|
|
5342
|
+
var collectNode = defineNode({
|
|
5343
|
+
id: "collect",
|
|
5344
|
+
version: "1.0.0",
|
|
5345
|
+
category: "data",
|
|
5346
|
+
location: "local",
|
|
5347
|
+
passthroughRefs: true,
|
|
5348
|
+
summary: "Gather images from multiple upstream nodes into one ordered array \u2014 the standard terminal for multi-variant canvases whose final output is several images.",
|
|
5349
|
+
when_to_use: "Point the canvas `output` at this node when several independent branches (e.g. one image_generate per scene/variant) must ALL be finals. Wire `inputs.images` as an array of refs like `$ref:gen_billboard_03.images#0` \u2014 each final inherits its producer node id as its label (or set `params.labels` to override), so variants stay identifiable in the dashboard and selection.",
|
|
5350
|
+
inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5351
|
+
params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
|
|
5352
|
+
outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5353
|
+
outputKinds: { images: "image" },
|
|
5354
|
+
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5355
|
+
// Arity is only knowable at validate time when `images` is a literal array;
|
|
5356
|
+
// a single `$ref:` string to an upstream array output defers to runtime.
|
|
5357
|
+
validateExtra: ({ rawParams, rawInputs }) => {
|
|
5358
|
+
const labels = rawParams?.labels;
|
|
5359
|
+
if (!Array.isArray(labels)) return [];
|
|
5360
|
+
if (new Set(labels).size !== labels.length) {
|
|
5361
|
+
return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
|
|
5362
|
+
}
|
|
5363
|
+
const images = rawInputs?.images;
|
|
5364
|
+
if (Array.isArray(images) && labels.length !== images.length) {
|
|
5365
|
+
return [
|
|
5366
|
+
{
|
|
5367
|
+
path: "params.labels",
|
|
5368
|
+
message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
|
|
5369
|
+
}
|
|
5370
|
+
];
|
|
5371
|
+
}
|
|
5372
|
+
return [];
|
|
5373
|
+
},
|
|
5374
|
+
execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
|
|
5375
|
+
});
|
|
5376
|
+
|
|
5377
|
+
// src/engine/nodes/local/ffmpeg.ts
|
|
5378
|
+
import { z as z8 } from "zod";
|
|
4334
5379
|
var FFMPEG_BIN2 = "ffmpeg";
|
|
4335
|
-
var OutputDecl =
|
|
4336
|
-
kind:
|
|
4337
|
-
ext:
|
|
5380
|
+
var OutputDecl = z8.object({
|
|
5381
|
+
kind: z8.enum(["image", "video", "audio"]),
|
|
5382
|
+
ext: z8.string().min(1).max(8)
|
|
4338
5383
|
}).strict();
|
|
4339
|
-
var FfmpegParams =
|
|
4340
|
-
args:
|
|
4341
|
-
outputs:
|
|
5384
|
+
var FfmpegParams = z8.object({
|
|
5385
|
+
args: z8.array(z8.string()).min(1),
|
|
5386
|
+
outputs: z8.record(z8.string(), OutputDecl).default({})
|
|
4342
5387
|
}).strict();
|
|
4343
|
-
var FfmpegInputs =
|
|
4344
|
-
var FfmpegOutputs =
|
|
5388
|
+
var FfmpegInputs = z8.record(z8.string(), z8.unknown());
|
|
5389
|
+
var FfmpegOutputs = z8.record(z8.string(), z8.custom());
|
|
4345
5390
|
var ffmpegNode = defineNode({
|
|
4346
5391
|
id: "ffmpeg",
|
|
4347
5392
|
version: "2.0.0",
|
|
@@ -4371,17 +5416,17 @@ var ffmpegNode = defineNode({
|
|
|
4371
5416
|
import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4372
5417
|
import { createRequire } from "module";
|
|
4373
5418
|
import { tmpdir as tmpdir3 } from "os";
|
|
4374
|
-
import
|
|
4375
|
-
import { z as
|
|
5419
|
+
import path7 from "path";
|
|
5420
|
+
import { z as z9 } from "zod";
|
|
4376
5421
|
|
|
4377
5422
|
// src/engine/nodes/local/lib/assets.ts
|
|
4378
|
-
import { copyFile as copyFile3, readFile as
|
|
4379
|
-
import
|
|
5423
|
+
import { copyFile as copyFile3, readFile as readFile5 } from "fs/promises";
|
|
5424
|
+
import path6 from "path";
|
|
4380
5425
|
async function stageAsset(ref, destDir, filename) {
|
|
4381
5426
|
if (!ref.path) {
|
|
4382
5427
|
throw new Error(`stageAsset: ref (${ref.kind}/${ref.mime}) has no local path`);
|
|
4383
5428
|
}
|
|
4384
|
-
const dest =
|
|
5429
|
+
const dest = path6.join(destDir, filename);
|
|
4385
5430
|
await copyFile3(ref.path, dest);
|
|
4386
5431
|
return dest;
|
|
4387
5432
|
}
|
|
@@ -4390,11 +5435,11 @@ async function refToUrl(ref) {
|
|
|
4390
5435
|
if (!ref.path) {
|
|
4391
5436
|
throw new Error("refToUrl: AssetRef has neither url nor path");
|
|
4392
5437
|
}
|
|
4393
|
-
const bytes = await
|
|
5438
|
+
const bytes = await readFile5(ref.path);
|
|
4394
5439
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4395
5440
|
}
|
|
4396
5441
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
4397
|
-
function
|
|
5442
|
+
function isAssetRefLike2(value) {
|
|
4398
5443
|
if (!value || typeof value !== "object") return false;
|
|
4399
5444
|
const v = value;
|
|
4400
5445
|
return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
|
|
@@ -4408,15 +5453,15 @@ var DEFAULT_SPECIMEN = [
|
|
|
4408
5453
|
"abcdefghijklmnopqrstuvwxyz",
|
|
4409
5454
|
`0123456789 !?&@#$%().,:;'"-`
|
|
4410
5455
|
].join("\n");
|
|
4411
|
-
var FontSpecimenParams =
|
|
4412
|
-
text:
|
|
4413
|
-
font_size:
|
|
4414
|
-
padding:
|
|
4415
|
-
line_height:
|
|
4416
|
-
max_width:
|
|
5456
|
+
var FontSpecimenParams = z9.object({
|
|
5457
|
+
text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
|
|
5458
|
+
font_size: z9.number().int().min(8).max(512).optional().default(72),
|
|
5459
|
+
padding: z9.number().int().min(0).max(512).optional().default(64),
|
|
5460
|
+
line_height: z9.number().min(0.8).max(3).optional().default(1.35),
|
|
5461
|
+
max_width: z9.number().int().min(256).max(4096).optional()
|
|
4417
5462
|
}).strict();
|
|
4418
|
-
var FontSpecimenInputs =
|
|
4419
|
-
var FontSpecimenOutputs =
|
|
5463
|
+
var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
|
|
5464
|
+
var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
|
|
4420
5465
|
var DEVICE_SCALE_FACTOR = 2;
|
|
4421
5466
|
var PAGE_TIMEOUT_MS = 3e4;
|
|
4422
5467
|
function escapeHtml(text) {
|
|
@@ -4464,11 +5509,11 @@ var fontSpecimenNode = defineNode({
|
|
|
4464
5509
|
outputKinds: { image: "image" },
|
|
4465
5510
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
4466
5511
|
async execute({ inputs, params, ctx }) {
|
|
4467
|
-
const tmp = await mkdtemp3(
|
|
5512
|
+
const tmp = await mkdtemp3(path7.join(tmpdir3(), "font-specimen-"));
|
|
4468
5513
|
try {
|
|
4469
5514
|
const fontFilename = `font.${extForMime(inputs.font.mime)}`;
|
|
4470
5515
|
await stageAsset(inputs.font, tmp, fontFilename);
|
|
4471
|
-
const entryPath =
|
|
5516
|
+
const entryPath = path7.join(tmp, "index.html");
|
|
4472
5517
|
await writeFile3(entryPath, buildSpecimenHtml(params, fontFilename), "utf-8");
|
|
4473
5518
|
ctx.log(`rendering specimen (${params.font_size}px, ${params.text.split("\n").length} lines)`);
|
|
4474
5519
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -4552,16 +5597,16 @@ var fontSpecimenNode = defineNode({
|
|
|
4552
5597
|
|
|
4553
5598
|
// src/engine/nodes/local/hyperframe.ts
|
|
4554
5599
|
import { execFile as execFile4 } from "child_process";
|
|
4555
|
-
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as
|
|
5600
|
+
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as readFile9, rm as rm4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
4556
5601
|
import { createRequire as createRequire2 } from "module";
|
|
4557
5602
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
4558
|
-
import
|
|
5603
|
+
import path12 from "path";
|
|
4559
5604
|
import { promisify as promisify4 } from "util";
|
|
4560
|
-
import { z as
|
|
5605
|
+
import { z as z11 } from "zod";
|
|
4561
5606
|
|
|
4562
5607
|
// src/engine/engine/composition-hash.ts
|
|
4563
|
-
import { readdir as readdir2, readFile as
|
|
4564
|
-
import
|
|
5608
|
+
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
5609
|
+
import path8 from "path";
|
|
4565
5610
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".cache", ".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
4566
5611
|
function isSkippedName(name) {
|
|
4567
5612
|
if (name.startsWith(".")) return true;
|
|
@@ -4578,79 +5623,79 @@ async function collectFiles(root, current) {
|
|
|
4578
5623
|
const names = await readdir2(current);
|
|
4579
5624
|
for (const name of names) {
|
|
4580
5625
|
if (isSkippedName(name)) continue;
|
|
4581
|
-
const abs =
|
|
5626
|
+
const abs = path8.join(current, name);
|
|
4582
5627
|
const s = await stat4(abs);
|
|
4583
5628
|
if (s.isDirectory()) {
|
|
4584
5629
|
out.push(...await collectFiles(root, abs));
|
|
4585
5630
|
continue;
|
|
4586
5631
|
}
|
|
4587
5632
|
if (!s.isFile()) continue;
|
|
4588
|
-
const bytes = await
|
|
4589
|
-
const relPath =
|
|
5633
|
+
const bytes = await readFile6(abs);
|
|
5634
|
+
const relPath = path8.relative(root, abs).split(path8.sep).join("/");
|
|
4590
5635
|
out.push({ relPath, contentSha: sha256Hex(bytes) });
|
|
4591
5636
|
}
|
|
4592
5637
|
return out;
|
|
4593
5638
|
}
|
|
4594
5639
|
|
|
4595
5640
|
// src/engine/engine/composition-meta.ts
|
|
4596
|
-
import { readFile as
|
|
4597
|
-
import
|
|
4598
|
-
import { z as
|
|
4599
|
-
var InputKind =
|
|
4600
|
-
var InputSpec =
|
|
5641
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5642
|
+
import path9 from "path";
|
|
5643
|
+
import { z as z10 } from "zod";
|
|
5644
|
+
var InputKind = z10.enum(["video", "image", "audio", "json"]);
|
|
5645
|
+
var InputSpec = z10.object({
|
|
4601
5646
|
kind: InputKind,
|
|
4602
|
-
required:
|
|
5647
|
+
required: z10.boolean().optional().default(false),
|
|
4603
5648
|
// Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
|
|
4604
5649
|
// Defaults to `<key><ext>` derived from the kind.
|
|
4605
|
-
staged_as:
|
|
4606
|
-
description:
|
|
5650
|
+
staged_as: z10.string().min(1).optional(),
|
|
5651
|
+
description: z10.string().optional()
|
|
4607
5652
|
}).strict();
|
|
4608
5653
|
var ParamSpecBase = {
|
|
4609
|
-
required:
|
|
4610
|
-
description:
|
|
5654
|
+
required: z10.boolean().optional().default(false),
|
|
5655
|
+
description: z10.string().optional()
|
|
4611
5656
|
};
|
|
4612
|
-
var StringParam =
|
|
5657
|
+
var StringParam = z10.object({
|
|
4613
5658
|
...ParamSpecBase,
|
|
4614
|
-
kind:
|
|
4615
|
-
default:
|
|
4616
|
-
enum:
|
|
5659
|
+
kind: z10.literal("string"),
|
|
5660
|
+
default: z10.string().optional(),
|
|
5661
|
+
enum: z10.array(z10.string()).optional()
|
|
4617
5662
|
}).strict();
|
|
4618
|
-
var IntegerParam =
|
|
5663
|
+
var IntegerParam = z10.object({
|
|
4619
5664
|
...ParamSpecBase,
|
|
4620
|
-
kind:
|
|
4621
|
-
default:
|
|
4622
|
-
min:
|
|
4623
|
-
max:
|
|
5665
|
+
kind: z10.literal("integer"),
|
|
5666
|
+
default: z10.number().int().optional(),
|
|
5667
|
+
min: z10.number().int().optional(),
|
|
5668
|
+
max: z10.number().int().optional()
|
|
4624
5669
|
}).strict();
|
|
4625
|
-
var NumberParam =
|
|
5670
|
+
var NumberParam = z10.object({
|
|
4626
5671
|
...ParamSpecBase,
|
|
4627
|
-
kind:
|
|
4628
|
-
default:
|
|
4629
|
-
min:
|
|
4630
|
-
max:
|
|
5672
|
+
kind: z10.literal("number"),
|
|
5673
|
+
default: z10.number().optional(),
|
|
5674
|
+
min: z10.number().optional(),
|
|
5675
|
+
max: z10.number().optional()
|
|
4631
5676
|
}).strict();
|
|
4632
|
-
var BooleanParam =
|
|
5677
|
+
var BooleanParam = z10.object({
|
|
4633
5678
|
...ParamSpecBase,
|
|
4634
|
-
kind:
|
|
4635
|
-
default:
|
|
5679
|
+
kind: z10.literal("boolean"),
|
|
5680
|
+
default: z10.boolean().optional()
|
|
4636
5681
|
}).strict();
|
|
4637
|
-
var ColorParam =
|
|
5682
|
+
var ColorParam = z10.object({
|
|
4638
5683
|
...ParamSpecBase,
|
|
4639
|
-
kind:
|
|
4640
|
-
default:
|
|
5684
|
+
kind: z10.literal("color"),
|
|
5685
|
+
default: z10.string().optional()
|
|
4641
5686
|
}).strict();
|
|
4642
|
-
var ImageParam =
|
|
5687
|
+
var ImageParam = z10.object({
|
|
4643
5688
|
...ParamSpecBase,
|
|
4644
|
-
kind:
|
|
4645
|
-
default:
|
|
5689
|
+
kind: z10.literal("image"),
|
|
5690
|
+
default: z10.string().optional()
|
|
4646
5691
|
}).strict();
|
|
4647
|
-
var JsonParam =
|
|
5692
|
+
var JsonParam = z10.object({
|
|
4648
5693
|
...ParamSpecBase,
|
|
4649
|
-
kind:
|
|
4650
|
-
schema:
|
|
4651
|
-
default:
|
|
5694
|
+
kind: z10.literal("json"),
|
|
5695
|
+
schema: z10.unknown().optional(),
|
|
5696
|
+
default: z10.unknown().optional()
|
|
4652
5697
|
}).strict();
|
|
4653
|
-
var ParamSpec =
|
|
5698
|
+
var ParamSpec = z10.discriminatedUnion("kind", [
|
|
4654
5699
|
StringParam,
|
|
4655
5700
|
IntegerParam,
|
|
4656
5701
|
NumberParam,
|
|
@@ -4659,22 +5704,22 @@ var ParamSpec = z9.discriminatedUnion("kind", [
|
|
|
4659
5704
|
ImageParam,
|
|
4660
5705
|
JsonParam
|
|
4661
5706
|
]);
|
|
4662
|
-
var CompositionMetaSchema =
|
|
4663
|
-
id:
|
|
4664
|
-
title:
|
|
4665
|
-
description:
|
|
4666
|
-
width:
|
|
4667
|
-
height:
|
|
4668
|
-
fps:
|
|
4669
|
-
default_duration:
|
|
4670
|
-
inputs:
|
|
4671
|
-
params:
|
|
5707
|
+
var CompositionMetaSchema = z10.object({
|
|
5708
|
+
id: z10.string().min(1),
|
|
5709
|
+
title: z10.string().min(1),
|
|
5710
|
+
description: z10.string().optional(),
|
|
5711
|
+
width: z10.number().int().positive(),
|
|
5712
|
+
height: z10.number().int().positive(),
|
|
5713
|
+
fps: z10.number().int().positive().default(30),
|
|
5714
|
+
default_duration: z10.number().positive().default(10),
|
|
5715
|
+
inputs: z10.record(z10.string(), InputSpec).default({}),
|
|
5716
|
+
params: z10.record(z10.string(), ParamSpec).default({})
|
|
4672
5717
|
}).strict();
|
|
4673
5718
|
async function loadCompositionMeta(compositionDir) {
|
|
4674
|
-
const metaPath =
|
|
5719
|
+
const metaPath = path9.join(compositionDir, "meta.json");
|
|
4675
5720
|
let raw;
|
|
4676
5721
|
try {
|
|
4677
|
-
raw = await
|
|
5722
|
+
raw = await readFile7(metaPath, "utf-8");
|
|
4678
5723
|
} catch (e) {
|
|
4679
5724
|
throw new Error(`composition meta: cannot read ${metaPath} (${e.message})`);
|
|
4680
5725
|
}
|
|
@@ -4696,39 +5741,39 @@ function buildParamsSchema(meta) {
|
|
|
4696
5741
|
for (const [name, spec] of Object.entries(meta.params)) {
|
|
4697
5742
|
shape[name] = buildParamFieldSchema(name, spec);
|
|
4698
5743
|
}
|
|
4699
|
-
return
|
|
5744
|
+
return z10.object(shape).strict();
|
|
4700
5745
|
}
|
|
4701
5746
|
function buildParamFieldSchema(name, spec) {
|
|
4702
5747
|
switch (spec.kind) {
|
|
4703
5748
|
case "string": {
|
|
4704
|
-
const s = spec.enum && spec.enum.length > 0 ?
|
|
5749
|
+
const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
|
|
4705
5750
|
return finalize(s, spec.default, spec.required);
|
|
4706
5751
|
}
|
|
4707
5752
|
case "integer": {
|
|
4708
|
-
let s =
|
|
5753
|
+
let s = z10.number().int();
|
|
4709
5754
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
4710
5755
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
4711
5756
|
return finalize(s, spec.default, spec.required);
|
|
4712
5757
|
}
|
|
4713
5758
|
case "number": {
|
|
4714
|
-
let s =
|
|
5759
|
+
let s = z10.number();
|
|
4715
5760
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
4716
5761
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
4717
5762
|
return finalize(s, spec.default, spec.required);
|
|
4718
5763
|
}
|
|
4719
5764
|
case "boolean":
|
|
4720
|
-
return finalize(
|
|
5765
|
+
return finalize(z10.boolean(), spec.default, spec.required);
|
|
4721
5766
|
case "color": {
|
|
4722
|
-
const s =
|
|
5767
|
+
const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
4723
5768
|
message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
|
|
4724
5769
|
});
|
|
4725
5770
|
return finalize(s, spec.default, spec.required);
|
|
4726
5771
|
}
|
|
4727
5772
|
case "image":
|
|
4728
|
-
return finalize(
|
|
5773
|
+
return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
|
|
4729
5774
|
case "json":
|
|
4730
5775
|
return finalize(
|
|
4731
|
-
|
|
5776
|
+
z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
|
|
4732
5777
|
spec.default,
|
|
4733
5778
|
spec.required
|
|
4734
5779
|
);
|
|
@@ -4752,8 +5797,8 @@ function defaultFilenameForInput(key, kind) {
|
|
|
4752
5797
|
|
|
4753
5798
|
// src/engine/nodes/local/lib/hyperframe-check.ts
|
|
4754
5799
|
import { execFile as execFile3 } from "child_process";
|
|
4755
|
-
import { readFile as
|
|
4756
|
-
import
|
|
5800
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5801
|
+
import path10 from "path";
|
|
4757
5802
|
import { promisify as promisify3 } from "util";
|
|
4758
5803
|
var execFileAsync = promisify3(execFile3);
|
|
4759
5804
|
var NEVER_BLOCK = [
|
|
@@ -4766,8 +5811,8 @@ var NEVER_BLOCK = [
|
|
|
4766
5811
|
/text[_-]?occluded/i
|
|
4767
5812
|
];
|
|
4768
5813
|
var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
|
|
4769
|
-
function isAdvisory(code,
|
|
4770
|
-
const hay = `${code} ${
|
|
5814
|
+
function isAdvisory(code, message2) {
|
|
5815
|
+
const hay = `${code} ${message2}`;
|
|
4771
5816
|
return NEVER_BLOCK.some((re) => re.test(hay));
|
|
4772
5817
|
}
|
|
4773
5818
|
function parseCheckJson(raw) {
|
|
@@ -4795,10 +5840,10 @@ function classifyLint(json) {
|
|
|
4795
5840
|
for (const f of findings) {
|
|
4796
5841
|
const rec = f;
|
|
4797
5842
|
const code = String(rec?.code ?? "");
|
|
4798
|
-
const
|
|
5843
|
+
const message2 = String(rec?.message ?? "");
|
|
4799
5844
|
const severity = String(rec?.severity ?? "info");
|
|
4800
|
-
const blocking = severity === "error" && !isAdvisory(code,
|
|
4801
|
-
out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
|
|
5845
|
+
const blocking = severity === "error" && !isAdvisory(code, message2);
|
|
5846
|
+
out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
|
|
4802
5847
|
}
|
|
4803
5848
|
return out;
|
|
4804
5849
|
}
|
|
@@ -4810,9 +5855,9 @@ function classifyInspect(json) {
|
|
|
4810
5855
|
for (const iss of issues) {
|
|
4811
5856
|
const rec = iss;
|
|
4812
5857
|
const code = String(rec?.code ?? rec?.type ?? "overflow");
|
|
4813
|
-
const
|
|
5858
|
+
const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
|
|
4814
5859
|
const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
|
|
4815
|
-
out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
|
|
5860
|
+
out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
|
|
4816
5861
|
}
|
|
4817
5862
|
return out;
|
|
4818
5863
|
}
|
|
@@ -4905,7 +5950,7 @@ ${detail}`);
|
|
|
4905
5950
|
}
|
|
4906
5951
|
let indexHtml = "";
|
|
4907
5952
|
try {
|
|
4908
|
-
indexHtml = await
|
|
5953
|
+
indexHtml = await readFile8(path10.join(dir, "index.html"), "utf-8");
|
|
4909
5954
|
} catch {
|
|
4910
5955
|
indexHtml = "";
|
|
4911
5956
|
}
|
|
@@ -4970,9 +6015,9 @@ ${stderr.slice(0, 1500)}`;
|
|
|
4970
6015
|
|
|
4971
6016
|
// src/engine/nodes/local/lib/hyperframe-meta.ts
|
|
4972
6017
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
4973
|
-
import
|
|
6018
|
+
import path11 from "path";
|
|
4974
6019
|
async function ensureHyperframesMetaJson(tmp, nodeId, meta, duration) {
|
|
4975
|
-
const metaPath =
|
|
6020
|
+
const metaPath = path11.join(tmp, "meta.json");
|
|
4976
6021
|
await writeFile4(
|
|
4977
6022
|
metaPath,
|
|
4978
6023
|
JSON.stringify(
|
|
@@ -5028,17 +6073,17 @@ function literalize(value) {
|
|
|
5028
6073
|
// src/engine/nodes/local/hyperframe.ts
|
|
5029
6074
|
var execFileAsync2 = promisify4(execFile4);
|
|
5030
6075
|
var require_2 = createRequire2(import.meta.url);
|
|
5031
|
-
var HyperframeParams =
|
|
5032
|
-
composition:
|
|
6076
|
+
var HyperframeParams = z11.object({
|
|
6077
|
+
composition: z11.string().min(1),
|
|
5033
6078
|
// Output container. mp4 (default) for delivery; webm/mov render WITH
|
|
5034
6079
|
// transparency (alpha) when the composition background is transparent —
|
|
5035
6080
|
// use for motion-graphic overlays dropped into Premiere/AE/Nuke.
|
|
5036
|
-
format:
|
|
5037
|
-
timeout_ms:
|
|
5038
|
-
}).catchall(
|
|
5039
|
-
var HyperframeInputs =
|
|
5040
|
-
var HyperframeOutputs =
|
|
5041
|
-
video:
|
|
6081
|
+
format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
|
|
6082
|
+
timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
|
|
6083
|
+
}).catchall(z11.unknown());
|
|
6084
|
+
var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
|
|
6085
|
+
var HyperframeOutputs = z11.object({
|
|
6086
|
+
video: z11.custom()
|
|
5042
6087
|
}).strict();
|
|
5043
6088
|
var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
|
|
5044
6089
|
var MIME_BY_FORMAT = {
|
|
@@ -5072,7 +6117,7 @@ var hyperframeRenderNode = defineNode({
|
|
|
5072
6117
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5073
6118
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5074
6119
|
const compositionParams = validateAndParseDynamicParams(meta, params);
|
|
5075
|
-
const tmp = await mkdtemp4(
|
|
6120
|
+
const tmp = await mkdtemp4(path12.join(tmpdir4(), "hf-render-"));
|
|
5076
6121
|
try {
|
|
5077
6122
|
await copyComposition(compositionDir, tmp);
|
|
5078
6123
|
await vendorGsap(tmp, ctx);
|
|
@@ -5082,9 +6127,9 @@ var hyperframeRenderNode = defineNode({
|
|
|
5082
6127
|
await substituteCompositionFiles(tmp, substitutionValues);
|
|
5083
6128
|
await ensureHyperframesMetaJson(tmp, ctx.nodeId, meta, duration);
|
|
5084
6129
|
await runHyperframesCheck({ dir: tmp, nodeId: "hyperframe_render", ctx, timeoutMs: params.timeout_ms });
|
|
5085
|
-
const outputPath =
|
|
6130
|
+
const outputPath = path12.join(tmp, `output.${params.format}`);
|
|
5086
6131
|
await runRender({ tmp, outputPath, params, meta, ctx });
|
|
5087
|
-
const bytes = await
|
|
6132
|
+
const bytes = await readFile9(outputPath);
|
|
5088
6133
|
ctx.log(`rendered ${bytes.length} bytes`);
|
|
5089
6134
|
const ref = await ctx.assets.ingestBytes({
|
|
5090
6135
|
bytes: Buffer.from(bytes),
|
|
@@ -5106,10 +6151,10 @@ var hyperframeRenderNode = defineNode({
|
|
|
5106
6151
|
}
|
|
5107
6152
|
});
|
|
5108
6153
|
async function resolveCompositionDir(composition) {
|
|
5109
|
-
const compositionPath =
|
|
6154
|
+
const compositionPath = path12.isAbsolute(composition) ? composition : path12.resolve(process.cwd(), composition);
|
|
5110
6155
|
const s = await stat5(compositionPath);
|
|
5111
6156
|
if (s.isDirectory()) return compositionPath;
|
|
5112
|
-
return
|
|
6157
|
+
return path12.dirname(compositionPath);
|
|
5113
6158
|
}
|
|
5114
6159
|
async function validateComposition(rawParams) {
|
|
5115
6160
|
const issues = await validateCompositionParams(rawParams);
|
|
@@ -5191,7 +6236,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5191
6236
|
await cp(srcDir, destDir, {
|
|
5192
6237
|
recursive: true,
|
|
5193
6238
|
filter: (src) => {
|
|
5194
|
-
const name =
|
|
6239
|
+
const name = path12.basename(src);
|
|
5195
6240
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5196
6241
|
return true;
|
|
5197
6242
|
}
|
|
@@ -5200,7 +6245,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5200
6245
|
async function vendorGsap(tmp, ctx) {
|
|
5201
6246
|
try {
|
|
5202
6247
|
const gsapMin = require_2.resolve("gsap/dist/gsap.min.js");
|
|
5203
|
-
await copyFile4(gsapMin,
|
|
6248
|
+
await copyFile4(gsapMin, path12.join(tmp, "gsap.min.js"));
|
|
5204
6249
|
} catch (e) {
|
|
5205
6250
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5206
6251
|
}
|
|
@@ -5215,7 +6260,7 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
|
|
|
5215
6260
|
await stageAsset(ref, tmp, filename);
|
|
5216
6261
|
ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
|
|
5217
6262
|
if (spec.kind === "video" && primaryDuration === null) {
|
|
5218
|
-
primaryDuration = await probeDurationSeconds(
|
|
6263
|
+
primaryDuration = await probeDurationSeconds(path12.join(tmp, filename));
|
|
5219
6264
|
}
|
|
5220
6265
|
}
|
|
5221
6266
|
return primaryDuration;
|
|
@@ -5257,12 +6302,12 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
|
|
|
5257
6302
|
}
|
|
5258
6303
|
function coerceImageParam(value) {
|
|
5259
6304
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5260
|
-
if (
|
|
6305
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5261
6306
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5262
6307
|
}
|
|
5263
6308
|
async function substituteCompositionFiles(tmp, values) {
|
|
5264
|
-
const entryPath =
|
|
5265
|
-
const original = await
|
|
6309
|
+
const entryPath = path12.join(tmp, "index.html");
|
|
6310
|
+
const original = await readFile9(entryPath, "utf-8");
|
|
5266
6311
|
const { output, missing } = substituteVariables(original, values);
|
|
5267
6312
|
if (missing.length > 0) {
|
|
5268
6313
|
throw new Error(
|
|
@@ -5278,7 +6323,7 @@ function workerCount() {
|
|
|
5278
6323
|
async function runRender(opts) {
|
|
5279
6324
|
const { tmp, outputPath, params, meta, ctx } = opts;
|
|
5280
6325
|
const args = buildRenderArgs(tmp, outputPath, meta, params.format);
|
|
5281
|
-
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${
|
|
6326
|
+
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${path12.basename(tmp)}`);
|
|
5282
6327
|
try {
|
|
5283
6328
|
await execFileAsync2("npx", args, { timeout: params.timeout_ms, maxBuffer: 64 * 1024 * 1024 });
|
|
5284
6329
|
} catch (e) {
|
|
@@ -5322,28 +6367,28 @@ async function probeDurationSeconds(filePath) {
|
|
|
5322
6367
|
|
|
5323
6368
|
// src/engine/nodes/local/hyperframe-snapshot.ts
|
|
5324
6369
|
import { execFile as execFile5 } from "child_process";
|
|
5325
|
-
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as
|
|
6370
|
+
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as readFile10, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
5326
6371
|
import { createRequire as createRequire3 } from "module";
|
|
5327
6372
|
import { tmpdir as tmpdir5 } from "os";
|
|
5328
|
-
import
|
|
6373
|
+
import path13 from "path";
|
|
5329
6374
|
import { promisify as promisify5 } from "util";
|
|
5330
|
-
import { z as
|
|
6375
|
+
import { z as z12 } from "zod";
|
|
5331
6376
|
var _execFileAsync = promisify5(execFile5);
|
|
5332
6377
|
var require_3 = createRequire3(import.meta.url);
|
|
5333
|
-
var WaitForSpec =
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
6378
|
+
var WaitForSpec = z12.discriminatedUnion("kind", [
|
|
6379
|
+
z12.object({ kind: z12.literal("auto") }),
|
|
6380
|
+
z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
|
|
6381
|
+
z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
|
|
6382
|
+
z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
|
|
5338
6383
|
]);
|
|
5339
|
-
var HyperframeSnapshotParams =
|
|
5340
|
-
composition:
|
|
6384
|
+
var HyperframeSnapshotParams = z12.object({
|
|
6385
|
+
composition: z12.string().min(1),
|
|
5341
6386
|
wait_for: WaitForSpec.optional().default({ kind: "auto" }),
|
|
5342
|
-
timeout_ms:
|
|
5343
|
-
}).catchall(
|
|
5344
|
-
var HyperframeSnapshotInputs =
|
|
5345
|
-
var HyperframeSnapshotOutputs =
|
|
5346
|
-
image:
|
|
6387
|
+
timeout_ms: z12.number().int().positive().optional().default(6e4)
|
|
6388
|
+
}).catchall(z12.unknown());
|
|
6389
|
+
var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
|
|
6390
|
+
var HyperframeSnapshotOutputs = z12.object({
|
|
6391
|
+
image: z12.custom()
|
|
5347
6392
|
}).strict();
|
|
5348
6393
|
var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
|
|
5349
6394
|
var DEVICE_SCALE_FACTOR2 = 2;
|
|
@@ -5372,7 +6417,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5372
6417
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5373
6418
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5374
6419
|
const compositionParams = validateAndParseDynamicParams2(meta, params);
|
|
5375
|
-
const tmp = await mkdtemp5(
|
|
6420
|
+
const tmp = await mkdtemp5(path13.join(tmpdir5(), "hf-snap-"));
|
|
5376
6421
|
try {
|
|
5377
6422
|
await copyComposition2(compositionDir, tmp);
|
|
5378
6423
|
await vendorGsap2(tmp, ctx);
|
|
@@ -5387,7 +6432,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5387
6432
|
timeoutMs: params.timeout_ms,
|
|
5388
6433
|
samples: 1
|
|
5389
6434
|
});
|
|
5390
|
-
const entryPath =
|
|
6435
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
5391
6436
|
const entryUrl = `file://${entryPath}`;
|
|
5392
6437
|
ctx.log(`snapshotting ${meta.width}x${meta.height}@${DEVICE_SCALE_FACTOR2}x wait=${params.wait_for.kind}`);
|
|
5393
6438
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -5448,7 +6493,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5448
6493
|
await cp(srcDir, destDir, {
|
|
5449
6494
|
recursive: true,
|
|
5450
6495
|
filter: (src) => {
|
|
5451
|
-
const name =
|
|
6496
|
+
const name = path13.basename(src);
|
|
5452
6497
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5453
6498
|
return true;
|
|
5454
6499
|
}
|
|
@@ -5457,7 +6502,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5457
6502
|
async function vendorGsap2(tmp, ctx) {
|
|
5458
6503
|
try {
|
|
5459
6504
|
const gsapMin = require_3.resolve("gsap/dist/gsap.min.js");
|
|
5460
|
-
await copyFile5(gsapMin,
|
|
6505
|
+
await copyFile5(gsapMin, path13.join(tmp, "gsap.min.js"));
|
|
5461
6506
|
} catch (e) {
|
|
5462
6507
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5463
6508
|
}
|
|
@@ -5487,12 +6532,12 @@ async function buildSubstitutionValues2(compositionParams, meta) {
|
|
|
5487
6532
|
}
|
|
5488
6533
|
function coerceImageParam2(value) {
|
|
5489
6534
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5490
|
-
if (
|
|
6535
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5491
6536
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5492
6537
|
}
|
|
5493
6538
|
async function substituteCompositionFiles2(tmp, values) {
|
|
5494
|
-
const entryPath =
|
|
5495
|
-
const original = await
|
|
6539
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
6540
|
+
const original = await readFile10(entryPath, "utf-8");
|
|
5496
6541
|
const { output, missing } = substituteVariables(original, values);
|
|
5497
6542
|
if (missing.length > 0) {
|
|
5498
6543
|
throw new Error(
|
|
@@ -5535,18 +6580,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
|
|
|
5535
6580
|
// src/engine/nodes/local/imagemagick.ts
|
|
5536
6581
|
import { execFile as execFile6 } from "child_process";
|
|
5537
6582
|
import { promisify as promisify6 } from "util";
|
|
5538
|
-
import { z as
|
|
6583
|
+
import { z as z13 } from "zod";
|
|
5539
6584
|
var execFileAsync3 = promisify6(execFile6);
|
|
5540
|
-
var OutputDecl2 =
|
|
5541
|
-
kind:
|
|
5542
|
-
ext:
|
|
6585
|
+
var OutputDecl2 = z13.object({
|
|
6586
|
+
kind: z13.enum(["image", "video", "audio"]),
|
|
6587
|
+
ext: z13.string().min(1).max(8)
|
|
5543
6588
|
}).strict();
|
|
5544
|
-
var ImageMagickParams =
|
|
5545
|
-
args:
|
|
5546
|
-
outputs:
|
|
6589
|
+
var ImageMagickParams = z13.object({
|
|
6590
|
+
args: z13.array(z13.string()).min(1),
|
|
6591
|
+
outputs: z13.record(z13.string(), OutputDecl2).default({})
|
|
5547
6592
|
}).strict();
|
|
5548
|
-
var ImageMagickInputs =
|
|
5549
|
-
var ImageMagickOutputs =
|
|
6593
|
+
var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
|
|
6594
|
+
var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
|
|
5550
6595
|
var resolvedBin;
|
|
5551
6596
|
async function resolveBin() {
|
|
5552
6597
|
if (resolvedBin) return resolvedBin;
|
|
@@ -5588,29 +6633,29 @@ var imagemagickNode = defineNode({
|
|
|
5588
6633
|
});
|
|
5589
6634
|
|
|
5590
6635
|
// src/engine/nodes/local/text.ts
|
|
5591
|
-
import { z as
|
|
6636
|
+
import { z as z14 } from "zod";
|
|
5592
6637
|
var textNode = defineNode({
|
|
5593
6638
|
id: "text",
|
|
5594
6639
|
version: "1.0.0",
|
|
5595
6640
|
category: "data",
|
|
5596
6641
|
location: "local",
|
|
5597
6642
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
5598
|
-
inputs:
|
|
5599
|
-
params:
|
|
5600
|
-
outputs:
|
|
6643
|
+
inputs: z14.object({}).strict(),
|
|
6644
|
+
params: z14.object({ value: z14.string() }).strict(),
|
|
6645
|
+
outputs: z14.object({ text: z14.string() }).strict(),
|
|
5601
6646
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5602
6647
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
5603
6648
|
});
|
|
5604
6649
|
|
|
5605
6650
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
5606
|
-
import { z as
|
|
5607
|
-
var AudioVoiceConvertParams =
|
|
5608
|
-
model:
|
|
6651
|
+
import { z as z15 } from "zod";
|
|
6652
|
+
var AudioVoiceConvertParams = z15.object({
|
|
6653
|
+
model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
5609
6654
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
5610
|
-
voice:
|
|
5611
|
-
output_format:
|
|
6655
|
+
voice: z15.string().min(1),
|
|
6656
|
+
output_format: z15.string().optional(),
|
|
5612
6657
|
/** Strip the source clip's background noise before re-voicing. */
|
|
5613
|
-
remove_background_noise:
|
|
6658
|
+
remove_background_noise: z15.boolean().optional()
|
|
5614
6659
|
}).strict();
|
|
5615
6660
|
var audioVoiceConvertNode = delegated({
|
|
5616
6661
|
id: "audio_voice_convert",
|
|
@@ -5618,44 +6663,44 @@ var audioVoiceConvertNode = delegated({
|
|
|
5618
6663
|
category: "audio",
|
|
5619
6664
|
summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
|
|
5620
6665
|
when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
|
|
5621
|
-
inputs:
|
|
6666
|
+
inputs: z15.object({
|
|
5622
6667
|
audio: AudioRef,
|
|
5623
6668
|
voice_ref: TextRef.optional()
|
|
5624
6669
|
}).strict(),
|
|
5625
6670
|
params: AudioVoiceConvertParams,
|
|
5626
|
-
outputs:
|
|
6671
|
+
outputs: z15.object({ audio: AudioRef }).strict(),
|
|
5627
6672
|
outputKinds: { audio: "audio" },
|
|
5628
6673
|
cost: () => ({ credits: 1, seconds_estimate: 20 })
|
|
5629
6674
|
});
|
|
5630
6675
|
|
|
5631
6676
|
// src/engine/nodes/remote/dialogue.ts
|
|
5632
|
-
import { z as
|
|
5633
|
-
var DialogueInput =
|
|
5634
|
-
text:
|
|
5635
|
-
voice_id:
|
|
6677
|
+
import { z as z16 } from "zod";
|
|
6678
|
+
var DialogueInput = z16.object({
|
|
6679
|
+
text: z16.string().min(1),
|
|
6680
|
+
voice_id: z16.string().min(1)
|
|
5636
6681
|
});
|
|
5637
6682
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
5638
|
-
var DialogueParams =
|
|
5639
|
-
model:
|
|
6683
|
+
var DialogueParams = z16.object({
|
|
6684
|
+
model: z16.enum(DIALOGUE_MODELS),
|
|
5640
6685
|
/**
|
|
5641
6686
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
5642
6687
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
5643
6688
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
5644
6689
|
*/
|
|
5645
|
-
inputs:
|
|
5646
|
-
language_code:
|
|
6690
|
+
inputs: z16.array(DialogueInput).min(1).max(50),
|
|
6691
|
+
language_code: z16.string().optional(),
|
|
5647
6692
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
5648
|
-
settings:
|
|
5649
|
-
seed:
|
|
5650
|
-
apply_text_normalization:
|
|
6693
|
+
settings: z16.record(z16.string(), z16.unknown()).optional(),
|
|
6694
|
+
seed: z16.number().int().min(0).max(4294967295).optional(),
|
|
6695
|
+
apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
|
|
5651
6696
|
/**
|
|
5652
6697
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
5653
6698
|
* separate `timestamps` output — character-level alignment plus
|
|
5654
6699
|
* per-voice segment markers usable for captions, lipsync, or
|
|
5655
6700
|
* beat-matched cuts in ad creatives.
|
|
5656
6701
|
*/
|
|
5657
|
-
with_timestamps:
|
|
5658
|
-
output_format:
|
|
6702
|
+
with_timestamps: z16.boolean().optional(),
|
|
6703
|
+
output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5659
6704
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
5660
6705
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
5661
6706
|
path: ["inputs"]
|
|
@@ -5666,9 +6711,9 @@ var dialogueNode = delegated({
|
|
|
5666
6711
|
category: "audio",
|
|
5667
6712
|
summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
|
|
5668
6713
|
when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
|
|
5669
|
-
inputs:
|
|
6714
|
+
inputs: z16.object({}).loose(),
|
|
5670
6715
|
params: DialogueParams,
|
|
5671
|
-
outputs:
|
|
6716
|
+
outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
5672
6717
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
5673
6718
|
cost: ({ params }) => {
|
|
5674
6719
|
const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
|
|
@@ -5677,7 +6722,7 @@ var dialogueNode = delegated({
|
|
|
5677
6722
|
});
|
|
5678
6723
|
|
|
5679
6724
|
// src/engine/nodes/remote/image.ts
|
|
5680
|
-
import { z as
|
|
6725
|
+
import { z as z17 } from "zod";
|
|
5681
6726
|
var IMAGE_GENERATE_MODELS2 = [
|
|
5682
6727
|
"openai/gpt-5.4-image-2",
|
|
5683
6728
|
"google/gemini-3.5-flash",
|
|
@@ -5685,41 +6730,44 @@ var IMAGE_GENERATE_MODELS2 = [
|
|
|
5685
6730
|
"google/gemini-3-pro-image-preview",
|
|
5686
6731
|
"recraft/recraft-v4.1-pro-vector"
|
|
5687
6732
|
];
|
|
5688
|
-
var ImageGenerateParams =
|
|
5689
|
-
model:
|
|
5690
|
-
prompt:
|
|
5691
|
-
aspect_ratio:
|
|
5692
|
-
image_size:
|
|
6733
|
+
var ImageGenerateParams = z17.object({
|
|
6734
|
+
model: z17.enum(IMAGE_GENERATE_MODELS2),
|
|
6735
|
+
prompt: z17.string().min(1),
|
|
6736
|
+
aspect_ratio: z17.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
|
|
6737
|
+
image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
|
|
6738
|
+
// Rendering quality — forwarded into `image_config`. OpenRouter models without a
|
|
6739
|
+
// quality knob ignore it; the registry gates which models accept it (gpt-image, Gemini).
|
|
6740
|
+
quality: z17.enum(["auto", "low", "medium", "high"]).optional(),
|
|
5693
6741
|
// Recraft v4 vector controls — forwarded into `image_config`. Registry
|
|
5694
6742
|
// rejects them on non-Recraft models.
|
|
5695
|
-
strength:
|
|
5696
|
-
rgb_colors:
|
|
5697
|
-
background_rgb_color:
|
|
6743
|
+
strength: z17.number().min(0).max(1).optional(),
|
|
6744
|
+
rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
|
|
6745
|
+
background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
|
|
5698
6746
|
}).strict();
|
|
5699
6747
|
var imageGenerateNode = delegated({
|
|
5700
6748
|
id: "image_generate",
|
|
5701
|
-
version: "2.
|
|
6749
|
+
version: "2.2.0",
|
|
5702
6750
|
category: "image",
|
|
5703
6751
|
summary: "Generate images for ad creatives. Curated model set: GPT-5.4 Image, Gemini 3.5 Flash, Gemini 3.1 Flash Image Preview, Gemini 3 Pro Image, Recraft v4.1 Pro Vector. Per-model param support comes from the canvas-engine model registry.",
|
|
5704
6752
|
when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-5.4-image-2` for photorealistic; Gemini variants for fast iteration and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order.",
|
|
5705
6753
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
5706
6754
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
5707
|
-
inputs:
|
|
6755
|
+
inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
|
|
5708
6756
|
params: ImageGenerateParams,
|
|
5709
|
-
outputs:
|
|
6757
|
+
outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
|
|
5710
6758
|
outputKinds: { images: "image" },
|
|
5711
6759
|
cost: () => ({ credits: 5, seconds_estimate: 10 })
|
|
5712
6760
|
});
|
|
5713
6761
|
|
|
5714
6762
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
5715
|
-
import { z as
|
|
6763
|
+
import { z as z18 } from "zod";
|
|
5716
6764
|
var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
5717
6765
|
var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
5718
|
-
var ImageAspectAdaptParams =
|
|
5719
|
-
model:
|
|
5720
|
-
formats:
|
|
5721
|
-
guidance:
|
|
5722
|
-
image_size:
|
|
6766
|
+
var ImageAspectAdaptParams = z18.object({
|
|
6767
|
+
model: z18.enum(ASPECT_ADAPT_MODELS),
|
|
6768
|
+
formats: z18.array(z18.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }),
|
|
6769
|
+
guidance: z18.string().min(1).optional(),
|
|
6770
|
+
image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
5723
6771
|
}).strict();
|
|
5724
6772
|
var imageAspectAdaptNode = delegated({
|
|
5725
6773
|
id: "image_aspect_adapt",
|
|
@@ -5727,9 +6775,9 @@ var imageAspectAdaptNode = delegated({
|
|
|
5727
6775
|
category: "image",
|
|
5728
6776
|
summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as `formats`.",
|
|
5729
6777
|
when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and list the target ratios in `formats`. Cost is estimated per format; formats matching the source ratio are free pass-throughs. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
|
|
5730
|
-
inputs:
|
|
6778
|
+
inputs: z18.object({ source: ImageRef }).loose(),
|
|
5731
6779
|
params: ImageAspectAdaptParams,
|
|
5732
|
-
outputs:
|
|
6780
|
+
outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
|
|
5733
6781
|
outputKinds: { images: "image" },
|
|
5734
6782
|
cost: ({ params }) => {
|
|
5735
6783
|
const p = params;
|
|
@@ -5742,12 +6790,12 @@ var imageAspectAdaptNode = delegated({
|
|
|
5742
6790
|
});
|
|
5743
6791
|
|
|
5744
6792
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
5745
|
-
import { z as
|
|
5746
|
-
var ImageBackgroundRemoveParams =
|
|
5747
|
-
model:
|
|
5748
|
-
model_variant:
|
|
5749
|
-
operating_resolution:
|
|
5750
|
-
mask_only:
|
|
6793
|
+
import { z as z19 } from "zod";
|
|
6794
|
+
var ImageBackgroundRemoveParams = z19.object({
|
|
6795
|
+
model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
|
|
6796
|
+
model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
|
|
6797
|
+
operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
6798
|
+
mask_only: z19.boolean().optional().default(false)
|
|
5751
6799
|
}).strict();
|
|
5752
6800
|
var imageBackgroundRemoveNode = delegated({
|
|
5753
6801
|
id: "image_background_remove",
|
|
@@ -5755,11 +6803,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
5755
6803
|
category: "image",
|
|
5756
6804
|
summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
|
|
5757
6805
|
when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. Set `mask_only:true` to return the binary mask instead of the alpha-cut image.",
|
|
5758
|
-
inputs:
|
|
6806
|
+
inputs: z19.object({
|
|
5759
6807
|
image: ImageRef
|
|
5760
6808
|
}).strict(),
|
|
5761
6809
|
params: ImageBackgroundRemoveParams,
|
|
5762
|
-
outputs:
|
|
6810
|
+
outputs: z19.object({
|
|
5763
6811
|
image: ImageRef,
|
|
5764
6812
|
mask: ImageRef.optional()
|
|
5765
6813
|
}).strict(),
|
|
@@ -5768,7 +6816,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
5768
6816
|
});
|
|
5769
6817
|
|
|
5770
6818
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
5771
|
-
import { z as
|
|
6819
|
+
import { z as z20 } from "zod";
|
|
5772
6820
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
5773
6821
|
var imageDescribeNode = delegated({
|
|
5774
6822
|
id: "image_describe",
|
|
@@ -5776,33 +6824,33 @@ var imageDescribeNode = delegated({
|
|
|
5776
6824
|
category: "vision",
|
|
5777
6825
|
summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
|
|
5778
6826
|
when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
|
|
5779
|
-
inputs:
|
|
5780
|
-
params:
|
|
5781
|
-
model:
|
|
5782
|
-
focus:
|
|
5783
|
-
context:
|
|
5784
|
-
temperature:
|
|
5785
|
-
max_tokens:
|
|
6827
|
+
inputs: z20.object({ image: ImageRef }).loose(),
|
|
6828
|
+
params: z20.object({
|
|
6829
|
+
model: z20.enum(IMAGE_DESCRIBE_MODELS),
|
|
6830
|
+
focus: z20.string().optional(),
|
|
6831
|
+
context: z20.string().optional(),
|
|
6832
|
+
temperature: z20.number().min(0).max(2).optional(),
|
|
6833
|
+
max_tokens: z20.number().int().positive().optional()
|
|
5786
6834
|
}).strict(),
|
|
5787
|
-
outputs:
|
|
6835
|
+
outputs: z20.object({ description: JsonRef }).strict(),
|
|
5788
6836
|
outputKinds: { description: "json" },
|
|
5789
6837
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
5790
6838
|
});
|
|
5791
6839
|
|
|
5792
6840
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
5793
|
-
import { z as
|
|
6841
|
+
import { z as z21 } from "zod";
|
|
5794
6842
|
var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
5795
|
-
var ImageReferenceSheetParams =
|
|
5796
|
-
model:
|
|
5797
|
-
subject_description:
|
|
6843
|
+
var ImageReferenceSheetParams = z21.object({
|
|
6844
|
+
model: z21.enum(REFERENCE_SHEET_MODELS),
|
|
6845
|
+
subject_description: z21.string().min(1),
|
|
5798
6846
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
5799
6847
|
// so a multi-scene shoot keeps one consistent set.
|
|
5800
|
-
subject_type:
|
|
5801
|
-
views:
|
|
5802
|
-
style:
|
|
5803
|
-
prompt_override:
|
|
5804
|
-
aspect_ratio:
|
|
5805
|
-
image_size:
|
|
6848
|
+
subject_type: z21.enum(["character", "person", "product", "location"]),
|
|
6849
|
+
views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
|
|
6850
|
+
style: z21.string().optional(),
|
|
6851
|
+
prompt_override: z21.string().min(1).optional(),
|
|
6852
|
+
aspect_ratio: z21.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
|
|
6853
|
+
image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
5806
6854
|
}).strict();
|
|
5807
6855
|
var imageReferenceSheetNode = delegated({
|
|
5808
6856
|
id: "image_reference_sheet",
|
|
@@ -5810,9 +6858,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
5810
6858
|
category: "image",
|
|
5811
6859
|
summary: "Fuse 1\u20136 images of a single subject (person, character, product, or location/set) into ONE multi-view reference sheet \u2014 a labeled grid in consistent style and lighting: a turnaround (FRONT / SIDE / BACK\u2026) for a person/character/product, or several camera angles of the same room (WIDE / REVERSE / DETAIL\u2026) for a location. Curated models: Gemini 3 Pro Image (best fusion + labels), Gemini 3.1 Flash Image (cheap iteration).",
|
|
5812
6860
|
when_to_use: "Use before image_generate / video_generate when a subject must stay consistent across many creatives \u2014 wire the `sheet` output into their `reference` input instead of re-describing the subject per prompt. `subject_description` should be the exact wording you reuse downstream. Pick `google/gemini-3-pro-image-preview` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image-preview` while iterating.",
|
|
5813
|
-
inputs:
|
|
6861
|
+
inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
|
|
5814
6862
|
params: ImageReferenceSheetParams,
|
|
5815
|
-
outputs:
|
|
6863
|
+
outputs: z21.object({ sheet: ImageRef }).strict(),
|
|
5816
6864
|
outputKinds: { sheet: "image" },
|
|
5817
6865
|
cost: ({ params }) => ({
|
|
5818
6866
|
credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
|
|
@@ -5821,10 +6869,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
5821
6869
|
});
|
|
5822
6870
|
|
|
5823
6871
|
// src/engine/nodes/remote/imageSearch.ts
|
|
5824
|
-
import { z as
|
|
5825
|
-
var ImageSearchParams =
|
|
5826
|
-
prompt:
|
|
5827
|
-
count:
|
|
6872
|
+
import { z as z22 } from "zod";
|
|
6873
|
+
var ImageSearchParams = z22.object({
|
|
6874
|
+
prompt: z22.string().min(1),
|
|
6875
|
+
count: z22.number().int().min(1).max(20).default(5)
|
|
5828
6876
|
}).strict();
|
|
5829
6877
|
var imageSearchNode = delegated({
|
|
5830
6878
|
id: "image_search",
|
|
@@ -5832,15 +6880,15 @@ var imageSearchNode = delegated({
|
|
|
5832
6880
|
category: "image",
|
|
5833
6881
|
summary: "Agentic image search across Google Images, stock photography (Freepik), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
|
|
5834
6882
|
when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
|
|
5835
|
-
inputs:
|
|
6883
|
+
inputs: z22.object({}).loose(),
|
|
5836
6884
|
params: ImageSearchParams,
|
|
5837
|
-
outputs:
|
|
6885
|
+
outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
|
|
5838
6886
|
outputKinds: { images: "image" },
|
|
5839
6887
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
5840
6888
|
});
|
|
5841
6889
|
|
|
5842
6890
|
// src/engine/nodes/remote/imageSelect.ts
|
|
5843
|
-
import { z as
|
|
6891
|
+
import { z as z23 } from "zod";
|
|
5844
6892
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
5845
6893
|
var imageSelectNode = delegated({
|
|
5846
6894
|
id: "image_select",
|
|
@@ -5848,15 +6896,15 @@ var imageSelectNode = delegated({
|
|
|
5848
6896
|
category: "vision",
|
|
5849
6897
|
summary: "Pick the best `count` images out of 2+ candidates with a vision LLM, judged against a prompt. Outputs a passthrough subset of the input refs (no new pixels) plus the model's comparative reasoning.",
|
|
5850
6898
|
when_to_use: "Use after fanning out several image_generate variants (or any pool of 2+ images) to keep only the strongest before expensive downstream steps \u2014 video generation, reference sheets, final delivery. `count` fixes the output size, so `images#0`\u2026`images#count-1` are always safe to wire. Pick `~google/gemini-flash-latest` for cheap/fast picks and `~google/gemini-pro-latest` for harder aesthetic judgement.",
|
|
5851
|
-
inputs:
|
|
5852
|
-
params:
|
|
5853
|
-
model:
|
|
5854
|
-
prompt:
|
|
5855
|
-
count:
|
|
5856
|
-
temperature:
|
|
5857
|
-
max_tokens:
|
|
6899
|
+
inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
|
|
6900
|
+
params: z23.object({
|
|
6901
|
+
model: z23.enum(IMAGE_SELECT_MODELS),
|
|
6902
|
+
prompt: z23.string().min(1),
|
|
6903
|
+
count: z23.number().int().min(1).default(1),
|
|
6904
|
+
temperature: z23.number().min(0).max(2).optional(),
|
|
6905
|
+
max_tokens: z23.number().int().positive().optional()
|
|
5858
6906
|
}).strict(),
|
|
5859
|
-
outputs:
|
|
6907
|
+
outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
|
|
5860
6908
|
outputKinds: { images: "image", reasoning: "text" },
|
|
5861
6909
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
5862
6910
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -5881,34 +6929,34 @@ var imageSelectNode = delegated({
|
|
|
5881
6929
|
});
|
|
5882
6930
|
|
|
5883
6931
|
// src/engine/nodes/remote/music.ts
|
|
5884
|
-
import { z as
|
|
6932
|
+
import { z as z24 } from "zod";
|
|
5885
6933
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
5886
|
-
var MusicParams =
|
|
5887
|
-
model:
|
|
6934
|
+
var MusicParams = z24.object({
|
|
6935
|
+
model: z24.enum(MUSIC_MODELS),
|
|
5888
6936
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
5889
|
-
prompt:
|
|
6937
|
+
prompt: z24.string().optional(),
|
|
5890
6938
|
/**
|
|
5891
6939
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
5892
6940
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
5893
6941
|
*/
|
|
5894
|
-
composition_plan:
|
|
6942
|
+
composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
|
|
5895
6943
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
5896
|
-
music_length_ms:
|
|
5897
|
-
seed:
|
|
6944
|
+
music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
6945
|
+
seed: z24.number().int().optional(),
|
|
5898
6946
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
5899
|
-
force_instrumental:
|
|
6947
|
+
force_instrumental: z24.boolean().optional(),
|
|
5900
6948
|
/** composition_plan only — honor exact section durations. */
|
|
5901
|
-
respect_sections_durations:
|
|
6949
|
+
respect_sections_durations: z24.boolean().optional(),
|
|
5902
6950
|
/** Emit word-level timestamps alongside the audio. */
|
|
5903
|
-
with_timestamps:
|
|
6951
|
+
with_timestamps: z24.boolean().optional(),
|
|
5904
6952
|
/**
|
|
5905
6953
|
* video-to-music only — short description of the desired score
|
|
5906
6954
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
5907
6955
|
*/
|
|
5908
|
-
description:
|
|
6956
|
+
description: z24.string().max(1e3).optional(),
|
|
5909
6957
|
/** video-to-music only — up to 10 style tags. */
|
|
5910
|
-
tags:
|
|
5911
|
-
output_format:
|
|
6958
|
+
tags: z24.array(z24.string()).max(10).optional(),
|
|
6959
|
+
output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5912
6960
|
}).strict();
|
|
5913
6961
|
var musicNode = delegated({
|
|
5914
6962
|
id: "music",
|
|
@@ -5916,9 +6964,9 @@ var musicNode = delegated({
|
|
|
5916
6964
|
category: "audio",
|
|
5917
6965
|
summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
|
|
5918
6966
|
when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
|
|
5919
|
-
inputs:
|
|
6967
|
+
inputs: z24.object({ video: VideoRef.optional() }).loose(),
|
|
5920
6968
|
params: MusicParams,
|
|
5921
|
-
outputs:
|
|
6969
|
+
outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
5922
6970
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
5923
6971
|
cost: ({ params }) => {
|
|
5924
6972
|
const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
|
|
@@ -5949,25 +6997,25 @@ var musicNode = delegated({
|
|
|
5949
6997
|
});
|
|
5950
6998
|
|
|
5951
6999
|
// src/engine/nodes/remote/soundEffect.ts
|
|
5952
|
-
import { z as
|
|
7000
|
+
import { z as z25 } from "zod";
|
|
5953
7001
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
5954
|
-
var SoundEffectParams =
|
|
5955
|
-
model:
|
|
7002
|
+
var SoundEffectParams = z25.object({
|
|
7003
|
+
model: z25.enum(SOUND_EFFECT_MODELS),
|
|
5956
7004
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
5957
|
-
text:
|
|
7005
|
+
text: z25.string().min(1),
|
|
5958
7006
|
/**
|
|
5959
7007
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
5960
7008
|
* natural length for the described effect.
|
|
5961
7009
|
*/
|
|
5962
|
-
duration_seconds:
|
|
7010
|
+
duration_seconds: z25.number().min(0.5).max(30).optional(),
|
|
5963
7011
|
/**
|
|
5964
7012
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
5965
7013
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
5966
7014
|
*/
|
|
5967
|
-
prompt_influence:
|
|
7015
|
+
prompt_influence: z25.number().min(0).max(1).optional(),
|
|
5968
7016
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
5969
|
-
loop:
|
|
5970
|
-
output_format:
|
|
7017
|
+
loop: z25.boolean().optional(),
|
|
7018
|
+
output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
5971
7019
|
}).strict();
|
|
5972
7020
|
var soundEffectNode = delegated({
|
|
5973
7021
|
id: "sound_effect",
|
|
@@ -5975,9 +7023,9 @@ var soundEffectNode = delegated({
|
|
|
5975
7023
|
category: "audio",
|
|
5976
7024
|
summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
|
|
5977
7025
|
when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
|
|
5978
|
-
inputs:
|
|
7026
|
+
inputs: z25.object({}).loose(),
|
|
5979
7027
|
params: SoundEffectParams,
|
|
5980
|
-
outputs:
|
|
7028
|
+
outputs: z25.object({ audio: AudioRef }).strict(),
|
|
5981
7029
|
outputKinds: { audio: "audio" },
|
|
5982
7030
|
cost: ({ params }) => {
|
|
5983
7031
|
const seconds = params.duration_seconds ?? 5;
|
|
@@ -5986,7 +7034,7 @@ var soundEffectNode = delegated({
|
|
|
5986
7034
|
});
|
|
5987
7035
|
|
|
5988
7036
|
// src/engine/nodes/remote/textGenerate.ts
|
|
5989
|
-
import { z as
|
|
7037
|
+
import { z as z26 } from "zod";
|
|
5990
7038
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
5991
7039
|
var textGenerateNode = delegated({
|
|
5992
7040
|
id: "text_generate",
|
|
@@ -5994,58 +7042,58 @@ var textGenerateNode = delegated({
|
|
|
5994
7042
|
category: "language",
|
|
5995
7043
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
5996
7044
|
when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
|
|
5997
|
-
inputs:
|
|
5998
|
-
params:
|
|
5999
|
-
model:
|
|
6000
|
-
prompt:
|
|
6001
|
-
system:
|
|
6002
|
-
response_format:
|
|
6003
|
-
web_search:
|
|
6004
|
-
temperature:
|
|
6005
|
-
max_tokens:
|
|
7045
|
+
inputs: z26.object({}).loose(),
|
|
7046
|
+
params: z26.object({
|
|
7047
|
+
model: z26.enum(TEXT_GENERATE_MODELS),
|
|
7048
|
+
prompt: z26.string().min(1),
|
|
7049
|
+
system: z26.string().optional(),
|
|
7050
|
+
response_format: z26.enum(["text", "json_object"]).optional(),
|
|
7051
|
+
web_search: z26.boolean().optional(),
|
|
7052
|
+
temperature: z26.number().min(0).max(2).optional(),
|
|
7053
|
+
max_tokens: z26.number().int().positive().optional()
|
|
6006
7054
|
}).strict(),
|
|
6007
|
-
outputs:
|
|
7055
|
+
outputs: z26.object({ text: TextRef }).strict(),
|
|
6008
7056
|
outputKinds: { text: "text" },
|
|
6009
7057
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
6010
7058
|
});
|
|
6011
7059
|
|
|
6012
7060
|
// src/engine/nodes/remote/tts.ts
|
|
6013
|
-
import { z as
|
|
7061
|
+
import { z as z27 } from "zod";
|
|
6014
7062
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
6015
|
-
var TtsVoiceSettings =
|
|
6016
|
-
stability:
|
|
6017
|
-
similarity_boost:
|
|
6018
|
-
style:
|
|
6019
|
-
use_speaker_boost:
|
|
6020
|
-
speed:
|
|
7063
|
+
var TtsVoiceSettings = z27.object({
|
|
7064
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7065
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
7066
|
+
style: z27.number().min(0).max(1).optional(),
|
|
7067
|
+
use_speaker_boost: z27.boolean().optional(),
|
|
7068
|
+
speed: z27.number().min(0.25).max(4).optional()
|
|
6021
7069
|
}).strict();
|
|
6022
|
-
var TtsPronunciationLocator =
|
|
6023
|
-
pronunciation_dictionary_id:
|
|
6024
|
-
version_id:
|
|
7070
|
+
var TtsPronunciationLocator = z27.object({
|
|
7071
|
+
pronunciation_dictionary_id: z27.string().min(1),
|
|
7072
|
+
version_id: z27.string().nullable().optional()
|
|
6025
7073
|
}).strict();
|
|
6026
|
-
var TtsParams =
|
|
6027
|
-
model:
|
|
6028
|
-
text:
|
|
6029
|
-
voice:
|
|
7074
|
+
var TtsParams = z27.object({
|
|
7075
|
+
model: z27.enum(TTS_MODELS),
|
|
7076
|
+
text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
7077
|
+
voice: z27.string().min(1),
|
|
6030
7078
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
6031
|
-
output_format:
|
|
6032
|
-
seed:
|
|
7079
|
+
output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
7080
|
+
seed: z27.number().int().min(0).max(4294967295).optional(),
|
|
6033
7081
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
6034
|
-
stability:
|
|
6035
|
-
similarity_boost:
|
|
7082
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7083
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
6036
7084
|
voice_settings: TtsVoiceSettings.optional(),
|
|
6037
7085
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
6038
|
-
language_code:
|
|
6039
|
-
pronunciation_dictionary_locators:
|
|
6040
|
-
apply_text_normalization:
|
|
7086
|
+
language_code: z27.string().optional(),
|
|
7087
|
+
pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
|
|
7088
|
+
apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
|
|
6041
7089
|
/** Currently Japanese-only. Adds latency. */
|
|
6042
|
-
apply_language_text_normalization:
|
|
7090
|
+
apply_language_text_normalization: z27.boolean().optional(),
|
|
6043
7091
|
/**
|
|
6044
7092
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
6045
7093
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
6046
7094
|
* rendering, lipsync, and beat-matched cuts.
|
|
6047
7095
|
*/
|
|
6048
|
-
with_timestamps:
|
|
7096
|
+
with_timestamps: z27.boolean().optional()
|
|
6049
7097
|
}).strict();
|
|
6050
7098
|
var ttsNode = delegated({
|
|
6051
7099
|
id: "tts",
|
|
@@ -6053,9 +7101,9 @@ var ttsNode = delegated({
|
|
|
6053
7101
|
category: "audio",
|
|
6054
7102
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
6055
7103
|
when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
|
|
6056
|
-
inputs:
|
|
7104
|
+
inputs: z27.object({}).loose(),
|
|
6057
7105
|
params: TtsParams,
|
|
6058
|
-
outputs:
|
|
7106
|
+
outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6059
7107
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6060
7108
|
cost: ({ params }) => ({
|
|
6061
7109
|
credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
|
|
@@ -6064,47 +7112,49 @@ var ttsNode = delegated({
|
|
|
6064
7112
|
});
|
|
6065
7113
|
|
|
6066
7114
|
// src/engine/nodes/remote/video.ts
|
|
6067
|
-
import { z as
|
|
6068
|
-
var
|
|
6069
|
-
var VideoGenerateParams =
|
|
6070
|
-
model:
|
|
6071
|
-
prompt:
|
|
6072
|
-
duration:
|
|
6073
|
-
resolution:
|
|
7115
|
+
import { z as z28 } from "zod";
|
|
7116
|
+
var videoModelEnum = z28.enum(VIDEO_GENERATE_MODELS);
|
|
7117
|
+
var VideoGenerateParams = z28.object({
|
|
7118
|
+
model: videoModelEnum,
|
|
7119
|
+
prompt: z28.string().min(1),
|
|
7120
|
+
duration: z28.number().int().positive().optional(),
|
|
7121
|
+
resolution: z28.string().optional(),
|
|
6074
7122
|
// Union of ratios accepted by at least one curated model (registry gates
|
|
6075
7123
|
// per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
|
|
6076
|
-
aspect_ratio:
|
|
6077
|
-
generate_audio:
|
|
6078
|
-
seed:
|
|
7124
|
+
aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
|
|
7125
|
+
generate_audio: z28.boolean().optional(),
|
|
7126
|
+
seed: z28.number().int().nonnegative().optional(),
|
|
6079
7127
|
// Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
|
|
6080
|
-
negative_prompt:
|
|
6081
|
-
person_generation:
|
|
6082
|
-
enhance_prompt:
|
|
6083
|
-
conditioning_scale:
|
|
7128
|
+
negative_prompt: z28.string().optional(),
|
|
7129
|
+
person_generation: z28.string().optional(),
|
|
7130
|
+
enhance_prompt: z28.boolean().optional(),
|
|
7131
|
+
conditioning_scale: z28.number().optional(),
|
|
7132
|
+
// Kling-only passthrough (prompt-adherence dial, sent top-level).
|
|
7133
|
+
cfg_scale: z28.number().optional()
|
|
6084
7134
|
}).strict();
|
|
6085
7135
|
var videoGenerateNode = delegated({
|
|
6086
7136
|
id: "video_generate",
|
|
6087
7137
|
version: "2.0.0",
|
|
6088
7138
|
category: "video",
|
|
6089
|
-
summary: "Generate video for ad creatives.
|
|
6090
|
-
when_to_use: "Use `bytedance/seedance-2.0` for
|
|
6091
|
-
inputs:
|
|
7139
|
+
summary: "Generate video for ad creatives. Curated roster: `bytedance/seedance-2.0` (identity/product workhorse), `google/veo-3.1` (photoreal cine ceiling + real-face fallback), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-3.0` (motion-transfer/dynamic). Async with polling.",
|
|
7140
|
+
when_to_use: "Use `bytedance/seedance-2.0` for identity/product output. Route real human likenesses to `google/veo-3.1` (dodges the ByteDance real-person filter); use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-3.0` for motion-transfer/hyper-dynamic beats. The scaffolder's scored router picks for you. Each model gates its own durations/resolutions/aspect ratios in the registry \u2014 see the README per-model section.",
|
|
7141
|
+
inputs: z28.object({
|
|
6092
7142
|
first_frame: ImageRef.optional(),
|
|
6093
7143
|
last_frame: ImageRef.optional(),
|
|
6094
7144
|
reference: ImageRef.optional()
|
|
6095
7145
|
}).loose(),
|
|
6096
7146
|
params: VideoGenerateParams,
|
|
6097
|
-
outputs:
|
|
7147
|
+
outputs: z28.object({ video: VideoRef }).strict(),
|
|
6098
7148
|
outputKinds: { video: "video" },
|
|
6099
7149
|
cost: () => ({ credits: 50, seconds_estimate: 120 })
|
|
6100
7150
|
});
|
|
6101
7151
|
|
|
6102
7152
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
6103
|
-
import { z as
|
|
6104
|
-
var VideoBackgroundRemoveParams =
|
|
6105
|
-
model:
|
|
6106
|
-
edge_refinement:
|
|
6107
|
-
output_codec:
|
|
7153
|
+
import { z as z29 } from "zod";
|
|
7154
|
+
var VideoBackgroundRemoveParams = z29.object({
|
|
7155
|
+
model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
|
|
7156
|
+
edge_refinement: z29.boolean().optional().default(true),
|
|
7157
|
+
output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
|
|
6108
7158
|
}).strict();
|
|
6109
7159
|
var videoBackgroundRemoveNode = delegated({
|
|
6110
7160
|
id: "video_background_remove",
|
|
@@ -6112,18 +7162,18 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
6112
7162
|
category: "video",
|
|
6113
7163
|
summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by fal.ai `veed/video-background-removal/fast`.",
|
|
6114
7164
|
when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
|
|
6115
|
-
inputs:
|
|
7165
|
+
inputs: z29.object({
|
|
6116
7166
|
video: VideoRef
|
|
6117
7167
|
}).strict(),
|
|
6118
7168
|
params: VideoBackgroundRemoveParams,
|
|
6119
|
-
outputs:
|
|
7169
|
+
outputs: z29.object({ video: VideoRef }).strict(),
|
|
6120
7170
|
outputKinds: { video: "video" },
|
|
6121
7171
|
// $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
|
|
6122
7172
|
cost: () => ({ credits: 50, seconds_estimate: 60 })
|
|
6123
7173
|
});
|
|
6124
7174
|
|
|
6125
7175
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
6126
|
-
import { z as
|
|
7176
|
+
import { z as z30 } from "zod";
|
|
6127
7177
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6128
7178
|
var videoDeconstructNode = delegated({
|
|
6129
7179
|
id: "video_deconstruct",
|
|
@@ -6131,34 +7181,34 @@ var videoDeconstructNode = delegated({
|
|
|
6131
7181
|
category: "video",
|
|
6132
7182
|
summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
|
|
6133
7183
|
when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
|
|
6134
|
-
inputs:
|
|
6135
|
-
params:
|
|
6136
|
-
model:
|
|
6137
|
-
mode:
|
|
6138
|
-
language:
|
|
6139
|
-
max_scenes:
|
|
6140
|
-
focus:
|
|
6141
|
-
start_s:
|
|
6142
|
-
end_s:
|
|
7184
|
+
inputs: z30.object({ video: VideoRef }).loose(),
|
|
7185
|
+
params: z30.object({
|
|
7186
|
+
model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
7187
|
+
mode: z30.enum(["full", "index"]).optional(),
|
|
7188
|
+
language: z30.string().min(2).max(8).optional(),
|
|
7189
|
+
max_scenes: z30.number().int().min(1).max(60).optional(),
|
|
7190
|
+
focus: z30.string().optional(),
|
|
7191
|
+
start_s: z30.number().min(0).optional(),
|
|
7192
|
+
end_s: z30.number().positive().optional(),
|
|
6143
7193
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
6144
7194
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
6145
7195
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
6146
7196
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
6147
|
-
shot_cuts:
|
|
7197
|
+
shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
|
|
6148
7198
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
6149
7199
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
6150
7200
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
6151
7201
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
6152
|
-
max_clip_s:
|
|
7202
|
+
max_clip_s: z30.number().positive().max(60).optional(),
|
|
6153
7203
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
6154
7204
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
6155
|
-
transcriber:
|
|
7205
|
+
transcriber: z30.enum(["groq", "deepgram"]).optional()
|
|
6156
7206
|
}).strict(),
|
|
6157
|
-
outputs:
|
|
7207
|
+
outputs: z30.object({
|
|
6158
7208
|
analysis: JsonRef,
|
|
6159
7209
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
6160
|
-
start_frames:
|
|
6161
|
-
end_frames:
|
|
7210
|
+
start_frames: z30.array(ImageRef).min(1).optional(),
|
|
7211
|
+
end_frames: z30.array(ImageRef).min(1).optional(),
|
|
6162
7212
|
transcript: JsonRef
|
|
6163
7213
|
}).strict(),
|
|
6164
7214
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -6166,31 +7216,31 @@ var videoDeconstructNode = delegated({
|
|
|
6166
7216
|
});
|
|
6167
7217
|
|
|
6168
7218
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
6169
|
-
import { z as
|
|
6170
|
-
var FalLipsyncParams =
|
|
6171
|
-
model:
|
|
7219
|
+
import { z as z31 } from "zod";
|
|
7220
|
+
var FalLipsyncParams = z31.object({
|
|
7221
|
+
model: z31.literal("fal/veed-lipsync")
|
|
6172
7222
|
}).strict();
|
|
6173
|
-
var VideoLipsyncParams =
|
|
7223
|
+
var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
|
|
6174
7224
|
var videoLipsyncNode = delegated({
|
|
6175
7225
|
id: "video_lipsync",
|
|
6176
7226
|
version: "1.0.0",
|
|
6177
7227
|
category: "video",
|
|
6178
7228
|
summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
|
|
6179
|
-
inputs:
|
|
7229
|
+
inputs: z31.object({
|
|
6180
7230
|
video: VideoRef,
|
|
6181
7231
|
audio: AudioRef
|
|
6182
7232
|
}).strict(),
|
|
6183
7233
|
params: VideoLipsyncParams,
|
|
6184
|
-
outputs:
|
|
7234
|
+
outputs: z31.object({ video: VideoRef }).strict(),
|
|
6185
7235
|
outputKinds: { video: "video" },
|
|
6186
7236
|
cost: () => ({ credits: 20, seconds_estimate: 120 })
|
|
6187
7237
|
});
|
|
6188
7238
|
|
|
6189
7239
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6190
|
-
import { mkdtemp as mkdtemp6, readFile as
|
|
7240
|
+
import { mkdtemp as mkdtemp6, readFile as readFile11, rm as rm6 } from "fs/promises";
|
|
6191
7241
|
import { tmpdir as tmpdir6 } from "os";
|
|
6192
|
-
import
|
|
6193
|
-
import { z as
|
|
7242
|
+
import path14 from "path";
|
|
7243
|
+
import { z as z32 } from "zod";
|
|
6194
7244
|
|
|
6195
7245
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
6196
7246
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -6269,29 +7319,32 @@ ${detail.slice(-4e3)}`);
|
|
|
6269
7319
|
}
|
|
6270
7320
|
|
|
6271
7321
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6272
|
-
var VideoTranscribeParams =
|
|
6273
|
-
language:
|
|
7322
|
+
var VideoTranscribeParams = z32.object({
|
|
7323
|
+
language: z32.string().min(2).max(8).optional(),
|
|
6274
7324
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
6275
7325
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
6276
7326
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
6277
|
-
transcriber:
|
|
7327
|
+
transcriber: z32.enum(["groq", "deepgram"]).optional()
|
|
6278
7328
|
}).strict();
|
|
6279
|
-
var VideoTranscribeInputs =
|
|
6280
|
-
video
|
|
7329
|
+
var VideoTranscribeInputs = z32.object({
|
|
7330
|
+
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
7331
|
+
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
7332
|
+
// the local extraction path has been shipping one for every video input.
|
|
7333
|
+
video: z32.union([VideoRef, AudioRef])
|
|
6281
7334
|
}).strict();
|
|
6282
|
-
var VideoTranscribeOutputs =
|
|
6283
|
-
transcript:
|
|
7335
|
+
var VideoTranscribeOutputs = z32.object({
|
|
7336
|
+
transcript: z32.custom(),
|
|
6284
7337
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
6285
7338
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
6286
|
-
rich:
|
|
7339
|
+
rich: z32.custom().optional()
|
|
6287
7340
|
}).strict();
|
|
6288
7341
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
6289
7342
|
var videoTranscribeNode = defineNode({
|
|
6290
7343
|
id: "video_transcribe",
|
|
6291
|
-
version: "2.
|
|
7344
|
+
version: "2.3.0",
|
|
6292
7345
|
category: "language",
|
|
6293
7346
|
location: "local",
|
|
6294
|
-
summary: 'Transcribe a video
|
|
7347
|
+
summary: 'Transcribe a video or audio track to a word-level JSON transcript. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo ($0.04/hr, 10s min); `transcriber:"deepgram"` uses Deepgram Nova-3 ($0.0043/min) and additionally emits a `rich` JSON output with punctuated words + paragraph/sentence grouping (and speaker indices). Automatically extracts audio locally (mono 16 kHz MP3) before uploading \u2014 reduces payload ~100\xD7 and lifts the effective duration limit well beyond Groq\'s 100 MB file cap. The `transcript` output is always an array of {text, start, end} entries ready to feed Hyperframes caption compositions (Deepgram prefers the punctuated word form).',
|
|
6295
7348
|
when_to_use: 'Use to generate burned-in captions for a stitched video. Pair with `hyperframe_render` and a captions composition (e.g. `tiktok-captions`) by passing the transcript JSON through `params.variables.transcript`. Pick `params.language` to filter out non-target speech (e.g. "es" for Spanish). Pick `transcriber:"deepgram"` when you want punctuation/paragraphs (read them from the `rich` output) or speaker grouping. Requires ffmpeg on PATH for audio extraction (falls back to full video upload if unavailable).',
|
|
6296
7349
|
inputs: VideoTranscribeInputs,
|
|
6297
7350
|
params: VideoTranscribeParams,
|
|
@@ -6303,7 +7356,7 @@ var videoTranscribeNode = defineNode({
|
|
|
6303
7356
|
const effectiveInputs = audioInput ?? inputs;
|
|
6304
7357
|
return await callBackendExec({
|
|
6305
7358
|
nodeType: "video_transcribe",
|
|
6306
|
-
nodeVersion: "2.
|
|
7359
|
+
nodeVersion: "2.3.0",
|
|
6307
7360
|
params,
|
|
6308
7361
|
inputs: effectiveInputs,
|
|
6309
7362
|
outputKinds: { transcript: "json", rich: "json" },
|
|
@@ -6321,14 +7374,14 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6321
7374
|
ctx.log("video_transcribe: no audio track detected, sending full video");
|
|
6322
7375
|
return null;
|
|
6323
7376
|
}
|
|
6324
|
-
tmpDir = await mkdtemp6(
|
|
6325
|
-
const audioPath =
|
|
7377
|
+
tmpDir = await mkdtemp6(path14.join(tmpdir6(), "vtx-"));
|
|
7378
|
+
const audioPath = path14.join(tmpDir, "audio.mp3");
|
|
6326
7379
|
ctx.log("video_transcribe: extracting audio (mono 16kHz mp3)");
|
|
6327
7380
|
await runFfmpeg(
|
|
6328
7381
|
["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
|
|
6329
7382
|
{ timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
|
|
6330
7383
|
);
|
|
6331
|
-
const bytes = await
|
|
7384
|
+
const bytes = await readFile11(audioPath);
|
|
6332
7385
|
if (bytes.byteLength === 0) {
|
|
6333
7386
|
ctx.log("video_transcribe: extracted audio is empty, sending full video");
|
|
6334
7387
|
return null;
|
|
@@ -6368,29 +7421,29 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6368
7421
|
}
|
|
6369
7422
|
|
|
6370
7423
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
6371
|
-
import { z as
|
|
7424
|
+
import { z as z33 } from "zod";
|
|
6372
7425
|
var voiceSelectNode = delegated({
|
|
6373
7426
|
id: "voice_select",
|
|
6374
7427
|
version: "1.0.0",
|
|
6375
7428
|
category: "audio",
|
|
6376
7429
|
summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
|
|
6377
7430
|
when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
|
|
6378
|
-
inputs:
|
|
6379
|
-
params:
|
|
6380
|
-
description:
|
|
6381
|
-
gender:
|
|
6382
|
-
age:
|
|
6383
|
-
accent:
|
|
6384
|
-
language:
|
|
6385
|
-
limit:
|
|
7431
|
+
inputs: z33.object({}).loose(),
|
|
7432
|
+
params: z33.object({
|
|
7433
|
+
description: z33.string().min(1),
|
|
7434
|
+
gender: z33.string().optional(),
|
|
7435
|
+
age: z33.string().optional(),
|
|
7436
|
+
accent: z33.string().optional(),
|
|
7437
|
+
language: z33.string().optional(),
|
|
7438
|
+
limit: z33.number().int().min(1).max(20).optional()
|
|
6386
7439
|
}).strict(),
|
|
6387
|
-
outputs:
|
|
7440
|
+
outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
6388
7441
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
6389
7442
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
6390
7443
|
});
|
|
6391
7444
|
|
|
6392
7445
|
// src/engine/schema/catalog.ts
|
|
6393
|
-
import { z as
|
|
7446
|
+
import { z as z34 } from "zod";
|
|
6394
7447
|
function generateCatalog(registry, opts = {}) {
|
|
6395
7448
|
const entries = registry.all().map((def) => {
|
|
6396
7449
|
const cost = def.cost ? safeCost(def) : void 0;
|
|
@@ -6401,9 +7454,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
6401
7454
|
summary: def.summary,
|
|
6402
7455
|
when_to_use: def.when_to_use,
|
|
6403
7456
|
location: def.location,
|
|
6404
|
-
inputs:
|
|
6405
|
-
params:
|
|
6406
|
-
outputs:
|
|
7457
|
+
inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
7458
|
+
params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
7459
|
+
outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
6407
7460
|
cost_estimate_credits: cost?.credits,
|
|
6408
7461
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
6409
7462
|
};
|
|
@@ -6435,19 +7488,19 @@ function safeCost(def) {
|
|
|
6435
7488
|
|
|
6436
7489
|
// src/engine/storage/cache-store.ts
|
|
6437
7490
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6438
|
-
import { mkdir as mkdir3, readFile as
|
|
6439
|
-
import
|
|
7491
|
+
import { mkdir as mkdir3, readFile as readFile12, rename as rename2, writeFile as writeFile7 } from "fs/promises";
|
|
7492
|
+
import path15 from "path";
|
|
6440
7493
|
var LocalCacheStore = class {
|
|
6441
7494
|
rootDir;
|
|
6442
7495
|
constructor(rootDir) {
|
|
6443
7496
|
this.rootDir = rootDir;
|
|
6444
7497
|
}
|
|
6445
7498
|
filePath(cacheKey) {
|
|
6446
|
-
return
|
|
7499
|
+
return path15.join(this.rootDir, `${cacheKey}.json`);
|
|
6447
7500
|
}
|
|
6448
7501
|
async get(cacheKey) {
|
|
6449
7502
|
try {
|
|
6450
|
-
const buf = await
|
|
7503
|
+
const buf = await readFile12(this.filePath(cacheKey), "utf8");
|
|
6451
7504
|
return JSON.parse(buf);
|
|
6452
7505
|
} catch (e) {
|
|
6453
7506
|
if (e.code === "ENOENT") return null;
|
|
@@ -6456,7 +7509,7 @@ var LocalCacheStore = class {
|
|
|
6456
7509
|
}
|
|
6457
7510
|
async put(entry) {
|
|
6458
7511
|
const dest = this.filePath(entry.cacheKey);
|
|
6459
|
-
await mkdir3(
|
|
7512
|
+
await mkdir3(path15.dirname(dest), { recursive: true });
|
|
6460
7513
|
const tmp = `${dest}.tmp-${process.pid}-${randomUUID2()}`;
|
|
6461
7514
|
await writeFile7(tmp, JSON.stringify(entry, null, 0));
|
|
6462
7515
|
await rename2(tmp, dest);
|
|
@@ -6480,7 +7533,8 @@ var LOCAL_NODES = [
|
|
|
6480
7533
|
imagemagickNode,
|
|
6481
7534
|
videoTranscribeNode,
|
|
6482
7535
|
fontSpecimenNode,
|
|
6483
|
-
audioTimelineNode
|
|
7536
|
+
audioTimelineNode,
|
|
7537
|
+
collectNode
|
|
6484
7538
|
];
|
|
6485
7539
|
var REMOTE_NODES = [
|
|
6486
7540
|
textGenerateNode,
|
|
@@ -6510,29 +7564,55 @@ function defaultRegistry() {
|
|
|
6510
7564
|
}
|
|
6511
7565
|
function createEngineFromEnv(opts = {}) {
|
|
6512
7566
|
const cwd = opts.cwd ?? process.cwd();
|
|
6513
|
-
const cacheDir = opts.cacheDir ??
|
|
6514
|
-
const outputsDir = opts.outputsDir ??
|
|
7567
|
+
const cacheDir = opts.cacheDir ?? path16.join(cwd, "canvas", ".cache");
|
|
7568
|
+
const outputsDir = opts.outputsDir ?? path16.join(cwd, "canvas");
|
|
6515
7569
|
const creds = requireCredentialsFromEnv();
|
|
7570
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
7571
|
+
const assets = new LocalAssetStore(path16.join(cacheDir, "assets"));
|
|
7572
|
+
const localCache = new LocalCacheStore(path16.join(cacheDir, "index"));
|
|
7573
|
+
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
7574
|
+
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
7575
|
+
local: localCache,
|
|
7576
|
+
remote: new RemoteCacheStore(client, opts.log),
|
|
7577
|
+
assets,
|
|
7578
|
+
log: opts.log
|
|
7579
|
+
}) : localCache;
|
|
6516
7580
|
return new Engine({
|
|
6517
7581
|
registry: defaultRegistry(),
|
|
6518
|
-
client
|
|
6519
|
-
assets
|
|
6520
|
-
cache
|
|
7582
|
+
client,
|
|
7583
|
+
assets,
|
|
7584
|
+
cache,
|
|
6521
7585
|
outputsDir,
|
|
6522
|
-
log: opts.log
|
|
7586
|
+
log: opts.log,
|
|
7587
|
+
persistAssets: remoteCacheEnabled
|
|
6523
7588
|
});
|
|
6524
7589
|
}
|
|
6525
7590
|
|
|
6526
7591
|
export {
|
|
7592
|
+
BackendClient,
|
|
7593
|
+
requireCredentialsFromEnv,
|
|
7594
|
+
RunAbortedError,
|
|
6527
7595
|
LayerExecutionError,
|
|
6528
7596
|
describeFailureReason,
|
|
6529
7597
|
SEEDANCE_DURATIONS,
|
|
6530
7598
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
6531
7599
|
IMAGE_GENERATE_MODELS,
|
|
7600
|
+
DEFAULT_VIDEO_GENERATE_MODEL,
|
|
6532
7601
|
MODEL_REGISTRY,
|
|
6533
7602
|
resolveConcurrency,
|
|
7603
|
+
ulid,
|
|
7604
|
+
isPersistedAssetRef,
|
|
7605
|
+
collectAssetRefLikes,
|
|
7606
|
+
REF_PREFIX,
|
|
7607
|
+
parseRefExpr,
|
|
7608
|
+
sha256Hex,
|
|
7609
|
+
SEEDANCE_PROFILE,
|
|
7610
|
+
clipProfileFor,
|
|
7611
|
+
clipParamRecipe,
|
|
7612
|
+
imageProfileFor,
|
|
6534
7613
|
elementMentionKeywords,
|
|
6535
|
-
|
|
7614
|
+
toModelSafeImage,
|
|
7615
|
+
BackendClient2,
|
|
6536
7616
|
Engine2 as Engine,
|
|
6537
7617
|
LocalAssetStore2 as LocalAssetStore,
|
|
6538
7618
|
LocalCacheStore2 as LocalCacheStore,
|
|
@@ -6542,4 +7622,4 @@ export {
|
|
|
6542
7622
|
defaultRegistry,
|
|
6543
7623
|
createEngineFromEnv
|
|
6544
7624
|
};
|
|
6545
|
-
//# sourceMappingURL=chunk-
|
|
7625
|
+
//# sourceMappingURL=chunk-J2LYFDVC.js.map
|