@koda-sl/baker-cli 0.406.0 → 0.407.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4046,24 +4046,6 @@ baker canvas validate my-canvas.json
4046
4046
  # PROMPT_OVER_MODEL_LIMIT, naming the node and the overshoot, before anything bills.
4047
4047
  # `scaffold-static-ad` runs the same check on the prompt.json it writes and, when it is over,
4048
4048
  # still writes the canvas and returns the overshoot in `hints[]` — trim prompt.json, then validate.
4049
- # Where `hyperframes` is installed, a static ad's composition is laid out with the copy and
4050
- # fonts the run will draw, over a stand-in picture: two lines of copy that collide are refused
4051
- # as `layout/content_overlap` before any picture is paid for.
4052
- # At render, each static format reads the picture it is drawn on: a line that would sit on
4053
- # the subject, a device or the seam of two painted panels moves to the nearest calm room
4054
- # (never past another line, never more than a few of its own heights), takes white or
4055
- # near-black when its own colour does not read (WCAG 3:1, display type), or gets a frosted
4056
- # surface when no room is calm. A region's card or pill is drawn with its words; when the
4057
- # picture painted that shape anyway, the drawn panel is laid over it (a creative scaffolded
4058
- # before this draws a panel only there, so its plates stay cached). Copy never shrinks
4059
- # below 72% of its size. The static-ad template in each creative's `layout-*/` folders is
4060
- # refreshed on every validate and run.
4061
- # Before the pictures are painted, the scene's subjects are read (Jev, OpenRouter) for
4062
- # shapes made to hold copy, each matched to a shape the composition draws (or none); the
4063
- # plate leaves out only the matched ones, and is told which shapes the designer draws
4064
- # (`drawn_by_designer` in prompt.plate.json); each answer is kept in
4065
- # `prompt.compose.json` (`scene_reads`), and one Jev could not give is kept as part of the
4066
- # picture, so the plate spec does not change between runs.
4067
4049
 
4068
4050
  # 2. Run (executes nodes, writes files to ./canvas/<run_id>/)
4069
4051
  baker canvas run my-canvas.json
@@ -5564,7 +5546,7 @@ Cache policies (via `--cache-policy`):
5564
5546
 
5565
5547
  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.
5566
5548
 
5567
- **The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's copy block and type specimen (from `prompt.json`), and the derived spec files the frames read (`prompt.style.json`, `prompt.plate.json`, `prompt.compose.json`). `validate`, `run`, `critique` and `set-prompt` share one preflight that applies every projection and **writes the result back to the canvas file** (only when something changed; `canvas_updated` says so; `sync-definition`, which the file watcher runs over every creative, applies it without writing), so the file on disk, the graph the dashboard shows, the graph the critic scores and the graph that renders are the same. A scene edit that changes timing or the number of scenes is structural and still asks for a re-scaffold. Every preflight failure prints `{ ok: false, error: { code, message, fix? } }` and exits 2.
5549
+ **The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's copy block and type specimen (from `prompt.json`), and the derived style file the frames read (`prompt.style.json`). `validate`, `run`, `critique` and `set-prompt` share one preflight that applies every projection and **writes the result back to the canvas file** (only when something changed; `canvas_updated` says so; `sync-definition`, which the file watcher runs over every creative, applies it without writing), so the file on disk, the graph the dashboard shows, the graph the critic scores and the graph that renders are the same. A scene edit that changes timing or the number of scenes is structural and still asks for a re-scaffold. Every preflight failure prints `{ ok: false, error: { code, message, fix? } }` and exits 2.
5568
5550
 
5569
5551
  ```bash
5570
5552
  $ baker canvas validate my-canvas.json
@@ -5755,30 +5737,27 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
5755
5737
 
5756
5738
  **New-creative limit:** a chat makes at most 10 new creatives. The eleventh `scaffold-static-ad`, `scaffold-ad` or `scaffold-video` with a new `--slug` exits 2 with `CREATIVE_LIMIT` before any billed pass. Re-scaffolding a creative already made in the chat, or one whose `_definition.md` existed before, does not count.
5757
5739
 
5758
- `<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.
5740
+ `<image>` is a **local path or an `http(s)` URL** (e.g. a presigned S3 link from ad-dna). With `--slug` the source is localized to durable, content-addressed storage before anything bills, so the emitted canvas stays runnable in any sandbox; without a slug a URL is passed straight into the `original` ingest node (a presigned one expires). 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.
5759
5741
 
