@koda-sl/baker-cli 0.394.0-dev.d28cef496 → 0.395.0-dev.f8ecbaa42

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.
@@ -20,9 +20,9 @@ import {
20
20
  shouldEscalate
21
21
  } from "./chunk-AOVFDDVW.js";
22
22
 
23
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js
23
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
24
24
  var require_safe_stable_stringify = __commonJS({
25
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js"(exports, module) {
25
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
26
26
  "use strict";
27
27
  var { hasOwnProperty } = Object.prototype;
28
28
  var stringify = configure2();
@@ -1123,7 +1123,7 @@ function resolveAdaptFormats(params) {
1123
1123
  return params.formats ?? [];
1124
1124
  }
1125
1125
 
1126
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/esm/wrapper.js
1126
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
1127
1127
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1128
1128
  var configure = import__.default.configure;
1129
1129
  var wrapper_default = import__.default;
@@ -1340,12 +1340,17 @@ var IMAGE_GENERATE_MODELS = [
1340
1340
  "openai/gpt-image-2.5-sunburst",
1341
1341
  "openai/gpt-image-2",
1342
1342
  "openai/gpt-5.4-image-2",
1343
- "google/gemini-3.1-flash-image-preview",
1343
+ "google/gemini-3.1-flash-image",
1344
1344
  "google/gemini-3.1-flash-lite-image",
1345
- "google/gemini-3-pro-image-preview",
1346
- "recraft/recraft-v4.1-pro-vector"
1345
+ "google/gemini-3-pro-image",
1346
+ "recraft/recraft-v4.1-pro-vector",
1347
+ // LEGACY — the preview ids the Gemini image models shipped under until they went GA
1348
+ // on 2026-06-18. Kept so every canvas written with them still validates and re-runs;
1349
+ // never pick them for new work.
1350
+ "google/gemini-3.1-flash-image-preview",
1351
+ "google/gemini-3-pro-image-preview"
1347
1352
  ];
1348
- var DEFAULT_IMAGE_GENERATE_MODEL = "google/gemini-3.1-flash-image-preview";
1353
+ var DEFAULT_IMAGE_GENERATE_MODEL = "google/gemini-3.1-flash-image";
1349
1354
  var VIDEO_GENERATE_MODELS = [
1350
1355
  "bytedance/seedance-2.0",
1351
1356
  "bytedance/seedance-2.5",
@@ -1454,7 +1459,9 @@ var MODEL_REGISTRY = {
1454
1459
  // Vision judgement — the selection contract (numbered images, JSON
1455
1460
  // response) is baked into the backend prompt; `prompt` carries only the
1456
1461
  // selection criteria. `count` has an engine-side default of 1, so it is
1457
- // not registry-required.
1462
+ // not registry-required. `keep_reserved_areas` is read by the Runtime, which
1463
+ // drops a candidate that filled the copy's areas before the judge, and is
1464
+ // never sent on to the backend.
1458
1465
  "~google/gemini-flash-latest": {
1459
1466
  label: "Google Gemini Flash (latest, Vision)",
1460
1467
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
@@ -1463,7 +1470,8 @@ var MODEL_REGISTRY = {
1463
1470
  prompt: { kind: "string" },
1464
1471
  count: { kind: "number", min: 1 },
1465
1472
  temperature: { kind: "number", min: 0, max: 2 },
1466
- max_tokens: { kind: "number", min: 1 }
1473
+ max_tokens: { kind: "number", min: 1 },
1474
+ keep_reserved_areas: { kind: "boolean" }
1467
1475
  }
1468
1476
  },
1469
1477
  "~google/gemini-pro-latest": {
@@ -1474,7 +1482,8 @@ var MODEL_REGISTRY = {
1474
1482
  prompt: { kind: "string" },
1475
1483
  count: { kind: "number", min: 1 },
1476
1484
  temperature: { kind: "number", min: 0, max: 2 },
1477
- max_tokens: { kind: "number", min: 1 }
1485
+ max_tokens: { kind: "number", min: 1 },
1486
+ keep_reserved_areas: { kind: "boolean" }
1478
1487
  }
1479
1488
  }
1480
1489
  },
@@ -1536,6 +1545,32 @@ var MODEL_REGISTRY = {
1536
1545
  quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
1537
1546
  }
1538
1547
  },
1548
+ // The GA Nano Banana ids (2026-06-18). Same surface as the preview entries they
1549
+ // replace — read off GET /api/v1/images/models/google/gemini-3.1-flash-image/endpoints
1550
+ // and .../gemini-3-pro-image/endpoints — so a canvas moves between them unchanged.
1551
+ "google/gemini-3.1-flash-image": {
1552
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1553
+ inputs: [],
1554
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1555
+ required: ["prompt"],
1556
+ params: {
1557
+ prompt: { kind: "string" },
1558
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
1559
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1560
+ }
1561
+ },
1562
+ "google/gemini-3-pro-image": {
1563
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1564
+ inputs: [],
1565
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1566
+ required: ["prompt"],
1567
+ params: {
1568
+ prompt: { kind: "string" },
1569
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
1570
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1571
+ }
1572
+ },
1573
+ // LEGACY preview ids — see IMAGE_GENERATE_MODELS.
1539
1574
  "google/gemini-3.1-flash-image-preview": {
1540
1575
  label: "Google Gemini 3.1 Flash Image (Preview)",
1541
1576
  inputs: [],
@@ -1573,7 +1608,7 @@ var MODEL_REGISTRY = {
1573
1608
  }
1574
1609
  },
1575
1610
  "google/gemini-3-pro-image-preview": {
1576
- label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1611
+ label: "Google Gemini 3 Pro Image (Preview)",
1577
1612
  inputs: [],
1578
1613
  optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1579
1614
  required: ["prompt"],
@@ -1616,6 +1651,35 @@ var MODEL_REGISTRY = {
1616
1651
  // is baked into the backend handler — params here are semantic levers
1617
1652
  // only. Both models consume MULTIPLE reference images via the `references`
1618
1653
  // input slot (required: a sheet without source images is just image_generate).
1654
+ "google/gemini-3-pro-image": {
1655
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1656
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1657
+ required: ["subject_description", "subject_type"],
1658
+ params: {
1659
+ subject_description: { kind: "string" },
1660
+ subject_type: { kind: "string", enum: ["character", "person", "product", "location"] },
1661
+ views: { kind: "json" },
1662
+ style: { kind: "string" },
1663
+ prompt_override: { kind: "string" },
1664
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
1665
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1666
+ }
1667
+ },
1668
+ "google/gemini-3.1-flash-image": {
1669
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1670
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1671
+ required: ["subject_description", "subject_type"],
1672
+ params: {
1673
+ subject_description: { kind: "string" },
1674
+ subject_type: { kind: "string", enum: ["character", "person", "product", "location"] },
1675
+ views: { kind: "json" },
1676
+ style: { kind: "string" },
1677
+ prompt_override: { kind: "string" },
1678
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
1679
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1680
+ }
1681
+ },
1682
+ // LEGACY preview ids, kept so existing canvases re-run.
1619
1683
  "google/gemini-3-pro-image-preview": {
1620
1684
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1621
1685
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
@@ -1654,6 +1718,29 @@ var MODEL_REGISTRY = {
1654
1718
  // registry can't express "one of" or gate array elements), so `formats`
1655
1719
  // is not listed as registry-required. The source creative is a required
1656
1720
  // input slot.
1721
+ "google/gemini-3-pro-image": {
1722
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1723
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1724
+ required: [],
1725
+ params: {
1726
+ formats: { kind: "json" },
1727
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1728
+ guidance: { kind: "string" },
1729
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1730
+ }
1731
+ },
1732
+ "google/gemini-3.1-flash-image": {
1733
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1734
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1735
+ required: [],
1736
+ params: {
1737
+ formats: { kind: "json" },
1738
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1739
+ guidance: { kind: "string" },
1740
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1741
+ }
1742
+ },
1743
+ // LEGACY preview ids, kept so existing canvases re-run.
1657
1744
  "google/gemini-3-pro-image-preview": {
1658
1745
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1659
1746
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
@@ -2262,8 +2349,8 @@ function promptMaxLength(kind, model) {
2262
2349
  return schema?.kind === "string" ? schema.maxLength : void 0;
2263
2350
  }
2264
2351
  function inputSlot(kind, model, inputKind) {
2265
- const spec = MODEL_REGISTRY[kind]?.[model];
2266
- return spec?.optional_inputs?.find((input) => input.kind === inputKind) ?? spec?.inputs.find((input) => input.kind === inputKind);
2352
+ const spec2 = MODEL_REGISTRY[kind]?.[model];
2353
+ return spec2?.optional_inputs?.find((input) => input.kind === inputKind) ?? spec2?.inputs.find((input) => input.kind === inputKind);
2267
2354
  }
2268
2355
  function maxInputSlot(kind, model, inputKind) {
2269
2356
  return inputSlot(kind, model, inputKind)?.max;
@@ -2332,9 +2419,9 @@ function nearestSupportedImageSize(kind, model, target) {
2332
2419
  function isManagedNodeKind(nodeType) {
2333
2420
  return Object.keys(MODEL_REGISTRY).includes(nodeType);
2334
2421
  }
2335
- function checkRequiredParams(spec, params, model) {
2422
+ function checkRequiredParams(spec2, params, model) {
2336
2423
  const errors = [];
2337
- for (const req of spec.required) {
2424
+ for (const req of spec2.required) {
2338
2425
  if (params[req] === void 0 || params[req] === null) {
2339
2426
  errors.push({
2340
2427
  code: "missing_required_param",
@@ -2345,12 +2432,12 @@ function checkRequiredParams(spec, params, model) {
2345
2432
  }
2346
2433
  return errors;
2347
2434
  }
2348
- function checkParamValues(spec, params, model) {
2435
+ function checkParamValues(spec2, params, model) {
2349
2436
  const errors = [];
2350
2437
  for (const [key, value] of Object.entries(params)) {
2351
2438
  if (key === "model") continue;
2352
2439
  if (value === void 0) continue;
2353
- const schema = spec.params[key];
2440
+ const schema = spec2.params[key];
2354
2441
  if (!schema) {
2355
2442
  errors.push({
2356
2443
  code: "unknown_param",
@@ -2365,14 +2452,14 @@ function checkParamValues(spec, params, model) {
2365
2452
  return errors;
2366
2453
  }
2367
2454
  function validateModelParams(nodeType, model, params) {
2368
- const spec = MODEL_REGISTRY[nodeType][model];
2369
- if (!spec) {
2455
+ const spec2 = MODEL_REGISTRY[nodeType][model];
2456
+ if (!spec2) {
2370
2457
  return {
2371
2458
  ok: false,
2372
2459
  errors: [{ code: "unknown_model", message: `unknown model "${model}" for node "${nodeType}"` }]
2373
2460
  };
2374
2461
  }
2375
- const errors = [...checkRequiredParams(spec, params, model), ...checkParamValues(spec, params, model)];
2462
+ const errors = [...checkRequiredParams(spec2, params, model), ...checkParamValues(spec2, params, model)];
2376
2463
  if (errors.length > 0) return { ok: false, errors };
2377
2464
  if (nodeType === "video_generate") {
2378
2465
  const tooHeavy = clipTooHeavyToIngest(
@@ -2384,8 +2471,8 @@ function validateModelParams(nodeType, model, params) {
2384
2471
  return { ok: true };
2385
2472
  }
2386
2473
  function validateModelInputs(nodeType, model, providedKinds) {
2387
- const spec = MODEL_REGISTRY[nodeType][model];
2388
- if (!spec) {
2474
+ const spec2 = MODEL_REGISTRY[nodeType][model];
2475
+ if (!spec2) {
2389
2476
  return {
2390
2477
  ok: false,
2391
2478
  errors: [{ code: "unknown_model", message: `unknown model "${model}" for node "${nodeType}"` }]
@@ -2393,7 +2480,7 @@ function validateModelInputs(nodeType, model, providedKinds) {
2393
2480
  }
2394
2481
  const have = new Set(providedKinds);
2395
2482
  const errors = [];
2396
- for (const inputSpec of spec.inputs) {
2483
+ for (const inputSpec of spec2.inputs) {
2397
2484
  if (!have.has(inputSpec.kind)) {
2398
2485
  errors.push({
2399
2486
  code: "missing_input",
@@ -3133,7 +3220,7 @@ var LocalAssetStore = class {
3133
3220
  );
3134
3221
  const written = await readFile(tmp);
3135
3222
  const computed = sha256Hex(written);
3136
- if (computed !== args.sha256) {
3223
+ if (computed !== args.sha256 && !cdnReencodedImage(res, args.kind)) {
3137
3224
  throw new Error(`asset-store: sha mismatch for ${args.url} (got ${computed}, expected ${args.sha256})`);
3138
3225
  }
3139
3226
  await rename(tmp, dest);
@@ -3154,6 +3241,9 @@ var LocalAssetStore = class {
3154
3241
  });
3155
3242
  }
3156
3243
  };
3244
+ function cdnReencodedImage(res, kind) {
3245
+ return kind === "image" && res.headers.get("cf-polished") !== null;
3246
+ }
3157
3247
  function byteLimit(url, maxBytes) {
3158
3248
  let total = 0;
3159
3249
  return new Transform({
@@ -3600,9 +3690,9 @@ function parseSpineInputLens(args) {
3600
3690
  }
3601
3691
 
3602
3692
  // src/engine/engine/define.ts
3603
- function resolveOutputKinds(spec, params) {
3604
- if (!spec) return {};
3605
- return typeof spec === "function" ? spec(params) : spec;
3693
+ function resolveOutputKinds(spec2, params) {
3694
+ if (!spec2) return {};
3695
+ return typeof spec2 === "function" ? spec2(params) : spec2;
3606
3696
  }
3607
3697
  function defineNode(def) {
3608
3698
  return def;
@@ -3680,9 +3770,9 @@ function cappedParamsOf(n) {
3680
3770
  if (!isManagedNodeKind(n.type)) return [];
3681
3771
  const model = n.params?.model;
3682
3772
  if (typeof model !== "string") return [];
3683
- const spec = MODEL_REGISTRY[n.type][model];
3684
- if (!spec) return [];
3685
- return Object.entries(spec.params).flatMap(
3773
+ const spec2 = MODEL_REGISTRY[n.type][model];
3774
+ if (!spec2) return [];
3775
+ return Object.entries(spec2.params).flatMap(
3686
3776
  ([key, schema]) => schema.kind === "string" && schema.maxLength !== void 0 ? [{ key, max: schema.maxLength, model }] : []
3687
3777
  );
3688
3778
  }
@@ -4625,8 +4715,8 @@ function checkReferenceCompleteness(ctx, meta) {
4625
4715
  }
4626
4716
  }
4627
4717
  function videoModelMaxDuration(model) {
4628
- const spec = MODEL_REGISTRY.video_generate[model];
4629
- const durations = spec?.params?.duration?.enum;
4718
+ const spec2 = MODEL_REGISTRY.video_generate[model];
4719
+ const durations = spec2?.params?.duration?.enum;
4630
4720
  if (!durations) return void 0;
4631
4721
  const nums = durations.filter((d) => typeof d === "number");
4632
4722
  return nums.length > 0 ? Math.max(...nums) : void 0;
@@ -5275,6 +5365,7 @@ var Engine = class {
5275
5365
  }
5276
5366
  }
5277
5367
  const t0 = Date.now();
5368
+ let charged;
5278
5369
  const ctx = {
5279
5370
  canvasRunId: runId,
5280
5371
  nodeId: node.id,
@@ -5284,13 +5375,16 @@ var Engine = class {
5284
5375
  client: this.client,
5285
5376
  assets: this.assets,
5286
5377
  log: this.log,
5287
- signal: opts.signal
5378
+ signal: opts.signal,
5379
+ reportCharge: (credits2) => {
5380
+ charged = (charged ?? 0) + credits2;
5381
+ }
5288
5382
  };
5289
5383
  const preparedForExec = needsLocalMaterialization(def) ? { ...prepared, resolvedInputs: await this.materializeLocalInputs(prepared.resolvedInputs) } : prepared;
5290
5384
  const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
5291
5385
  const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
5292
5386
  const elapsed = Date.now() - t0;
5293
- const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
5387
+ const credits = charged ?? estimatedNodeCredits(def, parsedParams);
5294
5388
  const outputsObj = result;
5295
5389
  outputs[node.id] = outputsObj;
5296
5390
  if (this.persistAssets) {
@@ -5428,6 +5522,9 @@ async function invokeExecute(def, parsedInputs, parsedParams, ctx, nodeId, nodeT
5428
5522
  throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
5429
5523
  }
5430
5524
  }
5525
+ function estimatedNodeCredits(def, params) {
5526
+ return def.cost ? def.cost({ params }).credits : 0;
5527
+ }
5431
5528
  function needsLocalMaterialization(def) {
5432
5529
  return def.location === "local" && !def.passthroughRefs;
5433
5530
  }
@@ -6280,17 +6377,17 @@ var FontRef = BaseAssetRef.extend({
6280
6377
  var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
6281
6378
 
6282
6379
  // src/engine/nodes/remote/delegate.ts
6283
- function delegated(spec) {
6380
+ function delegated(spec2) {
6284
6381
  return {
6285
- ...spec,
6382
+ ...spec2,
6286
6383
  location: "remote",
6287
6384
  execute: async ({ inputs, params, ctx }) => {
6288
6385
  return await callBackendExec({
6289
- nodeType: spec.id,
6290
- nodeVersion: spec.version,
6386
+ nodeType: spec2.id,
6387
+ nodeVersion: spec2.version,
6291
6388
  params,
6292
6389
  inputs,
6293
- outputKinds: resolveOutputKinds(spec.outputKinds, params),
6390
+ outputKinds: resolveOutputKinds(spec2.outputKinds, params),
6294
6391
  ctx
6295
6392
  });
6296
6393
  }
@@ -6312,6 +6409,9 @@ async function callBackendExec(args) {
6312
6409
  },
6313
6410
  args.ctx.signal
6314
6411
  );
6412
+ if (typeof res.credits_charged === "number" && Number.isFinite(res.credits_charged)) {
6413
+ args.ctx.reportCharge?.(res.credits_charged);
6414
+ }
6315
6415
  return await ingestOutputs(res.outputs, args.ctx, args.outputKinds);
6316
6416
  } catch (e) {
6317
6417
  throw mapClientError(args.ctx, e);
@@ -6663,22 +6763,22 @@ function inferMimeFromPath(absPath, sniffBytes) {
6663
6763
  return "application/octet-stream";
6664
6764
  }
6665
6765
  async function rasterizeSvgToPng(bytes) {
6666
- const { default: sharp } = await import("sharp");
6667
- const base = await sharp(bytes).metadata();
6766
+ const { default: sharp2 } = await import("sharp");
6767
+ const base = await sharp2(bytes).metadata();
6668
6768
  const longest = Math.max(base.width ?? 0, base.height ?? 0);
6669
6769
  let density = SVG_BASE_DENSITY;
6670
6770
  if (longest > 0) {
6671
6771
  const scaled = Math.round(SVG_BASE_DENSITY * SVG_TARGET_LONGEST_PX / longest);
6672
6772
  density = Math.min(SVG_MAX_DENSITY, Math.max(SVG_BASE_DENSITY, scaled));
6673
6773
  }
6674
- return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
6774
+ return await sharp2(bytes, { density }).png({ force: true, palette: false }).toBuffer();
6675
6775
  }
6676
6776
  var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
6677
6777
  var MAX_MODEL_REFERENCE_BYTES = 30 * 1024 * 1024;
6678
6778
  async function fitWithinReferenceLimit(bytes, mime, maxBytes) {
6679
6779
  if (bytes.byteLength <= maxBytes) return { bytes, mime };
6680
- const { default: sharp } = await import("sharp");
6681
- const read = () => sharp(bytes, { animated: true });
6780
+ const { default: sharp2 } = await import("sharp");
6781
+ const read = () => sharp2(bytes, { animated: true });
6682
6782
  const meta = await read().metadata();
6683
6783
  const animated = (meta.pages ?? 1) > 1;
6684
6784
  const keepAlpha = meta.hasAlpha === true;
@@ -6716,11 +6816,11 @@ async function toModelSafeImage(bytes, opts) {
6716
6816
  if (sniffSvg(bytes)) {
6717
6817
  return await fit(await rasterizeSvgToPng(bytes), "image/png", "svg");
6718
6818
  }
6719
- const { default: sharp } = await import("sharp");
6819
+ const { default: sharp2 } = await import("sharp");
6720
6820
  let png;
6721
6821
  let format;
6722
6822
  try {
6723
- const img = sharp(bytes);
6823
+ const img = sharp2(bytes);
6724
6824
  format = (await img.metadata()).format ?? "unknown";
6725
6825
  png = await img.png({ force: true }).toBuffer();
6726
6826
  } catch (e) {
@@ -7081,10 +7181,10 @@ function planSingleInputSlot(tmpDir, slot, ref, lookup, stagedInputs) {
7081
7181
  }
7082
7182
  function planOutputs(tmpDir, outputs, lookup) {
7083
7183
  const outputPaths = [];
7084
- for (const [name, spec] of Object.entries(outputs)) {
7085
- const ext = spec.ext.startsWith(".") ? spec.ext : `.${spec.ext}`;
7184
+ for (const [name, spec2] of Object.entries(outputs)) {
7185
+ const ext = spec2.ext.startsWith(".") ? spec2.ext : `.${spec2.ext}`;
7086
7186
  const absPath = path8.join(tmpDir, `out_${name}${ext}`);
7087
- outputPaths.push({ name, absPath, spec });
7187
+ outputPaths.push({ name, absPath, spec: spec2 });
7088
7188
  lookup.set(`out.${name}`, absPath);
7089
7189
  }
7090
7190
  return outputPaths;
@@ -7192,7 +7292,7 @@ async function runCli(opts) {
7192
7292
  ${tailLines(stderr, 40)}`);
7193
7293
  }
7194
7294
  const result = {};
7195
- for (const { name, absPath, spec } of outputPaths) {
7295
+ for (const { name, absPath, spec: spec2 } of outputPaths) {
7196
7296
  const s = await stat3(absPath).catch(() => null);
7197
7297
  if (!s?.isFile() || s.size === 0) {
7198
7298
  throw new Error(`cli-runner: declared output "${name}" missing or empty at ${absPath}`);
@@ -7200,8 +7300,8 @@ ${tailLines(stderr, 40)}`);
7200
7300
  const bytes = await readFile6(absPath);
7201
7301
  const ref = await ctx.assets.ingestBytes({
7202
7302
  bytes: Buffer.from(bytes),
7203
- kind: spec.kind,
7204
- mime: mimeForExt(spec.ext)
7303
+ kind: spec2.kind,
7304
+ mime: mimeForExt(spec2.ext)
7205
7305
  });
7206
7306
  result[name] = ref;
7207
7307
  }
@@ -7692,7 +7792,7 @@ async function collectFiles(root, current) {
7692
7792
  import { readFile as readFile9 } from "fs/promises";
7693
7793
  import path12 from "path";
7694
7794
  import { z as z10 } from "zod";
7695
- var InputKind = z10.enum(["video", "image", "audio", "json"]);
7795
+ var InputKind = z10.enum(["video", "image", "audio", "json", "font"]);
7696
7796
  var InputSpec = z10.object({
7697
7797
  kind: InputKind,
7698
7798
  required: z10.boolean().optional().default(false),
@@ -7789,47 +7889,47 @@ async function loadCompositionMeta(compositionDir) {
7789
7889
  }
7790
7890
  function buildParamsSchema(meta) {
7791
7891
  const shape = {};
7792
- for (const [name, spec] of Object.entries(meta.params)) {
7793
- shape[name] = buildParamFieldSchema(name, spec);
7892
+ for (const [name, spec2] of Object.entries(meta.params)) {
7893
+ shape[name] = buildParamFieldSchema(name, spec2);
7794
7894
  }
7795
7895
  return z10.object(shape).strict();
7796
7896
  }
7797
- function buildParamFieldSchema(name, spec) {
7798
- switch (spec.kind) {
7897
+ function buildParamFieldSchema(name, spec2) {
7898
+ switch (spec2.kind) {
7799
7899
  case "string": {
7800
- const s = spec.enum && spec.enum.length > 0 ? z10.enum(spec.enum) : z10.string();
7801
- return finalize(s, spec.default, spec.required);
7900
+ const s = spec2.enum && spec2.enum.length > 0 ? z10.enum(spec2.enum) : z10.string();
7901
+ return finalize(s, spec2.default, spec2.required);
7802
7902
  }
7803
7903
  case "integer": {
7804
7904
  let s = z10.number().int();
7805
- if (spec.min !== void 0) s = s.min(spec.min);
7806
- if (spec.max !== void 0) s = s.max(spec.max);
7807
- return finalize(s, spec.default, spec.required);
7905
+ if (spec2.min !== void 0) s = s.min(spec2.min);
7906
+ if (spec2.max !== void 0) s = s.max(spec2.max);
7907
+ return finalize(s, spec2.default, spec2.required);
7808
7908
  }
7809
7909
  case "number": {
7810
7910
  let s = z10.number();
7811
- if (spec.min !== void 0) s = s.min(spec.min);
7812
- if (spec.max !== void 0) s = s.max(spec.max);
7813
- return finalize(s, spec.default, spec.required);
7911
+ if (spec2.min !== void 0) s = s.min(spec2.min);
7912
+ if (spec2.max !== void 0) s = s.max(spec2.max);
7913
+ return finalize(s, spec2.default, spec2.required);
7814
7914
  }
7815
7915
  case "boolean":
7816
- return finalize(z10.boolean(), spec.default, spec.required);
7916
+ return finalize(z10.boolean(), spec2.default, spec2.required);
7817
7917
  case "color": {
7818
7918
  const s = z10.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
7819
7919
  message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
7820
7920
  });
7821
- return finalize(s, spec.default, spec.required);
7921
+ return finalize(s, spec2.default, spec2.required);
7822
7922
  }
7823
7923
  case "image":
7824
- return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec.default, spec.required);
7924
+ return finalize(z10.union([z10.string().min(1), z10.record(z10.string(), z10.unknown())]), spec2.default, spec2.required);
7825
7925
  case "json":
7826
7926
  return finalize(
7827
7927
  z10.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
7828
- spec.default,
7829
- spec.required
7928
+ spec2.default,
7929
+ spec2.required
7830
7930
  );
7831
7931
  default: {
7832
- const _exhaustive = spec;
7932
+ const _exhaustive = spec2;
7833
7933
  throw new Error(`buildParamFieldSchema: unknown kind ${_exhaustive.kind}`);
7834
7934
  }
7835
7935
  }
@@ -7841,6 +7941,7 @@ function finalize(schema, dflt, required) {
7841
7941
  }
7842
7942
  function defaultFilenameForInput(key, kind) {
7843
7943
  if (kind === "video") return `${key}.mp4`;
7944
+ if (kind === "font") return `${key}.font`;
7844
7945
  if (kind === "audio") return `${key}.mp3`;
7845
7946
  if (kind === "json") return `${key}.json`;
7846
7947
  return `${key}.png`;
@@ -8335,7 +8436,7 @@ async function validateComposition(rawParams) {
8335
8436
  }
8336
8437
  return issues;
8337
8438
  }
8338
- async function validateCompositionParams(rawParams) {
8439
+ async function validateCompositionParams(rawParams, ownedKeys = NODE_OWNED_PARAM_KEYS) {
8339
8440
  const issues = [];
8340
8441
  if (!rawParams || typeof rawParams !== "object") return issues;
8341
8442
  const params = rawParams;
@@ -8357,7 +8458,7 @@ async function validateCompositionParams(rawParams) {
8357
8458
  }
8358
8459
  const dynamic = {};
8359
8460
  for (const [k, v] of Object.entries(params)) {
8360
- if (NODE_OWNED_PARAM_KEYS.has(k)) continue;
8461
+ if (ownedKeys.has(k)) continue;
8361
8462
  dynamic[k] = v;
8362
8463
  }
8363
8464
  for (const key of Object.keys(dynamic)) {
@@ -8418,12 +8519,12 @@ async function stageInputs2(tmp, inputs, meta, ctx) {
8418
8519
  assertInputsMatchMeta(inputs, meta, "hyperframe_render");
8419
8520
  let primaryDuration = null;
8420
8521
  for (const [key, ref] of Object.entries(inputs ?? {})) {
8421
- const spec = meta.inputs[key];
8422
- if (!spec) continue;
8423
- const filename = spec.staged_as ?? defaultFilenameForInput(key, spec.kind);
8522
+ const spec2 = meta.inputs[key];
8523
+ if (!spec2) continue;
8524
+ const filename = spec2.staged_as ?? defaultFilenameForInput(key, spec2.kind);
8424
8525
  await stageAsset(ref, tmp, filename);
8425
- ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
8426
- if (spec.kind === "video" && primaryDuration === null) {
8526
+ ctx.log(`staged ${spec2.kind} \u2192 ${filename}`);
8527
+ if (spec2.kind === "video" && primaryDuration === null) {
8427
8528
  primaryDuration = await probeDurationSeconds(path15.join(tmp, filename));
8428
8529
  }
8429
8530
  }
@@ -8438,24 +8539,24 @@ function assertInputsMatchMeta(inputs, meta, nodeId) {
8438
8539
  throw new Error(`${nodeId}: inputs.${key} is not declared in composition meta.json (declares: ${declared})`);
8439
8540
  }
8440
8541
  }
8441
- for (const [key, spec] of Object.entries(meta.inputs)) {
8442
- if (spec.required && !providedKeys.has(key)) {
8443
- throw new Error(`${nodeId}: inputs.${key} is required (kind=${spec.kind})`);
8542
+ for (const [key, spec2] of Object.entries(meta.inputs)) {
8543
+ if (spec2.required && !providedKeys.has(key)) {
8544
+ throw new Error(`${nodeId}: inputs.${key} is required (kind=${spec2.kind})`);
8444
8545
  }
8445
8546
  }
8446
8547
  for (const [key, ref] of Object.entries(inputs ?? {})) {
8447
- const spec = meta.inputs[key];
8448
- if (!spec) continue;
8449
- if (ref.kind !== spec.kind) {
8450
- throw new Error(`${nodeId}: inputs.${key} expected kind "${spec.kind}", got "${ref.kind}"`);
8548
+ const spec2 = meta.inputs[key];
8549
+ if (!spec2) continue;
8550
+ if (ref.kind !== spec2.kind) {
8551
+ throw new Error(`${nodeId}: inputs.${key} expected kind "${spec2.kind}", got "${ref.kind}"`);
8451
8552
  }
8452
8553
  }
8453
8554
  }
8454
8555
  async function buildSubstitutionValues(compositionParams, meta, duration) {
8455
8556
  const out = {};
8456
8557
  for (const [name, value] of Object.entries(compositionParams)) {
8457
- const spec = meta.params[name];
8458
- if (spec?.kind === "image") {
8558
+ const spec2 = meta.params[name];
8559
+ if (spec2?.kind === "image") {
8459
8560
  out[name] = await coerceImageParam(value);
8460
8561
  continue;
8461
8562
  }
@@ -8567,7 +8668,9 @@ var hyperframeSnapshotNode = defineNode({
8567
8668
  params: HyperframeSnapshotParams,
8568
8669
  outputs: HyperframeSnapshotOutputs,
8569
8670
  cost: () => ({ credits: 0, seconds_estimate: 3 }),
8570
- validateExtra: ({ rawParams }) => validateCompositionParams(rawParams),
8671
+ // The snapshot's own keys (`wait_for`) are not composition params: checked against the
8672
+ // render node's key set, a canvas that waited on a selector failed validate every time.
8673
+ validateExtra: ({ rawParams }) => validateCompositionParams(rawParams, NODE_OWNED_PARAM_KEYS2),
8571
8674
  cacheKeyExtras: async ({ params }) => {
8572
8675
  const out = {};
8573
8676
  const composition = params.composition;
@@ -8674,18 +8777,18 @@ async function vendorGsap2(tmp, ctx) {
8674
8777
  async function stageInputs3(tmp, inputs, meta, ctx) {
8675
8778
  assertInputsMatchMeta(inputs, meta, "hyperframe_snapshot");
8676
8779
  for (const [key, ref] of Object.entries(inputs ?? {})) {
8677
- const spec = meta.inputs[key];
8678
- if (!spec) continue;
8679
- const filename = spec.staged_as ?? defaultFilenameForInput(key, spec.kind);
8780
+ const spec2 = meta.inputs[key];
8781
+ if (!spec2) continue;
8782
+ const filename = spec2.staged_as ?? defaultFilenameForInput(key, spec2.kind);
8680
8783
  await stageAsset(ref, tmp, filename);
8681
- ctx.log(`staged ${spec.kind} \u2192 ${filename}`);
8784
+ ctx.log(`staged ${spec2.kind} \u2192 ${filename}`);
8682
8785
  }
8683
8786
  }
8684
8787
  async function buildSubstitutionValues2(compositionParams, meta) {
8685
8788
  const out = {};
8686
8789
  for (const [name, value] of Object.entries(compositionParams)) {
8687
- const spec = meta.params[name];
8688
- if (spec?.kind === "image") {
8790
+ const spec2 = meta.params[name];
8791
+ if (spec2?.kind === "image") {
8689
8792
  out[name] = await coerceImageParam2(value);
8690
8793
  continue;
8691
8794
  }
@@ -8918,12 +9021,26 @@ var imageGenerateNode = delegated({
8918
9021
  params: ImageGenerateParams,
8919
9022
  outputs: z17.object({ images: z17.array(ImageRef).min(1) }).strict(),
8920
9023
  outputKinds: { images: "image" },
8921
- cost: () => ({ credits: 5, seconds_estimate: 10 })
9024
+ // An estimate for the pre-flight cap only: the run reports what the backend billed. A
9025
+ // top-quality render measured 8-12 credits on gpt-image-2.5 (2026-09), so it is priced 10.
9026
+ cost: ({ params }) => ({
9027
+ credits: params.quality === "xhigh" || params.quality === "max" ? 10 : 5,
9028
+ seconds_estimate: 10
9029
+ })
8922
9030
  });
8923
9031
 
8924
9032
  // src/engine/nodes/remote/imageAspectAdapt.ts
8925
9033
  import { z as z18 } from "zod";
8926
- var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
9034
+ var ASPECT_ADAPT_MODELS = [
9035
+ "google/gemini-3-pro-image",
9036
+ "google/gemini-3.1-flash-image",
9037
+ "google/gemini-3-pro-image-preview",
9038
+ "google/gemini-3.1-flash-image-preview"
9039
+ ];
9040
+ var PRO_ADAPT_MODELS = /* @__PURE__ */ new Set([
9041
+ "google/gemini-3-pro-image",
9042
+ "google/gemini-3-pro-image-preview"
9043
+ ]);
8927
9044
  var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
8928
9045
  var ImageAspectAdaptParams = z18.object({
8929
9046
  model: z18.enum(ASPECT_ADAPT_MODELS),
@@ -8940,7 +9057,7 @@ var imageAspectAdaptNode = delegated({
8940
9057
  version: "1.0.0",
8941
9058
  category: "image",
8942
9059
  summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. Give it either an explicit `formats` list OR a `platform` preset (meta_feed, meta_feed_stories, meta_all, linkedin, google_demandgen, tiktok, youtube) that expands to that platform's canonical set. 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 the resolved format list.",
8943
- 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 set EITHER `formats` (exact ratios) OR `platform` (e.g. `meta_feed_stories`). Cost is estimated per format; formats matching the source ratio are free pass-throughs. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here \u2014 crop them from the nearest ratio with `baker images crop`. 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.",
9060
+ 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 set EITHER `formats` (exact ratios) OR `platform` (e.g. `meta_feed_stories`). Cost is estimated per format; formats matching the source ratio are free pass-throughs. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here \u2014 crop them from the nearest ratio with `baker images crop`. Pick `google/gemini-3.1-flash-image` (Nano Banana 2) while iterating, `google/gemini-3-pro-image` (Nano Banana Pro) for final-quality adaptation.",
8944
9061
  inputs: z18.object({ source: ImageRef }).loose(),
8945
9062
  params: ImageAspectAdaptParams,
8946
9063
  outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
@@ -8949,7 +9066,7 @@ var imageAspectAdaptNode = delegated({
8949
9066
  const p = params;
8950
9067
  const count = Math.max(1, resolveAdaptFormats(p ?? {}).length);
8951
9068
  return {
8952
- credits: count * (p?.model === "google/gemini-3-pro-image-preview" ? 20 : 5),
9069
+ credits: count * (PRO_ADAPT_MODELS.has(String(p?.model)) ? 20 : 5),
8953
9070
  seconds_estimate: 30 + 30 * count
8954
9071
  };
8955
9072
  }
@@ -9035,7 +9152,16 @@ var imageLayerizeNode = delegated({
9035
9152
 
9036
9153
  // src/engine/nodes/remote/imageReferenceSheet.ts
9037
9154
  import { z as z22 } from "zod";
9038
- var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
9155
+ var REFERENCE_SHEET_MODELS = [
9156
+ "google/gemini-3-pro-image",
9157
+ "google/gemini-3.1-flash-image",
9158
+ "google/gemini-3-pro-image-preview",
9159
+ "google/gemini-3.1-flash-image-preview"
9160
+ ];
9161
+ var PRO_SHEET_MODELS = /* @__PURE__ */ new Set([
9162
+ "google/gemini-3-pro-image",
9163
+ "google/gemini-3-pro-image-preview"
9164
+ ]);
9039
9165
  var ImageReferenceSheetParams = z22.object({
9040
9166
  model: z22.enum(REFERENCE_SHEET_MODELS),
9041
9167
  subject_description: z22.string().min(1),
@@ -9053,13 +9179,13 @@ var imageReferenceSheetNode = delegated({
9053
9179
  version: "1.0.0",
9054
9180
  category: "image",
9055
9181
  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).",
9056
- 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.",
9182
+ 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` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image` while iterating.",
9057
9183
  inputs: z22.object({ references: z22.array(ImageRef).min(1).max(6) }).loose(),
9058
9184
  params: ImageReferenceSheetParams,
9059
9185
  outputs: z22.object({ sheet: ImageRef }).strict(),
9060
9186
  outputKinds: { sheet: "image" },
9061
9187
  cost: ({ params }) => ({
9062
- credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
9188
+ credits: PRO_SHEET_MODELS.has(String(params?.model)) ? 20 : 5,
9063
9189
  seconds_estimate: 30
9064
9190
  })
9065
9191
  });
@@ -9084,24 +9210,103 @@ var imageSearchNode = delegated({
9084
9210
  });
9085
9211
 
9086
9212
  // src/engine/nodes/remote/imageSelect.ts
9213
+ import { readFile as readFile13 } from "fs/promises";
9087
9214
  import { z as z24 } from "zod";
9215
+
9216
+ // src/engine/nodes/local/lib/plate-space.ts
9217
+ import sharp from "sharp";
9218
+ var CELL = 6;
9219
+ var SAMPLE_WIDTH = 384;
9220
+ var CELL_EDGE = 0.02;
9221
+ var BUSY_LIMIT = 0.08;
9222
+ function clampSpan([a, b]) {
9223
+ const lo = Math.max(0, Math.min(100, Math.min(a, b)));
9224
+ const hi = Math.max(0, Math.min(100, Math.max(a, b)));
9225
+ return [lo, hi];
9226
+ }
9227
+ function cellGradient(pixels, width, height, cx, cy) {
9228
+ let sum = 0;
9229
+ let n = 0;
9230
+ for (let y = cy; y < cy + CELL && y + 1 < height; y++) {
9231
+ for (let x = cx; x < cx + CELL && x + 1 < width; x++) {
9232
+ const at = pixels[y * width + x] ?? 0;
9233
+ sum += Math.abs((pixels[y * width + x + 1] ?? at) - at) + Math.abs((pixels[(y + 1) * width + x] ?? at) - at);
9234
+ n += 2;
9235
+ }
9236
+ }
9237
+ return n === 0 ? 0 : sum / n / 255;
9238
+ }
9239
+ function areaBusy(pixels, width, height, area) {
9240
+ const [x0, x1] = clampSpan(area.x_pct).map((p) => Math.round(p / 100 * width));
9241
+ const [y0, y1] = clampSpan(area.y_pct).map((p) => Math.round(p / 100 * height));
9242
+ let cells = 0;
9243
+ let busy = 0;
9244
+ for (let cy = y0; cy + CELL <= y1; cy += CELL) {
9245
+ for (let cx = x0; cx + CELL <= x1; cx += CELL) {
9246
+ cells++;
9247
+ if (cellGradient(pixels, width, height, cx, cy) > CELL_EDGE) busy++;
9248
+ }
9249
+ }
9250
+ return cells === 0 ? 0 : busy / cells;
9251
+ }
9252
+ async function readReservedAreas(image, areas) {
9253
+ if (areas.length === 0) return [];
9254
+ const { data, info } = await sharp(image).greyscale().resize({ width: SAMPLE_WIDTH }).raw().toBuffer({ resolveWithObject: true });
9255
+ const pixels = new Uint8Array(data.buffer, data.byteOffset, info.width * info.height);
9256
+ return areas.map((area) => ({
9257
+ for: area.for ?? "copy",
9258
+ busy: Math.round(areaBusy(pixels, info.width, info.height, area) * 1e3) / 1e3
9259
+ }));
9260
+ }
9261
+ var spanOf = (v) => Array.isArray(v) && v.length === 2 && typeof v[0] === "number" && typeof v[1] === "number" ? [v[0], v[1]] : null;
9262
+ function reservedAreasOf(spec2) {
9263
+ const raw = spec2?.reserved_for_overlay;
9264
+ if (!Array.isArray(raw)) return [];
9265
+ return raw.flatMap((entry) => {
9266
+ const e = entry;
9267
+ const x = spanOf(e.x_pct);
9268
+ const y = spanOf(e.y_pct);
9269
+ return x && y ? [{ for: typeof e.for === "string" ? e.for : "copy", x_pct: x, y_pct: y }] : [];
9270
+ });
9271
+ }
9272
+ function spaceVerdict(readings, limit = BUSY_LIMIT) {
9273
+ const worst = readings.map((areas) => Math.max(0, ...areas.map((a) => a.busy)));
9274
+ const calm = worst.flatMap((w, i) => w <= limit ? [i] : []);
9275
+ const calmest = worst.reduce((best, w, i) => w < (worst[best] ?? Number.POSITIVE_INFINITY) ? i : best, 0);
9276
+ const crowded = readings.flatMap((areas, image) => {
9277
+ const over = areas.filter((a) => a.busy > limit);
9278
+ return over.length > 0 ? [{ image, areas: over }] : [];
9279
+ });
9280
+ return { calm, calmest, crowded };
9281
+ }
9282
+
9283
+ // src/engine/nodes/remote/imageSelect.ts
9088
9284
  var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
9089
- var imageSelectNode = delegated({
9285
+ var ImageSelectParams = z24.object({
9286
+ model: z24.enum(IMAGE_SELECT_MODELS),
9287
+ prompt: z24.string().min(1),
9288
+ count: z24.number().int().min(1).default(1),
9289
+ temperature: z24.number().min(0).max(2).optional(),
9290
+ max_tokens: z24.number().int().positive().optional(),
9291
+ /**
9292
+ * Drop, before the judge, every candidate that filled an area the plate spec
9293
+ * (`inputs.plate_spec`, prompt.plate.json) reserves for the copy. Read off the pixels in
9294
+ * the Runtime: no model call. Never sent to the backend.
9295
+ */
9296
+ keep_reserved_areas: z24.boolean().optional()
9297
+ }).strict();
9298
+ var ImageSelectInputs = z24.object({ images: z24.array(ImageRef).min(2) }).loose();
9299
+ var ImageSelectOutputs = z24.object({ images: z24.array(ImageRef).min(1), reasoning: TextRef, space: JsonRef.optional() }).strict();
9300
+ var spec = {
9090
9301
  id: "image_select",
9091
9302
  version: "1.0.0",
9092
9303
  category: "vision",
9093
9304
  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.",
9094
- 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.",
9095
- inputs: z24.object({ images: z24.array(ImageRef).min(2) }).loose(),
9096
- params: z24.object({
9097
- model: z24.enum(IMAGE_SELECT_MODELS),
9098
- prompt: z24.string().min(1),
9099
- count: z24.number().int().min(1).default(1),
9100
- temperature: z24.number().min(0).max(2).optional(),
9101
- max_tokens: z24.number().int().positive().optional()
9102
- }).strict(),
9103
- outputs: z24.object({ images: z24.array(ImageRef).min(1), reasoning: TextRef }).strict(),
9104
- outputKinds: { images: "image", reasoning: "text" },
9305
+ 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. With `keep_reserved_areas` and a `plate_spec` input, a candidate that put its subject where the copy goes is dropped before the judge (deterministic, free).",
9306
+ inputs: ImageSelectInputs,
9307
+ params: ImageSelectParams,
9308
+ outputs: ImageSelectOutputs,
9309
+ outputKinds: { images: "image", reasoning: "text", space: "json" },
9105
9310
  cost: () => ({ credits: 1, seconds_estimate: 5 }),
9106
9311
  // Arity is only knowable at validate time when `images` is a literal array
9107
9312
  // of refs; a single `$ref:` string to an upstream array output is deferred
@@ -9122,7 +9327,76 @@ var imageSelectNode = delegated({
9122
9327
  }
9123
9328
  return issues;
9124
9329
  }
9125
- });
9330
+ };
9331
+ async function refBytes(ref, ctx) {
9332
+ if (ref.path) return readFile13(ref.path);
9333
+ if (!ref.url) throw new Error("image_select: an input has neither a local path nor a url");
9334
+ const local = await ctx.assets.ingestRemote({
9335
+ kind: ref.kind,
9336
+ url: ref.url,
9337
+ sha256: ref.sha256,
9338
+ mime: ref.mime
9339
+ });
9340
+ return readFile13(local.path ?? "");
9341
+ }
9342
+ async function localText(ctx, body) {
9343
+ const ref = await ctx.assets.ingestBytes({ bytes: Buffer.from(body, "utf8"), kind: "text", mime: "text/plain" });
9344
+ return TextRef.parse(ref);
9345
+ }
9346
+ async function localJson(ctx, value) {
9347
+ const bytes = Buffer.from(JSON.stringify(value), "utf8");
9348
+ return JsonRef.parse(await ctx.assets.ingestBytes({ bytes, kind: "json", mime: "application/json" }));
9349
+ }
9350
+ function judge(inputs, params, ctx) {
9351
+ return callBackendExec({
9352
+ nodeType: spec.id,
9353
+ nodeVersion: spec.version,
9354
+ params,
9355
+ inputs,
9356
+ outputKinds: { images: "image", reasoning: "text" },
9357
+ ctx
9358
+ });
9359
+ }
9360
+ async function selectKeepingSpace(inputs, params, ctx) {
9361
+ const { keep_reserved_areas: _keep, ...backendParams } = params;
9362
+ const specRef = inputs.plate_spec;
9363
+ const parsedSpec = JsonRef.safeParse(specRef);
9364
+ const areas = parsedSpec.success ? reservedAreasOf(JSON.parse((await refBytes(parsedSpec.data, ctx)).toString("utf8"))) : [];
9365
+ if (areas.length === 0) return judge(inputs, backendParams, ctx);
9366
+ const readings = await Promise.all(
9367
+ inputs.images.map(async (image) => readReservedAreas(await refBytes(image, ctx), areas))
9368
+ );
9369
+ const verdict = spaceVerdict(readings);
9370
+ const kept = verdict.calm.length > 0 ? verdict.calm : [verdict.calmest];
9371
+ for (const { image, areas: over } of verdict.crowded) {
9372
+ ctx.log(
9373
+ `[space ] ${ctx.nodeId}: picture ${image + 1} put its subject where the ${over.map((a) => a.for).join(", ")} goes${verdict.calm.length > 0 ? " \u2014 dropped before the judge" : ""}`
9374
+ );
9375
+ }
9376
+ const report = {
9377
+ none_calm: verdict.calm.length === 0,
9378
+ kept: kept.map((i) => i + 1),
9379
+ dropped: verdict.crowded.map((c) => c.image + 1).filter((n) => !kept.includes(n - 1)),
9380
+ crowded: verdict.crowded.map((c) => ({ picture: c.image + 1, areas: c.areas }))
9381
+ };
9382
+ const space = await localJson(ctx, report);
9383
+ if (kept.length < Math.max(2, params.count)) {
9384
+ const chosen = kept.map((i) => inputs.images[i]).filter((i) => i !== void 0);
9385
+ const why = verdict.calm.length === 0 ? `No picture kept the room the copy needs; picture ${(kept[0] ?? 0) + 1} crowds it least.` : `Only picture ${(kept[0] ?? 0) + 1} kept the room the copy needs; the others were dropped before judging.`;
9386
+ return { images: chosen, reasoning: await localText(ctx, why), space };
9387
+ }
9388
+ const subset = { ...inputs, images: kept.map((i) => inputs.images[i]).filter((i) => i !== void 0) };
9389
+ return { ...await judge(subset, backendParams, ctx), space };
9390
+ }
9391
+ var remote = delegated(spec);
9392
+ var imageSelectNode = {
9393
+ ...remote,
9394
+ execute: ({ inputs, params, ctx }) => {
9395
+ if (params.keep_reserved_areas) return selectKeepingSpace(inputs, params, ctx);
9396
+ const { keep_reserved_areas: _keep, ...backendParams } = params;
9397
+ return judge(inputs, backendParams, ctx);
9398
+ }
9399
+ };
9126
9400
 
9127
9401
  // src/engine/nodes/remote/music.ts
9128
9402
  import { z as z25 } from "zod";
@@ -9468,7 +9742,7 @@ var videoLipsyncNode = delegated({
9468
9742
  });
9469
9743
 
9470
9744
  // src/engine/nodes/remote/videoTranscribe.ts
9471
- import { mkdtemp as mkdtemp6, readFile as readFile13, rm as rm6 } from "fs/promises";
9745
+ import { mkdtemp as mkdtemp6, readFile as readFile14, rm as rm6 } from "fs/promises";
9472
9746
  import { tmpdir as tmpdir6 } from "os";
9473
9747
  import path17 from "path";
9474
9748
  import { z as z33 } from "zod";
@@ -9615,7 +9889,7 @@ async function tryExtractAudio(inputs, ctx) {
9615
9889
  ["-i", video.path, "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", "-f", "mp3", "-y", audioPath],
9616
9890
  { timeout_ms: AUDIO_EXTRACT_TIMEOUT_MS }
9617
9891
  );
9618
- const bytes = await readFile13(audioPath);
9892
+ const bytes = await readFile14(audioPath);
9619
9893
  if (bytes.byteLength === 0) {
9620
9894
  ctx.log("video_transcribe: extracted audio is empty, sending full video");
9621
9895
  return null;
@@ -9810,7 +10084,7 @@ function safeCost(def) {
9810
10084
 
9811
10085
  // src/engine/storage/cache-store.ts
9812
10086
  import { randomUUID as randomUUID2 } from "crypto";
9813
- import { mkdir as mkdir3, readFile as readFile14, rename as rename2, writeFile as writeFile8 } from "fs/promises";
10087
+ import { mkdir as mkdir3, readFile as readFile15, rename as rename2, writeFile as writeFile8 } from "fs/promises";
9814
10088
  import path18 from "path";
9815
10089
  var LocalCacheStore = class {
9816
10090
  rootDir;
@@ -9822,7 +10096,7 @@ var LocalCacheStore = class {
9822
10096
  }
9823
10097
  async get(cacheKey) {
9824
10098
  try {
9825
- const buf = await readFile14(this.filePath(cacheKey), "utf8");
10099
+ const buf = await readFile15(this.filePath(cacheKey), "utf8");
9826
10100
  return JSON.parse(buf);
9827
10101
  } catch (e) {
9828
10102
  if (e.code === "ENOENT") return null;
@@ -9920,6 +10194,7 @@ export {
9920
10194
  runFailureCode,
9921
10195
  AD_FORMAT_PLATFORMS,
9922
10196
  platformFormats,
10197
+ resolveAdaptFormats,
9923
10198
  withoutCastName,
9924
10199
  frameRealismDirection,
9925
10200
  SEEDANCE_DURATIONS,
@@ -9946,6 +10221,7 @@ export {
9946
10221
  bestSupportedQuality,
9947
10222
  nearestSupportedAspectRatio,
9948
10223
  nearestSupportedImageSize,
10224
+ CREDITS_PER_USD,
9949
10225
  estimateVideoCredits,
9950
10226
  resolveConcurrency,
9951
10227
  ulid,
@@ -9975,6 +10251,10 @@ export {
9975
10251
  canonicalUrl,
9976
10252
  coercePathUrlToUrl,
9977
10253
  toModelSafeImage,
10254
+ loadCompositionMeta,
10255
+ readReservedAreas,
10256
+ reservedAreasOf,
10257
+ spaceVerdict,
9978
10258
  catalogView,
9979
10259
  BackendClient2,
9980
10260
  Engine2 as Engine,
@@ -9986,4 +10266,4 @@ export {
9986
10266
  defaultRegistry,
9987
10267
  createEngineFromEnv
9988
10268
  };
9989
- //# sourceMappingURL=chunk-FEJQJ64L.js.map
10269
+ //# sourceMappingURL=chunk-Q3LMLQAI.js.map