@koda-sl/baker-cli 0.388.0 → 0.390.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +132 -40
  2. package/canvas/static-ad-composition/index.html +253 -0
  3. package/canvas/static-ad-composition/meta.json +53 -0
  4. package/dist/{chunk-FPJ5FXAX.js → chunk-6UE43HEQ.js} +324 -27
  5. package/dist/chunk-6UE43HEQ.js.map +1 -0
  6. package/dist/{chunk-AOVFDDVW.js → chunk-7OVZ6SOA.js} +3 -2
  7. package/dist/{chunk-AOVFDDVW.js.map → chunk-7OVZ6SOA.js.map} +1 -1
  8. package/dist/{chunk-NEZ3NAJV.js → chunk-EYN7G4KF.js} +4 -4
  9. package/dist/{chunk-T2UNK5BW.js → chunk-J76AGFBH.js} +3 -3
  10. package/dist/{chunk-QIQ57EIA.js → chunk-TWSWUIDI.js} +2 -2
  11. package/dist/{chunk-OKE2QJSJ.js → chunk-Z7WHG6TA.js} +3 -3
  12. package/dist/cli.js +7639 -5247
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-EQOSDYJL.js → client-YUWLIYLF.js} +4 -4
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-ANKM47IJ.js → env-RQGMYIQN.js} +2 -2
  17. package/dist/{output-NR4EJQDV.js → output-POG3LVBW.js} +5 -5
  18. package/dist/{shared-SLCDVV45.js → shared-ZAEUTUNU.js} +6 -6
  19. package/package.json +2 -1
  20. package/dist/chunk-FPJ5FXAX.js.map +0 -1
  21. /package/dist/{chunk-NEZ3NAJV.js.map → chunk-EYN7G4KF.js.map} +0 -0
  22. /package/dist/{chunk-T2UNK5BW.js.map → chunk-J76AGFBH.js.map} +0 -0
  23. /package/dist/{chunk-QIQ57EIA.js.map → chunk-TWSWUIDI.js.map} +0 -0
  24. /package/dist/{chunk-OKE2QJSJ.js.map → chunk-Z7WHG6TA.js.map} +0 -0
  25. /package/dist/{client-EQOSDYJL.js.map → client-YUWLIYLF.js.map} +0 -0
  26. /package/dist/{env-ANKM47IJ.js.map → env-RQGMYIQN.js.map} +0 -0
  27. /package/dist/{output-NR4EJQDV.js.map → output-POG3LVBW.js.map} +0 -0
  28. /package/dist/{shared-SLCDVV45.js.map → shared-ZAEUTUNU.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-QIQ57EIA.js";
4
+ } from "./chunk-TWSWUIDI.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -18,7 +18,7 @@ import {
18
18
  refuseNonPublicUrl,
19
19
  requireConnection,
20
20
  shouldEscalate
21
- } from "./chunk-AOVFDDVW.js";
21
+ } from "./chunk-7OVZ6SOA.js";
22
22
 
23
23
  // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
24
24
  var require_safe_stable_stringify = __commonJS({
@@ -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",
@@ -1536,6 +1541,32 @@ var MODEL_REGISTRY = {
1536
1541
  quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
1537
1542
  }
1538
1543
  },
1544
+ // The GA Nano Banana ids (2026-06-18). Same surface as the preview entries they
1545
+ // replace — read off GET /api/v1/images/models/google/gemini-3.1-flash-image/endpoints
1546
+ // and .../gemini-3-pro-image/endpoints — so a canvas moves between them unchanged.
1547
+ "google/gemini-3.1-flash-image": {
1548
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1549
+ inputs: [],
1550
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1551
+ required: ["prompt"],
1552
+ params: {
1553
+ prompt: { kind: "string" },
1554
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
1555
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1556
+ }
1557
+ },
1558
+ "google/gemini-3-pro-image": {
1559
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1560
+ inputs: [],
1561
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1562
+ required: ["prompt"],
1563
+ params: {
1564
+ prompt: { kind: "string" },
1565
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
1566
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1567
+ }
1568
+ },
1569
+ // LEGACY preview ids — see IMAGE_GENERATE_MODELS.
1539
1570
  "google/gemini-3.1-flash-image-preview": {
1540
1571
  label: "Google Gemini 3.1 Flash Image (Preview)",
1541
1572
  inputs: [],
@@ -1573,7 +1604,7 @@ var MODEL_REGISTRY = {
1573
1604
  }
1574
1605
  },
1575
1606
  "google/gemini-3-pro-image-preview": {
1576
- label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1607
+ label: "Google Gemini 3 Pro Image (Preview)",
1577
1608
  inputs: [],
1578
1609
  optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES, max: GEMINI_IMAGE_MAX_REFERENCES }],
