@koda-sl/baker-cli 0.114.0-dev.249eaa8ed → 0.115.0-dev.3bcc79f9c

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.
@@ -159,9 +159,9 @@ var require_safe_stable_stringify = __commonJS({
159
159
  }
160
160
  if (value) {
161
161
  return (value2) => {
162
- let message = `Object can not safely be stringified. Received type ${typeof value2}`;
163
- if (typeof value2 !== "function") message += ` (${value2.toString()})`;
164
- throw new Error(message);
162
+ let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
163
+ if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
164
+ throw new Error(message2);
165
165
  };
166
166
  }
167
167
  }
@@ -624,6 +624,7 @@ ${originalIndentation}`;
624
624
  import path15 from "path";
625
625
 
626
626
  // src/engine/client/http.ts
627
+ var CONTENT_POLICY_CODE = "content_policy_blocked";
627
628
  var BackendHttpError = class extends Error {
628
629
  detail;
629
630
  constructor(detail) {
@@ -640,6 +641,8 @@ function describe(d) {
640
641
  return `invalid request: ${d.message}`;
641
642
  case "provider":
642
643
  return `provider error${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
644
+ case "content_policy":
645
+ return `content policy blocked${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
643
646
  case "timeout":
644
647
  return `provider timeout${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
645
648
  case "server":
@@ -670,6 +673,9 @@ var HttpClient = class {
670
673
  async postJson(path16, body, signal) {
671
674
  return await this.requestJson("POST", path16, body, signal);
672
675
  }
676
+ async putJson(path16, body, signal) {
677
+ return await this.requestJson("PUT", path16, body, signal);
678
+ }
673
679
  async getJson(path16, signal) {
674
680
  return await this.requestJson("GET", path16, void 0, signal);
675
681
  }
@@ -697,8 +703,8 @@ var HttpClient = class {
697
703
  try {
698
704
  const res = await this.fetchFn(url, {
699
705
  method,
700
- headers: method === "POST" ? { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` } : { Authorization: `Bearer ${this.apiKey}` },
701
- body: method === "POST" ? JSON.stringify(body) : void 0,
706
+ headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
707
+ body: method === "GET" ? void 0 : JSON.stringify(body),
702
708
  signal: controller.signal
703
709
  });
704
710
  if (res.ok) return { kind: "value", value: await res.json() };
@@ -735,30 +741,33 @@ async function parseErrorBody(res) {
735
741
  const errObj = body.error ?? {};
736
742
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
737
743
  }
738
- function classifyHttpError(status, errObj, message) {
744
+ function classifyHttpError(status, errObj, message2) {
745
+ if (errObj.code === CONTENT_POLICY_CODE) {
746
+ return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
747
+ }
739
748
  if (status === 401 || status === 403) {
740
- return { kind: "unauthorized", status, message };
749
+ return { kind: "unauthorized", status, message: message2 };
741
750
  }
742
751
  if (status === 400 || status === 422) {
743
- return { kind: "validation", status, message, details: errObj.details };
752
+ return { kind: "validation", status, message: message2, details: errObj.details };
744
753
  }
745
754
  if (status === 502 || status === 504) {
746
755
  if (errObj.code === "provider_timeout" || status === 504) {
747
- return { kind: "timeout", provider: errObj.provider, message };
756
+ return { kind: "timeout", provider: errObj.provider, message: message2 };
748
757
  }
749
758
  return {
750
759
  kind: "provider",
751
760
  status,
752
761
  provider: errObj.provider,
753
762
  code: errObj.code ?? "provider_error",
754
- message,
763
+ message: message2,
755
764
  retryable: errObj.retryable ?? true
756
765
  };
757
766
  }
758
767
  if (status >= 500 || status === 429) {
759
- return { kind: "server", status, message };
768
+ return { kind: "server", status, message: message2 };
760
769
  }
761
- return { kind: "validation", status, message, details: errObj.details };
770
+ return { kind: "validation", status, message: message2, details: errObj.details };
762
771
  }
763
772
  function backoffMs(attempt) {
764
773
  return 1e3 * 2 ** attempt;
@@ -774,6 +783,24 @@ function isAsyncJob(res) {
774
783
  return typeof res.job_id === "string";
775
784
  }
776
785
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
786
+ function failedJobError(error) {
787
+ if (error.code === CONTENT_POLICY_CODE) {
788
+ return new BackendHttpError({
789
+ kind: "content_policy",
790
+ status: error.status ?? 422,
791
+ provider: error.provider,
792
+ message: error.message ?? "content policy blocked"
793
+ });
794
+ }
795
+ return new BackendHttpError({
796
+ kind: "provider",
797
+ status: error.status ?? 502,
798
+ provider: error.provider,
799
+ code: error.code ?? "provider_error",
800
+ message: error.message ?? "deconstruct failed",
801
+ retryable: error.retryable ?? false
802
+ });
803
+ }
777
804
  var JOB_POLL_INTERVAL_MS = 3e3;
778
805
  var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
779
806
  var BackendClient = class {
@@ -797,16 +824,7 @@ var BackendClient = class {
797
824
  }
798
825
  const job = await this.http.getJson(path16, signal);
799
826
  if (job.status === "completed") return job.result;
800
- if (job.status === "failed") {
801
- throw new BackendHttpError({
802
- kind: "provider",
803
- status: job.error.status ?? 502,
804
- provider: job.error.provider,
805
- code: job.error.code ?? "provider_error",
806
- message: job.error.message ?? "deconstruct failed",
807
- retryable: job.error.retryable ?? false
808
- });
809
- }
827
+ if (job.status === "failed") throw failedJobError(job.error);
810
828
  if (Date.now() > deadline) {
811
829
  throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
812
830
  }
@@ -820,6 +838,27 @@ var BackendClient = class {
820
838
  signal
821
839
  );
822
840
  }
841
+ /** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
842
+ async getCacheEntry(cacheKey, signal) {
843
+ try {
844
+ const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
845
+ return res.entry;
846
+ } catch (e) {
847
+ if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
848
+ throw e;
849
+ }
850
+ }
851
+ async putCacheEntry(entry, signal) {
852
+ await this.http.putJson(
853
+ `/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
854
+ entry,
855
+ signal
856
+ );
857
+ }
858
+ /** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
859
+ async recordRun(payload, signal) {
860
+ await this.http.postJson("/api/canvas/runs", payload, signal);
861
+ }
823
862
  getArtifact(kind, name, version, signal) {
824
863
  const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
825
864
  return this.http.getJson(path16, signal);
@@ -843,11 +882,17 @@ function requireCredentialsFromEnv(env = process.env) {
843
882
  }
844
883
  return c;
845
884
  }
885
+ function remoteCacheEnabledFromEnv(env = process.env) {
886
+ return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
887
+ }
846
888
 
847
889
  // src/engine/engine/errors.ts
890
+ function isBlocking(issue) {
891
+ return issue.severity !== "warning";
892
+ }
848
893
  var CanvasError = class extends Error {
849
- constructor(message) {
850
- super(message);
894
+ constructor(message2) {
895
+ super(message2);
851
896
  this.name = "CanvasError";
852
897
  }
853
898
  };
@@ -895,6 +940,8 @@ function describeCause(c) {
895
940
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
896
941
  case "network":
897
942
  return c.cause instanceof Error ? `network: ${c.cause.message}` : "network error";
943
+ case "content_policy":
944
+ return `CONTENT_POLICY_BLOCKED${c.provider ? ` (${c.provider})` : ""}: ${c.message}`;
898
945
  default: {
899
946
  const _exhaustive = c;
900
947
  return String(_exhaustive);
@@ -1563,6 +1610,158 @@ function encodeRandom() {
1563
1610
  return out;
1564
1611
  }
1565
1612
 
1613
+ // src/engine/storage/remote-cache-store.ts
1614
+ var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
1615
+ function isPersistedAssetRef(ref) {
1616
+ return typeof ref.url === "string" && ref.url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${ref.sha256}`);
1617
+ }
1618
+ function isAssetRefLike(value) {
1619
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
1620
+ }
1621
+ function collectAssetRefLikes(value, out = []) {
1622
+ if (Array.isArray(value)) {
1623
+ for (const item of value) collectAssetRefLikes(item, out);
1624
+ return out;
1625
+ }
1626
+ if (typeof value !== "object" || value === null) return out;
1627
+ if (isAssetRefLike(value)) {
1628
+ out.push(value);
1629
+ }
1630
+ for (const item of Object.values(value)) collectAssetRefLikes(item, out);
1631
+ return out;
1632
+ }
1633
+ function entryFullyPersisted(entry) {
1634
+ return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
1635
+ }
1636
+ function stripLocalFields(entry) {
1637
+ const clone = JSON.parse(JSON.stringify(entry));
1638
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1639
+ delete ref.path;
1640
+ delete ref.bytes;
1641
+ }
1642
+ return clone;
1643
+ }
1644
+ var RemoteCacheStore = class {
1645
+ client;
1646
+ log;
1647
+ constructor(client, log) {
1648
+ this.client = client;
1649
+ this.log = log ?? (() => void 0);
1650
+ }
1651
+ async get(cacheKey) {
1652
+ return await this.client.getCacheEntry(cacheKey);
1653
+ }
1654
+ async put(entry) {
1655
+ if (!entryFullyPersisted(entry)) {
1656
+ this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
1657
+ return;
1658
+ }
1659
+ const stripped = stripLocalFields(entry);
1660
+ if (stripped.refs.length > MAX_REMOTE_REFS) {
1661
+ stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
1662
+ }
1663
+ await this.client.putCacheEntry(stripped);
1664
+ }
1665
+ };
1666
+ var MAX_REMOTE_REFS = 512;
1667
+ var LayeredCacheStore = class {
1668
+ rootDir;
1669
+ local;
1670
+ remote;
1671
+ assets;
1672
+ log;
1673
+ constructor(opts) {
1674
+ this.local = opts.local;
1675
+ this.remote = opts.remote;
1676
+ this.assets = opts.assets;
1677
+ this.rootDir = opts.local.rootDir;
1678
+ this.log = opts.log ?? (() => void 0);
1679
+ }
1680
+ async get(cacheKey) {
1681
+ const localHit = await this.local.get(cacheKey);
1682
+ if (localHit) return localHit;
1683
+ let remoteEntry;
1684
+ try {
1685
+ remoteEntry = await this.remote.get(cacheKey);
1686
+ } catch (e) {
1687
+ this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
1688
+ return null;
1689
+ }
1690
+ if (!remoteEntry) return null;
1691
+ let rehydrated;
1692
+ try {
1693
+ rehydrated = await this.rehydrate(remoteEntry);
1694
+ } catch (e) {
1695
+ this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
1696
+ return null;
1697
+ }
1698
+ await this.local.put(rehydrated);
1699
+ return rehydrated;
1700
+ }
1701
+ async put(entry) {
1702
+ await this.local.put(entry);
1703
+ try {
1704
+ await this.remote.put(entry);
1705
+ } catch (e) {
1706
+ this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Download every referenced asset into the local content-addressed store
1711
+ * (sha-verified) and stamp fresh local paths. Any ref that cannot be
1712
+ * rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
1713
+ * crash materialization later with a far less actionable error.
1714
+ */
1715
+ async rehydrate(entry) {
1716
+ const clone = JSON.parse(JSON.stringify(entry));
1717
+ for (const ref of collectAssetRefLikes(clone.outputs)) {
1718
+ if (!isPersistedAssetRef(ref)) {
1719
+ throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
1720
+ }
1721
+ const ingested = await this.assets.ingestRemote({
1722
+ kind: typeof ref.kind === "string" ? ref.kind : "json",
1723
+ url: ref.url,
1724
+ sha256: ref.sha256,
1725
+ mime: ref.mime,
1726
+ metadata: ref.metadata ?? void 0
1727
+ });
1728
+ ref.path = ingested.path;
1729
+ }
1730
+ return clone;
1731
+ }
1732
+ };
1733
+ function message(e) {
1734
+ return e instanceof Error ? e.message : String(e);
1735
+ }
1736
+
1737
+ // src/engine/nodes/remote/upload.ts
1738
+ async function presignAndPut(args) {
1739
+ const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
1740
+ const putRes = await fetch(putUrl, {
1741
+ method: "PUT",
1742
+ body: new Uint8Array(args.bytes),
1743
+ headers: { "Content-Type": args.mime },
1744
+ signal: args.ctx.signal
1745
+ });
1746
+ if (!putRes.ok) {
1747
+ throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
1748
+ }
1749
+ return publicUrl;
1750
+ }
1751
+ async function ensureUploaded(ref, ctx) {
1752
+ if (ref.url) return ref;
1753
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1754
+ const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1755
+ return { ...ref, url };
1756
+ }
1757
+ async function persistOutputAssetUrls(outputs, ctx) {
1758
+ for (const ref of collectAssetRefLikes(outputs)) {
1759
+ if (isPersistedAssetRef(ref)) continue;
1760
+ const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
1761
+ ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
1762
+ }
1763
+ }
1764
+
1566
1765
  // src/engine/schema/canvas.ts
1567
1766
  import { z } from "zod";
1568
1767
  var REF_PREFIX = "$ref:";
@@ -1638,7 +1837,23 @@ var VideoMeta = z.object({
1638
1837
  // on which spoken beat" map emitted by scaffold-video (per-scene window,
1639
1838
  // spoken line, storyboard frames, scheduled graphics). Free-form rows so the
1640
1839
  // schema stays decoupled from the scaffold's exact shape.
1641
- motion_board: z.array(z.unknown()).optional()
1840
+ motion_board: z.array(z.unknown()).optional(),
1841
+ // The recurring identity elements the scaffold wired (one shared reference slot
1842
+ // per cast member / pet / product / logo). The reference-completeness check reads
1843
+ // this to warn when a frame's description mentions an element whose ref isn't
1844
+ // wired on that frame node (the wrong-identity-animal failure).
1845
+ elements: z.array(
1846
+ z.object({
1847
+ ref: z.string(),
1848
+ label: z.string(),
1849
+ type: z.string(),
1850
+ description: z.string().optional()
1851
+ })
1852
+ ).optional(),
1853
+ // Per video_generate node: the scene's natural visual span. The validator warns
1854
+ // when a clip's span exceeds the assigned model's max clip duration (e.g. a 9.2s
1855
+ // scene on Veo, which caps at 8s) — the clip would truncate.
1856
+ clip_spans: z.array(z.object({ node: z.string(), span_s: z.number() })).optional()
1642
1857
  }).strict().optional();
1643
1858
  var CanvasMetadata = z.object({
1644
1859
  name: z.string().optional(),
@@ -2136,8 +2351,11 @@ var STAGE_CODES = {
2136
2351
  AUDIO_DURATION: "VIDEO_AUDIO_DURATION",
2137
2352
  LIPSYNC_MISSING: "VIDEO_LIPSYNC_MISSING",
2138
2353
  SPEECH_OVERRUN: "VIDEO_SPEECH_OVERRUN",
2139
- ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH"
2354
+ ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH",
2355
+ REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
2356
+ SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL"
2140
2357
  };
2358
+ var SPAN_MODEL_SLACK_S = 0.25;
2141
2359
  var VIDEO_TIME_SLACK_S = 0.75;
2142
2360
  var SPEECH_WORDS_PER_SECOND = 2.5;
2143
2361
  var SPEECH_OVERRUN_RATIO = 1.6;
@@ -2168,8 +2386,10 @@ function validateCanvas(input, registry) {
2168
2386
  const estimatedCredits = estimateCredits(ctx);
2169
2387
  checkOutputRef(ctx);
2170
2388
  checkVideoInvariants(ctx);
2171
- if (issues.length > 0) return { ok: false, issues };
2172
- return { ok: true, canvas, estimatedCredits };
2389
+ const hasBlocking = issues.some(isBlocking);
2390
+ if (hasBlocking) return { ok: false, issues };
2391
+ const warnings = issues.filter((i) => !isBlocking(i));
2392
+ return { ok: true, canvas, estimatedCredits, warnings: warnings.length > 0 ? warnings : void 0 };
2173
2393
  }
2174
2394
  async function validateCanvasDeep(input, registry) {
2175
2395
  const shallow = validateCanvas(input, registry);
@@ -2204,7 +2424,9 @@ async function validateCanvasDeep(input, registry) {
2204
2424
  });
2205
2425
  }
2206
2426
  }
2207
- if (issues.length > 0) return { ok: false, issues };
2427
+ const hasBlocking = issues.some(isBlocking);
2428
+ if (hasBlocking) return { ok: false, issues };
2429
+ const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
2208
2430
  const perNodeCredits = canvas.nodes.map((n) => {
2209
2431
  const def = registry.get(n.type);
2210
2432
  let credits = 0;
@@ -2217,7 +2439,13 @@ async function validateCanvasDeep(input, registry) {
2217
2439
  }
2218
2440
  return { node_id: n.id, node_type: n.type, credits };
2219
2441
  });
2220
- return { ok: true, canvas, estimatedCredits: shallow.estimatedCredits, perNodeCredits };
2442
+ return {
2443
+ ok: true,
2444
+ canvas,
2445
+ estimatedCredits: shallow.estimatedCredits,
2446
+ perNodeCredits,
2447
+ warnings: warnings.length > 0 ? warnings : void 0
2448
+ };
2221
2449
  }
2222
2450
  function buildIdToIndex(canvas) {
2223
2451
  const m = /* @__PURE__ */ new Map();
@@ -2504,6 +2732,95 @@ function checkVideoInvariants(ctx) {
2504
2732
  }
2505
2733
  checkSpeechOverrun(ctx, meta.talking_scenes);
2506
2734
  checkAspectConsistency(ctx);
2735
+ checkReferenceCompleteness(ctx, meta);
2736
+ checkClipSpanFitsModel(ctx, meta);
2737
+ }
2738
+ var ELEMENT_TYPE_KEYWORDS = {
2739
+ animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
2740
+ logo: ["logo", "wordmark"],
2741
+ badge: ["badge", "seal"],
2742
+ product: []
2743
+ };
2744
+ var CHECKED_ELEMENT_TYPES = new Set(Object.keys(ELEMENT_TYPE_KEYWORDS));
2745
+ var KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
2746
+ "the",
2747
+ "and",
2748
+ "with",
2749
+ "for",
2750
+ "brand",
2751
+ "logo",
2752
+ "image",
2753
+ "shot",
2754
+ "main",
2755
+ "hero",
2756
+ "element",
2757
+ "product",
2758
+ "reference"
2759
+ ]);
2760
+ function keywordTokens(text) {
2761
+ if (!text) return [];
2762
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !KEYWORD_STOPWORDS.has(t));
2763
+ }
2764
+ function keywordsForElement(el) {
2765
+ const type = el.type.toLowerCase();
2766
+ const typeWords = ELEMENT_TYPE_KEYWORDS[type] ?? [];
2767
+ return [.../* @__PURE__ */ new Set([...typeWords, ...keywordTokens(el.label), ...keywordTokens(el.description)])];
2768
+ }
2769
+ function containsWord(text, word) {
2770
+ const esc = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2771
+ return new RegExp(`\\b${esc}\\b`, "i").test(text);
2772
+ }
2773
+ function checkFrameReferences(ctx, node, index, keyworded) {
2774
+ const prompt = node.params?.prompt;
2775
+ if (typeof prompt !== "string" || prompt.length === 0) return;
2776
+ const inputsBlob = JSON.stringify(node.inputs ?? {});
2777
+ for (const { el, keywords } of keyworded) {
2778
+ if (inputsBlob.includes(el.ref)) continue;
2779
+ const hit = keywords.find((kw) => containsWord(prompt, kw));
2780
+ if (!hit) continue;
2781
+ ctx.issues.push({
2782
+ path: `nodes[${index}].inputs.reference`,
2783
+ code: STAGE_CODES.REFERENCE_MISSING,
2784
+ severity: "warning",
2785
+ message: `frame "${node.id}" describes a "${hit}" but the "${el.label}" reference (${el.ref}) isn't wired on this node \u2014 the generator will invent a wrong-identity ${el.type} from prose. Add ${el.ref} to inputs.reference`,
2786
+ node_id: node.id,
2787
+ node_type: node.type
2788
+ });
2789
+ }
2790
+ }
2791
+ function checkReferenceCompleteness(ctx, meta) {
2792
+ const elements = (meta.elements ?? []).filter((el) => CHECKED_ELEMENT_TYPES.has(el.type.toLowerCase()));
2793
+ const keyworded = elements.map((el) => ({ el, keywords: keywordsForElement(el) })).filter((e) => e.keywords.length > 0);
2794
+ if (keyworded.length === 0) return;
2795
+ for (let i = 0; i < ctx.canvas.nodes.length; i++) {
2796
+ const n = ctx.canvas.nodes[i];
2797
+ if (n?.type === "image_generate") checkFrameReferences(ctx, n, i, keyworded);
2798
+ }
2799
+ }
2800
+ function videoModelMaxDuration(model) {
2801
+ const spec = MODEL_REGISTRY.video_generate[model];
2802
+ const durations = spec?.params?.duration?.enum;
2803
+ if (!durations) return void 0;
2804
+ const nums = durations.filter((d) => typeof d === "number");
2805
+ return nums.length > 0 ? Math.max(...nums) : void 0;
2806
+ }
2807
+ function checkClipSpanFitsModel(ctx, meta) {
2808
+ for (const { node, span_s } of meta.clip_spans ?? []) {
2809
+ const target = ctx.canvas.nodes.find((n) => n.id === node && n.type === "video_generate");
2810
+ if (!target) continue;
2811
+ const model = target.params?.model;
2812
+ if (typeof model !== "string") continue;
2813
+ const max = videoModelMaxDuration(model);
2814
+ if (max === void 0 || span_s <= max + SPAN_MODEL_SLACK_S) continue;
2815
+ ctx.issues.push({
2816
+ path: `nodes[${ctx.idToIndex.get(node) ?? -1}].params.duration`,
2817
+ code: STAGE_CODES.SPAN_EXCEEDS_MODEL,
2818
+ severity: "warning",
2819
+ message: `scene for clip "${node}" needs ~${span_s}s but ${model} caps clips at ${max}s \u2014 the clip will truncate. Split the scene, or keep it on a model whose max duration covers the span`,
2820
+ node_id: node,
2821
+ node_type: "video_generate"
2822
+ });
2823
+ }
2507
2824
  }
2508
2825
  function speechOverrunOf(node) {
2509
2826
  const params = node.params;
@@ -2661,6 +2978,7 @@ var Engine = class {
2661
2978
  cache;
2662
2979
  outputsDir;
2663
2980
  log;
2981
+ persistAssets;
2664
2982
  constructor(opts) {
2665
2983
  this.registry = opts.registry;
2666
2984
  this.client = opts.client;
@@ -2668,6 +2986,7 @@ var Engine = class {
2668
2986
  this.cache = opts.cache;
2669
2987
  this.outputsDir = opts.outputsDir;
2670
2988
  this.log = opts.log ?? (() => void 0);
2989
+ this.persistAssets = opts.persistAssets ?? false;
2671
2990
  }
2672
2991
  validate(canvas) {
2673
2992
  return validateCanvas(canvas, this.registry);
@@ -2684,6 +3003,9 @@ var Engine = class {
2684
3003
  const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
2685
3004
  await writer.ensure();
2686
3005
  this.log(`[validate] ok (${canvas.nodes.length} nodes, est. ${validation.estimatedCredits} credits)`);
3006
+ for (const w of validation.warnings ?? []) {
3007
+ this.log(`[warn ] ${w.code}${w.node_id ? ` (${w.node_id})` : ""}: ${w.message}`);
3008
+ }
2687
3009
  const t0 = Date.now();
2688
3010
  const outputs = {};
2689
3011
  const counters = { cachedNodes: 0, totalCredits: 0 };
@@ -2711,7 +3033,7 @@ var Engine = class {
2711
3033
  `[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
2712
3034
  );
2713
3035
  this.log(`outputs in: ${writer.runDir}`);
2714
- return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
3036
+ return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
2715
3037
  }
2716
3038
  async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
2717
3039
  const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
@@ -2808,6 +3130,14 @@ var Engine = class {
2808
3130
  const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
2809
3131
  const outputsObj = result;
2810
3132
  outputs[node.id] = outputsObj;
3133
+ if (this.persistAssets) {
3134
+ try {
3135
+ await persistOutputAssetUrls(outputsObj, ctx);
3136
+ } catch (e) {
3137
+ const msg = e instanceof Error ? e.message : String(e);
3138
+ this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
3139
+ }
3140
+ }
2811
3141
  if (policy === "read_write") {
2812
3142
  await this.cache.put({
2813
3143
  cacheKey: prepared.cacheKey,
@@ -3126,27 +3456,6 @@ var FontRef = BaseAssetRef.extend({
3126
3456
  });
3127
3457
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
3128
3458
 
3129
- // src/engine/nodes/remote/upload.ts
3130
- async function presignAndPut(args) {
3131
- const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
3132
- const putRes = await fetch(putUrl, {
3133
- method: "PUT",
3134
- body: new Uint8Array(args.bytes),
3135
- headers: { "Content-Type": args.mime },
3136
- signal: args.ctx.signal
3137
- });
3138
- if (!putRes.ok) {
3139
- throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
3140
- }
3141
- return publicUrl;
3142
- }
3143
- async function ensureUploaded(ref, ctx) {
3144
- if (ref.url) return ref;
3145
- const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
3146
- const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
3147
- return { ...ref, url };
3148
- }
3149
-
3150
3459
  // src/engine/nodes/remote/delegate.ts
3151
3460
  function delegated(spec) {
3152
3461
  return {
@@ -3253,6 +3562,13 @@ function mapClientError(ctx, e) {
3253
3562
  provider: d.provider
3254
3563
  });
3255
3564
  }
3565
+ if (d.kind === "content_policy") {
3566
+ return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3567
+ kind: "content_policy",
3568
+ provider: d.provider,
3569
+ message: d.message
3570
+ });
3571
+ }
3256
3572
  if (d.kind === "timeout") {
3257
3573
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, { kind: "timeout", provider: d.provider });
3258
3574
  }
@@ -3534,10 +3850,10 @@ function inferKindFromMime(mime) {
3534
3850
  if (mime.startsWith("font/")) return "font";
3535
3851
  return null;
3536
3852
  }
3537
- function localExecError(ctx, message) {
3853
+ function localExecError(ctx, message2) {
3538
3854
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3539
3855
  kind: "local",
3540
- cause: new Error(`ingest: ${message}`)
3856
+ cause: new Error(`ingest: ${message2}`)
3541
3857
  });
3542
3858
  }
3543
3859
  async function execLocalFile(params, ctx) {
@@ -4204,7 +4520,7 @@ async function refToUrl(ref) {
4204
4520
  return `data:${ref.mime};base64,${bytes.toString("base64")}`;
4205
4521
  }
4206
4522
  var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
4207
- function isAssetRefLike(value) {
4523
+ function isAssetRefLike2(value) {
4208
4524
  if (!value || typeof value !== "object") return false;
4209
4525
  const v = value;
4210
4526
  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");
@@ -4576,8 +4892,8 @@ var NEVER_BLOCK = [
4576
4892
  /text[_-]?occluded/i
4577
4893
  ];
4578
4894
  var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
4579
- function isAdvisory(code, message) {
4580
- const hay = `${code} ${message}`;
4895
+ function isAdvisory(code, message2) {
4896
+ const hay = `${code} ${message2}`;
4581
4897
  return NEVER_BLOCK.some((re) => re.test(hay));
4582
4898
  }
4583
4899
  function parseCheckJson(raw) {
@@ -4605,10 +4921,10 @@ function classifyLint(json) {
4605
4921
  for (const f of findings) {
4606
4922
  const rec = f;
4607
4923
  const code = String(rec?.code ?? "");
4608
- const message = String(rec?.message ?? "");
4924
+ const message2 = String(rec?.message ?? "");
4609
4925
  const severity = String(rec?.severity ?? "info");
4610
- const blocking = severity === "error" && !isAdvisory(code, message);
4611
- out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
4926
+ const blocking = severity === "error" && !isAdvisory(code, message2);
4927
+ out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
4612
4928
  }
4613
4929
  return out;
4614
4930
  }
@@ -4620,9 +4936,9 @@ function classifyInspect(json) {
4620
4936
  for (const iss of issues) {
4621
4937
  const rec = iss;
4622
4938
  const code = String(rec?.code ?? rec?.type ?? "overflow");
4623
- const message = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4939
+ const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
4624
4940
  const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
4625
- out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
4941
+ out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
4626
4942
  }
4627
4943
  return out;
4628
4944
  }
@@ -5067,7 +5383,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
5067
5383
  }
5068
5384
  function coerceImageParam(value) {
5069
5385
  if (typeof value === "string") return Promise.resolve(value);
5070
- if (isAssetRefLike(value)) return refToUrl(value);
5386
+ if (isAssetRefLike2(value)) return refToUrl(value);
5071
5387
  throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
5072
5388
  }
5073
5389
  async function substituteCompositionFiles(tmp, values) {
@@ -5297,7 +5613,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
5297
5613
  }
5298
5614
  function coerceImageParam2(value) {
5299
5615
  if (typeof value === "string") return Promise.resolve(value);
5300
- if (isAssetRefLike(value)) return refToUrl(value);
5616
+ if (isAssetRefLike2(value)) return refToUrl(value);
5301
5617
  throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
5302
5618
  }
5303
5619
  async function substituteCompositionFiles2(tmp, values) {
@@ -6323,17 +6639,29 @@ function createEngineFromEnv(opts = {}) {
6323
6639
  const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
6324
6640
  const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
6325
6641
  const creds = requireCredentialsFromEnv();
6642
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
6643
+ const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
6644
+ const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
6645
+ const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
6646
+ const cache = remoteCacheEnabled ? new LayeredCacheStore({
6647
+ local: localCache,
6648
+ remote: new RemoteCacheStore(client, opts.log),
6649
+ assets,
6650
+ log: opts.log
6651
+ }) : localCache;
6326
6652
  return new Engine({
6327
6653
  registry: defaultRegistry(),
6328
- client: new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey }),
6329
- assets: new LocalAssetStore(path15.join(cacheDir, "assets")),
6330
- cache: new LocalCacheStore(path15.join(cacheDir, "index")),
6654
+ client,
6655
+ assets,
6656
+ cache,
6331
6657
  outputsDir,
6332
- log: opts.log
6658
+ log: opts.log,
6659
+ persistAssets: remoteCacheEnabled
6333
6660
  });
6334
6661
  }
6335
6662
 
6336
6663
  export {
6664
+ requireCredentialsFromEnv,
6337
6665
  LayerExecutionError,
6338
6666
  describeFailureReason,
6339
6667
  SEEDANCE_DURATIONS,
@@ -6341,6 +6669,10 @@ export {
6341
6669
  IMAGE_GENERATE_MODELS,
6342
6670
  MODEL_REGISTRY,
6343
6671
  resolveConcurrency,
6672
+ ulid,
6673
+ isPersistedAssetRef,
6674
+ collectAssetRefLikes,
6675
+ sha256Hex,
6344
6676
  BackendClient2 as BackendClient,
6345
6677
  Engine2 as Engine,
6346
6678
  LocalAssetStore2 as LocalAssetStore,
@@ -6351,4 +6683,4 @@ export {
6351
6683
  defaultRegistry,
6352
6684
  createEngineFromEnv
6353
6685
  };
6354
- //# sourceMappingURL=chunk-5AWO4BHJ.js.map
6686
+ //# sourceMappingURL=chunk-OCMOQOIJ.js.map