@koda-sl/baker-cli 0.122.0 → 0.123.0-dev.31b784126

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.
@@ -3,9 +3,9 @@ import {
3
3
  __toESM
4
4
  } from "./chunk-5WRI5ZAA.js";
5
5
 
6
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
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
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
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 message = `Object can not safely be stringified. Received type ${typeof value2}`;
142
- if (typeof value2 !== "function") message += ` (${value2.toString()})`;
143
- throw new Error(message);
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 === "POST" ? { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` } : { Authorization: `Bearer ${this.apiKey}` },
683
- body: method === "POST" ? JSON.stringify(body) : void 0,
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, message) {
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
- var JOB_POLL_INTERVAL_MS = 3e3;
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
- while (true) {
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(JOB_POLL_INTERVAL_MS);
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(message) {
847
- super(message);
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
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
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 FAL_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
963
- var FAL_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
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 FAL_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
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 fal.ai (not OpenRouter) because OpenRouter's Seedance
1184
- // passthrough rejects photorealistic human reference frames via
1185
- // ByteDance's "real person" safety filter.
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: FAL_IMAGE_MIMES }],
1226
+ optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
1189
1227
  required: ["prompt"],
1190
1228
  params: {
1191
- prompt: { kind: "string" },
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
- person_generation: { kind: "string", enum: ["allow_all"] },
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: FAL_VIDEO_MIMES },
1269
- { kind: "audio", mimes: FAL_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: FAL_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: FAL_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 = 5;
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
- await this.runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns);
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(this.pruneToOutput(canvas, buildGraph(canvas)));
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
- layer,
2905
- limit,
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
- nodeRuns.push({
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) failures.push({ nodeId, reason: result.reason });
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 prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, this.assets);
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 { parsedInputs, parsedParams } = parseNodeArgs(def, prepared, node.id, node.type);
3217
+ const preparedForExec = needsLocalMaterialization(def) ? { ...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
- async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
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,
@@ -3061,6 +3336,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
3061
3336
  throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3062
3337
  }
3063
3338
  }
3339
+ function needsLocalMaterialization(def) {
3340
+ return def.location === "local" && !def.passthroughRefs;
3341
+ }
3064
3342
  function pickFinalOutput(canvas, outputs) {
3065
3343
  if (canvas.output) {
3066
3344
  const node = outputs[canvas.output.node];
@@ -3071,6 +3349,16 @@ function pickFinalOutput(canvas, outputs) {
3071
3349
  const lastOut = outputs[last.id];
3072
3350
  return lastOut ? Object.values(lastOut)[0] : void 0;
3073
3351
  }
3352
+ function computeNeedsLocalBytes(canvas, graph, registry) {
3353
+ const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
3354
+ const needs = /* @__PURE__ */ new Set();
3355
+ for (const [consumerId, deps] of graph) {
3356
+ const def = registry.get(typeById.get(consumerId) ?? "");
3357
+ if (def?.location !== "local" || def.passthroughRefs) continue;
3358
+ for (const dep of deps) needs.add(dep);
3359
+ }
3360
+ return needs;
3361
+ }
3074
3362
  function buildGraph(canvas) {
3075
3363
  const graph = /* @__PURE__ */ new Map();
3076
3364
  for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
@@ -3201,7 +3489,16 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
3201
3489
  try {
3202
3490
  bytes = await assets.readBytes(value.sha256, value.mime);
3203
3491
  } catch (e) {
3204
- throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3492
+ if (value.url) {
3493
+ try {
3494
+ await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
3495
+ bytes = await assets.readBytes(value.sha256, value.mime);
3496
+ } catch (e2) {
3497
+ throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
3498
+ }
3499
+ } else {
3500
+ throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3501
+ }
3205
3502
  }
3206
3503
  if (bytes.length > MAX_INLINE_TEXT_BYTES) {
3207
3504
  throw new NodeExecutionError(nodeId, nodeType, {
@@ -3309,27 +3606,6 @@ var FontRef = BaseAssetRef.extend({
3309
3606
  });
3310
3607
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
3311
3608
 
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
3609
  // src/engine/nodes/remote/delegate.ts
3334
3610
  function delegated(spec) {
3335
3611
  return {
@@ -3357,7 +3633,9 @@ async function callBackendExec(args) {
3357
3633
  nodeVersion: args.nodeVersion,
3358
3634
  params: args.params,
3359
3635
  inputs: serialized,
3360
- idempotency_key: idempotencyKey
3636
+ idempotency_key: idempotencyKey,
3637
+ canvas_run_id: args.ctx.canvasRunId,
3638
+ node_id: args.ctx.nodeId
3361
3639
  },
3362
3640
  args.ctx.signal
3363
3641
  );
@@ -3409,6 +3687,9 @@ async function ingestValue(value, ctx, declaredKind) {
3409
3687
  }
3410
3688
  if (isRawAsset(value)) {
3411
3689
  const kind = value.kind ?? declaredKind ?? "json";
3690
+ if (ctx.downloadOutputs === false) {
3691
+ return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
3692
+ }
3412
3693
  return ctx.assets.ingestRemote({
3413
3694
  kind,
3414
3695
  url: value.url,
@@ -3523,7 +3804,7 @@ function safePathname(rawUrl) {
3523
3804
  }
3524
3805
  var ingestNode = defineNode({
3525
3806
  id: "ingest",
3526
- version: "1.1.0",
3807
+ version: "1.2.0",
3527
3808
  category: "io",
3528
3809
  location: "local",
3529
3810
  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 +3847,9 @@ function runStrategy(strategy, params, ctx) {
3566
3847
  }
3567
3848
  }
3568
3849
  async function execDirectFetch(params, ctx) {
3850
+ if (params.expect === "image") {
3851
+ return ingestImageUrl(params.url, ctx);
3852
+ }
3569
3853
  const result = await callBackendExec({
3570
3854
  nodeType: "ingest",
3571
3855
  nodeVersion: ingestNode.version,
@@ -3576,6 +3860,37 @@ async function execDirectFetch(params, ctx) {
3576
3860
  });
3577
3861
  return assertAssetOutput(result, params.expect);
3578
3862
  }
3863
+ async function ingestImageUrl(url, ctx) {
3864
+ const res = await fetch(url);
3865
+ if (!res.ok) {
3866
+ throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
3867
+ }
3868
+ const ab = await res.arrayBuffer();
3869
+ if (ab.byteLength > MAX_ASSET_BYTES) {
3870
+ throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
3871
+ }
3872
+ let normalized;
3873
+ try {
3874
+ normalized = await toModelSafeImage(Buffer.from(ab));
3875
+ } catch (e) {
3876
+ throw localExecError(ctx, `${url}: ${e.message}`);
3877
+ }
3878
+ if (normalized.rasterizedFrom) {
3879
+ ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
3880
+ }
3881
+ return uploadAndIngest({
3882
+ bytes: normalized.bytes,
3883
+ kind: "image",
3884
+ mime: normalized.mime,
3885
+ metadata: {
3886
+ source_url: url,
3887
+ strategy: "direct_fetch",
3888
+ ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
3889
+ ...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
3890
+ },
3891
+ ctx
3892
+ });
3893
+ }
3579
3894
  async function execHandinger(params, ctx) {
3580
3895
  const result = await callBackendExec({
3581
3896
  nodeType: "ingest",
@@ -3612,7 +3927,15 @@ var EXT_TO_MIME = {
3612
3927
  jpeg: "image/jpeg",
3613
3928
  webp: "image/webp",
3614
3929
  gif: "image/gif",
3930
+ // Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
3931
+ // must resolve to an image mime here or the kind-check rejects the local file
3932
+ // before normalization ever runs.
3615
3933
  avif: "image/avif",
3934
+ heic: "image/heic",
3935
+ heif: "image/heif",
3936
+ tif: "image/tiff",
3937
+ tiff: "image/tiff",
3938
+ bmp: "image/bmp",
3616
3939
  mp4: "video/mp4",
3617
3940
  webm: "video/webm",
3618
3941
  mov: "video/quicktime",
@@ -3663,15 +3986,45 @@ async function rasterizeSvgToPng(bytes) {
3663
3986
  }
3664
3987
  return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
3665
3988
  }
3989
+ var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
3990
+ async function toModelSafeImage(bytes) {
3991
+ const safe = sniffImageMime(bytes);
3992
+ if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
3993
+ return { bytes, mime: safe };
3994
+ }
3995
+ if (sniffSvg(bytes)) {
3996
+ return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
3997
+ }
3998
+ const { default: sharp } = await import("sharp");
3999
+ try {
4000
+ const img = sharp(bytes);
4001
+ const format = (await img.metadata()).format;
4002
+ const png = await img.png({ force: true }).toBuffer();
4003
+ return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
4004
+ } catch (e) {
4005
+ throw new Error(`bytes are not a decodable image (${e.message})`);
4006
+ }
4007
+ }
4008
+ function hasAscii(buf, offset, sig) {
4009
+ return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
4010
+ }
4011
+ var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
4012
+ function sniffIsoBmff(buf) {
4013
+ if (!hasAscii(buf, 4, "ftyp")) return null;
4014
+ const brand = buf.subarray(8, 12).toString("ascii");
4015
+ if (brand === "avif" || brand === "avis") return "image/avif";
4016
+ if (HEIC_BRANDS.has(brand)) return "image/heic";
4017
+ return null;
4018
+ }
3666
4019
  function sniffImageMime(buf) {
3667
4020
  if (buf.length < 4) return null;
3668
- if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
4021
+ if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
3669
4022
  if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
3670
- if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
3671
- if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
3672
- return "image/webp";
3673
- }
3674
- return null;
4023
+ if (hasAscii(buf, 0, "GIF")) return "image/gif";
4024
+ if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
4025
+ if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
4026
+ if (hasAscii(buf, 0, "BM")) return "image/bmp";
4027
+ return sniffIsoBmff(buf);
3675
4028
  }
3676
4029
  function findBoxPayload(buf, start, end, type) {
3677
4030
  let offset = start;
@@ -3724,10 +4077,10 @@ function inferKindFromMime(mime) {
3724
4077
  if (mime.startsWith("font/")) return "font";
3725
4078
  return null;
3726
4079
  }
3727
- function localExecError(ctx, message) {
4080
+ function localExecError(ctx, message2) {
3728
4081
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3729
4082
  kind: "local",
3730
- cause: new Error(`ingest: ${message}`)
4083
+ cause: new Error(`ingest: ${message2}`)
3731
4084
  });
3732
4085
  }
3733
4086
  async function execLocalFile(params, ctx) {
@@ -3774,17 +4127,20 @@ async function execLocalFile(params, ctx) {
3774
4127
  ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
3775
4128
  let outBytes = bytes;
3776
4129
  let outMime = mime;
3777
- if (mime === SVG_MIME) {
3778
- outBytes = await rasterizeSvgToPng(bytes);
3779
- outMime = "image/png";
3780
- ctx.log(`ingest: rasterized SVG -> PNG (${outBytes.length}B)`);
4130
+ let rasterizedFrom;
4131
+ if (kind === "image") {
4132
+ const normalized = await toModelSafeImage(bytes);
4133
+ outBytes = normalized.bytes;
4134
+ outMime = normalized.mime;
4135
+ rasterizedFrom = normalized.rasterizedFrom;
4136
+ if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
3781
4137
  }
3782
4138
  const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
3783
4139
  const ref = await uploadAndIngest({
3784
4140
  bytes: outBytes,
3785
4141
  kind: params.expect,
3786
4142
  mime: outMime,
3787
- metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
4143
+ metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
3788
4144
  ctx
3789
4145
  });
3790
4146
  return withProbedDuration(ref, durationMs);
@@ -3802,7 +4158,7 @@ function localFileMetadata(args) {
3802
4158
  ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
3803
4159
  file_size: args.fileSize,
3804
4160
  original_filename: path3.basename(args.absPath),
3805
- ...args.mime === SVG_MIME ? { rasterized_from: "svg" } : {},
4161
+ ...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
3806
4162
  ...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
3807
4163
  };
3808
4164
  }
@@ -4329,19 +4685,56 @@ var audioTimelineNode = defineNode({
4329
4685
  }
4330
4686
  });
4331
4687
 
4332
- // src/engine/nodes/local/ffmpeg.ts
4688
+ // src/engine/nodes/local/collect.ts
4333
4689
  import { z as z7 } from "zod";
4690
+ var collectNode = defineNode({
4691
+ id: "collect",
4692
+ version: "1.0.0",
4693
+ category: "data",
4694
+ location: "local",
4695
+ passthroughRefs: true,
4696
+ summary: "Gather images from multiple upstream nodes into one ordered array \u2014 the standard terminal for multi-variant canvases whose final output is several images.",
4697
+ when_to_use: "Point the canvas `output` at this node when several independent branches (e.g. one image_generate per scene/variant) must ALL be finals. Wire `inputs.images` as an array of refs like `$ref:gen_billboard_03.images#0` \u2014 each final inherits its producer node id as its label (or set `params.labels` to override), so variants stay identifiable in the dashboard and selection.",
4698
+ inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
4699
+ params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
4700
+ outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
4701
+ outputKinds: { images: "image" },
4702
+ cost: () => ({ credits: 0, seconds_estimate: 0 }),
4703
+ // Arity is only knowable at validate time when `images` is a literal array;
4704
+ // a single `$ref:` string to an upstream array output defers to runtime.
4705
+ validateExtra: ({ rawParams, rawInputs }) => {
4706
+ const labels = rawParams?.labels;
4707
+ if (!Array.isArray(labels)) return [];
4708
+ if (new Set(labels).size !== labels.length) {
4709
+ return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
4710
+ }
4711
+ const images = rawInputs?.images;
4712
+ if (Array.isArray(images) && labels.length !== images.length) {
4713
+ return [
4714
+ {
4715
+ path: "params.labels",
4716
+ message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
4717
+ }
4718
+ ];
4719
+ }
4720
+ return [];
4721
+ },
4722
+ execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
4723
+ });
4724
+
4725
+ // src/engine/nodes/local/ffmpeg.ts
4726
+ import { z as z8 } from "zod";
4334
4727
  var FFMPEG_BIN2 = "ffmpeg";
