@koda-sl/baker-cli 0.124.0 → 0.128.0-dev.70bf43ce4
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 +320 -113
- package/dist/{chunk-IWPAXJC3.js → chunk-43KBQLP5.js} +479 -107
- package/dist/chunk-43KBQLP5.js.map +1 -0
- package/dist/cli.js +2816 -3049
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +91 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IWPAXJC3.js.map +0 -1
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
__toESM
|
|
4
4
|
} from "./chunk-5WRI5ZAA.js";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
|
|
7
7
|
var require_safe_stable_stringify = __commonJS({
|
|
8
|
-
"
|
|
8
|
+
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
9
9
|
"use strict";
|
|
10
10
|
var { hasOwnProperty } = Object.prototype;
|
|
11
11
|
var stringify = configure2();
|
|
@@ -138,9 +138,9 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
138
138
|
}
|
|
139
139
|
if (value) {
|
|
140
140
|
return (value2) => {
|
|
141
|
-
let
|
|
142
|
-
if (typeof value2 !== "function")
|
|
143
|
-
throw new Error(
|
|
141
|
+
let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
|
|
142
|
+
if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
|
|
143
|
+
throw new Error(message2);
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -652,6 +652,9 @@ var HttpClient = class {
|
|
|
652
652
|
async postJson(path16, body, signal) {
|
|
653
653
|
return await this.requestJson("POST", path16, body, signal);
|
|
654
654
|
}
|
|
655
|
+
async putJson(path16, body, signal) {
|
|
656
|
+
return await this.requestJson("PUT", path16, body, signal);
|
|
657
|
+
}
|
|
655
658
|
async getJson(path16, signal) {
|
|
656
659
|
return await this.requestJson("GET", path16, void 0, signal);
|
|
657
660
|
}
|
|
@@ -679,8 +682,8 @@ var HttpClient = class {
|
|
|
679
682
|
try {
|
|
680
683
|
const res = await this.fetchFn(url, {
|
|
681
684
|
method,
|
|
682
|
-
headers: method === "
|
|
683
|
-
body: method === "
|
|
685
|
+
headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
|
686
|
+
body: method === "GET" ? void 0 : JSON.stringify(body),
|
|
684
687
|
signal: controller.signal
|
|
685
688
|
});
|
|
686
689
|
if (res.ok) return { kind: "value", value: await res.json() };
|
|
@@ -717,33 +720,33 @@ async function parseErrorBody(res) {
|
|
|
717
720
|
const errObj = body.error ?? {};
|
|
718
721
|
return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
|
|
719
722
|
}
|
|
720
|
-
function classifyHttpError(status, errObj,
|
|
723
|
+
function classifyHttpError(status, errObj, message2) {
|
|
721
724
|
if (errObj.code === CONTENT_POLICY_CODE) {
|
|
722
|
-
return { kind: "content_policy", status, provider: errObj.provider, message };
|
|
725
|
+
return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
|
|
723
726
|
}
|
|
724
727
|
if (status === 401 || status === 403) {
|
|
725
|
-
return { kind: "unauthorized", status, message };
|
|
728
|
+
return { kind: "unauthorized", status, message: message2 };
|
|
726
729
|
}
|
|
727
730
|
if (status === 400 || status === 422) {
|
|
728
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
731
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
729
732
|
}
|
|
730
733
|
if (status === 502 || status === 504) {
|
|
731
734
|
if (errObj.code === "provider_timeout" || status === 504) {
|
|
732
|
-
return { kind: "timeout", provider: errObj.provider, message };
|
|
735
|
+
return { kind: "timeout", provider: errObj.provider, message: message2 };
|
|
733
736
|
}
|
|
734
737
|
return {
|
|
735
738
|
kind: "provider",
|
|
736
739
|
status,
|
|
737
740
|
provider: errObj.provider,
|
|
738
741
|
code: errObj.code ?? "provider_error",
|
|
739
|
-
message,
|
|
742
|
+
message: message2,
|
|
740
743
|
retryable: errObj.retryable ?? true
|
|
741
744
|
};
|
|
742
745
|
}
|
|
743
746
|
if (status >= 500 || status === 429) {
|
|
744
|
-
return { kind: "server", status, message };
|
|
747
|
+
return { kind: "server", status, message: message2 };
|
|
745
748
|
}
|
|
746
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
749
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
747
750
|
}
|
|
748
751
|
function backoffMs(attempt) {
|
|
749
752
|
return 1e3 * 2 ** attempt;
|
|
@@ -777,7 +780,9 @@ function failedJobError(error) {
|
|
|
777
780
|
retryable: error.retryable ?? false
|
|
778
781
|
});
|
|
779
782
|
}
|
|
780
|
-
|
|
783
|
+
function pollInterval(attempt) {
|
|
784
|
+
return attempt < 15 ? 1e3 : 3e3;
|
|
785
|
+
}
|
|
781
786
|
var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
|
|
782
787
|
var BackendClient = class {
|
|
783
788
|
http;
|
|
@@ -794,7 +799,7 @@ var BackendClient = class {
|
|
|
794
799
|
async pollJob(jobId, signal) {
|
|
795
800
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
796
801
|
const path16 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
797
|
-
|
|
802
|
+
for (let attempt = 0; ; attempt++) {
|
|
798
803
|
if (signal?.aborted) {
|
|
799
804
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
800
805
|
}
|
|
@@ -804,7 +809,7 @@ var BackendClient = class {
|
|
|
804
809
|
if (Date.now() > deadline) {
|
|
805
810
|
throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
|
|
806
811
|
}
|
|
807
|
-
await sleep(
|
|
812
|
+
await sleep(pollInterval(attempt));
|
|
808
813
|
}
|
|
809
814
|
}
|
|
810
815
|
presignAssetUpload(sha256, mime, signal) {
|
|
@@ -814,6 +819,35 @@ var BackendClient = class {
|
|
|
814
819
|
signal
|
|
815
820
|
);
|
|
816
821
|
}
|
|
822
|
+
/** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
|
|
823
|
+
async getCacheEntry(cacheKey, signal) {
|
|
824
|
+
try {
|
|
825
|
+
const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
|
|
826
|
+
return res.entry;
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async putCacheEntry(entry, signal) {
|
|
833
|
+
await this.http.putJson(
|
|
834
|
+
`/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
|
|
835
|
+
entry,
|
|
836
|
+
signal
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
/** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
|
|
840
|
+
async recordRun(payload, signal) {
|
|
841
|
+
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
845
|
+
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
846
|
+
* Additive on the backend (never archives siblings, never sets definitionPath).
|
|
847
|
+
*/
|
|
848
|
+
async syncCreativeDefinition(payload, signal) {
|
|
849
|
+
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
850
|
+
}
|
|
817
851
|
getArtifact(kind, name, version, signal) {
|
|
818
852
|
const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
819
853
|
return this.http.getJson(path16, signal);
|
|
@@ -837,14 +871,17 @@ function requireCredentialsFromEnv(env = process.env) {
|
|
|
837
871
|
}
|
|
838
872
|
return c;
|
|
839
873
|
}
|
|
874
|
+
function remoteCacheEnabledFromEnv(env = process.env) {
|
|
875
|
+
return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
|
|
876
|
+
}
|
|
840
877
|
|
|
841
878
|
// src/engine/engine/errors.ts
|
|
842
879
|
function isBlocking(issue) {
|
|
843
880
|
return issue.severity !== "warning";
|
|
844
881
|
}
|
|
845
882
|
var CanvasError = class extends Error {
|
|
846
|
-
constructor(
|
|
847
|
-
super(
|
|
883
|
+
constructor(message2) {
|
|
884
|
+
super(message2);
|
|
848
885
|
this.name = "CanvasError";
|
|
849
886
|
}
|
|
850
887
|
};
|
|
@@ -901,7 +938,7 @@ function describeCause(c) {
|
|
|
901
938
|
}
|
|
902
939
|
}
|
|
903
940
|
|
|
904
|
-
//
|
|
941
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
905
942
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
906
943
|
var configure = import__.default.configure;
|
|
907
944
|
var wrapper_default = import__.default;
|
|
@@ -959,10 +996,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
|
|
|
959
996
|
var ELEVENLABS_MAX_TEXT_CHARS = 45454;
|
|
960
997
|
var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
|
|
961
998
|
var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
962
|
-
var
|
|
963
|
-
var
|
|
999
|
+
var REPLICATE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
1000
|
+
var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
964
1001
|
var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
965
|
-
var
|
|
1002
|
+
var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
|
|
966
1003
|
var IMAGE_GENERATE_MODELS = [
|
|
967
1004
|
"openai/gpt-5.4-image-2",
|
|
968
1005
|
"google/gemini-3.5-flash",
|
|
@@ -1180,20 +1217,23 @@ var MODEL_REGISTRY = {
|
|
|
1180
1217
|
},
|
|
1181
1218
|
video_generate: {
|
|
1182
1219
|
"bytedance/seedance-2.0": {
|
|
1183
|
-
// Routed via
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1220
|
+
// Routed via Replicate's official `bytedance/seedance-2.0` model. NOTE:
|
|
1221
|
+
// ByteDance's upstream "real person" likeness filter still blocks photoreal
|
|
1222
|
+
// human reference frames on ANY reseller — the escape is a synthetic/AI
|
|
1223
|
+
// presenter face or routing real faces to Veo, not the provider choice.
|
|
1186
1224
|
label: "ByteDance Seedance 2.0",
|
|
1187
1225
|
inputs: [],
|
|
1188
|
-
optional_inputs: [{ kind: "image", mimes:
|
|
1226
|
+
optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
|
|
1189
1227
|
required: ["prompt"],
|
|
1190
1228
|
params: {
|
|
1191
|
-
prompt
|
|
1229
|
+
// Replicate's Seedance wrapper hard-caps the prompt at 4000 chars; gate
|
|
1230
|
+
// it here so an over-length prompt fails validate (free) not the billed call.
|
|
1231
|
+
prompt: { kind: "string", maxLength: 4e3 },
|
|
1192
1232
|
aspect_ratio: {
|
|
1193
1233
|
kind: "string",
|
|
1194
1234
|
enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
|
|
1195
1235
|
},
|
|
1196
|
-
resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
|
|
1236
|
+
resolution: { kind: "string", enum: ["480p", "720p", "1080p", "4k"] },
|
|
1197
1237
|
duration: { kind: "number", enum: SEEDANCE_DURATIONS },
|
|
1198
1238
|
seed: { kind: "number" },
|
|
1199
1239
|
generate_audio: { kind: "boolean" }
|
|
@@ -1214,7 +1254,10 @@ var MODEL_REGISTRY = {
|
|
|
1214
1254
|
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1215
1255
|
seed: { kind: "number" },
|
|
1216
1256
|
generate_audio: { kind: "boolean" },
|
|
1217
|
-
|
|
1257
|
+
// Image-to-video and EU/UK/CH/MENA regions cap this at `allow_adult`;
|
|
1258
|
+
// `allow_all` is text-to-video only. Allow both so an image-conditioned
|
|
1259
|
+
// Veo clip (the real-face fallback) validates.
|
|
1260
|
+
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1218
1261
|
enhance_prompt: { kind: "boolean" },
|
|
1219
1262
|
conditioning_scale: { kind: "number" }
|
|
1220
1263
|
}
|
|
@@ -1265,8 +1308,8 @@ var MODEL_REGISTRY = {
|
|
|
1265
1308
|
"fal/veed-lipsync": {
|
|
1266
1309
|
label: "VEED Lipsync (fal.ai)",
|
|
1267
1310
|
inputs: [
|
|
1268
|
-
{ kind: "video", mimes:
|
|
1269
|
-
{ kind: "audio", mimes:
|
|
1311
|
+
{ kind: "video", mimes: REPLICATE_VIDEO_MIMES },
|
|
1312
|
+
{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }
|
|
1270
1313
|
],
|
|
1271
1314
|
required: [],
|
|
1272
1315
|
params: {}
|
|
@@ -1302,7 +1345,7 @@ var MODEL_REGISTRY = {
|
|
|
1302
1345
|
// TARGET voice, preserving timing/prosody. Used to normalize a talking-head
|
|
1303
1346
|
// clip's native (generator-chosen) voice into ONE consistent brand voice.
|
|
1304
1347
|
label: "ElevenLabs Voice Changer (multilingual STS v2)",
|
|
1305
|
-
inputs: [{ kind: "audio", mimes:
|
|
1348
|
+
inputs: [{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }],
|
|
1306
1349
|
required: ["voice"],
|
|
1307
1350
|
params: {
|
|
1308
1351
|
voice: { kind: "string" },
|
|
@@ -1329,7 +1372,7 @@ var MODEL_REGISTRY = {
|
|
|
1329
1372
|
},
|
|
1330
1373
|
"elevenlabs/video-background-music-v1": {
|
|
1331
1374
|
label: "ElevenLabs Video Background Music v1",
|
|
1332
|
-
inputs: [{ kind: "video", mimes:
|
|
1375
|
+
inputs: [{ kind: "video", mimes: REPLICATE_VIDEO_MIMES }],
|
|
1333
1376
|
required: [],
|
|
1334
1377
|
params: {
|
|
1335
1378
|
description: { kind: "string" },
|
|
@@ -1500,7 +1543,7 @@ function validateValue(key, value, schema, model) {
|
|
|
1500
1543
|
}
|
|
1501
1544
|
|
|
1502
1545
|
// src/engine/lib/concurrency.ts
|
|
1503
|
-
var DEFAULT_CONCURRENCY =
|
|
1546
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
1504
1547
|
function resolveConcurrency(...candidates) {
|
|
1505
1548
|
for (const candidate of candidates) {
|
|
1506
1549
|
if (candidate === void 0 || candidate === "") continue;
|
|
@@ -1562,6 +1605,160 @@ function encodeRandom() {
|
|
|
1562
1605
|
return out;
|
|
1563
1606
|
}
|
|
1564
1607
|
|
|
1608
|
+
// src/engine/storage/remote-cache-store.ts
|
|
1609
|
+
var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
|
|
1610
|
+
function isPersistedAssetRef(ref) {
|
|
1611
|
+
const { url, sha256 } = ref;
|
|
1612
|
+
if (typeof url !== "string" || typeof sha256 !== "string") return false;
|
|
1613
|
+
return url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256.slice(0, 2)}/${sha256}`) || url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256}`);
|
|
1614
|
+
}
|
|
1615
|
+
function isAssetRefLike(value) {
|
|
1616
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
|
|
1617
|
+
}
|
|
1618
|
+
function collectAssetRefLikes(value, out = []) {
|
|
1619
|
+
if (Array.isArray(value)) {
|
|
1620
|
+
for (const item of value) collectAssetRefLikes(item, out);
|
|
1621
|
+
return out;
|
|
1622
|
+
}
|
|
1623
|
+
if (typeof value !== "object" || value === null) return out;
|
|
1624
|
+
if (isAssetRefLike(value)) {
|
|
1625
|
+
out.push(value);
|
|
1626
|
+
}
|
|
1627
|
+
for (const item of Object.values(value)) collectAssetRefLikes(item, out);
|
|
1628
|
+
return out;
|
|
1629
|
+
}
|
|
1630
|
+
function entryFullyPersisted(entry) {
|
|
1631
|
+
return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
|
|
1632
|
+
}
|
|
1633
|
+
function stripLocalFields(entry) {
|
|
1634
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1635
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1636
|
+
delete ref.path;
|
|
1637
|
+
delete ref.bytes;
|
|
1638
|
+
}
|
|
1639
|
+
return clone;
|
|
1640
|
+
}
|
|
1641
|
+
var RemoteCacheStore = class {
|
|
1642
|
+
client;
|
|
1643
|
+
log;
|
|
1644
|
+
constructor(client, log) {
|
|
1645
|
+
this.client = client;
|
|
1646
|
+
this.log = log ?? (() => void 0);
|
|
1647
|
+
}
|
|
1648
|
+
async get(cacheKey) {
|
|
1649
|
+
return await this.client.getCacheEntry(cacheKey);
|
|
1650
|
+
}
|
|
1651
|
+
async put(entry) {
|
|
1652
|
+
if (!entryFullyPersisted(entry)) {
|
|
1653
|
+
this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const stripped = stripLocalFields(entry);
|
|
1657
|
+
if (stripped.refs.length > MAX_REMOTE_REFS) {
|
|
1658
|
+
stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
|
|
1659
|
+
}
|
|
1660
|
+
await this.client.putCacheEntry(stripped);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
var MAX_REMOTE_REFS = 512;
|
|
1664
|
+
var LayeredCacheStore = class {
|
|
1665
|
+
rootDir;
|
|
1666
|
+
local;
|
|
1667
|
+
remote;
|
|
1668
|
+
assets;
|
|
1669
|
+
log;
|
|
1670
|
+
constructor(opts) {
|
|
1671
|
+
this.local = opts.local;
|
|
1672
|
+
this.remote = opts.remote;
|
|
1673
|
+
this.assets = opts.assets;
|
|
1674
|
+
this.rootDir = opts.local.rootDir;
|
|
1675
|
+
this.log = opts.log ?? (() => void 0);
|
|
1676
|
+
}
|
|
1677
|
+
async get(cacheKey) {
|
|
1678
|
+
const localHit = await this.local.get(cacheKey);
|
|
1679
|
+
if (localHit) return localHit;
|
|
1680
|
+
let remoteEntry;
|
|
1681
|
+
try {
|
|
1682
|
+
remoteEntry = await this.remote.get(cacheKey);
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
if (!remoteEntry) return null;
|
|
1688
|
+
let rehydrated;
|
|
1689
|
+
try {
|
|
1690
|
+
rehydrated = await this.rehydrate(remoteEntry);
|
|
1691
|
+
} catch (e) {
|
|
1692
|
+
this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
await this.local.put(rehydrated);
|
|
1696
|
+
return rehydrated;
|
|
1697
|
+
}
|
|
1698
|
+
async put(entry) {
|
|
1699
|
+
await this.local.put(entry);
|
|
1700
|
+
try {
|
|
1701
|
+
await this.remote.put(entry);
|
|
1702
|
+
} catch (e) {
|
|
1703
|
+
this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Download every referenced asset into the local content-addressed store
|
|
1708
|
+
* (sha-verified) and stamp fresh local paths. Any ref that cannot be
|
|
1709
|
+
* rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
|
|
1710
|
+
* crash materialization later with a far less actionable error.
|
|
1711
|
+
*/
|
|
1712
|
+
async rehydrate(entry) {
|
|
1713
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1714
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1715
|
+
if (!isPersistedAssetRef(ref)) {
|
|
1716
|
+
throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
|
|
1717
|
+
}
|
|
1718
|
+
const ingested = await this.assets.ingestRemote({
|
|
1719
|
+
kind: typeof ref.kind === "string" ? ref.kind : "json",
|
|
1720
|
+
url: ref.url,
|
|
1721
|
+
sha256: ref.sha256,
|
|
1722
|
+
mime: ref.mime,
|
|
1723
|
+
metadata: ref.metadata ?? void 0
|
|
1724
|
+
});
|
|
1725
|
+
ref.path = ingested.path;
|
|
1726
|
+
}
|
|
1727
|
+
return clone;
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
function message(e) {
|
|
1731
|
+
return e instanceof Error ? e.message : String(e);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/engine/nodes/remote/upload.ts
|
|
1735
|
+
async function presignAndPut(args) {
|
|
1736
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1737
|
+
const putRes = await fetch(putUrl, {
|
|
1738
|
+
method: "PUT",
|
|
1739
|
+
body: new Uint8Array(args.bytes),
|
|
1740
|
+
headers: { "Content-Type": args.mime },
|
|
1741
|
+
signal: args.ctx.signal
|
|
1742
|
+
});
|
|
1743
|
+
if (!putRes.ok) {
|
|
1744
|
+
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
1745
|
+
}
|
|
1746
|
+
return publicUrl;
|
|
1747
|
+
}
|
|
1748
|
+
async function ensureUploaded(ref, ctx) {
|
|
1749
|
+
if (ref.url) return ref;
|
|
1750
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1751
|
+
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1752
|
+
return { ...ref, url };
|
|
1753
|
+
}
|
|
1754
|
+
async function persistOutputAssetUrls(outputs, ctx) {
|
|
1755
|
+
for (const ref of collectAssetRefLikes(outputs)) {
|
|
1756
|
+
if (isPersistedAssetRef(ref)) continue;
|
|
1757
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1758
|
+
ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1565
1762
|
// src/engine/schema/canvas.ts
|
|
1566
1763
|
import { z } from "zod";
|
|
1567
1764
|
var REF_PREFIX = "$ref:";
|
|
@@ -1591,7 +1788,14 @@ var NodeDecl = z.object({
|
|
|
1591
1788
|
version: z.string().min(1).optional(),
|
|
1592
1789
|
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
1593
1790
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
1594
|
-
when: z.unknown().optional()
|
|
1791
|
+
when: z.unknown().optional(),
|
|
1792
|
+
// Regenerate knob. The engine is content-addressed: identical params + inputs
|
|
1793
|
+
// return the cached render, so re-running an unchanged node NEVER re-bills or
|
|
1794
|
+
// produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
|
|
1795
|
+
// a note) and re-run to force THIS node to render fresh; because its new output
|
|
1796
|
+
// changes downstream input hashes, everything depending on it regenerates too.
|
|
1797
|
+
// This is the declarative "change a value, re-run, get a new render" affordance.
|
|
1798
|
+
regenerate: z.union([z.string(), z.number()]).optional()
|
|
1595
1799
|
}).strict();
|
|
1596
1800
|
var OutputRef = z.object({
|
|
1597
1801
|
node: z.string(),
|
|
@@ -2841,6 +3045,7 @@ var Engine = class {
|
|
|
2841
3045
|
cache;
|
|
2842
3046
|
outputsDir;
|
|
2843
3047
|
log;
|
|
3048
|
+
persistAssets;
|
|
2844
3049
|
constructor(opts) {
|
|
2845
3050
|
this.registry = opts.registry;
|
|
2846
3051
|
this.client = opts.client;
|
|
@@ -2848,6 +3053,7 @@ var Engine = class {
|
|
|
2848
3053
|
this.cache = opts.cache;
|
|
2849
3054
|
this.outputsDir = opts.outputsDir;
|
|
2850
3055
|
this.log = opts.log ?? (() => void 0);
|
|
3056
|
+
this.persistAssets = opts.persistAssets ?? false;
|
|
2851
3057
|
}
|
|
2852
3058
|
validate(canvas) {
|
|
2853
3059
|
return validateCanvas(canvas, this.registry);
|
|
@@ -2871,7 +3077,16 @@ var Engine = class {
|
|
|
2871
3077
|
const outputs = {};
|
|
2872
3078
|
const counters = { cachedNodes: 0, totalCredits: 0 };
|
|
2873
3079
|
const nodeRuns = [];
|
|
2874
|
-
|
|
3080
|
+
const graph = this.pruneToOutput(canvas, buildGraph(canvas));
|
|
3081
|
+
const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
|
|
3082
|
+
this.emitProgress(opts, {
|
|
3083
|
+
kind: "plan",
|
|
3084
|
+
nodes: [...graph.entries()].map(([id, deps]) => {
|
|
3085
|
+
const node = canvas.nodes.find((n) => n.id === id);
|
|
3086
|
+
return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
|
|
3087
|
+
})
|
|
3088
|
+
});
|
|
3089
|
+
await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes);
|
|
2875
3090
|
const output = pickFinalOutput(canvas, outputs);
|
|
2876
3091
|
const stats = {
|
|
2877
3092
|
total_nodes: canvas.nodes.length,
|
|
@@ -2894,39 +3109,51 @@ var Engine = class {
|
|
|
2894
3109
|
`[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
|
|
2895
3110
|
);
|
|
2896
3111
|
this.log(`outputs in: ${writer.runDir}`);
|
|
2897
|
-
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
|
|
3112
|
+
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
2898
3113
|
}
|
|
2899
|
-
async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
2900
|
-
const layers = topologicalLayers(
|
|
3114
|
+
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
3115
|
+
const layers = topologicalLayers(graph);
|
|
2901
3116
|
const limit = resolveConcurrency(opts.concurrency);
|
|
2902
3117
|
for (const layer of layers) {
|
|
2903
|
-
const settled = await mapWithConcurrency(
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
(nodeId) => this.executeOne(canvas, nodeId, outputs, runId, writer, opts).then((r) => {
|
|
3118
|
+
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3119
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3120
|
+
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
|
|
2907
3121
|
if (r.cached) counters.cachedNodes++;
|
|
2908
3122
|
counters.totalCredits += r.credits;
|
|
2909
3123
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2910
3124
|
if (node) {
|
|
2911
|
-
|
|
3125
|
+
const run = {
|
|
2912
3126
|
node_id: nodeId,
|
|
2913
3127
|
node_type: node.type,
|
|
2914
3128
|
cached: r.cached,
|
|
2915
3129
|
duration_ms: r.durationMs,
|
|
2916
3130
|
credits: r.credits
|
|
2917
|
-
}
|
|
3131
|
+
};
|
|
3132
|
+
nodeRuns.push(run);
|
|
3133
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
2918
3134
|
}
|
|
2919
|
-
})
|
|
2920
|
-
);
|
|
3135
|
+
});
|
|
3136
|
+
});
|
|
2921
3137
|
const failures = [];
|
|
2922
3138
|
settled.forEach((result, i) => {
|
|
2923
3139
|
const nodeId = layer[i];
|
|
2924
|
-
if (result.status === "rejected" && nodeId)
|
|
3140
|
+
if (result.status === "rejected" && nodeId) {
|
|
3141
|
+
failures.push({ nodeId, reason: result.reason });
|
|
3142
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
|
|
3143
|
+
}
|
|
2925
3144
|
});
|
|
2926
3145
|
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
2927
3146
|
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
2928
3147
|
}
|
|
2929
3148
|
}
|
|
3149
|
+
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
3150
|
+
emitProgress(opts, event) {
|
|
3151
|
+
if (!opts.onProgress) return;
|
|
3152
|
+
try {
|
|
3153
|
+
opts.onProgress(event);
|
|
3154
|
+
} catch {
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
2930
3157
|
/**
|
|
2931
3158
|
* Dead-node elimination: when the canvas declares an `output`, execute only the
|
|
2932
3159
|
* nodes that output transitively depends on. Orphaned nodes (left by an edit or
|
|
@@ -2957,12 +3184,13 @@ var Engine = class {
|
|
|
2957
3184
|
}
|
|
2958
3185
|
await writer.writeManifest("_final", output);
|
|
2959
3186
|
}
|
|
2960
|
-
async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
|
|
3187
|
+
async executeOne(canvas, nodeId, outputs, runId, writer, opts, downloadOutputs) {
|
|
2961
3188
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
2962
3189
|
if (!node) throw new Error(`executor: missing node ${nodeId}`);
|
|
2963
3190
|
const def = this.registry.get(node.type);
|
|
2964
3191
|
if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
|
|
2965
|
-
const
|
|
3192
|
+
const regenerateToken = resolveRegenerateToken(node, opts.regenerate, runId);
|
|
3193
|
+
const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
|
|
2966
3194
|
const policy = opts.cache_policy ?? "read_write";
|
|
2967
3195
|
if (policy !== "bypass") {
|
|
2968
3196
|
const cacheT0 = Date.now();
|
|
@@ -2980,17 +3208,27 @@ var Engine = class {
|
|
|
2980
3208
|
nodeId: node.id,
|
|
2981
3209
|
nodeType: node.type,
|
|
2982
3210
|
cacheKey: prepared.cacheKey,
|
|
3211
|
+
downloadOutputs,
|
|
2983
3212
|
client: this.client,
|
|
2984
3213
|
assets: this.assets,
|
|
2985
3214
|
log: this.log,
|
|
2986
3215
|
signal: opts.signal
|
|
2987
3216
|
};
|
|
2988
|
-
const
|
|
3217
|
+
const preparedForExec = def.location === "local" ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
|
|
3218
|
+
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
2989
3219
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
2990
3220
|
const elapsed = Date.now() - t0;
|
|
2991
3221
|
const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
|
|
2992
3222
|
const outputsObj = result;
|
|
2993
3223
|
outputs[node.id] = outputsObj;
|
|
3224
|
+
if (this.persistAssets) {
|
|
3225
|
+
try {
|
|
3226
|
+
await persistOutputAssetUrls(outputsObj, ctx);
|
|
3227
|
+
} catch (e) {
|
|
3228
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3229
|
+
this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
2994
3232
|
if (policy === "read_write") {
|
|
2995
3233
|
await this.cache.put({
|
|
2996
3234
|
cacheKey: prepared.cacheKey,
|
|
@@ -3019,8 +3257,42 @@ var Engine = class {
|
|
|
3019
3257
|
}
|
|
3020
3258
|
}
|
|
3021
3259
|
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Download any URL-only asset ref reachable in a local node's inputs so the
|
|
3262
|
+
* bytes are on disk before the local runner stages them. Returns a copy —
|
|
3263
|
+
* refs are replaced, never mutated in place, so the producer's cached output
|
|
3264
|
+
* (shared object) keeps its URL-only shape.
|
|
3265
|
+
*/
|
|
3266
|
+
async materializeLocalInputs(inputs) {
|
|
3267
|
+
const fix = async (value) => {
|
|
3268
|
+
if (Array.isArray(value)) return Promise.all(value.map(fix));
|
|
3269
|
+
if (value && typeof value === "object") {
|
|
3270
|
+
const v = value;
|
|
3271
|
+
if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
|
|
3272
|
+
this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
|
|
3273
|
+
return this.assets.ingestRemote({
|
|
3274
|
+
kind: v.kind,
|
|
3275
|
+
url: v.url,
|
|
3276
|
+
sha256: v.sha256,
|
|
3277
|
+
mime: v.mime,
|
|
3278
|
+
metadata: v.metadata
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
const out = {};
|
|
3282
|
+
for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
|
|
3283
|
+
return out;
|
|
3284
|
+
}
|
|
3285
|
+
return value;
|
|
3286
|
+
};
|
|
3287
|
+
return await fix(inputs);
|
|
3288
|
+
}
|
|
3022
3289
|
};
|
|
3023
|
-
|
|
3290
|
+
function resolveRegenerateToken(node, forced, runId) {
|
|
3291
|
+
if (forced?.has(node.id)) return `run:${runId}`;
|
|
3292
|
+
if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
|
|
3293
|
+
return void 0;
|
|
3294
|
+
}
|
|
3295
|
+
async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
|
|
3024
3296
|
const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
|
|
3025
3297
|
const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
|
|
3026
3298
|
const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
|
|
@@ -3033,6 +3305,9 @@ async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
|
|
|
3033
3305
|
throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
|
|
3034
3306
|
}
|
|
3035
3307
|
}
|
|
3308
|
+
if (regenerateToken !== void 0) {
|
|
3309
|
+
extras = { ...extras ?? {}, __regenerate__: regenerateToken };
|
|
3310
|
+
}
|
|
3036
3311
|
const cacheKey = computeCacheKey({
|
|
3037
3312
|
node_id: node.type,
|
|
3038
3313
|
node_version: def.version,
|
|
@@ -3071,6 +3346,16 @@ function pickFinalOutput(canvas, outputs) {
|
|
|
3071
3346
|
const lastOut = outputs[last.id];
|
|
3072
3347
|
return lastOut ? Object.values(lastOut)[0] : void 0;
|
|
3073
3348
|
}
|
|
3349
|
+
function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
3350
|
+
const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
|
|
3351
|
+
const needs = /* @__PURE__ */ new Set();
|
|
3352
|
+
for (const [consumerId, deps] of graph) {
|
|
3353
|
+
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
3354
|
+
if (def?.location !== "local") continue;
|
|
3355
|
+
for (const dep of deps) needs.add(dep);
|
|
3356
|
+
}
|
|
3357
|
+
return needs;
|
|
3358
|
+
}
|
|
3074
3359
|
function buildGraph(canvas) {
|
|
3075
3360
|
const graph = /* @__PURE__ */ new Map();
|
|
3076
3361
|
for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
|
|
@@ -3201,7 +3486,16 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
|
|
|
3201
3486
|
try {
|
|
3202
3487
|
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3203
3488
|
} catch (e) {
|
|
3204
|
-
|
|
3489
|
+
if (value.url) {
|
|
3490
|
+
try {
|
|
3491
|
+
await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
|
|
3492
|
+
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3493
|
+
} catch (e2) {
|
|
3494
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
|
|
3495
|
+
}
|
|
3496
|
+
} else {
|
|
3497
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3498
|
+
}
|
|
3205
3499
|
}
|
|
3206
3500
|
if (bytes.length > MAX_INLINE_TEXT_BYTES) {
|
|
3207
3501
|
throw new NodeExecutionError(nodeId, nodeType, {
|
|
@@ -3309,27 +3603,6 @@ var FontRef = BaseAssetRef.extend({
|
|
|
3309
3603
|
});
|
|
3310
3604
|
var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
|
|
3311
3605
|
|
|
3312
|
-
// src/engine/nodes/remote/upload.ts
|
|
3313
|
-
async function presignAndPut(args) {
|
|
3314
|
-
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
3315
|
-
const putRes = await fetch(putUrl, {
|
|
3316
|
-
method: "PUT",
|
|
3317
|
-
body: new Uint8Array(args.bytes),
|
|
3318
|
-
headers: { "Content-Type": args.mime },
|
|
3319
|
-
signal: args.ctx.signal
|
|
3320
|
-
});
|
|
3321
|
-
if (!putRes.ok) {
|
|
3322
|
-
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
3323
|
-
}
|
|
3324
|
-
return publicUrl;
|
|
3325
|
-
}
|
|
3326
|
-
async function ensureUploaded(ref, ctx) {
|
|
3327
|
-
if (ref.url) return ref;
|
|
3328
|
-
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
3329
|
-
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
3330
|
-
return { ...ref, url };
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
3606
|
// src/engine/nodes/remote/delegate.ts
|
|
3334
3607
|
function delegated(spec) {
|
|
3335
3608
|
return {
|
|
@@ -3357,7 +3630,9 @@ async function callBackendExec(args) {
|
|
|
3357
3630
|
nodeVersion: args.nodeVersion,
|
|
3358
3631
|
params: args.params,
|
|
3359
3632
|
inputs: serialized,
|
|
3360
|
-
idempotency_key: idempotencyKey
|
|
3633
|
+
idempotency_key: idempotencyKey,
|
|
3634
|
+
canvas_run_id: args.ctx.canvasRunId,
|
|
3635
|
+
node_id: args.ctx.nodeId
|
|
3361
3636
|
},
|
|
3362
3637
|
args.ctx.signal
|
|
3363
3638
|
);
|
|
@@ -3409,6 +3684,9 @@ async function ingestValue(value, ctx, declaredKind) {
|
|
|
3409
3684
|
}
|
|
3410
3685
|
if (isRawAsset(value)) {
|
|
3411
3686
|
const kind = value.kind ?? declaredKind ?? "json";
|
|
3687
|
+
if (ctx.downloadOutputs === false) {
|
|
3688
|
+
return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
|
|
3689
|
+
}
|
|
3412
3690
|
return ctx.assets.ingestRemote({
|
|
3413
3691
|
kind,
|
|
3414
3692
|
url: value.url,
|
|
@@ -3523,7 +3801,7 @@ function safePathname(rawUrl) {
|
|
|
3523
3801
|
}
|
|
3524
3802
|
var ingestNode = defineNode({
|
|
3525
3803
|
id: "ingest",
|
|
3526
|
-
version: "1.
|
|
3804
|
+
version: "1.2.0",
|
|
3527
3805
|
category: "io",
|
|
3528
3806
|
location: "local",
|
|
3529
3807
|
summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Handinger for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
|
|
@@ -3566,6 +3844,9 @@ function runStrategy(strategy, params, ctx) {
|
|
|
3566
3844
|
}
|
|
3567
3845
|
}
|
|
3568
3846
|
async function execDirectFetch(params, ctx) {
|
|
3847
|
+
if (params.expect === "image") {
|
|
3848
|
+
return ingestImageUrl(params.url, ctx);
|
|
3849
|
+
}
|
|
3569
3850
|
const result = await callBackendExec({
|
|
3570
3851
|
nodeType: "ingest",
|
|
3571
3852
|
nodeVersion: ingestNode.version,
|
|
@@ -3576,6 +3857,37 @@ async function execDirectFetch(params, ctx) {
|
|
|
3576
3857
|
});
|
|
3577
3858
|
return assertAssetOutput(result, params.expect);
|
|
3578
3859
|
}
|
|
3860
|
+
async function ingestImageUrl(url, ctx) {
|
|
3861
|
+
const res = await fetch(url);
|
|
3862
|
+
if (!res.ok) {
|
|
3863
|
+
throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
|
|
3864
|
+
}
|
|
3865
|
+
const ab = await res.arrayBuffer();
|
|
3866
|
+
if (ab.byteLength > MAX_ASSET_BYTES) {
|
|
3867
|
+
throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
|
|
3868
|
+
}
|
|
3869
|
+
let normalized;
|
|
3870
|
+
try {
|
|
3871
|
+
normalized = await toModelSafeImage(Buffer.from(ab));
|
|
3872
|
+
} catch (e) {
|
|
3873
|
+
throw localExecError(ctx, `${url}: ${e.message}`);
|
|
3874
|
+
}
|
|
3875
|
+
if (normalized.rasterizedFrom) {
|
|
3876
|
+
ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
|
|
3877
|
+
}
|
|
3878
|
+
return uploadAndIngest({
|
|
3879
|
+
bytes: normalized.bytes,
|
|
3880
|
+
kind: "image",
|
|
3881
|
+
mime: normalized.mime,
|
|
3882
|
+
metadata: {
|
|
3883
|
+
source_url: url,
|
|
3884
|
+
strategy: "direct_fetch",
|
|
3885
|
+
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3886
|
+
...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
|
|
3887
|
+
},
|
|
3888
|
+
ctx
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3579
3891
|
async function execHandinger(params, ctx) {
|
|
3580
3892
|
const result = await callBackendExec({
|
|
3581
3893
|
nodeType: "ingest",
|
|
@@ -3612,7 +3924,15 @@ var EXT_TO_MIME = {
|
|
|
3612
3924
|
jpeg: "image/jpeg",
|
|
3613
3925
|
webp: "image/webp",
|
|
3614
3926
|
gif: "image/gif",
|
|
3927
|
+
// Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
|
|
3928
|
+
// must resolve to an image mime here or the kind-check rejects the local file
|
|
3929
|
+
// before normalization ever runs.
|
|
3615
3930
|
avif: "image/avif",
|
|
3931
|
+
heic: "image/heic",
|
|
3932
|
+
heif: "image/heif",
|
|
3933
|
+
tif: "image/tiff",
|
|
3934
|
+
tiff: "image/tiff",
|
|
3935
|
+
bmp: "image/bmp",
|
|
3616
3936
|
mp4: "video/mp4",
|
|
3617
3937
|
webm: "video/webm",
|
|
3618
3938
|
mov: "video/quicktime",
|
|
@@ -3663,15 +3983,45 @@ async function rasterizeSvgToPng(bytes) {
|
|
|
3663
3983
|
}
|
|
3664
3984
|
return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
|
|
3665
3985
|
}
|
|
3986
|
+
var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
3987
|
+
async function toModelSafeImage(bytes) {
|
|
3988
|
+
const safe = sniffImageMime(bytes);
|
|
3989
|
+
if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
|
|
3990
|
+
return { bytes, mime: safe };
|
|
3991
|
+
}
|
|
3992
|
+
if (sniffSvg(bytes)) {
|
|
3993
|
+
return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
|
|
3994
|
+
}
|
|
3995
|
+
const { default: sharp } = await import("sharp");
|
|
3996
|
+
try {
|
|
3997
|
+
const img = sharp(bytes);
|
|
3998
|
+
const format = (await img.metadata()).format;
|
|
3999
|
+
const png = await img.png({ force: true }).toBuffer();
|
|
4000
|
+
return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
|
|
4001
|
+
} catch (e) {
|
|
4002
|
+
throw new Error(`bytes are not a decodable image (${e.message})`);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
function hasAscii(buf, offset, sig) {
|
|
4006
|
+
return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
|
|
4007
|
+
}
|
|
4008
|
+
var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
|
|
4009
|
+
function sniffIsoBmff(buf) {
|
|
4010
|
+
if (!hasAscii(buf, 4, "ftyp")) return null;
|
|
4011
|
+
const brand = buf.subarray(8, 12).toString("ascii");
|
|
4012
|
+
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
4013
|
+
if (HEIC_BRANDS.has(brand)) return "image/heic";
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
3666
4016
|
function sniffImageMime(buf) {
|
|
3667
4017
|
if (buf.length < 4) return null;
|
|
3668
|
-
if (buf[0] === 137 && buf
|
|
4018
|
+
if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
|
|
3669
4019
|
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
3670
|
-
if (buf
|
|
3671
|
-
if (buf
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
return
|
|
4020
|
+
if (hasAscii(buf, 0, "GIF")) return "image/gif";
|
|
4021
|
+
if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
|
|
4022
|
+
if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
|
|
4023
|
+
if (hasAscii(buf, 0, "BM")) return "image/bmp";
|
|
4024
|
+
return sniffIsoBmff(buf);
|
|
3675
4025
|
}
|
|
3676
4026
|
function findBoxPayload(buf, start, end, type) {
|
|
3677
4027
|
let offset = start;
|
|
@@ -3724,10 +4074,10 @@ function inferKindFromMime(mime) {
|
|
|
3724
4074
|
if (mime.startsWith("font/")) return "font";
|
|
3725
4075
|
return null;
|
|
3726
4076
|
}
|
|
3727
|
-
function localExecError(ctx,
|
|
4077
|
+
function localExecError(ctx, message2) {
|
|
3728
4078
|
return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
|
|
3729
4079
|
kind: "local",
|
|
3730
|
-
cause: new Error(`ingest: ${
|
|
4080
|
+
cause: new Error(`ingest: ${message2}`)
|
|
3731
4081
|
});
|
|
3732
4082
|
}
|
|
3733
4083
|
async function execLocalFile(params, ctx) {
|
|
@@ -3774,17 +4124,20 @@ async function execLocalFile(params, ctx) {
|
|
|
3774
4124
|
ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
|
|
3775
4125
|
let outBytes = bytes;
|
|
3776
4126
|
let outMime = mime;
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4127
|
+
let rasterizedFrom;
|
|
4128
|
+
if (kind === "image") {
|
|
4129
|
+
const normalized = await toModelSafeImage(bytes);
|
|
4130
|
+
outBytes = normalized.bytes;
|
|
4131
|
+
outMime = normalized.mime;
|
|
4132
|
+
rasterizedFrom = normalized.rasterizedFrom;
|
|
4133
|
+
if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
|
|
3781
4134
|
}
|
|
3782
4135
|
const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
|
|
3783
4136
|
const ref = await uploadAndIngest({
|
|
3784
4137
|
bytes: outBytes,
|
|
3785
4138
|
kind: params.expect,
|
|
3786
4139
|
mime: outMime,
|
|
3787
|
-
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
|
|
4140
|
+
metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
|
|
3788
4141
|
ctx
|
|
3789
4142
|
});
|
|
3790
4143
|
return withProbedDuration(ref, durationMs);
|
|
@@ -3802,7 +4155,7 @@ function localFileMetadata(args) {
|
|
|
3802
4155
|
ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3803
4156
|
file_size: args.fileSize,
|
|
3804
4157
|
original_filename: path3.basename(args.absPath),
|
|
3805
|
-
...args.
|
|
4158
|
+
...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
|
|
3806
4159
|
...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
|
|
3807
4160
|
};
|
|
3808
4161
|
}
|
|
@@ -4394,7 +4747,7 @@ async function refToUrl(ref) {
|
|
|
4394
4747
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4395
4748
|
}
|
|
4396
4749
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
4397
|
-
function
|
|
4750
|
+
function isAssetRefLike2(value) {
|
|
4398
4751
|
if (!value || typeof value !== "object") return false;
|
|
4399
4752
|
const v = value;
|
|
4400
4753
|
return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
|
|
@@ -4766,8 +5119,8 @@ var NEVER_BLOCK = [
|
|
|
4766
5119
|
/text[_-]?occluded/i
|
|
4767
5120
|
];
|
|
4768
5121
|
var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
|
|
4769
|
-
function isAdvisory(code,
|
|
4770
|
-
const hay = `${code} ${
|
|
5122
|
+
function isAdvisory(code, message2) {
|
|
5123
|
+
const hay = `${code} ${message2}`;
|
|
4771
5124
|
return NEVER_BLOCK.some((re) => re.test(hay));
|
|
4772
5125
|
}
|
|
4773
5126
|
function parseCheckJson(raw) {
|
|
@@ -4795,10 +5148,10 @@ function classifyLint(json) {
|
|
|
4795
5148
|
for (const f of findings) {
|
|
4796
5149
|
const rec = f;
|
|
4797
5150
|
const code = String(rec?.code ?? "");
|
|
4798
|
-
const
|
|
5151
|
+
const message2 = String(rec?.message ?? "");
|
|
4799
5152
|
const severity = String(rec?.severity ?? "info");
|
|
4800
|
-
const blocking = severity === "error" && !isAdvisory(code,
|
|
4801
|
-
out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
|
|
5153
|
+
const blocking = severity === "error" && !isAdvisory(code, message2);
|
|
5154
|
+
out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
|
|
4802
5155
|
}
|
|
4803
5156
|
return out;
|
|
4804
5157
|
}
|
|
@@ -4810,9 +5163,9 @@ function classifyInspect(json) {
|
|
|
4810
5163
|
for (const iss of issues) {
|
|
4811
5164
|
const rec = iss;
|
|
4812
5165
|
const code = String(rec?.code ?? rec?.type ?? "overflow");
|
|
4813
|
-
const
|
|
5166
|
+
const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
|
|
4814
5167
|
const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
|
|
4815
|
-
out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
|
|
5168
|
+
out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
|
|
4816
5169
|
}
|
|
4817
5170
|
return out;
|
|
4818
5171
|
}
|
|
@@ -5257,7 +5610,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
|
|
|
5257
5610
|
}
|
|
5258
5611
|
function coerceImageParam(value) {
|
|
5259
5612
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5260
|
-
if (
|
|
5613
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5261
5614
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5262
5615
|
}
|
|
5263
5616
|
async function substituteCompositionFiles(tmp, values) {
|
|
@@ -5487,7 +5840,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
|
|
|
5487
5840
|
}
|
|
5488
5841
|
function coerceImageParam2(value) {
|
|
5489
5842
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5490
|
-
if (
|
|
5843
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5491
5844
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5492
5845
|
}
|
|
5493
5846
|
async function substituteCompositionFiles2(tmp, values) {
|
|
@@ -6513,17 +6866,29 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6513
6866
|
const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
|
|
6514
6867
|
const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
|
|
6515
6868
|
const creds = requireCredentialsFromEnv();
|
|
6869
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
6870
|
+
const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
|
|
6871
|
+
const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
|
|
6872
|
+
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
6873
|
+
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
6874
|
+
local: localCache,
|
|
6875
|
+
remote: new RemoteCacheStore(client, opts.log),
|
|
6876
|
+
assets,
|
|
6877
|
+
log: opts.log
|
|
6878
|
+
}) : localCache;
|
|
6516
6879
|
return new Engine({
|
|
6517
6880
|
registry: defaultRegistry(),
|
|
6518
|
-
client
|
|
6519
|
-
assets
|
|
6520
|
-
cache
|
|
6881
|
+
client,
|
|
6882
|
+
assets,
|
|
6883
|
+
cache,
|
|
6521
6884
|
outputsDir,
|
|
6522
|
-
log: opts.log
|
|
6885
|
+
log: opts.log,
|
|
6886
|
+
persistAssets: remoteCacheEnabled
|
|
6523
6887
|
});
|
|
6524
6888
|
}
|
|
6525
6889
|
|
|
6526
6890
|
export {
|
|
6891
|
+
requireCredentialsFromEnv,
|
|
6527
6892
|
LayerExecutionError,
|
|
6528
6893
|
describeFailureReason,
|
|
6529
6894
|
SEEDANCE_DURATIONS,
|
|
@@ -6531,7 +6896,14 @@ export {
|
|
|
6531
6896
|
IMAGE_GENERATE_MODELS,
|
|
6532
6897
|
MODEL_REGISTRY,
|
|
6533
6898
|
resolveConcurrency,
|
|
6899
|
+
ulid,
|
|
6900
|
+
isPersistedAssetRef,
|
|
6901
|
+
collectAssetRefLikes,
|
|
6902
|
+
REF_PREFIX,
|
|
6903
|
+
parseRefExpr,
|
|
6904
|
+
sha256Hex,
|
|
6534
6905
|
elementMentionKeywords,
|
|
6906
|
+
toModelSafeImage,
|
|
6535
6907
|
BackendClient2 as BackendClient,
|
|
6536
6908
|
Engine2 as Engine,
|
|
6537
6909
|
LocalAssetStore2 as LocalAssetStore,
|
|
@@ -6542,4 +6914,4 @@ export {
|
|
|
6542
6914
|
defaultRegistry,
|
|
6543
6915
|
createEngineFromEnv
|
|
6544
6916
|
};
|
|
6545
|
-
//# sourceMappingURL=chunk-
|
|
6917
|
+
//# sourceMappingURL=chunk-43KBQLP5.js.map
|