@koda-sl/baker-cli 0.199.0-dev.6d2f498f5 → 0.200.0-dev.0a9adf6f7
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 +70 -4
- package/dist/{chunk-ZS6BR7CU.js → chunk-2R2DEPG7.js} +3 -3
- package/dist/{chunk-6B5RAIAC.js → chunk-6NZG2TCM.js} +82 -4
- package/dist/chunk-6NZG2TCM.js.map +1 -0
- package/dist/{chunk-CVIGWKG2.js → chunk-KHHD26LZ.js} +3 -3
- package/dist/{chunk-MMP7BRV7.js → chunk-ONFW7HIK.js} +511 -408
- package/dist/chunk-ONFW7HIK.js.map +1 -0
- package/dist/{chunk-VFRDAUQH.js → chunk-SVHFT7AN.js} +4 -4
- package/dist/{chunk-PGPHKRK4.js → chunk-UZYVSPCA.js} +2 -2
- package/dist/cli.js +6091 -5244
- package/dist/cli.js.map +1 -1
- package/dist/client-MP7BKFKM.js +15 -0
- package/dist/engine/index.js +3 -3
- package/dist/{env-S4LMU6EN.js → env-FF7ZB5W5.js} +2 -2
- package/dist/{output-VCUZ4EBX.js → output-YGPWST77.js} +5 -5
- package/dist/{shared-WG5FCIHH.js → shared-CRWB5B7P.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-6B5RAIAC.js.map +0 -1
- package/dist/chunk-MMP7BRV7.js.map +0 -1
- package/dist/client-BBH4AYOK.js +0 -15
- /package/dist/{chunk-ZS6BR7CU.js.map → chunk-2R2DEPG7.js.map} +0 -0
- /package/dist/{chunk-CVIGWKG2.js.map → chunk-KHHD26LZ.js.map} +0 -0
- /package/dist/{chunk-VFRDAUQH.js.map → chunk-SVHFT7AN.js.map} +0 -0
- /package/dist/{chunk-PGPHKRK4.js.map → chunk-UZYVSPCA.js.map} +0 -0
- /package/dist/{client-BBH4AYOK.js.map → client-MP7BKFKM.js.map} +0 -0
- /package/dist/{env-S4LMU6EN.js.map → env-FF7ZB5W5.js.map} +0 -0
- /package/dist/{output-VCUZ4EBX.js.map → output-YGPWST77.js.map} +0 -0
- /package/dist/{shared-WG5FCIHH.js.map → shared-CRWB5B7P.js.map} +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debugLogHttp,
|
|
3
3
|
readBodyForLog
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-UZYVSPCA.js";
|
|
5
5
|
import {
|
|
6
6
|
__commonJS,
|
|
7
7
|
__toESM,
|
|
8
8
|
captureProxyCredentials,
|
|
9
9
|
childEnvWith,
|
|
10
10
|
isChallengeBody,
|
|
11
|
+
isPrivateAddress,
|
|
11
12
|
isProxyFailure,
|
|
12
13
|
plannedRoutes,
|
|
13
14
|
refuseNonPublicUrl,
|
|
14
15
|
shouldEscalate
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-6NZG2TCM.js";
|
|
16
17
|
|
|
17
18
|
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
|
|
18
19
|
var require_safe_stable_stringify = __commonJS({
|
|
@@ -611,7 +612,7 @@ ${originalIndentation}`;
|
|
|
611
612
|
});
|
|
612
613
|
|
|
613
614
|
// src/engine/index.ts
|
|
614
|
-
import
|
|
615
|
+
import path17 from "path";
|
|
615
616
|
|
|
616
617
|
// src/engine/client/http.ts
|
|
617
618
|
var CONTENT_POLICY_CODE = "content_policy_blocked";
|
|
@@ -660,17 +661,17 @@ var HttpClient = class {
|
|
|
660
661
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
661
662
|
this.sleepFn = opts.sleepFn ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
662
663
|
}
|
|
663
|
-
async postJson(
|
|
664
|
-
return await this.requestJson("POST",
|
|
664
|
+
async postJson(path18, body, signal) {
|
|
665
|
+
return await this.requestJson("POST", path18, body, signal);
|
|
665
666
|
}
|
|
666
|
-
async putJson(
|
|
667
|
-
return await this.requestJson("PUT",
|
|
667
|
+
async putJson(path18, body, signal) {
|
|
668
|
+
return await this.requestJson("PUT", path18, body, signal);
|
|
668
669
|
}
|
|
669
|
-
async getJson(
|
|
670
|
-
return await this.requestJson("GET",
|
|
670
|
+
async getJson(path18, signal) {
|
|
671
|
+
return await this.requestJson("GET", path18, void 0, signal);
|
|
671
672
|
}
|
|
672
|
-
async requestJson(method,
|
|
673
|
-
const url = `${this.baseUrl}${
|
|
673
|
+
async requestJson(method, path18, body, signal) {
|
|
674
|
+
const url = `${this.baseUrl}${path18.startsWith("/") ? path18 : `/${path18}`}`;
|
|
674
675
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
675
676
|
const outcome = await this.attempt(method, url, body, attempt, signal);
|
|
676
677
|
if (outcome.kind === "value") return outcome.value;
|
|
@@ -833,12 +834,12 @@ var BackendClient = class {
|
|
|
833
834
|
}
|
|
834
835
|
async pollJob(jobId, signal) {
|
|
835
836
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
836
|
-
const
|
|
837
|
+
const path18 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
837
838
|
for (let attempt = 0; ; attempt++) {
|
|
838
839
|
if (signal?.aborted) {
|
|
839
840
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
840
841
|
}
|
|
841
|
-
const job = await this.http.getJson(
|
|
842
|
+
const job = await this.http.getJson(path18, signal);
|
|
842
843
|
if (job.status === "completed") return job.result;
|
|
843
844
|
if (job.status === "failed") throw failedJobError(job.error);
|
|
844
845
|
if (Date.now() > deadline) {
|
|
@@ -917,8 +918,8 @@ var BackendClient = class {
|
|
|
917
918
|
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
918
919
|
}
|
|
919
920
|
getArtifact(kind, name, version, signal) {
|
|
920
|
-
const
|
|
921
|
-
return this.http.getJson(
|
|
921
|
+
const path18 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
922
|
+
return this.http.getJson(path18, signal);
|
|
922
923
|
}
|
|
923
924
|
};
|
|
924
925
|
|
|
@@ -4288,9 +4289,9 @@ function checkOutputRef(ctx) {
|
|
|
4288
4289
|
function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
4289
4290
|
for (const issue of err.issues) {
|
|
4290
4291
|
const tail2 = pathToString(issue.path);
|
|
4291
|
-
const
|
|
4292
|
+
const path18 = pathPrefix ? tail2 ? `${pathPrefix}.${tail2}` : pathPrefix : tail2;
|
|
4292
4293
|
issues.push({
|
|
4293
|
-
path:
|
|
4294
|
+
path: path18,
|
|
4294
4295
|
code,
|
|
4295
4296
|
message: issue.message,
|
|
4296
4297
|
received: issue.code === "invalid_type" ? issue.received : void 0,
|
|
@@ -4299,8 +4300,8 @@ function pushZodIssues(issues, err, pathPrefix, code, nodeId, nodeType) {
|
|
|
4299
4300
|
});
|
|
4300
4301
|
}
|
|
4301
4302
|
}
|
|
4302
|
-
function pathToString(
|
|
4303
|
-
return
|
|
4303
|
+
function pathToString(path18) {
|
|
4304
|
+
return path18.map((p) => typeof p === "number" ? `[${p}]` : `.${String(p)}`).join("").replace(/^\./, "");
|
|
4304
4305
|
}
|
|
4305
4306
|
function buildDepGraph(canvas) {
|
|
4306
4307
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -4909,10 +4910,10 @@ var NodeRegistry = class {
|
|
|
4909
4910
|
};
|
|
4910
4911
|
|
|
4911
4912
|
// src/engine/nodes/ingest.ts
|
|
4912
|
-
import { execFile as execFileCb
|
|
4913
|
-
import { mkdtemp,
|
|
4913
|
+
import { execFile as execFileCb } from "child_process";
|
|
4914
|
+
import { mkdtemp, readFile as readFile4, rm, stat as stat2 } from "fs/promises";
|
|
4914
4915
|
import { tmpdir } from "os";
|
|
4915
|
-
import
|
|
4916
|
+
import path5 from "path";
|
|
4916
4917
|
import { promisify } from "util";
|
|
4917
4918
|
import { z as z5 } from "zod";
|
|
4918
4919
|
|
|
@@ -4975,35 +4976,6 @@ function shouldSniffChallenge(contentType, contentLength, maxSniffBytes) {
|
|
|
4975
4976
|
return type.includes("text/") || type.includes("html") || type.includes("json") || type.includes("xml");
|
|
4976
4977
|
}
|
|
4977
4978
|
|
|
4978
|
-
// ../proxy/src/privateAddress.ts
|
|
4979
|
-
function isPrivateIpv4(ip) {
|
|
4980
|
-
const parts = ip.split(".");
|
|
4981
|
-
if (parts.length !== 4) return false;
|
|
4982
|
-
const octets = parts.map((part) => Number(part));
|
|
4983
|
-
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return false;
|
|
4984
|
-
const [a, b] = octets;
|
|
4985
|
-
if (a === 0) return true;
|
|
4986
|
-
if (a === 10) return true;
|
|
4987
|
-
if (a === 127) return true;
|
|
4988
|
-
if (a === 169 && b === 254) return true;
|
|
4989
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
4990
|
-
if (a === 192 && b === 168) return true;
|
|
4991
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
4992
|
-
if (a >= 224) return true;
|
|
4993
|
-
return false;
|
|
4994
|
-
}
|
|
4995
|
-
function isPrivateAddress(ip) {
|
|
4996
|
-
const address = ip.trim().toLowerCase();
|
|
4997
|
-
if (!address) return true;
|
|
4998
|
-
const mapped = address.startsWith("::ffff:") ? address.slice("::ffff:".length) : null;
|
|
4999
|
-
if (mapped?.includes(".")) return isPrivateIpv4(mapped);
|
|
5000
|
-
if (address.includes(".")) return isPrivateIpv4(address);
|
|
5001
|
-
if (address === "::" || address === "::1") return true;
|
|
5002
|
-
if (/^f[cd][0-9a-f]{2}:/.test(address)) return true;
|
|
5003
|
-
if (/^fe[89ab][0-9a-f]:/.test(address)) return true;
|
|
5004
|
-
return false;
|
|
5005
|
-
}
|
|
5006
|
-
|
|
5007
4979
|
// ../proxy/src/fetch.ts
|
|
5008
4980
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
5009
4981
|
var MAX_SNIFF_BYTES = 64 * 1024;
|
|
@@ -5037,6 +5009,39 @@ function directAgent() {
|
|
|
5037
5009
|
});
|
|
5038
5010
|
}
|
|
5039
5011
|
var PRIVATE_ADDRESS_CODE = "EPRIVATEADDR";
|
|
5012
|
+
var MAX_REDIRECTS = 20;
|
|
5013
|
+
var REDIRECT_STATUS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
5014
|
+
function originOf(url) {
|
|
5015
|
+
try {
|
|
5016
|
+
return new URL(url).origin;
|
|
5017
|
+
} catch {
|
|
5018
|
+
return null;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
function refuseRedirect(entry, to) {
|
|
5022
|
+
if (refuseNonPublicUrl(to) === null) return false;
|
|
5023
|
+
const destination = originOf(to);
|
|
5024
|
+
return destination === null || destination !== originOf(entry);
|
|
5025
|
+
}
|
|
5026
|
+
async function followRedirects(url, init) {
|
|
5027
|
+
let current = url;
|
|
5028
|
+
let request = { ...init, redirect: "manual" };
|
|
5029
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
5030
|
+
const response = await undiciFetch(current, request);
|
|
5031
|
+
const location = response.headers.get("location");
|
|
5032
|
+
if (!REDIRECT_STATUS.has(response.status) || location === null) return response;
|
|
5033
|
+
await response.body?.cancel();
|
|
5034
|
+
const next = new URL(location, current).toString();
|
|
5035
|
+
if (refuseRedirect(url, next)) {
|
|
5036
|
+
throw new Error(`${PRIVATE_ADDRESS_CODE}: redirected to ${next}`);
|
|
5037
|
+
}
|
|
5038
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
5039
|
+
const downgrade = response.status === 303 || (response.status === 301 || response.status === 302) && method === "POST";
|
|
5040
|
+
request = downgrade ? { ...request, method: "GET", body: void 0 } : request;
|
|
5041
|
+
current = next;
|
|
5042
|
+
}
|
|
5043
|
+
throw new Error(`too many redirects following ${url}`);
|
|
5044
|
+
}
|
|
5040
5045
|
var NULL_BODY_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
5041
5046
|
function rebuild(response, body) {
|
|
5042
5047
|
if (response.status < 200 || response.status > 599 || NULL_BODY_STATUS.has(response.status)) {
|
|
@@ -5072,7 +5077,7 @@ async function attemptRoute(url, init, route, options) {
|
|
|
5072
5077
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5073
5078
|
const signal = init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
|
|
5074
5079
|
try {
|
|
5075
|
-
const raw = await
|
|
5080
|
+
const raw = await followRedirects(url, { ...init, dispatcher: agent ?? void 0, signal });
|
|
5076
5081
|
const read = await readOnce(raw, options.sniffChallenge ?? true, options.maxBytes);
|
|
5077
5082
|
const outcome = classifyFetchOutcome({ status: raw.status, challenge: read.challenge });
|
|
5078
5083
|
return {
|
|
@@ -5151,6 +5156,7 @@ var BROWSER_HEADERS = {
|
|
|
5151
5156
|
"Accept-Language": "en-US,en;q=0.9"
|
|
5152
5157
|
};
|
|
5153
5158
|
var NOT_PUBLIC_MESSAGE = "That address is not reachable from the public web, so there is nothing to read.";
|
|
5159
|
+
var MAX_REMOTE_IMAGE_BYTES = 25e6;
|
|
5154
5160
|
var ExternalFetchError = class extends Error {
|
|
5155
5161
|
/** True when the fault was ours — a route we could not open, not a dead host. */
|
|
5156
5162
|
ourFault;
|
|
@@ -5190,7 +5196,22 @@ async function fetchExternal(url, options = {}) {
|
|
|
5190
5196
|
}
|
|
5191
5197
|
return { response: result.response, blocked: endedBlocked(result) };
|
|
5192
5198
|
}
|
|
5193
|
-
|
|
5199
|
+
function refuseOversize(bytes, maxBytes) {
|
|
5200
|
+
if (bytes === null || bytes <= maxBytes) return null;
|
|
5201
|
+
return `That file is ${humanBytes(bytes)} \u2014 the limit is ${humanBytes(maxBytes)}.`;
|
|
5202
|
+
}
|
|
5203
|
+
function humanBytes(n) {
|
|
5204
|
+
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(1)} GB`;
|
|
5205
|
+
if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
|
|
5206
|
+
return `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
5207
|
+
}
|
|
5208
|
+
function declaredBytes(response) {
|
|
5209
|
+
const raw = response.headers.get("content-length");
|
|
5210
|
+
if (raw === null) return null;
|
|
5211
|
+
const value = Number(raw);
|
|
5212
|
+
return Number.isFinite(value) ? value : null;
|
|
5213
|
+
}
|
|
5214
|
+
async function fetchExternalBytes(url, options) {
|
|
5194
5215
|
const { response, blocked } = await fetchExternal(url, options);
|
|
5195
5216
|
if (!response.ok) {
|
|
5196
5217
|
throw new ExternalFetchError(`${response.status} ${response.statusText}`, false);
|
|
@@ -5198,11 +5219,162 @@ async function fetchExternalBytes(url, options = {}) {
|
|
|
5198
5219
|
if (blocked) {
|
|
5199
5220
|
throw new ExternalFetchError("That host served a security check instead of the file.", false);
|
|
5200
5221
|
}
|
|
5222
|
+
const declared = refuseOversize(declaredBytes(response), options.maxBytes);
|
|
5223
|
+
if (declared !== null) {
|
|
5224
|
+
throw new ExternalFetchError(declared, false);
|
|
5225
|
+
}
|
|
5226
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
5227
|
+
const actual = refuseOversize(buffer.byteLength, options.maxBytes);
|
|
5228
|
+
if (actual !== null) {
|
|
5229
|
+
throw new ExternalFetchError(actual, false);
|
|
5230
|
+
}
|
|
5231
|
+
return { buffer, contentType: response.headers.get("content-type") };
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
// src/lib/ytDlp.ts
|
|
5235
|
+
import { spawn } from "child_process";
|
|
5236
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
5237
|
+
import path4 from "path";
|
|
5238
|
+
|
|
5239
|
+
// src/lib/ytDlpSignal.ts
|
|
5240
|
+
var HTTP_ERROR = /HTTP Error (\d{3})/i;
|
|
5241
|
+
var CHALLENGE_PHRASES = [
|
|
5242
|
+
"sign in to confirm you're not a bot",
|
|
5243
|
+
"sign in to confirm you\u2019re not a bot",
|
|
5244
|
+
"confirm you are not a bot",
|
|
5245
|
+
"unusual traffic",
|
|
5246
|
+
"captcha"
|
|
5247
|
+
];
|
|
5248
|
+
var NOT_A_BLOCK_PHRASES = ["private video", "video unavailable", "members-only", "this video is available to"];
|
|
5249
|
+
function ytDlpBlockSignal(stderr) {
|
|
5250
|
+
const text = stderr.toLowerCase();
|
|
5251
|
+
if (NOT_A_BLOCK_PHRASES.some((phrase) => text.includes(phrase))) return {};
|
|
5252
|
+
const status = HTTP_ERROR.exec(stderr);
|
|
5253
|
+
if (status) return { status: Number(status[1]) };
|
|
5254
|
+
if (CHALLENGE_PHRASES.some((phrase) => text.includes(phrase))) return { challenge: true };
|
|
5255
|
+
return {};
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
// src/lib/ytDlp.ts
|
|
5259
|
+
var YT_DLP_BIN = "yt-dlp";
|
|
5260
|
+
var YT_DLP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5261
|
+
var YtDlpError = class extends Error {
|
|
5262
|
+
constructor(message2, stderrTail) {
|
|
5263
|
+
super(message2);
|
|
5264
|
+
this.stderrTail = stderrTail;
|
|
5265
|
+
this.name = "YtDlpError";
|
|
5266
|
+
}
|
|
5267
|
+
stderrTail;
|
|
5268
|
+
};
|
|
5269
|
+
function tail(text, maxLines) {
|
|
5270
|
+
return text.split("\n").slice(-maxLines).join("\n");
|
|
5271
|
+
}
|
|
5272
|
+
async function runYtDlp(args) {
|
|
5273
|
+
const outTemplate = path4.join(args.workDir, "out.%(ext)s");
|
|
5274
|
+
const infoPath = path4.join(args.workDir, "out.info.json");
|
|
5275
|
+
const argv = [
|
|
5276
|
+
args.url,
|
|
5277
|
+
"--no-playlist",
|
|
5278
|
+
"--no-progress",
|
|
5279
|
+
"--no-warnings",
|
|
5280
|
+
"--quiet",
|
|
5281
|
+
"--write-info-json",
|
|
5282
|
+
"-o",
|
|
5283
|
+
outTemplate
|
|
5284
|
+
];
|
|
5285
|
+
if (args.audioOnly) {
|
|
5286
|
+
argv.push("-x", "--audio-format", "m4a");
|
|
5287
|
+
} else {
|
|
5288
|
+
const format = args.maxHeight ? `bv*[height<=${args.maxHeight}]+ba/b[height<=${args.maxHeight}]/bv*+ba/b` : "bv*+ba/b";
|
|
5289
|
+
argv.push("--format", format, "--merge-output-format", "mp4");
|
|
5290
|
+
}
|
|
5291
|
+
args.onLog?.(`yt-dlp: downloading ${args.audioOnly ? "audio" : "video"}`);
|
|
5292
|
+
const { code, stderr } = await runViaLadder({
|
|
5293
|
+
url: args.url,
|
|
5294
|
+
argv,
|
|
5295
|
+
timeoutMs: args.timeoutMs ?? YT_DLP_TIMEOUT_MS,
|
|
5296
|
+
onLog: args.onLog
|
|
5297
|
+
});
|
|
5298
|
+
if (code !== 0) {
|
|
5299
|
+
throw new YtDlpError(`yt-dlp exited ${code}`, tail(stderr, 40));
|
|
5300
|
+
}
|
|
5301
|
+
const files = await readdir(args.workDir);
|
|
5302
|
+
const wanted = args.audioOnly ? ".m4a" : ".mp4";
|
|
5303
|
+
const downloaded = files.find((f) => f.startsWith("out.") && f.endsWith(wanted));
|
|
5304
|
+
if (!downloaded) {
|
|
5305
|
+
throw new YtDlpError(`yt-dlp finished but produced no ${wanted} file (saw: ${files.join(", ")})`, "");
|
|
5306
|
+
}
|
|
5307
|
+
let info = {};
|
|
5308
|
+
try {
|
|
5309
|
+
info = JSON.parse(await readFile3(infoPath, "utf-8"));
|
|
5310
|
+
} catch {
|
|
5311
|
+
}
|
|
5312
|
+
return { filePath: path4.join(args.workDir, downloaded), info };
|
|
5313
|
+
}
|
|
5314
|
+
async function walkYtDlpLadder(args) {
|
|
5315
|
+
const routes = plannedRoutes(args.url, args.credentials);
|
|
5316
|
+
let last = { code: -1, stderr: "" };
|
|
5317
|
+
for (const route of routes) {
|
|
5318
|
+
if (route.kind === "proxy") args.onLog?.(`yt-dlp: retrying via ${route.tier}`);
|
|
5319
|
+
last = await args.attempt(route);
|
|
5320
|
+
if (last.code === 0) return last;
|
|
5321
|
+
if (!shouldEscalate(ytDlpBlockSignal(last.stderr))) return last;
|
|
5322
|
+
}
|
|
5323
|
+
return last;
|
|
5324
|
+
}
|
|
5325
|
+
function runViaLadder(args) {
|
|
5326
|
+
return walkYtDlpLadder({
|
|
5327
|
+
url: args.url,
|
|
5328
|
+
credentials: captureProxyCredentials(),
|
|
5329
|
+
attempt: (route) => spawnAndWait(YT_DLP_BIN, args.argv, args.timeoutMs, proxyEnvFor(route)),
|
|
5330
|
+
onLog: args.onLog
|
|
5331
|
+
});
|
|
5332
|
+
}
|
|
5333
|
+
function proxyEnvFor(route) {
|
|
5334
|
+
if (route.kind !== "proxy") return void 0;
|
|
5335
|
+
const uri = new URL(route.server);
|
|
5336
|
+
uri.username = encodeURIComponent(route.username);
|
|
5337
|
+
uri.password = encodeURIComponent(route.password);
|
|
5201
5338
|
return {
|
|
5202
|
-
|
|
5203
|
-
|
|
5339
|
+
HTTP_PROXY: uri.toString(),
|
|
5340
|
+
HTTPS_PROXY: uri.toString(),
|
|
5341
|
+
http_proxy: uri.toString(),
|
|
5342
|
+
https_proxy: uri.toString()
|
|
5204
5343
|
};
|
|
5205
5344
|
}
|
|
5345
|
+
function spawnAndWait(bin, argv, timeoutMs, extraEnv) {
|
|
5346
|
+
return new Promise((resolve, reject) => {
|
|
5347
|
+
const child = spawn(bin, argv, {
|
|
5348
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5349
|
+
// `childEnvWith` because `spawn`'s `env` REPLACES the environment rather
|
|
5350
|
+
// than extending it — passing only the additions would strip PATH.
|
|
5351
|
+
...extraEnv ? { env: childEnvWith(extraEnv) } : {}
|
|
5352
|
+
});
|
|
5353
|
+
const stderrChunks = [];
|
|
5354
|
+
let stderrLen = 0;
|
|
5355
|
+
const MAX_STDERR = 256 * 1024;
|
|
5356
|
+
child.stdout.on("data", () => {
|
|
5357
|
+
});
|
|
5358
|
+
child.stderr.on("data", (chunk) => {
|
|
5359
|
+
if (stderrLen < MAX_STDERR) {
|
|
5360
|
+
stderrChunks.push(chunk);
|
|
5361
|
+
stderrLen += chunk.length;
|
|
5362
|
+
}
|
|
5363
|
+
});
|
|
5364
|
+
const timer = setTimeout(() => {
|
|
5365
|
+
child.kill("SIGKILL");
|
|
5366
|
+
reject(new Error(`${bin} timed out after ${timeoutMs}ms`));
|
|
5367
|
+
}, timeoutMs);
|
|
5368
|
+
child.on("error", (err) => {
|
|
5369
|
+
clearTimeout(timer);
|
|
5370
|
+
reject(new Error(`failed to spawn "${bin}" \u2014 ${err.message}`));
|
|
5371
|
+
});
|
|
5372
|
+
child.on("close", (code) => {
|
|
5373
|
+
clearTimeout(timer);
|
|
5374
|
+
resolve({ code: code ?? -1, stderr: Buffer.concat(stderrChunks).toString("utf-8") });
|
|
5375
|
+
});
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5206
5378
|
|
|
5207
5379
|
// src/engine/schema/refs.ts
|
|
5208
5380
|
import { z as z4 } from "zod";
|
|
@@ -5399,25 +5571,6 @@ function mapClientError(ctx, e) {
|
|
|
5399
5571
|
return new NodeExecutionError(ctx.nodeId, ctx.nodeType, { kind: "local", cause: e });
|
|
5400
5572
|
}
|
|
5401
5573
|
|
|
5402
|
-
// src/engine/nodes/ytDlpSignal.ts
|
|
5403
|
-
var HTTP_ERROR = /HTTP Error (\d{3})/i;
|
|
5404
|
-
var CHALLENGE_PHRASES = [
|
|
5405
|
-
"sign in to confirm you're not a bot",
|
|
5406
|
-
"sign in to confirm you\u2019re not a bot",
|
|
5407
|
-
"confirm you are not a bot",
|
|
5408
|
-
"unusual traffic",
|
|
5409
|
-
"captcha"
|
|
5410
|
-
];
|
|
5411
|
-
var NOT_A_BLOCK_PHRASES = ["private video", "video unavailable", "members-only", "this video is available to"];
|
|
5412
|
-
function ytDlpBlockSignal(stderr) {
|
|
5413
|
-
const text = stderr.toLowerCase();
|
|
5414
|
-
if (NOT_A_BLOCK_PHRASES.some((phrase) => text.includes(phrase))) return {};
|
|
5415
|
-
const status = HTTP_ERROR.exec(stderr);
|
|
5416
|
-
if (status) return { status: Number(status[1]) };
|
|
5417
|
-
if (CHALLENGE_PHRASES.some((phrase) => text.includes(phrase))) return { challenge: true };
|
|
5418
|
-
return {};
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
5574
|
// src/engine/nodes/ingest.ts
|
|
5422
5575
|
var execFile = promisify(execFileCb);
|
|
5423
5576
|
var ExpectEnum = z5.enum(["image", "video", "audio", "text", "json", "font"]);
|
|
@@ -5541,9 +5694,6 @@ async function ingestImageUrl(url, ctx) {
|
|
|
5541
5694
|
} catch (e) {
|
|
5542
5695
|
throw localExecError(ctx, `fetch ${url} \u2192 ${e.message}`);
|
|
5543
5696
|
}
|
|
5544
|
-
if (bytes.byteLength > MAX_ASSET_BYTES) {
|
|
5545
|
-
throw localExecError(ctx, `file_too_large: ${url} is ${bytes.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
|
|
5546
|
-
}
|
|
5547
5697
|
let normalized;
|
|
5548
5698
|
try {
|
|
5549
5699
|
normalized = await toModelSafeImage(bytes);
|
|
@@ -5593,8 +5743,8 @@ function resolveLocalPath(input) {
|
|
|
5593
5743
|
`ingest: ~ path expansion is not supported (got "${input}"). Use an absolute path or a cwd-relative path.`
|
|
5594
5744
|
);
|
|
5595
5745
|
}
|
|
5596
|
-
if (
|
|
5597
|
-
return
|
|
5746
|
+
if (path5.isAbsolute(input)) return input;
|
|
5747
|
+
return path5.resolve(process.cwd(), input);
|
|
5598
5748
|
}
|
|
5599
5749
|
var EXT_TO_MIME = {
|
|
5600
5750
|
png: "image/png",
|
|
@@ -5642,7 +5792,7 @@ function sniffSvg(buf) {
|
|
|
5642
5792
|
return head.startsWith("<?xml") ? head.includes("<svg") : head.startsWith("<svg");
|
|
5643
5793
|
}
|
|
5644
5794
|
function inferMimeFromPath(absPath, sniffBytes) {
|
|
5645
|
-
const ext =
|
|
5795
|
+
const ext = path5.extname(absPath).slice(1).toLowerCase();
|
|
5646
5796
|
const fromExt = EXT_TO_MIME[ext];
|
|
5647
5797
|
if (fromExt) return fromExt;
|
|
5648
5798
|
const fromBytes = sniffImageMime(sniffBytes);
|
|
@@ -5781,7 +5931,7 @@ async function execLocalFile(params, ctx) {
|
|
|
5781
5931
|
}
|
|
5782
5932
|
let bytes;
|
|
5783
5933
|
try {
|
|
5784
|
-
bytes = await
|
|
5934
|
+
bytes = await readFile4(absPath);
|
|
5785
5935
|
} catch (e) {
|
|
5786
5936
|
if (e.code === "EACCES") {
|
|
5787
5937
|
throw localExecError(ctx, `permission_denied: ${absPath}`);
|
|
@@ -5832,7 +5982,7 @@ function localFileMetadata(args) {
|
|
|
5832
5982
|
strategy: "local_file",
|
|
5833
5983
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5834
5984
|
file_size: args.fileSize,
|
|
5835
|
-
original_filename:
|
|
5985
|
+
original_filename: path5.basename(args.absPath),
|
|
5836
5986
|
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
5837
5987
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
5838
5988
|
};
|
|
@@ -5841,8 +5991,6 @@ function withProbedDuration(ref, durationMs) {
|
|
|
5841
5991
|
if (durationMs === void 0 || ref.kind !== "video") return ref;
|
|
5842
5992
|
return { ...ref, duration_ms: durationMs };
|
|
5843
5993
|
}
|
|
5844
|
-
var YT_DLP_BIN = "yt-dlp";
|
|
5845
|
-
var YT_DLP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5846
5994
|
var YT_DLP_MIME = {
|
|
5847
5995
|
image: "image/png",
|
|
5848
5996
|
video: "video/mp4",
|
|
@@ -5856,9 +6004,9 @@ async function execYtDlp(params, ctx) {
|
|
|
5856
6004
|
if (params.expect !== "video" && params.expect !== "audio") {
|
|
5857
6005
|
throw new Error(`ingest: yt_dlp only handles video/audio, got expect=${params.expect}`);
|
|
5858
6006
|
}
|
|
5859
|
-
const workDir = await mkdtemp(
|
|
6007
|
+
const workDir = await mkdtemp(path5.join(tmpdir(), "ingest-yt-"));
|
|
5860
6008
|
try {
|
|
5861
|
-
const { filePath, info } = await
|
|
6009
|
+
const { filePath, info } = await runYtDlpForNode({
|
|
5862
6010
|
url: params.url,
|
|
5863
6011
|
audioOnly: params.expect === "audio",
|
|
5864
6012
|
workDir,
|
|
@@ -5871,7 +6019,7 @@ async function execYtDlp(params, ctx) {
|
|
|
5871
6019
|
`file_too_large: yt-dlp output for ${params.url} is ${downloadedStats.size} bytes (limit ${MAX_ASSET_BYTES})`
|
|
5872
6020
|
);
|
|
5873
6021
|
}
|
|
5874
|
-
const bytes = await
|
|
6022
|
+
const bytes = await readFile4(filePath);
|
|
5875
6023
|
const kind = params.expect;
|
|
5876
6024
|
const mime = YT_DLP_MIME[kind];
|
|
5877
6025
|
const metadata = buildYtDlpMetadata(params.url, info);
|
|
@@ -5882,69 +6030,24 @@ async function execYtDlp(params, ctx) {
|
|
|
5882
6030
|
});
|
|
5883
6031
|
}
|
|
5884
6032
|
}
|
|
5885
|
-
async function
|
|
5886
|
-
const routes = plannedRoutes(url, captureProxyCredentials());
|
|
5887
|
-
let last = { code: -1, stderr: "" };
|
|
5888
|
-
for (const route of routes) {
|
|
5889
|
-
if (route.kind === "proxy") ctx.log(`ingest: retrying yt-dlp via ${route.tier}`);
|
|
5890
|
-
last = await spawnAndWait(YT_DLP_BIN, argv, YT_DLP_TIMEOUT_MS, proxyEnvFor(route));
|
|
5891
|
-
if (last.code === 0) return last;
|
|
5892
|
-
if (!shouldEscalate(ytDlpBlockSignal(last.stderr))) return last;
|
|
5893
|
-
}
|
|
5894
|
-
return last;
|
|
5895
|
-
}
|
|
5896
|
-
function proxyEnvFor(route) {
|
|
5897
|
-
if (route.kind !== "proxy") return void 0;
|
|
5898
|
-
const uri = new URL(route.server);
|
|
5899
|
-
uri.username = encodeURIComponent(route.username);
|
|
5900
|
-
uri.password = encodeURIComponent(route.password);
|
|
5901
|
-
return {
|
|
5902
|
-
HTTP_PROXY: uri.toString(),
|
|
5903
|
-
HTTPS_PROXY: uri.toString(),
|
|
5904
|
-
http_proxy: uri.toString(),
|
|
5905
|
-
https_proxy: uri.toString()
|
|
5906
|
-
};
|
|
5907
|
-
}
|
|
5908
|
-
async function runYtDlp(args) {
|
|
5909
|
-
const outTemplate = path4.join(args.workDir, "out.%(ext)s");
|
|
5910
|
-
const infoPath = path4.join(args.workDir, "out.info.json");
|
|
5911
|
-
const argv = [
|
|
5912
|
-
args.url,
|
|
5913
|
-
"--no-playlist",
|
|
5914
|
-
"--no-progress",
|
|
5915
|
-
"--no-warnings",
|
|
5916
|
-
"--quiet",
|
|
5917
|
-
"--write-info-json",
|
|
5918
|
-
"-o",
|
|
5919
|
-
outTemplate
|
|
5920
|
-
];
|
|
5921
|
-
if (args.audioOnly) {
|
|
5922
|
-
argv.push("-x", "--audio-format", "m4a");
|
|
5923
|
-
} else {
|
|
5924
|
-
argv.push("--format", "bv*+ba/b", "--merge-output-format", "mp4");
|
|
5925
|
-
}
|
|
5926
|
-
args.ctx.log(`ingest: spawning yt-dlp (${args.audioOnly ? "audio" : "video"})`);
|
|
5927
|
-
const { code, stderr } = await runYtDlpViaLadder(args.url, argv, args.ctx);
|
|
5928
|
-
if (code !== 0) {
|
|
5929
|
-
throw new NodeExecutionError(args.ctx.nodeId, args.ctx.nodeType, {
|
|
5930
|
-
kind: "local",
|
|
5931
|
-
cause: new Error(`yt-dlp exited ${code}
|
|
5932
|
-
${tail(stderr, 40)}`)
|
|
5933
|
-
});
|
|
5934
|
-
}
|
|
5935
|
-
const files = await readdir(args.workDir);
|
|
5936
|
-
const wanted = args.audioOnly ? ".m4a" : ".mp4";
|
|
5937
|
-
const downloaded = files.find((f) => f.startsWith("out.") && f.endsWith(wanted));
|
|
5938
|
-
if (!downloaded) {
|
|
5939
|
-
throw new Error(`ingest: yt-dlp finished but no ${wanted} file in ${args.workDir} (saw: ${files.join(", ")})`);
|
|
5940
|
-
}
|
|
5941
|
-
let info = {};
|
|
6033
|
+
async function runYtDlpForNode(args) {
|
|
5942
6034
|
try {
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
6035
|
+
return await runYtDlp({
|
|
6036
|
+
url: args.url,
|
|
6037
|
+
audioOnly: args.audioOnly,
|
|
6038
|
+
workDir: args.workDir,
|
|
6039
|
+
onLog: (message2) => args.ctx.log(`ingest: ${message2}`)
|
|
6040
|
+
});
|
|
6041
|
+
} catch (error) {
|
|
6042
|
+
if (error instanceof YtDlpError) {
|
|
6043
|
+
throw new NodeExecutionError(args.ctx.nodeId, args.ctx.nodeType, {
|
|
6044
|
+
kind: "local",
|
|
6045
|
+
cause: new Error(error.stderrTail ? `${error.message}
|
|
6046
|
+
${error.stderrTail}` : error.message)
|
|
6047
|
+
});
|
|
6048
|
+
}
|
|
6049
|
+
throw error;
|
|
5946
6050
|
}
|
|
5947
|
-
return { filePath: path4.join(args.workDir, downloaded), info };
|
|
5948
6051
|
}
|
|
5949
6052
|
function buildYtDlpMetadata(sourceUrl, info) {
|
|
5950
6053
|
const out = {
|
|
@@ -5971,41 +6074,6 @@ async function uploadAndIngest(args) {
|
|
|
5971
6074
|
});
|
|
5972
6075
|
return { ...localRef, url: publicUrl };
|
|
5973
6076
|
}
|
|
5974
|
-
function spawnAndWait(bin, argv, timeoutMs, env) {
|
|
5975
|
-
return new Promise((resolve, reject) => {
|
|
5976
|
-
const child = spawn(bin, argv, {
|
|
5977
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
5978
|
-
...env ? { env: childEnvWith(env) } : {}
|
|
5979
|
-
});
|
|
5980
|
-
const stderrChunks = [];
|
|
5981
|
-
let stderrLen = 0;
|
|
5982
|
-
const MAX_STDERR = 256 * 1024;
|
|
5983
|
-
child.stdout.on("data", () => {
|
|
5984
|
-
});
|
|
5985
|
-
child.stderr.on("data", (chunk) => {
|
|
5986
|
-
if (stderrLen < MAX_STDERR) {
|
|
5987
|
-
stderrChunks.push(chunk);
|
|
5988
|
-
stderrLen += chunk.length;
|
|
5989
|
-
}
|
|
5990
|
-
});
|
|
5991
|
-
const timer = setTimeout(() => {
|
|
5992
|
-
child.kill("SIGKILL");
|
|
5993
|
-
reject(new Error(`ingest: ${bin} timed out after ${timeoutMs}ms`));
|
|
5994
|
-
}, timeoutMs);
|
|
5995
|
-
child.on("error", (err) => {
|
|
5996
|
-
clearTimeout(timer);
|
|
5997
|
-
reject(new Error(`ingest: failed to spawn "${bin}" \u2014 ${err.message}`));
|
|
5998
|
-
});
|
|
5999
|
-
child.on("close", (code) => {
|
|
6000
|
-
clearTimeout(timer);
|
|
6001
|
-
resolve({ code: code ?? -1, stderr: Buffer.concat(stderrChunks).toString("utf8") });
|
|
6002
|
-
});
|
|
6003
|
-
});
|
|
6004
|
-
}
|
|
6005
|
-
function tail(text, maxLines) {
|
|
6006
|
-
const lines = text.split("\n");
|
|
6007
|
-
return lines.slice(-maxLines).join("\n");
|
|
6008
|
-
}
|
|
6009
6077
|
async function ytDlpVersion() {
|
|
6010
6078
|
try {
|
|
6011
6079
|
const { stdout } = await execFile(YT_DLP_BIN, ["--version"], { encoding: "utf-8", maxBuffer: 64 * 1024 });
|
|
@@ -6036,9 +6104,9 @@ import { z as z6 } from "zod";
|
|
|
6036
6104
|
|
|
6037
6105
|
// src/engine/nodes/local/lib/cli-runner.ts
|
|
6038
6106
|
import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
|
|
6039
|
-
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as
|
|
6107
|
+
import { copyFile as copyFile2, mkdtemp as mkdtemp2, readFile as readFile5, rm as rm2, stat as stat3 } from "fs/promises";
|
|
6040
6108
|
import { tmpdir as tmpdir2 } from "os";
|
|
6041
|
-
import
|
|
6109
|
+
import path6 from "path";
|
|
6042
6110
|
import { promisify as promisify2 } from "util";
|
|
6043
6111
|
var execFile2 = promisify2(execFileCb2);
|
|
6044
6112
|
var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
@@ -6065,7 +6133,7 @@ function mimeForExt(ext) {
|
|
|
6065
6133
|
}
|
|
6066
6134
|
function extForAssetRef(ref) {
|
|
6067
6135
|
if (ref.path) {
|
|
6068
|
-
const e =
|
|
6136
|
+
const e = path6.extname(ref.path);
|
|
6069
6137
|
if (e) return e;
|
|
6070
6138
|
}
|
|
6071
6139
|
const reverse = {
|
|
@@ -6087,14 +6155,14 @@ function planArrayInputSlot(tmpDir, slot, values, lookup, stagedInputs) {
|
|
|
6087
6155
|
const ref = values[i];
|
|
6088
6156
|
if (!ref) continue;
|
|
6089
6157
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot}[${i}] has no local path`);
|
|
6090
|
-
const dest =
|
|
6158
|
+
const dest = path6.join(tmpDir, `in_${slot}_${i}${extForAssetRef(ref)}`);
|
|
6091
6159
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
6092
6160
|
lookup.set(`in.${slot}.${i}`, dest);
|
|
6093
6161
|
}
|
|
6094
6162
|
}
|
|
6095
6163
|
function planSingleInputSlot(tmpDir, slot, ref, lookup, stagedInputs) {
|
|
6096
6164
|
if (!ref.path) throw new Error(`cli-runner: inputs.${slot} has no local path`);
|
|
6097
|
-
const dest =
|
|
6165
|
+
const dest = path6.join(tmpDir, `in_${slot}${extForAssetRef(ref)}`);
|
|
6098
6166
|
stagedInputs.push({ srcPath: ref.path, destPath: dest });
|
|
6099
6167
|
lookup.set(`in.${slot}`, dest);
|
|
6100
6168
|
}
|
|
@@ -6102,7 +6170,7 @@ function planOutputs(tmpDir, outputs, lookup) {
|
|
|
6102
6170
|
const outputPaths = [];
|
|
6103
6171
|
for (const [name, spec] of Object.entries(outputs)) {
|
|
6104
6172
|
const ext = spec.ext.startsWith(".") ? spec.ext : `.${spec.ext}`;
|
|
6105
|
-
const absPath =
|
|
6173
|
+
const absPath = path6.join(tmpDir, `out_${name}${ext}`);
|
|
6106
6174
|
outputPaths.push({ name, absPath, spec });
|
|
6107
6175
|
lookup.set(`out.${name}`, absPath);
|
|
6108
6176
|
}
|
|
@@ -6143,8 +6211,8 @@ function rejectRawPaths(substituted, original, stagingDir) {
|
|
|
6143
6211
|
throw new Error(`cli-runner: home-relative path "${original}" not allowed in args.`);
|
|
6144
6212
|
}
|
|
6145
6213
|
if (substituted.startsWith("/")) {
|
|
6146
|
-
const resolved =
|
|
6147
|
-
if (!resolved.startsWith(`${stagingDir}${
|
|
6214
|
+
const resolved = path6.resolve(substituted);
|
|
6215
|
+
if (!resolved.startsWith(`${stagingDir}${path6.sep}`) && resolved !== stagingDir) {
|
|
6148
6216
|
throw new Error(
|
|
6149
6217
|
`cli-runner: raw filesystem path "${original}" not allowed \u2014 declare an input slot and use {{in.<slot>}} instead.`
|
|
6150
6218
|
);
|
|
@@ -6194,7 +6262,7 @@ function tailLines(text, maxLines) {
|
|
|
6194
6262
|
}
|
|
6195
6263
|
async function runCli(opts) {
|
|
6196
6264
|
const { bin, args, inputs, outputs, ctx, timeoutMs = DEFAULT_TIMEOUT_MS2 } = opts;
|
|
6197
|
-
const tmpDir = await mkdtemp2(
|
|
6265
|
+
const tmpDir = await mkdtemp2(path6.join(tmpdir2(), `cli-${bin.replace(/[^a-z0-9]/gi, "")}-`));
|
|
6198
6266
|
try {
|
|
6199
6267
|
const { lookup, stagedInputs, outputPaths } = planPlaceholders(tmpDir, inputs, outputs);
|
|
6200
6268
|
await stageInputs(stagedInputs);
|
|
@@ -6216,7 +6284,7 @@ ${tailLines(stderr, 40)}`);
|
|
|
6216
6284
|
if (!s?.isFile() || s.size === 0) {
|
|
6217
6285
|
throw new Error(`cli-runner: declared output "${name}" missing or empty at ${absPath}`);
|
|
6218
6286
|
}
|
|
6219
|
-
const bytes = await
|
|
6287
|
+
const bytes = await readFile5(absPath);
|
|
6220
6288
|
const ref = await ctx.assets.ingestBytes({
|
|
6221
6289
|
bytes: Buffer.from(bytes),
|
|
6222
6290
|
kind: spec.kind,
|
|
@@ -6474,17 +6542,17 @@ var ffmpegNode = defineNode({
|
|
|
6474
6542
|
import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
6475
6543
|
import { createRequire } from "module";
|
|
6476
6544
|
import { tmpdir as tmpdir3 } from "os";
|
|
6477
|
-
import
|
|
6545
|
+
import path8 from "path";
|
|
6478
6546
|
import { z as z9 } from "zod";
|
|
6479
6547
|
|
|
6480
6548
|
// src/engine/nodes/local/lib/assets.ts
|
|
6481
|
-
import { copyFile as copyFile3, readFile as
|
|
6482
|
-
import
|
|
6549
|
+
import { copyFile as copyFile3, readFile as readFile6 } from "fs/promises";
|
|
6550
|
+
import path7 from "path";
|
|
6483
6551
|
async function stageAsset(ref, destDir, filename) {
|
|
6484
6552
|
if (!ref.path) {
|
|
6485
6553
|
throw new Error(`stageAsset: ref (${ref.kind}/${ref.mime}) has no local path`);
|
|
6486
6554
|
}
|
|
6487
|
-
const dest =
|
|
6555
|
+
const dest = path7.join(destDir, filename);
|
|
6488
6556
|
await copyFile3(ref.path, dest);
|
|
6489
6557
|
return dest;
|
|
6490
6558
|
}
|
|
@@ -6493,7 +6561,7 @@ async function refToUrl(ref) {
|
|
|
6493
6561
|
if (!ref.path) {
|
|
6494
6562
|
throw new Error("refToUrl: AssetRef has neither url nor path");
|
|
6495
6563
|
}
|
|
6496
|
-
const bytes = await
|
|
6564
|
+
const bytes = await readFile6(ref.path);
|
|
6497
6565
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
6498
6566
|
}
|
|
6499
6567
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
@@ -6567,11 +6635,11 @@ var fontSpecimenNode = defineNode({
|
|
|
6567
6635
|
outputKinds: { image: "image" },
|
|
6568
6636
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
6569
6637
|
async execute({ inputs, params, ctx }) {
|
|
6570
|
-
const tmp = await mkdtemp3(
|
|
6638
|
+
const tmp = await mkdtemp3(path8.join(tmpdir3(), "font-specimen-"));
|
|
6571
6639
|
try {
|
|
6572
6640
|
const fontFilename = `font.${extForMime(inputs.font.mime)}`;
|
|
6573
6641
|
await stageAsset(inputs.font, tmp, fontFilename);
|
|
6574
|
-
const entryPath =
|
|
6642
|
+
const entryPath = path8.join(tmp, "index.html");
|
|
6575
6643
|
await writeFile3(entryPath, buildSpecimenHtml(params, fontFilename), "utf-8");
|
|
6576
6644
|
ctx.log(`rendering specimen (${params.font_size}px, ${params.text.split("\n").length} lines)`);
|
|
6577
6645
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -6655,16 +6723,16 @@ var fontSpecimenNode = defineNode({
|
|
|
6655
6723
|
|
|
6656
6724
|
// src/engine/nodes/local/hyperframe.ts
|
|
6657
6725
|
import { execFile as execFile4 } from "child_process";
|
|
6658
|
-
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as
|
|
6726
|
+
import { copyFile as copyFile4, mkdtemp as mkdtemp4, readFile as readFile10, rm as rm4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
6659
6727
|
import { createRequire as createRequire2 } from "module";
|
|
6660
6728
|
import { cpus, tmpdir as tmpdir4 } from "os";
|
|
6661
|
-
import
|
|
6729
|
+
import path13 from "path";
|
|
6662
6730
|
import { promisify as promisify4 } from "util";
|
|
6663
6731
|
import { z as z11 } from "zod";
|
|
6664
6732
|
|
|
6665
6733
|
// src/engine/engine/composition-hash.ts
|
|
6666
|
-
import { readdir as readdir2, readFile as
|
|
6667
|
-
import
|
|
6734
|
+
import { readdir as readdir2, readFile as readFile7, stat as stat4 } from "fs/promises";
|
|
6735
|
+
import path9 from "path";
|
|
6668
6736
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".cache", ".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
6669
6737
|
function isSkippedName(name) {
|
|
6670
6738
|
if (name.startsWith(".")) return true;
|
|
@@ -6681,23 +6749,23 @@ async function collectFiles(root, current) {
|
|
|
6681
6749
|
const names = await readdir2(current);
|
|
6682
6750
|
for (const name of names) {
|
|
6683
6751
|
if (isSkippedName(name)) continue;
|
|
6684
|
-
const abs =
|
|
6752
|
+
const abs = path9.join(current, name);
|
|
6685
6753
|
const s = await stat4(abs);
|
|
6686
6754
|
if (s.isDirectory()) {
|
|
6687
6755
|
out.push(...await collectFiles(root, abs));
|
|
6688
6756
|
continue;
|
|
6689
6757
|
}
|
|
6690
6758
|
if (!s.isFile()) continue;
|
|
6691
|
-
const bytes = await
|
|
6692
|
-
const relPath =
|
|
6759
|
+
const bytes = await readFile7(abs);
|
|
6760
|
+
const relPath = path9.relative(root, abs).split(path9.sep).join("/");
|
|
6693
6761
|
out.push({ relPath, contentSha: sha256Hex(bytes) });
|
|
6694
6762
|
}
|
|
6695
6763
|
return out;
|
|
6696
6764
|
}
|
|
6697
6765
|
|
|
6698
6766
|
// src/engine/engine/composition-meta.ts
|
|
6699
|
-
import { readFile as
|
|
6700
|
-
import
|
|
6767
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
6768
|
+
import path10 from "path";
|
|
6701
6769
|
import { z as z10 } from "zod";
|
|
6702
6770
|
var InputKind = z10.enum(["video", "image", "audio", "json"]);
|
|
6703
6771
|
var InputSpec = z10.object({
|
|
@@ -6774,10 +6842,10 @@ var CompositionMetaSchema = z10.object({
|
|
|
6774
6842
|
params: z10.record(z10.string(), ParamSpec).default({})
|
|
6775
6843
|
}).strict();
|
|
6776
6844
|
async function loadCompositionMeta(compositionDir) {
|
|
6777
|
-
const metaPath =
|
|
6845
|
+
const metaPath = path10.join(compositionDir, "meta.json");
|
|
6778
6846
|
let raw;
|
|
6779
6847
|
try {
|
|
6780
|
-
raw = await
|
|
6848
|
+
raw = await readFile8(metaPath, "utf-8");
|
|
6781
6849
|
} catch (e) {
|
|
6782
6850
|
throw new Error(`composition meta: cannot read ${metaPath} (${e.message})`);
|
|
6783
6851
|
}
|
|
@@ -6855,8 +6923,8 @@ function defaultFilenameForInput(key, kind) {
|
|
|
6855
6923
|
|
|
6856
6924
|
// src/engine/nodes/local/lib/hyperframe-check.ts
|
|
6857
6925
|
import { execFile as execFile3 } from "child_process";
|
|
6858
|
-
import { readFile as
|
|
6859
|
-
import
|
|
6926
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
6927
|
+
import path11 from "path";
|
|
6860
6928
|
import { promisify as promisify3 } from "util";
|
|
6861
6929
|
var execFileAsync = promisify3(execFile3);
|
|
6862
6930
|
var NEVER_BLOCK = [
|
|
@@ -7008,7 +7076,7 @@ ${detail}`);
|
|
|
7008
7076
|
}
|
|
7009
7077
|
let indexHtml = "";
|
|
7010
7078
|
try {
|
|
7011
|
-
indexHtml = await
|
|
7079
|
+
indexHtml = await readFile9(path11.join(dir, "index.html"), "utf-8");
|
|
7012
7080
|
} catch {
|
|
7013
7081
|
indexHtml = "";
|
|
7014
7082
|
}
|
|
@@ -7073,9 +7141,9 @@ ${stderr.slice(0, 1500)}`;
|
|
|
7073
7141
|
|
|
7074
7142
|
// src/engine/nodes/local/lib/hyperframe-meta.ts
|
|
7075
7143
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
7076
|
-
import
|
|
7144
|
+
import path12 from "path";
|
|
7077
7145
|
async function ensureHyperframesMetaJson(tmp, nodeId, meta, duration) {
|
|
7078
|
-
const metaPath =
|
|
7146
|
+
const metaPath = path12.join(tmp, "meta.json");
|
|
7079
7147
|
await writeFile4(
|
|
7080
7148
|
metaPath,
|
|
7081
7149
|
JSON.stringify(
|
|
@@ -7175,7 +7243,7 @@ var hyperframeRenderNode = defineNode({
|
|
|
7175
7243
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
7176
7244
|
const meta = await loadCompositionMeta(compositionDir);
|
|
7177
7245
|
const compositionParams = validateAndParseDynamicParams(meta, params);
|
|
7178
|
-
const tmp = await mkdtemp4(
|
|
7246
|
+
const tmp = await mkdtemp4(path13.join(tmpdir4(), "hf-render-"));
|
|
7179
7247
|
try {
|
|
7180
7248
|
await copyComposition(compositionDir, tmp);
|
|
7181
7249
|
await vendorGsap(tmp, ctx);
|
|
@@ -7185,9 +7253,9 @@ var hyperframeRenderNode = defineNode({
|
|
|
7185
7253
|
await substituteCompositionFiles(tmp, substitutionValues);
|
|
7186
7254
|
await ensureHyperframesMetaJson(tmp, ctx.nodeId, meta, duration);
|
|
7187
7255
|
await runHyperframesCheck({ dir: tmp, nodeId: "hyperframe_render", ctx, timeoutMs: params.timeout_ms });
|
|
7188
|
-
const outputPath =
|
|
7256
|
+
const outputPath = path13.join(tmp, `output.${params.format}`);
|
|
7189
7257
|
await runRender({ tmp, outputPath, params, meta, ctx });
|
|
7190
|
-
const bytes = await
|
|
7258
|
+
const bytes = await readFile10(outputPath);
|
|
7191
7259
|
ctx.log(`rendered ${bytes.length} bytes`);
|
|
7192
7260
|
const ref = await ctx.assets.ingestBytes({
|
|
7193
7261
|
bytes: Buffer.from(bytes),
|
|
@@ -7209,10 +7277,10 @@ var hyperframeRenderNode = defineNode({
|
|
|
7209
7277
|
}
|
|
7210
7278
|
});
|
|
7211
7279
|
async function resolveCompositionDir(composition) {
|
|
7212
|
-
const compositionPath =
|
|
7280
|
+
const compositionPath = path13.isAbsolute(composition) ? composition : path13.resolve(process.cwd(), composition);
|
|
7213
7281
|
const s = await stat5(compositionPath);
|
|
7214
7282
|
if (s.isDirectory()) return compositionPath;
|
|
7215
|
-
return
|
|
7283
|
+
return path13.dirname(compositionPath);
|
|
7216
7284
|
}
|
|
7217
7285
|
async function validateComposition(rawParams) {
|
|
7218
7286
|
const issues = await validateCompositionParams(rawParams);
|
|
@@ -7294,7 +7362,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
7294
7362
|
await cp(srcDir, destDir, {
|
|
7295
7363
|
recursive: true,
|
|
7296
7364
|
filter: (src) => {
|
|
7297
|
-
const name =
|
|
7365
|
+
const name = path13.basename(src);
|
|
7298
7366
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
7299
7367
|
return true;
|
|
7300
7368
|
}
|
|
@@ -7303,7 +7371,7 @@ async function copyComposition(srcDir, destDir) {
|
|
|
7303
7371
|
async function vendorGsap(tmp, ctx) {
|
|
7304
7372
|
try {
|
|
7305
7373
|
const gsapMin = require_2.resolve("gsap/dist/gsap.min.js");
|
|
7306
|
-
await copyFile4(gsapMin,
|
|
7374
|
+
await copyFile4(gsapMin, path13.join(tmp, "gsap.min.js"));
|
|
7307
7375
|
} catch (e) {
|
|
7308
7376
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
7309
7377
|
}
|
|
@@ -7318,7 +7386,7 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
|
|
|
7318
7386
|
await stageAsset(ref, tmp, filename);
|
|
7319
7387
|
ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
|
|
7320
7388
|
if (spec.kind === "video" && primaryDuration === null) {
|
|
7321
|
-
primaryDuration = await probeDurationSeconds(
|
|
7389
|
+
primaryDuration = await probeDurationSeconds(path13.join(tmp, filename));
|
|
7322
7390
|
}
|
|
7323
7391
|
}
|
|
7324
7392
|
return primaryDuration;
|
|
@@ -7364,8 +7432,8 @@ function coerceImageParam(value) {
|
|
|
7364
7432
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
7365
7433
|
}
|
|
7366
7434
|
async function substituteCompositionFiles(tmp, values) {
|
|
7367
|
-
const entryPath =
|
|
7368
|
-
const original = await
|
|
7435
|
+
const entryPath = path13.join(tmp, "index.html");
|
|
7436
|
+
const original = await readFile10(entryPath, "utf-8");
|
|
7369
7437
|
const { output, missing } = substituteVariables(original, values);
|
|
7370
7438
|
if (missing.length > 0) {
|
|
7371
7439
|
throw new Error(
|
|
@@ -7381,7 +7449,7 @@ function workerCount() {
|
|
|
7381
7449
|
async function runRender(opts) {
|
|
7382
7450
|
const { tmp, outputPath, params, meta, ctx } = opts;
|
|
7383
7451
|
const args = buildRenderArgs(tmp, outputPath, meta, params.format);
|
|
7384
|
-
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${
|
|
7452
|
+
ctx.log(`rendering ${meta.width}x${meta.height}@${meta.fps}fps ${params.format} from ${path13.basename(tmp)}`);
|
|
7385
7453
|
try {
|
|
7386
7454
|
await execFileAsync2("npx", args, { timeout: params.timeout_ms, maxBuffer: 64 * 1024 * 1024 });
|
|
7387
7455
|
} catch (e) {
|
|
@@ -7425,10 +7493,10 @@ async function probeDurationSeconds(filePath) {
|
|
|
7425
7493
|
|
|
7426
7494
|
// src/engine/nodes/local/hyperframe-snapshot.ts
|
|
7427
7495
|
import { execFile as execFile5 } from "child_process";
|
|
7428
|
-
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as
|
|
7496
|
+
import { copyFile as copyFile5, mkdtemp as mkdtemp5, readFile as readFile11, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
7429
7497
|
import { createRequire as createRequire3 } from "module";
|
|
7430
7498
|
import { tmpdir as tmpdir5 } from "os";
|
|
7431
|
-
import
|
|
7499
|
+
import path14 from "path";
|
|
7432
7500
|
import { promisify as promisify5 } from "util";
|
|
7433
7501
|
import { z as z12 } from "zod";
|
|
7434
7502
|
var _execFileAsync = promisify5(execFile5);
|
|
@@ -7475,7 +7543,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
7475
7543
|
const compositionDir = await resolveCompositionDir(params.composition);
|
|
7476
7544
|
const meta = await loadCompositionMeta(compositionDir);
|
|
7477
7545
|
const compositionParams = validateAndParseDynamicParams2(meta, params);
|
|
7478
|
-
const tmp = await mkdtemp5(
|
|
7546
|
+
const tmp = await mkdtemp5(path14.join(tmpdir5(), "hf-snap-"));
|
|
7479
7547
|
try {
|
|
7480
7548
|
await copyComposition2(compositionDir, tmp);
|
|
7481
7549
|
await vendorGsap2(tmp, ctx);
|
|
@@ -7490,7 +7558,7 @@ var hyperframeSnapshotNode = defineNode({
|
|
|
7490
7558
|
timeoutMs: params.timeout_ms,
|
|
7491
7559
|
samples: 1
|
|
7492
7560
|
});
|
|
7493
|
-
const entryPath =
|
|
7561
|
+
const entryPath = path14.join(tmp, "index.html");
|
|
7494
7562
|
const entryUrl = `file://${entryPath}`;
|
|
7495
7563
|
ctx.log(`snapshotting ${meta.width}x${meta.height}@${DEVICE_SCALE_FACTOR2}x wait=${params.wait_for.kind}`);
|
|
7496
7564
|
const pwSpecifier = ["play", "wright"].join("");
|
|
@@ -7551,7 +7619,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
7551
7619
|
await cp(srcDir, destDir, {
|
|
7552
7620
|
recursive: true,
|
|
7553
7621
|
filter: (src) => {
|
|
7554
|
-
const name =
|
|
7622
|
+
const name = path14.basename(src);
|
|
7555
7623
|
if (name === ".cache" || name === "node_modules" || name === ".git") return false;
|
|
7556
7624
|
return true;
|
|
7557
7625
|
}
|
|
@@ -7560,7 +7628,7 @@ async function copyComposition2(srcDir, destDir) {
|
|
|
7560
7628
|
async function vendorGsap2(tmp, ctx) {
|
|
7561
7629
|
try {
|
|
7562
7630
|
const gsapMin = require_3.resolve("gsap/dist/gsap.min.js");
|
|
7563
|
-
await copyFile5(gsapMin,
|
|
7631
|
+
await copyFile5(gsapMin, path14.join(tmp, "gsap.min.js"));
|
|
7564
7632
|
} catch (e) {
|
|
7565
7633
|
ctx.log(`warning: could not vendor gsap.min.js (${e.message}); compositions must self-supply`);
|
|
7566
7634
|
}
|
|
@@ -7594,8 +7662,8 @@ function coerceImageParam2(value) {
|
|
|
7594
7662
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
7595
7663
|
}
|
|
7596
7664
|
async function substituteCompositionFiles2(tmp, values) {
|
|
7597
|
-
const entryPath =
|
|
7598
|
-
const original = await
|
|
7665
|
+
const entryPath = path14.join(tmp, "index.html");
|
|
7666
|
+
const original = await readFile11(entryPath, "utf-8");
|
|
7599
7667
|
const { output, missing } = substituteVariables(original, values);
|
|
7600
7668
|
if (missing.length > 0) {
|
|
7601
7669
|
throw new Error(
|
|
@@ -7893,20 +7961,50 @@ var imageDescribeNode = delegated({
|
|
|
7893
7961
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
7894
7962
|
});
|
|
7895
7963
|
|
|
7896
|
-
// src/engine/nodes/remote/
|
|
7964
|
+
// src/engine/nodes/remote/imageLayerize.ts
|
|
7897
7965
|
import { z as z21 } from "zod";
|
|
7966
|
+
var ImageLayerizeParams = z21.object({
|
|
7967
|
+
/**
|
|
7968
|
+
* What to separate. Read as extraction instructions, not as a generation
|
|
7969
|
+
* prompt — "keep the bottle and its shadow as one layer", not "make it blue".
|
|
7970
|
+
*/
|
|
7971
|
+
instructions: z21.string().max(600).optional()
|
|
7972
|
+
}).strict();
|
|
7973
|
+
var imageLayerizeNode = delegated({
|
|
7974
|
+
id: "image_layerize",
|
|
7975
|
+
version: "1.0.0",
|
|
7976
|
+
category: "image",
|
|
7977
|
+
summary: "Split an image into editable layers: a transparent PNG per element, plus any copy baked into the picture recovered as real text with its typography. Powered by Seedream 5.0 Pro Layerize, with Ideogram lifting the text off first.",
|
|
7978
|
+
when_to_use: "Use when you need ONE element of a picture on its own (a product with no background, a logo lockup), or when a shot is right but its copy is wrong and regenerating would change the picture too. The manifest gives you each layer's box and, for text, its exact wording, font, size and colour \u2014 enough to re-set a headline in the original's type. For a plain subject cutout with no text involved, `image_background_remove` is cheaper and one call.",
|
|
7979
|
+
inputs: z21.object({
|
|
7980
|
+
image: ImageRef
|
|
7981
|
+
}).strict(),
|
|
7982
|
+
params: ImageLayerizeParams,
|
|
7983
|
+
outputs: z21.object({
|
|
7984
|
+
layers: z21.array(ImageRef),
|
|
7985
|
+
base: ImageRef,
|
|
7986
|
+
manifest: JsonRef
|
|
7987
|
+
}).strict(),
|
|
7988
|
+
outputKinds: { layers: "image", base: "image", manifest: "json" },
|
|
7989
|
+
// Billed per returned layer (2-17) plus an optional text pass; the backend
|
|
7990
|
+
// charges the real count on completion. This is the pre-run estimate.
|
|
7991
|
+
cost: () => ({ credits: 30, seconds_estimate: 100 })
|
|
7992
|
+
});
|
|
7993
|
+
|
|
7994
|
+
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
7995
|
+
import { z as z22 } from "zod";
|
|
7898
7996
|
var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
|
|
7899
|
-
var ImageReferenceSheetParams =
|
|
7900
|
-
model:
|
|
7901
|
-
subject_description:
|
|
7997
|
+
var ImageReferenceSheetParams = z22.object({
|
|
7998
|
+
model: z22.enum(REFERENCE_SHEET_MODELS),
|
|
7999
|
+
subject_description: z22.string().min(1),
|
|
7902
8000
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
7903
8001
|
// so a multi-scene shoot keeps one consistent set.
|
|
7904
|
-
subject_type:
|
|
7905
|
-
views:
|
|
7906
|
-
style:
|
|
7907
|
-
prompt_override:
|
|
7908
|
-
aspect_ratio:
|
|
7909
|
-
image_size:
|
|
8002
|
+
subject_type: z22.enum(["character", "person", "product", "location"]),
|
|
8003
|
+
views: z22.array(z22.string().min(1)).min(2).max(8).optional(),
|
|
8004
|
+
style: z22.string().optional(),
|
|
8005
|
+
prompt_override: z22.string().min(1).optional(),
|
|
8006
|
+
aspect_ratio: z22.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(),
|
|
8007
|
+
image_size: z22.enum(OPENROUTER_IMAGE_SIZES).optional()
|
|
7910
8008
|
}).strict();
|
|
7911
8009
|
var imageReferenceSheetNode = delegated({
|
|
7912
8010
|
id: "image_reference_sheet",
|
|
@@ -7914,9 +8012,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
7914
8012
|
category: "image",
|
|
7915
8013
|
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).",
|
|
7916
8014
|
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.",
|
|
7917
|
-
inputs:
|
|
8015
|
+
inputs: z22.object({ references: z22.array(ImageRef).min(1).max(6) }).loose(),
|
|
7918
8016
|
params: ImageReferenceSheetParams,
|
|
7919
|
-
outputs:
|
|
8017
|
+
outputs: z22.object({ sheet: ImageRef }).strict(),
|
|
7920
8018
|
outputKinds: { sheet: "image" },
|
|
7921
8019
|
cost: ({ params }) => ({
|
|
7922
8020
|
credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
|
|
@@ -7925,10 +8023,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
7925
8023
|
});
|
|
7926
8024
|
|
|
7927
8025
|
// src/engine/nodes/remote/imageSearch.ts
|
|
7928
|
-
import { z as
|
|
7929
|
-
var ImageSearchParams =
|
|
7930
|
-
prompt:
|
|
7931
|
-
count:
|
|
8026
|
+
import { z as z23 } from "zod";
|
|
8027
|
+
var ImageSearchParams = z23.object({
|
|
8028
|
+
prompt: z23.string().min(1),
|
|
8029
|
+
count: z23.number().int().min(1).max(20).default(5)
|
|
7932
8030
|
}).strict();
|
|
7933
8031
|
var imageSearchNode = delegated({
|
|
7934
8032
|
id: "image_search",
|
|
@@ -7936,15 +8034,15 @@ var imageSearchNode = delegated({
|
|
|
7936
8034
|
category: "image",
|
|
7937
8035
|
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.",
|
|
7938
8036
|
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.",
|
|
7939
|
-
inputs:
|
|
8037
|
+
inputs: z23.object({}).loose(),
|
|
7940
8038
|
params: ImageSearchParams,
|
|
7941
|
-
outputs:
|
|
8039
|
+
outputs: z23.object({ images: z23.array(ImageRef).min(1) }).strict(),
|
|
7942
8040
|
outputKinds: { images: "image" },
|
|
7943
8041
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
7944
8042
|
});
|
|
7945
8043
|
|
|
7946
8044
|
// src/engine/nodes/remote/imageSelect.ts
|
|
7947
|
-
import { z as
|
|
8045
|
+
import { z as z24 } from "zod";
|
|
7948
8046
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
7949
8047
|
var imageSelectNode = delegated({
|
|
7950
8048
|
id: "image_select",
|
|
@@ -7952,15 +8050,15 @@ var imageSelectNode = delegated({
|
|
|
7952
8050
|
category: "vision",
|
|
7953
8051
|
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.",
|
|
7954
8052
|
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.",
|
|
7955
|
-
inputs:
|
|
7956
|
-
params:
|
|
7957
|
-
model:
|
|
7958
|
-
prompt:
|
|
7959
|
-
count:
|
|
7960
|
-
temperature:
|
|
7961
|
-
max_tokens:
|
|
8053
|
+
inputs: z24.object({ images: z24.array(ImageRef).min(2) }).loose(),
|
|
8054
|
+
params: z24.object({
|
|
8055
|
+
model: z24.enum(IMAGE_SELECT_MODELS),
|
|
8056
|
+
prompt: z24.string().min(1),
|
|
8057
|
+
count: z24.number().int().min(1).default(1),
|
|
8058
|
+
temperature: z24.number().min(0).max(2).optional(),
|
|
8059
|
+
max_tokens: z24.number().int().positive().optional()
|
|
7962
8060
|
}).strict(),
|
|
7963
|
-
outputs:
|
|
8061
|
+
outputs: z24.object({ images: z24.array(ImageRef).min(1), reasoning: TextRef }).strict(),
|
|
7964
8062
|
outputKinds: { images: "image", reasoning: "text" },
|
|
7965
8063
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
7966
8064
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -7985,34 +8083,34 @@ var imageSelectNode = delegated({
|
|
|
7985
8083
|
});
|
|
7986
8084
|
|
|
7987
8085
|
// src/engine/nodes/remote/music.ts
|
|
7988
|
-
import { z as
|
|
8086
|
+
import { z as z25 } from "zod";
|
|
7989
8087
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
7990
|
-
var MusicParams =
|
|
7991
|
-
model:
|
|
8088
|
+
var MusicParams = z25.object({
|
|
8089
|
+
model: z25.enum(MUSIC_MODELS),
|
|
7992
8090
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
7993
|
-
prompt:
|
|
8091
|
+
prompt: z25.string().optional(),
|
|
7994
8092
|
/**
|
|
7995
8093
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
7996
8094
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
7997
8095
|
*/
|
|
7998
|
-
composition_plan:
|
|
8096
|
+
composition_plan: z25.record(z25.string(), z25.unknown()).optional(),
|
|
7999
8097
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
8000
|
-
music_length_ms:
|
|
8001
|
-
seed:
|
|
8098
|
+
music_length_ms: z25.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
8099
|
+
seed: z25.number().int().optional(),
|
|
8002
8100
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
8003
|
-
force_instrumental:
|
|
8101
|
+
force_instrumental: z25.boolean().optional(),
|
|
8004
8102
|
/** composition_plan only — honor exact section durations. */
|
|
8005
|
-
respect_sections_durations:
|
|
8103
|
+
respect_sections_durations: z25.boolean().optional(),
|
|
8006
8104
|
/** Emit word-level timestamps alongside the audio. */
|
|
8007
|
-
with_timestamps:
|
|
8105
|
+
with_timestamps: z25.boolean().optional(),
|
|
8008
8106
|
/**
|
|
8009
8107
|
* video-to-music only — short description of the desired score
|
|
8010
8108
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
8011
8109
|
*/
|
|
8012
|
-
description:
|
|
8110
|
+
description: z25.string().max(1e3).optional(),
|
|
8013
8111
|
/** video-to-music only — up to 10 style tags. */
|
|
8014
|
-
tags:
|
|
8015
|
-
output_format:
|
|
8112
|
+
tags: z25.array(z25.string()).max(10).optional(),
|
|
8113
|
+
output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
8016
8114
|
}).strict();
|
|
8017
8115
|
var musicNode = delegated({
|
|
8018
8116
|
id: "music",
|
|
@@ -8020,9 +8118,9 @@ var musicNode = delegated({
|
|
|
8020
8118
|
category: "audio",
|
|
8021
8119
|
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`.",
|
|
8022
8120
|
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.",
|
|
8023
|
-
inputs:
|
|
8121
|
+
inputs: z25.object({ video: VideoRef.optional() }).loose(),
|
|
8024
8122
|
params: MusicParams,
|
|
8025
|
-
outputs:
|
|
8123
|
+
outputs: z25.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
8026
8124
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
8027
8125
|
cost: ({ params }) => {
|
|
8028
8126
|
const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
|
|
@@ -8053,25 +8151,25 @@ var musicNode = delegated({
|
|
|
8053
8151
|
});
|
|
8054
8152
|
|
|
8055
8153
|
// src/engine/nodes/remote/soundEffect.ts
|
|
8056
|
-
import { z as
|
|
8154
|
+
import { z as z26 } from "zod";
|
|
8057
8155
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
8058
|
-
var SoundEffectParams =
|
|
8059
|
-
model:
|
|
8156
|
+
var SoundEffectParams = z26.object({
|
|
8157
|
+
model: z26.enum(SOUND_EFFECT_MODELS),
|
|
8060
8158
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
8061
|
-
text:
|
|
8159
|
+
text: z26.string().min(1),
|
|
8062
8160
|
/**
|
|
8063
8161
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
8064
8162
|
* natural length for the described effect.
|
|
8065
8163
|
*/
|
|
8066
|
-
duration_seconds:
|
|
8164
|
+
duration_seconds: z26.number().min(0.5).max(30).optional(),
|
|
8067
8165
|
/**
|
|
8068
8166
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
8069
8167
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
8070
8168
|
*/
|
|
8071
|
-
prompt_influence:
|
|
8169
|
+
prompt_influence: z26.number().min(0).max(1).optional(),
|
|
8072
8170
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
8073
|
-
loop:
|
|
8074
|
-
output_format:
|
|
8171
|
+
loop: z26.boolean().optional(),
|
|
8172
|
+
output_format: z26.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
8075
8173
|
}).strict();
|
|
8076
8174
|
var soundEffectNode = delegated({
|
|
8077
8175
|
id: "sound_effect",
|
|
@@ -8079,9 +8177,9 @@ var soundEffectNode = delegated({
|
|
|
8079
8177
|
category: "audio",
|
|
8080
8178
|
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.",
|
|
8081
8179
|
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.",
|
|
8082
|
-
inputs:
|
|
8180
|
+
inputs: z26.object({}).loose(),
|
|
8083
8181
|
params: SoundEffectParams,
|
|
8084
|
-
outputs:
|
|
8182
|
+
outputs: z26.object({ audio: AudioRef }).strict(),
|
|
8085
8183
|
outputKinds: { audio: "audio" },
|
|
8086
8184
|
cost: ({ params }) => {
|
|
8087
8185
|
const seconds = params.duration_seconds ?? 5;
|
|
@@ -8090,7 +8188,7 @@ var soundEffectNode = delegated({
|
|
|
8090
8188
|
});
|
|
8091
8189
|
|
|
8092
8190
|
// src/engine/nodes/remote/textGenerate.ts
|
|
8093
|
-
import { z as
|
|
8191
|
+
import { z as z27 } from "zod";
|
|
8094
8192
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
8095
8193
|
var textGenerateNode = delegated({
|
|
8096
8194
|
id: "text_generate",
|
|
@@ -8098,58 +8196,58 @@ var textGenerateNode = delegated({
|
|
|
8098
8196
|
category: "language",
|
|
8099
8197
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
8100
8198
|
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.',
|
|
8101
|
-
inputs:
|
|
8102
|
-
params:
|
|
8103
|
-
model:
|
|
8104
|
-
prompt:
|
|
8105
|
-
system:
|
|
8106
|
-
response_format:
|
|
8107
|
-
web_search:
|
|
8108
|
-
temperature:
|
|
8109
|
-
max_tokens:
|
|
8199
|
+
inputs: z27.object({}).loose(),
|
|
8200
|
+
params: z27.object({
|
|
8201
|
+
model: z27.enum(TEXT_GENERATE_MODELS),
|
|
8202
|
+
prompt: z27.string().min(1),
|
|
8203
|
+
system: z27.string().optional(),
|
|
8204
|
+
response_format: z27.enum(["text", "json_object"]).optional(),
|
|
8205
|
+
web_search: z27.boolean().optional(),
|
|
8206
|
+
temperature: z27.number().min(0).max(2).optional(),
|
|
8207
|
+
max_tokens: z27.number().int().positive().optional()
|
|
8110
8208
|
}).strict(),
|
|
8111
|
-
outputs:
|
|
8209
|
+
outputs: z27.object({ text: TextRef }).strict(),
|
|
8112
8210
|
outputKinds: { text: "text" },
|
|
8113
8211
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
8114
8212
|
});
|
|
8115
8213
|
|
|
8116
8214
|
// src/engine/nodes/remote/tts.ts
|
|
8117
|
-
import { z as
|
|
8215
|
+
import { z as z28 } from "zod";
|
|
8118
8216
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
8119
|
-
var TtsVoiceSettings =
|
|
8120
|
-
stability:
|
|
8121
|
-
similarity_boost:
|
|
8122
|
-
style:
|
|
8123
|
-
use_speaker_boost:
|
|
8124
|
-
speed:
|
|
8217
|
+
var TtsVoiceSettings = z28.object({
|
|
8218
|
+
stability: z28.number().min(0).max(1).optional(),
|
|
8219
|
+
similarity_boost: z28.number().min(0).max(1).optional(),
|
|
8220
|
+
style: z28.number().min(0).max(1).optional(),
|
|
8221
|
+
use_speaker_boost: z28.boolean().optional(),
|
|
8222
|
+
speed: z28.number().min(0.25).max(4).optional()
|
|
8125
8223
|
}).strict();
|
|
8126
|
-
var TtsPronunciationLocator =
|
|
8127
|
-
pronunciation_dictionary_id:
|
|
8128
|
-
version_id:
|
|
8224
|
+
var TtsPronunciationLocator = z28.object({
|
|
8225
|
+
pronunciation_dictionary_id: z28.string().min(1),
|
|
8226
|
+
version_id: z28.string().nullable().optional()
|
|
8129
8227
|
}).strict();
|
|
8130
|
-
var TtsParams =
|
|
8131
|
-
model:
|
|
8132
|
-
text:
|
|
8133
|
-
voice:
|
|
8228
|
+
var TtsParams = z28.object({
|
|
8229
|
+
model: z28.enum(TTS_MODELS),
|
|
8230
|
+
text: z28.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
8231
|
+
voice: z28.string().min(1),
|
|
8134
8232
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
8135
|
-
output_format:
|
|
8136
|
-
seed:
|
|
8233
|
+
output_format: z28.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
8234
|
+
seed: z28.number().int().min(0).max(4294967295).optional(),
|
|
8137
8235
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
8138
|
-
stability:
|
|
8139
|
-
similarity_boost:
|
|
8236
|
+
stability: z28.number().min(0).max(1).optional(),
|
|
8237
|
+
similarity_boost: z28.number().min(0).max(1).optional(),
|
|
8140
8238
|
voice_settings: TtsVoiceSettings.optional(),
|
|
8141
8239
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
8142
|
-
language_code:
|
|
8143
|
-
pronunciation_dictionary_locators:
|
|
8144
|
-
apply_text_normalization:
|
|
8240
|
+
language_code: z28.string().optional(),
|
|
8241
|
+
pronunciation_dictionary_locators: z28.array(TtsPronunciationLocator).max(3).optional(),
|
|
8242
|
+
apply_text_normalization: z28.enum(["auto", "on", "off"]).optional(),
|
|
8145
8243
|
/** Currently Japanese-only. Adds latency. */
|
|
8146
|
-
apply_language_text_normalization:
|
|
8244
|
+
apply_language_text_normalization: z28.boolean().optional(),
|
|
8147
8245
|
/**
|
|
8148
8246
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
8149
8247
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
8150
8248
|
* rendering, lipsync, and beat-matched cuts.
|
|
8151
8249
|
*/
|
|
8152
|
-
with_timestamps:
|
|
8250
|
+
with_timestamps: z28.boolean().optional()
|
|
8153
8251
|
}).strict();
|
|
8154
8252
|
var ttsNode = delegated({
|
|
8155
8253
|
id: "tts",
|
|
@@ -8157,9 +8255,9 @@ var ttsNode = delegated({
|
|
|
8157
8255
|
category: "audio",
|
|
8158
8256
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
8159
8257
|
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).",
|
|
8160
|
-
inputs:
|
|
8258
|
+
inputs: z28.object({}).loose(),
|
|
8161
8259
|
params: TtsParams,
|
|
8162
|
-
outputs:
|
|
8260
|
+
outputs: z28.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
8163
8261
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
8164
8262
|
cost: ({ params }) => ({
|
|
8165
8263
|
credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
|
|
@@ -8168,25 +8266,25 @@ var ttsNode = delegated({
|
|
|
8168
8266
|
});
|
|
8169
8267
|
|
|
8170
8268
|
// src/engine/nodes/remote/video.ts
|
|
8171
|
-
import { z as
|
|
8172
|
-
var videoModelEnum =
|
|
8173
|
-
var VideoGenerateParams =
|
|
8269
|
+
import { z as z29 } from "zod";
|
|
8270
|
+
var videoModelEnum = z29.enum(VIDEO_GENERATE_MODELS);
|
|
8271
|
+
var VideoGenerateParams = z29.object({
|
|
8174
8272
|
model: videoModelEnum,
|
|
8175
|
-
prompt:
|
|
8176
|
-
duration:
|
|
8177
|
-
resolution:
|
|
8273
|
+
prompt: z29.string().min(1),
|
|
8274
|
+
duration: z29.number().int().positive().optional(),
|
|
8275
|
+
resolution: z29.string().optional(),
|
|
8178
8276
|
// Union of ratios accepted by at least one curated model (registry gates
|
|
8179
8277
|
// per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
|
|
8180
|
-
aspect_ratio:
|
|
8181
|
-
generate_audio:
|
|
8182
|
-
seed:
|
|
8278
|
+
aspect_ratio: z29.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
|
|
8279
|
+
generate_audio: z29.boolean().optional(),
|
|
8280
|
+
seed: z29.number().int().nonnegative().optional(),
|
|
8183
8281
|
// Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
|
|
8184
|
-
negative_prompt:
|
|
8185
|
-
person_generation:
|
|
8186
|
-
enhance_prompt:
|
|
8187
|
-
conditioning_scale:
|
|
8282
|
+
negative_prompt: z29.string().optional(),
|
|
8283
|
+
person_generation: z29.string().optional(),
|
|
8284
|
+
enhance_prompt: z29.boolean().optional(),
|
|
8285
|
+
conditioning_scale: z29.number().optional(),
|
|
8188
8286
|
// Kling-only passthrough (prompt-adherence dial, sent top-level).
|
|
8189
|
-
cfg_scale:
|
|
8287
|
+
cfg_scale: z29.number().optional()
|
|
8190
8288
|
}).strict();
|
|
8191
8289
|
var videoGenerateNode = delegated({
|
|
8192
8290
|
id: "video_generate",
|
|
@@ -8194,7 +8292,7 @@ var videoGenerateNode = delegated({
|
|
|
8194
8292
|
category: "video",
|
|
8195
8293
|
summary: "Generate video for ad creatives. Curated roster: `google/gemini-omni-flash` (DEFAULT \u2014 native audio + reference images in one call, 720p, 3-10s), `bytedance/seedance-2.0` (identity/product workhorse, NO photoreal human faces), `google/veo-3.1` (photoreal cine ceiling + the human-face model), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-3.0` (motion-transfer/dynamic). Async with polling.",
|
|
8196
8294
|
when_to_use: "`google/gemini-omni-flash` is the DEFAULT: it renders picture and synchronized audio in one call, and is the only model here that takes reference images alongside a prompt. SET `duration` (3-10s) to the length the beat actually needs \u2014 it bills ~$0.101 PER SECOND, so leaving it out renders the 10s default and charges ~$1.01 for a beat that may need 3s (~$0.30). Its resolution is fixed at 720p and it accepts only `16:9` or `9:16`; it takes NO `resolution`, `seed`, `negative_prompt` or `generate_audio` param \u2014 sending one fails validation. Keep the default for ordinary beats; move off it when a beat needs 1080p, a length outside 3-10s, or a 1:1 frame. Use `bytedance/seedance-2.0` for identity/product output WITHOUT a photoreal human face on camera. Any clip whose frame shows a photoreal human face \u2014 a UGC creator, presenter, or testimonial speaker, REAL OR AI-GENERATED \u2014 must use `google/veo-3.1`: ByteDance's filter rejects photoreal human faces on Seedance (content_policy_blocked/E005), and a synthetic AI face does NOT clear it. Use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-3.0` for motion-transfer/hyper-dynamic beats. `baker canvas scaffold-video` auto-routes a photoreal cast to Veo; a Seedance clip blocked at runtime also auto-recovers on Veo. Each model gates its own durations/resolutions/aspect ratios in the registry \u2014 see the README per-model section.",
|
|
8197
|
-
inputs:
|
|
8295
|
+
inputs: z29.object({
|
|
8198
8296
|
first_frame: ImageRef.optional(),
|
|
8199
8297
|
last_frame: ImageRef.optional(),
|
|
8200
8298
|
reference: ImageRef.optional(),
|
|
@@ -8204,10 +8302,10 @@ var videoGenerateNode = delegated({
|
|
|
8204
8302
|
* exclusive with `first_frame` — OpenRouter treats a request carrying both
|
|
8205
8303
|
* as image-to-video and ignores these, so wire one or the other.
|
|
8206
8304
|
*/
|
|
8207
|
-
references:
|
|
8305
|
+
references: z29.array(ImageRef).optional()
|
|
8208
8306
|
}).loose(),
|
|
8209
8307
|
params: VideoGenerateParams,
|
|
8210
|
-
outputs:
|
|
8308
|
+
outputs: z29.object({ video: VideoRef }).strict(),
|
|
8211
8309
|
outputKinds: { video: "video" },
|
|
8212
8310
|
// Priced from the shared contract, so `validate`'s quote and the charge the
|
|
8213
8311
|
// backend applies come from ONE table. A flat number here under-quoted a
|
|
@@ -8227,11 +8325,11 @@ var videoGenerateNode = delegated({
|
|
|
8227
8325
|
});
|
|
8228
8326
|
|
|
8229
8327
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
8230
|
-
import { z as
|
|
8231
|
-
var VideoBackgroundRemoveParams =
|
|
8232
|
-
model:
|
|
8233
|
-
edge_refinement:
|
|
8234
|
-
output_codec:
|
|
8328
|
+
import { z as z30 } from "zod";
|
|
8329
|
+
var VideoBackgroundRemoveParams = z30.object({
|
|
8330
|
+
model: z30.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
|
|
8331
|
+
edge_refinement: z30.boolean().optional().default(true),
|
|
8332
|
+
output_codec: z30.enum(["vp9", "h264"]).optional().default("vp9")
|
|
8235
8333
|
}).strict();
|
|
8236
8334
|
var videoBackgroundRemoveNode = delegated({
|
|
8237
8335
|
id: "video_background_remove",
|
|
@@ -8239,18 +8337,18 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
8239
8337
|
category: "video",
|
|
8240
8338
|
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`.",
|
|
8241
8339
|
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.",
|
|
8242
|
-
inputs:
|
|
8340
|
+
inputs: z30.object({
|
|
8243
8341
|
video: VideoRef
|
|
8244
8342
|
}).strict(),
|
|
8245
8343
|
params: VideoBackgroundRemoveParams,
|
|
8246
|
-
outputs:
|
|
8344
|
+
outputs: z30.object({ video: VideoRef }).strict(),
|
|
8247
8345
|
outputKinds: { video: "video" },
|
|
8248
8346
|
// $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
|
|
8249
8347
|
cost: () => ({ credits: 50, seconds_estimate: 60 })
|
|
8250
8348
|
});
|
|
8251
8349
|
|
|
8252
8350
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
8253
|
-
import { z as
|
|
8351
|
+
import { z as z31 } from "zod";
|
|
8254
8352
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
8255
8353
|
var videoDeconstructNode = delegated({
|
|
8256
8354
|
id: "video_deconstruct",
|
|
@@ -8258,34 +8356,34 @@ var videoDeconstructNode = delegated({
|
|
|
8258
8356
|
category: "video",
|
|
8259
8357
|
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).',
|
|
8260
8358
|
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.',
|
|
8261
|
-
inputs:
|
|
8262
|
-
params:
|
|
8263
|
-
model:
|
|
8264
|
-
mode:
|
|
8265
|
-
language:
|
|
8266
|
-
max_scenes:
|
|
8267
|
-
focus:
|
|
8268
|
-
start_s:
|
|
8269
|
-
end_s:
|
|
8359
|
+
inputs: z31.object({ video: VideoRef }).loose(),
|
|
8360
|
+
params: z31.object({
|
|
8361
|
+
model: z31.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
8362
|
+
mode: z31.enum(["full", "index"]).optional(),
|
|
8363
|
+
language: z31.string().min(2).max(8).optional(),
|
|
8364
|
+
max_scenes: z31.number().int().min(1).max(60).optional(),
|
|
8365
|
+
focus: z31.string().optional(),
|
|
8366
|
+
start_s: z31.number().min(0).optional(),
|
|
8367
|
+
end_s: z31.number().positive().optional(),
|
|
8270
8368
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
8271
8369
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
8272
8370
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
8273
8371
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
8274
|
-
shot_cuts:
|
|
8372
|
+
shot_cuts: z31.array(z31.number().min(0)).max(200).optional(),
|
|
8275
8373
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
8276
8374
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
8277
8375
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
8278
8376
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
8279
|
-
max_clip_s:
|
|
8377
|
+
max_clip_s: z31.number().positive().max(60).optional(),
|
|
8280
8378
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
8281
8379
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
8282
|
-
transcriber:
|
|
8380
|
+
transcriber: z31.enum(["groq", "deepgram"]).optional()
|
|
8283
8381
|
}).strict(),
|
|
8284
|
-
outputs:
|
|
8382
|
+
outputs: z31.object({
|
|
8285
8383
|
analysis: JsonRef,
|
|
8286
8384
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
8287
|
-
start_frames:
|
|
8288
|
-
end_frames:
|
|
8385
|
+
start_frames: z31.array(ImageRef).min(1).optional(),
|
|
8386
|
+
end_frames: z31.array(ImageRef).min(1).optional(),
|
|
8289
8387
|
transcript: JsonRef
|
|
8290
8388
|
}).strict(),
|
|
8291
8389
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -8293,31 +8391,31 @@ var videoDeconstructNode = delegated({
|
|
|
8293
8391
|
});
|
|
8294
8392
|
|
|
8295
8393
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
8296
|
-
import { z as
|
|
8297
|
-
var FalLipsyncParams =
|
|
8298
|
-
model:
|
|
8394
|
+
import { z as z32 } from "zod";
|
|
8395
|
+
var FalLipsyncParams = z32.object({
|
|
8396
|
+
model: z32.literal("fal/veed-lipsync")
|
|
8299
8397
|
}).strict();
|
|
8300
|
-
var VideoLipsyncParams =
|
|
8398
|
+
var VideoLipsyncParams = z32.discriminatedUnion("model", [FalLipsyncParams]);
|
|
8301
8399
|
var videoLipsyncNode = delegated({
|
|
8302
8400
|
id: "video_lipsync",
|
|
8303
8401
|
version: "1.0.0",
|
|
8304
8402
|
category: "video",
|
|
8305
8403
|
summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
|
|
8306
|
-
inputs:
|
|
8404
|
+
inputs: z32.object({
|
|
8307
8405
|
video: VideoRef,
|
|
8308
8406
|
audio: AudioRef
|
|
8309
8407
|
}).strict(),
|
|
8310
8408
|
params: VideoLipsyncParams,
|
|
8311
|
-
outputs:
|
|
8409
|
+
outputs: z32.object({ video: VideoRef }).strict(),
|
|
8312
8410
|
outputKinds: { video: "video" },
|
|
8313
8411
|
cost: () => ({ credits: 20, seconds_estimate: 120 })
|
|
8314
8412
|
});
|
|
8315
8413
|
|
|
8316
8414
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
8317
|
-
import { mkdtemp as mkdtemp6, readFile as
|
|
8415
|
+
import { mkdtemp as mkdtemp6, readFile as readFile12, rm as rm6 } from "fs/promises";
|
|
8318
8416
|
import { tmpdir as tmpdir6 } from "os";
|
|
8319
|
-
import
|
|
8320
|
-
import { z as
|
|
8417
|
+
import path15 from "path";
|
|
8418
|
+
import { z as z33 } from "zod";
|
|
8321
8419
|
|
|
8322
8420
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
8323
8421
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -8396,24 +8494,24 @@ ${detail.slice(-4e3)}`);
|
|
|
8396
8494
|
}
|
|
8397
8495
|
|
|
8398
8496
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
8399
|
-
var VideoTranscribeParams =
|
|
8400
|
-
language:
|
|
8497
|
+
var VideoTranscribeParams = z33.object({
|
|
8498
|
+
language: z33.string().min(2).max(8).optional(),
|
|
8401
8499
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
8402
8500
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
8403
8501
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
8404
|
-
transcriber:
|
|
8502
|
+
transcriber: z33.enum(["groq", "deepgram"]).optional()
|
|
8405
8503
|
}).strict();
|
|
8406
|
-
var VideoTranscribeInputs =
|
|
8504
|
+
var VideoTranscribeInputs = z33.object({
|
|
8407
8505
|
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
8408
8506
|
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
8409
8507
|
// the local extraction path has been shipping one for every video input.
|
|
8410
|
-
video:
|
|
8508
|
+
video: z33.union([VideoRef, AudioRef])
|
|
8411
8509
|
}).strict();
|
|
8412
|
-
var VideoTranscribeOutputs =
|
|
8413
|
-
transcript:
|
|
8510
|
+
var VideoTranscribeOutputs = z33.object({
|
|
8511
|
+
transcript: z33.custom(),
|
|
8414
8512
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
8415
8513
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
8416
|
-
rich:
|
|
8514
|
+
rich: z33.custom().optional()
|
|
8417
8515
|
}).strict();
|
|
8418
8516
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
8419
8517
|
var videoTranscribeNode = defineNode({
|
|
@@ -8451,14 +8549,14 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
8451
8549
|
ctx.log("video_transcribe: no audio track detected, sending full video");
|
|
8452
8550
|
return null;
|
|
8453
8551
|
}
|
|
8454
|
-
tmpDir = await mkdtemp6(
|
|
8455
|
-
const audioPath =
|
|
8552
|
+
tmpDir = await mkdtemp6(path15.join(tmpdir6(), "vtx-"));
|
|
8553
|
+
const audioPath = path15.join(tmpDir, "audio.mp3");
|
|
8456
8554
|
ctx.log("video_transcribe: extracting audio (mono 16kHz mp3)");
|
|
8457
8555
|
await runFfmpeg(
|
|
8458
8556
|
["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
|
|
8459
8557
|
{ timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
|
|
8460
8558
|
);
|
|
8461
|
-
const bytes = await
|
|
8559
|
+
const bytes = await readFile12(audioPath);
|
|
8462
8560
|
if (bytes.byteLength === 0) {
|
|
8463
8561
|
ctx.log("video_transcribe: extracted audio is empty, sending full video");
|
|
8464
8562
|
return null;
|
|
@@ -8498,29 +8596,29 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
8498
8596
|
}
|
|
8499
8597
|
|
|
8500
8598
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
8501
|
-
import { z as
|
|
8599
|
+
import { z as z34 } from "zod";
|
|
8502
8600
|
var voiceSelectNode = delegated({
|
|
8503
8601
|
id: "voice_select",
|
|
8504
8602
|
version: "1.0.0",
|
|
8505
8603
|
category: "audio",
|
|
8506
8604
|
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.',
|
|
8507
8605
|
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.',
|
|
8508
|
-
inputs:
|
|
8509
|
-
params:
|
|
8510
|
-
description:
|
|
8511
|
-
gender:
|
|
8512
|
-
age:
|
|
8513
|
-
accent:
|
|
8514
|
-
language:
|
|
8515
|
-
limit:
|
|
8606
|
+
inputs: z34.object({}).loose(),
|
|
8607
|
+
params: z34.object({
|
|
8608
|
+
description: z34.string().min(1),
|
|
8609
|
+
gender: z34.string().optional(),
|
|
8610
|
+
age: z34.string().optional(),
|
|
8611
|
+
accent: z34.string().optional(),
|
|
8612
|
+
language: z34.string().optional(),
|
|
8613
|
+
limit: z34.number().int().min(1).max(20).optional()
|
|
8516
8614
|
}).strict(),
|
|
8517
|
-
outputs:
|
|
8615
|
+
outputs: z34.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
8518
8616
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
8519
8617
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
8520
8618
|
});
|
|
8521
8619
|
|
|
8522
8620
|
// src/engine/schema/catalog.ts
|
|
8523
|
-
import { z as
|
|
8621
|
+
import { z as z35 } from "zod";
|
|
8524
8622
|
function generateCatalog(registry, opts = {}) {
|
|
8525
8623
|
const entries = registry.all().map((def) => {
|
|
8526
8624
|
const cost = def.cost ? safeCost(def) : void 0;
|
|
@@ -8531,9 +8629,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
8531
8629
|
summary: def.summary,
|
|
8532
8630
|
when_to_use: def.when_to_use,
|
|
8533
8631
|
location: def.location,
|
|
8534
|
-
inputs:
|
|
8535
|
-
params:
|
|
8536
|
-
outputs:
|
|
8632
|
+
inputs: z35.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
8633
|
+
params: z35.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
8634
|
+
outputs: z35.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
8537
8635
|
cost_estimate_credits: cost?.credits,
|
|
8538
8636
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
8539
8637
|
};
|
|
@@ -8565,19 +8663,19 @@ function safeCost(def) {
|
|
|
8565
8663
|
|
|
8566
8664
|
// src/engine/storage/cache-store.ts
|
|
8567
8665
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8568
|
-
import { mkdir as mkdir3, readFile as
|
|
8569
|
-
import
|
|
8666
|
+
import { mkdir as mkdir3, readFile as readFile13, rename as rename2, writeFile as writeFile7 } from "fs/promises";
|
|
8667
|
+
import path16 from "path";
|
|
8570
8668
|
var LocalCacheStore = class {
|
|
8571
8669
|
rootDir;
|
|
8572
8670
|
constructor(rootDir) {
|
|
8573
8671
|
this.rootDir = rootDir;
|
|
8574
8672
|
}
|
|
8575
8673
|
filePath(cacheKey) {
|
|
8576
|
-
return
|
|
8674
|
+
return path16.join(this.rootDir, `${cacheKey}.json`);
|
|
8577
8675
|
}
|
|
8578
8676
|
async get(cacheKey) {
|
|
8579
8677
|
try {
|
|
8580
|
-
const buf = await
|
|
8678
|
+
const buf = await readFile13(this.filePath(cacheKey), "utf8");
|
|
8581
8679
|
return JSON.parse(buf);
|
|
8582
8680
|
} catch (e) {
|
|
8583
8681
|
if (e.code === "ENOENT") return null;
|
|
@@ -8586,7 +8684,7 @@ var LocalCacheStore = class {
|
|
|
8586
8684
|
}
|
|
8587
8685
|
async put(entry) {
|
|
8588
8686
|
const dest = this.filePath(entry.cacheKey);
|
|
8589
|
-
await mkdir3(
|
|
8687
|
+
await mkdir3(path16.dirname(dest), { recursive: true });
|
|
8590
8688
|
const tmp = `${dest}.tmp-${process.pid}-${randomUUID2()}`;
|
|
8591
8689
|
await writeFile7(tmp, JSON.stringify(entry, null, 0));
|
|
8592
8690
|
await rename2(tmp, dest);
|
|
@@ -8631,7 +8729,8 @@ var REMOTE_NODES = [
|
|
|
8631
8729
|
videoBackgroundRemoveNode,
|
|
8632
8730
|
videoDeconstructNode,
|
|
8633
8731
|
voiceSelectNode,
|
|
8634
|
-
imageBackgroundRemoveNode
|
|
8732
|
+
imageBackgroundRemoveNode,
|
|
8733
|
+
imageLayerizeNode
|
|
8635
8734
|
];
|
|
8636
8735
|
function defaultRegistry() {
|
|
8637
8736
|
const r = new NodeRegistry();
|
|
@@ -8641,12 +8740,12 @@ function defaultRegistry() {
|
|
|
8641
8740
|
}
|
|
8642
8741
|
function createEngineFromEnv(opts = {}) {
|
|
8643
8742
|
const cwd = opts.cwd ?? process.cwd();
|
|
8644
|
-
const cacheDir = opts.cacheDir ??
|
|
8645
|
-
const outputsDir = opts.outputsDir ??
|
|
8743
|
+
const cacheDir = opts.cacheDir ?? path17.join(cwd, "canvas", ".cache");
|
|
8744
|
+
const outputsDir = opts.outputsDir ?? path17.join(cwd, "canvas");
|
|
8646
8745
|
const creds = requireCredentialsFromEnv();
|
|
8647
8746
|
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
8648
|
-
const assets = new LocalAssetStore(
|
|
8649
|
-
const localCache = new LocalCacheStore(
|
|
8747
|
+
const assets = new LocalAssetStore(path17.join(cacheDir, "assets"));
|
|
8748
|
+
const localCache = new LocalCacheStore(path17.join(cacheDir, "index"));
|
|
8650
8749
|
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
8651
8750
|
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
8652
8751
|
local: localCache,
|
|
@@ -8698,6 +8797,7 @@ export {
|
|
|
8698
8797
|
REF_PREFIX,
|
|
8699
8798
|
parseRefExpr,
|
|
8700
8799
|
sha256Hex,
|
|
8800
|
+
MAX_ASSET_BYTES,
|
|
8701
8801
|
looksLikeHttpUrl,
|
|
8702
8802
|
SEEDANCE_PROFILE,
|
|
8703
8803
|
clipProfileFor,
|
|
@@ -8706,7 +8806,10 @@ export {
|
|
|
8706
8806
|
spineInputFlags,
|
|
8707
8807
|
spineInputOps,
|
|
8708
8808
|
elementMentionKeywords,
|
|
8809
|
+
MAX_REMOTE_IMAGE_BYTES,
|
|
8709
8810
|
fetchExternalBytes,
|
|
8811
|
+
YtDlpError,
|
|
8812
|
+
runYtDlp,
|
|
8710
8813
|
toModelSafeImage,
|
|
8711
8814
|
BackendClient2,
|
|
8712
8815
|
Engine2 as Engine,
|
|
@@ -8718,4 +8821,4 @@ export {
|
|
|
8718
8821
|
defaultRegistry,
|
|
8719
8822
|
createEngineFromEnv
|
|
8720
8823
|
};
|
|
8721
|
-
//# sourceMappingURL=chunk-
|
|
8824
|
+
//# sourceMappingURL=chunk-ONFW7HIK.js.map
|