@koda-sl/baker-cli 0.439.0-dev.33cf4ca99 → 0.439.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +94 -22
  2. package/dist/{chunk-APTNJ2EY.js → chunk-6PP3MBGZ.js} +320 -133
  3. package/dist/chunk-6PP3MBGZ.js.map +1 -0
  4. package/dist/{chunk-UKHD423A.js → chunk-7THIX4WW.js} +8 -5
  5. package/dist/{chunk-UKHD423A.js.map → chunk-7THIX4WW.js.map} +1 -1
  6. package/dist/{chunk-5DJPTQU5.js → chunk-EURJVJQY.js} +2 -2
  7. package/dist/{chunk-PR3CXBAT.js → chunk-F7LXMR73.js} +13 -4
  8. package/dist/chunk-F7LXMR73.js.map +1 -0
  9. package/dist/{chunk-32UJLGZX.js → chunk-QLNGHVRT.js} +11 -3
  10. package/dist/chunk-QLNGHVRT.js.map +1 -0
  11. package/dist/{chunk-KP33GIDZ.js → chunk-VNROEZTR.js} +4 -4
  12. package/dist/cli.js +15941 -12517
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-JM3QBGR2.js → client-TCDMWAL3.js} +4 -4
  15. package/dist/engine/index.d.ts +3 -6
  16. package/dist/engine/index.js +3 -3
  17. package/dist/{env-WIAWLTDN.js → env-ZPCFZJN5.js} +4 -2
  18. package/dist/output-CW4JBWIM.js +22 -0
  19. package/dist/{shared-DJTAT7CM.js → shared-6I2TP4YQ.js} +6 -6
  20. package/package.json +1 -1
  21. package/dist/chunk-32UJLGZX.js.map +0 -1
  22. package/dist/chunk-APTNJ2EY.js.map +0 -1
  23. package/dist/chunk-PR3CXBAT.js.map +0 -1
  24. package/dist/output-UGMWOFQR.js +0 -18
  25. /package/dist/{chunk-5DJPTQU5.js.map → chunk-EURJVJQY.js.map} +0 -0
  26. /package/dist/{chunk-KP33GIDZ.js.map → chunk-VNROEZTR.js.map} +0 -0
  27. /package/dist/{client-JM3QBGR2.js.map → client-TCDMWAL3.js.map} +0 -0
  28. /package/dist/{env-WIAWLTDN.js.map → env-ZPCFZJN5.js.map} +0 -0
  29. /package/dist/{output-UGMWOFQR.js.map → output-CW4JBWIM.js.map} +0 -0
  30. /package/dist/{shared-DJTAT7CM.js.map → shared-6I2TP4YQ.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-5DJPTQU5.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-PR3CXBAT.js";
25
+ } from "./chunk-F7LXMR73.js";
26
26
 
27
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
27
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/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
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
1411
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/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;
@@ -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) {
@@ -6706,6 +6689,7 @@ function textFidelityDirective(strings2, hasFont) {
6706
6689
  return [
6707
6690
  exact,
6708
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.",
6709
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).",
6710
6694
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
6711
6695
  ].join("\n");
@@ -6770,22 +6754,32 @@ var Viewer = z7.object({
6770
6754
  /** Who will see the ad, in the words a marketer would use. */
6771
6755
  who: z7.string().trim().min(1),
6772
6756
  market: Market.optional(),
6773
- /** What they already know about the brand and the offer. */
6774
- 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()),
6775
6759
  temperature: z7.enum(["cold", "warm", "hot"]).optional(),
6776
6760
  /** Where they meet it (feed, Stories, Reels). */
6777
6761
  placement: z7.string().optional()
6778
6762
  }).strict();
6779
- 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({
6780
6770
  description: z7.string().trim().min(1),
6781
- /** A photograph, a designed graphic, or an illustration. */
6782
- 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
+ ),
6783
6776
  /** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
6784
6777
  capture: z7.string().optional(),
6785
6778
  mood: z7.string().optional(),
6786
6779
  /** How the frame is arranged, by what fills each part of it. */
6787
6780
  layout: z7.string().optional()
6788
6781
  }).strict();
