@koda-sl/baker-cli 0.124.0-dev.efe965ce2 → 0.125.0

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
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
6
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
7
7
  var require_safe_stable_stringify = __commonJS({
8
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
8
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/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 message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
142
- if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
143
- throw new Error(message2);
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);
144
144
  };
145
145
  }
146
146
  }
@@ -652,9 +652,6 @@ 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
- }
658
655
  async getJson(path16, signal) {
659
656
  return await this.requestJson("GET", path16, void 0, signal);
660
657
  }
@@ -682,8 +679,8 @@ var HttpClient = class {
682
679
  try {
683
680
  const res = await this.fetchFn(url, {
684
681
  method,
685
- headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
686
- body: method === "GET" ? void 0 : JSON.stringify(body),
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,
687
684
  signal: controller.signal
688
685
  });
689
686
  if (res.ok) return { kind: "value", value: await res.json() };
@@ -720,33 +717,33 @@ async function parseErrorBody(res) {
720
717
  const errObj = body.error ?? {};
721
718
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
722
719
  }
723
- function classifyHttpError(status, errObj, message2) {
720
+ function classifyHttpError(status, errObj, message) {
724
721
  if (errObj.code === CONTENT_POLICY_CODE) {
725
- return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
722
+ return { kind: "content_policy", status, provider: errObj.provider, message };
726
723
  }
727
724
  if (status === 401 || status === 403) {
728
- return { kind: "unauthorized", status, message: message2 };
725
+ return { kind: "unauthorized", status, message };
729
726
  }
730
727
  if (status === 400 || status === 422) {
731
- return { kind: "validation", status, message: message2, details: errObj.details };
728
+ return { kind: "validation", status, message, details: errObj.details };
732
729
  }
733
730
  if (status === 502 || status === 504) {
734
731
  if (errObj.code === "provider_timeout" || status === 504) {
735
- return { kind: "timeout", provider: errObj.provider, message: message2 };
732
+ return { kind: "timeout", provider: errObj.provider, message };
736
733
  }
737
734
  return {
738
735
  kind: "provider",
739
736
  status,
740
737
  provider: errObj.provider,
741
738
  code: errObj.code ?? "provider_error",
742
- message: message2,
739
+ message,
743
740
  retryable: errObj.retryable ?? true
744
741
  };
745
742
  }
746
743
  if (status >= 500 || status === 429) {
747
- return { kind: "server", status, message: message2 };
744
+ return { kind: "server", status, message };
748
745
  }
749
- return { kind: "validation", status, message: message2, details: errObj.details };
746
+ return { kind: "validation", status, message, details: errObj.details };
750
747
  }
751
748
  function backoffMs(attempt) {
752
749
  return 1e3 * 2 ** attempt;
@@ -780,9 +777,7 @@ function failedJobError(error) {
780
777
  retryable: error.retryable ?? false
781
778
  });
782
779
  }
783
- function pollInterval(attempt) {
784
- return attempt < 15 ? 1e3 : 3e3;
785
- }
780
+ var JOB_POLL_INTERVAL_MS = 3e3;
786
781
  var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
787
782
  var BackendClient = class {
788
783
  http;
@@ -799,7 +794,7 @@ var BackendClient = class {
799
794
  async pollJob(jobId, signal) {
800
795
  const deadline = Date.now() + JOB_POLL_MAX_MS;
801
796
  const path16 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
802
- for (let attempt = 0; ; attempt++) {
797
+ while (true) {
803
798
  if (signal?.aborted) {
804
799
  throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
805
800
  }
@@ -809,78 +804,16 @@ var BackendClient = class {
809
804
  if (Date.now() > deadline) {
810
805
  throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
811
806
  }
812
- await sleep(pollInterval(attempt));
807
+ await sleep(JOB_POLL_INTERVAL_MS);
813
808
  }
814
809
  }
815
- presignAssetUpload(sha256, mime, signal, purpose) {
810
+ presignAssetUpload(sha256, mime, signal) {
816
811
  return this.http.postJson(
817
812
  "/api/canvas/assets/presign",
818
- { sha256, mime, purpose },
813
+ { sha256, mime },
819
814
  signal
820
815
  );
821
816
  }
822
- /** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
823
- async getCacheEntry(cacheKey, signal) {
824
- try {
825
- const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
826
- return res.entry;
827
- } catch (e) {
828
- if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
829
- throw e;
830
- }
831
- }
832
- async putCacheEntry(entry, signal) {
833
- await this.http.putJson(
834
- `/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
835
- entry,
836
- signal
837
- );
838
- }
839
- /** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
840
- async recordRun(payload, signal) {
841
- await this.http.postJson("/api/canvas/runs", payload, signal);
842
- }
843
- /**
844
- * Resumable-run lookup — GET /api/canvas/runs/active. Returns the newest
845
- * still-running/interrupted run for the creative (pinned to the exact canvas
846
- * sha), so a fresh sandbox can adopt its run id and re-attach billed
847
- * in-flight jobs. Null on no active run — or an older backend without the
848
- * route (both 404).
849
- */
850
- async getActiveRun(creativeSlug, canvasSha, signal) {
851
- const params = new URLSearchParams({ slug: creativeSlug });
852
- if (canvasSha) params.set("sha", canvasSha);
853
- try {
854
- const res = await this.http.getJson(`/api/canvas/runs/active?${params}`, signal);
855
- return res.run;
856
- } catch (e) {
857
- if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
858
- throw e;
859
- }
860
- }
861
- /**
862
- * Portable-rerun lookup — GET /api/canvas/runs/latest. The newest run for
863
- * the creative that recorded a canvas snapshot manifest; null when none
864
- * exists (or the backend predates the route — both 404).
865
- */
866
- async getLatestSnapshotRun(creativeSlug, signal) {
867
- const params = new URLSearchParams({ slug: creativeSlug });
868
- try {
869
- const res = await this.http.getJson(`/api/canvas/runs/latest?${params}`, signal);
870
- return res.run;
871
- } catch (e) {
872
- if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
873
- throw e;
874
- }
875
- }
876
- /**
877
- * Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
878
- * dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
879
- * Additive on the backend (never archives siblings, never sets definitionPath).
880
- */
881
- async syncCreativeDefinition(payload, signal) {
882
- await this.http.postJson("/api/creatives/definition", payload, signal);
883
- }
884
817
  getArtifact(kind, name, version, signal) {
885
818
  const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
886
819
  return this.http.getJson(path16, signal);
@@ -904,17 +837,14 @@ function requireCredentialsFromEnv(env = process.env) {
904
837
  }
905
838
  return c;
906
839
  }
907
- function remoteCacheEnabledFromEnv(env = process.env) {
908
- return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
909
- }
910
840
 
911
841
  // src/engine/engine/errors.ts
912
842
  function isBlocking(issue) {
913
843
  return issue.severity !== "warning";
914
844
  }
915
845
  var CanvasError = class extends Error {
916
- constructor(message2) {
917
- super(message2);
846
+ constructor(message) {
847
+ super(message);
918
848
  this.name = "CanvasError";
919
849
  }
920
850
  };
@@ -940,14 +870,6 @@ var NodeExecutionError = class extends CanvasError {
940
870
  this.cause = cause;
941
871
  }
942
872
  };
943
- var RunAbortedError = class extends CanvasError {
944
- reason;
945
- constructor(reason, message2) {
946
- super(message2);
947
- this.name = "RunAbortedError";
948
- this.reason = reason;
949
- }
950
- };
951
873
  var LayerExecutionError = class extends CanvasError {
952
874
  failures;
953
875
  constructor(failures) {
@@ -979,7 +901,7 @@ function describeCause(c) {
979
901
  }
980
902
  }
981
903
 
982
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
904
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
983
905
  var import__ = __toESM(require_safe_stable_stringify(), 1);
984
906
  var configure = import__.default.configure;
985
907
  var wrapper_default = import__.default;
@@ -1037,10 +959,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
1037
959
  var ELEVENLABS_MAX_TEXT_CHARS = 45454;
1038
960
  var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
1039
961
  var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
1040
- var REPLICATE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
1041
- var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
962
+ var FAL_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
963
+ var FAL_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
1042
964
  var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
1043
- var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
965
+ var FAL_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
1044
966
  var IMAGE_GENERATE_MODELS = [
1045
967
  "openai/gpt-5.4-image-2",
1046
968
  "google/gemini-3.5-flash",
@@ -1258,23 +1180,20 @@ var MODEL_REGISTRY = {
1258
1180
  },
1259
1181
  video_generate: {
1260
1182
  "bytedance/seedance-2.0": {
1261
- // Routed via Replicate's official `bytedance/seedance-2.0` model. NOTE:
1262
- // ByteDance's upstream "real person" likeness filter still blocks photoreal
1263
- // human reference frames on ANY reseller — the escape is a synthetic/AI
1264
- // presenter face or routing real faces to Veo, not the provider choice.
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.
1265
1186
  label: "ByteDance Seedance 2.0",
1266
1187
  inputs: [],
1267
- optional_inputs: [{ kind: "image", mimes: REPLICATE_IMAGE_MIMES }],
1188
+ optional_inputs: [{ kind: "image", mimes: FAL_IMAGE_MIMES }],
1268
1189
  required: ["prompt"],
1269
1190
  params: {
1270
- // Replicate's Seedance wrapper hard-caps the prompt at 4000 chars; gate
1271
- // it here so an over-length prompt fails validate (free) not the billed call.
1272
- prompt: { kind: "string", maxLength: 4e3 },
1191
+ prompt: { kind: "string" },
1273
1192
  aspect_ratio: {
1274
1193
  kind: "string",
1275
1194
  enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
1276
1195
  },
1277
- resolution: { kind: "string", enum: ["480p", "720p", "1080p", "4k"] },
1196
+ resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
1278
1197
  duration: { kind: "number", enum: SEEDANCE_DURATIONS },
1279
1198
  seed: { kind: "number" },
1280
1199
  generate_audio: { kind: "boolean" }
@@ -1295,10 +1214,7 @@ var MODEL_REGISTRY = {
1295
1214
  duration: { kind: "number", enum: [4, 6, 8] },
1296
1215
  seed: { kind: "number" },
1297
1216
  generate_audio: { kind: "boolean" },
1298
- // Image-to-video and EU/UK/CH/MENA regions cap this at `allow_adult`;
1299
- // `allow_all` is text-to-video only. Allow both so an image-conditioned
1300
- // Veo clip (the real-face fallback) validates.
1301
- person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
1217
+ person_generation: { kind: "string", enum: ["allow_all"] },
1302
1218
  enhance_prompt: { kind: "boolean" },
1303
1219
  conditioning_scale: { kind: "number" }
1304
1220
  }
@@ -1349,8 +1265,8 @@ var MODEL_REGISTRY = {
1349
1265
  "fal/veed-lipsync": {
1350
1266
  label: "VEED Lipsync (fal.ai)",
1351
1267
  inputs: [
1352
- { kind: "video", mimes: REPLICATE_VIDEO_MIMES },
1353
- { kind: "audio", mimes: REPLICATE_AUDIO_MIMES }
1268
+ { kind: "video", mimes: FAL_VIDEO_MIMES },
1269
+ { kind: "audio", mimes: FAL_AUDIO_MIMES }
1354
1270
  ],
1355
1271
  required: [],
1356
1272
  params: {}
@@ -1386,7 +1302,7 @@ var MODEL_REGISTRY = {
1386
1302
  // TARGET voice, preserving timing/prosody. Used to normalize a talking-head
1387
1303
  // clip's native (generator-chosen) voice into ONE consistent brand voice.
1388
1304
  label: "ElevenLabs Voice Changer (multilingual STS v2)",
1389
- inputs: [{ kind: "audio", mimes: REPLICATE_AUDIO_MIMES }],
1305
+ inputs: [{ kind: "audio", mimes: FAL_AUDIO_MIMES }],
1390
1306
  required: ["voice"],
1391
1307
  params: {
1392
1308
  voice: { kind: "string" },
@@ -1413,7 +1329,7 @@ var MODEL_REGISTRY = {
1413
1329
  },
1414
1330
  "elevenlabs/video-background-music-v1": {
1415
1331
  label: "ElevenLabs Video Background Music v1",
1416
- inputs: [{ kind: "video", mimes: REPLICATE_VIDEO_MIMES }],
1332
+ inputs: [{ kind: "video", mimes: FAL_VIDEO_MIMES }],
1417
1333
  required: [],
1418
1334
  params: {
1419
1335
  description: { kind: "string" },
@@ -1584,7 +1500,7 @@ function validateValue(key, value, schema, model) {
1584
1500
  }
1585
1501
 
1586
1502
  // src/engine/lib/concurrency.ts
1587
- var DEFAULT_CONCURRENCY = 8;
1503
+ var DEFAULT_CONCURRENCY = 5;
1588
1504
  function resolveConcurrency(...candidates) {
1589
1505
  for (const candidate of candidates) {
1590
1506
  if (candidate === void 0 || candidate === "") continue;
@@ -1646,185 +1562,6 @@ function encodeRandom() {
1646
1562
  return out;
1647
1563
  }
1648
1564
 
1649
- // src/engine/storage/remote-cache-store.ts
1650
- var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
1651
- function isPersistedAssetRef(ref) {
1652
- const { url, sha256 } = ref;
1653
- if (typeof url !== "string" || typeof sha256 !== "string") return false;
1654
- return url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256.slice(0, 2)}/${sha256}`) || url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${sha256}`);
1655
- }
1656
- function isAssetRefLike(value) {
1657
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
1658
- }
1659
- function collectAssetRefLikes(value, out = []) {
1660
- if (Array.isArray(value)) {
1661
- for (const item of value) collectAssetRefLikes(item, out);
1662
- return out;
1663
- }
1664
- if (typeof value !== "object" || value === null) return out;
1665
- if (isAssetRefLike(value)) {
1666
- out.push(value);
1667
- }
1668
- for (const item of Object.values(value)) collectAssetRefLikes(item, out);
1669
- return out;
1670
- }
1671
- function entryFullyPersisted(entry) {
1672
- return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
1673
- }
1674
- function stripLocalFields(entry) {
1675
- const clone = JSON.parse(JSON.stringify(entry));
1676
- for (const ref of collectAssetRefLikes(clone.outputs)) {
1677
- delete ref.path;
1678
- delete ref.bytes;
1679
- }
1680
- return clone;
1681
- }
1682
- var RemoteCacheStore = class {
1683
- client;
1684
- log;
1685
- constructor(client, log) {
1686
- this.client = client;
1687
- this.log = log ?? (() => void 0);
1688
- }
1689
- async get(cacheKey) {
1690
- return await this.client.getCacheEntry(cacheKey);
1691
- }
1692
- async put(entry) {
1693
- if (!entryFullyPersisted(entry)) {
1694
- this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
1695
- return;
1696
- }
1697
- const stripped = stripLocalFields(entry);
1698
- if (stripped.refs.length > MAX_REMOTE_REFS) {
1699
- stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
1700
- }
1701
- await this.client.putCacheEntry(stripped);
1702
- }
1703
- };
1704
- var MAX_REMOTE_REFS = 512;
1705
- var LayeredCacheStore = class {
1706
- rootDir;
1707
- local;
1708
- remote;
1709
- assets;
1710
- log;
1711
- constructor(opts) {
1712
- this.local = opts.local;
1713
- this.remote = opts.remote;
1714
- this.assets = opts.assets;
1715
- this.rootDir = opts.local.rootDir;
1716
- this.log = opts.log ?? (() => void 0);
1717
- }
1718
- async get(cacheKey) {
1719
- const localHit = await this.local.get(cacheKey);
1720
- if (localHit) return localHit;
1721
- let remoteEntry;
1722
- try {
1723
- remoteEntry = await this.remote.get(cacheKey);
1724
- } catch (e) {
1725
- this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
1726
- return null;
1727
- }
1728
- if (!remoteEntry) return null;
1729
- let rehydrated;
1730
- try {
1731
- rehydrated = await this.rehydrate(remoteEntry);
1732
- } catch (e) {
1733
- this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
1734
- return null;
1735
- }
1736
- await this.local.put(rehydrated);
1737
- return rehydrated;
1738
- }
1739
- async put(entry) {
1740
- await this.local.put(entry);
1741
- try {
1742
- await this.remote.put(entry);
1743
- } catch (e) {
1744
- this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
1745
- }
1746
- }
1747
- /**
1748
- * Download every referenced asset into the local content-addressed store
1749
- * (sha-verified) and stamp fresh local paths. Any ref that cannot be
1750
- * rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
1751
- * crash materialization later with a far less actionable error.
1752
- */
1753
- async rehydrate(entry) {
1754
- const clone = JSON.parse(JSON.stringify(entry));
1755
- for (const ref of collectAssetRefLikes(clone.outputs)) {
1756
- if (!isPersistedAssetRef(ref)) {
1757
- throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
1758
- }
1759
- const ingested = await this.assets.ingestRemote({
1760
- kind: typeof ref.kind === "string" ? ref.kind : "json",
1761
- url: ref.url,
1762
- sha256: ref.sha256,
1763
- mime: ref.mime,
1764
- metadata: ref.metadata ?? void 0
1765
- });
1766
- ref.path = ingested.path;
1767
- }
1768
- return clone;
1769
- }
1770
- };
1771
- function message(e) {
1772
- return e instanceof Error ? e.message : String(e);
1773
- }
1774
-
1775
- // src/engine/nodes/remote/upload.ts
1776
- var PUT_MAX_ATTEMPTS = 4;
1777
- var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1778
- async function presignAndPut(args) {
1779
- let lastFailure = null;
1780
- for (let attempt = 0; attempt < PUT_MAX_ATTEMPTS; attempt++) {
1781
- if (args.ctx.signal?.aborted) break;
1782
- if (attempt > 0) await sleep2(500 * 2 ** (attempt - 1) * (1 + Math.random() * 0.25));
1783
- const result = await attemptPresignedPut(args);
1784
- if (result.ok) return result.url;
1785
- lastFailure = result.failure;
1786
- if (!result.retryable) break;
1787
- }
1788
- throw lastFailure ?? new Error("upload: aborted before the PUT could start");
1789
- }
1790
- async function attemptPresignedPut(args) {
1791
- try {
1792
- const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
1793
- const putRes = await fetch(putUrl, {
1794
- method: "PUT",
1795
- body: new Uint8Array(args.bytes),
1796
- headers: { "Content-Type": args.mime },
1797
- signal: args.ctx.signal
1798
- });
1799
- if (putRes.ok) return { ok: true, url: publicUrl };
1800
- return {
1801
- ok: false,
1802
- failure: new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`),
1803
- // Only transient statuses warrant a replay — a 400/403 fails identically every attempt.
1804
- retryable: putRes.status >= 500 || putRes.status === 429 || putRes.status === 408
1805
- };
1806
- } catch (e) {
1807
- return {
1808
- ok: false,
1809
- failure: e instanceof Error ? e : new Error(String(e)),
1810
- retryable: args.ctx.signal?.aborted !== true
1811
- };
1812
- }
1813
- }
1814
- async function ensureUploaded(ref, ctx) {
1815
- if (ref.url) return ref;
1816
- const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1817
- const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1818
- return { ...ref, url };
1819
- }
1820
- async function persistOutputAssetUrls(outputs, ctx) {
1821
- for (const ref of collectAssetRefLikes(outputs)) {
1822
- if (isPersistedAssetRef(ref)) continue;
1823
- const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1824
- ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1825
- }
1826
- }
1827
-
1828
1565
  // src/engine/schema/canvas.ts
1829
1566
  import { z } from "zod";
1830
1567
  var REF_PREFIX = "$ref:";
@@ -1854,14 +1591,7 @@ var NodeDecl = z.object({
1854
1591
  version: z.string().min(1).optional(),
1855
1592
  inputs: z.record(z.string(), z.unknown()).optional(),
1856
1593
  params: z.record(z.string(), z.unknown()).optional(),
1857
- when: z.unknown().optional(),
1858
- // Regenerate knob. The engine is content-addressed: identical params + inputs
1859
- // return the cached render, so re-running an unchanged node NEVER re-bills or
1860
- // produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
1861
- // a note) and re-run to force THIS node to render fresh; because its new output
1862
- // changes downstream input hashes, everything depending on it regenerates too.
1863
- // This is the declarative "change a value, re-run, get a new render" affordance.
1864
- regenerate: z.union([z.string(), z.number()]).optional()
1594
+ when: z.unknown().optional()
1865
1595
  }).strict();
1866
1596
  var OutputRef = z.object({
1867
1597
  node: z.string(),
@@ -3111,7 +2841,6 @@ var Engine = class {
3111
2841
  cache;
3112
2842
  outputsDir;
3113
2843
  log;
3114
- persistAssets;
3115
2844
  constructor(opts) {
3116
2845
  this.registry = opts.registry;
3117
2846
  this.client = opts.client;
@@ -3119,7 +2848,6 @@ var Engine = class {
3119
2848
  this.cache = opts.cache;
3120
2849
  this.outputsDir = opts.outputsDir;
3121
2850
  this.log = opts.log ?? (() => void 0);
3122
- this.persistAssets = opts.persistAssets ?? false;
3123
2851
  }
3124
2852
  validate(canvas) {
3125
2853
  return validateCanvas(canvas, this.registry);
@@ -3131,12 +2859,6 @@ var Engine = class {
3131
2859
  async run(input, opts = {}) {
3132
2860
  const validation = await this.validateDeep(input);
3133
2861
  if (!validation.ok) throw new ValidationError(validation.issues);
3134
- if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
3135
- throw new RunAbortedError(
3136
- "cost_cap",
3137
- `estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
3138
- );
3139
- }
3140
2862
  const canvas = validation.canvas;
3141
2863
  const runId = opts.run_id ?? `r_${ulid()}`;
3142
2864
  const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
@@ -3149,16 +2871,7 @@ var Engine = class {
3149
2871
  const outputs = {};
3150
2872
  const counters = { cachedNodes: 0, totalCredits: 0 };
3151
2873
  const nodeRuns = [];
3152
- const graph = this.pruneToOutput(canvas, buildGraph(canvas));
3153
- const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
3154
- this.emitProgress(opts, {
3155
- kind: "plan",
3156
- nodes: [...graph.entries()].map(([id, deps]) => {
3157
- const node = canvas.nodes.find((n) => n.id === id);
3158
- return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
3159
- })
3160
- });
3161
- await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes);
2874
+ await this.runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns);
3162
2875
  const output = pickFinalOutput(canvas, outputs);
3163
2876
  const stats = {
3164
2877
  total_nodes: canvas.nodes.length,
@@ -3181,63 +2894,39 @@ var Engine = class {
3181
2894
  `[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
3182
2895
  );
3183
2896
  this.log(`outputs in: ${writer.runDir}`);
3184
- return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
2897
+ return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
3185
2898
  }
3186
- async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
3187
- const layers = topologicalLayers(graph);
2899
+ async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
2900
+ const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
3188
2901
  const limit = resolveConcurrency(opts.concurrency);
3189
2902
  for (const layer of layers) {
3190
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
3191
- if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
3192
- throw new RunAbortedError(
3193
- "cost_cap",
3194
- `spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
3195
- );
3196
- }
3197
- const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
3198
- if (opts.signal?.aborted) {
3199
- return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
3200
- }
3201
- this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
3202
- return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
2903
+ const settled = await mapWithConcurrency(
2904
+ layer,
2905
+ limit,
2906
+ (nodeId) => this.executeOne(canvas, nodeId, outputs, runId, writer, opts).then((r) => {
3203
2907
  if (r.cached) counters.cachedNodes++;
3204
2908
  counters.totalCredits += r.credits;
3205
2909
  const node = canvas.nodes.find((n) => n.id === nodeId);
3206
2910
  if (node) {
3207
- const run = {
2911
+ nodeRuns.push({
3208
2912
  node_id: nodeId,
3209
2913
  node_type: node.type,
3210
2914
  cached: r.cached,
3211
2915
  duration_ms: r.durationMs,
3212
2916
  credits: r.credits
3213
- };
3214
- nodeRuns.push(run);
3215
- this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
2917
+ });
3216
2918
  }
3217
- });
3218
- });
2919
+ })
2920
+ );
3219
2921
  const failures = [];
3220
2922
  settled.forEach((result, i) => {
3221
2923
  const nodeId = layer[i];
3222
- if (result.status === "rejected" && nodeId) {
3223
- if (result.reason instanceof RunAbortedError) return;
3224
- failures.push({ nodeId, reason: result.reason });
3225
- this.emitProgress(opts, { kind: "node_failed", node_id: nodeId });
3226
- }
2924
+ if (result.status === "rejected" && nodeId) failures.push({ nodeId, reason: result.reason });
3227
2925
  });
3228
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
3229
2926
  if (failures.length === 1 && failures[0]) throw failures[0].reason;
3230
2927
  if (failures.length > 1) throw new LayerExecutionError(failures);
3231
2928
  }
3232
2929
  }
3233
- /** Progress consumers are observers only — an exception there must never fail the run. */
3234
- emitProgress(opts, event) {
3235
- if (!opts.onProgress) return;
3236
- try {
3237
- opts.onProgress(event);
3238
- } catch {
3239
- }
3240
- }
3241
2930
  /**
3242
2931
  * Dead-node elimination: when the canvas declares an `output`, execute only the
3243
2932
  * nodes that output transitively depends on. Orphaned nodes (left by an edit or
@@ -3268,13 +2957,12 @@ var Engine = class {
3268
2957
  }
3269
2958
  await writer.writeManifest("_final", output);
3270
2959
  }
3271
- async executeOne(canvas, nodeId, outputs, runId, writer, opts, downloadOutputs) {
2960
+ async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
3272
2961
  const node = canvas.nodes.find((n) => n.id === nodeId);
3273
2962
  if (!node) throw new Error(`executor: missing node ${nodeId}`);
3274
2963
  const def = this.registry.get(node.type);
3275
2964
  if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
3276
- const regenerateToken = resolveRegenerateToken(node, opts.regenerate, runId);
3277
- const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
2965
+ const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, this.assets);
3278
2966
  const policy = opts.cache_policy ?? "read_write";
3279
2967
  if (policy !== "bypass") {
3280
2968
  const cacheT0 = Date.now();
@@ -3292,27 +2980,17 @@ var Engine = class {
3292
2980
  nodeId: node.id,
3293
2981
  nodeType: node.type,
3294
2982
  cacheKey: prepared.cacheKey,
3295
- downloadOutputs,
3296
2983
  client: this.client,
3297
2984
  assets: this.assets,
3298
2985
  log: this.log,
3299
2986
  signal: opts.signal
3300
2987
  };
3301
- const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
3302
- const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
2988
+ const { parsedInputs, parsedParams } = parseNodeArgs(def, prepared, node.id, node.type);
3303
2989
  const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
3304
2990
  const elapsed = Date.now() - t0;
3305
2991
  const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
3306
2992
  const outputsObj = result;
3307
2993
  outputs[node.id] = outputsObj;
3308
- if (this.persistAssets) {
3309
- try {
3310
- await persistOutputAssetUrls(outputsObj, ctx);
3311
- } catch (e) {
3312
- const msg = e instanceof Error ? e.message : String(e);
3313
- this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
3314
- }
3315
- }
3316
2994
  if (policy === "read_write") {
3317
2995
  await this.cache.put({
3318
2996
  cacheKey: prepared.cacheKey,
@@ -3341,42 +3019,8 @@ var Engine = class {
3341
3019
  }
3342
3020
  }
3343
3021
  }
3344
- /**
3345
- * Download any URL-only asset ref reachable in a local node's inputs so the
3346
- * bytes are on disk before the local runner stages them. Returns a copy —
3347
- * refs are replaced, never mutated in place, so the producer's cached output
3348
- * (shared object) keeps its URL-only shape.
3349
- */
3350
- async materializeLocalInputs(inputs) {
3351
- const fix = async (value) => {
3352
- if (Array.isArray(value)) return Promise.all(value.map(fix));
3353
- if (value && typeof value === "object") {
3354
- const v = value;
3355
- if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
3356
- this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
3357
- return this.assets.ingestRemote({
3358
- kind: v.kind,
3359
- url: v.url,
3360
- sha256: v.sha256,
3361
- mime: v.mime,
3362
- metadata: v.metadata
3363
- });
3364
- }
3365
- const out = {};
3366
- for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
3367
- return out;
3368
- }
3369
- return value;
3370
- };
3371
- return await fix(inputs);
3372
- }
3373
3022
  };
3374
- function resolveRegenerateToken(node, forced, runId) {
3375
- if (forced?.has(node.id)) return `run:${runId}`;
3376
- if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
3377
- return void 0;
3378
- }
3379
- async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
3023
+ async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
3380
3024
  const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
3381
3025
  const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
3382
3026
  const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
@@ -3389,9 +3033,6 @@ async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToke
3389
3033
  throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
3390
3034
  }
3391
3035
  }
3392
- if (regenerateToken !== void 0) {
3393
- extras = { ...extras ?? {}, __regenerate__: regenerateToken };
3394
- }
3395
3036
  const cacheKey = computeCacheKey({
3396
3037
  node_id: node.type,
3397
3038
  node_version: def.version,
@@ -3420,9 +3061,6 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
3420
3061
  throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3421
3062
  }
3422
3063
  }
3423
- function needsLocalMaterialization(def) {
3424
- return def.location === "local" && !def.passthroughRefs;
3425
- }
3426
3064
  function pickFinalOutput(canvas, outputs) {
3427
3065
  if (canvas.output) {
3428
3066
  const node = outputs[canvas.output.node];
@@ -3433,16 +3071,6 @@ function pickFinalOutput(canvas, outputs) {
3433
3071
  const lastOut = outputs[last.id];
3434
3072
  return lastOut ? Object.values(lastOut)[0] : void 0;
3435
3073
  }
3436
- function computeNeedsLocalBytes(canvas, graph, registry) {
3437
- const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
3438
- const needs = /* @__PURE__ */ new Set();
3439
- for (const [consumerId, deps] of graph) {
3440
- const def = registry.get(typeById.get(consumerId) ?? "");
3441
- if (def?.location !== "local" || def.passthroughRefs) continue;
3442
- for (const dep of deps) needs.add(dep);
3443
- }
3444
- return needs;
3445
- }
3446
3074
  function buildGraph(canvas) {
3447
3075
  const graph = /* @__PURE__ */ new Map();
3448
3076
  for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
@@ -3573,16 +3201,7 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
3573
3201
  try {
3574
3202
  bytes = await assets.readBytes(value.sha256, value.mime);
3575
3203
  } catch (e) {
3576
- if (value.url) {
3577
- try {
3578
- await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
3579
- bytes = await assets.readBytes(value.sha256, value.mime);
3580
- } catch (e2) {
3581
- throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
3582
- }
3583
- } else {
3584
- throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3585
- }
3204
+ throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
3586
3205
  }
3587
3206
  if (bytes.length > MAX_INLINE_TEXT_BYTES) {
3588
3207
  throw new NodeExecutionError(nodeId, nodeType, {
@@ -3690,6 +3309,27 @@ var FontRef = BaseAssetRef.extend({
3690
3309
  });
3691
3310
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
3692
3311
 
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
+
3693
3333
  // src/engine/nodes/remote/delegate.ts
3694
3334
  function delegated(spec) {
3695
3335
  return {
@@ -3717,9 +3357,7 @@ async function callBackendExec(args) {
3717
3357
  nodeVersion: args.nodeVersion,
3718
3358
  params: args.params,
3719
3359
  inputs: serialized,
3720
- idempotency_key: idempotencyKey,
3721
- canvas_run_id: args.ctx.canvasRunId,
3722
- node_id: args.ctx.nodeId
3360
+ idempotency_key: idempotencyKey
3723
3361
  },
3724
3362
  args.ctx.signal
3725
3363
  );
@@ -3771,9 +3409,6 @@ async function ingestValue(value, ctx, declaredKind) {
3771
3409
  }
3772
3410
  if (isRawAsset(value)) {
3773
3411
  const kind = value.kind ?? declaredKind ?? "json";
3774
- if (ctx.downloadOutputs === false) {
3775
- return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
3776
- }
3777
3412
  return ctx.assets.ingestRemote({
3778
3413
  kind,
3779
3414
  url: value.url,
@@ -3888,7 +3523,7 @@ function safePathname(rawUrl) {
3888
3523
  }
3889
3524
  var ingestNode = defineNode({
3890
3525
  id: "ingest",
3891
- version: "1.2.0",
3526
+ version: "1.1.0",
3892
3527
  category: "io",
3893
3528
  location: "local",
3894
3529
  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.",
@@ -3931,9 +3566,6 @@ function runStrategy(strategy, params, ctx) {
3931
3566
  }
3932
3567
  }
3933
3568
  async function execDirectFetch(params, ctx) {
3934
- if (params.expect === "image") {
3935
- return ingestImageUrl(params.url, ctx);
3936
- }
3937
3569
  const result = await callBackendExec({
3938
3570
  nodeType: "ingest",
3939
3571
  nodeVersion: ingestNode.version,
@@ -3944,37 +3576,6 @@ async function execDirectFetch(params, ctx) {
3944
3576
  });
3945
3577
  return assertAssetOutput(result, params.expect);
3946
3578
  }
3947
- async function ingestImageUrl(url, ctx) {
3948
- const res = await fetch(url);
3949
- if (!res.ok) {
3950
- throw localExecError(ctx, `fetch ${url} \u2192 ${res.status}`);
3951
- }
3952
- const ab = await res.arrayBuffer();
3953
- if (ab.byteLength > MAX_ASSET_BYTES) {
3954
- throw localExecError(ctx, `file_too_large: ${url} is ${ab.byteLength} bytes (limit ${MAX_ASSET_BYTES})`);
3955
- }
3956
- let normalized;
3957
- try {
3958
- normalized = await toModelSafeImage(Buffer.from(ab));
3959
- } catch (e) {
3960
- throw localExecError(ctx, `${url}: ${e.message}`);
3961
- }
3962
- if (normalized.rasterizedFrom) {
3963
- ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
3964
- }
3965
- return uploadAndIngest({
3966
- bytes: normalized.bytes,
3967
- kind: "image",
3968
- mime: normalized.mime,
3969
- metadata: {
3970
- source_url: url,
3971
- strategy: "direct_fetch",
3972
- ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
3973
- ...normalized.rasterizedFrom ? { rasterized_from: normalized.rasterizedFrom } : {}
3974
- },
3975
- ctx
3976
- });
3977
- }
3978
3579
  async function execHandinger(params, ctx) {
3979
3580
  const result = await callBackendExec({
3980
3581
  nodeType: "ingest",
@@ -4011,15 +3612,7 @@ var EXT_TO_MIME = {
4011
3612
  jpeg: "image/jpeg",
4012
3613
  webp: "image/webp",
4013
3614
  gif: "image/gif",
4014
- // Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
4015
- // must resolve to an image mime here or the kind-check rejects the local file
4016
- // before normalization ever runs.
4017
3615
  avif: "image/avif",
4018
- heic: "image/heic",
4019
- heif: "image/heif",
4020
- tif: "image/tiff",
4021
- tiff: "image/tiff",
4022
- bmp: "image/bmp",
4023
3616
  mp4: "video/mp4",
4024
3617
  webm: "video/webm",
4025
3618
  mov: "video/quicktime",
@@ -4070,45 +3663,15 @@ async function rasterizeSvgToPng(bytes) {
4070
3663
  }
4071
3664
  return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
4072
3665
  }
4073
- var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
4074
- async function toModelSafeImage(bytes) {
4075
- const safe = sniffImageMime(bytes);
4076
- if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
4077
- return { bytes, mime: safe };
4078
- }
4079
- if (sniffSvg(bytes)) {
4080
- return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
4081
- }
4082
- const { default: sharp } = await import("sharp");
4083
- try {
4084
- const img = sharp(bytes);
4085
- const format = (await img.metadata()).format;
4086
- const png = await img.png({ force: true }).toBuffer();
4087
- return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
4088
- } catch (e) {
4089
- throw new Error(`bytes are not a decodable image (${e.message})`);
4090
- }
4091
- }
4092
- function hasAscii(buf, offset, sig) {
4093
- return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
4094
- }
4095
- var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
4096
- function sniffIsoBmff(buf) {
4097
- if (!hasAscii(buf, 4, "ftyp")) return null;
4098
- const brand = buf.subarray(8, 12).toString("ascii");
4099
- if (brand === "avif" || brand === "avis") return "image/avif";
4100
- if (HEIC_BRANDS.has(brand)) return "image/heic";
4101
- return null;
4102
- }
4103
3666
  function sniffImageMime(buf) {
4104
3667
  if (buf.length < 4) return null;
4105
- if (buf[0] === 137 && hasAscii(buf, 1, "PNG")) return "image/png";
3668
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
4106
3669
  if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
4107
- if (hasAscii(buf, 0, "GIF")) return "image/gif";
4108
- if (hasAscii(buf, 0, "RIFF") && hasAscii(buf, 8, "WEBP")) return "image/webp";
4109
- if (hasAscii(buf, 0, "II*\0") || hasAscii(buf, 0, "MM\0*")) return "image/tiff";
4110
- if (hasAscii(buf, 0, "BM")) return "image/bmp";
4111
- return sniffIsoBmff(buf);
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;
4112
3675
  }
4113
3676
  function findBoxPayload(buf, start, end, type) {
4114
3677
  let offset = start;
@@ -4161,10 +3724,10 @@ function inferKindFromMime(mime) {
4161
3724
  if (mime.startsWith("font/")) return "font";
4162
3725
  return null;
4163
3726
  }
4164
- function localExecError(ctx, message2) {
3727
+ function localExecError(ctx, message) {
4165
3728
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
4166
3729
  kind: "local",
4167
- cause: new Error(`ingest: ${message2}`)
3730
+ cause: new Error(`ingest: ${message}`)
4168
3731
  });
4169
3732
  }
4170
3733
  async function execLocalFile(params, ctx) {
@@ -4211,20 +3774,17 @@ async function execLocalFile(params, ctx) {
4211
3774
  ctx.log(`ingest: local file ${stats.size}B mime=${mime}`);
4212
3775
  let outBytes = bytes;
4213
3776
  let outMime = mime;
4214
- let rasterizedFrom;
4215
- if (kind === "image") {
4216
- const normalized = await toModelSafeImage(bytes);
4217
- outBytes = normalized.bytes;
4218
- outMime = normalized.mime;
4219
- rasterizedFrom = normalized.rasterizedFrom;
4220
- if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
3777
+ if (mime === SVG_MIME) {
3778
+ outBytes = await rasterizeSvgToPng(bytes);
3779
+ outMime = "image/png";
3780
+ ctx.log(`ingest: rasterized SVG -> PNG (${outBytes.length}B)`);
4221
3781
  }
4222
3782
  const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
4223
3783
  const ref = await uploadAndIngest({
4224
3784
  bytes: outBytes,
4225
3785
  kind: params.expect,
4226
3786
  mime: outMime,
4227
- metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs, rasterizedFrom }),
3787
+ metadata: localFileMetadata({ absPath, fileSize: stats.size, mime, durationMs }),
4228
3788
  ctx
4229
3789
  });
4230
3790
  return withProbedDuration(ref, durationMs);
@@ -4242,7 +3802,7 @@ function localFileMetadata(args) {
4242
3802
  ingested_at: (/* @__PURE__ */ new Date()).toISOString(),
4243
3803
  file_size: args.fileSize,
4244
3804
  original_filename: path3.basename(args.absPath),
4245
- ...args.rasterizedFrom ? { rasterized_from: args.rasterizedFrom } : {},
3805
+ ...args.mime === SVG_MIME ? { rasterized_from: "svg" } : {},
4246
3806
  ...args.durationMs !== void 0 ? { duration_ms: args.durationMs } : {}
4247
3807
  };
4248
3808
  }
@@ -4769,56 +4329,19 @@ var audioTimelineNode = defineNode({
4769
4329
  }
4770
4330
  });
4771
4331
 
4772
- // src/engine/nodes/local/collect.ts
4773
- import { z as z7 } from "zod";
4774
- var collectNode = defineNode({
4775
- id: "collect",
4776
- version: "1.0.0",
4777
- category: "data",
4778
- location: "local",
4779
- passthroughRefs: true,
4780
- 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.",
4781
- 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.",
4782
- inputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
4783
- params: z7.object({ labels: z7.array(z7.string().min(1)).min(1).optional() }).strict(),
4784
- outputs: z7.object({ images: z7.array(ImageRef).min(1) }).strict(),
4785
- outputKinds: { images: "image" },
4786
- cost: () => ({ credits: 0, seconds_estimate: 0 }),
4787
- // Arity is only knowable at validate time when `images` is a literal array;
4788
- // a single `$ref:` string to an upstream array output defers to runtime.
4789
- validateExtra: ({ rawParams, rawInputs }) => {
4790
- const labels = rawParams?.labels;
4791
- if (!Array.isArray(labels)) return [];
4792
- if (new Set(labels).size !== labels.length) {
4793
- return [{ path: "params.labels", message: "labels must be unique \u2014 each names one output variant" }];
4794
- }
4795
- const images = rawInputs?.images;
4796
- if (Array.isArray(images) && labels.length !== images.length) {
4797
- return [
4798
- {
4799
- path: "params.labels",
4800
- message: `labels has ${labels.length} entries but ${images.length} images are wired \u2014 provide one label per image`
4801
- }
4802
- ];
4803
- }
4804
- return [];
4805
- },
4806
- execute: ({ inputs }) => Promise.resolve({ images: inputs.images })
4807
- });
4808
-
4809
4332
  // src/engine/nodes/local/ffmpeg.ts
4810
- import { z as z8 } from "zod";
4333
+ import { z as z7 } from "zod";
4811
4334
  var FFMPEG_BIN2 = "ffmpeg";
4812
- var OutputDecl = z8.object({
4813
- kind: z8.enum(["image", "video", "audio"]),
4814
- ext: z8.string().min(1).max(8)
4335
+ var OutputDecl = z7.object({
4336
+ kind: z7.enum(["image", "video", "audio"]),
4337
+ ext: z7.string().min(1).max(8)
4815
4338
  }).strict();
4816
- var FfmpegParams = z8.object({
4817
- args: z8.array(z8.string()).min(1),
4818
- outputs: z8.record(z8.string(), OutputDecl).default({})
4339
+ var FfmpegParams = z7.object({
4340
+ args: z7.array(z7.string()).min(1),
4341
+ outputs: z7.record(z7.string(), OutputDecl).default({})
4819
4342
  }).strict();
4820
- var FfmpegInputs = z8.record(z8.string(), z8.unknown());
4821
- var FfmpegOutputs = z8.record(z8.string(), z8.custom());
4343
+ var FfmpegInputs = z7.record(z7.string(), z7.unknown());
4344
+ var FfmpegOutputs = z7.record(z7.string(), z7.custom());
4822
4345
  var ffmpegNode = defineNode({
4823
4346
  id: "ffmpeg",
4824
4347
  version: "2.0.0",
@@ -4849,7 +4372,7 @@ import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/prom
4849
4372
  import { createRequire } from "module";
4850
4373
  import { tmpdir as tmpdir3 } from "os";
4851
4374
  import path6 from "path";
4852
- import { z as z9 } from "zod";
4375
+ import { z as z8 } from "zod";
4853
4376
 
4854
4377
  // src/engine/nodes/local/lib/assets.ts
4855
4378
  import { copyFile as copyFile3, readFile as readFile4 } from "fs/promises";
@@ -4871,7 +4394,7 @@ async function refToUrl(ref) {
4871
4394
  return `data:${ref.mime};base64,${bytes.toString("base64")}`;
4872
4395
  }
4873
4396
  var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
4874
- function isAssetRefLike2(value) {
4397
+ function isAssetRefLike(value) {
4875
4398
  if (!value || typeof value !== "object") return false;
4876
4399
  const v = value;
4877
4400
  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");
@@ -4885,15 +4408,15 @@ var DEFAULT_SPECIMEN = [
4885
4408
  "abcdefghijklmnopqrstuvwxyz",
4886
4409
  `0123456789 !?&@#$%().,:;'"-`
4887
4410
  ].join("\n");
4888
- var FontSpecimenParams = z9.object({
4889
- text: z9.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
4890
- font_size: z9.number().int().min(8).max(512).optional().default(72),
4891
- padding: z9.number().int().min(0).max(512).optional().default(64),
4892
- line_height: z9.number().min(0.8).max(3).optional().default(1.35),
4893
- max_width: z9.number().int().min(256).max(4096).optional()
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()
4894
4417
  }).strict();
4895
- var FontSpecimenInputs = z9.object({ font: FontRef }).loose();
4896
- var FontSpecimenOutputs = z9.object({ image: ImageRef }).strict();
4418
+ var FontSpecimenInputs = z8.object({ font: FontRef }).loose();
4419
+ var FontSpecimenOutputs = z8.object({ image: ImageRef }).strict();
4897
4420
  var DEVICE_SCALE_FACTOR = 2;
4898
4421
  var PAGE_TIMEOUT_MS = 3e4;
4899
4422
  function escapeHtml(text) {
@@ -5034,7 +4557,7 @@ import { createRequire as createRequire2 } from "module";
5034
4557
  import { cpus, tmpdir as tmpdir4 } from "os";
5035
4558
  import path11 from "path";
5036
4559
  import { promisify as promisify4 } from "util";
5037
- import { z as z11 } from "zod";
4560
+ import { z as z10 } from "zod";
5038
4561
 
5039
4562
  // src/engine/engine/composition-hash.ts
5040
4563
  import { readdir as readdir2, readFile as readFile5, stat as stat4 } from "fs/promises";
@@ -5072,62 +4595,62 @@ async function collectFiles(root, current) {
5072
4595
  // src/engine/engine/composition-meta.ts
5073
4596
  import { readFile as readFile6 } from "fs/promises";
5074
4597
  import path8 from "path";
5075
- import { z as z10 } from "zod";
5076
- var InputKind = z10.enum(["video", "image", "audio", "json"]);
5077
- var InputSpec = z10.object({
4598
+ import { z as z9 } from "zod";
4599
+ var InputKind = z9.enum(["video", "image", "audio", "json"]);
4600
+ var InputSpec = z9.object({
5078
4601
  kind: InputKind,
5079
- required: z10.boolean().optional().default(false),
4602
+ required: z9.boolean().optional().default(false),
5080
4603
  // Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
5081
4604
  // Defaults to `<key><ext>` derived from the kind.
5082
- staged_as: z10.string().min(1).optional(),
5083
- description: z10.string().optional()
4605
+ staged_as: z9.string().min(1).optional(),
4606
+ description: z9.string().optional()
5084
4607
  }).strict();
5085
4608
  var ParamSpecBase = {
5086
- required: z10.boolean().optional().default(false),
5087
- description: z10.string().optional()
4609
+ required: z9.boolean().optional().default(false),
4610
+ description: z9.string().optional()
5088
4611
  };
5089
- var StringParam = z10.object({
4612
+ var StringParam = z9.object({
5090
4613
  ...ParamSpecBase,
5091
- kind: z10.literal("string"),
5092
- default: z10.string().optional(),
5093
- enum: z10.array(z10.string()).optional()
4614
+ kind: z9.literal("string"),
4615
+ default: z9.string().optional(),
4616
+ enum: z9.array(z9.string()).optional()
5094
4617
  }).strict();
5095
- var IntegerParam = z10.object({
4618
+ var IntegerParam = z9.object({
5096
4619
  ...ParamSpecBase,
5097
- kind: z10.literal("integer"),
5098
- default: z10.number().int().optional(),
5099
- min: z10.number().int().optional(),
5100
- max: z10.number().int().optional()
4620
+ kind: z9.literal("integer"),
4621
+ default: z9.number().int().optional(),
4622
+ min: z9.number().int().optional(),
4623
+ max: z9.number().int().optional()
5101
4624
  }).strict();
5102
- var NumberParam = z10.object({
4625
+ var NumberParam = z9.object({
5103
4626
  ...ParamSpecBase,
5104
- kind: z10.literal("number"),
5105
- default: z10.number().optional(),
5106
- min: z10.number().optional(),
5107
- max: z10.number().optional()
4627
+ kind: z9.literal("number"),
4628
+ default: z9.number().optional(),
4629
+ min: z9.number().optional(),
4630
+ max: z9.number().optional()
5108
4631
  }).strict();
5109
- var BooleanParam = z10.object({
4632
+ var BooleanParam = z9.object({
5110
4633
  ...ParamSpecBase,
5111
- kind: z10.literal("boolean"),
5112
- default: z10.boolean().optional()
4634
+ kind: z9.literal("boolean"),
4635
+ default: z9.boolean().optional()
5113
4636
  }).strict();
5114
- var ColorParam = z10.object({
4637
+ var ColorParam = z9.object({
5115
4638
  ...ParamSpecBase,
5116
- kind: z10.literal("color"),
5117
- default: z10.string().optional()
4639
+ kind: z9.literal("color"),
4640
+ default: z9.string().optional()
5118
4641
  }).strict();
5119
- var ImageParam = z10.object({
4642
+ var ImageParam = z9.object({
5120
4643
  ...ParamSpecBase,
5121
- kind: z10.literal("image"),
5122
- default: z10.string().optional()
4644
+ kind: z9.literal("image"),
4645
+ default: z9.string().optional()
5123
4646
  }).strict();
5124
- var JsonParam = z10.object({
4647
+ var JsonParam = z9.object({
5125
4648
  ...ParamSpecBase,
5126
- kind: z10.literal("json"),
5127
- schema: z10.unknown().optional(),
5128
- default: z10.unknown().optional()
4649
+ kind: z9.literal("json"),
4650
+ schema: z9.unknown().optional(),
4651
+ default: z9.unknown().optional()
5129
4652
  }).strict();
5130
- var ParamSpec = z10.discriminatedUnion("kind", [
4653
+ var ParamSpec = z9.discriminatedUnion("kind", [
5131
4654
  StringParam,
5132
4655
  IntegerParam,
5133
4656
  NumberParam,
@@ -5136,16 +4659,16 @@ var ParamSpec = z10.discriminatedUnion("kind", [
5136
4659
  ImageParam,
5137
4660
  JsonParam
5138
4661
  ]);
5139
- var CompositionMetaSchema = z10.object({
5140
- id: z10.string().min(1),
5141
- title: z10.string().min(1),
5142
- description: z10.string().optional(),
5143
- width: z10.number().int().positive(),
5144
- height: z10.number().int().positive(),
5145
- fps: z10.number().int().positive().default(30),
5146
- default_duration: z10.number().positive().default(10),
5147
- inputs: z10.record(z10.string(), InputSpec).default({}),
5148
- params: z10.record(z10.string(), ParamSpec).default({})
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({})
5149
4672
  }).strict();
5150
4673
  async function loadCompositionMeta(compositionDir) {
5151
4674
  const metaPath = path8.join(compositionDir, "meta.json");
@@ -5173,39 +4696,39 @@ function buildParamsSchema(meta) {
5173
4696
  for (const [name, spec] of Object.entries(meta.params)) {
5174
4697
  shape[name] = buildParamFieldSchema(name, spec);
5175
4698
  }
5176
- return z10.object(shape).strict();
4699
+ return z9.object(shape).strict();
5177
4700
  }
5178
4701
  function buildParamFieldSchema(name, spec) {
5179
4702
  switch (spec.kind) {
5180
4703
  case "string": {
5181
- const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
4704
+ const s = spec.enum && spec.enum.length > 0 ? z9.enum(spec.enum) : z9.string();
5182
4705
  return finalize(s, spec.default, spec.required);
5183
4706
  }
5184
4707
  case "integer": {
5185
- let s = z10.number().int();
4708
+ let s = z9.number().int();
5186
4709
  if (spec.min !== void 0) s = s.min(spec.min);
5187
4710
  if (spec.max !== void 0) s = s.max(spec.max);
5188
4711
  return finalize(s, spec.default, spec.required);
5189
4712
  }
5190
4713
  case "number": {
5191
- let s = z10.number();
4714
+ let s = z9.number();
5192
4715
  if (spec.min !== void 0) s = s.min(spec.min);
5193
4716
  if (spec.max !== void 0) s = s.max(spec.max);
5194
4717
  return finalize(s, spec.default, spec.required);
5195
4718
  }
5196
4719
  case "boolean":
5197
- return finalize(z10.boolean(), spec.default, spec.required);
4720
+ return finalize(z9.boolean(), spec.default, spec.required);
5198
4721
  case "color": {
5199
- const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
4722
+ const s = z9.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
5200
4723
  message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
5201
4724
  });
5202
4725
  return finalize(s, spec.default, spec.required);
5203
4726
  }
5204
4727
  case "image":
5205
- return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
4728
+ return finalize(z9.union([z9.string().min(1), z9.record(z9.string(), z9.unknown())]), spec.default, spec.required);
5206
4729
  case "json":
5207
4730
  return finalize(
5208
- z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
4731
+ z9.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
5209
4732
  spec.default,
5210
4733
  spec.required
5211
4734
  );
@@ -5243,8 +4766,8 @@ var NEVER_BLOCK = [
5243
4766
  /text[_-]?occluded/i
5244
4767
  ];
5245
4768
  var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
5246
- function isAdvisory(code, message2) {
5247
- const hay = `${code} ${message2}`;
4769
+ function isAdvisory(code, message) {
4770
+ const hay = `${code} ${message}`;
5248
4771
  return NEVER_BLOCK.some((re) => re.test(hay));
5249
4772
  }
5250
4773
  function parseCheckJson(raw) {
@@ -5272,10 +4795,10 @@ function classifyLint(json) {
5272
4795
  for (const f of findings) {
5273
4796
  const rec = f;
5274
4797
  const code = String(rec?.code ?? "");
5275
- const message2 = String(rec?.message ?? "");
4798
+ const message = String(rec?.message ?? "");
5276
4799
  const severity = String(rec?.severity ?? "info");
5277
- const blocking = severity === "error" && !isAdvisory(code, message2);
5278
- out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
4800
+ const blocking = severity === "error" && !isAdvisory(code, message);
4801
+ out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
5279
4802
  }
5280
4803
  return out;
5281
4804
  }
@@ -5287,9 +4810,9 @@ function classifyInspect(json) {
5287
4810
  for (const iss of issues) {
5288
4811
  const rec = iss;
5289
4812
  const code = String(rec?.code ?? rec?.type ?? "overflow");
5290
- const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4813
+ const message = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
5291
4814
  const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
5292
- out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
4815
+ out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
5293
4816
  }
5294
4817
  return out;
5295
4818
  }
@@ -5505,17 +5028,17 @@ function literalize(value) {
5505
5028
  // src/engine/nodes/local/hyperframe.ts
5506
5029
  var execFileAsync2 = promisify4(execFile4);
5507
5030
  var require_2 = createRequire2(import.meta.url);
5508
- var HyperframeParams = z11.object({
5509
- composition: z11.string().min(1),
5031
+ var HyperframeParams = z10.object({
5032
+ composition: z10.string().min(1),
5510
5033
  // Output container. mp4 (default) for delivery; webm/mov render WITH
5511
5034
  // transparency (alpha) when the composition background is transparent —
5512
5035
  // use for motion-graphic overlays dropped into Premiere/AE/Nuke.
5513
- format: z11.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
5514
- timeout_ms: z11.number().int().positive().optional().default(10 * 60 * 1e3)
5515
- }).catchall(z11.unknown());
5516
- var HyperframeInputs = z11.record(z11.string(), z11.custom()).optional().default({});
5517
- var HyperframeOutputs = z11.object({
5518
- video: z11.custom()
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()
5519
5042
  }).strict();
5520
5043
  var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
5521
5044
  var MIME_BY_FORMAT = {
@@ -5734,7 +5257,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
5734
5257
  }
5735
5258
  function coerceImageParam(value) {
5736
5259
  if (typeof value === "string") return Promise.resolve(value);
5737
- if (isAssetRefLike2(value)) return refToUrl(value);
5260
+ if (isAssetRefLike(value)) return refToUrl(value);
5738
5261
  throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
5739
5262
  }
5740
5263
  async function substituteCompositionFiles(tmp, values) {
@@ -5804,23 +5327,23 @@ import { createRequire as createRequire3 } from "module";
5804
5327
  import { tmpdir as tmpdir5 } from "os";
5805
5328
  import path12 from "path";
5806
5329
  import { promisify as promisify5 } from "util";
5807
- import { z as z12 } from "zod";
5330
+ import { z as z11 } from "zod";
5808
5331
  var _execFileAsync = promisify5(execFile5);
5809
5332
  var require_3 = createRequire3(import.meta.url);
5810
- var WaitForSpec = z12.discriminatedUnion("kind", [
5811
- z12.object({ kind: z12.literal("auto") }),
5812
- z12.object({ kind: z12.literal("selector"), value: z12.string().min(1) }),
5813
- z12.object({ kind: z12.literal("function"), value: z12.string().min(1) }),
5814
- z12.object({ kind: z12.literal("timeout"), ms: z12.number().int().min(0).max(6e4) })
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) })
5815
5338
  ]);
5816
- var HyperframeSnapshotParams = z12.object({
5817
- composition: z12.string().min(1),
5339
+ var HyperframeSnapshotParams = z11.object({
5340
+ composition: z11.string().min(1),
5818
5341
  wait_for: WaitForSpec.optional().default({ kind: "auto" }),
5819
- timeout_ms: z12.number().int().positive().optional().default(6e4)
5820
- }).catchall(z12.unknown());
5821
- var HyperframeSnapshotInputs = z12.record(z12.string(), z12.custom()).optional().default({});
5822
- var HyperframeSnapshotOutputs = z12.object({
5823
- image: z12.custom()
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()
5824
5347
  }).strict();
5825
5348
  var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
5826
5349
  var DEVICE_SCALE_FACTOR2 = 2;
@@ -5964,7 +5487,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
5964
5487
  }
5965
5488
  function coerceImageParam2(value) {
5966
5489
  if (typeof value === "string") return Promise.resolve(value);
5967
- if (isAssetRefLike2(value)) return refToUrl(value);
5490
+ if (isAssetRefLike(value)) return refToUrl(value);
5968
5491
  throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
5969
5492
  }
5970
5493
  async function substituteCompositionFiles2(tmp, values) {
@@ -6012,18 +5535,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
6012
5535
  // src/engine/nodes/local/imagemagick.ts
6013
5536
  import { execFile as execFile6 } from "child_process";
6014
5537
  import { promisify as promisify6 } from "util";
6015
- import { z as z13 } from "zod";
5538
+ import { z as z12 } from "zod";
6016
5539
  var execFileAsync3 = promisify6(execFile6);
6017
- var OutputDecl2 = z13.object({
6018
- kind: z13.enum(["image", "video", "audio"]),
6019
- ext: z13.string().min(1).max(8)
5540
+ var OutputDecl2 = z12.object({
5541
+ kind: z12.enum(["image", "video", "audio"]),
5542
+ ext: z12.string().min(1).max(8)
6020
5543
  }).strict();
6021
- var ImageMagickParams = z13.object({
6022
- args: z13.array(z13.string()).min(1),
6023
- outputs: z13.record(z13.string(), OutputDecl2).default({})
5544
+ var ImageMagickParams = z12.object({
5545
+ args: z12.array(z12.string()).min(1),
5546
+ outputs: z12.record(z12.string(), OutputDecl2).default({})
6024
5547
  }).strict();
6025
- var ImageMagickInputs = z13.record(z13.string(), z13.unknown());
6026
- var ImageMagickOutputs = z13.record(z13.string(), z13.custom());
5548
+ var ImageMagickInputs = z12.record(z12.string(), z12.unknown());
5549
+ var ImageMagickOutputs = z12.record(z12.string(), z12.custom());
6027
5550
  var resolvedBin;
6028
5551
  async function resolveBin() {
6029
5552
  if (resolvedBin) return resolvedBin;
@@ -6065,29 +5588,29 @@ var imagemagickNode = defineNode({
6065
5588
  });
6066
5589
 
6067
5590
  // src/engine/nodes/local/text.ts
6068
- import { z as z14 } from "zod";
5591
+ import { z as z13 } from "zod";
6069
5592
  var textNode = defineNode({
6070
5593
  id: "text",
6071
5594
  version: "1.0.0",
6072
5595
  category: "data",
6073
5596
  location: "local",
6074
5597
  summary: "A literal text value. Use for prompts, descriptions, copy.",
6075
- inputs: z14.object({}).strict(),
6076
- params: z14.object({ value: z14.string() }).strict(),
6077
- outputs: z14.object({ text: z14.string() }).strict(),
5598
+ inputs: z13.object({}).strict(),
5599
+ params: z13.object({ value: z13.string() }).strict(),
5600
+ outputs: z13.object({ text: z13.string() }).strict(),
6078
5601
  cost: () => ({ credits: 0, seconds_estimate: 0 }),
6079
5602
  execute: ({ params }) => Promise.resolve({ text: params.value })
6080
5603
  });
6081
5604
 
6082
5605
  // src/engine/nodes/remote/audioVoiceConvert.ts
6083
- import { z as z15 } from "zod";
6084
- var AudioVoiceConvertParams = z15.object({
6085
- model: z15.literal("elevenlabs/eleven_multilingual_sts_v2"),
5606
+ import { z as z14 } from "zod";
5607
+ var AudioVoiceConvertParams = z14.object({
5608
+ model: z14.literal("elevenlabs/eleven_multilingual_sts_v2"),
6086
5609
  /** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
6087
- voice: z15.string().min(1),
6088
- output_format: z15.string().optional(),
5610
+ voice: z14.string().min(1),
5611
+ output_format: z14.string().optional(),
6089
5612
  /** Strip the source clip's background noise before re-voicing. */
6090
- remove_background_noise: z15.boolean().optional()
5613
+ remove_background_noise: z14.boolean().optional()
6091
5614
  }).strict();
6092
5615
  var audioVoiceConvertNode = delegated({
6093
5616
  id: "audio_voice_convert",
@@ -6095,44 +5618,44 @@ var audioVoiceConvertNode = delegated({
6095
5618
  category: "audio",
6096
5619
  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.",
6097
5620
  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}}"`.',
6098
- inputs: z15.object({
5621
+ inputs: z14.object({
6099
5622
  audio: AudioRef,
6100
5623
  voice_ref: TextRef.optional()
6101
5624
  }).strict(),
6102
5625
  params: AudioVoiceConvertParams,
6103
- outputs: z15.object({ audio: AudioRef }).strict(),
5626
+ outputs: z14.object({ audio: AudioRef }).strict(),
6104
5627
  outputKinds: { audio: "audio" },
6105
5628
  cost: () => ({ credits: 1, seconds_estimate: 20 })
6106
5629
  });
6107
5630
 
6108
5631
  // src/engine/nodes/remote/dialogue.ts
6109
- import { z as z16 } from "zod";
6110
- var DialogueInput = z16.object({
6111
- text: z16.string().min(1),
6112
- voice_id: z16.string().min(1)
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)
6113
5636
  });
6114
5637
  var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
6115
- var DialogueParams = z16.object({
6116
- model: z16.enum(DIALOGUE_MODELS),
5638
+ var DialogueParams = z15.object({
5639
+ model: z15.enum(DIALOGUE_MODELS),
6117
5640
  /**
6118
5641
  * Ordered list of lines, each tagged with the voice that should speak it.
6119
5642
  * Up to 10 unique voice_ids; total text across all lines should stay under
6120
5643
  * ~2000 characters for best quality (ElevenLabs guidance).
6121
5644
  */
6122
- inputs: z16.array(DialogueInput).min(1).max(50),
6123
- language_code: z16.string().optional(),
5645
+ inputs: z15.array(DialogueInput).min(1).max(50),
5646
+ language_code: z15.string().optional(),
6124
5647
  /** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
6125
- settings: z16.record(z16.string(), z16.unknown()).optional(),
6126
- seed: z16.number().int().min(0).max(4294967295).optional(),
6127
- apply_text_normalization: z16.enum(["auto", "on", "off"]).optional(),
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(),
6128
5651
  /**
6129
5652
  * When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
6130
5653
  * separate `timestamps` output — character-level alignment plus
6131
5654
  * per-voice segment markers usable for captions, lipsync, or
6132
5655
  * beat-matched cuts in ad creatives.
6133
5656
  */
6134
- with_timestamps: z16.boolean().optional(),
6135
- output_format: z16.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5657
+ with_timestamps: z15.boolean().optional(),
5658
+ output_format: z15.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6136
5659
  }).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
6137
5660
  message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
6138
5661
  path: ["inputs"]
@@ -6143,9 +5666,9 @@ var dialogueNode = delegated({
6143
5666
  category: "audio",
6144
5667
  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.",
6145
5668
  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.",
6146
- inputs: z16.object({}).loose(),
5669
+ inputs: z15.object({}).loose(),
6147
5670
  params: DialogueParams,
6148
- outputs: z16.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
5671
+ outputs: z15.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6149
5672
  outputKinds: { audio: "audio", timestamps: "json" },
6150
5673
  cost: ({ params }) => {
6151
5674
  const chars = params.inputs.reduce((sum, line) => sum + line.text.length, 0);
@@ -6154,7 +5677,7 @@ var dialogueNode = delegated({
6154
5677
  });
6155
5678
 
6156
5679
  // src/engine/nodes/remote/image.ts
6157
- import { z as z17 } from "zod";
5680
+ import { z as z16 } from "zod";
6158
5681
  var IMAGE_GENERATE_MODELS2 = [
6159
5682
  "openai/gpt-5.4-image-2",
6160
5683
  "google/gemini-3.5-flash",
@@ -6162,16 +5685,16 @@ var IMAGE_GENERATE_MODELS2 = [
6162
5685
  "google/gemini-3-pro-image-preview",
6163
5686
  "recraft/recraft-v4.1-pro-vector"
6164
5687
  ];
6165
- var ImageGenerateParams = z17.object({
6166
- model: z17.enum(IMAGE_GENERATE_MODELS2),
6167
- prompt: z17.string().min(1),
6168
- 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(),
6169
- image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
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(),
6170
5693
  // Recraft v4 vector controls — forwarded into `image_config`. Registry
6171
5694
  // rejects them on non-Recraft models.
6172
- strength: z17.number().min(0).max(1).optional(),
6173
- rgb_colors: z17.array(z17.array(z17.number().int().min(0).max(255))).optional(),
6174
- background_rgb_color: z17.array(z17.number().int().min(0).max(255)).optional()
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()
6175
5698
  }).strict();
6176
5699
  var imageGenerateNode = delegated({
6177
5700
  id: "image_generate",
@@ -6181,22 +5704,22 @@ var imageGenerateNode = delegated({
6181
5704
  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.",
6182
5705
  // `reference` is one image or an ordered array of images. The backend forwards
6183
5706
  // each as a separate `image_url` to the provider (OpenRouter accepts many).
6184
- inputs: z17.object({ reference: z17.union([ImageRef, z17.array(ImageRef).min(1)]).optional() }).loose(),
5707
+ inputs: z16.object({ reference: z16.union([ImageRef, z16.array(ImageRef).min(1)]).optional() }).loose(),
6185
5708
  params: ImageGenerateParams,
6186
- outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
5709
+ outputs: z16.object({ images: z16.array(ImageRef).min(1) }).strict(),
6187
5710
  outputKinds: { images: "image" },
6188
5711
  cost: () => ({ credits: 5, seconds_estimate: 10 })
6189
5712
  });
6190
5713
 
6191
5714
  // src/engine/nodes/remote/imageAspectAdapt.ts
6192
- import { z as z18 } from "zod";
5715
+ import { z as z17 } from "zod";
6193
5716
  var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
6194
5717
  var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
6195
- var ImageAspectAdaptParams = z18.object({
6196
- model: z18.enum(ASPECT_ADAPT_MODELS),
6197
- 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" }),
6198
- guidance: z18.string().min(1).optional(),
6199
- image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
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()
6200
5723
  }).strict();
6201
5724
  var imageAspectAdaptNode = delegated({
6202
5725
  id: "image_aspect_adapt",
@@ -6204,9 +5727,9 @@ var imageAspectAdaptNode = delegated({
6204
5727
  category: "image",
6205
5728
  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`.",
6206
5729
  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.",
6207
- inputs: z18.object({ source: ImageRef }).loose(),
5730
+ inputs: z17.object({ source: ImageRef }).loose(),
6208
5731
  params: ImageAspectAdaptParams,
6209
- outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
5732
+ outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
6210
5733
  outputKinds: { images: "image" },
6211
5734
  cost: ({ params }) => {
6212
5735
  const p = params;
@@ -6219,12 +5742,12 @@ var imageAspectAdaptNode = delegated({
6219
5742
  });
6220
5743
 
6221
5744
  // src/engine/nodes/remote/imageBackgroundRemove.ts
6222
- import { z as z19 } from "zod";
6223
- var ImageBackgroundRemoveParams = z19.object({
6224
- model: z19.literal("fal/birefnet-v2").optional().default("fal/birefnet-v2"),
6225
- model_variant: z19.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().default("General Use (Light)"),
6226
- operating_resolution: z19.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
6227
- mask_only: z19.boolean().optional().default(false)
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)
6228
5751
  }).strict();
6229
5752
  var imageBackgroundRemoveNode = delegated({
6230
5753
  id: "image_background_remove",
@@ -6232,11 +5755,11 @@ var imageBackgroundRemoveNode = delegated({
6232
5755
  category: "image",
6233
5756
  summary: "Remove the background from an image and return a transparent PNG (or the segmentation mask). Powered by fal.ai `fal-ai/birefnet/v2`.",
6234
5757
  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.",
6235
- inputs: z19.object({
5758
+ inputs: z18.object({
6236
5759
  image: ImageRef
6237
5760
  }).strict(),
6238
5761
  params: ImageBackgroundRemoveParams,
6239
- outputs: z19.object({
5762
+ outputs: z18.object({
6240
5763
  image: ImageRef,
6241
5764
  mask: ImageRef.optional()
6242
5765
  }).strict(),
@@ -6245,7 +5768,7 @@ var imageBackgroundRemoveNode = delegated({
6245
5768
  });
6246
5769
 
6247
5770
  // src/engine/nodes/remote/imageDescribe.ts
6248
- import { z as z20 } from "zod";
5771
+ import { z as z19 } from "zod";
6249
5772
  var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
6250
5773
  var imageDescribeNode = delegated({
6251
5774
  id: "image_describe",
@@ -6253,33 +5776,33 @@ var imageDescribeNode = delegated({
6253
5776
  category: "vision",
6254
5777
  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.",
6255
5778
  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.',
6256
- inputs: z20.object({ image: ImageRef }).loose(),
6257
- params: z20.object({
6258
- model: z20.enum(IMAGE_DESCRIBE_MODELS),
6259
- focus: z20.string().optional(),
6260
- context: z20.string().optional(),
6261
- temperature: z20.number().min(0).max(2).optional(),
6262
- max_tokens: z20.number().int().positive().optional()
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()
6263
5786
  }).strict(),
6264
- outputs: z20.object({ description: JsonRef }).strict(),
5787
+ outputs: z19.object({ description: JsonRef }).strict(),
6265
5788
  outputKinds: { description: "json" },
6266
5789
  cost: () => ({ credits: 2, seconds_estimate: 10 })
6267
5790
  });
6268
5791
 
6269
5792
  // src/engine/nodes/remote/imageReferenceSheet.ts
6270
- import { z as z21 } from "zod";
5793
+ import { z as z20 } from "zod";
6271
5794
  var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
6272
- var ImageReferenceSheetParams = z21.object({
6273
- model: z21.enum(REFERENCE_SHEET_MODELS),
6274
- subject_description: z21.string().min(1),
5795
+ var ImageReferenceSheetParams = z20.object({
5796
+ model: z20.enum(REFERENCE_SHEET_MODELS),
5797
+ subject_description: z20.string().min(1),
6275
5798
  // `location` = a set/room shown from several camera ANGLES (not a rotated subject),
6276
5799
  // so a multi-scene shoot keeps one consistent set.
6277
- subject_type: z21.enum(["character", "person", "product", "location"]),
6278
- views: z21.array(z21.string().min(1)).min(2).max(8).optional(),
6279
- style: z21.string().optional(),
6280
- prompt_override: z21.string().min(1).optional(),
6281
- 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(),
6282
- image_size: z21.enum(["0.5K", "1K", "2K", "4K"]).optional()
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()
6283
5806
  }).strict();
6284
5807
  var imageReferenceSheetNode = delegated({
6285
5808
  id: "image_reference_sheet",
@@ -6287,9 +5810,9 @@ var imageReferenceSheetNode = delegated({
6287
5810
  category: "image",
6288
5811
  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).",
6289
5812
  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.",
6290
- inputs: z21.object({ references: z21.array(ImageRef).min(1).max(6) }).loose(),
5813
+ inputs: z20.object({ references: z20.array(ImageRef).min(1).max(6) }).loose(),
6291
5814
  params: ImageReferenceSheetParams,
6292
- outputs: z21.object({ sheet: ImageRef }).strict(),
5815
+ outputs: z20.object({ sheet: ImageRef }).strict(),
6293
5816
  outputKinds: { sheet: "image" },
6294
5817
  cost: ({ params }) => ({
6295
5818
  credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
@@ -6298,10 +5821,10 @@ var imageReferenceSheetNode = delegated({
6298
5821
  });
6299
5822
 
6300
5823
  // src/engine/nodes/remote/imageSearch.ts
6301
- import { z as z22 } from "zod";
6302
- var ImageSearchParams = z22.object({
6303
- prompt: z22.string().min(1),
6304
- count: z22.number().int().min(1).max(20).default(5)
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)
6305
5828
  }).strict();
6306
5829
  var imageSearchNode = delegated({
6307
5830
  id: "image_search",
@@ -6309,15 +5832,15 @@ var imageSearchNode = delegated({
6309
5832
  category: "image",
6310
5833
  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.",
6311
5834
  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.",
6312
- inputs: z22.object({}).loose(),
5835
+ inputs: z21.object({}).loose(),
6313
5836
  params: ImageSearchParams,
6314
- outputs: z22.object({ images: z22.array(ImageRef).min(1) }).strict(),
5837
+ outputs: z21.object({ images: z21.array(ImageRef).min(1) }).strict(),
6315
5838
  outputKinds: { images: "image" },
6316
5839
  cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
6317
5840
  });
6318
5841
 
6319
5842
  // src/engine/nodes/remote/imageSelect.ts
6320
- import { z as z23 } from "zod";
5843
+ import { z as z22 } from "zod";
6321
5844
  var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
6322
5845
  var imageSelectNode = delegated({
6323
5846
  id: "image_select",
@@ -6325,15 +5848,15 @@ var imageSelectNode = delegated({
6325
5848
  category: "vision",
6326
5849
  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.",
6327
5850
  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.",
6328
- inputs: z23.object({ images: z23.array(ImageRef).min(2) }).loose(),
6329
- params: z23.object({
6330
- model: z23.enum(IMAGE_SELECT_MODELS),
6331
- prompt: z23.string().min(1),
6332
- count: z23.number().int().min(1).default(1),
6333
- temperature: z23.number().min(0).max(2).optional(),
6334
- max_tokens: z23.number().int().positive().optional()
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()
6335
5858
  }).strict(),
6336
- outputs: z23.object({ images: z23.array(ImageRef).min(1), reasoning: TextRef }).strict(),
5859
+ outputs: z22.object({ images: z22.array(ImageRef).min(1), reasoning: TextRef }).strict(),
6337
5860
  outputKinds: { images: "image", reasoning: "text" },
6338
5861
  cost: () => ({ credits: 1, seconds_estimate: 5 }),
6339
5862
  // Arity is only knowable at validate time when `images` is a literal array
@@ -6358,34 +5881,34 @@ var imageSelectNode = delegated({
6358
5881
  });
6359
5882
 
6360
5883
  // src/engine/nodes/remote/music.ts
6361
- import { z as z24 } from "zod";
5884
+ import { z as z23 } from "zod";
6362
5885
  var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
6363
- var MusicParams = z24.object({
6364
- model: z24.enum(MUSIC_MODELS),
5886
+ var MusicParams = z23.object({
5887
+ model: z23.enum(MUSIC_MODELS),
6365
5888
  /** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
6366
- prompt: z24.string().optional(),
5889
+ prompt: z23.string().optional(),
6367
5890
  /**
6368
5891
  * Structured composition plan (intro / hook / verse / outro sections with
6369
5892
  * per-section styles + durations). Mutually exclusive with `prompt`.
6370
5893
  */
6371
- composition_plan: z24.record(z24.string(), z24.unknown()).optional(),
5894
+ composition_plan: z23.record(z23.string(), z23.unknown()).optional(),
6372
5895
  /** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
6373
- music_length_ms: z24.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
6374
- seed: z24.number().int().optional(),
5896
+ music_length_ms: z23.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
5897
+ seed: z23.number().int().optional(),
6375
5898
  /** Prompt mode only — forces an instrumental (no vocals) track. */
6376
- force_instrumental: z24.boolean().optional(),
5899
+ force_instrumental: z23.boolean().optional(),
6377
5900
  /** composition_plan only — honor exact section durations. */
6378
- respect_sections_durations: z24.boolean().optional(),
5901
+ respect_sections_durations: z23.boolean().optional(),
6379
5902
  /** Emit word-level timestamps alongside the audio. */
6380
- with_timestamps: z24.boolean().optional(),
5903
+ with_timestamps: z23.boolean().optional(),
6381
5904
  /**
6382
5905
  * video-to-music only — short description of the desired score
6383
5906
  * ("upbeat synth, fast cuts, 80s") used to bias the model.
6384
5907
  */
6385
- description: z24.string().max(1e3).optional(),
5908
+ description: z23.string().max(1e3).optional(),
6386
5909
  /** video-to-music only — up to 10 style tags. */
6387
- tags: z24.array(z24.string()).max(10).optional(),
6388
- output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5910
+ tags: z23.array(z23.string()).max(10).optional(),
5911
+ output_format: z23.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6389
5912
  }).strict();
6390
5913
  var musicNode = delegated({
6391
5914
  id: "music",
@@ -6393,9 +5916,9 @@ var musicNode = delegated({
6393
5916
  category: "audio",
6394
5917
  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`.",
6395
5918
  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.",
6396
- inputs: z24.object({ video: VideoRef.optional() }).loose(),
5919
+ inputs: z23.object({ video: VideoRef.optional() }).loose(),
6397
5920
  params: MusicParams,
6398
- outputs: z24.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
5921
+ outputs: z23.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6399
5922
  outputKinds: { audio: "audio", timestamps: "json" },
6400
5923
  cost: ({ params }) => {
6401
5924
  const seconds = params.music_length_ms ? Math.ceil(params.music_length_ms / 1e3) : 30;
@@ -6426,25 +5949,25 @@ var musicNode = delegated({
6426
5949
  });
6427
5950
 
6428
5951
  // src/engine/nodes/remote/soundEffect.ts
6429
- import { z as z25 } from "zod";
5952
+ import { z as z24 } from "zod";
6430
5953
  var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
6431
- var SoundEffectParams = z25.object({
6432
- model: z25.enum(SOUND_EFFECT_MODELS),
5954
+ var SoundEffectParams = z24.object({
5955
+ model: z24.enum(SOUND_EFFECT_MODELS),
6433
5956
  /** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
6434
- text: z25.string().min(1),
5957
+ text: z24.string().min(1),
6435
5958
  /**
6436
5959
  * Target length in seconds. 0.5–30. Leave unset to let the model pick the
6437
5960
  * natural length for the described effect.
6438
5961
  */
6439
- duration_seconds: z25.number().min(0.5).max(30).optional(),
5962
+ duration_seconds: z24.number().min(0.5).max(30).optional(),
6440
5963
  /**
6441
5964
  * 0–1. Higher = stick closer to the prompt at the cost of variety; lower
6442
5965
  * = let the model interpret more freely. Defaults to 0.3 on the provider.
6443
5966
  */
6444
- prompt_influence: z25.number().min(0).max(1).optional(),
5967
+ prompt_influence: z24.number().min(0).max(1).optional(),
6445
5968
  /** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
6446
- loop: z25.boolean().optional(),
6447
- output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
5969
+ loop: z24.boolean().optional(),
5970
+ output_format: z24.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
6448
5971
  }).strict();
6449
5972
  var soundEffectNode = delegated({
6450
5973
  id: "sound_effect",
@@ -6452,9 +5975,9 @@ var soundEffectNode = delegated({
6452
5975
  category: "audio",
6453
5976
  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.",
6454
5977
  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.",
6455
- inputs: z25.object({}).loose(),
5978
+ inputs: z24.object({}).loose(),
6456
5979
  params: SoundEffectParams,
6457
- outputs: z25.object({ audio: AudioRef }).strict(),
5980
+ outputs: z24.object({ audio: AudioRef }).strict(),
6458
5981
  outputKinds: { audio: "audio" },
6459
5982
  cost: ({ params }) => {
6460
5983
  const seconds = params.duration_seconds ?? 5;
@@ -6463,7 +5986,7 @@ var soundEffectNode = delegated({
6463
5986
  });
6464
5987
 
6465
5988
  // src/engine/nodes/remote/textGenerate.ts
6466
- import { z as z26 } from "zod";
5989
+ import { z as z25 } from "zod";
6467
5990
  var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
6468
5991
  var textGenerateNode = delegated({
6469
5992
  id: "text_generate",
@@ -6471,58 +5994,58 @@ var textGenerateNode = delegated({
6471
5994
  category: "language",
6472
5995
  summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
6473
5996
  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.',
6474
- inputs: z26.object({}).loose(),
6475
- params: z26.object({
6476
- model: z26.enum(TEXT_GENERATE_MODELS),
6477
- prompt: z26.string().min(1),
6478
- system: z26.string().optional(),
6479
- response_format: z26.enum(["text", "json_object"]).optional(),
6480
- web_search: z26.boolean().optional(),
6481
- temperature: z26.number().min(0).max(2).optional(),
6482
- max_tokens: z26.number().int().positive().optional()
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()
6483
6006
  }).strict(),
6484
- outputs: z26.object({ text: TextRef }).strict(),
6007
+ outputs: z25.object({ text: TextRef }).strict(),
6485
6008
  outputKinds: { text: "text" },
6486
6009
  cost: () => ({ credits: 1, seconds_estimate: 3 })
6487
6010
  });
6488
6011
 
6489
6012
  // src/engine/nodes/remote/tts.ts
6490
- import { z as z27 } from "zod";
6013
+ import { z as z26 } from "zod";
6491
6014
  var TTS_MODELS = ["elevenlabs/eleven_v3"];
6492
- var TtsVoiceSettings = z27.object({
6493
- stability: z27.number().min(0).max(1).optional(),
6494
- similarity_boost: z27.number().min(0).max(1).optional(),
6495
- style: z27.number().min(0).max(1).optional(),
6496
- use_speaker_boost: z27.boolean().optional(),
6497
- speed: z27.number().min(0.25).max(4).optional()
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()
6498
6021
  }).strict();
6499
- var TtsPronunciationLocator = z27.object({
6500
- pronunciation_dictionary_id: z27.string().min(1),
6501
- version_id: z27.string().nullable().optional()
6022
+ var TtsPronunciationLocator = z26.object({
6023
+ pronunciation_dictionary_id: z26.string().min(1),
6024
+ version_id: z26.string().nullable().optional()
6502
6025
  }).strict();
6503
- var TtsParams = z27.object({
6504
- model: z27.enum(TTS_MODELS),
6505
- text: z27.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
6506
- voice: z27.string().min(1),
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),
6507
6030
  /** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
6508
- output_format: z27.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
6509
- seed: z27.number().int().min(0).max(4294967295).optional(),
6031
+ output_format: z26.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
6032
+ seed: z26.number().int().min(0).max(4294967295).optional(),
6510
6033
  // Top-level shortcuts; structured form is `voice_settings`.
6511
- stability: z27.number().min(0).max(1).optional(),
6512
- similarity_boost: z27.number().min(0).max(1).optional(),
6034
+ stability: z26.number().min(0).max(1).optional(),
6035
+ similarity_boost: z26.number().min(0).max(1).optional(),
6513
6036
  voice_settings: TtsVoiceSettings.optional(),
6514
6037
  /** ISO 639-1 language code. eleven_v3 supports language hints. */
6515
- language_code: z27.string().optional(),
6516
- pronunciation_dictionary_locators: z27.array(TtsPronunciationLocator).max(3).optional(),
6517
- apply_text_normalization: z27.enum(["auto", "on", "off"]).optional(),
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(),
6518
6041
  /** Currently Japanese-only. Adds latency. */
6519
- apply_language_text_normalization: z27.boolean().optional(),
6042
+ apply_language_text_normalization: z26.boolean().optional(),
6520
6043
  /**
6521
6044
  * When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
6522
6045
  * adds a `timestamps` output (character-level alignment) for caption
6523
6046
  * rendering, lipsync, and beat-matched cuts.
6524
6047
  */
6525
- with_timestamps: z27.boolean().optional()
6048
+ with_timestamps: z26.boolean().optional()
6526
6049
  }).strict();
6527
6050
  var ttsNode = delegated({
6528
6051
  id: "tts",
@@ -6530,9 +6053,9 @@ var ttsNode = delegated({
6530
6053
  category: "audio",
6531
6054
  summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
6532
6055
  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).",
6533
- inputs: z27.object({}).loose(),
6056
+ inputs: z26.object({}).loose(),
6534
6057
  params: TtsParams,
6535
- outputs: z27.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6058
+ outputs: z26.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
6536
6059
  outputKinds: { audio: "audio", timestamps: "json" },
6537
6060
  cost: ({ params }) => ({
6538
6061
  credits: Math.max(1, Math.ceil(params.text.length * 15e-4)),
@@ -6541,23 +6064,23 @@ var ttsNode = delegated({
6541
6064
  });
6542
6065
 
6543
6066
  // src/engine/nodes/remote/video.ts
6544
- import { z as z28 } from "zod";
6067
+ import { z as z27 } from "zod";
6545
6068
  var VIDEO_GENERATE_MODELS = ["bytedance/seedance-2.0", "google/veo-3.1-fast"];
6546
- var VideoGenerateParams = z28.object({
6547
- model: z28.enum(VIDEO_GENERATE_MODELS),
6548
- prompt: z28.string().min(1),
6549
- duration: z28.number().int().positive().optional(),
6550
- resolution: z28.string().optional(),
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(),
6551
6074
  // Union of ratios accepted by at least one curated model (registry gates
6552
6075
  // per-model). 3:2/2:3 are deliberately absent: no registered model takes them.
6553
- aspect_ratio: z28.enum(["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional(),
6554
- generate_audio: z28.boolean().optional(),
6555
- seed: z28.number().int().nonnegative().optional(),
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(),
6556
6079
  // Veo-only passthroughs (routed via `provider.options.google-vertex.parameters`).
6557
- negative_prompt: z28.string().optional(),
6558
- person_generation: z28.string().optional(),
6559
- enhance_prompt: z28.boolean().optional(),
6560
- conditioning_scale: z28.number().optional()
6080
+ negative_prompt: z27.string().optional(),
6081
+ person_generation: z27.string().optional(),
6082
+ enhance_prompt: z27.boolean().optional(),
6083
+ conditioning_scale: z27.number().optional()
6561
6084
  }).strict();
6562
6085
  var videoGenerateNode = delegated({
6563
6086
  id: "video_generate",
@@ -6565,23 +6088,23 @@ var videoGenerateNode = delegated({
6565
6088
  category: "video",
6566
6089
  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.",
6567
6090
  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.",
6568
- inputs: z28.object({
6091
+ inputs: z27.object({
6569
6092
  first_frame: ImageRef.optional(),
6570
6093
  last_frame: ImageRef.optional(),
6571
6094
  reference: ImageRef.optional()
6572
6095
  }).loose(),
6573
6096
  params: VideoGenerateParams,
6574
- outputs: z28.object({ video: VideoRef }).strict(),
6097
+ outputs: z27.object({ video: VideoRef }).strict(),
6575
6098
  outputKinds: { video: "video" },
6576
6099
  cost: () => ({ credits: 50, seconds_estimate: 120 })
6577
6100
  });
6578
6101
 
6579
6102
  // src/engine/nodes/remote/videoBackgroundRemove.ts
6580
- import { z as z29 } from "zod";
6581
- var VideoBackgroundRemoveParams = z29.object({
6582
- model: z29.literal("fal/veed-video-background-removal").optional().default("fal/veed-video-background-removal"),
6583
- edge_refinement: z29.boolean().optional().default(true),
6584
- output_codec: z29.enum(["vp9", "h264"]).optional().default("vp9")
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")
6585
6108
  }).strict();
6586
6109
  var videoBackgroundRemoveNode = delegated({
6587
6110
  id: "video_background_remove",
@@ -6589,18 +6112,18 @@ var videoBackgroundRemoveNode = delegated({
6589
6112
  category: "video",
6590
6113
  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`.",
6591
6114
  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.",
6592
- inputs: z29.object({
6115
+ inputs: z28.object({
6593
6116
  video: VideoRef
6594
6117
  }).strict(),
6595
6118
  params: VideoBackgroundRemoveParams,
6596
- outputs: z29.object({ video: VideoRef }).strict(),
6119
+ outputs: z28.object({ video: VideoRef }).strict(),
6597
6120
  outputKinds: { video: "video" },
6598
6121
  // $0.012 per 30 frames (edge refinement on) — assume ~30fps; refine via fal dashboard.
6599
6122
  cost: () => ({ credits: 50, seconds_estimate: 60 })
6600
6123
  });
6601
6124
 
6602
6125
  // src/engine/nodes/remote/videoDeconstruct.ts
6603
- import { z as z30 } from "zod";
6126
+ import { z as z29 } from "zod";
6604
6127
  var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
6605
6128
  var videoDeconstructNode = delegated({
6606
6129
  id: "video_deconstruct",
@@ -6608,34 +6131,34 @@ var videoDeconstructNode = delegated({
6608
6131
  category: "video",
6609
6132
  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).',
6610
6133
  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.',
6611
- inputs: z30.object({ video: VideoRef }).loose(),
6612
- params: z30.object({
6613
- model: z30.enum(VIDEO_DECONSTRUCT_MODELS),
6614
- mode: z30.enum(["full", "index"]).optional(),
6615
- language: z30.string().min(2).max(8).optional(),
6616
- max_scenes: z30.number().int().min(1).max(60).optional(),
6617
- focus: z30.string().optional(),
6618
- start_s: z30.number().min(0).optional(),
6619
- end_s: z30.number().positive().optional(),
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(),
6620
6143
  // Real visual shot-cut timestamps (absolute seconds), detected locally with
6621
6144
  // ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
6622
6145
  // onto these and SPLITS any scene that spans one, so a scene's frames never
6623
6146
  // straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
6624
- shot_cuts: z30.array(z30.number().min(0)).max(200).optional(),
6147
+ shot_cuts: z29.array(z29.number().min(0)).max(200).optional(),
6625
6148
  // The video model's per-clip ceiling (seconds). A shot longer than this is
6626
6149
  // split into seamless continuation sub-scenes (shared splice frame), so long
6627
6150
  // shots reproduce in full instead of being truncated. `scaffold-video` sets
6628
6151
  // the Seedance ceiling (15); omit to disable length splitting.
6629
- max_clip_s: z30.number().positive().max(60).optional(),
6152
+ max_clip_s: z29.number().positive().max(60).optional(),
6630
6153
  // Transcript provider for the blueprint's dialogue/transcript. Default
6631
6154
  // Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
6632
- transcriber: z30.enum(["groq", "deepgram"]).optional()
6155
+ transcriber: z29.enum(["groq", "deepgram"]).optional()
6633
6156
  }).strict(),
6634
- outputs: z30.object({
6157
+ outputs: z29.object({
6635
6158
  analysis: JsonRef,
6636
6159
  // Absent in mode:"index" (structure only, no Mux frame extraction).
6637
- start_frames: z30.array(ImageRef).min(1).optional(),
6638
- end_frames: z30.array(ImageRef).min(1).optional(),
6160
+ start_frames: z29.array(ImageRef).min(1).optional(),
6161
+ end_frames: z29.array(ImageRef).min(1).optional(),
6639
6162
  transcript: JsonRef
6640
6163
  }).strict(),
6641
6164
  outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
@@ -6643,22 +6166,22 @@ var videoDeconstructNode = delegated({
6643
6166
  });
6644
6167
 
6645
6168
  // src/engine/nodes/remote/videoLipsync.ts
6646
- import { z as z31 } from "zod";
6647
- var FalLipsyncParams = z31.object({
6648
- model: z31.literal("fal/veed-lipsync")
6169
+ import { z as z30 } from "zod";
6170
+ var FalLipsyncParams = z30.object({
6171
+ model: z30.literal("fal/veed-lipsync")
6649
6172
  }).strict();
6650
- var VideoLipsyncParams = z31.discriminatedUnion("model", [FalLipsyncParams]);
6173
+ var VideoLipsyncParams = z30.discriminatedUnion("model", [FalLipsyncParams]);
6651
6174
  var videoLipsyncNode = delegated({
6652
6175
  id: "video_lipsync",
6653
6176
  version: "1.0.0",
6654
6177
  category: "video",
6655
6178
  summary: "Lip-sync a video to an audio track. Currently backed by VEED via fal.ai (`fal/veed-lipsync`). $0.40/min of output.",
6656
- inputs: z31.object({
6179
+ inputs: z30.object({
6657
6180
  video: VideoRef,
6658
6181
  audio: AudioRef
6659
6182
  }).strict(),
6660
6183
  params: VideoLipsyncParams,
6661
- outputs: z31.object({ video: VideoRef }).strict(),
6184
+ outputs: z30.object({ video: VideoRef }).strict(),
6662
6185
  outputKinds: { video: "video" },
6663
6186
  cost: () => ({ credits: 20, seconds_estimate: 120 })
6664
6187
  });
@@ -6667,7 +6190,7 @@ var videoLipsyncNode = delegated({
6667
6190
  import { mkdtemp as mkdtemp6, readFile as readFile10, rm as rm6 } from "fs/promises";
6668
6191
  import { tmpdir as tmpdir6 } from "os";
6669
6192
  import path13 from "path";
6670
- import { z as z32 } from "zod";
6193
+ import { z as z31 } from "zod";
6671
6194
 
6672
6195
  // src/engine/nodes/local/lib/ffmpeg.ts
6673
6196
  import { execFile as execFile7 } from "child_process";
@@ -6746,21 +6269,21 @@ ${detail.slice(-4e3)}`);
6746
6269
  }
6747
6270
 
6748
6271
  // src/engine/nodes/remote/videoTranscribe.ts
6749
- var VideoTranscribeParams = z32.object({
6750
- language: z32.string().min(2).max(8).optional(),
6272
+ var VideoTranscribeParams = z31.object({
6273
+ language: z31.string().min(2).max(8).optional(),
6751
6274
  // Provider choice is explicit (no env-based silent branching). Default Groq
6752
6275
  // Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
6753
6276
  // `rich` JSON output with punctuated words + paragraph/sentence grouping.
6754
- transcriber: z32.enum(["groq", "deepgram"]).optional()
6277
+ transcriber: z31.enum(["groq", "deepgram"]).optional()
6755
6278
  }).strict();
6756
- var VideoTranscribeInputs = z32.object({
6279
+ var VideoTranscribeInputs = z31.object({
6757
6280
  video: VideoRef
6758
6281
  }).strict();
6759
- var VideoTranscribeOutputs = z32.object({
6760
- transcript: z32.custom(),
6282
+ var VideoTranscribeOutputs = z31.object({
6283
+ transcript: z31.custom(),
6761
6284
  // Only emitted by the Deepgram path: full punctuated words + paragraph /
6762
6285
  // sentence grouping with speaker indices. Absent for the default Groq path.
6763
- rich: z32.custom().optional()
6286
+ rich: z31.custom().optional()
6764
6287
  }).strict();
6765
6288
  var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
6766
6289
  var videoTranscribeNode = defineNode({
@@ -6845,29 +6368,29 @@ async function tryExtractAudio(inputs, ctx) {
6845
6368
  }
6846
6369
 
6847
6370
  // src/engine/nodes/remote/voiceSelect.ts
6848
- import { z as z33 } from "zod";
6371
+ import { z as z32 } from "zod";
6849
6372
  var voiceSelectNode = delegated({
6850
6373
  id: "voice_select",
6851
6374
  version: "1.0.0",
6852
6375
  category: "audio",
6853
6376
  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.',
6854
6377
  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.',
6855
- inputs: z33.object({}).loose(),
6856
- params: z33.object({
6857
- description: z33.string().min(1),
6858
- gender: z33.string().optional(),
6859
- age: z33.string().optional(),
6860
- accent: z33.string().optional(),
6861
- language: z33.string().optional(),
6862
- limit: z33.number().int().min(1).max(20).optional()
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()
6863
6386
  }).strict(),
6864
- outputs: z33.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
6387
+ outputs: z32.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
6865
6388
  outputKinds: { voice_id: "text", candidates: "json" },
6866
6389
  cost: () => ({ credits: 0, seconds_estimate: 5 })
6867
6390
  });
6868
6391
 
6869
6392
  // src/engine/schema/catalog.ts
6870
- import { z as z34 } from "zod";
6393
+ import { z as z33 } from "zod";
6871
6394
  function generateCatalog(registry, opts = {}) {
6872
6395
  const entries = registry.all().map((def) => {
6873
6396
  const cost = def.cost ? safeCost(def) : void 0;
@@ -6878,9 +6401,9 @@ function generateCatalog(registry, opts = {}) {
6878
6401
  summary: def.summary,
6879
6402
  when_to_use: def.when_to_use,
6880
6403
  location: def.location,
6881
- inputs: z34.toJSONSchema(def.inputs, { unrepresentable: "any" }),
6882
- params: z34.toJSONSchema(def.params, { unrepresentable: "any" }),
6883
- outputs: z34.toJSONSchema(def.outputs, { unrepresentable: "any" }),
6404
+ inputs: z33.toJSONSchema(def.inputs, { unrepresentable: "any" }),
6405
+ params: z33.toJSONSchema(def.params, { unrepresentable: "any" }),
6406
+ outputs: z33.toJSONSchema(def.outputs, { unrepresentable: "any" }),
6884
6407
  cost_estimate_credits: cost?.credits,
6885
6408
  runtime_estimate_seconds: cost?.seconds_estimate
6886
6409
  };
@@ -6957,8 +6480,7 @@ var LOCAL_NODES = [
6957
6480
  imagemagickNode,
6958
6481
  videoTranscribeNode,
6959
6482
  fontSpecimenNode,
6960
- audioTimelineNode,
6961
- collectNode
6483
+ audioTimelineNode
6962
6484
  ];
6963
6485
  var REMOTE_NODES = [
6964
6486
  textGenerateNode,
@@ -6991,31 +6513,17 @@ function createEngineFromEnv(opts = {}) {
6991
6513
  const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
6992
6514
  const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
6993
6515
  const creds = requireCredentialsFromEnv();
6994
- const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
6995
- const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
6996
- const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
6997
- const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
6998
- const cache = remoteCacheEnabled ? new LayeredCacheStore({
6999
- local: localCache,
7000
- remote: new RemoteCacheStore(client, opts.log),
7001
- assets,
7002
- log: opts.log
7003
- }) : localCache;
7004
6516
  return new Engine({
7005
6517
  registry: defaultRegistry(),
7006
- client,
7007
- assets,
7008
- cache,
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")),
7009
6521
  outputsDir,
7010
- log: opts.log,
7011
- persistAssets: remoteCacheEnabled
6522
+ log: opts.log
7012
6523
  });
7013
6524
  }
7014
6525
 
7015
6526
  export {
7016
- BackendClient,
7017
- requireCredentialsFromEnv,
7018
- RunAbortedError,
7019
6527
  LayerExecutionError,
7020
6528
  describeFailureReason,
7021
6529
  SEEDANCE_DURATIONS,
@@ -7023,15 +6531,8 @@ export {
7023
6531
  IMAGE_GENERATE_MODELS,
7024
6532
  MODEL_REGISTRY,
7025
6533
  resolveConcurrency,
7026
- ulid,
7027
- isPersistedAssetRef,
7028
- collectAssetRefLikes,
7029
- REF_PREFIX,
7030
- parseRefExpr,
7031
- sha256Hex,
7032
6534
  elementMentionKeywords,
7033
- toModelSafeImage,
7034
- BackendClient2,
6535
+ BackendClient2 as BackendClient,
7035
6536
  Engine2 as Engine,
7036
6537
  LocalAssetStore2 as LocalAssetStore,
7037
6538
  LocalCacheStore2 as LocalCacheStore,
@@ -7041,4 +6542,4 @@ export {
7041
6542
  defaultRegistry,
7042
6543
  createEngineFromEnv
7043
6544
  };
7044
- //# sourceMappingURL=chunk-TKL2CJ6G.js.map
6545
+ //# sourceMappingURL=chunk-4EPKHOTF.js.map