@koda-sl/baker-cli 0.393.0-dev.799d3f88c → 0.395.0-dev.f8ecbaa42

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 CHANGED
@@ -4066,8 +4066,37 @@ baker canvas catalog --node video_generate
4066
4066
  # multi-line / accented copy; the edit is re-validated before it is written back)
4067
4067
  baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and breathless"
4068
4068
  baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
4069
+
4070
+ # 6. Set a static ad's words without editing prompt.json by hand. No flags lists
4071
+ # every line with its role and layout region; flags change them in the copy
4072
+ # and the layout together and keep everything else in prompt.json.
4073
+ baker canvas copy spring-offer
4074
+ baker canvas copy spring-offer --headline "Nine days, not 31." --cta "Book your free survey" \
4075
+ --line body="Scaffolding included" --line body="MCS certificate included" \
4076
+ --line eyebrow@header="ROOFTOP SOLAR" --treatment cta=pill --remove caption --remove-logo
4069
4077
  ```
4070
4078
 
4079
+ `baker canvas copy <slug>` flags:
4080
+
4081
+ | Flag | What it does |
4082
+ |---|---|
4083
+ | `--headline`, `--sub`, `--cta` | Shorthand for `--line headline=…`, `--line subheadline=…`, `--line cta=…` |
4084
+ | `--line <role>[@<region>]=<text>` | Repeatable. A role you name gets exactly the lines you give, in order; old extra ones are dropped. A new line goes to the region of its role's last line, or the region you name. Roles: `headline`, `subheadline`, `body`, `cta`, `caption`, `fine_print`, `eyebrow` |
4085
+ | `--remove <role>` | Repeatable. Drops every line of that role |
4086
+ | `--treatment <role>=<kind>` | `plain`, `underline`, `arrow`, `button`, `pill`, `sticker`, `highlight` |
4087
+ | `--color <role>=<#rrggbb>` | The ink of that role's lines |
4088
+ | `--remove-logo` | Draws the ad without a logo |
4089
+ | `--mechanism "<verbal\|visual\|structural>: <device>"` | Repeatable. Replaces the reference's winning mechanisms with ours |
4090
+ | `--alternate "<hook>"` | Repeatable. Records a hook alternate on the creative's `_definition.md` |
4091
+ | `--spec <file.json>` | The same fields as JSON, for long copy |
4092
+ | `--dry-run` | Prints the changes without writing |
4093
+
4094
+ It refuses, before writing anything, a role it does not know, a region the layout does not have, a new
4095
+ line with no region to go to, and copy that says the same line twice. Replacing the headline that is
4096
+ the creative's recorded `hook` keeps the old one in `hookAlternates` and makes the new one the hook.
4097
+ The output lists each change, the resulting copy, what was recorded, and the next command
4098
+ (`baker canvas run <slug> --draft`).
4099
+
4071
4100
  #### The brand, the ending and the cast are drawn, not generated
4072
4101
 
4073
4102
  `baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
@@ -5513,6 +5542,9 @@ Takes a creative's slug (`baker canvas run spring-offer`) or a canvas path, reso
5513
5542
  | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
5514
5543
  | `--max-credits <n>` | uncapped (or `BAKER_CANVAS_MAX_CREDITS`) | Credit ceiling: aborts before billing when the estimate exceeds it, and at layer boundaries once actual spend does. Completed nodes stay cached, so retrying with a higher cap loses nothing. |
5515
5544
  | `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
5545
+ | `--draft` | off | A static ad only: render one picture at `high` quality with no judge, then draw and review every format exactly as the final does. Iterate on the copy and the layout with it, and run once without it for the final (three pictures at the best quality, judged). The canvas file is not changed. |
5546
+
5547
+ `stats.total_credits` is what the credits ledger debits for the run: each node's charge as the backend billed it (an estimate only when it reported none), plus the review's own model calls. `validate`'s figure is an estimate.
5516
5548
 
5517
5549
  **Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
5518
5550
 
@@ -5664,7 +5696,7 @@ baker canvas run spring-offer
5664
5696
  | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
5665
5697
  | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5666
5698
  | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
5667
- | `--text-only` | on when the reference pictures nothing but copy | Draw a typographic ad: no picture generated, the copy on the brand colours in its fonts. `--no-text-only` renders a picture anyway. |
5699
+ | `--text-only` | on when the reference pictures nothing but copy (a note, a letter, a comparison with no photo) | Draw a typographic ad: no picture generated; each line in the colour, weight and size prompt.json gives it, on the layout's background colour, with its cards, dividers, icons, tick and cross marks and app chrome. `--no-text-only` renders a picture anyway. |
5668
5700
  | `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set the hero fans out to (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` fan out to nothing). Any other value is refused. |
