@koda-sl/baker-cli 0.434.0-dev.42b55fa66 → 0.434.0-dev.b788a58e0

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 (30) hide show
  1. package/README.md +76 -21
  2. package/dist/{chunk-Q6F5UNOU.js → chunk-7THIX4WW.js} +8 -5
  3. package/dist/{chunk-Q6F5UNOU.js.map → chunk-7THIX4WW.js.map} +1 -1
  4. package/dist/{chunk-MMTPMFFH.js → chunk-ENTQULTV.js} +350 -141
  5. package/dist/chunk-ENTQULTV.js.map +1 -0
  6. package/dist/{chunk-GCTR4MDE.js → chunk-EURJVJQY.js} +2 -2
  7. package/dist/{chunk-FQOKJYKN.js → chunk-F7LXMR73.js} +17 -4
  8. package/dist/chunk-F7LXMR73.js.map +1 -0
  9. package/dist/{chunk-RFZYRG2Q.js → chunk-QLNGHVRT.js} +11 -3
  10. package/dist/chunk-QLNGHVRT.js.map +1 -0
  11. package/dist/{chunk-PHLFNYGQ.js → chunk-VNROEZTR.js} +4 -4
  12. package/dist/cli.js +16353 -12693
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-GS7T7ETG.js → client-TCDMWAL3.js} +4 -4
  15. package/dist/engine/index.d.ts +12 -6
  16. package/dist/engine/index.js +3 -3
  17. package/dist/{env-SL22S6CM.js → env-ZPCFZJN5.js} +6 -2
  18. package/dist/output-CW4JBWIM.js +22 -0
  19. package/dist/{shared-XR54KFMH.js → shared-6I2TP4YQ.js} +6 -6
  20. package/package.json +1 -1
  21. package/dist/chunk-FQOKJYKN.js.map +0 -1
  22. package/dist/chunk-MMTPMFFH.js.map +0 -1
  23. package/dist/chunk-RFZYRG2Q.js.map +0 -1
  24. package/dist/output-OFDKEPPJ.js +0 -18
  25. /package/dist/{chunk-GCTR4MDE.js.map → chunk-EURJVJQY.js.map} +0 -0
  26. /package/dist/{chunk-PHLFNYGQ.js.map → chunk-VNROEZTR.js.map} +0 -0
  27. /package/dist/{client-GS7T7ETG.js.map → client-TCDMWAL3.js.map} +0 -0
  28. /package/dist/{env-SL22S6CM.js.map → env-ZPCFZJN5.js.map} +0 -0
  29. /package/dist/{output-OFDKEPPJ.js.map → output-CW4JBWIM.js.map} +0 -0
  30. /package/dist/{shared-XR54KFMH.js.map → shared-6I2TP4YQ.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GCTR4MDE.js";
4
+ } from "./chunk-EURJVJQY.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -22,7 +22,7 @@ import {
22
22
  refuseNonPublicUrl,
23
23
  requireConnection,
24
24
  shouldEscalate
25
- } from "./chunk-FQOKJYKN.js";
25
+ } from "./chunk-F7LXMR73.js";
26
26
 
27
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({
@@ -3594,23 +3594,6 @@ function resolveConcurrency(...candidates) {
3594
3594
  }
3595
3595
  return DEFAULT_CONCURRENCY;
3596
3596
  }
3597
- async function mapWithConcurrency(items, limit, fn) {
3598
- const results = new Array(items.length);
3599
- let next = 0;
3600
- const worker = async () => {
3601
- while (next < items.length) {
3602
- const index = next++;
3603
- try {
3604
- results[index] = { status: "fulfilled", value: await fn(items[index], index) };
3605
- } catch (reason) {
3606
- results[index] = { status: "rejected", reason };
3607
- }
3608
- }
3609
- };
3610
- const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
3611
- await Promise.all(workers);
3612
- return results;
3613
- }
3614
3597
 
3615
3598
  // src/engine/lib/json-walk.ts
3616
3599
  function isContainerObject(value) {
@@ -4415,7 +4398,16 @@ var CanvasMetadata = z4.object({
4415
4398
  // `scaffold-video`) so the editable checklist travels inside the canvas
4416
4399
  // instead of only printing to stdout. Ignored by the engine.
4417
4400
  todo: z4.unknown().optional(),
4418
- 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
+ }).strict().optional()
4419
4411
  }).strict().optional();
