@koda-sl/baker-cli 0.433.0 → 0.434.0-dev.2729b1413

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 +45 -16
  2. package/dist/{chunk-X46KRGZU.js → chunk-7THIX4WW.js} +3 -3
  3. package/dist/{chunk-LDS7LUKK.js → chunk-EURJVJQY.js} +2 -2
  4. package/dist/{chunk-SPPJRGQF.js → chunk-F7LXMR73.js} +5 -1
  5. package/dist/chunk-F7LXMR73.js.map +1 -0
  6. package/dist/{chunk-7BQIIJLV.js → chunk-O5HQRXS6.js} +453 -157
  7. package/dist/chunk-O5HQRXS6.js.map +1 -0
  8. package/dist/{chunk-YBJ7KJNA.js → chunk-QLNGHVRT.js} +3 -3
  9. package/dist/{chunk-4JBT2CSW.js → chunk-VNROEZTR.js} +4 -4
  10. package/dist/cli.js +8202 -4408
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{client-HQ7LO4CO.js → client-TCDMWAL3.js} +4 -4
  13. package/dist/engine/index.d.ts +13 -6
  14. package/dist/engine/index.js +3 -3
  15. package/dist/{env-M4DKUWLD.js → env-ZPCFZJN5.js} +4 -2
  16. package/dist/{output-T4MY3I56.js → output-CW4JBWIM.js} +5 -5
  17. package/dist/{shared-RZMKZE4W.js → shared-6I2TP4YQ.js} +6 -6
  18. package/package.json +1 -1
  19. package/dist/chunk-7BQIIJLV.js.map +0 -1
  20. package/dist/chunk-SPPJRGQF.js.map +0 -1
  21. /package/dist/{chunk-X46KRGZU.js.map → chunk-7THIX4WW.js.map} +0 -0
  22. /package/dist/{chunk-LDS7LUKK.js.map → chunk-EURJVJQY.js.map} +0 -0
  23. /package/dist/{chunk-YBJ7KJNA.js.map → chunk-QLNGHVRT.js.map} +0 -0
  24. /package/dist/{chunk-4JBT2CSW.js.map → chunk-VNROEZTR.js.map} +0 -0
  25. /package/dist/{client-HQ7LO4CO.js.map → client-TCDMWAL3.js.map} +0 -0
  26. /package/dist/{env-M4DKUWLD.js.map → env-ZPCFZJN5.js.map} +0 -0
  27. /package/dist/{output-T4MY3I56.js.map → output-CW4JBWIM.js.map} +0 -0
  28. /package/dist/{shared-RZMKZE4W.js.map → shared-6I2TP4YQ.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-LDS7LUKK.js";
4
+ } from "./chunk-EURJVJQY.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -22,11 +22,11 @@ import {
22
22
  refuseNonPublicUrl,
23
23
  requireConnection,
24
24
  shouldEscalate
25
- } from "./chunk-SPPJRGQF.js";
25
+ } from "./chunk-F7LXMR73.js";
26
26
 
27
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
27
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
30
30
  "use strict";
31
31
  var { hasOwnProperty } = Object.prototype;
32
32
  var stringify = configure2();
@@ -1408,7 +1408,7 @@ function resolveAdaptFormats(params) {
1408
1408
  return params.formats ?? [];
1409
1409
  }
1410
1410
 
1411
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1411
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
1412
1412
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1413
1413
  var configure = import__.default.configure;
1414
1414
  var wrapper_default = import__.default;
@@ -2826,6 +2826,12 @@ var MODEL_ROLES = {
2826
2826
  * only and asked once per ad and viewer, so Flash; the calibration set (E8) decides.
2827
2827
  */
2828
2828
  viewerCheck: { kind: "text_generate", model: "~google/gemini-flash-latest" },
2829
+ /**
2830
+ * Picks the customer truths out of reviews, testimonials and threads (`baker research
2831
+ * customers`): returns exact spans the code then checks against the source. Text only and
2832
+ * one call per batch, so Flash.
2833
+ */
2834
+ customerInsights: { kind: "text_generate", model: "~google/gemini-flash-latest" },
2829
2835
  /**
2830
2836
  * The same viewer shown the FINISHED static ad at phone size for about three seconds, with no
2831
2837
  * brand and no brief: what is sold, whether it is for them, what to do next, what stopped them
@@ -2867,6 +2873,7 @@ var PLATE_JUDGE_MODEL = MODEL_ROLES.plateJudge.model;
2867
2873
  var AD_DESCRIBE_MODEL = MODEL_ROLES.adDescribe.model;
2868
2874
  var BLUEPRINT_ANALYSIS_MODEL = MODEL_ROLES.blueprintAnalysis.model;
2869
2875
  var VIEWER_CHECK_MODEL = MODEL_ROLES.viewerCheck.model;
2876
+ var CUSTOMER_INSIGHTS_MODEL = MODEL_ROLES.customerInsights.model;
2870
2877
  var GLANCE_CHECK_MODEL = MODEL_ROLES.glanceCheck.model;
2871
2878
  var TEXT_BOX_READER_MODEL = MODEL_ROLES.textBoxReader.model;
2872
2879
  var VIDEO_DECONSTRUCT_MODEL = MODEL_ROLES.videoDeconstruct.model;
@@ -3587,23 +3594,6 @@ function resolveConcurrency(...candidates) {
3587
3594
  }
3588
3595
  return DEFAULT_CONCURRENCY;
3589
3596
  }
3590
- async function mapWithConcurrency(items, limit, fn) {
3591
- const results = new Array(items.length);
3592
- let next = 0;
3593
- const worker = async () => {
3594
- while (next < items.length) {
3595
- const index = next++;
3596
- try {
3597
- results[index] = { status: "fulfilled", value: await fn(items[index], index) };
3598
- } catch (reason) {
3599
- results[index] = { status: "rejected", reason };
3600
- }
3601
- }
3602
- };
3603
- const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
3604
- await Promise.all(workers);
3605
- return results;
3606
- }
3607
3597
 
3608
3598
  // src/engine/lib/json-walk.ts
3609
3599
  function isContainerObject(value) {
@@ -4408,7 +4398,18 @@ var CanvasMetadata = z4.object({
4408
4398
  // `scaffold-video`) so the editable checklist travels inside the canvas
4409
4399
  // instead of only printing to stdout. Ignored by the engine.
4410
4400
  todo: z4.unknown().optional(),
4411
- video: VideoMeta
4401
+ video: VideoMeta,
4402
+ // A static ad of a series (`seriesMemberAd`): the lead ad its picture is an edit of, the
4403
+ // lead's version, and the lead's words as that version drew them, which this ad's replace.
4404
+ // Ignored by the engine; the brief projection reads it.
4405
+ series: z4.object({
4406
+ lead: z4.string().min(1),
4407
+ run_id: z4.string().min(1),
4408
+ copy: z4.array(z4.object({ text: z4.string(), role: z4.string().optional() }).strict()),
4409
+ design: z4.string().optional(),
4410
+ // A translation of the lead: the language (BCP-47) its words are in.
4411
+ language: z4.string().min(2).optional()
4412
+ }).strict().optional()
4412
4413
  }).strict().optional();
4413
4414
  var CanvasSchema = z4.object({
4414
4415
  schema: z4.literal("baker-canvas/1"),
@@ -4992,10 +4993,10 @@ var GPT_IMAGE_BRIEF = {
4992
4993
  // (.context/static-review/BENCH.md#E3).
4993
4994
  // quality max: the deterministic checks tie (shippable 9-12% on every step, n=100), but on the
4994
4995
  // E4 gallery (30 rated pairs) the user's win rate is max 69%, xhigh 54%, high 47%, Flare 34%,
4995
- // and the judge's on the same pairs 73%, 65%, 39%, 31%. The user: results over cost ("use the
4996
- // best one, not the cheaper"), so the step is the best, at $0.166 per image vs $0.085 for xhigh
4997
- // and 80 s vs 44 s (BENCH.md#E4, § "E1 v2").
4998
- params: { n: 1, quality: "max", imageSize: "2K" }
4996
+ // and the judge's on the same pairs 73%, 65%, 39%, 31%. Max costs $0.166 per image vs $0.085
4997
+ // for xhigh and 80 s vs 44 s (BENCH.md#E4, § "E1 v2"); a round renders twice in a row (the
4998
+ // options, then each size), so three ads took most of an hour. Oct 2026 the user chose xhigh.
4999
+ params: { n: 1, quality: "xhigh", imageSize: "2K" }
4999
5000
  };
5000
5001
  var GEMINI_IMAGE_BRIEF = {
5001
5002
  // "Use Natural Language & Full Sentences… as if you were briefing a human artist", and
@@ -6699,6 +6700,7 @@ function textFidelityDirective(strings2, hasFont) {
6699
6700
  return [
6700
6701
  exact,
6701
6702
  "Render NO other words anywhere in the image \u2014 no invented labels, captions, sub-headers, or filler text.",
6703
+ "Any bill, quote, receipt, screen or price tag in the scene shows no readable prices or amounts beyond these strings: its figures are blurred or out of focus.",
6702
6704
  "For any brand name or uncommon word, lock its letters exactly as written (spell it out letter-by-letter if that helps the characters render correctly).",
6703
6705
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
6704
6706
  ].join("\n");
@@ -6763,22 +6765,32 @@ var Viewer = z7.object({
6763
6765
  /** Who will see the ad, in the words a marketer would use. */
6764
6766
  who: z7.string().trim().min(1),
6765
6767
  market: Market.optional(),
6766
- /** What they already know about the brand and the offer. */
6767
- knows: z7.string().optional(),
6768
+ /** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
6769
+ knows: z7.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z7.string().optional()),
6768
6770
  temperature: z7.enum(["cold", "warm", "hot"]).optional(),
6769
6771
  /** Where they meet it (feed, Stories, Reels). */
6770
6772
  placement: z7.string().optional()
6771
6773
  }).strict();
