@koda-sl/baker-cli 0.388.0 → 0.390.0

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.
Files changed (28) hide show
  1. package/README.md +132 -40
  2. package/canvas/static-ad-composition/index.html +253 -0
  3. package/canvas/static-ad-composition/meta.json +53 -0
  4. package/dist/{chunk-FPJ5FXAX.js → chunk-6UE43HEQ.js} +324 -27
  5. package/dist/chunk-6UE43HEQ.js.map +1 -0
  6. package/dist/{chunk-AOVFDDVW.js → chunk-7OVZ6SOA.js} +3 -2
  7. package/dist/{chunk-AOVFDDVW.js.map → chunk-7OVZ6SOA.js.map} +1 -1
  8. package/dist/{chunk-NEZ3NAJV.js → chunk-EYN7G4KF.js} +4 -4
  9. package/dist/{chunk-T2UNK5BW.js → chunk-J76AGFBH.js} +3 -3
  10. package/dist/{chunk-QIQ57EIA.js → chunk-TWSWUIDI.js} +2 -2
  11. package/dist/{chunk-OKE2QJSJ.js → chunk-Z7WHG6TA.js} +3 -3
  12. package/dist/cli.js +7639 -5247
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-EQOSDYJL.js → client-YUWLIYLF.js} +4 -4
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-ANKM47IJ.js → env-RQGMYIQN.js} +2 -2
  17. package/dist/{output-NR4EJQDV.js → output-POG3LVBW.js} +5 -5
  18. package/dist/{shared-SLCDVV45.js → shared-ZAEUTUNU.js} +6 -6
  19. package/package.json +2 -1
  20. package/dist/chunk-FPJ5FXAX.js.map +0 -1
  21. /package/dist/{chunk-NEZ3NAJV.js.map → chunk-EYN7G4KF.js.map} +0 -0
  22. /package/dist/{chunk-T2UNK5BW.js.map → chunk-J76AGFBH.js.map} +0 -0
  23. /package/dist/{chunk-QIQ57EIA.js.map → chunk-TWSWUIDI.js.map} +0 -0
  24. /package/dist/{chunk-OKE2QJSJ.js.map → chunk-Z7WHG6TA.js.map} +0 -0
  25. /package/dist/{client-EQOSDYJL.js.map → client-YUWLIYLF.js.map} +0 -0
  26. /package/dist/{env-ANKM47IJ.js.map → env-RQGMYIQN.js.map} +0 -0
  27. /package/dist/{output-NR4EJQDV.js.map → output-POG3LVBW.js.map} +0 -0
  28. /package/dist/{shared-SLCDVV45.js.map → shared-ZAEUTUNU.js.map} +0 -0
package/README.md CHANGED
@@ -3830,7 +3830,7 @@ Starting a batch returns 202 + a `generationId`; the CLI polls until the takes s
3830
3830
  ```bash
3831
3831
  baker studio generate "a friendly golden retriever in a bright modern living room" --aspect-ratio 16:9
3832
3832
  baker studio generate "hero shot of a matte black water bottle on wet marble, studio light" \
3833
- --model google/gemini-3-pro-image-preview --image-size 2K --aspect-ratio 4:5 --count 3
3833
+ --model google/gemini-3-pro-image --image-size 2K --aspect-ratio 4:5 --count 3
3834
3834
  baker studio generate "home office hero, warm minimalist, natural light, 35mm photo" \
3835
3835
  --reference "https://i.pinimg.com/…/pin1.jpg,./src/brand/logos/product.png,j57abc123def456ghi789"
3836
3836
  baker studio generate "founder-style selfie, kitchen background" --skill ugc-selfie-hook
@@ -3838,13 +3838,13 @@ baker studio generate "flat geometric mascot, brand palette" \
3838
3838
  --model recraft/recraft-v4.1-pro-vector --rgb-colors "[[10,10,10],[255,80,0]]" --bg-rgb "[255,255,255]"
3839
3839
  ```
3840
3840
 
3841
- **Models** (`--model`, default `google/gemini-3.1-flash-image-preview`):
3841
+ **Models** (`--model`, default `google/gemini-3.1-flash-image`):
3842
3842
 
3843
3843
  | Model | Best for | Aspect ratios | Sizes |
3844
3844
  |---|---|---|---|