4420
4412
  var CanvasSchema = z4.object({
4421
4413
  schema: z4.literal("baker-canvas/1"),
@@ -6706,6 +6698,7 @@ function textFidelityDirective(strings2, hasFont) {
6706
6698
  return [
6707
6699
  exact,
6708
6700
  "Render NO other words anywhere in the image \u2014 no invented labels, captions, sub-headers, or filler text.",
6701
+ "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.",
6709
6702
  "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).",
6710
6703
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
6711
6704
  ].join("\n");
@@ -6770,22 +6763,32 @@ var Viewer = z7.object({
6770
6763
  /** Who will see the ad, in the words a marketer would use. */
6771
6764
  who: z7.string().trim().min(1),
6772
6765
  market: Market.optional(),
6773
- /** What they already know about the brand and the offer. */
6774
- knows: z7.string().optional(),
6766
+ /** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
6767
+ knows: z7.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z7.string().optional()),
6775
6768
  temperature: z7.enum(["cold", "warm", "hot"]).optional(),
6776
6769
  /** Where they meet it (feed, Stories, Reels). */
6777
6770
  placement: z7.string().optional()
6778
6771
  }).strict();
6779
- var Scene = z7.object({
6772
+ var withoutFormatName = (v) => {
6773
+ if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
6774
+ return v;
6775
+ const { concept: _format, ...scene } = v;
6776
+ return scene;
6777
+ };
6778
+ var SceneFields = z7.object({
6780
6779
  description: z7.string().trim().min(1),
6781
- /** A photograph, a designed graphic, or an illustration. */
6782
- look: z7.enum(["photo", "designed", "illustration"]),
6780
+ /** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
6781
+ look: z7.preprocess(
6782
+ (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,
6783
+ z7.enum(["photo", "designed", "illustration"])
6784
+ ),
6783
6785
  /** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
6784
6786
  capture: z7.string().optional(),
6785
6787
  mood: z7.string().optional(),
6786
6788
  /** How the frame is arranged, by what fills each part of it. */
6787
6789
  layout: z7.string().optional()
6788
6790
  }).strict();
6791
+ var Scene = z7.preprocess(withoutFormatName, SceneFields);
6789
6792
  var Brand = z7.object({
6790
6793
  name: z7.string().optional(),
6791
6794
  /** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
@@ -6857,13 +6860,19 @@ var AdSpecSchema = z7.object({
6857
6860
  * editor's line numbers, a product's own label, a platform's badge. The render may draw them
6858
6861
  * and the review never calls them extra words.
6859
6862
  */
6860
- scene_text: z7.array(z7.string().trim().min(1)).default([]),
6863
+ scene_text: z7.preprocess(
6864
+ (v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
6865
+ z7.array(z7.string().trim().min(1)).default([])
6866
+ ),
6861
6867
  /** The formats the ad is made for; the canvas delivers them. */
6862
6868
  formats: z7.array(z7.string()).optional(),
6863
6869
  /** An optional note on direction for this concept. */
6864
6870
  direction: z7.string().optional(),
6865
6871
  /** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
6866
- mechanism: z7.array(Mechanism).default([]),
6872
+ mechanism: z7.preprocess((v) => {
6873
+ const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
6874
+ return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
6875
+ }, z7.array(Mechanism).default([])),
6867
6876
  /** Why the reference won and what this ad keeps of it (schema /3). */
6868
6877
  concept: Concept.optional(),
6869
6878
  /**
@@ -6931,6 +6940,49 @@ function unwrittenProblems(spec) {
6931
6940
  }
6932
6941
  return problems;
6933
6942
  }
6943
+ var QUOTED = /"([^"]{2,})"|“([^”]{2,})”|(?<![\p{L}\p{N}])['‘]([^'’]{2,}?)['’](?![\p{L}\p{N}])/gu;
6944
+ var FIGURE = /[£$€]\s?\d[\d.,]*|\d+(?:[.,]\d+)?\s?%/g;
6945
+ var comparable = (text2) => text2.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}£$€%]+/gu, " ").trim();
6946
+ function sceneWordProblems(spec) {
6947
+ const held = [...spec.copy.map((l) => l.text), ...spec.scene_text].map(comparable);
6948
+ const holds = (text2) => {
6949
+ const want = comparable(text2);
6950
+ return want.length === 0 || held.some((h) => h.includes(want));
6951
+ };
6952
+ const scene = [spec.scene.description, spec.scene.layout ?? ""].join(" ");
6953
+ const quoted = [...scene.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter((q) => !holds(q));
6954
+ const figures = (scene.match(FIGURE) ?? []).map((f) => f.trim()).filter((f) => !holds(f));
6955
+ const unheld = [.../* @__PURE__ */ new Set([...quoted, ...figures])];
6956
+ if (unheld.length === 0) return [];
6957
+ return [
6958
+ `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).`
6959
+ ];
6960
+ }
6961
+ var FIELD_SHAPES = {
6962
+ copy: '"copy": [{"text": "Book your free survey", "lettering": "designed", "role": "cta"}]',
6963
+ scene: '"scene": {"description": "a family on a sunny roof terrace", "look": "photo"}',
6964
+ colors: '"colors": [{"object": "the button", "name": "deep green", "hex": "#1F5C3A"}]',
6965
+ references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
6966
+ third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
6967
+ mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
6968
+ viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
6969
+ };
6970
+ var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
6971
+ function adSpecErrorText(e) {
6972
+ if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
6973
+ const items = /* @__PURE__ */ new Map();
6974
+ for (const issue of e.issues) {
6975
+ const [field2, index, key] = issue.path;
6976
+ const at2 = typeof index === "number" ? `${String(field2)}[${index}]` : issue.path.map(String).join(".") || "the spec";
6977
+ 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: /, "");
6978
+ items.set(at2, [...items.get(at2) ?? [], what]);
6979
+ }
6980
+ const fields = [...new Set(e.issues.map((i) => String(i.path[0] ?? "")))].filter((f) => FIELD_SHAPES[f]);
6981
+ return [
6982
+ ...[...items].map(([at2, whats]) => `${at2}: ${[...new Set(whats)].join("; ")}`),
6983
+ ...fields.map((f) => `Shape: ${FIELD_SHAPES[f]}`)
6984
+ ].join("\n");
6985
+ }
6934
6986
  function readAdSpec(raw) {
6935
6987
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6936
6988
  const advertiser = raw?.reference?.advertiser;
@@ -7421,7 +7473,7 @@ function pictureInstruction(p, ctx) {
7421
7473
  const keep = p.spec?.keep ? ` Keep ${plain(p.spec.keep, ctx.clean)}.` : "";
7422
7474
  switch (p.role) {
7423
7475
  case "base":
7424
- return ctx.job === "fix" ? "the current version of this ad, to edit." : "the finished ad, to re-lay for this shape.";
7476
+ 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.";
7425
7477
  case "identity":
7426
7478
  return identityInstruction(p, what, keep, ctx.flowing);
7427
7479
  // The user saw one label come back two ways (E4 gallery, BENCH.md § "E1 v2"): the label is the product's.
@@ -7800,6 +7852,10 @@ function deliverableSection(c) {
7800
7852
  const change = sentence(c.opts.instruction?.trim() || "correct what is wrong");
7801
7853
  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.`;
7802
7854
  }
7855
+ if (c.job === "series") {
7856
+ const change = sentence(c.opts.instruction?.trim() || "set the new words");
7857
+ 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.`;
7858
+ }
7803
7859
  if (c.job === "relay") {
7804
7860
  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.`;
7805
7861
  }
@@ -8383,14 +8439,16 @@ function scaleText(plan, spec, flowing, zoned, short) {
8383
8439
  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." : "";
8384
8440
  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}`;
8385
8441
  }
8386
- var ZONE_HEADROOM = 0;
8442
+ var COVERED_HEADROOM = { top: 0.06, bottom: 0.08, side: 0.02 };
8443
+ var NO_HEADROOM = { top: 0, bottom: 0, side: 0 };
8387
8444
  var pct = (share) => `${Math.round(share * 100)}%`;
8388
8445
  function zonesText(plan, flowing, ground, short) {
8389
- const top = plan.safe.top + ZONE_HEADROOM;
8390
- const bottom = plan.safe.bottom + ZONE_HEADROOM;
8391
- const side = Math.max(plan.safe.left, plan.safe.right) + ZONE_HEADROOM;
8392
- const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
8393
8446
  const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
8447
+ const room = isTall(plan) && covered ? COVERED_HEADROOM : NO_HEADROOM;
8448
+ const top = plan.safe.top + room.top;
8449
+ const bottom = plan.safe.bottom + room.bottom;
8450
+ const side = Math.max(plan.safe.left, plan.safe.right) + room.side;
8451
+ const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
8394
8452
  if (short && !flowing) {
8395
8453
  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";
8396
8454
  return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
@@ -8399,7 +8457,7 @@ function zonesText(plan, flowing, ground, short) {
8399
8457
  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.`;
8400
8458
  }
8401
8459
  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";
8402
- 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.`;
8460
+ 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.`;
8403
8461
  }
8404
8462
  function adaptBrief(spec, profile, opts) {
8405
8463
  const { plan, images } = opts;
@@ -8468,6 +8526,7 @@ var STATIC_ADAPT_ROLE = "static_adapt";
8468
8526
  var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
8469
8527
  var STATIC_PLACE_ROLE = "static_place";
8470
8528
  var LOGO_COMPOSITE_ROLE = "logo_composite";
8529
+ var SERIES_LEAD_ROLE = "series_lead";
8471
8530
  var STATIC_FIX_ROLE = "static_fix";
8472
8531
  var LOGO_COMPOSITE_DEFAULT = false;
8473
8532
  var ADAPT_IMAGE_SIZE = "2K";
@@ -10136,7 +10195,7 @@ function checkOrphanNodes(ctx) {
10136
10195
  path: `nodes[${ctx.idToIndex.get(n.id)}]`,
10137
10196
  code: STAGE_CODES.ORPHAN_NODE,
10138
10197
  severity: "warning",
10139
- 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`
10198
+ 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`
10140
10199
  });
10141
10200
  }
10142
10201
  }
@@ -10207,6 +10266,17 @@ function unwrap(schema) {
10207
10266
  }
10208
10267
 
10209
10268
  // src/engine/engine/executor.ts
10269
+ function throwRunOutcome(signal, failures, held) {
10270
+ if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10271
+ if (failures.length === 1 && failures[0]) throw failures[0].reason;
10272
+ if (failures.length > 1) throw new LayerExecutionError([...failures]);
10273
+ if (held.length > 0) {
10274
+ throw new RunAbortedError(
10275
+ "checkpoint",
10276
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10277
+ );
10278
+ }
10279
+ }
10210
10280
  function heldByCheckpoint(canvas, layer, kinds) {
10211
10281
  if (!kinds?.size) return [];
10212
10282
  return layer.filter((id) => {
@@ -10295,45 +10365,63 @@ var Engine = class {
10295
10365
  return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
10296
10366
  }
10297
10367
  async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
10298
- const layers = topologicalLayers(graph);
10368
+ const order = topologicalLayers(graph).flat();
10299
10369
  const limit = resolveConcurrency(opts.concurrency);
10300
- for (const layer of layers) {
10301
- this.assertLayerMayRun(canvas, layer, opts);
10302
- const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
10303
- if (opts.signal?.aborted) {
10304
- return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
10305
- }
10306
- this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10307
- return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
10308
- if (r.cached) counters.cachedNodes++;
10309
- counters.totalCredits += r.credits;
10310
- const node = canvas.nodes.find((n) => n.id === nodeId);
10311
- if (node) {
10312
- const run = {
10313
- node_id: nodeId,
10314
- node_type: node.type,
10315
- cached: r.cached,
10316
- duration_ms: r.durationMs,
10317
- credits: r.credits
10318
- };
10319
- nodeRuns.push(run);
10320
- this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10321
- }
10322
- });
10323
- });
10324
- const failures = [];
10325
- settled.forEach((result, i) => {
10326
- const nodeId = layer[i];
10327
- if (result.status === "rejected" && nodeId) {
10328
- if (result.reason instanceof RunAbortedError) return;
10329
- failures.push({ nodeId, reason: result.reason });
10330
- this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(result.reason) });
10370
+ if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
10371
+ const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
10372
+ const running = /* @__PURE__ */ new Map();
10373
+ const failures = [];
10374
+ const held = [];
10375
+ const dispatch = (nodeId) => {
10376
+ waitingOn.delete(nodeId);
10377
+ this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10378
+ const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
10379
+ (r) => {
10380
+ this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
10381
+ for (const deps of waitingOn.values()) deps.delete(nodeId);
10382
+ },
10383
+ (reason) => {
10384
+ failures.push({ nodeId, reason });
10385
+ this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
10331
10386
  }
10332
- });
10333
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10334
- if (failures.length === 1 && failures[0]) throw failures[0].reason;
10335
- if (failures.length > 1) throw new LayerExecutionError(failures);
10387
+ );
10388
+ running.set(
10389
+ nodeId,
10390
+ settled.finally(() => running.delete(nodeId))
10391
+ );
10392
+ };
10393
+ const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
10394
+ for (; ; ) {
10395
+ const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
10396
+ for (const nodeId of ready) {
10397
+ if (running.size >= limit) break;
10398
+ if (isHeld(nodeId)) {
10399
+ waitingOn.delete(nodeId);
10400
+ held.push(nodeId);
10401
+ } else dispatch(nodeId);
10402
+ }
10403
+ if (running.size === 0) break;
10404
+ await Promise.race(running.values());
10336
10405
  }
10406
+ const rank = new Map(order.map((id, i) => [id, i]));
10407
+ failures.sort((a, b) => (rank.get(a.nodeId) ?? 0) - (rank.get(b.nodeId) ?? 0));
10408
+ throwRunOutcome(opts.signal, failures, held);
10409
+ }
10410
+ /** A settled node counted, recorded and announced. */
10411
+ recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
10412
+ if (r.cached) counters.cachedNodes++;
10413
+ counters.totalCredits += r.credits;
10414
+ const node = canvas.nodes.find((n) => n.id === nodeId);
10415
+ if (!node) return;
10416
+ const run = {
10417
+ node_id: nodeId,
10418
+ node_type: node.type,
10419
+ cached: r.cached,
10420
+ duration_ms: r.durationMs,
10421
+ credits: r.credits
10422
+ };
10423
+ nodeRuns.push(run);
10424
+ this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10337
10425
  }
10338
10426
  /** Progress consumers are observers only — an exception there must never fail the run. */
10339
10427
  emitProgress(opts, event) {
@@ -10425,20 +10513,6 @@ var Engine = class {
10425
10513
  this.logOutputWarnings(node.id, outputsObj);
10426
10514
  return { cached: false, credits, durationMs: elapsed };
10427
10515
  }
10428
- /**
10429
- * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
10430
- * never leaves a layer half billed: a signal, the cost cap and a checkpoint.
10431
- */
10432
- assertLayerMayRun(canvas, layer, opts) {
10433
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
10434
- const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
10435
- if (held.length > 0) {
10436
- throw new RunAbortedError(
10437
- "checkpoint",
10438
- `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10439
- );
10440
- }
10441
- }
10442
10516
  /** A cache hit replayed as this node's result: its outputs written, and free. */
10443
10517
  async replayCached(node, cacheKey, outputs, writer) {
10444
10518
  const cacheT0 = Date.now();
@@ -14603,6 +14677,18 @@ function boxesOfLine(line, boxes) {
14603
14677
  return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
14604
14678
  });
14605
14679
  }
14680
+ var BUTTON_PAD = 0.7;
14681
+ var isButtonLine = (line) => /^(cta|button|call_to_action)$/.test(roleKey2(line.role));
14682
+ function buttonShape(box) {
14683
+ const pad = BUTTON_PAD * (box.y1 - box.y0);
14684
+ return {
14685
+ ...box,
14686
+ x0: Math.max(0, box.x0 - pad),
14687
+ y0: Math.max(0, box.y0 - pad),
14688
+ x1: Math.min(1, box.x1 + pad),
14689
+ y1: Math.min(1, box.y1 + pad)
14690
+ };
14691
+ }
14606
14692
  function shownBoxes(boxes, lines, clear) {
14607
14693
  const linesOfBox = /* @__PURE__ */ new Map();
14608
14694
  for (const line of lines) {
@@ -14986,7 +15072,7 @@ async function videoDuration(video) {
14986
15072
  return null;
14987
15073
  }
14988
15074
  }
14989
- async function askGateway(model, parts, timeoutMs, spent) {
15075
+ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
14990
15076
  const key = openRouterKey();
14991
15077
  if (!key) return null;
14992
15078
  try {
@@ -14996,7 +15082,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
14996
15082
  body: JSON.stringify({
14997
15083
  model,
14998
15084
  messages: [{ role: "user", content: parts }],
14999
- usage: { include: true }
15085
+ usage: { include: true },
15086
+ ...reasoning ? { reasoning } : {}
15000
15087
  }),
15001
15088
  signal: AbortSignal.timeout(timeoutMs)
15002
15089
  });
@@ -15045,7 +15132,7 @@ async function askAboutStills(images, prompt, onSpend) {
15045
15132
  if (!hasOpenRouterKey()) return null;
15046
15133
  return await ask(images, prompt, (usage) => onSpend?.(usage));
15047
15134
  }
15048
- async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15135
+ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
15049
15136
  if (!hasOpenRouterKey()) return null;
15050
15137
  return await askGateway(
15051
15138
  model,
@@ -15056,7 +15143,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15056
15143
  )
15057
15144
  ],
15058
15145
  timeoutMs,
15059
- (usage) => onSpend?.(usage)
15146
+ (usage) => onSpend?.(usage),
15147
+ reasoning
15060
15148
  );
15061
15149
  }
15062
15150
  async function askAboutText(model, prompt, onSpend) {
@@ -15220,7 +15308,9 @@ async function readTextBoxes(image, opts = {}) {
15220
15308
  return { status: "not_checked" };
15221
15309
  }
15222
15310
  for (let attempt = 0; attempt < 2; attempt++) {
15223
- const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend);
15311
+ const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend, 6e4, {
15312
+ effort: "minimal"
15313
+ });
15224
15314
  const boxes = textBoxesFromReader(answer);
15225
15315
  if (boxes) return { status: "read", boxes };
15226
15316
  }
@@ -15310,6 +15400,8 @@ var StaticFinding = z21.object({
15310
15400
  });
15311
15401
  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;
15312
15402
  var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
15403
+ var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
15404
+ 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;
15313
15405
  function classifyStaticFindings(json) {
15314
15406
  const findings = json?.findings;
15315
15407
  if (!Array.isArray(findings)) return [];
@@ -15318,7 +15410,7 @@ function classifyStaticFindings(json) {
15318
15410
  if (!parsed.success) return [];
15319
15411
  const { severity, format, what } = parsed.data;
15320
15412
  if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
15321
- const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : severity;
15413
+ const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
15322
15414
  return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
15323
15415
  });
15324
15416
  }
@@ -15329,7 +15421,7 @@ var CopyRead = z21.object({
15329
15421
  seen: z21.string().nullable()
15330
15422
  });
15331
15423
  function comparableCopy(text2) {
15332
- return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/\s+/g, " ").trim().toLowerCase();
15424
+ return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
15333
15425
  }
15334
15426
  function copyFindings(json, copy, formats, scrolled = {}) {
15335
15427
  if (copy.length === 0) return [];
@@ -15704,7 +15796,7 @@ function contextItems(opts) {
15704
15796
  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."
15705
15797
  ],
15706
15798
  sizes: opts.formats.length > 1 ? [
15707
- "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."
15799
+ `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.`
15708
15800
  ] : []
15709
15801
  };
15710
15802
  }
@@ -15757,6 +15849,7 @@ function mostOfALine(chunk, copy) {
15757
15849
  });
