@koda-sl/baker-cli 0.434.0-dev.42b55fa66 → 0.434.0-dev.4dbd56be6
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 +79 -22
- package/dist/{chunk-MMTPMFFH.js → chunk-7NKAKF3K.js} +438 -149
- package/dist/chunk-7NKAKF3K.js.map +1 -0
- 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-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 +16765 -12592
- package/dist/cli.js.map +1 -1
- package/dist/{client-GS7T7ETG.js → client-TCDMWAL3.js} +4 -4
- package/dist/engine/index.d.ts +13 -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,18 @@ 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
|
+
// A translation of the lead: the language (BCP-47) its words are in.
|
|
4411
|
+
language: z4.string().min(2).optional()
|
|
4412
|
+
}).strict().optional()
|
|
4419
4413
|
}).strict().optional();
|
|
4420
4414
|
var CanvasSchema = z4.object({
|
|
4421
4415
|
schema: z4.literal("baker-canvas/1"),
|
|
@@ -6706,6 +6700,7 @@ function textFidelityDirective(strings2, hasFont) {
|
|
|
6706
6700
|
return [
|
|
6707
6701
|
exact,
|
|
6708
6702
|
"Render NO other words anywhere in the image \u2014 no invented labels, captions, sub-headers, or filler text.",
|
|
6703
|
+
"Any bill, quote, receipt, screen or price tag in the scene shows no readable prices or amounts beyond these strings: its figures are blurred or out of focus.",
|
|
6709
6704
|
"For any brand name or uncommon word, lock its letters exactly as written (spell it out letter-by-letter if that helps the characters render correctly).",
|
|
6710
6705
|
"Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
|
|
6711
6706
|
].join("\n");
|
|
@@ -6770,22 +6765,32 @@ var Viewer = z7.object({
|
|
|
6770
6765
|
/** Who will see the ad, in the words a marketer would use. */
|
|
6771
6766
|
who: z7.string().trim().min(1),
|
|
6772
6767
|
market: Market.optional(),
|
|
6773
|
-
/** What they already know about the brand and the offer. */
|
|
6774
|
-
knows: z7.string().optional(),
|
|
6768
|
+
/** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
|
|
6769
|
+
knows: z7.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z7.string().optional()),
|
|
6775
6770
|
temperature: z7.enum(["cold", "warm", "hot"]).optional(),
|
|
6776
6771
|
/** Where they meet it (feed, Stories, Reels). */
|
|
6777
6772
|
placement: z7.string().optional()
|
|
6778
6773
|
}).strict();
|
|
6779
|
-
var
|
|
6774
|
+
var withoutFormatName = (v) => {
|
|
6775
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
|
|
6776
|
+
return v;
|
|
6777
|
+
const { concept: _format, ...scene } = v;
|
|
6778
|
+
return scene;
|
|
6779
|
+
};
|
|
6780
|
+
var SceneFields = z7.object({
|
|
6780
6781
|
description: z7.string().trim().min(1),
|
|
6781
|
-
/** A photograph, a designed graphic, or an illustration. */
|
|
6782
|
-
look: z7.
|
|
6782
|
+
/** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
|
|
6783
|
+
look: z7.preprocess(
|
|
6784
|
+
(v) => typeof v !== "string" ? v : /^photo/i.test(v.trim()) ? "photo" : /^illustrat/i.test(v.trim()) ? "illustration" : /^(design|graphic)/i.test(v.trim()) ? "designed" : v,
|
|
6785
|
+
z7.enum(["photo", "designed", "illustration"])
|
|
6786
|
+
),
|
|
6783
6787
|
/** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
|
|
6784
6788
|
capture: z7.string().optional(),
|
|
6785
6789
|
mood: z7.string().optional(),
|
|
6786
6790
|
/** How the frame is arranged, by what fills each part of it. */
|
|
6787
6791
|
layout: z7.string().optional()
|
|
6788
6792
|
}).strict();
|
|
6793
|
+
var Scene = z7.preprocess(withoutFormatName, SceneFields);
|
|
6789
6794
|
var Brand = z7.object({
|
|
6790
6795
|
name: z7.string().optional(),
|
|
6791
6796
|
/** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
|
|
@@ -6857,13 +6862,19 @@ var AdSpecSchema = z7.object({
|
|
|
6857
6862
|
* editor's line numbers, a product's own label, a platform's badge. The render may draw them
|
|
6858
6863
|
* and the review never calls them extra words.
|
|
6859
6864
|
*/
|
|
6860
|
-
scene_text: z7.
|
|
6865
|
+
scene_text: z7.preprocess(
|
|
6866
|
+
(v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
|
|
6867
|
+
z7.array(z7.string().trim().min(1)).default([])
|
|
6868
|
+
),
|
|
6861
6869
|
/** The formats the ad is made for; the canvas delivers them. */
|
|
6862
6870
|
formats: z7.array(z7.string()).optional(),
|
|
6863
6871
|
/** An optional note on direction for this concept. */
|
|
6864
6872
|
direction: z7.string().optional(),
|
|
6865
6873
|
/** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
|
|
6866
|
-
mechanism: z7.
|
|
6874
|
+
mechanism: z7.preprocess((v) => {
|
|
6875
|
+
const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
|
|
6876
|
+
return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
|
|
6877
|
+
}, z7.array(Mechanism).default([])),
|
|
6867
6878
|
/** Why the reference won and what this ad keeps of it (schema /3). */
|
|
6868
6879
|
concept: Concept.optional(),
|
|
6869
6880
|
/**
|
|
@@ -6931,6 +6942,49 @@ function unwrittenProblems(spec) {
|
|
|
6931
6942
|
}
|
|
6932
6943
|
return problems;
|
|
6933
6944
|
}
|
|
6945
|
+
var QUOTED = /"([^"]{2,})"|“([^”]{2,})”|(?<![\p{L}\p{N}])['‘]([^'’]{2,}?)['’](?![\p{L}\p{N}])/gu;
|
|
6946
|
+
var FIGURE = /[£$€]\s?\d[\d.,]*|\d+(?:[.,]\d+)?\s?%/g;
|
|
6947
|
+
var comparable = (text2) => text2.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}£$€%]+/gu, " ").trim();
|
|
6948
|
+
function sceneWordProblems(spec) {
|
|
6949
|
+
const held = [...spec.copy.map((l) => l.text), ...spec.scene_text].map(comparable);
|
|
6950
|
+
const holds = (text2) => {
|
|
6951
|
+
const want = comparable(text2);
|
|
6952
|
+
return want.length === 0 || held.some((h) => h.includes(want));
|
|
6953
|
+
};
|
|
6954
|
+
const scene = [spec.scene.description, spec.scene.layout ?? ""].join(" ");
|
|
6955
|
+
const quoted = [...scene.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter((q) => !holds(q));
|
|
6956
|
+
const figures = (scene.match(FIGURE) ?? []).map((f) => f.trim()).filter((f) => !holds(f));
|
|
6957
|
+
const unheld = [.../* @__PURE__ */ new Set([...quoted, ...figures])];
|
|
6958
|
+
if (unheld.length === 0) return [];
|
|
6959
|
+
return [
|
|
6960
|
+
`The scene asks the picture to show ${unheld.map((w) => `"${w}"`).join(", ")}, which the copy does not hold: the picture would print it and the run would then spend a round erasing it. Put each one the ad needs in \`copy\` as an \`in_scene\` line with \`on\` (the bill, the phone), or describe that surface without words (blank, blurred, face-down).`
|
|
6961
|
+
];
|
|
6962
|
+
}
|
|
6963
|
+
var FIELD_SHAPES = {
|
|
6964
|
+
copy: '"copy": [{"text": "Book your free survey", "lettering": "designed", "role": "cta"}]',
|
|
6965
|
+
scene: '"scene": {"description": "a family on a sunny roof terrace", "look": "photo"}',
|
|
6966
|
+
colors: '"colors": [{"object": "the button", "name": "deep green", "hex": "#1F5C3A"}]',
|
|
6967
|
+
references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
|
|
6968
|
+
third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
|
|
6969
|
+
mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
|
|
6970
|
+
viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
|
|
6971
|
+
};
|
|
6972
|
+
var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
|
|
6973
|
+
function adSpecErrorText(e) {
|
|
6974
|
+
if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
|
|
6975
|
+
const items = /* @__PURE__ */ new Map();
|
|
6976
|
+
for (const issue of e.issues) {
|
|
6977
|
+
const [field2, index, key] = issue.path;
|
|
6978
|
+
const at2 = typeof index === "number" ? `${String(field2)}[${index}]` : issue.path.map(String).join(".") || "the spec";
|
|
6979
|
+
const what = issue.code === "unrecognized_keys" ? `no field ${issue.keys.map((k) => `"${k}"`).join(", ")}` : key !== void 0 && typeof index === "number" ? `"${String(key)}" ${issue.message.replace(/^Invalid input: /, "")}` : issue.message.replace(/^Invalid input: /, "");
|
|
6980
|
+
items.set(at2, [...items.get(at2) ?? [], what]);
|
|
6981
|
+
}
|
|
6982
|
+
const fields = [...new Set(e.issues.map((i) => String(i.path[0] ?? "")))].filter((f) => FIELD_SHAPES[f]);
|
|
6983
|
+
return [
|
|
6984
|
+
...[...items].map(([at2, whats]) => `${at2}: ${[...new Set(whats)].join("; ")}`),
|
|
6985
|
+
...fields.map((f) => `Shape: ${FIELD_SHAPES[f]}`)
|
|
6986
|
+
].join("\n");
|
|
6987
|
+
}
|
|
6934
6988
|
function readAdSpec(raw) {
|
|
6935
6989
|
if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
|
|
6936
6990
|
const advertiser = raw?.reference?.advertiser;
|
|
@@ -7421,7 +7475,7 @@ function pictureInstruction(p, ctx) {
|
|
|
7421
7475
|
const keep = p.spec?.keep ? ` Keep ${plain(p.spec.keep, ctx.clean)}.` : "";
|
|
7422
7476
|
switch (p.role) {
|
|
7423
7477
|
case "base":
|
|
7424
|
-
return ctx.job === "fix" ? "the current version of this ad, to edit." : "the finished ad, to re-lay for this shape.";
|
|
7478
|
+
return ctx.job === "fix" ? "the current version of this ad, to edit." : ctx.job === "series" ? "the finished ad this series repeats, to copy exactly with new words." : "the finished ad, to re-lay for this shape.";
|
|
7425
7479
|
case "identity":
|
|
7426
7480
|
return identityInstruction(p, what, keep, ctx.flowing);
|
|
7427
7481
|
// The user saw one label come back two ways (E4 gallery, BENCH.md § "E1 v2"): the label is the product's.
|
|
@@ -7800,6 +7854,10 @@ function deliverableSection(c) {
|
|
|
7800
7854
|
const change = sentence(c.opts.instruction?.trim() || "correct what is wrong");
|
|
7801
7855
|
return c.flowing ? `Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else in Image 1 exactly the same, preserving its picture, layout, colours, lettering and every word.` : `Edit: Image 1 is the current version of this ad. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the colours, the lettering and every word.`;
|
|
7802
7856
|
}
|
|
7857
|
+
if (c.job === "series") {
|
|
7858
|
+
const change = sentence(c.opts.instruction?.trim() || "set the new words");
|
|
7859
|
+
return c.flowing ? `Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}: ${change} Keep everything else in Image 1 exactly the same, preserving its picture, layout, background, shapes, colours, lettering style and logo, each word in the place and style of the one it replaces.` : `Edit: Image 1 is a finished ad of this series. Make the next ad of the same series${forBrand}. ${capitalize(change)} Keep everything else exactly as it is in Image 1: the picture, the layout, the background, the shapes, the colours, the lettering style and the logo, each new line in the place, size and style of the line it replaces.`;
|
|
7860
|
+
}
|
|
7803
7861
|
if (c.job === "relay") {
|
|
7804
7862
|
return c.flowing ? `Image 1 is a finished ad. Recompose this same ad as a ${noun}${forBrand}: the same picture, colours, lettering and every word, rearranged for the new shape, with more of the scene revealed at the edges.` : `Deliverable: the same ad as Image 1, re-laid as a finished ${noun}${forBrand}. Keep the same picture, colours, lettering and every word, rearranged for the new shape; show more of the scene rather than cropping it.`;
|
|
7805
7863
|
}
|
|
@@ -8383,14 +8441,16 @@ function scaleText(plan, spec, flowing, zoned, short) {
|
|
|
8383
8441
|
const rows = hasRows(spec) ? flowing ? " Rows, columns and lists are restacked for the tall frame, one item per line, each larger." : " Rows, columns and lists are restacked for the tall frame, one item per line, each larger, rather than shrunk." : "";
|
|
8384
8442
|
return flowing ? `The words are set for this frame, larger than in Image 1: the headline large enough to fill most of the band's width, and every line, the logo and the button spread through the band's height, inside its edges.${rows}` : `Scale: larger than in Image 1: the headline set large enough to fill most of the band's width; every line, the logo and the button spread through the band's height, inside its edges, never a small block in its middle.${rows}`;
|
|
8385
8443
|
}
|
|
8386
|
-
var
|
|
8444
|
+
var COVERED_HEADROOM = { top: 0.06, bottom: 0.08, side: 0.02 };
|
|
8445
|
+
var NO_HEADROOM = { top: 0, bottom: 0, side: 0 };
|
|
8387
8446
|
var pct = (share) => `${Math.round(share * 100)}%`;
|
|
8388
8447
|
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
8448
|
const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
|
|
8449
|
+
const room = isTall(plan) && covered ? COVERED_HEADROOM : NO_HEADROOM;
|
|
8450
|
+
const top = plan.safe.top + room.top;
|
|
8451
|
+
const bottom = plan.safe.bottom + room.bottom;
|
|
8452
|
+
const side = Math.max(plan.safe.left, plan.safe.right) + room.side;
|
|
8453
|
+
const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
|
|
8394
8454
|
if (short && !flowing) {
|
|
8395
8455
|
const around = isTall(plan) && covered ? "only the ad's own picture, larger, never a flat colour, a panel or a square" : "only the ad's own picture";
|
|
8396
8456
|
return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
|
|
@@ -8399,7 +8459,7 @@ function zonesText(plan, flowing, ground, short) {
|
|
|
8399
8459
|
return flowing ? `The logo, every line and the button sit ${band}; around them the frame shows only the ad's own picture.` : `Zones: the logo, every line and the button sit ${band}; around them, only the ad's own picture.`;
|
|
8400
8460
|
}
|
|
8401
8461
|
const picture = ground ? "Image 1's background with its shapes and graphics enlarged in the brand's colours" : "Image 1's people, product and setting extended";
|
|
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}
|
|
8462
|
+
return flowing ? `The words' band runs ${band}, holding the logo, every line and the button. Above and below it the app's interface covers the frame, so there it shows only the ad's own picture, larger: ${picture}, edge to edge.` : `Zones: the app's buttons and captions cover the top and the bottom of this frame, hiding any word there. The logo, every line and the button sit in one band, ${band}: the headline starts below the top fifth, and the button ends directly under the last line, never at the bottom edge where a feed ad puts it. Above and below it, where the app's interface covers the frame, only the ad's own picture, larger: ${picture}, never a flat empty colour, a panel or a square on a background.`;
|
|
8403
8463
|
}
|
|
8404
8464
|
function adaptBrief(spec, profile, opts) {
|
|
8405
8465
|
const { plan, images } = opts;
|
|
@@ -8468,6 +8528,7 @@ var STATIC_ADAPT_ROLE = "static_adapt";
|
|
|
8468
8528
|
var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
|
|
8469
8529
|
var STATIC_PLACE_ROLE = "static_place";
|
|
8470
8530
|
var LOGO_COMPOSITE_ROLE = "logo_composite";
|
|
8531
|
+
var SERIES_LEAD_ROLE = "series_lead";
|
|
8471
8532
|
var STATIC_FIX_ROLE = "static_fix";
|
|
8472
8533
|
var LOGO_COMPOSITE_DEFAULT = false;
|
|
8473
8534
|
var ADAPT_IMAGE_SIZE = "2K";
|
|
@@ -8487,6 +8548,19 @@ function rolesById(canvas) {
|
|
|
8487
8548
|
}
|
|
8488
8549
|
return out;
|
|
8489
8550
|
}
|
|
8551
|
+
function relaidPictureRole(canvas) {
|
|
8552
|
+
const roles = rolesById(canvas);
|
|
8553
|
+
const final = nodesOf(canvas).find((n) => n.id === "final" && n.type === "collect");
|
|
8554
|
+
const labels = final?.params?.labels;
|
|
8555
|
+
const images = final?.inputs?.images;
|
|
8556
|
+
if (!Array.isArray(labels) || !Array.isArray(images)) return {};
|
|
8557
|
+
const at2 = images.findIndex((ref) => {
|
|
8558
|
+
const from = typeof ref === "string" ? /^\$ref:([^.]+)\./.exec(ref)?.[1] : void 0;
|
|
8559
|
+
return from !== void 0 && roles[from] === STATIC_MASTER_ROLE;
|
|
8560
|
+
});
|
|
8561
|
+
const format = labels[at2];
|
|
8562
|
+
return typeof format === "string" ? { [`last_${formatSlug(format)}`]: STATIC_MASTER_ROLE } : {};
|
|
8563
|
+
}
|
|
8490
8564
|
function formatSlug(format) {
|
|
8491
8565
|
return format.replace(":", "x").replace(".", "_");
|
|
8492
8566
|
}
|
|
@@ -10136,7 +10210,7 @@ function checkOrphanNodes(ctx) {
|
|
|
10136
10210
|
path: `nodes[${ctx.idToIndex.get(n.id)}]`,
|
|
10137
10211
|
code: STAGE_CODES.ORPHAN_NODE,
|
|
10138
10212
|
severity: "warning",
|
|
10139
|
-
message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}"
|
|
10213
|
+
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
10214
|
});
|
|
10141
10215
|
}
|
|
10142
10216
|
}
|
|
@@ -10207,6 +10281,17 @@ function unwrap(schema) {
|
|
|
10207
10281
|
}
|
|
10208
10282
|
|
|
10209
10283
|
// src/engine/engine/executor.ts
|
|
10284
|
+
function throwRunOutcome(signal, failures, held) {
|
|
10285
|
+
if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
10286
|
+
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
10287
|
+
if (failures.length > 1) throw new LayerExecutionError([...failures]);
|
|
10288
|
+
if (held.length > 0) {
|
|
10289
|
+
throw new RunAbortedError(
|
|
10290
|
+
"checkpoint",
|
|
10291
|
+
`stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
|
|
10292
|
+
);
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10210
10295
|
function heldByCheckpoint(canvas, layer, kinds) {
|
|
10211
10296
|
if (!kinds?.size) return [];
|
|
10212
10297
|
return layer.filter((id) => {
|
|
@@ -10295,45 +10380,63 @@ var Engine = class {
|
|
|
10295
10380
|
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
10296
10381
|
}
|
|
10297
10382
|
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
10298
|
-
const
|
|
10383
|
+
const order = topologicalLayers(graph).flat();
|
|
10299
10384
|
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) });
|
|
10385
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
|
|
10386
|
+
const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
|
|
10387
|
+
const running = /* @__PURE__ */ new Map();
|
|
10388
|
+
const failures = [];
|
|
10389
|
+
const held = [];
|
|
10390
|
+
const dispatch = (nodeId) => {
|
|
10391
|
+
waitingOn.delete(nodeId);
|
|
10392
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
10393
|
+
const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
|
|
10394
|
+
(r) => {
|
|
10395
|
+
this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
|
|
10396
|
+
for (const deps of waitingOn.values()) deps.delete(nodeId);
|
|
10397
|
+
},
|
|
10398
|
+
(reason) => {
|
|
10399
|
+
failures.push({ nodeId, reason });
|
|
10400
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
|
|
10331
10401
|
}
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10402
|
+
);
|
|
10403
|
+
running.set(
|
|
10404
|
+
nodeId,
|
|
10405
|
+
settled.finally(() => running.delete(nodeId))
|
|
10406
|
+
);
|
|
10407
|
+
};
|
|
10408
|
+
const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
|
|
10409
|
+
for (; ; ) {
|
|
10410
|
+
const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
|
|
10411
|
+
for (const nodeId of ready) {
|
|
10412
|
+
if (running.size >= limit) break;
|
|
10413
|
+
if (isHeld(nodeId)) {
|
|
10414
|
+
waitingOn.delete(nodeId);
|
|
10415
|
+
held.push(nodeId);
|
|
10416
|
+
} else dispatch(nodeId);
|
|
10417
|
+
}
|
|
10418
|
+
if (running.size === 0) break;
|
|
10419
|
+
await Promise.race(running.values());
|
|
10336
10420
|
}
|
|
10421
|
+
const rank = new Map(order.map((id, i) => [id, i]));
|
|
10422
|
+
failures.sort((a, b) => (rank.get(a.nodeId) ?? 0) - (rank.get(b.nodeId) ?? 0));
|
|
10423
|
+
throwRunOutcome(opts.signal, failures, held);
|
|
10424
|
+
}
|
|
10425
|
+
/** A settled node counted, recorded and announced. */
|
|
10426
|
+
recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
|
|
10427
|
+
if (r.cached) counters.cachedNodes++;
|
|
10428
|
+
counters.totalCredits += r.credits;
|
|
10429
|
+
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
10430
|
+
if (!node) return;
|
|
10431
|
+
const run = {
|
|
10432
|
+
node_id: nodeId,
|
|
10433
|
+
node_type: node.type,
|
|
10434
|
+
cached: r.cached,
|
|
10435
|
+
duration_ms: r.durationMs,
|
|
10436
|
+
credits: r.credits
|
|
10437
|
+
};
|
|
10438
|
+
nodeRuns.push(run);
|
|
10439
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
10337
10440
|
}
|
|
10338
10441
|
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
10339
10442
|
emitProgress(opts, event) {
|
|
@@ -10425,20 +10528,6 @@ var Engine = class {
|
|
|
10425
10528
|
this.logOutputWarnings(node.id, outputsObj);
|
|
10426
10529
|
return { cached: false, credits, durationMs: elapsed };
|
|
10427
10530
|
}
|
|
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
10531
|
/** A cache hit replayed as this node's result: its outputs written, and free. */
|
|
10443
10532
|
async replayCached(node, cacheKey, outputs, writer) {
|
|
10444
10533
|
const cacheT0 = Date.now();
|
|
@@ -14603,6 +14692,18 @@ function boxesOfLine(line, boxes) {
|
|
|
14603
14692
|
return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
|
|
14604
14693
|
});
|
|
14605
14694
|
}
|
|
14695
|
+
var BUTTON_PAD = 0.7;
|
|
14696
|
+
var isButtonLine = (line) => /^(cta|button|call_to_action)$/.test(roleKey2(line.role));
|
|
14697
|
+
function buttonShape(box) {
|
|
14698
|
+
const pad = BUTTON_PAD * (box.y1 - box.y0);
|
|
14699
|
+
return {
|
|
14700
|
+
...box,
|
|
14701
|
+
x0: Math.max(0, box.x0 - pad),
|
|
14702
|
+
y0: Math.max(0, box.y0 - pad),
|
|
14703
|
+
x1: Math.min(1, box.x1 + pad),
|
|
14704
|
+
y1: Math.min(1, box.y1 + pad)
|
|
14705
|
+
};
|
|
14706
|
+
}
|
|
14606
14707
|
function shownBoxes(boxes, lines, clear) {
|
|
14607
14708
|
const linesOfBox = /* @__PURE__ */ new Map();
|
|
14608
14709
|
for (const line of lines) {
|
|
@@ -14986,7 +15087,7 @@ async function videoDuration(video) {
|
|
|
14986
15087
|
return null;
|
|
14987
15088
|
}
|
|
14988
15089
|
}
|
|
14989
|
-
async function askGateway(model, parts, timeoutMs, spent) {
|
|
15090
|
+
async function askGateway(model, parts, timeoutMs, spent, reasoning) {
|
|
14990
15091
|
const key = openRouterKey();
|
|
14991
15092
|
if (!key) return null;
|
|
14992
15093
|
try {
|
|
@@ -14996,7 +15097,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
|
|
|
14996
15097
|
body: JSON.stringify({
|
|
14997
15098
|
model,
|
|
14998
15099
|
messages: [{ role: "user", content: parts }],
|
|
14999
|
-
usage: { include: true }
|
|
15100
|
+
usage: { include: true },
|
|
15101
|
+
...reasoning ? { reasoning } : {}
|
|
15000
15102
|
}),
|
|
15001
15103
|
signal: AbortSignal.timeout(timeoutMs)
|
|
15002
15104
|
});
|
|
@@ -15045,7 +15147,7 @@ async function askAboutStills(images, prompt, onSpend) {
|
|
|
15045
15147
|
if (!hasOpenRouterKey()) return null;
|
|
15046
15148
|
return await ask(images, prompt, (usage) => onSpend?.(usage));
|
|
15047
15149
|
}
|
|
15048
|
-
async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
|
|
15150
|
+
async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
|
|
15049
15151
|
if (!hasOpenRouterKey()) return null;
|
|
15050
15152
|
return await askGateway(
|
|
15051
15153
|
model,
|
|
@@ -15056,7 +15158,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
|
|
|
15056
15158
|
)
|
|
15057
15159
|
],
|
|
15058
15160
|
timeoutMs,
|
|
15059
|
-
(usage) => onSpend?.(usage)
|
|
15161
|
+
(usage) => onSpend?.(usage),
|
|
15162
|
+
reasoning
|
|
15060
15163
|
);
|
|
15061
15164
|
}
|
|
15062
15165
|
async function askAboutText(model, prompt, onSpend) {
|
|
@@ -15220,7 +15323,9 @@ async function readTextBoxes(image, opts = {}) {
|
|
|
15220
15323
|
return { status: "not_checked" };
|
|
15221
15324
|
}
|
|
15222
15325
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
15223
|
-
const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend
|
|
15326
|
+
const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend, 6e4, {
|
|
15327
|
+
effort: "minimal"
|
|
15328
|
+
});
|
|
15224
15329
|
const boxes = textBoxesFromReader(answer);
|
|
15225
15330
|
if (boxes) return { status: "read", boxes };
|
|
15226
15331
|
}
|
|
@@ -15310,6 +15415,8 @@ var StaticFinding = z21.object({
|
|
|
15310
15415
|
});
|
|
15311
15416
|
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
15417
|
var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
|
|
15418
|
+
var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
|
|
15419
|
+
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
15420
|
function classifyStaticFindings(json) {
|
|
15314
15421
|
const findings = json?.findings;
|
|
15315
15422
|
if (!Array.isArray(findings)) return [];
|
|
@@ -15318,7 +15425,7 @@ function classifyStaticFindings(json) {
|
|
|
15318
15425
|
if (!parsed.success) return [];
|
|
15319
15426
|
const { severity, format, what } = parsed.data;
|
|
15320
15427
|
if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
|
|
15321
|
-
const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : severity;
|
|
15428
|
+
const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
|
|
15322
15429
|
return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
|
|
15323
15430
|
});
|
|
15324
15431
|
}
|
|
@@ -15329,7 +15436,7 @@ var CopyRead = z21.object({
|
|
|
15329
15436
|
seen: z21.string().nullable()
|
|
15330
15437
|
});
|
|
15331
15438
|
function comparableCopy(text2) {
|
|
15332
|
-
return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/\s+/g, " ").trim().toLowerCase();
|
|
15439
|
+
return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
|
|
15333
15440
|
}
|
|
15334
15441
|
function copyFindings(json, copy, formats, scrolled = {}) {
|
|
15335
15442
|
if (copy.length === 0) return [];
|
|
@@ -15704,7 +15811,7 @@ function contextItems(opts) {
|
|
|
15704
15811
|
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
15812
|
],
|
|
15706
15813
|
sizes: opts.formats.length > 1 ? [
|
|
15707
|
-
|
|
15814
|
+
`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
15815
|
] : []
|
|
15709
15816
|
};
|
|
15710
15817
|
}
|
|
@@ -15757,6 +15864,7 @@ function mostOfALine(chunk, copy) {
|
|
|
15757
15864
|
});
|
|
15758
15865
|
}
|
|
15759
15866
|
var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
|
|
15867
|
+
var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
|
|
15760
15868
|
var COPY_SIZE_SHARE = 0.8;
|
|
15761
15869
|
function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
15762
15870
|
if (extra.length === 0) return [];
|
|
@@ -15774,7 +15882,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15774
15882
|
const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
|
|
15775
15883
|
const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
|
|
15776
15884
|
const asCopy = extra.filter((chunk) => !printed(chunk));
|
|
15777
|
-
const
|
|
15885
|
+
const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
|
|
15886
|
+
const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
|
|
15778
15887
|
return [
|
|
15779
15888
|
...asCopy.length > 0 ? [
|
|
15780
15889
|
{
|
|
@@ -15783,6 +15892,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15783
15892
|
what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
|
|
15784
15893
|
}
|
|
15785
15894
|
] : [],
|
|
15895
|
+
...prices.length > 0 ? [
|
|
15896
|
+
{
|
|
15897
|
+
severity: "blocking",
|
|
15898
|
+
// `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
|
|
15899
|
+
what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
|
|
15900
|
+
}
|
|
15901
|
+
] : [],
|
|
15786
15902
|
...inScene.length > 0 ? [
|
|
15787
15903
|
{
|
|
15788
15904
|
severity: "warning",
|
|
@@ -15847,7 +15963,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
|
|
|
15847
15963
|
const element = claimedBoxes(f.what, boxes, buttons);
|
|
15848
15964
|
if (element.length === 0) return f;
|
|
15849
15965
|
const named = claimedEdges(f.what);
|
|
15850
|
-
const
|
|
15966
|
+
const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
|
|
15967
|
+
const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
|
|
15851
15968
|
return atEdge ? f : { ...f, severity: "warning" };
|
|
15852
15969
|
});
|
|
15853
15970
|
}
|
|
@@ -15855,9 +15972,11 @@ function claimedEdges(what) {
|
|
|
15855
15972
|
const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
|
|
15856
15973
|
return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
|
|
15857
15974
|
}
|
|
15975
|
+
var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
|
|
15976
|
+
var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
|
|
15858
15977
|
function claimedBoxes(what, boxes, buttons) {
|
|
15859
|
-
if (
|
|
15860
|
-
if (
|
|
15978
|
+
if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
|
|
15979
|
+
if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
|
|
15861
15980
|
return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
|
|
15862
15981
|
}
|
|
15863
15982
|
async function bytesOf(source) {
|
|
@@ -15987,6 +16106,22 @@ function everySizeChecks(opts) {
|
|
|
15987
16106
|
onSpend: opts.onSpend
|
|
15988
16107
|
};
|
|
15989
16108
|
}
|
|
16109
|
+
async function confirmBlocking(findings, stills, formats, ask2) {
|
|
16110
|
+
const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
|
|
16111
|
+
if (claims.length === 0 || stills.length === 0) return [...findings];
|
|
16112
|
+
const prompt = [
|
|
16113
|
+
`These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
|
|
16114
|
+
"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.",
|
|
16115
|
+
"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.",
|
|
16116
|
+
...claims.map((c, i) => `${i + 1}. ${c.what}`),
|
|
16117
|
+
`Reply with only this JSON: {"answers":[true|false, ...]}, one answer per claim, in order.`
|
|
16118
|
+
].join("\n");
|
|
16119
|
+
const answer = await ask2([...stills], prompt).catch(() => null);
|
|
16120
|
+
const answers = answer?.answers;
|
|
16121
|
+
if (!Array.isArray(answers) || answers.length !== claims.length) return [...findings];
|
|
16122
|
+
const unconfirmed = new Set(claims.filter((_, i) => answers[i] === false));
|
|
16123
|
+
return findings.map((f) => unconfirmed.has(f) ? { ...f, severity: "warning" } : f);
|
|
16124
|
+
}
|
|
15990
16125
|
async function reviewRenderedStatic(opts) {
|
|
15991
16126
|
const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
|
|
15992
16127
|
if (measured.length === 0) return null;
|
|
@@ -15995,6 +16130,12 @@ async function reviewRenderedStatic(opts) {
|
|
|
15995
16130
|
const formats = measured.map((m) => m.format);
|
|
15996
16131
|
const lines = opts.lines ?? opts.copy.map((text3) => ({ text: text3, lettering: "designed" }));
|
|
15997
16132
|
const every = { lines, ...everySizeChecks(opts) };
|
|
16133
|
+
const ask2 = (measuredRead) => {
|
|
16134
|
+
const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, measuredRead, reference !== null));
|
|
16135
|
+
const shown = [...measuredRead ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
|
|
16136
|
+
return askAboutStills(shown, prompt, opts.onSpend);
|
|
16137
|
+
};
|
|
16138
|
+
const asking = ask2(true);
|
|
15998
16139
|
const checks = await Promise.all(
|
|
15999
16140
|
originals.map(
|
|
16000
16141
|
(bytes, i) => checkStaticImage(bytes, {
|
|
@@ -16006,13 +16147,19 @@ async function reviewRenderedStatic(opts) {
|
|
|
16006
16147
|
);
|
|
16007
16148
|
const read = checks.every((c) => c.status === "checked");
|
|
16008
16149
|
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);
|
|
16150
|
+
const first = read ? await asking : (void asking.catch(() => null), null);
|
|
16151
|
+
const answer = first ?? await ask2(read);
|
|
16012
16152
|
if (answer === null) return unreviewed(shape, measuredText);
|
|
16013
16153
|
const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
|
|
16014
16154
|
const boxesByFormat = read ? Object.fromEntries(checks.flatMap((c, i) => c.status === "checked" ? [[formats[i] ?? "", c.boxes]] : [])) : {};
|
|
16015
|
-
|
|
16155
|
+
const judged = corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines);
|
|
16156
|
+
const confirmed = await confirmBlocking(
|
|
16157
|
+
judged,
|
|
16158
|
+
stills,
|
|
16159
|
+
formats,
|
|
16160
|
+
(images, prompt) => askAboutStills(images, prompt, opts.onSpend)
|
|
16161
|
+
);
|
|
16162
|
+
return [...shape, ...text2, ...confirmed];
|
|
16016
16163
|
}
|
|
16017
16164
|
|
|
16018
16165
|
// src/engine/nodes/local/staticNative.ts
|
|
@@ -16256,7 +16403,9 @@ function boxesToPlace(boxes, format, opts = {}) {
|
|
|
16256
16403
|
const past = (box) => bandViolations([box], format, { placements })[0];
|
|
16257
16404
|
const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
|
|
16258
16405
|
const logoWords = opts.copy.filter((l) => roleKey2(l.role) === "logo").map((l) => l.text);
|
|
16259
|
-
|
|
16406
|
+
const read = withWordmark(boxes, logoWords);
|
|
16407
|
+
const buttons = new Set(opts.copy.filter(isButtonLine).flatMap((line) => boxesOfLine(line, read)));
|
|
16408
|
+
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
16409
|
}
|
|
16261
16410
|
function planSafeFit(boxes, format, opts = {}) {
|
|
16262
16411
|
const area = blockingArea(format, opts.placements);
|
|
@@ -16360,6 +16509,9 @@ var MIN_EXTEND_SCALE = 0.6;
|
|
|
16360
16509
|
var GREY_LEVEL = 128;
|
|
16361
16510
|
var GREY = { r: GREY_LEVEL, g: GREY_LEVEL, b: GREY_LEVEL };
|
|
16362
16511
|
var FEATHER = 0.012;
|
|
16512
|
+
var KEEP_PAD = 0.3;
|
|
16513
|
+
var KEEP_FEATHER = 0.03;
|
|
16514
|
+
var MOVED = 120;
|
|
16363
16515
|
function ratioOf2(ratio) {
|
|
16364
16516
|
const [w = 1, h = 1] = ratio.split(":").map(Number);
|
|
16365
16517
|
return w / h;
|
|
@@ -16422,14 +16574,16 @@ function clearBusyMargins(rgba, frame, keep) {
|
|
|
16422
16574
|
}
|
|
16423
16575
|
return shown;
|
|
16424
16576
|
}
|
|
16425
|
-
async function mergeExtended(extended, placed, ratio) {
|
|
16577
|
+
async function mergeExtended(extended, placed, ratio, keep = []) {
|
|
16426
16578
|
const ad = await sharp7(placed).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
16427
16579
|
const { width, height } = ad.info;
|
|
16428
16580
|
const bounds = placedBounds(ad.data, width, height);
|
|
16429
16581
|
if (!bounds) return sharp7(placed).removeAlpha().png().toBuffer();
|
|
16430
16582
|
const canvas = renderCanvas({ width, height }, ratio);
|
|
16431
16583
|
const paint = await sharp7(extended).removeAlpha().resize(canvas.width, canvas.height, { fit: "fill" }).extract({ left: canvas.left, top: canvas.top, width, height }).raw().toBuffer();
|
|
16432
|
-
const
|
|
16584
|
+
const edge = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
|
|
16585
|
+
const boxes = keep.length > 0 && keptInPlace(keep, ad.data, paint, width, height) ? keptWeights(keep, width, height) : null;
|
|
16586
|
+
const weight = boxes ? (x, y) => Math.min(edge(x, y), boxes[y * width + x] ?? 0) : edge;
|
|
16433
16587
|
const out = Buffer.alloc(width * height * 3);
|
|
16434
16588
|
for (let y = 0; y < height; y++) {
|
|
16435
16589
|
for (let x = 0; x < width; x++) {
|
|
@@ -16468,18 +16622,65 @@ function edgeWeight(b, width, height, feather) {
|
|
|
16468
16622
|
ramp(b.y1 - y, b.y1 === height - 1)
|
|
16469
16623
|
);
|
|
16470
16624
|
}
|
|
16625
|
+
function keptWeights(keep, width, height) {
|
|
16626
|
+
const out = new Float32Array(width * height);
|
|
16627
|
+
const feather = Math.max(1, KEEP_FEATHER * width);
|
|
16628
|
+
for (const box of keep) {
|
|
16629
|
+
const pad = KEEP_PAD * (box.y1 - box.y0) * height;
|
|
16630
|
+
const x0 = box.x0 * width - pad;
|
|
16631
|
+
const x1 = box.x1 * width + pad;
|
|
16632
|
+
const y0 = box.y0 * height - pad;
|
|
16633
|
+
const y1 = box.y1 * height + pad;
|
|
16634
|
+
const ys = Math.max(0, Math.floor(y0 - feather));
|
|
16635
|
+
const ye = Math.min(height - 1, Math.ceil(y1 + feather));
|
|
16636
|
+
const xs = Math.max(0, Math.floor(x0 - feather));
|
|
16637
|
+
const xe = Math.min(width - 1, Math.ceil(x1 + feather));
|
|
16638
|
+
for (let y = ys; y <= ye; y++) {
|
|
16639
|
+
const dy = Math.max(0, y0 - y, y - y1);
|
|
16640
|
+
for (let x = xs; x <= xe; x++) {
|
|
16641
|
+
const dx = Math.max(0, x0 - x, x - x1);
|
|
16642
|
+
const t = Math.max(0, 1 - Math.hypot(dx, dy) / feather);
|
|
16643
|
+
const w = t * t * (3 - 2 * t);
|
|
16644
|
+
const i = y * width + x;
|
|
16645
|
+
if (w > (out[i] ?? 0)) out[i] = w;
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
return out;
|
|
16650
|
+
}
|
|
16651
|
+
function keptInPlace(keep, ad, paint, width, height) {
|
|
16652
|
+
const diffs = keep.map((box) => boxDiff(box, ad, paint, width, height)).filter((d) => d !== null).sort((a, b) => a - b);
|
|
16653
|
+
return (diffs[Math.floor(diffs.length / 2)] ?? Number.POSITIVE_INFINITY) <= MOVED;
|
|
16654
|
+
}
|
|
16655
|
+
function boxDiff(box, ad, paint, width, height) {
|
|
16656
|
+
let sum = 0;
|
|
16657
|
+
let n = 0;
|
|
16658
|
+
const xs = Math.floor(box.x0 * width);
|
|
16659
|
+
const xe = Math.min(width - 1, Math.ceil(box.x1 * width));
|
|
16660
|
+
for (let y = Math.floor(box.y0 * height); y <= Math.min(height - 1, Math.ceil(box.y1 * height)); y++) {
|
|
16661
|
+
for (let x = xs; x <= xe; x++) {
|
|
16662
|
+
const i = y * width + x;
|
|
16663
|
+
if ((ad[i * 4 + 3] ?? 0) === 0) continue;
|
|
16664
|
+
for (let c = 0; c < 3; c++) sum += Math.abs((ad[i * 4 + c] ?? 0) - (paint[i * 3 + c] ?? 0));
|
|
16665
|
+
n++;
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16668
|
+
return n > 0 ? sum / n : null;
|
|
16669
|
+
}
|
|
16471
16670
|
|
|
16472
16671
|
// src/engine/nodes/local/staticPlace.ts
|
|
16473
16672
|
var Usage = z25.object({ model: z25.string(), inputTokens: z25.number(), outputTokens: z25.number(), costUsd: z25.number() }).strict();
|
|
16474
16673
|
var Fit = z25.object({ scale: z25.number(), x: z25.number(), y: z25.number() }).strict();
|
|
16674
|
+
var Unit = z25.number().min(0).max(1);
|
|
16675
|
+
var Span = z25.object({ x0: Unit, y0: Unit, x1: Unit, y1: Unit }).strict();
|
|
16475
16676
|
var RenderRatio = z25.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
|
|
16476
16677
|
var Copy = z25.array(z25.object({ text: z25.string().min(1), role: z25.string().optional() }).strict());
|
|
16477
16678
|
var staticPlaceNode = defineNode({
|
|
16478
16679
|
id: "static_place",
|
|
16479
|
-
version: "1.
|
|
16680
|
+
version: "1.1.0",
|
|
16480
16681
|
category: "image",
|
|
16481
16682
|
location: "local",
|
|
16482
|
-
summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
|
|
16683
|
+
summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint; `keep` is where every line, the logo and the buttons read sit in `placed`, all the merge lays back of the ad's own pixels. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
|
|
16483
16684
|
when_to_use: "Added by a static ad's autofix round (place-and-extend) before an `image_generate` that paints the margins and a `static_extend_merge`; never scaffolded. One box-reader call (about $0.005).",
|
|
16484
16685
|
inputs: z25.object({ image: ImageRef }).strict(),
|
|
16485
16686
|
params: z25.object({
|
|
@@ -16492,6 +16693,8 @@ var staticPlaceNode = defineNode({
|
|
|
16492
16693
|
placed: ImageRef,
|
|
16493
16694
|
shown: ImageRef,
|
|
16494
16695
|
fit: Fit.nullable(),
|
|
16696
|
+
/** Where the words, logo and buttons read sit in `placed`: all the merge lays back of the ad's own pixels. */
|
|
16697
|
+
keep: z25.array(Span).optional(),
|
|
16495
16698
|
reason: z25.enum(["not_read", "inside_band", "too_far"]).optional(),
|
|
16496
16699
|
usage: z25.array(Usage).optional()
|
|
16497
16700
|
}).strict(),
|
|
@@ -16534,18 +16737,28 @@ var staticPlaceNode = defineNode({
|
|
|
16534
16737
|
placed: placedRef,
|
|
16535
16738
|
shown: shownRef,
|
|
16536
16739
|
fit,
|
|
16740
|
+
keep: keptBoxes([...reading.boxes, ...boxes.filter((b) => !reading.boxes.includes(b))], fit),
|
|
16537
16741
|
...spend
|
|
16538
16742
|
};
|
|
16539
16743
|
}
|
|
16540
16744
|
});
|
|
16745
|
+
function keptBoxes(boxes, fit) {
|
|
16746
|
+
const at2 = (offset2, v) => Math.min(1, Math.max(0, offset2 + fit.scale * v));
|
|
16747
|
+
return boxes.map((b) => ({ x0: at2(fit.x, b.x0), y0: at2(fit.y, b.y0), x1: at2(fit.x, b.x1), y1: at2(fit.y, b.y1) }));
|
|
16748
|
+
}
|
|
16541
16749
|
var staticExtendMergeNode = defineNode({
|
|
16542
16750
|
id: "static_extend_merge",
|
|
16543
|
-
version: "1.
|
|
16751
|
+
version: "1.2.0",
|
|
16544
16752
|
category: "image",
|
|
16545
16753
|
location: "local",
|
|
16546
|
-
summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad
|
|
16754
|
+
summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad's own pixels back over the painting only in `keep` (`static_place`'s boxes of the words, logo and buttons, padded and feathered): the painting is the rest of the frame, one picture with no edge where the shrunk ad was. Without `keep`, or when the painting moved the words, the whole ad (`placed`) is laid back, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
|
|
16547
16755
|
when_to_use: "Added by a static ad's autofix round after a `static_place`; never scaffolded. One picture at the painter's price, only for a size that was placed.",
|
|
16548
|
-
inputs: z25.object({
|
|
16756
|
+
inputs: z25.object({
|
|
16757
|
+
placed: ImageRef,
|
|
16758
|
+
shown: ImageRef.optional(),
|
|
16759
|
+
image: ImageRef.optional(),
|
|
16760
|
+
keep: z25.array(Span).optional()
|
|
16761
|
+
}).strict(),
|
|
16549
16762
|
params: z25.object({ render_ratio: RenderRatio, paint: z25.record(z25.string(), z25.unknown()).optional() }).strict(),
|
|
16550
16763
|
outputs: z25.object({ image: ImageRef }).strict(),
|
|
16551
16764
|
outputKinds: { image: "image" },
|
|
@@ -16555,7 +16768,7 @@ var staticExtendMergeNode = defineNode({
|
|
|
16555
16768
|
const painted = painting?.path ?? painting?.url;
|
|
16556
16769
|
const placed = inputs.placed.path ?? inputs.placed.url;
|
|
16557
16770
|
if (!painted || !placed) return { image: inputs.placed };
|
|
16558
|
-
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio);
|
|
16771
|
+
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio, inputs.keep);
|
|
16559
16772
|
const image = await ctx.assets.ingestBytes({ bytes: merged, kind: "image", mime: "image/png" });
|
|
16560
16773
|
return { image };
|
|
16561
16774
|
}
|
|
@@ -16584,12 +16797,64 @@ import { z as z26 } from "zod";
|
|
|
16584
16797
|
// src/engine/nodes/local/lib/variant-prefilter.ts
|
|
16585
16798
|
import { createHash as createHash3 } from "crypto";
|
|
16586
16799
|
import sharp8 from "sharp";
|
|
16587
|
-
function
|
|
16800
|
+
function coverOf(from, to) {
|
|
16588
16801
|
const scale = Math.max(to.width / from.width, to.height / from.height);
|
|
16589
|
-
const
|
|
16590
|
-
const
|
|
16591
|
-
|
|
16592
|
-
|
|
16802
|
+
const width = from.width * scale;
|
|
16803
|
+
const height = from.height * scale;
|
|
16804
|
+
return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
|
|
16805
|
+
}
|
|
16806
|
+
function framingOf(from, to) {
|
|
16807
|
+
let frame;
|
|
16808
|
+
return {
|
|
16809
|
+
place: (boxes) => {
|
|
16810
|
+
frame = frameKeeping(boxes, from, to);
|
|
16811
|
+
return coverFitBoxes(boxes, from, to, frame);
|
|
16812
|
+
},
|
|
16813
|
+
moved: () => {
|
|
16814
|
+
const { centre } = coverOf(from, to);
|
|
16815
|
+
return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
|
|
16816
|
+
}
|
|
16817
|
+
};
|
|
16818
|
+
}
|
|
16819
|
+
async function cutToFrame(bytes, frame, to) {
|
|
16820
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16821
|
+
const { scale } = coverOf({ width, height }, to);
|
|
16822
|
+
const w = Math.min(width, Math.round(to.width / scale));
|
|
16823
|
+
const h = Math.min(height, Math.round(to.height / scale));
|
|
16824
|
+
const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
|
|
16825
|
+
const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
|
|
16826
|
+
return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
|
|
16827
|
+
}
|
|
16828
|
+
var FRAME_MARGIN = 0.02;
|
|
16829
|
+
function frameKeeping(boxes, from, to) {
|
|
16830
|
+
const cover = coverOf(from, to);
|
|
16831
|
+
if (boxes.length === 0) return cover.centre;
|
|
16832
|
+
const along = (offset2, extent, frame, lo, hi) => {
|
|
16833
|
+
const low = lo * extent;
|
|
16834
|
+
const high = hi * extent;
|
|
16835
|
+
const room = extent - frame;
|
|
16836
|
+
const margin = FRAME_MARGIN * frame;
|
|
16837
|
+
for (const m of [margin, 0]) {
|
|
16838
|
+
const min = Math.max(0, high + m - frame);
|
|
16839
|
+
const max = Math.min(room, low - m);
|
|
16840
|
+
if (min <= max) return Math.min(max, Math.max(min, offset2));
|
|
16841
|
+
}
|
|
16842
|
+
return offset2;
|
|
16843
|
+
};
|
|
16844
|
+
const x0 = Math.min(...boxes.map((b) => b.x0));
|
|
16845
|
+
const x1 = Math.max(...boxes.map((b) => b.x1));
|
|
16846
|
+
const y0 = Math.min(...boxes.map((b) => b.y0));
|
|
16847
|
+
const y1 = Math.max(...boxes.map((b) => b.y1));
|
|
16848
|
+
return {
|
|
16849
|
+
left: along(cover.centre.left, cover.width, to.width, x0, x1),
|
|
16850
|
+
top: along(cover.centre.top, cover.height, to.height, y0, y1)
|
|
16851
|
+
};
|
|
16852
|
+
}
|
|
16853
|
+
function coverFitBoxes(boxes, from, to, frame) {
|
|
16854
|
+
const cover = coverOf(from, to);
|
|
16855
|
+
const { left: offX, top: offY } = frame ?? cover.centre;
|
|
16856
|
+
const x = (f) => (f * cover.width - offX) / to.width;
|
|
16857
|
+
const y = (f) => (f * cover.height - offY) / to.height;
|
|
16593
16858
|
return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
|
|
16594
16859
|
}
|
|
16595
16860
|
var NO_LOGO = "The brand's logo was not found on it.";
|
|
@@ -16643,34 +16908,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
16643
16908
|
});
|
|
16644
16909
|
const format = plan.master.format;
|
|
16645
16910
|
const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
|
|
16911
|
+
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
|
|
16646
16912
|
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
|
-
})
|
|
16913
|
+
variants.map(
|
|
16914
|
+
(variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
|
|
16915
|
+
)
|
|
16671
16916
|
);
|
|
16672
16917
|
const outcome = prefilterOutcome(checked);
|
|
16673
|
-
|
|
16918
|
+
const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
|
|
16919
|
+
return {
|
|
16920
|
+
...outcome,
|
|
16921
|
+
kept: seededOrder(outcome.kept, variants),
|
|
16922
|
+
...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
|
|
16923
|
+
};
|
|
16924
|
+
}
|
|
16925
|
+
async function checkVariant(variant, c) {
|
|
16926
|
+
const { format, lines, brand, grade, want, opts } = c;
|
|
16927
|
+
const source = variant.path ?? variant.url;
|
|
16928
|
+
if (!source) return { index: variant.index, blocking: null };
|
|
16929
|
+
try {
|
|
16930
|
+
const bytes = await bytesOf(source);
|
|
16931
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16932
|
+
const framing = framingOf({ width, height }, c.size);
|
|
16933
|
+
const check = await checkStaticImage(bytes, {
|
|
16934
|
+
format,
|
|
16935
|
+
lines,
|
|
16936
|
+
allowed: brand ? [brand] : [],
|
|
16937
|
+
...grade,
|
|
16938
|
+
toFrame: framing.place,
|
|
16939
|
+
...opts.read ? { read: opts.read } : {},
|
|
16940
|
+
...opts.onSpend ? { onSpend: opts.onSpend } : {}
|
|
16941
|
+
});
|
|
16942
|
+
if (check.status === "not_checked") return { index: variant.index, blocking: null };
|
|
16943
|
+
const prefix = `${format}: `;
|
|
16944
|
+
const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
|
|
16945
|
+
const frame = framing.moved();
|
|
16946
|
+
return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
|
|
16947
|
+
} catch {
|
|
16948
|
+
return { index: variant.index, blocking: null };
|
|
16949
|
+
}
|
|
16674
16950
|
}
|
|
16675
16951
|
function seededOrder(indexes, variants) {
|
|
16676
16952
|
const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
|
|
@@ -16746,7 +17022,7 @@ async function readSpec(ref) {
|
|
|
16746
17022
|
}
|
|
16747
17023
|
var staticPrefilterNode = defineNode({
|
|
16748
17024
|
id: "static_prefilter",
|
|
16749
|
-
version: "1.
|
|
17025
|
+
version: "1.1.0",
|
|
16750
17026
|
category: "image",
|
|
16751
17027
|
location: "local",
|
|
16752
17028
|
// Reads each variant's pixels itself, from its file or its durable URL, so the variants
|
|
@@ -16779,7 +17055,8 @@ var staticPrefilterNode = defineNode({
|
|
|
16779
17055
|
images: z26.array(ImageRef).min(1),
|
|
16780
17056
|
// Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
|
|
16781
17057
|
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.
|
|
17058
|
+
// The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
|
|
17059
|
+
// master's frame where every word stays whole when its centre would cut one, else as rendered.
|
|
16783
17060
|
image: ImageRef.optional(),
|
|
16784
17061
|
// Which variant it is (its place among the inputs).
|
|
16785
17062
|
chosen: z26.number().int().min(0).optional(),
|
|
@@ -16792,12 +17069,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16792
17069
|
}).strict(),
|
|
16793
17070
|
outputKinds: { images: "image", image: "image" },
|
|
16794
17071
|
cost: () => ({ credits: 0, seconds_estimate: 1 }),
|
|
16795
|
-
async execute({ inputs, params }) {
|
|
16796
|
-
const variants = inputs.images.map((
|
|
17072
|
+
async execute({ inputs, params, ctx }) {
|
|
17073
|
+
const variants = inputs.images.map((image2, index) => ({
|
|
16797
17074
|
index,
|
|
16798
|
-
sha256:
|
|
16799
|
-
...
|
|
16800
|
-
...
|
|
17075
|
+
sha256: image2.sha256,
|
|
17076
|
+
...image2.path ? { path: image2.path } : {},
|
|
17077
|
+
...image2.url ? { url: image2.url } : {}
|
|
16801
17078
|
}));
|
|
16802
17079
|
const usage = [];
|
|
16803
17080
|
const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
|
|
@@ -16813,10 +17090,11 @@ var staticPrefilterNode = defineNode({
|
|
|
16813
17090
|
{ model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
|
|
16814
17091
|
) : null;
|
|
16815
17092
|
const chosen = tie?.chosen ?? shown[0] ?? 0;
|
|
17093
|
+
const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
|
|
16816
17094
|
return {
|
|
16817
17095
|
images: kept.length > 0 ? kept : inputs.images,
|
|
16818
17096
|
order: shown,
|
|
16819
|
-
...
|
|
17097
|
+
...image ? { image } : {},
|
|
16820
17098
|
chosen,
|
|
16821
17099
|
...tie ? { reasoning: tie.reasoning } : {},
|
|
16822
17100
|
dropped: result.dropped,
|
|
@@ -16825,6 +17103,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16825
17103
|
};
|
|
16826
17104
|
}
|
|
16827
17105
|
});
|
|
17106
|
+
async function framed(picture, frame, size2, ctx) {
|
|
17107
|
+
const source = picture?.path ?? picture?.url;
|
|
17108
|
+
if (!picture || !frame || !size2 || !source) return picture;
|
|
17109
|
+
const bytes = await cutToFrame(await bytesOf(source), frame, size2);
|
|
17110
|
+
return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
|
|
17111
|
+
}
|
|
16828
17112
|
|
|
16829
17113
|
// src/engine/nodes/local/staticSafeFit.ts
|
|
16830
17114
|
import { z as z27 } from "zod";
|
|
@@ -18477,6 +18761,9 @@ export {
|
|
|
18477
18761
|
conceptSummary,
|
|
18478
18762
|
conceptProblems,
|
|
18479
18763
|
unwrittenProblems,
|
|
18764
|
+
sceneWordProblems,
|
|
18765
|
+
SPEC_SHAPES_HINT,
|
|
18766
|
+
adSpecErrorText,
|
|
18480
18767
|
readAdSpec,
|
|
18481
18768
|
withRenderedLettering,
|
|
18482
18769
|
conceptClassOf,
|
|
@@ -18503,10 +18790,12 @@ export {
|
|
|
18503
18790
|
STATIC_SAFE_FIT_ROLE,
|
|
18504
18791
|
STATIC_PLACE_ROLE,
|
|
18505
18792
|
LOGO_COMPOSITE_ROLE,
|
|
18793
|
+
SERIES_LEAD_ROLE,
|
|
18506
18794
|
STATIC_FIX_ROLE,
|
|
18507
18795
|
LOGO_COMPOSITE_DEFAULT,
|
|
18508
18796
|
nodesWithRole,
|
|
18509
18797
|
rolesById,
|
|
18798
|
+
relaidPictureRole,
|
|
18510
18799
|
formatSlug,
|
|
18511
18800
|
staticMasterFormat,
|
|
18512
18801
|
staticRenderNodes,
|
|
@@ -18564,4 +18853,4 @@ export {
|
|
|
18564
18853
|
defaultRegistry,
|
|
18565
18854
|
createEngineFromEnv
|
|
18566
18855
|
};
|
|
18567
|
-
//# sourceMappingURL=chunk-
|
|
18856
|
+
//# sourceMappingURL=chunk-7NKAKF3K.js.map
|