@koda-sl/baker-cli 0.290.3 → 0.293.0-dev.cf9d5462d

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.
Files changed (28) hide show
  1. package/README.md +74 -5
  2. package/canvas/tiktok-captions-composition/index.html +23 -2
  3. package/dist/{chunk-2IB3IL4K.js → chunk-2IUN3S3E.js} +2 -2
  4. package/dist/{chunk-D3HHUBIL.js → chunk-4YF56EGK.js} +4 -4
  5. package/dist/{chunk-EX2OIOVL.js → chunk-A2VZOTCV.js} +3 -3
  6. package/dist/{chunk-GU7IWEBC.js → chunk-KDTHRRAC.js} +3 -3
  7. package/dist/{chunk-NNQDWCFG.js → chunk-LTXNRRXW.js} +282 -102
  8. package/dist/chunk-LTXNRRXW.js.map +1 -0
  9. package/dist/{chunk-DZUVUGEP.js → chunk-WFWU3CHS.js} +9 -1
  10. package/dist/{chunk-DZUVUGEP.js.map → chunk-WFWU3CHS.js.map} +1 -1
  11. package/dist/cli.js +2876 -1048
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-VDCCDEHE.js → client-6KQHCXS2.js} +4 -4
  14. package/dist/engine/index.d.ts +13 -0
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-FWMZXMQK.js → env-C7YXL6C6.js} +6 -2
  17. package/dist/{output-4H2RNBXL.js → output-RBM32FKJ.js} +5 -5
  18. package/dist/{shared-UKL36AXT.js → shared-WJIJTWST.js} +6 -6
  19. package/package.json +1 -1
  20. package/dist/chunk-NNQDWCFG.js.map +0 -1
  21. /package/dist/{chunk-2IB3IL4K.js.map → chunk-2IUN3S3E.js.map} +0 -0
  22. /package/dist/{chunk-D3HHUBIL.js.map → chunk-4YF56EGK.js.map} +0 -0
  23. /package/dist/{chunk-EX2OIOVL.js.map → chunk-A2VZOTCV.js.map} +0 -0
  24. /package/dist/{chunk-GU7IWEBC.js.map → chunk-KDTHRRAC.js.map} +0 -0
  25. /package/dist/{client-VDCCDEHE.js.map → client-6KQHCXS2.js.map} +0 -0
  26. /package/dist/{env-FWMZXMQK.js.map → env-C7YXL6C6.js.map} +0 -0
  27. /package/dist/{output-4H2RNBXL.js.map → output-RBM32FKJ.js.map} +0 -0
  28. /package/dist/{shared-UKL36AXT.js.map → shared-WJIJTWST.js.map} +0 -0
@@ -1,24 +1,25 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-2IB3IL4K.js";
4
+ } from "./chunk-2IUN3S3E.js";
5
5
  import {
6
6
  DIRECT_ROUTE,
7
7
  __commonJS,
8
8
  __toESM,
9
9
  captureProxyCredentials,
10
10
  childEnvWith,
11
+ hasFrameVisionKey,
11
12
  isChallengeBody,
12
13
  isPrivateAddress,
13
14
  isProxyFailure,
14
15
  plannedRoutes,
15
16
  refuseNonPublicUrl,
16
17
  shouldEscalate
17
- } from "./chunk-DZUVUGEP.js";
18
+ } from "./chunk-WFWU3CHS.js";
18
19
 
