@koda-sl/baker-cli 0.132.1 → 0.133.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
@@ -3094,7 +3094,8 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
3094
3094
  | Name | Type | Required | Notes |
3095
3095
  |---|---|---|---|
3096
3096
  | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
3097
- | `formats` | enum[] | yes | 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 |
3097
+ | `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 |
3098
+ | `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`. |
3098
3099
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
3099
3100
  | `image_size` | enum | no | `1K \| 2K \| 4K` on `3-pro`, `0.5K \| 1K \| 2K \| 4K` on `3.1-flash` |
3100
3101
 
@@ -3104,9 +3105,10 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
3104
3105
  |---|---|---|
3105
3106
  | `images` | image[] | one per requested format, in `formats` order; each carries `metadata.aspect_ratio` and `metadata.adapted` (`false` = source passed through) |
3106
3107
 
3107
- **Cost** — per-format estimate: 20 credits on `3-pro`, 5 on `3.1-flash`, × `formats.length` (backend charges actual OpenRouter usage; pass-through formats are free).
3108
+ **Cost** — per-format estimate: 20 credits on `3-pro`, 5 on `3.1-flash`, × the resolved format count (a `platform` preset expands first; backend charges actual OpenRouter usage; pass-through formats are free).
3108
3109
 
3109
3110
  Guidance:
3111
+ - 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.
3110
3112
  - List the source's own ratio in `formats` too when you want the full set in one output — it passes through for free.
3111
3113
  - Use `google/gemini-3.1-flash-image-preview` while iterating; switch to `google/gemini-3-pro-image-preview` for final-quality adaptation.
3112
3114
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
@@ -3992,7 +3994,9 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
3992
3994
  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.
3993
3995
  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.
3994
3996
 
3995
- It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. Each **person/animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the face/muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
3997
+ It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. Each **person/animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the face/muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
3998
+
3999
+ **Automatic placement fan-out.** By default the canvas doesn't stop at one ratio: the hero renders once at the platform's primary ratio, then an `image_aspect_adapt` node recomposes it into that platform's full placement set, and the canvas `output` points at the adapted images. The placement set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or disable it with `--placements none` for a single base ad. The hero ratio passes through the adapt node **free**; each other ratio is a billed recomposition — and because adaptation re-generates, every ratio should be eyeballed for text and brand-color fidelity before publish (`ad-formats.md` in the scaffold skill covers this).
3996
4000
 
3997
4001
  The generation prompt is **model-aware and typography-hardened** (a static ad is the only path that bakes text *into* the image — video frames are clean plates). It resolves the image model's profile, so for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition, and it quotes every blueprint string with an instruction to render each **once, verbatim**, lock brand names letter-by-letter, and keep type crisp and legible — the fix for the #1 static-ad defect (garbled/duplicated/dropped copy). Run `baker canvas critique` on the scaffolded canvas for an advisory read on text-legibility risk, identity grounding, and brand-type fidelity before you spend.
3998
4002
 
@@ -4013,7 +4017,9 @@ baker canvas run ./static-ad.canvas.json
4013
4017
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
4014
4018
  | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
4015
4019
  | `--gen-model <id>` | registry default (`openai/gpt-5.4-image-2`) | Override the `image_generate` model. |
4016
- | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the output aspect ratio. |
4020
+ | `--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). |
4021
+ | `--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. |
4022
+ | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the hero's aspect ratio (clamped into the placement set so it stays a free pass-through). |
4017
4023
  | `--skip-font` | off | Skip the brand-font → type-specimen slot. |
4018
4024
  | `--skip-actor-sheets` | off | Ground each person/animal on its lone dropped photo instead of a generated multi-view reference sheet. |
4019
4025
 
@@ -1002,6 +1002,38 @@ function describeCause(c) {
1002
1002
  }
1003
1003
  }
1004
1004
 
1005
+ // ../canvas-contract/src/adPlatformFormats.ts
1006
+ var AD_FORMAT_PLATFORMS = [
1007
+ "meta_feed",
1008
+ "meta_feed_stories",
1009
+ "meta_all",
1010
+ "linkedin",
1011
+ "google_demandgen",
1012
+ "tiktok",
1013
+ "youtube"
1014
+ ];
1015
+ var AD_PLATFORM_FORMAT_PRESETS = {
1016
+ meta_feed: { formats: ["4:5"], note: "Meta/Instagram feed static." },
1017
+ meta_feed_stories: { formats: ["4:5", "9:16"], note: "Meta feed + Stories/Reels." },
1018
+ meta_all: { formats: ["4:5", "9:16", "1:1"], note: "Meta feed + Stories/Reels + square." },
1019
+ linkedin: { formats: ["1:1"], note: "LinkedIn single-image feed (square)." },
1020
+ google_demandgen: {
1021
+ // Renders 16:9 too so the off-grid 1.91:1 landscape has a native source to crop from.
1022
+ formats: ["1:1", "4:5", "16:9"],
1023
+ offGrid: ["1.91:1"],
1024
+ note: "Google Demand Gen; also wants 1.91:1 landscape \u2014 crop that from the rendered 16:9."
1025
+ },
1026
+ tiktok: { formats: ["9:16"], note: "TikTok full-screen vertical." },
1027
+ youtube: { formats: ["16:9"], note: "YouTube in-stream / companion display." }
1028
+ };
1029
+ function platformFormats(platform) {
1030
+ return AD_PLATFORM_FORMAT_PRESETS[platform].formats;
1031
+ }
1032
+ function resolveAdaptFormats(params) {
1033
+ if (params.platform) return platformFormats(params.platform);
1034
+ return params.formats ?? [];
1035
+ }
1036
+
1005
1037
  // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1006
1038
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1007
1039
  var configure = import__.default.configure;
@@ -1265,16 +1297,20 @@ var MODEL_REGISTRY = {
1265
1297
  },
1266
1298
  image_aspect_adapt: {
1267
1299
  // Multi-format adaptation of one creative. The recomposition prompt is
1268
- // baked into the backend handler; `formats` is the target-ratio list
1269
- // (ad-format set only the registry can't gate array elements, the node's
1270
- // Zod schema is the strict per-element gate). The source creative is a
1271
- // required input slot.
1300
+ // baked into the backend handler. The target ratios come from EITHER an
1301
+ // explicit `formats` list OR a `platform` preset that expands to the
1302
+ // canonical set; exactly one of the two is required that "one of" gate,
1303
+ // and per-element ratio validation, live in the node's Zod schema (the
1304
+ // registry can't express "one of" or gate array elements), so `formats`
1305
+ // is not listed as registry-required. The source creative is a required
1306
+ // input slot.
1272
1307
  "google/gemini-3-pro-image-preview": {
1273
1308
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1274
1309
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1275
- required: ["formats"],
1310
+ required: [],
1276
1311
  params: {
1277
1312
  formats: { kind: "json" },
1313
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1278
1314
  guidance: { kind: "string" },
1279
1315
  image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1280
1316
  }
@@ -1282,9 +1318,10 @@ var MODEL_REGISTRY = {
1282
1318
  "google/gemini-3.1-flash-image-preview": {
1283
1319
  label: "Google Gemini 3.1 Flash Image (Preview)",
1284
1320
  inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1285
- required: ["formats"],
1321
+ required: [],
1286
1322
  params: {
1287
1323
  formats: { kind: "json" },
1324
+ platform: { kind: "string", enum: AD_FORMAT_PLATFORMS },
1288
1325
  guidance: { kind: "string" },
1289
1326
  image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1290
1327
  }
@@ -6860,23 +6897,27 @@ var ASPECT_ADAPT_MODELS = ["google/gemini-3-pro-image-preview", "google/gemini-3
6860
6897
  var ASPECT_ADAPT_FORMATS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
6861
6898
  var ImageAspectAdaptParams = z18.object({
6862
6899
  model: z18.enum(ASPECT_ADAPT_MODELS),
6863
- formats: z18.array(z18.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }),
6900
+ formats: z18.array(z18.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
6901
+ platform: z18.enum(AD_FORMAT_PLATFORMS).optional(),
6864
6902
  guidance: z18.string().min(1).optional(),
6865
6903
  image_size: z18.enum(["0.5K", "1K", "2K", "4K"]).optional()
6866
- }).strict();
6904
+ }).strict().refine((p) => p.formats === void 0 !== (p.platform === void 0), {
6905
+ message: "provide exactly one of `formats` (explicit ratios) or `platform` (a placement preset)",
6906
+ path: ["formats"]
6907
+ });
6867
6908
  var imageAspectAdaptNode = delegated({
6868
6909
  id: "image_aspect_adapt",
6869
6910
  version: "1.0.0",
6870
6911
  category: "image",
6871
- summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as `formats`.",
6872
- when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and list the target ratios in `formats`. Cost is estimated per format; formats matching the source ratio are free pass-throughs. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
6912
+ summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. Give it either an explicit `formats` list OR a `platform` preset (meta_feed, meta_feed_stories, meta_all, linkedin, google_demandgen, tiktok, youtube) that expands to that platform's canonical set. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as the resolved format list.",
6913
+ when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and set EITHER `formats` (exact ratios) OR `platform` (e.g. `meta_feed_stories`). Cost is estimated per format; formats matching the source ratio are free pass-throughs. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here \u2014 crop them from the nearest ratio with `baker images crop`. Pick `google/gemini-3.1-flash-image-preview` (Nano Banana flash) while iterating, `google/gemini-3-pro-image-preview` (Nano Banana Pro) for final-quality adaptation.",
6873
6914
  inputs: z18.object({ source: ImageRef }).loose(),
6874
6915
  params: ImageAspectAdaptParams,
6875
6916
  outputs: z18.object({ images: z18.array(ImageRef).min(1) }).strict(),
6876
6917
  outputKinds: { images: "image" },
6877
6918
  cost: ({ params }) => {
6878
6919
  const p = params;
6879
- const count = Array.isArray(p?.formats) ? p.formats.length : 1;
6920
+ const count = Math.max(1, resolveAdaptFormats(p ?? {}).length);
6880
6921
  return {
6881
6922
  credits: count * (p?.model === "google/gemini-3-pro-image-preview" ? 20 : 5),
6882
6923
  seconds_estimate: 30 + 30 * count
@@ -7689,6 +7730,8 @@ export {
7689
7730
  RunAbortedError,
7690
7731
  LayerExecutionError,
7691
7732
  describeFailureReason,
7733
+ AD_FORMAT_PLATFORMS,
7734
+ platformFormats,
7692
7735
  SEEDANCE_DURATIONS,
7693
7736
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
7694
7737
  IMAGE_GENERATE_MODELS,
@@ -7719,4 +7762,4 @@ export {
7719
7762
  defaultRegistry,
7720
7763
  createEngineFromEnv
7721
7764
  };
7722
- //# sourceMappingURL=chunk-MWR25ZMU.js.map
7765
+ //# sourceMappingURL=chunk-UBFU3E7L.js.map