6782
+ var Scene = z7.preprocess(withoutFormatName, SceneFields);
6789
6783
  var Brand = z7.object({
6790
6784
  name: z7.string().optional(),
6791
6785
  /** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
@@ -6857,13 +6851,19 @@ var AdSpecSchema = z7.object({
6857
6851
  * editor's line numbers, a product's own label, a platform's badge. The render may draw them
6858
6852
  * and the review never calls them extra words.
6859
6853
  */
6860
- 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
+ ),
6861
6858
  /** The formats the ad is made for; the canvas delivers them. */
6862
6859
  formats: z7.array(z7.string()).optional(),
6863
6860
  /** An optional note on direction for this concept. */
6864
6861
  direction: z7.string().optional(),
6865
6862
  /** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
6866
- 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([])),
6867
6867
  /** Why the reference won and what this ad keeps of it (schema /3). */
6868
6868
  concept: Concept.optional(),
6869
6869
  /**
@@ -6931,6 +6931,49 @@ function unwrittenProblems(spec) {
6931
6931
  }
6932
6932
  return problems;
6933
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
+ }
6934
6977
  function readAdSpec(raw) {
6935
6978
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6936
6979
  const advertiser = raw?.reference?.advertiser;
@@ -10138,7 +10181,7 @@ function checkOrphanNodes(ctx) {
10138
10181
  path: `nodes[${ctx.idToIndex.get(n.id)}]`,
10139
10182
  code: STAGE_CODES.ORPHAN_NODE,
10140
10183
  severity: "warning",
10141
- 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`
10142
10185
  });
10143
10186
  }
10144
10187
  }
@@ -10209,6 +10252,17 @@ function unwrap(schema) {
10209
10252
  }
10210
10253
 
10211
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
+ }
10212
10266
  function heldByCheckpoint(canvas, layer, kinds) {
10213
10267
  if (!kinds?.size) return [];
10214
10268
  return layer.filter((id) => {
@@ -10297,45 +10351,63 @@ var Engine = class {
10297
10351
  return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
10298
10352
  }
10299
10353
  async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
10300
- const layers = topologicalLayers(graph);
10354
+ const order = topologicalLayers(graph).flat();
10301
10355
  const limit = resolveConcurrency(opts.concurrency);
10302
- for (const layer of layers) {
10303
- this.assertLayerMayRun(canvas, layer, opts);
10304
- const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
10305
- if (opts.signal?.aborted) {
10306
- return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
10307
- }
10308
- this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10309
- return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
10310
- if (r.cached) counters.cachedNodes++;
10311
- counters.totalCredits += r.credits;
10312
- const node = canvas.nodes.find((n) => n.id === nodeId);
10313
- if (node) {
10314
- const run = {
10315
- node_id: nodeId,
10316
- node_type: node.type,
10317
- cached: r.cached,
10318
- duration_ms: r.durationMs,
10319
- credits: r.credits
10320
- };
10321
- nodeRuns.push(run);
10322
- this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10323
- }
10324
- });
10325
- });
10326
- const failures = [];
10327
- settled.forEach((result, i) => {
10328
- const nodeId = layer[i];
10329
- if (result.status === "rejected" && nodeId) {
10330
- if (result.reason instanceof RunAbortedError) return;
10331
- failures.push({ nodeId, reason: result.reason });
10332
- 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) });
10333
10372
  }
10334
- });
10335
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10336
- if (failures.length === 1 && failures[0]) throw failures[0].reason;
10337
- 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());
10338
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] ?? {} });
10339
10411
  }
10340
10412
  /** Progress consumers are observers only — an exception there must never fail the run. */
10341
10413
  emitProgress(opts, event) {
@@ -10427,20 +10499,6 @@ var Engine = class {
10427
10499
  this.logOutputWarnings(node.id, outputsObj);
10428
10500
  return { cached: false, credits, durationMs: elapsed };
10429
10501
  }
10430
- /**
10431
- * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
10432
- * never leaves a layer half billed: a signal, the cost cap and a checkpoint.
10433
- */
10434
- assertLayerMayRun(canvas, layer, opts) {
10435
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
10436
- const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
10437
- if (held.length > 0) {
10438
- throw new RunAbortedError(
10439
- "checkpoint",
10440
- `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10441
- );
10442
- }
10443
- }
10444
10502
  /** A cache hit replayed as this node's result: its outputs written, and free. */
10445
10503
  async replayCached(node, cacheKey, outputs, writer) {
10446
10504
  const cacheT0 = Date.now();
@@ -14605,6 +14663,18 @@ function boxesOfLine(line, boxes) {
14605
14663
  return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
14606
14664
  });
14607
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
+ }
14608
14678
  function shownBoxes(boxes, lines, clear) {
14609
14679
  const linesOfBox = /* @__PURE__ */ new Map();
14610
14680
  for (const line of lines) {
@@ -14988,7 +15058,7 @@ async function videoDuration(video) {
14988
15058
  return null;
14989
15059
  }
14990
15060
  }
14991
- async function askGateway(model, parts, timeoutMs, spent) {
15061
+ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
14992
15062
  const key = openRouterKey();
14993
15063
  if (!key) return null;
14994
15064
  try {
@@ -14998,7 +15068,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
14998
15068
  body: JSON.stringify({
14999
15069
  model,
15000
15070
  messages: [{ role: "user", content: parts }],
15001
- usage: { include: true }
15071
+ usage: { include: true },
15072
+ ...reasoning ? { reasoning } : {}
15002
15073
  }),
15003
15074
  signal: AbortSignal.timeout(timeoutMs)
15004
15075
  });
@@ -15047,7 +15118,7 @@ async function askAboutStills(images, prompt, onSpend) {
15047
15118
  if (!hasOpenRouterKey()) return null;
15048
15119
  return await ask(images, prompt, (usage) => onSpend?.(usage));
15049
15120
  }
15050
- async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15121
+ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
15051
15122
  if (!hasOpenRouterKey()) return null;
15052
15123
  return await askGateway(
15053
15124
  model,
@@ -15058,7 +15129,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
15058
15129
  )
15059
15130
  ],
15060
15131
  timeoutMs,
15061
- (usage) => onSpend?.(usage)
15132
+ (usage) => onSpend?.(usage),
15133
+ reasoning
15062
15134
  );
15063
15135
  }
15064
15136
  async function askAboutText(model, prompt, onSpend) {
@@ -15222,7 +15294,9 @@ async function readTextBoxes(image, opts = {}) {
15222
15294
  return { status: "not_checked" };
15223
15295
  }
15224
15296
  for (let attempt = 0; attempt < 2; attempt++) {
15225
- 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
+ });
15226
15300
  const boxes = textBoxesFromReader(answer);
15227
15301
  if (boxes) return { status: "read", boxes };
15228
15302
  }
@@ -15312,6 +15386,8 @@ var StaticFinding = z21.object({
15312
15386
  });
15313
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;
15314
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;
15315
15391
  function classifyStaticFindings(json) {
15316
15392
  const findings = json?.findings;
15317
15393
  if (!Array.isArray(findings)) return [];
@@ -15320,7 +15396,7 @@ function classifyStaticFindings(json) {
15320
15396
  if (!parsed.success) return [];
15321
15397
  const { severity, format, what } = parsed.data;
15322
15398
  if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
15323
- 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;
15324
15400
  return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
15325
15401
  });
15326
15402
  }
@@ -15331,7 +15407,7 @@ var CopyRead = z21.object({
15331
15407
  seen: z21.string().nullable()
15332
15408
  });
15333
15409
  function comparableCopy(text2) {
15334
- 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();
15335
15411
  }
15336
15412
  function copyFindings(json, copy, formats, scrolled = {}) {
15337
15413
  if (copy.length === 0) return [];
@@ -15706,7 +15782,7 @@ function contextItems(opts) {
15706
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."
15707
15783
  ],
15708
15784
  sizes: opts.formats.length > 1 ? [
15709
- "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.`
15710
15786
  ] : []
15711
15787
  };
15712
15788
  }
@@ -15759,6 +15835,7 @@ function mostOfALine(chunk, copy) {
15759
15835
  });
15760
15836
  }