6772
- var Scene = z7.object({
6774
+ var withoutFormatName = (v) => {
6775
+ if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
6776
+ return v;
6777
+ const { concept: _format, ...scene } = v;
6778
+ return scene;
6779
+ };
6780
+ var SceneFields = z7.object({
6773
6781
  description: z7.string().trim().min(1),
6774
- /** A photograph, a designed graphic, or an illustration. */
6775
- look: z7.enum(["photo", "designed", "illustration"]),
6782
+ /** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
6783
+ look: z7.preprocess(
6784
+ (v) => typeof v !== "string" ? v : /^photo/i.test(v.trim()) ? "photo" : /^illustrat/i.test(v.trim()) ? "illustration" : /^(design|graphic)/i.test(v.trim()) ? "designed" : v,
6785
+ z7.enum(["photo", "designed", "illustration"])
6786
+ ),
6776
6787
  /** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
6777
6788
  capture: z7.string().optional(),
6778
6789
  mood: z7.string().optional(),
6779
6790
  /** How the frame is arranged, by what fills each part of it. */
6780
6791
  layout: z7.string().optional()
6781
6792
  }).strict();
6793
+ var Scene = z7.preprocess(withoutFormatName, SceneFields);
6782
6794
  var Brand = z7.object({
6783
6795
  name: z7.string().optional(),
6784
6796
  /** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
@@ -6850,13 +6862,19 @@ var AdSpecSchema = z7.object({
6850
6862
  * editor's line numbers, a product's own label, a platform's badge. The render may draw them
6851
6863
  * and the review never calls them extra words.
6852
6864
  */
6853
- scene_text: z7.array(z7.string().trim().min(1)).default([]),
6865
+ scene_text: z7.preprocess(
6866
+ (v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
6867
+ z7.array(z7.string().trim().min(1)).default([])
6868
+ ),
6854
6869
  /** The formats the ad is made for; the canvas delivers them. */
6855
6870
  formats: z7.array(z7.string()).optional(),
6856
6871
  /** An optional note on direction for this concept. */
6857
6872
  direction: z7.string().optional(),
6858
6873
  /** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
6859
- mechanism: z7.array(Mechanism).default([]),
6874
+ mechanism: z7.preprocess((v) => {
6875
+ const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
6876
+ return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
6877
+ }, z7.array(Mechanism).default([])),
6860
6878
  /** Why the reference won and what this ad keeps of it (schema /3). */
6861
6879
  concept: Concept.optional(),
6862
6880
  /**
@@ -6912,6 +6930,61 @@ function conceptProblems(spec) {
6912
6930
  }
6913
6931
  return problems;
6914
6932
  }
6933
+ var PLACEHOLDER = "[TODO";
6934
+ function unwrittenProblems(spec) {
6935
+ const problems = [];
6936
+ if (spec.scene.description.includes(PLACEHOLDER)) {
6937
+ problems.push("The scene is still a placeholder: write what the viewer sees in `scene.description`.");
6938
+ }
6939
+ const lines = spec.copy.filter((line) => line.text.includes(PLACEHOLDER));
6940
+ if (lines.length > 0) {
6941
+ problems.push(`${lines.length} line(s) of copy are still placeholders: write each as it prints, or drop it.`);
6942
+ }
6943
+ return problems;
6944
+ }
6945
+ var QUOTED = /"([^"]{2,})"|“([^”]{2,})”|(?<![\p{L}\p{N}])['‘]([^'’]{2,}?)['’](?![\p{L}\p{N}])/gu;
6946
+ var FIGURE = /[£$€]\s?\d[\d.,]*|\d+(?:[.,]\d+)?\s?%/g;
6947
+ var comparable = (text2) => text2.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}£$€%]+/gu, " ").trim();
6948
+ function sceneWordProblems(spec) {
6949
+ const held = [...spec.copy.map((l) => l.text), ...spec.scene_text].map(comparable);
6950
+ const holds = (text2) => {
6951
+ const want = comparable(text2);
6952
+ return want.length === 0 || held.some((h) => h.includes(want));
6953
+ };
6954
+ const scene = [spec.scene.description, spec.scene.layout ?? ""].join(" ");
6955
+ const quoted = [...scene.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter((q) => !holds(q));
6956
+ const figures = (scene.match(FIGURE) ?? []).map((f) => f.trim()).filter((f) => !holds(f));
6957
+ const unheld = [.../* @__PURE__ */ new Set([...quoted, ...figures])];
6958
+ if (unheld.length === 0) return [];
6959
+ return [
6960
+ `The scene asks the picture to show ${unheld.map((w) => `"${w}"`).join(", ")}, which the copy does not hold: the picture would print it and the run would then spend a round erasing it. Put each one the ad needs in \`copy\` as an \`in_scene\` line with \`on\` (the bill, the phone), or describe that surface without words (blank, blurred, face-down).`
6961
+ ];
6962
+ }
6963
+ var FIELD_SHAPES = {
6964
+ copy: '"copy": [{"text": "Book your free survey", "lettering": "designed", "role": "cta"}]',
6965
+ scene: '"scene": {"description": "a family on a sunny roof terrace", "look": "photo"}',
6966
+ colors: '"colors": [{"object": "the button", "name": "deep green", "hex": "#1F5C3A"}]',
6967
+ references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
6968
+ third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
6969
+ mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
6970
+ viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
6971
+ };
6972
+ var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
6973
+ function adSpecErrorText(e) {
6974
+ if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
6975
+ const items = /* @__PURE__ */ new Map();
6976
+ for (const issue of e.issues) {
6977
+ const [field2, index, key] = issue.path;
6978
+ const at2 = typeof index === "number" ? `${String(field2)}[${index}]` : issue.path.map(String).join(".") || "the spec";
6979
+ const what = issue.code === "unrecognized_keys" ? `no field ${issue.keys.map((k) => `"${k}"`).join(", ")}` : key !== void 0 && typeof index === "number" ? `"${String(key)}" ${issue.message.replace(/^Invalid input: /, "")}` : issue.message.replace(/^Invalid input: /, "");
6980
+ items.set(at2, [...items.get(at2) ?? [], what]);
6981
+ }
6982
+ const fields = [...new Set(e.issues.map((i) => String(i.path[0] ?? "")))].filter((f) => FIELD_SHAPES[f]);
6983
+ return [
6984
+ ...[...items].map(([at2, whats]) => `${at2}: ${[...new Set(whats)].join("; ")}`),
6985
+ ...fields.map((f) => `Shape: ${FIELD_SHAPES[f]}`)
6986
+ ].join("\n");
6987
+ }
6915
6988
  function readAdSpec(raw) {
6916
6989
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6917
6990
  const advertiser = raw?.reference?.advertiser;
@@ -7402,7 +7475,7 @@ function pictureInstruction(p, ctx) {
7402
7475
  const keep = p.spec?.keep ? ` Keep ${plain(p.spec.keep, ctx.clean)}.` : "";
7403
7476
  switch (p.role) {
7404
7477
  case "base":
7405
- return ctx.job === "fix" ? "the current version of this ad, to edit." : "the finished ad, to re-lay for this shape.";
7478
+ return ctx.job === "fix" ? "the current version of this ad, to edit." : ctx.job === "series" ? "the finished ad this series repeats, to copy exactly with new words." : "the finished ad, to re-lay for this shape.";
7406
7479
  case "identity":
7407
7480
  return identityInstruction(p, what, keep, ctx.flowing);
7408
7481
  // The user saw one label come back two ways (E4 gallery, BENCH.md § "E1 v2"): the label is the product's.
@@ -7781,6 +7854,10 @@ function deliverableSection(c) {
7781
7854
  const change = sentence(c.opts.instruction?.trim() || "correct what is wrong");
7782
7855
  return c.flowing ? `Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else in Image 1 exactly the same, preserving its picture, layout, colours, lettering and every word.` : `Edit: Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the colours, the lettering and every word.`;
7783
7856
  }
7857
+ if (c.job === "series") {
7858
+ const change = sentence(c.opts.instruction?.trim() || "set the new words");
7859
+ return c.flowing ? `Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}: ${change} Keep everything else in Image 1 exactly the same, preserving its picture, layout, background, shapes, colours, lettering style and logo, each word in the place and style of the one it replaces.` : `Edit: Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the background, the shapes, the colours, the lettering style and the logo, each new line in the place, size and style of the line it replaces.`;
7860
+ }
7784
7861
  if (c.job === "relay") {
7785
7862
  return c.flowing ? `Image 1 is a finished ad. Recompose this same ad as a ${noun}${forBrand}: the same picture, colours, lettering and every word, rearranged for the new shape, with more of the scene revealed at the edges.` : `Deliverable: the same ad as Image 1, re-laid as a finished ${noun}${forBrand}. Keep the same picture, colours, lettering and every word, rearranged for the new shape; show more of the scene rather than cropping it.`;
7786
7863
  }
@@ -8364,14 +8441,16 @@ function scaleText(plan, spec, flowing, zoned, short) {
8364
8441
  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." : "";
8365
8442
  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}`;
8366
8443
  }
8367
- var ZONE_HEADROOM = 0;
8444
+ var COVERED_HEADROOM = { top: 0.06, bottom: 0.08, side: 0.02 };
8445
+ var NO_HEADROOM = { top: 0, bottom: 0, side: 0 };
8368
8446
  var pct = (share) => `${Math.round(share * 100)}%`;
8369
8447
  function zonesText(plan, flowing, ground, short) {
8370
- const top = plan.safe.top + ZONE_HEADROOM;
8371
- const bottom = plan.safe.bottom + ZONE_HEADROOM;
8372
- const side = Math.max(plan.safe.left, plan.safe.right) + ZONE_HEADROOM;
8373
- const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
8374
8448
  const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
8449
+ const room = isTall(plan) && covered ? COVERED_HEADROOM : NO_HEADROOM;
8450
+ const top = plan.safe.top + room.top;
8451
+ const bottom = plan.safe.bottom + room.bottom;
8452
+ const side = Math.max(plan.safe.left, plan.safe.right) + room.side;
8453
+ const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
8375
8454
  if (short && !flowing) {
8376
8455
  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";
8377
8456
  return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
@@ -8380,7 +8459,7 @@ function zonesText(plan, flowing, ground, short) {
8380
8459
  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.`;
8381
8460
  }
8382
8461
  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";
8383
- 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.`;
8462
+ 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 app's buttons and captions cover the top and the bottom of this frame, hiding any word there. The logo, every line and the button sit in one band, ${band}: the headline starts below the top fifth, and the button ends directly under the last line, never at the bottom edge where a feed ad puts it. 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.`;
8384
8463
  }
8385
8464
  function adaptBrief(spec, profile, opts) {
8386
8465
  const { plan, images } = opts;
@@ -8449,6 +8528,7 @@ var STATIC_ADAPT_ROLE = "static_adapt";
8449
8528
  var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
8450
8529
  var STATIC_PLACE_ROLE = "static_place";
8451
8530
  var LOGO_COMPOSITE_ROLE = "logo_composite";
8531
+ var SERIES_LEAD_ROLE = "series_lead";
8452
8532
  var STATIC_FIX_ROLE = "static_fix";
8453
8533
  var LOGO_COMPOSITE_DEFAULT = false;
8454
8534
  var ADAPT_IMAGE_SIZE = "2K";
@@ -8655,7 +8735,7 @@ function sizeNode(s, adaptFormats) {
8655
8735
  const index = adaptFormats.indexOf(s.adaptFormat ?? s.format);
8656
8736
  return magick(id, STATIC_SIZE_ROLE, `$ref:adapt.images#${index}`, fillArgs(s.size));
8657
8737
  }
8658
- var RELAYOUT_QUALITY = "max";
8738
+ var RELAYOUT_QUALITY = "xhigh";
8659
8739
  function relayoutNodes(g) {
8660
8740
  return g.plan.sizes.filter((s) => s.from === "relayout").map((s) => relayoutNode(s, g));
8661
8741
  }
@@ -10117,7 +10197,7 @@ function checkOrphanNodes(ctx) {
10117
10197
  path: `nodes[${ctx.idToIndex.get(n.id)}]`,
10118
10198
  code: STAGE_CODES.ORPHAN_NODE,
10119
10199
  severity: "warning",
10120
- message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}" \u2014 it will still execute (and bill, if billable) but nothing consumes its result; wire it in or remove it`
10200
+ message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}": a run skips it, so nothing it would make is used; wire it in or remove it`
10121
10201
  });
10122
10202
  }
10123
10203
  }
