@koda-sl/baker-cli 0.433.0 → 0.434.0-dev.0af4c01ba

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 +41 -15
  2. package/dist/{chunk-X46KRGZU.js → chunk-7THIX4WW.js} +3 -3
  3. package/dist/{chunk-7BQIIJLV.js → chunk-EFRX2BLB.js} +362 -147
  4. package/dist/chunk-EFRX2BLB.js.map +1 -0
  5. package/dist/{chunk-LDS7LUKK.js → chunk-EURJVJQY.js} +2 -2
  6. package/dist/{chunk-SPPJRGQF.js → chunk-F7LXMR73.js} +5 -1
  7. package/dist/chunk-F7LXMR73.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 +6715 -3896
  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 +3 -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) {
@@ -4992,10 +4982,10 @@ var GPT_IMAGE_BRIEF = {
4992
4982
  // (.context/static-review/BENCH.md#E3).
4993
4983
  // quality max: the deterministic checks tie (shippable 9-12% on every step, n=100), but on the
4994
4984
  // 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" }
4985
+ // and the judge's on the same pairs 73%, 65%, 39%, 31%. Max costs $0.166 per image vs $0.085
4986
+ // for xhigh and 80 s vs 44 s (BENCH.md#E4, § "E1 v2"); a round renders twice in a row (the
4987
+ // options, then each size), so three ads took most of an hour. Oct 2026 the user chose xhigh.
4988
+ params: { n: 1, quality: "xhigh", imageSize: "2K" }
4999
4989
  };
5000
4990
  var GEMINI_IMAGE_BRIEF = {
5001
4991
  // "Use Natural Language & Full Sentences… as if you were briefing a human artist", and
@@ -6699,6 +6689,7 @@ function textFidelityDirective(strings2, hasFont) {
6699
6689
  return [
6700
6690
  exact,
6701
6691
  "Render NO other words anywhere in the image \u2014 no invented labels, captions, sub-headers, or filler text.",
6692
+ "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
6693
  "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
6694
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
6704
6695
  ].join("\n");
@@ -6763,22 +6754,32 @@ var Viewer = z7.object({
6763
6754
  /** Who will see the ad, in the words a marketer would use. */
6764
6755
  who: z7.string().trim().min(1),
6765
6756
  market: Market.optional(),
6766
- /** What they already know about the brand and the offer. */
6767
- knows: z7.string().optional(),
6757
+ /** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
6758
+ knows: z7.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z7.string().optional()),
6768
6759
  temperature: z7.enum(["cold", "warm", "hot"]).optional(),
6769
6760
  /** Where they meet it (feed, Stories, Reels). */
6770
6761
  placement: z7.string().optional()
6771
6762
  }).strict();
6772
- var Scene = z7.object({
6763
+ var withoutFormatName = (v) => {
6764
+ if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
6765
+ return v;
6766
+ const { concept: _format, ...scene } = v;
6767
+ return scene;
6768
+ };
6769
+ var SceneFields = z7.object({
6773
6770
  description: z7.string().trim().min(1),
6774
- /** A photograph, a designed graphic, or an illustration. */
6775
- look: z7.enum(["photo", "designed", "illustration"]),
6771
+ /** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
6772
+ look: z7.preprocess(
6773
+ (v) => typeof v !== "string" ? v : /^photo/i.test(v.trim()) ? "photo" : /^illustrat/i.test(v.trim()) ? "illustration" : /^(design|graphic)/i.test(v.trim()) ? "designed" : v,
6774
+ z7.enum(["photo", "designed", "illustration"])
6775
+ ),
6776
6776
  /** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
6777
6777
  capture: z7.string().optional(),
6778
6778
  mood: z7.string().optional(),
6779
6779
  /** How the frame is arranged, by what fills each part of it. */
6780
6780
  layout: z7.string().optional()
6781
6781
  }).strict();
6782
+ var Scene = z7.preprocess(withoutFormatName, SceneFields);
6782
6783
  var Brand = z7.object({
6783
6784
  name: z7.string().optional(),
6784
6785
  /** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
@@ -6850,13 +6851,19 @@ var AdSpecSchema = z7.object({
6850
6851
  * editor's line numbers, a product's own label, a platform's badge. The render may draw them
6851
6852
  * and the review never calls them extra words.
6852
6853
  */
6853
- scene_text: z7.array(z7.string().trim().min(1)).default([]),
6854
+ scene_text: z7.preprocess(
6855
+ (v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
6856
+ z7.array(z7.string().trim().min(1)).default([])
6857
+ ),
6854
6858
  /** The formats the ad is made for; the canvas delivers them. */
6855
6859
  formats: z7.array(z7.string()).optional(),
6856
6860
  /** An optional note on direction for this concept. */
6857
6861
  direction: z7.string().optional(),
6858
6862
  /** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
6859
- mechanism: z7.array(Mechanism).default([]),
6863
+ mechanism: z7.preprocess((v) => {
6864
+ const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
6865
+ return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
6866
+ }, z7.array(Mechanism).default([])),
6860
6867
  /** Why the reference won and what this ad keeps of it (schema /3). */
6861
6868
  concept: Concept.optional(),
6862
6869
  /**
@@ -6912,6 +6919,61 @@ function conceptProblems(spec) {
6912
6919
  }
6913
6920
  return problems;
6914
6921
  }
6922
+ var PLACEHOLDER = "[TODO";
6923
+ function unwrittenProblems(spec) {
6924
+ const problems = [];
6925
+ if (spec.scene.description.includes(PLACEHOLDER)) {
6926
+ problems.push("The scene is still a placeholder: write what the viewer sees in `scene.description`.");
6927
+ }
6928
+ const lines = spec.copy.filter((line) => line.text.includes(PLACEHOLDER));
6929
+ if (lines.length > 0) {
6930
+ problems.push(`${lines.length} line(s) of copy are still placeholders: write each as it prints, or drop it.`);
6931
+ }
6932
+ return problems;
6933
+ }
6934
+ var QUOTED = /"([^"]{2,})"|“([^”]{2,})”|(?<![\p{L}\p{N}])['‘]([^'’]{2,}?)['’](?![\p{L}\p{N}])/gu;
6935
+ var FIGURE = /[£$€]\s?\d[\d.,]*|\d+(?:[.,]\d+)?\s?%/g;
6936
+ var comparable = (text2) => text2.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}£$€%]+/gu, " ").trim();
6937
+ function sceneWordProblems(spec) {
6938
+ const held = [...spec.copy.map((l) => l.text), ...spec.scene_text].map(comparable);
6939
+ const holds = (text2) => {
6940
+ const want = comparable(text2);
6941
+ return want.length === 0 || held.some((h) => h.includes(want));
6942
+ };
6943
+ const scene = [spec.scene.description, spec.scene.layout ?? ""].join(" ");
6944
+ const quoted = [...scene.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter((q) => !holds(q));
6945
+ const figures = (scene.match(FIGURE) ?? []).map((f) => f.trim()).filter((f) => !holds(f));
6946
+ const unheld = [.../* @__PURE__ */ new Set([...quoted, ...figures])];
6947
+ if (unheld.length === 0) return [];
6948
+ return [
6949
+ `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).`
6950
+ ];
6951
+ }
6952
+ var FIELD_SHAPES = {
6953
+ copy: '"copy": [{"text": "Book your free survey", "lettering": "designed", "role": "cta"}]',
6954
+ scene: '"scene": {"description": "a family on a sunny roof terrace", "look": "photo"}',
6955
+ colors: '"colors": [{"object": "the button", "name": "deep green", "hex": "#1F5C3A"}]',
6956
+ references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
6957
+ third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
6958
+ mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
6959
+ viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
6960
+ };
6961
+ var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
6962
+ function adSpecErrorText(e) {
6963
+ if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
6964
+ const items = /* @__PURE__ */ new Map();
6965
+ for (const issue of e.issues) {
6966
+ const [field2, index, key] = issue.path;
6967
+ const at2 = typeof index === "number" ? `${String(field2)}[${index}]` : issue.path.map(String).join(".") || "the spec";
6968
+ 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: /, "");
6969
+ items.set(at2, [...items.get(at2) ?? [], what]);
6970
+ }
6971
+ const fields = [...new Set(e.issues.map((i) => String(i.path[0] ?? "")))].filter((f) => FIELD_SHAPES[f]);
6972
+ return [
6973
+ ...[...items].map(([at2, whats]) => `${at2}: ${[...new Set(whats)].join("; ")}`),
6974
+ ...fields.map((f) => `Shape: ${FIELD_SHAPES[f]}`)
6975
+ ].join("\n");
6976
+ }
6915
6977
  function readAdSpec(raw) {
6916
6978
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6917
6979
  const advertiser = raw?.reference?.advertiser;
@@ -8364,14 +8426,16 @@ function scaleText(plan, spec, flowing, zoned, short) {
8364
8426
  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
8427
  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
8428
  }
8367
- var ZONE_HEADROOM = 0;
8429
+ var COVERED_HEADROOM = { top: 0.06, bottom: 0.08, side: 0.02 };
8430
+ var NO_HEADROOM = { top: 0, bottom: 0, side: 0 };
8368
8431
  var pct = (share) => `${Math.round(share * 100)}%`;
8369
8432
  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
8433
  const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
8434
+ const room = isTall(plan) && covered ? COVERED_HEADROOM : NO_HEADROOM;
8435
+ const top = plan.safe.top + room.top;
8436
+ const bottom = plan.safe.bottom + room.bottom;
8437
+ const side = Math.max(plan.safe.left, plan.safe.right) + room.side;
8438
+ const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
8375
8439
  if (short && !flowing) {
8376
8440
  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
8441
  return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
@@ -8380,7 +8444,7 @@ function zonesText(plan, flowing, ground, short) {
8380
8444
  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
8445
  }
8382
8446
  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.`;
8447
+ 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
8448
  }
8385
8449
  function adaptBrief(spec, profile, opts) {
8386
8450
  const { plan, images } = opts;
@@ -8655,7 +8719,7 @@ function sizeNode(s, adaptFormats) {
8655
8719
  const index = adaptFormats.indexOf(s.adaptFormat ?? s.format);
8656
8720
  return magick(id, STATIC_SIZE_ROLE, `$ref:adapt.images#${index}`, fillArgs(s.size));
8657
8721
  }
8658
- var RELAYOUT_QUALITY = "max";
8722
+ var RELAYOUT_QUALITY = "xhigh";
8659
8723
  function relayoutNodes(g) {
8660
8724
  return g.plan.sizes.filter((s) => s.from === "relayout").map((s) => relayoutNode(s, g));
8661
8725
  }
@@ -10117,7 +10181,7 @@ function checkOrphanNodes(ctx) {
10117
10181
  path: `nodes[${ctx.idToIndex.get(n.id)}]`,
10118
10182
  code: STAGE_CODES.ORPHAN_NODE,
10119
10183
  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`
10184
+ 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
10185
  });
10122
10186
  }
10123
10187
  }
@@ -10188,6 +10252,17 @@ function unwrap(schema) {
10188
10252
  }
10189
10253
 
10190
10254
  // src/engine/engine/executor.ts
10255
+ function throwRunOutcome(signal, failures, held) {
10256
+ if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10257
+ if (failures.length === 1 && failures[0]) throw failures[0].reason;
10258
+ if (failures.length > 1) throw new LayerExecutionError([...failures]);
10259
+ if (held.length > 0) {
10260
+ throw new RunAbortedError(
10261
+ "checkpoint",
10262
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10263
+ );
10264
+ }
10265
+ }
10191
10266
  function heldByCheckpoint(canvas, layer, kinds) {
10192
10267
  if (!kinds?.size) return [];
10193
10268
  return layer.filter((id) => {
@@ -10276,45 +10351,63 @@ var Engine = class {
10276
10351
  return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
10277
10352
  }
10278
10353
  async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
10279
- const layers = topologicalLayers(graph);
10354
+ const order = topologicalLayers(graph).flat();
10280
10355
  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) });
10356
+ if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
10357
+ const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
10358
+ const running = /* @__PURE__ */ new Map();
10359
+ const failures = [];
10360
+ const held = [];
10361
+ const dispatch = (nodeId) => {
10362
+ waitingOn.delete(nodeId);
10363
+ this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10364
+ const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
10365
+ (r) => {
10366
+ this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
10367
+ for (const deps of waitingOn.values()) deps.delete(nodeId);
10368
+ },
10369
+ (reason) => {
10370
+ failures.push({ nodeId, reason });
10371
+ this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
10312
10372
  }
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);
10373
+ );
10374
+ running.set(
10375
+ nodeId,
10376
+ settled.finally(() => running.delete(nodeId))
10377
+ );
10378
+ };
10379
+ const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
10380
+ for (; ; ) {
10381
+ const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
10382
+ for (const nodeId of ready) {
10383
+ if (running.size >= limit) break;
10384
+ if (isHeld(nodeId)) {
10385
+ waitingOn.delete(nodeId);
10386
+ held.push(nodeId);
10387
+ } else dispatch(nodeId);
10388
+ }
10389
+ if (running.size === 0) break;
10390
+ await Promise.race(running.values());
10317
10391
  }
10392
+ const rank = new Map(order.map((id, i) => [id, i]));
10393
+ failures.sort((a, b) => (rank.get(a.nodeId) ?? 0) - (rank.get(b.nodeId) ?? 0));
10394
+ throwRunOutcome(opts.signal, failures, held);
10395
+ }
10396
+ /** A settled node counted, recorded and announced. */
10397
+ recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
10398
+ if (r.cached) counters.cachedNodes++;
10399
+ counters.totalCredits += r.credits;
10400
+ const node = canvas.nodes.find((n) => n.id === nodeId);
10401
+ if (!node) return;
10402
+ const run = {
10403
+ node_id: nodeId,
10404
+ node_type: node.type,
10405
+ cached: r.cached,
10406
+ duration_ms: r.durationMs,
10407
+ credits: r.credits
10408
+ };
10409
+ nodeRuns.push(run);
10410
+ this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10318
10411
  }
10319
10412
  /** Progress consumers are observers only — an exception there must never fail the run. */
10320
10413
  emitProgress(opts, event) {
@@ -10406,20 +10499,6 @@ var Engine = class {
10406
10499
  this.logOutputWarnings(node.id, outputsObj);
10407
10500
  return { cached: false, credits, durationMs: elapsed };
10408
10501
  }
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
10502
  /** A cache hit replayed as this node's result: its outputs written, and free. */
10424
10503
  async replayCached(node, cacheKey, outputs, writer) {
10425
10504
  const cacheT0 = Date.now();
@@ -14584,6 +14663,18 @@ function boxesOfLine(line, boxes) {
14584
14663
  return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
14585
14664
  });
14586
14665
  }
14666
+ var BUTTON_PAD = 0.7;
14667
+ var isButtonLine = (line) => /^(cta|button|call_to_action)$/.test(roleKey2(line.role));
14668
+ function buttonShape(box) {
14669
+ const pad = BUTTON_PAD * (box.y1 - box.y0);
14670
+ return {
14671
+ ...box,
14672
+ x0: Math.max(0, box.x0 - pad),
14673
+ y0: Math.max(0, box.y0 - pad),
14674
+ x1: Math.min(1, box.x1 + pad),
14675
+ y1: Math.min(1, box.y1 + pad)
14676
+ };
14677
+ }
14587
14678
  function shownBoxes(boxes, lines, clear) {
14588
14679
  const linesOfBox = /* @__PURE__ */ new Map();
14589
14680
  for (const line of lines) {
@@ -14967,7 +15058,7 @@ async function videoDuration(video) {
14967
15058
  return null;
14968
15059
  }
14969
15060
  }
14970
- async function askGateway(model, parts, timeoutMs, spent) {
15061
+ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
14971
15062
  const key = openRouterKey();
14972
15063
  if (!key) return null;
14973
15064
  try {
@@ -14977,7 +15068,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
14977
15068
  body: JSON.stringify({
14978
15069
  model,
14979
15070
  messages: [{ role: "user", content: parts }],
14980
- usage: { include: true }
15071
+ usage: { include: true },
15072
+ ...reasoning ? { reasoning } : {}
14981
15073
  }),
14982
15074
  signal: AbortSignal.timeout(timeoutMs)
14983
15075
  });
@@ -15026,7 +15118,7 @@ async function askAboutStills(images, prompt, onSpend) {
15026
15118
  if (!hasOpenRouterKey()) return null;
15027
15119
  return await ask(images, prompt, (usage) => onSpend?.(usage));
15028
15120
  }
15029
- async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15121
+ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
15030
15122
  if (!hasOpenRouterKey()) return null;
15031
15123
  return await askGateway(
15032
15124
  model,
@@ -15037,7 +15129,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15037
15129
  )
15038
15130
  ],
15039
15131
  timeoutMs,
15040
- (usage) => onSpend?.(usage)
15132
+ (usage) => onSpend?.(usage),
15133
+ reasoning
15041
15134
  );
15042
15135
  }
15043
15136
  async function askAboutText(model, prompt, onSpend) {
@@ -15201,7 +15294,9 @@ async function readTextBoxes(image, opts = {}) {
15201
15294
  return { status: "not_checked" };
15202
15295
  }
15203
15296
  for (let attempt = 0; attempt < 2; attempt++) {
15204
- const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend);
15297
+ const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend, 6e4, {
15298
+ effort: "minimal"
15299
+ });
15205
15300
  const boxes = textBoxesFromReader(answer);
15206
15301
  if (boxes) return { status: "read", boxes };
15207
15302
  }
@@ -15291,6 +15386,8 @@ var StaticFinding = z21.object({
15291
15386
  });
15292
15387
  var ABSENCE_NEVER_A_FINDING = /\b(?:missing|no|lacks?|without)\s+(?:an?\s+|any\s+|clear\s+|visible\s+)?(?:call to action|cta)\b|\b(?:does not|doesn't) (?:show|tell) the viewer what to do/i;
15293
15388
  var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
15389
+ var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
15390
+ var BLURRED_FIGURES = /\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b(?=[^.]*\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b)|\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b(?=[^.]*\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b)/i;
15294
15391
  function classifyStaticFindings(json) {
15295
15392
  const findings = json?.findings;
15296
15393
  if (!Array.isArray(findings)) return [];
@@ -15299,7 +15396,7 @@ function classifyStaticFindings(json) {
15299
15396
  if (!parsed.success) return [];
15300
15397
  const { severity, format, what } = parsed.data;
15301
15398
  if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
15302
- const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : severity;
15399
+ const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
15303
15400
  return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
15304
15401
  });
15305
15402
  }
@@ -15310,7 +15407,7 @@ var CopyRead = z21.object({
15310
15407
  seen: z21.string().nullable()
15311
15408
  });
15312
15409
  function comparableCopy(text2) {
15313
- return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/\s+/g, " ").trim().toLowerCase();
15410
+ return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
15314
15411
  }
15315
15412
  function copyFindings(json, copy, formats, scrolled = {}) {
15316
15413
  if (copy.length === 0) return [];
@@ -15685,7 +15782,7 @@ function contextItems(opts) {
15685
15782
  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
15783
  ],
15687
15784
  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."
15785
+ `12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; start the finding with "SIZES:" and report it on the size that differs). Rearranged for its shape is fine.`
15689
15786
  ] : []
15690
15787
  };
15691
15788
  }
@@ -15738,6 +15835,7 @@ function mostOfALine(chunk, copy) {
15738
15835
  });
15739
15836
  }
15740
15837
  var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
15838
+ var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
15741
15839
  var COPY_SIZE_SHARE = 0.8;
15742
15840
  function extraWordFindings(format, extra, boxes, copyBoxes) {
15743
15841
  if (extra.length === 0) return [];
@@ -15755,7 +15853,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15755
15853
  const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
15756
15854
  const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
15757
15855
  const asCopy = extra.filter((chunk) => !printed(chunk));
15758
- const inScene = extra.filter(printed);
15856
+ const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
15857
+ const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
15759
15858
  return [
15760
15859
  ...asCopy.length > 0 ? [
15761
15860
  {
@@ -15764,6 +15863,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15764
15863
  what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
15765
15864
  }
15766
15865
  ] : [],
15866
+ ...prices.length > 0 ? [
15867
+ {
15868
+ severity: "blocking",
15869
+ // `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
15870
+ what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
15871
+ }
15872
+ ] : [],
15767
15873
  ...inScene.length > 0 ? [
15768
15874
  {
15769
15875
  severity: "warning",
@@ -15828,7 +15934,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
15828
15934
  const element = claimedBoxes(f.what, boxes, buttons);
15829
15935
  if (element.length === 0) return f;
15830
15936
  const named = claimedEdges(f.what);
15831
- const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) <= AT_EDGE));
15937
+ const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
15938
+ const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
15832
15939
  return atEdge ? f : { ...f, severity: "warning" };
15833
15940
  });
15834
15941
  }
