@koda-sl/baker-cli 0.394.0-dev.d28cef496 → 0.396.0-dev.88120240

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 +95 -31
  2. package/canvas/static-ad-composition/index.html +197 -0
  3. package/canvas/static-ad-composition/meta.json +53 -0
  4. package/dist/{chunk-AOVFDDVW.js → chunk-7OVZ6SOA.js} +3 -2
  5. package/dist/{chunk-AOVFDDVW.js.map → chunk-7OVZ6SOA.js.map} +1 -1
  6. package/dist/{chunk-NEZ3NAJV.js → chunk-EYN7G4KF.js} +4 -4
  7. package/dist/{chunk-T2UNK5BW.js → chunk-J76AGFBH.js} +3 -3
  8. package/dist/{chunk-FEJQJ64L.js → chunk-MNSF5SQ3.js} +131 -22
  9. package/dist/chunk-MNSF5SQ3.js.map +1 -0
  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 +6828 -5268
  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-FEJQJ64L.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
 
@@ -4163,7 +4163,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
4163
4163
  "type": "text_generate",
4164
4164
  "inputs": { "topic": "$ref:topic.text" },
4165
4165
  "params": {
4166
- "model": "google/gemini-3.1-flash-image-preview",
4166
+ "model": "google/gemini-3.1-flash-image",
4167
4167
  "prompt": "Write a 6-word tagline for {{topic}}."
4168
4168
  }
4169
4169
  }
@@ -4345,7 +4345,7 @@ Example pipeline:
4345
4345
  { "id": "font", "type": "ingest", "params": { "source": "url", "url": "https://cdn.example.com/brand.woff2", "expect": "font" } }
4346
4346
  { "id": "specimen", "type": "font_specimen", "inputs": { "font": "$ref:font.asset" }, "params": {} }
4347
4347
  { "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" } }