5760
- 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:
5742
+ Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. It runs **billed Gemini passes** up front:
5761
5743
 
5762
- 1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image into a blueprint JSON, written next to the canvas as **`prompt.json`**. This is the editable "prompt": you rewrite it by hand into the ad you want (palette, copy, claims, subjects). It feeds the generator directly — there is **no automatic brand-transform step**. The blueprint also names the **`winning_mechanisms`** — the special sauce that makes the ad a candidate winner, each tagged `kind` (verbal: rhyme/pun/rhythm; visual: unexpected crop, visual gag, juxtaposition, pattern interrupt, before/after; structural: hook order/reveal) with a `device` and `why_it_works` — so your rewrite rebuilds the mechanism that makes the ad win instead of adapting only the surface and losing it.
5763
- 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.
5764
- 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.
5744
+ 1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image into a blueprint JSON, written next to the canvas as **`prompt.json`**. This is the editable "prompt": you rewrite it by hand into the ad you want (palette, copy, claims, subjects). It feeds the generator directly — there is **no automatic brand-transform step**. The blueprint also names the **`winning_mechanisms`** — the special sauce that makes the ad a candidate winner, each tagged `kind` (verbal: rhyme/pun/rhythm; visual: unexpected crop, visual gag, juxtaposition, pattern interrupt, before/after; structural: hook order/reveal) with a `device` and `why_it_works` — so your rewrite rebuilds the mechanism that makes the ad win instead of adapting only the surface and losing it. Every line of `text_content` carries its `surface` (`overlay` ad copy, or `in_scene`: written on a phone screen, a note, a sign, packaging) and `placement`.
5745
+ 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`). 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). 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.
5765
5746
 
5766
- 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:
5747
+ It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
5767
5748
 
5768
- - **`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.
5769
- - **`picture_only`**: the concept has no copy, so the plate is the ad and nothing is drawn on it.
5770
- - **`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.
5771
- - **`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.
5749
+ - Up to **three candidates** (`--takes`, default 3) render on `openai/gpt-image-2.5-sunburst` at quality `xhigh`, each a finished ad: the picture and every word of the copy set in type. The prompt quotes every string of `text_content` verbatim (never the logo's text) and asks for each once; the block is re-read from `prompt.json` on every `validate` and `run`, so an edit to the copy re-keys exactly the renders it touches. The brand's **typeface** is shown to the model as a `font_specimen` set on the ad's own copy: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading family `tokens.json` or `BRAND.md` names, fetched as a web font file; when the brand names none, the model follows the blueprint's `fonts` and no neutral default is put in front of it. **The logo is optional**: the real file in `src/brand/logos/` goes to the model as a reference to reproduce, only when `prompt.json` keeps an `advertiser_brand` entry in `brands_logos` and the file exists; no logo is never an error, never a slot to fill and never a stand-in. Deleting that entry from `prompt.json` drops the logo on the next run; adding one to a canvas scaffolded without the file needs the file in `src/brand/logos/` and a re-scaffold. `prompt.json` also carries a **`brand`** block (ground, ink, accent, CTA and palette, from `src/brand/tokens.json` roles or `BRAND.md`), and the render binds its colours to it over the reference ad's. A 9:16 is told to keep every word, logo and button out of the Stories interface.
5750
+ - An `image_select` **judge** scores the candidates on one dominant idea, thumbnail legibility, exact words, layout (nothing overlaps or is clipped), mechanism kept, brand with the logo covered and artifacts, and picks the highest total among those whose words are exact.
5751
+ - Every other placement format is the winner **re-laid** by the same model from the ad itself (one billed render each, the winner as its first reference), so the set stays one ad. A format the model has no shape for (gpt-image has no 4:5) renders at its nearest shape (3:4) and is cropped exact by a free `imagemagick` node; the render prompt keeps every word 6% from the edges for it.
5752
+ - 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. Each **animal hero** is fused into a generated **multi-view reference sheet** (`image_reference_sheet`) that the render grounds on; pass `--skip-actor-sheets` to ground 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.
5772
5753
 
5773
- **Fonts** come from the first source that has one: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading and body families `tokens.json` or `BRAND.md`'s typography section name, fetched as web font files from Google Fonts or Fontsource and ingested by URL, else Inter (800 and 400). **The logo is optional**: it is drawn from `src/brand/logos/` only when `prompt.json` keeps a logo (a `logo` layout region, or an `advertiser_brand` entry in `brands_logos`) and a real file exists. Meta shows the page name and picture on every ad, so no logo is never an error, never a slot to fill and never a stand-in; a canvas scaffolded with an unfilled logo slot runs without it. Text that belongs to something in the scene (a phone note, a screenshot, a sign, packaging: `surface: "in_scene"` in `text_content`) is painted by the plate, not overlaid, and listed in the report's `in_scene_text`. The plates read `prompt.plate.json` (the scene, the reserved areas, the visual mechanisms and the in-scene text; no overlay copy, no brand names) and the compositions read `prompt.compose.json`; both are re-derived from `prompt.json` by `validate` and `run`, so a copy edit re-draws the formats from the cached plates without paying for a picture again. A format within 10% of the plate's shape is a free crop; a farther one extends the chosen plate once on the same model. Detected elements get **one `[TODO]` ingest slot** each (never the logo), and the original image is wired in for composition only, left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }`; the **checklist** carries `mode`, `summary` (what a run does: pictures, judge, fonts, logo), `fonts`, `logo`, `in_scene_text` and every real asset to drop in.
5754
+ The canvas is validated before it is written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, stats, checklist }`; the **checklist** carries `summary` (what a run does: candidates, judge, type, logo, formats), `font`, `logo`, `in_scene_text`, `formats` and every real asset to drop in.
5774
5755
 
5775
- **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.
5756
+ **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 shape the candidates really render at (the model's nearest to the lead format), and `format_plan` says how each format is made (`render` or `relay`, and whether it is cropped exact).
5776
5757
 
5777
- **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; its hints name the cheapest fix (overlapping or unreadable copy is fixed with a smaller `size`, a `highlight` or `sticker` treatment or shorter words, which re-draw the formats from the cached plates). **Render budget:** a static ad gets, per creative in a chat before the person reacts, 3 renders that paint pictures and then 4 re-draws from those pictures; a render that paints nothing (every billed node from the cache) counts as a re-draw. With the pictures spent a re-draw runs only from the cache (the engine's `billed_from_cache_only`), and one that would paint exits 2 with `RENDER_BUDGET` before billing; with both spent every render exits 2 with `RENDER_BUDGET` without rendering, and `baker creatives feedback record` or `baker creatives select` on that creative opens a new round. Resuming an interrupted run does not count again, and a run that died only on an outage (timeout, network, a provider that says retry) gives its render back.
5758
+ **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; **every string of `prompt.json`'s copy transcribed off each format and compared character for character** (case and accents kept; a string read differently or not found is blocking on its own, and a format the model transcribed nothing for is reported as unread, never passed); and one vision pass for clipping, overlaps, artifacts, the Stories safe zones on a 9:16, one dominant idea, 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-candidate 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; every fix is an edit to `prompt.json` and a re-run (wrong words: the strings in `text_content`; wrong picture: the scene; a bad roll: `--regenerate ad_a`). **Render budget:** a static ad gets 3 renders per creative in a chat before the person reacts; the fourth exits 2 with `RENDER_BUDGET` without rendering, and `baker creatives feedback record` or `baker creatives select` on that creative opens a new round. Resuming an interrupted run does not count again, and a run that died only on an outage (timeout, network, a provider that says retry) gives its render back.
5778
5759
 
5779
- 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.
5780
-
5781
- 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.
5760
+ The render 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. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
5782
5761
 
5783
5762
  ```bash
