@koda-sl/baker-cli 0.433.0 → 0.434.0-dev.2729b1413
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 +45 -16
- package/dist/{chunk-X46KRGZU.js → chunk-7THIX4WW.js} +3 -3
- 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-7BQIIJLV.js → chunk-O5HQRXS6.js} +453 -157
- package/dist/chunk-O5HQRXS6.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 +8202 -4408
- package/dist/cli.js.map +1 -1
- package/dist/{client-HQ7LO4CO.js → client-TCDMWAL3.js} +4 -4
- package/dist/engine/index.d.ts +13 -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) {
|
|
@@ -4408,7 +4398,18 @@ var CanvasMetadata = z4.object({
|
|
|
4408
4398
|
// `scaffold-video`) so the editable checklist travels inside the canvas
|
|
4409
4399
|
// instead of only printing to stdout. Ignored by the engine.
|
|
4410
4400
|
todo: z4.unknown().optional(),
|
|
4411
|
-
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()
|
|
4412
4413
|
}).strict().optional();
|
|
4413
4414
|
var CanvasSchema = z4.object({
|
|
4414
4415
|
schema: z4.literal("baker-canvas/1"),
|
|
@@ -4992,10 +4993,10 @@ var GPT_IMAGE_BRIEF = {
|
|
|
4992
4993
|
// (.context/static-review/BENCH.md#E3).
|
|
4993
4994
|
// quality max: the deterministic checks tie (shippable 9-12% on every step, n=100), but on the
|
|
4994
4995
|
// 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: "
|
|
4996
|
+
// and the judge's on the same pairs 73%, 65%, 39%, 31%. Max costs $0.166 per image vs $0.085
|
|
4997
|
+
// for xhigh and 80 s vs 44 s (BENCH.md#E4, § "E1 v2"); a round renders twice in a row (the
|
|
4998
|
+
// options, then each size), so three ads took most of an hour. Oct 2026 the user chose xhigh.
|
|
4999
|
+
params: { n: 1, quality: "xhigh", imageSize: "2K" }
|
|
4999
5000
|
};
|
|
5000
5001
|
var GEMINI_IMAGE_BRIEF = {
|
|
5001
5002
|
// "Use Natural Language & Full Sentences… as if you were briefing a human artist", and
|
|
@@ -6699,6 +6700,7 @@ function textFidelityDirective(strings2, hasFont) {
|
|
|
6699
6700
|
return [
|
|
6700
6701
|
exact,
|
|
6701
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.",
|
|
6702
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).",
|
|
6703
6705
|
"Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
|
|
6704
6706
|
].join("\n");
|
|
@@ -6763,22 +6765,32 @@ var Viewer = z7.object({
|
|
|
6763
6765
|
/** Who will see the ad, in the words a marketer would use. */
|
|
6764
6766
|
who: z7.string().trim().min(1),
|
|
6765
6767
|
market: Market.optional(),
|
|
6766
|
-
/** What they already know about the brand and the offer. */
|
|
6767
|
-
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()),
|
|
6768
6770
|
temperature: z7.enum(["cold", "warm", "hot"]).optional(),
|
|
6769
6771
|
/** Where they meet it (feed, Stories, Reels). */
|
|
6770
6772
|
placement: z7.string().optional()
|
|
6771
6773
|
}).strict();
|
|
6772
|
-
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({
|
|
6773
6781
|
description: z7.string().trim().min(1),
|
|
6774
|
-
/** A photograph, a designed graphic, or an illustration. */
|
|
6775
|
-
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
|
+
),
|
|
6776
6787
|
/** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
|
|
6777
6788
|
capture: z7.string().optional(),
|
|
6778
6789
|
mood: z7.string().optional(),
|
|
6779
6790
|
/** How the frame is arranged, by what fills each part of it. */
|
|
6780
6791
|
layout: z7.string().optional()
|
|
6781
6792
|
}).strict();
|
|
6793
|
+
var Scene = z7.preprocess(withoutFormatName, SceneFields);
|
|
6782
6794
|
var Brand = z7.object({
|
|
6783
6795
|
name: z7.string().optional(),
|
|
6784
6796
|
/** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
|
|
@@ -6850,13 +6862,19 @@ var AdSpecSchema = z7.object({
|
|
|
6850
6862
|
* editor's line numbers, a product's own label, a platform's badge. The render may draw them
|
|
6851
6863
|
* and the review never calls them extra words.
|
|
6852
6864
|
*/
|
|
6853
|
-
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
|
+
),
|
|
6854
6869
|
/** The formats the ad is made for; the canvas delivers them. */
|
|
6855
6870
|
formats: z7.array(z7.string()).optional(),
|
|
6856
6871
|
/** An optional note on direction for this concept. */
|
|
6857
6872
|
direction: z7.string().optional(),
|
|
6858
6873
|
/** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
|
|
6859
|
-
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([])),
|
|
6860
6878
|
/** Why the reference won and what this ad keeps of it (schema /3). */
|
|
6861
6879
|
concept: Concept.optional(),
|
|
6862
6880
|
/**
|
|
@@ -6912,6 +6930,61 @@ function conceptProblems(spec) {
|
|
|
6912
6930
|
}
|
|
6913
6931
|
return problems;
|
|
6914
6932
|
}
|
|
6933
|
+
var PLACEHOLDER = "[TODO";
|
|
6934
|
+
function unwrittenProblems(spec) {
|
|
6935
|
+
const problems = [];
|
|
6936
|
+
if (spec.scene.description.includes(PLACEHOLDER)) {
|
|
6937
|
+
problems.push("The scene is still a placeholder: write what the viewer sees in `scene.description`.");
|
|
6938
|
+
}
|
|
6939
|
+
const lines = spec.copy.filter((line) => line.text.includes(PLACEHOLDER));
|
|
6940
|
+
if (lines.length > 0) {
|
|
6941
|
+
problems.push(`${lines.length} line(s) of copy are still placeholders: write each as it prints, or drop it.`);
|
|
6942
|
+
}
|
|
6943
|
+
return problems;
|
|
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
|
+
}
|
|
6915
6988
|
function readAdSpec(raw) {
|
|
6916
6989
|
if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
|
|
6917
6990
|
const advertiser = raw?.reference?.advertiser;
|
|
@@ -7402,7 +7475,7 @@ function pictureInstruction(p, ctx) {
|
|
|
7402
7475
|
const keep = p.spec?.keep ? ` Keep ${plain(p.spec.keep, ctx.clean)}.` : "";
|
|
7403
7476
|
switch (p.role) {
|
|
7404
7477
|
case "base":
|
|
7405
|
-
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.";
|
|
7406
7479
|
case "identity":
|
|
7407
7480
|
return identityInstruction(p, what, keep, ctx.flowing);
|
|
7408
7481
|
// The user saw one label come back two ways (E4 gallery, BENCH.md § "E1 v2"): the label is the product's.
|
|
@@ -7781,6 +7854,10 @@ function deliverableSection(c) {
|
|
|
7781
7854
|
const change = sentence(c.opts.instruction?.trim() || "correct what is wrong");
|
|
7782
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.`;
|
|
7783
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
|
+
}
|
|
7784
7861
|
if (c.job === "relay") {
|
|
7785
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.`;
|
|
7786
7863
|
}
|
|
@@ -8364,14 +8441,16 @@ function scaleText(plan, spec, flowing, zoned, short) {
|
|
|
8364
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." : "";
|
|
8365
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}`;
|
|
8366
8443
|
}
|
|
8367
|
-
var
|
|
8444
|
+
var COVERED_HEADROOM = { top: 0.06, bottom: 0.08, side: 0.02 };
|
|
8445
|
+
var NO_HEADROOM = { top: 0, bottom: 0, side: 0 };
|
|
8368
8446
|
var pct = (share) => `${Math.round(share * 100)}%`;
|
|
8369
8447
|
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
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`;
|
|
8375
8454
|
if (short && !flowing) {
|
|
8376
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";
|
|
8377
8456
|
return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
|
|
@@ -8380,7 +8459,7 @@ function zonesText(plan, flowing, ground, short) {
|
|
|
8380
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.`;
|
|
8381
8460
|
}
|
|
8382
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";
|
|
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}
|
|
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.`;
|
|
8384
8463
|
}
|
|
8385
8464
|
function adaptBrief(spec, profile, opts) {
|
|
8386
8465
|
const { plan, images } = opts;
|
|
@@ -8449,6 +8528,7 @@ var STATIC_ADAPT_ROLE = "static_adapt";
|
|
|
8449
8528
|
var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
|
|
8450
8529
|
var STATIC_PLACE_ROLE = "static_place";
|
|
8451
8530
|
var LOGO_COMPOSITE_ROLE = "logo_composite";
|
|
8531
|
+
var SERIES_LEAD_ROLE = "series_lead";
|
|
8452
8532
|
var STATIC_FIX_ROLE = "static_fix";
|
|
8453
8533
|
var LOGO_COMPOSITE_DEFAULT = false;
|
|
8454
8534
|
var ADAPT_IMAGE_SIZE = "2K";
|
|
@@ -8655,7 +8735,7 @@ function sizeNode(s, adaptFormats) {
|
|
|
8655
8735
|
const index = adaptFormats.indexOf(s.adaptFormat ?? s.format);
|
|
8656
8736
|
return magick(id, STATIC_SIZE_ROLE, `$ref:adapt.images#${index}`, fillArgs(s.size));
|
|
8657
8737
|
}
|
|
8658
|
-
var RELAYOUT_QUALITY = "
|
|
8738
|
+
var RELAYOUT_QUALITY = "xhigh";
|
|
8659
8739
|
function relayoutNodes(g) {
|
|
8660
8740
|
return g.plan.sizes.filter((s) => s.from === "relayout").map((s) => relayoutNode(s, g));
|
|
8661
8741
|
}
|
|
@@ -10117,7 +10197,7 @@ function checkOrphanNodes(ctx) {
|
|
|
10117
10197
|
path: `nodes[${ctx.idToIndex.get(n.id)}]`,
|
|
10118
10198
|
code: STAGE_CODES.ORPHAN_NODE,
|
|
10119
10199
|
severity: "warning",
|
|
10120
|
-
message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}"
|
|
10200
|
+
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
10201
|
});
|
|
10122
10202
|
}
|
|
10123
10203
|
}
|
|
@@ -10188,6 +10268,17 @@ function unwrap(schema) {
|
|
|
10188
10268
|
}
|
|
10189
10269
|
|
|
10190
10270
|
// src/engine/engine/executor.ts
|
|
10271
|
+
function throwRunOutcome(signal, failures, held) {
|
|
10272
|
+
if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
10273
|
+
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
10274
|
+
if (failures.length > 1) throw new LayerExecutionError([...failures]);
|
|
10275
|
+
if (held.length > 0) {
|
|
10276
|
+
throw new RunAbortedError(
|
|
10277
|
+
"checkpoint",
|
|
10278
|
+
`stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
|
|
10279
|
+
);
|
|
10280
|
+
}
|
|
10281
|
+
}
|
|
10191
10282
|
function heldByCheckpoint(canvas, layer, kinds) {
|
|
10192
10283
|
if (!kinds?.size) return [];
|
|
10193
10284
|
return layer.filter((id) => {
|
|
@@ -10276,45 +10367,63 @@ var Engine = class {
|
|
|
10276
10367
|
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
10277
10368
|
}
|
|
10278
10369
|
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
10279
|
-
const
|
|
10370
|
+
const order = topologicalLayers(graph).flat();
|
|
10280
10371
|
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) });
|
|
10372
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
|
|
10373
|
+
const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
|
|
10374
|
+
const running = /* @__PURE__ */ new Map();
|
|
10375
|
+
const failures = [];
|
|
10376
|
+
const held = [];
|
|
10377
|
+
const dispatch = (nodeId) => {
|
|
10378
|
+
waitingOn.delete(nodeId);
|
|
10379
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
10380
|
+
const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
|
|
10381
|
+
(r) => {
|
|
10382
|
+
this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
|
|
10383
|
+
for (const deps of waitingOn.values()) deps.delete(nodeId);
|
|
10384
|
+
},
|
|
10385
|
+
(reason) => {
|
|
10386
|
+
failures.push({ nodeId, reason });
|
|
10387
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
|
|
10312
10388
|
}
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10389
|
+
);
|
|
10390
|
+
running.set(
|
|
10391
|
+
nodeId,
|
|
10392
|
+
settled.finally(() => running.delete(nodeId))
|
|
10393
|
+
);
|
|
10394
|
+
};
|
|
10395
|
+
const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
|
|
10396
|
+
for (; ; ) {
|
|
10397
|
+
const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
|
|
10398
|
+
for (const nodeId of ready) {
|
|
10399
|
+
if (running.size >= limit) break;
|
|
10400
|
+
if (isHeld(nodeId)) {
|
|
10401
|
+
waitingOn.delete(nodeId);
|
|
10402
|
+
held.push(nodeId);
|
|
10403
|
+
} else dispatch(nodeId);
|
|
10404
|
+
}
|
|
10405
|
+
if (running.size === 0) break;
|
|
10406
|
+
await Promise.race(running.values());
|
|
10317
10407
|
}
|
|
10408
|
+
const rank = new Map(order.map((id, i) => [id, i]));
|
|
10409
|
+
failures.sort((a, b) => (rank.get(a.nodeId) ?? 0) - (rank.get(b.nodeId) ?? 0));
|
|
10410
|
+
throwRunOutcome(opts.signal, failures, held);
|
|
10411
|
+
}
|
|
10412
|
+
/** A settled node counted, recorded and announced. */
|
|
10413
|
+
recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
|
|
10414
|
+
if (r.cached) counters.cachedNodes++;
|
|
10415
|
+
counters.totalCredits += r.credits;
|
|
10416
|
+
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
10417
|
+
if (!node) return;
|
|
10418
|
+
const run = {
|
|
10419
|
+
node_id: nodeId,
|
|
10420
|
+
node_type: node.type,
|
|
10421
|
+
cached: r.cached,
|
|
10422
|
+
duration_ms: r.durationMs,
|
|
10423
|
+
credits: r.credits
|
|
10424
|
+
};
|
|
10425
|
+
nodeRuns.push(run);
|
|
10426
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
10318
10427
|
}
|
|
10319
10428
|
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
10320
10429
|
emitProgress(opts, event) {
|
|
@@ -10406,20 +10515,6 @@ var Engine = class {
|
|
|
10406
10515
|
this.logOutputWarnings(node.id, outputsObj);
|
|
10407
10516
|
return { cached: false, credits, durationMs: elapsed };
|
|
10408
10517
|
}
|
|
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
10518
|
/** A cache hit replayed as this node's result: its outputs written, and free. */
|
|
10424
10519
|
async replayCached(node, cacheKey, outputs, writer) {
|
|
10425
10520
|
const cacheT0 = Date.now();
|
|
@@ -14584,6 +14679,18 @@ function boxesOfLine(line, boxes) {
|
|
|
14584
14679
|
return b.kind === "text" && read.length > 0 && words.includes(` ${read} `);
|
|
14585
14680
|
});
|
|
14586
14681
|
}
|
|
14682
|
+
var BUTTON_PAD = 0.7;
|
|
14683
|
+
var isButtonLine = (line) => /^(cta|button|call_to_action)$/.test(roleKey2(line.role));
|
|
14684
|
+
function buttonShape(box) {
|
|
14685
|
+
const pad = BUTTON_PAD * (box.y1 - box.y0);
|
|
14686
|
+
return {
|
|
14687
|
+
...box,
|
|
14688
|
+
x0: Math.max(0, box.x0 - pad),
|
|
14689
|
+
y0: Math.max(0, box.y0 - pad),
|
|
14690
|
+
x1: Math.min(1, box.x1 + pad),
|
|
14691
|
+
y1: Math.min(1, box.y1 + pad)
|
|
14692
|
+
};
|
|
14693
|
+
}
|
|
14587
14694
|
function shownBoxes(boxes, lines, clear) {
|
|
14588
14695
|
const linesOfBox = /* @__PURE__ */ new Map();
|
|
14589
14696
|
for (const line of lines) {
|
|
@@ -14967,7 +15074,7 @@ async function videoDuration(video) {
|
|
|
14967
15074
|
return null;
|
|
14968
15075
|
}
|
|
14969
15076
|
}
|
|
14970
|
-
async function askGateway(model, parts, timeoutMs, spent) {
|
|
15077
|
+
async function askGateway(model, parts, timeoutMs, spent, reasoning) {
|
|
14971
15078
|
const key = openRouterKey();
|
|
14972
15079
|
if (!key) return null;
|
|
14973
15080
|
try {
|
|
@@ -14977,7 +15084,8 @@ async function askGateway(model, parts, timeoutMs, spent) {
|
|
|
14977
15084
|
body: JSON.stringify({
|
|
14978
15085
|
model,
|
|
14979
15086
|
messages: [{ role: "user", content: parts }],
|
|
14980
|
-
usage: { include: true }
|
|
15087
|
+
usage: { include: true },
|
|
15088
|
+
...reasoning ? { reasoning } : {}
|
|
14981
15089
|
}),
|
|
14982
15090
|
signal: AbortSignal.timeout(timeoutMs)
|
|
14983
15091
|
});
|
|
@@ -15026,7 +15134,7 @@ async function askAboutStills(images, prompt, onSpend) {
|
|
|
15026
15134
|
if (!hasOpenRouterKey()) return null;
|
|
15027
15135
|
return await ask(images, prompt, (usage) => onSpend?.(usage));
|
|
15028
15136
|
}
|
|
15029
|
-
async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
|
|
15137
|
+
async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, reasoning) {
|
|
15030
15138
|
if (!hasOpenRouterKey()) return null;
|
|
15031
15139
|
return await askGateway(
|
|
15032
15140
|
model,
|
|
@@ -15037,7 +15145,8 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4) {
|
|
|
15037
15145
|
)
|
|
15038
15146
|
],
|
|
15039
15147
|
timeoutMs,
|
|
15040
|
-
(usage) => onSpend?.(usage)
|
|
15148
|
+
(usage) => onSpend?.(usage),
|
|
15149
|
+
reasoning
|
|
15041
15150
|
);
|
|
15042
15151
|
}
|
|
15043
15152
|
async function askAboutText(model, prompt, onSpend) {
|
|
@@ -15201,7 +15310,9 @@ async function readTextBoxes(image, opts = {}) {
|
|
|
15201
15310
|
return { status: "not_checked" };
|
|
15202
15311
|
}
|
|
15203
15312
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
15204
|
-
const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend
|
|
15313
|
+
const answer = await askAboutImages(TEXT_BOX_READER_MODEL, [jpeg], TEXT_BOX_PROMPT, opts.onSpend, 6e4, {
|
|
15314
|
+
effort: "minimal"
|
|
15315
|
+
});
|
|
15205
15316
|
const boxes = textBoxesFromReader(answer);
|
|
15206
15317
|
if (boxes) return { status: "read", boxes };
|
|
15207
15318
|
}
|
|
@@ -15291,6 +15402,8 @@ var StaticFinding = z21.object({
|
|
|
15291
15402
|
});
|
|
15292
15403
|
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
15404
|
var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
|
|
15405
|
+
var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
|
|
15406
|
+
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
15407
|
function classifyStaticFindings(json) {
|
|
15295
15408
|
const findings = json?.findings;
|
|
15296
15409
|
if (!Array.isArray(findings)) return [];
|
|
@@ -15299,7 +15412,7 @@ function classifyStaticFindings(json) {
|
|
|
15299
15412
|
if (!parsed.success) return [];
|
|
15300
15413
|
const { severity, format, what } = parsed.data;
|
|
15301
15414
|
if (ABSENCE_NEVER_A_FINDING.test(what)) return [];
|
|
15302
|
-
const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : severity;
|
|
15415
|
+
const graded = ALWAYS_BLOCKING.test(what) ? "blocking" : BLURRED_FIGURES.test(what) && !ABOUT_OWN_COPY.test(what) ? "warning" : severity;
|
|
15303
15416
|
return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what }];
|
|
15304
15417
|
});
|
|
15305
15418
|
}
|
|
@@ -15310,7 +15423,7 @@ var CopyRead = z21.object({
|
|
|
15310
15423
|
seen: z21.string().nullable()
|
|
15311
15424
|
});
|
|
15312
15425
|
function comparableCopy(text2) {
|
|
15313
|
-
return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/\s+/g, " ").trim().toLowerCase();
|
|
15426
|
+
return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
|
|
15314
15427
|
}
|
|
15315
15428
|
function copyFindings(json, copy, formats, scrolled = {}) {
|
|
15316
15429
|
if (copy.length === 0) return [];
|
|
@@ -15685,7 +15798,7 @@ function contextItems(opts) {
|
|
|
15685
15798
|
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
15799
|
],
|
|
15687
15800
|
sizes: opts.formats.length > 1 ? [
|
|
15688
|
-
|
|
15801
|
+
`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
15802
|
] : []
|
|
15690
15803
|
};
|
|
15691
15804
|
}
|
|
@@ -15738,6 +15851,7 @@ function mostOfALine(chunk, copy) {
|
|
|
15738
15851
|
});
|
|
15739
15852
|
}
|
|
15740
15853
|
var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
|
|
15854
|
+
var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
|
|
15741
15855
|
var COPY_SIZE_SHARE = 0.8;
|
|
15742
15856
|
function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
15743
15857
|
if (extra.length === 0) return [];
|
|
@@ -15755,7 +15869,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15755
15869
|
const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
|
|
15756
15870
|
const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
|
|
15757
15871
|
const asCopy = extra.filter((chunk) => !printed(chunk));
|
|
15758
|
-
const
|
|
15872
|
+
const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
|
|
15873
|
+
const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
|
|
15759
15874
|
return [
|
|
15760
15875
|
...asCopy.length > 0 ? [
|
|
15761
15876
|
{
|
|
@@ -15764,6 +15879,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15764
15879
|
what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
|
|
15765
15880
|
}
|
|
15766
15881
|
] : [],
|
|
15882
|
+
...prices.length > 0 ? [
|
|
15883
|
+
{
|
|
15884
|
+
severity: "blocking",
|
|
15885
|
+
// `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
|
|
15886
|
+
what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
|
|
15887
|
+
}
|
|
15888
|
+
] : [],
|
|
15767
15889
|
...inScene.length > 0 ? [
|
|
15768
15890
|
{
|
|
15769
15891
|
severity: "warning",
|
|
@@ -15828,7 +15950,8 @@ function corroborateEdgeClaims(findings, boxesByFormat, lines) {
|
|
|
15828
15950
|
const element = claimedBoxes(f.what, boxes, buttons);
|
|
15829
15951
|
if (element.length === 0) return f;
|
|
15830
15952
|
const named = claimedEdges(f.what);
|
|
15831
|
-
const
|
|
15953
|
+
const pad = BUTTON.test(f.what) && !LOGO_CLAIM.test(f.what) ? BUTTON_PAD : 0;
|
|
15954
|
+
const atEdge = element.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
|
|
15832
15955
|
return atEdge ? f : { ...f, severity: "warning" };
|
|
15833
15956
|
});
|
|
15834
15957
|
}
|
|
@@ -15836,9 +15959,11 @@ function claimedEdges(what) {
|
|
|
15836
15959
|
const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
|
|
15837
15960
|
return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
|
|
15838
15961
|
}
|
|
15962
|
+
var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
|
|
15963
|
+
var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
|
|
15839
15964
|
function claimedBoxes(what, boxes, buttons) {
|
|
15840
|
-
if (
|
|
15841
|
-
if (
|
|
15965
|
+
if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
|
|
15966
|
+
if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
|
|
15842
15967
|
return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
|
|
15843
15968
|
}
|
|
15844
15969
|
async function bytesOf(source) {
|
|
@@ -15968,6 +16093,22 @@ function everySizeChecks(opts) {
|
|
|
15968
16093
|
onSpend: opts.onSpend
|
|
15969
16094
|
};
|
|
15970
16095
|
}
|
|
16096
|
+
async function confirmBlocking(findings, stills, formats, ask2) {
|
|
16097
|
+
const claims = findings.filter((f) => f.severity === "blocking" && !CLIPPED.test(f.what));
|
|
16098
|
+
if (claims.length === 0 || stills.length === 0) return [...findings];
|
|
16099
|
+
const prompt = [
|
|
16100
|
+
`These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
|
|
16101
|
+
"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.",
|
|
16102
|
+
"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.",
|
|
16103
|
+
...claims.map((c, i) => `${i + 1}. ${c.what}`),
|
|
16104
|
+
`Reply with only this JSON: {"answers":[true|false, ...]}, one answer per claim, in order.`
|
|
16105
|
+
].join("\n");
|
|
16106
|
+
const answer = await ask2([...stills], prompt).catch(() => null);
|
|
16107
|
+
const answers = answer?.answers;
|
|
16108
|
+
if (!Array.isArray(answers) || answers.length !== claims.length) return [...findings];
|
|
16109
|
+
const unconfirmed = new Set(claims.filter((_, i) => answers[i] === false));
|
|
16110
|
+
return findings.map((f) => unconfirmed.has(f) ? { ...f, severity: "warning" } : f);
|
|
16111
|
+
}
|
|
15971
16112
|
async function reviewRenderedStatic(opts) {
|
|
15972
16113
|
const { measured, originals, stills, thumbs } = await loadDelivered(opts.images);
|
|
15973
16114
|
if (measured.length === 0) return null;
|
|
@@ -15976,6 +16117,12 @@ async function reviewRenderedStatic(opts) {
|
|
|
15976
16117
|
const formats = measured.map((m) => m.format);
|
|
15977
16118
|
const lines = opts.lines ?? opts.copy.map((text3) => ({ text: text3, lettering: "designed" }));
|
|
15978
16119
|
const every = { lines, ...everySizeChecks(opts) };
|
|
16120
|
+
const ask2 = (measuredRead) => {
|
|
16121
|
+
const prompt = buildStaticReviewPrompt(reviewPromptOpts(opts, formats, measuredRead, reference !== null));
|
|
16122
|
+
const shown = [...measuredRead ? stills : [...stills, ...thumbs], ...reference ? [reference] : []];
|
|
16123
|
+
return askAboutStills(shown, prompt, opts.onSpend);
|
|
16124
|
+
};
|
|
16125
|
+
const asking = ask2(true);
|
|
15979
16126
|
const checks = await Promise.all(
|
|
15980
16127
|
originals.map(
|
|
15981
16128
|
(bytes, i) => checkStaticImage(bytes, {
|
|
@@ -15987,13 +16134,19 @@ async function reviewRenderedStatic(opts) {
|
|
|
15987
16134
|
);
|
|
15988
16135
|
const read = checks.every((c) => c.status === "checked");
|
|
15989
16136
|
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);
|
|
16137
|
+
const first = read ? await asking : (void asking.catch(() => null), null);
|
|
16138
|
+
const answer = first ?? await ask2(read);
|
|
15993
16139
|
if (answer === null) return unreviewed(shape, measuredText);
|
|
15994
16140
|
const text2 = read ? measuredText : copyFindings(answer, opts.copy, formats, opts.scrolled);
|
|
15995
16141
|
const boxesByFormat = read ? Object.fromEntries(checks.flatMap((c, i) => c.status === "checked" ? [[formats[i] ?? "", c.boxes]] : [])) : {};
|
|
15996
|
-
|
|
16142
|
+
const judged = corroborateEdgeClaims(classifyStaticFindings(answer), boxesByFormat, lines);
|
|
16143
|
+
const confirmed = await confirmBlocking(
|
|
16144
|
+
judged,
|
|
16145
|
+
stills,
|
|
16146
|
+
formats,
|
|
16147
|
+
(images, prompt) => askAboutStills(images, prompt, opts.onSpend)
|
|
16148
|
+
);
|
|
16149
|
+
return [...shape, ...text2, ...confirmed];
|
|
15997
16150
|
}
|
|
15998
16151
|
|
|
15999
16152
|
// src/engine/nodes/local/staticNative.ts
|
|
@@ -16237,7 +16390,9 @@ function boxesToPlace(boxes, format, opts = {}) {
|
|
|
16237
16390
|
const past = (box) => bandViolations([box], format, { placements })[0];
|
|
16238
16391
|
const cut = (box) => (past(box)?.clearance ?? 1) <= FRAME_CUT;
|
|
16239
16392
|
const logoWords = opts.copy.filter((l) => roleKey2(l.role) === "logo").map((l) => l.text);
|
|
16240
|
-
|
|
16393
|
+
const read = withWordmark(boxes, logoWords);
|
|
16394
|
+
const buttons = new Set(opts.copy.filter(isButtonLine).flatMap((line) => boxesOfLine(line, read)));
|
|
16395
|
+
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
16396
|
}
|
|
16242
16397
|
function planSafeFit(boxes, format, opts = {}) {
|
|
16243
16398
|
const area = blockingArea(format, opts.placements);
|
|
@@ -16341,6 +16496,9 @@ var MIN_EXTEND_SCALE = 0.6;
|
|
|
16341
16496
|
var GREY_LEVEL = 128;
|
|
16342
16497
|
var GREY = { r: GREY_LEVEL, g: GREY_LEVEL, b: GREY_LEVEL };
|
|
16343
16498
|
var FEATHER = 0.012;
|
|
16499
|
+
var KEEP_PAD = 0.3;
|
|
16500
|
+
var KEEP_FEATHER = 0.03;
|
|
16501
|
+
var MOVED = 120;
|
|
16344
16502
|
function ratioOf2(ratio) {
|
|
16345
16503
|
const [w = 1, h = 1] = ratio.split(":").map(Number);
|
|
16346
16504
|
return w / h;
|
|
@@ -16403,14 +16561,16 @@ function clearBusyMargins(rgba, frame, keep) {
|
|
|
16403
16561
|
}
|
|
16404
16562
|
return shown;
|
|
16405
16563
|
}
|
|
16406
|
-
async function mergeExtended(extended, placed, ratio) {
|
|
16564
|
+
async function mergeExtended(extended, placed, ratio, keep = []) {
|
|
16407
16565
|
const ad = await sharp7(placed).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
16408
16566
|
const { width, height } = ad.info;
|
|
16409
16567
|
const bounds = placedBounds(ad.data, width, height);
|
|
16410
16568
|
if (!bounds) return sharp7(placed).removeAlpha().png().toBuffer();
|
|
16411
16569
|
const canvas = renderCanvas({ width, height }, ratio);
|
|
16412
16570
|
const paint = await sharp7(extended).removeAlpha().resize(canvas.width, canvas.height, { fit: "fill" }).extract({ left: canvas.left, top: canvas.top, width, height }).raw().toBuffer();
|
|
16413
|
-
const
|
|
16571
|
+
const edge = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
|
|
16572
|
+
const boxes = keep.length > 0 && keptInPlace(keep, ad.data, paint, width, height) ? keptWeights(keep, width, height) : null;
|
|
16573
|
+
const weight = boxes ? (x, y) => Math.min(edge(x, y), boxes[y * width + x] ?? 0) : edge;
|
|
16414
16574
|
const out = Buffer.alloc(width * height * 3);
|
|
16415
16575
|
for (let y = 0; y < height; y++) {
|
|
16416
16576
|
for (let x = 0; x < width; x++) {
|
|
@@ -16449,18 +16609,65 @@ function edgeWeight(b, width, height, feather) {
|
|
|
16449
16609
|
ramp(b.y1 - y, b.y1 === height - 1)
|
|
16450
16610
|
);
|
|
16451
16611
|
}
|
|
16612
|
+
function keptWeights(keep, width, height) {
|
|
16613
|
+
const out = new Float32Array(width * height);
|
|
16614
|
+
const feather = Math.max(1, KEEP_FEATHER * width);
|
|
16615
|
+
for (const box of keep) {
|
|
16616
|
+
const pad = KEEP_PAD * (box.y1 - box.y0) * height;
|
|
16617
|
+
const x0 = box.x0 * width - pad;
|
|
16618
|
+
const x1 = box.x1 * width + pad;
|
|
16619
|
+
const y0 = box.y0 * height - pad;
|
|
16620
|
+
const y1 = box.y1 * height + pad;
|
|
16621
|
+
const ys = Math.max(0, Math.floor(y0 - feather));
|
|
16622
|
+
const ye = Math.min(height - 1, Math.ceil(y1 + feather));
|
|
16623
|
+
const xs = Math.max(0, Math.floor(x0 - feather));
|
|
16624
|
+
const xe = Math.min(width - 1, Math.ceil(x1 + feather));
|
|
16625
|
+
for (let y = ys; y <= ye; y++) {
|
|
16626
|
+
const dy = Math.max(0, y0 - y, y - y1);
|
|
16627
|
+
for (let x = xs; x <= xe; x++) {
|
|
16628
|
+
const dx = Math.max(0, x0 - x, x - x1);
|
|
16629
|
+
const t = Math.max(0, 1 - Math.hypot(dx, dy) / feather);
|
|
16630
|
+
const w = t * t * (3 - 2 * t);
|
|
16631
|
+
const i = y * width + x;
|
|
16632
|
+
if (w > (out[i] ?? 0)) out[i] = w;
|
|
16633
|
+
}
|
|
16634
|
+
}
|
|
16635
|
+
}
|
|
16636
|
+
return out;
|
|
16637
|
+
}
|
|
16638
|
+
function keptInPlace(keep, ad, paint, width, height) {
|
|
16639
|
+
const diffs = keep.map((box) => boxDiff(box, ad, paint, width, height)).filter((d) => d !== null).sort((a, b) => a - b);
|
|
16640
|
+
return (diffs[Math.floor(diffs.length / 2)] ?? Number.POSITIVE_INFINITY) <= MOVED;
|
|
16641
|
+
}
|
|
16642
|
+
function boxDiff(box, ad, paint, width, height) {
|
|
16643
|
+
let sum = 0;
|
|
16644
|
+
let n = 0;
|
|
16645
|
+
const xs = Math.floor(box.x0 * width);
|
|
16646
|
+
const xe = Math.min(width - 1, Math.ceil(box.x1 * width));
|
|
16647
|
+
for (let y = Math.floor(box.y0 * height); y <= Math.min(height - 1, Math.ceil(box.y1 * height)); y++) {
|
|
16648
|
+
for (let x = xs; x <= xe; x++) {
|
|
16649
|
+
const i = y * width + x;
|
|
16650
|
+
if ((ad[i * 4 + 3] ?? 0) === 0) continue;
|
|
16651
|
+
for (let c = 0; c < 3; c++) sum += Math.abs((ad[i * 4 + c] ?? 0) - (paint[i * 3 + c] ?? 0));
|
|
16652
|
+
n++;
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
return n > 0 ? sum / n : null;
|
|
16656
|
+
}
|
|
16452
16657
|
|
|
16453
16658
|
// src/engine/nodes/local/staticPlace.ts
|
|
16454
16659
|
var Usage = z25.object({ model: z25.string(), inputTokens: z25.number(), outputTokens: z25.number(), costUsd: z25.number() }).strict();
|
|
16455
16660
|
var Fit = z25.object({ scale: z25.number(), x: z25.number(), y: z25.number() }).strict();
|
|
16661
|
+
var Unit = z25.number().min(0).max(1);
|
|
16662
|
+
var Span = z25.object({ x0: Unit, y0: Unit, x1: Unit, y1: Unit }).strict();
|
|
16456
16663
|
var RenderRatio = z25.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
|
|
16457
16664
|
var Copy = z25.array(z25.object({ text: z25.string().min(1), role: z25.string().optional() }).strict());
|
|
16458
16665
|
var staticPlaceNode = defineNode({
|
|
16459
16666
|
id: "static_place",
|
|
16460
|
-
version: "1.
|
|
16667
|
+
version: "1.1.0",
|
|
16461
16668
|
category: "image",
|
|
16462
16669
|
location: "local",
|
|
16463
|
-
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.",
|
|
16670
|
+
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.",
|
|
16464
16671
|
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).",
|
|
16465
16672
|
inputs: z25.object({ image: ImageRef }).strict(),
|
|
16466
16673
|
params: z25.object({
|
|
@@ -16473,6 +16680,8 @@ var staticPlaceNode = defineNode({
|
|
|
16473
16680
|
placed: ImageRef,
|
|
16474
16681
|
shown: ImageRef,
|
|
16475
16682
|
fit: Fit.nullable(),
|
|
16683
|
+
/** Where the words, logo and buttons read sit in `placed`: all the merge lays back of the ad's own pixels. */
|
|
16684
|
+
keep: z25.array(Span).optional(),
|
|
16476
16685
|
reason: z25.enum(["not_read", "inside_band", "too_far"]).optional(),
|
|
16477
16686
|
usage: z25.array(Usage).optional()
|
|
16478
16687
|
}).strict(),
|
|
@@ -16515,18 +16724,28 @@ var staticPlaceNode = defineNode({
|
|
|
16515
16724
|
placed: placedRef,
|
|
16516
16725
|
shown: shownRef,
|
|
16517
16726
|
fit,
|
|
16727
|
+
keep: keptBoxes([...reading.boxes, ...boxes.filter((b) => !reading.boxes.includes(b))], fit),
|
|
16518
16728
|
...spend
|
|
16519
16729
|
};
|
|
16520
16730
|
}
|
|
16521
16731
|
});
|
|
16732
|
+
function keptBoxes(boxes, fit) {
|
|
16733
|
+
const at2 = (offset2, v) => Math.min(1, Math.max(0, offset2 + fit.scale * v));
|
|
16734
|
+
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) }));
|
|
16735
|
+
}
|
|
16522
16736
|
var staticExtendMergeNode = defineNode({
|
|
16523
16737
|
id: "static_extend_merge",
|
|
16524
|
-
version: "1.
|
|
16738
|
+
version: "1.2.0",
|
|
16525
16739
|
category: "image",
|
|
16526
16740
|
location: "local",
|
|
16527
|
-
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
|
|
16741
|
+
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`.",
|
|
16528
16742
|
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.",
|
|
16529
|
-
inputs: z25.object({
|
|
16743
|
+
inputs: z25.object({
|
|
16744
|
+
placed: ImageRef,
|
|
16745
|
+
shown: ImageRef.optional(),
|
|
16746
|
+
image: ImageRef.optional(),
|
|
16747
|
+
keep: z25.array(Span).optional()
|
|
16748
|
+
}).strict(),
|
|
16530
16749
|
params: z25.object({ render_ratio: RenderRatio, paint: z25.record(z25.string(), z25.unknown()).optional() }).strict(),
|
|
16531
16750
|
outputs: z25.object({ image: ImageRef }).strict(),
|
|
16532
16751
|
outputKinds: { image: "image" },
|
|
@@ -16536,7 +16755,7 @@ var staticExtendMergeNode = defineNode({
|
|
|
16536
16755
|
const painted = painting?.path ?? painting?.url;
|
|
16537
16756
|
const placed = inputs.placed.path ?? inputs.placed.url;
|
|
16538
16757
|
if (!painted || !placed) return { image: inputs.placed };
|
|
16539
|
-
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio);
|
|
16758
|
+
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio, inputs.keep);
|
|
16540
16759
|
const image = await ctx.assets.ingestBytes({ bytes: merged, kind: "image", mime: "image/png" });
|
|
16541
16760
|
return { image };
|
|
16542
16761
|
}
|
|
@@ -16565,12 +16784,64 @@ import { z as z26 } from "zod";
|
|
|
16565
16784
|
// src/engine/nodes/local/lib/variant-prefilter.ts
|
|
16566
16785
|
import { createHash as createHash3 } from "crypto";
|
|
16567
16786
|
import sharp8 from "sharp";
|
|
16568
|
-
function
|
|
16787
|
+
function coverOf(from, to) {
|
|
16569
16788
|
const scale = Math.max(to.width / from.width, to.height / from.height);
|
|
16570
|
-
const
|
|
16571
|
-
const
|
|
16572
|
-
|
|
16573
|
-
|
|
16789
|
+
const width = from.width * scale;
|
|
16790
|
+
const height = from.height * scale;
|
|
16791
|
+
return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
|
|
16792
|
+
}
|
|
16793
|
+
function framingOf(from, to) {
|
|
16794
|
+
let frame;
|
|
16795
|
+
return {
|
|
16796
|
+
place: (boxes) => {
|
|
16797
|
+
frame = frameKeeping(boxes, from, to);
|
|
16798
|
+
return coverFitBoxes(boxes, from, to, frame);
|
|
16799
|
+
},
|
|
16800
|
+
moved: () => {
|
|
16801
|
+
const { centre } = coverOf(from, to);
|
|
16802
|
+
return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
|
|
16803
|
+
}
|
|
16804
|
+
};
|
|
16805
|
+
}
|
|
16806
|
+
async function cutToFrame(bytes, frame, to) {
|
|
16807
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16808
|
+
const { scale } = coverOf({ width, height }, to);
|
|
16809
|
+
const w = Math.min(width, Math.round(to.width / scale));
|
|
16810
|
+
const h = Math.min(height, Math.round(to.height / scale));
|
|
16811
|
+
const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
|
|
16812
|
+
const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
|
|
16813
|
+
return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
|
|
16814
|
+
}
|
|
16815
|
+
var FRAME_MARGIN = 0.02;
|
|
16816
|
+
function frameKeeping(boxes, from, to) {
|
|
16817
|
+
const cover = coverOf(from, to);
|
|
16818
|
+
if (boxes.length === 0) return cover.centre;
|
|
16819
|
+
const along = (offset2, extent, frame, lo, hi) => {
|
|
16820
|
+
const low = lo * extent;
|
|
16821
|
+
const high = hi * extent;
|
|
16822
|
+
const room = extent - frame;
|
|
16823
|
+
const margin = FRAME_MARGIN * frame;
|
|
16824
|
+
for (const m of [margin, 0]) {
|
|
16825
|
+
const min = Math.max(0, high + m - frame);
|
|
16826
|
+
const max = Math.min(room, low - m);
|
|
16827
|
+
if (min <= max) return Math.min(max, Math.max(min, offset2));
|
|
16828
|
+
}
|
|
16829
|
+
return offset2;
|
|
16830
|
+
};
|
|
16831
|
+
const x0 = Math.min(...boxes.map((b) => b.x0));
|
|
16832
|
+
const x1 = Math.max(...boxes.map((b) => b.x1));
|
|
16833
|
+
const y0 = Math.min(...boxes.map((b) => b.y0));
|
|
16834
|
+
const y1 = Math.max(...boxes.map((b) => b.y1));
|
|
16835
|
+
return {
|
|
16836
|
+
left: along(cover.centre.left, cover.width, to.width, x0, x1),
|
|
16837
|
+
top: along(cover.centre.top, cover.height, to.height, y0, y1)
|
|
16838
|
+
};
|
|
16839
|
+
}
|
|
16840
|
+
function coverFitBoxes(boxes, from, to, frame) {
|
|
16841
|
+
const cover = coverOf(from, to);
|
|
16842
|
+
const { left: offX, top: offY } = frame ?? cover.centre;
|
|
16843
|
+
const x = (f) => (f * cover.width - offX) / to.width;
|
|
16844
|
+
const y = (f) => (f * cover.height - offY) / to.height;
|
|
16574
16845
|
return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
|
|
16575
16846
|
}
|
|
16576
16847
|
var NO_LOGO = "The brand's logo was not found on it.";
|
|
@@ -16624,34 +16895,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
16624
16895
|
});
|
|
16625
16896
|
const format = plan.master.format;
|
|
16626
16897
|
const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
|
|
16898
|
+
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
|
|
16627
16899
|
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
|
-
})
|
|
16900
|
+
variants.map(
|
|
16901
|
+
(variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
|
|
16902
|
+
)
|
|
16652
16903
|
);
|
|
16653
16904
|
const outcome = prefilterOutcome(checked);
|
|
16654
|
-
|
|
16905
|
+
const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
|
|
16906
|
+
return {
|
|
16907
|
+
...outcome,
|
|
16908
|
+
kept: seededOrder(outcome.kept, variants),
|
|
16909
|
+
...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
|
|
16910
|
+
};
|
|
16911
|
+
}
|
|
16912
|
+
async function checkVariant(variant, c) {
|
|
16913
|
+
const { format, lines, brand, grade, want, opts } = c;
|
|
16914
|
+
const source = variant.path ?? variant.url;
|
|
16915
|
+
if (!source) return { index: variant.index, blocking: null };
|
|
16916
|
+
try {
|
|
16917
|
+
const bytes = await bytesOf(source);
|
|
16918
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16919
|
+
const framing = framingOf({ width, height }, c.size);
|
|
16920
|
+
const check = await checkStaticImage(bytes, {
|
|
16921
|
+
format,
|
|
16922
|
+
lines,
|
|
16923
|
+
allowed: brand ? [brand] : [],
|
|
16924
|
+
...grade,
|
|
16925
|
+
toFrame: framing.place,
|
|
16926
|
+
...opts.read ? { read: opts.read } : {},
|
|
16927
|
+
...opts.onSpend ? { onSpend: opts.onSpend } : {}
|
|
16928
|
+
});
|
|
16929
|
+
if (check.status === "not_checked") return { index: variant.index, blocking: null };
|
|
16930
|
+
const prefix = `${format}: `;
|
|
16931
|
+
const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
|
|
16932
|
+
const frame = framing.moved();
|
|
16933
|
+
return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
|
|
16934
|
+
} catch {
|
|
16935
|
+
return { index: variant.index, blocking: null };
|
|
16936
|
+
}
|
|
16655
16937
|
}
|
|
16656
16938
|
function seededOrder(indexes, variants) {
|
|
16657
16939
|
const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
|
|
@@ -16727,7 +17009,7 @@ async function readSpec(ref) {
|
|
|
16727
17009
|
}
|
|
16728
17010
|
var staticPrefilterNode = defineNode({
|
|
16729
17011
|
id: "static_prefilter",
|
|
16730
|
-
version: "1.
|
|
17012
|
+
version: "1.1.0",
|
|
16731
17013
|
category: "image",
|
|
16732
17014
|
location: "local",
|
|
16733
17015
|
// Reads each variant's pixels itself, from its file or its durable URL, so the variants
|
|
@@ -16760,7 +17042,8 @@ var staticPrefilterNode = defineNode({
|
|
|
16760
17042
|
images: z26.array(ImageRef).min(1),
|
|
16761
17043
|
// Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
|
|
16762
17044
|
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.
|
|
17045
|
+
// The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
|
|
17046
|
+
// master's frame where every word stays whole when its centre would cut one, else as rendered.
|
|
16764
17047
|
image: ImageRef.optional(),
|
|
16765
17048
|
// Which variant it is (its place among the inputs).
|
|
16766
17049
|
chosen: z26.number().int().min(0).optional(),
|
|
@@ -16773,12 +17056,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16773
17056
|
}).strict(),
|
|
16774
17057
|
outputKinds: { images: "image", image: "image" },
|
|
16775
17058
|
cost: () => ({ credits: 0, seconds_estimate: 1 }),
|
|
16776
|
-
async execute({ inputs, params }) {
|
|
16777
|
-
const variants = inputs.images.map((
|
|
17059
|
+
async execute({ inputs, params, ctx }) {
|
|
17060
|
+
const variants = inputs.images.map((image2, index) => ({
|
|
16778
17061
|
index,
|
|
16779
|
-
sha256:
|
|
16780
|
-
...
|
|
16781
|
-
...
|
|
17062
|
+
sha256: image2.sha256,
|
|
17063
|
+
...image2.path ? { path: image2.path } : {},
|
|
17064
|
+
...image2.url ? { url: image2.url } : {}
|
|
16782
17065
|
}));
|
|
16783
17066
|
const usage = [];
|
|
16784
17067
|
const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
|
|
@@ -16794,10 +17077,11 @@ var staticPrefilterNode = defineNode({
|
|
|
16794
17077
|
{ model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
|
|
16795
17078
|
) : null;
|
|
16796
17079
|
const chosen = tie?.chosen ?? shown[0] ?? 0;
|
|
17080
|
+
const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
|
|
16797
17081
|
return {
|
|
16798
17082
|
images: kept.length > 0 ? kept : inputs.images,
|
|
16799
17083
|
order: shown,
|
|
16800
|
-
...
|
|
17084
|
+
...image ? { image } : {},
|
|
16801
17085
|
chosen,
|
|
16802
17086
|
...tie ? { reasoning: tie.reasoning } : {},
|
|
16803
17087
|
dropped: result.dropped,
|
|
@@ -16806,6 +17090,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16806
17090
|
};
|
|
16807
17091
|
}
|
|
16808
17092
|
});
|
|
17093
|
+
async function framed(picture, frame, size2, ctx) {
|
|
17094
|
+
const source = picture?.path ?? picture?.url;
|
|
17095
|
+
if (!picture || !frame || !size2 || !source) return picture;
|
|
17096
|
+
const bytes = await cutToFrame(await bytesOf(source), frame, size2);
|
|
17097
|
+
return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
|
|
17098
|
+
}
|
|
16809
17099
|
|
|
16810
17100
|
// src/engine/nodes/local/staticSafeFit.ts
|
|
16811
17101
|
import { z as z27 } from "zod";
|
|
@@ -18385,6 +18675,7 @@ export {
|
|
|
18385
18675
|
AD_DESCRIBE_MODEL,
|
|
18386
18676
|
BLUEPRINT_ANALYSIS_MODEL,
|
|
18387
18677
|
VIEWER_CHECK_MODEL,
|
|
18678
|
+
CUSTOMER_INSIGHTS_MODEL,
|
|
18388
18679
|
GLANCE_CHECK_MODEL,
|
|
18389
18680
|
VIDEO_DECONSTRUCT_MODEL,
|
|
18390
18681
|
VOICEOVER_MODEL,
|
|
@@ -18456,6 +18747,10 @@ export {
|
|
|
18456
18747
|
isAdSpec,
|
|
18457
18748
|
conceptSummary,
|
|
18458
18749
|
conceptProblems,
|
|
18750
|
+
unwrittenProblems,
|
|
18751
|
+
sceneWordProblems,
|
|
18752
|
+
SPEC_SHAPES_HINT,
|
|
18753
|
+
adSpecErrorText,
|
|
18459
18754
|
readAdSpec,
|
|
18460
18755
|
withRenderedLettering,
|
|
18461
18756
|
conceptClassOf,
|
|
@@ -18482,6 +18777,7 @@ export {
|
|
|
18482
18777
|
STATIC_SAFE_FIT_ROLE,
|
|
18483
18778
|
STATIC_PLACE_ROLE,
|
|
18484
18779
|
LOGO_COMPOSITE_ROLE,
|
|
18780
|
+
SERIES_LEAD_ROLE,
|
|
18485
18781
|
STATIC_FIX_ROLE,
|
|
18486
18782
|
LOGO_COMPOSITE_DEFAULT,
|
|
18487
18783
|
nodesWithRole,
|
|
@@ -18543,4 +18839,4 @@ export {
|
|
|
18543
18839
|
defaultRegistry,
|
|
18544
18840
|
createEngineFromEnv
|
|
18545
18841
|
};
|
|
18546
|
-
//# sourceMappingURL=chunk-
|
|
18842
|
+
//# sourceMappingURL=chunk-O5HQRXS6.js.map
|