15758
15850
  }
15759
15851
  var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
15852
+ var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
15760
15853
  var COPY_SIZE_SHARE = 0.8;
15761
15854
  function extraWordFindings(format, extra, boxes, copyBoxes) {
15762
15855
  if (extra.length === 0) return [];
@@ -15774,7 +15867,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15774
15867
  const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
15775
15868
  const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
15776
15869
  const asCopy = extra.filter((chunk) => !printed(chunk));
15777
- const inScene = extra.filter(printed);
15870
+ const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
15871
+ const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
15778
15872
  return [
15779
15873
  ...asCopy.length > 0 ? [
15780
15874
  {
@@ -15783,6 +15877,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15783
15877
  what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
15784
15878
  }
15785
15879
  ] : [],
15880
+ ...prices.length > 0 ? [
15881
+ {
15882
+ severity: "blocking",
15883
+ // `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
15884
+ what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
15885
+ }
15886
+ ] : [],
15786
15887
  ...inScene.length > 0 ? [
15787
15888
  {
15788
15889
  severity: "warning",
@@ -15847,7 +15948,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
15847
15948
  const element = claimedBoxes(f.what, boxes, buttons);
15848
15949
  if (element.length === 0) return f;
15849
15950
  const named = claimedEdges(f.what);
15850
- const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) <= AT_EDGE));
15951
+ const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
15952
+ const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
15851
15953
  return atEdge ? f : { ...f, severity: "warning" };
15852
15954
  });
