@koda-sl/baker-cli 0.128.0-dev.70bf43ce4 → 0.131.0-dev.597b50181
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 +255 -288
- package/canvas/tiktok-captions-composition/index.html +1 -1
- package/canvas/video-overlay-composition/index.html +78 -6
- package/canvas/video-overlay-composition/meta.json +30 -2
- package/dist/{chunk-43KBQLP5.js → chunk-4QK7JC6O.js} +1243 -461
- package/dist/chunk-4QK7JC6O.js.map +1 -0
- package/dist/cli.js +8796 -6171
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +62 -2
- package/dist/engine/index.js +2 -2
- package/package.json +4 -2
- package/dist/chunk-43KBQLP5.js.map +0 -1
|
@@ -600,7 +600,7 @@ ${originalIndentation}`;
|
|
|
600
600
|
});
|
|
601
601
|
|
|
602
602
|
// src/engine/index.ts
|
|
603
|
-
import
|
|
603
|
+
import path16 from "path";
|
|
604
604
|
|
|
605
605
|
// src/engine/client/http.ts
|
|
606
606
|
var CONTENT_POLICY_CODE = "content_policy_blocked";
|
|
@@ -649,17 +649,17 @@ var HttpClient = class {
|
|
|
649
649
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
650
650
|
this.sleepFn = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
651
651
|
}
|
|
652
|
-
async postJson(
|
|
653
|
-
return await this.requestJson("POST",
|
|
652
|
+
async postJson(path17, body, signal) {
|
|
653
|
+
return await this.requestJson("POST", path17, body, signal);
|
|
654
654
|
}
|
|
655
|
-
async putJson(
|
|
656
|
-
return await this.requestJson("PUT",
|
|
655
|
+
async putJson(path17, body, signal) {
|
|
656
|
+
return await this.requestJson("PUT", path17, body, signal);
|
|
657
657
|
}
|
|
658
|
-
async getJson(
|
|
659
|
-
return await this.requestJson("GET",
|
|
658
|
+
async getJson(path17, signal) {
|
|
659
|
+
return await this.requestJson("GET", path17, void 0, signal);
|
|
660
660
|
}
|
|
661
|
-
async requestJson(method,
|
|
662
|
-
const url = `${this.baseUrl}${
|
|
661
|
+
async requestJson(method, path17, body, signal) {
|
|
662
|
+
const url = `${this.baseUrl}${path17.startsWith("/") ? path17 : `/${path17}`}`;
|
|
663
663
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
664
664
|
const outcome = await this.attempt(method, url, body, attempt, signal);
|
|
665
665
|
if (outcome.kind === "value") return outcome.value;
|
|
@@ -798,12 +798,12 @@ var BackendClient = class {
|
|
|
798
798
|
}
|
|
799
799
|
async pollJob(jobId, signal) {
|
|
800
800
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
801
|
-
const
|
|
801
|
+
const path17 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
802
802
|
for (let attempt = 0; ; attempt++) {
|
|
803
803
|
if (signal?.aborted) {
|
|
804
804
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
805
805
|
}
|
|
806
|
-
const job = await this.http.getJson(
|
|
806
|
+
const job = await this.http.getJson(path17, signal);
|
|
807
807
|
if (job.status === "completed") return job.result;
|
|
808
808
|
if (job.status === "failed") throw failedJobError(job.error);
|
|
809
809
|
if (Date.now() > deadline) {
|
|
@@ -812,10 +812,10 @@ var BackendClient = class {
|
|
|
812
812
|
await sleep(pollInterval(attempt));
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
|
-
presignAssetUpload(sha256, mime, signal) {
|
|
815
|
+
presignAssetUpload(sha256, mime, signal, purpose) {
|
|
816
816
|
return this.http.postJson(
|
|
817
817
|
"/api/canvas/assets/presign",
|
|
818
|
-
{ sha256, mime },
|
|
818
|
+
{ sha256, mime, purpose },
|
|
819
819
|
signal
|
|
820
820
|
);
|
|
821
821
|
}
|
|
@@ -840,6 +840,39 @@ var BackendClient = class {
|
|
|
840
840
|
async recordRun(payload, signal) {
|
|
841
841
|
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
842
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Resumable-run lookup — GET /api/canvas/runs/active. Returns the newest
|
|
845
|
+
* still-running/interrupted run for the creative (pinned to the exact canvas
|
|
846
|
+
* sha), so a fresh sandbox can adopt its run id and re-attach billed
|
|
847
|
+
* in-flight jobs. Null on no active run — or an older backend without the
|
|
848
|
+
* route (both 404).
|
|
849
|
+
*/
|
|
850
|
+
async getActiveRun(creativeSlug, canvasSha, signal) {
|
|
851
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
852
|
+
if (canvasSha) params.set("sha", canvasSha);
|
|
853
|
+
try {
|
|
854
|
+
const res = await this.http.getJson(`/api/canvas/runs/active?${params}`, signal);
|
|
855
|
+
return res.run;
|
|
856
|
+
} catch (e) {
|
|
857
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
858
|
+
throw e;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Portable-rerun lookup — GET /api/canvas/runs/latest. The newest run for
|
|
863
|
+
* the creative that recorded a canvas snapshot manifest; null when none
|
|
864
|
+
* exists (or the backend predates the route — both 404).
|
|
865
|
+
*/
|
|
866
|
+
async getLatestSnapshotRun(creativeSlug, signal) {
|
|
867
|
+
const params = new URLSearchParams({ slug: creativeSlug });
|
|
868
|
+
try {
|
|
869
|
+
const res = await this.http.getJson(`/api/canvas/runs/latest?${params}`, signal);
|
|
870
|
+
return res.run;
|
|
871
|
+
} catch (e) {
|
|
872
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
873
|
+
throw e;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
843
876
|
/**
|
|
844
877
|
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
845
878
|
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
@@ -849,8 +882,8 @@ var BackendClient = class {
|
|
|
849
882
|
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
850
883
|
}
|
|
851
884
|
getArtifact(kind, name, version, signal) {
|
|
852
|
-
const
|
|
853
|
-
return this.http.getJson(
|
|
885
|
+
const path17 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
886
|
+
return this.http.getJson(path17, signal);
|
|
854
887
|
}
|
|
855
888
|
};
|
|
856
889
|
|
|
@@ -907,6 +940,14 @@ var NodeExecutionError = class extends CanvasError {
|
|
|
907
940
|
this.cause = cause;
|
|
908
941
|
}
|
|
909
942
|
};
|
|
943
|
+
var RunAbortedError = class extends CanvasError {
|
|
944
|
+
reason;
|
|
945
|
+
constructor(reason, message2) {
|
|
946
|
+
super(message2);
|
|
947
|
+
this.name = "RunAbortedError";
|
|
948
|
+
this.reason = reason;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
910
951
|
var LayerExecutionError = class extends CanvasError {
|
|
911
952
|
failures;
|
|
912
953
|
constructor(failures) {
|
|
@@ -984,6 +1025,7 @@ var OPENROUTER_IMAGE_AR = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:1
|
|
|
984
1025
|
var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8", "8:1"];
|
|
985
1026
|
var OPENROUTER_IMAGE_SIZE = ["1K", "2K", "4K"];
|
|
986
1027
|
var OPENROUTER_IMAGE_SIZE_EXTENDED = ["0.5K", ...OPENROUTER_IMAGE_SIZE];
|
|
1028
|
+
var OPENROUTER_IMAGE_QUALITY = ["auto", "low", "medium", "high"];
|
|
987
1029
|
var SEEDANCE_DURATIONS = [4, 5, 6, 8, 10, 12, 15];
|
|
988
1030
|
var ELEVENLABS_OUTPUT_FORMATS = [
|
|
989
1031
|
"mp3_22050_32",
|
|
@@ -1007,6 +1049,13 @@ var IMAGE_GENERATE_MODELS = [
|
|
|
1007
1049
|
"google/gemini-3-pro-image-preview",
|
|
1008
1050
|
"recraft/recraft-v4.1-pro-vector"
|
|
1009
1051
|
];
|
|
1052
|
+
var VIDEO_GENERATE_MODELS = [
|
|
1053
|
+
"bytedance/seedance-2.0",
|
|
1054
|
+
"google/veo-3.1",
|
|
1055
|
+
"google/veo-3.1-fast",
|
|
1056
|
+
"kwaivgi/kling-v3.0-pro"
|
|
1057
|
+
];
|
|
1058
|
+
var DEFAULT_VIDEO_GENERATE_MODEL = "bytedance/seedance-2.0";
|
|
1010
1059
|
var MODEL_REGISTRY = {
|
|
1011
1060
|
text_generate: {
|
|
1012
1061
|
"~google/gemini-flash-latest": {
|
|
@@ -1102,7 +1151,8 @@ var MODEL_REGISTRY = {
|
|
|
1102
1151
|
params: {
|
|
1103
1152
|
prompt: { kind: "string" },
|
|
1104
1153
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1105
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1154
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1155
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1106
1156
|
}
|
|
1107
1157
|
},
|
|
1108
1158
|
"google/gemini-3.5-flash": {
|
|
@@ -1114,7 +1164,8 @@ var MODEL_REGISTRY = {
|
|
|
1114
1164
|
params: {
|
|
1115
1165
|
prompt: { kind: "string" },
|
|
1116
1166
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
|
|
1117
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
|
|
1167
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED },
|
|
1168
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1118
1169
|
}
|
|
1119
1170
|
},
|
|
1120
1171
|
"google/gemini-3.1-flash-image-preview": {
|
|
@@ -1136,7 +1187,8 @@ var MODEL_REGISTRY = {
|
|
|
1136
1187
|
params: {
|
|
1137
1188
|
prompt: { kind: "string" },
|
|
1138
1189
|
aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
|
|
1139
|
-
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
|
|
1190
|
+
image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
|
|
1191
|
+
quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
|
|
1140
1192
|
}
|
|
1141
1193
|
},
|
|
1142
1194
|
"recraft/recraft-v4.1-pro-vector": {
|
|
@@ -1239,6 +1291,53 @@ var MODEL_REGISTRY = {
|
|
|
1239
1291
|
generate_audio: { kind: "boolean" }
|
|
1240
1292
|
}
|
|
1241
1293
|
},
|
|
1294
|
+
"google/veo-3.1": {
|
|
1295
|
+
// Photoreal CINE CEILING + the real-face fallback (Veo generates adult
|
|
1296
|
+
// humans from a keyframe, dodging ByteDance's real-person filter). Same
|
|
1297
|
+
// OpenRouter google-vertex routing as the fast tier; the quality dial is
|
|
1298
|
+
// `resolution: 1080p` + `generate_audio` + a real `negative_prompt`, not a
|
|
1299
|
+
// separate provider knob. Reach for this for hero beats and any clip that
|
|
1300
|
+
// must carry a real human likeness.
|
|
1301
|
+
label: "Google Veo 3.1",
|
|
1302
|
+
inputs: [],
|
|
1303
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1304
|
+
required: ["prompt"],
|
|
1305
|
+
params: {
|
|
1306
|
+
prompt: { kind: "string" },
|
|
1307
|
+
negative_prompt: { kind: "string" },
|
|
1308
|
+
aspect_ratio: { kind: "string", enum: ["16:9", "9:16"] },
|
|
1309
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1310
|
+
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1311
|
+
seed: { kind: "number" },
|
|
1312
|
+
generate_audio: { kind: "boolean" },
|
|
1313
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1314
|
+
enhance_prompt: { kind: "boolean" },
|
|
1315
|
+
conditioning_scale: { kind: "number" }
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
"kwaivgi/kling-v3.0-pro": {
|
|
1319
|
+
// Motion-transfer / dynamic multi-shot beats. Reachable through the default
|
|
1320
|
+
// OpenRouter gateway (generic video body — no google-vertex block), so it
|
|
1321
|
+
// needs no direct-provider exception. Cost is usage-based (known from the
|
|
1322
|
+
// provider response), so it has no pre-flight cost estimate. `cfg_scale`
|
|
1323
|
+
// trades prompt adherence vs motion freedom; higher = closer to prompt.
|
|
1324
|
+
label: "Kling 3.0",
|
|
1325
|
+
inputs: [],
|
|
1326
|
+
optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
|
|
1327
|
+
required: ["prompt"],
|
|
1328
|
+
params: {
|
|
1329
|
+
// Kling caps the prompt shorter than Seedance; gate it here so an
|
|
1330
|
+
// over-length prompt fails validate (free) not the billed call.
|
|
1331
|
+
prompt: { kind: "string", maxLength: 2500 },
|
|
1332
|
+
negative_prompt: { kind: "string" },
|
|
1333
|
+
aspect_ratio: { kind: "string", enum: ["1:1", "16:9", "9:16"] },
|
|
1334
|
+
resolution: { kind: "string", enum: ["720p", "1080p"] },
|
|
1335
|
+
duration: { kind: "number", enum: [5, 10] },
|
|
1336
|
+
seed: { kind: "number" },
|
|
1337
|
+
generate_audio: { kind: "boolean" },
|
|
1338
|
+
cfg_scale: { kind: "number", min: 0, max: 1 }
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1242
1341
|
"google/veo-3.1-fast": {
|
|
1243
1342
|
// Cheap test/iteration model. Forwarded by the backend via
|
|
1244
1343
|
// `provider.options.google-vertex.parameters` (camelCased on the wire).
|
|
@@ -1732,18 +1831,43 @@ function message(e) {
|
|
|
1732
1831
|
}
|
|
1733
1832
|
|
|
1734
1833
|
// src/engine/nodes/remote/upload.ts
|
|
1834
|
+
var PUT_MAX_ATTEMPTS = 4;
|
|
1835
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1735
1836
|
async function presignAndPut(args) {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1837
|
+
let lastFailure = null;
|
|
1838
|
+
for (let attempt = 0; attempt < PUT_MAX_ATTEMPTS; attempt++) {
|
|
1839
|
+
if (args.ctx.signal?.aborted) break;
|
|
1840
|
+
if (attempt > 0) await sleep2(500 * 2 ** (attempt - 1) * (1 + Math.random() * 0.25));
|
|
1841
|
+
const result = await attemptPresignedPut(args);
|
|
1842
|
+
if (result.ok) return result.url;
|
|
1843
|
+
lastFailure = result.failure;
|
|
1844
|
+
if (!result.retryable) break;
|
|
1845
|
+
}
|
|
1846
|
+
throw lastFailure ?? new Error("upload: aborted before the PUT could start");
|
|
1847
|
+
}
|
|
1848
|
+
async function attemptPresignedPut(args) {
|
|
1849
|
+
try {
|
|
1850
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1851
|
+
const putRes = await fetch(putUrl, {
|
|
1852
|
+
method: "PUT",
|
|
1853
|
+
body: new Uint8Array(args.bytes),
|
|
1854
|
+
headers: { "Content-Type": args.mime },
|
|
1855
|
+
signal: args.ctx.signal
|
|
1856
|
+
});
|
|
1857
|
+
if (putRes.ok) return { ok: true, url: publicUrl };
|
|
1858
|
+
return {
|
|
1859
|
+
ok: false,
|
|
1860
|
+
failure: new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`),
|
|
1861
|
+
// Only transient statuses warrant a replay — a 400/403 fails identically every attempt.
|
|
1862
|
+
retryable: putRes.status >= 500 || putRes.status === 429 || putRes.status === 408
|
|
1863
|
+
};
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
return {
|
|
1866
|
+
ok: false,
|
|
1867
|
+
failure: e instanceof Error ? e : new Error(String(e)),
|
|
1868
|
+
retryable: args.ctx.signal?.aborted !== true
|
|
1869
|
+
};
|
|
1745
1870
|
}
|
|
1746
|
-
return publicUrl;
|
|
1747
1871
|
}
|
|
1748
1872
|
async function ensureUploaded(ref, ctx) {
|
|
1749
1873
|
if (ref.url) return ref;
|
|
@@ -1803,6 +1927,16 @@ var OutputRef = z.object({
|
|
|
1803
1927
|
}).strict();
|
|
1804
1928
|
var VideoMeta = z.object({
|
|
1805
1929
|
duration_s: z.number(),
|
|
1930
|
+
// The one-clock contract: the spine's summed clip lengths and every audio
|
|
1931
|
+
// timeline reachable from the final mux must equal `expected_total_s` — the
|
|
1932
|
+
// validator recomputes both from the LIVE graph (`-shortest` at the final mux
|
|
1933
|
+
// silently truncates the longer stream on any mismatch). Emitted by
|
|
1934
|
+
// scaffold-video; optional so hand-authored canvases still parse.
|
|
1935
|
+
timeline: z.object({
|
|
1936
|
+
spine_node: z.string(),
|
|
1937
|
+
expected_total_s: z.number(),
|
|
1938
|
+
audio_mix_node: z.string().optional()
|
|
1939
|
+
}).strict().optional(),
|
|
1806
1940
|
// Each sequenced voiceover turn on the absolute timeline.
|
|
1807
1941
|
vo_segments: z.array(
|
|
1808
1942
|
z.object({
|
|
@@ -1861,7 +1995,17 @@ var VideoMeta = z.object({
|
|
|
1861
1995
|
// Per video_generate node: the scene's natural visual span. The validator warns
|
|
1862
1996
|
// when a clip's span exceeds the assigned model's max clip duration (e.g. a 9.2s
|
|
1863
1997
|
// scene on Veo, which caps at 8s) — the clip would truncate.
|
|
1864
|
-
clip_spans: z.array(z.object({ node: z.string(), span_s: z.number() })).optional()
|
|
1998
|
+
clip_spans: z.array(z.object({ node: z.string(), span_s: z.number() })).optional(),
|
|
1999
|
+
// WHY the graph has the nodes it has: one route per scene (clip / still_hold /
|
|
2000
|
+
// screen_still / graphic_plate / brand_card / phrase_slice / phrase_run /
|
|
2001
|
+
// composite) + a node-type histogram, stamped by scaffold-video. Read this first
|
|
2002
|
+
// when a canvas looks big.
|
|
2003
|
+
graph_stats: z.object({
|
|
2004
|
+
nodes_total: z.number(),
|
|
2005
|
+
nodes_billable: z.number(),
|
|
2006
|
+
node_types: z.record(z.string(), z.number()),
|
|
2007
|
+
scene_routes: z.array(z.object({ scene: z.number(), route: z.string(), window_s: z.number() }))
|
|
2008
|
+
}).strict().optional()
|
|
1865
2009
|
}).strict().optional();
|
|
1866
2010
|
var CanvasMetadata = z.object({
|
|
1867
2011
|
name: z.string().optional(),
|
|
@@ -2280,8 +2424,146 @@ function dfsCycle(u, color, stack, reverseAdj) {
|
|
|
2280
2424
|
}
|
|
2281
2425
|
|
|
2282
2426
|
// src/engine/engine/validator.ts
|
|
2427
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2428
|
+
import path3 from "path";
|
|
2283
2429
|
import { z as z2 } from "zod";
|
|
2284
2430
|
|
|
2431
|
+
// src/engine/scaffold/lib/prompt-profiles.ts
|
|
2432
|
+
var VEO_PERSON_GENERATION = "allow_adult";
|
|
2433
|
+
var VEO_NEGATIVE_PROMPT = "subtitles, captions, on-screen text, watermark, logo, warped face, distorted hands, extra fingers, low quality";
|
|
2434
|
+
var VEO_DURATIONS = [4, 6, 8];
|
|
2435
|
+
var KLING_DURATIONS = [5, 10];
|
|
2436
|
+
var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
|
|
2437
|
+
var KLING_CFG_SCALE = 0.7;
|
|
2438
|
+
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}"`;
|
|
2439
|
+
var SEEDANCE_PROFILE = {
|
|
2440
|
+
id: "seedance",
|
|
2441
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2442
|
+
extraDirectives: [],
|
|
2443
|
+
// Seedance has no negative_prompt field. Phrase the anti-artifact intent as the POSITIVE
|
|
2444
|
+
// state we want (crisp hands, one stable face, smooth coherent motion, locked identity) —
|
|
2445
|
+
// an "Avoid: warped fingers, morphing faces" list tends to plant those very artifacts.
|
|
2446
|
+
stabilityDirectives: [
|
|
2447
|
+
"hands and fingers crisp and anatomically correct",
|
|
2448
|
+
"one consistent face and identity across every frame",
|
|
2449
|
+
"smooth, temporally coherent motion that holds steady frame to frame"
|
|
2450
|
+
],
|
|
2451
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; change only the motion described.",
|
|
2452
|
+
// ~120 words is the Seedance quality sweet spot: past it the model starts dropping beats and
|
|
2453
|
+
// the attention budget dilutes. The validator warns above this; composite scenes that stack
|
|
2454
|
+
// many brief `parts` are the usual offenders.
|
|
2455
|
+
wordBudget: 120,
|
|
2456
|
+
durationSet: SEEDANCE_DURATIONS,
|
|
2457
|
+
paramDefaults: {}
|
|
2458
|
+
};
|
|
2459
|
+
var VEO_PROFILE = {
|
|
2460
|
+
id: "veo",
|
|
2461
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2462
|
+
// Veo's known failure mode: quoted dialogue triggers burned-in subtitles. Belt
|
|
2463
|
+
// (inline) and braces (the negative_prompt param, below) — the param is the real lever.
|
|
2464
|
+
extraDirectives: ["No subtitles, no captions, no on-screen text of any kind."],
|
|
2465
|
+
stabilityDirectives: [],
|
|
2466
|
+
// Veo takes a negative_prompt PARAM instead (paramDefaults).
|
|
2467
|
+
keyframeInstruction: "Describe the transition and camera move between the frames; the frames fix the content.",
|
|
2468
|
+
wordBudget: 200,
|
|
2469
|
+
durationSet: VEO_DURATIONS,
|
|
2470
|
+
paramDefaults: { person_generation: VEO_PERSON_GENERATION, negative_prompt: VEO_NEGATIVE_PROMPT }
|
|
2471
|
+
};
|
|
2472
|
+
var KLING_PROFILE = {
|
|
2473
|
+
id: "kling",
|
|
2474
|
+
dialogueDirective: SPEAKS_PROSE,
|
|
2475
|
+
// Belt (inline) and braces (the negative_prompt param) — the param is the real lever.
|
|
2476
|
+
extraDirectives: ["Single, deliberate camera move \u2014 no whip pans or rapid cuts within the clip."],
|
|
2477
|
+
stabilityDirectives: [],
|
|
2478
|
+
// Kling takes a negative_prompt PARAM instead (paramDefaults).
|
|
2479
|
+
keyframeInstruction: "Preserve the composition and colors of the first frame; animate the motion described.",
|
|
2480
|
+
wordBudget: 200,
|
|
2481
|
+
durationSet: KLING_DURATIONS,
|
|
2482
|
+
paramDefaults: { negative_prompt: KLING_NEGATIVE_PROMPT, cfg_scale: KLING_CFG_SCALE }
|
|
2483
|
+
};
|
|
2484
|
+
function clipProfileFor(modelId) {
|
|
2485
|
+
if (/^bytedance\/seedance/.test(modelId)) return SEEDANCE_PROFILE;
|
|
2486
|
+
if (/^google\/veo/.test(modelId)) return VEO_PROFILE;
|
|
2487
|
+
if (/^kwaivgi\/kling|^kling\//.test(modelId)) return KLING_PROFILE;
|
|
2488
|
+
return void 0;
|
|
2489
|
+
}
|
|
2490
|
+
function clipParamRecipe(profile, intent) {
|
|
2491
|
+
const out = {};
|
|
2492
|
+
if (intent === "hero") {
|
|
2493
|
+
out.resolution = "1080p";
|
|
2494
|
+
}
|
|
2495
|
+
if (intent === "hook" && profile.id === "kling") {
|
|
2496
|
+
out.cfg_scale = 0.85;
|
|
2497
|
+
}
|
|
2498
|
+
return out;
|
|
2499
|
+
}
|
|
2500
|
+
function nativeDialogueOf(prompt) {
|
|
2501
|
+
if (typeof prompt !== "string") return void 0;
|
|
2502
|
+
const m = prompt.match(/Dialogue: "(.*)"/);
|
|
2503
|
+
return m?.[1]?.trim() || void 0;
|
|
2504
|
+
}
|
|
2505
|
+
var GPT_IMAGE_PROFILE = {
|
|
2506
|
+
id: "gpt-image",
|
|
2507
|
+
constraintPlacement: "last",
|
|
2508
|
+
photorealCue: true,
|
|
2509
|
+
// OpenRouter forwards `quality`; gpt-image-2 already processes inputs at high
|
|
2510
|
+
// fidelity automatically, so we deliberately do NOT send `input_fidelity`.
|
|
2511
|
+
paramDefaults: { quality: "high" }
|
|
2512
|
+
};
|
|
2513
|
+
var GEMINI_IMAGE_PROFILE = {
|
|
2514
|
+
id: "gemini",
|
|
2515
|
+
constraintPlacement: "inline",
|
|
2516
|
+
photorealCue: true,
|
|
2517
|
+
paramDefaults: { quality: "high" }
|
|
2518
|
+
};
|
|
2519
|
+
var RECRAFT_IMAGE_PROFILE = {
|
|
2520
|
+
id: "recraft",
|
|
2521
|
+
constraintPlacement: "inline",
|
|
2522
|
+
photorealCue: false,
|
|
2523
|
+
paramDefaults: {}
|
|
2524
|
+
};
|
|
2525
|
+
function imageProfileFor(modelId) {
|
|
2526
|
+
if (/^openai\/gpt-.*image/.test(modelId)) return GPT_IMAGE_PROFILE;
|
|
2527
|
+
if (/^google\/gemini/.test(modelId)) return GEMINI_IMAGE_PROFILE;
|
|
2528
|
+
if (/^recraft\//.test(modelId)) return RECRAFT_IMAGE_PROFILE;
|
|
2529
|
+
return void 0;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/engine/scaffold/spine-input.ts
|
|
2533
|
+
function spineInputFlags(work, lenS) {
|
|
2534
|
+
return work.still ? ["-loop", "1", "-t", lenS.toFixed(3)] : [];
|
|
2535
|
+
}
|
|
2536
|
+
function spineInputOps(work, lenS, dims) {
|
|
2537
|
+
const fit = work.still?.fit ?? "fill";
|
|
2538
|
+
const scale = fit === "pad" ? `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=decrease,pad=${dims.w}:${dims.h}:(ow-iw)/2:(oh-ih)/2:color=black` : `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=increase,crop=${dims.w}:${dims.h}`;
|
|
2539
|
+
const normalize = `${scale},format=yuv420p,fps=30,setsar=1,settb=AVTB`;
|
|
2540
|
+
if (work.trim) {
|
|
2541
|
+
const start = work.trim.offset_s > 0 ? `start=${work.trim.offset_s.toFixed(3)}:` : "";
|
|
2542
|
+
return `${normalize},trim=${start}duration=${lenS.toFixed(3)},setpts=PTS-STARTPTS`;
|
|
2543
|
+
}
|
|
2544
|
+
if (work.still) return `${normalize},trim=duration=${lenS.toFixed(3)}`;
|
|
2545
|
+
return normalize;
|
|
2546
|
+
}
|
|
2547
|
+
function parseSpineInputLens(args) {
|
|
2548
|
+
const lens = /* @__PURE__ */ new Map();
|
|
2549
|
+
let inputIndex = 0;
|
|
2550
|
+
for (let i = 0; i < args.length; i++) {
|
|
2551
|
+
if (args[i] !== "-i") continue;
|
|
2552
|
+
if (args[i - 2] === "-t") {
|
|
2553
|
+
const v = Number(args[i - 1]);
|
|
2554
|
+
if (Number.isFinite(v)) lens.set(inputIndex, v);
|
|
2555
|
+
}
|
|
2556
|
+
inputIndex++;
|
|
2557
|
+
}
|
|
2558
|
+
const graph = args.join(" ");
|
|
2559
|
+
for (const m of graph.matchAll(/\[(\d+):v\][^;[]*?trim=(?:start=[\d.]+:)?duration=([\d.]+)/g)) {
|
|
2560
|
+
const idx = Number(m[1]);
|
|
2561
|
+
const v = Number(m[2]);
|
|
2562
|
+
if (Number.isFinite(idx) && Number.isFinite(v)) lens.set(idx, v);
|
|
2563
|
+
}
|
|
2564
|
+
return lens;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2285
2567
|
// src/engine/engine/define.ts
|
|
2286
2568
|
function resolveOutputKinds(spec, params) {
|
|
2287
2569
|
if (!spec) return {};
|
|
@@ -2363,16 +2645,63 @@ var STAGE_CODES = {
|
|
|
2363
2645
|
REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
|
|
2364
2646
|
SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL",
|
|
2365
2647
|
UI_IN_PROMPT: "VIDEO_UI_IN_PROMPT",
|
|
2366
|
-
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT"
|
|
2648
|
+
BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT",
|
|
2649
|
+
SPEECH_EXCEEDS_EXTRACT: "VIDEO_SPEECH_EXCEEDS_EXTRACT",
|
|
2650
|
+
PROMPT_PROFILE_MISSING: "VIDEO_PROMPT_PROFILE_MISSING",
|
|
2651
|
+
PROMPT_DECISION_MISSING: "VIDEO_PROMPT_DECISION_MISSING",
|
|
2652
|
+
HOOK_LAYER_MISSING: "VIDEO_HOOK_LAYER_MISSING",
|
|
2653
|
+
IMAGE_PROFILE_MISSING: "IMAGE_PROMPT_PROFILE_MISSING",
|
|
2654
|
+
PROMPT_OVER_BUDGET: "VIDEO_PROMPT_OVER_BUDGET",
|
|
2655
|
+
PERSON_GENERATION_MISSING: "VIDEO_PERSON_GENERATION_MISSING",
|
|
2656
|
+
RAW_FACE_KEYFRAME: "VIDEO_RAW_FACE_KEYFRAME",
|
|
2657
|
+
TIMELINE_TOTAL: "VIDEO_TIMELINE_TOTAL_MISMATCH",
|
|
2658
|
+
NATIVE_SEG_OVERLAP: "VIDEO_NATIVE_SEG_OVERLAP",
|
|
2659
|
+
SPINE_UNNORMALIZED: "VIDEO_SPINE_UNNORMALIZED",
|
|
2660
|
+
ODD_DIMENSIONS: "VIDEO_ODD_DIMENSIONS",
|
|
2661
|
+
REGION_DROPPED: "VIDEO_REGION_DROPPED",
|
|
2662
|
+
OVERLAY_OUT_OF_BOUNDS: "VIDEO_OVERLAY_OUT_OF_BOUNDS",
|
|
2663
|
+
ORPHAN_NODE: "ORPHAN_NODE"
|
|
2367
2664
|
};
|
|
2368
2665
|
var SPAN_MODEL_SLACK_S = 0.25;
|
|
2369
2666
|
var VIDEO_TIME_SLACK_S = 0.75;
|
|
2370
2667
|
var SPEECH_WORDS_PER_SECOND = 2.5;
|
|
2371
2668
|
var SPEECH_OVERRUN_RATIO = 1.6;
|
|
2372
|
-
function
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2669
|
+
function ffmpegTrimSeconds(node) {
|
|
2670
|
+
const args = node.params?.args;
|
|
2671
|
+
if (!Array.isArray(args)) return null;
|
|
2672
|
+
let t = null;
|
|
2673
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
2674
|
+
if (args[i] === "-t") {
|
|
2675
|
+
const v = Number(args[i + 1]);
|
|
2676
|
+
if (Number.isFinite(v)) t = v;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return t;
|
|
2680
|
+
}
|
|
2681
|
+
function refNodeOf(ctx, value) {
|
|
2682
|
+
if (typeof value !== "string" || !value.startsWith(REF_PREFIX)) return null;
|
|
2683
|
+
const parsed = parseRefExpr(value);
|
|
2684
|
+
if (!parsed) return null;
|
|
2685
|
+
const idx = ctx.idToIndex.get(parsed.nodeId);
|
|
2686
|
+
return idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
2687
|
+
}
|
|
2688
|
+
function liveNodeDurationS(ctx, node, depth = 0) {
|
|
2689
|
+
if (!node || depth > 3) return null;
|
|
2690
|
+
if (node.type === "video_generate") {
|
|
2691
|
+
const d = node.params?.duration;
|
|
2692
|
+
return typeof d === "number" ? d : null;
|
|
2693
|
+
}
|
|
2694
|
+
if (node.type === "ffmpeg") {
|
|
2695
|
+
const t = ffmpegTrimSeconds(node);
|
|
2696
|
+
if (t !== null) return t;
|
|
2697
|
+
for (const v of Object.values(node.inputs ?? {})) {
|
|
2698
|
+
const upstream = refNodeOf(ctx, v);
|
|
2699
|
+
const d = liveNodeDurationS(ctx, upstream, depth + 1);
|
|
2700
|
+
if (d !== null) return d;
|
|
2701
|
+
}
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
return null;
|
|
2376
2705
|
}
|
|
2377
2706
|
function validateCanvas(input, registry) {
|
|
2378
2707
|
const issues = [];
|
|
@@ -2395,6 +2724,7 @@ function validateCanvas(input, registry) {
|
|
|
2395
2724
|
checkAllSlots(ctx);
|
|
2396
2725
|
const estimatedCredits = estimateCredits(ctx);
|
|
2397
2726
|
checkOutputRef(ctx);
|
|
2727
|
+
checkOrphanNodes(ctx);
|
|
2398
2728
|
checkVideoInvariants(ctx);
|
|
2399
2729
|
const hasBlocking = issues.some(isBlocking);
|
|
2400
2730
|
if (hasBlocking) return { ok: false, issues };
|
|
@@ -2434,6 +2764,7 @@ async function validateCanvasDeep(input, registry) {
|
|
|
2434
2764
|
});
|
|
2435
2765
|
}
|
|
2436
2766
|
}
|
|
2767
|
+
await checkOverlayTimingBounds(canvas, issues);
|
|
2437
2768
|
const hasBlocking = issues.some(isBlocking);
|
|
2438
2769
|
if (hasBlocking) return { ok: false, issues };
|
|
2439
2770
|
const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
|
|
@@ -2675,6 +3006,38 @@ function estimateCredits(ctx) {
|
|
|
2675
3006
|
}
|
|
2676
3007
|
return total;
|
|
2677
3008
|
}
|
|
3009
|
+
function overlayTimingIssues(html, durationS) {
|
|
3010
|
+
const out = [];
|
|
3011
|
+
for (const m of html.matchAll(/<[^>]*\bdata-start="([\d.]+)"[^>]*\bdata-dur="([\d.]+)"[^>]*>/g)) {
|
|
3012
|
+
const start = Number(m[1]);
|
|
3013
|
+
const dur = Number(m[2]);
|
|
3014
|
+
if (!Number.isFinite(start) || !Number.isFinite(dur)) continue;
|
|
3015
|
+
if (start >= durationS || start + dur > durationS + 0.25) out.push({ start, dur });
|
|
3016
|
+
}
|
|
3017
|
+
return out;
|
|
3018
|
+
}
|
|
3019
|
+
async function checkOverlayTimingBounds(canvas, issues) {
|
|
3020
|
+
const durationS = canvas.metadata?.video?.duration_s;
|
|
3021
|
+
if (typeof durationS !== "number") return;
|
|
3022
|
+
for (let i = 0; i < canvas.nodes.length; i++) {
|
|
3023
|
+
const n = canvas.nodes[i];
|
|
3024
|
+
if (n?.type !== "hyperframe_render") continue;
|
|
3025
|
+
const composition = n.params?.composition;
|
|
3026
|
+
if (typeof composition !== "string" || !path3.isAbsolute(composition)) continue;
|
|
3027
|
+
const html = await readFile2(path3.join(composition, "index.html"), "utf8").catch(() => null);
|
|
3028
|
+
if (!html) continue;
|
|
3029
|
+
for (const w of overlayTimingIssues(html, durationS)) {
|
|
3030
|
+
issues.push({
|
|
3031
|
+
path: `nodes[${i}].params.composition`,
|
|
3032
|
+
code: STAGE_CODES.OVERLAY_OUT_OF_BOUNDS,
|
|
3033
|
+
severity: "warning",
|
|
3034
|
+
node_id: n.id,
|
|
3035
|
+
node_type: n.type,
|
|
3036
|
+
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`
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
2678
3041
|
function nativeAudioReachesMix(ctx, scene) {
|
|
2679
3042
|
const wanted = [
|
|
2680
3043
|
`$ref:s${scene}_voextract.audio`,
|
|
@@ -2707,6 +3070,13 @@ function talkingSceneSatisfied(ctx, entry, scene) {
|
|
|
2707
3070
|
});
|
|
2708
3071
|
}
|
|
2709
3072
|
function checkVideoInvariants(ctx) {
|
|
3073
|
+
checkPromptProfile(ctx);
|
|
3074
|
+
checkImageProfile(ctx);
|
|
3075
|
+
checkPersonGeneration(ctx);
|
|
3076
|
+
checkPromptBudget(ctx);
|
|
3077
|
+
checkRawFaceKeyframe(ctx);
|
|
3078
|
+
checkPromptDecisions(ctx);
|
|
3079
|
+
checkHookLayers(ctx);
|
|
2710
3080
|
const meta = ctx.canvas.metadata?.video;
|
|
2711
3081
|
if (!meta) return;
|
|
2712
3082
|
const segments = [...meta.vo_segments].sort((a, b) => a.start_s - b.start_s);
|
|
@@ -2746,6 +3116,289 @@ function checkVideoInvariants(ctx) {
|
|
|
2746
3116
|
checkBrandmarkInPrompt(ctx);
|
|
2747
3117
|
checkReferenceCompleteness(ctx, meta);
|
|
2748
3118
|
checkClipSpanFitsModel(ctx, meta);
|
|
3119
|
+
checkTimelineContract(ctx, meta);
|
|
3120
|
+
checkNativeSegOverlap(ctx);
|
|
3121
|
+
checkSpineNormalized(ctx, meta);
|
|
3122
|
+
checkOddDimensions(ctx);
|
|
3123
|
+
checkRegionDropped(ctx);
|
|
3124
|
+
}
|
|
3125
|
+
function checkPromptProfile(ctx) {
|
|
3126
|
+
for (const n of ctx.canvas.nodes) {
|
|
3127
|
+
if (n.type !== "video_generate") continue;
|
|
3128
|
+
const model = n.params?.model;
|
|
3129
|
+
if (typeof model !== "string" || clipProfileFor(model)) continue;
|
|
3130
|
+
ctx.issues.push({
|
|
3131
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3132
|
+
code: STAGE_CODES.PROMPT_PROFILE_MISSING,
|
|
3133
|
+
severity: "warning",
|
|
3134
|
+
node_id: n.id,
|
|
3135
|
+
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`
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
var TECHNIQUE_CUE = /\b(camera|push[- ]?in|pull[- ]?out|pan|dolly|zoom|tilt|track|handheld|locked|orbit|crane|whip|motion|move)\b/i;
|
|
3140
|
+
var NEGATIVES_CUE = /\bavoid:|\bno subtitles|\bnegative\b|keep it clean and stable/i;
|
|
3141
|
+
var VIBE_CUE = /\b(light|lighting|golden|moody|warm|cool|tone|grade|contrast|shadow|glow|rim|backlit|neon|soft|harsh)\b/i;
|
|
3142
|
+
function checkPromptDecisions(ctx) {
|
|
3143
|
+
for (const n of ctx.canvas.nodes) {
|
|
3144
|
+
if (n.type !== "video_generate") continue;
|
|
3145
|
+
const params = n.params;
|
|
3146
|
+
const prompt = typeof params?.prompt === "string" ? params.prompt : "";
|
|
3147
|
+
if (!prompt) continue;
|
|
3148
|
+
const hasTechnique = TECHNIQUE_CUE.test(prompt);
|
|
3149
|
+
const hasNegatives = NEGATIVES_CUE.test(prompt) || typeof params?.negative_prompt === "string";
|
|
3150
|
+
if (hasTechnique || hasNegatives) continue;
|
|
3151
|
+
ctx.issues.push({
|
|
3152
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3153
|
+
code: STAGE_CODES.PROMPT_DECISION_MISSING,
|
|
3154
|
+
severity: "warning",
|
|
3155
|
+
node_id: n.id,
|
|
3156
|
+
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`
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function checkHookLayers(ctx) {
|
|
3161
|
+
const scene0 = ctx.canvas.nodes.filter((n) => /^s0(?:[_a-z0-9])*_/.test(n.id) || n.id === "s0");
|
|
3162
|
+
const hookClip = scene0.find((n) => n.type === "video_generate");
|
|
3163
|
+
if (!hookClip) return;
|
|
3164
|
+
const scene0Text = scene0.map((n) => {
|
|
3165
|
+
const p = n.params?.prompt;
|
|
3166
|
+
return typeof p === "string" ? p : "";
|
|
3167
|
+
}).join("\n");
|
|
3168
|
+
const hookParams = hookClip.params;
|
|
3169
|
+
const hasOverlayLayer = ctx.canvas.nodes.some((n) => /hyperframe/.test(n.type));
|
|
3170
|
+
const hasSound = hookParams?.generate_audio === true || /Dialogue:|Audio:/.test(scene0Text) || ctx.canvas.nodes.some((n) => n.type === "tts" || n.type === "dialogue" || n.type === "music");
|
|
3171
|
+
const hasVisual = scene0Text.trim().length > 40;
|
|
3172
|
+
const hasVibe = VIBE_CUE.test(scene0Text);
|
|
3173
|
+
const missing = [];
|
|
3174
|
+
if (!hasOverlayLayer) missing.push("text (overlay)");
|
|
3175
|
+
if (!hasSound) missing.push("sound (line/SFX)");
|
|
3176
|
+
if (!hasVisual) missing.push("visual (the frame)");
|
|
3177
|
+
if (!hasVibe) missing.push("vibe (lighting/tone)");
|
|
3178
|
+
if (missing.length < 2) return;
|
|
3179
|
+
ctx.issues.push({
|
|
3180
|
+
path: `nodes[${ctx.idToIndex.get(hookClip.id) ?? -1}]`,
|
|
3181
|
+
code: STAGE_CODES.HOOK_LAYER_MISSING,
|
|
3182
|
+
severity: "warning",
|
|
3183
|
+
node_id: hookClip.id,
|
|
3184
|
+
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`
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
function checkImageProfile(ctx) {
|
|
3188
|
+
for (const n of ctx.canvas.nodes) {
|
|
3189
|
+
if (n.type !== "image_generate") continue;
|
|
3190
|
+
const model = n.params?.model;
|
|
3191
|
+
if (typeof model !== "string" || imageProfileFor(model)) continue;
|
|
3192
|
+
ctx.issues.push({
|
|
3193
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.model`,
|
|
3194
|
+
code: STAGE_CODES.IMAGE_PROFILE_MISSING,
|
|
3195
|
+
severity: "warning",
|
|
3196
|
+
node_id: n.id,
|
|
3197
|
+
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`
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
function checkPersonGeneration(ctx) {
|
|
3202
|
+
for (const n of ctx.canvas.nodes) {
|
|
3203
|
+
if (n.type !== "video_generate") continue;
|
|
3204
|
+
const params = n.params;
|
|
3205
|
+
const model = params?.model;
|
|
3206
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "veo") continue;
|
|
3207
|
+
const hasKeyframe = Boolean(n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3208
|
+
if (!hasKeyframe || params?.person_generation) continue;
|
|
3209
|
+
ctx.issues.push({
|
|
3210
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params`,
|
|
3211
|
+
code: STAGE_CODES.PERSON_GENERATION_MISSING,
|
|
3212
|
+
severity: "warning",
|
|
3213
|
+
node_id: n.id,
|
|
3214
|
+
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)`
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
function checkPromptBudget(ctx) {
|
|
3219
|
+
for (const n of ctx.canvas.nodes) {
|
|
3220
|
+
if (n.type !== "video_generate") continue;
|
|
3221
|
+
const params = n.params;
|
|
3222
|
+
const model = params?.model;
|
|
3223
|
+
const prompt = params?.prompt;
|
|
3224
|
+
const profile = typeof model === "string" ? clipProfileFor(model) : void 0;
|
|
3225
|
+
if (!profile || typeof prompt !== "string") continue;
|
|
3226
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean).length;
|
|
3227
|
+
if (words <= profile.wordBudget) continue;
|
|
3228
|
+
ctx.issues.push({
|
|
3229
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3230
|
+
code: STAGE_CODES.PROMPT_OVER_BUDGET,
|
|
3231
|
+
severity: "warning",
|
|
3232
|
+
node_id: n.id,
|
|
3233
|
+
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)`
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
function checkRawFaceKeyframe(ctx) {
|
|
3238
|
+
for (const n of ctx.canvas.nodes) {
|
|
3239
|
+
if (n.type !== "video_generate") continue;
|
|
3240
|
+
const model = n.params?.model;
|
|
3241
|
+
if (clipProfileFor(typeof model === "string" ? model : "")?.id !== "seedance") continue;
|
|
3242
|
+
const src = refNodeOf(ctx, n.inputs?.first_frame ?? n.inputs?.reference);
|
|
3243
|
+
if (!src || src.type !== "ingest") continue;
|
|
3244
|
+
ctx.issues.push({
|
|
3245
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].inputs.first_frame`,
|
|
3246
|
+
code: STAGE_CODES.RAW_FACE_KEYFRAME,
|
|
3247
|
+
severity: "warning",
|
|
3248
|
+
node_id: n.id,
|
|
3249
|
+
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`
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function spineTotalS(ctx, spine) {
|
|
3254
|
+
let total = 0;
|
|
3255
|
+
const inputs = Object.entries(spine.inputs ?? {}).filter(([k]) => /^c\d+$/.test(k));
|
|
3256
|
+
if (inputs.length === 0) return null;
|
|
3257
|
+
const rawArgs = spine.params.args;
|
|
3258
|
+
const absorbed = Array.isArray(rawArgs) ? parseSpineInputLens(rawArgs.map(String)) : /* @__PURE__ */ new Map();
|
|
3259
|
+
for (const [k, v] of inputs) {
|
|
3260
|
+
const inputIdx = Number(k.slice(1));
|
|
3261
|
+
const d = absorbed.get(inputIdx) ?? liveNodeDurationS(ctx, refNodeOf(ctx, v));
|
|
3262
|
+
if (d === null) return null;
|
|
3263
|
+
total += d;
|
|
3264
|
+
}
|
|
3265
|
+
const graph = spine.params.args?.join?.(" ") ?? "";
|
|
3266
|
+
for (const m of String(graph).matchAll(/xfade=transition=[^:]+:duration=([\d.]+)/g)) {
|
|
3267
|
+
total -= Number(m[1]);
|
|
3268
|
+
}
|
|
3269
|
+
return total;
|
|
3270
|
+
}
|
|
3271
|
+
function checkTimelineContract(ctx, meta) {
|
|
3272
|
+
const stamp = meta.timeline;
|
|
3273
|
+
const spineId = stamp?.spine_node ?? (ctx.idToIndex.has("spine") ? "spine" : null);
|
|
3274
|
+
if (!spineId) return;
|
|
3275
|
+
const expected = stamp?.expected_total_s ?? meta.duration_s;
|
|
3276
|
+
checkSpineTotal(ctx, spineId, expected);
|
|
3277
|
+
checkAudioTimelineTotals(ctx, stamp?.audio_mix_node, expected);
|
|
3278
|
+
}
|
|
3279
|
+
function checkSpineTotal(ctx, spineId, expected) {
|
|
3280
|
+
const spineIdx = ctx.idToIndex.get(spineId);
|
|
3281
|
+
const spine = spineIdx === void 0 ? null : ctx.canvas.nodes[spineIdx];
|
|
3282
|
+
const spineLen = spine ? spineTotalS(ctx, spine) : null;
|
|
3283
|
+
if (spineLen === null || Math.abs(spineLen - expected) <= VIDEO_TIME_SLACK_S) return;
|
|
3284
|
+
ctx.issues.push({
|
|
3285
|
+
path: `nodes[${spineIdx}].params.args`,
|
|
3286
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3287
|
+
node_id: spineId,
|
|
3288
|
+
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`
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
function checkAudioTimelineTotals(ctx, audioMixNode, expected) {
|
|
3292
|
+
const mustBeFullLength = /* @__PURE__ */ new Set();
|
|
3293
|
+
if (audioMixNode) mustBeFullLength.add(audioMixNode);
|
|
3294
|
+
ctx.canvas.nodes.forEach((n) => {
|
|
3295
|
+
if (n.type !== "audio_voice_convert") return;
|
|
3296
|
+
const track = refNodeOf(ctx, (n.inputs ?? {}).audio);
|
|
3297
|
+
if (track?.type === "audio_timeline") mustBeFullLength.add(track.id);
|
|
3298
|
+
});
|
|
3299
|
+
for (const id of mustBeFullLength) {
|
|
3300
|
+
const idx = ctx.idToIndex.get(id);
|
|
3301
|
+
const node = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3302
|
+
const totalMs = node?.params?.total_ms;
|
|
3303
|
+
if (typeof totalMs !== "number") continue;
|
|
3304
|
+
if (Math.abs(totalMs / 1e3 - expected) > VIDEO_TIME_SLACK_S) {
|
|
3305
|
+
ctx.issues.push({
|
|
3306
|
+
path: `nodes[${idx}].params.total_ms`,
|
|
3307
|
+
code: STAGE_CODES.TIMELINE_TOTAL,
|
|
3308
|
+
node_id: id,
|
|
3309
|
+
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)}`
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
function checkNativeSegOverlap(ctx) {
|
|
3315
|
+
for (const conv of ctx.canvas.nodes) {
|
|
3316
|
+
if (conv.type !== "audio_voice_convert") continue;
|
|
3317
|
+
const track = refNodeOf(ctx, (conv.inputs ?? {}).audio);
|
|
3318
|
+
if (track?.type !== "audio_timeline") continue;
|
|
3319
|
+
const params = track.params;
|
|
3320
|
+
const windows = (params.tracks ?? []).map((t) => {
|
|
3321
|
+
const extract = refNodeOf(ctx, (track.inputs ?? {})[t.slot]);
|
|
3322
|
+
const len = t.duration_s ?? (extract ? ffmpegTrimSeconds(extract) : null);
|
|
3323
|
+
return len === null ? null : { slot: t.slot, start: t.start_s, end: t.start_s + len };
|
|
3324
|
+
}).filter((w) => w !== null).sort((a, b) => a.start - b.start);
|
|
3325
|
+
for (let i = 1; i < windows.length; i++) {
|
|
3326
|
+
const prev = windows[i - 1];
|
|
3327
|
+
const cur = windows[i];
|
|
3328
|
+
if (!prev || !cur || cur.start >= prev.end - 0.01) continue;
|
|
3329
|
+
const trackIdx = ctx.idToIndex.get(track.id);
|
|
3330
|
+
ctx.issues.push({
|
|
3331
|
+
path: `nodes[${trackIdx}].params.tracks`,
|
|
3332
|
+
code: STAGE_CODES.NATIVE_SEG_OVERLAP,
|
|
3333
|
+
node_id: track.id,
|
|
3334
|
+
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`
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
function checkSpineNormalized(ctx, meta) {
|
|
3340
|
+
const spineId = meta.timeline?.spine_node ?? "spine";
|
|
3341
|
+
const idx = ctx.idToIndex.get(spineId);
|
|
3342
|
+
const spine = idx === void 0 ? null : ctx.canvas.nodes[idx];
|
|
3343
|
+
if (!spine || spine.type !== "ffmpeg") return;
|
|
3344
|
+
const args = spine.params.args;
|
|
3345
|
+
const graph = Array.isArray(args) ? args.join(" ") : "";
|
|
3346
|
+
if (!graph.includes("concat=n=")) return;
|
|
3347
|
+
if (/\[\d+:v\](?:\[|concat)/.test(graph)) {
|
|
3348
|
+
ctx.issues.push({
|
|
3349
|
+
path: `nodes[${idx}].params.args`,
|
|
3350
|
+
code: STAGE_CODES.SPINE_UNNORMALIZED,
|
|
3351
|
+
severity: "warning",
|
|
3352
|
+
node_id: spineId,
|
|
3353
|
+
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`
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function checkOddDimensions(ctx) {
|
|
3358
|
+
ctx.canvas.nodes.forEach((node, idx) => {
|
|
3359
|
+
if (node.type !== "ffmpeg") return;
|
|
3360
|
+
const args = node.params.args;
|
|
3361
|
+
const blob = Array.isArray(args) ? args.join(" ") : "";
|
|
3362
|
+
for (const m of blob.matchAll(/(?:scale|crop|pad)=(\d+):(\d+)/g)) {
|
|
3363
|
+
const w = Number(m[1]);
|
|
3364
|
+
const h = Number(m[2]);
|
|
3365
|
+
if (w % 2 === 0 && h % 2 === 0) continue;
|
|
3366
|
+
ctx.issues.push({
|
|
3367
|
+
path: `nodes[${idx}].params.args`,
|
|
3368
|
+
code: STAGE_CODES.ODD_DIMENSIONS,
|
|
3369
|
+
severity: "warning",
|
|
3370
|
+
node_id: node.id,
|
|
3371
|
+
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`
|
|
3372
|
+
});
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
function checkRegionDropped(ctx) {
|
|
3378
|
+
const regionClips = /* @__PURE__ */ new Map();
|
|
3379
|
+
for (const node of ctx.canvas.nodes) {
|
|
3380
|
+
const m = /^s(\d+)_r\d+_clip$/.exec(node.id);
|
|
3381
|
+
if (m && node.type === "video_generate")
|
|
3382
|
+
regionClips.set(m[1], (regionClips.get(m[1]) ?? 0) + 1);
|
|
3383
|
+
}
|
|
3384
|
+
for (const [scene, billed] of regionClips) {
|
|
3385
|
+
const idx = ctx.idToIndex.get(`s${scene}_composite`);
|
|
3386
|
+
if (idx === void 0) continue;
|
|
3387
|
+
const composite = ctx.canvas.nodes[idx];
|
|
3388
|
+
const consumed = Object.keys(composite.inputs ?? {}).filter((k) => /^c\d+$/.test(k)).length;
|
|
3389
|
+
if (billed > consumed) {
|
|
3390
|
+
ctx.issues.push({
|
|
3391
|
+
path: `nodes[${idx}].inputs`,
|
|
3392
|
+
code: STAGE_CODES.REGION_DROPPED,
|
|
3393
|
+
severity: "warning",
|
|
3394
|
+
node_id: composite.id,
|
|
3395
|
+
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`
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
function round2(n) {
|
|
3401
|
+
return Math.round(n * 100) / 100;
|
|
2749
3402
|
}
|
|
2750
3403
|
var ELEMENT_TYPE_KEYWORDS = {
|
|
2751
3404
|
animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
|
|
@@ -2873,15 +3526,37 @@ function checkSpeechOverrun(ctx, talkingScenes) {
|
|
|
2873
3526
|
for (const n of ctx.canvas.nodes) {
|
|
2874
3527
|
if (!nativeClipRe.test(n.id) || n.type !== "video_generate") continue;
|
|
2875
3528
|
const overrun = speechOverrunOf(n, secondsPerWord(entry));
|
|
2876
|
-
if (
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3529
|
+
if (overrun) {
|
|
3530
|
+
ctx.issues.push({
|
|
3531
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
3532
|
+
code: STAGE_CODES.SPEECH_OVERRUN,
|
|
3533
|
+
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`
|
|
3534
|
+
});
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
checkSpeechExceedsExtract(ctx, n, entry);
|
|
2882
3538
|
}
|
|
2883
3539
|
}
|
|
2884
3540
|
}
|
|
3541
|
+
function checkSpeechExceedsExtract(ctx, clip, entry) {
|
|
3542
|
+
const params = clip.params;
|
|
3543
|
+
if (params?.generate_audio !== true) return;
|
|
3544
|
+
const line = nativeDialogueOf(params.prompt);
|
|
3545
|
+
if (!line) return;
|
|
3546
|
+
const regionTag = /^s\d+(_r\d+)?_clip$/.exec(clip.id)?.[1] ?? "";
|
|
3547
|
+
const extractIdx = ctx.idToIndex.get(`s${entry.scene}${regionTag}_voextract`) ?? ctx.idToIndex.get(`s${entry.scene}_voextract`);
|
|
3548
|
+
const extract = extractIdx === void 0 ? null : ctx.canvas.nodes[extractIdx];
|
|
3549
|
+
const windowS = extract ? ffmpegTrimSeconds(extract) : null;
|
|
3550
|
+
if (windowS === null) return;
|
|
3551
|
+
const estSpeechS = line.split(/\s+/).filter(Boolean).length * secondsPerWord(entry);
|
|
3552
|
+
if (estSpeechS <= windowS * SPEECH_OVERRUN_RATIO) return;
|
|
3553
|
+
ctx.issues.push({
|
|
3554
|
+
path: `nodes[${ctx.idToIndex.get(clip.id) ?? -1}].params.prompt`,
|
|
3555
|
+
code: STAGE_CODES.SPEECH_EXCEEDS_EXTRACT,
|
|
3556
|
+
node_id: clip.id,
|
|
3557
|
+
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`
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
2885
3560
|
var UI_IN_PROMPT_RE = /\bscreen[- ]?(?:recording|capture|grab|share)\b|\bapp (?:interface|screen)\b|\bphone screen overlay\b/i;
|
|
2886
3561
|
function checkUiInPrompt(ctx) {
|
|
2887
3562
|
for (const n of ctx.canvas.nodes) {
|
|
@@ -2925,6 +3600,28 @@ function checkAspectConsistency(ctx) {
|
|
|
2925
3600
|
message: `video_generate nodes disagree on aspect_ratio (${[...ratios].sort().join(", ")}) \u2014 the spine's segments would render at different shapes and the composite silently crops. Pin every clip to the same aspect_ratio`
|
|
2926
3601
|
});
|
|
2927
3602
|
}
|
|
3603
|
+
function checkOrphanNodes(ctx) {
|
|
3604
|
+
const out = ctx.canvas.output;
|
|
3605
|
+
if (!out || !ctx.idToIndex.has(out.node)) return;
|
|
3606
|
+
const graph = buildDepGraph(ctx.canvas);
|
|
3607
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
3608
|
+
const stack = [out.node];
|
|
3609
|
+
while (stack.length > 0) {
|
|
3610
|
+
const id = stack.pop();
|
|
3611
|
+
if (id === void 0 || reachable.has(id)) continue;
|
|
3612
|
+
reachable.add(id);
|
|
3613
|
+
for (const dep of graph.get(id) ?? []) stack.push(dep);
|
|
3614
|
+
}
|
|
3615
|
+
for (const n of ctx.canvas.nodes) {
|
|
3616
|
+
if (reachable.has(n.id) || n.type === "ingest") continue;
|
|
3617
|
+
ctx.issues.push({
|
|
3618
|
+
path: `nodes[${ctx.idToIndex.get(n.id)}]`,
|
|
3619
|
+
code: STAGE_CODES.ORPHAN_NODE,
|
|
3620
|
+
severity: "warning",
|
|
3621
|
+
message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}" \u2014 it will still execute (and bill, if billable) but nothing consumes its result; wire it in or remove it`
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
2928
3625
|
function checkOutputRef(ctx) {
|
|
2929
3626
|
const out = ctx.canvas.output;
|
|
2930
3627
|
if (!out) return;
|
|
@@ -2954,9 +3651,9 @@ function checkOutputRef(ctx) {
|
|
|
2954
3651
|
function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
2955
3652
|
for (const issue of err.issues) {
|
|
2956
3653
|
const tail2 = pathToString(issue.path);
|
|
2957
|
-
const
|
|
3654
|
+
const path17 = pathPrefix ? tail2 ? `${pathPrefix}.${tail2}` : pathPrefix : tail2;
|
|
2958
3655
|
issues.push({
|
|
2959
|
-
path:
|
|
3656
|
+
path: path17,
|
|
2960
3657
|
code,
|
|
2961
3658
|
message: issue.message,
|
|
2962
3659
|
received: issue.code === "invalid_type" ? issue.received : void 0,
|
|
@@ -2965,8 +3662,8 @@ function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
|
2965
3662
|
});
|
|
2966
3663
|
}
|
|
2967
3664
|
}
|
|
2968
|
-
function pathToString(
|
|
2969
|
-
return
|
|
3665
|
+
function pathToString(path17) {
|
|
3666
|
+
return path17.map((p) => typeof p === "number" ? `[${p}]` : `.${String(p)}`).join("").replace(/^\./, "");
|
|
2970
3667
|
}
|
|
2971
3668
|
function buildDepGraph(canvas) {
|
|
2972
3669
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -3065,6 +3762,12 @@ var Engine = class {
|
|
|
3065
3762
|
async run(input, opts = {}) {
|
|
3066
3763
|
const validation = await this.validateDeep(input);
|
|
3067
3764
|
if (!validation.ok) throw new ValidationError(validation.issues);
|
|
3765
|
+
if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
|
|
3766
|
+
throw new RunAbortedError(
|
|
3767
|
+
"cost_cap",
|
|
3768
|
+
`estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
|
|
3769
|
+
);
|
|
3770
|
+
}
|
|
3068
3771
|
const canvas = validation.canvas;
|
|
3069
3772
|
const runId = opts.run_id ?? `r_${ulid()}`;
|
|
3070
3773
|
const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
|
|
@@ -3115,7 +3818,17 @@ var Engine = class {
|
|
|
3115
3818
|
const layers = topologicalLayers(graph);
|
|
3116
3819
|
const limit = resolveConcurrency(opts.concurrency);
|
|
3117
3820
|
for (const layer of layers) {
|
|
3821
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
|
|
3822
|
+
if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
|
|
3823
|
+
throw new RunAbortedError(
|
|
3824
|
+
"cost_cap",
|
|
3825
|
+
`spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3118
3828
|
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3829
|
+
if (opts.signal?.aborted) {
|
|
3830
|
+
return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
|
|
3831
|
+
}
|
|
3119
3832
|
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3120
3833
|
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
|
|
3121
3834
|
if (r.cached) counters.cachedNodes++;
|
|
@@ -3138,10 +3851,12 @@ var Engine = class {
|
|
|
3138
3851
|
settled.forEach((result, i) => {
|
|
3139
3852
|
const nodeId = layer[i];
|
|
3140
3853
|
if (result.status === "rejected" && nodeId) {
|
|
3854
|
+
if (result.reason instanceof RunAbortedError) return;
|
|
3141
3855
|
failures.push({ nodeId, reason: result.reason });
|
|
3142
3856
|
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
|
|
3143
3857
|
}
|
|
3144
3858
|
});
|
|
3859
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
3145
3860
|
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
3146
3861
|
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
3147
3862
|
}
|
|
@@ -3214,7 +3929,7 @@ var Engine = class {
|
|
|
3214
3929
|
log: this.log,
|
|
3215
3930
|
signal: opts.signal
|
|
3216
3931
|
};
|
|
3217
|
-
const preparedForExec = def
|
|
3932
|
+
const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
|
|
3218
3933
|
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
3219
3934
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
3220
3935
|
const elapsed = Date.now() - t0;
|
|
@@ -3336,6 +4051,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
|
|
|
3336
4051
|
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3337
4052
|
}
|
|
3338
4053
|
}
|
|
4054
|
+
function needsLocalMaterialization(def) {
|
|
4055
|
+
return def.location === "local" && !def.passthroughRefs;
|
|
4056
|
+
}
|
|
3339
4057
|
function pickFinalOutput(canvas, outputs) {
|
|
3340
4058
|
if (canvas.output) {
|
|
3341
4059
|
const node = outputs[canvas.output.node];
|
|
@@ -3351,7 +4069,7 @@ function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
|
3351
4069
|
const needs = /* @__PURE__ */ new Set();
|
|
3352
4070
|
for (const [consumerId, deps] of graph) {
|
|
3353
4071
|
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
3354
|
-
if (def?.location !== "local") continue;
|
|
4072
|
+
if (def?.location !== "local" || def.passthroughRefs) continue;
|
|
3355
4073
|
for (const dep of deps) needs.add(dep);
|
|
3356
4074
|
}
|
|
3357
4075
|
return needs;
|
|
@@ -3390,11 +4108,11 @@ function hashInputs(inputs) {
|
|
|
3390
4108
|
const out = {};
|
|
3391
4109
|
for (const [k, v] of Object.entries(inputs)) {
|
|
3392
4110
|
if (Array.isArray(v)) {
|
|
3393
|
-
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(el));
|
|
4111
|
+
out[k] = v.map((el) => extractSha(el) ?? canonicalLiteral(normalizeParamsForCacheKey(el)));
|
|
3394
4112
|
} else {
|
|
3395
4113
|
const s = extractSha(v);
|
|
3396
4114
|
if (s !== null) out[k] = s;
|
|
3397
|
-
else out[k] = canonicalLiteral(v);
|
|
4115
|
+
else out[k] = canonicalLiteral(normalizeParamsForCacheKey(v));
|
|
3398
4116
|
}
|
|
3399
4117
|
}
|
|
3400
4118
|
return out;
|
|
@@ -3555,9 +4273,9 @@ var NodeRegistry = class {
|
|
|
3555
4273
|
|
|
3556
4274
|
// src/engine/nodes/ingest.ts
|
|
3557
4275
|
import { execFile as execFileCb, spawn } from "child_process";
|
|
3558
|
-
import { mkdtemp, readdir, readFile as
|
|
4276
|
+
import { mkdtemp, readdir, readFile as readFile3, rm, stat as stat2 } from "fs/promises";
|
|
3559
4277
|
import { tmpdir } from "os";
|
|
3560
|
-
import
|
|
4278
|
+
import path4 from "path";
|
|
3561
4279
|
import { promisify } from "util";
|
|
3562
4280
|
import { z as z5 } from "zod";
|
|
3563
4281
|
|
|
@@ -3915,8 +4633,8 @@ function resolveLocalPath(input) {
|
|
|
3915
4633
|
`ingest: ~ path expansion is not supported (got "${input}"). Use an absolute path or a cwd-relative path.`
|
|
3916
4634
|
);
|
|
3917
4635
|
}
|
|
3918
|
-
if (
|
|
3919
|
-
return
|
|
4636
|
+
if (path4.isAbsolute(input)) return input;
|
|
4637
|
+
return path4.resolve(process.cwd(), input);
|
|
3920
4638
|
}
|
|
3921
4639
|
var EXT_TO_MIME = {
|
|
3922
4640
|
png: "image/png",
|
|
@@ -3964,7 +4682,7 @@ function sniffSvg(buf) {
|
|
|
3964
4682
|
return head.startsWith("<?xml") ? head.includes("<svg") : head.startsWith("<svg");
|
|
3965
4683
|
}
|
|
3966
4684
|
function inferMimeFromPath(absPath, sniffBytes) {
|
|
3967
|
-
const ext =
|
|
4685
|
+
const ext = path4.extname(absPath).slice(1).toLowerCase();
|
|
3968
4686
|
const fromExt = EXT_TO_MIME[ext];
|
|
3969
4687
|
if (fromExt) return fromExt;
|
|
3970
4688
|
const fromBytes = sniffImageMime(sniffBytes);
|
|
@@ -4103,7 +4821,7 @@ async function execLocalFile(params, ctx) {
|
|
|
4103
4821
|
}
|
|
4104
4822
|
let bytes;
|
|
4105
4823
|
try {
|
|
4106
|
-
bytes = await
|
|
4824
|
+
bytes = await readFile3(absPath);
|
|
4107
4825
|
} catch (e) {
|
|
4108
4826
|
if (e.code === "EACCES") {
|
|
4109
4827
|
throw localExecError(ctx, `permission_denied: ${absPath}`);
|
|
@@ -4154,7 +4872,7 @@ function localFileMetadata(args) {
|
|
|
4154
4872
|
strategy: "local_file",
|
|
4155
4873
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4156
4874
|
file_size: args.fileSize,
|
|
4157
|
-
original_filename:
|
|
4875
|
+
original_filename: path4.basename(args.absPath),
|
|
4158
4876
|
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
4159
4877
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
4160
4878
|
};
|
|
@@ -4178,7 +4896,7 @@ async function execYtDlp(params, ctx) {
|
|
|
4178
4896
|
if (params.expect !== "video" && params.expect !== "audio") {
|
|
4179
4897
|
throw new Error(`ingest: yt_dlp only handles video/audio, got expect=${params.expect}`);
|
|
4180
4898
|
}
|
|
4181
|
-
const workDir = await mkdtemp(
|
|
4899
|
+
const workDir = await mkdtemp(path4.join(tmpdir(), "ingest-yt-"));
|
|
4182
4900
|
try {
|
|
4183
4901
|
const { filePath, info } = await runYtDlp({
|
|
4184
4902
|
url: params.url,
|
|
@@ -4193,7 +4911,7 @@ async function execYtDlp(params, ctx) {
|
|
|
4193
4911
|
`file_too_large: yt-dlp output for ${params.url} is ${downloadedStats.size} bytes (limit ${MAX_ASSET_BYTES})`
|
|
4194
4912
|
);
|
|
4195
4913
|
}
|
|
4196
|
-
const bytes = await
|
|
4914
|
+
const bytes = await readFile3(filePath);
|
|
4197
4915
|
const kind = params.expect;
|
|
4198
4916
|
const mime = YT_DLP_MIME[kind];
|
|
4199
4917
|
const metadata = buildYtDlpMetadata(params.url, info);
|
|
@@ -4205,8 +4923,8 @@ async function execYtDlp(params, ctx) {
|
|
|
4205
4923
|
}
|
|
4206
4924
|
}
|
|
4207
4925
|
async function runYtDlp(args) {
|
|
4208
|
-
const outTemplate =
|
|
4209
|
-
const infoPath =
|
|
4926
|
+
const outTemplate = path4.join(args.workDir, "out.%(ext)s");
|
|
4927
|
+
const infoPath = path4.join(args.workDir, "out.info.json");
|
|
4210
4928
|
const argv = [
|
|
4211
4929
|
args.url,
|
|
4212
4930
|
"--no-playlist",
|
|
@@ -4239,11 +4957,11 @@ ${tail(stderr, 40)}`)
|
|
|
4239
4957
|
}
|
|
4240
4958
|
let info = {};
|
|
4241
4959
|
try {
|
|
4242
|
-
const raw = await
|
|
4960
|
+
const raw = await readFile3(infoPath, "utf-8");
|
|
4243
4961
|
info = JSON.parse(raw);
|
|
4244
4962
|
} catch {
|
|
4245
4963
|
}
|
|
4246
|
-
return { filePath:
|
|
4964
|
+
return { filePath: path4.join(args.workDir, downloaded), info };
|
|
4247
4965
|
}
|
|
4248
4966
|
function buildYtDlpMetadata(sourceUrl, info) {
|
|
4249
4967
|
const out = {
|
|
@@ -4332,9 +5050,9 @@ import { z as z6 } from "zod";
|
|
|
4332
5050
|
|
|
4333
5051
|
// src/engine/nodes/local/lib/cli-runner.ts
|
|
4334
5052
|
import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
|
|
4335
|
-
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as
|
|
5053
|
+
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, stat as stat3 } from "fs/promises";
|
|
4336
5054
|
import { tmpdir as tmpdir2 } from "os";
|
|
4337
|
-
import
|
|
5055
|
+
import path5 from "path";
|
|
4338
5056
|
import { promisify as promisify2 } from "util";
|
|
4339
5057
|
var execFile2 = promisify2(execFileCb2);
|
|
4340
5058
|
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -4361,7 +5079,7 @@ function mimeForExt(ext) {
|
|
|
4361
5079
|
}
|
|
4362
5080
|
function extForAssetRef(ref) {
|
|
4363
5081
|
if (ref.path) {
|
|
4364
|
-
const e =
|
|
5082
|
+
const e = path5.extname(ref.path);
|
|
4365
5083
|
if (e) return e;
|
|
4366
5084
|
}
|
|
4367
5085
|
const reverse = {
|
|
@@ -4383,14 +5101,14 @@ function planArrayInputSlot(tmpDir, slot, values, lookup, stagedInputs) {
|
|
|
4383
5101
|
const ref = values[i];
|
|
4384
5102
|
if (!ref) continue;
|
|
4385
5103
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot}[${i}] has no local path`);
|
|
4386
|
-
const dest =
|
|
5104
|
+
const dest = path5.join(tmpDir, `in_${slot}_${i}${extForAssetRef(ref)}`);
|
|
4387
5105
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4388
5106
|
lookup.set(`in.${slot}.${i}`, dest);
|
|
4389
5107
|
}
|
|
4390
5108
|
}
|
|
4391
5109
|
function planSingleInputSlot(tmpDir, slot, ref, lookup, stagedInputs) {
|
|
4392
5110
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot} has no local path`);
|
|
4393
|
-
const dest =
|
|
5111
|
+
const dest = path5.join(tmpDir, `in_${slot}${extForAssetRef(ref)}`);
|
|
4394
5112
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
4395
5113
|
lookup.set(`in.${slot}`, dest);
|
|
4396
5114
|
}
|
|
@@ -4398,7 +5116,7 @@ function planOutputs(tmpDir, outputs, lookup) {
|
|
|
4398
5116
|
const outputPaths = [];
|
|
4399
5117
|
for (const [name, spec] of Object.entries(outputs)) {
|
|
4400
5118
|
const ext = spec.ext.startsWith(".") ? spec.ext : `.${spec.ext}`;
|
|
4401
|
-
const absPath =
|
|
5119
|
+
const absPath = path5.join(tmpDir, `out_${name}${ext}`);
|
|
4402
5120
|
outputPaths.push({ name, absPath, spec });
|
|
4403
5121
|
lookup.set(`out.${name}`, absPath);
|
|
4404
5122
|
}
|
|
@@ -4439,8 +5157,8 @@ function rejectRawPaths(substituted, original, stagingDir) {
|
|
|
4439
5157
|
throw new Error(`cli-runner: home-relative path "${original}" not allowed in args.`);
|
|
4440
5158
|
}
|
|
4441
5159
|
if (substituted.startsWith("/")) {
|
|
4442
|
-
const resolved =
|
|
4443
|
-
if (!resolved.startsWith(`${stagingDir}${
|
|
5160
|
+
const resolved = path5.resolve(substituted);
|
|
5161
|
+
if (!resolved.startsWith(`${stagingDir}${path5.sep}`) && resolved !== stagingDir) {
|
|
4444
5162
|
throw new Error(
|
|
4445
5163
|
`cli-runner: raw filesystem path "${original}" not allowed \u2014 declare an input slot and use {{in.<slot>}} instead.`
|
|
4446
5164
|
);
|
|
@@ -4490,7 +5208,7 @@ function tailLines(text, maxLines) {
|
|
|
4490
5208
|
}
|
|
4491
5209
|
async function runCli(opts) {
|
|
4492
5210
|
const { bin, args, inputs, outputs, ctx, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
4493
|
-
const tmpDir = await mkdtemp2(
|
|
5211
|
+
const tmpDir = await mkdtemp2(path5.join(tmpdir2(), `cli-${bin.replace(/[^a-z0-9]/gi, "")}-`));
|
|
4494
5212
|
try {
|
|
4495
5213
|
const { lookup, stagedInputs, outputPaths } = planPlaceholders(tmpDir, inputs, outputs);
|
|
4496
5214
|
await stageInputs(stagedInputs);
|
|
@@ -4512,7 +5230,7 @@ ${tailLines(stderr, 40)}`);
|
|
|
4512
5230
|
if (!s?.isFile() || s.size === 0) {
|
|
4513
5231
|
throw new Error(`cli-runner: declared output "${name}" missing or empty at ${absPath}`);
|
|
4514
5232
|
}
|
|
4515
|
-
const bytes = await
|
|
5233
|
+
const bytes = await readFile4(absPath);
|
|
4516
5234
|
const ref = await ctx.assets.ingestBytes({
|
|
4517
5235
|
bytes: Buffer.from(bytes),
|
|
4518
5236
|
kind: spec.kind,
|
|
@@ -4552,6 +5270,12 @@ var Track = z6.object({
|
|
|
4552
5270
|
slot: z6.string().min(1),
|
|
4553
5271
|
/** When this track starts on the timeline, seconds from 0. */
|
|
4554
5272
|
start_s: z6.number().min(0),
|
|
5273
|
+
/**
|
|
5274
|
+
* Optional hard cap on this track's length, seconds. The clip is trimmed BEFORE
|
|
5275
|
+
* placement, so a source that runs long (an over-long voice extract, a converted
|
|
5276
|
+
* track that came back oversized) cannot bleed into the next track's window.
|
|
5277
|
+
*/
|
|
5278
|
+
duration_s: z6.number().positive().optional(),
|
|
4555
5279
|
/** Optional level adjustment in dB (negative ducks, e.g. a music bed at -12). */
|
|
4556
5280
|
gain_db: z6.number().optional()
|
|
4557
5281
|
}).strict();
|
|
@@ -4608,7 +5332,10 @@ function buildAudioTimelineArgs(params) {
|
|
|
4608
5332
|
params.tracks.forEach((track, i) => {
|
|
4609
5333
|
inputArgs.push("-i", `{{in.${track.slot}}}`);
|
|
4610
5334
|
const delayMs = Math.round(track.start_s * 1e3);
|
|
4611
|
-
const steps = [
|
|
5335
|
+
const steps = [
|
|
5336
|
+
...track.duration_s !== void 0 ? [`atrim=0:${track.duration_s}`] : [],
|
|
5337
|
+
`adelay=${delayMs}:all=1`
|
|
5338
|
+
];
|
|
4612
5339
|
if (track.gain_db !== void 0) steps.push(`volume=${track.gain_db}dB`);
|
|
4613
5340
|
const label = `a${i}`;
|
|
4614
5341
|
filterChains.push(`[${i}:a]${steps.join(",")}[${label}]`);
|
|
@@ -4626,11 +5353,11 @@ function buildAudioTimelineArgs(params) {
|
|
|
4626
5353
|
}
|
|
4627
5354
|
var audioTimelineNode = defineNode({
|
|
4628
5355
|
id: "audio_timeline",
|
|
4629
|
-
version: "1.
|
|
5356
|
+
version: "1.2.0",
|
|
4630
5357
|
category: "audio",
|
|
4631
5358
|
location: "local",
|
|
4632
5359
|
summary: "Place and mix several audio clips onto one timeline: each track starts at a given second (optionally level-adjusted in dB), then they're combined into a single track. Built for laying a music bed plus timed voiceover lines and sound effects under a video.",
|
|
4633
|
-
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, gain_db?}
|
|
5360
|
+
when_to_use: "Use to assemble a full audio bed from separately-generated clips \u2014 e.g. a `music` bed at 0 (ducked via `gain_db: -12`), each scene's `tts` voiceover at its scene start, and `sound_effect` hits at their timestamps. Wire each clip as `inputs.<slot>` (audio AssetRef) and list it in `params.tracks` with `{slot, start_s, duration_s?, gain_db?}` (`duration_s` hard-caps a clip so it can't bleed into the next track's window). Set `total_ms` to pin the final length to the video. Requires `ffmpeg` on PATH.",
|
|
4634
5361
|
inputs: AudioTimelineInputs,
|
|
4635
5362
|
params: AudioTimelineParams,
|
|
4636
5363
|
outputs: AudioTimelineOutputs,
|
|
@@ -4682,19 +5409,56 @@ var audioTimelineNode = defineNode({
|
|
|
4682
5409
|
}
|
|
4683
5410
|
});
|
|
4684
5411
|
|
|
4685
|
-
// src/engine/nodes/local/
|
|
5412
|
+
// src/engine/nodes/local/collect.ts
|
|
4686
5413
|
import { z as z7 } from "zod";
|
|
5414
|
+
var collectNode = defineNode({
|
|
5415
|
+
id: "collect",
|
|
5416
|
+
version: "1.0.0",
|
|
5417
|
+
category: "data",
|
|
5418
|
+
location: "local",
|
|
5419
|
+
passthroughRefs: true,
|
|
5420
|
+
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.",
|
|
5421
|
+
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.",
|
|
5422
|
+
inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5423
|
+
params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
|
|
5424
|
+
outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
|
|
5425
|
+
outputKinds: { images: "image" },
|
|
5426
|
+
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5427
|
+
// Arity is only knowable at validate time when `images` is a literal array;
|
|
5428
|
+
// a single `$ref:` string to an upstream array output defers to runtime.
|
|
5429
|
+
validateExtra: ({ rawParams, rawInputs }) => {
|
|
5430
|
+
const labels = rawParams?.labels;
|
|
5431
|
+
if (!Array.isArray(labels)) return [];
|
|
5432
|
+
if (new Set(labels).size !== labels.length) {
|
|
5433
|
+
return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
|
|
5434
|
+
}
|
|
5435
|
+
const images = rawInputs?.images;
|
|
5436
|
+
if (Array.isArray(images) && labels.length !== images.length) {
|
|
5437
|
+
return [
|
|
5438
|
+
{
|
|
5439
|
+
path: "params.labels",
|
|
5440
|
+
message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
|
|
5441
|
+
}
|
|
5442
|
+
];
|
|
5443
|
+
}
|
|
5444
|
+
return [];
|
|
5445
|
+
},
|
|
5446
|
+
execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
|
|
5447
|
+
});
|
|
5448
|
+
|
|
5449
|
+
// src/engine/nodes/local/ffmpeg.ts
|
|
5450
|
+
import { z as z8 } from "zod";
|
|
4687
5451
|
var FFMPEG_BIN2 = "ffmpeg";
|
|
4688
|
-
var OutputDecl =
|
|
4689
|
-
kind:
|
|
4690
|
-
ext:
|
|
5452
|
+
var OutputDecl = z8.object({
|
|
5453
|
+
kind: z8.enum(["image", "video", "audio"]),
|
|
5454
|
+
ext: z8.string().min(1).max(8)
|
|
4691
5455
|
}).strict();
|
|
4692
|
-
var FfmpegParams =
|
|
4693
|
-
args:
|
|
4694
|
-
outputs:
|
|
5456
|
+
var FfmpegParams = z8.object({
|
|
5457
|
+
args: z8.array(z8.string()).min(1),
|
|
5458
|
+
outputs: z8.record(z8.string(), OutputDecl).default({})
|
|
4695
5459
|
}).strict();
|
|
4696
|
-
var FfmpegInputs =
|
|
4697
|
-
var FfmpegOutputs =
|
|
5460
|
+
var FfmpegInputs = z8.record(z8.string(), z8.unknown());
|
|
5461
|
+
var FfmpegOutputs = z8.record(z8.string(), z8.custom());
|
|
4698
5462
|
var ffmpegNode = defineNode({
|
|
4699
5463
|
id: "ffmpeg",
|
|
4700
5464
|
version: "2.0.0",
|
|
@@ -4724,17 +5488,17 @@ var ffmpegNode = defineNode({
|
|
|
4724
5488
|
import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4725
5489
|
import { createRequire } from "module";
|
|
4726
5490
|
import { tmpdir as tmpdir3 } from "os";
|
|
4727
|
-
import
|
|
4728
|
-
import { z as
|
|
5491
|
+
import path7 from "path";
|
|
5492
|
+
import { z as z9 } from "zod";
|
|
4729
5493
|
|
|
4730
5494
|
// src/engine/nodes/local/lib/assets.ts
|
|
4731
|
-
import { copyFile as copyFile3, readFile as
|
|
4732
|
-
import
|
|
5495
|
+
import { copyFile as copyFile3, readFile as readFile5 } from "fs/promises";
|
|
5496
|
+
import path6 from "path";
|
|
4733
5497
|
async function stageAsset(ref, destDir, filename) {
|
|
4734
5498
|
if (!ref.path) {
|
|
4735
5499
|
throw new Error(`stageAsset: ref (${ref.kind}/${ref.mime}) has no local path`);
|
|
4736
5500
|
}
|
|
4737
|
-
const dest =
|
|
5501
|
+
const dest = path6.join(destDir, filename);
|
|
4738
5502
|
await copyFile3(ref.path, dest);
|
|
4739
5503
|
return dest;
|
|
4740
5504
|
}
|
|
@@ -4743,7 +5507,7 @@ async function refToUrl(ref) {
|
|
|
4743
5507
|
if (!ref.path) {
|
|
4744
5508
|
throw new Error("refToUrl: AssetRef has neither url nor path");
|
|
4745
5509
|
}
|
|
4746
|
-
const bytes = await
|
|
5510
|
+
const bytes = await readFile5(ref.path);
|
|
4747
5511
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4748
5512
|
}
|
|
4749
5513
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
@@ -4761,15 +5525,15 @@ var DEFAULT_SPECIMEN = [
|
|
|
4761
5525
|
"abcdefghijklmnopqrstuvwxyz",
|
|
4762
5526
|
`0123456789 !?&@#$%().,:;'"-`
|
|
4763
5527
|
].join("\n");
|
|
4764
|
-
var FontSpecimenParams =
|
|
4765
|
-
text:
|
|
4766
|
-
font_size:
|
|
4767
|
-
padding:
|
|
4768
|
-
line_height:
|
|
4769
|
-
max_width:
|
|
5528
|
+
var FontSpecimenParams = z9.object({
|
|
5529
|
+
text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
|
|
5530
|
+
font_size: z9.number().int().min(8).max(512).optional().default(72),
|
|
5531
|
+
padding: z9.number().int().min(0).max(512).optional().default(64),
|
|
5532
|
+
line_height: z9.number().min(0.8).max(3).optional().default(1.35),
|
|
5533
|
+
max_width: z9.number().int().min(256).max(4096).optional()
|
|
4770
5534
|
}).strict();
|
|
4771
|
-
var FontSpecimenInputs =
|
|
4772
|
-
var FontSpecimenOutputs =
|
|
5535
|
+
var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
|
|
5536
|
+
var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
|
|
4773
5537
|
var DEVICE_SCALE_FACTOR = 2;
|
|
4774
5538
|
var PAGE_TIMEOUT_MS = 3e4;
|
|
4775
5539
|
function escapeHtml(text) {
|
|
@@ -4817,11 +5581,11 @@ var fontSpecimenNode = defineNode({
|
|
|
4817
5581
|
outputKinds: { image: "image" },
|
|
4818
5582
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
4819
5583
|
async execute({ inputs, params, ctx }) {
|
|
4820
|
-
const tmp = await mkdtemp3(
|
|
5584
|
+
const tmp = await mkdtemp3(path7.join(tmpdir3(), "font-specimen-"));
|
|
4821
5585
|
try {
|
|
4822
5586
|
const fontFilename = `font.${extForMime(inputs.font.mime)}`;
|
|
4823
5587
|
await stageAsset(inputs.font, tmp, fontFilename);
|
|
4824
|
-
const entryPath =
|
|
5588
|
+
const entryPath = path7.join(tmp, "index.html");
|
|
4825
5589
|
await writeFile3(entryPath, buildSpecimenHtml(params, fontFilename), "utf-8");
|
|
4826
5590
|
ctx.log(`rendering specimen (${params.font_size}px, ${params.text.split("\n").length} lines)`);
|
|
4827
5591
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -4905,16 +5669,16 @@ var fontSpecimenNode = defineNode({
|
|
|
4905
5669
|
|
|
4906
5670
|
// src/engine/nodes/local/hyperframe.ts
|
|
4907
5671
|
import { execFile as execFile4 } from "child_process";
|
|
4908
|
-
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as
|
|
5672
|
+
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as readFile9, rm as rm4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
4909
5673
|
import { createRequire as createRequire2 } from "module";
|
|
4910
5674
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
4911
|
-
import
|
|
5675
|
+
import path12 from "path";
|
|
4912
5676
|
import { promisify as promisify4 } from "util";
|
|
4913
|
-
import { z as
|
|
5677
|
+
import { z as z11 } from "zod";
|
|
4914
5678
|
|
|
4915
5679
|
// src/engine/engine/composition-hash.ts
|
|
4916
|
-
import { readdir as readdir2, readFile as
|
|
4917
|
-
import
|
|
5680
|
+
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
5681
|
+
import path8 from "path";
|
|
4918
5682
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".cache", ".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
4919
5683
|
function isSkippedName(name) {
|
|
4920
5684
|
if (name.startsWith(".")) return true;
|
|
@@ -4931,79 +5695,79 @@ async function collectFiles(root, current) {
|
|
|
4931
5695
|
const names = await readdir2(current);
|
|
4932
5696
|
for (const name of names) {
|
|
4933
5697
|
if (isSkippedName(name)) continue;
|
|
4934
|
-
const abs =
|
|
5698
|
+
const abs = path8.join(current, name);
|
|
4935
5699
|
const s = await stat4(abs);
|
|
4936
5700
|
if (s.isDirectory()) {
|
|
4937
5701
|
out.push(...await collectFiles(root, abs));
|
|
4938
5702
|
continue;
|
|
4939
5703
|
}
|
|
4940
5704
|
if (!s.isFile()) continue;
|
|
4941
|
-
const bytes = await
|
|
4942
|
-
const relPath =
|
|
5705
|
+
const bytes = await readFile6(abs);
|
|
5706
|
+
const relPath = path8.relative(root, abs).split(path8.sep).join("/");
|
|
4943
5707
|
out.push({ relPath, contentSha: sha256Hex(bytes) });
|
|
4944
5708
|
}
|
|
4945
5709
|
return out;
|
|
4946
5710
|
}
|
|
4947
5711
|
|
|
4948
5712
|
// src/engine/engine/composition-meta.ts
|
|
4949
|
-
import { readFile as
|
|
4950
|
-
import
|
|
4951
|
-
import { z as
|
|
4952
|
-
var InputKind =
|
|
4953
|
-
var InputSpec =
|
|
5713
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5714
|
+
import path9 from "path";
|
|
5715
|
+
import { z as z10 } from "zod";
|
|
5716
|
+
var InputKind = z10.enum(["video", "image", "audio", "json"]);
|
|
5717
|
+
var InputSpec = z10.object({
|
|
4954
5718
|
kind: InputKind,
|
|
4955
|
-
required:
|
|
5719
|
+
required: z10.boolean().optional().default(false),
|
|
4956
5720
|
// Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
|
|
4957
5721
|
// Defaults to `<key><ext>` derived from the kind.
|
|
4958
|
-
staged_as:
|
|
4959
|
-
description:
|
|
5722
|
+
staged_as: z10.string().min(1).optional(),
|
|
5723
|
+
description: z10.string().optional()
|
|
4960
5724
|
}).strict();
|
|
4961
5725
|
var ParamSpecBase = {
|
|
4962
|
-
required:
|
|
4963
|
-
description:
|
|
5726
|
+
required: z10.boolean().optional().default(false),
|
|
5727
|
+
description: z10.string().optional()
|
|
4964
5728
|
};
|
|
4965
|
-
var StringParam =
|
|
5729
|
+
var StringParam = z10.object({
|
|
4966
5730
|
...ParamSpecBase,
|
|
4967
|
-
kind:
|
|
4968
|
-
default:
|
|
4969
|
-
enum:
|
|
5731
|
+
kind: z10.literal("string"),
|
|
5732
|
+
default: z10.string().optional(),
|
|
5733
|
+
enum: z10.array(z10.string()).optional()
|
|
4970
5734
|
}).strict();
|
|
4971
|
-
var IntegerParam =
|
|
5735
|
+
var IntegerParam = z10.object({
|
|
4972
5736
|
...ParamSpecBase,
|
|
4973
|
-
kind:
|
|
4974
|
-
default:
|
|
4975
|
-
min:
|
|
4976
|
-
max:
|
|
5737
|
+
kind: z10.literal("integer"),
|
|
5738
|
+
default: z10.number().int().optional(),
|
|
5739
|
+
min: z10.number().int().optional(),
|
|
5740
|
+
max: z10.number().int().optional()
|
|
4977
5741
|
}).strict();
|
|
4978
|
-
var NumberParam =
|
|
5742
|
+
var NumberParam = z10.object({
|
|
4979
5743
|
...ParamSpecBase,
|
|
4980
|
-
kind:
|
|
4981
|
-
default:
|
|
4982
|
-
min:
|
|
4983
|
-
max:
|
|
5744
|
+
kind: z10.literal("number"),
|
|
5745
|
+
default: z10.number().optional(),
|
|
5746
|
+
min: z10.number().optional(),
|
|
5747
|
+
max: z10.number().optional()
|
|
4984
5748
|
}).strict();
|
|
4985
|
-
var BooleanParam =
|
|
5749
|
+
var BooleanParam = z10.object({
|
|
4986
5750
|
...ParamSpecBase,
|
|
4987
|
-
kind:
|
|
4988
|
-
default:
|
|
5751
|
+
kind: z10.literal("boolean"),
|
|
5752
|
+
default: z10.boolean().optional()
|
|
4989
5753
|
}).strict();
|
|
4990
|
-
var ColorParam =
|
|
5754
|
+
var ColorParam = z10.object({
|
|
4991
5755
|
...ParamSpecBase,
|
|
4992
|
-
kind:
|
|
4993
|
-
default:
|
|
5756
|
+
kind: z10.literal("color"),
|
|
5757
|
+
default: z10.string().optional()
|
|
4994
5758
|
}).strict();
|
|
4995
|
-
var ImageParam =
|
|
5759
|
+
var ImageParam = z10.object({
|
|
4996
5760
|
...ParamSpecBase,
|
|
4997
|
-
kind:
|
|
4998
|
-
default:
|
|
5761
|
+
kind: z10.literal("image"),
|
|
5762
|
+
default: z10.string().optional()
|
|
4999
5763
|
}).strict();
|
|
5000
|
-
var JsonParam =
|
|
5764
|
+
var JsonParam = z10.object({
|
|
5001
5765
|
...ParamSpecBase,
|
|
5002
|
-
kind:
|
|
5003
|
-
schema:
|
|
5004
|
-
default:
|
|
5766
|
+
kind: z10.literal("json"),
|
|
5767
|
+
schema: z10.unknown().optional(),
|
|
5768
|
+
default: z10.unknown().optional()
|
|
5005
5769
|
}).strict();
|
|
5006
|
-
var ParamSpec =
|
|
5770
|
+
var ParamSpec = z10.discriminatedUnion("kind", [
|
|
5007
5771
|
StringParam,
|
|
5008
5772
|
IntegerParam,
|
|
5009
5773
|
NumberParam,
|
|
@@ -5012,22 +5776,22 @@ var ParamSpec = z9.discriminatedUnion("kind", [
|
|
|
5012
5776
|
ImageParam,
|
|
5013
5777
|
JsonParam
|
|
5014
5778
|
]);
|
|
5015
|
-
var CompositionMetaSchema =
|
|
5016
|
-
id:
|
|
5017
|
-
title:
|
|
5018
|
-
description:
|
|
5019
|
-
width:
|
|
5020
|
-
height:
|
|
5021
|
-
fps:
|
|
5022
|
-
default_duration:
|
|
5023
|
-
inputs:
|
|
5024
|
-
params:
|
|
5779
|
+
var CompositionMetaSchema = z10.object({
|
|
5780
|
+
id: z10.string().min(1),
|
|
5781
|
+
title: z10.string().min(1),
|
|
5782
|
+
description: z10.string().optional(),
|
|
5783
|
+
width: z10.number().int().positive(),
|
|
5784
|
+
height: z10.number().int().positive(),
|
|
5785
|
+
fps: z10.number().int().positive().default(30),
|
|
5786
|
+
default_duration: z10.number().positive().default(10),
|
|
5787
|
+
inputs: z10.record(z10.string(), InputSpec).default({}),
|
|
5788
|
+
params: z10.record(z10.string(), ParamSpec).default({})
|
|
5025
5789
|
}).strict();
|
|
5026
5790
|
async function loadCompositionMeta(compositionDir) {
|
|
5027
|
-
const metaPath =
|
|
5791
|
+
const metaPath = path9.join(compositionDir, "meta.json");
|
|
5028
5792
|
let raw;
|
|
5029
5793
|
try {
|
|
5030
|
-
raw = await
|
|
5794
|
+
raw = await readFile7(metaPath, "utf-8");
|
|
5031
5795
|
} catch (e) {
|
|
5032
5796
|
throw new Error(`composition meta: cannot read ${metaPath} (${e.message})`);
|
|
5033
5797
|
}
|
|
@@ -5049,39 +5813,39 @@ function buildParamsSchema(meta) {
|
|
|
5049
5813
|
for (const [name, spec] of Object.entries(meta.params)) {
|
|
5050
5814
|
shape[name] = buildParamFieldSchema(name, spec);
|
|
5051
5815
|
}
|
|
5052
|
-
return
|
|
5816
|
+
return z10.object(shape).strict();
|
|
5053
5817
|
}
|
|
5054
5818
|
function buildParamFieldSchema(name, spec) {
|
|
5055
5819
|
switch (spec.kind) {
|
|
5056
5820
|
case "string": {
|
|
5057
|
-
const s = spec.enum && spec.enum.length > 0 ?
|
|
5821
|
+
const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
|
|
5058
5822
|
return finalize(s, spec.default, spec.required);
|
|
5059
5823
|
}
|
|
5060
5824
|
case "integer": {
|
|
5061
|
-
let s =
|
|
5825
|
+
let s = z10.number().int();
|
|
5062
5826
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
5063
5827
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
5064
5828
|
return finalize(s, spec.default, spec.required);
|
|
5065
5829
|
}
|
|
5066
5830
|
case "number": {
|
|
5067
|
-
let s =
|
|
5831
|
+
let s = z10.number();
|
|
5068
5832
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
5069
5833
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
5070
5834
|
return finalize(s, spec.default, spec.required);
|
|
5071
5835
|
}
|
|
5072
5836
|
case "boolean":
|
|
5073
|
-
return finalize(
|
|
5837
|
+
return finalize(z10.boolean(), spec.default, spec.required);
|
|
5074
5838
|
case "color": {
|
|
5075
|
-
const s =
|
|
5839
|
+
const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
5076
5840
|
message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
|
|
5077
5841
|
});
|
|
5078
5842
|
return finalize(s, spec.default, spec.required);
|
|
5079
5843
|
}
|
|
5080
5844
|
case "image":
|
|
5081
|
-
return finalize(
|
|
5845
|
+
return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
|
|
5082
5846
|
case "json":
|
|
5083
5847
|
return finalize(
|
|
5084
|
-
|
|
5848
|
+
z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
|
|
5085
5849
|
spec.default,
|
|
5086
5850
|
spec.required
|
|
5087
5851
|
);
|
|
@@ -5105,8 +5869,8 @@ function defaultFilenameForInput(key, kind) {
|
|
|
5105
5869
|
|
|
5106
5870
|
// src/engine/nodes/local/lib/hyperframe-check.ts
|
|
5107
5871
|
import { execFile as execFile3 } from "child_process";
|
|
5108
|
-
import { readFile as
|
|
5109
|
-
import
|
|
5872
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5873
|
+
import path10 from "path";
|
|
5110
5874
|
import { promisify as promisify3 } from "util";
|
|
5111
5875
|
var execFileAsync = promisify3(execFile3);
|
|
5112
5876
|
var NEVER_BLOCK = [
|
|
@@ -5258,7 +6022,7 @@ ${detail}`);
|
|
|
5258
6022
|
}
|
|
5259
6023
|
let indexHtml = "";
|
|
5260
6024
|
try {
|
|
5261
|
-
indexHtml = await
|
|
6025
|
+
indexHtml = await readFile8(path10.join(dir, "index.html"), "utf-8");
|
|
5262
6026
|
} catch {
|
|
5263
6027
|
indexHtml = "";
|
|
5264
6028
|
}
|
|
@@ -5323,9 +6087,9 @@ ${stderr.slice(0, 1500)}`;
|
|
|
5323
6087
|
|
|
5324
6088
|
// src/engine/nodes/local/lib/hyperframe-meta.ts
|
|
5325
6089
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
5326
|
-
import
|
|
6090
|
+
import path11 from "path";
|
|
5327
6091
|
async function ensureHyperframesMetaJson(tmp, nodeId, meta, duration) {
|
|
5328
|
-
const metaPath =
|
|
6092
|
+
const metaPath = path11.join(tmp, "meta.json");
|
|
5329
6093
|
await writeFile4(
|
|
5330
6094
|
metaPath,
|
|
5331
6095
|
JSON.stringify(
|
|
@@ -5381,17 +6145,17 @@ function literalize(value) {
|
|
|
5381
6145
|
// src/engine/nodes/local/hyperframe.ts
|
|
5382
6146
|
var execFileAsync2 = promisify4(execFile4);
|
|
5383
6147
|
var require_2 = createRequire2(import.meta.url);
|
|
5384
|
-
var HyperframeParams =
|
|
5385
|
-
composition:
|
|
6148
|
+
var HyperframeParams = z11.object({
|
|
6149
|
+
composition: z11.string().min(1),
|
|
5386
6150
|
// Output container. mp4 (default) for delivery; webm/mov render WITH
|
|
5387
6151
|
// transparency (alpha) when the composition background is transparent —
|
|
5388
6152
|
// use for motion-graphic overlays dropped into Premiere/AE/Nuke.
|
|
5389
|
-
format:
|
|
5390
|
-
timeout_ms:
|
|
5391
|
-
}).catchall(
|
|
5392
|
-
var HyperframeInputs =
|
|
5393
|
-
var HyperframeOutputs =
|
|
5394
|
-
video:
|
|
6153
|
+
format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
|
|
6154
|
+
timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
|
|
6155
|
+
}).catchall(z11.unknown());
|
|
6156
|
+
var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
|
|
6157
|
+
var HyperframeOutputs = z11.object({
|
|
6158
|
+
video: z11.custom()
|
|
5395
6159
|
}).strict();
|
|
5396
6160
|
var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
|
|
5397
6161
|
var MIME_BY_FORMAT = {
|
|
@@ -5425,7 +6189,7 @@ var hyperframeRenderNode = defineNode({
|
|
|
5425
6189
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5426
6190
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5427
6191
|
const compositionParams = validateAndParseDynamicParams(meta, params);
|
|
5428
|
-
const tmp = await mkdtemp4(
|
|
6192
|
+
const tmp = await mkdtemp4(path12.join(tmpdir4(), "hf-render-"));
|
|
5429
6193
|
try {
|
|
5430
6194
|
await copyComposition(compositionDir, tmp);
|
|
5431
6195
|
await vendorGsap(tmp, ctx);
|
|
@@ -5435,9 +6199,9 @@ var hyperframeRenderNode = defineNode({
|
|
|
5435
6199
|
await substituteCompositionFiles(tmp, substitutionValues);
|
|
5436
6200
|
await ensureHyperframesMetaJson(tmp, ctx.nodeId, meta, duration);
|
|
5437
6201
|
await runHyperframesCheck({ dir: tmp, nodeId: "hyperframe_render", ctx, timeoutMs: params.timeout_ms });
|
|
5438
|
-
const outputPath =
|
|
6202
|
+
const outputPath = path12.join(tmp, `output.${params.format}`);
|
|
5439
6203
|
await runRender({ tmp, outputPath, params, meta, ctx });
|
|
5440
|
-
const bytes = await
|
|
6204
|
+
const bytes = await readFile9(outputPath);
|
|
5441
6205
|
ctx.log(`rendered ${bytes.length} bytes`);
|
|
5442
6206
|
const ref = await ctx.assets.ingestBytes({
|
|
5443
6207
|
bytes: Buffer.from(bytes),
|
|
@@ -5459,10 +6223,10 @@ var hyperframeRenderNode = defineNode({
|
|
|
5459
6223
|
}
|
|
5460
6224
|
});
|
|
5461
6225
|
async function resolveCompositionDir(composition) {
|
|
5462
|
-
const compositionPath =
|
|
6226
|
+
const compositionPath = path12.isAbsolute(composition) ? composition : path12.resolve(process.cwd(), composition);
|
|
5463
6227
|
const s = await stat5(compositionPath);
|
|
5464
6228
|
if (s.isDirectory()) return compositionPath;
|
|
5465
|
-
return
|
|
6229
|
+
return path12.dirname(compositionPath);
|
|
5466
6230
|
}
|
|
5467
6231
|
async function validateComposition(rawParams) {
|
|
5468
6232
|
const issues = await validateCompositionParams(rawParams);
|
|
@@ -5544,7 +6308,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5544
6308
|
await cp(srcDir, destDir, {
|
|
5545
6309
|
recursive: true,
|
|
5546
6310
|
filter: (src) => {
|
|
5547
|
-
const name =
|
|
6311
|
+
const name = path12.basename(src);
|
|
5548
6312
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5549
6313
|
return true;
|
|
5550
6314
|
}
|
|
@@ -5553,7 +6317,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
5553
6317
|
async function vendorGsap(tmp, ctx) {
|
|
5554
6318
|
try {
|
|
5555
6319
|
const gsapMin = require_2.resolve("gsap/dist/gsap.min.js");
|
|
5556
|
-
await copyFile4(gsapMin,
|
|
6320
|
+
await copyFile4(gsapMin, path12.join(tmp, "gsap.min.js"));
|
|
5557
6321
|
} catch (e) {
|
|
5558
6322
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5559
6323
|
}
|
|
@@ -5568,7 +6332,7 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
|
|
|
5568
6332
|
await stageAsset(ref, tmp, filename);
|
|
5569
6333
|
ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
|
|
5570
6334
|
if (spec.kind === "video" && primaryDuration === null) {
|
|
5571
|
-
primaryDuration = await probeDurationSeconds(
|
|
6335
|
+
primaryDuration = await probeDurationSeconds(path12.join(tmp, filename));
|
|
5572
6336
|
}
|
|
5573
6337
|
}
|
|
5574
6338
|
return primaryDuration;
|
|
@@ -5614,8 +6378,8 @@ function coerceImageParam(value) {
|
|
|
5614
6378
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5615
6379
|
}
|
|
5616
6380
|
async function substituteCompositionFiles(tmp, values) {
|
|
5617
|
-
const entryPath =
|
|
5618
|
-
const original = await
|
|
6381
|
+
const entryPath = path12.join(tmp, "index.html");
|
|
6382
|
+
const original = await readFile9(entryPath, "utf-8");
|
|
5619
6383
|
const { output, missing } = substituteVariables(original, values);
|
|
5620
6384
|
if (missing.length > 0) {
|
|
5621
6385
|
throw new Error(
|
|
@@ -5631,7 +6395,7 @@ function workerCount() {
|
|
|
5631
6395
|
async function runRender(opts) {
|
|
5632
6396
|
const { tmp, outputPath, params, meta, ctx } = opts;
|
|
5633
6397
|
const args = buildRenderArgs(tmp, outputPath, meta, params.format);
|
|
5634
|
-
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${
|
|
6398
|
+
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${path12.basename(tmp)}`);
|
|
5635
6399
|
try {
|
|
5636
6400
|
await execFileAsync2("npx", args, { timeout: params.timeout_ms, maxBuffer: 64 * 1024 * 1024 });
|
|
5637
6401
|
} catch (e) {
|
|
@@ -5675,28 +6439,28 @@ async function probeDurationSeconds(filePath) {
|
|
|
5675
6439
|
|
|
5676
6440
|
// src/engine/nodes/local/hyperframe-snapshot.ts
|
|
5677
6441
|
import { execFile as execFile5 } from "child_process";
|
|
5678
|
-
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as
|
|
6442
|
+
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as readFile10, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
5679
6443
|
import { createRequire as createRequire3 } from "module";
|
|
5680
6444
|
import { tmpdir as tmpdir5 } from "os";
|
|
5681
|
-
import
|
|
6445
|
+
import path13 from "path";
|
|
5682
6446
|
import { promisify as promisify5 } from "util";
|
|
5683
|
-
import { z as
|
|
6447
|
+
import { z as z12 } from "zod";
|
|
5684
6448
|
var _execFileAsync = promisify5(execFile5);
|
|
5685
6449
|
var require_3 = createRequire3(import.meta.url);
|
|
5686
|
-
var WaitForSpec =
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6450
|
+
var WaitForSpec = z12.discriminatedUnion("kind", [
|
|
6451
|
+
z12.object({ kind: z12.literal("auto") }),
|
|
6452
|
+
z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
|
|
6453
|
+
z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
|
|
6454
|
+
z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
|
|
5691
6455
|
]);
|
|
5692
|
-
var HyperframeSnapshotParams =
|
|
5693
|
-
composition:
|
|
6456
|
+
var HyperframeSnapshotParams = z12.object({
|
|
6457
|
+
composition: z12.string().min(1),
|
|
5694
6458
|
wait_for: WaitForSpec.optional().default({ kind: "auto" }),
|
|
5695
|
-
timeout_ms:
|
|
5696
|
-
}).catchall(
|
|
5697
|
-
var HyperframeSnapshotInputs =
|
|
5698
|
-
var HyperframeSnapshotOutputs =
|
|
5699
|
-
image:
|
|
6459
|
+
timeout_ms: z12.number().int().positive().optional().default(6e4)
|
|
6460
|
+
}).catchall(z12.unknown());
|
|
6461
|
+
var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
|
|
6462
|
+
var HyperframeSnapshotOutputs = z12.object({
|
|
6463
|
+
image: z12.custom()
|
|
5700
6464
|
}).strict();
|
|
5701
6465
|
var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
|
|
5702
6466
|
var DEVICE_SCALE_FACTOR2 = 2;
|
|
@@ -5725,7 +6489,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5725
6489
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
5726
6490
|
const meta = await loadCompositionMeta(compositionDir);
|
|
5727
6491
|
const compositionParams = validateAndParseDynamicParams2(meta, params);
|
|
5728
|
-
const tmp = await mkdtemp5(
|
|
6492
|
+
const tmp = await mkdtemp5(path13.join(tmpdir5(), "hf-snap-"));
|
|
5729
6493
|
try {
|
|
5730
6494
|
await copyComposition2(compositionDir, tmp);
|
|
5731
6495
|
await vendorGsap2(tmp, ctx);
|
|
@@ -5740,7 +6504,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
5740
6504
|
timeoutMs: params.timeout_ms,
|
|
5741
6505
|
samples: 1
|
|
5742
6506
|
});
|
|
5743
|
-
const entryPath =
|
|
6507
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
5744
6508
|
const entryUrl = `file://${entryPath}`;
|
|
5745
6509
|
ctx.log(`snapshotting ${meta.width}x${meta.height}@${DEVICE_SCALE_FACTOR2}x wait=${params.wait_for.kind}`);
|
|
5746
6510
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -5801,7 +6565,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5801
6565
|
await cp(srcDir, destDir, {
|
|
5802
6566
|
recursive: true,
|
|
5803
6567
|
filter: (src) => {
|
|
5804
|
-
const name =
|
|
6568
|
+
const name = path13.basename(src);
|
|
5805
6569
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
5806
6570
|
return true;
|
|
5807
6571
|
}
|
|
@@ -5810,7 +6574,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
5810
6574
|
async function vendorGsap2(tmp, ctx) {
|
|
5811
6575
|
try {
|
|
5812
6576
|
const gsapMin = require_3.resolve("gsap/dist/gsap.min.js");
|
|
5813
|
-
await copyFile5(gsapMin,
|
|
6577
|
+
await copyFile5(gsapMin, path13.join(tmp, "gsap.min.js"));
|
|
5814
6578
|
} catch (e) {
|
|
5815
6579
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
5816
6580
|
}
|
|
@@ -5844,8 +6608,8 @@ function coerceImageParam2(value) {
|
|
|
5844
6608
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5845
6609
|
}
|
|
5846
6610
|
async function substituteCompositionFiles2(tmp, values) {
|
|
5847
|
-
const entryPath =
|
|
5848
|
-
const original = await
|
|
6611
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
6612
|
+
const original = await readFile10(entryPath, "utf-8");
|
|
5849
6613
|
const { output, missing } = substituteVariables(original, values);
|
|
5850
6614
|
if (missing.length > 0) {
|
|
5851
6615
|
throw new Error(
|
|
@@ -5888,18 +6652,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
|
|
|
5888
6652
|
// src/engine/nodes/local/imagemagick.ts
|
|
5889
6653
|
import { execFile as execFile6 } from "child_process";
|
|
5890
6654
|
import { promisify as promisify6 } from "util";
|
|
5891
|
-
import { z as
|
|
6655
|
+
import { z as z13 } from "zod";
|
|
5892
6656
|
var execFileAsync3 = promisify6(execFile6);
|
|
5893
|
-
var OutputDecl2 =
|
|
5894
|
-
kind:
|
|
5895
|
-
ext:
|
|
6657
|
+
var OutputDecl2 = z13.object({
|
|
6658
|
+
kind: z13.enum(["image", "video", "audio"]),
|
|
6659
|
+
ext: z13.string().min(1).max(8)
|
|
5896
6660
|
}).strict();
|
|
5897
|
-
var ImageMagickParams =
|
|
5898
|
-
args:
|
|
5899
|
-
outputs:
|
|
6661
|
+
var ImageMagickParams = z13.object({
|
|
6662
|
+
args: z13.array(z13.string()).min(1),
|
|
6663
|
+
outputs: z13.record(z13.string(), OutputDecl2).default({})
|
|
5900
6664
|
}).strict();
|
|
5901
|
-
var ImageMagickInputs =
|
|
5902
|
-
var ImageMagickOutputs =
|
|
6665
|
+
var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
|
|
6666
|
+
var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
|
|
5903
6667
|
var resolvedBin;
|
|
5904
6668
|
async function resolveBin() {
|
|
5905
6669
|
if (resolvedBin) return resolvedBin;
|
|
@@ -5941,29 +6705,29 @@ var imagemagickNode = defineNode({
|
|
|
5941
6705
|
});
|
|
5942
6706
|
|
|
5943
6707
|
// src/engine/nodes/local/text.ts
|
|
5944
|
-
import { z as
|
|
6708
|
+
import { z as z14 } from "zod";
|
|
5945
6709
|
var textNode = defineNode({
|
|
5946
6710
|
id: "text",
|
|
5947
6711
|
version: "1.0.0",
|
|
5948
6712
|
category: "data",
|
|
5949
6713
|
location: "local",
|
|
5950
6714
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
5951
|
-
inputs:
|
|
5952
|
-
params:
|
|
5953
|
-
outputs:
|
|
6715
|
+
inputs: z14.object({}).strict(),
|
|
6716
|
+
params: z14.object({ value: z14.string() }).strict(),
|
|
6717
|
+
outputs: z14.object({ text: z14.string() }).strict(),
|
|
5954
6718
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
5955
6719
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
5956
6720
|
});
|
|
5957
6721
|
|
|
5958
6722
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
5959
|
-
import { z as
|
|
5960
|
-
var AudioVoiceConvertParams =
|
|
5961
|
-
model:
|
|
6723
|
+
import { z as z15 } from "zod";
|
|
6724
|
+
var AudioVoiceConvertParams = z15.object({
|
|
6725
|
+
model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
5962
6726
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
5963
|
-
voice:
|
|
5964
|
-
output_format:
|
|
6727
|
+
voice: z15.string().min(1),
|
|
6728
|
+
output_format: z15.string().optional(),
|
|
5965
6729
|
/** Strip the source clip's background noise before re-voicing. */
|
|
5966
|
-
remove_background_noise:
|
|
6730
|
+
remove_background_noise: z15.boolean().optional()
|
|
5967
6731
|
}).strict();
|
|
5968
6732
|
var audioVoiceConvertNode = delegated({
|
|
5969
6733
|
id: "audio_voice_convert",
|
|
@@ -5971,44 +6735,44 @@ var audioVoiceConvertNode = delegated({
|
|
|
5971
6735
|
category: "audio",
|
|
5972
6736
|
summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
|
|
5973
6737
|
when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
|
|
5974
|
-
inputs:
|
|
6738
|
+
inputs: z15.object({
|
|
5975
6739
|
audio: AudioRef,
|
|
5976
6740
|
voice_ref: TextRef.optional()
|
|
5977
6741
|
}).strict(),
|
|
5978
6742
|
params: AudioVoiceConvertParams,
|
|
5979
|
-
outputs:
|
|
6743
|
+
outputs: z15.object({ audio: AudioRef }).strict(),
|
|
5980
6744
|
outputKinds: { audio: "audio" },
|
|
5981
6745
|
cost: () => ({ credits: 1, seconds_estimate: 20 })
|
|
5982
6746
|
});
|
|
5983
6747
|
|
|
5984
6748
|
// src/engine/nodes/remote/dialogue.ts
|
|
5985
|
-
import { z as
|
|
5986
|
-
var DialogueInput =
|
|
5987
|
-
text:
|
|
5988
|
-
voice_id:
|
|
6749
|
+
import { z as z16 } from "zod";
|
|
6750
|
+
var DialogueInput = z16.object({
|
|
6751
|
+
text: z16.string().min(1),
|
|
6752
|
+
voice_id: z16.string().min(1)
|
|
5989
6753
|
});
|
|
5990
6754
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
5991
|
-
var DialogueParams =
|
|
5992
|
-
model:
|
|
6755
|
+
var DialogueParams = z16.object({
|
|
6756
|
+
model: z16.enum(DIALOGUE_MODELS),
|
|
5993
6757
|
/**
|
|
5994
6758
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
5995
6759
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
5996
6760
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
5997
6761
|
*/
|
|
5998
|
-
inputs:
|
|
5999
|
-
language_code:
|
|
6762
|
+
inputs: z16.array(DialogueInput).min(1).max(50),
|
|
6763
|
+
language_code: z16.string().optional(),
|
|
6000
6764
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
6001
|
-
settings:
|
|
6002
|
-
seed:
|
|
6003
|
-
apply_text_normalization:
|
|
6765
|
+
settings: z16.record(z16.string(), z16.unknown()).optional(),
|
|
6766
|
+
seed: z16.number().int().min(0).max(4294967295).optional(),
|
|
6767
|
+
apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
|
|
6004
6768
|
/**
|
|
6005
6769
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
6006
6770
|
* separate `timestamps` output — character-level alignment plus
|
|
6007
6771
|
* per-voice segment markers usable for captions, lipsync, or
|
|
6008
6772
|
* beat-matched cuts in ad creatives.
|
|
6009
6773
|
*/
|
|
6010
|
-
with_timestamps:
|
|
6011
|
-
output_format:
|
|
6774
|
+
with_timestamps: z16.boolean().optional(),
|
|
6775
|
+
output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6012
6776
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
6013
6777
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
6014
6778
|
path: ["inputs"]
|
|
@@ -6019,9 +6783,9 @@ var dialogueNode = delegated({
|
|
|
6019
6783
|
category: "audio",
|
|
6020
6784
|
summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
|
|
6021
6785
|
when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
|
|
6022
|
-
inputs:
|
|
6786
|
+
inputs: z16.object({}).loose(),
|
|
6023
6787
|
params: DialogueParams,
|
|
6024
|
-
outputs:
|
|
6788
|
+
outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6025
6789
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6026
6790
|
cost: ({ params }) => {
|
|
6027
6791
|
const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
|
|
@@ -6030,7 +6794,7 @@ var dialogueNode = delegated({
|
|
|
6030
6794
|
});
|
|
6031
6795
|
|
|
6032
6796
|
// src/engine/nodes/remote/image.ts
|
|
6033
|
-
import { z as
|
|
6797
|
+
import { z as z17 } from "zod";
|
|
6034
6798
|
var IMAGE_GENERATE_MODELS2 = [
|
|
6035
6799
|
"openai/gpt-5.4-image-2",
|
|
6036
6800
|
"google/gemini-3.5-flash",
|
|
@@ -6038,41 +6802,44 @@ var IMAGE_GENERATE_MODELS2 = [
|
|
|
6038
6802
|
"google/gemini-3-pro-image-preview",
|
|
6039
6803
|
"recraft/recraft-v4.1-pro-vector"
|
|
6040
6804
|
];
|
|
6041
|
-
var ImageGenerateParams =
|
|
6042
|
-
model:
|
|
6043
|
-
prompt:
|
|
6044
|
-
aspect_ratio:
|
|
6045
|
-
image_size:
|
|
6805
|
+
var ImageGenerateParams = z17.object({
|
|
6806
|
+
model: z17.enum(IMAGE_GENERATE_MODELS2),
|
|
6807
|
+
prompt: z17.string().min(1),
|
|
6808
|
+
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(),
|
|
6809
|
+
image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
|
|
6810
|
+
// Rendering quality — forwarded into `image_config`. OpenRouter models without a
|
|
6811
|
+
// quality knob ignore it; the registry gates which models accept it (gpt-image, Gemini).
|
|
6812
|
+
quality: z17.enum(["auto", "low", "medium", "high"]).optional(),
|
|
6046
6813
|
// Recraft v4 vector controls — forwarded into `image_config`. Registry
|
|
6047
6814
|
// rejects them on non-Recraft models.
|
|
6048
|
-
strength:
|
|
6049
|
-
rgb_colors:
|
|
6050
|
-
background_rgb_color:
|
|
6815
|
+
strength: z17.number().min(0).max(1).optional(),
|
|
6816
|
+
rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
|
|
6817
|
+
background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
|
|
6051
6818
|
}).strict();
|
|
6052
6819
|
var imageGenerateNode = delegated({
|
|
6053
6820
|
id: "image_generate",
|
|
6054
|
-
version: "2.
|
|
6821
|
+
version: "2.2.0",
|
|
6055
6822
|
category: "image",
|
|
6056
6823
|
summary: "Generate images for ad creatives. Curated model set: GPT-5.4 Image, Gemini 3.5 Flash, Gemini 3.1 Flash Image Preview, Gemini 3 Pro Image, Recraft v4.1 Pro Vector. Per-model param support comes from the canvas-engine model registry.",
|
|
6057
6824
|
when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-5.4-image-2` for photorealistic; Gemini variants for fast iteration and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order.",
|
|
6058
6825
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
6059
6826
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
6060
|
-
inputs:
|
|
6827
|
+
inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
|
|
6061
6828
|
params: ImageGenerateParams,
|
|
6062
|
-
outputs:
|
|
6829
|
+
outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
|
|
6063
6830
|
outputKinds: { images: "image" },
|
|
6064
6831
|
cost: () => ({ credits: 5, seconds_estimate: 10 })
|
|
6065
6832
|
});
|
|
6066
6833
|
|
|
6067
6834
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
6068
|
-
import { z as
|
|
6835
|
+
import { z as z18 } from "zod";
|
|
6069
6836
|
var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
6070
6837
|
var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
6071
|
-
var ImageAspectAdaptParams =
|
|
6072
|
-
model:
|
|
6073
|
-
formats:
|
|
6074
|
-
guidance:
|
|
6075
|
-
image_size:
|
|
6838
|
+
var ImageAspectAdaptParams = z18.object({
|
|
6839
|
+
model: z18.enum(ASPECT_ADAPT_MODELS),
|
|
6840
|
+
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" }),
|
|
6841
|
+
guidance: z18.string().min(1).optional(),
|
|
6842
|
+
image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
6076
6843
|
}).strict();
|
|
6077
6844
|
var imageAspectAdaptNode = delegated({
|
|
6078
6845
|
id: "image_aspect_adapt",
|
|
@@ -6080,9 +6847,9 @@ var imageAspectAdaptNode = delegated({
|
|
|
6080
6847
|
category: "image",
|
|
6081
6848
|
summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as `formats`.",
|
|
6082
6849
|
when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and list the target ratios in `formats`. Cost is estimated per format; formats matching the source ratio are free pass-throughs. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
|
|
6083
|
-
inputs:
|
|
6850
|
+
inputs: z18.object({ source: ImageRef }).loose(),
|
|
6084
6851
|
params: ImageAspectAdaptParams,
|
|
6085
|
-
outputs:
|
|
6852
|
+
outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
|
|
6086
6853
|
outputKinds: { images: "image" },
|
|
6087
6854
|
cost: ({ params }) => {
|
|
6088
6855
|
const p = params;
|
|
@@ -6095,12 +6862,12 @@ var imageAspectAdaptNode = delegated({
|
|
|
6095
6862
|
});
|
|
6096
6863
|
|
|
6097
6864
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
6098
|
-
import { z as
|
|
6099
|
-
var ImageBackgroundRemoveParams =
|
|
6100
|
-
model:
|
|
6101
|
-
model_variant:
|
|
6102
|
-
operating_resolution:
|
|
6103
|
-
mask_only:
|
|
6865
|
+
import { z as z19 } from "zod";
|
|
6866
|
+
var ImageBackgroundRemoveParams = z19.object({
|
|
6867
|
+
model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
|
|
6868
|
+
model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
|
|
6869
|
+
operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
6870
|
+
mask_only: z19.boolean().optional().default(false)
|
|
6104
6871
|
}).strict();
|
|
6105
6872
|
var imageBackgroundRemoveNode = delegated({
|
|
6106
6873
|
id: "image_background_remove",
|
|
@@ -6108,11 +6875,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
6108
6875
|
category: "image",
|
|
6109
6876
|
summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
|
|
6110
6877
|
when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. Set `mask_only:true` to return the binary mask instead of the alpha-cut image.",
|
|
6111
|
-
inputs:
|
|
6878
|
+
inputs: z19.object({
|
|
6112
6879
|
image: ImageRef
|
|
6113
6880
|
}).strict(),
|
|
6114
6881
|
params: ImageBackgroundRemoveParams,
|
|
6115
|
-
outputs:
|
|
6882
|
+
outputs: z19.object({
|
|
6116
6883
|
image: ImageRef,
|
|
6117
6884
|
mask: ImageRef.optional()
|
|
6118
6885
|
}).strict(),
|
|
@@ -6121,7 +6888,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
6121
6888
|
});
|
|
6122
6889
|
|
|
6123
6890
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
6124
|
-
import { z as
|
|
6891
|
+
import { z as z20 } from "zod";
|
|
6125
6892
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
6126
6893
|
var imageDescribeNode = delegated({
|
|
6127
6894
|
id: "image_describe",
|
|
@@ -6129,33 +6896,33 @@ var imageDescribeNode = delegated({
|
|
|
6129
6896
|
category: "vision",
|
|
6130
6897
|
summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
|
|
6131
6898
|
when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
|
|
6132
|
-
inputs:
|
|
6133
|
-
params:
|
|
6134
|
-
model:
|
|
6135
|
-
focus:
|
|
6136
|
-
context:
|
|
6137
|
-
temperature:
|
|
6138
|
-
max_tokens:
|
|
6899
|
+
inputs: z20.object({ image: ImageRef }).loose(),
|
|
6900
|
+
params: z20.object({
|
|
6901
|
+
model: z20.enum(IMAGE_DESCRIBE_MODELS),
|
|
6902
|
+
focus: z20.string().optional(),
|
|
6903
|
+
context: z20.string().optional(),
|
|
6904
|
+
temperature: z20.number().min(0).max(2).optional(),
|
|
6905
|
+
max_tokens: z20.number().int().positive().optional()
|
|
6139
6906
|
}).strict(),
|
|
6140
|
-
outputs:
|
|
6907
|
+
outputs: z20.object({ description: JsonRef }).strict(),
|
|
6141
6908
|
outputKinds: { description: "json" },
|
|
6142
6909
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
6143
6910
|
});
|
|
6144
6911
|
|
|
6145
6912
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
6146
|
-
import { z as
|
|
6913
|
+
import { z as z21 } from "zod";
|
|
6147
6914
|
var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
6148
|
-
var ImageReferenceSheetParams =
|
|
6149
|
-
model:
|
|
6150
|
-
subject_description:
|
|
6915
|
+
var ImageReferenceSheetParams = z21.object({
|
|
6916
|
+
model: z21.enum(REFERENCE_SHEET_MODELS),
|
|
6917
|
+
subject_description: z21.string().min(1),
|
|
6151
6918
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
6152
6919
|
// so a multi-scene shoot keeps one consistent set.
|
|
6153
|
-
subject_type:
|
|
6154
|
-
views:
|
|
6155
|
-
style:
|
|
6156
|
-
prompt_override:
|
|
6157
|
-
aspect_ratio:
|
|
6158
|
-
image_size:
|
|
6920
|
+
subject_type: z21.enum(["character", "person", "product", "location"]),
|
|
6921
|
+
views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
|
|
6922
|
+
style: z21.string().optional(),
|
|
6923
|
+
prompt_override: z21.string().min(1).optional(),
|
|
6924
|
+
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(),
|
|
6925
|
+
image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
|
|
6159
6926
|
}).strict();
|
|
6160
6927
|
var imageReferenceSheetNode = delegated({
|
|
6161
6928
|
id: "image_reference_sheet",
|
|
@@ -6163,9 +6930,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
6163
6930
|
category: "image",
|
|
6164
6931
|
summary: "Fuse 1\u20136 images of a single subject (person, character, product, or location/set) into ONE multi-view reference sheet \u2014 a labeled grid in consistent style and lighting: a turnaround (FRONT / SIDE / BACK\u2026) for a person/character/product, or several camera angles of the same room (WIDE / REVERSE / DETAIL\u2026) for a location. Curated models: Gemini 3 Pro Image (best fusion + labels), Gemini 3.1 Flash Image (cheap iteration).",
|
|
6165
6932
|
when_to_use: "Use before image_generate / video_generate when a subject must stay consistent across many creatives \u2014 wire the `sheet` output into their `reference` input instead of re-describing the subject per prompt. `subject_description` should be the exact wording you reuse downstream. Pick `google/gemini-3-pro-image-preview` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image-preview` while iterating.",
|
|
6166
|
-
inputs:
|
|
6933
|
+
inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
|
|
6167
6934
|
params: ImageReferenceSheetParams,
|
|
6168
|
-
outputs:
|
|
6935
|
+
outputs: z21.object({ sheet: ImageRef }).strict(),
|
|
6169
6936
|
outputKinds: { sheet: "image" },
|
|
6170
6937
|
cost: ({ params }) => ({
|
|
6171
6938
|
credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
|
|
@@ -6174,10 +6941,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
6174
6941
|
});
|
|
6175
6942
|
|
|
6176
6943
|
// src/engine/nodes/remote/imageSearch.ts
|
|
6177
|
-
import { z as
|
|
6178
|
-
var ImageSearchParams =
|
|
6179
|
-
prompt:
|
|
6180
|
-
count:
|
|
6944
|
+
import { z as z22 } from "zod";
|
|
6945
|
+
var ImageSearchParams = z22.object({
|
|
6946
|
+
prompt: z22.string().min(1),
|
|
6947
|
+
count: z22.number().int().min(1).max(20).default(5)
|
|
6181
6948
|
}).strict();
|
|
6182
6949
|
var imageSearchNode = delegated({
|
|
6183
6950
|
id: "image_search",
|
|
@@ -6185,15 +6952,15 @@ var imageSearchNode = delegated({
|
|
|
6185
6952
|
category: "image",
|
|
6186
6953
|
summary: "Agentic image search across Google Images, stock photography (Freepik), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
|
|
6187
6954
|
when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
|
|
6188
|
-
inputs:
|
|
6955
|
+
inputs: z22.object({}).loose(),
|
|
6189
6956
|
params: ImageSearchParams,
|
|
6190
|
-
outputs:
|
|
6957
|
+
outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
|
|
6191
6958
|
outputKinds: { images: "image" },
|
|
6192
6959
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
6193
6960
|
});
|
|
6194
6961
|
|
|
6195
6962
|
// src/engine/nodes/remote/imageSelect.ts
|
|
6196
|
-
import { z as
|
|
6963
|
+
import { z as z23 } from "zod";
|
|
6197
6964
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6198
6965
|
var imageSelectNode = delegated({
|
|
6199
6966
|
id: "image_select",
|
|
@@ -6201,15 +6968,15 @@ var imageSelectNode = delegated({
|
|
|
6201
6968
|
category: "vision",
|
|
6202
6969
|
summary: "Pick the best `count` images out of 2+ candidates with a vision LLM, judged against a prompt. Outputs a passthrough subset of the input refs (no new pixels) plus the model's comparative reasoning.",
|
|
6203
6970
|
when_to_use: "Use after fanning out several image_generate variants (or any pool of 2+ images) to keep only the strongest before expensive downstream steps \u2014 video generation, reference sheets, final delivery. `count` fixes the output size, so `images#0`\u2026`images#count-1` are always safe to wire. Pick `~google/gemini-flash-latest` for cheap/fast picks and `~google/gemini-pro-latest` for harder aesthetic judgement.",
|
|
6204
|
-
inputs:
|
|
6205
|
-
params:
|
|
6206
|
-
model:
|
|
6207
|
-
prompt:
|
|
6208
|
-
count:
|
|
6209
|
-
temperature:
|
|
6210
|
-
max_tokens:
|
|
6971
|
+
inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
|
|
6972
|
+
params: z23.object({
|
|
6973
|
+
model: z23.enum(IMAGE_SELECT_MODELS),
|
|
6974
|
+
prompt: z23.string().min(1),
|
|
6975
|
+
count: z23.number().int().min(1).default(1),
|
|
6976
|
+
temperature: z23.number().min(0).max(2).optional(),
|
|
6977
|
+
max_tokens: z23.number().int().positive().optional()
|
|
6211
6978
|
}).strict(),
|
|
6212
|
-
outputs:
|
|
6979
|
+
outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
|
|
6213
6980
|
outputKinds: { images: "image", reasoning: "text" },
|
|
6214
6981
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
6215
6982
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -6234,34 +7001,34 @@ var imageSelectNode = delegated({
|
|
|
6234
7001
|
});
|
|
6235
7002
|
|
|
6236
7003
|
// src/engine/nodes/remote/music.ts
|
|
6237
|
-
import { z as
|
|
7004
|
+
import { z as z24 } from "zod";
|
|
6238
7005
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
6239
|
-
var MusicParams =
|
|
6240
|
-
model:
|
|
7006
|
+
var MusicParams = z24.object({
|
|
7007
|
+
model: z24.enum(MUSIC_MODELS),
|
|
6241
7008
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
6242
|
-
prompt:
|
|
7009
|
+
prompt: z24.string().optional(),
|
|
6243
7010
|
/**
|
|
6244
7011
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
6245
7012
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
6246
7013
|
*/
|
|
6247
|
-
composition_plan:
|
|
7014
|
+
composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
|
|
6248
7015
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
6249
|
-
music_length_ms:
|
|
6250
|
-
seed:
|
|
7016
|
+
music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
7017
|
+
seed: z24.number().int().optional(),
|
|
6251
7018
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
6252
|
-
force_instrumental:
|
|
7019
|
+
force_instrumental: z24.boolean().optional(),
|
|
6253
7020
|
/** composition_plan only — honor exact section durations. */
|
|
6254
|
-
respect_sections_durations:
|
|
7021
|
+
respect_sections_durations: z24.boolean().optional(),
|
|
6255
7022
|
/** Emit word-level timestamps alongside the audio. */
|
|
6256
|
-
with_timestamps:
|
|
7023
|
+
with_timestamps: z24.boolean().optional(),
|
|
6257
7024
|
/**
|
|
6258
7025
|
* video-to-music only — short description of the desired score
|
|
6259
7026
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
6260
7027
|
*/
|
|
6261
|
-
description:
|
|
7028
|
+
description: z24.string().max(1e3).optional(),
|
|
6262
7029
|
/** video-to-music only — up to 10 style tags. */
|
|
6263
|
-
tags:
|
|
6264
|
-
output_format:
|
|
7030
|
+
tags: z24.array(z24.string()).max(10).optional(),
|
|
7031
|
+
output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6265
7032
|
}).strict();
|
|
6266
7033
|
var musicNode = delegated({
|
|
6267
7034
|
id: "music",
|
|
@@ -6269,9 +7036,9 @@ var musicNode = delegated({
|
|
|
6269
7036
|
category: "audio",
|
|
6270
7037
|
summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
|
|
6271
7038
|
when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
|
|
6272
|
-
inputs:
|
|
7039
|
+
inputs: z24.object({ video: VideoRef.optional() }).loose(),
|
|
6273
7040
|
params: MusicParams,
|
|
6274
|
-
outputs:
|
|
7041
|
+
outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6275
7042
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6276
7043
|
cost: ({ params }) => {
|
|
6277
7044
|
const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
|
|
@@ -6302,25 +7069,25 @@ var musicNode = delegated({
|
|
|
6302
7069
|
});
|
|
6303
7070
|
|
|
6304
7071
|
// src/engine/nodes/remote/soundEffect.ts
|
|
6305
|
-
import { z as
|
|
7072
|
+
import { z as z25 } from "zod";
|
|
6306
7073
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
6307
|
-
var SoundEffectParams =
|
|
6308
|
-
model:
|
|
7074
|
+
var SoundEffectParams = z25.object({
|
|
7075
|
+
model: z25.enum(SOUND_EFFECT_MODELS),
|
|
6309
7076
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
6310
|
-
text:
|
|
7077
|
+
text: z25.string().min(1),
|
|
6311
7078
|
/**
|
|
6312
7079
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
6313
7080
|
* natural length for the described effect.
|
|
6314
7081
|
*/
|
|
6315
|
-
duration_seconds:
|
|
7082
|
+
duration_seconds: z25.number().min(0.5).max(30).optional(),
|
|
6316
7083
|
/**
|
|
6317
7084
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
6318
7085
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
6319
7086
|
*/
|
|
6320
|
-
prompt_influence:
|
|
7087
|
+
prompt_influence: z25.number().min(0).max(1).optional(),
|
|
6321
7088
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
6322
|
-
loop:
|
|
6323
|
-
output_format:
|
|
7089
|
+
loop: z25.boolean().optional(),
|
|
7090
|
+
output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
6324
7091
|
}).strict();
|
|
6325
7092
|
var soundEffectNode = delegated({
|
|
6326
7093
|
id: "sound_effect",
|
|
@@ -6328,9 +7095,9 @@ var soundEffectNode = delegated({
|
|
|
6328
7095
|
category: "audio",
|
|
6329
7096
|
summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
|
|
6330
7097
|
when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
|
|
6331
|
-
inputs:
|
|
7098
|
+
inputs: z25.object({}).loose(),
|
|
6332
7099
|
params: SoundEffectParams,
|
|
6333
|
-
outputs:
|
|
7100
|
+
outputs: z25.object({ audio: AudioRef }).strict(),
|
|
6334
7101
|
outputKinds: { audio: "audio" },
|
|
6335
7102
|
cost: ({ params }) => {
|
|
6336
7103
|
const seconds = params.duration_seconds ?? 5;
|
|
@@ -6339,7 +7106,7 @@ var soundEffectNode = delegated({
|
|
|
6339
7106
|
});
|
|
6340
7107
|
|
|
6341
7108
|
// src/engine/nodes/remote/textGenerate.ts
|
|
6342
|
-
import { z as
|
|
7109
|
+
import { z as z26 } from "zod";
|
|
6343
7110
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6344
7111
|
var textGenerateNode = delegated({
|
|
6345
7112
|
id: "text_generate",
|
|
@@ -6347,58 +7114,58 @@ var textGenerateNode = delegated({
|
|
|
6347
7114
|
category: "language",
|
|
6348
7115
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
6349
7116
|
when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
|
|
6350
|
-
inputs:
|
|
6351
|
-
params:
|
|
6352
|
-
model:
|
|
6353
|
-
prompt:
|
|
6354
|
-
system:
|
|
6355
|
-
response_format:
|
|
6356
|
-
web_search:
|
|
6357
|
-
temperature:
|
|
6358
|
-
max_tokens:
|
|
7117
|
+
inputs: z26.object({}).loose(),
|
|
7118
|
+
params: z26.object({
|
|
7119
|
+
model: z26.enum(TEXT_GENERATE_MODELS),
|
|
7120
|
+
prompt: z26.string().min(1),
|
|
7121
|
+
system: z26.string().optional(),
|
|
7122
|
+
response_format: z26.enum(["text", "json_object"]).optional(),
|
|
7123
|
+
web_search: z26.boolean().optional(),
|
|
7124
|
+
temperature: z26.number().min(0).max(2).optional(),
|
|
7125
|
+
max_tokens: z26.number().int().positive().optional()
|
|
6359
7126
|
}).strict(),
|
|
6360
|
-
outputs:
|
|
7127
|
+
outputs: z26.object({ text: TextRef }).strict(),
|
|
6361
7128
|
outputKinds: { text: "text" },
|
|
6362
7129
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
6363
7130
|
});
|
|
6364
7131
|
|
|
6365
7132
|
// src/engine/nodes/remote/tts.ts
|
|
6366
|
-
import { z as
|
|
7133
|
+
import { z as z27 } from "zod";
|
|
6367
7134
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
6368
|
-
var TtsVoiceSettings =
|
|
6369
|
-
stability:
|
|
6370
|
-
similarity_boost:
|
|
6371
|
-
style:
|
|
6372
|
-
use_speaker_boost:
|
|
6373
|
-
speed:
|
|
7135
|
+
var TtsVoiceSettings = z27.object({
|
|
7136
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7137
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
7138
|
+
style: z27.number().min(0).max(1).optional(),
|
|
7139
|
+
use_speaker_boost: z27.boolean().optional(),
|
|
7140
|
+
speed: z27.number().min(0.25).max(4).optional()
|
|
6374
7141
|
}).strict();
|
|
6375
|
-
var TtsPronunciationLocator =
|
|
6376
|
-
pronunciation_dictionary_id:
|
|
6377
|
-
version_id:
|
|
7142
|
+
var TtsPronunciationLocator = z27.object({
|
|
7143
|
+
pronunciation_dictionary_id: z27.string().min(1),
|
|
7144
|
+
version_id: z27.string().nullable().optional()
|
|
6378
7145
|
}).strict();
|
|
6379
|
-
var TtsParams =
|
|
6380
|
-
model:
|
|
6381
|
-
text:
|
|
6382
|
-
voice:
|
|
7146
|
+
var TtsParams = z27.object({
|
|
7147
|
+
model: z27.enum(TTS_MODELS),
|
|
7148
|
+
text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
7149
|
+
voice: z27.string().min(1),
|
|
6383
7150
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
6384
|
-
output_format:
|
|
6385
|
-
seed:
|
|
7151
|
+
output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
7152
|
+
seed: z27.number().int().min(0).max(4294967295).optional(),
|
|
6386
7153
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
6387
|
-
stability:
|
|
6388
|
-
similarity_boost:
|
|
7154
|
+
stability: z27.number().min(0).max(1).optional(),
|
|
7155
|
+
similarity_boost: z27.number().min(0).max(1).optional(),
|
|
6389
7156
|
voice_settings: TtsVoiceSettings.optional(),
|
|
6390
7157
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
6391
|
-
language_code:
|
|
6392
|
-
pronunciation_dictionary_locators:
|
|
6393
|
-
apply_text_normalization:
|
|
7158
|
+
language_code: z27.string().optional(),
|
|
7159
|
+
pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
|
|
7160
|
+
apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
|
|
6394
7161
|
/** Currently Japanese-only. Adds latency. */
|
|
6395
|
-
apply_language_text_normalization:
|
|
7162
|
+
apply_language_text_normalization: z27.boolean().optional(),
|
|
6396
7163
|
/**
|
|
6397
7164
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
6398
7165
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
6399
7166
|
* rendering, lipsync, and beat-matched cuts.
|
|
6400
7167
|
*/
|
|
6401
|
-
with_timestamps:
|
|
7168
|
+
with_timestamps: z27.boolean().optional()
|
|
6402
7169
|
}).strict();
|
|
6403
7170
|
var ttsNode = delegated({
|
|
6404
7171
|
id: "tts",
|
|
@@ -6406,9 +7173,9 @@ var ttsNode = delegated({
|
|
|
6406
7173
|
category: "audio",
|
|
6407
7174
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
6408
7175
|
when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
|
|
6409
|
-
inputs:
|
|
7176
|
+
inputs: z27.object({}).loose(),
|
|
6410
7177
|
params: TtsParams,
|
|
6411
|
-
outputs:
|
|
7178
|
+
outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
6412
7179
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
6413
7180
|
cost: ({ params }) => ({
|
|
6414
7181
|
credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
|
|
@@ -6417,47 +7184,49 @@ var ttsNode = delegated({
|
|
|
6417
7184
|
});
|
|
6418
7185
|
|
|
6419
7186
|
// src/engine/nodes/remote/video.ts
|
|
6420
|
-
import { z as
|
|
6421
|
-
var
|
|
6422
|
-
var VideoGenerateParams =
|
|
6423
|
-
model:
|
|
6424
|
-
prompt:
|
|
6425
|
-
duration:
|
|
6426
|
-
resolution:
|
|
7187
|
+
import { z as z28 } from "zod";
|
|
7188
|
+
var videoModelEnum = z28.enum(VIDEO_GENERATE_MODELS);
|
|
7189
|
+
var VideoGenerateParams = z28.object({
|
|
7190
|
+
model: videoModelEnum,
|
|
7191
|
+
prompt: z28.string().min(1),
|
|
7192
|
+
duration: z28.number().int().positive().optional(),
|
|
7193
|
+
resolution: z28.string().optional(),
|
|
6427
7194
|
// Union of ratios accepted by at least one curated model (registry gates
|
|
6428
7195
|
// per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
|
|
6429
|
-
aspect_ratio:
|
|
6430
|
-
generate_audio:
|
|
6431
|
-
seed:
|
|
7196
|
+
aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
|
|
7197
|
+
generate_audio: z28.boolean().optional(),
|
|
7198
|
+
seed: z28.number().int().nonnegative().optional(),
|
|
6432
7199
|
// Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
|
|
6433
|
-
negative_prompt:
|
|
6434
|
-
person_generation:
|
|
6435
|
-
enhance_prompt:
|
|
6436
|
-
conditioning_scale:
|
|
7200
|
+
negative_prompt: z28.string().optional(),
|
|
7201
|
+
person_generation: z28.string().optional(),
|
|
7202
|
+
enhance_prompt: z28.boolean().optional(),
|
|
7203
|
+
conditioning_scale: z28.number().optional(),
|
|
7204
|
+
// Kling-only passthrough (prompt-adherence dial, sent top-level).
|
|
7205
|
+
cfg_scale: z28.number().optional()
|
|
6437
7206
|
}).strict();
|
|
6438
7207
|
var videoGenerateNode = delegated({
|
|
6439
7208
|
id: "video_generate",
|
|
6440
7209
|
version: "2.0.0",
|
|
6441
7210
|
category: "video",
|
|
6442
|
-
summary: "Generate video for ad creatives.
|
|
6443
|
-
when_to_use: "Use `bytedance/seedance-2.0` for
|
|
6444
|
-
inputs:
|
|
7211
|
+
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.",
|
|
7212
|
+
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.",
|
|
7213
|
+
inputs: z28.object({
|
|
6445
7214
|
first_frame: ImageRef.optional(),
|
|
6446
7215
|
last_frame: ImageRef.optional(),
|
|
6447
7216
|
reference: ImageRef.optional()
|
|
6448
7217
|
}).loose(),
|
|
6449
7218
|
params: VideoGenerateParams,
|
|
6450
|
-
outputs:
|
|
7219
|
+
outputs: z28.object({ video: VideoRef }).strict(),
|
|
6451
7220
|
outputKinds: { video: "video" },
|
|
6452
7221
|
cost: () => ({ credits: 50, seconds_estimate: 120 })
|
|
6453
7222
|
});
|
|
6454
7223
|
|
|
6455
7224
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
6456
|
-
import { z as
|
|
6457
|
-
var VideoBackgroundRemoveParams =
|
|
6458
|
-
model:
|
|
6459
|
-
edge_refinement:
|
|
6460
|
-
output_codec:
|
|
7225
|
+
import { z as z29 } from "zod";
|
|
7226
|
+
var VideoBackgroundRemoveParams = z29.object({
|
|
7227
|
+
model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
|
|
7228
|
+
edge_refinement: z29.boolean().optional().default(true),
|
|
7229
|
+
output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
|
|
6461
7230
|
}).strict();
|
|
6462
7231
|
var videoBackgroundRemoveNode = delegated({
|
|
6463
7232
|
id: "video_background_remove",
|
|
@@ -6465,18 +7234,18 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
6465
7234
|
category: "video",
|
|
6466
7235
|
summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by fal.ai `veed/video-background-removal/fast`.",
|
|
6467
7236
|
when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
|
|
6468
|
-
inputs:
|
|
7237
|
+
inputs: z29.object({
|
|
6469
7238
|
video: VideoRef
|
|
6470
7239
|
}).strict(),
|
|
6471
7240
|
params: VideoBackgroundRemoveParams,
|
|
6472
|
-
outputs:
|
|
7241
|
+
outputs: z29.object({ video: VideoRef }).strict(),
|
|
6473
7242
|
outputKinds: { video: "video" },
|
|
6474
7243
|
// $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
|
|
6475
7244
|
cost: () => ({ credits: 50, seconds_estimate: 60 })
|
|
6476
7245
|
});
|
|
6477
7246
|
|
|
6478
7247
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
6479
|
-
import { z as
|
|
7248
|
+
import { z as z30 } from "zod";
|
|
6480
7249
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
6481
7250
|
var videoDeconstructNode = delegated({
|
|
6482
7251
|
id: "video_deconstruct",
|
|
@@ -6484,34 +7253,34 @@ var videoDeconstructNode = delegated({
|
|
|
6484
7253
|
category: "video",
|
|
6485
7254
|
summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
|
|
6486
7255
|
when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
|
|
6487
|
-
inputs:
|
|
6488
|
-
params:
|
|
6489
|
-
model:
|
|
6490
|
-
mode:
|
|
6491
|
-
language:
|
|
6492
|
-
max_scenes:
|
|
6493
|
-
focus:
|
|
6494
|
-
start_s:
|
|
6495
|
-
end_s:
|
|
7256
|
+
inputs: z30.object({ video: VideoRef }).loose(),
|
|
7257
|
+
params: z30.object({
|
|
7258
|
+
model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
7259
|
+
mode: z30.enum(["full", "index"]).optional(),
|
|
7260
|
+
language: z30.string().min(2).max(8).optional(),
|
|
7261
|
+
max_scenes: z30.number().int().min(1).max(60).optional(),
|
|
7262
|
+
focus: z30.string().optional(),
|
|
7263
|
+
start_s: z30.number().min(0).optional(),
|
|
7264
|
+
end_s: z30.number().positive().optional(),
|
|
6496
7265
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
6497
7266
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
6498
7267
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
6499
7268
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
6500
|
-
shot_cuts:
|
|
7269
|
+
shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
|
|
6501
7270
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
6502
7271
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
6503
7272
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
6504
7273
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
6505
|
-
max_clip_s:
|
|
7274
|
+
max_clip_s: z30.number().positive().max(60).optional(),
|
|
6506
7275
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
6507
7276
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
6508
|
-
transcriber:
|
|
7277
|
+
transcriber: z30.enum(["groq", "deepgram"]).optional()
|
|
6509
7278
|
}).strict(),
|
|
6510
|
-
outputs:
|
|
7279
|
+
outputs: z30.object({
|
|
6511
7280
|
analysis: JsonRef,
|
|
6512
7281
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
6513
|
-
start_frames:
|
|
6514
|
-
end_frames:
|
|
7282
|
+
start_frames: z30.array(ImageRef).min(1).optional(),
|
|
7283
|
+
end_frames: z30.array(ImageRef).min(1).optional(),
|
|
6515
7284
|
transcript: JsonRef
|
|
6516
7285
|
}).strict(),
|
|
6517
7286
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -6519,31 +7288,31 @@ var videoDeconstructNode = delegated({
|
|
|
6519
7288
|
});
|
|
6520
7289
|
|
|
6521
7290
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
6522
|
-
import { z as
|
|
6523
|
-
var FalLipsyncParams =
|
|
6524
|
-
model:
|
|
7291
|
+
import { z as z31 } from "zod";
|
|
7292
|
+
var FalLipsyncParams = z31.object({
|
|
7293
|
+
model: z31.literal("fal/veed-lipsync")
|
|
6525
7294
|
}).strict();
|
|
6526
|
-
var VideoLipsyncParams =
|
|
7295
|
+
var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
|
|
6527
7296
|
var videoLipsyncNode = delegated({
|
|
6528
7297
|
id: "video_lipsync",
|
|
6529
7298
|
version: "1.0.0",
|
|
6530
7299
|
category: "video",
|
|
6531
7300
|
summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
|
|
6532
|
-
inputs:
|
|
7301
|
+
inputs: z31.object({
|
|
6533
7302
|
video: VideoRef,
|
|
6534
7303
|
audio: AudioRef
|
|
6535
7304
|
}).strict(),
|
|
6536
7305
|
params: VideoLipsyncParams,
|
|
6537
|
-
outputs:
|
|
7306
|
+
outputs: z31.object({ video: VideoRef }).strict(),
|
|
6538
7307
|
outputKinds: { video: "video" },
|
|
6539
7308
|
cost: () => ({ credits: 20, seconds_estimate: 120 })
|
|
6540
7309
|
});
|
|
6541
7310
|
|
|
6542
7311
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6543
|
-
import { mkdtemp as mkdtemp6, readFile as
|
|
7312
|
+
import { mkdtemp as mkdtemp6, readFile as readFile11, rm as rm6 } from "fs/promises";
|
|
6544
7313
|
import { tmpdir as tmpdir6 } from "os";
|
|
6545
|
-
import
|
|
6546
|
-
import { z as
|
|
7314
|
+
import path14 from "path";
|
|
7315
|
+
import { z as z32 } from "zod";
|
|
6547
7316
|
|
|
6548
7317
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
6549
7318
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -6622,29 +7391,32 @@ ${detail.slice(-4e3)}`);
|
|
|
6622
7391
|
}
|
|
6623
7392
|
|
|
6624
7393
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
6625
|
-
var VideoTranscribeParams =
|
|
6626
|
-
language:
|
|
7394
|
+
var VideoTranscribeParams = z32.object({
|
|
7395
|
+
language: z32.string().min(2).max(8).optional(),
|
|
6627
7396
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
6628
7397
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
6629
7398
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
6630
|
-
transcriber:
|
|
7399
|
+
transcriber: z32.enum(["groq", "deepgram"]).optional()
|
|
6631
7400
|
}).strict();
|
|
6632
|
-
var VideoTranscribeInputs =
|
|
6633
|
-
video
|
|
7401
|
+
var VideoTranscribeInputs = z32.object({
|
|
7402
|
+
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
7403
|
+
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
7404
|
+
// the local extraction path has been shipping one for every video input.
|
|
7405
|
+
video: z32.union([VideoRef, AudioRef])
|
|
6634
7406
|
}).strict();
|
|
6635
|
-
var VideoTranscribeOutputs =
|
|
6636
|
-
transcript:
|
|
7407
|
+
var VideoTranscribeOutputs = z32.object({
|
|
7408
|
+
transcript: z32.custom(),
|
|
6637
7409
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
6638
7410
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
6639
|
-
rich:
|
|
7411
|
+
rich: z32.custom().optional()
|
|
6640
7412
|
}).strict();
|
|
6641
7413
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
6642
7414
|
var videoTranscribeNode = defineNode({
|
|
6643
7415
|
id: "video_transcribe",
|
|
6644
|
-
version: "2.
|
|
7416
|
+
version: "2.3.0",
|
|
6645
7417
|
category: "language",
|
|
6646
7418
|
location: "local",
|
|
6647
|
-
summary: 'Transcribe a video
|
|
7419
|
+
summary: 'Transcribe a video or audio track to a word-level JSON transcript. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo ($0.04/hr, 10s min); `transcriber:"deepgram"` uses Deepgram Nova-3 ($0.0043/min) and additionally emits a `rich` JSON output with punctuated words + paragraph/sentence grouping (and speaker indices). Automatically extracts audio locally (mono 16 kHz MP3) before uploading \u2014 reduces payload ~100\xD7 and lifts the effective duration limit well beyond Groq\'s 100 MB file cap. The `transcript` output is always an array of {text, start, end} entries ready to feed Hyperframes caption compositions (Deepgram prefers the punctuated word form).',
|
|
6648
7420
|
when_to_use: 'Use to generate burned-in captions for a stitched video. Pair with `hyperframe_render` and a captions composition (e.g. `tiktok-captions`) by passing the transcript JSON through `params.variables.transcript`. Pick `params.language` to filter out non-target speech (e.g. "es" for Spanish). Pick `transcriber:"deepgram"` when you want punctuation/paragraphs (read them from the `rich` output) or speaker grouping. Requires ffmpeg on PATH for audio extraction (falls back to full video upload if unavailable).',
|
|
6649
7421
|
inputs: VideoTranscribeInputs,
|
|
6650
7422
|
params: VideoTranscribeParams,
|
|
@@ -6656,7 +7428,7 @@ var videoTranscribeNode = defineNode({
|
|
|
6656
7428
|
const effectiveInputs = audioInput ?? inputs;
|
|
6657
7429
|
return await callBackendExec({
|
|
6658
7430
|
nodeType: "video_transcribe",
|
|
6659
|
-
nodeVersion: "2.
|
|
7431
|
+
nodeVersion: "2.3.0",
|
|
6660
7432
|
params,
|
|
6661
7433
|
inputs: effectiveInputs,
|
|
6662
7434
|
outputKinds: { transcript: "json", rich: "json" },
|
|
@@ -6674,14 +7446,14 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6674
7446
|
ctx.log("video_transcribe: no audio track detected, sending full video");
|
|
6675
7447
|
return null;
|
|
6676
7448
|
}
|
|
6677
|
-
tmpDir = await mkdtemp6(
|
|
6678
|
-
const audioPath =
|
|
7449
|
+
tmpDir = await mkdtemp6(path14.join(tmpdir6(), "vtx-"));
|
|
7450
|
+
const audioPath = path14.join(tmpDir, "audio.mp3");
|
|
6679
7451
|
ctx.log("video_transcribe: extracting audio (mono 16kHz mp3)");
|
|
6680
7452
|
await runFfmpeg(
|
|
6681
7453
|
["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
|
|
6682
7454
|
{ timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
|
|
6683
7455
|
);
|
|
6684
|
-
const bytes = await
|
|
7456
|
+
const bytes = await readFile11(audioPath);
|
|
6685
7457
|
if (bytes.byteLength === 0) {
|
|
6686
7458
|
ctx.log("video_transcribe: extracted audio is empty, sending full video");
|
|
6687
7459
|
return null;
|
|
@@ -6721,29 +7493,29 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
6721
7493
|
}
|
|
6722
7494
|
|
|
6723
7495
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
6724
|
-
import { z as
|
|
7496
|
+
import { z as z33 } from "zod";
|
|
6725
7497
|
var voiceSelectNode = delegated({
|
|
6726
7498
|
id: "voice_select",
|
|
6727
7499
|
version: "1.0.0",
|
|
6728
7500
|
category: "audio",
|
|
6729
7501
|
summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
|
|
6730
7502
|
when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
|
|
6731
|
-
inputs:
|
|
6732
|
-
params:
|
|
6733
|
-
description:
|
|
6734
|
-
gender:
|
|
6735
|
-
age:
|
|
6736
|
-
accent:
|
|
6737
|
-
language:
|
|
6738
|
-
limit:
|
|
7503
|
+
inputs: z33.object({}).loose(),
|
|
7504
|
+
params: z33.object({
|
|
7505
|
+
description: z33.string().min(1),
|
|
7506
|
+
gender: z33.string().optional(),
|
|
7507
|
+
age: z33.string().optional(),
|
|
7508
|
+
accent: z33.string().optional(),
|
|
7509
|
+
language: z33.string().optional(),
|
|
7510
|
+
limit: z33.number().int().min(1).max(20).optional()
|
|
6739
7511
|
}).strict(),
|
|
6740
|
-
outputs:
|
|
7512
|
+
outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
6741
7513
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
6742
7514
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
6743
7515
|
});
|
|
6744
7516
|
|
|
6745
7517
|
// src/engine/schema/catalog.ts
|
|
6746
|
-
import { z as
|
|
7518
|
+
import { z as z34 } from "zod";
|
|
6747
7519
|
function generateCatalog(registry, opts = {}) {
|
|
6748
7520
|
const entries = registry.all().map((def) => {
|
|
6749
7521
|
const cost = def.cost ? safeCost(def) : void 0;
|
|
@@ -6754,9 +7526,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
6754
7526
|
summary: def.summary,
|
|
6755
7527
|
when_to_use: def.when_to_use,
|
|
6756
7528
|
location: def.location,
|
|
6757
|
-
inputs:
|
|
6758
|
-
params:
|
|
6759
|
-
outputs:
|
|
7529
|
+
inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
7530
|
+
params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
7531
|
+
outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
6760
7532
|
cost_estimate_credits: cost?.credits,
|
|
6761
7533
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
6762
7534
|
};
|
|
@@ -6788,19 +7560,19 @@ function safeCost(def) {
|
|
|
6788
7560
|
|
|
6789
7561
|
// src/engine/storage/cache-store.ts
|
|
6790
7562
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6791
|
-
import { mkdir as mkdir3, readFile as
|
|
6792
|
-
import
|
|
7563
|
+
import { mkdir as mkdir3, readFile as readFile12, rename as rename2, writeFile as writeFile7 } from "fs/promises";
|
|
7564
|
+
import path15 from "path";
|
|
6793
7565
|
var LocalCacheStore = class {
|
|
6794
7566
|
rootDir;
|
|
6795
7567
|
constructor(rootDir) {
|
|
6796
7568
|
this.rootDir = rootDir;
|
|
6797
7569
|
}
|
|
6798
7570
|
filePath(cacheKey) {
|
|
6799
|
-
return
|
|
7571
|
+
return path15.join(this.rootDir, `${cacheKey}.json`);
|
|
6800
7572
|
}
|
|
6801
7573
|
async get(cacheKey) {
|
|
6802
7574
|
try {
|
|
6803
|
-
const buf = await
|
|
7575
|
+
const buf = await readFile12(this.filePath(cacheKey), "utf8");
|
|
6804
7576
|
return JSON.parse(buf);
|
|
6805
7577
|
} catch (e) {
|
|
6806
7578
|
if (e.code === "ENOENT") return null;
|
|
@@ -6809,7 +7581,7 @@ var LocalCacheStore = class {
|
|
|
6809
7581
|
}
|
|
6810
7582
|
async put(entry) {
|
|
6811
7583
|
const dest = this.filePath(entry.cacheKey);
|
|
6812
|
-
await mkdir3(
|
|
7584
|
+
await mkdir3(path15.dirname(dest), { recursive: true });
|
|
6813
7585
|
const tmp = `${dest}.tmp-${process.pid}-${randomUUID2()}`;
|
|
6814
7586
|
await writeFile7(tmp, JSON.stringify(entry, null, 0));
|
|
6815
7587
|
await rename2(tmp, dest);
|
|
@@ -6833,7 +7605,8 @@ var LOCAL_NODES = [
|
|
|
6833
7605
|
imagemagickNode,
|
|
6834
7606
|
videoTranscribeNode,
|
|
6835
7607
|
fontSpecimenNode,
|
|
6836
|
-
audioTimelineNode
|
|
7608
|
+
audioTimelineNode,
|
|
7609
|
+
collectNode
|
|
6837
7610
|
];
|
|
6838
7611
|
var REMOTE_NODES = [
|
|
6839
7612
|
textGenerateNode,
|
|
@@ -6863,12 +7636,12 @@ function defaultRegistry() {
|
|
|
6863
7636
|
}
|
|
6864
7637
|
function createEngineFromEnv(opts = {}) {
|
|
6865
7638
|
const cwd = opts.cwd ?? process.cwd();
|
|
6866
|
-
const cacheDir = opts.cacheDir ??
|
|
6867
|
-
const outputsDir = opts.outputsDir ??
|
|
7639
|
+
const cacheDir = opts.cacheDir ?? path16.join(cwd, "canvas", ".cache");
|
|
7640
|
+
const outputsDir = opts.outputsDir ?? path16.join(cwd, "canvas");
|
|
6868
7641
|
const creds = requireCredentialsFromEnv();
|
|
6869
7642
|
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
6870
|
-
const assets = new LocalAssetStore(
|
|
6871
|
-
const localCache = new LocalCacheStore(
|
|
7643
|
+
const assets = new LocalAssetStore(path16.join(cacheDir, "assets"));
|
|
7644
|
+
const localCache = new LocalCacheStore(path16.join(cacheDir, "index"));
|
|
6872
7645
|
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
6873
7646
|
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
6874
7647
|
local: localCache,
|
|
@@ -6888,12 +7661,15 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6888
7661
|
}
|
|
6889
7662
|
|
|
6890
7663
|
export {
|
|
7664
|
+
BackendClient,
|
|
6891
7665
|
requireCredentialsFromEnv,
|
|
7666
|
+
RunAbortedError,
|
|
6892
7667
|
LayerExecutionError,
|
|
6893
7668
|
describeFailureReason,
|
|
6894
7669
|
SEEDANCE_DURATIONS,
|
|
6895
7670
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
6896
7671
|
IMAGE_GENERATE_MODELS,
|
|
7672
|
+
DEFAULT_VIDEO_GENERATE_MODEL,
|
|
6897
7673
|
MODEL_REGISTRY,
|
|
6898
7674
|
resolveConcurrency,
|
|
6899
7675
|
ulid,
|
|
@@ -6902,9 +7678,15 @@ export {
|
|
|
6902
7678
|
REF_PREFIX,
|
|
6903
7679
|
parseRefExpr,
|
|
6904
7680
|
sha256Hex,
|
|
7681
|
+
SEEDANCE_PROFILE,
|
|
7682
|
+
clipProfileFor,
|
|
7683
|
+
clipParamRecipe,
|
|
7684
|
+
imageProfileFor,
|
|
7685
|
+
spineInputFlags,
|
|
7686
|
+
spineInputOps,
|
|
6905
7687
|
elementMentionKeywords,
|
|
6906
7688
|
toModelSafeImage,
|
|
6907
|
-
BackendClient2
|
|
7689
|
+
BackendClient2,
|
|
6908
7690
|
Engine2 as Engine,
|
|
6909
7691
|
LocalAssetStore2 as LocalAssetStore,
|
|
6910
7692
|
LocalCacheStore2 as LocalCacheStore,
|
|
@@ -6914,4 +7696,4 @@ export {
|
|
|
6914
7696
|
defaultRegistry,
|
|
6915
7697
|
createEngineFromEnv
|
|
6916
7698
|
};
|
|
6917
|
-
//# sourceMappingURL=chunk-
|
|
7699
|
+
//# sourceMappingURL=chunk-4QK7JC6O.js.map
|