15761
15837
  var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
15838
+ var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
15762
15839
  var COPY_SIZE_SHARE = 0.8;
15763
15840
  function extraWordFindings(format, extra, boxes, copyBoxes) {
15764
15841
  if (extra.length === 0) return [];
@@ -15776,7 +15853,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15776
15853
  const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
15777
15854
  const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
15778
15855
  const asCopy = extra.filter((chunk) => !printed(chunk));
15779
- 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));
15780
15858
  return [
15781
15859
  ...asCopy.length > 0 ? [
15782
15860
  {
@@ -15785,6 +15863,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
15785
15863
  what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
15786
15864
  }
15787
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
+ ] : [],
15788
15873
  ...inScene.length > 0 ? [
15789
15874
  {
15790
15875
  severity: "warning",
@@ -15849,7 +15934,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
15849
15934
  const element = claimedBoxes(f.what, boxes, buttons);
15850
15935
  if (element.length === 0) return f;
15851
15936
  const named = claimedEdges(f.what);
15852
- 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));
15853
15939
  return atEdge ? f : { ...f, severity: "warning" };
15854
15940
  });
15855
15941
  }
@@ -15857,9 +15943,11 @@ function claimedEdges(what) {
15857
15943
  const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
15858
15944
  return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
15859
15945
  }