@@ -15836,9 +15943,11 @@ function claimedEdges(what) {
15836
15943
  const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
15837
15944
  return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
15838
15945
  }
15946
+ var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
15947
+ var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
15839
15948
  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));
15949
+ if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
15950
+ if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15842
15951
  return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
15843
15952
  }
15844
15953
  async function bytesOf(source) {
@@ -15968,6 +16077,22 @@ function everySizeChecks(opts) {
15968
16077
  onSpend: opts.onSpend
15969
16078
  };
15970
16079
  }
16080
+ async function confirmBlocking(findings, stills, formats, ask2) {
16081
+ const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
16082
+ if (claims.length === 0 || stills.length === 0) return [...findings];
16083
+ const prompt = [
16084
+ `These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
16085
+ "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.",
16086
+ "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.",
16087
+ ...claims.map((c, i) => `${i + 1}. ${c.what}`),
16088
+ `Reply with only this JSON: {"answers":[true|false, ...]}, one answer per claim, in order.`
16089
+ ].join("\n");
16090
+ const answer = await ask2([...stills], prompt).catch(() => null);
16091
+ const answers = answer?.answers;
16092
+ if (!Array.isArray(answers) || answers.length !== claims.length) return [...findings];
16093
+ const unconfirmed = new Set(claims.filter((_, i) => answers[i] === false));
16094
+ return findings.map((f) => unconfirmed.has(f) ? { ...f, severity: "warning" } : f);
16095
+ }
15971
16096
  async function reviewRenderedStatic(opts) {
15972
16097
  const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
15973
16098
  if (measured.length === 0) return null;
@@ -15976,6 +16101,12 @@ async function reviewRenderedStatic(opts) {
15976
16101
  const formats = measured.map((m) => m.format);
15977
16102
  const lines = opts.lines ?? opts.copy.map((text3) => ({ text: text3, lettering: "designed" }));
15978
16103
  const every = { lines, ...everySizeChecks(opts) };
16104
+ const ask2 = (measuredRead) => {
16105
+ const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, measuredRead, reference !== null));
16106
+ const shown = [...measuredRead ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
16107
+ return askAboutStills(shown, prompt, opts.onSpend);
16108
+ };
16109
+ const asking = ask2(true);
15979
16110
  const checks = await Promise.all(
15980
16111
  originals.map(
15981
16112
  (bytes, i) => checkStaticImage(bytes, {
@@ -15987,13 +16118,19 @@ async function reviewRenderedStatic(opts) {
15987
16118
  );
15988
16119
  const read = checks.every((c) => c.status === "checked");
15989
16120
  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);
16121
+ const first = read ? await asking : (void asking.catch(() => null), null);
16122
+ const answer = first ?? await ask2(read);
15993
16123
  if (answer === null) return unreviewed(shape, measuredText);
15994
16124
  const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
15995
16125
  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)];
16126
+ const judged = corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines);
16127
+ const confirmed = await confirmBlocking(
16128
+ judged,
16129
+ stills,
16130
+ formats,
16131
+ (images, prompt) => askAboutStills(images, prompt, opts.onSpend)
16132
+ );
16133
+ return [...shape, ...text2, ...confirmed];
15997
16134
  }
15998
16135
 
15999
16136
  // src/engine/nodes/local/staticNative.ts
@@ -16237,7 +16374,9 @@ function boxesToPlace(boxes, format, opts = {}) {
16237
16374
  const past = (box) => bandViolations([box], format, { placements })[0];
16238
16375
  const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
16239
16376
  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);
16377
+ const read = withWordmark(boxes, logoWords);
16378
+ const buttons = new Set(opts.copy.filter(isButtonLine).flatMap((line) => boxesOfLine(line, read)));
16379
+ 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
16380
  }
16242
16381
  function planSafeFit(boxes, format, opts = {}) {
16243
16382
  const area = blockingArea(format, opts.placements);
@@ -16565,12 +16704,64 @@ import { z as z26 } from "zod";
16565
16704
  // src/engine/nodes/local/lib/variant-prefilter.ts
16566
16705
  import { createHash as createHash3 } from "crypto";
16567
16706
  import sharp8 from "sharp";
16568
- function coverFitBoxes(boxes, from, to) {
16707
+ function coverOf(from, to) {
16569
16708
  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;
16709
+ const width = from.width * scale;
16710
+ const height = from.height * scale;
16711
+ return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
16712
+ }
16713
+ function framingOf(from, to) {
16714
+ let frame;
16715
+ return {
16716
+ place: (boxes) => {
16717
+ frame = frameKeeping(boxes, from, to);
16718
+ return coverFitBoxes(boxes, from, to, frame);
16719
+ },
16720
+ moved: () => {
16721
+ const { centre } = coverOf(from, to);
16722
+ return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
16723
+ }
16724
+ };
16725
+ }
16726
+ async function cutToFrame(bytes, frame, to) {
16727
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16728
+ const { scale } = coverOf({ width, height }, to);
16729
+ const w = Math.min(width, Math.round(to.width / scale));
16730
+ const h = Math.min(height, Math.round(to.height / scale));
16731
+ const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
16732
+ const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
16733
+ return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
16734
+ }
16735
+ var FRAME_MARGIN = 0.02;
16736
+ function frameKeeping(boxes, from, to) {
16737
+ const cover = coverOf(from, to);
16738
+ if (boxes.length === 0) return cover.centre;
16739
+ const along = (offset2, extent, frame, lo, hi) => {
16740
+ const low = lo * extent;
16741
+ const high = hi * extent;
16742
+ const room = extent - frame;
16743
+ const margin = FRAME_MARGIN * frame;
16744
+ for (const m of [margin, 0]) {
16745
+ const min = Math.max(0, high + m - frame);
16746
+ const max = Math.min(room, low - m);
16747
+ if (min <= max) return Math.min(max, Math.max(min, offset2));
16748
+ }
16749
+ return offset2;
16750
+ };
16751
+ const x0 = Math.min(...boxes.map((b) => b.x0));
16752
+ const x1 = Math.max(...boxes.map((b) => b.x1));
16753
+ const y0 = Math.min(...boxes.map((b) => b.y0));
16754
+ const y1 = Math.max(...boxes.map((b) => b.y1));
16755
+ return {
16756
+ left: along(cover.centre.left, cover.width, to.width, x0, x1),
16757
+ top: along(cover.centre.top, cover.height, to.height, y0, y1)
16758
+ };
16759
+ }
16760
+ function coverFitBoxes(boxes, from, to, frame) {
16761
+ const cover = coverOf(from, to);
16762
+ const { left: offX, top: offY } = frame ?? cover.centre;
16763
+ const x = (f) => (f * cover.width - offX) / to.width;
16764
+ const y = (f) => (f * cover.height - offY) / to.height;
16574
16765
  return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
16575
16766
  }
16576
16767
  var NO_LOGO = "The brand's logo was not found on it.";
@@ -16624,34 +16815,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
16624
16815
  });
16625
16816
  const format = plan.master.format;
16626
16817
  const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
16818
+ const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16627
16819
  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
- })
16820
+ variants.map(
16821
+ (variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
16822
+ )
16652
16823
  );
16653
16824
  const outcome = prefilterOutcome(checked);
16654
- return { ...outcome, kept: seededOrder(outcome.kept, variants) };
16825
+ const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
16826
+ return {
16827
+ ...outcome,
16828
+ kept: seededOrder(outcome.kept, variants),
16829
+ ...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
16830
+ };
16831
+ }
16832
+ async function checkVariant(variant, c) {
16833
+ const { format, lines, brand, grade, want, opts } = c;
16834
+ const source = variant.path ?? variant.url;
16835
+ if (!source) return { index: variant.index, blocking: null };
16836
+ try {
16837
+ const bytes = await bytesOf(source);
16838
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16839
+ const framing = framingOf({ width, height }, c.size);
16840
+ const check = await checkStaticImage(bytes, {
16841
+ format,
16842
+ lines,
16843
+ allowed: brand ? [brand] : [],
16844
+ ...grade,
16845
+ toFrame: framing.place,
16846
+ ...opts.read ? { read: opts.read } : {},
16847
+ ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16848
+ });
16849
+ if (check.status === "not_checked") return { index: variant.index, blocking: null };
16850
+ const prefix = `${format}: `;
16851
+ const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16852
+ const frame = framing.moved();
16853
+ return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
16854
+ } catch {
16855
+ return { index: variant.index, blocking: null };
16856
+ }
16655
16857
  }
16656
16858
  function seededOrder(indexes, variants) {
16657
16859
  const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
@@ -16727,7 +16929,7 @@ async function readSpec(ref) {
16727
16929
  }
16728
16930
  var staticPrefilterNode = defineNode({
16729
16931
  id: "static_prefilter",
16730
- version: "1.0.0",
16932
+ version: "1.1.0",
16731
16933
  category: "image",
16732
16934
  location: "local",
16733
16935
  // Reads each variant's pixels itself, from its file or its durable URL, so the variants
@@ -16760,7 +16962,8 @@ var staticPrefilterNode = defineNode({
16760
16962
  images: z26.array(ImageRef).min(1),
16761
16963
  // Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
16762
16964
  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.
16965
+ // The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
16966
+ // master's frame where every word stays whole when its centre would cut one, else as rendered.
16764
16967
  image: ImageRef.optional(),
16765
16968
  // Which variant it is (its place among the inputs).
16766
16969
  chosen: z26.number().int().min(0).optional(),
@@ -16773,12 +16976,12 @@ var staticPrefilterNode = defineNode({
16773
16976
  }).strict(),
16774
16977
  outputKinds: { images: "image", image: "image" },
16775
16978
  cost: () => ({ credits: 0, seconds_estimate: 1 }),
16776
- async execute({ inputs, params }) {
16777
- const variants = inputs.images.map((image, index) => ({
16979
+ async execute({ inputs, params, ctx }) {
16980
+ const variants = inputs.images.map((image2, index) => ({
16778
16981
  index,
16779
- sha256: image.sha256,
16780
- ...image.path ? { path: image.path } : {},
16781
- ...image.url ? { url: image.url } : {}
16982
+ sha256: image2.sha256,
16983
+ ...image2.path ? { path: image2.path } : {},
16984
+ ...image2.url ? { url: image2.url } : {}
16782
16985
  }));
16783
16986
  const usage = [];
16784
16987
  const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
@@ -16794,10 +16997,11 @@ var staticPrefilterNode = defineNode({
16794
16997
  { model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
16795
16998
  ) : null;
16796
16999
  const chosen = tie?.chosen ?? shown[0] ?? 0;
17000
+ const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
16797
17001
  return {
16798
17002
  images: kept.length > 0 ? kept : inputs.images,
16799
17003
  order: shown,
16800
- ...inputs.images[chosen] ? { image: inputs.images[chosen] } : {},
17004
+ ...image ? { image } : {},
16801
17005
  chosen,
16802
17006
  ...tie ? { reasoning: tie.reasoning } : {},
16803
17007
  dropped: result.dropped,
@@ -16806,6 +17010,12 @@ var staticPrefilterNode = defineNode({
16806
17010
  };
16807
17011
  }
16808
17012
  });
17013
+ async function framed(picture, frame, size2, ctx) {
17014
+ const source = picture?.path ?? picture?.url;
17015
+ if (!picture || !frame || !size2 || !source) return picture;
17016
+ const bytes = await cutToFrame(await bytesOf(source), frame, size2);
17017
+ return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
17018
+ }
16809
17019
 
16810
17020
  // src/engine/nodes/local/staticSafeFit.ts
16811
17021
  import { z as z27 } from "zod";
@@ -18385,6 +18595,7 @@ export {
18385
18595
  AD_DESCRIBE_MODEL,
18386
18596
  BLUEPRINT_ANALYSIS_MODEL,
18387
18597
  VIEWER_CHECK_MODEL,
18598
+ CUSTOMER_INSIGHTS_MODEL,
18388
18599
  GLANCE_CHECK_MODEL,
18389
18600
  VIDEO_DECONSTRUCT_MODEL,
18390
18601
  VOICEOVER_MODEL,
@@ -18456,6 +18667,10 @@ export {
18456
18667
  isAdSpec,
18457
18668
  conceptSummary,
18458
18669
  conceptProblems,
18670
+ unwrittenProblems,
18671
+ sceneWordProblems,
18672
+ SPEC_SHAPES_HINT,
18673
+ adSpecErrorText,
18459
18674
  readAdSpec,
18460
18675
  withRenderedLettering,
18461
18676
  conceptClassOf,
@@ -18543,4 +18758,4 @@ export {
18543
18758
  defaultRegistry,
18544
18759
  createEngineFromEnv
18545
18760
  };
18546
- //# sourceMappingURL=chunk-7BQIIJLV.js.map
18761
+ //# sourceMappingURL=chunk-EFRX2BLB.js.map