@@ -10188,6 +10268,17 @@ function unwrap(schema) {
10188
10268
  }
10189
10269
 
10190
10270
  // src/engine/engine/executor.ts
10271
+ function throwRunOutcome(signal, failures, held) {
10272
+ if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10273
+ if (failures.length === 1 && failures[0]) throw failures[0].reason;
10274
+ if (failures.length > 1) throw new LayerExecutionError([...failures]);
10275
+ if (held.length > 0) {
10276
+ throw new RunAbortedError(
10277
+ "checkpoint",
10278
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10279
+ );
10280
+ }
10281
+ }
10191
10282
  function heldByCheckpoint(canvas, layer, kinds) {
10192
10283
  if (!kinds?.size) return [];
10193
10284
  return layer.filter((id) => {
@@ -10276,45 +10367,63 @@ var Engine = class {
10276
10367
  return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
10277
10368
  }
10278
10369
  async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
10279
- const layers = topologicalLayers(graph);
10370
+ const order = topologicalLayers(graph).flat();
10280
10371
  const limit = resolveConcurrency(opts.concurrency);
10281
- for (const layer of layers) {
10282
- this.assertLayerMayRun(canvas, layer, opts);
10283
- const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
10284
- if (opts.signal?.aborted) {
10285
- return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
10286
- }
10287
- this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10288
- return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
10289
- if (r.cached) counters.cachedNodes++;
10290
- counters.totalCredits += r.credits;
10291
- const node = canvas.nodes.find((n) => n.id === nodeId);
10292
- if (node) {
10293
- const run = {
10294
- node_id: nodeId,
10295
- node_type: node.type,
10296
- cached: r.cached,
10297
- duration_ms: r.durationMs,
10298
- credits: r.credits
10299
- };
10300
- nodeRuns.push(run);
10301
- this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10302
- }
10303
- });
10304
- });
10305
- const failures = [];
10306
- settled.forEach((result, i) => {
10307
- const nodeId = layer[i];
10308
- if (result.status === "rejected" && nodeId) {
10309
- if (result.reason instanceof RunAbortedError) return;
10310
- failures.push({ nodeId, reason: result.reason });
10311
- this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(result.reason) });
10372
+ if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
10373
+ const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
10374
+ const running = /* @__PURE__ */ new Map();
10375
+ const failures = [];
10376
+ const held = [];
10377
+ const dispatch = (nodeId) => {
10378
+ waitingOn.delete(nodeId);
10379
+ this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10380
+ const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
10381
+ (r) => {
10382
+ this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
10383
+ for (const deps of waitingOn.values()) deps.delete(nodeId);
10384
+ },
10385
+ (reason) => {
10386
+ failures.push({ nodeId, reason });
10387
+ this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
10312
10388
  }
10313
- });
10314
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10315
- if (failures.length === 1 && failures[0]) throw failures[0].reason;
10316
- if (failures.length > 1) throw new LayerExecutionError(failures);
10389
+ );
10390
+ running.set(
10391
+ nodeId,
10392
+ settled.finally(() => running.delete(nodeId))
10393
+ );
10394
+ };
10395
+ const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
10396
+ for (; ; ) {
10397
+ const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
10398
+ for (const nodeId of ready) {
10399
+ if (running.size >= limit) break;
10400
+ if (isHeld(nodeId)) {
10401
+ waitingOn.delete(nodeId);
10402
+ held.push(nodeId);
10403
+ } else dispatch(nodeId);
10404
+ }
10405
+ if (running.size === 0) break;
10406
+ await Promise.race(running.values());
10317
10407
  }
