@koda-sl/baker-cli 0.394.0-dev.d28cef496 → 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
@@ -110,6 +110,12 @@ question — and nothing in the output said so. Both spellings of an option work
110
110
  that lists every option: `baker schema <command>` (`check_schema`) when the command
111
111
  registers a schema, `baker <command> --help` (`check_help`) when it does not.
112
112
 
113
+ ### Unknown commands are refused
114
+
115
+ A subcommand that does not exist (say, `scaffold` under `baker canvas`) stops with `UNKNOWN_COMMAND`
116
+ and exit code 1, naming the real commands closest to it and every command the parent
117
+ has, instead of printing the parent's help screen. Nothing runs.
118
+
113
119
  ### Narrowing a result: `--fields`
114
120
 
115
121
  `--fields a,b,c` works on every command that prints the JSON envelope. It keeps only
@@ -3830,7 +3836,7 @@ Starting a batch returns 202 + a `generationId`; the CLI polls until the takes s
3830
3836
  ```bash
3831
3837
  baker studio generate "a friendly golden retriever in a bright modern living room" --aspect-ratio 16:9
3832
3838
  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
3839
+ --model google/gemini-3-pro-image --image-size 2K --aspect-ratio 4:5 --count 3
3834
3840
  baker studio generate "home office hero, warm minimalist, natural light, 35mm photo" \
3835
3841
  --reference "https://i.pinimg.com/…/pin1.jpg,./src/brand/logos/product.png,j57abc123def456ghi789"
3836
3842
  baker studio generate "founder-style selfie, kitchen background" --skill ugc-selfie-hook
@@ -3838,13 +3844,13 @@ baker studio generate "flat geometric mascot, brand palette" \
3838
3844
  --model recraft/recraft-v4.1-pro-vector --rgb-colors "[[10,10,10],[255,80,0]]" --bg-rgb "[255,255,255]"
3839
3845
  ```
3840
3846
 
3841
- **Models** (`--model`, default `google/gemini-3.1-flash-image-preview`):
3847
+ **Models** (`--model`, default `google/gemini-3.1-flash-image`):
3842
3848
 
3843
3849
  | Model | Best for | Aspect ratios | Sizes |
3844
3850
  |---|---|---|---|
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` |
3851
+ | `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
3852
  | `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` |
3853
+ | `google/gemini-3-pro-image` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
3848
3854
  | `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
3855
  | `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
3856
  | `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 +3859,7 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
3853
3859
 
3854
3860
  `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
3861
 
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.
3862
+ `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
3863
 
3858
3864
  `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
3865
 
@@ -3863,7 +3869,7 @@ A knob the chosen model does not advertise is **refused, not silently dropped**
3863
3869
 
3864
3870
  | Flag | Description |
3865
3871
  |---|---|
3866
- | `--model` | Model id (default `google/gemini-3.1-flash-image-preview`) |
3872
+ | `--model` | Model id (default `google/gemini-3.1-flash-image`) |
3867
3873
  | `--aspect-ratio` | Output aspect ratio (default `1:1`) |
3868
3874
  | `--count` | Takes to render, 1–8 (default 1) |
