@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.
- package/README.md +76 -21
- package/dist/{chunk-Q6F5UNOU.js → chunk-7THIX4WW.js} +8 -5
- package/dist/{chunk-Q6F5UNOU.js.map → chunk-7THIX4WW.js.map} +1 -1
- package/dist/{chunk-MMTPMFFH.js → chunk-ENTQULTV.js} +350 -141
- package/dist/chunk-ENTQULTV.js.map +1 -0
- package/dist/{chunk-GCTR4MDE.js → chunk-EURJVJQY.js} +2 -2
- package/dist/{chunk-FQOKJYKN.js → chunk-F7LXMR73.js} +17 -4
- package/dist/chunk-F7LXMR73.js.map +1 -0
- package/dist/{chunk-RFZYRG2Q.js → chunk-QLNGHVRT.js} +11 -3
- package/dist/chunk-QLNGHVRT.js.map +1 -0
- package/dist/{chunk-PHLFNYGQ.js → chunk-VNROEZTR.js} +4 -4
- package/dist/cli.js +16353 -12693
- package/dist/cli.js.map +1 -1
- package/dist/{client-GS7T7ETG.js → client-TCDMWAL3.js} +4 -4
- package/dist/engine/index.d.ts +12 -6
- package/dist/engine/index.js +3 -3
- package/dist/{env-SL22S6CM.js → env-ZPCFZJN5.js} +6 -2
- package/dist/output-CW4JBWIM.js +22 -0
- package/dist/{shared-XR54KFMH.js → shared-6I2TP4YQ.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-FQOKJYKN.js.map +0 -1
- package/dist/chunk-MMTPMFFH.js.map +0 -1
- package/dist/chunk-RFZYRG2Q.js.map +0 -1
- package/dist/output-OFDKEPPJ.js +0 -18
- /package/dist/{chunk-GCTR4MDE.js.map → chunk-EURJVJQY.js.map} +0 -0
- /package/dist/{chunk-PHLFNYGQ.js.map → chunk-VNROEZTR.js.map} +0 -0
- /package/dist/{client-GS7T7ETG.js.map → client-TCDMWAL3.js.map} +0 -0
- /package/dist/{env-SL22S6CM.js.map → env-ZPCFZJN5.js.map} +0 -0
- /package/dist/{output-OFDKEPPJ.js.map → output-CW4JBWIM.js.map} +0 -0
- /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-
|
|
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-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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}
|
|
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}"
|
|
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
|
|
10368
|
+
const order = topologicalLayers(graph).flat();
|
|
10299
10369
|
const limit = resolveConcurrency(opts.concurrency);
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
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
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
15860
|
-
if (
|
|
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
|
|
16010
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
16721
|
+
function coverOf(from, to) {
|
|
16588
16722
|
const scale = Math.max(to.width / from.width, to.height / from.height);
|
|
16589
|
-
const
|
|
16590
|
-
const
|
|
16591
|
-
|
|
16592
|
-
|
|
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(
|
|
16648
|
-
|
|
16649
|
-
|
|
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
|
-
|
|
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.
|
|
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((
|
|
16993
|
+
async execute({ inputs, params, ctx }) {
|
|
16994
|
+
const variants = inputs.images.map((image2, index) => ({
|
|
16797
16995
|
index,
|
|
16798
|
-
sha256:
|
|
16799
|
-
...
|
|
16800
|
-
...
|
|
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
|
-
...
|
|
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-
|
|
18776
|
+
//# sourceMappingURL=chunk-ENTQULTV.js.map
|