@koda-sl/baker-cli 0.434.0 → 0.435.0-dev.e59c50e4b

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.
@@ -24,9 +24,9 @@ import {
24
24
  shouldEscalate
25
25
  } from "./chunk-F7LXMR73.js";
26
26
 
27
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
27
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
30
30
  "use strict";
31
31
  var { hasOwnProperty } = Object.prototype;
32
32
  var stringify = configure2();
@@ -1408,7 +1408,7 @@ function resolveAdaptFormats(params) {
1408
1408
  return params.formats ?? [];
1409
1409
  }
1410
1410
 
1411
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1411
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
1412
1412
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1413
1413
  var configure = import__.default.configure;
1414
1414
  var wrapper_default = import__.default;
@@ -4398,7 +4398,18 @@ var CanvasMetadata = z4.object({
4398
4398
  // `scaffold-video`) so the editable checklist travels inside the canvas
4399
4399
  // instead of only printing to stdout. Ignored by the engine.
4400
4400
  todo: z4.unknown().optional(),
4401
- video: VideoMeta
4401
+ video: VideoMeta,
4402
+ // A static ad of a series (`seriesMemberAd`): the lead ad its picture is an edit of, the
4403
+ // lead's version, and the lead's words as that version drew them, which this ad's replace.
4404
+ // Ignored by the engine; the brief projection reads it.
4405
+ series: z4.object({
4406
+ lead: z4.string().min(1),
4407
+ run_id: z4.string().min(1),
4408
+ copy: z4.array(z4.object({ text: z4.string(), role: z4.string().optional() }).strict()),
4409
+ design: z4.string().optional(),
4410
+ // A translation of the lead: the language (BCP-47) its words are in.
4411
+ language: z4.string().min(2).optional()
4412
+ }).strict().optional()
4402
4413
  }).strict().optional();