5784
5763
  baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
@@ -5792,23 +5771,20 @@ baker canvas run ./static-ad.canvas.json
5792
5771
  |---|---|---|
5793
5772
  | `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
5794
5773
  | `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
5795
- | `--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. |
5774
+ | `--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 localized to durable storage and normalized to a model-safe format (SVG/AVIF/HEIC/… → PNG), so the canvas ingests a stable URL instead of an expiring one. |
5796
5775
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5797
5776
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5798
- | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
5799
- | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. An unknown model id in any `--*-model` flag fails the command (`unknown_model`) before anything is billed. |
5800
- | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
5801
- | `--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. |
5802
- | `--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). |
5803
- | `--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. |
5777
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the render model. An unknown model id in any `--*-model` flag fails the command (`unknown_model`) before anything is billed. |
5778
+ | `--takes <n>` | `3` | Finished ads rendered and judged (1-3); `1` skips the judge. |
5779
+ | `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` deliver one format). |
5780
+ | `--placements <preset\|none>` | derived from `--platform` | Override the set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
5804
5781
  | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
5805
- | `--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). |
5806
5782
  | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5807
5783
  | `--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`. |
5808
5784
 
5809
- Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
5785
+ Scaffolding runs (and bills) the two vision passes; **running** the result generates billed images. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
5810
5786
 