15946
+ var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
15947
+ var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
15860
15948
  function claimedBoxes(what, boxes, buttons) {
15861
- if (/\b(?:logos?|wordmark|mark)\b/i.test(what)) return boxes.filter((b) => b.kind === "logo");
15862
- 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));
15863
15951
  return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
15864
15952
  }
15865
15953
  async function bytesOf(source) {
@@ -15989,6 +16077,22 @@ function everySizeChecks(opts) {
15989
16077
  onSpend: opts.onSpend
15990
16078
  };
15991
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
+ }
15992
16096
  async function reviewRenderedStatic(opts) {
15993
16097
  const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
15994
16098
  if (measured.length === 0) return null;
@@ -16019,7 +16123,14 @@ async function reviewRenderedStatic(opts) {
16019
16123
  if (answer === null) return unreviewed(shape, measuredText);
16020
16124
  const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
16021
16125
  const boxesByFormat = read ? Object.fromEntries(checks.flatMap((c, i) => c.status === "checked" ? [[formats[i] ?? "", c.boxes]] : [])) : {};
16022
- 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];
16023
16134
  }
16024
16135
 
16025
16136
  // src/engine/nodes/local/staticNative.ts
@@ -16263,7 +16374,9 @@ function boxesToPlace(boxes, format, opts = {}) {
16263
16374
  const past = (box) => bandViolations([box], format, { placements })[0];
16264
16375
  const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
16265
16376
  const logoWords = opts.copy.filter((l) => roleKey2(l.role) === "logo").map((l) => l.text);
16266
- 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);
16267
16380
  }
16268
16381
  function planSafeFit(boxes, format, opts = {}) {
16269
16382
  const area = blockingArea(format, opts.placements);
@@ -16591,12 +16704,64 @@ import { z as z26 } from "zod";
16591
16704
  // src/engine/nodes/local/lib/variant-prefilter.ts
16592
16705
  import { createHash as createHash3 } from "crypto";
16593
16706
  import sharp8 from "sharp";
16594
- function coverFitBoxes(boxes, from, to) {
16707
+ function coverOf(from, to) {
16595
16708
  const scale = Math.max(to.width / from.width, to.height / from.height);
16596
- const offX = (from.width * scale - to.width) / 2;
16597
- const offY = (from.height * scale - to.height) / 2;
16598
- const x = (f) => (f * from.width * scale - offX) / to.width;
16599
- 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;
16600
16765
  return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
16601
16766
  }
16602
16767
  var NO_LOGO = "The brand's logo was not found on it.";
@@ -16650,34 +16815,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
16650
16815
  });
16651
16816
  const format = plan.master.format;
16652
16817
  const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