1579
1610
  required: ["prompt"],
@@ -1616,6 +1647,35 @@ var MODEL_REGISTRY = {
1616
1647
  // is baked into the backend handler — params here are semantic levers
1617
1648
  // only. Both models consume MULTIPLE reference images via the `references`
1618
1649
  // input slot (required: a sheet without source images is just image_generate).
1650
+ "google/gemini-3-pro-image": {
1651
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1652
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1653
+ required: ["subject_description", "subject_type"],
1654
+ params: {
1655
+ subject_description: { kind: "string" },
1656
+ subject_type: { kind: "string", enum: ["character", "person", "product", "location"] },
1657
+ views: { kind: "json" },
1658
+ style: { kind: "string" },
1659
+ prompt_override: { kind: "string" },
1660
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
1661
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1662
+ }
1663
+ },
1664
+ "google/gemini-3.1-flash-image": {
1665
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1666
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1667
+ required: ["subject_description", "subject_type"],
1668
+ params: {
1669
+ subject_description: { kind: "string" },
1670
+ subject_type: { kind: "string", enum: ["character", "person", "product", "location"] },
1671
+ views: { kind: "json" },
1672
+ style: { kind: "string" },
1673
+ prompt_override: { kind: "string" },
1674
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
1675
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1676
+ }
1677
+ },
1678
+ // LEGACY preview ids, kept so existing canvases re-run.
1619
1679
  "google/gemini-3-pro-image-preview": {
1620
1680
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1621
1681
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
@@ -1654,6 +1714,29 @@ var MODEL_REGISTRY = {
1654
1714
  // registry can't express "one of" or gate array elements), so `formats`
1655
1715
  // is not listed as registry-required. The source creative is a required
1656
1716
  // input slot.
1717
+ "google/gemini-3-pro-image": {
1718
+ label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1719
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1720
+ required: [],
1721
+ params: {
1722
+ formats: { kind: "json" },
1723
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1724
+ guidance: { kind: "string" },
1725
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1726
+ }
1727
+ },
1728
+ "google/gemini-3.1-flash-image": {
1729
+ label: "Google Gemini 3.1 Flash Image (Nano Banana 2)",
1730
+ inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1731
+ required: [],
1732
+ params: {
1733
+ formats: { kind: "json" },
1734
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1735
+ guidance: { kind: "string" },
1736
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1737
+ }
1738
+ },
1739
+ // LEGACY preview ids, kept so existing canvases re-run.
1657
1740
  "google/gemini-3-pro-image-preview": {
1658
1741
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1659
1742
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
@@ -3133,7 +3216,7 @@ var LocalAssetStore = class {
3133
3216
  );
3134
3217
  const written = await readFile(tmp);
3135
3218
  const computed = sha256Hex(written);
3136
- if (computed !== args.sha256) {
3219
+ if (computed !== args.sha256 && !cdnReencodedImage(res, args.kind)) {
3137
3220
  throw new Error(`asset-store: sha mismatch for ${args.url} (got ${computed}, expected ${args.sha256})`);
3138
3221
  }
3139
3222
  await rename(tmp, dest);
@@ -3154,6 +3237,9 @@ var LocalAssetStore = class {
3154
3237
  });
3155
3238
  }
3156
3239
  };
3240
+ function cdnReencodedImage(res, kind) {
3241
+ return kind === "image" && res.headers.get("cf-polished") !== null;
3242
+ }
3157
3243
  function byteLimit(url, maxBytes) {
3158
3244
  let total = 0;
3159
3245
  return new Transform({
@@ -3571,13 +3657,13 @@ function spineInputFlags(work, lenS) {
3571
3657
  function spineInputOps(work, lenS, dims) {
3572
3658
  const fit = work.still?.fit ?? "fill";
3573
3659
  const scale = fit === "pad" ? `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=decrease,pad=${dims.w}:${dims.h}:(ow-iw)/2:(oh-ih)/2:color=black` : `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=increase,crop=${dims.w}:${dims.h}`;
3574
- const normalize = `${scale},format=yuv420p,fps=30,setsar=1,settb=AVTB`;
3660
+ const normalize2 = `${scale},format=yuv420p,fps=30,setsar=1,settb=AVTB`;
3575
3661
  if (work.trim) {
3576
3662
  const start = work.trim.offset_s > 0 ? `start=${work.trim.offset_s.toFixed(3)}:` : "";
3577
- return `${normalize},trim=${start}duration=${lenS.toFixed(3)},setpts=PTS-STARTPTS`;
3663
+ return `${normalize2},trim=${start}duration=${lenS.toFixed(3)},setpts=PTS-STARTPTS`;
3578
3664
  }
3579
- if (work.still) return `${normalize},trim=duration=${lenS.toFixed(3)}`;
3580
- return normalize;
3665
+ if (work.still) return `${normalize2},trim=duration=${lenS.toFixed(3)}`;
3666
+ return normalize2;
3581
3667
  }
3582
3668
  function parseSpineInputLens(args) {
3583
3669
  const lens = /* @__PURE__ */ new Map();
@@ -7296,10 +7382,10 @@ function applyDuck(params, labelFor, filterChains) {
7296
7382
  }
7297
7383
  const th = duck.threshold ?? 0.03;
7298
7384
  const ra = duck.ratio ?? 8;
7299
- const at = duck.attack ?? 5;
7385
+ const at2 = duck.attack ?? 5;
7300
7386
  const re = duck.release ?? 300;
7301
7387
  filterChains.push(
7302
- `[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${at}:release=${re}[ducked]`
7388
+ `[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${at2}:release=${re}[ducked]`
7303
7389
  );
7304
7390
  labelFor[trackIdx] = "ducked";
7305
7391
  }
@@ -7692,7 +7778,7 @@ async function collectFiles(root, current) {
7692
7778
  import { readFile as readFile9 } from "fs/promises";
7693
7779
  import path12 from "path";
7694
7780
  import { z as z10 } from "zod";
7695
- var InputKind = z10.enum(["video", "image", "audio", "json"]);
7781
+ var InputKind = z10.enum(["video", "image", "audio", "json", "font"]);
7696
7782
  var InputSpec = z10.object({
7697
7783
  kind: InputKind,
7698
7784
  required: z10.boolean().optional().default(false),
@@ -7841,6 +7927,7 @@ function finalize(schema, dflt, required) {
7841
7927
  }
7842
7928
  function defaultFilenameForInput(key, kind) {
7843
7929
  if (kind === "video") return `${key}.mp4`;
7930
+ if (kind === "font") return `${key}.font`;
7844
7931
  if (kind === "audio") return `${key}.mp3`;
7845
7932
  if (kind === "json") return `${key}.json`;
7846
7933
  return `${key}.png`;
@@ -8335,7 +8422,7 @@ async function validateComposition(rawParams) {
8335
8422
  }
8336
8423
  return issues;
8337
8424
  }
8338
- async function validateCompositionParams(rawParams) {
8425
+ async function validateCompositionParams(rawParams, ownedKeys = NODE_OWNED_PARAM_KEYS) {
8339
8426
  const issues = [];
8340
8427
  if (!rawParams || typeof rawParams !== "object") return issues;
8341
8428
  const params = rawParams;
@@ -8357,7 +8444,7 @@ async function validateCompositionParams(rawParams) {
8357
8444
  }
8358
8445
  const dynamic = {};
8359
8446
  for (const [k, v] of Object.entries(params)) {
8360
- if (NODE_OWNED_PARAM_KEYS.has(k)) continue;
8447
+ if (ownedKeys.has(k)) continue;
8361
8448
  dynamic[k] = v;
8362
8449
  }
8363
8450
  for (const key of Object.keys(dynamic)) {
@@ -8567,7 +8654,9 @@ var hyperframeSnapshotNode = defineNode({
8567
8654
  params: HyperframeSnapshotParams,
8568
8655
  outputs: HyperframeSnapshotOutputs,
8569
8656
  cost: () => ({ credits: 0, seconds_estimate: 3 }),
8570
- validateExtra: ({ rawParams }) => validateCompositionParams(rawParams),
8657
+ // The snapshot's own keys (`wait_for`) are not composition params: checked against the
8658
+ // render node's key set, a canvas that waited on a selector failed validate every time.
8659
+ validateExtra: ({ rawParams }) => validateCompositionParams(rawParams, NODE_OWNED_PARAM_KEYS2),
8571
8660
  cacheKeyExtras: async ({ params }) => {
8572
8661
  const out = {};
8573
8662
  const composition = params.composition;
@@ -8923,7 +9012,16 @@ var imageGenerateNode = delegated({
8923
9012
 
8924
9013
  // src/engine/nodes/remote/imageAspectAdapt.ts
8925
9014
  import { z as z18 } from "zod";
8926
- var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
9015
+ var ASPECT_ADAPT_MODELS = [
9016
+ "google/gemini-3-pro-image",
9017
+ "google/gemini-3.1-flash-image",
9018
+ "google/gemini-3-pro-image-preview",
9019
+ "google/gemini-3.1-flash-image-preview"
9020
+ ];
9021
+ var PRO_ADAPT_MODELS = /* @__PURE__ */ new Set([
9022
+ "google/gemini-3-pro-image",
9023
+ "google/gemini-3-pro-image-preview"
9024
+ ]);
8927
9025
  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
9026
  var ImageAspectAdaptParams = z18.object({
8929
9027
  model: z18.enum(ASPECT_ADAPT_MODELS),
@@ -8940,7 +9038,7 @@ var imageAspectAdaptNode = delegated({
8940
9038
  version: "1.0.0",
8941
9039
  category: "image",
8942
9040
  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.",
9041
+ 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
9042
  inputs: z18.object({ source: ImageRef }).loose(),
8945
9043
  params: ImageAspectAdaptParams,
8946
9044
  outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
@@ -8949,7 +9047,7 @@ var imageAspectAdaptNode = delegated({
8949
9047
  const p = params;
8950
9048
  const count = Math.max(1, resolveAdaptFormats(p ?? {}).length);
8951
9049
  return {
8952
- credits: count * (p?.model === "google/gemini-3-pro-image-preview" ? 20 : 5),
9050
+ credits: count * (PRO_ADAPT_MODELS.has(String(p?.model)) ? 20 : 5),
8953
9051
  seconds_estimate: 30 + 30 * count
8954
9052
  };
8955
9053
  }
@@ -9035,7 +9133,16 @@ var imageLayerizeNode = delegated({
9035
9133
 
9036
9134
  // src/engine/nodes/remote/imageReferenceSheet.ts
9037
9135
  import { z as z22 } from "zod";
9038
- var REFERENCE_SHEET_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3.1-flash-image-preview"];
9136
+ var REFERENCE_SHEET_MODELS = [
9137
+ "google/gemini-3-pro-image",
9138
+ "google/gemini-3.1-flash-image",
9139
+ "google/gemini-3-pro-image-preview",
9140
+ "google/gemini-3.1-flash-image-preview"
9141
+ ];
9142
+ var PRO_SHEET_MODELS = /* @__PURE__ */ new Set([
9143
+ "google/gemini-3-pro-image",
9144
+ "google/gemini-3-pro-image-preview"
9145
+ ]);
9039
9146
  var ImageReferenceSheetParams = z22.object({
9040
9147
  model: z22.enum(REFERENCE_SHEET_MODELS),
9041
9148
  subject_description: z22.string().min(1),
@@ -9053,13 +9160,13 @@ var imageReferenceSheetNode = delegated({
9053
9160
  version: "1.0.0",
9054
9161
  category: "image",
9055
9162
  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.",
9163
+ 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
9164
  inputs: z22.object({ references: z22.array(ImageRef).min(1).max(6) }).loose(),
9058
9165
  params: ImageReferenceSheetParams,
9059
9166
  outputs: z22.object({ sheet: ImageRef }).strict(),
9060
9167
  outputKinds: { sheet: "image" },
9061
9168
  cost: ({ params }) => ({
9062
- credits: params?.model === "google/gemini-3-pro-image-preview" ? 20 : 5,
9169
+ credits: PRO_SHEET_MODELS.has(String(params?.model)) ? 20 : 5,
9063
9170
  seconds_estimate: 30
9064
9171
  })
9065
9172
  });
@@ -9549,6 +9656,165 @@ ${detail.slice(-4e3)}`);
9549
9656
  }
9550
9657
  }
9551
9658
 
9659
+ // src/engine/lib/spell-number.ts
9660
+ import { toCardinal as nwAr } from "n2words/ar-SA";
9661
+ import { toCardinal as nwDe } from "n2words/de-DE";
9662
+ import { toCardinal as nwEn } from "n2words/en-US";
9663
+ import { toCardinal as nwEs } from "n2words/es-ES";
9664
+ import { toCardinal as nwFr } from "n2words/fr-FR";
9665
+ import { toCardinal as nwHi } from "n2words/hi-IN";
9666
+ import { toCardinal as nwIt } from "n2words/it-IT";
9667
+ import { toCardinal as nwJa } from "n2words/ja-JP";
9668
+ import { toCardinal as nwKo } from "n2words/ko-KR";
9669
+ import { toCardinal as nwNl } from "n2words/nl-NL";
9670
+ import { toCardinal as nwPl } from "n2words/pl-PL";
9671
+ import { toCardinal as nwPt } from "n2words/pt-PT";
9672
+ var INTEGER_SPELLERS = {
9673
+ fr: nwFr,
9674
+ es: nwEs,
9675
+ en: nwEn,
9676
+ de: nwDe,
9677
+ it: nwIt,
9678
+ pt: nwPt,
9679
+ nl: nwNl,
9680
+ pl: nwPl,
9681
+ ar: nwAr,
9682
+ ja: nwJa,
9683
+ ko: nwKo,
9684
+ hi: nwHi
9685
+ };
9686
+ function canSpellNumbers(langCode) {
9687
+ return langCode in INTEGER_SPELLERS;
9688
+ }
9689
+ function spellNumber(langCode, n) {
9690
+ const spell = langCode ? INTEGER_SPELLERS[langCode] : void 0;
9691
+ if (!spell || !Number.isFinite(n)) return String(n);
9692
+ try {
9693
+ return spell(n);
9694
+ } catch {
9695
+ return String(n);
9696
+ }
9697
+ }
9698
+
9699
+ // src/engine/nodes/local/lib/script-word-timing.ts
9700
+ var UNBOUNDED_WORD_S = 0.3;
9701
+ var EXACT = 3;
9702
+ var NEAR = 2;
9703
+ var SUBSTITUTE = -1;
9704
+ var SKIP = -2;
9705
+ var NEAR_SIMILARITY = 0.7;
9706
+ function normalize(word) {
9707
+ return word.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
9708
+ }
9709
+ function tokensOf(word, index, language) {
9710
+ const key = normalize(word);
9711
+ if (!key) return [];
9712
+ if (language && /^\d{1,9}$/.test(key)) {
9713
+ const spoken = spellNumber(language, Number.parseInt(key, 10));
9714
+ if (spoken !== key) {
9715
+ const parts = spoken.split(/[\s-]+/).map(normalize).filter(Boolean);
9716
+ if (parts.length > 0) return parts.map((part) => ({ key: part, word: index }));
9717
+ }
9718
+ }
9719
+ return [{ key, word: index }];
9720
+ }
9721
+ function similarity(a, b) {
9722
+ if (a === b) return 1;
9723
+ const longest = Math.max(a.length, b.length);
9724
+ if (longest === 0) return 1;
9725
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
9726
+ for (let i = 1; i <= a.length; i++) {
9727
+ const row = [i];
9728
+ for (let j = 1; j <= b.length; j++) {
9729
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
9730
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
9731
+ }
9732
+ prev = row;
9733
+ }
9734
+ return 1 - (prev[b.length] ?? longest) / longest;
9735
+ }
9736
+ function pairScore(a, b) {
9737
+ if (a === b) return EXACT;
9738
+ return similarity(a, b) >= NEAR_SIMILARITY ? NEAR : SUBSTITUTE;
9739
+ }
9740
+ var at = (grid, i, j) => grid[i]?.[j] ?? 0;
9741
+ function diagonal(grid, script, transcript, i, j) {
9742
+ return at(grid, i - 1, j - 1) + pairScore(script[i - 1]?.key ?? "", transcript[j - 1]?.key ?? "");
9743
+ }
9744
+ function scoreGrid(script, transcript) {
9745
+ const grid = Array.from(
9746
+ { length: script.length + 1 },
9747
+ (_, i) => Array.from({ length: transcript.length + 1 }, (_2, j) => i === 0 ? j * SKIP : i * SKIP)
9748
+ );
9749
+ for (let i = 1; i <= script.length; i++) {
9750
+ const row = grid[i];
9751
+ for (let j = 1; j <= transcript.length; j++) {
9752
+ row[j] = Math.max(diagonal(grid, script, transcript, i, j), at(grid, i - 1, j) + SKIP, at(grid, i, j - 1) + SKIP);
9753
+ }
9754
+ }
9755
+ return grid;
9756
+ }
9757
+ function align(script, transcript) {
9758
+ const grid = scoreGrid(script, transcript);
9759
+ const pairs = new Array(script.length).fill(-1);
9760
+ let i = script.length;
9761
+ let j = transcript.length;
9762
+ while (i > 0 && j > 0) {
9763
+ const here = at(grid, i, j);
9764
+ if (here === diagonal(grid, script, transcript, i, j)) {
9765
+ pairs[i - 1] = j - 1;
9766
+ i--;
9767
+ j--;
9768
+ } else if (here === at(grid, i - 1, j) + SKIP) {
9769
+ i--;
9770
+ } else {
9771
+ j--;
9772
+ }
9773
+ }
9774
+ return pairs;
9775
+ }
9776
+ var round = (s) => Math.round(s * 1e3) / 1e3;
9777
+ function pairedSpans(wordCount, scriptTokens, heardTokens, pairs, transcript) {
9778
+ const spans = new Array(wordCount).fill(null);
9779
+ pairs.forEach((heardIndex, t) => {
9780
+ const token = scriptTokens[t];
9781
+ const said = heardIndex >= 0 ? transcript[heardTokens[heardIndex]?.word ?? -1] : void 0;
9782
+ if (!token || !said) return;
9783
+ const current = spans[token.word];
9784
+ spans[token.word] = current ? { start: Math.min(current.start, said.start), end: Math.max(current.end, said.end) } : { start: said.start, end: said.end };
9785
+ });
9786
+ return spans;
9787
+ }
9788
+ function fillRun(spans, first, end) {
9789
+ const before = first > 0 ? spans[first - 1] : null;
9790
+ const after = end < spans.length ? spans[end] : null;
9791
+ const count = end - first;
9792
+ const from = before ? before.end : Math.max(0, (after?.start ?? 0) - count * UNBOUNDED_WORD_S);
9793
+ const to = after ? Math.max(after.start, from) : from + count * UNBOUNDED_WORD_S;
9794
+ const each = (to - from) / count;
9795
+ for (let w = first; w < end; w++) spans[w] = { start: from + (w - first) * each, end: from + (w - first + 1) * each };
9796
+ }
9797
+ function fillDropped(spans) {
9798
+ const filled = [...spans];
9799
+ let i = 0;
9800
+ while (i < filled.length) {
9801
+ let end = i;
9802
+ while (end < filled.length && !filled[end]) end++;
9803
+ if (end > i) fillRun(filled, i, end);
9804
+ i = end + 1;
9805
+ }
9806
+ return filled.map((span) => span ?? { start: 0, end: 0 });
9807
+ }
9808
+ function timeScriptWords(script, transcript, language) {
9809
+ const words = script.split(/\s+/).filter(Boolean);
9810
+ if (words.length === 0 || transcript.length === 0) return [];
9811
+ const scriptTokens = words.flatMap((word, i) => tokensOf(word, i, language));
9812
+ const heardTokens = transcript.flatMap((w, i) => tokensOf(String(w.text ?? ""), i, language));
9813
+ const pairs = align(scriptTokens, heardTokens);
9814
+ const spans = fillDropped(pairedSpans(words.length, scriptTokens, heardTokens, pairs, transcript));
9815
+ return words.map((text, i) => ({ text, start: round(spans[i]?.start ?? 0), end: round(spans[i]?.end ?? 0) }));
9816
+ }
9817
+
9552
9818
  // src/engine/nodes/remote/videoTranscribe.ts
9553
9819
  var VideoTranscribeParams = z33.object({
9554
9820
  language: z33.string().min(2).max(8).optional(),
@@ -9558,6 +9824,9 @@ var VideoTranscribeParams = z33.object({
9558
9824
  transcriber: z33.enum(["groq", "deepgram"]).optional(),
9559
9825
  keyterms: z33.array(z33.string().min(2).max(60)).max(30).optional().describe(
9560
9826
  "Names the recogniser must not mangle \u2014 a product, a person, a phrase the script coins. The COMPANY'S OWN NAME is always applied server-side, so you never list it: this is for the rest. Every term is biased into the request AND its spelling enforced on the way out, because a caption prints a mishearing at full size over the ad (a real cut shipped with the brand name spelled wrong). Never patch a misspelling inside a caption composition \u2014 that fix dies with that one creative."
9827
+ ),
9828
+ script: z33.string().min(1).optional().describe(
9829
+ "The words the track was asked to say, when there is a script: the transcript's words are then the script's, spelled as written, at the times the recogniser heard them. A misheard word stops being a typo on screen, and a word the recogniser added (a filler) is never printed. Aligned locally, never sent."
9561
9830
  )
9562
9831
  }).strict();
9563
9832
  var VideoTranscribeInputs = z33.object({
@@ -9588,16 +9857,40 @@ var videoTranscribeNode = defineNode({
9588
9857
  async execute({ inputs, params, ctx }) {
9589
9858
  const audioInput = await tryExtractAudio(inputs, ctx);
9590
9859
  const effectiveInputs = audioInput ?? inputs;
9591
- return await callBackendExec({
9860
+ const { script, ...recogniserParams } = params;
9861
+ const outputs = await callBackendExec({
9592
9862
  nodeType: "video_transcribe",
9593
9863
  nodeVersion: "2.4.0",
9594
- params,
9864
+ params: recogniserParams,
9595
9865
  inputs: effectiveInputs,
9596
9866
  outputKinds: { transcript: "json", rich: "json" },
9597
9867
  ctx
9598
9868
  });
9869
+ if (!script) return outputs;
9870
+ return { ...outputs, transcript: await scriptTranscript(outputs.transcript, script, params.language, ctx) };
9599
9871
  }
9600
9872
  });
9873
+ async function scriptTranscript(heardRef, script, language, ctx) {
9874
+ const local = heardRef.path ? heardRef : await ctx.assets.ingestRemote({
9875
+ kind: "json",
9876
+ url: String(heardRef.url),
9877
+ sha256: heardRef.sha256,
9878
+ mime: heardRef.mime
9879
+ });
9880
+ const heard = JSON.parse(await readFile13(String(local.path), "utf8"));
9881
+ const words = Array.isArray(heard) ? heard.filter(isTimedWord) : [];
9882
+ const timed = timeScriptWords(script, words, language);
9883
+ ctx.log(`video_transcribe: ${timed.length} script words timed off ${words.length} heard`);
9884
+ const bytes = Buffer.from(JSON.stringify(timed));
9885
+ const metadata = { ...heardRef.metadata, text_of_record: "script", heard_sha256: heardRef.sha256 };
9886
+ return uploadAndIngest({ bytes, kind: "json", mime: "application/json", metadata, ctx }).catch(
9887
+ () => ctx.assets.ingestBytes({ bytes, kind: "json", mime: "application/json", metadata })
9888
+ );
9889
+ }
9890
+ function isTimedWord(value) {
9891
+ const w = value;
9892
+ return Boolean(w) && typeof w?.start === "number" && typeof w?.end === "number" && w?.text !== void 0;
9893
+ }
9601
9894
  async function tryExtractAudio(inputs, ctx) {
9602
9895
  const video = inputs.video;
9603
9896
  if (!video.path || !video.mime?.startsWith("video/")) return null;
@@ -9920,6 +10213,7 @@ export {
9920
10213
  runFailureCode,
9921
10214
  AD_FORMAT_PLATFORMS,
9922
10215
  platformFormats,
10216
+ resolveAdaptFormats,
9923
10217
  withoutCastName,
9924
10218
  frameRealismDirection,
9925
10219
  SEEDANCE_DURATIONS,
@@ -9975,6 +10269,9 @@ export {
9975
10269
  canonicalUrl,
9976
10270
  coercePathUrlToUrl,
9977
10271
  toModelSafeImage,
10272
+ loadCompositionMeta,
10273
+ canSpellNumbers,
10274
+ spellNumber,
9978
10275
  catalogView,
9979
10276
  BackendClient2,
9980
10277
  Engine2 as Engine,
@@ -9986,4 +10283,4 @@ export {
9986
10283
  defaultRegistry,
9987
10284
  createEngineFromEnv
9988
10285
  };
9989
- //# sourceMappingURL=chunk-FPJ5FXAX.js.map
10286
+ //# sourceMappingURL=chunk-6UE43HEQ.js.map