@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 +68 -49
- package/canvas/static-ad-composition/index.html +253 -0
- package/canvas/static-ad-composition/meta.json +53 -0
- package/dist/{chunk-FEJQJ64L.js → chunk-JLKUHDBB.js} +129 -20
- package/dist/chunk-JLKUHDBB.js.map +1 -0
- package/dist/cli.js +4271 -2399
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FEJQJ64L.js.map +0 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4806
|
-
{ "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image
|
|
4807
|
-
{ "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5637
|
-
baker canvas run
|
|
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
|
-
| `--
|
|
5644
|
-
| `--
|
|
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>` |
|
|
5649
|
-
| `--
|
|
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
|
|
5652
|
-
| `--skip-font` | off |
|
|
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>
|