15853
15955
  }
@@ -15855,9 +15957,11 @@ function claimedEdges(what) {
15855
15957
  const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
15856
15958
  return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
15857
15959
  }
15960
+ var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
15961
+ var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
15858
15962
  function claimedBoxes(what, boxes, buttons) {
15859
- if (/\b(?:logos?|wordmark|mark)\b/i.test(what)) return boxes.filter((b) => b.kind === "logo");
15860
- if (/\b(button|cta|call[- ]to[- ]action)\b/i.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15963
+ if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
15964
+ if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
15861
15965
  return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
15862
15966
  }
15863
15967
  async function bytesOf(source) {
@@ -15987,6 +16091,22 @@ function everySizeChecks(opts) {
15987
16091
  onSpend: opts.onSpend
15988
16092
  };
15989
16093
  }
16094
+ async function confirmBlocking(findings, stills, formats, ask2) {
16095
+ const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
16096
+ if (claims.length === 0 || stills.length === 0) return [...findings];
16097
+ const prompt = [
16098
+ `These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
16099
+ "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.",
16100
+ "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.",
16101
+ ...claims.map((c, i) => `${i + 1}. ${c.what}`),
16102
+ `Reply with only this JSON: {"answers":[true|false, ...]}, one answer per claim, in order.`
16103
+ ].join("\n");
16104
+ const answer = await ask2([...stills], prompt).catch(() => null);
16105
+ const answers = answer?.answers;
16106
+ if (!Array.isArray(answers) || answers.length !== claims.length) return [...findings];
16107
+ const unconfirmed = new Set(claims.filter((_, i) => answers[i] === false));
16108
+ return findings.map((f) => unconfirmed.has(f) ? { ...f, severity: "warning" } : f);
16109
+ }
15990
16110
  async function reviewRenderedStatic(opts) {
15991
16111
  const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
15992
16112
  if (measured.length === 0) return null;
@@ -15995,6 +16115,12 @@ async function reviewRenderedStatic(opts) {
15995
16115
  const formats = measured.map((m) => m.format);
15996
16116
  const lines = opts.lines ?? opts.copy.map((text3) => ({ text: text3, lettering: "designed" }));
15997
16117
  const every = { lines, ...everySizeChecks(opts) };
16118
+ const ask2 = (measuredRead) => {
16119
+ const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, measuredRead, reference !== null));
16120
+ const shown = [...measuredRead ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
16121
+ return askAboutStills(shown, prompt, opts.onSpend);
16122
+ };
16123
+ const asking = ask2(true);
15998
16124
  const checks = await Promise.all(
15999
16125
  originals.map(
16000
16126
  (bytes, i) => checkStaticImage(bytes, {
@@ -16006,13 +16132,19 @@ async function reviewRenderedStatic(opts) {
16006
16132
  );
16007
16133
  const read = checks.every((c) => c.status === "checked");
16008
16134
  const measuredText = read ? checks.flatMap((c) => c.status === "checked" ? c.findings : []) : [];
16009
- const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, read, reference !== null));
16010
- const shown = [...read ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
16011
- const answer = await askAboutStills(shown, prompt, opts.onSpend) ?? await askAboutStills(shown, prompt, opts.onSpend);
16135
+ const first = read ? await asking : (void asking.catch(() => null), null);
16136
+ const answer = first ?? await ask2(read);
16012
16137
  if (answer === null) return unreviewed(shape, measuredText);
16013
16138
  const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
16014
16139
  const boxesByFormat = read ? Object.fromEntries(checks.flatMap((c, i) => c.status === "checked" ? [[formats[i] ?? "", c.boxes]] : [])) : {};
16015
- return [...shape, ...text2, ...corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines)];
16140
+ const judged = corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines);
16141
+ const confirmed = await confirmBlocking(
16142
+ judged,
16143
+ stills,
16144
+ formats,
16145
+ (images, prompt) => askAboutStills(images, prompt, opts.onSpend)
16146
+ );
16147
+ return [...shape, ...text2, ...confirmed];
16016
16148
  }
16017
16149
 
16018
16150
  // src/engine/nodes/local/staticNative.ts
@@ -16256,7 +16388,9 @@ function boxesToPlace(boxes, format, opts = {}) {
16256
16388
  const past = (box) => bandViolations([box], format, { placements })[0];
16257
16389
  const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
16258
16390
  const logoWords = opts.copy.filter((l) => roleKey2(l.role) === "logo").map((l) => l.text);
16259
- return shownBoxes(withWordmark(boxes, logoWords), opts.copy, (box) => !past(box)).filter((shown) => !shown.supporting || cut(shown.box)).map((shown) => shown.box);
16391
+ const read = withWordmark(boxes, logoWords);
16392
+ const buttons = new Set(opts.copy.filter(isButtonLine).flatMap((line) => boxesOfLine(line, read)));
16393
+ 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);
16260
16394
  }
16261
16395
  function planSafeFit(boxes, format, opts = {}) {
16262
16396
  const area = blockingArea(format, opts.placements);
@@ -16584,12 +16718,64 @@ import { z as z26 } from "zod";
16584
16718
  // src/engine/nodes/local/lib/variant-prefilter.ts
16585
16719
  import { createHash as createHash3 } from "crypto";
16586
16720
  import sharp8 from "sharp";
16587
- function coverFitBoxes(boxes, from, to) {
16721
+ function coverOf(from, to) {
16588
16722
  const scale = Math.max(to.width / from.width, to.height / from.height);
16589
- const offX = (from.width * scale - to.width) / 2;
16590
- const offY = (from.height * scale - to.height) / 2;
16591
- const x = (f) => (f * from.width * scale - offX) / to.width;
16592
- const y = (f) => (f * from.height * scale - offY) / to.height;
16723
+ const width = from.width * scale;
16724
+ const height = from.height * scale;
16725
+ return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
16726
+ }
16727
+ function framingOf(from, to) {
16728
+ let frame;
16729
+ return {
16730
+ place: (boxes) => {
16731
+ frame = frameKeeping(boxes, from, to);
16732
+ return coverFitBoxes(boxes, from, to, frame);
16733
+ },
16734
+ moved: () => {
16735
+ const { centre } = coverOf(from, to);
16736
+ return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
16737
+ }
16738
+ };
16739
+ }
16740
+ async function cutToFrame(bytes, frame, to) {
16741
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16742
+ const { scale } = coverOf({ width, height }, to);
16743
+ const w = Math.min(width, Math.round(to.width / scale));
16744
+ const h = Math.min(height, Math.round(to.height / scale));
16745
+ const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
16746
+ const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
16747
+ return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
16748
+ }
16749
+ var FRAME_MARGIN = 0.02;
16750
+ function frameKeeping(boxes, from, to) {
16751
+ const cover = coverOf(from, to);
16752
+ if (boxes.length === 0) return cover.centre;
16753
+ const along = (offset2, extent, frame, lo, hi) => {
16754
+ const low = lo * extent;
16755
+ const high = hi * extent;
16756
+ const room = extent - frame;
16757
+ const margin = FRAME_MARGIN * frame;
16758
+ for (const m of [margin, 0]) {
16759
+ const min = Math.max(0, high + m - frame);
16760
+ const max = Math.min(room, low - m);
16761
+ if (min <= max) return Math.min(max, Math.max(min, offset2));
16762
+ }
16763
+ return offset2;
16764
+ };
16765
+ const x0 = Math.min(...boxes.map((b) => b.x0));
16766
+ const x1 = Math.max(...boxes.map((b) => b.x1));
16767
+ const y0 = Math.min(...boxes.map((b) => b.y0));
16768
+ const y1 = Math.max(...boxes.map((b) => b.y1));
16769
+ return {
16770
+ left: along(cover.centre.left, cover.width, to.width, x0, x1),
16771
+ top: along(cover.centre.top, cover.height, to.height, y0, y1)
16772
+ };
16773
+ }
16774
+ function coverFitBoxes(boxes, from, to, frame) {
16775
+ const cover = coverOf(from, to);
16776
+ const { left: offX, top: offY } = frame ?? cover.centre;
16777
+ const x = (f) => (f * cover.width - offX) / to.width;
16778
+ const y = (f) => (f * cover.height - offY) / to.height;
16593
16779
  return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
16594
16780
  }
16595
16781
  var NO_LOGO = "The brand's logo was not found on it.";
@@ -16643,34 +16829,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
16643
16829
  });
16644
16830
  const format = plan.master.format;
16645
16831
  const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
16832
+ const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16646
16833
  const checked = await Promise.all(
16647
- variants.map(async (variant) => {
16648
- const source = variant.path ?? variant.url;
16649
- if (!source) return { index: variant.index, blocking: null };
16650
- try {
16651
- const bytes = await bytesOf(source);
16652
- const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16653
- const check = await checkStaticImage(bytes, {
16654
- format,
16655
- lines,
16656
- allowed: brand ? [brand] : [],
16657
- ...grade,
16658
- toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
16659
- ...opts.read ? { read: opts.read } : {},
16660
- ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16661
- });
16662
- if (check.status === "not_checked") return { index: variant.index, blocking: null };
16663
- const prefix = `${format}: `;
16664
- const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16665
- const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16666
- return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
16667
- } catch {
16668
- return { index: variant.index, blocking: null };
16669
- }
16670
- })
16834
+ variants.map(
16835
+ (variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
16836
+ )
16671
16837
  );
16672
16838
  const outcome = prefilterOutcome(checked);
16673
- return { ...outcome, kept: seededOrder(outcome.kept, variants) };
16839
+ const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
16840
+ return {
16841
+ ...outcome,
16842
+ kept: seededOrder(outcome.kept, variants),
16843
+ ...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
16844
+ };
16845
+ }
16846
+ async function checkVariant(variant, c) {
16847
+ const { format, lines, brand, grade, want, opts } = c;
16848
+ const source = variant.path ?? variant.url;
16849
+ if (!source) return { index: variant.index, blocking: null };
16850
+ try {
16851
+ const bytes = await bytesOf(source);
16852
+ const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16853
+ const framing = framingOf({ width, height }, c.size);
16854
+ const check = await checkStaticImage(bytes, {
16855
+ format,
16856
+ lines,
16857
+ allowed: brand ? [brand] : [],
16858
+ ...grade,
16859
+ toFrame: framing.place,
16860
+ ...opts.read ? { read: opts.read } : {},
16861
+ ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16862
+ });
16863
+ if (check.status === "not_checked") return { index: variant.index, blocking: null };
16864
+ const prefix = `${format}: `;
16865
+ const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16866
+ const frame = framing.moved();
16867
+ return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
16868
+ } catch {
16869
+ return { index: variant.index, blocking: null };
16870
+ }
16674
16871
  }
16675
16872
  function seededOrder(indexes, variants) {
16676
16873
  const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
@@ -16746,7 +16943,7 @@ async function readSpec(ref) {
16746
16943
  }
16747
16944
  var staticPrefilterNode = defineNode({
16748
16945
  id: "static_prefilter",
16749
- version: "1.0.0",
16946
+ version: "1.1.0",
16750
16947
  category: "image",
16751
16948
  location: "local",
16752
16949
  // Reads each variant's pixels itself, from its file or its durable URL, so the variants
@@ -16779,7 +16976,8 @@ var staticPrefilterNode = defineNode({
16779
16976
  images: z26.array(ImageRef).min(1),
16780
16977
  // Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
16781
16978
  order: z26.array(z26.number().int().min(0)).optional(),
16782
- // The picture every size is made from: the checks' first, or on a tie the judge's.
16979
+ // The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
16980
+ // master's frame where every word stays whole when its centre would cut one, else as rendered.
16783
16981
  image: ImageRef.optional(),
16784
16982
  // Which variant it is (its place among the inputs).
16785
16983
  chosen: z26.number().int().min(0).optional(),
@@ -16792,12 +16990,12 @@ var staticPrefilterNode = defineNode({
16792
16990
  }).strict(),
16793
16991
  outputKinds: { images: "image", image: "image" },
16794
16992
  cost: () => ({ credits: 0, seconds_estimate: 1 }),
16795
- async execute({ inputs, params }) {
16796
- const variants = inputs.images.map((image, index) => ({
16993
+ async execute({ inputs, params, ctx }) {
16994
+ const variants = inputs.images.map((image2, index) => ({
16797
16995
  index,
16798
- sha256: image.sha256,
16799
- ...image.path ? { path: image.path } : {},
16800
- ...image.url ? { url: image.url } : {}
16996
+ sha256: image2.sha256,
16997
+ ...image2.path ? { path: image2.path } : {},
16998
+ ...image2.url ? { url: image2.url } : {}
16801
16999
  }));
16802
17000
  const usage = [];
16803
17001
  const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
@@ -16813,10 +17011,11 @@ var staticPrefilterNode = defineNode({
16813
17011
  { model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
16814
17012
  ) : null;
16815
17013
  const chosen = tie?.chosen ?? shown[0] ?? 0;
17014
+ const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
16816
17015
  return {
16817
17016
  images: kept.length > 0 ? kept : inputs.images,
16818
17017
  order: shown,
16819
- ...inputs.images[chosen] ? { image: inputs.images[chosen] } : {},
17018
+ ...image ? { image } : {},
16820
17019
  chosen,
16821
17020
  ...tie ? { reasoning: tie.reasoning } : {},
16822
17021
  dropped: result.dropped,
@@ -16825,6 +17024,12 @@ var staticPrefilterNode = defineNode({
16825
17024
  };
16826
17025
  }
16827
17026
  });
17027
+ async function framed(picture, frame, size2, ctx) {
17028
+ const source = picture?.path ?? picture?.url;
17029
+ if (!picture || !frame || !size2 || !source) return picture;
17030
+ const bytes = await cutToFrame(await bytesOf(source), frame, size2);
17031
+ return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
17032
+ }
16828
17033
 
16829
17034
  // src/engine/nodes/local/staticSafeFit.ts
16830
17035
  import { z as z27 } from "zod";
@@ -18477,6 +18682,9 @@ export {
18477
18682
  conceptSummary,
18478
18683
  conceptProblems,
18479
18684
  unwrittenProblems,
18685
+ sceneWordProblems,
18686
+ SPEC_SHAPES_HINT,
18687
+ adSpecErrorText,
18480
18688
  readAdSpec,
18481
18689
  withRenderedLettering,
18482
18690
  conceptClassOf,
@@ -18503,6 +18711,7 @@ export {
18503
18711
  STATIC_SAFE_FIT_ROLE,
18504
18712
  STATIC_PLACE_ROLE,
18505
18713
  LOGO_COMPOSITE_ROLE,
18714
+ SERIES_LEAD_ROLE,
18506
18715
  STATIC_FIX_ROLE,
18507
18716
  LOGO_COMPOSITE_DEFAULT,
18508
18717
  nodesWithRole,
@@ -18564,4 +18773,4 @@ export {
18564
18773
  defaultRegistry,
18565
18774
  createEngineFromEnv
18566
18775
  };
18567
- //# sourceMappingURL=chunk-MMTPMFFH.js.map
18776
+ //# sourceMappingURL=chunk-ENTQULTV.js.map