4335
- var OutputDecl = z7.object({
4336
- kind: z7.enum(["image", "video", "audio"]),
4337
- ext: z7.string().min(1).max(8)
4728
+ var OutputDecl = z8.object({
4729
+ kind: z8.enum(["image", "video", "audio"]),
4730
+ ext: z8.string().min(1).max(8)
4338
4731
  }).strict();
4339
- var FfmpegParams = z7.object({
4340
- args: z7.array(z7.string()).min(1),
4341
- outputs: z7.record(z7.string(), OutputDecl).default({})
4732
+ var FfmpegParams = z8.object({
4733
+ args: z8.array(z8.string()).min(1),
4734
+ outputs: z8.record(z8.string(), OutputDecl).default({})
4342
4735
  }).strict();
4343
- var FfmpegInputs = z7.record(z7.string(), z7.unknown());
4344
- var FfmpegOutputs = z7.record(z7.string(), z7.custom());
4736
+ var FfmpegInputs = z8.record(z8.string(), z8.unknown());
4737
+ var FfmpegOutputs = z8.record(z8.string(), z8.custom());
4345
4738
  var ffmpegNode = defineNode({
4346
4739
  id: "ffmpeg",
4347
4740
  version: "2.0.0",
@@ -4372,7 +4765,7 @@ import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/prom
4372
4765
  import { createRequire } from "module";
4373
4766
  import { tmpdir as tmpdir3 } from "os";
4374
4767
  import path6 from "path";
4375
- import { z as z8 } from "zod";
4768
+ import { z as z9 } from "zod";
4376
4769
 
4377
4770
  // src/engine/nodes/local/lib/assets.ts
4378
4771
  import { copyFile as copyFile3, readFile as readFile4 } from "fs/promises";
@@ -4394,7 +4787,7 @@ async function refToUrl(ref) {
4394
4787
  return `data:${ref.mime};base64,${bytes.toString("base64")}`;
4395
4788
  }
4396
4789
  var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
4397
- function isAssetRefLike(value) {
4790
+ function isAssetRefLike2(value) {
4398
4791
  if (!value || typeof value !== "object") return false;
4399
4792
  const v = value;
4400
4793
  return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
@@ -4408,15 +4801,15 @@ var DEFAULT_SPECIMEN = [
4408
4801
  "abcdefghijklmnopqrstuvwxyz",
4409
4802
  `0123456789 !?&@#$%().,:;'"-`
4410
4803
  ].join("\n");
4411
- var FontSpecimenParams = z8.object({
4412
- text: z8.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
4413
- font_size: z8.number().int().min(8).max(512).optional().default(72),
4414
- padding: z8.number().int().min(0).max(512).optional().default(64),
4415
- line_height: z8.number().min(0.8).max(3).optional().default(1.35),
4416
- max_width: z8.number().int().min(256).max(4096).optional()
4804
+ var FontSpecimenParams = z9.object({
4805
+ text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
4806
+ font_size: z9.number().int().min(8).max(512).optional().default(72),
4807
+ padding: z9.number().int().min(0).max(512).optional().default(64),
4808
+ line_height: z9.number().min(0.8).max(3).optional().default(1.35),
4809
+ max_width: z9.number().int().min(256).max(4096).optional()
4417
4810
  }).strict();
4418
- var FontSpecimenInputs = z8.object({ font: FontRef }).loose();
4419
- var FontSpecimenOutputs = z8.object({ image: ImageRef }).strict();
4811
+ var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
4812
+ var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
4420
4813
  var DEVICE_SCALE_FACTOR = 2;
4421
4814
  var PAGE_TIMEOUT_MS = 3e4;
4422
4815
  function escapeHtml(text) {
@@ -4557,7 +4950,7 @@ import { createRequire as createRequire2 } from "module";
4557
4950
  import { cpus, tmpdir as tmpdir4 } from "os";
4558
4951
  import path11 from "path";
4559
4952
  import { promisify as promisify4 } from "util";
4560
- import { z as z10 } from "zod";
4953
+ import { z as z11 } from "zod";
4561
4954
 
4562
4955
  // src/engine/engine/composition-hash.ts
4563
4956
  import { readdir as readdir2, readFile as readFile5, stat as stat4 } from "fs/promises";
@@ -4595,62 +4988,62 @@ async function collectFiles(root, current) {
4595
4988
  // src/engine/engine/composition-meta.ts
4596
4989
  import { readFile as readFile6 } from "fs/promises";
4597
4990
  import path8 from "path";
4598
- import { z as z9 } from "zod";
4599
- var InputKind = z9.enum(["video", "image", "audio", "json"]);
4600
- var InputSpec = z9.object({
4991
+ import { z as z10 } from "zod";
4992
+ var InputKind = z10.enum(["video", "image", "audio", "json"]);
4993
+ var InputSpec = z10.object({
4601
4994
  kind: InputKind,
4602
- required: z9.boolean().optional().default(false),
4995
+ required: z10.boolean().optional().default(false),
4603
4996
  // Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
4604
4997
  // Defaults to `<key><ext>` derived from the kind.
4605
- staged_as: z9.string().min(1).optional(),
4606
- description: z9.string().optional()
4998
+ staged_as: z10.string().min(1).optional(),
4999
+ description: z10.string().optional()
4607
5000
  }).strict();
4608
5001
  var ParamSpecBase = {
4609
- required: z9.boolean().optional().default(false),
4610
- description: z9.string().optional()
5002
+ required: z10.boolean().optional().default(false),
5003
+ description: z10.string().optional()
4611
5004
  };
4612
- var StringParam = z9.object({
5005
+ var StringParam = z10.object({
4613
5006
  ...ParamSpecBase,
4614
- kind: z9.literal("string"),
4615
- default: z9.string().optional(),
4616
- enum: z9.array(z9.string()).optional()
5007
+ kind: z10.literal("string"),
5008
+ default: z10.string().optional(),
5009
+ enum: z10.array(z10.string()).optional()
4617
5010
  }).strict();
4618
- var IntegerParam = z9.object({
5011
+ var IntegerParam = z10.object({
4619
5012
  ...ParamSpecBase,
4620
- kind: z9.literal("integer"),
4621
- default: z9.number().int().optional(),
4622
- min: z9.number().int().optional(),
4623
- max: z9.number().int().optional()
5013
+ kind: z10.literal("integer"),
5014
+ default: z10.number().int().optional(),
5015
+ min: z10.number().int().optional(),
5016
+ max: z10.number().int().optional()
4624
5017
  }).strict();
4625
- var NumberParam = z9.object({
5018
+ var NumberParam = z10.object({
4626
5019
  ...ParamSpecBase,
4627
- kind: z9.literal("number"),
4628
- default: z9.number().optional(),
4629
- min: z9.number().optional(),
4630
- max: z9.number().optional()
5020
+ kind: z10.literal("number"),
5021
+ default: z10.number().optional(),
5022
+ min: z10.number().optional(),
5023
+ max: z10.number().optional()
4631
5024
  }).strict();
4632
- var BooleanParam = z9.object({
5025
+ var BooleanParam = z10.object({
4633
5026
  ...ParamSpecBase,
4634
- kind: z9.literal("boolean"),
4635
- default: z9.boolean().optional()
5027
+ kind: z10.literal("boolean"),
5028
+ default: z10.boolean().optional()
4636
5029
  }).strict();
4637
- var ColorParam = z9.object({
5030
+ var ColorParam = z10.object({
4638
5031
  ...ParamSpecBase,
4639
- kind: z9.literal("color"),
4640
- default: z9.string().optional()
5032
+ kind: z10.literal("color"),
5033
+ default: z10.string().optional()
4641
5034
  }).strict();
4642
- var ImageParam = z9.object({
5035
+ var ImageParam = z10.object({
4643
5036
  ...ParamSpecBase,
4644
- kind: z9.literal("image"),
4645
- default: z9.string().optional()
5037
+ kind: z10.literal("image"),
5038
+ default: z10.string().optional()
4646
5039
  }).strict();
4647
- var JsonParam = z9.object({
5040
+ var JsonParam = z10.object({
4648
5041
  ...ParamSpecBase,
4649
- kind: z9.literal("json"),
4650
- schema: z9.unknown().optional(),
4651
- default: z9.unknown().optional()
5042
+ kind: z10.literal("json"),
5043
+ schema: z10.unknown().optional(),
5044
+ default: z10.unknown().optional()
4652
5045
  }).strict();
4653
- var ParamSpec = z9.discriminatedUnion("kind", [
5046
+ var ParamSpec = z10.discriminatedUnion("kind", [
4654
5047
  StringParam,
4655
5048
  IntegerParam,
4656
5049
  NumberParam,
@@ -4659,16 +5052,16 @@ var ParamSpec = z9.discriminatedUnion("kind", [
4659
5052
  ImageParam,
4660
5053
  JsonParam
4661
5054
  ]);
4662
- var CompositionMetaSchema = z9.object({
4663
- id: z9.string().min(1),
4664
- title: z9.string().min(1),
4665
- description: z9.string().optional(),
4666
- width: z9.number().int().positive(),
4667
- height: z9.number().int().positive(),
4668
- fps: z9.number().int().positive().default(30),
4669
- default_duration: z9.number().positive().default(10),
4670
- inputs: z9.record(z9.string(), InputSpec).default({}),
4671
- params: z9.record(z9.string(), ParamSpec).default({})
5055
+ var CompositionMetaSchema = z10.object({
5056
+ id: z10.string().min(1),
5057
+ title: z10.string().min(1),
5058
+ description: z10.string().optional(),
5059
+ width: z10.number().int().positive(),
5060
+ height: z10.number().int().positive(),
5061
+ fps: z10.number().int().positive().default(30),
5062
+ default_duration: z10.number().positive().default(10),
5063
+ inputs: z10.record(z10.string(), InputSpec).default({}),
5064
+ params: z10.record(z10.string(), ParamSpec).default({})
4672
5065
  }).strict();
4673
5066
  async function loadCompositionMeta(compositionDir) {
4674
5067
  const metaPath = path8.join(compositionDir, "meta.json");
@@ -4696,39 +5089,39 @@ function buildParamsSchema(meta) {
4696
5089
  for (const [name, spec] of Object.entries(meta.params)) {
4697
5090
  shape[name] = buildParamFieldSchema(name, spec);
4698
5091
  }
4699
- return z9.object(shape).strict();
5092
+ return z10.object(shape).strict();
4700
5093
  }
4701
5094
  function buildParamFieldSchema(name, spec) {
4702
5095
  switch (spec.kind) {
4703
5096
  case "string": {
4704
- const s = spec.enum && spec.enum.length > 0 ? z9.enum(spec.enum) : z9.string();
5097
+ const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
4705
5098
  return finalize(s, spec.default, spec.required);
4706
5099
  }
4707
5100
  case "integer": {
4708
- let s = z9.number().int();
5101
+ let s = z10.number().int();
4709
5102
  if (spec.min !== void 0) s = s.min(spec.min);
4710
5103
  if (spec.max !== void 0) s = s.max(spec.max);
4711
5104
  return finalize(s, spec.default, spec.required);
4712
5105
  }
4713
5106
  case "number": {
4714
- let s = z9.number();
5107
+ let s = z10.number();
4715
5108
  if (spec.min !== void 0) s = s.min(spec.min);
4716
5109
  if (spec.max !== void 0) s = s.max(spec.max);
4717
5110
  return finalize(s, spec.default, spec.required);
4718
5111
  }
4719
5112
  case "boolean":
4720
- return finalize(z9.boolean(), spec.default, spec.required);
5113
+ return finalize(z10.boolean(), spec.default, spec.required);
4721
5114
  case "color": {
4722
- const s = z9.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
5115
+ const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
4723
5116
  message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
4724
5117
  });
4725
5118
  return finalize(s, spec.default, spec.required);
4726
5119
  }
4727
5120
  case "image":
4728
- return finalize(z9.union([z9.string().min(1), z9.record(z9.string(), z9.unknown())]), spec.default, spec.required);
5121
+ return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
4729
5122
  case "json":
4730
5123
  return finalize(
4731
- z9.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
5124
+ z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
4732
5125
  spec.default,
4733
5126
  spec.required
4734
5127
  );
@@ -4766,8 +5159,8 @@ var NEVER_BLOCK = [
4766
5159
  /text[_-]?occluded/i
4767
5160
  ];
4768
5161
  var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
4769
- function isAdvisory(code, message) {
4770
- const hay = `${code} ${message}`;
5162
+ function isAdvisory(code, message2) {
5163
+ const hay = `${code} ${message2}`;
4771
5164
  return NEVER_BLOCK.some((re) => re.test(hay));
4772
5165
  }
4773
5166
  function parseCheckJson(raw) {
@@ -4795,10 +5188,10 @@ function classifyLint(json) {
4795
5188
  for (const f of findings) {
4796
5189
  const rec = f;
4797
5190
  const code = String(rec?.code ?? "");
4798
- const message = String(rec?.message ?? "");
5191
+ const message2 = String(rec?.message ?? "");
4799
5192
  const severity = String(rec?.severity ?? "info");
4800
- const blocking = severity === "error" && !isAdvisory(code, message);
4801
- out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
5193
+ const blocking = severity === "error" && !isAdvisory(code, message2);
5194
+ out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
4802
5195
  }
4803
5196
  return out;
4804
5197
  }
@@ -4810,9 +5203,9 @@ function classifyInspect(json) {
4810
5203
  for (const iss of issues) {
4811
5204
  const rec = iss;
4812
5205
  const code = String(rec?.code ?? rec?.type ?? "overflow");
4813
- const message = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
5206
+ const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4814
5207
  const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
4815
- out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
5208
+ out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
4816
5209
  }
4817
5210
  return out;
4818
5211
  }
@@ -5028,17 +5421,17 @@ function literalize(value) {
5028
5421
  // src/engine/nodes/local/hyperframe.ts
5029
5422
  var execFileAsync2 = promisify4(execFile4);
5030
5423
  var require_2 = createRequire2(import.meta.url);
5031
- var HyperframeParams = z10.object({
5032
- composition: z10.string().min(1),
5424
+ var HyperframeParams = z11.object({
5425
+ composition: z11.string().min(1),
5033
5426
  // Output container. mp4 (default) for delivery; webm/mov render WITH
5034
5427
  // transparency (alpha) when the composition background is transparent —
5035
5428
  // use for motion-graphic overlays dropped into Premiere/AE/Nuke.
5036
- format: z10.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
5037
- timeout_ms: z10.number().int().positive().optional().default(10 * 60 * 1e3)
5038
- }).catchall(z10.unknown());
5039
- var HyperframeInputs = z10.record(z10.string(), z10.custom()).optional().default({});
5040
- var HyperframeOutputs = z10.object({
5041
- video: z10.custom()
5429
+ format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
5430
+ timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
5431
+ }).catchall(z11.unknown());
5432
+ var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
5433
+ var HyperframeOutputs = z11.object({
5434
+ video: z11.custom()
5042
5435
  }).strict();
5043
5436
  var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
5044
5437
  var MIME_BY_FORMAT = {
@@ -5257,7 +5650,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
5257
5650
  }
5258
5651
  function coerceImageParam(value) {
5259
5652
  if (typeof value === "string") return Promise.resolve(value);
5260
- if (isAssetRefLike(value)) return refToUrl(value);
5653
+ if (isAssetRefLike2(value)) return refToUrl(value);
5261
5654
  throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
5262
5655
  }
5263
5656
  async function substituteCompositionFiles(tmp, values) {
@@ -5327,23 +5720,23 @@ import { createRequire as createRequire3 } from "module";
5327
5720
  import { tmpdir as tmpdir5 } from "os";
5328
5721
  import path12 from "path";
5329
5722
  import { promisify as promisify5 } from "util";
5330
- import { z as z11 } from "zod";
5723
+ import { z as z12 } from "zod";
5331
5724
  var _execFileAsync = promisify5(execFile5);
5332
5725
  var require_3 = createRequire3(import.meta.url);
5333
- var WaitForSpec = z11.discriminatedUnion("kind", [
5334
- z11.object({ kind: z11.literal("auto") }),
5335
- z11.object({ kind: z11.literal("selector"), value: z11.string().min(1) }),
5336
- z11.object({ kind: z11.literal("function"), value: z11.string().min(1) }),
5337
- z11.object({ kind: z11.literal("timeout"), ms: z11.number().int().min(0).max(6e4) })
5726
+ var WaitForSpec = z12.discriminatedUnion("kind", [
5727
+ z12.object({ kind: z12.literal("auto") }),
5728
+ z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
5729
+ z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
5730
+ z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
5338
5731
  ]);
5339
- var HyperframeSnapshotParams = z11.object({
5340
- composition: z11.string().min(1),
5732
+ var HyperframeSnapshotParams = z12.object({
5733
+ composition: z12.string().min(1),
5341
5734
  wait_for: WaitForSpec.optional().default({ kind: "auto" }),
5342
- timeout_ms: z11.number().int().positive().optional().default(6e4)
5343
- }).catchall(z11.unknown());
5344
- var HyperframeSnapshotInputs = z11.record(z11.string(), z11.custom()).optional().default({});
5345
- var HyperframeSnapshotOutputs = z11.object({
5346
- image: z11.custom()
5735
+ timeout_ms: z12.number().int().positive().optional().default(6e4)
5736
+ }).catchall(z12.unknown());
5737
+ var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
5738
+ var HyperframeSnapshotOutputs = z12.object({
5739
+ image: z12.custom()
5347
5740
  }).strict();
5348
5741
  var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
5349
5742
  var DEVICE_SCALE_FACTOR2 = 2;
@@ -5487,7 +5880,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
5487
5880
  }
5488
5881
  function coerceImageParam2(value) {
5489
5882
  if (typeof value === "string") return Promise.resolve(value);
5490
- if (isAssetRefLike(value)) return refToUrl(value);
5883
+ if (isAssetRefLike2(value)) return refToUrl(value);
5491
5884
  throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
5492
5885
  }
5493
5886
  async function substituteCompositionFiles2(tmp, values) {
@@ -5535,18 +5928,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
5535
5928
  // src/engine/nodes/local/imagemagick.ts
5536
5929
  import { execFile as execFile6 } from "child_process";
5537
5930
  import { promisify as promisify6 } from "util";
5538
- import { z as z12 } from "zod";
5931
+ import { z as z13 } from "zod";
5539
5932
  var execFileAsync3 = promisify6(execFile6);
5540
- var OutputDecl2 = z12.object({
5541
- kind: z12.enum(["image", "video", "audio"]),
5542
- ext: z12.string().min(1).max(8)
5933
+ var OutputDecl2 = z13.object({
5934
+ kind: z13.enum(["image", "video", "audio"]),
5935
+ ext: z13.string().min(1).max(8)
5543
5936
  }).strict();
5544
- var ImageMagickParams = z12.object({
5545
- args: z12.array(z12.string()).min(1),
5546
- outputs: z12.record(z12.string(), OutputDecl2).default({})
5937
+ var ImageMagickParams = z13.object({
5938
+ args: z13.array(z13.string()).min(1),
5939
+ outputs: z13.record(z13.string(), OutputDecl2).default({})
5547
5940
  }).strict();
5548
- var ImageMagickInputs = z12.record(z12.string(), z12.unknown());
5549
- var ImageMagickOutputs = z12.record(z12.string(), z12.custom());
5941
+ var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
5942
+ var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
5550
5943
  var resolvedBin;
5551
5944
  async function resolveBin() {
5552
5945
  if (resolvedBin) return resolvedBin;
@@ -5588,29 +5981,29 @@ var imagemagickNode = defineNode({
5588
5981
  });
5589
5982
 
5590
5983
  // src/engine/nodes/local/text.ts
5591
- import { z as z13 } from "zod";
5984
+ import { z as z14 } from "zod";
5592
5985
  var textNode = defineNode({
5593
5986
  id: "text",
5594
5987
  version: "1.0.0",
5595
5988
  category: "data",
5596
5989
  location: "local",
5597
5990
  summary: "A literal text value. Use for prompts, descriptions, copy.",
5598
- inputs: z13.object({}).strict(),
5599
- params: z13.object({ value: z13.string() }).strict(),
5600
- outputs: z13.object({ text: z13.string() }).strict(),
5991
+ inputs: z14.object({}).strict(),
5992
+ params: z14.object({ value: z14.string() }).strict(),
5993
+ outputs: z14.object({ text: z14.string() }).strict(),
5601
5994
  cost: () => ({ credits: 0, seconds_estimate: 0 }),
5602
5995
  execute: ({ params }) => Promise.resolve({ text: params.value })
5603
5996
  });
5604
5997
 
5605
5998
  // src/engine/nodes/remote/audioVoiceConvert.ts
5606
- import { z as z14 } from "zod";
5607
- var AudioVoiceConvertParams = z14.object({
5608
- model: z14.literal("elevenlabs/eleven_multilingual_sts_v2"),
5999
+ import { z as z15 } from "zod";
6000
+ var AudioVoiceConvertParams = z15.object({
6001
+ model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
5609
6002
  /** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
5610
- voice: z14.string().min(1),
5611
- output_format: z14.string().optional(),
6003
+ voice: z15.string().min(1),
6004
+ output_format: z15.string().optional(),
5612
6005
  /** Strip the source clip's background noise before re-voicing. */
5613
- remove_background_noise: z14.boolean().optional()
6006
+ remove_background_noise: z15.boolean().optional()
5614
6007
  }).strict();
5615
6008
  var audioVoiceConvertNode = delegated({
5616
6009
  id: "audio_voice_convert",
@@ -5618,44 +6011,44 @@ var audioVoiceConvertNode = delegated({
5618
6011
  category: "audio",
5619
6012
  summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
5620
6013
  when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
5621
- inputs: z14.object({
6014
+ inputs: z15.object({
5622
6015
  audio: AudioRef,
5623
6016
  voice_ref: TextRef.optional()
5624
6017
  }).strict(),
5625
6018
  params: AudioVoiceConvertParams,
5626
- outputs: z14.object({ audio: AudioRef }).strict(),
6019
+ outputs: z15.object({ audio: AudioRef }).strict(),
5627
6020
  outputKinds: { audio: "audio" },
5628
6021
  cost: () => ({ credits: 1, seconds_estimate: 20 })
5629
6022
  });
5630
6023
 
5631
6024
  // src/engine/nodes/remote/dialogue.ts
5632
- import { z as z15 } from "zod";
5633
- var DialogueInput = z15.object({
5634
- text: z15.string().min(1),
5635
- voice_id: z15.string().min(1)
6025
+ import { z as z16 } from "zod";
6026
+ var DialogueInput = z16.object({
6027
+ text: z16.string().min(1),
6028
+ voice_id: z16.string().min(1)
5636
6029
  });
5637
6030
  var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
5638
- var DialogueParams = z15.object({
5639
- model: z15.enum(DIALOGUE_MODELS),
6031
+ var DialogueParams = z16.object({
6032
+ model: z16.enum(DIALOGUE_MODELS),
5640
6033
  /**
5641
6034
  * Ordered list of lines, each tagged with the voice that should speak it.
5642
6035
  * Up to 10 unique voice_ids; total text across all lines should stay under
5643
6036
  * ~2000 characters for best quality (ElevenLabs guidance).
5644
6037
  */
5645
- inputs: z15.array(DialogueInput).min(1).max(50),
5646
- language_code: z15.string().optional(),
6038
+ inputs: z16.array(DialogueInput).min(1).max(50),
6039
+ language_code: z16.string().optional(),
5647
6040
  /** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
5648
- settings: z15.record(z15.string(), z15.unknown()).optional(),
5649
- seed: z15.number().int().min(0).max(4294967295).optional(),
5650
- apply_text_normalization: z15.enum(["auto", "on", "off"]).optional(),
6041
+ settings: z16.record(z16.string(), z16.unknown()).optional(),
6042
+ seed: z16.number().int().min(0).max(4294967295).optional(),
6043
+ apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
5651
6044
  /**
5652
6045
  * When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
5653
6046
  * separate `timestamps` output — character-level alignment plus
5654
6047
  * per-voice segment markers usable for captions, lipsync, or
5655
6048
  * beat-matched cuts in ad creatives.
5656
6049
  */
5657
- with_timestamps: z15.boolean().optional(),
5658
- output_format: z15.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6050
+ with_timestamps: z16.boolean().optional(),
6051
+ output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5659
6052
  }).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
5660
6053
  message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
5661
6054
  path: ["inputs"]
@@ -5666,9 +6059,9 @@ var dialogueNode = delegated({
5666
6059
  category: "audio",
5667
6060
  summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
5668
6061
  when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
5669
- inputs: z15.object({}).loose(),
6062
+ inputs: z16.object({}).loose(),
5670
6063
  params: DialogueParams,
5671
- outputs: z15.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6064
+ outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
5672
6065
  outputKinds: { audio: "audio", timestamps: "json" },
5673
6066
  cost: ({ params }) => {
5674
6067
  const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
@@ -5677,7 +6070,7 @@ var dialogueNode = delegated({
5677
6070
  });
5678
6071
 
5679
6072
  // src/engine/nodes/remote/image.ts
5680
- import { z as z16 } from "zod";
6073
+ import { z as z17 } from "zod";
5681
6074
  var IMAGE_GENERATE_MODELS2 = [
5682
6075
  "openai/gpt-5.4-image-2",
5683
6076
  "google/gemini-3.5-flash",
@@ -5685,16 +6078,16 @@ var IMAGE_GENERATE_MODELS2 = [
5685
6078
  "google/gemini-3-pro-image-preview",
5686
6079
  "recraft/recraft-v4.1-pro-vector"
5687
6080
  ];
5688
- var ImageGenerateParams = z16.object({
5689
- model: z16.enum(IMAGE_GENERATE_MODELS2),
5690
- prompt: z16.string().min(1),
5691
- aspect_ratio: z16.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
5692
- image_size: z16.enum(["0.5K", "1K", "2K", "4K"]).optional(),
6081
+ var ImageGenerateParams = z17.object({
6082
+ model: z17.enum(IMAGE_GENERATE_MODELS2),
6083
+ prompt: z17.string().min(1),
6084
+ aspect_ratio: z17.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
6085
+ image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
5693
6086
  // Recraft v4 vector controls — forwarded into `image_config`. Registry
5694
6087
  // rejects them on non-Recraft models.
5695
- strength: z16.number().min(0).max(1).optional(),
5696
- rgb_colors: z16.array(z16.array(z16.number().int().min(0).max(255))).optional(),
5697
- background_rgb_color: z16.array(z16.number().int().min(0).max(255)).optional()
6088
+ strength: z17.number().min(0).max(1).optional(),
6089
+ rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
6090
+ background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
5698
6091
  }).strict();
5699
6092
  var imageGenerateNode = delegated({
5700
6093
  id: "image_generate",
@@ -5704,22 +6097,22 @@ var imageGenerateNode = delegated({
5704
6097
  when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-5.4-image-2` for photorealistic; Gemini variants for fast iteration and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order.",
5705
6098
  // `reference` is one image or an ordered array of images. The backend forwards
5706
6099
  // each as a separate `image_url` to the provider (OpenRouter accepts many).
5707
- inputs: z16.object({ reference: z16.union([ImageRef, z16.array(ImageRef).min(1)]).optional() }).loose(),
6100
+ inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
5708
6101
  params: ImageGenerateParams,
5709
- outputs: z16.object({ images: z16.array(ImageRef).min(1) }).strict(),
6102
+ outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
5710
6103
  outputKinds: { images: "image" },
5711
6104
  cost: () => ({ credits: 5, seconds_estimate: 10 })
5712
6105
  });
5713
6106
 
5714
6107
  // src/engine/nodes/remote/imageAspectAdapt.ts
5715
- import { z as z17 } from "zod";
6108
+ import { z as z18 } from "zod";
5716
6109
  var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
5717
6110
  var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
5718
- var ImageAspectAdaptParams = z17.object({
5719
- model: z17.enum(ASPECT_ADAPT_MODELS),
5720
- formats: z17.array(z17.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }),
5721
- guidance: z17.string().min(1).optional(),
5722
- image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional()
6111
+ var ImageAspectAdaptParams = z18.object({
6112
+ model: z18.enum(ASPECT_ADAPT_MODELS),
6113
+ formats: z18.array(z18.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }),
6114
+ guidance: z18.string().min(1).optional(),
6115
+ image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
5723
6116
  }).strict();
5724
6117
  var imageAspectAdaptNode = delegated({
5725
6118
  id: "image_aspect_adapt",
@@ -5727,9 +6120,9 @@ var imageAspectAdaptNode = delegated({
5727
6120
  category: "image",
5728
6121
  summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as `formats`.",
5729
6122
  when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and list the target ratios in `formats`. Cost is estimated per format; formats matching the source ratio are free pass-throughs. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
5730
- inputs: z17.object({ source: ImageRef }).loose(),
6123
+ inputs: z18.object({ source: ImageRef }).loose(),
5731
6124
  params: ImageAspectAdaptParams,
5732
- outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
6125
+ outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
5733
6126
  outputKinds: { images: "image" },
5734
6127
  cost: ({ params }) => {
5735
6128
  const p = params;
@@ -5742,12 +6135,12 @@ var imageAspectAdaptNode = delegated({
5742
6135
  });
5743
6136
 
5744
6137
  // src/engine/nodes/remote/imageBackgroundRemove.ts
5745
- import { z as z18 } from "zod";
5746
- var ImageBackgroundRemoveParams = z18.object({
5747
- model: z18.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
5748
- model_variant: z18.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
5749
- operating_resolution: z18.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
5750
- mask_only: z18.boolean().optional().default(false)
6138
+ import { z as z19 } from "zod";
6139
+ var ImageBackgroundRemoveParams = z19.object({
6140
+ model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
6141
+ model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
6142
+ operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
6143
+ mask_only: z19.boolean().optional().default(false)
5751
6144
  }).strict();
5752
6145
  var imageBackgroundRemoveNode = delegated({
5753
6146
  id: "image_background_remove",
@@ -5755,11 +6148,11 @@ var imageBackgroundRemoveNode = delegated({
5755
6148
  category: "image",
5756
6149
  summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
5757
6150
  when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. Set `mask_only:true` to return the binary mask instead of the alpha-cut image.",
5758
- inputs: z18.object({
6151
+ inputs: z19.object({
5759
6152
  image: ImageRef
5760
6153
  }).strict(),
5761
6154
  params: ImageBackgroundRemoveParams,
5762
- outputs: z18.object({
6155
+ outputs: z19.object({
5763
6156
  image: ImageRef,
5764
6157
  mask: ImageRef.optional()
5765
6158
  }).strict(),
@@ -5768,7 +6161,7 @@ var imageBackgroundRemoveNode = delegated({
5768
6161
  });
5769
6162
 
5770
6163
  // src/engine/nodes/remote/imageDescribe.ts
5771
- import { z as z19 } from "zod";
6164
+ import { z as z20 } from "zod";
5772
6165
  var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
5773
6166
  var imageDescribeNode = delegated({
5774
6167
  id: "image_describe",
@@ -5776,33 +6169,33 @@ var imageDescribeNode = delegated({
5776
6169
  category: "vision",
5777
6170
  summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
5778
6171
  when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
5779
- inputs: z19.object({ image: ImageRef }).loose(),
5780
- params: z19.object({
5781
- model: z19.enum(IMAGE_DESCRIBE_MODELS),
5782
- focus: z19.string().optional(),
5783
- context: z19.string().optional(),
5784
- temperature: z19.number().min(0).max(2).optional(),
5785
- max_tokens: z19.number().int().positive().optional()
6172
+ inputs: z20.object({ image: ImageRef }).loose(),
6173
+ params: z20.object({
6174
+ model: z20.enum(IMAGE_DESCRIBE_MODELS),
6175
+ focus: z20.string().optional(),
6176
+ context: z20.string().optional(),
6177
+ temperature: z20.number().min(0).max(2).optional(),
6178
+ max_tokens: z20.number().int().positive().optional()
5786
6179
  }).strict(),
5787
- outputs: z19.object({ description: JsonRef }).strict(),
6180
+ outputs: z20.object({ description: JsonRef }).strict(),
5788
6181
  outputKinds: { description: "json" },
5789
6182
  cost: () => ({ credits: 2, seconds_estimate: 10 })
5790
6183
  });
5791
6184
 
5792
6185
  // src/engine/nodes/remote/imageReferenceSheet.ts
5793
- import { z as z20 } from "zod";
6186
+ import { z as z21 } from "zod";
5794
6187
  var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
5795
- var ImageReferenceSheetParams = z20.object({
5796
- model: z20.enum(REFERENCE_SHEET_MODELS),
5797
- subject_description: z20.string().min(1),
6188
+ var ImageReferenceSheetParams = z21.object({
6189
+ model: z21.enum(REFERENCE_SHEET_MODELS),
6190
+ subject_description: z21.string().min(1),
5798
6191
  // `location` = a set/room shown from several camera ANGLES (not a rotated subject),
5799
6192
  // so a multi-scene shoot keeps one consistent set.
5800
- subject_type: z20.enum(["character", "person", "product", "location"]),
5801
- views: z20.array(z20.string().min(1)).min(2).max(8).optional(),
5802
- style: z20.string().optional(),
5803
- prompt_override: z20.string().min(1).optional(),
5804
- aspect_ratio: z20.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
5805
- image_size: z20.enum(["0.5K", "1K", "2K", "4K"]).optional()
6193
+ subject_type: z21.enum(["character", "person", "product", "location"]),
6194
+ views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
6195
+ style: z21.string().optional(),
6196
+ prompt_override: z21.string().min(1).optional(),
6197
+ aspect_ratio: z21.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
6198
+ image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
5806
6199
  }).strict();
5807
6200
  var imageReferenceSheetNode = delegated({
5808
6201
  id: "image_reference_sheet",
@@ -5810,9 +6203,9 @@ var imageReferenceSheetNode = delegated({
5810
6203
  category: "image",
5811
6204
  summary: "Fuse 1\u20136 images of a single subject (person, character, product, or location/set) into ONE multi-view reference sheet \u2014 a labeled grid in consistent style and lighting: a turnaround (FRONT / SIDE / BACK\u2026) for a person/character/product, or several camera angles of the same room (WIDE / REVERSE / DETAIL\u2026) for a location. Curated models: Gemini 3 Pro Image (best fusion + labels), Gemini 3.1 Flash Image (cheap iteration).",
5812
6205
  when_to_use: "Use before image_generate / video_generate when a subject must stay consistent across many creatives \u2014 wire the `sheet` output into their `reference` input instead of re-describing the subject per prompt. `subject_description` should be the exact wording you reuse downstream. Pick `google/gemini-3-pro-image-preview` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image-preview` while iterating.",
5813
- inputs: z20.object({ references: z20.array(ImageRef).min(1).max(6) }).loose(),
6206
+ inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
5814
6207
  params: ImageReferenceSheetParams,
5815
- outputs: z20.object({ sheet: ImageRef }).strict(),
6208
+ outputs: z21.object({ sheet: ImageRef }).strict(),
5816
6209
  outputKinds: { sheet: "image" },
5817
6210
  cost: ({ params }) => ({
5818
6211
  credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
@@ -5821,10 +6214,10 @@ var imageReferenceSheetNode = delegated({
5821
6214
  });
5822
6215
 
5823
6216
  // src/engine/nodes/remote/imageSearch.ts
5824
- import { z as z21 } from "zod";
5825
- var ImageSearchParams = z21.object({
5826
- prompt: z21.string().min(1),
5827
- count: z21.number().int().min(1).max(20).default(5)
6217
+ import { z as z22 } from "zod";
6218
+ var ImageSearchParams = z22.object({
6219
+ prompt: z22.string().min(1),
6220
+ count: z22.number().int().min(1).max(20).default(5)
5828
6221
  }).strict();
5829
6222
  var imageSearchNode = delegated({
5830
6223
  id: "image_search",
@@ -5832,15 +6225,15 @@ var imageSearchNode = delegated({
5832
6225
  category: "image",
5833
6226
  summary: "Agentic image search across Google Images, stock photography (Freepik), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
5834
6227
  when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
5835
- inputs: z21.object({}).loose(),
6228
+ inputs: z22.object({}).loose(),
5836
6229
  params: ImageSearchParams,
5837
- outputs: z21.object({ images: z21.array(ImageRef).min(1) }).strict(),
6230
+ outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
5838
6231
  outputKinds: { images: "image" },
5839
6232
  cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
5840
6233
  });
5841
6234
 
5842
6235
  // src/engine/nodes/remote/imageSelect.ts
5843
- import { z as z22 } from "zod";
6236
+ import { z as z23 } from "zod";
5844
6237
  var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
5845
6238
  var imageSelectNode = delegated({
5846
6239
  id: "image_select",
@@ -5848,15 +6241,15 @@ var imageSelectNode = delegated({
5848
6241
  category: "vision",
5849
6242
  summary: "Pick the best `count` images out of 2+ candidates with a vision LLM, judged against a prompt. Outputs a passthrough subset of the input refs (no new pixels) plus the model's comparative reasoning.",
5850
6243
  when_to_use: "Use after fanning out several image_generate variants (or any pool of 2+ images) to keep only the strongest before expensive downstream steps \u2014 video generation, reference sheets, final delivery. `count` fixes the output size, so `images#0`\u2026`images#count-1` are always safe to wire. Pick `~google/gemini-flash-latest` for cheap/fast picks and `~google/gemini-pro-latest` for harder aesthetic judgement.",
5851
- inputs: z22.object({ images: z22.array(ImageRef).min(2) }).loose(),
5852
- params: z22.object({
5853
- model: z22.enum(IMAGE_SELECT_MODELS),
5854
- prompt: z22.string().min(1),
5855
- count: z22.number().int().min(1).default(1),
5856
- temperature: z22.number().min(0).max(2).optional(),
5857
- max_tokens: z22.number().int().positive().optional()
6244
+ inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
6245
+ params: z23.object({
6246
+ model: z23.enum(IMAGE_SELECT_MODELS),
6247
+ prompt: z23.string().min(1),
6248
+ count: z23.number().int().min(1).default(1),
6249
+ temperature: z23.number().min(0).max(2).optional(),
6250
+ max_tokens: z23.number().int().positive().optional()
5858
6251
  }).strict(),
5859
- outputs: z22.object({ images: z22.array(ImageRef).min(1), reasoning: TextRef }).strict(),
6252
+ outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
5860
6253
  outputKinds: { images: "image", reasoning: "text" },
5861
6254
  cost: () => ({ credits: 1, seconds_estimate: 5 }),
5862
6255
  // Arity is only knowable at validate time when `images` is a literal array
@@ -5881,34 +6274,34 @@ var imageSelectNode = delegated({
5881
6274
  });
5882
6275
 
5883
6276
  // src/engine/nodes/remote/music.ts
5884
- import { z as z23 } from "zod";
6277
+ import { z as z24 } from "zod";
5885
6278
  var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
5886
- var MusicParams = z23.object({
5887
- model: z23.enum(MUSIC_MODELS),
6279
+ var MusicParams = z24.object({
6280
+ model: z24.enum(MUSIC_MODELS),
5888
6281
  /** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
5889
- prompt: z23.string().optional(),
6282
+ prompt: z24.string().optional(),
5890
6283
  /**
5891
6284
  * Structured composition plan (intro / hook / verse / outro sections with
5892
6285
  * per-section styles + durations). Mutually exclusive with `prompt`.
5893
6286
  */
5894
- composition_plan: z23.record(z23.string(), z23.unknown()).optional(),
6287
+ composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
5895
6288
  /** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
5896
- music_length_ms: z23.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
5897
- seed: z23.number().int().optional(),
6289
+ music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
6290
+ seed: z24.number().int().optional(),
5898
6291
  /** Prompt mode only — forces an instrumental (no vocals) track. */
5899
- force_instrumental: z23.boolean().optional(),
6292
+ force_instrumental: z24.boolean().optional(),
5900
6293
  /** composition_plan only — honor exact section durations. */
5901
- respect_sections_durations: z23.boolean().optional(),
6294
+ respect_sections_durations: z24.boolean().optional(),
5902
6295
  /** Emit word-level timestamps alongside the audio. */
5903
- with_timestamps: z23.boolean().optional(),
6296
+ with_timestamps: z24.boolean().optional(),
5904
6297
  /**
5905
6298
  * video-to-music only — short description of the desired score
5906
6299
  * ("upbeat synth, fast cuts, 80s") used to bias the model.
5907
6300
  */
5908
- description: z23.string().max(1e3).optional(),
6301
+ description: z24.string().max(1e3).optional(),
5909
6302
  /** video-to-music only — up to 10 style tags. */
5910
- tags: z23.array(z23.string()).max(10).optional(),
5911
- output_format: z23.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6303
+ tags: z24.array(z24.string()).max(10).optional(),
6304
+ output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5912
6305
  }).strict();
5913
6306
  var musicNode = delegated({
5914
6307
  id: "music",
@@ -5916,9 +6309,9 @@ var musicNode = delegated({
5916
6309
  category: "audio",
5917
6310
  summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
5918
6311
  when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
5919
- inputs: z23.object({ video: VideoRef.optional() }).loose(),
6312
+ inputs: z24.object({ video: VideoRef.optional() }).loose(),
5920
6313
  params: MusicParams,
5921
- outputs: z23.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6314
+ outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
5922
6315
  outputKinds: { audio: "audio", timestamps: "json" },
5923
6316
  cost: ({ params }) => {
5924
6317
  const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
@@ -5949,25 +6342,25 @@ var musicNode = delegated({
5949
6342
  });
5950
6343
 
5951
6344
  // src/engine/nodes/remote/soundEffect.ts
5952
- import { z as z24 } from "zod";
6345
+ import { z as z25 } from "zod";
5953
6346
  var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
5954
- var SoundEffectParams = z24.object({
5955
- model: z24.enum(SOUND_EFFECT_MODELS),
6347
+ var SoundEffectParams = z25.object({
6348
+ model: z25.enum(SOUND_EFFECT_MODELS),
5956
6349
  /** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
5957
- text: z24.string().min(1),
6350
+ text: z25.string().min(1),
5958
6351
  /**
5959
6352
  * Target length in seconds. 0.5–30. Leave unset to let the model pick the
5960
6353
  * natural length for the described effect.
5961
6354
  */
5962
- duration_seconds: z24.number().min(0.5).max(30).optional(),
6355
+ duration_seconds: z25.number().min(0.5).max(30).optional(),
5963
6356
  /**
5964
6357
  * 0–1. Higher = stick closer to the prompt at the cost of variety; lower
5965
6358
  * = let the model interpret more freely. Defaults to 0.3 on the provider.
5966
6359
  */
5967
- prompt_influence: z24.number().min(0).max(1).optional(),
6360
+ prompt_influence: z25.number().min(0).max(1).optional(),
5968
6361
  /** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
5969
- loop: z24.boolean().optional(),
5970
- output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6362
+ loop: z25.boolean().optional(),
6363
+ output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5971
6364
  }).strict();
5972
6365
  var soundEffectNode = delegated({
5973
6366
  id: "sound_effect",
@@ -5975,9 +6368,9 @@ var soundEffectNode = delegated({
5975
6368
  category: "audio",
5976
6369
  summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
5977
6370
  when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
5978
- inputs: z24.object({}).loose(),
6371
+ inputs: z25.object({}).loose(),
5979
6372
  params: SoundEffectParams,
5980
- outputs: z24.object({ audio: AudioRef }).strict(),
6373
+ outputs: z25.object({ audio: AudioRef }).strict(),
5981
6374
  outputKinds: { audio: "audio" },
5982
6375
  cost: ({ params }) => {
5983
6376
  const seconds = params.duration_seconds ?? 5;
@@ -5986,7 +6379,7 @@ var soundEffectNode = delegated({
5986
6379
  });
5987
6380
 
5988
6381
  // src/engine/nodes/remote/textGenerate.ts
5989
- import { z as z25 } from "zod";
6382
+ import { z as z26 } from "zod";
5990
6383
  var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
5991
6384
  var textGenerateNode = delegated({
5992
6385
  id: "text_generate",
@@ -5994,58 +6387,58 @@ var textGenerateNode = delegated({
5994
6387
  category: "language",
5995
6388
  summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
5996
6389
  when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
5997
- inputs: z25.object({}).loose(),
5998
- params: z25.object({
5999
- model: z25.enum(TEXT_GENERATE_MODELS),
6000
- prompt: z25.string().min(1),
6001
- system: z25.string().optional(),
6002
- response_format: z25.enum(["text", "json_object"]).optional(),
6003
- web_search: z25.boolean().optional(),
6004
- temperature: z25.number().min(0).max(2).optional(),
6005
- max_tokens: z25.number().int().positive().optional()
6390
+ inputs: z26.object({}).loose(),
6391
+ params: z26.object({
6392
+ model: z26.enum(TEXT_GENERATE_MODELS),
6393
+ prompt: z26.string().min(1),
6394
+ system: z26.string().optional(),
6395
+ response_format: z26.enum(["text", "json_object"]).optional(),
6396
+ web_search: z26.boolean().optional(),
6397
+ temperature: z26.number().min(0).max(2).optional(),
6398
+ max_tokens: z26.number().int().positive().optional()
6006
6399
  }).strict(),
6007
- outputs: z25.object({ text: TextRef }).strict(),
6400
+ outputs: z26.object({ text: TextRef }).strict(),
6008
6401
  outputKinds: { text: "text" },
6009
6402
  cost: () => ({ credits: 1, seconds_estimate: 3 })
6010
6403
  });
6011
6404
 
6012
6405
  // src/engine/nodes/remote/tts.ts
6013
- import { z as z26 } from "zod";
6406
+ import { z as z27 } from "zod";
6014
6407
  var TTS_MODELS = ["elevenlabs/eleven_v3"];
6015
- var TtsVoiceSettings = z26.object({
6016
- stability: z26.number().min(0).max(1).optional(),
6017
- similarity_boost: z26.number().min(0).max(1).optional(),
6018
- style: z26.number().min(0).max(1).optional(),
6019
- use_speaker_boost: z26.boolean().optional(),
6020
- speed: z26.number().min(0.25).max(4).optional()
6408
+ var TtsVoiceSettings = z27.object({
6409
+ stability: z27.number().min(0).max(1).optional(),
6410
+ similarity_boost: z27.number().min(0).max(1).optional(),
6411
+ style: z27.number().min(0).max(1).optional(),
6412
+ use_speaker_boost: z27.boolean().optional(),
6413
+ speed: z27.number().min(0.25).max(4).optional()
6021
6414
  }).strict();
6022
- var TtsPronunciationLocator = z26.object({
6023
- pronunciation_dictionary_id: z26.string().min(1),
6024
- version_id: z26.string().nullable().optional()
6415
+ var TtsPronunciationLocator = z27.object({
6416
+ pronunciation_dictionary_id: z27.string().min(1),
6417
+ version_id: z27.string().nullable().optional()
6025
6418
  }).strict();
6026
- var TtsParams = z26.object({
6027
- model: z26.enum(TTS_MODELS),
6028
- text: z26.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
6029
- voice: z26.string().min(1),
6419
+ var TtsParams = z27.object({
6420
+ model: z27.enum(TTS_MODELS),
6421
+ text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
6422
+ voice: z27.string().min(1),
6030
6423
  /** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
6031
- output_format: z26.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
6032
- seed: z26.number().int().min(0).max(4294967295).optional(),
6424
+ output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
6425
+ seed: z27.number().int().min(0).max(4294967295).optional(),
6033
6426
  // Top-level shortcuts; structured form is `voice_settings`.
6034
- stability: z26.number().min(0).max(1).optional(),
6035
- similarity_boost: z26.number().min(0).max(1).optional(),
6427
+ stability: z27.number().min(0).max(1).optional(),
6428
+ similarity_boost: z27.number().min(0).max(1).optional(),
6036
6429
  voice_settings: TtsVoiceSettings.optional(),
6037
6430
  /** ISO 639-1 language code. eleven_v3 supports language hints. */
6038
- language_code: z26.string().optional(),
6039
- pronunciation_dictionary_locators: z26.array(TtsPronunciationLocator).max(3).optional(),
6040
- apply_text_normalization: z26.enum(["auto", "on", "off"]).optional(),
6431
+ language_code: z27.string().optional(),
6432
+ pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
6433
+ apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
6041
6434
  /** Currently Japanese-only. Adds latency. */
6042
- apply_language_text_normalization: z26.boolean().optional(),
6435
+ apply_language_text_normalization: z27.boolean().optional(),
6043
6436
  /**
6044
6437
  * When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
6045
6438
  * adds a `timestamps` output (character-level alignment) for caption
6046
6439
  * rendering, lipsync, and beat-matched cuts.
6047
6440
  */
6048
- with_timestamps: z26.boolean().optional()
6441
+ with_timestamps: z27.boolean().optional()
6049
6442
  }).strict();
6050
6443
  var ttsNode = delegated({
6051
6444
  id: "tts",
@@ -6053,9 +6446,9 @@ var ttsNode = delegated({
6053
6446
  category: "audio",
6054
6447
  summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
6055
6448
  when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
6056
- inputs: z26.object({}).loose(),
6449
+ inputs: z27.object({}).loose(),
6057
6450
  params: TtsParams,
6058
- outputs: z26.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6451
+ outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6059
6452
  outputKinds: { audio: "audio", timestamps: "json" },
6060
6453
  cost: ({ params }) => ({
6061
6454
  credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
@@ -6064,23 +6457,23 @@ var ttsNode = delegated({
6064
6457
  });
6065
6458
 
6066
6459
  // src/engine/nodes/remote/video.ts
6067
- import { z as z27 } from "zod";
6460
+ import { z as z28 } from "zod";
6068
6461
  var VIDEO_GENERATE_MODELS = ["bytedance/seedance-2.0", "google/veo-3.1-fast"];
6069
- var VideoGenerateParams = z27.object({
6070
- model: z27.enum(VIDEO_GENERATE_MODELS),
6071
- prompt: z27.string().min(1),
6072
- duration: z27.number().int().positive().optional(),
6073
- resolution: z27.string().optional(),
6462
+ var VideoGenerateParams = z28.object({
6463
+ model: z28.enum(VIDEO_GENERATE_MODELS),
6464
+ prompt: z28.string().min(1),
6465
+ duration: z28.number().int().positive().optional(),
6466
+ resolution: z28.string().optional(),
6074
6467
  // Union of ratios accepted by at least one curated model (registry gates
6075
6468
  // per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
6076
- aspect_ratio: z27.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
6077
- generate_audio: z27.boolean().optional(),
6078
- seed: z27.number().int().nonnegative().optional(),
6469
+ aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
6470
+ generate_audio: z28.boolean().optional(),
6471
+ seed: z28.number().int().nonnegative().optional(),
6079
6472
  // Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
6080
- negative_prompt: z27.string().optional(),
6081
- person_generation: z27.string().optional(),
6082
- enhance_prompt: z27.boolean().optional(),
6083
- conditioning_scale: z27.number().optional()
6473
+ negative_prompt: z28.string().optional(),
6474
+ person_generation: z28.string().optional(),
6475
+ enhance_prompt: z28.boolean().optional(),
6476
+ conditioning_scale: z28.number().optional()
6084
6477
  }).strict();
6085
6478
  var videoGenerateNode = delegated({
6086
6479
  id: "video_generate",
@@ -6088,23 +6481,23 @@ var videoGenerateNode = delegated({
6088
6481
  category: "video",
6089
6482
  summary: "Generate video for ad creatives. Two curated models: `bytedance/seedance-2.0` (production quality, photorealistic humans via fal.ai) and `google/veo-3.1-fast` (cheap/fast for iteration and tests). Async with polling.",
6090
6483
  when_to_use: "Use `bytedance/seedance-2.0` for final ad output (photoreal subjects, image-to-video with first/last frames). Use `google/veo-3.1-fast` while iterating to keep cost low. Each model has different supported durations, resolutions, and aspect ratios \u2014 see the README per-model section.",
6091
- inputs: z27.object({
6484
+ inputs: z28.object({
6092
6485
  first_frame: ImageRef.optional(),
6093
6486
  last_frame: ImageRef.optional(),
6094
6487
  reference: ImageRef.optional()
6095
6488
  }).loose(),
6096
6489
  params: VideoGenerateParams,
6097
- outputs: z27.object({ video: VideoRef }).strict(),
6490
+ outputs: z28.object({ video: VideoRef }).strict(),
6098
6491
  outputKinds: { video: "video" },
6099
6492
  cost: () => ({ credits: 50, seconds_estimate: 120 })
6100
6493
  });
6101
6494
 
6102
6495
  // src/engine/nodes/remote/videoBackgroundRemove.ts
6103
- import { z as z28 } from "zod";
6104
- var VideoBackgroundRemoveParams = z28.object({
6105
- model: z28.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
6106
- edge_refinement: z28.boolean().optional().default(true),
6107
- output_codec: z28.enum(["vp9", "h264"]).optional().default("vp9")
6496
+ import { z as z29 } from "zod";
6497
+ var VideoBackgroundRemoveParams = z29.object({
6498
+ model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
6499
+ edge_refinement: z29.boolean().optional().default(true),
6500
+ output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
6108
6501
  }).strict();
6109
6502
  var videoBackgroundRemoveNode = delegated({
6110
6503
  id: "video_background_remove",
@@ -6112,18 +6505,18 @@ var videoBackgroundRemoveNode = delegated({
6112
6505
  category: "video",
6113
6506
  summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by fal.ai `veed/video-background-removal/fast`.",
6114
6507
  when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
6115
- inputs: z28.object({
6508
+ inputs: z29.object({
6116
6509
  video: VideoRef
6117
6510
  }).strict(),
6118
6511
  params: VideoBackgroundRemoveParams,
6119
- outputs: z28.object({ video: VideoRef }).strict(),
6512
+ outputs: z29.object({ video: VideoRef }).strict(),
6120
6513
  outputKinds: { video: "video" },
6121
6514
  // $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
6122
6515
  cost: () => ({ credits: 50, seconds_estimate: 60 })
6123
6516
  });
6124
6517
 
6125
6518
  // src/engine/nodes/remote/videoDeconstruct.ts
6126
- import { z as z29 } from "zod";
6519
+ import { z as z30 } from "zod";
6127
6520
  var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
6128
6521
  var videoDeconstructNode = delegated({
6129
6522
  id: "video_deconstruct",
@@ -6131,34 +6524,34 @@ var videoDeconstructNode = delegated({
6131
6524
  category: "video",
6132
6525
  summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
6133
6526
  when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
6134
- inputs: z29.object({ video: VideoRef }).loose(),
6135
- params: z29.object({
6136
- model: z29.enum(VIDEO_DECONSTRUCT_MODELS),
6137
- mode: z29.enum(["full", "index"]).optional(),
6138
- language: z29.string().min(2).max(8).optional(),
6139
- max_scenes: z29.number().int().min(1).max(60).optional(),
6140
- focus: z29.string().optional(),
6141
- start_s: z29.number().min(0).optional(),
6142
- end_s: z29.number().positive().optional(),
6527
+ inputs: z30.object({ video: VideoRef }).loose(),
6528
+ params: z30.object({
6529
+ model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
6530
+ mode: z30.enum(["full", "index"]).optional(),
6531
+ language: z30.string().min(2).max(8).optional(),
6532
+ max_scenes: z30.number().int().min(1).max(60).optional(),
6533
+ focus: z30.string().optional(),
6534
+ start_s: z30.number().min(0).optional(),
6535
+ end_s: z30.number().positive().optional(),
6143
6536
  // Real visual shot-cut timestamps (absolute seconds), detected locally with
6144
6537
  // ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
6145
6538
  // onto these and SPLITS any scene that spans one, so a scene's frames never
6146
6539
  // straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
6147
- shot_cuts: z29.array(z29.number().min(0)).max(200).optional(),
6540
+ shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
6148
6541
  // The video model's per-clip ceiling (seconds). A shot longer than this is
6149
6542
  // split into seamless continuation sub-scenes (shared splice frame), so long
6150
6543
  // shots reproduce in full instead of being truncated. `scaffold-video` sets
6151
6544
  // the Seedance ceiling (15); omit to disable length splitting.
6152
- max_clip_s: z29.number().positive().max(60).optional(),
6545
+ max_clip_s: z30.number().positive().max(60).optional(),
6153
6546
  // Transcript provider for the blueprint's dialogue/transcript. Default
6154
6547
  // Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
6155
- transcriber: z29.enum(["groq", "deepgram"]).optional()
6548
+ transcriber: z30.enum(["groq", "deepgram"]).optional()
6156
6549
  }).strict(),
6157
- outputs: z29.object({
6550
+ outputs: z30.object({
6158
6551
  analysis: JsonRef,
6159
6552
  // Absent in mode:"index" (structure only, no Mux frame extraction).
6160
- start_frames: z29.array(ImageRef).min(1).optional(),
6161
- end_frames: z29.array(ImageRef).min(1).optional(),
6553
+ start_frames: z30.array(ImageRef).min(1).optional(),
6554
+ end_frames: z30.array(ImageRef).min(1).optional(),
6162
6555
  transcript: JsonRef
6163
6556
  }).strict(),
6164
6557
  outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
@@ -6166,22 +6559,22 @@ var videoDeconstructNode = delegated({
6166
6559
  });
6167
6560
 
6168
6561
  // src/engine/nodes/remote/videoLipsync.ts
6169
- import { z as z30 } from "zod";
6170
- var FalLipsyncParams = z30.object({
6171
- model: z30.literal("fal/veed-lipsync")
6562
+ import { z as z31 } from "zod";
6563
+ var FalLipsyncParams = z31.object({
6564
+ model: z31.literal("fal/veed-lipsync")
6172
6565
  }).strict();
6173
- var VideoLipsyncParams = z30.discriminatedUnion("model", [FalLipsyncParams]);
6566
+ var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
6174
6567
  var videoLipsyncNode = delegated({
6175
6568
  id: "video_lipsync",
6176
6569
  version: "1.0.0",
6177
6570
  category: "video",
6178
6571
  summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
6179
- inputs: z30.object({
6572
+ inputs: z31.object({
6180
6573
  video: VideoRef,
6181
6574
  audio: AudioRef
6182
6575
  }).strict(),
6183
6576
  params: VideoLipsyncParams,
6184
- outputs: z30.object({ video: VideoRef }).strict(),
6577
+ outputs: z31.object({ video: VideoRef }).strict(),
6185
6578
  outputKinds: { video: "video" },
6186
6579
  cost: () => ({ credits: 20, seconds_estimate: 120 })
6187
6580
  });
@@ -6190,7 +6583,7 @@ var videoLipsyncNode = delegated({
6190
6583
  import { mkdtemp as mkdtemp6, readFile as readFile10, rm as rm6 } from "fs/promises";
6191
6584
  import { tmpdir as tmpdir6 } from "os";
6192
6585
  import path13 from "path";
6193
- import { z as z31 } from "zod";
6586
+ import { z as z32 } from "zod";
6194
6587
 
6195
6588
  // src/engine/nodes/local/lib/ffmpeg.ts
6196
6589
  import { execFile as execFile7 } from "child_process";
@@ -6269,21 +6662,21 @@ ${detail.slice(-4e3)}`);
6269
6662
  }
6270
6663
 
6271
6664
  // src/engine/nodes/remote/videoTranscribe.ts
6272
- var VideoTranscribeParams = z31.object({
6273
- language: z31.string().min(2).max(8).optional(),
6665
+ var VideoTranscribeParams = z32.object({
6666
+ language: z32.string().min(2).max(8).optional(),
6274
6667
  // Provider choice is explicit (no env-based silent branching). Default Groq
6275
6668
  // Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
6276
6669
  // `rich` JSON output with punctuated words + paragraph/sentence grouping.
6277
- transcriber: z31.enum(["groq", "deepgram"]).optional()
6670
+ transcriber: z32.enum(["groq", "deepgram"]).optional()
6278
6671
  }).strict();
6279
- var VideoTranscribeInputs = z31.object({
6672
+ var VideoTranscribeInputs = z32.object({
6280
6673
  video: VideoRef
6281
6674
  }).strict();
6282
- var VideoTranscribeOutputs = z31.object({
6283
- transcript: z31.custom(),
6675
+ var VideoTranscribeOutputs = z32.object({
6676
+ transcript: z32.custom(),
6284
6677
  // Only emitted by the Deepgram path: full punctuated words + paragraph /
6285
6678
  // sentence grouping with speaker indices. Absent for the default Groq path.
6286
- rich: z31.custom().optional()
6679
+ rich: z32.custom().optional()
6287
6680
  }).strict();
6288
6681
  var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
6289
6682
  var videoTranscribeNode = defineNode({
@@ -6368,29 +6761,29 @@ async function tryExtractAudio(inputs, ctx) {
6368
6761
  }
6369
6762
 
6370
6763
  // src/engine/nodes/remote/voiceSelect.ts
6371
- import { z as z32 } from "zod";
6764
+ import { z as z33 } from "zod";
6372
6765
  var voiceSelectNode = delegated({
6373
6766
  id: "voice_select",
6374
6767
  version: "1.0.0",
6375
6768
  category: "audio",
6376
6769
  summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
6377
6770
  when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
6378
- inputs: z32.object({}).loose(),
6379
- params: z32.object({
6380
- description: z32.string().min(1),
6381
- gender: z32.string().optional(),
6382
- age: z32.string().optional(),
6383
- accent: z32.string().optional(),
6384
- language: z32.string().optional(),
6385
- limit: z32.number().int().min(1).max(20).optional()
6771
+ inputs: z33.object({}).loose(),
6772
+ params: z33.object({
6773
+ description: z33.string().min(1),
6774
+ gender: z33.string().optional(),
6775
+ age: z33.string().optional(),
6776
+ accent: z33.string().optional(),
6777
+ language: z33.string().optional(),
6778
+ limit: z33.number().int().min(1).max(20).optional()
6386
6779
  }).strict(),
6387
- outputs: z32.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
6780
+ outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
6388
6781
  outputKinds: { voice_id: "text", candidates: "json" },
6389
6782
  cost: () => ({ credits: 0, seconds_estimate: 5 })
6390
6783
  });
6391
6784
 
6392
6785
  // src/engine/schema/catalog.ts
6393
- import { z as z33 } from "zod";
6786
+ import { z as z34 } from "zod";
6394
6787
  function generateCatalog(registry, opts = {}) {
6395
6788
  const entries = registry.all().map((def) => {
6396
6789
  const cost = def.cost ? safeCost(def) : void 0;
@@ -6401,9 +6794,9 @@ function generateCatalog(registry, opts = {}) {
6401
6794
  summary: def.summary,
6402
6795
  when_to_use: def.when_to_use,
6403
6796
  location: def.location,
6404
- inputs: z33.toJSONSchema(def.inputs, { unrepresentable: "any" }),
6405
- params: z33.toJSONSchema(def.params, { unrepresentable: "any" }),
6406
- outputs: z33.toJSONSchema(def.outputs, { unrepresentable: "any" }),
6797
+ inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
6798
+ params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
6799
+ outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
6407
6800
  cost_estimate_credits: cost?.credits,
6408
6801
  runtime_estimate_seconds: cost?.seconds_estimate
6409
6802
  };
@@ -6480,7 +6873,8 @@ var LOCAL_NODES = [
6480
6873
  imagemagickNode,
6481
6874
  videoTranscribeNode,
6482
6875
  fontSpecimenNode,
6483
- audioTimelineNode
6876
+ audioTimelineNode,
6877
+ collectNode
6484
6878
  ];
6485
6879
  var REMOTE_NODES = [
6486
6880
  textGenerateNode,
@@ -6513,17 +6907,29 @@ function createEngineFromEnv(opts = {}) {
6513
6907
  const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
6514
6908
  const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
6515
6909
  const creds = requireCredentialsFromEnv();
6910
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
6911
+ const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
6912
+ const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
6913
+ const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
6914
+ const cache = remoteCacheEnabled ? new LayeredCacheStore({
6915
+ local: localCache,
6916
+ remote: new RemoteCacheStore(client, opts.log),
6917
+ assets,
6918
+ log: opts.log
6919
+ }) : localCache;
6516
6920
  return new Engine({
6517
6921
  registry: defaultRegistry(),
6518
- client: new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey }),
6519
- assets: new LocalAssetStore(path15.join(cacheDir, "assets")),
6520
- cache: new LocalCacheStore(path15.join(cacheDir, "index")),
6922
+ client,
6923
+ assets,
6924
+ cache,
6521
6925
  outputsDir,
6522
- log: opts.log
6926
+ log: opts.log,
6927
+ persistAssets: remoteCacheEnabled
6523
6928
  });
6524
6929
  }
6525
6930
 
6526
6931
  export {
6932
+ requireCredentialsFromEnv,
6527
6933
  LayerExecutionError,
6528
6934
  describeFailureReason,
6529
6935
  SEEDANCE_DURATIONS,
@@ -6531,7 +6937,14 @@ export {
6531
6937
  IMAGE_GENERATE_MODELS,
6532
6938
  MODEL_REGISTRY,
6533
6939
  resolveConcurrency,
6940
+ ulid,
6941
+ isPersistedAssetRef,
6942
+ collectAssetRefLikes,
6943
+ REF_PREFIX,
6944
+ parseRefExpr,
6945
+ sha256Hex,
6534
6946
  elementMentionKeywords,
6947
+ toModelSafeImage,
6535
6948
  BackendClient2 as BackendClient,
6536
6949
  Engine2 as Engine,
6537
6950
  LocalAssetStore2 as LocalAssetStore,
@@ -6542,4 +6955,4 @@ export {
6542
6955
  defaultRegistry,
6543
6956
  createEngineFromEnv
6544
6957
  };
6545
- //# sourceMappingURL=chunk-IWPAXJC3.js.map
6958
+ //# sourceMappingURL=chunk-Q3K5TXC6.js.map