@koda-sl/baker-cli 0.352.0-dev.09c54d90c → 0.352.0-dev.fabc18041
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 +2 -2
- package/dist/cli.js +45 -21
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5487,7 +5487,7 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
|
|
|
5487
5487
|
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`) so you drop the right file in each slot instead of stretching one logo to cover both. The describe pass also records the ad's **typography** under a `fonts` block (each typeface's classification, a best-guess family, and its weight/case) so you know exactly what to drop at the brand-font slot. Background extras are dropped. Each element is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
|
|
5488
5488
|
3. **global layout** (`~google/gemini-flash-latest`) — produces a structured `layout` block in `prompt.json`: the column/row grid, each region's `x_pct`/`y_pct` bounds, panel splits, background/shape, and every text block's relative size/weight/case/alignment. This is what gives the generator a precise composition to rebuild.
|
|
5489
5489
|
|
|
5490
|
-
It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only
|
|
5490
|
+
It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. Each **person/animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the face/muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
|
|
5491
5491
|
|
|
5492
5492
|
**Automatic placement fan-out.** By default the canvas doesn't stop at one ratio: the hero renders once at the platform's primary ratio, then an `image_aspect_adapt` node recomposes it into that platform's full placement set, and the canvas `output` points at the adapted images. The placement set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or disable it with `--placements none` for a single base ad. The hero ratio passes through the adapt node **free**; each other ratio is a billed recomposition — and because adaptation re-generates, every ratio should be eyeballed for text and brand-color fidelity before publish (`ad-formats.md` in the scaffold skill covers this).
|
|
5493
5493
|
|
|
@@ -5516,7 +5516,7 @@ baker canvas run ./static-ad.canvas.json
|
|
|
5516
5516
|
| `--placements <preset\|none>` | derived from `--platform` | Override the fan-out set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
|
|
5517
5517
|
| `--aspect <ratio>` | inferred from the image, else `9:16` | Force the hero's aspect ratio (clamped into the placement set so it stays a free pass-through). |
|
|
5518
5518
|
| `--skip-font` | off | Skip the brand-font → type-specimen slot. |
|
|
5519
|
-
| `--skip-actor-sheets` | off | Ground each animal
|
|
5519
|
+
| `--skip-actor-sheets` | off | Ground each person/animal on its lone dropped photo instead of a generated multi-view reference sheet. |
|
|
5520
5520
|
|
|
5521
5521
|
Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
|
|
5522
5522
|
|
package/dist/cli.js
CHANGED
|
@@ -11035,6 +11035,41 @@ function adCopyHints(payload) {
|
|
|
11035
11035
|
return tells.slice(0, MAX_HINTS).map(hintFor);
|
|
11036
11036
|
}
|
|
11037
11037
|
|
|
11038
|
+
// src/commands/ads/google/copy-list-usage.ts
|
|
11039
|
+
var AD_COPY_LIST_USAGE = `repeat the flag once per item, or pass one comma-separated list and wrap an item that contains a comma in double quotes: '"Fast, free quotes",Book today'. A flag given once is read as a list, so a lone item with a comma needs its double quotes too: '"Fast, free quotes"'`;
|
|
11040
|
+
var COPY_LIST_FLAG = {
|
|
11041
|
+
headlines: "--headlines",
|
|
11042
|
+
descriptions: "--descriptions",
|
|
11043
|
+
longHeadlines: "--long-headlines"
|
|
11044
|
+
};
|
|
11045
|
+
function valueAt(root, path48) {
|
|
11046
|
+
let node = root;
|
|
11047
|
+
for (const key of path48) {
|
|
11048
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
11049
|
+
node = node[key];
|
|
11050
|
+
}
|
|
11051
|
+
return node;
|
|
11052
|
+
}
|
|
11053
|
+
function countRefusal(op, issue) {
|
|
11054
|
+
if (issue.code !== "too_big" && issue.code !== "too_small" || issue.origin !== "array") return void 0;
|
|
11055
|
+
const flag = COPY_LIST_FLAG[String(issue.path.at(-1))];
|
|
11056
|
+
if (!flag) return void 0;
|
|
11057
|
+
const value = valueAt(op, issue.path);
|
|
11058
|
+
const count2 = Array.isArray(value) ? value.length : void 0;
|
|
11059
|
+
return issue.code === "too_big" ? { flag, count: count2, bound: `at most ${issue.maximum}`, tooMany: true } : { flag, count: count2, bound: `at least ${issue.minimum}`, tooMany: false };
|
|
11060
|
+
}
|
|
11061
|
+
function copyListCountFix(op, issues) {
|
|
11062
|
+
const refusals = issues.map((issue) => countRefusal(op, issue));
|
|
11063
|
+
if (refusals.length === 0 || refusals.some((refusal) => refusal === void 0)) return void 0;
|
|
11064
|
+
const counted = refusals.filter((refusal) => refusal !== void 0);
|
|
11065
|
+
const limits = counted.map(({ flag, count: count2, bound }) => `${flag} has ${count2 ?? "the wrong number of"} items; this ad takes ${bound}.`).join(" ");
|
|
11066
|
+
const commaCause = counted.some((refusal) => refusal.tooMany) ? ` If you meant fewer items, the copy was cut at its commas: a flag given once is read as a comma-separated list, so an item with a comma in it ("Fast, free quotes", a decimal like "4,5/5") becomes two. To keep a comma, ${AD_COPY_LIST_USAGE} \u2014 or put the content in --file as JSON.` : "";
|
|
11067
|
+
return {
|
|
11068
|
+
action: "fix_copy_list_count",
|
|
11069
|
+
explanation: `${limits}${commaCause} ${commaCause ? "Otherwise add" : "Add"} or drop items to fit the limit, then stage again; the field names are right.`
|
|
11070
|
+
};
|
|
11071
|
+
}
|
|
11072
|
+
|
|
11038
11073
|
// src/commands/ads/google/draft-status.ts
|
|
11039
11074
|
var OPERATION_LABELS = {
|
|
11040
11075
|
create: "Creating",
|
|
@@ -11355,7 +11390,7 @@ async function stageGoogleOp(raw, hints2) {
|
|
|
11355
11390
|
code: "VALIDATION_ERROR",
|
|
11356
11391
|
message: fields.map((f) => f.path ? `${f.path}: ${f.message}` : f.message).join("; "),
|
|
11357
11392
|
fields,
|
|
11358
|
-
fix: capabilityFix("google-ads")
|
|
11393
|
+
fix: copyListCountFix(raw, preflight.error.issues) ?? capabilityFix("google-ads")
|
|
11359
11394
|
}
|
|
11360
11395
|
});
|
|
11361
11396
|
process.exit(1);
|
|
@@ -11391,7 +11426,7 @@ async function stageGoogleOps(rawOps, hints2) {
|
|
|
11391
11426
|
code: "VALIDATION_ERROR",
|
|
11392
11427
|
message: fields.map((f) => `${f.path}: ${f.message}`).join("; "),
|
|
11393
11428
|
fields,
|
|
11394
|
-
fix: capabilityFix("google-ads")
|
|
11429
|
+
fix: copyListCountFix(raw, preflight.error.issues) ?? capabilityFix("google-ads")
|
|
11395
11430
|
}
|
|
11396
11431
|
});
|
|
11397
11432
|
process.exit(1);
|
|
@@ -16798,7 +16833,6 @@ import { defineCommand as defineCommand31 } from "citty";
|
|
|
16798
16833
|
|
|
16799
16834
|
// src/commands/ads/google/ad-copy-flags.ts
|
|
16800
16835
|
var AD_COPY_LIST_FLAGS = ["headlines", "descriptions", "long-headlines"];
|
|
16801
|
-
var AD_COPY_LIST_USAGE = `repeat the flag once per item, or pass one comma-separated list and wrap an item that contains a comma in double quotes: '"Fast, free quotes",Book today'. A flag given once is read as a list, so a lone item with a comma needs its double quotes too: '"Fast, free quotes"'`;
|
|
16802
16836
|
function skipBlanks(value, at) {
|
|
16803
16837
|
let i = at;
|
|
16804
16838
|
while (value[i] === " " || value[i] === " ") i++;
|
|
@@ -41330,6 +41364,7 @@ import { z as z40 } from "zod";
|
|
|
41330
41364
|
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"]);
|
|
41331
41365
|
var DEFAULT_ASPECT_RATIO = "9:16";
|
|
41332
41366
|
var SHEET_SUBJECT_TYPE2 = {
|
|
41367
|
+
person: "person",
|
|
41333
41368
|
animal: "character"
|
|
41334
41369
|
};
|
|
41335
41370
|
var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
|
|
@@ -41339,8 +41374,7 @@ var ADAPT_IMAGE_SIZE = "2K";
|
|
|
41339
41374
|
var ADAPT_GUIDANCE = "Keep the headline, logo, CTA, and hero subject fully visible in every ratio. Reproduce every text string verbatim \u2014 no dropped, added, or altered characters \u2014 and preserve the exact brand-color treatment (e.g. a black\u2192red word pivot), never flattening it.";
|
|
41340
41375
|
var Blueprint = z40.object({
|
|
41341
41376
|
meta: z40.object({ estimated_aspect_ratio: z40.string().optional() }).loose().optional(),
|
|
41342
|
-
text_content: z40.array(z40.object({ text: z40.string().optional() }).loose()).optional()
|
|
41343
|
-
people: z40.array(z40.unknown()).optional()
|
|
41377
|
+
text_content: z40.array(z40.object({ text: z40.string().optional() }).loose()).optional()
|
|
41344
41378
|
}).loose();
|
|
41345
41379
|
var ElementLocator = z40.object({
|
|
41346
41380
|
collection: z40.enum(["subjects", "people", "brands_logos"]),
|
|
@@ -41396,7 +41430,6 @@ function roleForType2(type) {
|
|
|
41396
41430
|
case "product":
|
|
41397
41431
|
return "the showcased product; keep this exact product identity. Ignore any caption text printed on this reference.";
|
|
41398
41432
|
case "person":
|
|
41399
|
-
return "the emotional hero \u2014 the brand's cast person: a company Avatar's identity sheet, a multi-view turnaround of ONE person the brand may show. Keep this exact face, hair and build, and render them once in the pose, gaze, wardrobe and expression the blueprint specifies \u2014 never as a grid. Ignore the cell labels printed on the sheet.";
|
|
41400
41433
|
case "animal":
|
|
41401
41434
|
return "the emotional hero \u2014 the face/muzzle/eyes the viewer locks onto and exactly where AI artifacts (malformed mouths, teeth, foam, extra fingers) betray a fake. It MUST be grounded in this real reference, never invented from prose. Keep this identity AND the expression the blueprint specifies. Ignore any caption text printed on this reference.";
|
|
41402
41435
|
default:
|
|
@@ -41407,10 +41440,7 @@ function todoPath2(el, label) {
|
|
|
41407
41440
|
const desc = el.description ? ` \u2014 ${el.description}` : "";
|
|
41408
41441
|
const expr = el.expression ? `, with a ${el.expression} expression` : "";
|
|
41409
41442
|
const t = el.type.toLowerCase();
|
|
41410
|
-
|
|
41411
|
-
return `[TODO: drop the identity sheet of a company Avatar for ${label}${desc}${expr} \u2014 REQUIRED: this is the emotional hero and the render keeps this face, so use \`baker avatars list\` or cast one with \`baker avatars create\`, then put its sheetUrl here; never a found photo of a real person (web search, the source ad), who never agreed to appear in this ad]`;
|
|
41412
|
-
}
|
|
41413
|
-
const lock = t === "animal" ? " \u2014 REQUIRED: this is the emotional hero; ground it in a real reference (image-library / video-library / Pinterest), do not delete this slot and free-generate" : "";
|
|
41443
|
+
const lock = t === "person" || t === "animal" ? " \u2014 REQUIRED: this is the emotional hero; ground it in a real reference (image-library / video-library / Pinterest), do not delete this slot and free-generate" : "";
|
|
41414
41444
|
return `[TODO: drop a real image for ${label} (${el.type})${desc}${expr}${lock} \u2014 replace with a path relative to this canvas file, never an absolute path]`;
|
|
41415
41445
|
}
|
|
41416
41446
|
function sheetSubjectDescription(el) {
|
|
@@ -41450,9 +41480,6 @@ function textFidelityDirective(blueprint, hasFont) {
|
|
|
41450
41480
|
"Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
|
|
41451
41481
|
].join("\n");
|
|
41452
41482
|
}
|
|
41453
|
-
function showsPerson(blueprint, elements) {
|
|
41454
|
-
return (blueprint.people?.length ?? 0) > 0 || elements.some((el) => el.type.toLowerCase() === "person");
|
|
41455
|
-
}
|
|
41456
41483
|
function scaffoldStaticAd(input, elementsInput, opts) {
|
|
41457
41484
|
const blueprint = Blueprint.parse(input);
|
|
41458
41485
|
const elements = MainElements.parse(elementsInput);
|
|
@@ -41518,18 +41545,15 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
41518
41545
|
params: { text: specimenText(blueprint), font_size: 72, line_height: 1.2, max_width: 1600 }
|
|
41519
41546
|
});
|
|
41520
41547
|
}
|
|
41521
|
-
const originalAsReference = !showsPerson(blueprint, elements);
|
|
41522
41548
|
const reference = [
|
|
41523
41549
|
...elementSlots.map((s) => s.ref),
|
|
41524
41550
|
...hasFont ? ["$ref:type_ref.image"] : [],
|
|
41525
|
-
|
|
41551
|
+
"$ref:original.asset"
|
|
41526
41552
|
];
|
|
41527
41553
|
const legend = [
|
|
41528
41554
|
...elementSlots.map((s) => `- ${s.label} \u2014 ${roleForType2(s.type)}`),
|
|
41529
41555
|
...hasFont ? ["- TYPE SPECIMEN \u2014 render every text string in this exact typeface; ignore its color and layout."] : [],
|
|
41530
|
-
|
|
41531
|
-
"- ORIGINAL \u2014 use ONLY for composition, layout, and proportions. IGNORE its text, its logo, its brand name, and its colors entirely \u2014 they belong to a different brand."
|
|
41532
|
-
] : []
|
|
41556
|
+
"- ORIGINAL \u2014 use ONLY for composition, layout, and proportions. IGNORE its text, its logo, its brand name, and its colors entirely \u2014 they belong to a different brand."
|
|
41533
41557
|
].join("\n");
|
|
41534
41558
|
const exclusionsLast = imageProfileFor(opts.genModel)?.constraintPlacement === "last";
|
|
41535
41559
|
const exclusionsBlock = "Exclusions: no invented badges, icons, or seals; render exactly the subjects the blueprint names and no others; no competitor branding; no watermark.";
|
|
@@ -41593,7 +41617,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
41593
41617
|
schema: "baker-canvas/1",
|
|
41594
41618
|
metadata: {
|
|
41595
41619
|
name: "static ad reproduction",
|
|
41596
|
-
description: "Scaffolded by `baker canvas scaffold-static-ad`. Edit `prompt.json` (the blueprint generated from the source image) into the ad you want, drop a real asset at each [TODO] ingest path (logo/product/subject/badge + brand font), then `baker canvas run`. The original is wired for composition only
|
|
41620
|
+
description: "Scaffolded by `baker canvas scaffold-static-ad`. Edit `prompt.json` (the blueprint generated from the source image) into the ad you want, drop a real asset at each [TODO] ingest path (logo/product/subject/badge + brand font), then `baker canvas run`. The original is wired for composition only."
|
|
41597
41621
|
},
|
|
41598
41622
|
nodes,
|
|
41599
41623
|
output: fanOut ? { node: "adapt", output: "images" } : { node: "gen", output: "images" }
|
|
@@ -42063,7 +42087,7 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
42063
42087
|
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
|
|
42064
42088
|
"skip-actor-sheets": {
|
|
42065
42089
|
type: "boolean",
|
|
42066
|
-
description: "Ground each animal
|
|
42090
|
+
description: "Ground each person/animal on its lone dropped photo instead of a generated multi-view sheet"
|
|
42067
42091
|
}
|
|
42068
42092
|
},
|
|
42069
42093
|
async run({ args: args5 }) {
|
|
@@ -42204,7 +42228,7 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
42204
42228
|
edit_prompt: `Edit ${path21.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
|
|
42205
42229
|
assets_to_supply: report.elements,
|
|
42206
42230
|
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
|
|
42207
|
-
actor_sheets: report.actor_sheets.length > 0 ? `Each
|
|
42231
|
+
actor_sheets: report.actor_sheets.length > 0 ? `Each living 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.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
|
|
42208
42232
|
placements: report.placements === null ? `Single format (${report.aspect_ratio}). Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 to auto-fan-out to a platform's set.` : `Auto-fans the approved hero out to ${report.placements} \u2192 ${report.output_formats.join(", ")}. The hero ratio (${report.aspect_ratio}) passes through free; each other ratio is a billed render, AI-recomposed (not cropped). Re-read the copy and confirm the brand-color treatment on EVERY ratio before publishing \u2014 adaptation re-generates and can silently mangle text or flatten a color device. Pass --placements none for a single base ad.`,
|
|
42209
42233
|
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."
|
|
42210
42234
|
}
|