4403
4414
  var CanvasSchema = z4.object({
4404
4415
  schema: z4.literal("baker-canvas/1"),
@@ -4629,7 +4640,7 @@ function dfsCycle(u, color, stack, reverseAdj) {
4629
4640
 
4630
4641
  // src/engine/engine/validator.ts
4631
4642
  import path5 from "path";
4632
- import { z as z8 } from "zod";
4643
+ import { z as z9 } from "zod";
4633
4644
 
4634
4645
  // src/engine/lib/url.ts
4635
4646
  function looksLikeHttpUrl(value) {
@@ -5397,27 +5408,26 @@ function audienceRefs(value) {
5397
5408
  return id ? [{ id, name: text(ref?.name) }] : [];
5398
5409
  });
5399
5410
  }
5400
- var BUYERS = /purchas|buyer|customer|client|compra|cliente|achat|acheteur|kunde|käufer/i;
5401
- var LOOKALIKE_NAME = /lookalike|\blal\b|similar|parecid/i;
5402
5411
  var COLD = { temperature: "cold", knows: [] };
5403
- function tierOf(audience, ref) {
5412
+ function tierOf(audience, ref, readName) {
5404
5413
  const subtype = text(audience?.subtype).toUpperCase();
5405
5414
  const name = text(audience?.name) || ref.name;
5406
- if (subtype === "LOOKALIKE" || !subtype && LOOKALIKE_NAME.test(name)) {
5415
+ const read = readName?.(name);
5416
+ if (subtype === "LOOKALIKE" || !subtype && read?.lookalike) {
5407
5417
  return { ...COLD, who: "a lookalike: strangers who resemble the seed" };
5408
5418
  }
5409
- if (BUYERS.test(name)) return { temperature: "hot", knows: ["brand", "offer", "product"], who: "buyers" };
5419
+ if (read?.buyers) return { temperature: "hot", knows: ["brand", "offer", "product"], who: "buyers" };
5410
5420
  if (subtype === "WEBSITE") return { temperature: "warm", knows: ["brand", "offer"], who: "site visitors" };
5411
5421
  if (subtype === "CUSTOM") {
5412
5422
  return { temperature: "hot", knows: ["brand", "offer", "product"], who: "an uploaded customer list" };
5413
5423
  }
5414
5424
  return { temperature: "warm", knows: ["brand"], who: "people who engaged with the brand" };
5415
5425
  }
5416
- function classifyAudience(adSet, audiences, campaign) {
5426
+ function classifyAudience(adSet, audiences, campaign, readName) {
5417
5427
  const targeting = asRecord(adSet.targeting) ?? {};
5418
5428
  const byId = new Map(audiences.map((audience) => [text(audience.id), audience]));
5419
5429
  const includedRefs = audienceRefs(targeting.custom_audiences);
5420
- const included = includedRefs.map((ref) => tierOf(byId.get(ref.id), ref));
5430
+ const included = includedRefs.map((ref) => tierOf(byId.get(ref.id), ref, readName));
5421
5431
  const reasons = [
5422
5432
  includedRefs.length === 0 ? "No custom audience: Meta finds new people from age, place and interests, so every viewer is a stranger." : `Reaches ${includedRefs.map((ref, i) => `"${ref.name || ref.id}" (${included[i]?.who})`).join(", ")}.`
5423
5433
  ];
@@ -5434,7 +5444,7 @@ function classifyAudience(adSet, audiences, campaign) {
5434
5444
  }
5435
5445
  const excludedRefs = audienceRefs(targeting.excluded_custom_audiences);
5436
5446
  if (excludedRefs.length > 0) reasons.push(`Keeps out ${quoteNames(excludedRefs)}.`);
5437
- if (excludedRefs.some((ref) => tierOf(byId.get(ref.id), ref).temperature === "hot")) {
5447
+ if (excludedRefs.some((ref) => tierOf(byId.get(ref.id), ref, readName).temperature === "hot")) {
5438
5448
  reached = belowHot(reached);
5439
5449
  reasons.push("Buyers are excluded, so nobody reached has used the product.");
5440
5450
  }
@@ -5538,7 +5548,7 @@ function viewerFromKnowledge(input) {
5538
5548
  }
5539
5549
 
5540
5550
  // src/engine/scaffold/static-ad-spec.ts
5541
- import { z as z7 } from "zod";
5551
+ import { z as z8 } from "zod";
5542
5552
 
5543
5553
  // src/engine/scaffold/lib/elements.ts
5544
5554
  var ACTOR_SHEET_MODEL = REFERENCE_SHEET_MODEL;
@@ -5626,7 +5636,72 @@ function staticElementTodo(el, t, label, brief) {
5626
5636
  }
5627
5637
 
5628
5638
  // src/engine/scaffold/native-surface.ts
5639
+ import { z as z6 } from "zod";
5640
+
5641
+ // src/engine/nodes/local/lib/jev.ts
5629
5642
  import { z as z5 } from "zod";
5643
+ var JEV_MODEL = "typesafe/jev-1.13";
5644
+ var ENDPOINT = "https://openrouter.ai/api/v1/systemone";
5645
+ var TIMEOUT_MS = 2500;
5646
+ var Reply = z5.object({
5647
+ answers: z5.record(z5.string(), z5.object({ noul: z5.number() })),
5648
+ usage: z5.object({ cost: z5.number() }).optional()
5649
+ });
5650
+ var Answer = z5.union([
5651
+ z5.object({ choice: z5.string(), probabilities: z5.record(z5.string(), z5.number()), confidence: z5.number() }),
5652
+ z5.object({ noul: z5.number() })
5653
+ ]);
5654
+ var AnswersReply = z5.object({
5655
+ answers: z5.record(z5.string(), z5.unknown()),
5656
+ usage: z5.object({ cost: z5.number() }).optional()
5657
+ });
5658
+ function readAnswers(body, ids) {
5659
+ const parsed = AnswersReply.safeParse(body);
5660
+ if (!parsed.success) return null;
5661
+ const answers = [];
5662
+ for (const id of ids) {
5663
+ const answer = Answer.safeParse(parsed.data.answers[id]);
5664
+ if (!answer.success) return null;
5665
+ answers.push(
5666
+ "choice" in answer.data ? { choice: answer.data.choice, probabilities: answer.data.probabilities, confidence: answer.data.confidence } : { noul: answer.data.noul }
5667
+ );
5668
+ }
5669
+ return { answers, costUsd: parsed.data.usage?.cost ?? 0 };
5670
+ }
5671
+ async function askJevQuestions(questions, onSpend) {
5672
+ const key = openRouterKey();
5673
+ if (questions.length === 0) return [];
5674
+ if (!key) return null;
5675
+ const ids = questions.map((_, i) => `q${i}`);
5676
+ try {
5677
+ const response = await fetch(ENDPOINT, {
5678
+ method: "POST",
5679
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
5680
+ body: JSON.stringify({
5681
+ model: JEV_MODEL,
5682
+ state: {},
5683
+ questions: Object.fromEntries(questions.map((q, i) => [ids[i], q]))
5684
+ }),
5685
+ signal: AbortSignal.timeout(TIMEOUT_MS)
5686
+ });
5687
+ if (!response.ok) return null;
5688
+ const read = readAnswers(await response.json(), ids);
5689
+ if (!read) return null;
5690
+ onSpend?.({ model: JEV_MODEL, inputTokens: 0, outputTokens: 0, costUsd: read.costUsd });
5691
+ return read.answers;
5692
+ } catch {
5693
+ return null;
5694
+ }
5695
+ }
5696
+ async function askJev(questions, onSpend) {
5697
+ const answers = await askJevQuestions(
5698
+ questions.map((q) => ({ type: "noul", instructions: q.instructions })),
5699
+ onSpend
5700
+ );
5701
+ if (!answers) return null;
5702
+ const nouls = answers.flatMap((a) => "noul" in a ? [a.noul] : []);
5703
+ return nouls.length === answers.length ? nouls : null;
5704
+ }
5630
5705
 
5631
5706
  // src/engine/scaffold/review-platforms.ts
5632
5707
  var REVIEW_PLATFORMS = [
@@ -5641,24 +5716,40 @@ var REVIEW_PLATFORMS = [
5641
5716
  { name: "Google", domain: "google.com", pattern: /\bgoogle\b/i, reviewOnly: false },
5642
5717
  { name: "the App Store", domain: "apple.com", pattern: /\bapp ?store\b/i, reviewOnly: false }
5643
5718
  ];
5644
- var RATING_WORDS = /★|\b(?:reviews?|reseñas?|rese[ñn]as?|ratings?|rated|valoraci[oó]n(?:es)?|opiniones|estrellas|stars?|avis|bewertung(?:en)?|verificad[ao]s?|verified|recensioni|avalia[cç][oõ]es)\b|\d[.,]\d\s*(?:\/\s*5|★)/i;
5719
+ var RATING_SIGN = /[★☆⭐]|(?<![\d.,])[1-5][.,]\d(?![\d]|[.,]\d)/u;
5645
5720
  function namedPlatform(text2) {
5646
5721
  return REVIEW_PLATFORMS.find((p) => p.pattern.test(text2)) ?? null;
5647
5722
  }
5648
5723
  function claimedPlatform(text2) {
5649
- return REVIEW_PLATFORMS.find((p) => p.pattern.test(text2) && (p.reviewOnly || RATING_WORDS.test(text2))) ?? null;
5724
+ const platform = namedPlatform(text2);
5725
+ return platform && (platform.reviewOnly || RATING_SIGN.test(text2)) ? platform : null;
5726
+ }
5727
+ var CITES_PLATFORM = 0.5;
5728
+ var CITATION_QUESTION = "Does `line` cite `platform` as the source of a rating or of customers' reviews (stars, a score, reviews or opinions on it), in whatever language it is written? Only naming the platform (download it on, find us on, search on, sign in with) is not citing it for reviews.";
5729
+ function jevCitationJudge(onSpend) {
5730
+ return (items) => askJev(
5731
+ items.map((i) => ({ instructions: { line: i.line, platform: i.platform, question: CITATION_QUESTION } })),
5732
+ onSpend
5733
+ );
5650
5734
  }
5651
- function ratingFigures(text2) {
5652
- const out = /* @__PURE__ */ new Set();
5653
- for (const m of text2.matchAll(/(\d[.,]\d)\s*(?:\/\s*5|★|\s?(?:stars?|estrellas|étoiles|sterne))/gi))
5654
- if (m[1]) out.add(m[1]);
5655
- for (const m of text2.matchAll(/★\s*(\d[.,]\d)/g)) if (m[1]) out.add(m[1]);
5656
- for (const m of text2.matchAll(
5657
- /\+?\s?(\d{1,3}(?:[.,]\d{3})+|\d{2,})\s+(?:reviews|reseñas|opiniones|valoraciones|avis|bewertungen|avaliações)/gi
5658
- )) {
5659
- if (m[1]) out.add(m[1]);
5660
- }
5661
- return [...out];
5735
+ async function citedPlatforms(lines, judge = jevCitationJudge()) {
5736
+ const cited = /* @__PURE__ */ new Map();
5737
+ const asked = [];
5738
+ for (const line of new Set(lines)) {
5739
+ const shown = claimedPlatform(line);
5740
+ const platform = namedPlatform(line);
5741
+ if (shown) cited.set(line, shown);
5742
+ else if (platform) asked.push({ line, platform });
5743
+ }
5744
+ if (asked.length === 0) return cited;
5745
+ const judged = await judge(asked.map((a) => ({ line: a.line, platform: a.platform.name }))).catch(() => null);
5746
+ asked.forEach((a, k) => {
5747
+ if ((judged?.[k] ?? 0) >= CITES_PLATFORM) cited.set(a.line, a.platform);
5748
+ });
5749
+ return cited;
5750
+ }
5751
+ function citedBy(line, cited) {
5752
+ return cited ? cited.get(line) ?? null : claimedPlatform(line);
5662
5753
  }
5663
5754
  function figureForms(figure) {
5664
5755
  const forms = [figure, figure.replace(/,/g, "."), figure.replace(/\./g, ",")];
@@ -5667,128 +5758,128 @@ function figureForms(figure) {
5667
5758
  }
5668
5759
 
5669
5760
  // src/engine/scaffold/native-surface.ts
5670
- var Hex = z5.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
5671
- var PhotoId = z5.string().regex(/^PHOTO_\d+$/, "a photo id is PHOTO_1, PHOTO_2\u2026");
5672
- var Author = z5.object({
5673
- name: z5.string().trim().min(1),
5761
+ var Hex = z6.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
5762
+ var PhotoId = z6.string().regex(/^PHOTO_\d+$/, "a photo id is PHOTO_1, PHOTO_2\u2026");
5763
+ var Author = z6.object({
5764
+ name: z6.string().trim().min(1),
5674
5765
  /** Their handle with the @, as the platform prints it: a real one of ours, never an invented one. */
5675
- handle: z5.string().trim().min(2).optional(),
5676
- avatar: z5.union([z5.literal("logo"), z5.literal("initials"), PhotoId]).default("initials"),
5677
- verified: z5.boolean().optional(),
5766
+ handle: z6.string().trim().min(2).optional(),
5767
+ avatar: z6.union([z6.literal("logo"), z6.literal("initials"), PhotoId]).default("initials"),
5768
+ verified: z6.boolean().optional(),
5678
5769
  /** A line under the name: a review site's "3 reviews · ES", a chat's "online". */
5679
- subtitle: z5.string().trim().min(1).optional()
5770
+ subtitle: z6.string().trim().min(1).optional()
5680
5771
  }).strict();
5681
- var Photo = z5.object({
5772
+ var Photo = z6.object({
5682
5773
  id: PhotoId,
5683
5774
  /** What the photo shows, in plain words: who, doing what, where. */
5684
- what: z5.string().trim().min(1),
5775
+ what: z6.string().trim().min(1),
5685
5776
  /** Its shape on the screen. */
5686
- ratio: z5.enum(["1:1", "4:3", "3:4", "16:9", "4:5"]).default("4:3"),
5687
- look: z5.enum(["photo", "illustration"]).default("photo"),
5777
+ ratio: z6.enum(["1:1", "4:3", "3:4", "16:9", "4:5"]).default("4:3"),
5778
+ look: z6.enum(["photo", "illustration"]).default("photo"),
5688
5779
  /**
5689
5780
  * A picture of ours (a path from the creative's folder, or a URL): used as it is, nothing
5690
5781
  * rendered. There is no `avatar`: nothing grounds a rendered photo on an avatar's sheet, so a
5691
5782
  * photo naming one would still render a made-up face (the strict schema refuses the key).
5692
5783
  */
5693
- image: z5.string().trim().min(1).optional()
5784
+ image: z6.string().trim().min(1).optional()
5694
5785
  }).strict();
5695
- var XPost = z5.object({
5786
+ var XPost = z6.object({
5696
5787
  author: Author,
5697
- text: z5.string().trim().min(1),
5788
+ text: z6.string().trim().min(1),
5698
5789
  photo: PhotoId.optional(),
5699
5790
  /** The handle this post answers, printed above it as X does ("Replying to @alexbuilds"). */
5700
- reply_to: z5.string().trim().min(2).optional(),
5791
+ reply_to: z6.string().trim().min(2).optional(),
5701
5792
  /** How long ago, as X prints it: 2h, 14m, Sep 3. */
5702
- time: z5.string().trim().min(1).optional(),
5703
- stats: z5.object({
5704
- replies: z5.string().optional(),
5705
- reposts: z5.string().optional(),
5706
- likes: z5.string().optional(),
5707
- views: z5.string().optional()
5793
+ time: z6.string().trim().min(1).optional(),
5794
+ stats: z6.object({
5795
+ replies: z6.string().optional(),
5796
+ reposts: z6.string().optional(),
5797
+ likes: z6.string().optional(),
5798
+ views: z6.string().optional()
5708
5799
  }).strict().optional()
5709
5800
  }).strict();
5710
- var ChatCard = z5.object({
5801
+ var ChatCard = z6.object({
5711
5802
  photo: PhotoId.optional(),
5712
- title: z5.string().trim().min(1),
5713
- subtitle: z5.string().trim().min(1).optional(),
5803
+ title: z6.string().trim().min(1),
5804
+ subtitle: z6.string().trim().min(1).optional(),
5714
5805
  /** The card's button, as the chat prints it ("Pagar $899 MXN"). */
5715
- button: z5.string().trim().min(1).optional()
5806
+ button: z6.string().trim().min(1).optional()
5716
5807
  }).strict();
5717
- var ChatMessage = z5.object({
5808
+ var ChatMessage = z6.object({
5718
5809
  /** `them` is the other side of the chat (the contact in the header), `me` the phone's owner. */
5719
- from: z5.enum(["them", "me"]),
5720
- text: z5.string().trim().min(1).optional(),
5721
- time: z5.string().trim().min(1).optional(),
5810
+ from: z6.enum(["them", "me"]),
5811
+ text: z6.string().trim().min(1).optional(),
5812
+ time: z6.string().trim().min(1).optional(),
5722
5813
  card: ChatCard.optional()
5723
5814
  }).strict().refine((m) => m.text || m.card, "a message has text or a card");
5724
- var Review = z5.object({
5815
+ var Review = z6.object({
5725
5816
  author: Author,
5726
- stars: z5.number().int().min(1).max(5).default(5),
5727
- title: z5.string().trim().min(1).optional(),
5728
- text: z5.string().trim().min(1),
5729
- date: z5.string().trim().min(1).optional()
5817
+ stars: z6.number().int().min(1).max(5).default(5),
5818
+ title: z6.string().trim().min(1).optional(),
5819
+ text: z6.string().trim().min(1),
5820
+ date: z6.string().trim().min(1).optional()
5730
5821
  }).strict();
5731
- var NotesItem = z5.object({ text: z5.string().trim().min(1), checked: z5.boolean().default(false) }).strict();
5732
- var SlackMessage = z5.object({
5822
+ var NotesItem = z6.object({ text: z6.string().trim().min(1), checked: z6.boolean().default(false) }).strict();
5823
+ var SlackMessage = z6.object({
5733
5824
  author: Author,
5734
- text: z5.string().trim().min(1),
5735
- time: z5.string().trim().min(1).optional(),
5736
- reactions: z5.array(z5.object({ emoji: z5.string().trim().min(1), count: z5.number().int().min(1) }).strict()).default([]),
5825
+ text: z6.string().trim().min(1),
5826
+ time: z6.string().trim().min(1).optional(),
5827
+ reactions: z6.array(z6.object({ emoji: z6.string().trim().min(1), count: z6.number().int().min(1) }).strict()).default([]),
5737
5828
  /** "3 replies" under the message. */
5738
- replies: z5.string().trim().min(1).optional()
5829
+ replies: z6.string().trim().min(1).optional()
5739
5830
  }).strict();
5740
- var Frame = z5.object({
5741
- headline: z5.string().trim().min(1).optional(),
5742
- caption: z5.string().trim().min(1).optional(),
5743
- logo: z5.boolean().default(false),
5831
+ var Frame = z6.object({
5832
+ headline: z6.string().trim().min(1).optional(),
5833
+ caption: z6.string().trim().min(1).optional(),
5834
+ logo: z6.boolean().default(false),
5744
5835
  /** Where the frame's logo goes: under the screen, or over it as a badge (a review wall's anchor). */
5745
- logo_position: z5.enum(["under", "over"]).default("under"),
5836
+ logo_position: z6.enum(["under", "over"]).default("under"),
5746
5837
  background: Hex.optional()
5747
5838
  }).strict().default({ logo: false, logo_position: "under" });
5748
- var Base = { photos: z5.array(Photo).default([]), frame: Frame };
5749
- var NativeSurfaceSchema = z5.discriminatedUnion("surface", [
5750
- z5.object({
5751
- surface: z5.literal("x_post"),
5752
- theme: z5.enum(["light", "dark"]).default("light"),
5839
+ var Base = { photos: z6.array(Photo).default([]), frame: Frame };
5840
+ var NativeSurfaceSchema = z6.discriminatedUnion("surface", [
5841
+ z6.object({
5842
+ surface: z6.literal("x_post"),
5843
+ theme: z6.enum(["light", "dark"]).default("light"),
5753
5844
  /** One post, or a thread: each after the first a reply under it, its line joining them. */
5754
- posts: z5.array(XPost).min(1).max(4),
5845
+ posts: z6.array(XPost).min(1).max(4),
5755
5846
  ...Base
5756
5847
  }).strict(),
5757
- z5.object({
5758
- surface: z5.enum(["whatsapp", "imessage"]),
5848
+ z6.object({
5849
+ surface: z6.enum(["whatsapp", "imessage"]),
5759
5850
  /** Who the chat is with: the header's name and picture. */
5760
5851
  contact: Author,
5761
- messages: z5.array(ChatMessage).min(1).max(8),
5852
+ messages: z6.array(ChatMessage).min(1).max(8),
5762
5853
  /** The day pill above the messages ("Hoy", "Today"). */
5763
- day: z5.string().trim().min(1).optional(),
5854
+ day: z6.string().trim().min(1).optional(),
5764
5855
  /** Drawn inside a phone; else the screen alone. Default true. */
5765
- device: z5.boolean().default(true),
5856
+ device: z6.boolean().default(true),
5766
5857
  ...Base
5767
5858
  }).strict(),
5768
- z5.object({
5769
- surface: z5.enum(["trustpilot", "google_reviews"]),
5859
+ z6.object({
5860
+ surface: z6.enum(["trustpilot", "google_reviews"]),
5770
5861
  /** The rating the platform shows for the company: a real one, in `src/content`. */
5771
- summary: z5.object({ score: z5.string().trim().min(1), count: z5.string().trim().min(1), label: z5.string().optional() }).strict().optional(),
5862
+ summary: z6.object({ score: z6.string().trim().min(1), count: z6.string().trim().min(1), label: z6.string().optional() }).strict().optional(),
5772
5863
  /**
5773
5864
  * Every real review the wall shows, the strongest first: the first two are read as the ad's
5774
5865
  * copy, whole in the safe area; the rest are the wall they lie on, as large and as sharp, across
5775
5866
  * the whole frame and cut by its edges. A wall wins on how many there are (8 to 16), each one a
5776
5867
  * real review in the company's own words.
5777
5868
  */
5778
- reviews: z5.array(Review).min(1).max(16),
5869
+ reviews: z6.array(Review).min(1).max(16),
5779
5870
  ...Base
5780
5871
  }).strict(),
5781
- z5.object({
5782
- surface: z5.literal("notes"),
5783
- title: z5.string().trim().min(1),
5784
- items: z5.array(NotesItem).default([]),
5785
- body: z5.string().trim().min(1).optional(),
5872
+ z6.object({
5873
+ surface: z6.literal("notes"),
5874
+ title: z6.string().trim().min(1),
5875
+ items: z6.array(NotesItem).default([]),
5876
+ body: z6.string().trim().min(1).optional(),
5786
5877
  ...Base
5787
5878
  }).strict(),
5788
- z5.object({
5789
- surface: z5.literal("slack"),
5790
- channel: z5.string().trim().min(1),
5791
- messages: z5.array(SlackMessage).min(1).max(6),
5879
+ z6.object({
5880
+ surface: z6.literal("slack"),
5881
+ channel: z6.string().trim().min(1),
5882
+ messages: z6.array(SlackMessage).min(1).max(6),
5792
5883
  ...Base
5793
5884
  }).strict()
5794
5885
  ]);
@@ -5850,8 +5941,8 @@ function authorsOf(surface) {
5850
5941
  return [];
5851
5942
  }
5852
5943
  }
5853
- function nativeChrome(surface, language) {
5854
- const t = chromeWords(language);
5944
+ function nativeChrome(surface, language, words) {
5945
+ const t = chromeWords(language, words);
5855
5946
  const handleParts = (h) => h ? [h, h.replace(/^@/, ""), ...h.replace(/^@/, "").split(/[_.]/)] : [];
5856
5947
  const authors = authorsOf(surface).flatMap((a) => [a.name, ...handleParts(a.handle), a.subtitle, initials(a.name)]);
5857
5948
  const own = (() => {
@@ -5931,7 +6022,25 @@ function nativeGateProblems(surface, records, brandName) {
5931
6022
  }
5932
6023
  return problems;
5933
6024
  }
5934
- var WORDS = {
6025
+ var WORD_MEANINGS = {
6026
+ today: "the day label over today's messages in a chat",
6027
+ online: "the status under a chat contact's name",
6028
+ business: "the label under a WhatsApp business contact's name",
6029
+ message: "the placeholder in a chat's message bar",
6030
+ verified: "the badge on a verified review",
6031
+ excellent: "the word a review site prints beside a 4.5-5 star score",
6032
+ reviewsWord: "the plural noun after the review count (1,200 reviews)",
6033
+ notes: "the title of the phone's notes app",
6034
+ experience: "the label before the date a reviewer had the experience",
6035
+ replying: "the words before the handle a post on X replies to",
6036
+ post: "the title over a thread on X",
6037
+ replyPrompt: "the placeholder of the reply bar under a thread on X",
6038
+ messagePrompt: "the placeholder of Slack's message bar, before the channel name"
6039
+ };
6040
+ var InterfaceWordsSchema = z6.object(
6041
+ Object.fromEntries(Object.keys(WORD_MEANINGS).map((k) => [k, z6.string().trim().min(1)]))
6042
+ );
6043
+ var CHECKED = {
5935
6044
  es: {
5936
6045
  today: "Hoy",
5937
6046
  online: "en l\xEDnea",
@@ -5978,9 +6087,29 @@ var WORDS = {
5978
6087
  messagePrompt: "Message"
5979
6088
  }
5980
6089
  };
5981
- function chromeWords(language) {
5982
- const lang = (language ?? "en").slice(0, 2).toLowerCase();
5983
- return WORDS[lang] ?? WORDS.en;
6090
+ var primary = (language) => (language ?? "en").split(/[-_]/)[0]?.toLowerCase() ?? "en";
6091
+ function needsInterfaceWords(language) {
6092
+ return language !== void 0 && CHECKED[primary(language)] === void 0;
6093
+ }
6094
+ var INTERFACE_WORDS_PROMPT_VERSION = 1;
6095
+ function interfaceWordsPrompt(language) {
6096
+ const english = CHECKED.en;
6097
+ return [
6098
+ `These are the words WhatsApp, iMessage, X, Slack, Trustpilot, Google reviews and the phone's notes app print around what people write, in English. Give each as those apps print it in their interface for people whose language is ${language}: the apps' own wording, short, in that language's script and capitalisation.`,
6099
+ "",
6100
+ ...Object.entries(WORD_MEANINGS).map(
6101
+ ([key, meaning]) => `- "${key}": ${JSON.stringify(english[key])} (${meaning})`
6102
+ ),
6103
+ "",
6104
+ `Reply with only this JSON, every key above with its words: {${Object.keys(WORD_MEANINGS).map((k) => `"${k}": "\u2026"`).join(", ")}}`
6105
+ ].join("\n");
6106
+ }
6107
+ function readInterfaceWords(answer) {
6108
+ const parsed = InterfaceWordsSchema.safeParse(answer);
6109
+ return parsed.success ? parsed.data : null;
6110
+ }
6111
+ function chromeWords(language, words) {
6112
+ return CHECKED[primary(language)] ?? words ?? CHECKED.en;
5984
6113
  }
5985
6114
  function inked(assets, src, groundIsDark) {
5986
6115
  return src && !groundIsDark && assets.lightLogos?.includes(src) ? " inked" : "";
@@ -6521,7 +6650,7 @@ function frameGround(surface, brand, size2) {
6521
6650
  function nativeSurfaceHtml(surface, assets, opts) {
6522
6651
  const w = opts.width / 2;
6523
6652
  const h = opts.height / 2;
6524
- const t = chromeWords(opts.language);
6653
+ const t = chromeWords(opts.language, opts.words);
6525
6654
  const bg = backdrop(surface, opts.brand);
6526
6655
  const ink = isDark(bg) ? "#ffffff" : opts.brand.ink ?? "#111111";
6527
6656
  const font = assets.font ? `@font-face{font-family:BrandFont;src:url(${assets.font});font-display:block}` : "";
@@ -6629,7 +6758,7 @@ function layoutBox(safe, w, h) {
6629
6758
  }
6630
6759
 
6631
6760
  // src/engine/scaffold/static-copy.ts
6632
- import { z as z6 } from "zod";
6761
+ import { z as z7 } from "zod";
6633
6762
  var COPY_DIRECTIVE_HEADER = "TEXT \u2014 RENDER THE COPY EXACTLY:";
6634
6763
  var normalize = (s) => typeof s === "string" ? s.trim().toLowerCase() : "";
6635
6764
  function textEntries(blueprint) {
@@ -6671,7 +6800,7 @@ function conceptHasLogo(blueprint) {
6671
6800
  (l) => normalize(l?.function_in_image) === "advertiser_brand"
6672
6801
  );
6673
6802
  }
6674
- var WinningMechanismSchema = z6.object({ kind: z6.string(), device: z6.string(), why_it_works: z6.string().optional() }).loose();
6803
+ var WinningMechanismSchema = z7.object({ kind: z7.string(), device: z7.string(), why_it_works: z7.string().optional() }).loose();
6675
6804
  function winningMechanisms(blueprint) {
6676
6805
  const raw = blueprint?.winning_mechanisms;
6677
6806
  if (!Array.isArray(raw)) return [];
@@ -6707,58 +6836,58 @@ ${directive}${tail2}`;
6707
6836
  // src/engine/scaffold/static-ad-spec.ts
6708
6837
  var AD_SPEC_SCHEMA = "baker-static-ad/3";
6709
6838
  var AD_SPEC_SCHEMAS = ["baker-static-ad/2", AD_SPEC_SCHEMA];
6710
- var Hex2 = z7.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
6711
- var AdCopyLine = z7.object({
6712
- text: z7.string().trim().min(1),
6713
- lettering: z7.enum(["in_scene", "designed"]),
6839
+ var Hex2 = z8.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
6840
+ var AdCopyLine = z8.object({
6841
+ text: z8.string().trim().min(1),
6842
+ lettering: z8.enum(["in_scene", "designed"]),
6714
6843
  /** headline | body | cta | caption | fine_print | label. */
6715
- role: z7.string().optional(),
6844
+ role: z8.string().optional(),
6716
6845
  /** In-scene lines: the object it is written on (a whiteboard, a phone's notes app, a sticky note). */
6717
- on: z7.string().optional(),
6846
+ on: z8.string().optional(),
6718
6847
  /** How it looks: an in-scene line's own lettering (chunky navy marker), a designed line's size or treatment. */
6719
- look: z7.string().optional(),
6848
+ look: z8.string().optional(),
6720
6849
  /** Where it sits, by what is around it ("top of the board", "under the headline"). */
6721
- position: z7.string().optional()
6850
+ position: z8.string().optional()
6722
6851
  }).strict();
6723
6852
  var AD_REFERENCE_ROLES = ["identity", "product", "prop"];
6724
- var AdReference = z7.object({
6853
+ var AdReference = z8.object({
6725
6854
  /** A role-based label, never a brand's: PERSON_1, ANIMAL_1, PRODUCT_1, BADGE_1, PROP_1. */
6726
- id: z7.string().regex(
6855
+ id: z8.string().regex(
6727
6856
  /^(PERSON|ANIMAL|PRODUCT|BADGE|PROP)_\d+$/,
6728
6857
  "a reference id is PERSON_1, ANIMAL_1, PRODUCT_1, BADGE_1 or PROP_1"
6729
6858
  ),
6730
- role: z7.enum(AD_REFERENCE_ROLES),
6859
+ role: z8.enum(AD_REFERENCE_ROLES),
6731
6860
  /** The canvas ingest node that holds its picture (bookkeeping, never sent to a model). */
6732
- slot: z7.string().optional(),
6861
+ slot: z8.string().optional(),
6733
6862
  /** How it appears in this ad. */
6734
- what: z7.string().trim().min(1),
6863
+ what: z8.string().trim().min(1),
6735
6864
  /** What must stay exactly as in the picture, when more than the default for its role. */
6736
- keep: z7.string().optional(),
6865
+ keep: z8.string().optional(),
6737
6866
  /** The company avatar cast in this role (`baker avatars list`): a licensed face, never an invented one. */
6738
- avatar: z7.string().optional(),
6867
+ avatar: z8.string().optional(),
6739
6868
  /**
6740
6869
  * A picture of ours that shows it (a path from the creative's folder, or a URL): every run
6741
6870
  * wires it into the renders (`ref_<id>`), so a picture is named here, never hand-wired.
6742
6871
  */
6743
- image: z7.string().trim().min(1).optional()
6872
+ image: z8.string().trim().min(1).optional()
6744
6873
  }).strict();
6745
- var Market = z7.object({
6874
+ var Market = z8.object({
6746
6875
  /** ISO country code (ES, MX), or "global" for a version that runs in several. */
6747
- country: z7.string().trim().min(2),
6748
- language: z7.string().trim().min(2).optional(),
6749
- currency: z7.string().trim().min(1).optional(),
6876
+ country: z8.string().trim().min(2),
6877
+ language: z8.string().trim().min(2).optional(),
6878
+ currency: z8.string().trim().min(1).optional(),
6750
6879
  /** How the copy addresses the viewer: tú, usted, vos, você, du, Sie, vous… */
6751
- address: z7.string().trim().min(1).optional()
6880
+ address: z8.string().trim().min(1).optional()
6752
6881
  }).strict();
6753
- var Viewer = z7.object({
6882
+ var Viewer = z8.object({
6754
6883
  /** Who will see the ad, in the words a marketer would use. */
6755
- who: z7.string().trim().min(1),
6884
+ who: z8.string().trim().min(1),
6756
6885
  market: Market.optional(),
6757
6886
  /** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
6758
- knows: z7.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z7.string().optional()),
6759
- temperature: z7.enum(["cold", "warm", "hot"]).optional(),
6887
+ knows: z8.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z8.string().optional()),
6888
+ temperature: z8.enum(["cold", "warm", "hot"]).optional(),
6760
6889
  /** Where they meet it (feed, Stories, Reels). */
6761
- placement: z7.string().optional()
6890
+ placement: z8.string().optional()
6762
6891
  }).strict();
6763
6892
  var withoutFormatName = (v) => {
6764
6893
  if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
@@ -6766,104 +6895,104 @@ var withoutFormatName = (v) => {
6766
6895
  const { concept: _format, ...scene } = v;
6767
6896
  return scene;
6768
6897
  };
6769
- var SceneFields = z7.object({
6770
- description: z7.string().trim().min(1),
6898
+ var SceneFields = z8.object({
6899
+ description: z8.string().trim().min(1),
6771
6900
  /** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
6772
- look: z7.preprocess(
6901
+ look: z8.preprocess(
6773
6902
  (v) => typeof v !== "string" ? v : /^photo/i.test(v.trim()) ? "photo" : /^illustrat/i.test(v.trim()) ? "illustration" : /^(design|graphic)/i.test(v.trim()) ? "designed" : v,
6774
- z7.enum(["photo", "designed", "illustration"])
6903
+ z8.enum(["photo", "designed", "illustration"])
6775
6904
  ),
6776
6905
  /** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
6777
- capture: z7.string().optional(),
6778
- mood: z7.string().optional(),
6906
+ capture: z8.string().optional(),
6907
+ mood: z8.string().optional(),
6779
6908
  /** How the frame is arranged, by what fills each part of it. */
6780
- layout: z7.string().optional()
6909
+ layout: z8.string().optional()
6781
6910
  }).strict();
6782
- var Scene = z7.preprocess(withoutFormatName, SceneFields);
6783
- var Brand = z7.object({
6784
- name: z7.string().optional(),
6911
+ var Scene = z8.preprocess(withoutFormatName, SceneFields);
6912
+ var Brand = z8.object({
6913
+ name: z8.string().optional(),
6785
6914
  /** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
6786
- lockup: z7.array(z7.string().trim().min(1)).optional(),
6915
+ lockup: z8.array(z8.string().trim().min(1)).optional(),
6787
6916
  /** The brand's face in words, when no specimen of it is wired ("a heavy geometric sans"). */
6788
- typeface: z7.string().optional(),
6917
+ typeface: z8.string().optional(),
6789
6918
  ground: Hex2.optional(),
6790
6919
  ink: Hex2.optional(),
6791
6920
  accent: Hex2.optional(),
6792
6921
  cta: Hex2.optional(),
6793
- palette: z7.array(Hex2).default([])
6922
+ palette: z8.array(Hex2).default([])
6794
6923
  }).strict();
6795
- var ColorUse = z7.object({ object: z7.string().trim().min(1), name: z7.string().trim().min(1), hex: Hex2.optional() }).strict();
6796
- var Mechanism = z7.object({ device: z7.string().trim().min(1), why_it_works: z7.string().optional() }).strict();
6797
- var ThirdPartyBrand = z7.object({
6798
- name: z7.string().trim().min(1),
6924
+ var ColorUse = z8.object({ object: z8.string().trim().min(1), name: z8.string().trim().min(1), hex: Hex2.optional() }).strict();
6925
+ var Mechanism = z8.object({ device: z8.string().trim().min(1), why_it_works: z8.string().optional() }).strict();
6926
+ var ThirdPartyBrand = z8.object({
6927
+ name: z8.string().trim().min(1),
6799
6928
  /** Where it shows in this ad, by what carries it ("the car the loan is for"). */
6800
- what: z7.string().optional(),
6929
+ what: z8.string().optional(),
6801
6930
  /** The reference (BADGE_1) that holds a picture of its logo, when one is wired. */
6802
- logo: z7.string().regex(/^(PERSON|ANIMAL|PRODUCT|BADGE|PROP)_\d+$/, "a logo is a reference id like BADGE_1").optional(),
6931
+ logo: z8.string().regex(/^(PERSON|ANIMAL|PRODUCT|BADGE|PROP)_\d+$/, "a logo is a reference id like BADGE_1").optional(),
6803
6932
  /**
6804
6933
  * Why its logo is on our ad: `partner` (the offer is about it), `proof` (the review platform a
6805
6934
  * rating comes from), `category_signal` (how a stranger knows in a second what this is: the
6806
6935
  * Instagram and TikTok marks on a social media tool's ad).
6807
6936
  */
6808
- role: z7.enum(["partner", "proof", "category_signal"]).optional()
6937
+ role: z8.enum(["partner", "proof", "category_signal"]).optional()
6809
6938
  }).strict();
6810
- var LoadBearing = z7.object({
6939
+ var LoadBearing = z8.object({
6811
6940
  /** The element, in a few words ("the free gift", "a peer's face", "the notes app"). */
6812
- element: z7.string().trim().min(1),
6941
+ element: z8.string().trim().min(1),
6813
6942
  /** What it takes to carry it ("something the viewer wants for free"). */
6814
- needs: z7.string().optional(),
6943
+ needs: z8.string().optional(),
6815
6944
  /** Whether we have it as it is, can adapt it, or have nothing for it. */
6816
- have: z7.enum(["have", "adapt", "missing"]),
6945
+ have: z8.enum(["have", "adapt", "missing"]),
6817
6946
  /** What of ours carries it in this ad. */
6818
- ours: z7.string().optional()
6947
+ ours: z8.string().optional()
6819
6948
  }).strict();
6820
- var Concept = z7.object({
6821
- reference_ad_id: z7.string().optional(),
6822
- why_it_wins: z7.object({
6823
- disguise: z7.string().optional(),
6824
- voice: z7.string().optional(),
6825
- trigger: z7.string().optional(),
6826
- attention: z7.string().optional(),
6827
- logic: z7.string().optional()
6949
+ var Concept = z8.object({
6950
+ reference_ad_id: z8.string().optional(),
6951
+ why_it_wins: z8.object({
6952
+ disguise: z8.string().optional(),
6953
+ voice: z8.string().optional(),
6954
+ trigger: z8.string().optional(),
6955
+ attention: z8.string().optional(),
6956
+ logic: z8.string().optional()
6828
6957
  }).strict().default({}),
6829
- load_bearing: z7.array(LoadBearing).default([]),
6830
- verdict: z7.enum(["use", "adapt", "reject"]),
6958
+ load_bearing: z8.array(LoadBearing).default([]),
6959
+ verdict: z8.enum(["use", "adapt", "reject"]),
6831
6960
  /** How the idea transfers to us, in a sentence. Bookkeeping: never compiled into a render. */
6832
- transfer_note: z7.string().optional()
6961
+ transfer_note: z8.string().optional()
6833
6962
  }).strict();
6834
- var AdSpecSchema = z7.object({
6835
- schema: z7.enum(AD_SPEC_SCHEMAS),
6963
+ var AdSpecSchema = z8.object({
6964
+ schema: z8.enum(AD_SPEC_SCHEMAS),
6836
6965
  viewer: Viewer.optional(),
6837
6966
  /** The one thing the ad tells the viewer. Intent, never printed. */
6838
- message: z7.string().optional(),
6839
- copy: z7.array(AdCopyLine).default([]),
6967
+ message: z8.string().optional(),
6968
+ copy: z8.array(AdCopyLine).default([]),
6840
6969
  scene: Scene,
6841
6970
  brand: Brand.optional(),
6842
- colors: z7.array(ColorUse).default([]),
6843
- references: z7.array(AdReference).default([]),
6971
+ colors: z8.array(ColorUse).default([]),
6972
+ references: z8.array(AdReference).default([]),
6844
6973
  /** The ad shows the brand's logo (the real file, wired on the canvas). */
6845
- logo: z7.boolean().default(false),
6846
- logo_position: z7.string().optional(),
6974
+ logo: z8.boolean().default(false),
6975
+ logo_position: z8.string().optional(),
6847
6976
  /** Other brands the offer is about, whose logos the ad may show. */
6848
- third_party_brands: z7.array(ThirdPartyBrand).default([]),
6977
+ third_party_brands: z8.array(ThirdPartyBrand).default([]),
6849
6978
  /**
6850
6979
  * Words the scene prints by itself, never copy: a phone's status bar and app chrome, a code
6851
6980
  * editor's line numbers, a product's own label, a platform's badge. The render may draw them
6852
6981
  * and the review never calls them extra words.
6853
6982
  */
6854
- scene_text: z7.preprocess(
6983
+ scene_text: z8.preprocess(
6855
6984
  (v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
6856
- z7.array(z7.string().trim().min(1)).default([])
6985
+ z8.array(z8.string().trim().min(1)).default([])
6857
6986
  ),
6858
6987
  /** The formats the ad is made for; the canvas delivers them. */
6859
- formats: z7.array(z7.string()).optional(),
6988
+ formats: z8.array(z8.string()).optional(),
6860
6989
  /** An optional note on direction for this concept. */
6861
- direction: z7.string().optional(),
6990
+ direction: z8.string().optional(),
6862
6991
  /** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
6863
- mechanism: z7.preprocess((v) => {
6992
+ mechanism: z8.preprocess((v) => {
6864
6993
  const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
6865
6994
  return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
6866
- }, z7.array(Mechanism).default([])),
6995
+ }, z8.array(Mechanism).default([])),
6867
6996
  /** Why the reference won and what this ad keeps of it (schema /3). */
6868
6997
  concept: Concept.optional(),
6869
6998
  /**
@@ -6871,9 +7000,9 @@ var AdSpecSchema = z7.object({
6871
7000
  * chat) carries the logo as the sender or avatar and no big button; `standard` the logo large
6872
7001
  * and a solid button. Unset: standard.
6873
7002
  */
6874
- brand_presence: z7.enum(["native", "standard"]).optional(),
7003
+ brand_presence: z8.enum(["native", "standard"]).optional(),
6875
7004
  /** What kind of product a stranger takes it for in one second, never its format ("a trade course", "a payroll app"). */
6876
- recognition: z7.string().optional(),
7005
+ recognition: z8.string().optional(),
6877
7006
  /**
6878
7007
  * The ad is a screen people know (a post on X, a WhatsApp chat, a wall of Trustpilot reviews, a
6879
7008
  * note, a Slack message), drawn exactly from HTML at every size (`native-surface.ts`); the image
@@ -6881,9 +7010,9 @@ var AdSpecSchema = z7.object({
6881
7010
  */
6882
7011
  native: NativeSurfaceSchema.optional(),
6883
7012
  // Bookkeeping: read by the agent and the judge's context, never compiled into a render.
6884
- source: z7.unknown().optional(),
6885
- reference: z7.unknown().optional(),
6886
- notes: z7.unknown().optional()
7013
+ source: z8.unknown().optional(),
7014
+ reference: z8.unknown().optional(),
7015
+ notes: z8.unknown().optional()
6887
7016
  }).strict();
6888
7017
  function referenceNodeId(reference) {
6889
7018
  return `ref_${reference.id.toLowerCase()}`;
@@ -6960,7 +7089,7 @@ var FIELD_SHAPES = {
6960
7089
  };
6961
7090
  var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
6962
7091
  function adSpecErrorText(e) {
6963
- if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
7092
+ if (!(e instanceof z8.ZodError)) return e instanceof Error ? e.message : String(e);
6964
7093
  const items = /* @__PURE__ */ new Map();
6965
7094
  for (const issue of e.issues) {
6966
7095
  const [field2, index, key] = issue.path;
@@ -7236,7 +7365,7 @@ function adSpecFromBlueprint(raw, opts = {}) {
7236
7365
  };
7237
7366
  }
7238
7367
  function adSpecJsonSchema() {
7239
- return z7.toJSONSchema(AdSpecSchema, { io: "input", unrepresentable: "any" });
7368
+ return z8.toJSONSchema(AdSpecSchema, { io: "input", unrepresentable: "any" });
7240
7369
  }
7241
7370
 
7242
7371
  // src/engine/scaffold/static-brief.ts
@@ -7464,7 +7593,7 @@ function pictureInstruction(p, ctx) {
7464
7593
  const keep = p.spec?.keep ? ` Keep ${plain(p.spec.keep, ctx.clean)}.` : "";
7465
7594
  switch (p.role) {
7466
7595
  case "base":
7467
- return ctx.job === "fix" ? "the current version of this ad, to edit." : "the finished ad, to re-lay for this shape.";
7596
+ return ctx.job === "fix" ? "the current version of this ad, to edit." : ctx.job === "series" ? "the finished ad this series repeats, to copy exactly with new words." : "the finished ad, to re-lay for this shape.";
7468
7597
  case "identity":
7469
7598
  return identityInstruction(p, what, keep, ctx.flowing);
7470
7599
  // The user saw one label come back two ways (E4 gallery, BENCH.md § "E1 v2"): the label is the product's.
@@ -7843,6 +7972,10 @@ function deliverableSection(c) {
7843
7972
  const change = sentence(c.opts.instruction?.trim() || "correct what is wrong");
7844
7973
  return c.flowing ? `Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else in Image 1 exactly the same, preserving its picture, layout, colours, lettering and every word.` : `Edit: Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the colours, the lettering and every word.`;
7845
7974
  }
7975
+ if (c.job === "series") {
7976
+ const change = sentence(c.opts.instruction?.trim() || "set the new words");
7977
+ return c.flowing ? `Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}: ${change} Keep everything else in Image 1 exactly the same, preserving its picture, layout, background, shapes, colours, lettering style and logo, each word in the place and style of the one it replaces.` : `Edit: Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the background, the shapes, the colours, the lettering style and the logo, each new line in the place, size and style of the line it replaces.`;
7978
+ }
7846
7979
  if (c.job === "relay") {
7847
7980
  return c.flowing ? `Image 1 is a finished ad. Recompose this same ad as a ${noun}${forBrand}: the same picture, colours, lettering and every word, rearranged for the new shape, with more of the scene revealed at the edges.` : `Deliverable: the same ad as Image 1, re-laid as a finished ${noun}${forBrand}. Keep the same picture, colours, lettering and every word, rearranged for the new shape; show more of the scene rather than cropping it.`;
7848
7981
  }
@@ -8513,6 +8646,7 @@ var STATIC_ADAPT_ROLE = "static_adapt";
8513
8646
  var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
8514
8647
  var STATIC_PLACE_ROLE = "static_place";
8515
8648
  var LOGO_COMPOSITE_ROLE = "logo_composite";
8649
+ var SERIES_LEAD_ROLE = "series_lead";
8516
8650
  var STATIC_FIX_ROLE = "static_fix";
8517
8651
  var LOGO_COMPOSITE_DEFAULT = false;
8518
8652
  var ADAPT_IMAGE_SIZE = "2K";
@@ -8532,6 +8666,19 @@ function rolesById(canvas) {
8532
8666
  }
8533
8667
  return out;
8534
8668
  }
8669
+ function relaidPictureRole(canvas) {
8670
+ const roles = rolesById(canvas);
8671
+ const final = nodesOf(canvas).find((n) => n.id === "final" && n.type === "collect");
8672
+ const labels = final?.params?.labels;
8673
+ const images = final?.inputs?.images;
8674
+ if (!Array.isArray(labels) || !Array.isArray(images)) return {};
8675
+ const at2 = images.findIndex((ref) => {
8676
+ const from = typeof ref === "string" ? /^\$ref:([^.]+)\./.exec(ref)?.[1] : void 0;
8677
+ return from !== void 0 && roles[from] === STATIC_MASTER_ROLE;
8678
+ });
8679
+ const format = labels[at2];
8680
+ return typeof format === "string" ? { [`last_${formatSlug(format)}`]: STATIC_MASTER_ROLE } : {};
8681
+ }
8535
8682
  function formatSlug(format) {
8536
8683
  return format.replace(":", "x").replace(".", "_");
8537
8684
  }
@@ -10067,12 +10214,12 @@ function staticInputKind(ctx, value) {
10067
10214
  if (!value.startsWith(REF_PREFIX)) return "value";
10068
10215
  const target = refTarget(ctx, value);
10069
10216
  if (!target) return "unknown";
10070
- const outputSchema = unwrap(shapeOf(target.def.outputs)?.[target.expr.output] ?? z8.unknown());
10071
- if (outputSchema instanceof z8.ZodArray && target.expr.index === void 0) return "list";
10217
+ const outputSchema = unwrap(shapeOf(target.def.outputs)?.[target.expr.output] ?? z9.unknown());
10218
+ if (outputSchema instanceof z9.ZodArray && target.expr.index === void 0) return "list";
10072
10219
  const kind = refOutputKind(target);
10073
10220
  if (kind && ASSET_KINDS.has(kind)) return kind;
10074
- const plain2 = outputSchema instanceof z8.ZodString || outputSchema instanceof z8.ZodNumber;
10075
- return plain2 || outputSchema instanceof z8.ZodBoolean ? "value" : "unknown";
10221
+ const plain2 = outputSchema instanceof z9.ZodString || outputSchema instanceof z9.ZodNumber;
10222
+ return plain2 || outputSchema instanceof z9.ZodBoolean ? "value" : "unknown";
10076
10223
  }
10077
10224
  function staticInputSource(ctx, value) {
10078
10225
  if (typeof value !== "string" || !value.startsWith(REF_PREFIX)) return void 0;
@@ -10239,7 +10386,7 @@ function listShapeKeys(schema) {
10239
10386
  }
10240
10387
  function shapeOf(schema) {
10241
10388
  const obj = unwrap(schema);
10242
- return obj instanceof z8.ZodObject ? obj.shape : void 0;
10389
+ return obj instanceof z9.ZodObject ? obj.shape : void 0;
10243
10390
  }
10244
10391
  function unwrap(schema) {
10245
10392
  let cur = schema;
@@ -10781,7 +10928,7 @@ import { mkdtemp, readFile as readFile5, rm, stat as stat2 } from "fs/promises";
10781
10928
  import { tmpdir } from "os";
10782
10929
  import path8 from "path";
10783
10930
  import { promisify } from "util";
10784
- import { z as z9 } from "zod";
10931
+ import { z as z10 } from "zod";
10785
10932
 
10786
10933
  // ../proxy/src/fetch.ts
10787
10934
  import { lookup as dnsLookup } from "dns";
@@ -11536,7 +11683,7 @@ function mapClientError(ctx, e) {
11536
11683
 
11537
11684
  // src/engine/nodes/ingest.ts
11538
11685
  var execFile = promisify(execFileCb);
11539
- var ExpectEnum = z9.enum(["image", "video", "audio", "text", "json", "font"]);
11686
+ var ExpectEnum = z10.enum(["image", "video", "audio", "text", "json", "font"]);
11540
11687
  function coercePathUrlToUrl(input) {
11541
11688
  if (!input || typeof input !== "object") return input;
11542
11689
  const obj = input;
@@ -11546,23 +11693,23 @@ function coercePathUrlToUrl(input) {
11546
11693
  }
11547
11694
  return input;
11548
11695
  }
11549
- var IngestParams = z9.preprocess(
11696
+ var IngestParams = z10.preprocess(
11550
11697
  coercePathUrlToUrl,
11551
- z9.discriminatedUnion("source", [
11552
- z9.object({
11553
- source: z9.literal("url"),
11554
- url: z9.string().url(),
11698
+ z10.discriminatedUnion("source", [
11699
+ z10.object({
11700
+ source: z10.literal("url"),
11701
+ url: z10.string().url(),
11555
11702
  expect: ExpectEnum
11556
11703
  }).strict(),
11557
- z9.object({
11558
- source: z9.literal("path"),
11559
- path: z9.string().min(1),
11704
+ z10.object({
11705
+ source: z10.literal("path"),
11706
+ path: z10.string().min(1),
11560
11707
  expect: ExpectEnum
11561
11708
  }).strict()
11562
11709
  ])
11563
11710
  );
11564
- var IngestInputs = z9.object({}).strict();
11565
- var IngestOutputs = z9.object({ asset: AssetRef }).strict();
11711
+ var IngestInputs = z10.object({}).strict();
11712
+ var IngestOutputs = z10.object({ asset: AssetRef }).strict();
11566
11713
  var RAW_TEXT_EXTENSIONS = [".txt", ".md"];
11567
11714
  function resolveStrategy(params) {
11568
11715
  if (params.source === "path") return "local_file";
@@ -12146,7 +12293,7 @@ ingestNode.cacheKeyExtras = async ({ params }) => {
12146
12293
  };
12147
12294
 
12148
12295
  // src/engine/nodes/local/audioTimeline.ts
12149
- import { z as z10 } from "zod";
12296
+ import { z as z11 } from "zod";
12150
12297
 
12151
12298
  // src/engine/nodes/local/lib/cli-runner.ts
12152
12299
  import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
@@ -12365,50 +12512,50 @@ async function getToolVersion(bin) {
12365
12512
  var FFMPEG_BIN = "ffmpeg";
12366
12513
  var FORMAT_EXT = { mp3: "mp3", wav: "wav", m4a: "m4a" };
12367
12514
  var FORMAT_CODEC = { mp3: "libmp3lame", wav: "pcm_s16le", m4a: "aac" };
12368
- var Track = z10.object({
12515
+ var Track = z11.object({
12369
12516
  /** Input slot name (must match a wired `inputs.<slot>` audio ref). */
12370
- slot: z10.string().min(1),
12517
+ slot: z11.string().min(1),
12371
12518
  /** When this track starts on the timeline, seconds from 0. */
12372
- start_s: z10.number().min(0),
12519
+ start_s: z11.number().min(0),
12373
12520
  /**
12374
12521
  * Optional hard cap on this track's length, seconds. The clip is trimmed BEFORE
12375
12522
  * placement, so a source that runs long (an over-long voice extract, a converted
12376
12523
  * track that came back oversized) cannot bleed into the next track's window.
12377
12524
  */
12378
- duration_s: z10.number().positive().optional(),
12525
+ duration_s: z11.number().positive().optional(),
12379
12526
  /**
12380
12527
  * Level adjustment in dB — negative attenuates. A MUSIC BED under speech
12381
12528
  * belongs at −20 dB (see the `describe` text: this number is a settled
12382
12529
  * decision, not a per-cut judgement).
12383
12530
  */
12384
- gain_db: z10.number().optional().describe(
12531
+ gain_db: z11.number().optional().describe(
12385
12532
  "Level adjustment in dB (negative attenuates, 0 = untouched). A music bed under speech sits at \u221220 dB and is sidechain-ducked as well; \u221212 dB crowded the voice and \u221213 dB was rejected by the client as too loud. If a silent tail (an end card, a logo plate) sounds empty, do NOT raise the bed \u2014 wire the same bed as a SECOND track windowed over the tail with its own higher gain. Measuring loudness across a window that includes the silent tail is what makes a correct bed look quiet."
12386
12533
  )
12387
12534
  }).strict();
12388
- var DuckSpec = z10.object({
12389
- track: z10.string().min(1).describe("Slot to duck \u2014 the music bed."),
12390
- against: z10.array(z10.string().min(1)).min(1).describe("Key slots whose signal ducks it \u2014 the voice tracks."),
12391
- threshold: z10.number().min(0).max(1).optional().describe("Linear 0\u20131, not dB. Lower ducks more readily. Default 0.03."),
12392
- ratio: z10.number().min(1).max(20).optional().describe("How hard it ducks. Default 8; 12 for a firm hold."),
12535
+ var DuckSpec = z11.object({
12536
+ track: z11.string().min(1).describe("Slot to duck \u2014 the music bed."),
12537
+ against: z11.array(z11.string().min(1)).min(1).describe("Key slots whose signal ducks it \u2014 the voice tracks."),
12538
+ threshold: z11.number().min(0).max(1).optional().describe("Linear 0\u20131, not dB. Lower ducks more readily. Default 0.03."),
12539
+ ratio: z11.number().min(1).max(20).optional().describe("How hard it ducks. Default 8; 12 for a firm hold."),
12393
12540
  // MILLISECONDS is spelled out because a canvas shipped `attack: 0.05,
12394
12541
  // release: 0.45` — read as seconds — which is a 0.45 ms release: the bed
12395
12542
  // snapped back to full level between syllables and the client heard the
12396
12543
  // music as "too loud" at a level that measured correct.
12397
- attack: z10.number().min(0.01).optional().describe("MILLISECONDS (not seconds). Typical 5\u201350; default 5. A value below 0.5 is a unit mistake."),
12398
- release: z10.number().min(0.01).optional().describe(
12544
+ attack: z11.number().min(0.01).optional().describe("MILLISECONDS (not seconds). Typical 5\u201350; default 5. A value below 0.5 is a unit mistake."),
12545
+ release: z11.number().min(0.01).optional().describe(
12399
12546
  "MILLISECONDS (not seconds). Typical 200\u2013800; default 300. Anything under ~10 makes the bed pump back up between words and is a unit mistake."
12400
12547
  )
12401
12548
  }).strict();
12402
- var AudioTimelineParams = z10.object({
12403
- tracks: z10.array(Track).min(1),
12549
+ var AudioTimelineParams = z11.object({
12550
+ tracks: z11.array(Track).min(1),
12404
12551
  /** Final track length in ms — pads short / trims long. Defaults to the natural mix length. */
12405
- total_ms: z10.number().int().positive().optional(),
12406
- output_format: z10.enum(["mp3", "wav", "m4a"]).optional(),
12552
+ total_ms: z11.number().int().positive().optional(),
12553
+ output_format: z11.enum(["mp3", "wav", "m4a"]).optional(),
12407
12554
  /** Optional sidechain duck of one track under others (e.g. music under voice). */
12408
12555
  duck: DuckSpec.optional()
12409
12556
  }).strict();
12410
- var AudioTimelineInputs = z10.record(z10.string(), z10.unknown());
12411
- var AudioTimelineOutputs = z10.object({ audio: z10.custom() }).strict();
12557
+ var AudioTimelineInputs = z11.record(z11.string(), z11.unknown());
12558
+ var AudioTimelineOutputs = z11.object({ audio: z11.custom() }).strict();
12412
12559
  function applyDuck(params, labelFor, filterChains) {
12413
12560
  const duck = params.duck;
12414
12561
  if (!duck) return;
@@ -12522,28 +12669,28 @@ var audioTimelineNode = defineNode({
12522
12669
  });
12523
12670
 
12524
12671
  // src/engine/nodes/local/avMux.ts
12525
- import { z as z11 } from "zod";
12672
+ import { z as z12 } from "zod";
12526
12673
  var FFMPEG_BIN2 = "ffmpeg";
12527
- var Loudnorm = z11.object({
12528
- i: z11.number().default(-14).describe("Integrated loudness target, LUFS. -14 is the social/streaming target."),
12529
- tp: z11.number().default(-1.5).describe("True-peak ceiling, dBTP."),
12530
- lra: z11.number().default(11).describe("Loudness range target, LU.")
12674
+ var Loudnorm = z12.object({
12675
+ i: z12.number().default(-14).describe("Integrated loudness target, LUFS. -14 is the social/streaming target."),
12676
+ tp: z12.number().default(-1.5).describe("True-peak ceiling, dBTP."),
12677
+ lra: z12.number().default(11).describe("Loudness range target, LU.")
12531
12678
  }).strict();
12532
- var AvMuxParams = z11.object({
12533
- loudnorm: z11.union([z11.literal(false), Loudnorm]).default({ i: -14, tp: -1.5, lra: 11 }).describe(
12679
+ var AvMuxParams = z12.object({
12680
+ loudnorm: z12.union([z12.literal(false), Loudnorm]).default({ i: -14, tp: -1.5, lra: 11 }).describe(
12534
12681
  'Loudness normalization of the soundtrack (EBU R128). The raw mix is a quiet track that reads as "no sound" in casual players, so it is on by default at -14 LUFS / -1.5 dBTP. `false` keeps the mix level.'
12535
12682
  ),
12536
- audio_bitrate: z11.string().regex(/^\d+k$/, 'a bitrate like "192k"').default("192k").describe("AAC bitrate of the delivered soundtrack."),
12537
- sample_rate: z11.number().int().positive().default(48e3).describe("Delivered sample rate, Hz."),
12538
- length: z11.union([z11.literal("shortest"), z11.object({ total_s: z11.number().positive() }).strict()]).default("shortest").describe(
12683
+ audio_bitrate: z12.string().regex(/^\d+k$/, 'a bitrate like "192k"').default("192k").describe("AAC bitrate of the delivered soundtrack."),
12684
+ sample_rate: z12.number().int().positive().default(48e3).describe("Delivered sample rate, Hz."),
12685
+ length: z12.union([z12.literal("shortest"), z12.object({ total_s: z12.number().positive() }).strict()]).default("shortest").describe(
12539
12686
  "`shortest` ends the file with the shorter stream, so a picture and mix pinned to the same timeline come out exact (a longer one is silently cut: `canvas validate` checks both against the timeline). `{ total_s }` cuts both streams at that second; they must each run at least that long."
12540
12687
  )
12541
12688
  }).strict();
12542
- var AvMuxInputs = z11.object({
12543
- video: z11.custom().describe("The finished picture (its own audio, if any, is dropped)."),
12544
- audio: z11.custom().describe("The mixed soundtrack, usually an `audio_timeline`.")
12689
+ var AvMuxInputs = z12.object({
12690
+ video: z12.custom().describe("The finished picture (its own audio, if any, is dropped)."),
12691
+ audio: z12.custom().describe("The mixed soundtrack, usually an `audio_timeline`.")
12545
12692
  }).strict();
12546
- var AvMuxOutputs = z11.object({ video: z11.custom() }).strict();
12693
+ var AvMuxOutputs = z12.object({ video: z12.custom() }).strict();
12547
12694
  function audioFilter(params) {
12548
12695
  const stereo = "aformat=channel_layouts=stereo";
12549
12696
  if (params.loudnorm === false) return stereo;
@@ -12611,7 +12758,7 @@ var avMuxNode = defineNode({
12611
12758
  });
12612
12759
 
12613
12760
  // src/engine/nodes/local/collect.ts
12614
- import { z as z12 } from "zod";
12761
+ import { z as z13 } from "zod";
12615
12762
  var collectNode = defineNode({
12616
12763
  id: "collect",
12617
12764
  version: "1.0.0",
@@ -12620,9 +12767,9 @@ var collectNode = defineNode({
12620
12767
  passthroughRefs: true,
12621
12768
  summary: "Gather images from multiple upstream nodes into one ordered array \u2014 the standard terminal for multi-variant canvases whose final output is several images.",
12622
12769
  when_to_use: "Point the canvas `output` at this node when several independent branches (e.g. one image_generate per scene/variant) must ALL be finals. Wire `inputs.images` as an array of refs like `$ref:gen_billboard_03.images#0` \u2014 each final inherits its producer node id as its label (or set `params.labels` to override), so variants stay identifiable in the dashboard and selection.",
12623
- inputs: z12.object({ images: z12.array(ImageRef).min(1) }).strict(),
12624
- params: z12.object({ labels: z12.array(z12.string().min(1)).min(1).optional() }).strict(),
12625
- outputs: z12.object({ images: z12.array(ImageRef).min(1) }).strict(),
12770
+ inputs: z13.object({ images: z13.array(ImageRef).min(1) }).strict(),
12771
+ params: z13.object({ labels: z13.array(z13.string().min(1)).min(1).optional() }).strict(),
12772
+ outputs: z13.object({ images: z13.array(ImageRef).min(1) }).strict(),
12626
12773
  outputKinds: { images: "image" },
12627
12774
  cost: () => ({ credits: 0, seconds_estimate: 0 }),
12628
12775
  // Arity is only knowable at validate time when `images` is a literal array;
@@ -12648,18 +12795,18 @@ var collectNode = defineNode({
12648
12795
  });
12649
12796
 
12650
12797
  // src/engine/nodes/local/ffmpeg.ts
12651
- import { z as z13 } from "zod";
12798
+ import { z as z14 } from "zod";
12652
12799
  var FFMPEG_BIN3 = "ffmpeg";
12653
- var OutputDecl = z13.object({
12654
- kind: z13.enum(["image", "video", "audio"]),
12655
- ext: z13.string().min(1).max(8)
12800
+ var OutputDecl = z14.object({
12801
+ kind: z14.enum(["image", "video", "audio"]),
12802
+ ext: z14.string().min(1).max(8)
12656
12803
  }).strict();
12657
- var FfmpegParams = z13.object({
12658
- args: z13.array(z13.string()).min(1),
12659
- outputs: z13.record(z13.string(), OutputDecl).default({})
12804
+ var FfmpegParams = z14.object({
12805
+ args: z14.array(z14.string()).min(1),
12806
+ outputs: z14.record(z14.string(), OutputDecl).default({})
12660
12807
  }).strict();
12661
- var FfmpegInputs = z13.record(z13.string(), z13.unknown());
12662
- var FfmpegOutputs = z13.record(z13.string(), z13.custom());
12808
+ var FfmpegInputs = z14.record(z14.string(), z14.unknown());
12809
+ var FfmpegOutputs = z14.record(z14.string(), z14.custom());
12663
12810
  var ffmpegNode = defineNode({
12664
12811
  id: "ffmpeg",
12665
12812
  version: "2.0.0",
@@ -12690,7 +12837,7 @@ import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile4 } from "fs/prom
12690
12837
  import { createRequire } from "module";
12691
12838
  import { tmpdir as tmpdir3 } from "os";
12692
12839
  import path11 from "path";
12693
- import { z as z14 } from "zod";
12840
+ import { z as z15 } from "zod";
12694
12841
 
12695
12842
  // src/engine/nodes/local/lib/assets.ts
12696
12843
  import { copyFile as copyFile3, readFile as readFile7 } from "fs/promises";
@@ -12720,15 +12867,15 @@ var DEFAULT_SPECIMEN = [
12720
12867
  "abcdefghijklmnopqrstuvwxyz",
12721
12868
  `0123456789 !?&@#$%().,:;'"-`
12722
12869
  ].join("\n");
12723
- var FontSpecimenParams = z14.object({
12724
- text: z14.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
12725
- font_size: z14.number().int().min(8).max(512).optional().default(72),
12726
- padding: z14.number().int().min(0).max(512).optional().default(64),
12727
- line_height: z14.number().min(0.8).max(3).optional().default(1.35),
12728
- max_width: z14.number().int().min(256).max(4096).optional()
12870
+ var FontSpecimenParams = z15.object({
12871
+ text: z15.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
12872
+ font_size: z15.number().int().min(8).max(512).optional().default(72),
12873
+ padding: z15.number().int().min(0).max(512).optional().default(64),
12874
+ line_height: z15.number().min(0.8).max(3).optional().default(1.35),
12875
+ max_width: z15.number().int().min(256).max(4096).optional()
12729
12876
  }).strict();
12730
- var FontSpecimenInputs = z14.object({ font: FontRef }).loose();
12731
- var FontSpecimenOutputs = z14.object({ image: ImageRef }).strict();
12877
+ var FontSpecimenInputs = z15.object({ font: FontRef }).loose();
12878
+ var FontSpecimenOutputs = z15.object({ image: ImageRef }).strict();
12732
12879
  var DEVICE_SCALE_FACTOR = 2;
12733
12880
  var PAGE_TIMEOUT_MS = 3e4;
12734
12881
  function escapeHtml(text2) {
@@ -12868,7 +13015,7 @@ import { mkdtemp as mkdtemp5, readFile as readFile12, rm as rm5, stat as stat6,
12868
13015
  import { cpus, tmpdir as tmpdir5 } from "os";
12869
13016
  import path18 from "path";
12870
13017
  import { promisify as promisify4 } from "util";
12871
- import { z as z16 } from "zod";
13018
+ import { z as z17 } from "zod";
12872
13019
 
12873
13020
  // src/engine/engine/composition-hash.ts
12874
13021
  import { readdir as readdir2, readFile as readFile8, stat as stat4 } from "fs/promises";
@@ -12906,62 +13053,62 @@ async function collectFiles(root, current) {
12906
13053
  // src/engine/engine/composition-meta.ts
12907
13054
  import { readFile as readFile9 } from "fs/promises";
12908
13055
  import path13 from "path";
12909
- import { z as z15 } from "zod";
12910
- var InputKind = z15.enum(["video", "image", "audio", "json", "font"]);
12911
- var InputSpec = z15.object({
13056
+ import { z as z16 } from "zod";
13057
+ var InputKind = z16.enum(["video", "image", "audio", "json", "font"]);
13058
+ var InputSpec = z16.object({
12912
13059
  kind: InputKind,
12913
- required: z15.boolean().optional().default(false),
13060
+ required: z16.boolean().optional().default(false),
12914
13061
  // Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
12915
13062
  // Defaults to `<key><ext>` derived from the kind.
12916
- staged_as: z15.string().min(1).optional(),
12917
- description: z15.string().optional()
13063
+ staged_as: z16.string().min(1).optional(),
13064
+ description: z16.string().optional()
12918
13065
  }).strict();
12919
13066
  var ParamSpecBase = {
12920
- required: z15.boolean().optional().default(false),
12921
- description: z15.string().optional()
13067
+ required: z16.boolean().optional().default(false),
13068
+ description: z16.string().optional()
12922
13069
  };
12923
- var StringParam = z15.object({
13070
+ var StringParam = z16.object({
12924
13071
  ...ParamSpecBase,
12925
- kind: z15.literal("string"),
12926
- default: z15.string().optional(),
12927
- enum: z15.array(z15.string()).optional()
13072
+ kind: z16.literal("string"),
13073
+ default: z16.string().optional(),
13074
+ enum: z16.array(z16.string()).optional()
12928
13075
  }).strict();
12929
- var IntegerParam = z15.object({
13076
+ var IntegerParam = z16.object({
12930
13077
  ...ParamSpecBase,
12931
- kind: z15.literal("integer"),
12932
- default: z15.number().int().optional(),
12933
- min: z15.number().int().optional(),
12934
- max: z15.number().int().optional()
13078
+ kind: z16.literal("integer"),
13079
+ default: z16.number().int().optional(),
13080
+ min: z16.number().int().optional(),
13081
+ max: z16.number().int().optional()
12935
13082
  }).strict();
12936
- var NumberParam = z15.object({
13083
+ var NumberParam = z16.object({
12937
13084
  ...ParamSpecBase,
12938
- kind: z15.literal("number"),
12939
- default: z15.number().optional(),
12940
- min: z15.number().optional(),
12941
- max: z15.number().optional()
13085
+ kind: z16.literal("number"),
13086
+ default: z16.number().optional(),
13087
+ min: z16.number().optional(),
13088
+ max: z16.number().optional()
12942
13089
  }).strict();
12943
- var BooleanParam = z15.object({
13090
+ var BooleanParam = z16.object({
12944
13091
  ...ParamSpecBase,
12945
- kind: z15.literal("boolean"),
12946
- default: z15.boolean().optional()
13092
+ kind: z16.literal("boolean"),
13093
+ default: z16.boolean().optional()
12947
13094
  }).strict();
12948
- var ColorParam = z15.object({
13095
+ var ColorParam = z16.object({
12949
13096
  ...ParamSpecBase,
12950
- kind: z15.literal("color"),
12951
- default: z15.string().optional()
13097
+ kind: z16.literal("color"),
13098
+ default: z16.string().optional()
12952
13099
  }).strict();
12953
- var ImageParam = z15.object({
13100
+ var ImageParam = z16.object({
12954
13101
  ...ParamSpecBase,
12955
- kind: z15.literal("image"),
12956
- default: z15.string().optional()
13102
+ kind: z16.literal("image"),
13103
+ default: z16.string().optional()
12957
13104
  }).strict();
12958
- var JsonParam = z15.object({
13105
+ var JsonParam = z16.object({
12959
13106
  ...ParamSpecBase,
12960
- kind: z15.literal("json"),
12961
- schema: z15.unknown().optional(),
12962
- default: z15.unknown().optional()
13107
+ kind: z16.literal("json"),
13108
+ schema: z16.unknown().optional(),
13109
+ default: z16.unknown().optional()
12963
13110
  }).strict();
12964
- var ParamSpec = z15.discriminatedUnion("kind", [
13111
+ var ParamSpec = z16.discriminatedUnion("kind", [
12965
13112
  StringParam,
12966
13113
  IntegerParam,
12967
13114
  NumberParam,
@@ -12970,21 +13117,21 @@ var ParamSpec = z15.discriminatedUnion("kind", [
12970
13117
  ImageParam,
12971
13118
  JsonParam
12972
13119
  ]);
12973
- var CompositionMetaSchema = z15.object({
12974
- id: z15.string().min(1),
12975
- title: z15.string().min(1),
12976
- description: z15.string().optional(),
12977
- width: z15.number().int().positive(),
12978
- height: z15.number().int().positive(),
12979
- fps: z15.number().int().positive().default(30),
12980
- default_duration: z15.number().positive().default(10),
12981
- inputs: z15.record(z15.string(), InputSpec).default({}),
12982
- params: z15.record(z15.string(), ParamSpec).default({}),
13120
+ var CompositionMetaSchema = z16.object({
13121
+ id: z16.string().min(1),
13122
+ title: z16.string().min(1),
13123
+ description: z16.string().optional(),
13124
+ width: z16.number().int().positive(),
13125
+ height: z16.number().int().positive(),
13126
+ fps: z16.number().int().positive().default(30),
13127
+ default_duration: z16.number().positive().default(10),
13128
+ inputs: z16.record(z16.string(), InputSpec).default({}),
13129
+ params: z16.record(z16.string(), ParamSpec).default({}),
12983
13130
  /**
12984
13131
  * The page reads its own staged files (a picture's pixels), so a snapshot opens it with
12985
13132
  * file access. Off by default: Chromium grants it for every file:// path, not just these.
12986
13133
  */
12987
- reads_staged_files: z15.boolean().optional()
13134
+ reads_staged_files: z16.boolean().optional()
12988
13135
  }).strict();
12989
13136
  async function loadCompositionMeta(compositionDir) {
12990
13137
  const metaPath = path13.join(compositionDir, "meta.json");
@@ -13012,39 +13159,39 @@ function buildParamsSchema(meta) {
13012
13159
  for (const [name, spec] of Object.entries(meta.params)) {
13013
13160
  shape[name] = buildParamFieldSchema(name, spec);
13014
13161
  }
13015
- return z15.object(shape).strict();
13162
+ return z16.object(shape).strict();
13016
13163
  }
13017
13164
  function buildParamFieldSchema(name, spec) {
13018
13165
  switch (spec.kind) {
13019
13166
  case "string": {
13020
- const s = spec.enum && spec.enum.length > 0 ? z15.enum(spec.enum) : z15.string();
13167
+ const s = spec.enum && spec.enum.length > 0 ? z16.enum(spec.enum) : z16.string();
13021
13168
  return finalize(s, spec.default, spec.required);
13022
13169
  }
13023
13170
  case "integer": {
13024
- let s = z15.number().int();
13171
+ let s = z16.number().int();
13025
13172
  if (spec.min !== void 0) s = s.min(spec.min);
13026
13173
  if (spec.max !== void 0) s = s.max(spec.max);
13027
13174
  return finalize(s, spec.default, spec.required);
13028
13175
  }
13029
13176
  case "number": {
13030
- let s = z15.number();
13177
+ let s = z16.number();
13031
13178
  if (spec.min !== void 0) s = s.min(spec.min);
13032
13179
  if (spec.max !== void 0) s = s.max(spec.max);
13033
13180
  return finalize(s, spec.default, spec.required);
13034
13181
  }
13035
13182
  case "boolean":
13036
- return finalize(z15.boolean(), spec.default, spec.required);
13183
+ return finalize(z16.boolean(), spec.default, spec.required);
13037
13184
  case "color": {
13038
- const s = z15.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
13185
+ const s = z16.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
13039
13186
  message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
13040
13187
  });
13041
13188
  return finalize(s, spec.default, spec.required);
13042
13189
  }
13043
13190
  case "image":
13044
- return finalize(z15.union([z15.string().min(1), z15.record(z15.string(), z15.unknown())]), spec.default, spec.required);
13191
+ return finalize(z16.union([z16.string().min(1), z16.record(z16.string(), z16.unknown())]), spec.default, spec.required);
13045
13192
  case "json":
13046
13193
  return finalize(
13047
- z15.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
13194
+ z16.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
13048
13195
  spec.default,
13049
13196
  spec.required
13050
13197
  );
@@ -13640,17 +13787,17 @@ ${stderr.slice(0, 1500)}`;
13640
13787
 
13641
13788
  // src/engine/nodes/local/hyperframe.ts
13642
13789
  var execFileAsync2 = promisify4(execFile4);
13643
- var HyperframeParams = z16.object({
13644
- composition: z16.string().min(1),
13790
+ var HyperframeParams = z17.object({
13791
+ composition: z17.string().min(1),
13645
13792
  // Output container. mp4 (default) for delivery; webm/mov render WITH
13646
13793
  // transparency (alpha) when the composition background is transparent —
13647
13794
  // use for motion-graphic overlays dropped into Premiere/AE/Nuke.
13648
- format: z16.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
13649
- timeout_ms: z16.number().int().positive().optional().default(10 * 60 * 1e3)
13650
- }).catchall(z16.unknown());
13651
- var HyperframeInputs = z16.record(z16.string(), z16.custom()).optional().default({});
13652
- var HyperframeOutputs = z16.object({
13653
- video: z16.custom()
13795
+ format: z17.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
13796
+ timeout_ms: z17.number().int().positive().optional().default(10 * 60 * 1e3)
13797
+ }).catchall(z17.unknown());
13798
+ var HyperframeInputs = z17.record(z17.string(), z17.custom()).optional().default({});
13799
+ var HyperframeOutputs = z17.object({
13800
+ video: z17.custom()
13654
13801
  }).strict();
13655
13802
  var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
13656
13803
  var MIME_BY_FORMAT = {
@@ -13927,23 +14074,23 @@ import { createRequire as createRequire3 } from "module";
13927
14074
  import { tmpdir as tmpdir6 } from "os";
13928
14075
  import path19 from "path";
13929
14076
  import { promisify as promisify5 } from "util";
13930
- import { z as z17 } from "zod";
14077
+ import { z as z18 } from "zod";
13931
14078
  var _execFileAsync = promisify5(execFile5);
13932
14079
  var require_3 = createRequire3(import.meta.url);
13933
- var WaitForSpec = z17.discriminatedUnion("kind", [
13934
- z17.object({ kind: z17.literal("auto") }),
13935
- z17.object({ kind: z17.literal("selector"), value: z17.string().min(1) }),
13936
- z17.object({ kind: z17.literal("function"), value: z17.string().min(1) }),
13937
- z17.object({ kind: z17.literal("timeout"), ms: z17.number().int().min(0).max(6e4) })
14080
+ var WaitForSpec = z18.discriminatedUnion("kind", [
14081
+ z18.object({ kind: z18.literal("auto") }),
14082
+ z18.object({ kind: z18.literal("selector"), value: z18.string().min(1) }),
14083
+ z18.object({ kind: z18.literal("function"), value: z18.string().min(1) }),
14084
+ z18.object({ kind: z18.literal("timeout"), ms: z18.number().int().min(0).max(6e4) })
13938
14085
  ]);
13939
- var HyperframeSnapshotParams = z17.object({
13940
- composition: z17.string().min(1),
14086
+ var HyperframeSnapshotParams = z18.object({
14087
+ composition: z18.string().min(1),
13941
14088
  wait_for: WaitForSpec.optional().default({ kind: "auto" }),
13942
- timeout_ms: z17.number().int().positive().optional().default(6e4)
13943
- }).catchall(z17.unknown());
13944
- var HyperframeSnapshotInputs = z17.record(z17.string(), z17.custom()).optional().default({});
13945
- var HyperframeSnapshotOutputs = z17.object({
13946
- image: z17.custom()
14089
+ timeout_ms: z18.number().int().positive().optional().default(6e4)
14090
+ }).catchall(z18.unknown());
14091
+ var HyperframeSnapshotInputs = z18.record(z18.string(), z18.custom()).optional().default({});
14092
+ var HyperframeSnapshotOutputs = z18.object({
14093
+ image: z18.custom()
13947
14094
  }).strict();
13948
14095
  var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
13949
14096
  var DEVICE_SCALE_FACTOR2 = 2;
@@ -14121,18 +14268,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
14121
14268
  // src/engine/nodes/local/imagemagick.ts
14122
14269
  import { execFile as execFile6 } from "child_process";
14123
14270
  import { promisify as promisify6 } from "util";
14124
- import { z as z18 } from "zod";
14271
+ import { z as z19 } from "zod";
14125
14272
  var execFileAsync3 = promisify6(execFile6);
14126
- var OutputDecl2 = z18.object({
14127
- kind: z18.enum(["image", "video", "audio"]),
14128
- ext: z18.string().min(1).max(8)
14273
+ var OutputDecl2 = z19.object({
14274
+ kind: z19.enum(["image", "video", "audio"]),
14275
+ ext: z19.string().min(1).max(8)
14129
14276
  }).strict();
14130
- var ImageMagickParams = z18.object({
14131
- args: z18.array(z18.string()).min(1),
14132
- outputs: z18.record(z18.string(), OutputDecl2).default({})
14277
+ var ImageMagickParams = z19.object({
14278
+ args: z19.array(z19.string()).min(1),
14279
+ outputs: z19.record(z19.string(), OutputDecl2).default({})
14133
14280
  }).strict();
14134
- var ImageMagickInputs = z18.record(z18.string(), z18.unknown());
14135
- var ImageMagickOutputs = z18.record(z18.string(), z18.custom());
14281
+ var ImageMagickInputs = z19.record(z19.string(), z19.unknown());
14282
+ var ImageMagickOutputs = z19.record(z19.string(), z19.custom());
14136
14283
  var resolvedBin;
14137
14284
  async function resolveBin() {
14138
14285
  if (resolvedBin) return resolvedBin;
@@ -14175,7 +14322,7 @@ var imagemagickNode = defineNode({
14175
14322
 
14176
14323
  // src/engine/nodes/local/staticNative.ts
14177
14324
  import { createRequire as createRequire4 } from "module";
14178
- import { z as z22 } from "zod";
14325
+ import { z as z23 } from "zod";
14179
14326
 
14180
14327
  // src/engine/nodes/local/lib/native-assets.ts
14181
14328
  import sharp from "sharp";
@@ -14292,7 +14439,7 @@ function leadingMark(opaque, box) {
14292
14439
  import { createHash as createHash2 } from "crypto";
14293
14440
  import { readFile as readFile15 } from "fs/promises";
14294
14441
  import sharp5 from "sharp";
14295
- import { z as z21 } from "zod";
14442
+ import { z as z22 } from "zod";
14296
14443
 
14297
14444
  // src/engine/landing-library/visualHash.ts
14298
14445
  import sharp2 from "sharp";
@@ -14698,7 +14845,7 @@ import { mkdtemp as mkdtemp7, readdir as readdir3, readFile as readFile14, rm as
14698
14845
  import { tmpdir as tmpdir7 } from "os";
14699
14846
  import path20 from "path";
14700
14847
  import { promisify as promisify7 } from "util";
14701
- import { z as z19 } from "zod";
14848
+ import { z as z20 } from "zod";
14702
14849
  var RENDER_REVIEW_MODEL = "~google/gemini-pro-latest";
14703
14850
  var RENDER_REVIEW_FRAMES = 12;
14704
14851
  var RENDER_REVIEW_WIDTH = 1024;
@@ -14715,6 +14862,27 @@ function usageFrom(model, body) {
14715
14862
  };
14716
14863
  }
14717
14864
  var WATCH_MAX_BYTES = 60 * 1024 * 1024;
14865
+ var STATIC_REVIEW_ITEMS = [
14866
+ "text",
14867
+ "logo",
14868
+ "layout",
14869
+ "safe_zone",
14870
+ "picture",
14871
+ "subject",
14872
+ "person",
14873
+ "people",
14874
+ "one_idea",
14875
+ "thumbnail",
14876
+ "concept",
14877
+ "viewer",
14878
+ "proof",
14879
+ "sizes",
14880
+ "native",
14881
+ "other"
14882
+ ];
14883
+ var FINDING_ELEMENTS = ["copy", "button", "logo", "scene_text", "picture", "other"];
14884
+ var FINDING_DEFECTS = ["cut", "missing", "unreadable", "other"];
14885
+ var FRAME_EDGES = ["top", "bottom", "left", "right"];
14718
14886
  function sceneAt(windows, atS) {
14719
14887
  return windows.find((w) => atS >= w.startS && atS < w.endS)?.file;
14720
14888
  }
@@ -14740,24 +14908,24 @@ function buildFrameGrabArgs(video, atS, out) {
14740
14908
  "-y"
14741
14909
  ];
14742
14910
  }
14743
- var FrameReview = z19.object({
14911
+ var FrameReview = z20.object({
14744
14912
  /** Something in shot that could not happen, or is being done wrong. */
14745
- impossible: z19.array(z19.string()).default([]),
14913
+ impossible: z20.array(z20.string()).default([]),
14746
14914
  /** Objects present in frame that belong to no part of the scene. */
14747
- strays: z19.array(z19.string()).default([])
14915
+ strays: z20.array(z20.string()).default([])
14748
14916
  });
14749
- var ReelReview = z19.object({
14917
+ var ReelReview = z20.object({
14750
14918
  /** Whether the people the script treats as one person stay one person. */
14751
- cast_is_consistent: z19.boolean().default(true),
14752
- cast_note: z19.string().default(""),
14919
+ cast_is_consistent: z20.boolean().default(true),
14920
+ cast_note: z20.string().default(""),
14753
14921
  /** Whether the advertiser's mark is visible anywhere. */
14754
- brand_mark_visible: z19.boolean().default(false),
14922
+ brand_mark_visible: z20.boolean().default(false),
14755
14923
  /** Whether the last frames close the ad rather than trailing off. */
14756
- ends_on_a_close: z19.boolean().default(false),
14757
- ending_note: z19.string().default(""),
14924
+ ends_on_a_close: z20.boolean().default(false),
14925
+ ending_note: z20.string().default(""),
14758
14926
  /** Whether the ad ever cuts away from the presenter to show anything. */
14759
- shows_more_than_a_face: z19.boolean().default(true),
14760
- shows_note: z19.string().default("")
14927
+ shows_more_than_a_face: z20.boolean().default(true),
14928
+ shows_note: z20.string().default("")
14761
14929
  });
14762
14930
  function buildFrameReviewPrompt(opts) {
14763
14931
  const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
@@ -14850,20 +15018,20 @@ function buildWatchReviewPrompt(opts) {
14850
15018
  4. DEFECTS. List anything else wrong, each with the exact timestamp where it happens \u2014 a moment maps to the shot that produced it, and a defect without one is a complaint rather than an instruction.
14851
15019
  Answer JSON only: {"spoken_variety_matches":true|false,"variety_note":"...","voice_matches_mouth":true|false,"mouth_note":"...","cuts_are_clean":true|false,"cuts_note":"...","defects":[{"at":"MM:SS.s","kind":"cut|lipsync|audio|visual","what":"..."}]}`;
14852
15020
  }
14853
- var WatchReview = z19.object({
14854
- spoken_variety_matches: z19.boolean(),
14855
- variety_note: z19.string().default(""),
14856
- voice_matches_mouth: z19.boolean(),
14857
- mouth_note: z19.string().default(""),
14858
- cuts_are_clean: z19.boolean(),
14859
- cuts_note: z19.string().default("")
15021
+ var WatchReview = z20.object({
15022
+ spoken_variety_matches: z20.boolean(),
15023
+ variety_note: z20.string().default(""),
15024
+ voice_matches_mouth: z20.boolean(),
15025
+ mouth_note: z20.string().default(""),
15026
+ cuts_are_clean: z20.boolean(),
15027
+ cuts_note: z20.string().default("")
14860
15028
  });
14861
- var WatchDefects = z19.object({
14862
- defects: z19.array(
14863
- z19.object({
14864
- at: z19.string().default(""),
14865
- kind: z19.string().default(""),
14866
- what: z19.string().default("")
15029
+ var WatchDefects = z20.object({
15030
+ defects: z20.array(
15031
+ z20.object({
15032
+ at: z20.string().default(""),
15033
+ kind: z20.string().default(""),
15034
+ what: z20.string().default("")
14867
15035
  })
14868
15036
  ).default([])
14869
15037
  });
@@ -14888,10 +15056,10 @@ function classifyWatchDefects(json, windows) {
14888
15056
  };
14889
15057
  });
14890
15058
  }
14891
- var FidelityReview = z19.object({
14892
- delivers_the_ask: z19.boolean(),
14893
- missing: z19.array(z19.string()).default([]),
14894
- contradicted: z19.array(z19.string()).default([])
15059
+ var FidelityReview = z20.object({
15060
+ delivers_the_ask: z20.boolean(),
15061
+ missing: z20.array(z20.string()).default([]),
15062
+ contradicted: z20.array(z20.string()).default([])
14895
15063
  });
14896
15064
  function buildFidelityPrompt(brief) {
14897
15065
  return `Someone asked for an advertisement in these words:
@@ -15058,7 +15226,7 @@ async function videoDuration(video) {
15058
15226
  return null;
15059
15227
  }
15060
15228
  }
15061
- async function askGateway(model, parts, timeoutMs, spent, reasoning) {
15229
+ async function askGateway(model, parts, timeoutMs, spent, reasoning, extra = {}) {
15062
15230
  const key = openRouterKey();
15063
15231
  if (!key) return null;
15064
15232
  try {
@@ -15069,7 +15237,8 @@ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
15069
15237
  model,
15070
15238
  messages: [{ role: "user", content: parts }],
15071
15239
  usage: { include: true },
15072
- ...reasoning ? { reasoning } : {}
15240
+ ...reasoning ? { reasoning } : {},
15241
+ ...extra
15073
15242
  }),
15074
15243
  signal: AbortSignal.timeout(timeoutMs)
15075
15244
  });
@@ -15133,9 +15302,16 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, r
15133
15302
  reasoning
15134
15303
  );
15135
15304
  }
15136
- async function askAboutText(model, prompt, onSpend) {
15305
+ async function askAboutText(model, prompt, onSpend, opts = {}) {
15137
15306
  if (!hasOpenRouterKey()) return null;
15138
- return await askGateway(model, [{ type: "text", text: prompt }], 6e4, (usage) => onSpend?.(usage));
15307
+ return await askGateway(
15308
+ model,
15309
+ [{ type: "text", text: prompt }],
15310
+ opts.timeoutMs ?? 6e4,
15311
+ (usage) => onSpend?.(usage),
15312
+ void 0,
15313
+ opts.request
15314
+ );
15139
15315
  }
15140
15316
  async function reviewRenderedVideo(opts) {
15141
15317
  if (!hasOpenRouterKey()) return null;
@@ -15254,14 +15430,14 @@ function unreviewedNotes(state) {
15254
15430
 
15255
15431
  // src/engine/nodes/local/lib/text-boxes.ts
15256
15432
  import sharp4 from "sharp";
15257
- import { z as z20 } from "zod";
15433
+ import { z as z21 } from "zod";
15258
15434
  var GRID = 1e3;
15259
- var ReaderItem = z20.object({
15260
- text: z20.string(),
15261
- kind: z20.enum(["text", "logo"]),
15262
- box_2d: z20.array(z20.number()).length(4)
15435
+ var ReaderItem = z21.object({
15436
+ text: z21.string(),
15437
+ kind: z21.enum(["text", "logo"]),
15438
+ box_2d: z21.array(z21.number()).length(4)
15263
15439
  });
15264
- var ReaderAnswer = z20.object({ items: z20.array(ReaderItem) });
15440
+ var ReaderAnswer = z21.object({ items: z21.array(ReaderItem) });
15265
15441
  var fraction = (value) => Math.min(1, Math.max(0, value / GRID));
15266
15442
  function textBoxesFromReader(answer) {
15267
15443
  const parsed = ReaderAnswer.safeParse(answer);
@@ -15379,32 +15555,39 @@ function judgeWinner(judge) {
15379
15555
  };
15380
15556
  return [...judge].sort(byGate)[0] ?? null;
15381
15557
  }
15382
- var StaticFinding = z21.object({
15383
- severity: z21.enum(["blocking", "warning"]),
15384
- format: z21.string().optional(),
15385
- what: z21.string().trim().min(1)
15558
+ var StaticFinding = z22.object({
15559
+ severity: z22.enum(["blocking", "warning"]),
15560
+ format: z22.string().optional(),
15561
+ what: z22.string().trim().min(1),
15562
+ item: z22.enum(STATIC_REVIEW_ITEMS).catch("other"),
15563
+ element: z22.enum(FINDING_ELEMENTS).catch("other"),
15564
+ defect: z22.enum(FINDING_DEFECTS).catch("other"),
15565
+ edges: z22.array(z22.enum(FRAME_EDGES)).catch([])
15386
15566
  });
15387
- var ABSENCE_NEVER_A_FINDING = /\b(?:missing|no|lacks?|without)\s+(?:an?\s+|any\s+|clear\s+|visible\s+)?(?:call to action|cta)\b|\b(?:does not|doesn't) (?:show|tell) the viewer what to do/i;
15388
- var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
15389
- var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
15390
- var BLURRED_FIGURES = /\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b(?=[^.]*\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b)|\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b(?=[^.]*\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b)/i;
15567
+ var ALWAYS_BLOCKING = /* @__PURE__ */ new Set(["subject", "person"]);
15391
15568
  function classifyStaticFindings(json) {
15392
15569
  const findings = json?.findings;
15393
15570
  if (!Array.isArray(findings)) return [];
15394
15571
  return findings.flatMap((raw) => {
15395
15572
  const parsed = StaticFinding.safeParse(raw);
15396
15573
  if (!parsed.success) return [];
15397
- const { severity, format, what } = parsed.data;
15398
- if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
15399
- const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
15400
- return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
15574
+ const { severity, format, what, ...fields } = parsed.data;
15575
+ const kind = {
15576
+ item: fields.item,
15577
+ element: fields.element,
15578
+ defect: fields.defect,
15579
+ edges: [...new Set(fields.edges)]
15580
+ };
15581
+ if (kind.element === "button" && kind.defect === "missing") return [];
15582
+ const graded = ALWAYS_BLOCKING.has(kind.item) ? "blocking" : kind.element === "scene_text" && kind.defect === "unreadable" ? "warning" : severity;
15583
+ return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what, kind }];
15401
15584
  });
15402
15585
  }
15403
- var CopyRead = z21.object({
15404
- format: z21.string().trim().min(1),
15405
- string: z21.string(),
15586
+ var CopyRead = z22.object({
15587
+ format: z22.string().trim().min(1),
15588
+ string: z22.string(),
15406
15589
  /** What the model read where the string should be; null when it found nothing like it. */
15407
- seen: z21.string().nullable()
15590
+ seen: z22.string().nullable()
15408
15591
  });
15409
15592
  function comparableCopy(text2) {
15410
15593
  return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
@@ -15575,12 +15758,12 @@ function measuredFindings(input) {
15575
15758
  if (!input.adapted || loadBearing(l)) return [blocking(said(l))];
15576
15759
  return l.similarity >= MEANING_MATCH ? [] : [{ severity: "warning", what: `${format}: ${said(l)}` }];
15577
15760
  }),
15578
- ...extraWordFindings(format, extra, input.boxes, [...copyBoxes]),
15761
+ ...extraWordFindings(format, extra, input.boxes, [...copyBoxes], input.offers ?? /* @__PURE__ */ new Set()),
15579
15762
  ...twice.map(
15580
15763
  (t) => ({ severity: "warning", what: `${format}: "${t}" is on the ad twice; keep it once.` })
15581
15764
  ),
15582
15765
  ...misspelt.map((m) => blocking(`the brand's name reads "${m.read}", not "${m.name}".`)),
15583
- ...unbadgedClaims(input.lines, input.boxes).map(
15766
+ ...unbadgedClaims(input.lines, input.boxes, input.cited).map(
15584
15767
  (c) => blocking(
15585
15768
  `"${c.line}" names ${c.platform}, but ${c.platform}'s logo is not on the ad: show its real badge, or drop the claim.`
15586
15769
  )
@@ -15664,9 +15847,9 @@ function misspeltNames(texts, names) {
15664
15847
  }
15665
15848
  return out;
15666
15849
  }
15667
- function unbadgedClaims(lines, boxes) {
15850
+ function unbadgedClaims(lines, boxes, cited) {
15668
15851
  return lines.flatMap((line) => {
15669
- const platform = claimedPlatform(line.text);
15852
+ const platform = citedBy(line.text, cited);
15670
15853
  if (!platform) return [];
15671
15854
  const naming = boxes.filter((b) => b.kind === "text" && namedPlatform(b.text) === platform);
15672
15855
  const mentions = new RegExp(platform.pattern.source, `${platform.pattern.flags}g`);
@@ -15782,10 +15965,11 @@ function contextItems(opts) {
15782
15965
  opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the lead line (the largest words) cannot be read. Judge only whether those words can be read, nothing else. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line (the largest words) would be unreadable on a phone feed. That is blocking."
15783
15966
  ],
15784
15967
  sizes: opts.formats.length > 1 ? [
15785
- `12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; start the finding with "SIZES:" and report it on the size that differs). Rearranged for its shape is fine.`
15968
+ "12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; report it on the size that differs). Rearranged for its shape is fine."
15786
15969
  ] : []
15787
15970
  };
15788
15971
  }
15972
+ var FINDING_FIELDS = `For each finding also give: "item", the item above it answers, in lower case (TEXT \u2192 "text", ONE IDEA \u2192 "one_idea", the 9:16 safe-zone item \u2192 "safe_zone"); "element", what on the ad it is about ("copy" the ad's own words, "button", "logo", "scene_text" words or figures printed on something in the picture that are not the copy, "picture" anything else drawn, else "other"); "defect" ("cut" when the frame's edge cuts it, "missing" when it is not there, "unreadable" when it is too blurred or small to read, else "other"); "edges", the frame edges that cut it, [] unless "cut". Write "what" without a label.`;
15789
15973
  function buildStaticReviewPrompt(opts) {
15790
15974
  const brand = opts.brand ? ` for ${opts.brand}` : "";
15791
15975
  const order = opts.formats.join(", ");
@@ -15804,9 +15988,9 @@ function buildStaticReviewPrompt(opts) {
15804
15988
  ...storiesItem(opts),
15805
15989
  // Tier 0, Oct 2026: the real defects were where a size was extended; 8 of 8 hand claims checked were wrong.
15806
15990
  "5. PICTURE: a ghost, a smear, a seam, a blurred strip, a duplicated or mirrored object, or garbled pseudo-text in focus where a viewer reads it, typically where the picture was extended to fill a size (blocking). Text out of focus in the background (a screen, a shelf of books) is the scene; an ordinary object a viewer would not stop at on a phone is not malformed.",
15807
- `5a. SUBJECT: the ad's subject (its pet, person or product) shown more than once as visibly different ones where the ad means one, or a second, different subject beside it that leaves a viewer asking whose story it is (blocking; start the finding with "SUBJECT:").`,
15991
+ "5a. SUBJECT: the ad's subject (its pet, person or product) shown more than once as visibly different ones where the ad means one, or a second, different subject beside it that leaves a viewer asking whose story it is (blocking).",
15808
15992
  ...opts.uncastPeople && opts.uncastPeople.length > 0 ? [
15809
- `5c. PERSON: the copy names ${opts.uncastPeople.join(" and ")}, real, and this ad was given no photo of them: a face shown as them or beside their name is a stranger wearing that name (blocking; start the finding with "PERSON:").`
15993
+ `5c. PERSON: the copy names ${opts.uncastPeople.join(" and ")}, real, and this ad was given no photo of them: a face shown as them or beside their name is a stranger wearing that name (blocking).`
15810
15994
  ] : [],
15811
15995
  "5b. PEOPLE: a person's hands, face or body drawn so wrong a viewer would notice at a glance on a phone (warning). Never a detail you have to zoom in to see.",
15812
15996
  "6. ONE IDEA: two or more elements compete equally for the eye, so no single promise leads (warning).",
@@ -15820,7 +16004,8 @@ function buildStaticReviewPrompt(opts) {
15820
16004
  ] : [],
15821
16005
  "Blocking = it cannot ship as is (a broken logo, clipped content, a ghost or smear in the picture). Warning = it can ship but should improve.",
15822
16006
  "Never report an element as missing because ads usually have one: judge only what this concept holds. An empty findings list is the right answer for a clean ad. Keep each item under twenty words.",
15823
- `Reply with only this JSON: {"findings":[{"severity":"blocking|warning","format":"4:5","what":"..."}]${copyShape}}`
16007
+ FINDING_FIELDS,
16008
+ `Reply with only this JSON: {"findings":[{"severity":"blocking|warning","format":"4:5","item":"${STATIC_REVIEW_ITEMS.join("|")}","element":"${FINDING_ELEMENTS.join("|")}","defect":"${FINDING_DEFECTS.join("|")}","edges":["${FRAME_EDGES.join("|")}"],"what":"..."}]${copyShape}}`
15824
16009
  ].join("\n");
15825
16010
  }
15826
16011
  var SCENE_PRINT_SCALE = 0.45;
@@ -15834,10 +16019,10 @@ function mostOfALine(chunk, copy) {
15834
16019
  return words.length > 0 && words.filter((w) => said.has(w)).length / words.length >= LINE_REPEAT_SHARE;
15835
16020
  });
15836
16021
  }
15837
- var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
16022
+ var SIGN_CLAIM = /[%\p{Sc}]/u;
15838
16023
  var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
15839
16024
  var COPY_SIZE_SHARE = 0.8;
15840
- function extraWordFindings(format, extra, boxes, copyBoxes) {
16025
+ function extraWordFindings(format, extra, boxes, copyBoxes, offers) {
15841
16026
  if (extra.length === 0) return [];
15842
16027
  const heights = copyBoxes.map((b) => b.y1 - b.y0).filter((h) => h > 0);
15843
16028
  const smallest = heights.length > 0 ? Math.min(...heights) : 0;
@@ -15850,7 +16035,9 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15850
16035
  const usual = sorted[Math.floor(sorted.length / 2)] ?? 0;
15851
16036
  const underCopy = extra.filter((chunk) => height(chunk) < usual * COPY_SIZE_SHARE);
15852
16037
  const scenery = (chunk) => height(chunk) < smallest * SCENE_PRINT_SCALE || underCopy.length >= SCENE_BLOCK_LINES && underCopy.includes(chunk);
15853
- const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
16038
+ const offered = new Set([...offers].map(normalizeCopy));
16039
+ const claims = (chunk) => SIGN_CLAIM.test(chunk) || offered.has(normalizeCopy(chunk));
16040
+ const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!claims(chunk) || scenery(chunk));
15854
16041
  const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
15855
16042
  const asCopy = extra.filter((chunk) => !printed(chunk));
15856
16043
  const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
@@ -15915,41 +16102,29 @@ async function samePictureSizes(current, previous) {
15915
16102
  return same;
15916
16103
  }
15917
16104
  var AT_EDGE = 0.025;
15918
- var CLIPPED = /\b(?:clipped|cut off|cut by|cropped|runs? off)\b|\btouch(?:es|ing)?\s+(?:the\s+)?(?:(?:top|bottom|left|right|frame)\b|edge)/i;
15919
- var ABOUT_COPY = /\b(?:headline|text|copy|line|words?|title|subhead(?:line)?|tagline|price|letters?)\b|["“«]/i;
15920
- var EDGE_WORD = [
15921
- ["top", /\btop\b/i],
15922
- ["bottom", /\bbottom\b/i],
15923
- ["left", /\bleft\b/i],
15924
- ["right", /\bright\b/i]
15925
- ];
15926
16105
  var clearanceTo = (b, edge) => edge === "top" ? b.y0 : edge === "bottom" ? 1 - b.y1 : edge === "left" ? b.x0 : 1 - b.x1;
16106
+ function measuredBoxes(element, boxes, buttons) {
16107
+ if (element === "logo") return boxes.filter((b) => b.kind === "logo");
16108
+ if (element === "button") return buttons.flatMap((l) => boxesOfLine(l, boxes));
16109
+ return element === "copy" ? boxes.filter((b) => b.kind === "text") : [];
16110
+ }
16111
+ var cutByFrame = (f) => f.kind?.defect === "cut";
15927
16112
  function corroborateEdgeClaims(findings, boxesByFormat, lines) {
15928
16113
  const buttons = lines.filter((l) => /^(cta|button|call_to_action)$/.test(roleKey2(l.role)));
15929
16114
  return findings.map((f) => {
15930
- if (f.severity !== "blocking" || !CLIPPED.test(f.what)) return f;
16115
+ if (f.severity !== "blocking" || !f.kind || !cutByFrame(f)) return f;
15931
16116
  const format = f.what.match(/^([\d.]+:[\d.]+):/)?.[1];
15932
16117
  const boxes = format ? boxesByFormat[format] : void 0;
15933
16118
  if (!boxes) return f;
15934
- const element = claimedBoxes(f.what, boxes, buttons);
15935
- if (element.length === 0) return f;
15936
- const named = claimedEdges(f.what);
15937
- const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
15938
- const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
16119
+ const { element } = f.kind;
16120
+ const measured = measuredBoxes(element, boxes, buttons);
16121
+ if (measured.length === 0) return f;
16122
+ const named = f.kind.edges.length > 0 ? f.kind.edges : ["top", "bottom", "left", "right"];
16123
+ const pad = element === "button" ? BUTTON_PAD : 0;
16124
+ const atEdge = measured.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
15939
16125
  return atEdge ? f : { ...f, severity: "warning" };
15940
16126
  });
15941
16127
  }
15942
- function claimedEdges(what) {
15943
- const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
15944
- return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
15945
- }
15946
- var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
15947
- var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
15948
- function claimedBoxes(what, boxes, buttons) {
15949
- if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
15950
- if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15951
- return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
15952
- }
15953
16128
  async function bytesOf(source) {
15954
16129
  if (!/^https?:\/\//i.test(source)) return await readFile15(source);
15955
16130
  const res = await fetch(source);
@@ -15997,12 +16172,25 @@ function fragmentOf(chunk, own, opts = {}) {
15997
16172
  return shared / words.length >= share;
15998
16173
  });
15999
16174
  }
16175
+ var OFFER_MADE = 0.35;
16176
+ var OFFER_QUESTION = "Does `words` make an offer of its own: something free, a discount, a sale, a promotion, a deal or a price, in whatever language it is written? A sign, a label, a name or a screen's words are not an offer.";
16177
+ var offerQuestion = (words) => ({ instructions: { words, question: OFFER_QUESTION } });
16178
+ async function offersAmong(chunks, judge) {
16179
+ const worded = chunks.filter((c) => new RegExp("\\p{L}", "u").test(c));
16180
+ if (worded.length === 0) return /* @__PURE__ */ new Set();
16181
+ const judged = await judge(worded);
16182
+ return new Set(worded.filter((_, i) => (judged?.[i] ?? 0) >= OFFER_MADE));
16183
+ }
16000
16184
  async function checkStaticImage(image, opts) {
16001
16185
  const reading = await (opts.read ?? ((bytes) => readTextBoxes(bytes, { onSpend: opts.onSpend })))(image);
16002
16186
  if (reading.status === "not_checked") return reading;
16003
16187
  const palette = opts.palette ?? [];
16004
16188
  const inks = palette.length > 0 ? await designedInks(image, opts.lines, reading.boxes) : [];
16005
16189
  const boxes = opts.toFrame ? opts.toFrame(reading.boxes) : reading.boxes;
16190
+ const offers = await offersAmong(
16191
+ textMismatch(opts.lines, boxes, { allowed: opts.allowed }).extra,
16192
+ opts.judgeOffers ?? ((chunks) => askJev(chunks.map(offerQuestion), opts.onSpend))
16193
+ );
16006
16194
  const findings = measuredFindings({
16007
16195
  format: opts.format,
16008
16196
  boxes,
@@ -16014,7 +16202,12 @@ async function checkStaticImage(image, opts) {
16014
16202
  ...opts.native ? { native: true } : {},
16015
16203
  ...opts.placements ? { placements: opts.placements } : {},
16016
16204
  ...opts.band ? { band: opts.band } : {},
16017
- offBrand: offBrandInks(inks, palette)
16205
+ offBrand: offBrandInks(inks, palette),
16206
+ offers,
16207
+ cited: opts.cited ?? await citedPlatforms(
16208
+ opts.lines.map((l) => l.text),
16209
+ opts.judgeCitations ?? jevCitationJudge(opts.onSpend)
16210
+ )
16018
16211
  });
16019
16212
  return { status: "checked", findings, boxes };
16020
16213
  }
@@ -16078,7 +16271,7 @@ function everySizeChecks(opts) {
16078
16271
  };
16079
16272
  }
16080
16273
  async function confirmBlocking(findings, stills, formats, ask2) {
16081
- const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
16274
+ const claims = findings.filter((f) => f.severity === "blocking" && !cutByFrame(f));
16082
16275
  if (claims.length === 0 || stills.length === 0) return [...findings];
16083
16276
  const prompt = [
16084
16277
  `These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
@@ -16107,12 +16300,17 @@ async function reviewRenderedStatic(opts) {
16107
16300
  return askAboutStills(shown, prompt, opts.onSpend);
16108
16301
  };
16109
16302
  const asking = ask2(true);
16303
+ const cited = await citedPlatforms(
16304
+ lines.map((l) => l.text),
16305
+ jevCitationJudge(opts.onSpend)
16306
+ );
16110
16307
  const checks = await Promise.all(
16111
16308
  originals.map(
16112
16309
  (bytes, i) => checkStaticImage(bytes, {
16113
16310
  ...scrolledOff(every, opts.scrolled?.[formats[i] ?? ""] ?? []),
16114
16311
  format: formats[i] ?? "",
16115
- adapted: opts.masterFormat !== void 0 && formats[i] !== opts.masterFormat
16312
+ adapted: opts.masterFormat !== void 0 && formats[i] !== opts.masterFormat,
16313
+ cited
16116
16314
  })
16117
16315
  )
16118
16316
  );
@@ -16153,26 +16351,28 @@ var staticNativeNode = defineNode({
16153
16351
  location: "local",
16154
16352
  summary: "Draw a static ad that is a screen people know (a post on X, a WhatsApp or iMessage chat, a wall of Trustpilot or Google reviews, a note, a Slack message) exactly, from HTML, at one delivered size: the `native` block of the ad spec (prompt.json) is laid out in the platform's own layout, type and colours, with the brand's real logo and the photos given, fitted inside the size's safe area. Free and deterministic.",
16155
16353
  when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` for an ad spec with a `native` block, one node per size (`native_<size>`, role `static_native`). Edit prompt.json's `native` and run again: nothing is billed but the photos whose description changed.",
16156
- inputs: z22.object({
16354
+ inputs: z23.object({
16157
16355
  spec: JsonRef,
16158
16356
  /** The brand's logo files (every variant the brand has): the one that reads on the screen is used. */
16159
- logos: z22.array(ImageRef).optional(),
16357
+ logos: z23.array(ImageRef).optional(),
16160
16358
  /** The brand's square mark, for an avatar; else cut from the logo's lockup. */
16161
16359
  mark: ImageRef.optional(),
16162
16360
  /** The brand's face, for the frame's headline. */
16163
16361
  font: FontRef.optional(),
16164
16362
  /** The photos the screen shows, in `params.photo_ids` order. */
16165
- photos: z22.array(ImageRef).optional()
16363
+ photos: z23.array(ImageRef).optional()
16166
16364
  }).strict(),
16167
- params: z22.object({
16168
- format: z22.string().min(1),
16169
- placements: z22.array(z22.string().min(1)).optional(),
16170
- photo_ids: z22.array(z22.string().regex(/^PHOTO_\d+$/)).default([])
16365
+ params: z23.object({
16366
+ format: z23.string().min(1),
16367
+ placements: z23.array(z23.string().min(1)).optional(),
16368
+ photo_ids: z23.array(z23.string().regex(/^PHOTO_\d+$/)).default([]),
16369
+ /** The interface's own words written for the viewer's language, when no checked ones exist (`needsInterfaceWords`). */
16370
+ interface_words: z23.record(z23.string(), z23.string()).optional()
16171
16371
  }).strict(),
16172
- outputs: z22.object({
16372
+ outputs: z23.object({
16173
16373
  image: ImageRef,
16174
16374
  // A chat's earliest lines this size scrolled up under its header to keep the rest readable.
16175
- scrolled: z22.array(z22.string()).optional()
16375
+ scrolled: z23.array(z23.string()).optional()
16176
16376
  }).strict(),
16177
16377
  outputKinds: { image: "image" },
16178
16378
  cost: () => ({ credits: 0, seconds_estimate: 4 }),
@@ -16231,6 +16431,7 @@ async function nativePage(spec, files, params) {
16231
16431
  const native = spec.native;
16232
16432
  if (!native) throw new Error("static_native: prompt.json has no `native` block to draw");
16233
16433
  const size2 = deliveredSize(params.format);
16434
+ const words = readInterfaceWords(params.interface_words);
16234
16435
  const assets = await pageAssets(
16235
16436
  files,
16236
16437
  params.photo_ids,
@@ -16246,7 +16447,8 @@ async function nativePage(spec, files, params) {
16246
16447
  ...spec.brand?.ink ? { ink: spec.brand.ink } : {},
16247
16448
  ...spec.brand?.accent ? { accent: spec.brand.accent } : {}
16248
16449
  },
16249
- ...spec.viewer?.market?.language ? { language: spec.viewer.market.language } : {}
16450
+ ...spec.viewer?.market?.language ? { language: spec.viewer.market.language } : {},
16451
+ ...words ? { words } : {}
16250
16452
  });
16251
16453
  }
16252
16454
  async function screenshot(html, size2, log) {
@@ -16270,13 +16472,13 @@ async function screenshot(html, size2, log) {
16270
16472
  }
16271
16473
 
16272
16474
  // src/engine/nodes/local/staticPlace.ts
16273
- import { z as z25 } from "zod";
16475
+ import { z as z26 } from "zod";
16274
16476
 
16275
16477
  // src/engine/nodes/remote/image.ts
16276
- import { z as z24 } from "zod";
16478
+ import { z as z25 } from "zod";
16277
16479
 
16278
16480
  // src/engine/nodes/remote/registry-params.ts
16279
- import { z as z23 } from "zod";
16481
+ import { z as z24 } from "zod";
16280
16482
  function registryParamShape(kind, overrides = {}) {
16281
16483
  const specs = Object.values(MODEL_REGISTRY[kind]);
16282
16484
  const byParam = /* @__PURE__ */ new Map();
@@ -16297,7 +16499,7 @@ function unionOf(schemas) {
16297
16499
  const kinds = [...new Set(schemas.map((schema) => schema.kind))];
16298
16500
  if (kinds.length > 1) {
16299
16501
  const [first, second, ...rest] = kinds.map((k) => unionOf(schemas.filter((s) => s.kind === k)));
16300
- return z23.union([first, second, ...rest]);
16502
+ return z24.union([first, second, ...rest]);
16301
16503
  }
16302
16504
  switch (kinds[0]) {
16303
16505
  case "string":
@@ -16305,32 +16507,32 @@ function unionOf(schemas) {
16305
16507
  case "number":
16306
16508
  return numberUnion(schemas);
16307
16509
  case "boolean":
16308
- return z23.boolean();
16510
+ return z24.boolean();
16309
16511
  default:
16310
- return z23.unknown();
16512
+ return z24.unknown();
16311
16513
  }
16312
16514
  }
16313
16515
  function stringUnion(schemas) {
16314
- if (schemas.some((schema) => !schema.enum)) return z23.string();
16516
+ if (schemas.some((schema) => !schema.enum)) return z24.string();
16315
16517
  const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))];
16316
- return z23.enum(values);
16518
+ return z24.enum(values);
16317
16519
  }
16318
16520
  function numberUnion(schemas) {
16319
- if (schemas.some((schema) => !schema.enum)) return z23.number();
16521
+ if (schemas.some((schema) => !schema.enum)) return z24.number();
16320
16522
  const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))].sort((a, b) => a - b);
16321
- return z23.literal(values);
16523
+ return z24.literal(values);
16322
16524
  }
16323
16525
 
16324
16526
  // src/engine/nodes/remote/image.ts
16325
- var imageModelEnum = z24.enum(IMAGE_GENERATE_MODELS);
16326
- var ImageGenerateParams = z24.object({
16527
+ var imageModelEnum = z25.enum(IMAGE_GENERATE_MODELS);
16528
+ var ImageGenerateParams = z25.object({
16327
16529
  model: imageModelEnum,
16328
16530
  ...registryParamShape("image_generate", {
16329
- prompt: z24.string().min(1),
16531
+ prompt: z25.string().min(1),
16330
16532
  // Recraft v4 vector controls, sent as the provider's own `controls` block. The
16331
16533
  // registry types them as JSON; this is their shape.
16332
- rgb_colors: z24.array(z24.array(z24.number().int().min(0).max(255))),
16333
- background_rgb_color: z24.array(z24.number().int().min(0).max(255))
16534
+ rgb_colors: z25.array(z25.array(z25.number().int().min(0).max(255))),
16535
+ background_rgb_color: z25.array(z25.number().int().min(0).max(255))
16334
16536
  })
16335
16537
  }).strict();
16336
16538
  var PRO_QUALITY = "max";
@@ -16348,9 +16550,9 @@ var imageGenerateNode = delegated({
16348
16550
  when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-image-2` for photorealistic and in-image text (NOTE: it has no `image_size`, and no 4:5 / 5:4 \u2014 route portrait ad formats through Gemini or `image_aspect_adapt`); Gemini variants for fast iteration, extreme ratios, and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order. `n` (only on models that declare it, e.g. `openai/gpt-image-2.5-sunburst`) returns several candidates of the same brief from one call, billed per image like separate calls; wire the whole `images` array into `image_select` to keep the best.",
16349
16551
  // `reference` is one image or an ordered array of images. The backend forwards
16350
16552
  // each as a separate `image_url` to the provider (OpenRouter accepts many).
16351
- inputs: z24.object({ reference: z24.union([ImageRef, z24.array(ImageRef).min(1)]).optional() }).loose(),
16553
+ inputs: z25.object({ reference: z25.union([ImageRef, z25.array(ImageRef).min(1)]).optional() }).loose(),
16352
16554
  params: ImageGenerateParams,
16353
- outputs: z24.object({ images: z24.array(ImageRef).min(1) }).strict(),
16555
+ outputs: z25.object({ images: z25.array(ImageRef).min(1) }).strict(),
16354
16556
  outputKinds: { images: "image" },
16355
16557
  // Per image: `n` asks one call for several, and the provider bills each (measured: n=3
16356
16558
  // cost exactly 3x n=1, prompt tokens included). Usage-billed, so this is only the pre-run
@@ -16480,6 +16682,9 @@ var MIN_EXTEND_SCALE = 0.6;
16480
16682
  var GREY_LEVEL = 128;
16481
16683
  var GREY = { r: GREY_LEVEL, g: GREY_LEVEL, b: GREY_LEVEL };
16482
16684
  var FEATHER = 0.012;
16685
+ var KEEP_PAD = 0.3;
16686
+ var KEEP_FEATHER = 0.03;
16687
+ var MOVED = 120;
16483
16688
  function ratioOf2(ratio) {
16484
16689
  const [w = 1, h = 1] = ratio.split(":").map(Number);
16485
16690
  return w / h;
@@ -16542,14 +16747,16 @@ function clearBusyMargins(rgba, frame, keep) {
16542
16747
  }
16543
16748
  return shown;
16544
16749
  }
16545
- async function mergeExtended(extended, placed, ratio) {
16750
+ async function mergeExtended(extended, placed, ratio, keep = []) {
16546
16751
  const ad = await sharp7(placed).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
16547
16752
  const { width, height } = ad.info;
16548
16753
  const bounds = placedBounds(ad.data, width, height);
16549
16754
  if (!bounds) return sharp7(placed).removeAlpha().png().toBuffer();
16550
16755
  const canvas = renderCanvas({ width, height }, ratio);
16551
16756
  const paint = await sharp7(extended).removeAlpha().resize(canvas.width, canvas.height, { fit: "fill" }).extract({ left: canvas.left, top: canvas.top, width, height }).raw().toBuffer();
16552
- const weight = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
16757
+ const edge = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
16758
+ const boxes = keep.length > 0 && keptInPlace(keep, ad.data, paint, width, height) ? keptWeights(keep, width, height) : null;
16759
+ const weight = boxes ? (x, y) => Math.min(edge(x, y), boxes[y * width + x] ?? 0) : edge;
16553
16760
  const out = Buffer.alloc(width * height * 3);
16554
16761
  for (let y = 0; y < height; y++) {
16555
16762
  for (let x = 0; x < width; x++) {
@@ -16588,32 +16795,81 @@ function edgeWeight(b, width, height, feather) {
16588
16795
  ramp(b.y1 - y, b.y1 === height - 1)
16589
16796
  );
16590
16797
  }
16798
+ function keptWeights(keep, width, height) {
16799
+ const out = new Float32Array(width * height);
16800
+ const feather = Math.max(1, KEEP_FEATHER * width);
16801
+ for (const box of keep) {
16802
+ const pad = KEEP_PAD * (box.y1 - box.y0) * height;
16803
+ const x0 = box.x0 * width - pad;
16804
+ const x1 = box.x1 * width + pad;
16805
+ const y0 = box.y0 * height - pad;
16806
+ const y1 = box.y1 * height + pad;
16807
+ const ys = Math.max(0, Math.floor(y0 - feather));
16808
+ const ye = Math.min(height - 1, Math.ceil(y1 + feather));
16809
+ const xs = Math.max(0, Math.floor(x0 - feather));
16810
+ const xe = Math.min(width - 1, Math.ceil(x1 + feather));
16811
+ for (let y = ys; y <= ye; y++) {
16812
+ const dy = Math.max(0, y0 - y, y - y1);
16813
+ for (let x = xs; x <= xe; x++) {
16814
+ const dx = Math.max(0, x0 - x, x - x1);
16815
+ const t = Math.max(0, 1 - Math.hypot(dx, dy) / feather);
16816
+ const w = t * t * (3 - 2 * t);
16817
+ const i = y * width + x;
16818
+ if (w > (out[i] ?? 0)) out[i] = w;
16819
+ }
16820
+ }
16821
+ }
16822
+ return out;
16823
+ }
16824
+ function keptInPlace(keep, ad, paint, width, height) {
16825
+ const diffs = keep.map((box) => boxDiff(box, ad, paint, width, height)).filter((d) => d !== null).sort((a, b) => a - b);
16826
+ return (diffs[Math.floor(diffs.length / 2)] ?? Number.POSITIVE_INFINITY) <= MOVED;
16827
+ }
16828
+ function boxDiff(box, ad, paint, width, height) {
16829
+ let sum = 0;
16830
+ let n = 0;
16831
+ const xs = Math.floor(box.x0 * width);
16832
+ const xe = Math.min(width - 1, Math.ceil(box.x1 * width));
16833
+ for (let y = Math.floor(box.y0 * height); y <= Math.min(height - 1, Math.ceil(box.y1 * height)); y++) {
16834
+ for (let x = xs; x <= xe; x++) {
16835
+ const i = y * width + x;
16836
+ if ((ad[i * 4 + 3] ?? 0) === 0) continue;
16837
+ for (let c = 0; c < 3; c++) sum += Math.abs((ad[i * 4 + c] ?? 0) - (paint[i * 3 + c] ?? 0));
16838
+ n++;
16839
+ }
16840
+ }
16841
+ return n > 0 ? sum / n : null;
16842
+ }
16591
16843
 
16592
16844
  // src/engine/nodes/local/staticPlace.ts
16593
- var Usage = z25.object({ model: z25.string(), inputTokens: z25.number(), outputTokens: z25.number(), costUsd: z25.number() }).strict();
16594
- var Fit = z25.object({ scale: z25.number(), x: z25.number(), y: z25.number() }).strict();
16595
- var RenderRatio = z25.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
16596
- var Copy = z25.array(z25.object({ text: z25.string().min(1), role: z25.string().optional() }).strict());
16845
+ var Usage = z26.object({ model: z26.string(), inputTokens: z26.number(), outputTokens: z26.number(), costUsd: z26.number() }).strict();
16846
+ var Fit = z26.object({ scale: z26.number(), x: z26.number(), y: z26.number() }).strict();
16847
+ var Unit = z26.number().min(0).max(1);
16848
+ var Span = z26.object({ x0: Unit, y0: Unit, x1: Unit, y1: Unit }).strict();
16849
+ var RenderRatio = z26.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
16850
+ var Copy = z26.array(z26.object({ text: z26.string().min(1), role: z26.string().optional() }).strict());
16597
16851
  var staticPlaceNode = defineNode({
16598
16852
  id: "static_place",
16599
- version: "1.0.0",
16853
+ version: "1.1.0",
16600
16854
  category: "image",
16601
16855
  location: "local",
16602
- summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
16856
+ summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint; `keep` is where every line, the logo and the buttons read sit in `placed`, all the merge lays back of the ad's own pixels. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
16603
16857
  when_to_use: "Added by a static ad's autofix round (place-and-extend) before an `image_generate` that paints the margins and a `static_extend_merge`; never scaffolded. One box-reader call (about $0.005).",
16604
- inputs: z25.object({ image: ImageRef }).strict(),
16605
- params: z25.object({
16606
- format: z25.string().min(1),
16607
- placements: z25.array(z25.string().min(1)).optional(),
16858
+ inputs: z26.object({ image: ImageRef }).strict(),
16859
+ params: z26.object({
16860
+ format: z26.string().min(1),
16861
+ placements: z26.array(z26.string().min(1)).optional(),
16608
16862
  render_ratio: RenderRatio,
16609
16863
  copy: Copy.optional()
16610
16864
  }).strict(),
16611
- outputs: z25.object({
16865
+ outputs: z26.object({
16612
16866
  placed: ImageRef,
16613
16867
  shown: ImageRef,
16614
16868
  fit: Fit.nullable(),
16615
- reason: z25.enum(["not_read", "inside_band", "too_far"]).optional(),
16616
- usage: z25.array(Usage).optional()
16869
+ /** Where the words, logo and buttons read sit in `placed`: all the merge lays back of the ad's own pixels. */
16870
+ keep: z26.array(Span).optional(),
16871
+ reason: z26.enum(["not_read", "inside_band", "too_far"]).optional(),
16872
+ usage: z26.array(Usage).optional()
16617
16873
  }).strict(),
16618
16874
  outputKinds: { placed: "image", shown: "image" },
16619
16875
  cost: () => ({ credits: 0, seconds_estimate: 5 }),
@@ -16654,20 +16910,30 @@ var staticPlaceNode = defineNode({
16654
16910
  placed: placedRef,
16655
16911
  shown: shownRef,
16656
16912
  fit,
16913
+ keep: keptBoxes([...reading.boxes, ...boxes.filter((b) => !reading.boxes.includes(b))], fit),
16657
16914
  ...spend
16658
16915
  };
16659
16916
  }
16660
16917
  });
16918
+ function keptBoxes(boxes, fit) {
16919
+ const at2 = (offset2, v) => Math.min(1, Math.max(0, offset2 + fit.scale * v));
16920
+ return boxes.map((b) => ({ x0: at2(fit.x, b.x0), y0: at2(fit.y, b.y0), x1: at2(fit.x, b.x1), y1: at2(fit.y, b.y1) }));
16921
+ }
16661
16922
  var staticExtendMergeNode = defineNode({
16662
16923
  id: "static_extend_merge",
16663
- version: "1.1.0",
16924
+ version: "1.2.0",
16664
16925
  category: "image",
16665
16926
  location: "local",
16666
- summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad (`placed`) back over the painting: the ad's own pixels where it sits, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
16927
+ summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad's own pixels back over the painting only in `keep` (`static_place`'s boxes of the words, logo and buttons, padded and feathered): the painting is the rest of the frame, one picture with no edge where the shrunk ad was. Without `keep`, or when the painting moved the words, the whole ad (`placed`) is laid back, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
16667
16928
  when_to_use: "Added by a static ad's autofix round after a `static_place`; never scaffolded. One picture at the painter's price, only for a size that was placed.",
16668
- inputs: z25.object({ placed: ImageRef, shown: ImageRef.optional(), image: ImageRef.optional() }).strict(),
16669
- params: z25.object({ render_ratio: RenderRatio, paint: z25.record(z25.string(), z25.unknown()).optional() }).strict(),
16670
- outputs: z25.object({ image: ImageRef }).strict(),
16929
+ inputs: z26.object({
16930
+ placed: ImageRef,
16931
+ shown: ImageRef.optional(),
16932
+ image: ImageRef.optional(),
16933
+ keep: z26.array(Span).optional()
16934
+ }).strict(),
16935
+ params: z26.object({ render_ratio: RenderRatio, paint: z26.record(z26.string(), z26.unknown()).optional() }).strict(),
16936
+ outputs: z26.object({ image: ImageRef }).strict(),
16671
16937
  outputKinds: { image: "image" },
16672
16938
  cost: ({ params }) => params.paint ? imageGenerateNode.cost?.({ params: params.paint }) ?? NO_COST : NO_COST,
16673
16939
  async execute({ inputs, params, ctx }) {
@@ -16675,7 +16941,7 @@ var staticExtendMergeNode = defineNode({
16675
16941
  const painted = painting?.path ?? painting?.url;
16676
16942
  const placed = inputs.placed.path ?? inputs.placed.url;
16677
16943
  if (!painted || !placed) return { image: inputs.placed };
16678
- const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio);
16944
+ const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio, inputs.keep);
16679
16945
  const image = await ctx.assets.ingestBytes({ bytes: merged, kind: "image", mime: "image/png" });
16680
16946
  return { image };
16681
16947
  }
@@ -16699,7 +16965,7 @@ async function paintMargins(inputs, paint, ctx) {
16699
16965
 
16700
16966
  // src/engine/nodes/local/staticPrefilter.ts
16701
16967
  import { readFile as readFile16 } from "fs/promises";
16702
- import { z as z26 } from "zod";
16968
+ import { z as z27 } from "zod";
16703
16969
 
16704
16970
  // src/engine/nodes/local/lib/variant-prefilter.ts
16705
16971
  import { createHash as createHash3 } from "crypto";
@@ -16917,8 +17183,8 @@ The candidates are Image 1 to Image ${jpegs.length}, in the order shown. Reply w
16917
17183
  }
16918
17184
 
16919
17185
  // src/engine/nodes/local/staticPrefilter.ts
16920
- var Dropped = z26.object({ index: z26.number().int().min(0), reasons: z26.array(z26.string()) }).strict();
16921
- var Usage2 = z26.object({ model: z26.string(), inputTokens: z26.number(), outputTokens: z26.number(), costUsd: z26.number() }).strict();
17186
+ var Dropped = z27.object({ index: z27.number().int().min(0), reasons: z27.array(z27.string()) }).strict();
17187
+ var Usage2 = z27.object({ model: z27.string(), inputTokens: z27.number(), outputTokens: z27.number(), costUsd: z27.number() }).strict();
16922
17188
  async function readSpec(ref) {
16923
17189
  if (!ref?.path) return null;
16924
17190
  try {
@@ -16937,42 +17203,42 @@ var staticPrefilterNode = defineNode({
16937
17203
  passthroughRefs: true,
16938
17204
  summary: "Rank a static ad's variants before the judge by the deterministic checks (blocking defects, copy read back, the brand's logo found, the button's words, words inside the safe area) and pass on only the best-ranked, so the judge only breaks an exact tie. Outputs them in an order seeded by the pictures (never the render order), `order` (which variant each one is) and `dropped` with each set-aside variant's reasons.",
16939
17205
  when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` between the variants and the judge; wire the variants' whole `images` list and the ad spec (`$ref:prompt.asset`). Never leaves the judge empty: when every variant fails, the one with the fewest defects passes as `flagged`, and the review names what is wrong. A variant the reader could not read passes unchecked.",
16940
- inputs: z26.object({ images: z26.array(ImageRef).min(1), spec: JsonRef.optional() }).strict(),
17206
+ inputs: z27.object({ images: z27.array(ImageRef).min(1), spec: JsonRef.optional() }).strict(),
16941
17207
  // `master_shape` and `placements` are the ones the canvas's sizes were planned with, so the
16942
17208
  // band the variants are checked against is the band every size is cut to keep.
16943
- params: z26.object({
16944
- formats: z26.array(z26.string().min(1)).min(1),
16945
- master_shape: z26.enum(MASTER_SHAPES).optional(),
16946
- placements: z26.array(z26.string().min(1)).optional(),
17209
+ params: z27.object({
17210
+ formats: z27.array(z27.string().min(1)).min(1),
17211
+ master_shape: z27.enum(MASTER_SHAPES).optional(),
17212
+ placements: z27.array(z27.string().min(1)).optional(),
16947
17213
  // The judge, asked only on a real tie: its model, and the criteria compiled from prompt.json.
16948
- judge_model: z26.string().min(1).optional(),
16949
- judge_prompt: z26.string().min(1).optional(),
17214
+ judge_model: z27.string().min(1).optional(),
17215
+ judge_prompt: z27.string().min(1).optional(),
16950
17216
  // What the checks read of prompt.json, projected on every run (`staticChecksOf`); without it, the `spec` input.
16951
- checks: z26.object({
16952
- kind: z26.literal("static_checks"),
16953
- lines: z26.array(
16954
- z26.object({ text: z26.string(), lettering: z26.string(), role: z26.string().optional() }).passthrough()
17217
+ checks: z27.object({
17218
+ kind: z27.literal("static_checks"),
17219
+ lines: z27.array(
17220
+ z27.object({ text: z27.string(), lettering: z27.string(), role: z27.string().optional() }).passthrough()
16955
17221
  ),
16956
- buttons: z26.array(z26.string()),
16957
- brand: z26.string().optional(),
16958
- logo: z26.boolean()
17222
+ buttons: z27.array(z27.string()),
17223
+ brand: z27.string().optional(),
17224
+ logo: z27.boolean()
16959
17225
  }).optional()
16960
17226
  }).strict(),
16961
- outputs: z26.object({
16962
- images: z26.array(ImageRef).min(1),
17227
+ outputs: z27.object({
17228
+ images: z27.array(ImageRef).min(1),
16963
17229
  // Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
16964
- order: z26.array(z26.number().int().min(0)).optional(),
17230
+ order: z27.array(z27.number().int().min(0)).optional(),
16965
17231
  // The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
16966
17232
  // master's frame where every word stays whole when its centre would cut one, else as rendered.
16967
17233
  image: ImageRef.optional(),
16968
17234
  // Which variant it is (its place among the inputs).
16969
- chosen: z26.number().int().min(0).optional(),
17235
+ chosen: z27.number().int().min(0).optional(),
16970
17236
  // The judge's ranking lines, when a tie was put to it.
16971
- reasoning: z26.string().optional(),
16972
- dropped: z26.array(Dropped),
17237
+ reasoning: z27.string().optional(),
17238
+ dropped: z27.array(Dropped),
16973
17239
  flagged: Dropped.optional(),
16974
17240
  // What the reader's calls cost, as the gateway reported it (the run record carries it home).
16975
- usage: z26.array(Usage2).optional()
17241
+ usage: z27.array(Usage2).optional()
16976
17242
  }).strict(),
16977
17243
  outputKinds: { images: "image", image: "image" },
16978
17244
  cost: () => ({ credits: 0, seconds_estimate: 1 }),
@@ -17018,10 +17284,10 @@ async function framed(picture, frame, size2, ctx) {
17018
17284
  }
17019
17285
 
17020
17286
  // src/engine/nodes/local/staticSafeFit.ts
17021
- import { z as z27 } from "zod";
17022
- var Usage3 = z27.object({ model: z27.string(), inputTokens: z27.number(), outputTokens: z27.number(), costUsd: z27.number() }).strict();
17023
- var Fit2 = z27.object({ scale: z27.number(), x: z27.number(), y: z27.number() }).strict();
17024
- var Copy2 = z27.array(z27.object({ text: z27.string().min(1), role: z27.string().optional() }).strict());
17287
+ import { z as z28 } from "zod";
17288
+ var Usage3 = z28.object({ model: z28.string(), inputTokens: z28.number(), outputTokens: z28.number(), costUsd: z28.number() }).strict();
17289
+ var Fit2 = z28.object({ scale: z28.number(), x: z28.number(), y: z28.number() }).strict();
17290
+ var Copy2 = z28.array(z28.object({ text: z28.string().min(1), role: z28.string().optional() }).strict());
17025
17291
  var staticSafeFitNode = defineNode({
17026
17292
  id: "static_safe_fit",
17027
17293
  version: "1.0.0",
@@ -17029,19 +17295,19 @@ var staticSafeFitNode = defineNode({
17029
17295
  location: "local",
17030
17296
  summary: "Bring a finished static ad's words and logo inside the band its placement leaves clear (the Stories header, the Reels footer) without rendering again: reads where every line and the logo sit, then shrinks the picture a little (to 85% at most) and shifts it, carrying its plain edge into the uncovered margin. Outputs the same pixel size; `fit` is the scale and position used, or null with `reason` when the picture is passed on unchanged.",
17031
17297
  when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` after every 9:16 size (`safe_<size>`, role `static_safe_fit`). Free, besides one box-reader call (about $0.005, carried on the run record's review spend). A layout too tall or wide for the band at 85%, or whose edge is busy (cards, leaves, a scene that would smear), is passed on unchanged: the review still names what Reels or Stories covers, and the layout itself has to change (shorter copy, fewer lines).",
17032
- inputs: z27.object({ image: ImageRef }).strict(),
17298
+ inputs: z28.object({ image: ImageRef }).strict(),
17033
17299
  // `placements` are the ones the canvas's sizes were planned with (the ad set's), so the band
17034
17300
  // the fit clears is the one the review holds the size to.
17035
- params: z27.object({ format: z27.string().min(1), placements: z27.array(z27.string().min(1)).optional(), copy: Copy2.optional() }).strict(),
17036
- outputs: z27.object({
17301
+ params: z28.object({ format: z28.string().min(1), placements: z28.array(z28.string().min(1)).optional(), copy: Copy2.optional() }).strict(),
17302
+ outputs: z28.object({
17037
17303
  image: ImageRef,
17038
17304
  fit: Fit2.nullable(),
17039
17305
  // Why the picture passed unchanged: the reader could not read it, nothing was outside the
17040
17306
  // band, clearing it needed more than the smallest shrink that still looks native, or the
17041
17307
  // edge it would carry outward is not plain ground.
17042
- reason: z27.enum(["not_read", "inside_band", "too_far", "busy_edge"]).optional(),
17308
+ reason: z28.enum(["not_read", "inside_band", "too_far", "busy_edge"]).optional(),
17043
17309
  // What the reader's call cost, as the gateway reported it (the run record carries it home).
17044
- usage: z27.array(Usage3).optional()
17310
+ usage: z28.array(Usage3).optional()
17045
17311
  }).strict(),
17046
17312
  outputKinds: { image: "image" },
17047
17313
  cost: () => ({ credits: 0, seconds_estimate: 5 }),
@@ -17077,60 +17343,60 @@ var staticSafeFitNode = defineNode({
17077
17343
  });
17078
17344
 
17079
17345
  // src/engine/nodes/local/text.ts
17080
- import { z as z28 } from "zod";
17346
+ import { z as z29 } from "zod";
17081
17347
  var textNode = defineNode({
17082
17348
  id: "text",
17083
17349
  version: "1.0.0",
17084
17350
  category: "data",
17085
17351
  location: "local",
17086
17352
  summary: "A literal text value. Use for prompts, descriptions, copy.",
17087
- inputs: z28.object({}).strict(),
17088
- params: z28.object({ value: z28.string() }).strict(),
17089
- outputs: z28.object({ text: z28.string() }).strict(),
17353
+ inputs: z29.object({}).strict(),
17354
+ params: z29.object({ value: z29.string() }).strict(),
17355
+ outputs: z29.object({ text: z29.string() }).strict(),
17090
17356
  cost: () => ({ credits: 0, seconds_estimate: 0 }),
17091
17357
  execute: ({ params }) => Promise.resolve({ text: params.value })
17092
17358
  });
17093
17359
 
17094
17360
  // src/engine/nodes/local/videoConcat.ts
17095
- import { z as z29 } from "zod";
17361
+ import { z as z30 } from "zod";
17096
17362
  var FFMPEG_BIN4 = "ffmpeg";
17097
- var evenPixels = (axis) => z29.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
17098
- var Transition = z29.object({
17099
- type: z29.string().regex(/^[a-z]+$/, "an ffmpeg xfade transition name, e.g. fade, dissolve, zoomin, slideleft").describe("ffmpeg xfade transition name: fade, dissolve, zoomin, wipeleft, slideleft, \u2026"),
17100
- duration_s: z29.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
17363
+ var evenPixels = (axis) => z30.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
17364
+ var Transition = z30.object({
17365
+ type: z30.string().regex(/^[a-z]+$/, "an ffmpeg xfade transition name, e.g. fade, dissolve, zoomin, slideleft").describe("ffmpeg xfade transition name: fade, dissolve, zoomin, wipeleft, slideleft, \u2026"),
17366
+ duration_s: z30.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
17101
17367
  }).strict();
17102
- var ConcatClip = z29.object({
17103
- duration_s: z29.number().positive().describe(
17368
+ var ConcatClip = z30.object({
17369
+ duration_s: z30.number().positive().describe(
17104
17370
  "Seconds this clip occupies on the timeline. A clip with a `transition` must carry that much EXTRA footage past it (the overlap eats it), so the joined length is the sum of the clip durations."
17105
17371
  ),
17106
- length: z29.enum(["source", "trim", "still"]).default("source").describe(
17372
+ length: z30.enum(["source", "trim", "still"]).default("source").describe(
17107
17373
  "How the clip gets its length. `source`: play the input as rendered (it must already be duration_s long, plus the transition tail). `trim`: cut duration_s (+ tail) out of a longer input, starting at `offset_s`. `still`: the input is an IMAGE held on screen for duration_s (+ tail)."
17108
17374
  ),
17109
- offset_s: z29.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
17110
- fit: z29.enum(["fill", "pad"]).default("fill").describe(
17375
+ offset_s: z30.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
17376
+ fit: z30.enum(["fill", "pad"]).default("fill").describe(
17111
17377
  "`fill` scales to cover the frame and center-crops (footage). `pad` fits inside and letterboxes in black (a real screenshot or designed graphic, whose edges must never be cropped)."
17112
17378
  ),
17113
17379
  transition: Transition.optional().describe(
17114
17380
  "The transition OUT of this clip into the next one. Absent = a hard cut."
17115
17381
  ),
17116
- continues_frame: z29.boolean().optional().describe(
17382
+ continues_frame: z30.boolean().optional().describe(
17117
17383
  "True when this clip's first frame repeats the previous clip's last frame (a long shot split in two). `seam_dedup` drops the duplicate without shortening the timeline."
17118
17384
  )
17119
17385
  }).strict().refine((c) => c.offset_s === void 0 || c.length === "trim", {
17120
17386
  message: "offset_s only applies to a `trim` clip",
17121
17387
  path: ["offset_s"]
17122
17388
  });
17123
- var VideoConcatParams = z29.object({
17389
+ var VideoConcatParams = z30.object({
17124
17390
  width: evenPixels("width").describe("Output width in pixels (even). Every clip is normalized onto it."),
17125
17391
  height: evenPixels("height").describe("Output height in pixels (even)."),
17126
- fps: z29.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
17127
- seam_dedup: z29.enum(["head", "tail", "off"]).default("head").describe(
17392
+ fps: z30.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
17393
+ seam_dedup: z30.enum(["head", "tail", "off"]).default("head").describe(
17128
17394
  "Which copy of a repeated splice frame survives on a `continues_frame` clip: `head` drops the continuing clip's first frame, `tail` drops the previous clip's last frame, `off` keeps both."
17129
17395
  ),
17130
- clips: z29.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
17396
+ clips: z30.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
17131
17397
  }).strict();
17132
- var VideoConcatInputs = z29.record(z29.string(), z29.custom());
17133
- var VideoConcatOutputs = z29.object({ video: z29.custom() }).strict();
17398
+ var VideoConcatInputs = z30.record(z30.string(), z30.custom());
17399
+ var VideoConcatOutputs = z30.object({ video: z30.custom() }).strict();
17134
17400
  function inputLengthS(clip2) {
17135
17401
  return clip2.duration_s + (clip2.transition?.duration_s ?? 0);
17136
17402
  }
@@ -17273,14 +17539,14 @@ var videoConcatNode = defineNode({
17273
17539
  });
17274
17540
 
17275
17541
  // src/engine/nodes/remote/audioVoiceConvert.ts
17276
- import { z as z30 } from "zod";
17277
- var AudioVoiceConvertParams = z30.object({
17278
- model: z30.literal("elevenlabs/eleven_multilingual_sts_v2"),
17542
+ import { z as z31 } from "zod";
17543
+ var AudioVoiceConvertParams = z31.object({
17544
+ model: z31.literal("elevenlabs/eleven_multilingual_sts_v2"),
17279
17545
  /** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
17280
- voice: z30.string().min(1),
17281
- output_format: z30.string().optional(),
17546
+ voice: z31.string().min(1),
17547
+ output_format: z31.string().optional(),
17282
17548
  /** Strip the source clip's background noise before re-voicing. */
17283
- remove_background_noise: z30.boolean().optional()
17549
+ remove_background_noise: z31.boolean().optional()
17284
17550
  }).strict();
17285
17551
  var audioVoiceConvertNode = delegated({
17286
17552
  id: "audio_voice_convert",
@@ -17288,12 +17554,12 @@ var audioVoiceConvertNode = delegated({
17288
17554
  category: "audio",
17289
17555
  summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
17290
17556
  when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
17291
- inputs: z30.object({
17557
+ inputs: z31.object({
17292
17558
  audio: AudioRef,
17293
17559
  voice_ref: TextRef.optional()
17294
17560
  }).strict(),
17295
17561
  params: AudioVoiceConvertParams,
17296
- outputs: z30.object({ audio: AudioRef }).strict(),
17562
+ outputs: z31.object({ audio: AudioRef }).strict(),
17297
17563
  outputKinds: { audio: "audio" },
17298
17564
  // Priced by the shared contract per minute of source audio. The quote cannot see
17299
17565
  // the clip's length, so it assumes one minute; the charge uses the real length.
@@ -17304,33 +17570,33 @@ var audioVoiceConvertNode = delegated({
17304
17570
  });
17305
17571
 
17306
17572
  // src/engine/nodes/remote/dialogue.ts
17307
- import { z as z31 } from "zod";
17308
- var DialogueInput = z31.object({
17309
- text: z31.string().min(1),
17310
- voice_id: z31.string().min(1)
17573
+ import { z as z32 } from "zod";
17574
+ var DialogueInput = z32.object({
17575
+ text: z32.string().min(1),
17576
+ voice_id: z32.string().min(1)
17311
17577
  });
17312
17578
  var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
17313
- var DialogueParams = z31.object({
17314
- model: z31.enum(DIALOGUE_MODELS),
17579
+ var DialogueParams = z32.object({
17580
+ model: z32.enum(DIALOGUE_MODELS),
17315
17581
  /**
17316
17582
  * Ordered list of lines, each tagged with the voice that should speak it.
17317
17583
  * Up to 10 unique voice_ids; total text across all lines should stay under
17318
17584
  * ~2000 characters for best quality (ElevenLabs guidance).
17319
17585
  */
17320
- inputs: z31.array(DialogueInput).min(1).max(50),
17321
- language_code: z31.string().optional(),
17586
+ inputs: z32.array(DialogueInput).min(1).max(50),
17587
+ language_code: z32.string().optional(),
17322
17588
  /** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
17323
- settings: z31.record(z31.string(), z31.unknown()).optional(),
17324
- seed: z31.number().int().min(0).max(4294967295).optional(),
17325
- apply_text_normalization: z31.enum(["auto", "on", "off"]).optional(),
17589
+ settings: z32.record(z32.string(), z32.unknown()).optional(),
17590
+ seed: z32.number().int().min(0).max(4294967295).optional(),
17591
+ apply_text_normalization: z32.enum(["auto", "on", "off"]).optional(),
17326
17592
  /**
17327
17593
  * When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
17328
17594
  * separate `timestamps` output — character-level alignment plus
17329
17595
  * per-voice segment markers usable for captions, lipsync, or
17330
17596
  * beat-matched cuts in ad creatives.
17331
17597
  */
17332
- with_timestamps: z31.boolean().optional(),
17333
- output_format: z31.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17598
+ with_timestamps: z32.boolean().optional(),
17599
+ output_format: z32.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17334
17600
  }).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
17335
17601
  message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
17336
17602
  path: ["inputs"]
@@ -17341,36 +17607,36 @@ var dialogueNode = delegated({
17341
17607
  category: "audio",
17342
17608
  summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
17343
17609
  when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
17344
- inputs: z31.object({}).loose(),
17610
+ inputs: z32.object({}).loose(),
17345
17611
  params: DialogueParams,
17346
- outputs: z31.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
17612
+ outputs: z32.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
17347
17613
  outputKinds: { audio: "audio", timestamps: "json" },
17348
17614
  // Priced by the shared contract, the same per-character rate the backend charges.
17349
17615
  cost: ({ params }) => ({ credits: estimateNodeCredits({ nodeType: "dialogue", params }) ?? 1, seconds_estimate: 8 })
17350
17616
  });
17351
17617
 
17352
17618
  // src/engine/nodes/remote/imageAspectAdapt.ts
17353
- import { z as z32 } from "zod";
17619
+ import { z as z33 } from "zod";
17354
17620
  var ASPECT_ADAPT_MODELS = [
17355
17621
  "google/gemini-3-pro-image",
17356
17622
  "google/gemini-3.1-flash-image",
17357
17623
  "google/gemini-3-pro-image-preview",
17358
17624
  "google/gemini-3.1-flash-image-preview"
17359
17625
  ];
17360
- var ImageAspectAdaptParams = z32.object({
17361
- model: z32.enum(ASPECT_ADAPT_MODELS),
17362
- formats: z32.array(z32.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
17363
- platform: z32.enum(AD_FORMAT_PLATFORMS).optional(),
17364
- guidance: z32.string().min(1).optional(),
17626
+ var ImageAspectAdaptParams = z33.object({
17627
+ model: z33.enum(ASPECT_ADAPT_MODELS),
17628
+ formats: z33.array(z33.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
17629
+ platform: z33.enum(AD_FORMAT_PLATFORMS).optional(),
17630
+ guidance: z33.string().min(1).optional(),
17365
17631
  // The exact lines the ad carries: the reframe keeps each, spelled as given, once.
17366
- copy: z32.array(z32.string().min(1)).min(1).max(20).optional(),
17632
+ copy: z33.array(z33.string().min(1)).min(1).max(20).optional(),
17367
17633
  // Per target format, the fractions of each edge the platform's interface covers: words,
17368
17634
  // logo and button stay inside. The static scaffold sets both from prompt.json and `staticSafeArea`.
17369
- safe_areas: z32.record(
17370
- z32.string(),
17371
- z32.object({ top: z32.number(), bottom: z32.number(), left: z32.number(), right: z32.number() }).strict()
17635
+ safe_areas: z33.record(
17636
+ z33.string(),
17637
+ z33.object({ top: z33.number(), bottom: z33.number(), left: z33.number(), right: z33.number() }).strict()
17372
17638
  ).optional(),
17373
- image_size: z32.enum(OPENROUTER_IMAGE_SIZES).optional()
17639
+ image_size: z33.enum(OPENROUTER_IMAGE_SIZES).optional()
17374
17640
  }).strict().refine((p) => p.formats === void 0 !== (p.platform === void 0), {
17375
17641
  message: "provide exactly one of `formats` (explicit ratios) or `platform` (a placement preset)",
17376
17642
  path: ["formats"]
@@ -17381,9 +17647,9 @@ var imageAspectAdaptNode = delegated({
17381
17647
  category: "image",
17382
17648
  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.",
17383
17649
  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.",
17384
- inputs: z32.object({ source: ImageRef }).loose(),
17650
+ inputs: z33.object({ source: ImageRef }).loose(),
17385
17651
  params: ImageAspectAdaptParams,
17386
- outputs: z32.object({ images: z32.array(ImageRef).min(1) }).strict(),
17652
+ outputs: z33.object({ images: z33.array(ImageRef).min(1) }).strict(),
17387
17653
  outputKinds: { images: "image" },
17388
17654
  cost: ({ params }) => {
17389
17655
  const p = params;
@@ -17396,18 +17662,18 @@ var imageAspectAdaptNode = delegated({
17396
17662
  });
17397
17663
 
17398
17664
  // src/engine/nodes/remote/imageBackgroundRemove.ts
17399
- import { z as z33 } from "zod";
17400
- var ImageBackgroundRemoveParams = z33.object({
17665
+ import { z as z34 } from "zod";
17666
+ var ImageBackgroundRemoveParams = z34.object({
17401
17667
  // `fal/birefnet-v2` is the legacy id stored canvases carry. It stays the default for one
17402
17668
  // release: every backend accepts it, while `replicate/birefnet` needs the backend that ships with
17403
17669
  // this CLI, and the two deploy independently.
17404
- model: z33.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
17405
- operating_resolution: z33.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
17670
+ model: z34.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
17671
+ operating_resolution: z34.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
17406
17672
  // DEPRECATED, accepted so stored canvases still parse, and ignored: the model this
17407
17673
  // node runs (`men1scus/birefnet`) takes only an image and a resolution, has no
17408
17674
  // variant and returns no mask. `mask_only: true` still returns the cutout.
17409
- model_variant: z33.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
17410
- mask_only: z33.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
17675
+ model_variant: z34.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
17676
+ mask_only: z34.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
17411
17677
  }).strict();
17412
17678
  var imageBackgroundRemoveNode = delegated({
17413
17679
  id: "image_background_remove",
@@ -17415,11 +17681,11 @@ var imageBackgroundRemoveNode = delegated({
17415
17681
  category: "image",
17416
17682
  summary: "Remove the background from an image and return a transparent PNG. Powered by BiRefNet (`men1scus/birefnet` on Replicate).",
17417
17683
  when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. It returns the alpha-cut image only; there is no mask output and no model variant to pick.",
17418
- inputs: z33.object({
17684
+ inputs: z34.object({
17419
17685
  image: ImageRef
17420
17686
  }).strict(),
17421
17687
  params: ImageBackgroundRemoveParams,
17422
- outputs: z33.object({ image: ImageRef }).strict(),
17688
+ outputs: z34.object({ image: ImageRef }).strict(),
17423
17689
  outputKinds: { image: "image" },
17424
17690
  cost: ({ params }) => ({
17425
17691
  credits: estimateNodeCredits({ nodeType: "image_background_remove", params }) ?? 1,
@@ -17428,7 +17694,7 @@ var imageBackgroundRemoveNode = delegated({
17428
17694
  });
17429
17695
 
17430
17696
  // src/engine/nodes/remote/imageDescribe.ts
17431
- import { z as z34 } from "zod";
17697
+ import { z as z35 } from "zod";
17432
17698
  var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
17433
17699
  var imageDescribeNode = delegated({
17434
17700
  id: "image_describe",
@@ -17436,27 +17702,27 @@ var imageDescribeNode = delegated({
17436
17702
  category: "vision",
17437
17703
  summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
17438
17704
  when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
17439
- inputs: z34.object({ image: ImageRef }).loose(),
17440
- params: z34.object({
17441
- model: z34.enum(IMAGE_DESCRIBE_MODELS),
17442
- focus: z34.string().optional(),
17443
- context: z34.string().optional(),
17444
- temperature: z34.number().min(0).max(2).optional(),
17445
- max_tokens: z34.number().int().positive().optional()
17705
+ inputs: z35.object({ image: ImageRef }).loose(),
17706
+ params: z35.object({
17707
+ model: z35.enum(IMAGE_DESCRIBE_MODELS),
17708
+ focus: z35.string().optional(),
17709
+ context: z35.string().optional(),
17710
+ temperature: z35.number().min(0).max(2).optional(),
17711
+ max_tokens: z35.number().int().positive().optional()
17446
17712
  }).strict(),
17447
- outputs: z34.object({ description: JsonRef }).strict(),
17713
+ outputs: z35.object({ description: JsonRef }).strict(),
17448
17714
  outputKinds: { description: "json" },
17449
17715
  cost: () => ({ credits: 2, seconds_estimate: 10 })
17450
17716
  });
17451
17717
 
17452
17718
  // src/engine/nodes/remote/imageLayerize.ts
17453
- import { z as z35 } from "zod";
17454
- var ImageLayerizeParams = z35.object({
17719
+ import { z as z36 } from "zod";
17720
+ var ImageLayerizeParams = z36.object({
17455
17721
  /**
17456
17722
  * What to separate. Read as extraction instructions, not as a generation
17457
17723
  * prompt — "keep the bottle and its shadow as one layer", not "make it blue".
17458
17724
  */
17459
- instructions: z35.string().max(600).optional()
17725
+ instructions: z36.string().max(600).optional()
17460
17726
  }).strict();
17461
17727
  var imageLayerizeNode = delegated({
17462
17728
  id: "image_layerize",
@@ -17464,12 +17730,12 @@ var imageLayerizeNode = delegated({
17464
17730
  category: "image",
17465
17731
  summary: "Split an image into editable layers: a transparent PNG per element, plus any copy baked into the picture recovered as real text with its typography. Powered by Seedream 5.0 Pro Layerize, with Ideogram lifting the text off first.",
17466
17732
  when_to_use: "Use when you need ONE element of a picture on its own (a product with no background, a logo lockup), or when a shot is right but its copy is wrong and regenerating would change the picture too. The manifest gives you each layer's box and, for text, its exact wording, font, size and colour \u2014 enough to re-set a headline in the original's type. For a plain subject cutout with no text involved, `image_background_remove` is cheaper and one call.",
17467
- inputs: z35.object({
17733
+ inputs: z36.object({
17468
17734
  image: ImageRef
17469
17735
  }).strict(),
17470
17736
  params: ImageLayerizeParams,
17471
- outputs: z35.object({
17472
- layers: z35.array(ImageRef),
17737
+ outputs: z36.object({
17738
+ layers: z36.array(ImageRef),
17473
17739
  base: ImageRef,
17474
17740
  manifest: JsonRef
17475
17741
  }).strict(),
@@ -17480,24 +17746,24 @@ var imageLayerizeNode = delegated({
17480
17746
  });
17481
17747
 
17482
17748
  // src/engine/nodes/remote/imageReferenceSheet.ts
17483
- import { z as z36 } from "zod";
17749
+ import { z as z37 } from "zod";
17484
17750
  var REFERENCE_SHEET_MODELS = [
17485
17751
  "google/gemini-3-pro-image",
17486
17752
  "google/gemini-3.1-flash-image",
17487
17753
  "google/gemini-3-pro-image-preview",
17488
17754
  "google/gemini-3.1-flash-image-preview"
17489
17755
  ];
17490
- var ImageReferenceSheetParams = z36.object({
17491
- model: z36.enum(REFERENCE_SHEET_MODELS),
17492
- subject_description: z36.string().min(1),
17756
+ var ImageReferenceSheetParams = z37.object({
17757
+ model: z37.enum(REFERENCE_SHEET_MODELS),
17758
+ subject_description: z37.string().min(1),
17493
17759
  // `location` = a set/room shown from several camera ANGLES (not a rotated subject),
17494
17760
  // so a multi-scene shoot keeps one consistent set.
17495
- subject_type: z36.enum(["character", "person", "product", "location"]),
17496
- views: z36.array(z36.string().min(1)).min(2).max(8).optional(),
17497
- style: z36.string().optional(),
17498
- prompt_override: z36.string().min(1).optional(),
17499
- aspect_ratio: z36.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
17500
- image_size: z36.enum(OPENROUTER_IMAGE_SIZES).optional()
17761
+ subject_type: z37.enum(["character", "person", "product", "location"]),
17762
+ views: z37.array(z37.string().min(1)).min(2).max(8).optional(),
17763
+ style: z37.string().optional(),
17764
+ prompt_override: z37.string().min(1).optional(),
17765
+ aspect_ratio: z37.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
17766
+ image_size: z37.enum(OPENROUTER_IMAGE_SIZES).optional()
17501
17767
  }).strict();
17502
17768
  var imageReferenceSheetNode = delegated({
17503
17769
  id: "image_reference_sheet",
@@ -17505,9 +17771,9 @@ var imageReferenceSheetNode = delegated({
17505
17771
  category: "image",
17506
17772
  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).",
17507
17773
  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.",
17508
- inputs: z36.object({ references: z36.array(ImageRef).min(1).max(6) }).loose(),
17774
+ inputs: z37.object({ references: z37.array(ImageRef).min(1).max(6) }).loose(),
17509
17775
  params: ImageReferenceSheetParams,
17510
- outputs: z36.object({ sheet: ImageRef }).strict(),
17776
+ outputs: z37.object({ sheet: ImageRef }).strict(),
17511
17777
  outputKinds: { sheet: "image" },
17512
17778
  cost: ({ params }) => ({
17513
17779
  credits: imagePictureCredits(String(params?.model)),
@@ -17516,10 +17782,10 @@ var imageReferenceSheetNode = delegated({
17516
17782
  });
17517
17783
 
17518
17784
  // src/engine/nodes/remote/imageSearch.ts
17519
- import { z as z37 } from "zod";
17520
- var ImageSearchParams = z37.object({
17521
- prompt: z37.string().min(1),
17522
- count: z37.number().int().min(1).max(20).default(5)
17785
+ import { z as z38 } from "zod";
17786
+ var ImageSearchParams = z38.object({
17787
+ prompt: z38.string().min(1),
17788
+ count: z38.number().int().min(1).max(20).default(5)
17523
17789
  }).strict();
17524
17790
  var imageSearchNode = delegated({
17525
17791
  id: "image_search",
@@ -17527,15 +17793,15 @@ var imageSearchNode = delegated({
17527
17793
  category: "image",
17528
17794
  summary: "Agentic image search across Google Images, free stock photography (Pexels and Pixabay), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
17529
17795
  when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
17530
- inputs: z37.object({}).loose(),
17796
+ inputs: z38.object({}).loose(),
17531
17797
  params: ImageSearchParams,
17532
- outputs: z37.object({ images: z37.array(ImageRef).min(1) }).strict(),
17798
+ outputs: z38.object({ images: z38.array(ImageRef).min(1) }).strict(),
17533
17799
  outputKinds: { images: "image" },
17534
17800
  cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
17535
17801
  });
17536
17802
 
17537
17803
  // src/engine/nodes/remote/imageSelect.ts
17538
- import { z as z38 } from "zod";
17804
+ import { z as z39 } from "zod";
17539
17805
  var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
17540
17806
  var imageSelectNode = delegated({
17541
17807
  id: "image_select",
@@ -17543,16 +17809,16 @@ var imageSelectNode = delegated({
17543
17809
  category: "vision",
17544
17810
  summary: "Pick the best `count` images out of the 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. A single candidate passes through unjudged, free and without reasoning.",
17545
17811
  when_to_use: "Use after an image_generate run with `n` > 1 (wire its whole `images` array), several image_generate variants, or any pool of 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.",
17546
- inputs: z38.object({ images: z38.array(ImageRef).min(1) }).loose(),
17547
- params: z38.object({
17548
- model: z38.enum(IMAGE_SELECT_MODELS),
17549
- prompt: z38.string().min(1),
17550
- count: z38.number().int().min(1).default(1),
17551
- temperature: z38.number().min(0).max(2).optional(),
17552
- max_tokens: z38.number().int().positive().optional()
17812
+ inputs: z39.object({ images: z39.array(ImageRef).min(1) }).loose(),
17813
+ params: z39.object({
17814
+ model: z39.enum(IMAGE_SELECT_MODELS),
17815
+ prompt: z39.string().min(1),
17816
+ count: z39.number().int().min(1).default(1),
17817
+ temperature: z39.number().min(0).max(2).optional(),
17818
+ max_tokens: z39.number().int().positive().optional()
17553
17819
  }).strict(),
17554
17820
  // No `reasoning` when a lone candidate passed through: there was no judgement to explain.
17555
- outputs: z38.object({ images: z38.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
17821
+ outputs: z39.object({ images: z39.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
17556
17822
  outputKinds: { images: "image", reasoning: "text" },
17557
17823
  cost: () => ({ credits: 1, seconds_estimate: 5 }),
17558
17824
  // Arity is only knowable at validate time when `images` is a literal array
@@ -17577,26 +17843,26 @@ var imageSelectNode = delegated({
17577
17843
  });
17578
17844
 
17579
17845
  // src/engine/nodes/remote/music.ts
17580
- import { z as z39 } from "zod";
17846
+ import { z as z40 } from "zod";
17581
17847
  var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
17582
- var MusicParams = z39.object({
17583
- model: z39.enum(MUSIC_MODELS),
17848
+ var MusicParams = z40.object({
17849
+ model: z40.enum(MUSIC_MODELS),
17584
17850
  /** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
17585
- prompt: z39.string().optional(),
17851
+ prompt: z40.string().optional(),
17586
17852
  /**
17587
17853
  * Structured composition plan (intro / hook / verse / outro sections with
17588
17854
  * per-section styles + durations). Mutually exclusive with `prompt`.
17589
17855
  */
17590
- composition_plan: z39.record(z39.string(), z39.unknown()).optional(),
17856
+ composition_plan: z40.record(z40.string(), z40.unknown()).optional(),
17591
17857
  /** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
17592
- music_length_ms: z39.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
17593
- seed: z39.number().int().optional(),
17858
+ music_length_ms: z40.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
17859
+ seed: z40.number().int().optional(),
17594
17860
  /** Prompt mode only — forces an instrumental (no vocals) track. */
17595
- force_instrumental: z39.boolean().optional(),
17861
+ force_instrumental: z40.boolean().optional(),
17596
17862
  /** composition_plan only — honor exact section durations. */
17597
- respect_sections_durations: z39.boolean().optional(),
17863
+ respect_sections_durations: z40.boolean().optional(),
17598
17864
  /** Emit word-level timestamps alongside the audio. */
17599
- with_timestamps: z39.boolean().optional(),
17865
+ with_timestamps: z40.boolean().optional(),
17600
17866
  /**
17601
17867
  * video-to-music only — short description of the desired score
17602
17868
  * ("upbeat synth, fast cuts, 80s") used to bias the model.
@@ -17608,14 +17874,14 @@ var MusicParams = z39.object({
17608
17874
  * confident") came back as a film score and the client's verdict was "la
17609
17875
  * musica que has elegido es muy dramática".
17610
17876
  */
17611
- description: z39.string().max(1e3).optional().describe(
17877
+ description: z40.string().max(1e3).optional().describe(
17612
17878
  "The score you want, in sound (instruments, tempo, texture) \u2014 not in feelings. Under speech, name ONE steady character and forbid movement: no build, no drop, no climax, no key change, nothing that 'follows the arc'. This model WATCHES the cut, so it will score the footage's emotion unless told not to \u2014 asking it to track a speaker's emotional arc is how a testimonial bed comes back dramatic. A bed is furniture; the voice carries the story."
17613
17879
  ),
17614
17880
  /** video-to-music only — up to 10 style tags. */
17615
- tags: z39.array(z39.string()).max(10).optional().describe(
17881
+ tags: z40.array(z40.string()).max(10).optional().describe(
17616
17882
  "Style tags. Under speech, prefer tags that hold it still (instrumental, understated, steady, no drums) over tags that move it (cinematic, epic, building, resolving, trailer)."
17617
17883
  ),
17618
- output_format: z39.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17884
+ output_format: z40.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17619
17885
  }).strict();
17620
17886
  var musicNode = delegated({
17621
17887
  id: "music",
@@ -17626,9 +17892,9 @@ var musicNode = delegated({
17626
17892
  category: "audio",
17627
17893
  summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
17628
17894
  when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
17629
- inputs: z39.object({ video: VideoRef.optional() }).loose(),
17895
+ inputs: z40.object({ video: VideoRef.optional() }).loose(),
17630
17896
  params: MusicParams,
17631
- outputs: z39.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
17897
+ outputs: z40.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
17632
17898
  outputKinds: { audio: "audio", timestamps: "json" },
17633
17899
  // Priced by the shared contract: per second of audio, 30s when no length is pinned.
17634
17900
  cost: ({ params }) => {
@@ -17660,25 +17926,25 @@ var musicNode = delegated({
17660
17926
  });
17661
17927
 
17662
17928
  // src/engine/nodes/remote/soundEffect.ts
17663
- import { z as z40 } from "zod";
17929
+ import { z as z41 } from "zod";
17664
17930
  var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
17665
- var SoundEffectParams = z40.object({
17666
- model: z40.enum(SOUND_EFFECT_MODELS),
17931
+ var SoundEffectParams = z41.object({
17932
+ model: z41.enum(SOUND_EFFECT_MODELS),
17667
17933
  /** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
17668
- text: z40.string().min(1),
17934
+ text: z41.string().min(1),
17669
17935
  /**
17670
17936
  * Target length in seconds. 0.5–30. Leave unset to let the model pick the
17671
17937
  * natural length for the described effect.
17672
17938
  */
17673
- duration_seconds: z40.number().min(0.5).max(30).optional(),
17939
+ duration_seconds: z41.number().min(0.5).max(30).optional(),
17674
17940
  /**
17675
17941
  * 0–1. Higher = stick closer to the prompt at the cost of variety; lower
17676
17942
  * = let the model interpret more freely. Defaults to 0.3 on the provider.
17677
17943
  */
17678
- prompt_influence: z40.number().min(0).max(1).optional(),
17944
+ prompt_influence: z41.number().min(0).max(1).optional(),
17679
17945
  /** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
17680
- loop: z40.boolean().optional(),
17681
- output_format: z40.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17946
+ loop: z41.boolean().optional(),
17947
+ output_format: z41.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
17682
17948
  }).strict();
17683
17949
  var soundEffectNode = delegated({
17684
17950
  id: "sound_effect",
@@ -17686,9 +17952,9 @@ var soundEffectNode = delegated({
17686
17952
  category: "audio",
17687
17953
  summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
17688
17954
  when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
17689
- inputs: z40.object({}).loose(),
17955
+ inputs: z41.object({}).loose(),
17690
17956
  params: SoundEffectParams,
17691
- outputs: z40.object({ audio: AudioRef }).strict(),
17957
+ outputs: z41.object({ audio: AudioRef }).strict(),
17692
17958
  outputKinds: { audio: "audio" },
17693
17959
  // Priced by the shared contract: per second of audio, 5s when the model picks the length.
17694
17960
  cost: ({ params }) => {
@@ -17698,7 +17964,7 @@ var soundEffectNode = delegated({
17698
17964
  });
17699
17965
 
17700
17966
  // src/engine/nodes/remote/textGenerate.ts
17701
- import { z as z41 } from "zod";
17967
+ import { z as z42 } from "zod";
17702
17968
  var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
17703
17969
  var textGenerateNode = delegated({
17704
17970
  id: "text_generate",
@@ -17706,58 +17972,58 @@ var textGenerateNode = delegated({
17706
17972
  category: "language",
17707
17973
  summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
17708
17974
  when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
17709
- inputs: z41.object({}).loose(),
17710
- params: z41.object({
17711
- model: z41.enum(TEXT_GENERATE_MODELS),
17712
- prompt: z41.string().min(1),
17713
- system: z41.string().optional(),
17714
- response_format: z41.enum(["text", "json_object"]).optional(),
17715
- web_search: z41.boolean().optional(),
17716
- temperature: z41.number().min(0).max(2).optional(),
17717
- max_tokens: z41.number().int().positive().optional()
17975
+ inputs: z42.object({}).loose(),
17976
+ params: z42.object({
17977
+ model: z42.enum(TEXT_GENERATE_MODELS),
17978
+ prompt: z42.string().min(1),
17979
+ system: z42.string().optional(),
17980
+ response_format: z42.enum(["text", "json_object"]).optional(),
17981
+ web_search: z42.boolean().optional(),
17982
+ temperature: z42.number().min(0).max(2).optional(),
17983
+ max_tokens: z42.number().int().positive().optional()
17718
17984
  }).strict(),
17719
- outputs: z41.object({ text: TextRef }).strict(),
17985
+ outputs: z42.object({ text: TextRef }).strict(),
17720
17986
  outputKinds: { text: "text" },
17721
17987
  cost: () => ({ credits: 1, seconds_estimate: 3 })
17722
17988
  });
17723
17989
 
17724
17990
  // src/engine/nodes/remote/tts.ts
17725
- import { z as z42 } from "zod";
17991
+ import { z as z43 } from "zod";
17726
17992
  var TTS_MODELS = ["elevenlabs/eleven_v3"];
17727
- var TtsVoiceSettings = z42.object({
17728
- stability: z42.number().min(0).max(1).optional(),
17729
- similarity_boost: z42.number().min(0).max(1).optional(),
17730
- style: z42.number().min(0).max(1).optional(),
17731
- use_speaker_boost: z42.boolean().optional(),
17732
- speed: z42.number().min(0.25).max(4).optional()
17993
+ var TtsVoiceSettings = z43.object({
17994
+ stability: z43.number().min(0).max(1).optional(),
17995
+ similarity_boost: z43.number().min(0).max(1).optional(),
17996
+ style: z43.number().min(0).max(1).optional(),
17997
+ use_speaker_boost: z43.boolean().optional(),
17998
+ speed: z43.number().min(0.25).max(4).optional()
17733
17999
  }).strict();
17734
- var TtsPronunciationLocator = z42.object({
17735
- pronunciation_dictionary_id: z42.string().min(1),
17736
- version_id: z42.string().nullable().optional()
18000
+ var TtsPronunciationLocator = z43.object({
18001
+ pronunciation_dictionary_id: z43.string().min(1),
18002
+ version_id: z43.string().nullable().optional()
17737
18003
  }).strict();
17738
- var TtsParams = z42.object({
17739
- model: z42.enum(TTS_MODELS),
17740
- text: z42.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
17741
- voice: z42.string().min(1),
18004
+ var TtsParams = z43.object({
18005
+ model: z43.enum(TTS_MODELS),
18006
+ text: z43.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
18007
+ voice: z43.string().min(1),
17742
18008
  /** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
17743
- output_format: z42.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
17744
- seed: z42.number().int().min(0).max(4294967295).optional(),
18009
+ output_format: z43.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
18010
+ seed: z43.number().int().min(0).max(4294967295).optional(),
17745
18011
  // Top-level shortcuts; structured form is `voice_settings`.
17746
- stability: z42.number().min(0).max(1).optional(),
17747
- similarity_boost: z42.number().min(0).max(1).optional(),
18012
+ stability: z43.number().min(0).max(1).optional(),
18013
+ similarity_boost: z43.number().min(0).max(1).optional(),
17748
18014
  voice_settings: TtsVoiceSettings.optional(),
17749
18015
  /** ISO 639-1 language code. eleven_v3 supports language hints. */
17750
- language_code: z42.string().optional(),
17751
- pronunciation_dictionary_locators: z42.array(TtsPronunciationLocator).max(3).optional(),
17752
- apply_text_normalization: z42.enum(["auto", "on", "off"]).optional(),
18016
+ language_code: z43.string().optional(),
18017
+ pronunciation_dictionary_locators: z43.array(TtsPronunciationLocator).max(3).optional(),
18018
+ apply_text_normalization: z43.enum(["auto", "on", "off"]).optional(),
17753
18019
  /** Currently Japanese-only. Adds latency. */
17754
- apply_language_text_normalization: z42.boolean().optional(),
18020
+ apply_language_text_normalization: z43.boolean().optional(),
17755
18021
  /**
17756
18022
  * When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
17757
18023
  * adds a `timestamps` output (character-level alignment) for caption
17758
18024
  * rendering, lipsync, and beat-matched cuts.
17759
18025
  */
17760
- with_timestamps: z42.boolean().optional()
18026
+ with_timestamps: z43.boolean().optional()
17761
18027
  }).strict();
17762
18028
  var ttsNode = delegated({
17763
18029
  id: "tts",
@@ -17765,9 +18031,9 @@ var ttsNode = delegated({
17765
18031
  category: "audio",
17766
18032
  summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
17767
18033
  when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
17768
- inputs: z42.object({}).loose(),
18034
+ inputs: z43.object({}).loose(),
17769
18035
  params: TtsParams,
17770
- outputs: z42.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
18036
+ outputs: z43.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
17771
18037
  outputKinds: { audio: "audio", timestamps: "json" },
17772
18038
  // Priced by the shared contract, the same per-character rate the backend charges.
17773
18039
  cost: ({ params }) => ({
@@ -17777,11 +18043,11 @@ var ttsNode = delegated({
17777
18043
  });
17778
18044
 
17779
18045
  // src/engine/nodes/remote/video.ts
17780
- import { z as z43 } from "zod";
17781
- var videoModelEnum = z43.enum(VIDEO_GENERATE_MODELS);
17782
- var VideoGenerateParams = z43.object({
18046
+ import { z as z44 } from "zod";
18047
+ var videoModelEnum = z44.enum(VIDEO_GENERATE_MODELS);
18048
+ var VideoGenerateParams = z44.object({
17783
18049
  model: videoModelEnum,
17784
- ...registryParamShape("video_generate", { prompt: z43.string().min(1) })
18050
+ ...registryParamShape("video_generate", { prompt: z44.string().min(1) })
17785
18051
  }).strict();
17786
18052
  var videoGenerateNode = delegated({
17787
18053
  id: "video_generate",
@@ -17789,7 +18055,7 @@ var videoGenerateNode = delegated({
17789
18055
  category: "video",
17790
18056
  summary: "Generate video for ad creatives. Curated roster: `google/gemini-omni-1.1-flash` (DEFAULT \u2014 native audio + reference images in one call, 360p-4k, 3-10s), `bytedance/seedance-2.0` (identity/product workhorse, NO photoreal human faces), `google/veo-3.1` (photoreal cine ceiling + the human-face model), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-v3.0-pro` (motion-transfer/dynamic). Async with polling.",
17791
18057
  when_to_use: "`google/gemini-omni-1.1-flash` is the DEFAULT: it renders picture and synchronized audio in one call, and is the only model here that takes reference images alongside a prompt. SET `duration` (3-10s) to the length the beat actually needs \u2014 it bills PER SECOND, so leaving it out renders the 10s default and charges for ten seconds of a beat that may need three. SET `resolution` too: `360p` bills a THIRD of `720p` ($0.034 vs $0.101 per second) and is the right tier while you are still choosing between takes \u2014 render the drafts at 360p, then re-render the keeper at `720p` (default), `1080p` ($0.152/s) or `4k` ($0.304/s). It accepts only `16:9` or `9:16`, and takes NO `seed`, `negative_prompt` or `generate_audio` param \u2014 sending one fails validation, and it always renders audio. Keep the default for ordinary beats; move off it only when a beat needs a length outside 3-10s or a 1:1 frame. Use `bytedance/seedance-2.0` for identity/product output WITHOUT a photoreal human face on camera. Any clip whose frame shows a photoreal human face \u2014 a UGC creator, presenter, or testimonial speaker, REAL OR AI-GENERATED \u2014 must use `google/veo-3.1`: ByteDance's filter rejects photoreal human faces on Seedance (content_policy_blocked/E005), and a synthetic AI face does NOT clear it. Use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-v3.0-pro` for motion-transfer/hyper-dynamic beats. `baker canvas scaffold-video` auto-routes a photoreal cast to Veo. A Seedance clip that is refused for a face at runtime is NOT moved to another model: one guided only by reference images is re-rendered on the same model through a second route that accepts faces (about twice the price), and one that starts from a frame comes back as content_policy_blocked \u2014 re-run that clip on `google/veo-3.1`. Each model gates its own durations, resolutions and aspect ratios in the registry: `baker studio models --kind video --model <id>` shows them.",
17792
- inputs: z43.object({
18058
+ inputs: z44.object({
17793
18059
  first_frame: ImageRef.optional(),
17794
18060
  last_frame: ImageRef.optional(),
17795
18061
  reference: ImageRef.optional(),
@@ -17799,10 +18065,10 @@ var videoGenerateNode = delegated({
17799
18065
  * exclusive with `first_frame` — OpenRouter treats a request carrying both
17800
18066
  * as image-to-video and ignores these, so wire one or the other.
17801
18067
  */
17802
- references: z43.array(ImageRef).optional()
18068
+ references: z44.array(ImageRef).optional()
17803
18069
  }).loose(),
17804
18070
  params: VideoGenerateParams,
17805
- outputs: z43.object({ video: VideoRef }).strict(),
18071
+ outputs: z44.object({ video: VideoRef }).strict(),
17806
18072
  outputKinds: { video: "video" },
17807
18073
  // Priced from the shared contract, so `validate`'s quote and the charge the
17808
18074
  // backend applies come from ONE table. A flat number here under-quoted a
@@ -17817,15 +18083,15 @@ var videoGenerateNode = delegated({
17817
18083
  });
17818
18084
 
17819
18085
  // src/engine/nodes/remote/videoBackgroundRemove.ts
17820
- import { z as z44 } from "zod";
17821
- var VideoBackgroundRemoveParams = z44.object({
18086
+ import { z as z45 } from "zod";
18087
+ var VideoBackgroundRemoveParams = z45.object({
17822
18088
  // `fal/veed-video-background-removal` is the legacy id stored canvases carry. It stays the default for one
17823
18089
  // release: every backend accepts it, while `replicate/birefnet-video` needs the backend that ships with
17824
18090
  // this CLI, and the two deploy independently.
17825
- model: z44.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
18091
+ model: z45.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
17826
18092
  // Refines the matte's soft edges against background bleed (the model's `refine_fg`).
17827
- edge_refinement: z44.boolean().optional().default(true),
17828
- output_codec: z44.enum(["vp9", "h264"]).optional().default("vp9")
18093
+ edge_refinement: z45.boolean().optional().default(true),
18094
+ output_codec: z45.enum(["vp9", "h264"]).optional().default("vp9")
17829
18095
  }).strict();
17830
18096
  var videoBackgroundRemoveNode = delegated({
17831
18097
  id: "video_background_remove",
@@ -17833,11 +18099,11 @@ var videoBackgroundRemoveNode = delegated({
17833
18099
  category: "video",
17834
18100
  summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by BiRefNet Video (`sprited/birefnet-video` on Replicate).",
17835
18101
  when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
17836
- inputs: z44.object({
18102
+ inputs: z45.object({
17837
18103
  video: VideoRef
17838
18104
  }).strict(),
17839
18105
  params: VideoBackgroundRemoveParams,
17840
- outputs: z44.object({ video: VideoRef }).strict(),
18106
+ outputs: z45.object({ video: VideoRef }).strict(),
17841
18107
  outputKinds: { video: "video" },
17842
18108
  // Priced by the shared contract per 30 frames; the quote assumes the same 30s of
17843
18109
  // footage the charge falls back to when the clip carries no duration.
@@ -17848,7 +18114,7 @@ var videoBackgroundRemoveNode = delegated({
17848
18114
  });
17849
18115
 
17850
18116
  // src/engine/nodes/remote/videoDeconstruct.ts
17851
- import { z as z45 } from "zod";
18117
+ import { z as z46 } from "zod";
17852
18118
  var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
17853
18119
  var videoDeconstructNode = delegated({
17854
18120
  id: "video_deconstruct",
@@ -17856,34 +18122,34 @@ var videoDeconstructNode = delegated({
17856
18122
  category: "video",
17857
18123
  summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
17858
18124
  when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
17859
- inputs: z45.object({ video: VideoRef }).loose(),
17860
- params: z45.object({
17861
- model: z45.enum(VIDEO_DECONSTRUCT_MODELS),
17862
- mode: z45.enum(["full", "index"]).optional(),
17863
- language: z45.string().min(2).max(8).optional(),
17864
- max_scenes: z45.number().int().min(1).max(60).optional(),
17865
- focus: z45.string().optional(),
17866
- start_s: z45.number().min(0).optional(),
17867
- end_s: z45.number().positive().optional(),
18125
+ inputs: z46.object({ video: VideoRef }).loose(),
18126
+ params: z46.object({
18127
+ model: z46.enum(VIDEO_DECONSTRUCT_MODELS),
18128
+ mode: z46.enum(["full", "index"]).optional(),
18129
+ language: z46.string().min(2).max(8).optional(),
18130
+ max_scenes: z46.number().int().min(1).max(60).optional(),
18131
+ focus: z46.string().optional(),
18132
+ start_s: z46.number().min(0).optional(),
18133
+ end_s: z46.number().positive().optional(),
17868
18134
  // Real visual shot-cut timestamps (absolute seconds), detected locally with
17869
18135
  // ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
17870
18136
  // onto these and SPLITS any scene that spans one, so a scene's frames never
17871
18137
  // straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
17872
- shot_cuts: z45.array(z45.number().min(0)).max(200).optional(),
18138
+ shot_cuts: z46.array(z46.number().min(0)).max(200).optional(),
17873
18139
  // The video model's per-clip ceiling (seconds). A shot longer than this is
17874
18140
  // split into seamless continuation sub-scenes (shared splice frame), so long
17875
18141
  // shots reproduce in full instead of being truncated. `scaffold-video` sets
17876
18142
  // the Seedance ceiling (15); omit to disable length splitting.
17877
- max_clip_s: z45.number().positive().max(60).optional(),
18143
+ max_clip_s: z46.number().positive().max(60).optional(),
17878
18144
  // Transcript provider for the blueprint's dialogue/transcript. Default
17879
18145
  // Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
17880
- transcriber: z45.enum(["groq", "deepgram"]).optional()
18146
+ transcriber: z46.enum(["groq", "deepgram"]).optional()
17881
18147
  }).strict(),
17882
- outputs: z45.object({
18148
+ outputs: z46.object({
17883
18149
  analysis: JsonRef,
17884
18150
  // Absent in mode:"index" (structure only, no Mux frame extraction).
17885
- start_frames: z45.array(ImageRef).min(1).optional(),
17886
- end_frames: z45.array(ImageRef).min(1).optional(),
18151
+ start_frames: z46.array(ImageRef).min(1).optional(),
18152
+ end_frames: z46.array(ImageRef).min(1).optional(),
17887
18153
  transcript: JsonRef
17888
18154
  }).strict(),
17889
18155
  outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
@@ -17891,21 +18157,21 @@ var videoDeconstructNode = delegated({
17891
18157
  });
17892
18158
 
17893
18159
  // src/engine/nodes/remote/videoLipsync.ts
17894
- import { z as z46 } from "zod";
17895
- var VideoLipsyncParams = z46.object({
17896
- model: z46.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
18160
+ import { z as z47 } from "zod";
18161
+ var VideoLipsyncParams = z47.object({
18162
+ model: z47.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
17897
18163
  }).strict();
17898
18164
  var videoLipsyncNode = delegated({
17899
18165
  id: "video_lipsync",
17900
18166
  version: "1.0.0",
17901
18167
  category: "video",
17902
18168
  summary: "Lip-sync a video to an audio track (`replicate/sync-lipsync-2`: Sync Labs lipsync-2 on Replicate; the legacy id `fal/veed-lipsync` still runs). Billed at $0.40 per minute of the longer input. Runs as a background job.",
17903
- inputs: z46.object({
18169
+ inputs: z47.object({
17904
18170
  video: VideoRef,
17905
18171
  audio: AudioRef
17906
18172
  }).strict(),
17907
18173
  params: VideoLipsyncParams,
17908
- outputs: z46.object({ video: VideoRef }).strict(),
18174
+ outputs: z47.object({ video: VideoRef }).strict(),
17909
18175
  outputKinds: { video: "video" },
17910
18176
  // Priced by the shared contract per minute of output; the quote cannot see the
17911
18177
  // clips' lengths, so it assumes the same one minute the charge falls back to.
@@ -17919,7 +18185,7 @@ var videoLipsyncNode = delegated({
17919
18185
  import { mkdtemp as mkdtemp8, readFile as readFile17, rm as rm8 } from "fs/promises";
17920
18186
  import { tmpdir as tmpdir8 } from "os";
17921
18187
  import path21 from "path";
17922
- import { z as z47 } from "zod";
18188
+ import { z as z48 } from "zod";
17923
18189
 
17924
18190
  // src/engine/nodes/local/lib/ffmpeg.ts
17925
18191
  import { execFile as execFile8 } from "child_process";
@@ -18157,30 +18423,30 @@ function timeScriptWords(script, transcript, language) {
18157
18423
  }
18158
18424
 
18159
18425
  // src/engine/nodes/remote/videoTranscribe.ts
18160
- var VideoTranscribeParams = z47.object({
18161
- language: z47.string().min(2).max(8).optional(),
18426
+ var VideoTranscribeParams = z48.object({
18427
+ language: z48.string().min(2).max(8).optional(),
18162
18428
  // Provider choice is explicit (no env-based silent branching). Default Groq
18163
18429
  // Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
18164
18430
  // `rich` JSON output with punctuated words + paragraph/sentence grouping.
18165
- transcriber: z47.enum(["groq", "deepgram"]).optional(),
18166
- keyterms: z47.array(z47.string().min(2).max(60)).max(30).optional().describe(
18431
+ transcriber: z48.enum(["groq", "deepgram"]).optional(),
18432
+ keyterms: z48.array(z48.string().min(2).max(60)).max(30).optional().describe(
18167
18433
  "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."
18168
18434
  ),
18169
- script: z47.string().min(1).optional().describe(
18435
+ script: z48.string().min(1).optional().describe(
18170
18436
  "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."
18171
18437
  )
18172
18438
  }).strict();
18173
- var VideoTranscribeInputs = z47.object({
18439
+ var VideoTranscribeInputs = z48.object({
18174
18440
  // A video (audio auto-extracted locally) OR a bare audio track. The key stays
18175
18441
  // `video` for back-compat; the backend already accepts audio-kind refs on it —
18176
18442
  // the local extraction path has been shipping one for every video input.
18177
- video: z47.union([VideoRef, AudioRef])
18443
+ video: z48.union([VideoRef, AudioRef])
18178
18444
  }).strict();
18179
- var VideoTranscribeOutputs = z47.object({
18180
- transcript: z47.custom(),
18445
+ var VideoTranscribeOutputs = z48.object({
18446
+ transcript: z48.custom(),
18181
18447
  // Only emitted by the Deepgram path: full punctuated words + paragraph /
18182
18448
  // sentence grouping with speaker indices. Absent for the default Groq path.
18183
- rich: z47.custom().optional()
18449
+ rich: z48.custom().optional()
18184
18450
  }).strict();
18185
18451
  var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
18186
18452
  var videoTranscribeNode = defineNode({
@@ -18293,7 +18559,7 @@ async function tryExtractAudio(inputs, ctx) {
18293
18559
  }
18294
18560
 
18295
18561
  // src/engine/nodes/remote/voiceSelect.ts
18296
- import { z as z48 } from "zod";
18562
+ import { z as z49 } from "zod";
18297
18563
  var voiceSelectNode = delegated({
18298
18564
  id: "voice_select",
18299
18565
  // 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
@@ -18311,27 +18577,27 @@ var voiceSelectNode = delegated({
18311
18577
  category: "audio",
18312
18578
  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.',
18313
18579
  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. `language` is strict: when no voice on the account speaks it the node fails before anything is voiced (add a voice in that language, or pass its `voice_id`). A `gender` nobody in that language has is dropped and `baker canvas run` prints a `[warn]` naming the voice cast instead.',
18314
- inputs: z48.object({}).loose(),
18315
- params: z48.object({
18316
- description: z48.string().min(1),
18317
- gender: z48.string().optional(),
18318
- age: z48.string().optional(),
18319
- accent: z48.string().optional(),
18320
- language: z48.string().optional(),
18580
+ inputs: z49.object({}).loose(),
18581
+ params: z49.object({
18582
+ description: z49.string().min(1),
18583
+ gender: z49.string().optional(),
18584
+ age: z49.string().optional(),
18585
+ accent: z49.string().optional(),
18586
+ language: z49.string().optional(),
18321
18587
  /**
18322
18588
  * A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
18323
18589
  * ranking is skipped and this id is emitted as-is.
18324
18590
  */
18325
- voice_id: z48.string().min(1).max(120).optional(),
18326
- limit: z48.number().int().min(1).max(20).optional()
18591
+ voice_id: z49.string().min(1).max(120).optional(),
18592
+ limit: z49.number().int().min(1).max(20).optional()
18327
18593
  }).strict(),
18328
- outputs: z48.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
18594
+ outputs: z49.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
18329
18595
  outputKinds: { voice_id: "text", candidates: "json" },
18330
18596
  cost: () => ({ credits: 0, seconds_estimate: 5 })
18331
18597
  });
18332
18598
 
18333
18599
  // src/engine/schema/catalog.ts
18334
- import { z as z49 } from "zod";
18600
+ import { z as z50 } from "zod";
18335
18601
  function generateCatalog(registry, opts = {}) {
18336
18602
  const entries2 = registry.all().map((def) => {
18337
18603
  const cost = catalogCost(def);
@@ -18342,9 +18608,9 @@ function generateCatalog(registry, opts = {}) {
18342
18608
  summary: def.summary,
18343
18609
  when_to_use: def.when_to_use,
18344
18610
  location: def.location,
18345
- inputs: z49.toJSONSchema(def.inputs, { unrepresentable: "any" }),
18346
- params: z49.toJSONSchema(def.params, { unrepresentable: "any" }),
18347
- outputs: z49.toJSONSchema(def.outputs, { unrepresentable: "any" }),
18611
+ inputs: z50.toJSONSchema(def.inputs, { unrepresentable: "any" }),
18612
+ params: z50.toJSONSchema(def.params, { unrepresentable: "any" }),
18613
+ outputs: z50.toJSONSchema(def.outputs, { unrepresentable: "any" }),
18348
18614
  cost_estimate_credits: cost?.credits,
18349
18615
  runtime_estimate_seconds: cost?.seconds_estimate
18350
18616
  };
@@ -18553,6 +18819,33 @@ function createEngineFromEnv(opts = {}) {
18553
18819
  }
18554
18820
 
18555
18821
  export {
18822
+ JEV_MODEL,
18823
+ askJevQuestions,
18824
+ askJev,
18825
+ looksLikeHttpUrl,
18826
+ MAX_REMOTE_IMAGE_BYTES,
18827
+ fetchExternalBytes,
18828
+ isVimeoAuthFailure,
18829
+ ytDlpBlockSignal,
18830
+ YtDlpError,
18831
+ runYtDlp,
18832
+ probeYtDlp,
18833
+ isAudioOnly,
18834
+ durationSeconds,
18835
+ canonicalUrl,
18836
+ RunAbortedError,
18837
+ LayerExecutionError,
18838
+ describeFailureReason,
18839
+ runFailureCode,
18840
+ sha256Hex,
18841
+ MAX_ASSET_BYTES,
18842
+ isPersistedAssetRef,
18843
+ collectAssetRefLikes,
18844
+ HttpClient,
18845
+ coercePathUrlToUrl,
18846
+ canvasAssetMime,
18847
+ inferMimeFromPath,
18848
+ toModelSafeImage,
18556
18849
  asRecord,
18557
18850
  shapeListRows,
18558
18851
  VIEWER_TEMPERATURE_VALUES,
@@ -18563,13 +18856,16 @@ export {
18563
18856
  classifyAudience,
18564
18857
  AWARENESS_VALUES,
18565
18858
  viewerFromKnowledge,
18566
- HttpClient,
18859
+ RENDER_REVIEW_MODEL,
18860
+ videoPathFromOutput,
18861
+ RENDER_REVIEW_UNAVAILABLE,
18862
+ askAboutImages,
18863
+ askAboutText,
18864
+ reviewRenderedVideo,
18865
+ reviewContextBeside,
18866
+ sceneWindowsBeside,
18567
18867
  BackendClient,
18568
18868
  requireCredentialsFromEnv,
18569
- RunAbortedError,
18570
- LayerExecutionError,
18571
- describeFailureReason,
18572
- runFailureCode,
18573
18869
  AD_PLATFORMS,
18574
18870
  deliverySet,
18575
18871
  AD_FORMAT_PLATFORMS,
@@ -18624,14 +18920,9 @@ export {
18624
18920
  BlueprintFloatingElementSchema,
18625
18921
  resolveConcurrency,
18626
18922
  ulid,
18627
- isPersistedAssetRef,
18628
- collectAssetRefLikes,
18629
- sha256Hex,
18630
- MAX_ASSET_BYTES,
18631
18923
  REF_PREFIX,
18632
18924
  parseRefExpr,
18633
18925
  nodeDependencies,
18634
- looksLikeHttpUrl,
18635
18926
  SEEDANCE_PROFILE,
18636
18927
  clipProfileFor,
18637
18928
  clipParamRecipe,
@@ -18645,8 +18936,9 @@ export {
18645
18936
  assignElementLabels,
18646
18937
  referenceRole,
18647
18938
  elementTodo,
18648
- claimedPlatform,
18649
- ratingFigures,
18939
+ jevCitationJudge,
18940
+ citedPlatforms,
18941
+ citedBy,
18650
18942
  figureForms,
18651
18943
  nativeSurfaceName,
18652
18944
  nativeCopy,
@@ -18654,6 +18946,10 @@ export {
18654
18946
  nativePhotosShown,
18655
18947
  avatarPhotoIds,
18656
18948
  nativeGateProblems,
18949
+ needsInterfaceWords,
18950
+ INTERFACE_WORDS_PROMPT_VERSION,
18951
+ interfaceWordsPrompt,
18952
+ readInterfaceWords,
18657
18953
  COPY_DIRECTIVE_HEADER,
18658
18954
  copyStrings,
18659
18955
  inSceneText,
@@ -18697,10 +18993,12 @@ export {
18697
18993
  STATIC_SAFE_FIT_ROLE,
18698
18994
  STATIC_PLACE_ROLE,
18699
18995
  LOGO_COMPOSITE_ROLE,
18996
+ SERIES_LEAD_ROLE,
18700
18997
  STATIC_FIX_ROLE,
18701
18998
  LOGO_COMPOSITE_DEFAULT,
18702
18999
  nodesWithRole,
18703
19000
  rolesById,
19001
+ relaidPictureRole,
18704
19002
  formatSlug,
18705
19003
  staticMasterFormat,
18706
19004
  staticRenderNodes,
@@ -18713,30 +19011,8 @@ export {
18713
19011
  wordCount2 as wordCount,
18714
19012
  estSpeechS,
18715
19013
  estSpeechWindowS,
18716
- MAX_REMOTE_IMAGE_BYTES,
18717
- fetchExternalBytes,
18718
- isVimeoAuthFailure,
18719
- ytDlpBlockSignal,
18720
- YtDlpError,
18721
- runYtDlp,
18722
- probeYtDlp,
18723
- isAudioOnly,
18724
- durationSeconds,
18725
- canonicalUrl,
18726
- coercePathUrlToUrl,
18727
- canvasAssetMime,
18728
- inferMimeFromPath,
18729
- toModelSafeImage,
18730
19014
  loadCompositionMeta,
18731
19015
  perceptualHash,
18732
- RENDER_REVIEW_MODEL,
18733
- videoPathFromOutput,
18734
- RENDER_REVIEW_UNAVAILABLE,
18735
- askAboutImages,
18736
- askAboutText,
18737
- reviewRenderedVideo,
18738
- reviewContextBeside,
18739
- sceneWindowsBeside,
18740
19016
  parseJudgeScores,
18741
19017
  judgeWinner,
18742
19018
  staticVerdict,
@@ -18758,4 +19034,4 @@ export {
18758
19034
  defaultRegistry,
18759
19035
  createEngineFromEnv
18760
19036
  };
18761
- //# sourceMappingURL=chunk-6PP3MBGZ.js.map
19037
+ //# sourceMappingURL=chunk-QQX5LPMQ.js.map