@koda-sl/baker-cli 0.396.0-dev.88120240 → 0.397.0-dev.afb6a264b
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 +14 -6
- package/canvas/static-ad-composition/index.html +119 -63
- package/canvas/static-ad-composition/meta.json +6 -6
- package/dist/cli.js +792 -215
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -62543,10 +62543,10 @@ function parseColor(raw) {
|
|
|
62543
62543
|
function parseOklch(c) {
|
|
62544
62544
|
const m = c.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)/i);
|
|
62545
62545
|
if (!m) return null;
|
|
62546
|
-
const
|
|
62546
|
+
const pct2 = (s, scale) => s.endsWith("%") ? Number.parseFloat(s) / 100 * scale : Number.parseFloat(s);
|
|
62547
62547
|
const alpha = m[4];
|
|
62548
|
-
const a = alpha === void 0 ? 1 :
|
|
62549
|
-
return { ...oklchToRgb(
|
|
62548
|
+
const a = alpha === void 0 ? 1 : pct2(alpha, 1);
|
|
62549
|
+
return { ...oklchToRgb(pct2(m[1] ?? "0", 1), pct2(m[2] ?? "0", 0.4), Number.parseFloat(m[3] ?? "0")), a };
|
|
62550
62550
|
}
|
|
62551
62551
|
function oklchToRgb(L, C, H) {
|
|
62552
62552
|
const hRad = H * Math.PI / 180;
|
|
@@ -63920,6 +63920,36 @@ async function markInkIsLight(markPath) {
|
|
|
63920
63920
|
|
|
63921
63921
|
// src/engine/scaffold/static-compose.ts
|
|
63922
63922
|
import { z as z48 } from "zod";
|
|
63923
|
+
|
|
63924
|
+
// src/engine/scaffold/static-formats.ts
|
|
63925
|
+
var CROP_TOLERANCE = 0.1;
|
|
63926
|
+
function ratioValue(ratio) {
|
|
63927
|
+
const [w, h] = ratio.split(":").map(Number);
|
|
63928
|
+
if (!w || !h || !Number.isFinite(w) || !Number.isFinite(h)) throw new Error(`not a W:H ratio: "${ratio}"`);
|
|
63929
|
+
return w / h;
|
|
63930
|
+
}
|
|
63931
|
+
function ratioDelta(a, b) {
|
|
63932
|
+
return Math.abs(ratioValue(a) / ratioValue(b) - 1);
|
|
63933
|
+
}
|
|
63934
|
+
function round3(n) {
|
|
63935
|
+
return Math.round(n * 1e3) / 1e3;
|
|
63936
|
+
}
|
|
63937
|
+
function planStaticFormats(genModel, formats) {
|
|
63938
|
+
const first = formats[0];
|
|
63939
|
+
if (!first) throw new Error("a static ad needs at least one format");
|
|
63940
|
+
const plateRatio = nearestSupportedAspectRatio("image_generate", genModel, first);
|
|
63941
|
+
return {
|
|
63942
|
+
plateRatio,
|
|
63943
|
+
formats: formats.map((format) => {
|
|
63944
|
+
const delta = ratioDelta(format, plateRatio);
|
|
63945
|
+
if (delta <= CROP_TOLERANCE) return { format, source: "crop", delta: round3(delta) };
|
|
63946
|
+
const extendRatio = nearestSupportedAspectRatio("image_generate", genModel, format);
|
|
63947
|
+
return { format, source: "extend", delta: round3(ratioDelta(format, extendRatio)), extendRatio };
|
|
63948
|
+
})
|
|
63949
|
+
};
|
|
63950
|
+
}
|
|
63951
|
+
|
|
63952
|
+
// src/engine/scaffold/static-compose.ts
|
|
63923
63953
|
var WinningMechanismSchema = z48.object({ kind: z48.string(), device: z48.string(), why_it_works: z48.string().optional() }).loose();
|
|
63924
63954
|
function winningMechanisms(blueprint) {
|
|
63925
63955
|
const raw = blueprint?.winning_mechanisms;
|
|
@@ -63954,7 +63984,31 @@ function copyEntries(blueprint) {
|
|
|
63954
63984
|
return entries.flatMap((e) => {
|
|
63955
63985
|
const text2 = typeof e?.text === "string" ? e.text.trim() : "";
|
|
63956
63986
|
const role = normalize(e?.role);
|
|
63957
|
-
return text2 && role !== "logo" ? [
|
|
63987
|
+
return text2 && role !== "logo" ? [
|
|
63988
|
+
{
|
|
63989
|
+
text: text2,
|
|
63990
|
+
role,
|
|
63991
|
+
treatment: normalize(e?.treatment),
|
|
63992
|
+
surface: normalize(e?.surface),
|
|
63993
|
+
placement: normalize(e?.placement)
|
|
63994
|
+
}
|
|
63995
|
+
] : [];
|
|
63996
|
+
});
|
|
63997
|
+
}
|
|
63998
|
+
var IN_SCENE_ROLES = /* @__PURE__ */ new Set(["in_scene", "scene", "ui", "screen", "screenshot", "packaging", "signage", "sign"]);
|
|
63999
|
+
var IN_SCENE_PLACEMENT = /\b(phone|screen|screenshot|app|packaging|package|bottle|jar|signage|signboard|handwritten|receipt|whiteboard|sticky note|notepad)\b/;
|
|
64000
|
+
function isInScene(entry) {
|
|
64001
|
+
if (entry.surface === "in_scene") return true;
|
|
64002
|
+
if (entry.surface === "overlay") return false;
|
|
64003
|
+
return IN_SCENE_ROLES.has(entry.role) || IN_SCENE_PLACEMENT.test(entry.placement);
|
|
64004
|
+
}
|
|
64005
|
+
function inSceneText(blueprint) {
|
|
64006
|
+
const raw = blueprint?.text_content;
|
|
64007
|
+
const entries = Array.isArray(raw) ? raw : [];
|
|
64008
|
+
return copyEntries(blueprint).filter(isInScene).map((entry) => {
|
|
64009
|
+
const source = entries.find((e) => typeof e?.text === "string" && e.text.trim() === entry.text);
|
|
64010
|
+
const placement = typeof source?.placement === "string" ? source.placement.trim() : "";
|
|
64011
|
+
return { text: entry.text, placement };
|
|
63958
64012
|
});
|
|
63959
64013
|
}
|
|
63960
64014
|
function pairCopy(blueprint) {
|
|
@@ -64022,6 +64076,9 @@ function hexOrNull(value2) {
|
|
|
64022
64076
|
function ctaTextOf(blueprint) {
|
|
64023
64077
|
return normalize(blueprint?.ad_signals?.cta?.text);
|
|
64024
64078
|
}
|
|
64079
|
+
function conceptHasLogo(blueprint) {
|
|
64080
|
+
return logoPlacement(blueprint) !== null;
|
|
64081
|
+
}
|
|
64025
64082
|
function logoPlacement(blueprint) {
|
|
64026
64083
|
const region = regionsOf(blueprint).find(isLogoRegion);
|
|
64027
64084
|
if (region) return { band: bandOf(region, "top"), align: alignOf(void 0, region) };
|
|
@@ -64033,29 +64090,151 @@ function inkOn(background) {
|
|
|
64033
64090
|
const luma = hexLuma(background);
|
|
64034
64091
|
return luma !== null && luma > 150 ? "#111111" : "#ffffff";
|
|
64035
64092
|
}
|
|
64036
|
-
function
|
|
64093
|
+
function treatmentNamed(description) {
|
|
64094
|
+
if (!description) return null;
|
|
64095
|
+
if (/underline/.test(description)) return "underline";
|
|
64096
|
+
if (/\b(button|pill|capsule)\b/.test(description)) return "button";
|
|
64097
|
+
if (/arrow|→|->|chevron|›|»/.test(description)) return "arrow";
|
|
64098
|
+
if (/sticker|starburst|stamp|seal|badge/.test(description)) return "sticker";
|
|
64099
|
+
if (/highlight|marker|label|\btag\b|banner|ribbon|\bbox\b/.test(description)) return "highlight";
|
|
64100
|
+
if (/rounded|rectangle/.test(description)) return "button";
|
|
64101
|
+
if (/plain|\btext\b|\blink\b|\bnone\b/.test(description)) return "plain";
|
|
64102
|
+
return null;
|
|
64103
|
+
}
|
|
64104
|
+
function shapeNamed(description) {
|
|
64105
|
+
if (/pill|capsule/.test(description)) return "pill";
|
|
64106
|
+
if (/round/.test(description)) return "rounded";
|
|
64107
|
+
if (/square|sharp|rectangle/.test(description)) return "square";
|
|
64108
|
+
return "rounded";
|
|
64109
|
+
}
|
|
64110
|
+
function ctaStyleOf(blueprint) {
|
|
64111
|
+
return normalize(blueprint?.ad_signals?.cta?.style);
|
|
64112
|
+
}
|
|
64113
|
+
function treatmentOf(args8) {
|
|
64114
|
+
const { entry, slot, role, brand } = args8;
|
|
64115
|
+
const region = slot?.region;
|
|
64116
|
+
const regionShape = [normalize(region?.shape), normalize(region?.notes)].join(" ").trim();
|
|
64117
|
+
const descriptions = [
|
|
64118
|
+
entry.treatment,
|
|
64119
|
+
normalize(slot?.text.treatment),
|
|
64120
|
+
...role === "cta" ? [args8.ctaStyle, regionShape] : []
|
|
64121
|
+
];
|
|
64122
|
+
let kind = "plain";
|
|
64123
|
+
let described = "";
|
|
64124
|
+
for (const description of descriptions) {
|
|
64125
|
+
const named = treatmentNamed(description);
|
|
64126
|
+
if (named) {
|
|
64127
|
+
kind = named;
|
|
64128
|
+
described = `${description} ${regionShape}`;
|
|
64129
|
+
break;
|
|
64130
|
+
}
|
|
64131
|
+
}
|
|
64132
|
+
if (kind === "plain" || kind === "underline" || kind === "arrow") return { kind };
|
|
64133
|
+
const regionFill = hexOrNull(region?.background_color);
|
|
64134
|
+
const background = kind === "button" ? hexOrNull(brand.ctaColor) ?? hexOrNull(brand.palette[0]) ?? regionFill ?? "#111111" : hexOrNull(brand.palette[1]) ?? hexOrNull(brand.palette[0]) ?? regionFill ?? "#111111";
|
|
64135
|
+
return { kind, shape: shapeNamed(described), background, color: inkOn(background) };
|
|
64136
|
+
}
|
|
64137
|
+
function cover(from, to) {
|
|
64138
|
+
return from > to ? { sx: from / to, sy: 1 } : { sx: 1, sy: to / from };
|
|
64139
|
+
}
|
|
64140
|
+
function contain(from, to) {
|
|
64141
|
+
return from > to ? { sx: 1, sy: to / from } : { sx: from / to, sy: 1 };
|
|
64142
|
+
}
|
|
64143
|
+
var scaleSpan = ([a, b], k) => [(a - 0.5) * k + 0.5, (b - 0.5) * k + 0.5];
|
|
64144
|
+
function clampSpan([a, b], lo, hi) {
|
|
64145
|
+
if (b - a >= hi - lo) return [lo, hi];
|
|
64146
|
+
if (a < lo) return [lo, lo + (b - a)];
|
|
64147
|
+
if (b > hi) return [hi - (b - a), hi];
|
|
64148
|
+
return [a, b];
|
|
64149
|
+
}
|
|
64150
|
+
var pct = (n) => Math.round(n * 1e3) / 10;
|
|
64151
|
+
function placeRegion(x, y, geometry) {
|
|
64152
|
+
let xs = [x[0] / 100, x[1] / 100];
|
|
64153
|
+
let ys = [y[0] / 100, y[1] / 100];
|
|
64154
|
+
const plate = geometry.plate;
|
|
64155
|
+
if (plate) {
|
|
64156
|
+
const format = ratioValue(geometry.format);
|
|
64157
|
+
let from = ratioValue(plate.ratio);
|
|
64158
|
+
if (plate.extendRatio) {
|
|
64159
|
+
const extended = ratioValue(plate.extendRatio);
|
|
64160
|
+
const fit = contain(from, extended);
|
|
64161
|
+
xs = scaleSpan(xs, fit.sx);
|
|
64162
|
+
ys = scaleSpan(ys, fit.sy);
|
|
64163
|
+
from = extended;
|
|
64164
|
+
}
|
|
64165
|
+
const crop = cover(from, format);
|
|
64166
|
+
xs = scaleSpan(xs, crop.sx);
|
|
64167
|
+
ys = scaleSpan(ys, crop.sy);
|
|
64168
|
+
}
|
|
64169
|
+
const { safe } = geometry;
|
|
64170
|
+
const [x0, x1] = clampSpan(xs, safe.left, 1 - safe.right);
|
|
64171
|
+
const [y0, y1] = clampSpan(ys, safe.top, 1 - safe.bottom);
|
|
64172
|
+
return { x: [pct(x0), pct(x1)], y: [pct(y0), pct(y1)] };
|
|
64173
|
+
}
|
|
64174
|
+
function regionId(region, index) {
|
|
64175
|
+
const id = typeof region.id === "string" ? region.id.trim().replace(/[^A-Za-z0-9_-]+/g, "-") : "";
|
|
64176
|
+
return `region-${id || index}`;
|
|
64177
|
+
}
|
|
64178
|
+
function distinctFrom(ground, candidates) {
|
|
64179
|
+
return candidates.find((c) => c !== null && c !== ground) ?? inkOn(ground);
|
|
64180
|
+
}
|
|
64181
|
+
function composeSpec(blueprint, brand, options = {}) {
|
|
64037
64182
|
const ctaText = ctaTextOf(blueprint);
|
|
64038
|
-
const
|
|
64183
|
+
const ctaStyle = ctaStyleOf(blueprint);
|
|
64184
|
+
const formats = options.formats ?? [];
|
|
64185
|
+
const regions = regionsOf(blueprint);
|
|
64186
|
+
const groups = /* @__PURE__ */ new Map();
|
|
64187
|
+
const groupFor = (region, band, align) => {
|
|
64188
|
+
const x = span(region?.x_pct);
|
|
64189
|
+
const y = span(region?.y_pct);
|
|
64190
|
+
const id = region && x && y ? regionId(region, regions.indexOf(region)) : `band-${band}`;
|
|
64191
|
+
if (!groups.has(id)) {
|
|
64192
|
+
const at = x && y ? Object.fromEntries(formats.map((g) => [g.format, placeRegion(x, y, g)])) : {};
|
|
64193
|
+
groups.set(id, { id, band, align, at });
|
|
64194
|
+
}
|
|
64195
|
+
return id;
|
|
64196
|
+
};
|
|
64197
|
+
const ground = hexOrNull(brand.palette[0]) ?? "#111111";
|
|
64198
|
+
const overlay = pairCopy(blueprint).filter(({ entry }) => !isInScene(entry));
|
|
64199
|
+
const copy = overlay.map(({ entry, slot }) => {
|
|
64039
64200
|
const role = roleOf(entry, ctaText);
|
|
64040
64201
|
const region = slot?.region;
|
|
64202
|
+
const band = bandOf(region, DEFAULT_BAND[role]);
|
|
64203
|
+
const align = alignOf(slot?.text.align, region);
|
|
64204
|
+
let treatment = treatmentOf({ entry, slot, role, ctaStyle, brand });
|
|
64205
|
+
if (options.textOnly && "background" in treatment && treatment.background === ground) {
|
|
64206
|
+
const background = distinctFrom(ground, [hexOrNull(brand.ctaColor), ...brand.palette.map(hexOrNull)]);
|
|
64207
|
+
treatment = { ...treatment, background, color: inkOn(background) };
|
|
64208
|
+
}
|
|
64041
64209
|
return {
|
|
64042
64210
|
role,
|
|
64043
64211
|
// The source's line breaks fall elsewhere in our font: the composition wraps instead.
|
|
64044
64212
|
text: entry.text.replace(/\s*\n\s*/g, " "),
|
|
64045
|
-
band
|
|
64046
|
-
align
|
|
64213
|
+
band,
|
|
64214
|
+
align,
|
|
64047
64215
|
size: pick2(slot?.text.size, ["x_large", "large", "medium", "small"]) ?? DEFAULT_SIZE[role],
|
|
64048
64216
|
weight: pick2(slot?.text.weight, ["bold", "semibold", "regular"]) ?? (role === "headline" ? "bold" : "regular"),
|
|
64049
64217
|
case: pick2(slot?.text.case, ["upper", "title", "sentence"]),
|
|
64050
|
-
|
|
64218
|
+
// The source's ink was chosen for the source's picture; on our brand ground it is ours.
|
|
64219
|
+
color: options.textOnly ? null : hexOrNull(slot?.text.color),
|
|
64220
|
+
treatment,
|
|
64221
|
+
group: groupFor(region, band, align)
|
|
64051
64222
|
};
|
|
64052
64223
|
});
|
|
64224
|
+
const placement = options.logo ? logoPlacement(blueprint) : null;
|
|
64225
|
+
const logoRegion = regions.find(isLogoRegion);
|
|
64226
|
+
const logo = placement ? {
|
|
64227
|
+
...placement,
|
|
64228
|
+
group: logoRegion && span(logoRegion.x_pct) && span(logoRegion.y_pct) ? groupFor(logoRegion, placement.band, placement.align) : null
|
|
64229
|
+
} : null;
|
|
64053
64230
|
const ctaBackground = hexOrNull(brand.ctaColor) ?? hexOrNull(brand.palette[0]) ?? "#111111";
|
|
64054
64231
|
return {
|
|
64055
64232
|
copy,
|
|
64056
|
-
logo
|
|
64233
|
+
logo,
|
|
64234
|
+
groups: [...groups.values()],
|
|
64057
64235
|
palette: brand.palette.flatMap((h) => hexOrNull(h) ?? []),
|
|
64058
|
-
cta: { background: ctaBackground, color: inkOn(ctaBackground) }
|
|
64236
|
+
cta: { background: ctaBackground, color: inkOn(ctaBackground) },
|
|
64237
|
+
background: { color: ground, ink: inkOn(ground) }
|
|
64059
64238
|
};
|
|
64060
64239
|
}
|
|
64061
64240
|
var SCENE_KEYS = [
|
|
@@ -64090,15 +64269,16 @@ function scrub(value2, names) {
|
|
|
64090
64269
|
return value2;
|
|
64091
64270
|
}
|
|
64092
64271
|
var BAND_SPAN = { top: [4, 32], middle: [40, 60], bottom: [72, 96] };
|
|
64093
|
-
function reservedAreas(blueprint) {
|
|
64272
|
+
function reservedAreas(blueprint, logoDrawn2) {
|
|
64094
64273
|
const areas = [];
|
|
64095
64274
|
const seen = /* @__PURE__ */ new Set();
|
|
64096
|
-
const logo = regionsOf(blueprint).find(isLogoRegion);
|
|
64275
|
+
const logo = logoDrawn2 ? regionsOf(blueprint).find(isLogoRegion) : void 0;
|
|
64097
64276
|
const logoX = span(logo?.x_pct);
|
|
64098
64277
|
const logoY = span(logo?.y_pct);
|
|
64099
64278
|
if (logo && logoX && logoY) areas.push({ for: "logo", x_pct: logoX, y_pct: logoY });
|
|
64100
64279
|
const ctaText = ctaTextOf(blueprint);
|
|
64101
64280
|
for (const { entry, slot } of pairCopy(blueprint)) {
|
|
64281
|
+
if (isInScene(entry)) continue;
|
|
64102
64282
|
const role = roleOf(entry, ctaText);
|
|
64103
64283
|
const region = slot?.region;
|
|
64104
64284
|
if (region && seen.has(region)) continue;
|
|
@@ -64113,19 +64293,25 @@ function reservedAreas(blueprint) {
|
|
|
64113
64293
|
}
|
|
64114
64294
|
return areas;
|
|
64115
64295
|
}
|
|
64116
|
-
function platePlan(blueprint) {
|
|
64296
|
+
function platePlan(blueprint, options = {}) {
|
|
64117
64297
|
const names = brandNames(blueprint);
|
|
64118
64298
|
const source = blueprint ?? {};
|
|
64119
64299
|
const scene = Object.fromEntries(SCENE_KEYS.flatMap((k) => k in source ? [[k, scrub(source[k], names)]] : []));
|
|
64120
64300
|
const mechanisms = winningMechanisms(blueprint).filter((m) => normalize(m.kind) !== "verbal").map((m) => scrub(m, names));
|
|
64121
|
-
|
|
64301
|
+
const inScene = inSceneText(blueprint);
|
|
64302
|
+
return {
|
|
64303
|
+
scene,
|
|
64304
|
+
reserved_for_overlay: reservedAreas(blueprint, options.logo === true),
|
|
64305
|
+
mechanisms_to_keep: mechanisms,
|
|
64306
|
+
...inScene.length > 0 ? { in_scene_text: inScene } : {}
|
|
64307
|
+
};
|
|
64122
64308
|
}
|
|
64123
|
-
function renderPlateSpec(blueprint) {
|
|
64124
|
-
return `${JSON.stringify(platePlan(blueprint), null, 2)}
|
|
64309
|
+
function renderPlateSpec(blueprint, options = {}) {
|
|
64310
|
+
return `${JSON.stringify(platePlan(blueprint, options), null, 2)}
|
|
64125
64311
|
`;
|
|
64126
64312
|
}
|
|
64127
|
-
function renderComposeSpec(blueprint, brand) {
|
|
64128
|
-
return `${JSON.stringify(composeSpec(blueprint, brand), null, 2).replace(/</g, "\\u003c")}
|
|
64313
|
+
function renderComposeSpec(blueprint, brand, options = {}) {
|
|
64314
|
+
return `${JSON.stringify(composeSpec(blueprint, brand, options), null, 2).replace(/</g, "\\u003c")}
|
|
64129
64315
|
`;
|
|
64130
64316
|
}
|
|
64131
64317
|
|
|
@@ -64282,6 +64468,66 @@ async function findWorkspaceRoot(start) {
|
|
|
64282
64468
|
dir = parent;
|
|
64283
64469
|
}
|
|
64284
64470
|
}
|
|
64471
|
+
var snapshotsOf = (nodes) => nodes.filter((n) => n.type === "hyperframe_snapshot");
|
|
64472
|
+
var refId = (ref) => typeof ref === "string" ? ref.match(/^\$ref:([a-z][a-z0-9_]*)\./)?.[1] ?? null : null;
|
|
64473
|
+
async function isRealFile(file) {
|
|
64474
|
+
if (typeof file !== "string" || file.includes("[TODO")) return false;
|
|
64475
|
+
return stat(file).then(
|
|
64476
|
+
(s) => s.isFile(),
|
|
64477
|
+
() => false
|
|
64478
|
+
);
|
|
64479
|
+
}
|
|
64480
|
+
async function releaseUnsuppliedLogo(canvas) {
|
|
64481
|
+
const holder = canvas;
|
|
64482
|
+
if (!Array.isArray(holder.nodes)) return false;
|
|
64483
|
+
const nodes = holder.nodes;
|
|
64484
|
+
const logoIds = new Set(snapshotsOf(nodes).flatMap((n) => refId(n.inputs?.logo) ?? []));
|
|
64485
|
+
const empty = /* @__PURE__ */ new Set();
|
|
64486
|
+
for (const id of logoIds) {
|
|
64487
|
+
const node = nodes.find((n) => n.id === id && n.type === "ingest");
|
|
64488
|
+
const p = node?.params;
|
|
64489
|
+
if (p?.source === "path" && !await isRealFile(p.path)) empty.add(id);
|
|
64490
|
+
}
|
|
64491
|
+
if (empty.size === 0) return false;
|
|
64492
|
+
for (const node of snapshotsOf(nodes)) {
|
|
64493
|
+
const id = refId(node.inputs?.logo);
|
|
64494
|
+
if (id && empty.has(id) && node.inputs) delete node.inputs.logo;
|
|
64495
|
+
}
|
|
64496
|
+
holder.nodes = nodes.filter((n) => !(typeof n.id === "string" && empty.has(n.id)));
|
|
64497
|
+
return true;
|
|
64498
|
+
}
|
|
64499
|
+
function staticShape(nodes) {
|
|
64500
|
+
const snapshots = snapshotsOf(nodes);
|
|
64501
|
+
const formats = snapshots.flatMap((n) => {
|
|
64502
|
+
const frame = n.params?.frame ?? {};
|
|
64503
|
+
if (typeof frame.format !== "string") return [];
|
|
64504
|
+
const safe = frame.safe;
|
|
64505
|
+
if (!safe || typeof safe.top !== "number") return [];
|
|
64506
|
+
const plate = frame.plate;
|
|
64507
|
+
if (plate === void 0) return [];
|
|
64508
|
+
return [
|
|
64509
|
+
{
|
|
64510
|
+
format: frame.format,
|
|
64511
|
+
safe,
|
|
64512
|
+
...plate && typeof plate.ratio === "string" ? {
|
|
64513
|
+
plate: {
|
|
64514
|
+
ratio: plate.ratio,
|
|
64515
|
+
...typeof plate.extend_ratio === "string" ? { extendRatio: plate.extend_ratio } : {}
|
|
64516
|
+
}
|
|
64517
|
+
} : {}
|
|
64518
|
+
}
|
|
64519
|
+
];
|
|
64520
|
+
});
|
|
64521
|
+
return {
|
|
64522
|
+
logoWired: snapshots.some((n) => refId(n.inputs?.logo) !== null),
|
|
64523
|
+
textOnly: snapshots.length > 0 && snapshots.every((n) => n.params?.frame?.plate === null),
|
|
64524
|
+
formats
|
|
64525
|
+
};
|
|
64526
|
+
}
|
|
64527
|
+
function composeOptionsFor(canvas, blueprint) {
|
|
64528
|
+
const shape = staticShape(nodesOf(canvas));
|
|
64529
|
+
return { logo: shape.logoWired && conceptHasLogo(blueprint), formats: shape.formats, textOnly: shape.textOnly };
|
|
64530
|
+
}
|
|
64285
64531
|
async function syncStaticAdSpecs(canvas, log) {
|
|
64286
64532
|
const nodes = nodesOf(canvas);
|
|
64287
64533
|
const platePath = ingestPath(nodes, "plate_spec");
|
|
@@ -64291,6 +64537,10 @@ async function syncStaticAdSpecs(canvas, log) {
|
|
|
64291
64537
|
const blueprint = JSON.parse(await readFile5(source, "utf8"));
|
|
64292
64538
|
const root = await findWorkspaceRoot(path6.dirname(source));
|
|
64293
64539
|
const brand = root ? await readStaticBrandKit(root) : { palette: [] };
|
|
64540
|
+
if (await releaseUnsuppliedLogo(canvas)) {
|
|
64541
|
+
log("[logo] the logo slot holds no real file, so the ad is drawn without a logo (Meta shows the page name and picture)");
|
|
64542
|
+
}
|
|
64543
|
+
const { logo, formats, textOnly } = composeOptionsFor(canvas, blueprint);
|
|
64294
64544
|
let changed = 0;
|
|
64295
64545
|
const write = async (file, content) => {
|
|
64296
64546
|
if (!file) return;
|
|
@@ -64299,8 +64549,8 @@ async function syncStaticAdSpecs(canvas, log) {
|
|
|
64299
64549
|
await writeFile2(file, content, "utf8");
|
|
64300
64550
|
changed++;
|
|
64301
64551
|
};
|
|
64302
|
-
await write(platePath, renderPlateSpec(blueprint));
|
|
64303
|
-
await write(composePath, renderComposeSpec(blueprint, brand));
|
|
64552
|
+
await write(platePath, renderPlateSpec(blueprint, { logo }));
|
|
64553
|
+
await write(composePath, renderComposeSpec(blueprint, brand, { logo, formats, textOnly }));
|
|
64304
64554
|
if (changed === 0) return "up_to_date";
|
|
64305
64555
|
log("[copy] prompt.plate.json and prompt.compose.json were re-derived from prompt.json");
|
|
64306
64556
|
return "regenerated";
|
|
@@ -65075,12 +65325,12 @@ function spineClockFor(blueprint) {
|
|
|
65075
65325
|
spineStarts.push(acc);
|
|
65076
65326
|
acc += d;
|
|
65077
65327
|
}
|
|
65078
|
-
const totalS =
|
|
65079
|
-
const startOf = (scene) =>
|
|
65328
|
+
const totalS = round32(acc);
|
|
65329
|
+
const startOf = (scene) => round32(spineStarts[scene] ?? totalS);
|
|
65080
65330
|
const map = (scene, absS) => {
|
|
65081
65331
|
const i = Math.min(Math.max(scene, 0), durs.length - 1);
|
|
65082
65332
|
const delta = Math.min(Math.max(absS - refStarts[i], 0), durs[i]);
|
|
65083
|
-
return
|
|
65333
|
+
return round32(spineStarts[i] + delta);
|
|
65084
65334
|
};
|
|
65085
65335
|
const mapAbs = (absS) => {
|
|
65086
65336
|
let i = 0;
|
|
@@ -65089,7 +65339,7 @@ function spineClockFor(blueprint) {
|
|
|
65089
65339
|
};
|
|
65090
65340
|
return { startOf, map, mapAbs, totalS };
|
|
65091
65341
|
}
|
|
65092
|
-
function
|
|
65342
|
+
function round32(n) {
|
|
65093
65343
|
return Math.round(n * 1e3) / 1e3;
|
|
65094
65344
|
}
|
|
65095
65345
|
function canvasDims(ar) {
|
|
@@ -65796,9 +66046,9 @@ function ingestFrameRef(url, edge, ctx, nodes) {
|
|
|
65796
66046
|
const cached = ctx.ingestCache?.get(url);
|
|
65797
66047
|
if (cached) return cached;
|
|
65798
66048
|
const tag = ctx.tag ?? "";
|
|
65799
|
-
const
|
|
65800
|
-
nodes.push({ id:
|
|
65801
|
-
const ref = `$ref:${
|
|
66049
|
+
const refId2 = `s${ctx.sceneIndex}${tag}_${edge}_ref`;
|
|
66050
|
+
nodes.push({ id: refId2, type: "ingest", params: { source: "url", url, expect: "image" } });
|
|
66051
|
+
const ref = `$ref:${refId2}.asset`;
|
|
65802
66052
|
ctx.ingestCache?.set(url, ref);
|
|
65803
66053
|
return ref;
|
|
65804
66054
|
}
|
|
@@ -66414,12 +66664,12 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, nodes, clips) {
|
|
|
66414
66664
|
function emitScreenScene(i, scene, lengths, out, surfaceIngests, nodes, clips) {
|
|
66415
66665
|
const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
|
|
66416
66666
|
const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
|
|
66417
|
-
let
|
|
66418
|
-
if (!
|
|
66667
|
+
let refId2 = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
|
|
66668
|
+
if (!refId2) {
|
|
66419
66669
|
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
66420
|
-
|
|
66670
|
+
refId2 = `s${i}_screen_ref`;
|
|
66421
66671
|
nodes.push({
|
|
66422
|
-
id:
|
|
66672
|
+
id: refId2,
|
|
66423
66673
|
type: "ingest",
|
|
66424
66674
|
params: {
|
|
66425
66675
|
source: "path",
|
|
@@ -66427,9 +66677,9 @@ function emitScreenScene(i, scene, lengths, out, surfaceIngests, nodes, clips) {
|
|
|
66427
66677
|
expect: "image"
|
|
66428
66678
|
}
|
|
66429
66679
|
});
|
|
66430
|
-
if (surfaceId) surfaceIngests.set(surfaceId,
|
|
66680
|
+
if (surfaceId) surfaceIngests.set(surfaceId, refId2);
|
|
66431
66681
|
}
|
|
66432
|
-
clips.push({ ref: `$ref:${
|
|
66682
|
+
clips.push({ ref: `$ref:${refId2}.asset`, scene_s: lengths.dur, out, still: { fit: "pad" } });
|
|
66433
66683
|
}
|
|
66434
66684
|
function sceneIsAllGraphic(scene) {
|
|
66435
66685
|
const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
|
|
@@ -66466,12 +66716,12 @@ function fullFrameGraphicScenes(blueprint) {
|
|
|
66466
66716
|
function emitGraphicScene(i, scene, lengths, out, surfaceIngests, nodes, clips) {
|
|
66467
66717
|
const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
|
|
66468
66718
|
const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
|
|
66469
|
-
let
|
|
66470
|
-
if (!
|
|
66719
|
+
let refId2 = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
|
|
66720
|
+
if (!refId2) {
|
|
66471
66721
|
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the designed panel").slice(0, 120));
|
|
66472
|
-
|
|
66722
|
+
refId2 = `s${i}_graphic_ref`;
|
|
66473
66723
|
nodes.push({
|
|
66474
|
-
id:
|
|
66724
|
+
id: refId2,
|
|
66475
66725
|
type: "ingest",
|
|
66476
66726
|
params: {
|
|
66477
66727
|
source: "path",
|
|
@@ -66479,9 +66729,9 @@ function emitGraphicScene(i, scene, lengths, out, surfaceIngests, nodes, clips)
|
|
|
66479
66729
|
expect: "image"
|
|
66480
66730
|
}
|
|
66481
66731
|
});
|
|
66482
|
-
if (surfaceId) surfaceIngests.set(surfaceId,
|
|
66732
|
+
if (surfaceId) surfaceIngests.set(surfaceId, refId2);
|
|
66483
66733
|
}
|
|
66484
|
-
clips.push({ ref: `$ref:${
|
|
66734
|
+
clips.push({ ref: `$ref:${refId2}.asset`, scene_s: lengths.dur, out, still: { fit: "pad" } });
|
|
66485
66735
|
}
|
|
66486
66736
|
var BRAND_CARD_RE = /\b(?:solid|plain|flat|brand|logo|wordmark|end[- ]?card|cta card|title card|colou?r background|background colou?r)\b/i;
|
|
66487
66737
|
function sceneIsBrandCard(scene, present2, isCta) {
|
|
@@ -69624,23 +69874,36 @@ function staticVerdict(findings) {
|
|
|
69624
69874
|
return findings.length > 0 ? "ready_with_notes" : "ready";
|
|
69625
69875
|
}
|
|
69626
69876
|
function buildStaticReviewPrompt(opts) {
|
|
69627
|
-
const copy = opts.copy.length > 0 ? opts.copy.map((c) => `- "${c}"`).join("\n") : "(no copy was specified)";
|
|
69628
69877
|
const brand = opts.brand ? ` for ${opts.brand}` : "";
|
|
69878
|
+
const order = opts.formats.join(", ");
|
|
69879
|
+
const shown = opts.thumbnails ? `These are the finished versions of ONE static advertisement${brand}, in this order: ${order}. After them come the same versions again, in the same order, shrunk to phone-feed thumbnail size (about 240px wide).` : `These are the finished versions of ONE static advertisement${brand}, in this order: ${order}.`;
|
|
69880
|
+
const text2 = opts.copy.length > 0 ? [
|
|
69881
|
+
"Read every word in every full-size image. The words must be exactly these strings, each shown once, spelled exactly:",
|
|
69882
|
+
opts.copy.map((c) => `- "${c}"`).join("\n"),
|
|
69883
|
+
"1. TEXT: a listed string misspelled, duplicated, garbled or absent, or extra words that are not in the list (a brand name inside a drawn logo is fine)."
|
|
69884
|
+
] : [
|
|
69885
|
+
"This ad has no words on purpose: the picture is the ad. Do not report missing copy, a missing headline or a missing call to action.",
|
|
69886
|
+
"1. TEXT: any letters or words that appear anyway (invented or garbled text is blocking)."
|
|
69887
|
+
];
|
|
69888
|
+
const logo = opts.logo ? "2. LOGO: the brand logo distorted, cut off, or drawn more than once." : "2. LOGO: this ad carries no logo on purpose (the platform shows the brand's name and picture); never report a missing logo. A made-up logo or wordmark in the picture is blocking.";
|
|
69889
|
+
const mechanisms = opts.mechanisms && opts.mechanisms.length > 0 ? [`8. CONCEPT: the ad no longer carries these devices it is built on (warning): ${opts.mechanisms.join("; ")}.`] : [];
|
|
69629
69890
|
return [
|
|
69630
|
-
|
|
69631
|
-
|
|
69632
|
-
copy,
|
|
69891
|
+
shown,
|
|
69892
|
+
...text2,
|
|
69633
69893
|
"",
|
|
69634
|
-
"Report, per
|
|
69635
|
-
|
|
69636
|
-
"
|
|
69637
|
-
"3. LAYOUT: text or logo clipped by the frame edge, overlapping each other, or unreadable against the picture.",
|
|
69894
|
+
"Report, per format, only what is actually wrong:",
|
|
69895
|
+
logo,
|
|
69896
|
+
"3. LAYOUT: anything the ad shows clipped by the frame edge, overlapping, or unreadable against what is behind it.",
|
|
69638
69897
|
...opts.drawnInSafeArea ? [] : [
|
|
69639
|
-
"4. STORIES: on a 9:16 image,
|
|
69898
|
+
"4. STORIES: on a 9:16 image, text, a logo or a button that exists clearly inside the top 14% or the bottom 20% of the frame (measure it; an element that starts below the 14% line is fine)."
|
|
69640
69899
|
],
|
|
69641
69900
|
"5. PICTURE: AI artifacts on people, animals or products (hands, faces, mouths, warped objects), visible seams or smears.",
|
|
69642
|
-
"
|
|
69643
|
-
"
|
|
69901
|
+
"6. ONE IDEA: two or more elements compete equally for the eye, so no single promise leads (warning).",
|
|
69902
|
+
opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the dominant message (the lead line, or the hero when there are no words) cannot be read or understood in under two seconds. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line would be unreadable on a phone feed. That is blocking.",
|
|
69903
|
+
"BRAND: with the logo covered, nothing (colours, type, subject, format) says whose ad this is (warning).",
|
|
69904
|
+
...mechanisms,
|
|
69905
|
+
"Blocking = it cannot ship as is (wrong or garbled words, a broken logo, clipped content, a malformed face or product, unreadable at thumbnail size). Warning = it can ship but should improve.",
|
|
69906
|
+
"Never report an element as missing because ads usually have one: judge only what this concept holds. An empty list is the right answer for a clean ad. Keep each item under twenty words.",
|
|
69644
69907
|
'Reply with only this JSON: {"findings":[{"severity":"blocking|warning","format":"4:5","what":"..."}]}'
|
|
69645
69908
|
].join("\n");
|
|
69646
69909
|
}
|
|
@@ -69650,33 +69913,42 @@ async function bytesOf(source) {
|
|
|
69650
69913
|
if (!res.ok) throw new Error(`fetch ${res.status}`);
|
|
69651
69914
|
return Buffer.from(await res.arrayBuffer());
|
|
69652
69915
|
}
|
|
69653
|
-
|
|
69654
|
-
|
|
69916
|
+
var THUMBNAIL_WIDTH = 240;
|
|
69917
|
+
async function jpegFor(bytes, width) {
|
|
69918
|
+
const resized = width ? sharp2(bytes).resize({ width, withoutEnlargement: true }) : sharp2(bytes).resize(1024, 1024, { fit: "inside", withoutEnlargement: true });
|
|
69919
|
+
const jpeg = await resized.jpeg({ quality: 85 }).toBuffer();
|
|
69655
69920
|
return jpeg.toString("base64");
|
|
69656
69921
|
}
|
|
69657
69922
|
async function reviewRenderedStatic(opts) {
|
|
69658
69923
|
const measured = [];
|
|
69659
69924
|
const stills = [];
|
|
69925
|
+
const thumbs = [];
|
|
69660
69926
|
for (const image of opts.images) {
|
|
69661
69927
|
try {
|
|
69662
69928
|
const bytes = await bytesOf(image.path);
|
|
69663
69929
|
const meta = await sharp2(bytes).metadata();
|
|
69930
|
+
const still = await jpegFor(bytes);
|
|
69931
|
+
const thumb = await jpegFor(bytes, THUMBNAIL_WIDTH);
|
|
69664
69932
|
measured.push({ format: image.format, width: meta.width ?? 0, height: meta.height ?? 0 });
|
|
69665
|
-
stills.push(
|
|
69933
|
+
stills.push(still);
|
|
69934
|
+
thumbs.push(thumb);
|
|
69666
69935
|
} catch {
|
|
69667
69936
|
}
|
|
69668
69937
|
}
|
|
69669
69938
|
if (measured.length === 0) return null;
|
|
69670
69939
|
const shape = ratioFindings(measured);
|
|
69671
69940
|
const answer = await askAboutStills(
|
|
69672
|
-
stills,
|
|
69941
|
+
[...stills, ...thumbs],
|
|
69673
69942
|
buildStaticReviewPrompt({
|
|
69674
69943
|
copy: opts.copy,
|
|
69675
69944
|
// `measured` grows in lockstep with `stills`, so an unreadable image drops out of
|
|
69676
69945
|
// both and the model is never told a format it was not shown.
|
|
69677
69946
|
formats: measured.map((m) => m.format),
|
|
69678
69947
|
brand: opts.brand,
|
|
69679
|
-
drawnInSafeArea: opts.drawnInSafeArea
|
|
69948
|
+
drawnInSafeArea: opts.drawnInSafeArea,
|
|
69949
|
+
logo: opts.logo,
|
|
69950
|
+
thumbnails: true,
|
|
69951
|
+
mechanisms: opts.mechanisms
|
|
69680
69952
|
}),
|
|
69681
69953
|
opts.onSpend
|
|
69682
69954
|
);
|
|
@@ -69711,13 +69983,42 @@ function staticReviewTargets(parsed, output) {
|
|
|
69711
69983
|
})
|
|
69712
69984
|
};
|
|
69713
69985
|
}
|
|
69714
|
-
function
|
|
69986
|
+
function nodesOf2(parsed) {
|
|
69715
69987
|
const nodes = parsed?.nodes;
|
|
69716
|
-
|
|
69717
|
-
|
|
69718
|
-
|
|
69719
|
-
|
|
69720
|
-
|
|
69988
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
69989
|
+
}
|
|
69990
|
+
function staticRunShape(parsed) {
|
|
69991
|
+
const nodes = nodesOf2(parsed);
|
|
69992
|
+
const snapshots = nodes.filter((n) => n.type === "hyperframe_snapshot");
|
|
69993
|
+
const pictures = nodes.filter(
|
|
69994
|
+
(n) => n.type === "image_generate" && typeof n.id === "string" && /^plate(_[a-z])?$/.test(n.id)
|
|
69995
|
+
).length;
|
|
69996
|
+
return {
|
|
69997
|
+
pictures,
|
|
69998
|
+
judged: nodes.some((n) => n.type === "image_select" && n.id === "pick"),
|
|
69999
|
+
text: snapshots.some((n) => n.params?.frame?.draw_copy === true) ? "drawn" : "baked",
|
|
70000
|
+
logo: snapshots.some((n) => typeof n.inputs?.logo === "string")
|
|
70001
|
+
};
|
|
70002
|
+
}
|
|
70003
|
+
function drawsCopy(parsed) {
|
|
70004
|
+
return nodesOf2(parsed).some((n) => n.type === "hyperframe_snapshot" && n.params?.frame?.draw_copy === true);
|
|
70005
|
+
}
|
|
70006
|
+
function shapeHint(shape, copy) {
|
|
70007
|
+
const pictures = shape.pictures === 0 ? "no picture (a text-only ad)" : `${shape.pictures} picture${shape.pictures === 1 ? "" : "s"}${shape.judged ? ", judged" : ", no judge"}`;
|
|
70008
|
+
const text2 = copy === 0 ? "no copy (the picture is the ad)" : `copy ${shape.text === "drawn" ? "drawn in real fonts" : "baked into the picture"}`;
|
|
70009
|
+
return `This run: ${pictures}; ${text2}; ${shape.logo ? "the brand logo drawn" : "no logo"}.`;
|
|
70010
|
+
}
|
|
70011
|
+
async function drawsLogo(parsed) {
|
|
70012
|
+
const compose = nodesOf2(parsed).find((n) => n.id === "compose_spec" && n.type === "ingest");
|
|
70013
|
+
const file = compose?.params?.path;
|
|
70014
|
+
if (typeof file === "string") {
|
|
70015
|
+
try {
|
|
70016
|
+
const spec = JSON.parse(await readFile17(file, "utf8"));
|
|
70017
|
+
return spec.logo !== null && spec.logo !== void 0;
|
|
70018
|
+
} catch {
|
|
70019
|
+
}
|
|
70020
|
+
}
|
|
70021
|
+
return staticRunShape(parsed).logo;
|
|
69721
70022
|
}
|
|
69722
70023
|
async function textOf(ref) {
|
|
69723
70024
|
const r = ref;
|
|
@@ -69738,15 +70039,21 @@ async function reviewStaticRun(opts) {
|
|
|
69738
70039
|
const reasoning = await textOf(opts.outputsByNode.pick?.reasoning);
|
|
69739
70040
|
const judge = reasoning ? parseJudgeScores(reasoning) : null;
|
|
69740
70041
|
let copy = [];
|
|
70042
|
+
let mechanisms = [];
|
|
69741
70043
|
try {
|
|
69742
|
-
|
|
70044
|
+
const blueprint = JSON.parse(await readFile17(targets.promptPath, "utf8"));
|
|
70045
|
+
copy = copyStrings(blueprint);
|
|
70046
|
+
mechanisms = winningMechanisms(blueprint).map((m) => m.device);
|
|
69743
70047
|
} catch {
|
|
69744
70048
|
copy = [];
|
|
69745
70049
|
}
|
|
70050
|
+
const shape = staticRunShape(opts.parsed);
|
|
69746
70051
|
const review = await reviewRenderedStatic({
|
|
69747
70052
|
images: targets.images,
|
|
69748
70053
|
copy,
|
|
69749
70054
|
drawnInSafeArea: drawsCopy(opts.parsed),
|
|
70055
|
+
logo: await drawsLogo(opts.parsed),
|
|
70056
|
+
mechanisms,
|
|
69750
70057
|
onSpend: opts.onSpend
|
|
69751
70058
|
});
|
|
69752
70059
|
const verdict = staticVerdict(review);
|
|
@@ -69761,9 +70068,15 @@ async function reviewStaticRun(opts) {
|
|
|
69761
70068
|
hints2.push("Reviewed every format of the finished ad: nothing to fix.");
|
|
69762
70069
|
}
|
|
69763
70070
|
for (const f of review ?? []) hints2.push(`${f.severity === "blocking" ? "Must fix" : "Should fix"}: ${f.what}`);
|
|
70071
|
+
hints2.push(shapeHint(shape, copy.length));
|
|
69764
70072
|
if (judge && judge.length > 0) {
|
|
69765
70073
|
const best = [...judge].sort((a, b) => b.total - a.total)[0];
|
|
69766
|
-
if (best)
|
|
70074
|
+
if (best) {
|
|
70075
|
+
const max = Object.keys(best.scores).length * 5;
|
|
70076
|
+
hints2.push(
|
|
70077
|
+
`The judge picked picture ${best.image} of ${judge.length} (${best.total}/${max}); the others stay in this run as alternates.`
|
|
70078
|
+
);
|
|
70079
|
+
}
|
|
69767
70080
|
}
|
|
69768
70081
|
return { review, verdict, judge, hints: hints2 };
|
|
69769
70082
|
}
|
|
@@ -70366,38 +70679,126 @@ import { access, mkdir as mkdir6, readFile as readFile20, writeFile as writeFile
|
|
|
70366
70679
|
import path26 from "path";
|
|
70367
70680
|
import { defineCommand as defineCommand134 } from "citty";
|
|
70368
70681
|
|
|
70369
|
-
// src/engine/scaffold/
|
|
70370
|
-
import {
|
|
70371
|
-
|
|
70372
|
-
|
|
70373
|
-
var
|
|
70374
|
-
|
|
70375
|
-
|
|
70376
|
-
|
|
70377
|
-
|
|
70378
|
-
|
|
70379
|
-
|
|
70380
|
-
|
|
70682
|
+
// src/engine/scaffold/static-fonts.ts
|
|
70683
|
+
import { marked } from "marked";
|
|
70684
|
+
var DEFAULT_FONTS = { headline: { family: "Inter", weight: 800 }, body: { family: "Inter", weight: 400 } };
|
|
70685
|
+
var HEADLINE_WORDS = /\b(headings?|headlines?|display|titles?|h[1-3])\b/i;
|
|
70686
|
+
var BODY_WORDS = /\b(body|paragraphs?|text|copy)\b/i;
|
|
70687
|
+
var PROHIBITION = /\b(never|don'?t|do not|avoid|instead of|ban(?:ned)?|no)\b/i;
|
|
70688
|
+
var NOT_FAMILIES = /* @__PURE__ */ new Set([
|
|
70689
|
+
"system",
|
|
70690
|
+
"system stack",
|
|
70691
|
+
"sans",
|
|
70692
|
+
"font",
|
|
70693
|
+
"fonts",
|
|
70694
|
+
"typography",
|
|
70695
|
+
"typeface",
|
|
70696
|
+
"weight",
|
|
70697
|
+
"fallback"
|
|
70698
|
+
]);
|
|
70699
|
+
function isFamily(candidate) {
|
|
70700
|
+
const name = candidate.trim();
|
|
70701
|
+
const lower = name.toLowerCase();
|
|
70702
|
+
if (name.length < 2 || !/^[A-Z0-9]/.test(name)) return false;
|
|
70703
|
+
if (GENERIC_FAMILIES.has(lower) || NOT_FAMILIES.has(lower) || isNotAFamily(lower)) return false;
|
|
70704
|
+
if (HEADLINE_WORDS.test(name) || BODY_WORDS.test(name)) return false;
|
|
70705
|
+
return !/^#?[0-9a-f]{6}$/i.test(name) && !name.includes(":");
|
|
70706
|
+
}
|
|
70707
|
+
function familiesOn(line) {
|
|
70708
|
+
const found = [];
|
|
70709
|
+
for (const m of line.matchAll(/(\*\*|__|`|"|“|')([^*_`"”']{2,40}?)\1|“([^”]{2,40})”/g)) {
|
|
70710
|
+
found.push((m[2] ?? m[3] ?? "").trim());
|
|
70711
|
+
}
|
|
70712
|
+
const labelled = line.match(
|
|
70713
|
+
/\b(?:headings?|headlines?|display|titles?|body|text|font|typeface|family)\s*(?:font|face|typeface)?\s*[:=–-]\s*\**`?([A-Z][A-Za-z0-9]*(?: [A-Z][A-Za-z0-9]*){0,2})/
|
|
70714
|
+
);
|
|
70715
|
+
if (labelled?.[1]) found.push(labelled[1]);
|
|
70716
|
+
return found.map((f) => f.replace(/\s+\d{3}$/, "").trim()).filter(isFamily);
|
|
70381
70717
|
}
|
|
70382
|
-
function
|
|
70383
|
-
|
|
70718
|
+
function typographySection(brandMd) {
|
|
70719
|
+
const tokens = marked.lexer(brandMd);
|
|
70720
|
+
const out = [];
|
|
70721
|
+
let depth = null;
|
|
70722
|
+
for (const token of tokens) {
|
|
70723
|
+
if (token.type === "heading") {
|
|
70724
|
+
if (depth !== null && token.depth <= depth) break;
|
|
70725
|
+
if (depth === null && /typograph|fonts?\b|typefaces?/i.test(token.text)) {
|
|
70726
|
+
depth = token.depth;
|
|
70727
|
+
continue;
|
|
70728
|
+
}
|
|
70729
|
+
}
|
|
70730
|
+
if (depth !== null) out.push(token.raw);
|
|
70731
|
+
}
|
|
70732
|
+
return out.join("\n");
|
|
70384
70733
|
}
|
|
70385
|
-
function
|
|
70386
|
-
|
|
70387
|
-
|
|
70388
|
-
|
|
70734
|
+
function claimsIn(brandMd) {
|
|
70735
|
+
return typographySection(brandMd).split("\n").filter((line) => !PROHIBITION.test(line)).map((line) => ({ families: familiesOn(line), headline: HEADLINE_WORDS.test(line), body: BODY_WORDS.test(line) })).filter((claim) => claim.families.length > 0);
|
|
70736
|
+
}
|
|
70737
|
+
function fromBrandMd(brandMd) {
|
|
70738
|
+
const claims = claimsIn(brandMd);
|
|
70739
|
+
const named = (family) => family ? { family, origin: "brand_md" } : void 0;
|
|
70740
|
+
const loose = claims.filter((c) => !c.headline && !c.body).map((c) => c.families[0]);
|
|
70741
|
+
const headlineClaim = claims.find((c) => c.headline);
|
|
70742
|
+
const bodyClaim = claims.find((c) => c.body);
|
|
70743
|
+
const bodyFamily = bodyClaim?.headline ? bodyClaim.families[1] ?? bodyClaim.families[0] : bodyClaim?.families[0];
|
|
70744
|
+
const headline = named(headlineClaim?.families[0] ?? loose[0]);
|
|
70745
|
+
const body = named(bodyFamily ?? loose[1] ?? loose[0]);
|
|
70746
|
+
return { ...headline ? { headline } : {}, ...body ? { body } : {} };
|
|
70747
|
+
}
|
|
70748
|
+
function fromTokens2(tokensJson) {
|
|
70749
|
+
let json;
|
|
70750
|
+
try {
|
|
70751
|
+
json = JSON.parse(tokensJson);
|
|
70752
|
+
} catch {
|
|
70753
|
+
return {};
|
|
70754
|
+
}
|
|
70755
|
+
const parsed = brandTokensSchema.safeParse(json);
|
|
70756
|
+
if (!parsed.success) return {};
|
|
70757
|
+
const heading = parsed.data.fonts.find((f) => f.role === "heading")?.family;
|
|
70758
|
+
const body = parsed.data.fonts.find((f) => f.role === "body")?.family;
|
|
70389
70759
|
return {
|
|
70390
|
-
|
|
70391
|
-
|
|
70392
|
-
const delta = ratioDelta(format, plateRatio);
|
|
70393
|
-
if (delta <= CROP_TOLERANCE) return { format, source: "crop", delta: round32(delta) };
|
|
70394
|
-
const extendRatio = nearestSupportedAspectRatio("image_generate", genModel, format);
|
|
70395
|
-
return { format, source: "extend", delta: round32(ratioDelta(format, extendRatio)), extendRatio };
|
|
70396
|
-
})
|
|
70760
|
+
...heading ? { headline: { family: heading, origin: "tokens" } } : {},
|
|
70761
|
+
...body ? { body: { family: body, origin: "tokens" } } : {}
|
|
70397
70762
|
};
|
|
70398
70763
|
}
|
|
70764
|
+
function namedTypefaces(sources) {
|
|
70765
|
+
const tokens = sources.tokensJson ? fromTokens2(sources.tokensJson) : {};
|
|
70766
|
+
const doc = sources.brandMd ? fromBrandMd(sources.brandMd) : {};
|
|
70767
|
+
const headline = tokens.headline ?? doc.headline;
|
|
70768
|
+
const body = tokens.body ?? doc.body;
|
|
70769
|
+
return { ...headline ? { headline } : {}, ...body ? { body } : {} };
|
|
70770
|
+
}
|
|
70771
|
+
function nearest(faces, weight) {
|
|
70772
|
+
return [...faces].filter((f) => f.style === "normal").sort((a, b) => Math.abs(a.weight - weight) - Math.abs(b.weight - weight))[0];
|
|
70773
|
+
}
|
|
70774
|
+
async function webFontUrl(family, weight, deps) {
|
|
70775
|
+
const first = await resolveBrandFaces(family, [weight], ["latin"], deps);
|
|
70776
|
+
if (first.ok) return nearest(first.faces, weight)?.url ?? null;
|
|
70777
|
+
if (first.reason !== "weight" || first.available.length === 0) return null;
|
|
70778
|
+
const closest = [...first.available].sort((a, b) => Math.abs(a - weight) - Math.abs(b - weight))[0];
|
|
70779
|
+
if (closest === void 0) return null;
|
|
70780
|
+
const retry = await resolveBrandFaces(family, [closest], ["latin"], deps);
|
|
70781
|
+
return retry.ok ? nearest(retry.faces, closest)?.url ?? null : null;
|
|
70782
|
+
}
|
|
70783
|
+
async function webFace(named, fallback, weight, resolveUrl) {
|
|
70784
|
+
if (named) {
|
|
70785
|
+
const url2 = await resolveUrl(named.family, weight);
|
|
70786
|
+
if (url2) return { slot: { url: url2 }, family: named.family, origin: named.origin };
|
|
70787
|
+
}
|
|
70788
|
+
const url = await resolveUrl(fallback.family, fallback.weight);
|
|
70789
|
+
return url ? { slot: { url }, family: fallback.family, origin: "default" } : null;
|
|
70790
|
+
}
|
|
70791
|
+
async function resolveStaticFonts(args8) {
|
|
70792
|
+
const { files, named, resolveUrl } = args8;
|
|
70793
|
+
const file = (p) => ({ slot: { path: p }, family: null, origin: "brand_file" });
|
|
70794
|
+
const headline = files.headline ? file(files.headline) : await webFace(named.headline ?? named.body, DEFAULT_FONTS.headline, DEFAULT_FONTS.headline.weight, resolveUrl);
|
|
70795
|
+
if (!headline) return null;
|
|
70796
|
+
const body = files.body ? file(files.body) : files.headline ? headline : await webFace(named.body ?? named.headline, DEFAULT_FONTS.body, DEFAULT_FONTS.body.weight, resolveUrl) ?? headline;
|
|
70797
|
+
return { headline, body };
|
|
70798
|
+
}
|
|
70399
70799
|
|
|
70400
70800
|
// src/engine/scaffold/staticAd.ts
|
|
70801
|
+
import { z as z52 } from "zod";
|
|
70401
70802
|
var GEN_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "4:5", "9:16", "16:9", "4:3", "3:4", "2:3", "3:2", "21:9"]);
|
|
70402
70803
|
var DEFAULT_ASPECT_RATIO = "9:16";
|
|
70403
70804
|
var SHEET_SUBJECT_TYPE2 = {
|
|
@@ -70407,7 +70808,7 @@ var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image";
|
|
|
70407
70808
|
var ACTOR_SHEET_IMAGE_SIZE = "4K";
|
|
70408
70809
|
var ADAPT_MODEL = "google/gemini-3-pro-image";
|
|
70409
70810
|
var ADAPT_IMAGE_SIZE = "2K";
|
|
70410
|
-
var ADAPT_GUIDANCE = "Keep
|
|
70811
|
+
var ADAPT_GUIDANCE = "Keep every line of text and the hero subject fully visible. Reproduce every text string verbatim, with no dropped, added, or altered characters, and preserve the exact colour treatment of each line. Draw no logo or wordmark.";
|
|
70411
70812
|
var Blueprint = z52.object({
|
|
70412
70813
|
meta: z52.object({ estimated_aspect_ratio: z52.string().optional() }).loose().optional(),
|
|
70413
70814
|
text_content: z52.array(z52.object({ text: z52.string().optional() }).loose()).optional(),
|
|
@@ -70453,8 +70854,22 @@ function suppliedImage(el) {
|
|
|
70453
70854
|
const url = typeof el.source_image === "string" ? el.source_image.trim() : "";
|
|
70454
70855
|
return /^https?:\/\//i.test(url) ? url : void 0;
|
|
70455
70856
|
}
|
|
70456
|
-
function
|
|
70457
|
-
return
|
|
70857
|
+
function overlayCopy(blueprint) {
|
|
70858
|
+
return composeSpec(blueprint, { palette: [] }).copy.map((b) => b.text);
|
|
70859
|
+
}
|
|
70860
|
+
function staticMode(blueprint, opts) {
|
|
70861
|
+
if (opts.textOnly) return "text_only";
|
|
70862
|
+
if (overlayCopy(blueprint).length === 0) return "picture_only";
|
|
70863
|
+
return opts.brand?.fonts && !opts.bakeText ? "composed" : "baked";
|
|
70864
|
+
}
|
|
70865
|
+
var plateTextOf = (mode) => mode === "baked" ? "baked" : "clean";
|
|
70866
|
+
function isTextForward(input) {
|
|
70867
|
+
const blueprint = input;
|
|
70868
|
+
const none = (v) => !Array.isArray(v) || v.length === 0;
|
|
70869
|
+
return none(blueprint?.subjects) && none(blueprint?.people) && overlayCopy(input).length > 0;
|
|
70870
|
+
}
|
|
70871
|
+
function logoDrawn(blueprint, opts) {
|
|
70872
|
+
return Boolean(opts.brand?.logoPath) && conceptHasLogo(blueprint);
|
|
70458
70873
|
}
|
|
70459
70874
|
function layoutDirFor(format) {
|
|
70460
70875
|
return `./layout-${format.replace(":", "x")}`;
|
|
@@ -70480,27 +70895,29 @@ var TAKE_DIRECTIONS = [
|
|
|
70480
70895
|
"Take C: a fresh camera angle and lighting that still fit the brief, same subject and palette."
|
|
70481
70896
|
];
|
|
70482
70897
|
var PLATE_RULES = [
|
|
70483
|
-
"This is a CLEAN PLATE for an advertisement: the picture only.
|
|
70484
|
-
"- Render NO text of any kind: no letters, words, numbers, captions, labels, signage, packaging copy you invent, watermarks, UI, buttons or logos.",
|
|
70898
|
+
"This is a CLEAN PLATE for an advertisement: the picture only. Whatever copy the ad carries is added afterwards by a designer, so:",
|
|
70899
|
+
"- Render NO text of any kind: no letters, words, numbers, captions, labels, signage, packaging copy you invent, watermarks, UI, buttons or logos. The one exception is `in_scene_text`: each of those strings is written ON the object its placement names (a phone screen, a note, a sign, packaging), exactly once, spelled exactly.",
|
|
70485
70900
|
"- Keep every area in `reserved_for_overlay` calm: an even, low-detail continuation of the background, with no subject, face, product or strong edge inside it.",
|
|
70486
70901
|
"- Keep the hero subject and product whole and inside the frame, clear of those areas.",
|
|
70487
70902
|
"- Rebuild each device in `mechanisms_to_keep`: it is why the reference ad works."
|
|
70488
70903
|
].join("\n");
|
|
70489
70904
|
var JUDGE_RUBRIC = [
|
|
70490
|
-
"You are judging candidate PLATES for a static ad: the picture before
|
|
70905
|
+
"You are judging candidate PLATES for a static ad: the picture before any copy is drawn on it. A winning static is ONE specific promise understood at a glance.",
|
|
70491
70906
|
"Score every image 0-5 on each criterion:",
|
|
70492
|
-
"-
|
|
70493
|
-
"-
|
|
70494
|
-
"-
|
|
70907
|
+
"- idea: ONE dominant element carries the idea; nothing competes with it for the eye.",
|
|
70908
|
+
"- thumbnail: shrunk to a phone feed thumbnail (about 240px wide), the dominant element still reads in under two seconds.",
|
|
70909
|
+
"- mechanism: rebuilds the devices in `mechanisms_to_keep` below (the reason the reference ad works); 5 when the list is empty and the picture is on brief.",
|
|
70910
|
+
"- offer_space: the areas in `reserved_for_overlay` are calm enough for what is drawn there to read in one second; 5 when the list is empty.",
|
|
70911
|
+
"- brand: with any logo covered, the palette, the subject and the treatment still read as the brand's.",
|
|
70495
70912
|
"- artifacts: no AI artifacts (hands, faces, mouths, warped products, seams, duplicated limbs); 5 means none.",
|
|
70496
70913
|
"- safe_zones: the hero stays out of the outer top 14% and bottom 20% of the frame (Stories and Reels chrome).",
|
|
70497
70914
|
"- text: {{text_rule}}",
|
|
70498
|
-
'Begin `reasoning` with one line per image, exactly: "Image N: mechanism=x offer_space=x brand=x artifacts=x safe_zones=x text=x total=x" (total out of
|
|
70915
|
+
'Begin `reasoning` with one line per image, exactly: "Image N: idea=x thumbnail=x mechanism=x offer_space=x brand=x artifacts=x safe_zones=x text=x total=x" (total out of 40), then one sentence on why the winner won. Pick the highest total; break a tie on artifacts.',
|
|
70499
70916
|
"",
|
|
70500
70917
|
"BRIEF:",
|
|
70501
70918
|
"{{plate_spec}}"
|
|
70502
70919
|
].join("\n");
|
|
70503
|
-
var TEXT_RULE_COMPOSED = "
|
|
70920
|
+
var TEXT_RULE_COMPOSED = "no letters, words, logos or UI anywhere, except the strings in `in_scene_text`, each once and spelled exactly on the object it belongs to; any other text is a 0.";
|
|
70504
70921
|
var TEXT_RULE_BAKED = "read every word in the image: the copy is exactly the strings the brief quotes, each once, spelled right; a missing, extra or misspelled word lowers the score.";
|
|
70505
70922
|
function referenceLegend(slots, originalAsReference) {
|
|
70506
70923
|
return [
|
|
@@ -70512,14 +70929,14 @@ function referenceLegend(slots, originalAsReference) {
|
|
|
70512
70929
|
}
|
|
70513
70930
|
function platePrompt(args8) {
|
|
70514
70931
|
const exclusions = "Exclusions: no invented badges, icons, or seals; render exactly the subjects the blueprint names and no others; no competitor branding; no watermark.";
|
|
70515
|
-
const head = args8.mode === "
|
|
70932
|
+
const head = args8.mode === "clean" ? [
|
|
70516
70933
|
"Render the picture for the advertisement described by the PLATE SPEC below. The `scene` is authoritative (subjects, expressions, palette, lighting, camera, style); follow its composition and leave the `reserved_for_overlay` areas free.",
|
|
70517
70934
|
"",
|
|
70518
70935
|
PLATE_RULES
|
|
70519
70936
|
] : [
|
|
70520
70937
|
"Render the advertisement described by the TARGET BLUEPRINT below faithfully. The blueprint is authoritative \u2014 render exactly what it says (palette, expressions, intent, layout). Follow the blueprint's `layout` object for the global composition: reproduce the column/row grid, each region's position and size from its x_pct/y_pct bounds, the panel splits, and every text block at the relative size, weight, case, and alignment it specifies. Do not add brand logic, do not recolor on your own, do not invent any logo, badge, face, or product.",
|
|
70521
70938
|
"",
|
|
70522
|
-
"Leave
|
|
70939
|
+
"Leave any logo OUT: draw no logo, wordmark or brand name anywhere. When the ad carries a logo, the brand's real file is placed afterwards."
|
|
70523
70940
|
];
|
|
70524
70941
|
const copyBlock = args8.mode === "baked" ? ["", COPY_DIRECTIVE_HEADER, textFidelityDirective(args8.copy, false), ""] : [""];
|
|
70525
70942
|
return [
|
|
@@ -70536,15 +70953,15 @@ function platePrompt(args8) {
|
|
|
70536
70953
|
"Match the palette hex precisely. Render each subject's expression and the ad's intent.",
|
|
70537
70954
|
...args8.exclusionsLast ? [] : ["", exclusions],
|
|
70538
70955
|
"",
|
|
70539
|
-
args8.mode === "
|
|
70540
|
-
args8.mode === "
|
|
70956
|
+
args8.mode === "clean" ? "PLATE SPEC:" : "TARGET BLUEPRINT:",
|
|
70957
|
+
args8.mode === "clean" ? "{{plate_spec}}" : "{{target_blueprint}}",
|
|
70541
70958
|
...args8.exclusionsLast ? ["", exclusions] : []
|
|
70542
70959
|
].join("\n");
|
|
70543
70960
|
}
|
|
70544
70961
|
function extensionPrompt(format, legend, hasElementRefs) {
|
|
70545
70962
|
return [
|
|
70546
70963
|
`Extend the picture in the FIRST reference image to a ${format} frame. It is the same photograph with more of the scene revealed: keep the subject, the product, their scale, the lighting, the colors and the camera exactly as they are, and continue the background naturally into the new area.`,
|
|
70547
|
-
"Keep the new area calm and even:
|
|
70964
|
+
"Keep the new area calm and even: any ad copy is placed there afterwards. Keep text already written on objects in the picture exactly as it is, and add NO other text, letters, logos, UI or watermarks anywhere.",
|
|
70548
70965
|
"Keep the hero whole and away from the outer top 14% and bottom 20% of the frame.",
|
|
70549
70966
|
...hasElementRefs ? ["", "The other references keep identities exact:", legend] : [],
|
|
70550
70967
|
"",
|
|
@@ -70632,19 +71049,25 @@ function outputFormats(blueprint, opts) {
|
|
|
70632
71049
|
function showsPerson(blueprint, elements) {
|
|
70633
71050
|
return (blueprint.people?.length ?? 0) > 0 || elements.some(isPerson);
|
|
70634
71051
|
}
|
|
70635
|
-
function elementNodes(elements, opts) {
|
|
71052
|
+
function elementNodes(elements, opts, drawLogo) {
|
|
70636
71053
|
const includeActorSheets = opts.includeActorSheets !== false;
|
|
70637
71054
|
const usedIds = /* @__PURE__ */ new Set(["prompt", "plate_spec", "compose_spec", "original", "pick", "final"]);
|
|
70638
71055
|
const nodes = [];
|
|
70639
71056
|
const slots = [];
|
|
71057
|
+
const logoPath = drawLogo ? opts.brand?.logoPath : void 0;
|
|
70640
71058
|
let logoRef;
|
|
70641
|
-
|
|
70642
|
-
|
|
70643
|
-
if (isLogo && logoRef) return;
|
|
71059
|
+
const wanted = (isLogo) => isLogo ? !logoRef && Boolean(logoPath) : !opts.textOnly;
|
|
71060
|
+
const freshId = (label, i) => {
|
|
70644
71061
|
let id = sanitizeId2(`el_${label}`, `el_${i}`);
|
|
70645
71062
|
while (usedIds.has(id)) id = `${id}_${i}`;
|
|
70646
71063
|
usedIds.add(id);
|
|
70647
|
-
|
|
71064
|
+
return id;
|
|
71065
|
+
};
|
|
71066
|
+
assignElementLabels2(elements).forEach(({ el, label }, i) => {
|
|
71067
|
+
const isLogo = el.type.toLowerCase() === "logo";
|
|
71068
|
+
if (!wanted(isLogo)) return;
|
|
71069
|
+
const id = freshId(label, i);
|
|
71070
|
+
nodes.push(elementIngest(id, el, label, isLogo ? logoPath : void 0));
|
|
70648
71071
|
if (isLogo) {
|
|
70649
71072
|
logoRef = `$ref:${id}.asset`;
|
|
70650
71073
|
return;
|
|
@@ -70653,8 +71076,8 @@ function elementNodes(elements, opts) {
|
|
|
70653
71076
|
if (sheet) nodes.push(sheet);
|
|
70654
71077
|
slots.push({ ref: sheet ? `$ref:${sheet.id}.sheet` : `$ref:${id}.asset`, label, type: el.type });
|
|
70655
71078
|
});
|
|
70656
|
-
if (!logoRef &&
|
|
70657
|
-
nodes.push(ingestNode("brand_logo",
|
|
71079
|
+
if (!logoRef && logoPath) {
|
|
71080
|
+
nodes.push(ingestNode("brand_logo", logoPath, "image"));
|
|
70658
71081
|
logoRef = "$ref:brand_logo.asset";
|
|
70659
71082
|
}
|
|
70660
71083
|
return { nodes, slots, logoRef };
|
|
@@ -70682,15 +71105,18 @@ function actorSheetNode(el, ingestId) {
|
|
|
70682
71105
|
}
|
|
70683
71106
|
};
|
|
70684
71107
|
}
|
|
71108
|
+
function fontIngest(id, font) {
|
|
71109
|
+
return "path" in font.slot ? ingestNode(id, font.slot.path, "font") : { id, type: "ingest", params: { source: "url", url: font.slot.url, expect: "font" } };
|
|
71110
|
+
}
|
|
71111
|
+
var slotKey = (font) => "path" in font.slot ? font.slot.path : font.slot.url;
|
|
70685
71112
|
function fontNodes(brand) {
|
|
70686
|
-
const
|
|
70687
|
-
if (!
|
|
70688
|
-
const nodes = [
|
|
70689
|
-
|
|
70690
|
-
if (!body || body === headline) {
|
|
71113
|
+
const fonts = brand?.fonts;
|
|
71114
|
+
if (!fonts) return { nodes: [], refs: {} };
|
|
71115
|
+
const nodes = [fontIngest("font_headline", fonts.headline)];
|
|
71116
|
+
if (slotKey(fonts.body) === slotKey(fonts.headline)) {
|
|
70691
71117
|
return { nodes, refs: { font_headline: "$ref:font_headline.asset", font_body: "$ref:font_headline.asset" } };
|
|
70692
71118
|
}
|
|
70693
|
-
nodes.push(
|
|
71119
|
+
nodes.push(fontIngest("font_body", fonts.body));
|
|
70694
71120
|
return { nodes, refs: { font_headline: "$ref:font_headline.asset", font_body: "$ref:font_body.asset" } };
|
|
70695
71121
|
}
|
|
70696
71122
|
function renderParamsFor(genModel) {
|
|
@@ -70702,12 +71128,12 @@ function renderParamsFor(genModel) {
|
|
|
70702
71128
|
};
|
|
70703
71129
|
}
|
|
70704
71130
|
function plateNodes(args8) {
|
|
70705
|
-
const { blueprint, slots, opts,
|
|
71131
|
+
const { blueprint, slots, opts, text: text2 } = args8;
|
|
70706
71132
|
const takes = takesFor(opts);
|
|
70707
71133
|
const originalAsReference = !showsPerson(blueprint, args8.elements);
|
|
70708
71134
|
const reference = [...slots.map((s) => s.ref), ...originalAsReference ? ["$ref:original.asset"] : []];
|
|
70709
71135
|
const legend = referenceLegend(slots, originalAsReference);
|
|
70710
|
-
const specInput =
|
|
71136
|
+
const specInput = text2 === "clean" ? { plate_spec: "$ref:plate_spec.asset" } : { target_blueprint: "$ref:prompt.asset" };
|
|
70711
71137
|
const nodes = [];
|
|
70712
71138
|
const ids = Array.from({ length: takes }, (_, t) => takes === 1 ? "plate" : `plate_${String.fromCharCode(97 + t)}`);
|
|
70713
71139
|
ids.forEach((id, t) => {
|
|
@@ -70720,7 +71146,7 @@ function plateNodes(args8) {
|
|
|
70720
71146
|
aspect_ratio: args8.plateRatio,
|
|
70721
71147
|
...renderParamsFor(opts.genModel),
|
|
70722
71148
|
prompt: platePrompt({
|
|
70723
|
-
mode,
|
|
71149
|
+
mode: text2,
|
|
70724
71150
|
copy: copyStrings(blueprint),
|
|
70725
71151
|
take: TAKE_DIRECTIONS[t] ?? "",
|
|
70726
71152
|
legend,
|
|
@@ -70737,18 +71163,19 @@ function plateNodes(args8) {
|
|
|
70737
71163
|
type: "image_select",
|
|
70738
71164
|
inputs: {
|
|
70739
71165
|
images: ids.map((id) => `$ref:${id}.images#0`),
|
|
70740
|
-
plate_spec:
|
|
71166
|
+
plate_spec: text2 === "clean" ? "$ref:plate_spec.asset" : "$ref:prompt.asset"
|
|
70741
71167
|
},
|
|
70742
71168
|
params: {
|
|
70743
71169
|
model: JUDGE_MODEL,
|
|
70744
71170
|
count: 1,
|
|
70745
71171
|
temperature: 0,
|
|
70746
|
-
prompt: JUDGE_RUBRIC.replace("{{text_rule}}",
|
|
71172
|
+
prompt: JUDGE_RUBRIC.replace("{{text_rule}}", text2 === "clean" ? TEXT_RULE_COMPOSED : TEXT_RULE_BAKED)
|
|
70747
71173
|
}
|
|
70748
71174
|
});
|
|
70749
71175
|
return { nodes, chosen: "$ref:pick.images#0" };
|
|
70750
71176
|
}
|
|
70751
71177
|
function takesFor(opts) {
|
|
71178
|
+
if (opts.textOnly) return 0;
|
|
70752
71179
|
return Math.max(1, Math.min(TAKE_DIRECTIONS.length, opts.takes ?? TAKE_DIRECTIONS.length));
|
|
70753
71180
|
}
|
|
70754
71181
|
function bakedAdaptNode(plan, chosen) {
|
|
@@ -70764,72 +71191,151 @@ function bakedAdaptNode(plan, chosen) {
|
|
|
70764
71191
|
}
|
|
70765
71192
|
};
|
|
70766
71193
|
}
|
|
71194
|
+
function snapshotNode(args8) {
|
|
71195
|
+
const slug = args8.format.replace(":", "x");
|
|
71196
|
+
return {
|
|
71197
|
+
id: `ad_${slug}`,
|
|
71198
|
+
type: "hyperframe_snapshot",
|
|
71199
|
+
inputs: {
|
|
71200
|
+
...args8.plateRef ? { plate: args8.plateRef } : {},
|
|
71201
|
+
compose: "$ref:compose_spec.asset",
|
|
71202
|
+
...args8.logoRef ? { logo: args8.logoRef } : {},
|
|
71203
|
+
...args8.fontRefs
|
|
71204
|
+
},
|
|
71205
|
+
params: {
|
|
71206
|
+
composition: layoutDirFor(args8.format),
|
|
71207
|
+
wait_for: { kind: "selector", value: "#root[data-ready='1']" },
|
|
71208
|
+
ad: "{{compose}}",
|
|
71209
|
+
// `plate` tells the composition (and the run-time layout pass) which picture this
|
|
71210
|
+
// format is cut from, so each line lands where the source layout put it; null means
|
|
71211
|
+
// there is no picture at all.
|
|
71212
|
+
// `fonts` says whether any face is staged: one declared but never staged is a 404.
|
|
71213
|
+
frame: {
|
|
71214
|
+
format: args8.format,
|
|
71215
|
+
safe: safeAreaFor(args8.format),
|
|
71216
|
+
draw_copy: args8.drawCopy,
|
|
71217
|
+
plate: args8.plate,
|
|
71218
|
+
fonts: Object.keys(args8.fontRefs).length > 0
|
|
71219
|
+
}
|
|
71220
|
+
}
|
|
71221
|
+
};
|
|
71222
|
+
}
|
|
71223
|
+
function extendNode(entry, chosen, slots, genModel) {
|
|
71224
|
+
if (entry.source !== "extend" || !entry.extendRatio) return null;
|
|
71225
|
+
return {
|
|
71226
|
+
id: `extend_${entry.format.replace(":", "x")}`,
|
|
71227
|
+
type: "image_generate",
|
|
71228
|
+
inputs: { reference: [chosen, ...slots.map((s) => s.ref)], plate_spec: "$ref:plate_spec.asset" },
|
|
71229
|
+
params: {
|
|
71230
|
+
model: genModel,
|
|
71231
|
+
aspect_ratio: entry.extendRatio,
|
|
71232
|
+
...renderParamsFor(genModel),
|
|
71233
|
+
prompt: extensionPrompt(entry.format, referenceLegend(slots, false), slots.length > 0)
|
|
71234
|
+
}
|
|
71235
|
+
};
|
|
71236
|
+
}
|
|
71237
|
+
function formatSource(args8) {
|
|
71238
|
+
const { entry, plan, adapt, chosen } = args8;
|
|
71239
|
+
if (!chosen) return { plateRef: null, plate: null, extend: null };
|
|
71240
|
+
const adapted = adapt ? adapt.formats.indexOf(entry.format) : -1;
|
|
71241
|
+
if (adapted >= 0) return { plateRef: `$ref:adapt.images#${adapted}`, plate: { ratio: entry.format }, extend: null };
|
|
71242
|
+
const extend = args8.mode !== "baked" ? extendNode(entry, chosen, args8.slots, args8.genModel) : null;
|
|
71243
|
+
if (!extend) return { plateRef: chosen, plate: { ratio: plan.plateRatio }, extend: null };
|
|
71244
|
+
return {
|
|
71245
|
+
plateRef: `$ref:${extend.id}.images#0`,
|
|
71246
|
+
plate: { ratio: plan.plateRatio, extend_ratio: String(extend.params?.aspect_ratio) },
|
|
71247
|
+
extend
|
|
71248
|
+
};
|
|
71249
|
+
}
|
|
70767
71250
|
function formatNodes(args8) {
|
|
70768
71251
|
const { plan, mode, chosen, slots } = args8;
|
|
70769
71252
|
const nodes = [];
|
|
70770
71253
|
const layoutIds = [];
|
|
70771
|
-
const adapt = mode === "baked" ? bakedAdaptNode(plan, chosen) : null;
|
|
71254
|
+
const adapt = mode === "baked" && chosen ? bakedAdaptNode(plan, chosen) : null;
|
|
70772
71255
|
if (adapt) nodes.push(adapt.node);
|
|
70773
71256
|
for (const entry of plan.formats) {
|
|
70774
71257
|
const slug = entry.format.replace(":", "x");
|
|
70775
|
-
const
|
|
70776
|
-
|
|
70777
|
-
|
|
70778
|
-
|
|
70779
|
-
|
|
70780
|
-
|
|
70781
|
-
|
|
70782
|
-
|
|
70783
|
-
model: args8.genModel,
|
|
70784
|
-
aspect_ratio: entry.extendRatio,
|
|
70785
|
-
...renderParamsFor(args8.genModel),
|
|
70786
|
-
prompt: extensionPrompt(entry.format, referenceLegend(slots, false), slots.length > 0)
|
|
70787
|
-
}
|
|
70788
|
-
});
|
|
70789
|
-
plateRef = `$ref:extend_${slug}.images#0`;
|
|
70790
|
-
}
|
|
70791
|
-
layoutIds.push(`ad_${slug}`);
|
|
70792
|
-
nodes.push({
|
|
70793
|
-
id: `ad_${slug}`,
|
|
70794
|
-
type: "hyperframe_snapshot",
|
|
70795
|
-
inputs: {
|
|
70796
|
-
plate: plateRef,
|
|
70797
|
-
compose: "$ref:compose_spec.asset",
|
|
70798
|
-
...args8.logoRef ? { logo: args8.logoRef } : {},
|
|
70799
|
-
...args8.fontRefs
|
|
70800
|
-
},
|
|
70801
|
-
params: {
|
|
70802
|
-
composition: layoutDirFor(entry.format),
|
|
70803
|
-
wait_for: { kind: "selector", value: "#root[data-ready='1']" },
|
|
70804
|
-
ad: "{{compose}}",
|
|
70805
|
-
frame: {
|
|
70806
|
-
format: entry.format,
|
|
70807
|
-
safe: safeAreaFor(entry.format),
|
|
70808
|
-
draw_copy: mode === "composed"
|
|
70809
|
-
}
|
|
70810
|
-
}
|
|
71258
|
+
const { plateRef, plate, extend } = formatSource({
|
|
71259
|
+
entry,
|
|
71260
|
+
plan,
|
|
71261
|
+
adapt,
|
|
71262
|
+
chosen,
|
|
71263
|
+
mode,
|
|
71264
|
+
slots,
|
|
71265
|
+
genModel: args8.genModel
|
|
70811
71266
|
});
|
|
71267
|
+
if (extend) nodes.push(extend);
|
|
71268
|
+
layoutIds.push(`ad_${slug}`);
|
|
71269
|
+
nodes.push(
|
|
71270
|
+
snapshotNode({
|
|
71271
|
+
format: entry.format,
|
|
71272
|
+
plateRef,
|
|
71273
|
+
plate,
|
|
71274
|
+
drawCopy: mode !== "baked",
|
|
71275
|
+
logoRef: args8.logoRef,
|
|
71276
|
+
fontRefs: args8.fontRefs
|
|
71277
|
+
})
|
|
71278
|
+
);
|
|
70812
71279
|
}
|
|
70813
71280
|
return { nodes, layoutIds };
|
|
70814
71281
|
}
|
|
71282
|
+
function fontLabel(font) {
|
|
71283
|
+
const origin = {
|
|
71284
|
+
brand_file: "the brand's font file",
|
|
71285
|
+
tokens: "named in tokens.json, loaded as a web font",
|
|
71286
|
+
brand_md: "named in BRAND.md, loaded as a web font",
|
|
71287
|
+
default: "the default, since the brand names no typeface"
|
|
71288
|
+
};
|
|
71289
|
+
const name = font.family ?? ("path" in font.slot ? font.slot.path.split("/").pop() ?? font.slot.path : "a web font");
|
|
71290
|
+
return `${name} (${origin[font.origin]})`;
|
|
71291
|
+
}
|
|
71292
|
+
function describeText(mode, fonts) {
|
|
71293
|
+
if (mode === "picture_only") return "The ad has no copy: the picture is the ad, with nothing drawn on it.";
|
|
71294
|
+
if (mode === "baked") {
|
|
71295
|
+
return "The copy is baked into the picture by the image model (no font could be loaded, or --skip-font).";
|
|
71296
|
+
}
|
|
71297
|
+
if (!fonts) return "The copy is drawn from prompt.json in a fallback system face.";
|
|
71298
|
+
const oneFace = slotKey(fonts.body) === slotKey(fonts.headline) || fonts.body.family !== null && fonts.body.family === fonts.headline.family;
|
|
71299
|
+
const body = oneFace ? "" : ` and ${fontLabel(fonts.body)}`;
|
|
71300
|
+
return `The copy is drawn from prompt.json in ${fontLabel(fonts.headline)}${body}.`;
|
|
71301
|
+
}
|
|
71302
|
+
function describeRun(args8) {
|
|
71303
|
+
const { mode, takes, logo } = args8;
|
|
71304
|
+
const numbers = ["No", "One", "Two", "Three"];
|
|
71305
|
+
const pictures = mode === "text_only" ? "No picture is generated: every format is the copy drawn on the brand's colours." : takes > 1 ? `${numbers[takes] ?? takes} pictures are rendered and a judge picks one; every format is cut from it.` : "One picture is rendered (no judge); every format is cut from it.";
|
|
71306
|
+
const text2 = describeText(mode, args8.brand?.fonts);
|
|
71307
|
+
const scene = args8.inScene > 0 ? ` ${args8.inScene} line(s) written on something in the scene are painted into the picture.` : "";
|
|
71308
|
+
const mark = logo ? " The brand's logo file is drawn where the concept places it." : " No logo is drawn: the concept has none, or the brand has no logo file (Meta shows the page name and picture on every ad).";
|
|
71309
|
+
return `${pictures} ${text2}${scene}${mark}`;
|
|
71310
|
+
}
|
|
70815
71311
|
function scaffoldStaticAd(input, elementsInput, opts) {
|
|
70816
71312
|
const blueprint = Blueprint.parse(input);
|
|
70817
71313
|
const elements = MainElements.parse(elementsInput);
|
|
70818
|
-
const mode =
|
|
71314
|
+
const mode = staticMode(blueprint, opts);
|
|
70819
71315
|
const plan = planStaticFormats(opts.genModel, outputFormats(blueprint, opts));
|
|
71316
|
+
const drawLogo = logoDrawn(blueprint, opts);
|
|
70820
71317
|
const spine = [
|
|
70821
71318
|
ingestNode("prompt", opts.blueprintPath ?? "./prompt.json", "json"),
|
|
70822
|
-
...mode === "composed" ? [ingestNode("plate_spec", PLATE_SPEC_PATH, "json")] : [],
|
|
71319
|
+
...mode === "composed" || mode === "picture_only" ? [ingestNode("plate_spec", PLATE_SPEC_PATH, "json")] : [],
|
|
70823
71320
|
ingestNode("compose_spec", COMPOSE_SPEC_PATH, "json"),
|
|
70824
|
-
|
|
70825
|
-
|
|
70826
|
-
|
|
70827
|
-
|
|
70828
|
-
|
|
71321
|
+
...mode === "text_only" ? [] : [
|
|
71322
|
+
{
|
|
71323
|
+
id: "original",
|
|
71324
|
+
type: "ingest",
|
|
71325
|
+
params: opts.imageIsUrl ? { source: "url", url: opts.imagePath, expect: "image" } : { source: "path", path: opts.imagePath, expect: "image" }
|
|
71326
|
+
}
|
|
71327
|
+
]
|
|
70829
71328
|
];
|
|
70830
|
-
const els = elementNodes(elements, opts);
|
|
70831
|
-
const fonts = mode === "
|
|
70832
|
-
const plates =
|
|
71329
|
+
const els = elementNodes(elements, opts, drawLogo);
|
|
71330
|
+
const fonts = mode === "baked" ? { nodes: [], refs: {} } : fontNodes(opts.brand);
|
|
71331
|
+
const plates = mode === "text_only" ? { nodes: [], chosen: null } : plateNodes({
|
|
71332
|
+
blueprint,
|
|
71333
|
+
elements,
|
|
71334
|
+
slots: els.slots,
|
|
71335
|
+
opts,
|
|
71336
|
+
text: plateTextOf(mode),
|
|
71337
|
+
plateRatio: plan.plateRatio
|
|
71338
|
+
});
|
|
70833
71339
|
const formats = formatNodes({
|
|
70834
71340
|
plan,
|
|
70835
71341
|
mode,
|
|
@@ -70843,7 +71349,9 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
70843
71349
|
schema: "baker-canvas/1",
|
|
70844
71350
|
metadata: {
|
|
70845
71351
|
name: "static ad reproduction",
|
|
70846
|
-
description:
|
|
71352
|
+
description: `Scaffolded by \`baker canvas scaffold-static-ad\`. Edit \`prompt.json\` (the blueprint generated from the source image) into the ad you want; the copy and the layout are read from it on every run, and only what it holds is drawn. Drop a real asset at each [TODO] ingest path, then \`baker canvas run\`. ${describeRun(
|
|
71353
|
+
{ mode, takes: takesFor(opts), logo: drawLogo, brand: opts.brand, inScene: inSceneText(blueprint).length }
|
|
71354
|
+
)}`
|
|
70847
71355
|
},
|
|
70848
71356
|
nodes: [
|
|
70849
71357
|
...spine,
|
|
@@ -70861,26 +71369,45 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
70861
71369
|
output: { node: "final", output: "images" }
|
|
70862
71370
|
};
|
|
70863
71371
|
}
|
|
71372
|
+
function logoWhy(blueprint, opts) {
|
|
71373
|
+
if (!conceptHasLogo(blueprint)) {
|
|
71374
|
+
return "The concept shows no logo, so none is drawn: Meta shows the page name and picture on every ad.";
|
|
71375
|
+
}
|
|
71376
|
+
if (!opts.brand?.logoPath) {
|
|
71377
|
+
return "The concept shows a logo but src/brand/logos/ has no real logo file, so none is drawn (never a stand-in). Add the file and re-scaffold to have it drawn.";
|
|
71378
|
+
}
|
|
71379
|
+
return `The brand logo ${opts.brand.logoPath} is drawn where the concept places it, never generated.`;
|
|
71380
|
+
}
|
|
70864
71381
|
function staticAdReport(input, elementsInput, opts) {
|
|
70865
71382
|
const blueprint = Blueprint.parse(input);
|
|
70866
71383
|
const elements = MainElements.parse(elementsInput);
|
|
70867
71384
|
const includeActorSheets = opts.includeActorSheets !== false;
|
|
70868
71385
|
const plan = planStaticFormats(opts.genModel, outputFormats(blueprint, opts));
|
|
71386
|
+
const mode = staticMode(blueprint, opts);
|
|
71387
|
+
const drawLogo = logoDrawn(blueprint, opts);
|
|
71388
|
+
const takes = takesFor(opts);
|
|
71389
|
+
const fonts = mode !== "baked" ? opts.brand?.fonts : void 0;
|
|
71390
|
+
const inScene = inSceneText(blueprint).map((t) => t.text);
|
|
70869
71391
|
return {
|
|
70870
71392
|
element_count: elements.length,
|
|
70871
71393
|
elements: assignElementLabels2(elements).map(({ el, label }) => ({
|
|
70872
71394
|
label,
|
|
70873
71395
|
type: el.type,
|
|
70874
71396
|
description: el.description ?? null,
|
|
70875
|
-
asset_todo: suppliedImage(el) ? "none
|
|
71397
|
+
asset_todo: suppliedImage(el) ? "none: cast from a company Avatar's identity sheet" : el.type.toLowerCase() === "logo" ? drawLogo ? `none: the brand logo ${opts.brand?.logoPath} is drawn by the composition` : "none: no logo is drawn (the logo is optional)" : opts.textOnly ? "none: a text-only ad generates no picture" : todoPath2(el, label)
|
|
70876
71398
|
})),
|
|
70877
|
-
|
|
70878
|
-
|
|
71399
|
+
mode,
|
|
71400
|
+
summary: describeRun({ mode, takes, logo: drawLogo, brand: opts.brand, inScene: inScene.length }),
|
|
71401
|
+
fonts: fonts ? { headline: fontLabel(fonts.headline), body: fontLabel(fonts.body) } : null,
|
|
71402
|
+
logo: { drawn: drawLogo, why: logoWhy(blueprint, opts) },
|
|
71403
|
+
in_scene_text: mode === "text_only" ? [] : inScene,
|
|
71404
|
+
actor_sheets: includeActorSheets && !opts.textOnly ? assignElementLabels2(elements).filter(({ el }) => SHEET_SUBJECT_TYPE2[el.type.toLowerCase()]).map(({ label }) => label) : [],
|
|
70879
71405
|
aspect_ratio: plan.plateRatio,
|
|
70880
71406
|
placements: opts.placements ?? null,
|
|
70881
71407
|
output_formats: plan.formats.map((f) => f.format),
|
|
70882
71408
|
format_plan: plan.formats,
|
|
70883
|
-
takes
|
|
71409
|
+
takes,
|
|
71410
|
+
judged: takes > 1,
|
|
70884
71411
|
winning_mechanisms: winningMechanisms(blueprint),
|
|
70885
71412
|
describe_gaps: describeGaps(blueprint)
|
|
70886
71413
|
};
|
|
@@ -71192,6 +71719,46 @@ async function uploadSourceAsReference(source, isUrl, client) {
|
|
|
71192
71719
|
if (!putRes.ok) throw new Error(`source upload failed (${putRes.status})`);
|
|
71193
71720
|
return publicUrl;
|
|
71194
71721
|
}
|
|
71722
|
+
async function brandFaces(root, kit, toCanvas) {
|
|
71723
|
+
const read = (file) => readFile20(path26.join(root, BRAND_DIR, file), "utf8").catch(() => null);
|
|
71724
|
+
const named = namedTypefaces({ tokensJson: await read("tokens.json"), brandMd: await read("BRAND.md") });
|
|
71725
|
+
const fonts = await resolveStaticFonts({
|
|
71726
|
+
files: { headline: kit.headlineFont, body: kit.bodyFont },
|
|
71727
|
+
named,
|
|
71728
|
+
resolveUrl: (family, weight) => webFontUrl(family, weight, { lookupGoogleCss, fetchJson })
|
|
71729
|
+
});
|
|
71730
|
+
if (!fonts) return null;
|
|
71731
|
+
const local = (font) => "path" in font.slot ? { ...font, slot: { path: toCanvas(font.slot.path) } } : font;
|
|
71732
|
+
return { headline: local(fonts.headline), body: local(fonts.body) };
|
|
71733
|
+
}
|
|
71734
|
+
function staticTextNote(report, bakeText) {
|
|
71735
|
+
const faces = !report.fonts ? "a fallback system face" : report.fonts.headline === report.fonts.body ? report.fonts.headline : `${report.fonts.headline} / ${report.fonts.body}`;
|
|
71736
|
+
switch (report.mode) {
|
|
71737
|
+
case "composed":
|
|
71738
|
+
return `The copy is DRAWN, not generated: each line of text_content in prompt.json (minus the logo and any text written in the scene) is set in ${faces} where the source layout put it, styled the way the reference styled it. A call to action stays plain, underlined, an arrow or a button as the reference had it (role "cta", and a "treatment" to change it); nothing is added that prompt.json does not hold. Edit the copy in prompt.json; every run reads it again.`;
|
|
71739
|
+
case "picture_only":
|
|
71740
|
+
return "The concept has no copy: the picture is the ad, with nothing drawn on it. Add lines to text_content in prompt.json to draw copy.";
|
|
71741
|
+
case "text_only":
|
|
71742
|
+
return `Text-only: no picture is generated. Every line of prompt.json is drawn on the brand's colours in ${faces}. Pass --no-text-only to render a picture instead.`;
|
|
71743
|
+
default:
|
|
71744
|
+
return bakeText ? "The copy is baked into the picture (--skip-font). Re-read every word on every format before publishing." : "The copy is baked into the picture because no font could be loaded: no file in src/brand/fonts/, and neither the typeface the brand names nor the default web font could be fetched. Re-read every word before publishing.";
|
|
71745
|
+
}
|
|
71746
|
+
}
|
|
71747
|
+
function staticFormatsNote(report, genModel) {
|
|
71748
|
+
const set = report.placements === null ? " Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 for a platform's set." : "";
|
|
71749
|
+
if (report.mode === "text_only") {
|
|
71750
|
+
return `Delivers ${report.output_formats.join(", ")}, each drawn directly at its exact size (no picture).${set}`;
|
|
71751
|
+
}
|
|
71752
|
+
const how = report.format_plan.map((f) => {
|
|
71753
|
+
if (f.format === report.aspect_ratio) return `${f.format} is the picture itself`;
|
|
71754
|
+
if (report.mode === "baked") {
|
|
71755
|
+
return `${f.format} is re-framed from the chosen picture by Nano Banana Pro (one billed render; the copy is in the pixels, so it is never cropped)`;
|
|
71756
|
+
}
|
|
71757
|
+
return f.source === "crop" ? `${f.format} is a free crop of it` : `${f.format} extends the chosen picture to ${f.extendRatio} (one billed render) and crops the rest`;
|
|
71758
|
+
}).join("; ");
|
|
71759
|
+
const drawn = report.mode === "composed" ? " Nothing re-paints the copy or the logo." : "";
|
|
71760
|
+
return `Delivers ${report.output_formats.join(", ")}, each at its exact size. The pictures render at ${report.aspect_ratio} (the nearest shape ${genModel} renders); ${how}.${drawn}${set}`;
|
|
71761
|
+
}
|
|
71195
71762
|
function resolveModel(kind, preferred) {
|
|
71196
71763
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
71197
71764
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
@@ -71212,7 +71779,7 @@ Return a JSON object describing the GLOBAL LAYOUT of the frame precisely enough
|
|
|
71212
71779
|
"content": what is here (name the subject/badge; reuse its description),
|
|
71213
71780
|
"background_color": hex or null,
|
|
71214
71781
|
"shape": e.g. "rounded-rectangle" | "pill" | null,
|
|
71215
|
-
"text": [ { "string": verbatim, "size": "x_large"|"large"|"medium"|"small", "weight": "bold"|"semibold"|"regular", "case": "upper"|"title"|"sentence", "color": hex, "align": "left"|"center"|"right" } ],
|
|
71782
|
+
"text": [ { "string": verbatim, "size": "x_large"|"large"|"medium"|"small", "weight": "bold"|"semibold"|"regular", "case": "upper"|"title"|"sentence", "color": hex, "align": "left"|"center"|"right", "treatment": "plain"|"underline"|"arrow"|"button"|"sticker"|"highlight" } ],
|
|
71216
71783
|
"notes": splits / overlaps / margins worth rebuilding
|
|
71217
71784
|
} ] }
|
|
71218
71785
|
|
|
@@ -71374,7 +71941,7 @@ async function runVisionPasses(canvas) {
|
|
|
71374
71941
|
var scaffoldStaticAdCommand = defineCommand134({
|
|
71375
71942
|
meta: {
|
|
71376
71943
|
name: "scaffold-static-ad",
|
|
71377
|
-
description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that renders three candidate pictures (no text, no logo), judges them, and draws every placement format from the winner
|
|
71944
|
+
description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that renders up to three candidate pictures (no overlay text, no logo), judges them, and draws every placement format from the winner with only what prompt.json holds: each line where the source layout put it, styled as the reference styled it, in the brand's fonts (a font file, else the typeface the brand names as a web font, else Inter), and the logo from src/brand/logos/ only when the concept shows one. No copy means the picture is the ad; a reference that pictures nothing but copy becomes a text-only ad with no picture at all (--text-only). A format close to the picture's shape is a free crop; a far one extends the picture. The copy is baked into the picture only with --skip-font or when no font loads. Pass --placements none for a single base ad, or a preset to pick the set. Edit prompt.json and drop the real assets, then `baker canvas run` it. Start here: `baker avatars list`, then pass `--avatar <handle>` when the image shows a person \u2014 the ad's people are played by the company's avatars, never by a photo of anyone real."
|
|
71378
71945
|
},
|
|
71379
71946
|
args: {
|
|
71380
71947
|
file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
|
|
@@ -71404,7 +71971,11 @@ var scaffoldStaticAdCommand = defineCommand134({
|
|
|
71404
71971
|
},
|
|
71405
71972
|
"skip-font": {
|
|
71406
71973
|
type: "boolean",
|
|
71407
|
-
description: "Bake the copy into the picture instead of drawing it in
|
|
71974
|
+
description: "Bake the copy into the picture instead of drawing it in a real font. By default the copy is drawn in a font file from src/brand/fonts/, else the typeface tokens.json or BRAND.md names (loaded as a web font), else Inter."
|
|
71975
|
+
},
|
|
71976
|
+
"text-only": {
|
|
71977
|
+
type: "boolean",
|
|
71978
|
+
description: "A typographic ad: no picture is generated, the copy from prompt.json is drawn on the brand's colours in its fonts (near-zero credits). On by default when the reference pictures nothing but copy; --no-text-only renders a picture anyway."
|
|
71408
71979
|
},
|
|
71409
71980
|
takes: {
|
|
71410
71981
|
type: "string",
|
|
@@ -71490,6 +72061,9 @@ var scaffoldStaticAdCommand = defineCommand134({
|
|
|
71490
72061
|
const relative = (file) => file ? toCanvasRelative2(canvasDir, file) : void 0;
|
|
71491
72062
|
const takes = args8.takes !== void 0 ? Number.parseInt(String(args8.takes), 10) : void 0;
|
|
71492
72063
|
if (takes !== void 0 && !(takes >= 1 && takes <= 3)) return fail4("invalid_takes", "--takes must be 1, 2 or 3");
|
|
72064
|
+
const bakeText = args8["skip-font"] === true;
|
|
72065
|
+
const textOnly = args8["text-only"] === void 0 ? isTextForward(blueprint) : args8["text-only"] === true;
|
|
72066
|
+
const fonts = bakeText ? null : await brandFaces(workspaceRoot, kit, (file) => toCanvasRelative2(canvasDir, file));
|
|
71493
72067
|
const opts = {
|
|
71494
72068
|
genModel,
|
|
71495
72069
|
imagePath: canvasImagePath,
|
|
@@ -71498,13 +72072,10 @@ var scaffoldStaticAdCommand = defineCommand134({
|
|
|
71498
72072
|
aspectRatio: args8.aspect ? String(args8.aspect) : void 0,
|
|
71499
72073
|
includeActorSheets: !args8["skip-actor-sheets"],
|
|
71500
72074
|
placements,
|
|
71501
|
-
brand: {
|
|
71502
|
-
|
|
71503
|
-
|
|
71504
|
-
|
|
71505
|
-
},
|
|
71506
|
-
bakeText: args8["skip-font"] === true,
|
|
71507
|
-
takes
|
|
72075
|
+
brand: { logoPath: relative(kit.logo), ...fonts ? { fonts } : {} },
|
|
72076
|
+
bakeText,
|
|
72077
|
+
takes,
|
|
72078
|
+
textOnly
|
|
71508
72079
|
};
|
|
71509
72080
|
let canvas;
|
|
71510
72081
|
let report;
|
|
@@ -71517,8 +72088,9 @@ var scaffoldStaticAdCommand = defineCommand134({
|
|
|
71517
72088
|
for (const layout2 of staticAdCompositions(blueprint, opts)) {
|
|
71518
72089
|
await writeStaticComposition(path26.resolve(canvasDir, layout2.dir), layout2.width, layout2.height);
|
|
71519
72090
|
}
|
|
71520
|
-
|
|
71521
|
-
await writeFile9(path26.resolve(canvasDir,
|
|
72091
|
+
const specOptions = composeOptionsFor(canvas, annotated);
|
|
72092
|
+
await writeFile9(path26.resolve(canvasDir, PLATE_SPEC_PATH), renderPlateSpec(annotated, specOptions), "utf8");
|
|
72093
|
+
await writeFile9(path26.resolve(canvasDir, COMPOSE_SPEC_PATH), renderComposeSpec(annotated, kit, specOptions), "utf8");
|
|
71522
72094
|
const validation = await validateScaffoldedStaticAd(canvas, path26.dirname(outPath));
|
|
71523
72095
|
if (validation.blocking.length > 0) {
|
|
71524
72096
|
process.stderr.write(
|
|
@@ -71590,18 +72162,16 @@ var scaffoldStaticAdCommand = defineCommand134({
|
|
|
71590
72162
|
winning_mechanisms: report.winning_mechanisms,
|
|
71591
72163
|
rebuild_the_mechanism: "`winning_mechanisms` is WHY the source ad works. Rewrite each one for our brand in prompt.json (a rhyme stays a rhyme in our words, a before/after stays a before/after of our product); never copy the source's literal copy.",
|
|
71592
72164
|
assets_to_supply: report.elements,
|
|
71593
|
-
|
|
71594
|
-
|
|
71595
|
-
|
|
72165
|
+
mode: report.mode,
|
|
72166
|
+
summary: report.summary,
|
|
72167
|
+
text: staticTextNote(report, bakeText),
|
|
72168
|
+
fonts: report.fonts,
|
|
72169
|
+
in_scene_text: report.in_scene_text,
|
|
72170
|
+
logo: report.logo.why,
|
|
72171
|
+
takes: report.takes === 0 ? "No picture is rendered: a text-only ad costs no image credits." : report.takes === 1 ? "1 picture is rendered and used as it is (no judge)." : `${report.takes} pictures are rendered and judged (one idea, thumbnail legibility, mechanism, space for what is drawn, brand, artifacts, safe zones, text); the winner becomes the ad and the others stay in the run as alternates.`,
|
|
71596
72172
|
actor_sheets: report.actor_sheets.length > 0 ? `Each animal hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead. A person hero is cast from a company Avatar (--avatar <handle>), never a found photo.` : "none (no animal heroes detected, or --skip-actor-sheets). A person hero is cast from a company Avatar (--avatar <handle>), never a found photo.",
|
|
71597
|
-
formats:
|
|
71598
|
-
|
|
71599
|
-
if (report.text_mode === "baked") {
|
|
71600
|
-
return `${f.format} is re-framed from the chosen picture by Nano Banana Pro (one billed render; the copy is in the pixels, so it is never cropped)`;
|
|
71601
|
-
}
|
|
71602
|
-
return f.source === "crop" ? `${f.format} is a free crop of it` : `${f.format} extends the chosen picture to ${f.extendRatio} (one billed render) and crops the rest`;
|
|
71603
|
-
}).join("; ")}.${report.text_mode === "composed" ? " Nothing re-paints the copy or the logo." : ""}${report.placements === null ? " Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 for a platform's set." : ""}`,
|
|
71604
|
-
note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. For any LOGO slot, use the company's committed brand logo from `src/brand/logos/` (per BRAND.md's Logos map) \u2014 do NOT re-fetch it with `baker images logo <domain>` when a brand logo exists, because the Brandfetch CDN often serves an OUTDATED mark and that is how an old logo ships on a live ad. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
72173
|
+
formats: staticFormatsNote(report, opts.genModel),
|
|
72174
|
+
note: `Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away, one at a time. The logo is optional and never a slot to fill: it is drawn only when prompt.json keeps a logo (a layout region of kind "logo" or an advertiser entry in brands_logos) and src/brand/logos/ has the real file; never fetch or draw a stand-in. When every slot is filled, \`baker canvas validate\` then \`baker canvas run\`.${report.takes > 0 ? " Running generates a billed image: it is not free." : " A text-only ad renders no picture and costs next to nothing."}`
|
|
71605
72175
|
}
|
|
71606
72176
|
},
|
|
71607
72177
|
null,
|
|
@@ -74334,7 +74904,7 @@ import { readFileSync as readFileSync17 } from "fs";
|
|
|
74334
74904
|
import path32 from "path";
|
|
74335
74905
|
import { defineCommand as defineCommand144 } from "citty";
|
|
74336
74906
|
import { parse as parseYaml } from "yaml";
|
|
74337
|
-
var DESCRIPTION4 = `How each Baker creative is doing in the live Meta ads that show it: spend, impressions, CTR, CPC, results, cost per result, frequency and a fatigue verdict, next to the angle and
|
|
74907
|
+
var DESCRIPTION4 = `How each Baker creative is doing in the live Meta ads that show it: spend, impressions, CTR, CPC, results, cost per result, frequency and a fatigue verdict, next to the angle, hook, mechanism and dominant slot its definition set out to test. Use it before briefing new creatives, to learn which angle and mechanism to make next and which creatives to replace.
|
|
74338
74908
|
|
|
74339
74909
|
Start here:
|
|
74340
74910
|
baker creatives performance # every creative in an ad, last 30 days
|
|
@@ -74357,7 +74927,7 @@ var ARGS6 = {
|
|
|
74357
74927
|
}
|
|
74358
74928
|
};
|
|
74359
74929
|
registerSchema({ command: "creatives.performance", description: DESCRIPTION4, args: ARGS6 });
|
|
74360
|
-
var INSIGHT_KEYS = ["angle", "hook", "persona", "awareness", "hypothesis"];
|
|
74930
|
+
var INSIGHT_KEYS = ["angle", "hook", "mechanism", "dominantSlot", "persona", "awareness", "hypothesis"];
|
|
74361
74931
|
var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
74362
74932
|
function insightValue(value2) {
|
|
74363
74933
|
if (typeof value2 === "string") return value2.trim() || null;
|
|
@@ -74389,20 +74959,25 @@ function readLocalDefinition(slug) {
|
|
|
74389
74959
|
return null;
|
|
74390
74960
|
}
|
|
74391
74961
|
}
|
|
74392
|
-
|
|
74393
|
-
|
|
74962
|
+
var COMPARED_FIELDS = [
|
|
74963
|
+
{ key: "angle", label: "angle", advice: "Brief the next creative on the cheapest angle with a new hook." },
|
|
74964
|
+
{ key: "mechanism", label: "mechanism", advice: "Brief the next creative on the cheapest mechanism." },
|
|
74965
|
+
{ key: "dominantSlot", label: "dominant slot", advice: "Lead the next creative with the cheapest dominant slot." }
|
|
74966
|
+
];
|
|
74967
|
+
function cheapestHint(creatives, { key, label, advice }) {
|
|
74968
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
74394
74969
|
for (const creative of creatives) {
|
|
74395
|
-
const
|
|
74396
|
-
if (!
|
|
74397
|
-
const best =
|
|
74970
|
+
const value2 = creative.definition?.[key];
|
|
74971
|
+
if (!value2 || creative.costPerResult === null) continue;
|
|
74972
|
+
const best = byValue.get(value2);
|
|
74398
74973
|
if (!best || (best.costPerResult ?? Number.POSITIVE_INFINITY) > creative.costPerResult)
|
|
74399
|
-
|
|
74974
|
+
byValue.set(value2, creative);
|
|
74400
74975
|
}
|
|
74401
|
-
if (
|
|
74402
|
-
const ranked = [...
|
|
74403
|
-
([
|
|
74976
|
+
if (byValue.size < 2) return null;
|
|
74977
|
+
const ranked = [...byValue.entries()].sort(([, a], [, b]) => (a.costPerResult ?? 0) - (b.costPerResult ?? 0)).map(
|
|
74978
|
+
([value2, creative]) => `"${value2}" ${creative.costPerResult}${creative.currency ? ` ${creative.currency}` : ""} (${creative.slug ?? creative.title})`
|
|
74404
74979
|
);
|
|
74405
|
-
return `Cost per result by
|
|
74980
|
+
return `Cost per result by ${label}: ${ranked.join(", ")}. ${advice}`;
|
|
74406
74981
|
}
|
|
74407
74982
|
function withDefinitions(response, readDefinition) {
|
|
74408
74983
|
const creatives = response.data.creatives.map((creative) => {
|
|
@@ -74411,8 +74986,10 @@ function withDefinitions(response, readDefinition) {
|
|
|
74411
74986
|
return definition ? { ...creative, definition } : creative;
|
|
74412
74987
|
});
|
|
74413
74988
|
const hints2 = [...response.hints ?? []];
|
|
74414
|
-
const
|
|
74415
|
-
|
|
74989
|
+
for (const field of COMPARED_FIELDS) {
|
|
74990
|
+
const hint = cheapestHint(creatives, field);
|
|
74991
|
+
if (hint) hints2.push(hint);
|
|
74992
|
+
}
|
|
74416
74993
|
const unnamed = creatives.filter((creative) => !creative.definition?.angle).map((c) => c.slug ?? c.title);
|
|
74417
74994
|
if (unnamed.length > 0) {
|
|
74418
74995
|
hints2.push(
|
|
@@ -86235,14 +86812,14 @@ var ANALYZERS = [
|
|
|
86235
86812
|
const counts = {};
|
|
86236
86813
|
for (const v of rounded) counts[v] = (counts[v] || 0) + 1;
|
|
86237
86814
|
const maxCount = Math.max(...Object.values(counts));
|
|
86238
|
-
const
|
|
86815
|
+
const pct2 = maxCount / rounded.length;
|
|
86239
86816
|
const unique = [...new Set(rounded)].filter((v) => v > 0);
|
|
86240
|
-
if (
|
|
86817
|
+
if (pct2 <= 0.6 || unique.length > 3) return [];
|
|
86241
86818
|
const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "?";
|
|
86242
86819
|
return [
|
|
86243
86820
|
{
|
|
86244
86821
|
id: "monotonous-spacing",
|
|
86245
|
-
snippet: `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(
|
|
86822
|
+
snippet: `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(pct2 * 100)}%)`,
|
|
86246
86823
|
file
|
|
86247
86824
|
}
|
|
86248
86825
|
];
|