5669
5701
  | `--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. |
5670
5702
  | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it): `1:1`, `4:5`, `9:16`, `16:9`, `4:3`, `3:4`, `2:3`, `3:2` or `21:9`. |
@@ -20,10 +20,18 @@
20
20
  .block { font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; overflow-wrap: break-word; text-wrap: pretty; }
21
21
  .block.headline, .block.subheadline, .block.cta { font-family: 'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
22
22
  .block.headline { text-wrap: balance; letter-spacing: -0.01em; }
23
- .fill { display: inline-block; line-height: 1.1; padding: 0.5em 1.2em; }
23
+ .fill { display: inline-block; line-height: 1.1; padding: 0.5em 1.2em; white-space: nowrap; }
24
24
  .fill.highlight { padding: 0.12em 0.4em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
25
25
  .fill.sticker { transform: rotate(-3deg); }
26
+ .fill.outline { background: transparent !important; border: 0.09em solid currentColor; }
26
27
  .logo { display: block; width: auto; object-fit: contain; }
28
+ .shape { position: absolute; pointer-events: none; }
29
+ .shape svg { display: block; height: 100%; width: auto; }
30
+ .shape.chrome { display: flex; align-items: center; justify-content: space-between; }
31
+ .chrome-back { display: flex; align-items: center; gap: 0.2em; font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
32
+ .chrome-more { display: flex; align-items: center; justify-content: center; border-radius: 999px; }
33
+ .block.mark { display: flex; align-items: flex-start; gap: 0.45em; }
34
+ .block.mark svg { flex: none; width: 1.05em; height: 1.05em; margin-top: 0.1em; }
27
35
  </style>
28
36
  </head>
29
37
  <body>
@@ -122,6 +130,8 @@
122
130
  el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
123
131
  el.style.justifyContent = "center";
124
132
  el.style.gap = 1.2 * U + "px";
133
+ // A card's lines keep off its edges.
134
+ if (g.inset) el.style.padding = 2.6 * U + "px " + 3.2 * U + "px";
125
135
  // Copy rewritten for our brand runs longer than the source's: allow some growth
126
136
  // past the region before the type shrinks.
127
137
  el.dataset.max = String(((at.y[1] - at.y[0]) / 100) * H * 1.35);
@@ -146,6 +156,91 @@
146
156
  else el.insertBefore(img, el.firstChild);
147
157
  }
148
158
 
159
+ // The structures of a text-only ad, each at its layout region's box on this format:
160
+ // cards and panels, dividers, icons and an app's chrome. Drawn before the copy, under it.
161
+ var ICON_PATHS = {
162
+ check: '<path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="C" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>',
163
+ cross: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11" fill="none" stroke="C" stroke-width="3" stroke-linecap="round"/>',
164
+ dots: '<circle cx="5" cy="12" r="2" fill="C"/><circle cx="12" cy="12" r="2" fill="C"/><circle cx="19" cy="12" r="2" fill="C"/>',
165
+ chevron_left: '<path d="M15 4.5L7.5 12 15 19.5" fill="none" stroke="C" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
166
+ arrow_right: '<path d="M4 12h15M13 6l6 6-6 6" fill="none" stroke="C" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
167
+ star: '<path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z" fill="C"/>',
168
+ heart: '<path d="M12 20s-7-4.4-7-10a4 4 0 017-2.6A4 4 0 0119 10c0 5.6-7 10-7 10z" fill="C"/>',
169
+ };
170
+ function icon(name, color) {
171
+ var path = ICON_PATHS[name] || "";
172
+ return '<svg viewBox="0 0 24 24" aria-hidden="true">' + path.split('"C"').join('"' + color + '"') + "</svg>";
173
+ }
174
+ var SHAPE_RADIUS = { pill: "999px", rounded: 2.4 * U + "px", square: "0" };
175
+ function addShape(s) {
176
+ var at = s.at && s.at[FRAME.format];
177
+ if (!at) return;
178
+ var left = (at.x[0] / 100) * W;
179
+ var top = (at.y[0] / 100) * H;
180
+ var width = ((at.x[1] - at.x[0]) / 100) * W;
181
+ var height = ((at.y[1] - at.y[0]) / 100) * H;
182
+ var el = document.createElement("div");
183
+ el.className = "shape " + s.kind;
184
+ el.setAttribute("data-layout-ignore", "");
185
+ el.style.left = left + "px";
186
+ el.style.top = top + "px";
187
+ el.style.width = width + "px";
188
+ el.style.height = height + "px";
189
+ if (s.kind === "box") {
190
+ el.style.background = s.fill || "transparent";
191
+ if (s.border) el.style.border = Math.max(2, 0.28 * U) + "px solid " + s.border;
192
+ el.style.borderRadius = SHAPE_RADIUS[s.radius] || SHAPE_RADIUS.rounded;
193
+ if (s.shadow) el.style.boxShadow = "0 " + 0.8 * U + "px " + 2.6 * U + "px rgba(0,0,0,0.18)";
194
+ } else if (s.kind === "rule") {
195
+ var thick = Math.max(2, 0.32 * U);
196
+ el.style.background = s.color;
197
+ if (width >= height) {
198
+ el.style.top = top + height / 2 - thick / 2 + "px";
199
+ el.style.height = thick + "px";
200
+ } else {
201
+ el.style.left = left + width / 2 - thick / 2 + "px";
202
+ el.style.width = thick + "px";
203
+ }
204
+ } else if (s.kind === "icon") {
205
+ el.style.display = "flex";
206
+ el.style.justifyContent = "center";
207
+ el.style.alignItems = "center";
208
+ if (s.ring) {
209
+ // A circled icon (a note's "more" button): the ring is as tall as the region.
210
+ var ring = Math.min(width, height);
211
+ var circle = document.createElement("div");
212
+ circle.className = "chrome-more";
213
+ circle.style.width = ring + "px";
214
+ circle.style.height = ring + "px";
215
+ circle.style.border = Math.max(2, ring * 0.07) + "px solid " + s.color;
216
+ circle.innerHTML = '<span style="display:block;height:' + ring * 0.6 + 'px">' + icon(s.icon, s.color) + "</span>";
217
+ el.appendChild(circle);
218
+ } else {
219
+ el.innerHTML = icon(s.icon, s.color);
220
+ }
221
+ } else if (s.kind === "chrome") {
222
+ var size = Math.min(height * 0.62, 5 * U);
223
+ var back = document.createElement("div");
224
+ back.className = "chrome-back";
225
+ if (!facesStaged) back.style.fontFamily = FALLBACK_FACE;
226
+ back.style.color = s.color;
227
+ back.style.fontSize = size + "px";
228
+ back.innerHTML = '<span style="display:block;height:1.1em">' + icon("chevron_left", s.color) + "</span>";
229
+ var label = document.createElement("span");
230
+ label.textContent = s.label;
231
+ back.appendChild(label);
232
+ var more = document.createElement("div");
233
+ more.className = "chrome-more";
234
+ more.style.width = size * 1.35 + "px";
235
+ more.style.height = size * 1.35 + "px";
236
+ more.style.border = Math.max(2, size * 0.09) + "px solid " + s.color;
237
+ more.innerHTML = '<span style="display:block;height:' + size * 0.8 + 'px">' + icon("dots", s.color) + "</span>";
238
+ el.appendChild(back);
239
+ if (s.more) el.appendChild(more);
240
+ }
241
+ root.appendChild(el);
242
+ }
243
+
149
244
  function addBlock(b) {
150
245
  var el = document.createElement("div");
151
246
  el.className = "block " + b.role;
@@ -154,6 +249,11 @@
154
249
  el.dataset.base = String(size);
155
250
  el.style.fontSize = size * U + "px";
156
251
  el.style.fontWeight = String(WEIGHT[b.weight] || (b.role === "cta" ? 700 : 400));
252
+ // A staged face is one weight, so a line prompt.json sets bold (a bold paragraph, a
253
+ // sign-off) is drawn in the heavy face whatever its role.
254
+ if (facesStaged && WEIGHT[b.weight] >= 600) {
255
+ el.style.fontFamily = "'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif";
256
+ }
157
257
  el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
158
258
  el.style.textTransform = CASE[b.case] || "none";
159
259
  el.style.textAlign = b.align || "center";
@@ -169,15 +269,29 @@
169
269
  fill.style.background = t.background || (AD.cta && AD.cta.background) || "#111111";
170
270
  fill.style.color = t.color || (AD.cta && AD.cta.color) || "#ffffff";
171
271
  fill.style.borderRadius = RADIUS[t.shape] || RADIUS.rounded;
272
+ if (t.outline) fill.classList.add("outline");
172
273
  el.appendChild(fill);
173
274
  } else {
174
- el.textContent = text;
275
+ if (t.kind === "mark") {
276
+ // A tick or cross list item: the mark in its own colour, then the line.
277
+ el.classList.add("mark");
278
+ el.innerHTML = icon(t.mark, t.color);
279
+ var words = document.createElement("span");
280
+ words.textContent = text;
281
+ el.appendChild(words);
282
+ } else {
283
+ el.textContent = text;
284
+ }
175
285
  el.style.color = ink;
176
286
  if (t.kind === "underline") {
177
287
  el.style.textDecoration = "underline";
178
288
  el.style.textDecorationThickness = "0.08em";
179
289
  el.style.textUnderlineOffset = "0.18em";
180
290
  }
291
+ if (t.kind === "strike") {
292
+ el.style.textDecoration = "line-through";
293
+ el.style.textDecorationThickness = "0.08em";
294
+ }
181
295
  var dark = (luma(ink) || 255) < 110;
182
296
  el.dataset.dark = dark ? "1" : "0";
183
297
  if (hasPlate) el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
@@ -186,6 +300,7 @@
186
300
  }
187
301
 
188
302
  var drawCopy = FRAME.draw_copy !== false;
303
+ (hasPlate ? [] : AD.shapes || []).forEach(addShape);
189
304
  (drawCopy ? AD.copy || [] : []).forEach(addBlock);
190
305
  if (AD.logo) addLogo(AD.logo);
191
306
 
@@ -198,7 +313,9 @@
198
313
  // layout is written.
199
314
  function fit(el) {
200
315
  var max = Number(el.dataset.max);
201
- var width = el.getBoundingClientRect().width;
316
+ // The room inside a card's padding, not the card's outer width.
317
+ var pad = getComputedStyle(el);
318
+ var width = el.getBoundingClientRect().width - parseFloat(pad.paddingLeft) - parseFloat(pad.paddingRight);
202
319
  var scale = 1;
203
320
  for (var i = 0; i < 40; i++) {
204
321
  var tooTall = el.scrollHeight > max;
@@ -209,10 +326,42 @@
209
326
  scale *= 0.94;
210
327
  Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
211
328
  c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
329
+ c.dataset.scale = String(scale);
212
330
  });
213
331
  }
214
332
  }
215
333
 
334
+ // A region's copy may run past its box, but never into the next region down the same
335
+ // column: the room it has ends where that one starts.
336
+ function keepClearOfNext(el) {
337
+ var r = el.getBoundingClientRect();
338
+ var room = Infinity;
339
+ Array.prototype.forEach.call(root.querySelectorAll(".group.placed"), function (other) {
340
+ if (other === el) return;
341
+ var q = other.getBoundingClientRect();
342
+ var sameColumn = Math.min(r.right, q.right) - Math.max(r.left, q.left) > 0;
343
+ if (sameColumn && q.top > r.top + 1) room = Math.min(room, q.top - r.top - 0.8 * U);
344
+ });
345
+ // Two regions the format already stacks on each other are left as they are: holding
346
+ // one to less than its own box would only shrink its type to nothing.
347
+ if (room === Infinity || room < parseFloat(el.style.minHeight)) return;
348
+ el.dataset.max = String(Math.min(Number(el.dataset.max), room));
349
+ }
350
+
351
+ // Lines set at one size in prompt.json stay one size in the ad: every one is drawn at
352
+ // the smallest scale any of them needed to fit.
353
+ function evenSizes() {
354
+ var least = {};
355
+ var blocks = Array.prototype.slice.call(root.querySelectorAll(".block"));
356
+ blocks.forEach(function (b) {
357
+ var s = Number(b.dataset.scale || 1);
358
+ least[b.dataset.base] = Math.min(least[b.dataset.base] || 1, s);
359
+ });
360
+ blocks.forEach(function (b) {
361
+ b.style.fontSize = Number(b.dataset.base) * U * least[b.dataset.base] + "px";
362
+ });
363
+ }
364
+
216
365
  // A soft scrim behind a band of light type over the picture (or a light one behind
217
366
  // dark type), so copy with no position of its own reads on any plate. Copy placed
218
367
  // by the layout sits on the calm area the plate kept for it.
@@ -243,7 +392,9 @@
243
392
 
244
393
  var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
245
394
  ready.then(function () {
395
+ Array.prototype.forEach.call(root.querySelectorAll(".group.placed"), keepClearOfNext);
246
396
  containers().forEach(fit);
397
+ evenSizes();
247
398
  ["top", "bottom"].forEach(scrim);
248
399
  root.setAttribute("data-ready", "1");
249
400
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "static-ad",
3
- "title": "Static ad: the picture (or the brand colours) with what the concept holds drawn on top",
3
+ "title": "Static ad: the picture (or, text-only, the layout drawn from prompt.json) with what the concept holds drawn on top",
4
4
  "description": "Written by `baker canvas scaffold-static-ad`, one copy per delivered format. Draws only what prompt.compose.json (derived from prompt.json) holds: each line of copy where the source layout placed it, set the way the reference set it (plain, underlined, with an arrow, as a button or sticker), in the staged fonts, and the logo file only when the concept shows one. The plate is the image model's clean picture; a text-only ad has none and sits on the brand colour. Width and height are this format's delivered size.",
5
5
  "width": 1080,
6
6
  "height": 1350,