10408
+ const rank = new Map(order.map((id, i) => [id, i]));
10409
+ failures.sort((a, b) => (rank.get(a.nodeId) ?? 0) - (rank.get(b.nodeId) ?? 0));
10410
+ throwRunOutcome(opts.signal, failures, held);
10411
+ }
10412
+ /** A settled node counted, recorded and announced. */
10413
+ recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
10414
+ if (r.cached) counters.cachedNodes++;
10415
+ counters.totalCredits += r.credits;
10416
+ const node = canvas.nodes.find((n) => n.id === nodeId);
10417
+ if (!node) return;
10418
+ const run = {
10419
+ node_id: nodeId,
10420
+ node_type: node.type,
10421
+ cached: r.cached,
10422
+ duration_ms: r.durationMs,
10423
+ credits: r.credits
10424
+ };
10425
+ nodeRuns.push(run);
10426
+ this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10318
10427
  }
10319
10428
  /** Progress consumers are observers only — an exception there must never fail the run. */
10320
10429
  emitProgress(opts, event) {
@@ -10406,20 +10515,6 @@ var Engine = class {
10406
10515
  this.logOutputWarnings(node.id, outputsObj);
10407
10516
  return { cached: false, credits, durationMs: elapsed };
10408
10517
  }
10409
- /**
10410
- * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
10411
- * never leaves a layer half billed: a signal, the cost cap and a checkpoint.
10412
- */
10413
- assertLayerMayRun(canvas, layer, opts) {
10414
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
10415
- const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
10416
- if (held.length > 0) {
10417
- throw new RunAbortedError(
10418
- "checkpoint",
10419
- `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10420
- );
10421
- }
10422
- }
10423
10518
  /** A cache hit replayed as this node's result: its outputs written, and free. */
10424
10519
  async replayCached(node, cacheKey, outputs, writer) {
10425
10520
  const cacheT0 = Date.now();
@@ -14584,6 +14679,18 @@ function boxesOfLine(line, boxes) {
14584
14679
  return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
14585
14680
  });
14586
14681
  }
14682
+ var BUTTON_PAD = 0.7;
14683
+ var isButtonLine = (line) => /^(cta|button|call_to_action)$/.test(roleKey2(line.role));
14684
+ function buttonShape(box) {
14685
+ const pad = BUTTON_PAD * (box.y1 - box.y0);
14686
+ return {
14687
+ ...box,
14688
+ x0: Math.max(0, box.x0 - pad),
14689
+ y0: Math.max(0, box.y0 - pad),
14690
+ x1: Math.min(1, box.x1 + pad),
14691
+ y1: Math.min(1, box.y1 + pad)
14692
+ };
14693
+ }
14587
14694
  function shownBoxes(boxes, lines, clear) {
14588
14695
  const linesOfBox = /* @__PURE__ */ new Map();
14589
14696
  for (const line of lines) {
@@ -14967,7 +15074,7 @@ async function videoDuration(video) {
14967
15074
  return null;
14968
15075
  }
14969
15076
  }
14970
- async function askGateway(model, parts, timeoutMs, spent) {
15077
+ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
14971
15078
  const key = openRouterKey();
14972
15079
  if (!key) return null;
14973
15080
  try {
@@ -14977,7 +15084,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
14977
15084
  body: JSON.stringify({
14978
15085
  model,
14979
15086
  messages: [{ role: "user", content: parts }],
14980
- usage: { include: true }
15087
+ usage: { include: true },
15088
+ ...reasoning ? { reasoning } : {}
14981
15089
  }),
14982
15090
  signal: AbortSignal.timeout(timeoutMs)
14983
15091
  });
@@ -15026,7 +15134,7 @@ async function askAboutStills(images, prompt, onSpend) {
15026
15134
  if (!hasOpenRouterKey()) return null;
15027
15135
  return await ask(images, prompt, (usage) => onSpend?.(usage));
15028
15136
  }
15029
- async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15137
+ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
15030
15138
  if (!hasOpenRouterKey()) return null;
15031
15139
  return await askGateway(
15032
15140
  model,
@@ -15037,7 +15145,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15037
15145
  )
15038
15146
  ],
15039
15147
  timeoutMs,
15040
- (usage) => onSpend?.(usage)
15148
+ (usage) => onSpend?.(usage),
15149
+ reasoning
15041
15150
  );
15042
15151
  }
15043
15152
  async function askAboutText(model, prompt, onSpend) {
@@ -15201,7 +15310,9 @@ async function readTextBoxes(image, opts = {}) {
15201
15310
  return { status: "not_checked" };
15202
15311
  }
15203
15312
  for (let attempt = 0; attempt < 2; attempt++) {
15204
- const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend);
15313
+ const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend, 6e4, {
15314
+ effort: "minimal"
15315
+ });
15205
15316
  const boxes = textBoxesFromReader(answer);
15206
15317
  if (boxes) return { status: "read", boxes };
15207
15318
  }
@@ -15291,6 +15402,8 @@ var StaticFinding = z21.object({
15291
15402
  });
15292
15403
  var ABSENCE_NEVER_A_FINDING = /\b(?:missing|no|lacks?|without)\s+(?:an?\s+|any\s+|clear\s+|visible\s+)?(?:call to action|cta)\b|\b(?:does not|doesn't) (?:show|tell) the viewer what to do/i;
15293
15404
  var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
15405
+ var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
15406
+ var BLURRED_FIGURES = /\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b(?=[^.]*\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b)|\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b(?=[^.]*\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b)/i;
15294
15407
  function classifyStaticFindings(json) {
15295
15408
  const findings = json?.findings;
15296
15409
  if (!Array.isArray(findings)) return [];
@@ -15299,7 +15412,7 @@ function classifyStaticFindings(json) {
15299
15412
  if (!parsed.success) return [];
15300
15413
  const { severity, format, what } = parsed.data;
15301
15414
  if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
15302
- const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : severity;
15415
+ const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
15303
15416
  return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
15304
15417
  });
15305
15418
  }
@@ -15310,7 +15423,7 @@ var CopyRead = z21.object({
15310
15423
  seen: z21.string().nullable()
15311
15424
  });
15312
15425
  function comparableCopy(text2) {
15313
- return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/\s+/g, " ").trim().toLowerCase();
15426
+ return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
15314
15427
  }
15315
15428
  function copyFindings(json, copy, formats, scrolled = {}) {
15316
15429
  if (copy.length === 0) return [];
@@ -15685,7 +15798,7 @@ function contextItems(opts) {
15685
15798
  opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the lead line (the largest words) cannot be read. Judge only whether those words can be read, nothing else. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line (the largest words) would be unreadable on a phone feed. That is blocking."
15686
15799
  ],
15687
15800
  sizes: opts.formats.length > 1 ? [
15688
- "12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about (blocking). Rearranged for its shape is fine."
15801
+ `12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; start the finding with "SIZES:" and report it on the size that differs). Rearranged for its shape is fine.`
15689
15802
  ] : []
15690
15803
  };
15691
15804
  }
@@ -15738,6 +15851,7 @@ function mostOfALine(chunk, copy) {
15738
15851
  });
15739
15852
  }
15740
15853
  var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
15854
+ var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
15741
15855
  var COPY_SIZE_SHARE = 0.8;
15742
15856
  function extraWordFindings(format, extra, boxes, copyBoxes) {
15743
15857
  if (extra.length === 0) return [];
@@ -15755,7 +15869,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15755
15869
  const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
15756
15870
  const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
15757
15871
  const asCopy = extra.filter((chunk) => !printed(chunk));
15758
- const inScene = extra.filter(printed);
15872
+ const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
15873
+ const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
15759
15874
  return [
15760
15875
  ...asCopy.length > 0 ? [
15761
15876
  {
@@ -15764,6 +15879,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15764
15879
  what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
15765
15880
  }
15766
15881
  ] : [],
15882
+ ...prices.length > 0 ? [
15883
+ {
15884
+ severity: "blocking",
15885
+ // `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
15886
+ what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
15887
+ }
15888
+ ] : [],
15767
15889
  ...inScene.length > 0 ? [
15768
15890
  {
15769
15891
  severity: "warning",
@@ -15828,7 +15950,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
15828
15950
  const element = claimedBoxes(f.what, boxes, buttons);
15829
15951
  if (element.length === 0) return f;
15830
15952
  const named = claimedEdges(f.what);
15831
- const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) <= AT_EDGE));
15953
+ const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
15954
+ const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
15832
15955
  return atEdge ? f : { ...f, severity: "warning" };