19
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
20
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js
20
21
  var require_safe_stable_stringify = __commonJS({
21
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
23
  "use strict";
23
24
  var { hasOwnProperty } = Object.prototype;
24
25
  var stringify = configure2();
@@ -1033,12 +1034,17 @@ function failureDetail(reason) {
1033
1034
  }
1034
1035
  return { message: describeFailureReason(reason) };
1035
1036
  }
1037
+ function retryVerdict(retryable) {
1038
+ if (retryable === true) return "transient \u2014 retry the run";
1039
+ if (retryable === false) return "not retryable \u2014 retrying will not help, fix the input";
1040
+ return "retryability unknown";
1041
+ }
1036
1042
  function describeCause(c) {
1037
1043
  switch (c.kind) {
1038
1044
  case "local":
1039
1045
  return c.cause instanceof Error ? c.cause.message : String(c.cause);
1040
1046
  case "remote":
1041
- return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""}`;
1047
+ return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""} \u2014 ${retryVerdict(c.retryable)}`;
1042
1048
  case "timeout":
1043
1049
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
1044
1050
  case "network":
@@ -1092,7 +1098,7 @@ function resolveAdaptFormats(params) {
1092
1098
  return params.formats ?? [];
1093
1099
  }
1094
1100
 
1095
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1101
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/esm/wrapper.js
1096
1102
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1097
1103
  var configure = import__.default.configure;
1098
1104
  var wrapper_default = import__.default;
@@ -1133,6 +1139,14 @@ function normalizeForCanonical(value) {
1133
1139
  return void 0;
1134
1140
  }
1135
1141
 
1142
+ // ../canvas-contract/src/frameRealism.ts
1143
+ function frameRealismDirection(opts = {}) {
1144
+ return " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script. Hands are kept simple: no close-up of fingers manipulating small parts, no hand gripping the edge of an object, and each person has exactly TWO arms and TWO legs, all attached and all visible or all out of frame. Anyone working does so the way the trade actually does it: nobody stands or kneels on the equipment being installed, nothing is fitted overhanging an edge or floating unsupported, and every part rests on the structure that would really carry it." + // The newest clause, and the one no route had. A testimonial came back with the customer
1145
+ // repairing the panel herself: nothing said she was not the installer, so the model cast
1146
+ // her as one. Who someone IS in the picture has to be stated, or it is guessed.
1147
+ (opts.role ? ` The person on camera is ${opts.role} \u2014 they are shown as that and never doing somebody else's job.` : "") + (opts.currency ? ` If a currency is unavoidably visible it is ${opts.currency}.` : "");
1148
+ }
1149
+
1136
1150
  // ../canvas-contract/src/registry.ts
1137
1151
  var OPENROUTER_IMAGE_AR = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
1138
1152
  var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8", "8:1"];
@@ -1152,6 +1166,7 @@ var OPENROUTER_IMAGE_SIZES = ["0.5K", ...OPENROUTER_IMAGE_SIZE];
1152
1166
  var OPENROUTER_IMAGE_SIZE_EXTENDED = OPENROUTER_IMAGE_SIZES;
1153
1167
  var GEMINI_LITE_IMAGE_SIZE = ["1K"];
1154
1168
  var OPENROUTER_IMAGE_QUALITY = ["auto", "low", "medium", "high"];
1169
+ var OPENROUTER_IMAGE_QUALITY_25 = ["auto", "low", "medium", "high", "xhigh", "max"];
1155
1170
  var RECRAFT_IMAGE_AR = ["1:1", "4:3", "3:4", "16:9", "9:16"];
1156
1171
  var SEEDANCE_DURATIONS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
1157
1172
  var SEEDANCE_25_DURATIONS = [
@@ -1214,6 +1229,7 @@ var REPLICATE_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
1214
1229
  var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
1215
1230
  var REPLICATE_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
1216
1231
  var IMAGE_GENERATE_MODELS = [
1232
+ "openai/gpt-image-2.5-sunburst",
1217
1233
  "openai/gpt-image-2",
1218
1234
  "openai/gpt-5.4-image-2",
1219
1235
  "google/gemini-3.1-flash-image-preview",
@@ -1359,6 +1375,20 @@ var MODEL_REGISTRY = {
1359
1375
  // images as a side effect. Notable capability gaps vs the Gemini entries:
1360
1376
  // no `image_size` (OpenAI picks the pixel dimensions from the ratio, and a
1361
1377
  // `resolution` is silently ignored) and the narrower `..._AR_GPT` ratio set.
1378
+ "openai/gpt-image-2.5-sunburst": {
1379
+ // Identical to the gpt-image-2 entry below except for the quality ladder — the
1380
+ // ratio set, the 16-reference ceiling, the missing `image_size` and the reasons
1381
+ // `background` / `output_compression` stay undeclared all carry over unchanged.
1382
+ label: "OpenAI GPT Image 2.5",
1383
+ inputs: [],
1384
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GPT_IMAGE_MAX_REFERENCES }],
1385
+ required: ["prompt"],
1386
+ params: {
1387
+ prompt: { kind: "string", maxLength: GPT_IMAGE_PROMPT_MAX },
1388
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_GPT },
1389
+ quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY_25 }
1390
+ }
1391
+ },
1362
1392
  "openai/gpt-image-2": {
1363
1393
  // Its live endpoint also advertises `background` (auto|opaque) and
1364
1394
  // `output_compression` (0-100), which we deliberately do not declare. Not an
@@ -2033,6 +2063,19 @@ function maxInputSlot(kind, model, inputKind) {
2033
2063
  function maxInputReferences(kind, model) {
2034
2064
  return maxInputSlot(kind, model, "image");
2035
2065
  }
2066
+ function supportedQualities(kind, model) {
2067
+ const schema = MODEL_REGISTRY[kind]?.[model]?.params.quality;
2068
+ return schema?.kind === "string" ? schema.enum : void 0;
2069
+ }
2070
+ function bestSupportedQuality(kind, model, want) {
2071
+ const supported = supportedQualities(kind, model);
2072
+ if (!supported) return void 0;
2073
+ if (supported.includes(want)) return want;
2074
+ const ladder = supported.filter((step) => step !== "auto");
2075
+ const ceiling = ladder.indexOf(want);
2076
+ const reachable = ceiling === -1 ? ladder : ladder.slice(0, ceiling);
2077
+ return reachable[reachable.length - 1];
2078
+ }
2036
2079
  function supportedAspectRatios(kind, model) {
2037
2080
  const schema = MODEL_REGISTRY[kind]?.[model]?.params.aspect_ratio;
2038
2081
  return schema?.kind === "string" ? schema.enum : void 0;
@@ -3117,6 +3160,16 @@ function dfsCycle(u, color, stack, reverseAdj) {
3117
3160
  stack.pop();
3118
3161
  return null;
3119
3162
  }
3163
+ function nodesBeforeCheckpoint(graph, types, stopKinds) {
3164
+ const all = new Set(graph.keys());
3165
+ if (!stopKinds?.size) return all;
3166
+ const running = /* @__PURE__ */ new Set();
3167
+ for (const layer of topologicalLayers(graph)) {
3168
+ if (layer.some((id) => stopKinds.has(types.get(id) ?? ""))) return running;
3169
+ for (const id of layer) running.add(id);
3170
+ }
3171
+ return running;
3172
+ }
3120
3173
 
3121
3174
  // src/engine/engine/validator.ts
3122
3175
  import { readFile as readFile2 } from "fs/promises";
@@ -3135,7 +3188,7 @@ var VEO_DURATIONS = [4, 6, 8];
3135
3188
  var KLING_DURATIONS2 = [5, 10];
3136
3189
  var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
3137
3190
  var KLING_CFG_SCALE = 0.7;
3138
- var SPEAKS_PROSE = (line) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues). Dialogue: "${line}"`;
3191
+ var SPEAKS_PROSE = (line, accent) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues).${accent ? ` The speech is in ${accent}.` : ""} Dialogue: "${line}"`;
3139
3192
  var SEEDANCE_PROFILE = {
3140
3193
  id: "seedance",
3141
3194
  dialogueDirective: SPEAKS_PROSE,
@@ -3236,9 +3289,14 @@ var GPT_IMAGE_PROFILE = {
3236
3289
  id: "gpt-image",
3237
3290
  constraintPlacement: "last",
3238
3291
  photorealCue: true,
3239
- // OpenRouter forwards `quality`; gpt-image-2 already processes inputs at high
3240
- // fidelity automatically, so we deliberately do NOT send `input_fidelity`.
3241
- paramDefaults: { quality: "high" }
3292
+ // OpenRouter forwards `quality`; gpt-image already processes inputs at high fidelity
3293
+ // automatically, so we deliberately do NOT send `input_fidelity`.
3294
+ //
3295
+ // No `quality` here any more: it is the one default that depends on WHICH gpt-image is
3296
+ // pinned. 2.5 added `xhigh` and `max` above `high`, and the older entries would refuse
3297
+ // them at validate — so callers ask for the step they want through
3298
+ // `bestSupportedQuality` and get the best the chosen model actually has.
3299
+ paramDefaults: {}
3242
3300
  };
3243
3301
  var GEMINI_IMAGE_PROFILE = {
3244
3302
  id: "gemini",
@@ -3749,6 +3807,19 @@ function checkSlotsForNode(ctx, n, _i) {
3749
3807
  });
3750
3808
  }
3751
3809
  }
3810
+ function estimateCreditsFor(canvas, registry, ids) {
3811
+ let total = 0;
3812
+ for (const n of canvas.nodes) {
3813
+ if (!ids.has(n.id)) continue;
3814
+ const def = registry.get(n.type);
3815
+ if (!def?.cost) continue;
3816
+ try {
3817
+ total += def.cost({ params: def.params.parse(n.params ?? {}) }).credits;
3818
+ } catch {
3819
+ }
3820
+ }
3821
+ return total;
3822
+ }
3752
3823
  function estimateCredits(ctx) {
3753
3824
  let total = 0;
3754
3825
  for (const n of ctx.canvas.nodes) {
@@ -4055,7 +4126,7 @@ function checkAudioTimelineTotals(ctx, audioMixNode, expected) {
4055
4126
  if (audioMixNode) mustBeFullLength.add(audioMixNode);
4056
4127
  ctx.canvas.nodes.forEach((n) => {
4057
4128
  if (n.type !== "audio_voice_convert") return;
4058
- const track = refNodeOf(ctx, (n.inputs ?? {}).audio);
4129
+ const track = refNodeOf(ctx, n.inputs?.audio);
4059
4130
  if (track?.type === "audio_timeline") mustBeFullLength.add(track.id);
4060
4131
  });
4061
4132
  for (const id of mustBeFullLength) {
@@ -4076,11 +4147,11 @@ function checkAudioTimelineTotals(ctx, audioMixNode, expected) {
4076
4147
  function checkNativeSegOverlap(ctx) {
4077
4148
  for (const conv of ctx.canvas.nodes) {
4078
4149
  if (conv.type !== "audio_voice_convert") continue;
4079
- const track = refNodeOf(ctx, (conv.inputs ?? {}).audio);
4150
+ const track = refNodeOf(ctx, conv.inputs?.audio);
4080
4151
  if (track?.type !== "audio_timeline") continue;
4081
4152
  const params = track.params;
4082
4153
  const windows = (params.tracks ?? []).map((t) => {
4083
- const extract = refNodeOf(ctx, (track.inputs ?? {})[t.slot]);
4154
+ const extract = refNodeOf(ctx, track.inputs?.[t.slot]);
4084
4155
  const len = t.duration_s ?? (extract ? ffmpegTrimSeconds(extract) : null);
4085
4156
  return len === null ? null : { slot: t.slot, start: t.start_s, end: t.start_s + len };
4086
4157
  }).filter((w) => w !== null).sort((a, b) => a.start - b.start);
@@ -4573,6 +4644,13 @@ function unwrap(schema) {
4573
4644
  }
4574
4645
 
4575
4646
  // src/engine/engine/executor.ts
4647
+ function heldByCheckpoint(canvas, layer, kinds) {
4648
+ if (!kinds?.size) return [];
4649
+ return layer.filter((id) => {
4650
+ const type = canvas.nodes.find((n) => n.id === id)?.type;
4651
+ return type !== void 0 && kinds.has(type);
4652
+ });
4653
+ }
4576
4654
  var Engine = class {
4577
4655
  registry;
4578
4656
  client;
@@ -4600,7 +4678,7 @@ var Engine = class {
4600
4678
  async run(input, opts = {}) {
4601
4679
  const validation = await this.validateDeep(input);
4602
4680
  if (!validation.ok) throw new ValidationError(validation.issues);
4603
- if (opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
4681
+ if (!opts.stop_before_kinds?.size && opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
4604
4682
  throw new RunAbortedError(
4605
4683
  "cost_cap",
4606
4684
  `estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
@@ -4619,6 +4697,20 @@ var Engine = class {
4619
4697
  const counters = { cachedNodes: 0, totalCredits: 0 };
4620
4698
  const nodeRuns = [];
4621
4699
  const graph = this.pruneToOutput(canvas, buildGraph(canvas));
4700
+ if (opts.max_credits !== void 0 && opts.stop_before_kinds?.size) {
4701
+ const willRun = nodesBeforeCheckpoint(
4702
+ graph,
4703
+ new Map(canvas.nodes.map((n) => [n.id, n.type])),
4704
+ opts.stop_before_kinds
4705
+ );
4706
+ const estimate = estimateCreditsFor(canvas, this.registry, willRun);
4707
+ if (estimate > opts.max_credits) {
4708
+ throw new RunAbortedError(
4709
+ "cost_cap",
4710
+ `estimated ${estimate} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
4711
+ );
4712
+ }
4713
+ }
4622
4714
  const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
4623
4715
  this.emitProgress(opts, {
4624
4716
  kind: "plan",
@@ -4663,6 +4755,13 @@ var Engine = class {
4663
4755
  `spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
4664
4756
  );
4665
4757
  }
4758
+ const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
4759
+ if (held.length > 0) {
4760
+ throw new RunAbortedError(
4761
+ "checkpoint",
4762
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
4763
+ );
4764
+ }
4666
4765
  const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
4667
4766
  if (opts.signal?.aborted) {
4668
4767
  return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
@@ -6022,8 +6121,9 @@ async function ingestImageUrl(url, ctx) {
6022
6121
  } catch (e) {
6023
6122
  throw localExecError(ctx, `${url}: ${e.message}`);
6024
6123
  }
6025
- const note = normalizationNote(normalized);
6026
- if (note) ctx.log(`ingest: ${url} ${note}`);
6124
+ if (normalized.rasterizedFrom) {
6125
+ ctx.log(`ingest: normalized ${normalized.rasterizedFrom} URL -> PNG (${normalized.bytes.length}B)`);
6126
+ }
6027
6127
  return uploadAndIngest({
6028
6128
  bytes: normalized.bytes,
6029
6129
  kind: "image",
@@ -6133,69 +6233,23 @@ async function rasterizeSvgToPng(bytes) {
6133
6233
  return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
6134
6234
  }
6135
6235
  var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
6136
- var MAX_MODEL_REFERENCE_BYTES = 30 * 1024 * 1024;
6137
- async function fitWithinReferenceLimit(bytes, mime, maxBytes) {
6138
- if (bytes.byteLength <= maxBytes) return { bytes, mime };
6139
- const { default: sharp } = await import("sharp");
6140
- const read = () => sharp(bytes, { animated: true });
6141
- const meta = await read().metadata();
6142
- const animated = (meta.pages ?? 1) > 1;
6143
- const keepAlpha = meta.hasAlpha === true;
6144
- const width = meta.width ?? 0;
6145
- const encode = async (scale2) => {
6146
- let pipeline2 = read();
6147
- if (scale2 < 1 && width > 0) pipeline2 = pipeline2.resize({ width: Math.max(256, Math.round(width * scale2)) });
6148
- if (animated && mime === "image/gif") return { bytes: await pipeline2.gif().toBuffer(), mime: "image/gif" };
6149
- if (animated || keepAlpha) return { bytes: await pipeline2.webp({ quality: 90 }).toBuffer(), mime: "image/webp" };
6150
- return { bytes: await pipeline2.jpeg({ quality: 90, mozjpeg: true }).toBuffer(), mime: "image/jpeg" };
6151
- };
6152
- let out = await encode(1);
6153
- let scale = 1;
6154
- for (let pass = 0; pass < 4 && out.bytes.byteLength > maxBytes; pass++) {
6155
- scale *= Math.min(0.9, Math.sqrt(maxBytes / out.bytes.byteLength));
6156
- out = await encode(scale);
6157
- }
6158
- return out;
6159
- }
6160
- async function toModelSafeImage(bytes, opts) {
6161
- const maxBytes = opts?.maxBytes ?? MAX_MODEL_REFERENCE_BYTES;
6162
- const fit = async (candidate, mime, rasterizedFrom) => {
6163
- const fitted = await fitWithinReferenceLimit(candidate, mime, maxBytes);
6164
- return {
6165
- bytes: fitted.bytes,
6166
- mime: fitted.mime,
6167
- ...rasterizedFrom ? { rasterizedFrom } : {},
6168
- ...fitted.bytes === candidate ? {} : { shrunkFromBytes: candidate.byteLength }
6169
- };
6170
- };
6236
+ async function toModelSafeImage(bytes) {
6171
6237
  const safe = sniffImageMime(bytes);
6172
6238
  if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
6173
- return await fit(bytes, safe);
6239
+ return { bytes, mime: safe };
6174
6240
  }
6175
6241
  if (sniffSvg(bytes)) {
6176
- return await fit(await rasterizeSvgToPng(bytes), "image/png", "svg");
6242
+ return { bytes: await rasterizeSvgToPng(bytes), mime: "image/png", rasterizedFrom: "svg" };
6177
6243
  }
6178
6244
  const { default: sharp } = await import("sharp");
6179
- let png;
6180
- let format;
6181
6245
  try {
6182
6246
  const img = sharp(bytes);
6183
- format = (await img.metadata()).format ?? "unknown";
6184
- png = await img.png({ force: true }).toBuffer();
6247
+ const format = (await img.metadata()).format;
6248
+ const png = await img.png({ force: true }).toBuffer();
6249
+ return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
6185
6250
  } catch (e) {
6186
6251
  throw new Error(`bytes are not a decodable image (${e.message})`);
6187
6252
  }
6188
- return await fit(png, "image/png", format);
6189
- }
6190
- function normalizationNote(image) {
6191
- const parts = [];
6192
- if (image.rasterizedFrom) parts.push(`normalized ${image.rasterizedFrom} -> ${image.mime}`);
6193
- if (image.shrunkFromBytes !== void 0) {
6194
- parts.push(
6195
- `fitted ${image.shrunkFromBytes}B -> ${image.bytes.length}B as ${image.mime} (providers refuse a reference over ${MAX_MODEL_REFERENCE_BYTES}B)`
6196
- );
6197
- }
6198
- return parts.length > 0 ? parts.join("; ") : null;
6199
6253
  }
6200
6254
  function hasAscii(buf, offset, sig) {
6201
6255
  return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
@@ -6325,8 +6379,7 @@ async function execLocalFile(params, ctx) {
6325
6379
  outBytes = normalized.bytes;
6326
6380
  outMime = normalized.mime;
6327
6381
  rasterizedFrom = normalized.rasterizedFrom;
6328
- const note = normalizationNote(normalized);
6329
- if (note) ctx.log(`ingest: ${note}`);
6382
+ if (rasterizedFrom) ctx.log(`ingest: normalized ${rasterizedFrom} -> PNG (${outBytes.length}B)`);
6330
6383
  }
6331
6384
  const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
6332
6385
  const ref = await uploadAndIngest({
@@ -7298,6 +7351,24 @@ import { execFile as execFile3 } from "child_process";
7298
7351
  import { readFile as readFile9 } from "fs/promises";
7299
7352
  import path12 from "path";
7300
7353
  import { promisify as promisify3 } from "util";
7354
+
7355
+ // src/engine/nodes/local/lib/caption-style.ts
7356
+ var TERMINAL = /[.,;:!?…]$/;
7357
+ function captionTextsFrom(html) {
7358
+ const cards = html.match(/<div[^>]*class="[^"]*\bcaption\b[^"]*"[\s\S]*?<\/div>/g) ?? [];
7359
+ return cards.map(
7360
+ (card) => card.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim()
7361
+ ).filter((text) => text.length > 0);
7362
+ }
7363
+ function fragmentedCaptions(texts) {
7364
+ if (texts.length < 2) return null;
7365
+ const fragments = texts.slice(0, -1).filter((t) => !TERMINAL.test(t));
7366
+ if (fragments.length === 0) return null;
7367
+ const example = fragments[0];
7368
+ return `${fragments.length} of ${texts.length} caption cards end mid-clause, e.g. "${example}". Split captions where the script punctuates, not on a word count \u2014 a card that ends bare is a sentence cut in half by a timing decision, and it reads as one.`;
7369
+ }
7370
+
7371
+ // src/engine/nodes/local/lib/hyperframe-check.ts
7301
7372
  var execFileAsync = promisify3(execFile3);
7302
7373
  var NEVER_BLOCK = [
7303
7374
  /contrast/i,
@@ -7366,6 +7437,74 @@ function classifyCheckOutput(lintRaw, inspectRaw) {
7366
7437
  warnings: findings.filter((f) => f.severity === "warning")
7367
7438
  };
7368
7439
  }
7440
+ var GATE_SECTIONS = ["lint", "runtime", "layout", "motion", "contrast"];
7441
+ function buildCheckArgs(dir, samples) {
7442
+ return ["hyperframes", "check", dir, "--json", "--samples", String(samples), "--at-transitions"];
7443
+ }
7444
+ var PROMOTE_TO_BLOCKING = /GSAP target [^ ]+ not found|no deterministic font mapping/i;
7445
+ function toGateFinding(section, entry) {
7446
+ const f = entry;
7447
+ const code = typeof f.code === "string" ? f.code : "unknown";
7448
+ const message2 = typeof f.message === "string" ? f.message : "";
7449
+ const isError = f.severity === "error" || PROMOTE_TO_BLOCKING.test(message2);
7450
+ const fatal = PROMOTE_TO_BLOCKING.test(message2);
7451
+ return {
7452
+ source: section,
7453
+ code,
7454
+ message: message2,
7455
+ severity: fatal || isError && !isAdvisory(code, message2) ? "blocking" : "warning"
7456
+ };
7457
+ }
7458
+ function classifyGateOutput(raw) {
7459
+ const parsed = parseCheckJson(raw);
7460
+ const findings = GATE_SECTIONS.flatMap((section) => {
7461
+ const block = parsed?.[section];
7462
+ const entries = Array.isArray(block?.findings) ? block.findings : [];
7463
+ return entries.map((entry) => toGateFinding(section, entry));
7464
+ });
7465
+ return {
7466
+ blocking: findings.filter((f) => f.severity === "blocking"),
7467
+ warnings: findings.filter((f) => f.severity === "warning")
7468
+ };
7469
+ }
7470
+ function summariseWarnings(warnings) {
7471
+ const groups = /* @__PURE__ */ new Map();
7472
+ for (const w of warnings) {
7473
+ const key = `${w.source}/${w.code}`;
7474
+ const seen = groups.get(key);
7475
+ if (seen) seen.count += 1;
7476
+ else groups.set(key, { count: 1, message: w.message });
7477
+ }
7478
+ return [...groups.entries()].sort((a, b) => b[1].count - a[1].count).map(([key, g]) => `${g.count}\xD7 [${key}] ${g.message}`);
7479
+ }
7480
+ var FINDING_ADVICE = {
7481
+ sweep_static: "the timeline never advanced, so this renders as a STILL IMAGE while billing like a video \u2014 and every other green verdict in this run is unreliable. Check the composition registers a paused timeline on `window.__timelines[<composition-id>]` and that the tweens target elements that exist.",
7482
+ console_error: "the composition threw in the browser, so whatever that script was driving did not happen.",
7483
+ page_error: "a script failed to evaluate \u2014 animations and any JS-driven layout in this composition did not run.",
7484
+ http_error: "an asset 404ed at render time; it will be missing from the frame, not substituted.",
7485
+ console_warning: "if this names a GSAP target, the element it animates does not exist, so that tween is a no-op and whatever it was meant to reveal never appears. Fix the selector or add the element before rendering."
7486
+ };
7487
+ function explainGateFinding(finding) {
7488
+ const advice = FINDING_ADVICE[finding.code];
7489
+ return advice ? `${finding.message} \u2014 ${advice}` : finding.message;
7490
+ }
7491
+ function condenseFrameDescriptions(markdown) {
7492
+ const lines = [];
7493
+ let frame = null;
7494
+ for (const line of markdown.split("\n")) {
7495
+ const heading = line.match(/^## (.+)$/);
7496
+ if (heading) {
7497
+ frame = heading[1] ?? null;
7498
+ continue;
7499
+ }
7500
+ const text = line.trim();
7501
+ if (frame && text) {
7502
+ lines.push(`${frame}: ${text}`);
7503
+ frame = null;
7504
+ }
7505
+ }
7506
+ return lines;
7507
+ }
7369
7508
  function buildLintArgs(dir) {
7370
7509
  return ["hyperframes", "lint", dir, "--json"];
7371
7510
  }
@@ -7397,8 +7536,13 @@ async function lintStructuralIssues(dir, timeoutMs) {
7397
7536
  function buildInspectArgs(dir, samples) {
7398
7537
  return ["hyperframes", "inspect", dir, "--json", "--samples", String(samples)];
7399
7538
  }
7400
- function buildSnapshotArgs(dir, frames) {
7401
- return ["hyperframes", "snapshot", dir, "--frames", String(frames), "--describe", "false"];
7539
+ function buildSnapshotArgs(dir, frames, vision) {
7540
+ const base = ["hyperframes", "snapshot", dir, "--frames", String(frames)];
7541
+ return vision ? [
7542
+ ...base,
7543
+ "--describe",
7544
+ "Answer four things about this frame. (1) Is it empty or near-empty? Name every element actually visible. (2) Could what is shown physically happen \u2014 object sizes, human scale, and whether every object has its real-world form (a phone has ONE screen, on its front). Say so plainly if something is impossible. (3) Read out every number, currency symbol and word visible INSIDE the picture \u2014 phone screens, documents, signage \u2014 and say whether any of it contradicts the caption on this frame. (4) Is the caption legible against what is behind it?"
7545
+ ] : [...base, "--describe", "false"];
7402
7546
  }
7403
7547
  function usesNestedCompositions(indexHtml) {
7404
7548
  const withoutComments = indexHtml.replace(/<!--[\s\S]*?-->/g, "");
@@ -7427,44 +7571,66 @@ async function runSnapshotSmoke(args, timeoutMs) {
7427
7571
  return { ok: false, unavailable: false, message: (err.stderr || err.message || "snapshot failed").slice(0, 800) };
7428
7572
  }
7429
7573
  }
7430
- async function runHyperframesCheck(opts) {
7431
- const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7574
+ async function runNestedCompositionSmoke(opts) {
7575
+ const { dir, nodeId, ctx, timeoutMs, samples } = opts;
7576
+ const indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7577
+ if (!indexHtml || !usesNestedCompositions(indexHtml)) return;
7578
+ const snap = await runSnapshotSmoke(
7579
+ buildSnapshotArgs(dir, Math.min(samples, 3), hasFrameVisionKey()),
7580
+ Math.max(timeoutMs, 12e4)
7581
+ );
7582
+ if (snap.unavailable) {
7583
+ ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7584
+ return;
7585
+ }
7586
+ if (!snap.ok) {
7587
+ throw new Error(
7588
+ `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7589
+ ${snap.message}`
7590
+ );
7591
+ }
7592
+ const described = await readFile9(path12.join(dir, "snapshots", "descriptions.md"), "utf-8").catch(() => "");
7593
+ for (const line of condenseFrameDescriptions(described)) {
7594
+ ctx.log(`${nodeId}: frame check \u2014 ${line}`);
7595
+ }
7596
+ ctx.log(`${nodeId}: nested-composition smoke test passed`);
7597
+ }
7598
+ async function runGate(dir, timeoutMs, samples) {
7599
+ const checkRaw = await runOne(buildCheckArgs(dir, samples), timeoutMs);
7600
+ if (checkRaw !== null) return classifyGateOutput(checkRaw);
7432
7601
  const [lintRaw, inspectRaw] = await Promise.all([
7433
7602
  runOne(buildLintArgs(dir), timeoutMs),
7434
7603
  runOne(buildInspectArgs(dir, samples), timeoutMs)
7435
7604
  ]);
7436
- if (lintRaw === null && inspectRaw === null) {
7437
- ctx.log(`${nodeId}: hyperframes lint/inspect unavailable \u2014 skipping pre-render check`);
7605
+ if (lintRaw === null && inspectRaw === null) return null;
7606
+ return classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7607
+ }
7608
+ async function runHyperframesCheck(opts) {
7609
+ const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7610
+ const gate = await runGate(dir, timeoutMs, samples);
7611
+ if (gate === null) {
7612
+ ctx.log(`${nodeId}: hyperframes check/lint/inspect unavailable \u2014 skipping pre-render check`);
7438
7613
  return;
7439
7614
  }
7440
- const { blocking, warnings } = classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7441
- for (const w of warnings) {
7442
- ctx.log(`${nodeId}: hyperframe check warning [${w.source}/${w.code}] ${w.message}`);
7615
+ const { blocking, warnings } = gate;
7616
+ for (const line of summariseWarnings(warnings)) {
7617
+ ctx.log(`${nodeId}: hyperframe check warning ${line}`);
7443
7618
  }
7444
7619
  if (blocking.length > 0) {
7445
- const detail = blocking.map((b) => `\u2022 [${b.source}/${b.code}] ${b.message}`).join("\n");
7446
- throw new Error(`${nodeId}: pre-render check failed (${blocking.length} blocking)
7447
- ${detail}`);
7448
- }
7449
- let indexHtml = "";
7450
- try {
7451
- indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8");
7452
- } catch {
7453
- indexHtml = "";
7620
+ const seen = /* @__PURE__ */ new Map();
7621
+ for (const b of blocking) seen.set(`${b.source}/${b.code}/${b.message}`, b);
7622
+ const detail = [...seen.values()].map((b) => `\u2022 [${b.source}/${b.code}] ${explainGateFinding(b)}`).join("\n");
7623
+ throw new Error(
7624
+ `${nodeId}: pre-render check failed (${blocking.length} blocking, ${seen.size} distinct)
7625
+ ${detail}`
7626
+ );
7454
7627
  }
7455
- if (indexHtml && usesNestedCompositions(indexHtml)) {
7456
- const snap = await runSnapshotSmoke(buildSnapshotArgs(dir, Math.min(samples, 3)), Math.max(timeoutMs, 12e4));
7457
- if (snap.unavailable) {
7458
- ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7459
- } else if (!snap.ok) {
7460
- throw new Error(
7461
- `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7462
- ${snap.message}`
7463
- );
7464
- } else {
7465
- ctx.log(`${nodeId}: nested-composition smoke test passed`);
7466
- }
7628
+ const composition = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7629
+ const captionComplaint = fragmentedCaptions(captionTextsFrom(composition));
7630
+ if (captionComplaint) {
7631
+ ctx.log(`${nodeId}: caption style \u2014 ${captionComplaint}`);
7467
7632
  }
7633
+ await runNestedCompositionSmoke({ dir, nodeId, ctx, timeoutMs, samples });
7468
7634
  ctx.log(`${nodeId}: pre-render check passed (${warnings.length} warning${warnings.length === 1 ? "" : "s"})`);
7469
7635
  }
7470
7636
 
@@ -8971,7 +9137,14 @@ async function tryExtractAudio(inputs, ctx) {
8971
9137
  import { z as z34 } from "zod";
8972
9138
  var voiceSelectNode = delegated({
8973
9139
  id: "voice_select",
8974
- version: "1.0.0",
9140
+ // 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
9141
+ // and no mime, which every consumer taking the id as an INPUT rejected —
9142
+ // `audio_voice_convert` died on `voice_ref.sha256: expected string, received undefined`.
9143
+ // The backend now uploads it as a real asset like the ranked branch, and the version has
9144
+ // to move with it: the cache key is built from this string, so entries written under
9145
+ // 1.0.0 kept replaying the broken shape long after the fix was deployed. A node whose
9146
+ // OUTPUT shape changes has to bump, not only one whose behaviour does.
9147
+ version: "1.1.0",
8975
9148
  category: "audio",
8976
9149
  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.',
8977
9150
  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.',
@@ -8982,6 +9155,11 @@ var voiceSelectNode = delegated({
8982
9155
  age: z34.string().optional(),
8983
9156
  accent: z34.string().optional(),
8984
9157
  language: z34.string().optional(),
9158
+ /**
9159
+ * A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
9160
+ * ranking is skipped and this id is emitted as-is.
9161
+ */
9162
+ voice_id: z34.string().min(1).max(120).optional(),
8985
9163
  limit: z34.number().int().min(1).max(20).optional()
8986
9164
  }).strict(),
8987
9165
  outputs: z34.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
@@ -9144,6 +9322,7 @@ export {
9144
9322
  describeFailureReason,
9145
9323
  AD_FORMAT_PLATFORMS,
9146
9324
  platformFormats,
9325
+ frameRealismDirection,
9147
9326
  SEEDANCE_DURATIONS,
9148
9327
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
9149
9328
  IMAGE_GENERATE_MODELS,
@@ -9159,6 +9338,7 @@ export {
9159
9338
  promptMaxLength,
9160
9339
  maxInputSlot,
9161
9340
  maxInputReferences,
9341
+ bestSupportedQuality,
9162
9342
  nearestSupportedAspectRatio,
9163
9343
  nearestSupportedImageSize,
9164
9344
  estimateVideoCredits,
@@ -9199,4 +9379,4 @@ export {
9199
9379
  defaultRegistry,
9200
9380
  createEngineFromEnv
9201
9381
  };
9202
- //# sourceMappingURL=chunk-NNQDWCFG.js.map
9382
+ //# sourceMappingURL=chunk-LTXNRRXW.js.map