@koda-sl/baker-cli 0.393.0-dev.10b6da011 → 0.393.0-dev.799d3f88c

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
@@ -876,7 +882,7 @@ Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversation
876
882
  | `conversions` | Pixels (with the pixel id a landing page installs) and conversions, with the campaigns using each |
877
883
  | `insights` | Impressions, clicks, spend, CTR, CPC, CPM, conversions, CPA, ROAS — per `--level account\|campaign\|ad_group\|ad`, over `--days N` or `--since/--until`, `--granularity`, `--segment country\|device\|platform\|product` |
878
884
  | `campaigns create\|update\|pause\|resume\|archive` | Stage a campaign change: objective, daily or lifetime budget (currency units), schedule, countries and regions included and excluded, audiences, platforms, tracking template, status, the conversions it measures (`--conversion-event-ids`), a catalog (`--catalog-id`) |
879
- | `ad-groups create\|update\|pause\|resume\|archive` | Stage an ad group change: context hints, bid strategy, max bid, tracking template, product filters, status |
885
+ | `ad-groups create\|update\|pause\|resume\|archive` | Stage an ad group change: context hints, bid strategy, max bid, audience bid adjustments, tracking template, product filters, status |
880
886
  | `ads create\|update\|pause\|resume\|archive` | Stage a chat card: title, body, landing page, image and its crop, tracking template, status |
881
887
  | `ads create-product` | Stage a catalog ad group's product ad: a title template the catalog's products fill in |
882
888
  | `pixels create` | Stage a new website pixel |
@@ -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
 
@@ -4163,7 +4169,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
4163
4169
  "type": "text_generate",
4164
4170
  "inputs": { "topic": "$ref:topic.text" },
4165
4171
  "params": {
4166
- "model": "google/gemini-3.1-flash-image-preview",
4172
+ "model": "google/gemini-3.1-flash-image",
4167
4173
  "prompt": "Write a 6-word tagline for {{topic}}."
4168
4174
  }
4169
4175
  }
@@ -4345,7 +4351,7 @@ Example pipeline:
4345
4351
  { "id": "font", "type": "ingest", "params": { "source": "url", "url": "https://cdn.example.com/brand.woff2", "expect": "font" } }
4346
4352
  { "id": "specimen", "type": "font_specimen", "inputs": { "font": "$ref:font.asset" }, "params": {} }
4347
4353
  { "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" } }
