@koda-sl/baker-cli 0.421.0-dev.8ef74f67e → 0.421.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +35 -10
  2. package/dist/{chunk-BGAU2ZQF.js → chunk-EXWIIEZP.js} +4 -4
  3. package/dist/{chunk-6YAPMFMC.js → chunk-FKFJKE75.js} +43 -4
  4. package/dist/chunk-FKFJKE75.js.map +1 -0
  5. package/dist/{chunk-PFN3A4VQ.js → chunk-GLPQQP5C.js} +780 -408
  6. package/dist/chunk-GLPQQP5C.js.map +1 -0
  7. package/dist/{chunk-KBIEU3CK.js → chunk-LRRWIOJG.js} +3 -3
  8. package/dist/{chunk-QUFIEBEP.js → chunk-YZLLC7DL.js} +25 -4
  9. package/dist/chunk-YZLLC7DL.js.map +1 -0
  10. package/dist/{chunk-EXXI274U.js → chunk-ZADYWHL5.js} +3 -3
  11. package/dist/cli.js +3105 -1532
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-LYDB4ONI.js → client-CUHMSNK5.js} +4 -4
  14. package/dist/engine/index.js +3 -3
  15. package/dist/{env-37XB6PGZ.js → env-PJTJTW3N.js} +2 -2
  16. package/dist/{output-DYBL2XBN.js → output-A2ASA36U.js} +5 -5
  17. package/dist/{shared-XHOQLVMN.js → shared-LV6PIKFE.js} +6 -6
  18. package/package.json +1 -1
  19. package/dist/chunk-6YAPMFMC.js.map +0 -1
  20. package/dist/chunk-PFN3A4VQ.js.map +0 -1
  21. package/dist/chunk-QUFIEBEP.js.map +0 -1
  22. /package/dist/{chunk-BGAU2ZQF.js.map → chunk-EXWIIEZP.js.map} +0 -0
  23. /package/dist/{chunk-KBIEU3CK.js.map → chunk-LRRWIOJG.js.map} +0 -0
  24. /package/dist/{chunk-EXXI274U.js.map → chunk-ZADYWHL5.js.map} +0 -0
  25. /package/dist/{client-LYDB4ONI.js.map → client-CUHMSNK5.js.map} +0 -0
  26. /package/dist/{env-37XB6PGZ.js.map → env-PJTJTW3N.js.map} +0 -0
  27. /package/dist/{output-DYBL2XBN.js.map → output-A2ASA36U.js.map} +0 -0
  28. /package/dist/{shared-XHOQLVMN.js.map → shared-LV6PIKFE.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-6YAPMFMC.js";
4
+ } from "./chunk-FKFJKE75.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -15,16 +15,17 @@ import {
15
15
  isChallengeBody,
16
16
  isPrivateAddress,
17
17
  isProxyFailure,
18
+ nextPlannedRoute,
18
19
  openRouterKey,
19
20
  plannedRoutes,
20
21
  refuseNonPublicUrl,
21
22
  requireConnection,
22
23
  shouldEscalate
23
- } from "./chunk-QUFIEBEP.js";
24
+ } from "./chunk-YZLLC7DL.js";
24
25
 
