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