3869
3875
  | `--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 +3977,7 @@ Compact output per take is `{ slot, status, imageId | videoId, url, inLibrary, e
3971
3977
  | `lifestyle-scene` | `openai/gpt-image-2` | `4:5` | 3 | **yes** |
3972
3978
  | `cinematic-still` | `google/gemini-3-pro-image-preview` | `16:9` | 3 | no |
3973
3979
  | `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 |
3980
+ | `background-plate` | `google/gemini-3.1-flash-image` | `4:5` | 3 | no |
3975
3981
 
3976
3982
  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
3983
 
@@ -4060,8 +4066,37 @@ baker canvas catalog --node video_generate
4060
4066
  # multi-line / accented copy; the edit is re-validated before it is written back)
4061
4067
  baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and breathless"
4062
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
4063
4077
  ```
4064
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
+
4065
4100
  #### The brand, the ending and the cast are drawn, not generated
4066
4101
 
4067
4102
  `baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
@@ -4163,7 +4198,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
4163
4198
  "type": "text_generate",
4164
4199
  "inputs": { "topic": "$ref:topic.text" },
4165
4200
  "params": {
4166
- "model": "google/gemini-3.1-flash-image-preview",
4201
+ "model": "google/gemini-3.1-flash-image",
4167
4202
  "prompt": "Write a 6-word tagline for {{topic}}."
4168
4203
  }
4169
4204
  }
@@ -4345,7 +4380,7 @@ Example pipeline:
4345
4380
  { "id": "font", "type": "ingest", "params": { "source": "url", "url": "https://cdn.example.com/brand.woff2", "expect": "font" } }
4346
4381
  { "id": "specimen", "type": "font_specimen", "inputs": { "font": "$ref:font.asset" }, "params": {} }
4347
4382
  { "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" } }
4383
+ "params": { "model": "google/gemini-3.1-flash-image", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4349
4384
  ```
4350
4385
 
4351
4386
  ---
@@ -4484,13 +4519,13 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4484
4519
 
4485
4520
  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
4521
 
4487
- ###### Model: `google/gemini-3.1-flash-image-preview`
4522
+ ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4488
4523
 
4489
4524
  Gemini flash image model — the default. Fast iteration, reference editing, extreme aspect ratios, and the `0.5K` size for cheap previews.
4490
4525
 
4491
4526
  | Name | Type | Required | Notes |
4492
4527
  |---|---|---|---|
4493
- | `model` | literal | yes | `"google/gemini-3.1-flash-image-preview"` |
4528
+ | `model` | literal | yes | `"google/gemini-3.1-flash-image"` |
4494
4529
  | `prompt` | string | yes | non-empty |
4495
4530
  | `aspect_ratio` | enum | no | EXTREME AR |
4496
4531
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
@@ -4506,20 +4541,20 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
4506
4541
  | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
4507
4542
  | `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
4543
 
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.
4544
+ 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
4545
 
4511
4546
  ```json
4512
4547
  { "id": "variant_a", "type": "image_generate",
4513
4548
  "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
4549
  ```
4515
4550
 
4516
- ###### Model: `google/gemini-3-pro-image-preview`
4551
+ ###### Model: `google/gemini-3-pro-image` (legacy id `google/gemini-3-pro-image-preview` still accepted)
4517
4552
 
4518
4553
  "Nano Banana Pro" — heavier, higher-quality Gemini.
4519
4554
 
4520
4555
  | Name | Type | Required | Notes |
4521
4556
  |---|---|---|---|
4522
- | `model` | literal | yes | `"google/gemini-3-pro-image-preview"` |
4557
+ | `model` | literal | yes | `"google/gemini-3-pro-image"` |
4523
4558
  | `prompt` | string | yes | non-empty |
4524
4559
  | `aspect_ratio` | enum | no | STD AR |
4525
4560
  | `image_size` | enum | no | `1K \| 2K \| 4K` |
@@ -4563,7 +4598,7 @@ The sheet prompt is baked into the backend — there is no `prompt` param. `subj
4563
4598
 
4564
4599
  | Name | Type | Required | Notes |
4565
4600
  |---|---|---|---|
4566
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4601
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4567
4602
  | `subject_description` | string | yes | exact subject wording — reuse verbatim downstream |
4568
4603
  | `subject_type` | enum | yes | `character \| person \| product` — picks the template + default views |
4569
4604
  | `views` | string[] | no | 2–6 view labels; defaults per `subject_type` (below) |
@@ -4590,7 +4625,7 @@ Default views per `subject_type`:
4590
4625
 
4591
4626
  Guidance:
4592
4627
  - 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.
4628
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` at `2K`+ for final 6-view sheets.
4594
4629
  - Feed the whole sheet downstream as one `reference` input — don't crop individual views.
4595
4630
 
4596
4631
  **Example** — two photos of a sneaker → product sheet → consistent hero shot:
@@ -4603,14 +4638,14 @@ Guidance:
4603
4638
  { "id": "b", "type": "ingest", "params": { "source": "url", "url": "https://example.com/sneaker-side.jpg", "expect": "image" } },
4604
4639
  { "id": "sheet", "type": "image_reference_sheet",
4605
4640
  "inputs": { "references": ["$ref:a.asset", "$ref:b.asset"] },
4606
- "params": { "model": "google/gemini-3-pro-image-preview",
4641
+ "params": { "model": "google/gemini-3-pro-image",
4607
4642
  "subject_description": "navy leather low-top sneaker with white sole and gold logo",
4608
4643
  "subject_type": "product",
4609
4644
  "style": "matte black studio background, soft even key light",
4610
4645
  "aspect_ratio": "16:9", "image_size": "2K" } },
4611
4646
  { "id": "hero", "type": "image_generate",
4612
4647
  "inputs": { "reference": "$ref:sheet.sheet" },
4613
- "params": { "model": "google/gemini-3-pro-image-preview",
4648
+ "params": { "model": "google/gemini-3-pro-image",
4614
4649
  "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
4650
  ],
4616
4651
  "output": { "node": "hero", "output": "images" }
@@ -4635,7 +4670,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4635
4670
 
4636
4671
  | Name | Type | Required | Notes |
4637
4672
  |---|---|---|---|
4638
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4673
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4639
4674
  | `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
4675
  | `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
4676
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
@@ -4652,7 +4687,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4652
4687
  Guidance:
4653
4688
  - 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
4689
  - 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.
4690
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` for final-quality adaptation.
4656
4691
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
4657
4692
 
4658
4693
  **Example** — one 9:16 story creative fanned out to feed and landscape:
@@ -4662,12 +4697,12 @@ Guidance:
4662
4697
  "schema": "baker-canvas/1",
4663
4698
  "nodes": [
4664
4699
  { "id": "hero", "type": "image_generate",
4665
- "params": { "model": "google/gemini-3-pro-image-preview",
4700
+ "params": { "model": "google/gemini-3-pro-image",
4666
4701
  "prompt": "Story-format ad for a citrus energy drink: can centered, bold OFFER text top, logo bottom",
4667
4702
  "aspect_ratio": "9:16", "image_size": "2K" } },
4668
4703
  { "id": "adapt", "type": "image_aspect_adapt",
4669
4704
  "inputs": { "source": "$ref:hero.images#0" },
4670
- "params": { "model": "google/gemini-3-pro-image-preview",
4705
+ "params": { "model": "google/gemini-3-pro-image",
4671
4706
  "formats": ["9:16", "1:1", "16:9"],
4672
4707
  "guidance": "keep the offer text and logo fully visible" } }
4673
4708
  ],
@@ -4802,9 +4837,9 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
4802
4837
  {
4803
4838
  "schema": "baker-canvas/1",
4804
4839
  "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" } },
4840
+ { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4841
+ { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4842
+ { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4808
4843
  { "id": "best", "type": "image_select",
4809
4844
  "inputs": { "images": ["$ref:a.images#0", "$ref:b.images#0", "$ref:c.images#0"] },
4810
4845
  "params": { "model": "~google/gemini-pro-latest", "prompt": "most premium-feeling lighting, no harsh shadows" } }
@@ -5432,9 +5467,9 @@ Cache policies (via `--cache-policy`):
5432
5467
 
5433
5468
  ### CLI commands
5434
5469
 
5435
- #### `baker canvas validate <file.json>`
5470
+ #### `baker canvas validate <slug|file.json>`
5436
5471
 
5437
- Parse + schema-check + per-node deep validation (composition `meta.json` is consulted for `hyperframe_*`). Prints a structured result with a per-node cost preview. Exits non-zero on issues. Never calls Convex, never spends credits.
5472
+ Takes a creative's slug (`baker canvas validate <slug>` reads `src/creatives/<slug>/<slug>.canvas.json`) or a canvas path; a slug that names no creative fails with `not_found` and lists the creatives there are. Parse + schema-check + per-node deep validation (composition `meta.json` is consulted for `hyperframe_*`). Prints a structured result with a per-node cost preview. Exits non-zero on issues. Never calls Convex, never spends credits.
5438
5473
 
5439
5474
  ```bash
5440
5475
  $ baker canvas validate my-canvas.json
@@ -5474,9 +5509,9 @@ names no camera/motion move **and** no exclusions — the six-decision doctrine)
5474
5509
  (the hook, scene 0, is thin across two or more of the four hook layers — text/sound/visual/vibe). Warnings never
5475
5510
  block a run.
5476
5511
 
5477
- #### `baker canvas critique <file.json>`
5512
+ #### `baker canvas critique <slug|file.json>`
5478
5513
 
5479
- Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded creative 0–1 before a billed `run`,
5514
+ Takes a creative's slug or a canvas path, like `validate`. Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded creative 0–1 before a billed `run`,
5480
5515
  so a weak spot gets fixed for free. It branches on modality: a **video** creative is scored on **hook strength**,
5481
5516
  **sound-off first-frame legibility**, **retention risk**, and **mechanism clarity**; a **static ad** on **baked-text
5482
5517
  legibility risk**, **identity grounding** (Rule 1 — every subject on a real reference), **brand-type fidelity** (a
@@ -5493,9 +5528,9 @@ $ baker canvas critique my-static-ad.json
5493
5528
  "dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
5494
5529
  ```
5495
5530
 
5496
- #### `baker canvas run <file.json> [flags]`
5531
+ #### `baker canvas run <slug|file.json> [flags]`
5497
5532
 
5498
- Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
5533
+ Takes a creative's slug (`baker canvas run spring-offer`) or a canvas path, resolved like `validate`. Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
5499
5534
 
5500
5535
  | Flag | Default | Effect |
5501
5536
  |---|---|---|
@@ -5507,6 +5542,9 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
5507
5542
  | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
5508
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. |
5509
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.
5510
5548
 
5511
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.
5512
5550
 
@@ -5613,7 +5651,9 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
5613
5651
 
5614
5652
  #### `baker canvas scaffold-static-ad <image> [flags]`
5615
5653
 
5616
- `<image>` is a **local path or an `http(s)` URL** (e.g. a presigned S3 link from ad-dna). A URL is passed straight into the `original` ingest node as `source: "url"` — do not `curl` it to a file first. Note a presigned URL embedded in the emitted canvas will **expire** (the `original` node is composition-only, so a later re-run just skips it); pass a stable URL or a local file if you need the canvas to stay runnable. For the URL case, `--out` defaults to the current directory — pass `--out` to scaffold two URL sources into distinct files, or the second run overwrites the first.
5654
+ `<image>` is a **local path or an `http(s)` URL** (e.g. a presigned S3 link from ad-dna). A URL is passed straight in; do not `curl` it to a file first. The source is uploaded to durable storage before any billed pass, so the canvas keeps running after a presigned URL expires.
5655
+
5656
+ **Every scaffold is a creative in `src/creatives/<slug>/`** (`<slug>.canvas.json`, `prompt.json`, `_definition.md`), never a loose file in the workspace. Without `--slug` a slug is derived from `--title`, else the reference's file name, else `static-ad`, suffixed `-2`, `-3`… past any folder that already exists, and a hint names it. Every flag that shapes the bill (`--platform`, `--placements`, `--aspect`, `--takes`, `--avatar`) is checked before the first billed pass; an unknown value fails with `invalid_platform`, `invalid_placements`, `invalid_aspect`, `invalid_takes` or `avatar` and costs nothing. The output ends with `next_steps`: the one file to edit (`prompt.json`) and the exact command to run (`baker canvas run <slug>`).
5617
5657
 
5618
5658
  Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. Like the video scaffold, this runs **billed Gemini passes** up front:
5619
5659
 
@@ -5621,35 +5661,46 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5621
5661
  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
5662
  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
5663
 
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).
5664
+ 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:
5665
+
5666
+ - **`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.
5667
+ - **`picture_only`**: the concept has no copy, so the plate is the ad and nothing is drawn on it.
5668
+ - **`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.
5669
+ - **`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.
5670
+
5671
+ **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, slug, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist, next_steps }`; 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.
5672
+
5673
+ **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.
5625
5674
 
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).
5675
+ **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.
5627
5676
 
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).
5677
+ 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.
5629
5678
 
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.
5679
+ 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
5680
 
5632
5681
  ```bash
5633
- baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
5634
- # → writes ./static-ad.canvas.json + ./prompt.json
5682
+ baker canvas scaffold-static-ad ./reference-ad.png --slug spring-offer --context "competitor ad for <brand>, <category>, <market>"
5683
+ # → writes src/creatives/spring-offer/{spring-offer.canvas.json,prompt.json,_definition.md}
5635
5684
  # edit prompt.json into your ad + replace each [TODO] ingest path with a real file, then:
5636
- baker canvas validate ./static-ad.canvas.json
5637
- baker canvas run ./static-ad.canvas.json
5685
+ baker canvas validate spring-offer
5686
+ baker canvas run spring-offer
5638
5687
  ```
5639
5688
 
5640
5689
  | Flag | Default | Effect |
5641
5690
  |---|---|---|
5642
5691
  | `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
5643
- | `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
5644
- | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is **downloaded into `src/creatives/<slug>/references/` and normalized to a model-safe format** (SVG/AVIF/HEIC/… → PNG), named from the actual bytes (not the URL string) so a presigned/extensionless URL never lands as PNG-bytes-in-`.jpg` — the canvas ingests that committed, portable path instead of the expiring URL. |
5692
+ | `--slug <slug>` | derived from `--title` or the reference | Creative slug (lowercase kebab): the creative's folder `src/creatives/<slug>/`, which attaches every run to its generation history. |
5693
+ | `--title <text>` | title-cased slug | Creative title for `_definition.md`; also the source of a derived slug. |
5645
5694
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5646
5695
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5647
5696
  | `--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. |
5649
- | `--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). |
5697
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5698
+ | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
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. |
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. |
5650
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. |
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. |
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`. |
5703
+ | `--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
5704
  | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5654
5705
  | `--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
5706
 
@@ -5675,7 +5726,7 @@ baker canvas rerun spring-offer --force-remote --regenerate gen
5675
5726
 
5676
5727
  Use it when a creative was built in another conversation (or its sandbox is gone) and you need to continue or re-render it here. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings — supply those locally only if the run actually needs to regenerate the nodes that read them.
5677
5728
 
5678
- #### `baker canvas inspect <run_id> [--thumbnails]`
5729
+ #### `baker canvas inspect <run_id|slug> [--thumbnails]`
5679
5730
 
5680
5731
  One-page summary of a completed run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg).
5681
5732
 
@@ -5684,7 +5735,7 @@ baker canvas inspect r_01JXYZ...
5684
5735
  baker canvas inspect r_01JXYZ... --thumbnails
5685
5736
  ```
5686
5737
 
5687
- Resolves the run dir against `--outputs-dir` (default `./canvas`) or accepts an absolute path. Reads `manifest.json` for per-node stats.
5738
+ Resolves the run dir against `--outputs-dir` (default `./canvas`) or accepts an absolute path. A creative's slug (`baker canvas inspect spring-offer`) opens that creative's latest run. Reads `manifest.json` for per-node stats.
5688
5739
 
5689
5740
  #### `baker canvas catalog`
5690
5741