16818
+ const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16653
16819
  const checked = await Promise.all(
16654
- variants.map(async (variant) => {
16655
- const source = variant.path ?? variant.url;
16656
- if (!source) return { index: variant.index, blocking: null };
16657
- try {
16658
- const bytes = await bytesOf(source);
16659
- const { width = 0, height = 0 } = await sharp8(bytes).metadata();
16660
- const check = await checkStaticImage(bytes, {
16661
- format,
16662
- lines,
16663
- allowed: brand ? [brand] : [],
16664
- ...grade,
16665
- toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
16666
- ...opts.read ? { read: opts.read } : {},
16667
- ...opts.onSpend ? { onSpend: opts.onSpend } : {}
16668
- });
16669
- if (check.status === "not_checked") return { index: variant.index, blocking: null };
16670
- const prefix = `${format}: `;
16671
- const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
16672
- const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
16673
- return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
16674
- } catch {
16675
- return { index: variant.index, blocking: null };
16676
- }
16677
- })
16820
+ variants.map(
16821
+ (variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
16822
+ )
16678
16823
  );
16679
16824
  const outcome = prefilterOutcome(checked);
16680
- 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
+ }
16681
16857
  }
16682
16858
  function seededOrder(indexes, variants) {
16683
16859
  const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
@@ -16753,7 +16929,7 @@ async function readSpec(ref) {
16753
16929
  }
16754
16930
  var staticPrefilterNode = defineNode({
16755
16931
  id: "static_prefilter",
16756
- version: "1.0.0",
16932
+ version: "1.1.0",
16757
16933
  category: "image",
16758
16934
  location: "local",
16759
16935
  // Reads each variant's pixels itself, from its file or its durable URL, so the variants
@@ -16786,7 +16962,8 @@ var staticPrefilterNode = defineNode({
16786
16962
  images: z26.array(ImageRef).min(1),
16787
16963
  // Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
16788
16964
  order: z26.array(z26.number().int().min(0)).optional(),
16789
- // 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.
16790
16967
  image: ImageRef.optional(),
16791
16968
  // Which variant it is (its place among the inputs).
16792
16969
  chosen: z26.number().int().min(0).optional(),
@@ -16799,12 +16976,12 @@ var staticPrefilterNode = defineNode({
16799
16976
  }).strict(),
16800
16977
  outputKinds: { images: "image", image: "image" },
16801
16978
  cost: () => ({ credits: 0, seconds_estimate: 1 }),
16802
- async execute({ inputs, params }) {
16803
- const variants = inputs.images.map((image, index) => ({
16979
+ async execute({ inputs, params, ctx }) {
16980
+ const variants = inputs.images.map((image2, index) => ({
16804
16981
  index,
16805
- sha256: image.sha256,
16806
- ...image.path ? { path: image.path } : {},
16807
- ...image.url ? { url: image.url } : {}
16982
+ sha256: image2.sha256,
16983
+ ...image2.path ? { path: image2.path } : {},
16984
+ ...image2.url ? { url: image2.url } : {}
16808
16985
  }));
16809
16986
  const usage = [];
16810
16987
  const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
@@ -16820,10 +16997,11 @@ var staticPrefilterNode = defineNode({
16820
16997
  { model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
16821
16998
  ) : null;
16822
16999
  const chosen = tie?.chosen ?? shown[0] ?? 0;
17000
+ const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
16823
17001
  return {
16824
17002
  images: kept.length > 0 ? kept : inputs.images,
16825
17003
  order: shown,
16826
- ...inputs.images[chosen] ? { image: inputs.images[chosen] } : {},
17004
+ ...image ? { image } : {},
16827
17005
  chosen,
16828
17006
  ...tie ? { reasoning: tie.reasoning } : {},
16829
17007
  dropped: result.dropped,
@@ -16832,6 +17010,12 @@ var staticPrefilterNode = defineNode({
16832
17010
  };
16833
17011
  }
16834
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
+ }
16835
17019
 
16836
17020
  // src/engine/nodes/local/staticSafeFit.ts
16837
17021
  import { z as z27 } from "zod";
@@ -18484,6 +18668,9 @@ export {
18484
18668
  conceptSummary,
18485
18669
  conceptProblems,
18486
18670
  unwrittenProblems,
18671
+ sceneWordProblems,
18672
+ SPEC_SHAPES_HINT,
18673
+ adSpecErrorText,
18487
18674
  readAdSpec,
18488
18675
  withRenderedLettering,
18489
18676
  conceptClassOf,
@@ -18571,4 +18758,4 @@ export {
18571
18758
  defaultRegistry,
18572
18759
  createEngineFromEnv
18573
18760
  };
18574
- //# sourceMappingURL=chunk-APTNJ2EY.js.map
18761
+ //# sourceMappingURL=chunk-6PP3MBGZ.js.map