5811
- **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5787
+ **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. **Correct a finished static ad** with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; the other formats and the review follow, and it counts as one render of the round. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5812
5788
 
5813
5789
  #### `baker canvas rerun <slug> [flags]` (deprecated)
5814
5790
 
@@ -4689,9 +4689,17 @@ function parallelFormatRenderGroups(nodes) {
4689
4689
  return [...groups.values()].filter((g) => g.length > 1);
4690
4690
  }
4691
4691
  function checkParallelFormatRenders(ctx) {
4692
+ const rendered = new Set(
4693
+ ctx.canvas.nodes.filter((n) => n.type === "image_generate" || n.type === "image_select").map((n) => n.id)
4694
+ );
4695
+ const reframedFromSibling = (inputs) => {
4696
+ const reference = inputs?.reference;
4697
+ const refs = Array.isArray(reference) ? reference : [reference];
4698
+ return refs.some((r) => typeof r === "string" && rendered.has(r.match(/^\$ref:([a-z][a-z0-9_]*)\./)?.[1] ?? ""));
4699
+ };
4692
4700
  const candidates = [];
4693
4701
  for (const n of ctx.canvas.nodes) {
4694
- if (n.type !== "image_generate") continue;
4702
+ if (n.type !== "image_generate" || reframedFromSibling(n.inputs)) continue;
4695
4703
  const params = n.params;
4696
4704
  const prompt = params?.prompt;
4697
4705
  if (typeof prompt !== "string" || prompt.length === 0) continue;
@@ -6197,7 +6205,7 @@ var Engine = class {
6197
6205
  const layers = topologicalLayers(graph);
6198
6206
  const limit = resolveConcurrency(opts.concurrency);
6199
6207
  for (const layer of layers) {
6200
- await this.assertLayerMayRun(canvas, layer, outputs, runId, opts);
6208
+ this.assertLayerMayRun(canvas, layer, opts);
6201
6209
  const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
6202
6210
  if (opts.signal?.aborted) {
6203
6211
  return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
@@ -6326,10 +6334,9 @@ var Engine = class {
6326
6334
  }
6327
6335
  /**
6328
6336
  * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
6329
- * never leaves a layer half billed: a signal, the cost cap, a checkpoint, and a billed node
6330
- * the cache cannot serve when the run is cache-only.
6337
+ * never leaves a layer half billed: a signal, the cost cap and a checkpoint.
6331
6338
  */
6332
- async assertLayerMayRun(canvas, layer, outputs, runId, opts) {
6339
+ assertLayerMayRun(canvas, layer, opts) {
6333
6340
  if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
6334
6341
  const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
6335
6342
  if (held.length > 0) {
@@ -6338,38 +6345,6 @@ var Engine = class {
6338
6345
  `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
6339
6346
  );
6340
6347
  }
6341
- if (opts.billed_from_cache_only) {
6342
- const fresh = await this.billedMisses(canvas, layer, outputs, runId, opts);
6343
- if (fresh.length > 0) {
6344
- throw new RunAbortedError(
6345
- "cache_only",
6346
- `${fresh.join(", ")} would be rendered fresh and billed \u2014 nothing in this layer ran`
6347
- );
6348
- }
6349
- }
6350
- }
6351
- /**
6352
- * The billed nodes of a layer that would miss the cache, found before any of them runs:
6353
- * each is prepared exactly as `executeOne` would (same inputs, params and cache key).
6354
- *
6355
- * `executeOne` prepares each node again when it runs, so a cache-only layer hydrates its
6356
- * text slots and hashes its `cacheKeyExtras` twice. Deliberate: a re-draw's layers are
6357
- * small, and threading this preparation into `executeOne` would couple the check to the
6358
- * run path, where a stale preparation would bill the wrong key.
6359
- */
6360
- async billedMisses(canvas, layer, outputs, runId, opts) {
6361
- const misses = [];
6362
- for (const nodeId of layer) {
6363
- const node = canvas.nodes.find((n) => n.id === nodeId);
6364
- const def = node ? this.registry.get(node.type) : void 0;
6365
- if (!node || !def) continue;
6366
- const token = resolveRegenerateToken(node, opts.regenerate, runId);
6367
- const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, token, this.assets);
6368
- if (estimateNodeCredits2(def, prepared.renderedParams) <= 0) continue;
6369
- const hit = opts.cache_policy === "bypass" ? null : await this.cache.get(prepared.cacheKey);
6370
- if (!hit) misses.push(nodeId);
6371
- }
6372
- return misses;
6373
6348
  }
6374
6349
  /** A cache hit replayed as this node's result: its outputs written, and free. */
6375
6350
  async replayCached(node, cacheKey, outputs, writer) {
@@ -9065,9 +9040,7 @@ var FINDING_ADVICE = {
9065
9040
  console_error: "the composition threw in the browser, so whatever that script was driving did not happen.",
9066
9041
  page_error: "a script failed to evaluate \u2014 animations and any JS-driven layout in this composition did not run.",
9067
9042
  http_error: "an asset 404ed at render time; it will be missing from the frame, not substituted.",
9068
- // The most common reason a static ad failed its render in production (Sept 2026), and the
9069
- // agent answered it by re-painting all three pictures when only the composition was wrong.
9070
- content_overlap: "two lines of copy collide. In a static ad, make one smaller (`size` in its layout region's `text` in prompt.json) or shorten it: the formats re-draw from the cached pictures. Moving a layout region instead re-paints every picture.",
9043
+ content_overlap: "two text blocks collide. Make one smaller, shorten it, or move it, in the composition's HTML or the param that sets it; nothing generated needs to be paid for again.",
9071
9044
  console_warning: "if this names a GSAP target, the element it animates does not exist, so that tween is a no-op and whatever it was meant to reveal never appears. Fix the selector or add the element before rendering."
9072
9045
  };
9073
9046
  function explainGateFinding(finding) {
@@ -9115,13 +9088,6 @@ async function lintPreparedComposition(dir, timeoutMs) {
9115
9088
  const lintRaw = await runOne(buildLintArgsNoInstall(dir), timeoutMs);
9116
9089
  return lintRaw === null ? [] : lintBlockers(lintRaw);
9117
9090
  }
9118
- function layoutBlockers(checkRaw) {
9119
- return classifyGateOutput(checkRaw).blocking.filter((f) => f.source === "layout").map((f) => ({ code: f.code, message: explainGateFinding(f) }));
9120
- }
9121
- async function checkPreparedLayout(dir, timeoutMs) {
9122
- const checkRaw = await runOne(["--no-install", ...buildCheckArgs(dir, 1)], timeoutMs);
9123
- return checkRaw === null ? [] : layoutBlockers(checkRaw);
9124
- }
9125
9091
  function buildInspectArgs(dir, samples) {
9126
9092
  return ["hyperframes", "inspect", dir, "--json", "--samples", String(samples)];
9127
9093
  }
@@ -9362,24 +9328,16 @@ async function gateIssues(args) {
9362
9328
  if (!await isHyperframesAvailable()) return [];
9363
9329
  const tmp = await mkdtemp4(path17.join(tmpdir4(), "hf-validate-"));
9364
9330
  try {
9365
- const { faithful } = await prepareForGate(tmp, args);
9331
+ await prepareForGate(tmp, args);
9366
9332
  const lint = await lintPreparedComposition(tmp, args.lintTimeoutMs);
9367
- const layout = args.checkLayout && faithful ? await checkPreparedLayout(tmp, LAYOUT_CHECK_TIMEOUT_MS) : [];
9368
- return [
9369
- ...lint.map((b) => ({
9370
- path: "params.composition",
9371
- message: `pre-render gate would fail: [${b.code}] ${b.message}`
9372
- })),
9373
- ...layout.map((b) => ({
9374
- path: "params.composition",
9375
- message: `pre-render gate would fail: [layout/${b.code}] ${b.message} Nothing was rendered or billed.`
9376
- }))
9377
- ];
9333
+ return lint.map((b) => ({
9334
+ path: "params.composition",
9335
+ message: `pre-render gate would fail: [${b.code}] ${b.message}`
9336
+ }));
9378
9337
  } finally {
9379
9338
  await rm4(tmp, { recursive: true, force: true }).catch(() => void 0);
9380
9339
  }
9381
9340
  }
9382
- var LAYOUT_CHECK_TIMEOUT_MS = 6e4;
9383
9341
  var MAX_INPUT_BYTES = 20 * 1024 * 1024;
9384
9342
  var STAND_IN_PICTURE = Buffer.from(
9385
9343
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
@@ -9573,7 +9531,7 @@ async function resolveCompositionDir(composition) {
9573
9531
  if (s.isDirectory()) return compositionPath;
9574
9532
  return path18.dirname(compositionPath);
9575
9533
  }
9576
- async function validateCompositionNode(args, ownedKeys, options = {}) {
9534
+ async function validateCompositionNode(args, ownedKeys) {
9577
9535
  const issues = await validateCompositionParams(args.rawParams, ownedKeys);
9578
9536
  const params = args.rawParams ?? {};
9579
9537
  if (typeof params.composition !== "string") return issues;
@@ -9590,8 +9548,7 @@ async function validateCompositionNode(args, ownedKeys, options = {}) {
9590
9548
  rawInputs: args.rawInputs,
9591
9549
  inputKindOf: args.inputKindOf,
9592
9550
  inputSourceOf: args.inputSourceOf,
9593
- lintTimeoutMs: 2e4,
9594
- checkLayout: options.checkLayout
9551
+ lintTimeoutMs: 2e4
9595
9552
  })
9596
9553
  );
9597
9554
  return issues;
@@ -9811,9 +9768,7 @@ var hyperframeSnapshotNode = defineNode({
9811
9768
  cost: () => ({ credits: 0, seconds_estimate: 3 }),
9812
9769
  // The snapshot's own keys (`wait_for`) are not composition params: checked against the
9813
9770
  // render node's key set, a canvas that waited on a selector failed validate every time.
9814
- // A still has one frame and its copy is known before the run, so validate lays it out
9815
- // and refuses colliding lines before a single picture is paid for.
9816
- validateExtra: (args) => validateCompositionNode(args, NODE_OWNED_PARAM_KEYS2, { checkLayout: true }),
9771
+ validateExtra: (args) => validateCompositionNode(args, NODE_OWNED_PARAM_KEYS2),
9817
9772
  cacheKeyExtras: async ({ params }) => {
9818
9773
  const out = {};
9819
9774
  const composition = params.composition;
@@ -11598,7 +11553,6 @@ export {
11598
11553
  MODEL_ROLES,
11599
11554
  AD_IMAGE_MODEL,
11600
11555
  REFERENCE_SHEET_MODEL,
11601
- ASPECT_ADAPT_MODEL,
11602
11556
  PLATE_JUDGE_MODEL,
11603
11557
  AD_DESCRIBE_MODEL,
11604
11558
  BLUEPRINT_ANALYSIS_MODEL,
@@ -11676,4 +11630,4 @@ export {
11676
11630
  defaultRegistry,
11677
11631
  createEngineFromEnv
11678
11632
  };
11679
- //# sourceMappingURL=chunk-YOF4XTUW.js.map
11633
+ //# sourceMappingURL=chunk-KMCVSEGU.js.map