25
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
26
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
26
27
  var require_safe_stable_stringify = __commonJS({
27
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
28
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
28
29
  "use strict";
29
30
  var { hasOwnProperty } = Object.prototype;
30
31
  var stringify = configure2();
@@ -5173,6 +5174,8 @@ var chatChangeTypeSchema = z14.enum([
5173
5174
  // A review site listing feeding the testimonials library, keyed on the
5174
5175
  // source's id. Immediate — see convex/testimonials/mutations.ts.
5175
5176
  "testimonial-source",
5177
+ // A Credential stored or removed, keyed on its name. Immediate — see convex/credentials/mutations.ts.
5178
+ "credential",
5176
5179
  // A Google spreadsheet created for a Form's leads. Immediate — see
5177
5180
  // convex/flows/destinations/mutations.ts.
5178
5181
  "spreadsheet",
@@ -10145,7 +10148,7 @@ function resolveAdaptFormats(params) {
10145
10148
  return params.formats ?? [];
10146
10149
  }
10147
10150
 
10148
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
10151
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
10149
10152
  var import__ = __toESM(require_safe_stable_stringify(), 1);
10150
10153
  var configure = import__.default.configure;
10151
10154
  var wrapper_default = import__.default;
@@ -10389,6 +10392,22 @@ var IMAGE_GENERATE_MODELS = [
10389
10392
  "google/gemini-3-pro-image-preview"
10390
10393
  ];
10391
10394
  var DEFAULT_IMAGE_GENERATE_MODEL = "google/gemini-3.1-flash-image";
10395
+ var IMAGE_PRICE_TIER = {
10396
+ "openai/gpt-image-2.5-sunburst": "standard",
10397
+ "openai/gpt-image-2": "standard",
10398
+ "openai/gpt-5.4-image-2": "standard",
10399
+ "google/gemini-3.1-flash-image": "standard",
10400
+ "google/gemini-3.1-flash-lite-image": "standard",
10401
+ "google/gemini-3-pro-image": "pro",
10402
+ "recraft/recraft-v4.1-pro-vector": "standard",
10403
+ "google/gemini-3.1-flash-image-preview": "standard",
10404
+ "google/gemini-3-pro-image-preview": "pro"
10405
+ };
10406
+ var IMAGE_TIER_CREDITS = { pro: 20, standard: 5 };
10407
+ function imagePictureCredits(model) {
10408
+ const tier = IMAGE_PRICE_TIER[model] ?? "standard";
10409
+ return IMAGE_TIER_CREDITS[tier];
10410
+ }
10392
10411
  var VIDEO_GENERATE_MODELS = [
10393
10412
  "bytedance/seedance-2.0",
10394
10413
  "bytedance/seedance-2.5",
@@ -11947,6 +11966,22 @@ function estimateNodeCredits(input) {
11947
11966
  return usd === null ? null : usdToCredits(usd);
11948
11967
  }
11949
11968
 
11969
+ // ../canvas-contract/src/personRealism.ts
11970
+ var PHONE_CAMERA = "Shot on a phone, an iPhone or a Samsung Galaxy, with the main camera in the default photo mode (not portrait mode): slightly wide angle, held by a friend about a metre away or at arm's length, the background mostly in focus the way phone photos are, the phone's own processing \u2014 HDR evening out a bright window, a touch of over-sharpening, smoothed noise in the shadows, punchy everyday colour. Slightly imperfect framing. Not a professional camera: no bokeh or creamy background blur, no lens compression, no studio or flash lighting, no colour grade.";
11971
+ var REAL_SKIN = "Real skin and a real face: visible pores, fine lines, uneven tone, small marks, stray hairs, slight asymmetry. Clothes with real creases.";
11972
+ var NOT_A_MODEL = "Not stock photography, not a model or an influencer: no beauty retouching, no glowing or plastic skin, no perfectly symmetrical features.";
11973
+ function candidPersonPortraitPrompt(p) {
11974
+ return [
11975
+ `An unretouched candid phone photo of one ordinary, real person: ${p.subject}`,
11976
+ p.wardrobe?.trim() ? `Wearing: ${p.wardrobe.trim()}` : null,
11977
+ `Where: ${p.setting?.trim() || "an everyday place that fits who they are, in daylight"}, available light only. They look toward the lens mid-conversation with a relaxed, unposed expression. Framed from the head to mid-torso, face clearly visible.`,
11978
+ PHONE_CAMERA,
11979
+ REAL_SKIN,
11980
+ NOT_A_MODEL,
11981
+ "No text, logos or watermarks. One person only."
11982
+ ].filter((line) => line !== null).join("\n");
11983
+ }
11984
+
11950
11985
  // ../canvas-contract/src/validateParams.ts
11951
11986
  function isManagedNodeKind(nodeType) {
11952
11987
  return Object.keys(MODEL_REGISTRY).includes(nodeType);
@@ -13647,8 +13682,10 @@ var GPT_IMAGE_BRIEF = {
13647
13682
  texture: "real textures, true-to-life light and colour",
13648
13683
  purpose: ""
13649
13684
  },
13650
- // The user: people look "too AI", faces too dark; wants natural, real-looking people (BENCH.md § "E1 v2").
13651
- people: "people look real: natural skin texture, well-lit faces, relaxed everyday expressions"
13685
+ // The user: people look "too AI", faces too dark; wants natural, real-looking people (BENCH.md § "E1 v2"),
13686
+ // and again on the final sets, "more natural please" (§ "Fix round"); and real people's photos are
13687
+ // taken on a phone, not a professional camera (AV2, scripts/creative-evals/avatar-realism.mjs).
13688
+ people: "candid people shot on a phone: real skin, natural light, relaxed expressions"
13652
13689
  },
13653
13690
  // n 1: the variants are one call per direction, not one n=3 call. Same cost (n is billed per
13654
13691
  // image, E0) and returns 3/3 either way, but one n=3 call gave >=2 distinct options in 25% of
@@ -13683,7 +13720,7 @@ var GEMINI_IMAGE_BRIEF = {
13683
13720
  purpose: "because a polished picture reads as an ad and this one has to read as a real post"
13684
13721
  },
13685
13722
  produced: { photo: "a real photograph", texture: "real textures and true-to-life light and colour", purpose: "" },
13686
- people: "the people look real, with natural skin texture, well-lit faces and relaxed everyday expressions"
13723
+ people: "the people look real and candid, shot on a phone, with natural skin, natural light and relaxed expressions"
13687
13724
  },
13688
13725
  // No `n` on Gemini image (gemini §0.4); Pro 2K costs the same as 1K and small text blurs
13689
13726
  // at 1K (gemini §0.3).
@@ -13769,14 +13806,15 @@ function imageRenderParams(model, want) {
13769
13806
  var STORIES = { top: 0.16, bottom: 0.22, left: 0.06, right: 0.06 };
13770
13807
  var REELS_SAFE_BOTTOM = 0.37;
13771
13808
  var FEED = { top: 0.06, bottom: 0.06, left: 0.07, right: 0.07 };
13809
+ var FEED_IMAGE = { top: 0.04, bottom: 0.04, left: 0.04, right: 0.04 };
13772
13810
  var EVERY_SURFACE = /* @__PURE__ */ new Set(["automatic", "facebook", "instagram"]);
13773
13811
  function staticSafeArea(format, placements) {
13774
- if (format !== "9:16") return { ...FEED, placement: "feed" };
13812
+ if (format !== "9:16") return { ...FEED_IMAGE, placement: "feed" };
13775
13813
  const named = (placements ?? []).map((p) => p.toLowerCase());
13776
13814
  const reels = named.length === 0 || named.some((p) => p.includes("reels") || EVERY_SURFACE.has(p));
13777
13815
  if (reels) return { ...STORIES, bottom: REELS_SAFE_BOTTOM, placement: "reels" };
13778
13816
  if (named.some((p) => p.includes("stor"))) return { ...STORIES, placement: "stories" };
13779
- return { ...FEED, placement: "feed" };
13817
+ return { ...FEED_IMAGE, placement: "feed" };
13780
13818
  }
13781
13819
  function videoSafeArea(format) {
13782
13820
  return format === "9:16" ? { ...STORIES, bottom: REELS_SAFE_BOTTOM } : FEED;
@@ -14057,18 +14095,17 @@ function readAdSpec(raw) {
14057
14095
  const advertiser = raw?.reference?.advertiser;
14058
14096
  return adSpecFromBlueprint(raw, { scrub: typeof advertiser === "string" ? [advertiser] : [] });
14059
14097
  }
14060
- var IN_SCENE_MAX_WORDS = 3;
14061
- var wordsIn = (text) => text.split(/\s+/).filter(Boolean).length;
14062
- function withShortInSceneLettering(spec) {
14063
- const surface = (line, i) => line.on ? line.on.trim().toLowerCase() : `#${i}`;
14064
- const words = /* @__PURE__ */ new Map();
14065
- spec.copy.forEach((line, i) => {
14066
- if (line.lettering !== "in_scene") return;
14067
- words.set(surface(line, i), (words.get(surface(line, i)) ?? 0) + wordsIn(line.text));
14068
- });
14069
- if (![...words.values()].some((n) => n > IN_SCENE_MAX_WORDS)) return spec;
14070
- const copy = spec.copy.map((line, i) => {
14071
- if (line.lettering !== "in_scene" || (words.get(surface(line, i)) ?? 0) <= IN_SCENE_MAX_WORDS) return line;
14098
+ var HAND_LETTERED = /\b(?:hand[- ]?(?:written|writing|lettered|lettering|drawn)|handwrit\w*|scrawl\w*|scribbl\w*|markers?|felt[- ]?(?:tip|pen)s?|chalked|(?:written|drawn|lettered|painted) by hand)\b/i;
14099
+ var HANDWRITING = new RegExp(
14100
+ `${HAND_LETTERED.source}|\\b(?:chalk\\w*|whiteboards?|blackboards?|sticky[- ]notes?|post[- ]?its?|notepads?|notebooks?|napkins?)\\b`,
14101
+ "i"
14102
+ );
14103
+ var handwritten = (line) => !line.on?.trim() || HANDWRITING.test(`${line.on} ${line.look ?? ""} ${line.position ?? ""}`);
14104
+ function withRenderedLettering(spec) {
14105
+ const designed = (line) => line.lettering === "in_scene" && handwritten(line);
14106
+ if (!spec.copy.some(designed)) return spec;
14107
+ const copy = spec.copy.map((line) => {
14108
+ if (!designed(line)) return line;
14072
14109
  const { on, look: _look, position, ...rest } = line;
14073
14110
  const over = position ?? (on ? `over ${on}` : void 0);
14074
14111
  return { ...rest, lettering: "designed", ...over ? { position: over } : {} };
@@ -14169,14 +14206,22 @@ function referencesFromBlueprint(bp, scrub) {
14169
14206
  ({ collection, entry }) => str(entry.reference_image) && !(collection === "brands_logos" && str(entry.function_in_image) === "advertiser_brand")
14170
14207
  );
14171
14208
  const counters = /* @__PURE__ */ new Map();
14172
- return labelled.map(({ collection, entry }) => {
14209
+ const pictured = labelled.map(({ collection, entry }) => {
14173
14210
  const { role, prefix } = referenceRoleOf(collection, entry);
14174
14211
  const n = (counters.get(prefix) ?? 0) + 1;
14175
14212
  counters.set(prefix, n);
14176
- const described = entry.description ?? entry.apparent_identity_or_role ?? entry.what_it_represents;
14177
- const what = plainText(described, scrub) || `the ${role === "identity" ? "subject" : role} in this picture`;
14213
+ const described2 = entry.description ?? entry.apparent_identity_or_role ?? entry.what_it_represents;
14214
+ const what = plainText(described2, scrub) || `the ${role === "identity" ? "subject" : role} in this picture`;
14178
14215
  return { id: `${prefix}_${n}`, role, slot: sanitizeId(`el_${str(entry.reference_image)}`, "el"), what };
14179
14216
  });
14217
+ const described = entries(bp.subjects).flatMap((entry) => {
14218
+ const what = plainText(entry.description, scrub);
14219
+ if (str(entry.reference_image) || !what || referenceRoleOf("subjects", entry).role !== "product") return [];
14220
+ const n = (counters.get("PRODUCT") ?? 0) + 1;
14221
+ counters.set("PRODUCT", n);
14222
+ return [{ id: `PRODUCT_${n}`, role: "product", what }];
14223
+ });
14224
+ return [...pictured, ...described];
14180
14225
  }
14181
14226
  function logoPositionOf(bp, scrub) {
14182
14227
  const own = entries(bp.brands_logos).find((e) => str(e.function_in_image) === "advertiser_brand");
@@ -14354,6 +14399,7 @@ function copyAsLineRefs(text, copy) {
14354
14399
  }
14355
14400
  function unwanted(clause, k) {
14356
14401
  if (k.logo && /\bno (logos?|wordmarks?|branding)\b/i.test(clause)) return true;
14402
+ if (HAND_LETTERED.test(clause)) return true;
14357
14403
  if (/\bsource (ad|composition)\b|\bORIGINAL\b|\bcompetitor|\breference ad\b/.test(clause)) return true;
14358
14404
  return k.positive && NEGATION.test(clause.replace(/"[^"\n]*"/g, ""));
14359
14405
  }
@@ -14376,7 +14422,10 @@ function clip(text, max) {
14376
14422
  if (wordCount(out + s) > max) break;
14377
14423
  out += s;
14378
14424
  }
14379
- return sentence((out.trim() || text.split(/\s+/).slice(0, max).join(" ")).replace(/[\s,;:]+$/, ""));
14425
+ if (out.trim()) return sentence(out.trim().replace(/[\s,;:]+$/, ""));
14426
+ const head = text.split(/\s+/).slice(0, max).join(" ");
14427
+ const clause = head.match(/^(.*?)[,;:—–](?:\s[^,;:—–]*)?$/)?.[1];
14428
+ return sentence((clause?.trim() || head).replace(/[\s,;:]+$/, ""));
14380
14429
  }
14381
14430
  function neutralName(l) {
14382
14431
  if (l > 0.95) return "white";
@@ -14500,7 +14549,9 @@ function logoInstruction(p, ctx) {
14500
14549
  const at2 = position ? `, ${position.replace(/[\s,;:.!?]+$/, "")}` : "";
14501
14550
  const plate = ctx.plates.length > 0 ? ctx.plates.map((u) => `${u.name} (${u.hex})`).join(" or ") : "white or black";
14502
14551
  const where = `at least a fifth of the frame wide, on an area it contrasts with or a small ${plate} plate`;
14503
- return ctx.flowing ? `the brand's real logo. Reproduce it once${at2}, exactly as drawn in Image ${p.index}, ${where}.` : `the brand's real logo. Reproduce it once${at2}, exactly as drawn, ${where}; never tiny.`;
14552
+ if (ctx.short)
14553
+ return "the brand's real logo. Reproduce it once, exactly as drawn, at least a fifth of the frame wide.";
14554
+ return ctx.flowing ? `the brand's real logo. Reproduce it once, apart from the product${at2}, exactly as drawn in Image ${p.index}, ${where}.` : `the brand's real logo. Reproduce it once, never on the product${at2}, exactly as drawn, ${where}.`;
14504
14555
  }
14505
14556
  function identityInstruction(p, what, keep, flowing) {
14506
14557
  if (p.spec && personOrAnimal(p.spec) === "person") {
@@ -14600,6 +14651,7 @@ var ROLE_WORDS = {
14600
14651
  discount: "the discount",
14601
14652
  cta: "the button",
14602
14653
  button: "the button",
14654
+ call_to_action: "the button",
14603
14655
  caption: "a caption",
14604
14656
  fine_print: "the fine print",
14605
14657
  legal: "the fine print",
@@ -14640,7 +14692,8 @@ function textNotes(copy, profile) {
14640
14692
  notes.push(`Keep every accent and mark exactly: ${marks.join(" ")}.`);
14641
14693
  return notes;
14642
14694
  }
14643
- var isButton = (l) => l.role?.toLowerCase() === "cta" || /button/i.test(l.look ?? "");
14695
+ var BUTTON_ROLES = /* @__PURE__ */ new Set(["cta", "button", "call_to_action"]);
14696
+ var isButton = (l) => BUTTON_ROLES.has(roleKey(l.role)) || /button/i.test(l.look ?? "");
14644
14697
  var FIGURE_ROLES = /* @__PURE__ */ new Set(["price", "stat", "figure", "number", "discount"]);
14645
14698
  var HEADLINE_ROLES = /* @__PURE__ */ new Set(["headline", "title"]);
14646
14699
  var FINE_PRINT_ROLES = /* @__PURE__ */ new Set(["fine_print", "legal", "disclaimer", "footnote"]);
@@ -14651,7 +14704,7 @@ function fullHierarchy(ranked, lead) {
14651
14704
  const { designed, figure, headline, button } = ranked;
14652
14705
  const size = lead === figure ? "very large" : "at least half the frame wide";
14653
14706
  return [
14654
- ...lead ? [`line ${lead.n} largest (${size}), read first on a phone`] : [],
14707
+ ...lead ? [`line ${lead.n} largest (${size}), read first`] : [],
14655
14708
  ...lead && figure && headline ? [`then line ${headline.n}`] : [],
14656
14709
  ...button ? [`line ${button.n} ${BUTTON_SIZE}`] : [],
14657
14710
  ...designed.length > 1 ? ["every line readable at a glance"] : [],
@@ -14699,11 +14752,11 @@ function brandRoleColours(spec, brand) {
14699
14752
  return roles.flatMap(([applies, object, hex]) => applies && hex ? [{ object, name: colourName(hex), hex }] : []);
14700
14753
  }
14701
14754
  var NATIVE_CAPTURE = /\b(selfie|phone[- ]camera|phone photo|shot on (?:a )?phone|taken on (?:a )?phone|smartphone photo|ugc|user[- ]generated|webcam|front[- ]camera|snapshot|native caption)\b/i;
14702
- var PHONE_CAMERA = /\b(phone|smartphone|iphone)\b/i;
14755
+ var PHONE_CAMERA2 = /\b(phone|smartphone|iphone)\b/i;
14703
14756
  function isNativeCapture(spec) {
14704
14757
  if (spec.scene.look !== "photo") return false;
14705
14758
  const capture = spec.scene.capture ?? "";
14706
- return NATIVE_CAPTURE.test(`${spec.scene.description} ${capture}`) || PHONE_CAMERA.test(capture);
14759
+ return NATIVE_CAPTURE.test(`${spec.scene.description} ${capture}`) || PHONE_CAMERA2.test(capture);
14707
14760
  }
14708
14761
  function colourUses(spec, brand, k) {
14709
14762
  const allowed = brand ? new Set(
@@ -14747,10 +14800,10 @@ function variantDirection(c) {
14747
14800
  }
14748
14801
  var DEPARTS = "It leads where it differs from the scene and line notes above.";
14749
14802
  var DEPARTS_SHORT = "It leads over the notes above.";
14750
- var SUPPORT_ROLE = /\b(?:cta|fine[_ ]?print|legal|footnote|attribution|disclaimer)\b/i;
14803
+ var isSupport = (l) => isButton(l) || FINE_PRINT_ROLES.has(roleKey(l.role)) || roleKey(l.role) === "attribution";
14751
14804
  function hookDirection(c) {
14752
14805
  const hooks = c.spec.copy.flatMap(
14753
- (l, i) => l.lettering === "designed" && !isButton(l) && !SUPPORT_ROLE.test(l.role ?? "") ? [{ n: i + 1, text: l.text }] : []
14806
+ (l, i) => l.lettering === "designed" && !isSupport(l) ? [{ n: i + 1, text: l.text }] : []
14754
14807
  );
14755
14808
  const [first, ...rest] = hooks;
14756
14809
  const lead = rest.find((l) => /\d/.test(l.text)) ?? rest[0];
@@ -14806,7 +14859,7 @@ function pictureDirection(c) {
14806
14859
  ];
14807
14860
  }
14808
14861
  function compileStaticBrief(written, brandOverride, format, viewerOverride, profile, opts = {}) {
14809
- const spec = withShortInSceneLettering(written);
14862
+ const spec = withRenderedLettering(written);
14810
14863
  const job = opts.job ?? "generate";
14811
14864
  const designedLines2 = spec.copy.flatMap((l, i) => l.lettering === "designed" ? [i + 1] : []);
14812
14865
  const pics = pictures(spec, opts, job, profile, designedLines2.length > 0);
@@ -14844,7 +14897,9 @@ function compileStaticBrief(written, brandOverride, format, viewerOverride, prof
14844
14897
  words: wordCount(prompt)
14845
14898
  };
14846
14899
  }
14847
- var MAX_TRIM = 15;
14900
+ var MAX_TRIM = 16;
14901
+ var LAST_RESORT_LEVEL = 16;
14902
+ var PRODUCT_NOTE_WORDS = 10;
14848
14903
  var EXTRAS_SHORT_LEVEL = 15;
14849
14904
  var CAPTURE_LEVEL = 9;
14850
14905
  var SHORT_DIRECTION_LEVEL = 10;
@@ -14919,6 +14974,7 @@ function picturesSection(c) {
14919
14974
  plates: plateColours(c.spec, c.brand, c.clean),
14920
14975
  thirdParty: c.spec.third_party_brands,
14921
14976
  flowing: c.flowing,
14977
+ short: c.level >= LAST_RESORT_LEVEL,
14922
14978
  ...c.spec.logo_position ? { logoPosition: c.spec.logo_position } : {}
14923
14979
  };
14924
14980
  const lines = c.pics.map((p) => {
@@ -14945,8 +15001,12 @@ function photoLook(c) {
14945
15001
  const purpose = r.purpose ? `, ${r.purpose}` : "";
14946
15002
  return `It is ${r.photo}${capture ? `, ${capture}` : ""}, with ${r.texture}${purpose}.`;
14947
15003
  }
15004
+ function animalsText(c) {
15005
+ if (!showsAnimals(c.spec) || c.level >= EXTRAS_SHORT_LEVEL) return "";
15006
+ return c.flowing ? " The animals look real, with real fur, natural light and a candid moment." : " Animals look real: real fur, natural light, a candid moment.";
15007
+ }
14948
15008
  function lookText(c) {
14949
- const people = showsPeople(c.spec) ? peopleText(c) : "";
15009
+ const people = `${showsPeople(c.spec) ? peopleText(c) : ""}${animalsText(c)}`;
14950
15010
  if (c.spec.scene.look === "photo") return `${photoLook(c)}${people}`;
14951
15011
  const look = c.spec.scene.look === "illustration" ? ILLUSTRATION_LOOK : DESIGNED_LOOK;
14952
15012
  return `${c.flowing ? look.flowing : look.labelled}${people}`;
@@ -14965,27 +15025,66 @@ function peopleText(c) {
14965
15025
  const said = c.level >= EXTRAS_SHORT_LEVEL ? "People look real, faces well lit." : sentence(capitalize(cue));
14966
15026
  return ` ${said}${target2 ? ` Their gaze or pose leads toward line ${target2.n}.` : ""}`;
14967
15027
  }
15028
+ var looksRendered = (spec) => showsPeople(spec) || showsAnimals(spec) || showsScreen(spec);
15029
+ var ANIMALS = /\b(dogs?|puppy|puppies|cats?|kittens?|pets?|horses?|birds?|rabbits?|animals?)\b/i;
15030
+ function showsAnimals(spec) {
15031
+ if (spec.scene.look === "illustration") return false;
15032
+ if (spec.references.some((r) => r.id.startsWith("ANIMAL_"))) return true;
15033
+ return spec.scene.look === "photo" && ANIMALS.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
15034
+ }
14968
15035
  var SCREEN = /\b(apps?|screens?|lock[- ]screen|dashboard|interface|mockup|notification|laptop|tablet|browser|website|chat bubbles?|ui)\b/i;
14969
15036
  function showsScreen(spec) {
14970
15037
  const ons = spec.copy.map((l) => l.on ?? "").join(" ");
14971
15038
  return SCREEN.test(`${spec.scene.description} ${spec.scene.layout ?? ""} ${ons}`);
14972
15039
  }
14973
15040
  var SCREENS = {
14974
- labelled: "Screens: crisp, realistic device interfaces, sharp and true to proportion; any words on them from the quoted lines.",
14975
- flowing: "Every phone or app screen is a crisp, realistic device interface, sharp and true to proportion, any words on it from the quoted lines."
15041
+ labelled: "Screens: a real phone photographed, or a real screenshot: real app density, a real status bar, flat true-to-life colours; any words on them from the quoted lines.",
15042
+ flowing: "Every phone or app screen looks like a real phone photographed or a real screenshot, with real app density, a real status bar and flat true-to-life colours, any words on it from the quoted lines."
14976
15043
  };
15044
+ var SCREENSHOT = /\b(screenshots?|screen ?grabs?|screens?)\b/i;
15045
+ var screenshotOf = (pics) => pics.find((p) => p.spec && p.role !== "identity" && SCREENSHOT.test(p.spec.what));
14977
15046
  function screensText(c) {
15047
+ const shot = screenshotOf(c.pics);
15048
+ if (shot) {
15049
+ return c.flowing ? ` Every screen shows Image ${shot.index} exactly as it is, on a real phone, photographed.` : `
15050
+ Screens: every screen shows Image ${shot.index} exactly as it is, on a real phone, photographed.`;
15051
+ }
14978
15052
  if (!showsScreen(c.spec) || c.level >= EXTRAS_SHORT_LEVEL) return "";
14979
15053
  return c.flowing ? ` ${SCREENS.flowing}` : `
14980
15054
  ${SCREENS.labelled}`;
14981
15055
  }
15056
+ function unwiredProductsText(c) {
15057
+ const wired = new Set(c.pics.map((p) => p.label));
15058
+ return c.spec.references.filter((r) => r.role === "product" && !wired.has(r.id)).map((r) => {
15059
+ const what = plain(r.what, c.clean).replace(/[.!?]+$/, "");
15060
+ const told = c.level >= LAST_RESORT_LEVEL ? clipNote(what.split(/[,;]/)[0] ?? what, PRODUCT_NOTE_WORDS) : what;
15061
+ return ` The product, ${told}, is in the frame, whole and clearly visible.`;
15062
+ }).join("");
15063
+ }
15064
+ function unwiredPeopleText(c) {
15065
+ const wired = new Set(c.pics.map((p) => p.label));
15066
+ return c.spec.references.filter((r) => r.role === "identity" && r.id.startsWith("PERSON_") && !wired.has(r.id)).map((r) => {
15067
+ const what = plain(r.what, c.clean).replace(/[.!?]+$/, "");
15068
+ const told = c.level >= LAST_RESORT_LEVEL ? clipNote(what.split(/[,;]/)[0] ?? what, PRODUCT_NOTE_WORDS) : what;
15069
+ return ` The person, ${told}, is an invented, ordinary real person \u2014 real skin, unposed, never a model.`;
15070
+ }).join("");
15071
+ }
15072
+ var BEFORE_AFTER = /\bbefore\b[^.]{0,40}\bafter\b|\bantes\b[^.]{0,30}\bdespu[eé]s\b|\bavant\b[^.]{0,30}\bapr[eè]s\b|\bvorher\b[^.]{0,30}\bnachher\b|\bprima\b[^.]{0,30}\bdopo\b/i;
15073
+ function isBeforeAfter(spec) {
15074
+ const told = [spec.scene.description, spec.scene.layout, spec.direction, ...spec.mechanism.map((m) => m.device)];
15075
+ return BEFORE_AFTER.test(told.filter(Boolean).join(" \n "));
15076
+ }
15077
+ var BEFORE_AFTER_TEXT = " The two states it compares differ clearly at a glance, even as a thumbnail; only the result changes between them.";
14982
15078
  function sceneSection(c) {
14983
15079
  if (c.job !== "generate") return [];
14984
15080
  const scene = c.spec.scene;
14985
- const description = capitalize(clip(sentence(plain(scene.description, c.clean)), SCENE_CAP[c.level] ?? 15));
15081
+ const products = `${unwiredProductsText(c)}${unwiredPeopleText(c)}${isBeforeAfter(c.spec) ? BEFORE_AFTER_TEXT : ""}`;
15082
+ const told = normalized(products);
15083
+ const own = (sentence(plain(scene.description, c.clean)).match(/[^.!?]+[.!?]*/g) ?? []).filter((s) => !products || !told.includes(normalized(s.replace(/^\s*in the frame:?/i, "")))).join("").trim();
15084
+ const description = capitalize(clip(own || sentence(plain(scene.description, c.clean)), SCENE_CAP[c.level] ?? 15));
14986
15085
  const mood = scene.mood && c.level < 1 ? ` The mood is ${sentence(plain(scene.mood, c.clean))}` : "";
14987
15086
  const out = [
14988
- c.flowing ? `${description}${mood} ${lookText(c)}${screensText(c)}${otherBrandsText(c)}` : `Scene: ${description}${mood}
15087
+ c.flowing ? `${description}${products}${mood} ${lookText(c)}${screensText(c)}${otherBrandsText(c)}` : `Scene: ${description}${products}${mood}
14989
15088
  ${lookText(c)}${screensText(c)}${otherBrandsText(c)}`
14990
15089
  ];
14991
15090
  const layout = scene.layout && c.level < 2 ? unsaid(plain(scene.layout, c.clean), plain(scene.description, c.clean)) : "";
@@ -15009,7 +15108,11 @@ function textSection(c) {
15009
15108
  ...textNotes(copy, c.profile)
15010
15109
  ].join("\n");
15011
15110
  }
15012
- var productLabel = (c) => c.pics.some((p) => p.role === "product") ? " besides the product's own label" : "";
15111
+ function productLabel(c) {
15112
+ const shot = screenshotOf(c.pics);
15113
+ if (shot) return ` besides the words on Image ${shot.index}'s screen`;
15114
+ return c.pics.some((p) => p.role === "product") ? " besides the product's own label" : "";
15115
+ }
15013
15116
  function colourSection(c) {
15014
15117
  if (c.job !== "generate") return [];
15015
15118
  const uses = colourUses(c.spec, c.brand, c.clean);
@@ -15022,7 +15125,7 @@ function layoutSection(c) {
15022
15125
  c.pics.some((p) => p.role === "logo"),
15023
15126
  c.spec.copy.some(isButton)
15024
15127
  );
15025
- const safe = c.opts.band?.margin ? marginText(c.opts.band, items, c.profile.brief.structure) : c.opts.band ? bandText(c.opts.band, items, c.flowing, staticSafeArea(c.format, c.opts.placements).placement !== "feed") : safeAreaText(c.format, c.opts.placements, items, c.profile.brief.structure);
15128
+ const safe = c.opts.zoned ? "" : c.opts.band?.margin ? marginText(c.opts.band, items, c.profile.brief.structure) : c.opts.band ? bandText(c.opts.band, items, c.flowing, staticSafeArea(c.format, c.opts.placements).placement !== "feed") : safeAreaText(c.format, c.opts.placements, items, c.profile.brief.structure);
15026
15129
  const out = [safe, legibilityText(c), fillText(c)].filter(Boolean).join(" ");
15027
15130
  return c.flowing ? out : `Layout: ${out}`;
15028
15131
  }
@@ -15052,8 +15155,8 @@ function positiveConstraints(c) {
15052
15155
  const others = thirdPartyNames(c.spec);
15053
15156
  if (n === 0)
15054
15157
  return `The only words in the finished image are none at all: it is picture only${logo ? `, with the one logo from Image ${logo.index}` : ""}.`;
15055
- const label = productLabel(c) ? ", besides the product's own label" : "";
15056
- const onlyLogo = !logo ? "" : others ? `, and the logos are the one in Image ${logo.index} and ${others}` : `, and the one logo is the one in Image ${logo.index}`;
15158
+ const label = productLabel(c) ? `,${productLabel(c)}` : "";
15159
+ const onlyLogo = !logo ? "" : others ? `, and the logos are the one in Image ${logo.index} and ${others}, the only brand marks anywhere, vehicles, packaging and clothing included` : `, and the one logo is the one in Image ${logo.index}, the only brand mark anywhere, vehicles, packaging and clothing included`;
15057
15160
  return `The only words in the finished image are the ${n} quoted lines above, each exactly once; every other surface carries picture only${label}${onlyLogo}.`;
15058
15161
  }
15059
15162
  function constraintsSection(c) {
@@ -15066,12 +15169,13 @@ function constraintsSection(c) {
15066
15169
  ];
15067
15170
  const avoid = [
15068
15171
  n > 0 ? `any words other than the ${n} quoted lines` : "any words or letters",
15069
- logos.length > 0 ? `any logo other than ${logos.join(" and ")}` : "any logo or wordmark",
15172
+ // A Toyota badge appeared on a car-loan ad's SUV (BENCH.md § "Final"): the allow-list covers the scene's things too.
15070
15173
  // The words and the identity's "show them once" are said above; brief v2 spends the words on the logo and the button.
15071
- "badges, icons, seals or stickers not named here",
15174
+ `${logos.length > 0 ? `any logo, badge, icon, seal or sticker other than ${logos.join(" and ")}` : "any logo or wordmark, badge, icon, seal or sticker"}, on vehicles, packaging and clothing too`,
15072
15175
  "watermarks",
15073
15176
  ...c.job === "generate" && isNativeCapture(c.spec) ? ["a staged, retouched or studio-lit look"] : [],
15074
- ...c.job === "generate" && showsPeople(c.spec) ? ["glossy, airbrushed skin or faces in shadow"] : []
15177
+ // The user found the people, a dog and a phone screen "very AI" (BENCH.md § "Fix round").
15178
+ ...c.job !== "generate" ? [] : showsPeople(c.spec) ? ["glossy, CGI or airbrushed skin; faces in shadow"] : looksRendered(c.spec) ? ["a glossy CGI look"] : []
15075
15179
  ];
15076
15180
  return `Avoid: ${avoid.join("; ")}.`;
15077
15181
  }
@@ -15108,6 +15212,9 @@ function judgeBrief(brief, opts = {}) {
15108
15212
  brief
15109
15213
  ].join("\n");
15110
15214
  }
15215
+ var NUMBERED_COPY = /^(\d+\. )"(.*)"( — )/gm;
15216
+ var withoutCopy = (prompt) => prompt.replace(NUMBERED_COPY, '$1""$3');
15217
+ var briefWords = (prompt) => wordCount(withoutCopy(prompt));
15111
15218
 
15112
15219
  // src/engine/scaffold/static-sizes.ts
15113
15220
  function ratioValue2(ratio) {
@@ -15271,24 +15378,26 @@ function logoSlot(frame, safe, opts = {}) {
15271
15378
  var STATIC_ADAPT_FORMAT_ROLE = "static_adapt_format";
15272
15379
  function adaptPlan(model, format, placements) {
15273
15380
  const renderRatio = nearestSupportedAspectRatio("image_generate", model, format);
15381
+ return {
15382
+ format,
15383
+ ...placements && placements.length > 0 ? { placements: [...placements] } : {},
15384
+ renderRatio,
15385
+ size: deliveredSize(format),
15386
+ safe: safeAreaOnFrame(renderRatio, format, placements)
15387
+ };
15388
+ }
15389
+ function safeAreaOnFrame(frame, format, placements) {
15274
15390
  const own = staticSafeArea(format, placements);
15275
- const rendered = ratioValue2(renderRatio);
15391
+ const rendered = ratioValue2(frame);
15276
15392
  const delivered = ratioValue2(format);
15277
15393
  const keepH = rendered < delivered ? rendered / delivered : 1;
15278
15394
  const keepW = rendered > delivered ? delivered / rendered : 1;
15279
- const safe = {
15395
+ return {
15280
15396
  top: (1 - keepH) / 2 + own.top * keepH,
15281
15397
  bottom: (1 - keepH) / 2 + own.bottom * keepH,
15282
15398
  left: (1 - keepW) / 2 + own.left * keepW,
15283
15399
  right: (1 - keepW) / 2 + own.right * keepW
15284
15400
  };
15285
- return {
15286
- format,
15287
- ...placements && placements.length > 0 ? { placements: [...placements] } : {},
15288
- renderRatio,
15289
- size: deliveredSize(format),
15290
- safe
15291
- };
15292
15401
  }
15293
15402
  function trimmedRenderBand(plan) {
15294
15403
  const own = staticSafeArea(plan.format, plan.placements);
@@ -15322,65 +15431,119 @@ function adaptNoun(plan) {
15322
15431
  if (r === 1) return "square feed ad";
15323
15432
  return r < 1 ? "vertical feed ad" : "landscape ad";
15324
15433
  }
15325
- function adaptDeliverable(plan, brand, flowing, wordsOnly2) {
15434
+ function adaptDeliverable(plan, spec, flowing, ground, short) {
15435
+ const brand = spec.brand?.name;
15326
15436
  const noun = `${adaptNoun(plan)}${brand ? ` for ${brand}` : ""}`;
15327
- const cast = wordsOnly2 ? `${SAME_CAST} ${GROUND_ONLY}` : SAME_CAST;
15328
- return flowing ? `Image 1 is a finished ad. Re-lay out this exact ad as a ${noun}: its message, every word, its people, product, logo, colours and typeface, in a new layout built for this frame, with the text, the logo, the button and the picture moved and re-scaled to fit the shape and the scene filling the whole frame edge to edge, rather than Image 1 shrunk onto a background or set as a card or panel. Every person, product and line appears whole and once. ${cast}` : `Deliverable: the ad in Image 1, re-laid out as a finished ${noun}. Keep its message, every word, its people, product, logo, colours and typeface; build a new layout for this frame: move and re-scale the text, the logo, the button and the picture to fit the shape. Do not shrink Image 1 onto a background or set it as a card, panel or framed picture: the scene fills the whole frame, edge to edge. Nothing is cropped: every person, product and line appears whole, and once. ${cast}`;
15329
- }
15330
- var SAME_CAST = "The people, products and objects are exactly the ones in Image 1, the same individuals; add no person, animal, product or object that Image 1 does not show.";
15437
+ if (short && !flowing) {
15438
+ const keep = [
15439
+ ...showsPeople2(spec) ? ["each person the same face and clothes"] : [],
15440
+ ...isBeforeAfter(spec) ? ["the two states as different"] : []
15441
+ ];
15442
+ return `Deliverable: the ad in Image 1, re-laid out as a finished ${noun}: the same message, words, people, product, logo, colours and typeface, the scene filling the frame edge to edge, never Image 1 shrunk onto a background. Every person, product and line appears whole and once, as in Image 1${keep.length > 0 ? `, ${keep.join(", ")}` : ""}; add nothing new.`;
15443
+ }
15444
+ const cast = [
15445
+ SAME_CAST,
15446
+ ...showsPeople2(spec) ? [SAME_PERSON] : [],
15447
+ ...isBeforeAfter(spec) ? [SAME_CHANGE] : [],
15448
+ ...ground ? [GROUND_ONLY] : []
15449
+ ].join(" ");
15450
+ return flowing ? `Image 1 is a finished ad. Re-lay out this exact ad as a ${noun}: the same message, words, people, product, logo, colours and typeface in a new layout for this frame, the scene filling it edge to edge rather than Image 1 set on a background as a card, panel or square. ${cast}` : `Deliverable: the ad in Image 1, re-laid out as a finished ${noun}: the same message, words, people, product, logo, colours and typeface in a new layout for this frame, the scene filling it edge to edge, never Image 1 shrunk onto a background or set as a card, panel or square. ${cast}`;
15451
+ }
15452
+ var SAME_CAST = "Every person, product and line appears whole and once, the same individuals as in Image 1; add no person, animal, product or object that Image 1 does not show.";
15453
+ var SAME_PERSON = "Each person is the same person as in Image 1: same face, same hair, same clothes.";
15454
+ var SAME_CHANGE = "The two states it compares stay as clearly different as in Image 1.";
15455
+ var showsPeople2 = (spec) => spec.references.some((r) => r.id.startsWith("PERSON_")) || spec.scene.look !== "illustration" && PEOPLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
15331
15456
  var GROUND_ONLY = "Image 1 shows no people and no product, so the room this frame adds carries more of Image 1's own background and the brand's colours, never a new scene.";
15332
15457
  function wordsOnly(spec) {
15333
15458
  return spec.references.length === 0 && spec.scene.look !== "photo" && !PEOPLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
15334
15459
  }
15335
- function scaleText(plan, button, flowing) {
15336
- const share = ratioValue2(plan.format) < 0.7 ? "half" : "two fifths of";
15337
- const buttonClause = button ? flowing ? `, the button is a solid shape at least ${share} the frame wide with large bold words` : `; the button is a solid shape at least ${share} the frame wide, its words large and bold` : "";
15338
- return flowing ? `Every line is set at the size this frame needs rather than at its size in Image 1: the largest line spans most of the width between the margins${buttonClause}, and no small text floats in a large empty area.` : `Scale: set every line for this frame, not at its size in Image 1: the largest line spans most of the width between the margins${buttonClause}; no small text in a large empty area.`;
15460
+ var isTall = (plan) => ratioValue2(plan.format) < 0.7;
15461
+ var LIST_ROLES = /^(list_item|bullet|row|item)$/i;
15462
+ var TABLE = /\b(tables?|columns?|rows?|checklists?|lists?)\b/i;
15463
+ var hasRows = (spec) => spec.copy.some((l) => LIST_ROLES.test(l.role ?? "")) || TABLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
15464
+ function scaleText(plan, spec, flowing, zoned, short) {
15465
+ if (short && !flowing) {
15466
+ return isTall(plan) && zoned ? "Scale: larger than in Image 1, the headline across most of the band's width, the words spread through the band." : "Scale: larger than in Image 1, the largest line across most of the width.";
15467
+ }
15468
+ if (!(isTall(plan) && zoned)) {
15469
+ return flowing ? "Every line is set at the size this frame needs rather than at its size in Image 1: the largest line spans most of the width between the margins, and the words fill their space." : "Scale: set every line for this frame, not at its size in Image 1: the largest line spans most of the width between the margins; no small text in a large empty area.";
15470
+ }
15471
+ const rows = hasRows(spec) ? flowing ? " Rows, columns and lists are restacked for the tall frame, one item per line, each larger." : " Rows, columns and lists are restacked for the tall frame, one item per line, each larger, rather than shrunk." : "";
15472
+ return flowing ? `The words are set for this frame, larger than in Image 1: the headline large enough to fill most of the band's width, and every line, the logo and the button spread through the band's height, inside its edges.${rows}` : `Scale: larger than in Image 1: the headline set large enough to fill most of the band's width; every line, the logo and the button spread through the band's height, inside its edges, never a small block in its middle.${rows}`;
15339
15473
  }
15340
15474
  var ZONE_HEADROOM = 0.02;
15341
15475
  var pct = (share) => `${Math.round(share * 100)}%`;
15342
- function zonesText(plan, flowing, groundOnly) {
15476
+ function zonesText(plan, flowing, ground, short) {
15343
15477
  const top = plan.safe.top + ZONE_HEADROOM;
15344
15478
  const bottom = plan.safe.bottom + ZONE_HEADROOM;
15345
- const left = Math.max(plan.safe.left, plan.safe.right) + ZONE_HEADROOM;
15479
+ const side = Math.max(plan.safe.left, plan.safe.right) + ZONE_HEADROOM;
15480
+ const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
15346
15481
  const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
15347
- const why = covered ? ", because the app's interface covers it" : "";
15348
- const below = groundOnly ? `the bottom ${pct(bottom)} continues Image 1's own background in the brand's colours, with no words, no button and no person or object` : `the bottom ${pct(bottom)} shows only the scene (Image 1's own people, product and the surface they are on), with no words and no button`;
15349
- const zones = `the top ${pct(top)} of the frame shows only the scene (background, sky, wall), with no logo and no words; from ${pct(top)} down to ${pct(1 - bottom)} of the height sit the logo, every line and the button, large, at least ${pct(left)} in from each side; ${below}${why}`;
15350
- return flowing ? `The frame is built in zones: ${zones}.` : `Zones, top to bottom: ${zones}.`;
15482
+ if (short && !flowing) {
15483
+ const around = isTall(plan) && covered ? "only the ad's own picture, larger, never a flat colour, a panel or a square" : "only the ad's own picture";
15484
+ return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
15485
+ }
15486
+ if (!(isTall(plan) && covered)) {
15487
+ return flowing ? `The logo, every line and the button sit ${band}; around them the frame shows only the ad's own picture.` : `Zones: the logo, every line and the button sit ${band}; around them, only the ad's own picture.`;
15488
+ }
15489
+ const picture = ground ? "Image 1's background with its shapes and graphics enlarged in the brand's colours" : "Image 1's people, product and setting extended";
15490
+ return flowing ? `The words' band runs ${band}, holding the logo, every line and the button. Above and below it the app's interface covers the frame, so there it shows only the ad's own picture, larger: ${picture}, edge to edge.` : `Zones: the logo, every line and the button sit in one band, ${band}; the top ${pct(top)} and the bottom ${pct(bottom)} carry neither words, the logo nor the button. Above and below it, where the app's interface covers the frame, only the ad's own picture, larger: ${picture}, never a flat empty colour, a panel or a square on a background.`;
15351
15491
  }
15352
- var isButton2 = (line) => /cta|button/i.test(`${line.role ?? ""} ${line.look ?? ""}`);
15353
15492
  function adaptBrief(spec, profile, opts) {
15354
15493
  const { plan, images } = opts;
15355
15494
  const flowing = profile.brief.structure === "flowing_sentences";
15356
15495
  const { logo_position: _logoPosition, ...rest } = spec;
15357
15496
  const unplaced = { ...rest, copy: spec.copy.map(({ position: _position, ...line }) => line) };
15358
15497
  const band = trimmedRenderBand(plan);
15359
- const compiled = compileStaticBrief(unplaced, void 0, plan.format, void 0, profile, {
15360
- job: "relay",
15361
- images: {
15362
- base: images.base,
15363
- ...images.logo ? { logo: images.logo } : {},
15364
- ...images.type ? { type: images.type } : {}
15365
- },
15366
- ...plan.placements ? { placements: plan.placements } : {},
15367
- ...band ? { band } : {}
15368
- });
15369
- const paragraphs = compiled.prompt.split("\n\n");
15498
+ const zoned = opts.zones !== false;
15370
15499
  const ground = wordsOnly(spec);
15371
- paragraphs[0] = adaptDeliverable(plan, spec.brand?.name, flowing, ground);
15372
- const first = spec.copy[0]?.text;
15373
- const textAt = first ? paragraphs.findIndex((p) => p.includes(first)) : -1;
15374
- if (textAt >= 0) {
15375
- const added = [
15376
- scaleText(plan, spec.copy.some(isButton2), flowing),
15377
- ...opts.zones === false ? [] : [zonesText(plan, flowing, ground)]
15378
- ];
15379
- paragraphs.splice(textAt + 1, 0, ...added);
15500
+ const compile = (wordBudget) => compileStaticBrief(
15501
+ unplaced,
15502
+ void 0,
15503
+ plan.format,
15504
+ void 0,
15505
+ { ...profile, brief: { ...profile.brief, wordBudget } },
15506
+ {
15507
+ job: "relay",
15508
+ images: {
15509
+ base: images.base,
15510
+ ...images.logo ? { logo: images.logo } : {},
15511
+ ...images.type ? { type: images.type } : {},
15512
+ refs: images.refs ?? {}
15513
+ },
15514
+ ...plan.placements ? { placements: plan.placements } : {},
15515
+ ...band ? { band } : {},
15516
+ ...zoned ? { zoned } : {}
15517
+ }
15518
+ );
15519
+ const relaid = (compiled2, short = false) => {
15520
+ const paragraphs = compiled2.prompt.split("\n\n");
15521
+ paragraphs[0] = adaptDeliverable(plan, spec, flowing, ground && !zoned, short);
15522
+ const first = spec.copy[0] ? quotedCopy(spec.copy[0].text) : void 0;
15523
+ const textAt = first ? paragraphs.findIndex((p) => p.includes(first)) : -1;
15524
+ if (textAt >= 0) {
15525
+ const added = [
15526
+ ...zoned ? [zonesText(plan, flowing, ground, short)] : [],
15527
+ scaleText(plan, spec, flowing, zoned, short)
15528
+ ];
15529
+ paragraphs.splice(textAt + 1, 0, ...added);
15530
+ } else if (zoned) {
15531
+ const layoutAt = paragraphs.findIndex((p) => p.startsWith("Layout:"));
15532
+ paragraphs.splice(layoutAt > 0 ? layoutAt : 1, 0, zonesText(plan, flowing, ground, short));
15533
+ }
15534
+ return paragraphs.join("\n\n");
15535
+ };
15536
+ const budget = profile.brief.wordBudget;
15537
+ let compiled = compile(budget);
15538
+ let prompt = relaid(compiled);
15539
+ for (let pass = 0; pass < 3 && briefWords(prompt) > budget; pass++) {
15540
+ compiled = compile(compiled.words - (briefWords(prompt) - budget));
15541
+ prompt = relaid(compiled);
15380
15542
  }
15381
- const prompt = paragraphs.join("\n\n");
15382
- return { ...compiled, prompt, words: prompt.split(/\s+/).filter(Boolean).length };
15543
+ if (briefWords(prompt) > budget) prompt = relaid(compiled, true);
15544
+ return { ...compiled, prompt, words: wordsIn(prompt) };
15383
15545
  }
15546
+ var wordsIn = (text) => text.split(/\s+/).filter(Boolean).length;
15384
15547
 
15385
15548
  // src/engine/scaffold/static-topology.ts
15386
15549
  var STATIC_VARIANTS_ROLE = "static_variants";
@@ -15390,6 +15553,7 @@ var STATIC_PICK_ROLE = "pick";
15390
15553
  var STATIC_MASTER_ROLE = "static_master";
15391
15554
  var STATIC_SIZE_ROLE = "static_size";
15392
15555
  var STATIC_ADAPT_ROLE = "static_adapt";
15556
+ var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
15393
15557
  var LOGO_COMPOSITE_ROLE = "logo_composite";
15394
15558
  var STATIC_FIX_ROLE = "static_fix";
15395
15559
  var LOGO_COMPOSITE_DEFAULT = false;
@@ -15423,8 +15587,9 @@ function staticMasterFormat(canvas) {
15423
15587
  if (at2 < 0 || !match) return null;
15424
15588
  return formatOfSize({ width: Number(match[1]), height: Number(match[2]) });
15425
15589
  }
15590
+ var KNOWN_FORMATS = ["9:16", "4:5", "1:1", "16:9", "1.91:1", ...ASPECT_ADAPT_FORMATS];
15426
15591
  function formatOfSize(size) {
15427
- for (const format of ["9:16", "4:5", "1:1", "16:9", "1.91:1"]) {
15592
+ for (const format of KNOWN_FORMATS) {
15428
15593
  const d = deliveredSize(format);
15429
15594
  if (d.width === size.width && d.height === size.height) return format;
15430
15595
  }
@@ -15432,9 +15597,17 @@ function formatOfSize(size) {
15432
15597
  const g = gcd(size.width, size.height);
15433
15598
  return `${size.width / g}:${size.height / g}`;
15434
15599
  }
15435
- function safeAreaOf(format) {
15436
- const { top, bottom, left, right } = staticSafeArea(format);
15437
- return { top, bottom, left, right };
15600
+ function safeAreaOf(adaptFormat, delivered) {
15601
+ const { top, bottom, left, right } = staticSafeArea(adaptFormat);
15602
+ return delivered.map((f) => safeAreaOnFrame(adaptFormat, f)).reduce(
15603
+ (a, b) => ({
15604
+ top: Math.max(a.top, b.top),
15605
+ bottom: Math.max(a.bottom, b.bottom),
15606
+ left: Math.max(a.left, b.left),
15607
+ right: Math.max(a.right, b.right)
15608
+ }),
15609
+ { top, bottom, left, right }
15610
+ );
15438
15611
  }
15439
15612
  function magick(id, role, image, args, extra = {}) {
15440
15613
  return {
@@ -15503,7 +15676,15 @@ function staticRenderNodes(g) {
15503
15676
  model: g.adaptModel,
15504
15677
  formats: adaptFormats,
15505
15678
  image_size: ADAPT_IMAGE_SIZE,
15506
- safe_areas: Object.fromEntries(adaptFormats.map((f) => [f, safeAreaOf(f)]))
15679
+ safe_areas: Object.fromEntries(
15680
+ adaptFormats.map((f) => [
15681
+ f,
15682
+ safeAreaOf(
15683
+ f,
15684
+ adapted.filter((s) => (s.adaptFormat ?? s.format) === f).map((s) => s.format)
15685
+ )
15686
+ ])
15687
+ )
15507
15688
  },
15508
15689
  tags: { role: STATIC_ADAPT_ROLE }
15509
15690
  });
@@ -15513,7 +15694,9 @@ function staticRenderNodes(g) {
15513
15694
  const slug = formatSlug(s.format);
15514
15695
  const size = sizeNode(s, adaptFormats);
15515
15696
  if (size) nodes.push(size);
15516
- const ref = size ? `$ref:${size.id}.image` : "$ref:master.image";
15697
+ const fit = safeFitNode(s.format, size ? `$ref:${size.id}.image` : "$ref:master.image", plan.placements);
15698
+ if (fit) nodes.push(fit);
15699
+ const ref = fit ? `$ref:${fit.id}.image` : size ? `$ref:${size.id}.image` : "$ref:master.image";
15517
15700
  if (!g.compositeLogo) return ref;
15518
15701
  const slot = logoSlot(s.size, staticSafeArea(s.format)).rect;
15519
15702
  nodes.push(
@@ -15548,6 +15731,16 @@ function staticRenderNodes(g) {
15548
15731
  });
15549
15732
  return nodes;
15550
15733
  }
15734
+ function safeFitNode(format, image, placements) {
15735
+ if (staticSafeArea(format, placements).placement === "feed") return null;
15736
+ return {
15737
+ id: `safe_${formatSlug(format)}`,
15738
+ type: "static_safe_fit",
15739
+ inputs: { image },
15740
+ params: { format, ...placements ? { placements: [...placements] } : {} },
15741
+ tags: { role: STATIC_SAFE_FIT_ROLE }
15742
+ };
15743
+ }
15551
15744
  function sizeNode(s, adaptFormats) {
15552
15745
  const id = `size_${formatSlug(s.format)}`;
15553
15746
  if (s.from === "crop" && s.crop) {
@@ -15898,9 +16091,13 @@ function checkReferenceCompleteness(ctx, meta) {
15898
16091
  const elements = (meta.elements ?? []).filter((el) => CHECKED_ELEMENT_TYPES.has(el.type.toLowerCase()));
15899
16092
  const keyworded = elements.map((el) => ({ el, keywords: keywordsForElement(el) })).filter((e) => e.keywords.length > 0);
15900
16093
  if (keyworded.length === 0) return;
16094
+ const sheetSources = JSON.stringify(
16095
+ ctx.canvas.nodes.filter((n) => n.type === "image_reference_sheet").map((n) => n.inputs ?? {})
16096
+ );
15901
16097
  for (let i = 0; i < ctx.canvas.nodes.length; i++) {
15902
16098
  const n = ctx.canvas.nodes[i];
15903
- if (n?.type === "image_generate") checkFrameReferences(ctx, n, i, keyworded);
16099
+ if (n?.type !== "image_generate" || sheetSources.includes(`"$ref:${n.id}.`)) continue;
16100
+ checkFrameReferences(ctx, n, i, keyworded);
15904
16101
  }
15905
16102
  }
15906
16103
  function checkUnusableLastFrame(ctx) {
@@ -17799,9 +17996,12 @@ async function fetchWithEscalation(url, init, options) {
17799
17996
  let originResponse = null;
17800
17997
  let originTier = "direct";
17801
17998
  let lastError;
17802
- for (const route of plannedRoutes(url, options.credentials, options.routeOptions)) {
17999
+ const routes = plannedRoutes(url, options.credentials, options.routeOptions);
18000
+ const tried = [];
18001
+ for (let route = routes[0] ?? null; route !== null; route = nextPlannedRoute(routes, tried)) {
17803
18002
  const rung = await attemptRoute(url, init, route, options);
17804
18003
  attempts.push(rung.attempt);
18004
+ tried.push({ tier: rung.attempt.tier, proxyFailure: rung.attempt.outcome === "proxy_unavailable" });
17805
18005
  if (rung.response !== null) {
17806
18006
  originResponse = rung.response;
17807
18007
  originTier = rung.attempt.tier;
@@ -18585,22 +18785,22 @@ function inferMimeFromPath(absPath, sniffBytes) {
18585
18785
  return "application/octet-stream";
18586
18786
  }
18587
18787
  async function rasterizeSvgToPng(bytes) {
18588
- const { default: sharp5 } = await import("sharp");
18589
- const base = await sharp5(bytes).metadata();
18788
+ const { default: sharp6 } = await import("sharp");
18789
+ const base = await sharp6(bytes).metadata();
18590
18790
  const longest = Math.max(base.width ?? 0, base.height ?? 0);
18591
18791
  let density = SVG_BASE_DENSITY;
18592
18792
  if (longest > 0) {
18593
18793
  const scaled = Math.round(SVG_BASE_DENSITY * SVG_TARGET_LONGEST_PX / longest);
18594
18794
  density = Math.min(SVG_MAX_DENSITY, Math.max(SVG_BASE_DENSITY, scaled));
18595
18795
  }
18596
- return await sharp5(bytes, { density }).png({ force: true, palette: false }).toBuffer();
18796
+ return await sharp6(bytes, { density }).png({ force: true, palette: false }).toBuffer();
18597
18797
  }
18598
18798
  var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
18599
18799
  var MAX_MODEL_REFERENCE_BYTES = 30 * 1024 * 1024;
18600
18800
  async function fitWithinReferenceLimit(bytes, mime, maxBytes) {
18601
18801
  if (bytes.byteLength <= maxBytes) return { bytes, mime };
18602
- const { default: sharp5 } = await import("sharp");
18603
- const read = () => sharp5(bytes, { animated: true });
18802
+ const { default: sharp6 } = await import("sharp");
18803
+ const read = () => sharp6(bytes, { animated: true });
18604
18804
  const meta = await read().metadata();
18605
18805
  const animated = (meta.pages ?? 1) > 1;
18606
18806
  const keepAlpha = meta.hasAlpha === true;
@@ -18638,11 +18838,11 @@ async function toModelSafeImage(bytes, opts) {
18638
18838
  if (sniffSvg(bytes)) {
18639
18839
  return await fit(await rasterizeSvgToPng(bytes), "image/png", "svg");
18640
18840
  }
18641
- const { default: sharp5 } = await import("sharp");
18841
+ const { default: sharp6 } = await import("sharp");
18642
18842
  let png;
18643
18843
  let format;
18644
18844
  try {
18645
- const img = sharp5(bytes);
18845
+ const img = sharp6(bytes);
18646
18846
  format = (await img.metadata()).format ?? "unknown";
18647
18847
  png = await img.png({ force: true }).toBuffer();
18648
18848
  } catch (e) {
@@ -18660,8 +18860,8 @@ function normalizationNote(image) {
18660
18860
  }
18661
18861
  return parts.length > 0 ? parts.join("; ") : null;
18662
18862
  }
18663
- function hasAscii(buf, offset, sig) {
18664
- return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
18863
+ function hasAscii(buf, offset2, sig) {
18864
+ return buf.length >= offset2 + sig.length && buf.toString("ascii", offset2, offset2 + sig.length) === sig;
18665
18865
  }
18666
18866
  var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
18667
18867
  function sniffIsoBmff(buf) {
@@ -18682,21 +18882,21 @@ function sniffImageMime(buf) {
18682
18882
  return sniffIsoBmff(buf);
18683
18883
  }
18684
18884
  function findBoxPayload(buf, start, end, type) {
18685
- let offset = start;
18686
- while (offset + 8 <= end) {
18687
- let size = buf.readUInt32BE(offset);
18688
- const boxType = buf.toString("ascii", offset + 4, offset + 8);
18885
+ let offset2 = start;
18886
+ while (offset2 + 8 <= end) {
18887
+ let size = buf.readUInt32BE(offset2);
18888
+ const boxType = buf.toString("ascii", offset2 + 4, offset2 + 8);
18689
18889
  let headerLen = 8;
18690
18890
  if (size === 1) {
18691
- if (offset + 16 > end) return null;
18692
- size = Number(buf.readBigUInt64BE(offset + 8));
18891
+ if (offset2 + 16 > end) return null;
18892
+ size = Number(buf.readBigUInt64BE(offset2 + 8));
18693
18893
  headerLen = 16;
18694
18894
  } else if (size === 0) {
18695
- size = end - offset;
18895
+ size = end - offset2;
18696
18896
  }
18697
- if (size < headerLen || offset + size > end) return null;
18698
- if (boxType === type) return { start: offset + headerLen, end: offset + size };
18699
- offset += size;
18897
+ if (size < headerLen || offset2 + size > end) return null;
18898
+ if (boxType === type) return { start: offset2 + headerLen, end: offset2 + size };
18899
+ offset2 += size;
18700
18900
  }
18701
18901
  return null;
18702
18902
  }
@@ -20972,6 +21172,7 @@ import sharp4 from "sharp";
20972
21172
 
20973
21173
  // src/engine/nodes/local/lib/band-check.ts
20974
21174
  var BOX_TOLERANCE = 0.01;
21175
+ var FRAME_CUT = 5e-3;
20975
21176
  function bandViolations(boxes, format, opts = {}) {
20976
21177
  const band = opts.band ?? staticSafeArea(format, opts.placements);
20977
21178
  const tolerance = opts.tolerance ?? BOX_TOLERANCE;
@@ -20984,7 +21185,16 @@ function bandViolations(boxes, format, opts = {}) {
20984
21185
  };
20985
21186
  const edges = Object.keys(reach).filter((edge) => reach[edge] > tolerance);
20986
21187
  if (edges.length === 0) return [];
20987
- return [{ text: box.text, kind: box.kind, edges, overshoot: Math.max(...edges.map((edge) => reach[edge])) }];
21188
+ const toFrame = { top: box.y0, bottom: 1 - box.y1, left: box.x0, right: 1 - box.x1 };
21189
+ return [
21190
+ {
21191
+ text: box.text,
21192
+ kind: box.kind,
21193
+ edges,
21194
+ overshoot: Math.max(...edges.map((edge) => reach[edge])),
21195
+ clearance: Math.max(0, Math.min(...edges.map((edge) => toFrame[edge])))
21196
+ }
21197
+ ];
20988
21198
  });
20989
21199
  }
20990
21200
 
@@ -21030,11 +21240,25 @@ function assign(targets, words, claimed, threshold) {
21030
21240
  }
21031
21241
  return matches;
21032
21242
  }
21033
- function joinHyphenBreaks(texts) {
21243
+ function hyphenatedCompounds(texts) {
21244
+ const out = /* @__PURE__ */ new Set();
21245
+ for (const text of texts) {
21246
+ for (const [compound] of text.normalize("NFKC").toLowerCase().matchAll(new RegExp("\\p{L}+(?:[-\u2010\u2011]\\p{L}+)+", "gu"))) {
21247
+ const words = compound.split(/[-‐‑]/u);
21248
+ for (let i = 1; i < words.length; i++) out.add(`${words[i - 1]}-${words[i]}`);
21249
+ }
21250
+ }
21251
+ return out;
21252
+ }
21253
+ function joinHyphenBreaks(texts, compounds) {
21034
21254
  const out = [];
21035
21255
  let carry = "";
21036
21256
  for (const text of texts) {
21037
- const line = carry + text.trim();
21257
+ const next = text.trim();
21258
+ const before = carry.match(new RegExp("\\p{L}+$", "u"))?.[0] ?? "";
21259
+ const after = next.match(new RegExp("^\\p{L}+", "u"))?.[0] ?? "";
21260
+ const own = carry !== "" && compounds.has(`${before}-${after}`.normalize("NFKC").toLowerCase());
21261
+ const line = carry + (own ? "-" : "") + next;
21038
21262
  const broken = line.match(new RegExp("^(.*\\p{L})[-\u2010\u2011]$", "u"));
21039
21263
  if (broken) carry = broken[1] ?? "";
21040
21264
  else {
@@ -21045,23 +21269,36 @@ function joinHyphenBreaks(texts) {
21045
21269
  if (carry) out.push(`${carry}-`);
21046
21270
  return out;
21047
21271
  }
21272
+ function lineParts(text) {
21273
+ const parts = text.split(/\r?\n/).filter((part) => part.trim().length > 0);
21274
+ return parts.length > 0 ? parts : [text];
21275
+ }
21048
21276
  function textMismatch(expected, boxes, opts = {}) {
21049
- const words = joinHyphenBreaks(boxes.filter((b) => b.kind === "text").map((b) => b.text)).flatMap(
21277
+ const compounds = hyphenatedCompounds(expected.map((line) => line.text));
21278
+ const texts = boxes.filter((b) => b.kind === "text").map((b) => b.text);
21279
+ const words = joinHyphenBreaks(texts, compounds).flatMap(
21050
21280
  (text, box) => normalizeCopy(text).split(" ").filter((word) => word.length > 0).map((word) => ({ word, box }))
21051
21281
  );
21052
21282
  const claimed = /* @__PURE__ */ new Set();
21283
+ const parts = expected.flatMap(
21284
+ (line, lineIndex) => lineParts(line.text).map((text) => ({ lineIndex, text: normalizeCopy(text) }))
21285
+ );
21053
21286
  const matches = assign(
21054
- expected.map((l) => normalizeCopy(l.text)),
21287
+ parts.map((part) => part.text),
21055
21288
  words,
21056
21289
  claimed,
21057
21290
  MIN_MATCH
21058
21291
  );
21059
21292
  assign((opts.allowed ?? []).map(normalizeCopy), words, claimed, ALLOWED_MATCH);
21060
21293
  const lines = expected.flatMap((line, index) => {
21061
- const match = matches.get(index);
21062
- const score = match?.similarity ?? 0;
21294
+ const lineMatches = parts.flatMap(
21295
+ (part, partIndex) => part.lineIndex === index && part.text.length > 0 ? [matches.get(partIndex)] : []
21296
+ );
21297
+ if (lineMatches.length === 0) return [];
21298
+ const score = Math.min(...lineMatches.map((match) => match?.similarity ?? 0));
21063
21299
  if (score >= 1 - MAX_LINE_DISTANCE) return [];
21064
- return [{ ...line, read: match?.span.text ?? null, similarity: score }];
21300
+ const read = lineMatches.flatMap((match) => match ? [match.span.text] : []).join(" ");
21301
+ return [{ ...line, read: read.length > 0 ? read : null, similarity: score }];
21065
21302
  });
21066
21303
  const extra = [];
21067
21304
  words.forEach(({ word, box }, index) => {
@@ -21873,14 +22110,24 @@ function outsideBand(v, placement) {
21873
22110
  if (placement === "reels") return `${what} sits where Reels covers it (${edges}).`;
21874
22111
  if (placement === "stories") return `${what} sits where Stories covers it (${edges}).`;
21875
22112
  if (placement === "master") return `${what} sits outside the space every size keeps clear (${edges}).`;
21876
- return `${what} sits too close to the edge; the feed can cut it off (${edges}).`;
22113
+ if (v.clearance <= FRAME_CUT) return `${what} runs off the edge of the picture (${edges}).`;
22114
+ return `${what} sits close to the edge (${edges}).`;
22115
+ }
22116
+ function bandSeverity(v, placement) {
22117
+ return placement !== "feed" || v.clearance <= FRAME_CUT ? "blocking" : "warning";
21877
22118
  }
21878
22119
  var EDGE_ORDER = ["top", "bottom", "left", "right"];
21879
22120
  function oneLogo(violations) {
21880
22121
  const logo = violations.filter((v) => v.kind === "logo");
21881
22122
  if (logo.length <= 1) return [...violations];
21882
22123
  const edges = EDGE_ORDER.filter((e) => logo.some((v) => v.edges.includes(e)));
21883
- const merged = { text: "", kind: "logo", edges, overshoot: Math.max(...logo.map((v) => v.overshoot)) };
22124
+ const merged = {
22125
+ text: "",
22126
+ kind: "logo",
22127
+ edges,
22128
+ overshoot: Math.max(...logo.map((v) => v.overshoot)),
22129
+ clearance: Math.min(...logo.map((v) => v.clearance))
22130
+ };
21884
22131
  const first = violations.indexOf(logo[0]);
21885
22132
  const rest = violations.filter((v) => v.kind !== "logo");
21886
22133
  return [...rest.slice(0, first), merged, ...rest.slice(first)];
@@ -21901,7 +22148,9 @@ function measuredFindings(input) {
21901
22148
  (l) => blocking(l.read === null ? `Text: "${l.text}" is missing.` : `Text: "${l.text}" reads "${l.read}".`)
21902
22149
  ),
21903
22150
  ...text.extra.length > 0 ? [blocking(`Text: extra words nobody asked for: ${text.extra.map((e) => `"${e}"`).join(", ")}.`)] : [],
21904
- ...oneLogo(band).map((v) => blocking(outsideBand(v, placement))),
22151
+ ...oneLogo(band).map(
22152
+ (v) => ({ severity: bandSeverity(v, placement), what: `${format}: ${outsideBand(v, placement)}` })
22153
+ ),
21905
22154
  ...(input.offBrand ?? []).map(
21906
22155
  (ink) => ({
21907
22156
  severity: "warning",
@@ -21916,8 +22165,10 @@ function staticVerdict(findings) {
21916
22165
  return findings.length > 0 ? "ready_with_notes" : "ready";
21917
22166
  }
21918
22167
  function textInstructions(copy, measured) {
22168
+ const wordless = "This ad has no words on purpose: the picture is the ad. Do not report missing copy, a missing headline or a missing call to action.";
21919
22169
  if (measured) {
21920
22170
  return [
22171
+ ...copy.length === 0 ? [wordless] : [],
21921
22172
  "The words were already read back and measured by another check: do not transcribe them or report their spelling, extra words or position.",
21922
22173
  "1. TEXT: garbled or malformed letters, or letters that do not read as words (a brand name inside a drawn logo is fine)."
21923
22174
  ];
@@ -21929,10 +22180,7 @@ function textInstructions(copy, measured) {
21929
22180
  "1. TEXT: extra words that are not in the list, a string shown twice, or garbled letters (a brand name inside a drawn logo is fine)."
21930
22181
  ];
21931
22182
  }
21932
- return [
21933
- "This ad has no words on purpose: the picture is the ad. Do not report missing copy, a missing headline or a missing call to action.",
21934
- "1. TEXT: any letters or words that appear anyway (invented or garbled text is blocking)."
21935
- ];
22183
+ return [wordless, "1. TEXT: any letters or words that appear anyway (invented or garbled text is blocking)."];
21936
22184
  }
21937
22185
  function buildStaticReviewPrompt(opts) {
21938
22186
  const brand = opts.brand ? ` for ${opts.brand}` : "";
@@ -22091,13 +22339,13 @@ function rankOf(boxes, blocking, want) {
22091
22339
  const logoFound = boxes.some((b) => b.kind === "logo" || brand !== null && normalizeCopy(b.text) === brand);
22092
22340
  const noLogo = want.logo && !logoFound ? 1 : 0;
22093
22341
  const noButton = off.some((l) => want.buttons.has(l.text)) ? 1 : 0;
22094
- const outOfBand = bandViolations(boxes, want.format, { band: want.band }).length;
22342
+ const outOfBand = bandViolations(boxes, want.format, want.grade).length;
22095
22343
  return {
22096
22344
  rank: [blocking.length, off.length, noLogo, noButton, outOfBand],
22097
22345
  shortfalls: noLogo ? [NO_LOGO] : []
22098
22346
  };
22099
22347
  }
22100
- var isButton3 = (line) => /cta|button/i.test(`${line.role ?? ""} ${line.look ?? ""}`);
22348
+ var isButton2 = (line) => /cta|button/i.test(`${line.role ?? ""} ${line.look ?? ""}`);
22101
22349
  async function prefilterVariants(variants, spec, formats, opts = {}) {
22102
22350
  const everything = {
22103
22351
  kept: seededOrder(
@@ -22114,7 +22362,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
22114
22362
  try {
22115
22363
  const parsed = readAdSpec(spec);
22116
22364
  lines = parsed.copy.map((l) => ({ text: l.text, lettering: l.lettering }));
22117
- buttons = new Set(parsed.copy.filter(isButton3).map((l) => l.text));
22365
+ buttons = new Set(parsed.copy.filter(isButton2).map((l) => l.text));
22118
22366
  brand = parsed.brand?.name;
22119
22367
  logo = parsed.logo === true;
22120
22368
  } catch {
@@ -22126,6 +22374,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
22126
22374
  ...opts.placements ? { placements: opts.placements } : {}
22127
22375
  });
22128
22376
  const format = plan.master.format;
22377
+ const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
22129
22378
  const checked = await Promise.all(
22130
22379
  variants.map(async (variant) => {
22131
22380
  const source = variant.path ?? variant.url;
@@ -22137,7 +22386,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
22137
22386
  format,
22138
22387
  lines,
22139
22388
  allowed: brand ? [brand] : [],
22140
- band: plan.band,
22389
+ ...grade,
22141
22390
  toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
22142
22391
  ...opts.read ? { read: opts.read } : {},
22143
22392
  ...opts.onSpend ? { onSpend: opts.onSpend } : {}
@@ -22145,7 +22394,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
22145
22394
  if (check.status === "not_checked") return { index: variant.index, blocking: null };
22146
22395
  const prefix = `${format}: `;
22147
22396
  const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
22148
- const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, band: plan.band };
22397
+ const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
22149
22398
  return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
22150
22399
  } catch {
22151
22400
  return { index: variant.index, blocking: null };
@@ -22250,61 +22499,191 @@ var staticPrefilterNode = defineNode({
22250
22499
  }
22251
22500
  });
22252
22501
 
22253
- // src/engine/nodes/local/text.ts
22502
+ // src/engine/nodes/local/staticSafeFit.ts
22254
22503
  import { z as z58 } from "zod";
22504
+
22505
+ // src/engine/nodes/local/lib/safe-fit.ts
22506
+ import sharp5 from "sharp";
22507
+ var FIT_HEADROOM = 0.01;
22508
+ var MIN_FIT_SCALE = 0.85;
22509
+ function planSafeFit(boxes, format, opts = {}) {
22510
+ if (bandViolations(boxes, format, opts).length === 0) return null;
22511
+ const area = staticSafeArea(format, opts.placements);
22512
+ const union = {
22513
+ x0: Math.min(...boxes.map((b) => b.x0)),
22514
+ y0: Math.min(...boxes.map((b) => b.y0)),
22515
+ x1: Math.max(...boxes.map((b) => b.x1)),
22516
+ y1: Math.max(...boxes.map((b) => b.y1))
22517
+ };
22518
+ const band = {
22519
+ x0: area.left + FIT_HEADROOM,
22520
+ y0: area.top + FIT_HEADROOM,
22521
+ x1: 1 - area.right - FIT_HEADROOM,
22522
+ y1: 1 - area.bottom - FIT_HEADROOM
22523
+ };
22524
+ const scale = Math.min(1, (band.y1 - band.y0) / (union.y1 - union.y0), (band.x1 - band.x0) / (union.x1 - union.x0));
22525
+ if (scale < MIN_FIT_SCALE) return null;
22526
+ return {
22527
+ scale,
22528
+ x: offset(scale, union.x0, union.x1, band.x0, band.x1),
22529
+ y: offset(scale, union.y0, union.y1, band.y0, band.y1)
22530
+ };
22531
+ }
22532
+ function offset(scale, from, to, bandFrom, bandTo) {
22533
+ const lo = bandFrom - scale * from;
22534
+ const hi = bandTo - scale * to;
22535
+ if (lo > hi) return (lo + hi) / 2;
22536
+ return Math.min(hi, Math.max(lo, (1 - scale) / 2));
22537
+ }
22538
+ var MARGIN_BLUR = 0.012;
22539
+ var SEAM_FEATHER = 0.015;
22540
+ async function fitPicture(image, fit) {
22541
+ const { width = 0, height = 0 } = await sharp5(image).metadata();
22542
+ const sw = Math.max(1, Math.round(width * fit.scale));
22543
+ const sh = Math.max(1, Math.round(height * fit.scale));
22544
+ const px = Math.round(fit.x * width);
22545
+ const py = Math.round(fit.y * height);
22546
+ const left = Math.max(0, -px);
22547
+ const top = Math.max(0, -py);
22548
+ const w = Math.min(sw, width - px) - left;
22549
+ const h = Math.min(sh, height - py) - top;
22550
+ const shrunk = await sharp5(image).removeAlpha().resize(sw, sh, { fit: "fill", kernel: "lanczos3" }).extract({ left, top, width: w, height: h }).toBuffer();
22551
+ const pad = { left: Math.max(0, px), top: Math.max(0, py) };
22552
+ const extend = { ...pad, right: width - pad.left - w, bottom: height - pad.top - h };
22553
+ const extended = await sharp5(shrunk).extend({ ...extend, extendWith: "mirror" }).raw().toBuffer({ resolveWithObject: true });
22554
+ const frame = extended.data;
22555
+ const channels = extended.info.channels;
22556
+ const blurred = await sharp5(frame, { raw: { width, height, channels } }).blur(Math.max(0.3, MARGIN_BLUR * width)).raw().toBuffer();
22557
+ const feather = Math.max(1, SEAM_FEATHER * width);
22558
+ const keep = (at2, from, to, padFrom, padTo) => {
22559
+ const inFrom = padFrom ? (at2 - from) / feather + 0.5 : 1;
22560
+ const inTo = padTo ? (to - at2) / feather + 0.5 : 1;
22561
+ return Math.min(1, Math.max(0, Math.min(inFrom, inTo)));
22562
+ };
22563
+ const out = Buffer.alloc(frame.length);
22564
+ const x0 = pad.left;
22565
+ const x1 = pad.left + w;
22566
+ const y0 = pad.top;
22567
+ const y1 = pad.top + h;
22568
+ for (let y = 0; y < height; y++) {
22569
+ const ky = keep(y + 0.5, y0, y1, extend.top > 0, extend.bottom > 0);
22570
+ for (let x = 0; x < width; x++) {
22571
+ const k = Math.min(ky, keep(x + 0.5, x0, x1, extend.left > 0, extend.right > 0));
22572
+ const at2 = (y * width + x) * channels;
22573
+ for (let c = 0; c < channels; c++) {
22574
+ out[at2 + c] = Math.round(k * (frame[at2 + c] ?? 0) + (1 - k) * (blurred[at2 + c] ?? 0));
22575
+ }
22576
+ }
22577
+ }
22578
+ return sharp5(out, { raw: { width, height, channels } }).png().toBuffer();
22579
+ }
22580
+
22581
+ // src/engine/nodes/local/staticSafeFit.ts
22582
+ var Usage2 = z58.object({ model: z58.string(), inputTokens: z58.number(), outputTokens: z58.number(), costUsd: z58.number() }).strict();
22583
+ var Fit = z58.object({ scale: z58.number(), x: z58.number(), y: z58.number() }).strict();
22584
+ var staticSafeFitNode = defineNode({
22585
+ id: "static_safe_fit",
22586
+ version: "1.0.0",
22587
+ category: "image",
22588
+ location: "local",
22589
+ 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, filling the uncovered margin by mirroring the picture beside it. Outputs the same pixel size; `fit` is the scale and position used, or null with `reason` when the picture is passed on unchanged.",
22590
+ 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% is passed on unchanged: the review still names what Reels or Stories covers, and the layout itself has to change (shorter copy, fewer lines).",
22591
+ inputs: z58.object({ image: ImageRef }).strict(),
22592
+ // `placements` are the ones the canvas's sizes were planned with (the ad set's), so the band
22593
+ // the fit clears is the one the review holds the size to.
22594
+ params: z58.object({ format: z58.string().min(1), placements: z58.array(z58.string().min(1)).optional() }).strict(),
22595
+ outputs: z58.object({
22596
+ image: ImageRef,
22597
+ fit: Fit.nullable(),
22598
+ // Why the picture passed unchanged: the reader could not read it, nothing was outside the
22599
+ // band, or clearing it needed more than the smallest shrink that still looks native.
22600
+ reason: z58.enum(["not_read", "inside_band", "too_far"]).optional(),
22601
+ // What the reader's call cost, as the gateway reported it (the run record carries it home).
22602
+ usage: z58.array(Usage2).optional()
22603
+ }).strict(),
22604
+ outputKinds: { image: "image" },
22605
+ cost: () => ({ credits: 0, seconds_estimate: 5 }),
22606
+ async execute({ inputs, params, ctx }) {
22607
+ const source = inputs.image.path ?? inputs.image.url;
22608
+ if (!source) return { image: inputs.image, fit: null, reason: "not_read" };
22609
+ const bytes = await bytesOf(source);
22610
+ const usage = [];
22611
+ const reading = await readTextBoxes(bytes, { onSpend: (u) => usage.push(u) });
22612
+ const spend = usage.length > 0 ? { usage } : {};
22613
+ if (reading.status !== "read") return { image: inputs.image, fit: null, reason: "not_read", ...spend };
22614
+ const placements = params.placements ? { placements: params.placements } : {};
22615
+ const fit = planSafeFit(reading.boxes, params.format, placements);
22616
+ if (!fit) {
22617
+ const outside = bandViolations(reading.boxes, params.format, placements).length > 0;
22618
+ return {
22619
+ image: inputs.image,
22620
+ fit: null,
22621
+ reason: outside ? "too_far" : "inside_band",
22622
+ ...spend
22623
+ };
22624
+ }
22625
+ const fitted = await fitPicture(bytes, fit);
22626
+ ctx.log(`fit ${params.format} at scale ${fit.scale.toFixed(3)}, offset ${fit.x.toFixed(3)}, ${fit.y.toFixed(3)}`);
22627
+ const image = await ctx.assets.ingestBytes({ bytes: fitted, kind: "image", mime: "image/png" });
22628
+ return { image, fit, ...spend };
22629
+ }
22630
+ });
22631
+
22632
+ // src/engine/nodes/local/text.ts
22633
+ import { z as z59 } from "zod";
22255
22634
  var textNode = defineNode({
22256
22635
  id: "text",
22257
22636
  version: "1.0.0",
22258
22637
  category: "data",
22259
22638
  location: "local",
22260
22639
  summary: "A literal text value. Use for prompts, descriptions, copy.",
22261
- inputs: z58.object({}).strict(),
22262
- params: z58.object({ value: z58.string() }).strict(),
22263
- outputs: z58.object({ text: z58.string() }).strict(),
22640
+ inputs: z59.object({}).strict(),
22641
+ params: z59.object({ value: z59.string() }).strict(),
22642
+ outputs: z59.object({ text: z59.string() }).strict(),
22264
22643
  cost: () => ({ credits: 0, seconds_estimate: 0 }),
22265
22644
  execute: ({ params }) => Promise.resolve({ text: params.value })
22266
22645
  });
22267
22646
 
22268
22647
  // src/engine/nodes/local/videoConcat.ts
22269
- import { z as z59 } from "zod";
22648
+ import { z as z60 } from "zod";
22270
22649
  var FFMPEG_BIN4 = "ffmpeg";
22271
- var evenPixels = (axis) => z59.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
22272
- var Transition = z59.object({
22273
- type: z59.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"),
22274
- duration_s: z59.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
22650
+ var evenPixels = (axis) => z60.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
22651
+ var Transition = z60.object({
22652
+ type: z60.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"),
22653
+ duration_s: z60.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
22275
22654
  }).strict();
22276
- var ConcatClip = z59.object({
22277
- duration_s: z59.number().positive().describe(
22655
+ var ConcatClip = z60.object({
22656
+ duration_s: z60.number().positive().describe(
22278
22657
  "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."
22279
22658
  ),
22280
- length: z59.enum(["source", "trim", "still"]).default("source").describe(
22659
+ length: z60.enum(["source", "trim", "still"]).default("source").describe(
22281
22660
  "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)."
22282
22661
  ),
22283
- offset_s: z59.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
22284
- fit: z59.enum(["fill", "pad"]).default("fill").describe(
22662
+ offset_s: z60.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
22663
+ fit: z60.enum(["fill", "pad"]).default("fill").describe(
22285
22664
  "`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)."
22286
22665
  ),
22287
22666
  transition: Transition.optional().describe(
22288
22667
  "The transition OUT of this clip into the next one. Absent = a hard cut."
22289
22668
  ),
22290
- continues_frame: z59.boolean().optional().describe(
22669
+ continues_frame: z60.boolean().optional().describe(
22291
22670
  "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."
22292
22671
  )
22293
22672
  }).strict().refine((c) => c.offset_s === void 0 || c.length === "trim", {
22294
22673
  message: "offset_s only applies to a `trim` clip",
22295
22674
  path: ["offset_s"]
22296
22675
  });
22297
- var VideoConcatParams = z59.object({
22676
+ var VideoConcatParams = z60.object({
22298
22677
  width: evenPixels("width").describe("Output width in pixels (even). Every clip is normalized onto it."),
22299
22678
  height: evenPixels("height").describe("Output height in pixels (even)."),
22300
- fps: z59.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
22301
- seam_dedup: z59.enum(["head", "tail", "off"]).default("head").describe(
22679
+ fps: z60.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
22680
+ seam_dedup: z60.enum(["head", "tail", "off"]).default("head").describe(
22302
22681
  "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."
22303
22682
  ),
22304
- clips: z59.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
22683
+ clips: z60.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
22305
22684
  }).strict();
22306
- var VideoConcatInputs = z59.record(z59.string(), z59.custom());
22307
- var VideoConcatOutputs = z59.object({ video: z59.custom() }).strict();
22685
+ var VideoConcatInputs = z60.record(z60.string(), z60.custom());
22686
+ var VideoConcatOutputs = z60.object({ video: z60.custom() }).strict();
22308
22687
  function inputLengthS(clip2) {
22309
22688
  return clip2.duration_s + (clip2.transition?.duration_s ?? 0);
22310
22689
  }
@@ -22317,8 +22696,8 @@ function clipOps(clip2, params) {
22317
22696
  const normalize3 = `${scale},format=yuv420p,fps=${fps},setsar=1,settb=AVTB`;
22318
22697
  const len = inputLengthS(clip2).toFixed(3);
22319
22698
  if (clip2.length === "trim") {
22320
- const offset = clip2.offset_s ?? 0;
22321
- const start = offset > 0 ? `start=${offset.toFixed(3)}:` : "";
22699
+ const offset2 = clip2.offset_s ?? 0;
22700
+ const start = offset2 > 0 ? `start=${offset2.toFixed(3)}:` : "";
22322
22701
  return `${normalize3},trim=${start}duration=${len},setpts=PTS-STARTPTS`;
22323
22702
  }
22324
22703
  if (clip2.length === "still") return `${normalize3},trim=duration=${len}`;
@@ -22350,9 +22729,9 @@ function blendedJoins(params) {
22350
22729
  const out = k === clips.length - 2 ? "v" : `j${k + 1}`;
22351
22730
  const nextLen = inputLengthS(clips[k + 1]);
22352
22731
  if (transition) {
22353
- const offset = Math.max(0, accLen - transition.duration_s);
22732
+ const offset2 = Math.max(0, accLen - transition.duration_s);
22354
22733
  joins.push(
22355
- `[${cur}][${next}]xfade=transition=${transition.type}:duration=${transition.duration_s.toFixed(3)}:offset=${offset.toFixed(3)}[${out}]`
22734
+ `[${cur}][${next}]xfade=transition=${transition.type}:duration=${transition.duration_s.toFixed(3)}:offset=${offset2.toFixed(3)}[${out}]`
22356
22735
  );
22357
22736
  accLen = accLen - transition.duration_s + nextLen;
22358
22737
  } else {
@@ -22447,14 +22826,14 @@ var videoConcatNode = defineNode({
22447
22826
  });
22448
22827
 
22449
22828
  // src/engine/nodes/remote/audioVoiceConvert.ts
22450
- import { z as z60 } from "zod";
22451
- var AudioVoiceConvertParams = z60.object({
22452
- model: z60.literal("elevenlabs/eleven_multilingual_sts_v2"),
22829
+ import { z as z61 } from "zod";
22830
+ var AudioVoiceConvertParams = z61.object({
22831
+ model: z61.literal("elevenlabs/eleven_multilingual_sts_v2"),
22453
22832
  /** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
22454
- voice: z60.string().min(1),
22455
- output_format: z60.string().optional(),
22833
+ voice: z61.string().min(1),
22834
+ output_format: z61.string().optional(),
22456
22835
  /** Strip the source clip's background noise before re-voicing. */
22457
- remove_background_noise: z60.boolean().optional()
22836
+ remove_background_noise: z61.boolean().optional()
22458
22837
  }).strict();
22459
22838
  var audioVoiceConvertNode = delegated({
22460
22839
  id: "audio_voice_convert",
@@ -22462,12 +22841,12 @@ var audioVoiceConvertNode = delegated({
22462
22841
  category: "audio",
22463
22842
  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.",
22464
22843
  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}}"`.',
22465
- inputs: z60.object({
22844
+ inputs: z61.object({
22466
22845
  audio: AudioRef,
22467
22846
  voice_ref: TextRef.optional()
22468
22847
  }).strict(),
22469
22848
  params: AudioVoiceConvertParams,
22470
- outputs: z60.object({ audio: AudioRef }).strict(),
22849
+ outputs: z61.object({ audio: AudioRef }).strict(),
22471
22850
  outputKinds: { audio: "audio" },
22472
22851
  // Priced by the shared contract per minute of source audio. The quote cannot see
22473
22852
  // the clip's length, so it assumes one minute; the charge uses the real length.
@@ -22478,33 +22857,33 @@ var audioVoiceConvertNode = delegated({
22478
22857
  });
22479
22858
 
22480
22859
  // src/engine/nodes/remote/dialogue.ts
22481
- import { z as z61 } from "zod";
22482
- var DialogueInput = z61.object({
22483
- text: z61.string().min(1),
22484
- voice_id: z61.string().min(1)
22860
+ import { z as z62 } from "zod";
22861
+ var DialogueInput = z62.object({
22862
+ text: z62.string().min(1),
22863
+ voice_id: z62.string().min(1)
22485
22864
  });
22486
22865
  var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
22487
- var DialogueParams = z61.object({
22488
- model: z61.enum(DIALOGUE_MODELS),
22866
+ var DialogueParams = z62.object({
22867
+ model: z62.enum(DIALOGUE_MODELS),
22489
22868
  /**
22490
22869
  * Ordered list of lines, each tagged with the voice that should speak it.
22491
22870
  * Up to 10 unique voice_ids; total text across all lines should stay under
22492
22871
  * ~2000 characters for best quality (ElevenLabs guidance).
22493
22872
  */
22494
- inputs: z61.array(DialogueInput).min(1).max(50),
22495
- language_code: z61.string().optional(),
22873
+ inputs: z62.array(DialogueInput).min(1).max(50),
22874
+ language_code: z62.string().optional(),
22496
22875
  /** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
22497
- settings: z61.record(z61.string(), z61.unknown()).optional(),
22498
- seed: z61.number().int().min(0).max(4294967295).optional(),
22499
- apply_text_normalization: z61.enum(["auto", "on", "off"]).optional(),
22876
+ settings: z62.record(z62.string(), z62.unknown()).optional(),
22877
+ seed: z62.number().int().min(0).max(4294967295).optional(),
22878
+ apply_text_normalization: z62.enum(["auto", "on", "off"]).optional(),
22500
22879
  /**
22501
22880
  * When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
22502
22881
  * separate `timestamps` output — character-level alignment plus
22503
22882
  * per-voice segment markers usable for captions, lipsync, or
22504
22883
  * beat-matched cuts in ad creatives.
22505
22884
  */
22506
- with_timestamps: z61.boolean().optional(),
22507
- output_format: z61.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
22885
+ with_timestamps: z62.boolean().optional(),
22886
+ output_format: z62.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
22508
22887
  }).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
22509
22888
  message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
22510
22889
  path: ["inputs"]
@@ -22515,19 +22894,19 @@ var dialogueNode = delegated({
22515
22894
  category: "audio",
22516
22895
  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.",
22517
22896
  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.",
22518
- inputs: z61.object({}).loose(),
22897
+ inputs: z62.object({}).loose(),
22519
22898
  params: DialogueParams,
22520
- outputs: z61.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
22899
+ outputs: z62.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
22521
22900
  outputKinds: { audio: "audio", timestamps: "json" },
22522
22901
  // Priced by the shared contract, the same per-character rate the backend charges.
22523
22902
  cost: ({ params }) => ({ credits: estimateNodeCredits({ nodeType: "dialogue", params }) ?? 1, seconds_estimate: 8 })
22524
22903
  });
22525
22904
 
22526
22905
  // src/engine/nodes/remote/image.ts
22527
- import { z as z63 } from "zod";
22906
+ import { z as z64 } from "zod";
22528
22907
 
22529
22908
  // src/engine/nodes/remote/registry-params.ts
22530
- import { z as z62 } from "zod";
22909
+ import { z as z63 } from "zod";
22531
22910
  function registryParamShape(kind, overrides = {}) {
22532
22911
  const specs = Object.values(MODEL_REGISTRY[kind]);
22533
22912
  const byParam = /* @__PURE__ */ new Map();
@@ -22548,7 +22927,7 @@ function unionOf(schemas) {
22548
22927
  const kinds = [...new Set(schemas.map((schema) => schema.kind))];
22549
22928
  if (kinds.length > 1) {
22550
22929
  const [first, second, ...rest] = kinds.map((k) => unionOf(schemas.filter((s) => s.kind === k)));
22551
- return z62.union([first, second, ...rest]);
22930
+ return z63.union([first, second, ...rest]);
22552
22931
  }
22553
22932
  switch (kinds[0]) {
22554
22933
  case "string":
@@ -22556,41 +22935,37 @@ function unionOf(schemas) {
22556
22935
  case "number":
22557
22936
  return numberUnion(schemas);
22558
22937
  case "boolean":
22559
- return z62.boolean();
22938
+ return z63.boolean();
22560
22939
  default:
22561
- return z62.unknown();
22940
+ return z63.unknown();
22562
22941
  }
22563
22942
  }
22564
22943
  function stringUnion(schemas) {
22565
- if (schemas.some((schema) => !schema.enum)) return z62.string();
22944
+ if (schemas.some((schema) => !schema.enum)) return z63.string();
22566
22945
  const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))];
22567
- return z62.enum(values);
22946
+ return z63.enum(values);
22568
22947
  }
22569
22948
  function numberUnion(schemas) {
22570
- if (schemas.some((schema) => !schema.enum)) return z62.number();
22949
+ if (schemas.some((schema) => !schema.enum)) return z63.number();
22571
22950
  const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))].sort((a, b) => a - b);
22572
- return z62.literal(values);
22951
+ return z63.literal(values);
22573
22952
  }
22574
22953
 
22575
22954
  // src/engine/nodes/remote/image.ts
22576
- var imageModelEnum = z63.enum(IMAGE_GENERATE_MODELS);
22577
- var ImageGenerateParams = z63.object({
22955
+ var imageModelEnum = z64.enum(IMAGE_GENERATE_MODELS);
22956
+ var ImageGenerateParams = z64.object({
22578
22957
  model: imageModelEnum,
22579
22958
  ...registryParamShape("image_generate", {
22580
- prompt: z63.string().min(1),
22959
+ prompt: z64.string().min(1),
22581
22960
  // Recraft v4 vector controls, sent as the provider's own `controls` block. The
22582
22961
  // registry types them as JSON; this is their shape.
22583
- rgb_colors: z63.array(z63.array(z63.number().int().min(0).max(255))),
22584
- background_rgb_color: z63.array(z63.number().int().min(0).max(255))
22962
+ rgb_colors: z64.array(z64.array(z64.number().int().min(0).max(255))),
22963
+ background_rgb_color: z64.array(z64.number().int().min(0).max(255))
22585
22964
  })
22586
22965
  }).strict();
22587
- var PRO_IMAGE_MODELS = /* @__PURE__ */ new Set([
22588
- "google/gemini-3-pro-image",
22589
- "google/gemini-3-pro-image-preview"
22590
- ]);
22591
22966
  var PRO_QUALITY = "max";
22592
22967
  function creditsPerImage(params) {
22593
- return PRO_IMAGE_MODELS.has(String(params.model)) || params.quality === PRO_QUALITY ? 20 : 5;
22968
+ return params.quality === PRO_QUALITY ? IMAGE_TIER_CREDITS.pro : imagePictureCredits(String(params.model));
22594
22969
  }
22595
22970
  function imagesPerCall(params) {
22596
22971
  return "n" in params && typeof params.n === "number" ? params.n : 1;
@@ -22603,9 +22978,9 @@ var imageGenerateNode = delegated({
22603
22978
  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.",
22604
22979
  // `reference` is one image or an ordered array of images. The backend forwards
22605
22980
  // each as a separate `image_url` to the provider (OpenRouter accepts many).
22606
- inputs: z63.object({ reference: z63.union([ImageRef, z63.array(ImageRef).min(1)]).optional() }).loose(),
22981
+ inputs: z64.object({ reference: z64.union([ImageRef, z64.array(ImageRef).min(1)]).optional() }).loose(),
22607
22982
  params: ImageGenerateParams,
22608
- outputs: z63.object({ images: z63.array(ImageRef).min(1) }).strict(),
22983
+ outputs: z64.object({ images: z64.array(ImageRef).min(1) }).strict(),
22609
22984
  outputKinds: { images: "image" },
22610
22985
  // Per image: `n` asks one call for several, and the provider bills each (measured: n=3
22611
22986
  // cost exactly 3x n=1, prompt tokens included). Usage-billed, so this is only the pre-run
@@ -22617,31 +22992,27 @@ var imageGenerateNode = delegated({
22617
22992
  });
22618
22993
 
22619
22994
  // src/engine/nodes/remote/imageAspectAdapt.ts
22620
- import { z as z64 } from "zod";
22995
+ import { z as z65 } from "zod";
22621
22996
  var ASPECT_ADAPT_MODELS = [
22622
22997
  "google/gemini-3-pro-image",
22623
22998
  "google/gemini-3.1-flash-image",
22624
22999
  "google/gemini-3-pro-image-preview",
22625
23000
  "google/gemini-3.1-flash-image-preview"
22626
23001
  ];
22627
- var PRO_ADAPT_MODELS = /* @__PURE__ */ new Set([
22628
- "google/gemini-3-pro-image",
22629
- "google/gemini-3-pro-image-preview"
22630
- ]);
22631
- var ImageAspectAdaptParams = z64.object({
22632
- model: z64.enum(ASPECT_ADAPT_MODELS),
22633
- formats: z64.array(z64.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
22634
- platform: z64.enum(AD_FORMAT_PLATFORMS).optional(),
22635
- guidance: z64.string().min(1).optional(),
23002
+ var ImageAspectAdaptParams = z65.object({
23003
+ model: z65.enum(ASPECT_ADAPT_MODELS),
23004
+ formats: z65.array(z65.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
23005
+ platform: z65.enum(AD_FORMAT_PLATFORMS).optional(),
23006
+ guidance: z65.string().min(1).optional(),
22636
23007
  // The exact lines the ad carries: the reframe keeps each, spelled as given, once.
22637
- copy: z64.array(z64.string().min(1)).min(1).max(20).optional(),
23008
+ copy: z65.array(z65.string().min(1)).min(1).max(20).optional(),
22638
23009
  // Per target format, the fractions of each edge the platform's interface covers: words,
22639
23010
  // logo and button stay inside. The static scaffold sets both from prompt.json and `staticSafeArea`.
22640
- safe_areas: z64.record(
22641
- z64.string(),
22642
- z64.object({ top: z64.number(), bottom: z64.number(), left: z64.number(), right: z64.number() }).strict()
23011
+ safe_areas: z65.record(
23012
+ z65.string(),
23013
+ z65.object({ top: z65.number(), bottom: z65.number(), left: z65.number(), right: z65.number() }).strict()
22643
23014
  ).optional(),
22644
- image_size: z64.enum(OPENROUTER_IMAGE_SIZES).optional()
23015
+ image_size: z65.enum(OPENROUTER_IMAGE_SIZES).optional()
22645
23016
  }).strict().refine((p) => p.formats === void 0 !== (p.platform === void 0), {
22646
23017
  message: "provide exactly one of `formats` (explicit ratios) or `platform` (a placement preset)",
22647
23018
  path: ["formats"]
@@ -22652,33 +23023,33 @@ var imageAspectAdaptNode = delegated({
22652
23023
  category: "image",
22653
23024
  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.",
22654
23025
  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.",
22655
- inputs: z64.object({ source: ImageRef }).loose(),
23026
+ inputs: z65.object({ source: ImageRef }).loose(),
22656
23027
  params: ImageAspectAdaptParams,
22657
- outputs: z64.object({ images: z64.array(ImageRef).min(1) }).strict(),
23028
+ outputs: z65.object({ images: z65.array(ImageRef).min(1) }).strict(),
22658
23029
  outputKinds: { images: "image" },
22659
23030
  cost: ({ params }) => {
22660
23031
  const p = params;
22661
23032
  const count = Math.max(1, resolveAdaptFormats(p ?? {}).length);
22662
23033
  return {
22663
- credits: count * (PRO_ADAPT_MODELS.has(String(p?.model)) ? 20 : 5),
23034
+ credits: count * imagePictureCredits(String(p?.model)),
22664
23035
  seconds_estimate: 30 + 30 * count
22665
23036
  };
22666
23037
  }
22667
23038
  });
22668
23039
 
22669
23040
  // src/engine/nodes/remote/imageBackgroundRemove.ts
22670
- import { z as z65 } from "zod";
22671
- var ImageBackgroundRemoveParams = z65.object({
23041
+ import { z as z66 } from "zod";
23042
+ var ImageBackgroundRemoveParams = z66.object({
22672
23043
  // `fal/birefnet-v2` is the legacy id stored canvases carry. It stays the default for one
22673
23044
  // release: every backend accepts it, while `replicate/birefnet` needs the backend that ships with
22674
23045
  // this CLI, and the two deploy independently.
22675
- model: z65.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
22676
- operating_resolution: z65.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
23046
+ model: z66.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
23047
+ operating_resolution: z66.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
22677
23048
  // DEPRECATED, accepted so stored canvases still parse, and ignored: the model this
22678
23049
  // node runs (`men1scus/birefnet`) takes only an image and a resolution, has no
22679
23050
  // variant and returns no mask. `mask_only: true` still returns the cutout.
22680
- model_variant: z65.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
22681
- mask_only: z65.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
23051
+ model_variant: z66.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
23052
+ mask_only: z66.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
22682
23053
  }).strict();
22683
23054
  var imageBackgroundRemoveNode = delegated({
22684
23055
  id: "image_background_remove",
@@ -22686,11 +23057,11 @@ var imageBackgroundRemoveNode = delegated({
22686
23057
  category: "image",
22687
23058
  summary: "Remove the background from an image and return a transparent PNG. Powered by BiRefNet (`men1scus/birefnet` on Replicate).",
22688
23059
  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.",
22689
- inputs: z65.object({
23060
+ inputs: z66.object({
22690
23061
  image: ImageRef
22691
23062
  }).strict(),
22692
23063
  params: ImageBackgroundRemoveParams,
22693
- outputs: z65.object({ image: ImageRef }).strict(),
23064
+ outputs: z66.object({ image: ImageRef }).strict(),
22694
23065
  outputKinds: { image: "image" },
22695
23066
  cost: ({ params }) => ({
22696
23067
  credits: estimateNodeCredits({ nodeType: "image_background_remove", params }) ?? 1,
@@ -22699,7 +23070,7 @@ var imageBackgroundRemoveNode = delegated({
22699
23070
  });
22700
23071
 
22701
23072
  // src/engine/nodes/remote/imageDescribe.ts
22702
- import { z as z66 } from "zod";
23073
+ import { z as z67 } from "zod";
22703
23074
  var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
22704
23075
  var imageDescribeNode = delegated({
22705
23076
  id: "image_describe",
@@ -22707,27 +23078,27 @@ var imageDescribeNode = delegated({
22707
23078
  category: "vision",
22708
23079
  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.",
22709
23080
  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.',
22710
- inputs: z66.object({ image: ImageRef }).loose(),
22711
- params: z66.object({
22712
- model: z66.enum(IMAGE_DESCRIBE_MODELS),
22713
- focus: z66.string().optional(),
22714
- context: z66.string().optional(),
22715
- temperature: z66.number().min(0).max(2).optional(),
22716
- max_tokens: z66.number().int().positive().optional()
23081
+ inputs: z67.object({ image: ImageRef }).loose(),
23082
+ params: z67.object({
23083
+ model: z67.enum(IMAGE_DESCRIBE_MODELS),
23084
+ focus: z67.string().optional(),
23085
+ context: z67.string().optional(),
23086
+ temperature: z67.number().min(0).max(2).optional(),
23087
+ max_tokens: z67.number().int().positive().optional()
22717
23088
  }).strict(),
22718
- outputs: z66.object({ description: JsonRef }).strict(),
23089
+ outputs: z67.object({ description: JsonRef }).strict(),
22719
23090
  outputKinds: { description: "json" },
22720
23091
  cost: () => ({ credits: 2, seconds_estimate: 10 })
22721
23092
  });
22722
23093
 
22723
23094
  // src/engine/nodes/remote/imageLayerize.ts
22724
- import { z as z67 } from "zod";
22725
- var ImageLayerizeParams = z67.object({
23095
+ import { z as z68 } from "zod";
23096
+ var ImageLayerizeParams = z68.object({
22726
23097
  /**
22727
23098
  * What to separate. Read as extraction instructions, not as a generation
22728
23099
  * prompt — "keep the bottle and its shadow as one layer", not "make it blue".
22729
23100
  */
22730
- instructions: z67.string().max(600).optional()
23101
+ instructions: z68.string().max(600).optional()
22731
23102
  }).strict();
22732
23103
  var imageLayerizeNode = delegated({
22733
23104
  id: "image_layerize",
@@ -22735,12 +23106,12 @@ var imageLayerizeNode = delegated({
22735
23106
  category: "image",
22736
23107
  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.",
22737
23108
  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.",
22738
- inputs: z67.object({
23109
+ inputs: z68.object({
22739
23110
  image: ImageRef
22740
23111
  }).strict(),
22741
23112
  params: ImageLayerizeParams,
22742
- outputs: z67.object({
22743
- layers: z67.array(ImageRef),
23113
+ outputs: z68.object({
23114
+ layers: z68.array(ImageRef),
22744
23115
  base: ImageRef,
22745
23116
  manifest: JsonRef
22746
23117
  }).strict(),
@@ -22751,28 +23122,24 @@ var imageLayerizeNode = delegated({
22751
23122
  });
22752
23123
 
22753
23124
  // src/engine/nodes/remote/imageReferenceSheet.ts
22754
- import { z as z68 } from "zod";
23125
+ import { z as z69 } from "zod";
22755
23126
  var REFERENCE_SHEET_MODELS = [
22756
23127
  "google/gemini-3-pro-image",
22757
23128
  "google/gemini-3.1-flash-image",
22758
23129
  "google/gemini-3-pro-image-preview",
22759
23130
  "google/gemini-3.1-flash-image-preview"
22760
23131
  ];
22761
- var PRO_SHEET_MODELS = /* @__PURE__ */ new Set([
22762
- "google/gemini-3-pro-image",
22763
- "google/gemini-3-pro-image-preview"
22764
- ]);
22765
- var ImageReferenceSheetParams = z68.object({
22766
- model: z68.enum(REFERENCE_SHEET_MODELS),
22767
- subject_description: z68.string().min(1),
23132
+ var ImageReferenceSheetParams = z69.object({
23133
+ model: z69.enum(REFERENCE_SHEET_MODELS),
23134
+ subject_description: z69.string().min(1),
22768
23135
  // `location` = a set/room shown from several camera ANGLES (not a rotated subject),
22769
23136
  // so a multi-scene shoot keeps one consistent set.
22770
- subject_type: z68.enum(["character", "person", "product", "location"]),
22771
- views: z68.array(z68.string().min(1)).min(2).max(8).optional(),
22772
- style: z68.string().optional(),
22773
- prompt_override: z68.string().min(1).optional(),
22774
- aspect_ratio: z68.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(),
22775
- image_size: z68.enum(OPENROUTER_IMAGE_SIZES).optional()
23137
+ subject_type: z69.enum(["character", "person", "product", "location"]),
23138
+ views: z69.array(z69.string().min(1)).min(2).max(8).optional(),
23139
+ style: z69.string().optional(),
23140
+ prompt_override: z69.string().min(1).optional(),
23141
+ aspect_ratio: z69.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(),
23142
+ image_size: z69.enum(OPENROUTER_IMAGE_SIZES).optional()
22776
23143
  }).strict();
22777
23144
  var imageReferenceSheetNode = delegated({
22778
23145
  id: "image_reference_sheet",
@@ -22780,21 +23147,21 @@ var imageReferenceSheetNode = delegated({
22780
23147
  category: "image",
22781
23148
  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).",
22782
23149
  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.",
22783
- inputs: z68.object({ references: z68.array(ImageRef).min(1).max(6) }).loose(),
23150
+ inputs: z69.object({ references: z69.array(ImageRef).min(1).max(6) }).loose(),
22784
23151
  params: ImageReferenceSheetParams,
22785
- outputs: z68.object({ sheet: ImageRef }).strict(),
23152
+ outputs: z69.object({ sheet: ImageRef }).strict(),
22786
23153
  outputKinds: { sheet: "image" },
22787
23154
  cost: ({ params }) => ({
22788
- credits: PRO_SHEET_MODELS.has(String(params?.model)) ? 20 : 5,
23155
+ credits: imagePictureCredits(String(params?.model)),
22789
23156
  seconds_estimate: 30
22790
23157
  })
22791
23158
  });
22792
23159
 
22793
23160
  // src/engine/nodes/remote/imageSearch.ts
22794
- import { z as z69 } from "zod";
22795
- var ImageSearchParams = z69.object({
22796
- prompt: z69.string().min(1),
22797
- count: z69.number().int().min(1).max(20).default(5)
23161
+ import { z as z70 } from "zod";
23162
+ var ImageSearchParams = z70.object({
23163
+ prompt: z70.string().min(1),
23164
+ count: z70.number().int().min(1).max(20).default(5)
22798
23165
  }).strict();
22799
23166
  var imageSearchNode = delegated({
22800
23167
  id: "image_search",
@@ -22802,15 +23169,15 @@ var imageSearchNode = delegated({
22802
23169
  category: "image",
22803
23170
  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.",
22804
23171
  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.",
22805
- inputs: z69.object({}).loose(),
23172
+ inputs: z70.object({}).loose(),
22806
23173
  params: ImageSearchParams,
22807
- outputs: z69.object({ images: z69.array(ImageRef).min(1) }).strict(),
23174
+ outputs: z70.object({ images: z70.array(ImageRef).min(1) }).strict(),
22808
23175
  outputKinds: { images: "image" },
22809
23176
  cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
22810
23177
  });
22811
23178
 
22812
23179
  // src/engine/nodes/remote/imageSelect.ts
22813
- import { z as z70 } from "zod";
23180
+ import { z as z71 } from "zod";
22814
23181
  var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
22815
23182
  var imageSelectNode = delegated({
22816
23183
  id: "image_select",
@@ -22818,16 +23185,16 @@ var imageSelectNode = delegated({
22818
23185
  category: "vision",
22819
23186
  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.",
22820
23187
  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.",
22821
- inputs: z70.object({ images: z70.array(ImageRef).min(1) }).loose(),
22822
- params: z70.object({
22823
- model: z70.enum(IMAGE_SELECT_MODELS),
22824
- prompt: z70.string().min(1),
22825
- count: z70.number().int().min(1).default(1),
22826
- temperature: z70.number().min(0).max(2).optional(),
22827
- max_tokens: z70.number().int().positive().optional()
23188
+ inputs: z71.object({ images: z71.array(ImageRef).min(1) }).loose(),
23189
+ params: z71.object({
23190
+ model: z71.enum(IMAGE_SELECT_MODELS),
23191
+ prompt: z71.string().min(1),
23192
+ count: z71.number().int().min(1).default(1),
23193
+ temperature: z71.number().min(0).max(2).optional(),
23194
+ max_tokens: z71.number().int().positive().optional()
22828
23195
  }).strict(),
22829
23196
  // No `reasoning` when a lone candidate passed through: there was no judgement to explain.
22830
- outputs: z70.object({ images: z70.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
23197
+ outputs: z71.object({ images: z71.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
22831
23198
  outputKinds: { images: "image", reasoning: "text" },
22832
23199
  cost: () => ({ credits: 1, seconds_estimate: 5 }),
22833
23200
  // Arity is only knowable at validate time when `images` is a literal array
@@ -22852,26 +23219,26 @@ var imageSelectNode = delegated({
22852
23219
  });
22853
23220
 
22854
23221
  // src/engine/nodes/remote/music.ts
22855
- import { z as z71 } from "zod";
23222
+ import { z as z72 } from "zod";
22856
23223
  var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
22857
- var MusicParams = z71.object({
22858
- model: z71.enum(MUSIC_MODELS),
23224
+ var MusicParams = z72.object({
23225
+ model: z72.enum(MUSIC_MODELS),
22859
23226
  /** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
22860
- prompt: z71.string().optional(),
23227
+ prompt: z72.string().optional(),
22861
23228
  /**
22862
23229
  * Structured composition plan (intro / hook / verse / outro sections with
22863
23230
  * per-section styles + durations). Mutually exclusive with `prompt`.
22864
23231
  */
22865
- composition_plan: z71.record(z71.string(), z71.unknown()).optional(),
23232
+ composition_plan: z72.record(z72.string(), z72.unknown()).optional(),
22866
23233
  /** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
22867
- music_length_ms: z71.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
22868
- seed: z71.number().int().optional(),
23234
+ music_length_ms: z72.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
23235
+ seed: z72.number().int().optional(),
22869
23236
  /** Prompt mode only — forces an instrumental (no vocals) track. */
22870
- force_instrumental: z71.boolean().optional(),
23237
+ force_instrumental: z72.boolean().optional(),
22871
23238
  /** composition_plan only — honor exact section durations. */
22872
- respect_sections_durations: z71.boolean().optional(),
23239
+ respect_sections_durations: z72.boolean().optional(),
22873
23240
  /** Emit word-level timestamps alongside the audio. */
22874
- with_timestamps: z71.boolean().optional(),
23241
+ with_timestamps: z72.boolean().optional(),
22875
23242
  /**
22876
23243
  * video-to-music only — short description of the desired score
22877
23244
  * ("upbeat synth, fast cuts, 80s") used to bias the model.
@@ -22883,14 +23250,14 @@ var MusicParams = z71.object({
22883
23250
  * confident") came back as a film score and the client's verdict was "la
22884
23251
  * musica que has elegido es muy dramática".
22885
23252
  */
22886
- description: z71.string().max(1e3).optional().describe(
23253
+ description: z72.string().max(1e3).optional().describe(
22887
23254
  "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."
22888
23255
  ),
22889
23256
  /** video-to-music only — up to 10 style tags. */
22890
- tags: z71.array(z71.string()).max(10).optional().describe(
23257
+ tags: z72.array(z72.string()).max(10).optional().describe(
22891
23258
  "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)."
22892
23259
  ),
22893
- output_format: z71.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
23260
+ output_format: z72.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
22894
23261
  }).strict();
22895
23262
  var musicNode = delegated({
22896
23263
  id: "music",
@@ -22901,9 +23268,9 @@ var musicNode = delegated({
22901
23268
  category: "audio",
22902
23269
  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`.",
22903
23270
  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.",
22904
- inputs: z71.object({ video: VideoRef.optional() }).loose(),
23271
+ inputs: z72.object({ video: VideoRef.optional() }).loose(),
22905
23272
  params: MusicParams,
22906
- outputs: z71.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
23273
+ outputs: z72.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
22907
23274
  outputKinds: { audio: "audio", timestamps: "json" },
22908
23275
  // Priced by the shared contract: per second of audio, 30s when no length is pinned.
22909
23276
  cost: ({ params }) => {
@@ -22935,25 +23302,25 @@ var musicNode = delegated({
22935
23302
  });
22936
23303
 
22937
23304
  // src/engine/nodes/remote/soundEffect.ts
22938
- import { z as z72 } from "zod";
23305
+ import { z as z73 } from "zod";
22939
23306
  var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
22940
- var SoundEffectParams = z72.object({
22941
- model: z72.enum(SOUND_EFFECT_MODELS),
23307
+ var SoundEffectParams = z73.object({
23308
+ model: z73.enum(SOUND_EFFECT_MODELS),
22942
23309
  /** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
22943
- text: z72.string().min(1),
23310
+ text: z73.string().min(1),
22944
23311
  /**
22945
23312
  * Target length in seconds. 0.5–30. Leave unset to let the model pick the
22946
23313
  * natural length for the described effect.
22947
23314
  */
22948
- duration_seconds: z72.number().min(0.5).max(30).optional(),
23315
+ duration_seconds: z73.number().min(0.5).max(30).optional(),
22949
23316
  /**
22950
23317
  * 0–1. Higher = stick closer to the prompt at the cost of variety; lower
22951
23318
  * = let the model interpret more freely. Defaults to 0.3 on the provider.
22952
23319
  */
22953
- prompt_influence: z72.number().min(0).max(1).optional(),
23320
+ prompt_influence: z73.number().min(0).max(1).optional(),
22954
23321
  /** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
22955
- loop: z72.boolean().optional(),
22956
- output_format: z72.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
23322
+ loop: z73.boolean().optional(),
23323
+ output_format: z73.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
22957
23324
  }).strict();
22958
23325
  var soundEffectNode = delegated({
22959
23326
  id: "sound_effect",
@@ -22961,9 +23328,9 @@ var soundEffectNode = delegated({
22961
23328
  category: "audio",
22962
23329
  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.",
22963
23330
  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.",
22964
- inputs: z72.object({}).loose(),
23331
+ inputs: z73.object({}).loose(),
22965
23332
  params: SoundEffectParams,
22966
- outputs: z72.object({ audio: AudioRef }).strict(),
23333
+ outputs: z73.object({ audio: AudioRef }).strict(),
22967
23334
  outputKinds: { audio: "audio" },
22968
23335
  // Priced by the shared contract: per second of audio, 5s when the model picks the length.
22969
23336
  cost: ({ params }) => {
@@ -22973,7 +23340,7 @@ var soundEffectNode = delegated({
22973
23340
  });
22974
23341
 
22975
23342
  // src/engine/nodes/remote/textGenerate.ts
22976
- import { z as z73 } from "zod";
23343
+ import { z as z74 } from "zod";
22977
23344
  var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
22978
23345
  var textGenerateNode = delegated({
22979
23346
  id: "text_generate",
@@ -22981,58 +23348,58 @@ var textGenerateNode = delegated({
22981
23348
  category: "language",
22982
23349
  summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
22983
23350
  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.',
22984
- inputs: z73.object({}).loose(),
22985
- params: z73.object({
22986
- model: z73.enum(TEXT_GENERATE_MODELS),
22987
- prompt: z73.string().min(1),
22988
- system: z73.string().optional(),
22989
- response_format: z73.enum(["text", "json_object"]).optional(),
22990
- web_search: z73.boolean().optional(),
22991
- temperature: z73.number().min(0).max(2).optional(),
22992
- max_tokens: z73.number().int().positive().optional()
23351
+ inputs: z74.object({}).loose(),
23352
+ params: z74.object({
23353
+ model: z74.enum(TEXT_GENERATE_MODELS),
23354
+ prompt: z74.string().min(1),
23355
+ system: z74.string().optional(),
23356
+ response_format: z74.enum(["text", "json_object"]).optional(),
23357
+ web_search: z74.boolean().optional(),
23358
+ temperature: z74.number().min(0).max(2).optional(),
23359
+ max_tokens: z74.number().int().positive().optional()
22993
23360
  }).strict(),
22994
- outputs: z73.object({ text: TextRef }).strict(),
23361
+ outputs: z74.object({ text: TextRef }).strict(),
22995
23362
  outputKinds: { text: "text" },
22996
23363
  cost: () => ({ credits: 1, seconds_estimate: 3 })
22997
23364
  });
22998
23365
 
22999
23366
  // src/engine/nodes/remote/tts.ts
23000
- import { z as z74 } from "zod";
23367
+ import { z as z75 } from "zod";
23001
23368
  var TTS_MODELS = ["elevenlabs/eleven_v3"];
23002
- var TtsVoiceSettings = z74.object({
23003
- stability: z74.number().min(0).max(1).optional(),
23004
- similarity_boost: z74.number().min(0).max(1).optional(),
23005
- style: z74.number().min(0).max(1).optional(),
23006
- use_speaker_boost: z74.boolean().optional(),
23007
- speed: z74.number().min(0.25).max(4).optional()
23369
+ var TtsVoiceSettings = z75.object({
23370
+ stability: z75.number().min(0).max(1).optional(),
23371
+ similarity_boost: z75.number().min(0).max(1).optional(),
23372
+ style: z75.number().min(0).max(1).optional(),
23373
+ use_speaker_boost: z75.boolean().optional(),
23374
+ speed: z75.number().min(0.25).max(4).optional()
23008
23375
  }).strict();
23009
- var TtsPronunciationLocator = z74.object({
23010
- pronunciation_dictionary_id: z74.string().min(1),
23011
- version_id: z74.string().nullable().optional()
23376
+ var TtsPronunciationLocator = z75.object({
23377
+ pronunciation_dictionary_id: z75.string().min(1),
23378
+ version_id: z75.string().nullable().optional()
23012
23379
  }).strict();
23013
- var TtsParams = z74.object({
23014
- model: z74.enum(TTS_MODELS),
23015
- text: z74.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
23016
- voice: z74.string().min(1),
23380
+ var TtsParams = z75.object({
23381
+ model: z75.enum(TTS_MODELS),
23382
+ text: z75.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
23383
+ voice: z75.string().min(1),
23017
23384
  /** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
23018
- output_format: z74.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
23019
- seed: z74.number().int().min(0).max(4294967295).optional(),
23385
+ output_format: z75.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
23386
+ seed: z75.number().int().min(0).max(4294967295).optional(),
23020
23387
  // Top-level shortcuts; structured form is `voice_settings`.
23021
- stability: z74.number().min(0).max(1).optional(),
23022
- similarity_boost: z74.number().min(0).max(1).optional(),
23388
+ stability: z75.number().min(0).max(1).optional(),
23389
+ similarity_boost: z75.number().min(0).max(1).optional(),
23023
23390
  voice_settings: TtsVoiceSettings.optional(),
23024
23391
  /** ISO 639-1 language code. eleven_v3 supports language hints. */
23025
- language_code: z74.string().optional(),
23026
- pronunciation_dictionary_locators: z74.array(TtsPronunciationLocator).max(3).optional(),
23027
- apply_text_normalization: z74.enum(["auto", "on", "off"]).optional(),
23392
+ language_code: z75.string().optional(),
23393
+ pronunciation_dictionary_locators: z75.array(TtsPronunciationLocator).max(3).optional(),
23394
+ apply_text_normalization: z75.enum(["auto", "on", "off"]).optional(),
23028
23395
  /** Currently Japanese-only. Adds latency. */
23029
- apply_language_text_normalization: z74.boolean().optional(),
23396
+ apply_language_text_normalization: z75.boolean().optional(),
23030
23397
  /**
23031
23398
  * When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
23032
23399
  * adds a `timestamps` output (character-level alignment) for caption
23033
23400
  * rendering, lipsync, and beat-matched cuts.
23034
23401
  */
23035
- with_timestamps: z74.boolean().optional()
23402
+ with_timestamps: z75.boolean().optional()
23036
23403
  }).strict();
23037
23404
  var ttsNode = delegated({
23038
23405
  id: "tts",
@@ -23040,9 +23407,9 @@ var ttsNode = delegated({
23040
23407
  category: "audio",
23041
23408
  summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
23042
23409
  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).",
23043
- inputs: z74.object({}).loose(),
23410
+ inputs: z75.object({}).loose(),
23044
23411
  params: TtsParams,
23045
- outputs: z74.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
23412
+ outputs: z75.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
23046
23413
  outputKinds: { audio: "audio", timestamps: "json" },
23047
23414
  // Priced by the shared contract, the same per-character rate the backend charges.
23048
23415
  cost: ({ params }) => ({
@@ -23052,11 +23419,11 @@ var ttsNode = delegated({
23052
23419
  });
23053
23420
 
23054
23421
  // src/engine/nodes/remote/video.ts
23055
- import { z as z75 } from "zod";
23056
- var videoModelEnum = z75.enum(VIDEO_GENERATE_MODELS);
23057
- var VideoGenerateParams = z75.object({
23422
+ import { z as z76 } from "zod";
23423
+ var videoModelEnum = z76.enum(VIDEO_GENERATE_MODELS);
23424
+ var VideoGenerateParams = z76.object({
23058
23425
  model: videoModelEnum,
23059
- ...registryParamShape("video_generate", { prompt: z75.string().min(1) })
23426
+ ...registryParamShape("video_generate", { prompt: z76.string().min(1) })
23060
23427
  }).strict();
23061
23428
  var videoGenerateNode = delegated({
23062
23429
  id: "video_generate",
@@ -23064,7 +23431,7 @@ var videoGenerateNode = delegated({
23064
23431
  category: "video",
23065
23432
  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.",
23066
23433
  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.",
23067
- inputs: z75.object({
23434
+ inputs: z76.object({
23068
23435
  first_frame: ImageRef.optional(),
23069
23436
  last_frame: ImageRef.optional(),
23070
23437
  reference: ImageRef.optional(),
@@ -23074,10 +23441,10 @@ var videoGenerateNode = delegated({
23074
23441
  * exclusive with `first_frame` — OpenRouter treats a request carrying both
23075
23442
  * as image-to-video and ignores these, so wire one or the other.
23076
23443
  */
23077
- references: z75.array(ImageRef).optional()
23444
+ references: z76.array(ImageRef).optional()
23078
23445
  }).loose(),
23079
23446
  params: VideoGenerateParams,
23080
- outputs: z75.object({ video: VideoRef }).strict(),
23447
+ outputs: z76.object({ video: VideoRef }).strict(),
23081
23448
  outputKinds: { video: "video" },
23082
23449
  // Priced from the shared contract, so `validate`'s quote and the charge the
23083
23450
  // backend applies come from ONE table. A flat number here under-quoted a
@@ -23092,15 +23459,15 @@ var videoGenerateNode = delegated({
23092
23459
  });
23093
23460
 
23094
23461
  // src/engine/nodes/remote/videoBackgroundRemove.ts
23095
- import { z as z76 } from "zod";
23096
- var VideoBackgroundRemoveParams = z76.object({
23462
+ import { z as z77 } from "zod";
23463
+ var VideoBackgroundRemoveParams = z77.object({
23097
23464
  // `fal/veed-video-background-removal` is the legacy id stored canvases carry. It stays the default for one
23098
23465
  // release: every backend accepts it, while `replicate/birefnet-video` needs the backend that ships with
23099
23466
  // this CLI, and the two deploy independently.
23100
- model: z76.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
23467
+ model: z77.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
23101
23468
  // Refines the matte's soft edges against background bleed (the model's `refine_fg`).
23102
- edge_refinement: z76.boolean().optional().default(true),
23103
- output_codec: z76.enum(["vp9", "h264"]).optional().default("vp9")
23469
+ edge_refinement: z77.boolean().optional().default(true),
23470
+ output_codec: z77.enum(["vp9", "h264"]).optional().default("vp9")
23104
23471
  }).strict();
23105
23472
  var videoBackgroundRemoveNode = delegated({
23106
23473
  id: "video_background_remove",
@@ -23108,11 +23475,11 @@ var videoBackgroundRemoveNode = delegated({
23108
23475
  category: "video",
23109
23476
  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).",
23110
23477
  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.",
23111
- inputs: z76.object({
23478
+ inputs: z77.object({
23112
23479
  video: VideoRef
23113
23480
  }).strict(),
23114
23481
  params: VideoBackgroundRemoveParams,
23115
- outputs: z76.object({ video: VideoRef }).strict(),
23482
+ outputs: z77.object({ video: VideoRef }).strict(),
23116
23483
  outputKinds: { video: "video" },
23117
23484
  // Priced by the shared contract per 30 frames; the quote assumes the same 30s of
23118
23485
  // footage the charge falls back to when the clip carries no duration.
@@ -23123,7 +23490,7 @@ var videoBackgroundRemoveNode = delegated({
23123
23490
  });
23124
23491
 
23125
23492
  // src/engine/nodes/remote/videoDeconstruct.ts
23126
- import { z as z77 } from "zod";
23493
+ import { z as z78 } from "zod";
23127
23494
  var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
23128
23495
  var videoDeconstructNode = delegated({
23129
23496
  id: "video_deconstruct",
@@ -23131,34 +23498,34 @@ var videoDeconstructNode = delegated({
23131
23498
  category: "video",
23132
23499
  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).',
23133
23500
  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.',
23134
- inputs: z77.object({ video: VideoRef }).loose(),
23135
- params: z77.object({
23136
- model: z77.enum(VIDEO_DECONSTRUCT_MODELS),
23137
- mode: z77.enum(["full", "index"]).optional(),
23138
- language: z77.string().min(2).max(8).optional(),
23139
- max_scenes: z77.number().int().min(1).max(60).optional(),
23140
- focus: z77.string().optional(),
23141
- start_s: z77.number().min(0).optional(),
23142
- end_s: z77.number().positive().optional(),
23501
+ inputs: z78.object({ video: VideoRef }).loose(),
23502
+ params: z78.object({
23503
+ model: z78.enum(VIDEO_DECONSTRUCT_MODELS),
23504
+ mode: z78.enum(["full", "index"]).optional(),
23505
+ language: z78.string().min(2).max(8).optional(),
23506
+ max_scenes: z78.number().int().min(1).max(60).optional(),
23507
+ focus: z78.string().optional(),
23508
+ start_s: z78.number().min(0).optional(),
23509
+ end_s: z78.number().positive().optional(),
23143
23510
  // Real visual shot-cut timestamps (absolute seconds), detected locally with
23144
23511
  // ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
23145
23512
  // onto these and SPLITS any scene that spans one, so a scene's frames never
23146
23513
  // straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
23147
- shot_cuts: z77.array(z77.number().min(0)).max(200).optional(),
23514
+ shot_cuts: z78.array(z78.number().min(0)).max(200).optional(),
23148
23515
  // The video model's per-clip ceiling (seconds). A shot longer than this is
23149
23516
  // split into seamless continuation sub-scenes (shared splice frame), so long
23150
23517
  // shots reproduce in full instead of being truncated. `scaffold-video` sets
23151
23518
  // the Seedance ceiling (15); omit to disable length splitting.
23152
- max_clip_s: z77.number().positive().max(60).optional(),
23519
+ max_clip_s: z78.number().positive().max(60).optional(),
23153
23520
  // Transcript provider for the blueprint's dialogue/transcript. Default
23154
23521
  // Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
23155
- transcriber: z77.enum(["groq", "deepgram"]).optional()
23522
+ transcriber: z78.enum(["groq", "deepgram"]).optional()
23156
23523
  }).strict(),
23157
- outputs: z77.object({
23524
+ outputs: z78.object({
23158
23525
  analysis: JsonRef,
23159
23526
  // Absent in mode:"index" (structure only, no Mux frame extraction).
23160
- start_frames: z77.array(ImageRef).min(1).optional(),
23161
- end_frames: z77.array(ImageRef).min(1).optional(),
23527
+ start_frames: z78.array(ImageRef).min(1).optional(),
23528
+ end_frames: z78.array(ImageRef).min(1).optional(),
23162
23529
  transcript: JsonRef
23163
23530
  }).strict(),
23164
23531
  outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
@@ -23166,21 +23533,21 @@ var videoDeconstructNode = delegated({
23166
23533
  });
23167
23534
 
23168
23535
  // src/engine/nodes/remote/videoLipsync.ts
23169
- import { z as z78 } from "zod";
23170
- var VideoLipsyncParams = z78.object({
23171
- model: z78.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
23536
+ import { z as z79 } from "zod";
23537
+ var VideoLipsyncParams = z79.object({
23538
+ model: z79.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
23172
23539
  }).strict();
23173
23540
  var videoLipsyncNode = delegated({
23174
23541
  id: "video_lipsync",
23175
23542
  version: "1.0.0",
23176
23543
  category: "video",
23177
23544
  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.",
23178
- inputs: z78.object({
23545
+ inputs: z79.object({
23179
23546
  video: VideoRef,
23180
23547
  audio: AudioRef
23181
23548
  }).strict(),
23182
23549
  params: VideoLipsyncParams,
23183
- outputs: z78.object({ video: VideoRef }).strict(),
23550
+ outputs: z79.object({ video: VideoRef }).strict(),
23184
23551
  outputKinds: { video: "video" },
23185
23552
  // Priced by the shared contract per minute of output; the quote cannot see the
23186
23553
  // clips' lengths, so it assumes the same one minute the charge falls back to.
@@ -23194,7 +23561,7 @@ var videoLipsyncNode = delegated({
23194
23561
  import { mkdtemp as mkdtemp8, readFile as readFile17, rm as rm8 } from "fs/promises";
23195
23562
  import { tmpdir as tmpdir8 } from "os";
23196
23563
  import path21 from "path";
23197
- import { z as z79 } from "zod";
23564
+ import { z as z80 } from "zod";
23198
23565
 
23199
23566
  // src/engine/nodes/local/lib/ffmpeg.ts
23200
23567
  import { execFile as execFile8 } from "child_process";
@@ -23432,30 +23799,30 @@ function timeScriptWords(script, transcript, language) {
23432
23799
  }
23433
23800
 
23434
23801
  // src/engine/nodes/remote/videoTranscribe.ts
23435
- var VideoTranscribeParams = z79.object({
23436
- language: z79.string().min(2).max(8).optional(),
23802
+ var VideoTranscribeParams = z80.object({
23803
+ language: z80.string().min(2).max(8).optional(),
23437
23804
  // Provider choice is explicit (no env-based silent branching). Default Groq
23438
23805
  // Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
23439
23806
  // `rich` JSON output with punctuated words + paragraph/sentence grouping.
23440
- transcriber: z79.enum(["groq", "deepgram"]).optional(),
23441
- keyterms: z79.array(z79.string().min(2).max(60)).max(30).optional().describe(
23807
+ transcriber: z80.enum(["groq", "deepgram"]).optional(),
23808
+ keyterms: z80.array(z80.string().min(2).max(60)).max(30).optional().describe(
23442
23809
  "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."
23443
23810
  ),
23444
- script: z79.string().min(1).optional().describe(
23811
+ script: z80.string().min(1).optional().describe(
23445
23812
  "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."
23446
23813
  )
23447
23814
  }).strict();
23448
- var VideoTranscribeInputs = z79.object({
23815
+ var VideoTranscribeInputs = z80.object({
23449
23816
  // A video (audio auto-extracted locally) OR a bare audio track. The key stays
23450
23817
  // `video` for back-compat; the backend already accepts audio-kind refs on it —
23451
23818
  // the local extraction path has been shipping one for every video input.
23452
- video: z79.union([VideoRef, AudioRef])
23819
+ video: z80.union([VideoRef, AudioRef])
23453
23820
  }).strict();
23454
- var VideoTranscribeOutputs = z79.object({
23455
- transcript: z79.custom(),
23821
+ var VideoTranscribeOutputs = z80.object({
23822
+ transcript: z80.custom(),
23456
23823
  // Only emitted by the Deepgram path: full punctuated words + paragraph /
23457
23824
  // sentence grouping with speaker indices. Absent for the default Groq path.
23458
- rich: z79.custom().optional()
23825
+ rich: z80.custom().optional()
23459
23826
  }).strict();
23460
23827
  var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
23461
23828
  var videoTranscribeNode = defineNode({
@@ -23568,7 +23935,7 @@ async function tryExtractAudio(inputs, ctx) {
23568
23935
  }
23569
23936
 
23570
23937
  // src/engine/nodes/remote/voiceSelect.ts
23571
- import { z as z80 } from "zod";
23938
+ import { z as z81 } from "zod";
23572
23939
  var voiceSelectNode = delegated({
23573
23940
  id: "voice_select",
23574
23941
  // 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
@@ -23586,27 +23953,27 @@ var voiceSelectNode = delegated({
23586
23953
  category: "audio",
23587
23954
  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.',
23588
23955
  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.',
23589
- inputs: z80.object({}).loose(),
23590
- params: z80.object({
23591
- description: z80.string().min(1),
23592
- gender: z80.string().optional(),
23593
- age: z80.string().optional(),
23594
- accent: z80.string().optional(),
23595
- language: z80.string().optional(),
23956
+ inputs: z81.object({}).loose(),
23957
+ params: z81.object({
23958
+ description: z81.string().min(1),
23959
+ gender: z81.string().optional(),
23960
+ age: z81.string().optional(),
23961
+ accent: z81.string().optional(),
23962
+ language: z81.string().optional(),
23596
23963
  /**
23597
23964
  * A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
23598
23965
  * ranking is skipped and this id is emitted as-is.
23599
23966
  */
23600
- voice_id: z80.string().min(1).max(120).optional(),
23601
- limit: z80.number().int().min(1).max(20).optional()
23967
+ voice_id: z81.string().min(1).max(120).optional(),
23968
+ limit: z81.number().int().min(1).max(20).optional()
23602
23969
  }).strict(),
23603
- outputs: z80.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
23970
+ outputs: z81.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
23604
23971
  outputKinds: { voice_id: "text", candidates: "json" },
23605
23972
  cost: () => ({ credits: 0, seconds_estimate: 5 })
23606
23973
  });
23607
23974
 
23608
23975
  // src/engine/schema/catalog.ts
23609
- import { z as z81 } from "zod";
23976
+ import { z as z82 } from "zod";
23610
23977
  function generateCatalog(registry, opts = {}) {
23611
23978
  const entries2 = registry.all().map((def) => {
23612
23979
  const cost = catalogCost(def);
@@ -23617,9 +23984,9 @@ function generateCatalog(registry, opts = {}) {
23617
23984
  summary: def.summary,
23618
23985
  when_to_use: def.when_to_use,
23619
23986
  location: def.location,
23620
- inputs: z81.toJSONSchema(def.inputs, { unrepresentable: "any" }),
23621
- params: z81.toJSONSchema(def.params, { unrepresentable: "any" }),
23622
- outputs: z81.toJSONSchema(def.outputs, { unrepresentable: "any" }),
23987
+ inputs: z82.toJSONSchema(def.inputs, { unrepresentable: "any" }),
23988
+ params: z82.toJSONSchema(def.params, { unrepresentable: "any" }),
23989
+ outputs: z82.toJSONSchema(def.outputs, { unrepresentable: "any" }),
23623
23990
  cost_estimate_credits: cost?.credits,
23624
23991
  runtime_estimate_seconds: cost?.seconds_estimate
23625
23992
  };
@@ -23768,7 +24135,8 @@ var LOCAL_NODES = [
23768
24135
  videoConcatNode,
23769
24136
  avMuxNode,
23770
24137
  collectNode,
23771
- staticPrefilterNode
24138
+ staticPrefilterNode,
24139
+ staticSafeFitNode
23772
24140
  ];
23773
24141
  var REMOTE_NODES = [
23774
24142
  textGenerateNode,
@@ -23926,6 +24294,7 @@ export {
23926
24294
  testimonialSourcesRemoveRequestSchema,
23927
24295
  testimonialSourcesScheduleRequestSchema,
23928
24296
  videosIngestRequestSchema,
24297
+ HttpClient,
23929
24298
  BackendClient,
23930
24299
  requireCredentialsFromEnv,
23931
24300
  RunAbortedError,
@@ -23977,6 +24346,7 @@ export {
23977
24346
  supportedAspectRatios,
23978
24347
  nearestSupportedAspectRatio,
23979
24348
  estimateVideoCredits,
24349
+ candidPersonPortraitPrompt,
23980
24350
  VideoBlueprintSchema,
23981
24351
  BlueprintOverlaySchema,
23982
24352
  BlueprintFloatingElementSchema,
@@ -24014,7 +24384,7 @@ export {
24014
24384
  AD_SPEC_SCHEMA,
24015
24385
  AdSpecSchema,
24016
24386
  readAdSpec,
24017
- withShortInSceneLettering,
24387
+ withRenderedLettering,
24018
24388
  conceptClassOf,
24019
24389
  plainText,
24020
24390
  adSpecFromBlueprint,
@@ -24035,6 +24405,7 @@ export {
24035
24405
  STATIC_MASTER_ROLE,
24036
24406
  STATIC_SIZE_ROLE,
24037
24407
  STATIC_ADAPT_ROLE,
24408
+ STATIC_SAFE_FIT_ROLE,
24038
24409
  LOGO_COMPOSITE_ROLE,
24039
24410
  STATIC_FIX_ROLE,
24040
24411
  LOGO_COMPOSITE_DEFAULT,
@@ -24043,6 +24414,7 @@ export {
24043
24414
  formatSlug,
24044
24415
  staticMasterFormat,
24045
24416
  staticRenderNodes,
24417
+ safeFitNode,
24046
24418
  RENDER_GRAPH_IDS,
24047
24419
  regenerateTargets,
24048
24420
  elementMentionKeywords,
@@ -24087,4 +24459,4 @@ export {
24087
24459
  defaultRegistry,
24088
24460
  createEngineFromEnv
24089
24461
  };
24090
- //# sourceMappingURL=chunk-PFN3A4VQ.js.map
24462
+ //# sourceMappingURL=chunk-GLPQQP5C.js.map