4354
+ "params": { "model": "google/gemini-3.1-flash-image", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4349
4355
  ```
4350
4356
 
4351
4357
  ---
@@ -4484,13 +4490,13 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4484
4490
 
4485
4491
  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
4492
 
4487
- ###### Model: `google/gemini-3.1-flash-image-preview`
4493
+ ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4488
4494
 
4489
4495
  Gemini flash image model — the default. Fast iteration, reference editing, extreme aspect ratios, and the `0.5K` size for cheap previews.
4490
4496
 
4491
4497
  | Name | Type | Required | Notes |
4492
4498
  |---|---|---|---|
4493
- | `model` | literal | yes | `"google/gemini-3.1-flash-image-preview"` |
4499
+ | `model` | literal | yes | `"google/gemini-3.1-flash-image"` |
4494
4500
  | `prompt` | string | yes | non-empty |
4495
4501
  | `aspect_ratio` | enum | no | EXTREME AR |
4496
4502
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
@@ -4506,20 +4512,20 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
4506
4512
  | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
4507
4513
  | `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
4514
 
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.
4515
+ 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
4516
 
4511
4517
  ```json
4512
4518
  { "id": "variant_a", "type": "image_generate",
4513
4519
  "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
4520
  ```
4515
4521
 
4516
- ###### Model: `google/gemini-3-pro-image-preview`
4522
+ ###### Model: `google/gemini-3-pro-image` (legacy id `google/gemini-3-pro-image-preview` still accepted)
4517
4523
 
4518
4524
  "Nano Banana Pro" — heavier, higher-quality Gemini.
4519
4525
 
4520
4526
  | Name | Type | Required | Notes |
4521
4527
  |---|---|---|---|
4522
- | `model` | literal | yes | `"google/gemini-3-pro-image-preview"` |
4528
+ | `model` | literal | yes | `"google/gemini-3-pro-image"` |
4523
4529
  | `prompt` | string | yes | non-empty |
4524
4530
  | `aspect_ratio` | enum | no | STD AR |
4525
4531
  | `image_size` | enum | no | `1K \| 2K \| 4K` |
@@ -4563,7 +4569,7 @@ The sheet prompt is baked into the backend — there is no `prompt` param. `subj
4563
4569
 
4564
4570
  | Name | Type | Required | Notes |
4565
4571
  |---|---|---|---|
4566
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4572
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4567
4573
  | `subject_description` | string | yes | exact subject wording — reuse verbatim downstream |
4568
4574
  | `subject_type` | enum | yes | `character \| person \| product` — picks the template + default views |
4569
4575
  | `views` | string[] | no | 2–6 view labels; defaults per `subject_type` (below) |
@@ -4590,7 +4596,7 @@ Default views per `subject_type`:
4590
4596
 
4591
4597
  Guidance:
4592
4598
  - 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.
4599
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` at `2K`+ for final 6-view sheets.
4594
4600
  - Feed the whole sheet downstream as one `reference` input — don't crop individual views.
4595
4601
 
4596
4602
  **Example** — two photos of a sneaker → product sheet → consistent hero shot:
@@ -4603,14 +4609,14 @@ Guidance:
4603
4609
  { "id": "b", "type": "ingest", "params": { "source": "url", "url": "https://example.com/sneaker-side.jpg", "expect": "image" } },
4604
4610
  { "id": "sheet", "type": "image_reference_sheet",
4605
4611
  "inputs": { "references": ["$ref:a.asset", "$ref:b.asset"] },
4606
- "params": { "model": "google/gemini-3-pro-image-preview",
4612
+ "params": { "model": "google/gemini-3-pro-image",
4607
4613
  "subject_description": "navy leather low-top sneaker with white sole and gold logo",
4608
4614
  "subject_type": "product",
4609
4615
  "style": "matte black studio background, soft even key light",
4610
4616
  "aspect_ratio": "16:9", "image_size": "2K" } },
4611
4617
  { "id": "hero", "type": "image_generate",
4612
4618
  "inputs": { "reference": "$ref:sheet.sheet" },
4613
- "params": { "model": "google/gemini-3-pro-image-preview",
4619
+ "params": { "model": "google/gemini-3-pro-image",
4614
4620
  "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
4621
  ],
4616
4622
  "output": { "node": "hero", "output": "images" }
@@ -4635,7 +4641,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4635
4641
 
4636
4642
  | Name | Type | Required | Notes |
4637
4643
  |---|---|---|---|
4638
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
4644
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4639
4645
  | `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
4646
  | `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
4647
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
@@ -4652,7 +4658,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4652
4658
  Guidance:
4653
4659
  - 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
4660
  - 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.
4661
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` for final-quality adaptation.
4656
4662
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
4657
4663
 
4658
4664
  **Example** — one 9:16 story creative fanned out to feed and landscape:
@@ -4662,12 +4668,12 @@ Guidance:
4662
4668
  "schema": "baker-canvas/1",
4663
4669
  "nodes": [
4664
4670
  { "id": "hero", "type": "image_generate",
4665
- "params": { "model": "google/gemini-3-pro-image-preview",
4671
+ "params": { "model": "google/gemini-3-pro-image",
4666
4672
  "prompt": "Story-format ad for a citrus energy drink: can centered, bold OFFER text top, logo bottom",
4667
4673
  "aspect_ratio": "9:16", "image_size": "2K" } },
4668
4674
  { "id": "adapt", "type": "image_aspect_adapt",
4669
4675
  "inputs": { "source": "$ref:hero.images#0" },
4670
- "params": { "model": "google/gemini-3-pro-image-preview",
4676
+ "params": { "model": "google/gemini-3-pro-image",
4671
4677
  "formats": ["9:16", "1:1", "16:9"],
4672
4678
  "guidance": "keep the offer text and logo fully visible" } }
4673
4679
  ],
@@ -4802,9 +4808,9 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
4802
4808
  {
4803
4809
  "schema": "baker-canvas/1",
4804
4810
  "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" } },
4811
+ { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4812
+ { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4813
+ { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4808
4814
  { "id": "best", "type": "image_select",
4809
4815
  "inputs": { "images": ["$ref:a.images#0", "$ref:b.images#0", "$ref:c.images#0"] },
4810
4816
  "params": { "model": "~google/gemini-pro-latest", "prompt": "most premium-feeling lighting, no harsh shadows" } }
@@ -5432,9 +5438,9 @@ Cache policies (via `--cache-policy`):
5432
5438
 
5433
5439
  ### CLI commands
5434
5440
 
5435
- #### `baker canvas validate <file.json>`
5441
+ #### `baker canvas validate <slug|file.json>`
5436
5442
 
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.
5443
+ 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
5444
 
5439
5445
  ```bash
5440
5446
  $ baker canvas validate my-canvas.json
@@ -5474,9 +5480,9 @@ names no camera/motion move **and** no exclusions — the six-decision doctrine)
5474
5480
  (the hook, scene 0, is thin across two or more of the four hook layers — text/sound/visual/vibe). Warnings never
5475
5481
  block a run.
5476
5482
 
5477
- #### `baker canvas critique <file.json>`
5483
+ #### `baker canvas critique <slug|file.json>`
5478
5484
 
5479
- Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded creative 0–1 before a billed `run`,
5485
+ 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
5486
  so a weak spot gets fixed for free. It branches on modality: a **video** creative is scored on **hook strength**,
5481
5487
  **sound-off first-frame legibility**, **retention risk**, and **mechanism clarity**; a **static ad** on **baked-text
5482
5488
  legibility risk**, **identity grounding** (Rule 1 — every subject on a real reference), **brand-type fidelity** (a
@@ -5493,9 +5499,9 @@ $ baker canvas critique my-static-ad.json
5493
5499
  "dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
5494
5500
  ```
5495
5501
 
5496
- #### `baker canvas run <file.json> [flags]`
5502
+ #### `baker canvas run <slug|file.json> [flags]`
5497
5503
 
5498
- Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
5504
+ 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
5505
 
5500
5506
  | Flag | Default | Effect |
5501
5507
  |---|---|---|
@@ -5613,7 +5619,9 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
5613
5619
 
5614
5620
  #### `baker canvas scaffold-static-ad <image> [flags]`
5615
5621
 
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.
5622
+ `<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.
5623
+
5624
+ **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
5625
 
5618
5626
  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
5627
 
@@ -5621,35 +5629,46 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5621
5629
  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
5630
  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
5631
 
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).
5632
+ 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:
5633
+
5634
+ - **`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.
5635
+ - **`picture_only`**: the concept has no copy, so the plate is the ad and nothing is drawn on it.
5636
+ - **`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.
5637
+ - **`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.
5638
+
5639
+ **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.
5640
+
5641
+ **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
5642
 
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).
5643
+ **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
5644
 
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).
5645
+ 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
5646
 
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.
5647
+ 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
5648
 
5632
5649
  ```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
5650
+ baker canvas scaffold-static-ad ./reference-ad.png --slug spring-offer --context "competitor ad for <brand>, <category>, <market>"
5651
+ # → writes src/creatives/spring-offer/{spring-offer.canvas.json,prompt.json,_definition.md}
5635
5652
  # 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
5653
+ baker canvas validate spring-offer
5654
+ baker canvas run spring-offer
5638
5655
  ```
5639
5656
 
5640
5657
  | Flag | Default | Effect |
5641
5658
  |---|---|---|
5642
5659
  | `--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. |
5660
+ | `--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. |
5661
+ | `--title <text>` | title-cased slug | Creative title for `_definition.md`; also the source of a derived slug. |
5645
5662
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5646
5663
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5647
5664
  | `--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). |
5665
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5666
+ | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
5667
+ | `--text-only` | on when the reference pictures nothing but copy | Draw a typographic ad: no picture generated, the copy on the brand colours in its fonts. `--no-text-only` renders a picture anyway. |
5668
+ | `--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
5669
  | `--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. |
5670
+ | `--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`. |
5671
+ | `--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
5672
  | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5654
5673
  | `--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
5674
 
@@ -5675,7 +5694,7 @@ baker canvas rerun spring-offer --force-remote --regenerate gen
5675
5694
 
5676
5695
  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
5696
 
5678
- #### `baker canvas inspect <run_id> [--thumbnails]`
5697
+ #### `baker canvas inspect <run_id|slug> [--thumbnails]`
5679
5698
 
5680
5699
  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
5700
 
@@ -5684,7 +5703,7 @@ baker canvas inspect r_01JXYZ...
5684
5703
  baker canvas inspect r_01JXYZ... --thumbnails
5685
5704
  ```
5686
5705
 
5687
- Resolves the run dir against `--outputs-dir` (default `./canvas`) or accepts an absolute path. Reads `manifest.json` for per-node stats.
5706
+ 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
5707
 
5689
5708
  #### `baker canvas catalog`
5690
5709
 
@@ -0,0 +1,253 @@
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 ad's own faces, staged beside this file by the render (a brand file or a web font
9
+ file). One range per face, so a single-weight file is used as it is instead of being
10
+ faked bolder. The browser fetches a face only when text uses it, and text uses it
11
+ only when a face was staged (see FRAME.fonts below). */
12
+ @font-face { font-family: 'AdHeadline'; src: url('./headline.font'); font-weight: 100 900; font-display: block; }
13
+ @font-face { font-family: 'AdBody'; src: url('./body.font'); font-weight: 100 900; font-display: block; }
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+ html, body { width: __WIDTH__px; height: __HEIGHT__px; overflow: hidden; background: #000; }
16
+ #root { position: absolute; inset: 0; overflow: hidden; }
17
+ .plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
18
+ .scrim { position: absolute; left: 0; right: 0; pointer-events: none; display: none; }
19
+ .group { position: absolute; display: flex; flex-direction: column; gap: 0; }
20
+ .block { font-family: 'AdBody', 'Helvetica Neue', Arial, sans-serif; overflow-wrap: break-word; text-wrap: pretty; }
21
+ .block.headline, .block.subheadline, .block.cta { font-family: 'AdHeadline', 'AdBody', 'Helvetica Neue', Arial, sans-serif; }
22
+ .block.headline { text-wrap: balance; letter-spacing: -0.01em; }
23
+ .fill { display: inline-block; line-height: 1.1; padding: 0.5em 1.2em; }
24
+ .fill.highlight { padding: 0.12em 0.4em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
25
+ .fill.sticker { transform: rotate(-3deg); }
26
+ .logo { display: block; width: auto; object-fit: contain; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div id="root" data-composition-id="main" data-start="0" data-duration="1" data-width="__WIDTH__" data-height="__HEIGHT__">
31
+ <div class="scrim" id="scrim-top"></div>
32
+ <div class="scrim" id="scrim-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
+ // A text-only ad has no picture (`plate: null`): the copy sits on the brand's ground.
47
+ var hasPlate = FRAME.plate !== null;
48
+ var ground = AD.background || { color: "#111111", ink: "#ffffff" };
49
+
50
+ var SIZE = { x_large: 8.6, large: 6.6, medium: 4.4, small: 3.0 };
51
+ var WEIGHT = { bold: 800, semibold: 600, regular: 400 };
52
+ var CASE = { upper: "uppercase", title: "capitalize", sentence: "none" };
53
+ var MAX_SHARE = { top: 0.44, middle: 0.3, bottom: 0.36 };
54
+ var RADIUS = { pill: "999px", rounded: "0.35em", square: "0" };
55
+
56
+ // With no face staged, the text never names the ad's faces, so the browser never asks
57
+ // for a file that is not there (a 404 the pre-render check refuses).
58
+ var FALLBACK_FACE = "'Helvetica Neue', Arial, sans-serif";
59
+ var facesStaged = FRAME.fonts !== false;
60
+
61
+ if (hasPlate) {
62
+ var plate = document.createElement("img");
63
+ plate.className = "plate";
64
+ plate.id = "plate";
65
+ plate.src = "./plate.png";
66
+ plate.alt = "";
67
+ root.insertBefore(plate, root.firstChild);
68
+ } else {
69
+ root.style.background = ground.color;
70
+ }
71
+
72
+ function luma(hex) {
73
+ var m = /^#?([0-9a-f]{6})$/i.exec(hex || "");
74
+ if (!m) return null;
75
+ var v = parseInt(m[1], 16);
76
+ return 0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255);
77
+ }
78
+
79
+ function alignSelf(align) {
80
+ return align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
81
+ }
82
+
83
+ // Every line is drawn in its group: the layout region the reference put it in (placed
84
+ // at that region's box on this format), or a band when the layout gave no position.
85
+ var groups = {};
86
+ var bands = { top: null, middle: null, bottom: null };
87
+ (AD.groups || []).forEach(function (g) {
88
+ groups[g.id] = g;
89
+ });
90
+
91
+ function bandEl(name) {
92
+ if (bands[name]) return bands[name];
93
+ var el = document.createElement("div");
94
+ el.className = "group band";
95
+ el.dataset.band = name;
96
+ el.style.left = box.x + "px";
97
+ el.style.width = box.w + "px";
98
+ el.style.gap = 1.6 * U + "px";
99
+ if (name === "top") el.style.top = box.y + "px";
100
+ if (name === "bottom") el.style.bottom = H - (box.y + box.h) + "px";
101
+ if (name === "middle") {
102
+ el.style.top = box.y + box.h / 2 + "px";
103
+ el.style.transform = "translateY(-50%)";
104
+ }
105
+ el.dataset.max = String(box.h * MAX_SHARE[name]);
106
+ root.appendChild(el);
107
+ bands[name] = el;
108
+ return el;
109
+ }
110
+
111
+ var placed = {};
112
+ function containerFor(groupId, band) {
113
+ var g = groups[groupId];
114
+ var at = g && g.at && g.at[FRAME.format];
115
+ if (!at) return bandEl((g && g.band) || band || "top");
116
+ if (placed[groupId]) return placed[groupId];
117
+ var el = document.createElement("div");
118
+ el.className = "group placed";
119
+ el.style.left = (at.x[0] / 100) * W + "px";
120
+ el.style.width = ((at.x[1] - at.x[0]) / 100) * W + "px";
121
+ el.style.top = (at.y[0] / 100) * H + "px";
122
+ el.style.minHeight = ((at.y[1] - at.y[0]) / 100) * H + "px";
123
+ el.style.justifyContent = "center";
124
+ el.style.gap = 1.2 * U + "px";
125
+ // Copy rewritten for our brand runs longer than the source's: allow some growth
126
+ // past the region before the type shrinks.
127
+ el.dataset.max = String(((at.y[1] - at.y[0]) / 100) * H * 1.35);
128
+ root.appendChild(el);
129
+ placed[groupId] = el;
130
+ return el;
131
+ }
132
+
133
+ function addLogo(logo) {
134
+ var img = document.createElement("img");
135
+ img.className = "logo";
136
+ img.src = "./logo.png";
137
+ img.alt = "";
138
+ img.style.height = 5.4 * U + "px";
139
+ img.style.maxWidth = box.w * 0.42 + "px";
140
+ img.style.alignSelf = alignSelf(logo.align);
141
+ img.onerror = function () {
142
+ img.remove();
143
+ };
144
+ var el = containerFor(logo.group, logo.band);
145
+ if (el.dataset.band === "bottom") el.appendChild(img);
146
+ else el.insertBefore(img, el.firstChild);
147
+ }
148
+
149
+ function addBlock(b) {
150
+ var el = document.createElement("div");
151
+ el.className = "block " + b.role;
152
+ if (!facesStaged) el.style.fontFamily = FALLBACK_FACE;
153
+ var size = b.role === "fine_print" ? 2.4 : SIZE[b.size] || SIZE.medium;
154
+ el.dataset.base = String(size);
155
+ el.style.fontSize = size * U + "px";
156
+ el.style.fontWeight = String(WEIGHT[b.weight] || (b.role === "cta" ? 700 : 400));
157
+ el.style.lineHeight = b.role === "headline" ? "1.04" : "1.22";
158
+ el.style.textTransform = CASE[b.case] || "none";
159
+ el.style.textAlign = b.align || "center";
160
+ el.style.alignSelf = alignSelf(b.align);
161
+ var t = b.treatment || { kind: b.role === "cta" && !AD.groups ? "button" : "plain" };
162
+ var ink = b.color || (hasPlate ? "#ffffff" : ground.ink);
163
+ var text = b.text;
164
+ if (t.kind === "arrow" && !/[→›»]\s*$/.test(text)) text = text + " →";
165
+ if (t.kind === "button" || t.kind === "sticker" || t.kind === "highlight") {
166
+ var fill = document.createElement("span");
167
+ fill.className = "fill " + t.kind;
168
+ fill.textContent = text;
169
+ fill.style.background = t.background || (AD.cta && AD.cta.background) || "#111111";
170
+ fill.style.color = t.color || (AD.cta && AD.cta.color) || "#ffffff";
171
+ fill.style.borderRadius = RADIUS[t.shape] || RADIUS.rounded;
172
+ el.appendChild(fill);
173
+ } else {
174
+ el.textContent = text;
175
+ el.style.color = ink;
176
+ if (t.kind === "underline") {
177
+ el.style.textDecoration = "underline";
178
+ el.style.textDecorationThickness = "0.08em";
179
+ el.style.textUnderlineOffset = "0.18em";
180
+ }
181
+ var dark = (luma(ink) || 255) < 110;
182
+ el.dataset.dark = dark ? "1" : "0";
183
+ if (hasPlate) el.style.textShadow = dark ? "0 1px 10px rgba(255,255,255,0.35)" : "0 2px 14px rgba(0,0,0,0.45)";
184
+ }
185
+ containerFor(b.group, b.band).appendChild(el);
186
+ }
187
+
188
+ var drawCopy = FRAME.draw_copy !== false;
189
+ (drawCopy ? AD.copy || [] : []).forEach(addBlock);
190
+ if (AD.logo) addLogo(AD.logo);
191
+
192
+ function containers() {
193
+ return Array.prototype.slice.call(root.querySelectorAll(".group"));
194
+ }
195
+
196
+ // Shrink a group's type until it fits its room and no line runs past its edge. The
197
+ // copy is whatever prompt.json says today, so its length is not known when the
198
+ // layout is written.
199
+ function fit(el) {
200
+ var max = Number(el.dataset.max);
201
+ var width = el.getBoundingClientRect().width;
202
+ var scale = 1;
203
+ for (var i = 0; i < 40; i++) {
204
+ var tooTall = el.scrollHeight > max;
205
+ var tooWide = Array.prototype.some.call(el.children, function (c) {
206
+ return c.scrollWidth > width + 1;
207
+ });
208
+ if ((!tooTall && !tooWide) || scale < 0.45) break;
209
+ scale *= 0.94;
210
+ Array.prototype.forEach.call(el.querySelectorAll(".block"), function (c) {
211
+ c.style.fontSize = Number(c.dataset.base) * U * scale + "px";
212
+ });
213
+ }
214
+ }
215
+
216
+ // A soft scrim behind a band of light type over the picture (or a light one behind
217
+ // dark type), so copy with no position of its own reads on any plate. Copy placed
218
+ // by the layout sits on the calm area the plate kept for it.
219
+ function scrim(name) {
220
+ var el = bands[name];
221
+ if (!hasPlate || !el) return;
222
+ var s = document.getElementById("scrim-" + name);
223
+ var firstText = el.querySelector(".block[data-dark]");
224
+ if (!firstText) return;
225
+ var tint = firstText.dataset.dark === "1" ? "255,255,255" : "0,0,0";
226
+ var rect = el.getBoundingClientRect();
227
+ var reach = name === "top" ? rect.bottom + 6 * U : H - rect.top + 6 * U;
228
+ s.style.display = "block";
229
+ s.style.height = Math.min(H * 0.6, reach) + "px";
230
+ if (name === "top") {
231
+ s.style.top = "0";
232
+ s.style.background = "linear-gradient(to bottom, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
233
+ } else {
234
+ s.style.bottom = "0";
235
+ s.style.background = "linear-gradient(to top, rgba(" + tint + ",0.42), rgba(" + tint + ",0))";
236
+ }
237
+ }
238
+
239
+ var tl = gsap.timeline({ paused: true });
240
+ tl.to({}, { duration: 1 });
241
+ window.__timelines = window.__timelines || {};
242
+ window.__timelines.main = tl;
243
+
244
+ var ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
245
+ ready.then(function () {
246
+ containers().forEach(fit);
247
+ ["top", "bottom"].forEach(scrim);
248
+ root.setAttribute("data-ready", "1");
249
+ });
250
+ })();
251
+ </script>
252
+ </body>
253
+ </html>