15833
15956
  });
15834
15957
  }
@@ -15836,9 +15959,11 @@ function claimedEdges(what) {
15836
15959
  const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
15837
15960
  return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
15838
15961
  }
15962
+ var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
15963
+ var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
15839
15964
  function claimedBoxes(what, boxes, buttons) {
15840
- if (/\b(?:logos?|wordmark|mark)\b/i.test(what)) return boxes.filter((b) => b.kind === "logo");
15841
- if (/\b(button|cta|call[- ]to[- ]action)\b/i.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15965
+ if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
15966
+ if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15842
15967
  return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
15843
15968
  }
15844
15969
  async function bytesOf(source) {
@@ -15968,6 +16093,22 @@ function everySizeChecks(opts) {
15968
16093
  onSpend: opts.onSpend
15969
16094
  };
15970
16095
  }
16096
+ async function confirmBlocking(findings, stills, formats, ask2) {
16097
+ const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
16098
+ if (claims.length === 0 || stills.length === 0) return [...findings];
16099
+ const prompt = [
16100
+ `These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
16101
+ "A reviewer reported each claim below. Look again at the size each one names (and, for a claim comparing sizes, at the others) and answer true only when the defect is plainly there for someone looking at it on a phone.",
16102
+ "Answer false when the thing is not there, when it is an ordinary part of the scene (a second house, a neighbour's roof, a reflection), or when the sizes it compares show the same thing.",
16103
+ ...claims.map((c, i) => `${i + 1}. ${c.what}`),
16104
+ `Reply with only this JSON: {"answers":[true|false, ...]}, one answer per claim, in order.`
16105
+ ].join("\n");
16106
+ const answer = await ask2([...stills], prompt).catch(() => null);
16107
+ const answers = answer?.answers;
16108
+ if (!Array.isArray(answers) || answers.length !== claims.length) return [...findings];
16109
+ const unconfirmed = new Set(claims.filter((_, i) => answers[i] === false));
16110
+ return findings.map((f) => unconfirmed.has(f) ? { ...f, severity: "warning" } : f);
16111
+ }
15971
16112
  async function reviewRenderedStatic(opts) {
15972
16113
  const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
15973
16114
  if (measured.length === 0) return null;
@@ -15976,6 +16117,12 @@ async function reviewRenderedStatic(opts) {
15976
16117
  const formats = measured.map((m) => m.format);
15977
16118
  const lines = opts.lines ?? opts.copy.map((text3) => ({ text: text3, lettering: "designed" }));
15978
16119
  const every = { lines, ...everySizeChecks(opts) };
16120
+ const ask2 = (measuredRead) => {
16121
+ const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, measuredRead, reference !== null));
16122
+ const shown = [...measuredRead ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
16123
+ return askAboutStills(shown, prompt, opts.onSpend);
16124
+ };
16125
+ const asking = ask2(true);
15979
16126
  const checks = await Promise.all(
15980
16127
  originals.map(
15981
16128
  (bytes, i) => checkStaticImage(bytes, {
@@ -15987,13 +16134,19 @@ async function reviewRenderedStatic(opts) {
15987
16134
  );
15988
16135
  const read = checks.every((c) => c.status === "checked");
15989
16136
  const measuredText = read ? checks.flatMap((c) => c.status === "checked" ? c.findings : []) : [];
15990
- const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, read, reference !== null));
15991
- const shown = [...read ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
15992
- const answer = await askAboutStills(shown, prompt, opts.onSpend) ?? await askAboutStills(shown, prompt, opts.onSpend);
16137
+ const first = read ? await asking : (void asking.catch(() => null), null);
16138
+ const answer = first ?? await ask2(read);
15993
16139
  if (answer === null) return unreviewed(shape, measuredText);
15994
16140
  const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
15995
16141
  const boxesByFormat = read ? Object.fromEntries(checks.flatMap((c, i) => c.status === "checked" ? [[formats[i] ?? "", c.boxes]] : [])) : {};
15996
- return [...shape, ...text2, ...corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines)];
16142
+ const judged = corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines);
16143
+ const confirmed = await confirmBlocking(
16144
+ judged,
16145
+ stills,
16146
+ formats,
16147
+ (images, prompt) => askAboutStills(images, prompt, opts.onSpend)
16148
+ );
16149
+ return [...shape, ...text2, ...confirmed];
15997
16150
  }
15998
16151
 
15999
16152
  // src/engine/nodes/local/staticNative.ts
@@ -16237,7 +16390,9 @@ function boxesToPlace(boxes, format, opts = {}) {
16237
16390
  const past = (box) => bandViolations([box], format, { placements })[0];
16238
16391
  const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
16239
16392
  const logoWords = opts.copy.filter((l) => roleKey2(l.role) === "logo").map((l) => l.text);
16240
- return shownBoxes(withWordmark(boxes, logoWords), opts.copy, (box) => !past(box)).filter((shown) => !shown.supporting || cut(shown.box)).map((shown) => shown.box);
16393
+ const read = withWordmark(boxes, logoWords);
16394
+ const buttons = new Set(opts.copy.filter(isButtonLine).flatMap((line) => boxesOfLine(line, read)));
16395
+ return shownBoxes(read, opts.copy, (box) => !past(box)).filter((shown) => !shown.supporting || cut(shown.box)).map((shown) => buttons.has(shown.box) ? buttonShape(shown.box) : shown.box);
16241
16396
  }
16242
16397
  function planSafeFit(boxes, format, opts = {}) {
16243
16398
  const area = blockingArea(format, opts.placements);
@@ -16341,6 +16496,9 @@ var MIN_EXTEND_SCALE = 0.6;
16341
16496
  var GREY_LEVEL = 128;
16342
16497
  var GREY = { r: GREY_LEVEL, g: GREY_LEVEL, b: GREY_LEVEL };
16343
16498
  var FEATHER = 0.012;
16499
+ var KEEP_PAD = 0.3;
16500
+ var KEEP_FEATHER = 0.03;
16501
+ var MOVED = 120;
16344
16502
  function ratioOf2(ratio) {
16345
16503
  const [w = 1, h = 1] = ratio.split(":").map(Number);
16346
16504
  return w / h;
@@ -16403,14 +16561,16 @@ function clearBusyMargins(rgba, frame, keep) {
16403
16561
  }
16404
16562
  return shown;
16405
16563
  }
16406
- async function mergeExtended(extended, placed, ratio) {
16564
+ async function mergeExtended(extended, placed, ratio, keep = []) {
16407
16565
  const ad = await sharp7(placed).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
16408
16566
  const { width, height } = ad.info;
16409
16567
  const bounds = placedBounds(ad.data, width, height);
16410
16568
  if (!bounds) return sharp7(placed).removeAlpha().png().toBuffer();
16411
16569
  const canvas = renderCanvas({ width, height }, ratio);
16412
16570
  const paint = await sharp7(extended).removeAlpha().resize(canvas.width, canvas.height, { fit: "fill" }).extract({ left: canvas.left, top: canvas.top, width, height }).raw().toBuffer();
16413
- const weight = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
16571
+ const edge = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
16572
+ const boxes = keep.length > 0 && keptInPlace(keep, ad.data, paint, width, height) ? keptWeights(keep, width, height) : null;
16573
+ const weight = boxes ? (x, y) => Math.min(edge(x, y), boxes[y * width + x] ?? 0) : edge;
16414
16574
  const out = Buffer.alloc(width * height * 3);
16415
16575
  for (let y = 0; y < height; y++) {
16416
16576
  for (let x = 0; x < width; x++) {
@@ -16449,18 +16609,65 @@ function edgeWeight(b, width, height, feather) {
16449
16609
  ramp(b.y1 - y, b.y1 === height - 1)
16450
16610
  );
16451
16611
  }
16612
+ function keptWeights(keep, width, height) {
16613
+ const out = new Float32Array(width * height);
16614
+ const feather = Math.max(1, KEEP_FEATHER * width);
16615
+ for (const box of keep) {
16616
+ const pad = KEEP_PAD * (box.y1 - box.y0) * height;
16617
+ const x0 = box.x0 * width - pad;
16618
+ const x1 = box.x1 * width + pad;
16619
+ const y0 = box.y0 * height - pad;
16620
+ const y1 = box.y1 * height + pad;
16621
+ const ys = Math.max(0, Math.floor(y0 - feather));
16622
+ const ye = Math.min(height - 1, Math.ceil(y1 + feather));
16623
+ const xs = Math.max(0, Math.floor(x0 - feather));
16624
+ const xe = Math.min(width - 1, Math.ceil(x1 + feather));
16625
+ for (let y = ys; y <= ye; y++) {
16626
+ const dy = Math.max(0, y0 - y, y - y1);
16627
+ for (let x = xs; x <= xe; x++) {
16628
+ const dx = Math.max(0, x0 - x, x - x1);
16629
+ const t = Math.max(0, 1 - Math.hypot(dx, dy) / feather);
16630
+ const w = t * t * (3 - 2 * t);
16631
+ const i = y * width + x;
16632
+ if (w > (out[i] ?? 0)) out[i] = w;
16633
+ }
16634
+ }
16635
+ }
16636
+ return out;
16637
+ }
16638
+ function keptInPlace(keep, ad, paint, width, height) {
16639
+ const diffs = keep.map((box) => boxDiff(box, ad, paint, width, height)).filter((d) => d !== null).sort((a, b) => a - b);
16640
+ return (diffs[Math.floor(diffs.length / 2)] ?? Number.POSITIVE_INFINITY) <= MOVED;
16641
+ }
16642
+ function boxDiff(box, ad, paint, width, height) {
16643
+ let sum = 0;
16644
+ let n = 0;
16645
+ const xs = Math.floor(box.x0 * width);
16646
+ const xe = Math.min(width - 1, Math.ceil(box.x1 * width));
16647
+ for (let y = Math.floor(box.y0 * height); y <= Math.min(height - 1, Math.ceil(box.y1 * height)); y++) {
16648
+ for (let x = xs; x <= xe; x++) {
16649
+ const i = y * width + x;
16650
+ if ((ad[i * 4 + 3] ?? 0) === 0) continue;
16651
+ for (let c = 0; c < 3; c++) sum += Math.abs((ad[i * 4 + c] ?? 0) - (paint[i * 3 + c] ?? 0));
16652
+ n++;
16653
+ }
16654
+ }
16655
+ return n > 0 ? sum / n : null;
16656
+ }
16452
16657
 
16453
16658
  // src/engine/nodes/local/staticPlace.ts
16454
16659
  var Usage = z25.object({ model: z25.string(), inputTokens: z25.number(), outputTokens: z25.number(), costUsd: z25.number() }).strict();
16455
16660
  var Fit = z25.object({ scale: z25.number(), x: z25.number(), y: z25.number() }).strict();
16661
+ var Unit = z25.number().min(0).max(1);
16662
+ var Span = z25.object({ x0: Unit, y0: Unit, x1: Unit, y1: Unit }).strict();
16456
16663
  var RenderRatio = z25.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
16457
16664
  var Copy = z25.array(z25.object({ text: z25.string().min(1), role: z25.string().optional() }).strict());
16458
16665
  var staticPlaceNode = defineNode({
16459
16666
  id: "static_place",
16460
- version: "1.0.0",
16667
+ version: "1.1.0",
16461
16668
  category: "image",
16462
16669
  location: "local",
16463
- summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
16670
+ summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint; `keep` is where every line, the logo and the buttons read sit in `placed`, all the merge lays back of the ad's own pixels. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
16464
16671
  when_to_use: "Added by a static ad's autofix round (place-and-extend) before an `image_generate` that paints the margins and a `static_extend_merge`; never scaffolded. One box-reader call (about $0.005).",
16465
16672
  inputs: z25.object({ image: ImageRef }).strict(),
16466
16673
  params: z25.object({
@@ -16473,6 +16680,8 @@ var staticPlaceNode = defineNode({
16473
16680
  placed: ImageRef,
16474
16681
  shown: ImageRef,
16475
16682
  fit: Fit.nullable(),
16683
+ /** Where the words, logo and buttons read sit in `placed`: all the merge lays back of the ad's own pixels. */
16684
+ keep: z25.array(Span).optional(),
16476
16685
  reason: z25.enum(["not_read", "inside_band", "too_far"]).optional(),
16477
16686
  usage: z25.array(Usage).optional()
16478
16687
  }).strict(),
@@ -16515,18 +16724,28 @@ var staticPlaceNode = defineNode({
16515
16724
  placed: placedRef,
16516
16725
  shown: shownRef,
16517
16726
  fit,
16727
+ keep: keptBoxes([...reading.boxes, ...boxes.filter((b) => !reading.boxes.includes(b))], fit),
16518
16728
  ...spend
16519
16729
  };
16520
16730
  }
16521
16731
  });
16732
+ function keptBoxes(boxes, fit) {
16733
+ const at2 = (offset2, v) => Math.min(1, Math.max(0, offset2 + fit.scale * v));
16734
+ return boxes.map((b) => ({ x0: at2(fit.x, b.x0), y0: at2(fit.y, b.y0), x1: at2(fit.x, b.x1), y1: at2(fit.y, b.y1) }));
16735
+ }
16522
16736
  var staticExtendMergeNode = defineNode({
16523
16737
  id: "static_extend_merge",
16524
- version: "1.1.0",
16738
+ version: "1.2.0",
16525
16739
  category: "image",
16526
16740
  location: "local",
16527
- summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad (`placed`) back over the painting: the ad's own pixels where it sits, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
16741
+ summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad's own pixels back over the painting only in `keep` (`static_place`'s boxes of the words, logo and buttons, padded and feathered): the painting is the rest of the frame, one picture with no edge where the shrunk ad was. Without `keep`, or when the painting moved the words, the whole ad (`placed`) is laid back, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
16528
16742
  when_to_use: "Added by a static ad's autofix round after a `static_place`; never scaffolded. One picture at the painter's price, only for a size that was placed.",
16529
- inputs: z25.object({ placed: ImageRef, shown: ImageRef.optional(), image: ImageRef.optional() }).strict(),
16743
+ inputs: z25.object({
16744
+ placed: ImageRef,
16745
+ shown: ImageRef.optional(),
16746
+ image: ImageRef.optional(),
16747
+ keep: z25.array(Span).optional()
16748
+ }).strict(),
16530
16749
  params: z25.object({ render_ratio: RenderRatio, paint: z25.record(z25.string(), z25.unknown()).optional() }).strict(),
16531
16750
  outputs: z25.object({ image: ImageRef }).strict(),
16532
16751
  outputKinds: { image: "image" },
@@ -16536,7 +16755,7 @@ var staticExtendMergeNode = defineNode({
16536
16755
  const painted = painting?.path ?? painting?.url;
16537
16756
  const placed = inputs.placed.path ?? inputs.placed.url;
16538
16757
  if (!painted || !placed) return { image: inputs.placed };
16539
- const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio);
16758
+ const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio, inputs.keep);
16540
16759
  const image = await ctx.assets.ingestBytes({ bytes: merged, kind: "image", mime: "image/png" });
16541
16760
  return { image };
16542
16761
  }
@@ -16565,12 +16784,64 @@ import { z as z26 } from "zod";
16565
16784
  // src/engine/nodes/local/lib/variant-prefilter.ts
16566
16785
  import { createHash as createHash3 } from "crypto";
16567
16786
  import sharp8 from "sharp";
16568
- function coverFitBoxes(boxes, from, to) {
16787
+ function coverOf(from, to) {
16569
16788
  const scale = Math.max(to.width / from.width, to.height / from.height);
16570
- const offX = (from.width * scale - to.width) / 2;
16571
- const offY = (from.height * scale - to.height) / 2;
16572
- const x = (f) => (f * from.width * scale - offX) / to.width;
16573
- const y = (f) => (f * from.height * scale - offY) / to.height;
16789
+ const width = from.width * scale;
16790
+ const height = from.height * scale;
16791
+ return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
16792
+ }
16793
+ function framingOf(from, to) {
16794
+ let frame;
16795
+ return {
16796
+ place: (boxes) => {
16797
+ frame = frameKeeping(boxes, from, to);
16798
+ return coverFitBoxes(boxes, from, to, frame);
16799
+ },
16800
+ moved: () => {
16801
+ const { centre } = coverOf(from, to);
16802
+ return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
16803
+ }
16804
+ };
16805
+ }
16806
+ async function cutToFrame(bytes, frame, to) {
16807
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16808
+ const { scale } = coverOf({ width, height }, to);
16809
+ const w = Math.min(width, Math.round(to.width / scale));
16810
+ const h = Math.min(height, Math.round(to.height / scale));
16811
+ const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
16812
+ const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
16813
+ return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
16814
+ }
16815
+ var FRAME_MARGIN = 0.02;
16816
+ function frameKeeping(boxes, from, to) {
16817
+ const cover = coverOf(from, to);
16818
+ if (boxes.length === 0) return cover.centre;
16819
+ const along = (offset2, extent, frame, lo, hi) => {
16820
+ const low = lo * extent;
16821
+ const high = hi * extent;
16822
+ const room = extent - frame;
16823
+ const margin = FRAME_MARGIN * frame;
16824
+ for (const m of [margin, 0]) {
16825
+ const min = Math.max(0, high + m - frame);
16826
+ const max = Math.min(room, low - m);
16827
+ if (min <= max) return Math.min(max, Math.max(min, offset2));
16828
+ }
16829
+ return offset2;
16830
+ };
16831
+ const x0 = Math.min(...boxes.map((b) => b.x0));
16832
+ const x1 = Math.max(...boxes.map((b) => b.x1));
16833
+ const y0 = Math.min(...boxes.map((b) => b.y0));
16834
+ const y1 = Math.max(...boxes.map((b) => b.y1));
16835
+ return {
16836
+ left: along(cover.centre.left, cover.width, to.width, x0, x1),
16837
+ top: along(cover.centre.top, cover.height, to.height, y0, y1)
16838
+ };
16839
+ }
16840
+ function coverFitBoxes(boxes, from, to, frame) {
16841
+ const cover = coverOf(from, to);
16842
+ const { left: offX, top: offY } = frame ?? cover.centre;
16843
+ const x = (f) => (f * cover.width - offX) / to.width;
16844
+ const y = (f) => (f * cover.height - offY) / to.height;
16574
16845
  return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
16575
16846
  }
16576
16847
  var NO_LOGO = "The brand's logo was not found on it.";
@@ -16624,34 +16895,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
16624
16895
  });
16625
16896
  const format = plan.master.format;
16626
16897
  const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
16898
+ const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16627
16899
  const checked = await Promise.all(
16628
- variants.map(async (variant) => {
16629
- const source = variant.path ?? variant.url;
16630
- if (!source) return { index: variant.index, blocking: null };
16631
- try {
16632
- const bytes = await bytesOf(source);
16633
- const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16634
- const check = await checkStaticImage(bytes, {
16635
- format,
16636
- lines,
16637
- allowed: brand ? [brand] : [],
16638
- ...grade,
16639
- toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
16640
- ...opts.read ? { read: opts.read } : {},
16641
- ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16642
- });
16643
- if (check.status === "not_checked") return { index: variant.index, blocking: null };
16644
- const prefix = `${format}: `;
16645
- const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16646
- const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16647
- return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
16648
- } catch {
16649
- return { index: variant.index, blocking: null };
16650
- }
16651
- })
16900
+ variants.map(
16901
+ (variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
16902
+ )
16652
16903
  );
16653
16904
  const outcome = prefilterOutcome(checked);
16654
- return { ...outcome, kept: seededOrder(outcome.kept, variants) };
16905
+ const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
16906
+ return {
16907
+ ...outcome,
16908
+ kept: seededOrder(outcome.kept, variants),
16909
+ ...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
16910
+ };
16911
+ }
16912
+ async function checkVariant(variant, c) {
16913
+ const { format, lines, brand, grade, want, opts } = c;
16914
+ const source = variant.path ?? variant.url;
16915
+ if (!source) return { index: variant.index, blocking: null };
16916
+ try {
16917
+ const bytes = await bytesOf(source);
16918
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16919
+ const framing = framingOf({ width, height }, c.size);
16920
+ const check = await checkStaticImage(bytes, {
16921
+ format,
16922
+ lines,
16923
+ allowed: brand ? [brand] : [],
16924
+ ...grade,
16925
+ toFrame: framing.place,
16926
+ ...opts.read ? { read: opts.read } : {},
16927
+ ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16928
+ });
16929
+ if (check.status === "not_checked") return { index: variant.index, blocking: null };
16930
+ const prefix = `${format}: `;
16931
+ const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16932
+ const frame = framing.moved();
16933
+ return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
16934
+ } catch {
16935
+ return { index: variant.index, blocking: null };
16936
+ }
16655
16937
  }
16656
16938
  function seededOrder(indexes, variants) {
16657
16939
  const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
@@ -16727,7 +17009,7 @@ async function readSpec(ref) {
16727
17009
  }
16728
17010
  var staticPrefilterNode = defineNode({
16729
17011
  id: "static_prefilter",
16730
- version: "1.0.0",
17012
+ version: "1.1.0",
16731
17013
  category: "image",
16732
17014
  location: "local",
16733
17015
  // Reads each variant's pixels itself, from its file or its durable URL, so the variants
@@ -16760,7 +17042,8 @@ var staticPrefilterNode = defineNode({
16760
17042
  images: z26.array(ImageRef).min(1),
16761
17043
  // Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
16762
17044
  order: z26.array(z26.number().int().min(0)).optional(),
16763
- // The picture every size is made from: the checks' first, or on a tie the judge's.
17045
+ // The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
17046
+ // master's frame where every word stays whole when its centre would cut one, else as rendered.
16764
17047
  image: ImageRef.optional(),
16765
17048
  // Which variant it is (its place among the inputs).
16766
17049
  chosen: z26.number().int().min(0).optional(),
@@ -16773,12 +17056,12 @@ var staticPrefilterNode = defineNode({
16773
17056
  }).strict(),
16774
17057
  outputKinds: { images: "image", image: "image" },
16775
17058
  cost: () => ({ credits: 0, seconds_estimate: 1 }),
16776
- async execute({ inputs, params }) {
16777
- const variants = inputs.images.map((image, index) => ({
17059
+ async execute({ inputs, params, ctx }) {
17060
+ const variants = inputs.images.map((image2, index) => ({
16778
17061
  index,
16779
- sha256: image.sha256,
16780
- ...image.path ? { path: image.path } : {},
16781
- ...image.url ? { url: image.url } : {}
17062
+ sha256: image2.sha256,
17063
+ ...image2.path ? { path: image2.path } : {},
17064
+ ...image2.url ? { url: image2.url } : {}
16782
17065
  }));
16783
17066
  const usage = [];
16784
17067
  const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
@@ -16794,10 +17077,11 @@ var staticPrefilterNode = defineNode({
16794
17077
  { model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
16795
17078
  ) : null;
16796
17079
  const chosen = tie?.chosen ?? shown[0] ?? 0;
17080
+ const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
16797
17081
  return {
16798
17082
  images: kept.length > 0 ? kept : inputs.images,
16799
17083
  order: shown,
16800
- ...inputs.images[chosen] ? { image: inputs.images[chosen] } : {},
17084
+ ...image ? { image } : {},
16801
17085
  chosen,
16802
17086
  ...tie ? { reasoning: tie.reasoning } : {},
16803
17087
  dropped: result.dropped,
@@ -16806,6 +17090,12 @@ var staticPrefilterNode = defineNode({
16806
17090
  };
16807
17091
  }
16808
17092
  });
17093
+ async function framed(picture, frame, size2, ctx) {
17094
+ const source = picture?.path ?? picture?.url;
17095
+ if (!picture || !frame || !size2 || !source) return picture;
17096
+ const bytes = await cutToFrame(await bytesOf(source), frame, size2);
17097
+ return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
17098
+ }
16809
17099
 
16810
17100
  // src/engine/nodes/local/staticSafeFit.ts
16811
17101
  import { z as z27 } from "zod";
@@ -18385,6 +18675,7 @@ export {
18385
18675
  AD_DESCRIBE_MODEL,
18386
18676
  BLUEPRINT_ANALYSIS_MODEL,
18387
18677
  VIEWER_CHECK_MODEL,
18678
+ CUSTOMER_INSIGHTS_MODEL,
18388
18679
  GLANCE_CHECK_MODEL,
18389
18680
  VIDEO_DECONSTRUCT_MODEL,
18390
18681
  VOICEOVER_MODEL,
@@ -18456,6 +18747,10 @@ export {
18456
18747
  isAdSpec,
18457
18748
  conceptSummary,
18458
18749
  conceptProblems,
18750
+ unwrittenProblems,
18751
+ sceneWordProblems,
18752
+ SPEC_SHAPES_HINT,
18753
+ adSpecErrorText,
18459
18754
  readAdSpec,
18460
18755
  withRenderedLettering,
18461
18756
  conceptClassOf,
@@ -18482,6 +18777,7 @@ export {
18482
18777
  STATIC_SAFE_FIT_ROLE,
18483
18778
  STATIC_PLACE_ROLE,
18484
18779
  LOGO_COMPOSITE_ROLE,
18780
+ SERIES_LEAD_ROLE,
18485
18781
  STATIC_FIX_ROLE,
18486
18782
  LOGO_COMPOSITE_DEFAULT,
18487
18783
  nodesWithRole,
@@ -18543,4 +18839,4 @@ export {
18543
18839
  defaultRegistry,
18544
18840
  createEngineFromEnv
18545
18841
  };
18546
- //# sourceMappingURL=chunk-7BQIIJLV.js.map
18842
+ //# sourceMappingURL=chunk-O5HQRXS6.js.map