3845
- | `google/gemini-3.1-flash-image-preview` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
3845
+ | `google/gemini-3.1-flash-image` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
3846
3846
  | `google/gemini-3.1-flash-lite-image` | Cheapest tier (Nano Banana 2 Lite) — half the flash price per output token. High-volume variants and any 1K-sufficient frame, portrait ad formats included | standard **+** `1:4` `4:1` `1:8` `8:1` | `1K` **only** |
3847
- | `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
3847
+ | `google/gemini-3-pro-image` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
3848
3848
  | `openai/gpt-image-2` | Photoreal + cleanest in-image text — ad/landing reproduction | standard set **minus** `4:5` `5:4` | *(none — derived from the ratio)* |
3849
3849
  | `openai/gpt-5.4-image-2` | **Legacy** — accepted so canvases authored before the gpt-image-2 switch still re-run. Don't pick it for new work. | standard set | `1K` `2K` `4K` (ignored upstream) |
3850
3850
  | `recraft/recraft-v4.1-pro-vector` | Vector/flat/SVG-style with palette control. Writes **SVG**, so its output cannot be fed back as a reference | `1:1` `4:3` `3:4` `16:9` `9:16` **only** | *(none — refuses `image_size`)* |
@@ -3853,7 +3853,7 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
3853
3853
 
3854
3854
  `openai/gpt-image-2` accepts neither `image_size` nor the `4:5` / `5:4` portrait ratios — OpenAI picks the pixel dimensions from the ratio. To ship a 4:5 Meta feed ad from it, render the hero at `3:4` and adapt with `image_aspect_adapt`; the scaffolders do this for you.
3855
3855
 
3856
- `google/gemini-3.1-flash-lite-image` renders at `1K` and nothing else — `2K` / `4K` is a provider 400 (`resolution: not supported. Accepted: 1K`), not a soft clamp. It costs half of `google/gemini-3.1-flash-image-preview` per output image token ($0.00003 vs $0.00006), so it is the cheap-iteration tier for images. Keep hero stills and reference sheets that need `2K`+ on the flash or pro tier.
3856
+ `google/gemini-3.1-flash-lite-image` renders at `1K` and nothing else — `2K` / `4K` is a provider 400 (`resolution: not supported. Accepted: 1K`), not a soft clamp. It costs half of `google/gemini-3.1-flash-image` per output image token ($0.00003 vs $0.00006), so it is the cheap-iteration tier for images. Keep hero stills and reference sheets that need `2K`+ on the flash or pro tier.
3857
3857
 
3858
3858
  `recraft/recraft-v4.1-pro-vector` is the narrowest of the roster in three ways, all read off its live endpoint schema: it renders **five** ratios (no `4:5` / `5:4` / `2:3` / `3:2` / `21:9`, so a portrait ad shape snaps to `3:4`), it has **no `image_size`** — the provider accepts one and ignores it, which is why it is refused here — and it takes **exactly one** reference image against 14-16 elsewhere. Its output is an SVG, and the image models read only JPG, PNG and WebP, so a Recraft take cannot be used as a reference for the next render.
3859
3859
 
@@ -3863,7 +3863,7 @@ A knob the chosen model does not advertise is **refused, not silently dropped**
3863
3863
 
3864
3864
  | Flag | Description |
3865
3865
  |---|---|
3866
- | `--model` | Model id (default `google/gemini-3.1-flash-image-preview`) |
3866
+ | `--model` | Model id (default `google/gemini-3.1-flash-image`) |
3867
3867
  | `--aspect-ratio` | Output aspect ratio (default `1:1`) |
3868
3868
  | `--count` | Takes to render, 1–8 (default 1) |
3869
3869
  | `--reference` | Comma-separated visual references, in order: **library image ids**, **local file paths**, and/or **image URLs**. Paths and URLs are added to the media library first and passed as ids — the studio takes library ids only, so the client can see what grounded the render and the reference stays reusable. The biggest quality lever there is. |
@@ -3971,7 +3971,7 @@ Compact output per take is `{ slot, status, imageId | videoId, url, inLibrary, e
3971
3971
  | `lifestyle-scene` | `openai/gpt-image-2` | `4:5` | 3 | **yes** |
3972
3972
  | `cinematic-still` | `google/gemini-3-pro-image-preview` | `16:9` | 3 | no |
3973
3973
  | `brand-icon` | `recraft/recraft-v4.1-pro-vector` | `1:1` | 4 | no |
3974
- | `background-plate` | `google/gemini-3.1-flash-image-preview` | `4:5` | 3 | no |
3974
+ | `background-plate` | `google/gemini-3.1-flash-image` | `4:5` | 3 | no |
3975
3975
 
3976
3976
  A skill fills only what you did **not** pin — `--model`, `--aspect-ratio` and `--count` always win. Which means a skill can change the model, and the model decides what the rest of the flags may be: `--skill brand-icon` lands on Recraft, so it takes one reference image rather than fourteen, refuses `--image-size`, and writes SVG that cannot be fed back in as a reference. The CLI resolves the skill's model before checking the reference count, so an over-long `--reference` list is refused before anything is uploaded.
3977
3977
 
@@ -4067,7 +4067,7 @@ baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
4067
4067
  `baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
4068
4068
  because a video model garbles a wordmark every time:
4069
4069
 
4070
- - **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
4070
+ - **`brand.logo`** — the repo path to the mark. It is copied into the caption composition
4071
4071
  and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
4072
4072
  - **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
4073
4073
  becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
@@ -4087,9 +4087,24 @@ because a video model garbles a wordmark every time:
4087
4087
  alongside the music bed. Most beats should not have one: it belongs where the picture
4088
4088
  shows a specific physical event (a drill, a door, rain on glass), not on a talking head
4089
4089
  or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
4090
- - **A real brand mark is required.** `scaffold-ad` refuses to build without one in
4091
- `src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
4092
- card, so a stand-in ships an ad wearing the wrong brand.
4090
+ - **The logo is optional.** With no mark in `src/brand/logos/` (and none confirmed for the
4091
+ company's own domain), the ad is built without the corner mark, and one hint says so and
4092
+ how to add one: put the client's logo file in `src/brand/logos/` and re-run. Meta shows
4093
+ the page name and picture above every ad either way. The closing card still carries the
4094
+ call to action when the spec has a `cta`; with neither a mark nor a `cta`, the last beat
4095
+ keeps its footage. A typed stand-in (an SVG wordmark in live `<text>`) is still refused,
4096
+ and `--no-brand` still means no brand layer at all: no mark and no closing card.
4097
+ - **Captions and the mark sit inside the safe area.** On 9:16 the caption cards end above
4098
+ the bottom 37% of the frame (Reels covers the bottom 35%) and the mark starts below the
4099
+ top 16% (the Stories and Reels header); 4:5 and 1:1 use modest feed margins. The closing
4100
+ card is centred in the same area. Every composition is sized to the spec's
4101
+ `format.aspect_ratio`, so a 4:5 ad lays its captions out on a 4:5 frame.
4102
+ - **Captions and the closing button are set in the brand's face**: a font file in
4103
+ `src/brand/fonts/`, else the typeface `tokens.json` or `BRAND.md` names, loaded as a web
4104
+ font. With neither, they keep the default heavy sans.
4105
+ - **A spoken ad's captions print the script.** The voice track is transcribed only to time
4106
+ the words: each caption word is the `say` line as written, at the moment it was said, so
4107
+ a word the recogniser mishears never reaches the screen.
4093
4108
  - **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
4094
4109
  a separately cast voice, which is dubbing and looks like it.
4095
4110
  - **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
@@ -4163,7 +4178,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
4163
4178
  "type": "text_generate",
4164
4179
  "inputs": { "topic": "$ref:topic.text" },
4165
4180
  "params": {
4166
- "model": "google/gemini-3.1-flash-image-preview",
4181
+ "model": "google/gemini-3.1-flash-image",
4167
4182
  "prompt": "Write a 6-word tagline for {{topic}}."
4168
4183
  }
4169
4184
  }
@@ -4345,7 +4360,7 @@ Example pipeline:
4345
4360
  { "id": "font", "type": "ingest", "params": { "source": "url", "url": "https://cdn.example.com/brand.woff2", "expect": "font" } }
4346
4361
  { "id": "specimen", "type": "font_specimen", "inputs": { "font": "$ref:font.asset" }, "params": {} }
4347
4362
  { "id": "gen", "type": "image_generate", "inputs": { "reference": "$ref:specimen.image" },
4348
- "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4363
+ "params": { "model": "google/gemini-3.1-flash-image", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4349
4364
  ```
4350
4365
 
4351
4366
  ---
@@ -4484,13 +4499,13 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4484
4499
 
4485
4500
  Kept only so canvases authored before the gpt-image-2 switch still validate and re-run unchanged — `baker canvas rerun` replays a recorded canvas verbatim. It keeps the full standard ratio set (including `4:5` / `5:4`) and still accepts `image_size`, which the provider ignores. Prefer `openai/gpt-image-2` for anything new.
4486
4501
 
4487
- ###### Model: `google/gemini-3.1-flash-image-preview`
4502
+ ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4488
4503
 
4489
4504
  Gemini flash image model — the default. Fast iteration, reference editing, extreme aspect ratios, and the `0.5K` size for cheap previews.
4490
4505
 
4491
4506
  | Name | Type | Required | Notes |
4492
4507
  |---|---|---|---|
4493
- | `model` | literal | yes | `"google/gemini-3.1-flash-image-preview"` |
4508
+ | `model` | literal | yes | `"google/gemini-3.1-flash-image"` |
4494
4509
  | `prompt` | string | yes | non-empty |
4495
4510
  | `aspect_ratio` | enum | no | EXTREME AR |
4496
4511
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
@@ -4506,20 +4521,20 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
4506
4521
  | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
4507
4522
  | `image_size` | enum | no | `1K` only — `2K` / `4K` is a hard provider 400 (`resolution: not supported. Accepted: 1K`), so `validate` rejects it before the call costs anything |
4508
4523
 
4509
- Reach for it for high-volume variants and any frame `1K` covers. Send hero stills and the `image_reference_sheet` subject sheet — anything that needs `2K`+ — to `google/gemini-3.1-flash-image-preview` or `google/gemini-3-pro-image-preview`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
4524
+ Reach for it for high-volume variants and any frame `1K` covers. Send hero stills and the `image_reference_sheet` subject sheet — anything that needs `2K`+ — to `google/gemini-3.1-flash-image` or `google/gemini-3-pro-image`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
4510
4525
 
4511
4526
  ```json
4512
4527
  { "id": "variant_a", "type": "image_generate",
4513
4528
  "params": { "model": "google/gemini-3.1-flash-lite-image", "prompt": "Portrait feed ad for a citrus energy drink …", "aspect_ratio": "4:5", "image_size": "1K" } }
4514
4529
  ```
4515
4530
 
4516
- ###### Model: `google/gemini-3-pro-image-preview`
4531
+ ###### Model: `google/gemini-3-pro-image` (legacy id `google/gemini-3-pro-image-preview` still accepted)
4517
4532
 
4518
4533
  "Nano Banana Pro" — heavier, higher-quality Gemini.
4519
4534
 
4520
4535
  | Name | Type | Required | Notes |
4521
4536
  |---|---|---|---|
4522
- | `model` | literal | yes | `"google/gemini-3-pro-image-preview"` |
4537
+ | `model` | literal | yes | `"google/gemini-3-pro-image"` |
4523
4538
  | `prompt` | string | yes | non-empty |
4524
4539
  | `aspect_ratio` | enum | no | STD AR |
4525
4540
  | `image_size` | enum | no | `1K \| 2K \| 4K` |
@@ -4563,7 +4578,7 @@ The sheet prompt is baked into the backend — there is no `prompt` param. `subj
4563
4578
 
4564
4579
  | Name | Type | Required | Notes |
4565
4580
  |---|---|---|---|
4566
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4581
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4567
4582
  | `subject_description` | string | yes | exact subject wording — reuse verbatim downstream |
4568
4583
  | `subject_type` | enum | yes | `character \| person \| product` — picks the template + default views |
4569
4584
  | `views` | string[] | no | 2–6 view labels; defaults per `subject_type` (below) |
@@ -4590,7 +4605,7 @@ Default views per `subject_type`:
4590
4605
 
4591
4606
  Guidance:
4592
4607
  - 3–4 full-body views at `16:9` is the sweet spot for characters; more views shrink each figure and lose face detail.
4593
- - Use `google/gemini-3.1-flash-image-preview` while iterating; switch to `google/gemini-3-pro-image-preview` at `2K`+ for final 6-view sheets.
4608
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` at `2K`+ for final 6-view sheets.
4594
4609
  - Feed the whole sheet downstream as one `reference` input — don't crop individual views.
4595
4610
 
4596
4611
  **Example** — two photos of a sneaker → product sheet → consistent hero shot:
@@ -4603,14 +4618,14 @@ Guidance:
4603
4618
  { "id": "b", "type": "ingest", "params": { "source": "url", "url": "https://example.com/sneaker-side.jpg", "expect": "image" } },
4604
4619
  { "id": "sheet", "type": "image_reference_sheet",
4605
4620
  "inputs": { "references": ["$ref:a.asset", "$ref:b.asset"] },
4606
- "params": { "model": "google/gemini-3-pro-image-preview",
4621
+ "params": { "model": "google/gemini-3-pro-image",
4607
4622
  "subject_description": "navy leather low-top sneaker with white sole and gold logo",
4608
4623
  "subject_type": "product",
4609
4624
  "style": "matte black studio background, soft even key light",
4610
4625
  "aspect_ratio": "16:9", "image_size": "2K" } },
4611
4626
  { "id": "hero", "type": "image_generate",
4612
4627
  "inputs": { "reference": "$ref:sheet.sheet" },
4613
- "params": { "model": "google/gemini-3-pro-image-preview",
4628
+ "params": { "model": "google/gemini-3-pro-image",
4614
4629
  "prompt": "The navy leather low-top sneaker with white sole and gold logo from the reference sheet, hero shot on a rain-soaked neon street at night" } }
4615
4630
  ],
4616
4631
  "output": { "node": "hero", "output": "images" }
@@ -4635,7 +4650,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4635
4650
 
4636
4651
  | Name | Type | Required | Notes |
4637
4652
  |---|---|---|---|
4638
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4653
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4639
4654
  | `formats` | enum[] | one of `formats`/`platform` | 1–6 unique target ratios from `1:1 \| 2:3 \| 3:2 \| 3:4 \| 4:3 \| 4:5 \| 5:4 \| 9:16 \| 16:9 \| 21:9` — output order follows this list |
4640
4655
  | `platform` | enum | one of `formats`/`platform` | a placement preset that expands to that platform's canonical on-grid set: `meta_feed` (4:5), `meta_feed_stories` (4:5, 9:16), `meta_all` (4:5, 9:16, 1:1), `linkedin` (1:1), `google_demandgen` (1:1, 4:5, 16:9), `tiktok` (9:16), `youtube` (16:9). Mutually exclusive with `formats`. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here — crop them from the nearest rendered ratio with `baker images crop`. |
4641
4656
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
@@ -4652,7 +4667,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4652
4667
  Guidance:
4653
4668
  - Reach for `platform` when you want a placement's whole set without naming ratios (e.g. `"platform": "meta_feed_stories"`); use `formats` when you need an exact, custom ratio list. Set one, never both.
4654
4669
  - List the source's own ratio in `formats` too when you want the full set in one output — it passes through for free.
4655
- - Use `google/gemini-3.1-flash-image-preview` while iterating; switch to `google/gemini-3-pro-image-preview` for final-quality adaptation.
4670
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` for final-quality adaptation.
4656
4671
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
4657
4672
 
4658
4673
  **Example** — one 9:16 story creative fanned out to feed and landscape:
@@ -4662,12 +4677,12 @@ Guidance:
4662
4677
  "schema": "baker-canvas/1",
4663
4678
  "nodes": [
4664
4679
  { "id": "hero", "type": "image_generate",
4665
- "params": { "model": "google/gemini-3-pro-image-preview",
4680
+ "params": { "model": "google/gemini-3-pro-image",
4666
4681
  "prompt": "Story-format ad for a citrus energy drink: can centered, bold OFFER text top, logo bottom",
4667
4682
  "aspect_ratio": "9:16", "image_size": "2K" } },
4668
4683
  { "id": "adapt", "type": "image_aspect_adapt",
4669
4684
  "inputs": { "source": "$ref:hero.images#0" },
4670
- "params": { "model": "google/gemini-3-pro-image-preview",
4685
+ "params": { "model": "google/gemini-3-pro-image",
4671
4686
  "formats": ["9:16", "1:1", "16:9"],
4672
4687
  "guidance": "keep the offer text and logo fully visible" } }
4673
4688
  ],
@@ -4802,9 +4817,9 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
4802
4817
  {
4803
4818
  "schema": "baker-canvas/1",
4804
4819
  "nodes": [
4805
- { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4806
- { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4807
- { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4820
+ { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4821
+ { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4822
+ { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4808
4823
  { "id": "best", "type": "image_select",
4809
4824
  "inputs": { "images": ["$ref:a.images#0", "$ref:b.images#0", "$ref:c.images#0"] },
4810
4825
  "params": { "model": "~google/gemini-pro-latest", "prompt": "most premium-feeling lighting, no harsh shadows" } }
@@ -5075,6 +5090,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
5075
5090
  | `language` | string | no | auto | ISO 639-1, 2–8 chars (e.g. `en`, `es`) |
5076
5091
  | `transcriber` | enum | no | `groq` | `groq` (Whisper) \| `deepgram` (Nova-3, adds the `rich` output) |
5077
5092
  | `keyterms` | string[] | no | — | Up to 30 names the recogniser must not mangle — a product, a person, a phrase the script coins. The **company's own name is always applied server-side**, so it never needs listing here |
5093
+ | `script` | string | no | — | The words the track was asked to say. Given, the `transcript` output is the script's words, spelled as written, at the times the recogniser heard them: aligned locally (misheard words, fillers, accents and numbers said as words are all tolerated), never sent to the recogniser. `scaffold-ad` sets it on every spoken ad |
5078
5094
 
5079
5095
  **Outputs**
5080
5096
 
@@ -5621,13 +5637,22 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5621
5637
  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.
5622
5638
  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.
5623
5639
 
5624
- 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 — left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **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 muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. 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).
5640
+ It then scaffolds a canvas that draws **only what `prompt.json` holds**; no element is mandatory. The report's `mode` names which of four the ad is:
5625
5641
 
5626
- **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).
5642
+ - **`composed`**: the image model (`openai/gpt-image-2.5-sunburst`, quality `xhigh`) paints up to **three candidate plates** (`--takes`, default 3; the picture with no overlay text and no logo, keeping calm the areas the copy will occupy), an `image_select` judge scores them on one dominant idea, thumbnail legibility, mechanism kept, space for what is drawn, brand with the logo covered, artifacts, Stories safe zones and text, and every delivered format is drawn by an HTML composition (`layout-4x5/`, `layout-9x16/`, one per format, snapshotted by `hyperframe_snapshot`). Each line sits at its layout region, carried through the format's crop or extension into its safe area (a band only when the layout gives it no position), and is set the way the reference set it: plain, underlined, followed by an arrow, a button (in the brand CTA colour), a sticker or a highlight. A call to action is never forced into a pill.
5643
+ - **`picture_only`**: the concept has no copy, so the plate is the ad and nothing is drawn on it.
5644
+ - **`text_only`**: no picture is generated. The copy is drawn on the brand's lead colour in its fonts, for near-zero credits. On by default when the reference pictures nothing but copy (no subjects, no people); `--text-only` forces it, `--no-text-only` refuses it.
5645
+ - **`baked`**: the copy is painted into the plates, only with `--skip-font` or when no font can load; every other shape is re-framed by `image_aspect_adapt` on `google/gemini-3-pro-image`, never cropped.
5627
5646
 
5628
- The checklist `note` reminds you to fill **logo** slots from the company's committed brand logo in `src/brand/logos/` (per `BRAND.md`) — **not** from `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark (a common cause of an old logo shipping on a live ad).
5647
+ **Fonts** come from the first source that has one: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading and body families `tokens.json` or `BRAND.md`'s typography section name, fetched as web font files from Google Fonts or Fontsource and ingested by URL, else Inter (800 and 400). **The logo is optional**: it is drawn from `src/brand/logos/` only when `prompt.json` keeps a logo (a `logo` layout region, or an `advertiser_brand` entry in `brands_logos`) and a real file exists. Meta shows the page name and picture on every ad, so no logo is never an error, never a slot to fill and never a stand-in; a canvas scaffolded with an unfilled logo slot runs without it. Text that belongs to something in the scene (a phone note, a screenshot, a sign, packaging: `surface: "in_scene"` in `text_content`) is painted by the plate, not overlaid, and listed in the report's `in_scene_text`. The plates read `prompt.plate.json` (the scene, the reserved areas, the visual mechanisms and the in-scene text; no overlay copy, no brand names) and the compositions read `prompt.compose.json`; both are re-derived from `prompt.json` by `validate` and `run`, so a copy edit re-draws the formats from the cached plates without paying for a picture again. A format within 10% of the plate's shape is a free crop; a farther one extends the chosen plate once on the same model. Detected elements get **one `[TODO]` ingest slot** each (never the logo), and the original image is wired in for composition only, left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **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. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. 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** carries `mode`, `summary` (what a run does: pictures, judge, fonts, logo), `fonts`, `logo`, `in_scene_text` and every real asset to drop in.
5629
5648
 
5630
- The generation prompt is **model-aware and typography-hardened** (a static ad is the only path that bakes text *into* the image — video frames are clean plates). It resolves the image model's profile, so for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition, and it quotes every blueprint string with an instruction to render each **once, verbatim**, lock brand names letter-by-letter, and keep type crisp and legible — the fix for the #1 static-ad defect (garbled/duplicated/dropped copy). Run `baker canvas critique` on the scaffolded canvas for an advisory read on text-legibility risk, identity grounding, and brand-type fidelity before you spend.
5649
+ **Placement formats.** The canvas delivers the platform's full placement set, each format at its exact size, collected under `final` with the format as its label. The 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 `--placements none` for a single format. The report's `aspect_ratio` is the ratio the plates really render at (the model's nearest to the lead format), and `format_plan` says how each format is made.
5650
+
5651
+ **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed.
5652
+
5653
+ The logo is never a slot to fill: use the company's committed logo in `src/brand/logos/` (per `BRAND.md`), never `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark.
5654
+
5655
+ The plate prompt is **model-aware**: for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition. In the baked-copy fallback it quotes every line of `prompt.json`'s copy (never the logo's text) with an instruction to render each **once, verbatim**; that block is re-read from `prompt.json` on every `validate` and `run`, which also upgrades canvases scaffolded before this change. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
5631
5656
 
5632
5657
  ```bash
5633
5658
  baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
@@ -5645,11 +5670,13 @@ baker canvas run ./static-ad.canvas.json
5645
5670
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5646
5671
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5647
5672
  | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
5648
- | `--gen-model <id>` | registry default (`openai/gpt-image-2`) | Override the `image_generate` model. |
5673
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5674
+ | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
5675
+ | `--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. |
5649
5676
  | `--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). |
5650
5677
  | `--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. |
5651
- | `--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). |
5652
- | `--skip-font` | off | Skip the brand-font → type-specimen slot. |
5678
+ | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
5679
+ | `--skip-font` | off | Bake the copy into the plates instead of drawing it in a real font (a font file, else the typeface the brand names, else Inter). |
5653
5680
  | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5654
5681
  | `--avatar <handles>` | — | Cast company avatars as the image's people, comma-separated in the order they were detected (`marta` or `marta,joan`). Each person slot grounds on that avatar's identity sheet and takes its subject description. Checked before any billed pass; an unknown, not-ready or sheetless avatar fails with code `avatar`. A handle with no person to play is reported in `hints` as `NOT CAST`. |
5655
5682
 
@@ -6147,23 +6174,88 @@ import {
6147
6174
 
6148
6175
  ## Creatives
6149
6176
 
6150
- Publish an approved canvas render as a first-class Baker creative. The image uploads to the Baker image library (tagged `creative`), a creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
6177
+ Publish an approved canvas render as a first-class Baker creative: a static image (PNG, JPG, WebP) or a finished video (MP4, MOV, WebM). An image uploads to the Baker image library (tagged `creative`); a video uploads to the Baker video library and is saved once it plays (bounded at 3 minutes). A creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
6151
6178
 
6152
6179
  ```bash
6153
6180
  baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
6154
6181
  --slug spring-offer-4x5 --run-id r_01JXYZ... \
6155
6182
  --source-reference-url "https://www.facebook.com/ads/library/?id=..."
6183
+ baker creatives publish ./canvas/<run_id>/final.mp4 --title "Spring Offer Reel" \
6184
+ --slug spring-offer-reel --run-id r_01JXYZ...
6156
6185
  ```
6157
6186
 
6158
6187
  | Flag | Effect |
6159
6188
  |---|---|
6160
6189
  | `--title <text>` | Required. Human title for the creative. |
6161
- | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
6190
+ | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. |
6162
6191
  | `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
6163
6192
  | `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
6164
- | `--context <text>` | Optional describe context for the uploaded image asset. |
6193
+ | `--context <text>` | Optional describe context for the uploaded image or video. |
6194
+
6195
+ An image prints `{ ok, data: { creativeId, imageId, reference } }`. A video prints `{ ok, data: { creativeId, videoId, reference }, hints }`, where `reference.asset.kind` is `video` (with `videoId`, `muxPlaybackId`, the poster as `thumbnailUrl`, size and duration) and `hints` names the next step to put it in a Meta ad: `baker ads meta media upload --kind video --video-id <videoId>`.
6196
+
6197
+ Without `--slug` an image behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
6198
+
6199
+ ### `baker creatives performance`
6200
+
6201
+ How each published creative is doing in the live Meta ads that show it. A creative is linked to an ad when a chat's publish uploads its image or video to Meta and creates an ad with it (or swaps an existing ad onto it); the results are read live from Meta.
6202
+
6203
+ ```bash
6204
+ baker creatives performance # every creative in an ad, last 30 days
6205
+ baker creatives performance --slug spring-offer # one creative
6206
+ baker creatives performance --days 14 --full # + ids, last-7-day figures, one row per ad
6207
+ ```
6208
+
6209
+ | Flag | Effect |
6210
+ |---|---|
6211
+ | `--slug <slug>` | Only this creative. |
6212
+ | `--days <n>` | Window ending today, 1-90 (default 30). |
6213
+ | `--full` | Adds creative, image and video ids, result type, days live, last-7-day CTR and cost per result, a video's watch funnel (`video`: 3-second views, ThruPlays, 25/50/75/100% watched), and one row per ad. |
6214
+
6215
+ Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, mechanism, dominantSlot, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. A video creative also reports `hookRate` (percent of impressions that watched 3 seconds) and `holdRate` (percent of those that reached a ThruPlay); a static one carries neither. `hints` name worn-out creatives, videos whose hook rate is under 25% (the first 3 seconds need new hooks) or whose hold rate is under 30% behind a working hook (the body loses people), the angle, mechanism and dominant slot with the lowest cost per result (each ranked once two or more values have results), and unreadable ad accounts.
6216
+
6217
+ ### `baker creatives feedback [--slug <slug>] [--full]`
6218
+
6219
+ What reviewers said about creative versions in Baker (Approve, or Not this one with reasons and a note; choosing a version to keep counts as an approval). Compact by default: approved and rejected counts, the top rejection reasons, what approved versions share (format, platform, option) and the 8 latest verdicts, plus `hints[]` saying what to do differently. `--slug` narrows to one creative; `--full` returns every verdict (up to 200) with whole notes.
6220
+
6221
+ ```bash
6222
+ baker creatives feedback
6223
+ baker creatives feedback --slug spring-offer --full
6224
+ ```
6225
+
6226
+ Each entry carries its `source`: `review` (the buttons in Baker), `selection` (choosing or saving the version) or `chat` (what the person wrote in a chat, recorded with `feedback record`).
6227
+
6228
+ ### `baker creatives feedback record`
6229
+
6230
+ Record what the person said about one version in a chat: an approval, or a rejection with reasons and their own words. It is stored like the Approve / Not this one buttons, as the verdict of the person who wrote the chat's latest message (`recordedFor: "sender"`), or of the chat itself when nobody has written in it (`"chat"`). One verdict per person per version or option: a later one replaces theirs. Must run from a chat.
6231
+
6232
+ ```bash
6233
+ baker creatives feedback record --slug spring-offer --run r_01K… --verdict rejected \
6234
+ --reasons text_mistakes,weak_hook --note "the price is 49 not 59"
6235
+ baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --verdict approved
6236
+ ```
6237
+
6238
+ | Flag | Effect |
6239
+ |---|---|
6240
+ | `--slug <slug>` | Required. The creative (`src/creatives/<slug>/`). |
6241
+ | `--run <r_…>` | Required. The version. |
6242
+ | `--verdict approved\|rejected` | Required. |
6243
+ | `--option <o>` | One option of a version that made several: its label, its number (`2`) or its slot. |
6244
+ | `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `voice_or_sound`, `pacing`, `other`. The two video reasons: `voice_or_sound` (the voiceover, music or mix) and `pacing` (too slow, too fast, the hook lands late). A rejection needs reasons or a note. |
6245
+ | `--note <text>` | The person's own words (stored up to 1000 characters). |
6246
+
6247
+ After a rejection, `hints[]` restates what to change, starting from that version.
6248
+
6249
+ ### `baker creatives select`
6250
+
6251
+ Choose the version of a creative to keep, or only some of its options, the way choosing it in Baker does: the creative shows that version as the chosen one, and the choice records the chat sender's approval (`source: "selection"`). Naming options replaces the earlier pick. It does not add the image to the library; that is `baker creatives publish`, which, given `--run-id`, also counts as choosing that version.
6252
+
6253
+ ```bash
6254
+ baker creatives select --slug spring-offer --run r_01K…
6255
+ baker creatives select --slug spring-offer --run r_01K… --options "beach,2"
6256
+ ```
6165
6257
 
6166
- Without `--slug` the command behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
6258
+ Prints `{ ok, data: { creative, runId, kept?, options, approvalRecorded }, hints }`, where `options` lists every option the version offers.
6167
6259
 
6168
6260
  ## Landing Quality (`baker landing`)
6169
6261