4348
+ "params": { "model": "google/gemini-3.1-flash-image", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4349
4349
  ```
4350
4350
 
4351
4351
  ---
@@ -4484,13 +4484,13 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4484
4484
 
4485
4485
  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
4486
 
4487
- ###### Model: `google/gemini-3.1-flash-image-preview`
4487
+ ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4488
4488
 
4489
4489
  Gemini flash image model — the default. Fast iteration, reference editing, extreme aspect ratios, and the `0.5K` size for cheap previews.
4490
4490
 
4491
4491
  | Name | Type | Required | Notes |
4492
4492
  |---|---|---|---|
4493
- | `model` | literal | yes | `"google/gemini-3.1-flash-image-preview"` |
4493
+ | `model` | literal | yes | `"google/gemini-3.1-flash-image"` |
4494
4494
  | `prompt` | string | yes | non-empty |
4495
4495
  | `aspect_ratio` | enum | no | EXTREME AR |
4496
4496
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
@@ -4506,20 +4506,20 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
4506
4506
  | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
4507
4507
  | `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
4508
 
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.
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` or `google/gemini-3-pro-image`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
4510
4510
 
4511
4511
  ```json
4512
4512
  { "id": "variant_a", "type": "image_generate",
4513
4513
  "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
4514
  ```
4515
4515
 
4516
- ###### Model: `google/gemini-3-pro-image-preview`
4516
+ ###### Model: `google/gemini-3-pro-image` (legacy id `google/gemini-3-pro-image-preview` still accepted)
4517
4517
 
4518
4518
  "Nano Banana Pro" — heavier, higher-quality Gemini.
4519
4519
 
4520
4520
  | Name | Type | Required | Notes |
4521
4521
  |---|---|---|---|
4522
- | `model` | literal | yes | `"google/gemini-3-pro-image-preview"` |
4522
+ | `model` | literal | yes | `"google/gemini-3-pro-image"` |
4523
4523
  | `prompt` | string | yes | non-empty |
4524
4524
  | `aspect_ratio` | enum | no | STD AR |
4525
4525
  | `image_size` | enum | no | `1K \| 2K \| 4K` |
@@ -4563,7 +4563,7 @@ The sheet prompt is baked into the backend — there is no `prompt` param. `subj
4563
4563
 
4564
4564
  | Name | Type | Required | Notes |
4565
4565
  |---|---|---|---|
4566
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4566
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4567
4567
  | `subject_description` | string | yes | exact subject wording — reuse verbatim downstream |
4568
4568
  | `subject_type` | enum | yes | `character \| person \| product` — picks the template + default views |
4569
4569
  | `views` | string[] | no | 2–6 view labels; defaults per `subject_type` (below) |
@@ -4590,7 +4590,7 @@ Default views per `subject_type`:
4590
4590
 
4591
4591
  Guidance:
4592
4592
  - 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.
4593
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` at `2K`+ for final 6-view sheets.
4594
4594
  - Feed the whole sheet downstream as one `reference` input — don't crop individual views.
4595
4595
 
4596
4596
  **Example** — two photos of a sneaker → product sheet → consistent hero shot:
@@ -4603,14 +4603,14 @@ Guidance:
4603
4603
  { "id": "b", "type": "ingest", "params": { "source": "url", "url": "https://example.com/sneaker-side.jpg", "expect": "image" } },
4604
4604
  { "id": "sheet", "type": "image_reference_sheet",
4605
4605
  "inputs": { "references": ["$ref:a.asset", "$ref:b.asset"] },
4606
- "params": { "model": "google/gemini-3-pro-image-preview",
4606
+ "params": { "model": "google/gemini-3-pro-image",
4607
4607
  "subject_description": "navy leather low-top sneaker with white sole and gold logo",
4608
4608
  "subject_type": "product",
4609
4609
  "style": "matte black studio background, soft even key light",
4610
4610
  "aspect_ratio": "16:9", "image_size": "2K" } },
4611
4611
  { "id": "hero", "type": "image_generate",
4612
4612
  "inputs": { "reference": "$ref:sheet.sheet" },
4613
- "params": { "model": "google/gemini-3-pro-image-preview",
4613
+ "params": { "model": "google/gemini-3-pro-image",
4614
4614
  "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
4615
  ],
4616
4616
  "output": { "node": "hero", "output": "images" }
@@ -4635,7 +4635,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4635
4635
 
4636
4636
  | Name | Type | Required | Notes |
4637
4637
  |---|---|---|---|
4638
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4638
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4639
4639
  | `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
4640
  | `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
4641
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
@@ -4652,7 +4652,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4652
4652
  Guidance:
4653
4653
  - 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
4654
  - 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.
4655
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` for final-quality adaptation.
4656
4656
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
4657
4657
 
4658
4658
  **Example** — one 9:16 story creative fanned out to feed and landscape:
@@ -4662,12 +4662,12 @@ Guidance:
4662
4662
  "schema": "baker-canvas/1",
4663
4663
  "nodes": [
4664
4664
  { "id": "hero", "type": "image_generate",
4665
- "params": { "model": "google/gemini-3-pro-image-preview",
4665
+ "params": { "model": "google/gemini-3-pro-image",
4666
4666
  "prompt": "Story-format ad for a citrus energy drink: can centered, bold OFFER text top, logo bottom",
4667
4667
  "aspect_ratio": "9:16", "image_size": "2K" } },
4668
4668
  { "id": "adapt", "type": "image_aspect_adapt",
4669
4669
  "inputs": { "source": "$ref:hero.images#0" },
4670
- "params": { "model": "google/gemini-3-pro-image-preview",
4670
+ "params": { "model": "google/gemini-3-pro-image",
4671
4671
  "formats": ["9:16", "1:1", "16:9"],
4672
4672
  "guidance": "keep the offer text and logo fully visible" } }
4673
4673
  ],
@@ -4802,9 +4802,9 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
4802
4802
  {
4803
4803
  "schema": "baker-canvas/1",
4804
4804
  "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" } },
4805
+ { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4806
+ { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4807
+ { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4808
4808
  { "id": "best", "type": "image_select",
4809
4809
  "inputs": { "images": ["$ref:a.images#0", "$ref:b.images#0", "$ref:c.images#0"] },
4810
4810
  "params": { "model": "~google/gemini-pro-latest", "prompt": "most premium-feeling lighting, no harsh shadows" } }
@@ -5621,13 +5621,15 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5621
5621
  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
5622
  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
5623
 
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).
5624
+ It then scaffolds a canvas that renders the ad in two layers. The image model (`openai/gpt-image-2.5-sunburst`, quality `xhigh`) paints **three candidate plates** (the picture with no text and no logo, keeping calm the areas the copy will occupy), an `image_select` judge scores them on a fixed rubric (mechanism kept, space for the offer, brand, artifacts, Stories safe zones, text) and picks one, and every delivered format is drawn by an HTML composition (`layout-4x5/`, `layout-9x16/`, one per format, snapshotted by `hyperframe_snapshot`): the copy from `prompt.json` in the brand's heading and body fonts from `src/brand/fonts/` (via `src/brand/tokens.json` when present), the call to action as a button in the brand CTA colour, and the real logo file from `src/brand/logos/`. The plates read `prompt.plate.json` (the scene, the reserved areas and the visual mechanisms; no 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. With no font file in `src/brand/fonts/` (or `--skip-font`) the copy is baked into the plates instead, and every other shape is re-framed by `image_aspect_adapt` on `google/gemini-3-pro-image`, never cropped. Detected elements get **one `[TODO]` ingest slot** each (the logo slot is pre-filled with the brand logo and feeds the composition, never the picture model), 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 — 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).
5625
5625
 
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).
5626
+ **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.
5627
+
5628
+ **After a run** the CLI reads the finished ad back: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy and checks the logo, clipping, legibility and artifacts. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`) and `judge` (the per-plate scores); the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed.
5627
5629
 
5628
5630
  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).
5629
5631
 
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.
5632
+ 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
5633
 
5632
5634
  ```bash
5633
5635
  baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
@@ -5645,11 +5647,12 @@ baker canvas run ./static-ad.canvas.json
5645
5647
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5646
5648
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5647
5649
  | `--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. |
5650
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5651
+ | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. |
5649
5652
  | `--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
5653
  | `--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. |
5654
+ | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
5655
+ | `--skip-font` | off | Bake the copy into the plates even though a brand font exists. |
5653
5656
  | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5654
5657
  | `--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
5658
 
@@ -6165,6 +6168,67 @@ baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5"
6165
6168
 
6166
6169
  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.
6167
6170
 
6171
+ ### `baker creatives performance`
6172
+
6173
+ 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 to Meta and creates an ad with it (or swaps an existing ad onto it); the results are read live from Meta.
6174
+
6175
+ ```bash
6176
+ baker creatives performance # every creative in an ad, last 30 days
6177
+ baker creatives performance --slug spring-offer # one creative
6178
+ baker creatives performance --days 14 --full # + ids, last-7-day figures, one row per ad
6179
+ ```
6180
+
6181
+ | Flag | Effect |
6182
+ |---|---|
6183
+ | `--slug <slug>` | Only this creative. |
6184
+ | `--days <n>` | Window ending today, 1-90 (default 30). |
6185
+ | `--full` | Adds creative and image ids, result type, days live, last-7-day CTR and cost per result, and one row per ad. |
6186
+
6187
+ 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, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. `hints` name worn-out creatives, the angle with the lowest cost per result, and unreadable ad accounts.
6188
+
6189
+ ### `baker creatives feedback [--slug <slug>] [--full]`
6190
+
6191
+ 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.
6192
+
6193
+ ```bash
6194
+ baker creatives feedback
6195
+ baker creatives feedback --slug spring-offer --full
6196
+ ```
6197
+
6198
+ 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`).
6199
+
6200
+ ### `baker creatives feedback record`
6201
+
6202
+ 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.
6203
+
6204
+ ```bash
6205
+ baker creatives feedback record --slug spring-offer --run r_01K… --verdict rejected \
6206
+ --reasons text_mistakes,weak_hook --note "the price is 49 not 59"
6207
+ baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --verdict approved
6208
+ ```
6209
+
6210
+ | Flag | Effect |
6211
+ |---|---|
6212
+ | `--slug <slug>` | Required. The creative (`src/creatives/<slug>/`). |
6213
+ | `--run <r_…>` | Required. The version. |
6214
+ | `--verdict approved\|rejected` | Required. |
6215
+ | `--option <o>` | One option of a version that made several: its label, its number (`2`) or its slot. |
6216
+ | `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `other`. A rejection needs reasons or a note. |
6217
+ | `--note <text>` | The person's own words (stored up to 1000 characters). |
6218
+
6219
+ After a rejection, `hints[]` restates what to change, starting from that version.
6220
+
6221
+ ### `baker creatives select`
6222
+
6223
+ 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.
6224
+
6225
+ ```bash
6226
+ baker creatives select --slug spring-offer --run r_01K…
6227
+ baker creatives select --slug spring-offer --run r_01K… --options "beach,2"
6228
+ ```
6229
+
6230
+ Prints `{ ok, data: { creative, runId, kept?, options, approvalRecorded }, hints }`, where `options` lists every option the version offers.
6231
+
6168
6232
  ## Landing Quality (`baker landing`)
6169
6233
 
6170
6234
  Two tools that bracket a landing build: **`inspiration`** before you design (what does good look like here?) and **`critique`** after you build (did this settle into generic defaults?).
@@ -0,0 +1,197 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=__WIDTH__, height=__HEIGHT__" />
6
+ <script src="./gsap.min.js"></script>
7
+ <style>
8
+ /* The brand's own faces, staged beside this file by the render. One range per face,
9
+ so a single-weight file is used as it is instead of being faked bolder. */
10
+ @font-face { font-family: 'AdHeadline'; src: url('./headline.font'); font-weight: 100 900; font-display: block; }
11
+ @font-face { font-family: 'AdBody'; src: url('./body.font'); font-weight: 100 900; font-display: block; }
12
+ * { margin: 0; padding: 0; box-sizing: border-box; }
13
+ html, body { width: __WIDTH__px; height: __HEIGHT__px; overflow: hidden; background: #000; }
14
+ #root { position: absolute; inset: 0; overflow: hidden; }
15
+ .plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
16
+ .scrim { position: absolute; left: 0; right: 0; pointer-events: none; display: none; }
17
+ .band { position: absolute; display: flex; flex-direction: column; gap: 0; }
18
+ .block { font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; overflow-wrap: break-word; text-wrap: pretty; }
19
+ .block.headline, .block.subheadline, .block.cta { font-family: 'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
20
+ .block.headline { text-wrap: balance; letter-spacing: -0.01em; }
21
+ .cta-pill { display: inline-block; border-radius: 999px; font-weight: 700; line-height: 1.1; }
22
+ .logo { display: block; width: auto; object-fit: contain; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="__WIDTH__" data-height="__HEIGHT__">
27
+ <img class="plate" id="plate" src="./plate.png" alt="" />
28
+ <div class="scrim" id="scrim-top"></div>
29
+ <div class="scrim" id="scrim-bottom"></div>
30
+ <div class="band" id="band-top" data-role="copy-top"></div>
31
+ <div class="band" id="band-middle" data-role="copy-middle"></div>
32
+ <div class="band" id="band-bottom" data-role="copy-bottom"></div>
33
+ </div>
34
+
35
+ <script>
36
+ (function () {
37
+ var AD = {{ad}};
38
+ var FRAME = {{frame}};
39
+ var W = __WIDTH__;
40
+ var H = __HEIGHT__;
41
+ var root = document.getElementById("root");
42
+ var safe = FRAME.safe || { top: 0.05, bottom: 0.05, left: 0.06, right: 0.06 };
43
+ var box = { x: safe.left * W, y: safe.top * H, w: W * (1 - safe.left - safe.right), h: H * (1 - safe.top - safe.bottom) };
44
+ // One unit per percent of the shorter side, so type reads the same size on every format.
45
+ var U = Math.min(W, H) / 100;
46
+
47
+ var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
48
+ var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
49
+ var CASE = { upper: "uppercase", title: "capitalize", sentence: "none" };
50
+ var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
51
+
52
+ function luma(hex) {
53
+ var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
54
+ if (!m) return null;
55
+ var v = parseInt(m[1], 16);
56
+ return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
57
+ }
58
+
59
+ var bands = { top: [], middle: [], bottom: [] };
60
+ var drawCopy = FRAME.draw_copy !== false;
61
+ (drawCopy ? AD.copy || [] : []).forEach(function (b) {
62
+ (bands[b.band] || bands.top).push(b);
63
+ });
64
+
65
+ function bandEl(name) {
66
+ return document.getElementById("band-" + name);
67
+ }
68
+
69
+ function place(name) {
70
+ var el = bandEl(name);
71
+ el.style.left = box.x + "px";
72
+ el.style.width = box.w + "px";
73
+ el.style.gap = 1.6 * U + "px";
74
+ if (name === "top") el.style.top = box.y + "px";
75
+ if (name === "bottom") el.style.bottom = H - (box.y + box.h) + "px";
76
+ if (name === "middle") {
77
+ el.style.top = box.y + box.h / 2 + "px";
78
+ el.style.transform = "translateY(-50%)";
79
+ }
80
+ }
81
+
82
+ function alignSelf(align) {
83
+ return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
84
+ }
85
+
86
+ function addLogo(name, align) {
87
+ var img = document.createElement("img");
88
+ img.className = "logo";
89
+ img.src = "./logo.png";
90
+ img.alt = "";
91
+ img.style.height = 5.4 * U + "px";
92
+ img.style.maxWidth = box.w * 0.42 + "px";
93
+ img.style.alignSelf = alignSelf(align);
94
+ img.onerror = function () {
95
+ img.remove();
96
+ };
97
+ var el = bandEl(name);
98
+ if (name === "bottom") el.appendChild(img);
99
+ else el.insertBefore(img, el.firstChild);
100
+ }
101
+
102
+ function addBlock(name, b) {
103
+ var el = document.createElement("div");
104
+ el.className = "block " + b.role;
105
+ el.dataset.size = String(SIZE[b.size] || SIZE.medium);
106
+ var size = b.role === "fine_print" ? 2.4 : b.role === "cta" ? 4.2 : SIZE[b.size] || SIZE.medium;
107
+ el.dataset.base = String(size);
108
+ el.style.fontSize = size * U + "px";
109
+ el.style.fontWeight = String(b.role === "cta" ? 700 : WEIGHT[b.weight] || 400);
110
+ el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
111
+ el.style.textTransform = CASE[b.case] || "none";
112
+ el.style.textAlign = b.align || "center";
113
+ el.style.alignSelf = alignSelf(b.align);
114
+ if (b.role === "cta") {
115
+ var pill = document.createElement("span");
116
+ pill.className = "cta-pill";
117
+ pill.textContent = b.text;
118
+ pill.style.background = AD.cta.background;
119
+ pill.style.color = AD.cta.color;
120
+ pill.style.padding = "0.55em 1.4em";
121
+ el.appendChild(pill);
122
+ } else {
123
+ el.textContent = b.text;
124
+ var ink = b.color || "#ffffff";
125
+ el.style.color = ink;
126
+ var dark = (luma(ink) || 255) < 110;
127
+ el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
128
+ el.dataset.dark = dark ? "1" : "0";
129
+ }
130
+ bandEl(name).appendChild(el);
131
+ }
132
+
133
+ ["top", "middle", "bottom"].forEach(function (name) {
134
+ place(name);
135
+ bands[name].forEach(function (b) {
136
+ addBlock(name, b);
137
+ });
138
+ });
139
+ if (AD.logo) addLogo(AD.logo.band, AD.logo.align);
140
+
141
+ // Shrink a band's type until it fits its share of the safe area and no line runs
142
+ // past the edge. The copy is whatever prompt.json says today, so its length is
143
+ // not known when the layout is written.
144
+ function fit(name) {
145
+ var el = bandEl(name);
146
+ var max = box.h * MAX_SHARE[name];
147
+ var scale = 1;
148
+ for (var i = 0; i < 40; i++) {
149
+ var tooTall = el.scrollHeight > max;
150
+ var tooWide = Array.prototype.some.call(el.children, function (c) {
151
+ return c.scrollWidth > box.w + 1;
152
+ });
153
+ if (!tooTall && !tooWide) break;
154
+ scale *= 0.94;
155
+ Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
156
+ c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
157
+ });
158
+ }
159
+ }
160
+
161
+ // A soft scrim behind a band of light type (or a light one behind dark type), so the
162
+ // copy reads on any plate without boxing it in.
163
+ function scrim(name) {
164
+ if (name === "middle" || bands[name].length === 0) return;
165
+ var el = bandEl(name);
166
+ var s = document.getElementById("scrim-" + name);
167
+ var firstText = el.querySelector(".block:not(.cta)");
168
+ var dark = firstText && firstText.dataset.dark === "1";
169
+ var tint = dark ? "255,255,255" : "0,0,0";
170
+ var rect = el.getBoundingClientRect();
171
+ var reach = name === "top" ? rect.bottom + 6 * U : H - rect.top + 6 * U;
172
+ s.style.display = "block";
173
+ s.style.height = Math.min(H * 0.6, reach) + "px";
174
+ if (name === "top") {
175
+ s.style.top = "0";
176
+ s.style.background = "linear-gradient(to bottom, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
177
+ } else {
178
+ s.style.bottom = "0";
179
+ s.style.background = "linear-gradient(to top, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
180
+ }
181
+ }
182
+
183
+ var tl = gsap.timeline({ paused: true });
184
+ tl.to({}, { duration: 1 });
185
+ window.__timelines = window.__timelines || {};
186
+ window.__timelines.main = tl;
187
+
188
+ var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
189
+ ready.then(function () {
190
+ ["top", "middle", "bottom"].forEach(fit);
191
+ ["top", "bottom"].forEach(scrim);
192
+ root.setAttribute("data-ready", "1");
193
+ });
194
+ })();
195
+ </script>
196
+ </body>
197
+ </html>
@@ -0,0 +1,53 @@
1
+ {
2
+ "id": "static-ad",
3
+ "title": "Static ad: a rendered plate with the copy, the call to action and the logo drawn on top",
4
+ "description": "Written by `baker canvas scaffold-static-ad`, one copy per delivered format. The plate is the image model's clean picture; the copy, the CTA and the logo are drawn here from prompt.compose.json (derived from prompt.json) in the brand's own fonts and logo file. Width and height are this format's delivered size.",
5
+ "width": 1080,
6
+ "height": 1350,
7
+ "fps": 1,
8
+ "default_duration": 1,
9
+ "inputs": {
10
+ "plate": {
11
+ "kind": "image",
12
+ "required": true,
13
+ "staged_as": "plate.png",
14
+ "description": "The picture, with no text and no logo."
15
+ },
16
+ "logo": {
17
+ "kind": "image",
18
+ "required": false,
19
+ "staged_as": "logo.png",
20
+ "description": "The brand's logo file."
21
+ },
22
+ "font_headline": {
23
+ "kind": "font",
24
+ "required": false,
25
+ "staged_as": "headline.font",
26
+ "description": "The brand's headline face."
27
+ },
28
+ "font_body": {
29
+ "kind": "font",
30
+ "required": false,
31
+ "staged_as": "body.font",
32
+ "description": "The brand's body face."
33
+ },
34
+ "compose": {
35
+ "kind": "json",
36
+ "required": true,
37
+ "staged_as": "compose.json",
38
+ "description": "prompt.compose.json, also passed inline as the `ad` param."
39
+ }
40
+ },
41
+ "params": {
42
+ "ad": {
43
+ "kind": "json",
44
+ "required": true,
45
+ "description": "The copy blocks, logo placement and CTA colours (prompt.compose.json)."
46
+ },
47
+ "frame": {
48
+ "kind": "json",
49
+ "required": true,
50
+ "description": "This format's safe area and whether the copy is drawn here."
51
+ }
52
+ }
53
+ }
@@ -958,7 +958,8 @@ function compactWinningAd(record) {
958
958
  summary: String(dna.creative_concept ?? ""),
959
959
  advertiser_id: String(record.advertiser_id ?? ""),
960
960
  ad_id: String(record.ad_id ?? ""),
961
- media_url: String(record.media_url ?? "")
961
+ media_url: String(record.media_url ?? ""),
962
+ ...record.source === "followed" || record.source === "library" ? { source: record.source } : {}
962
963
  };
963
964
  }
964
965
  function fullWinningAd(record) {
@@ -1299,4 +1300,4 @@ export {
1299
1300
  captureVimeoCookie,
1300
1301
  childEnvWith
1301
1302
  };
1302
- //# sourceMappingURL=chunk-AOVFDDVW.js.map
1303
+ //# sourceMappingURL=chunk-7OVZ6SOA.js.map