@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/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AD_FORMAT_PLATFORMS,
3
4
  BackendClient,
4
5
  BackendClient2,
5
6
  DEFAULT_VIDEO_GENERATE_MODEL,
@@ -23,6 +24,7 @@ import {
23
24
  imageProfileFor,
24
25
  isPersistedAssetRef,
25
26
  parseRefExpr,
27
+ platformFormats,
26
28
  requireCredentialsFromEnv,
27
29
  resolveConcurrency,
28
30
  sha256Hex,
@@ -31,7 +33,7 @@ import {
31
33
  toModelSafeImage,
32
34
  ulid,
33
35
  validateCanvasDeep
34
- } from "./chunk-MWR25ZMU.js";
36
+ } from "./chunk-UBFU3E7L.js";
35
37
  import {
36
38
  csvOrJson,
37
39
  daysAgoIso,
@@ -19418,6 +19420,9 @@ var SHEET_SUBJECT_TYPE2 = {
19418
19420
  };
19419
19421
  var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
19420
19422
  var ACTOR_SHEET_IMAGE_SIZE = "4K";
19423
+ var ADAPT_MODEL = "google/gemini-3-pro-image-preview";
19424
+ var ADAPT_IMAGE_SIZE = "2K";
19425
+ var ADAPT_GUIDANCE = "Keep the headline, logo, CTA, and hero subject fully visible in every ratio. Reproduce every text string verbatim \u2014 no dropped, added, or altered characters \u2014 and preserve the exact brand-color treatment (e.g. a black\u2192red word pivot), never flattening it.";
19421
19426
  var Blueprint = z15.object({
19422
19427
  meta: z15.object({ estimated_aspect_ratio: z15.string().optional() }).loose().optional(),
19423
19428
  text_content: z15.array(z15.object({ text: z15.string().optional() }).loose()).optional()
@@ -19498,6 +19503,16 @@ function aspectRatio(blueprint, opts) {
19498
19503
  const requested = opts.aspectRatio ?? blueprint.meta?.estimated_aspect_ratio;
19499
19504
  return requested && GEN_ASPECT_RATIOS.has(requested) ? requested : DEFAULT_ASPECT_RATIO;
19500
19505
  }
19506
+ function baseAspectRatio(blueprint, opts) {
19507
+ const requested = aspectRatio(blueprint, opts);
19508
+ if (!opts.placements) return requested;
19509
+ const formats = platformFormats(opts.placements);
19510
+ return formats.includes(requested) ? requested : formats[0] ?? requested;
19511
+ }
19512
+ function outputFormats(blueprint, opts) {
19513
+ if (!opts.placements) return [baseAspectRatio(blueprint, opts)];
19514
+ return [...platformFormats(opts.placements)];
19515
+ }
19501
19516
  function blueprintTextStrings(blueprint) {
19502
19517
  return (blueprint.text_content ?? []).map((t) => typeof t.text === "string" ? t.text.trim() : "").filter((t) => t.length > 0);
19503
19518
  }
@@ -19615,11 +19630,25 @@ function scaffoldStaticAd(input, elementsInput, opts) {
19615
19630
  inputs: { reference, target_blueprint: "$ref:prompt.asset" },
19616
19631
  params: {
19617
19632
  model: opts.genModel,
19618
- aspect_ratio: aspectRatio(blueprint, opts),
19633
+ aspect_ratio: baseAspectRatio(blueprint, opts),
19619
19634
  image_size: "2K",
19620
19635
  prompt
19621
19636
  }
19622
19637
  });
19638
+ const fanOut = opts.placements !== void 0;
19639
+ if (fanOut) {
19640
+ nodes.push({
19641
+ id: "adapt",
19642
+ type: "image_aspect_adapt",
19643
+ inputs: { source: "$ref:gen.images#0" },
19644
+ params: {
19645
+ model: ADAPT_MODEL,
19646
+ platform: opts.placements,
19647
+ guidance: ADAPT_GUIDANCE,
19648
+ image_size: ADAPT_IMAGE_SIZE
19649
+ }
19650
+ });
19651
+ }
19623
19652
  return {
19624
19653
  schema: "baker-canvas/1",
19625
19654
  metadata: {
@@ -19627,7 +19656,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
19627
19656
  description: "Scaffolded by `baker canvas scaffold-static-ad`. Edit `prompt.json` (the blueprint generated from the source image) into the ad you want, drop a real asset at each [TODO] ingest path (logo/product/subject/badge + brand font), then `baker canvas run`. The original is wired for composition only."
19628
19657
  },
19629
19658
  nodes,
19630
- output: { node: "gen", output: "images" }
19659
+ output: fanOut ? { node: "adapt", output: "images" } : { node: "gen", output: "images" }
19631
19660
  };
19632
19661
  }
19633
19662
  function staticAdReport(input, elementsInput, opts) {
@@ -19644,7 +19673,9 @@ function staticAdReport(input, elementsInput, opts) {
19644
19673
  })),
19645
19674
  includes_font: opts.includeFont !== false,
19646
19675
  actor_sheets: includeActorSheets ? assignElementLabels2(elements).filter(({ el }) => SHEET_SUBJECT_TYPE2[el.type.toLowerCase()]).map(({ label }) => label) : [],
19647
- aspect_ratio: aspectRatio(blueprint, opts)
19676
+ aspect_ratio: baseAspectRatio(blueprint, opts),
19677
+ placements: opts.placements ?? null,
19678
+ output_formats: outputFormats(blueprint, opts)
19648
19679
  };
19649
19680
  }
19650
19681
 
@@ -19664,6 +19695,12 @@ function resolveFormats(aspect) {
19664
19695
  const value = aspect?.trim();
19665
19696
  return value && FORMAT_VALUES.includes(value) ? [value] : ["4:5"];
19666
19697
  }
19698
+ function resolveFormatList(formats) {
19699
+ const valid = [...new Set(formats.map((f) => f.trim()))].filter(
19700
+ (f) => FORMAT_VALUES.includes(f)
19701
+ );
19702
+ return valid.length > 0 ? valid : ["4:5"];
19703
+ }
19667
19704
  function describeBlueprintIntent(blueprint) {
19668
19705
  const intent = blueprint?.ad_intent;
19669
19706
  if (typeof intent === "string" && intent.trim()) return intent.trim();
@@ -19964,6 +20001,25 @@ function resolveModels(args) {
19964
20001
  genModel: pick("gen-model", "image_generate", "openai/gpt-5.4-image-2")
19965
20002
  };
19966
20003
  }
20004
+ var PLATFORM_PLACEMENTS = {
20005
+ meta: "meta_feed_stories",
20006
+ google: "google_demandgen",
20007
+ linkedin: "linkedin",
20008
+ tiktok: "tiktok",
20009
+ youtube: "youtube",
20010
+ x: void 0,
20011
+ other: void 0
20012
+ };
20013
+ function resolvePlacements(placementsFlag, definitionPlatform) {
20014
+ const flag = placementsFlag?.trim();
20015
+ if (flag === void 0 || flag === "") return { ok: true, placements: PLATFORM_PLACEMENTS[definitionPlatform] };
20016
+ if (flag === "none") return { ok: true, placements: void 0 };
20017
+ if (AD_FORMAT_PLATFORMS.includes(flag)) return { ok: true, placements: flag };
20018
+ return {
20019
+ ok: false,
20020
+ message: `--placements "${flag}" is not a valid placement set. Use one of: ${AD_FORMAT_PLATFORMS.join(", ")}, or "none" for a single base ad.`
20021
+ };
20022
+ }
19967
20023
  function regionCount(layout) {
19968
20024
  const regions = layout?.regions;
19969
20025
  return Array.isArray(regions) ? regions.length : 0;
@@ -20037,7 +20093,7 @@ async function runVisionPasses(canvas) {
20037
20093
  var scaffoldStaticAdCommand = defineCommand91({
20038
20094
  meta: {
20039
20095
  name: "scaffold-static-ad",
20040
- description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. Edit prompt.json and drop the real assets, then `baker canvas run` it."
20096
+ description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. By default it also fans the hero out to the platform's placement ratios (via image_aspect_adapt \u2014 the hero ratio is free, the rest are billed AI recompositions); pass --placements none for a single base ad, or a preset to pick the set. Edit prompt.json and drop the real assets, then `baker canvas run` it."
20041
20097
  },
20042
20098
  args: {
20043
20099
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
@@ -20050,14 +20106,18 @@ var scaffoldStaticAdCommand = defineCommand91({
20050
20106
  title: { type: "string", description: "Creative title for _definition.md (default: title-cased slug)" },
20051
20107
  platform: {
20052
20108
  type: "string",
20053
- description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
20109
+ description: "Ad platform (meta|google|linkedin|tiktok|youtube|x|other; default meta). Sets _definition.md AND, unless --placements is given, the default placement set the hero auto-fans-out to (meta \u2192 4:5+9:16, google \u2192 1:1+4:5+16:9, linkedin \u2192 1:1, tiktok \u2192 9:16, youtube \u2192 16:9; x/other \u2192 none)."
20054
20110
  },
20055
20111
  advertiser: { type: "string", description: "Source advertiser recorded in _definition.md" },
20056
20112
  "describe-model": { type: "string", description: "Override the image_describe model id" },
20057
20113
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
20058
20114
  "layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
20059
20115
  "gen-model": { type: "string", description: "Override the image_generate model id" },
20060
- aspect: { type: "string", description: "Force the output aspect ratio (else inferred from the image)" },
20116
+ aspect: { type: "string", description: "Force the hero's aspect ratio (else inferred from the image)" },
20117
+ placements: {
20118
+ type: "string",
20119
+ description: "Placement set the hero auto-fans-out to (image_aspect_adapt). One of meta_feed | meta_feed_stories | meta_all | linkedin | google_demandgen | tiktok | youtube, or `none` for a single base ad. Default: derived from --platform (meta \u2192 feed + stories). Each extra ratio is a billed render; the hero ratio passes through free."
20120
+ },
20061
20121
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
20062
20122
  "skip-actor-sheets": {
20063
20123
  type: "boolean",
@@ -20117,6 +20177,13 @@ var scaffoldStaticAdCommand = defineCommand91({
20117
20177
  canvasImageIsUrl = true;
20118
20178
  }
20119
20179
  }
20180
+ const definitionPlatform = resolvePlatform(args.platform ? String(args.platform) : void 0);
20181
+ const placementsResult = resolvePlacements(
20182
+ args.placements ? String(args.placements) : void 0,
20183
+ definitionPlatform
20184
+ );
20185
+ if (!placementsResult.ok) return fail2("invalid_placements", placementsResult.message);
20186
+ const placements = placementsResult.placements;
20120
20187
  const opts = {
20121
20188
  genModel,
20122
20189
  imagePath: canvasImagePath,
@@ -20124,7 +20191,8 @@ var scaffoldStaticAdCommand = defineCommand91({
20124
20191
  blueprintPath: canvasBlueprintPath,
20125
20192
  aspectRatio: args.aspect ? String(args.aspect) : void 0,
20126
20193
  includeFont: !args["skip-font"],
20127
- includeActorSheets: !args["skip-actor-sheets"]
20194
+ includeActorSheets: !args["skip-actor-sheets"],
20195
+ placements
20128
20196
  };
20129
20197
  let canvas;
20130
20198
  let report;
@@ -20150,8 +20218,8 @@ var scaffoldStaticAdCommand = defineCommand91({
20150
20218
  buildCreativeDefinition({
20151
20219
  title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
20152
20220
  kind: "static",
20153
- platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
20154
- formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
20221
+ platform: definitionPlatform,
20222
+ formats: resolveFormatList(report.output_formats),
20155
20223
  sourceReferenceUrl: durableSourceUrl ?? (imageIsUrl ? imageSource : void 0),
20156
20224
  sourceAdvertiser: args.advertiser ? String(args.advertiser) : args.context ? String(args.context) : void 0,
20157
20225
  sourceKind: "image",
@@ -20164,8 +20232,8 @@ var scaffoldStaticAdCommand = defineCommand91({
20164
20232
  await syncCreativeDefinitionBestEffort({
20165
20233
  slug,
20166
20234
  title: args.title ? String(args.title) : titleFromSlug(slug),
20167
- platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
20168
- formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
20235
+ platform: definitionPlatform,
20236
+ formats: resolveFormatList(report.output_formats),
20169
20237
  sourceReferenceUrl: durableSourceUrl ?? (imageIsUrl ? imageSource : void 0),
20170
20238
  canvas,
20171
20239
  sourceSha: await computeSourceSha(outPath)
@@ -20182,6 +20250,8 @@ var scaffoldStaticAdCommand = defineCommand91({
20182
20250
  output: canvas.output,
20183
20251
  models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
20184
20252
  aspect_ratio: report.aspect_ratio,
20253
+ placements: report.placements,
20254
+ output_formats: report.output_formats,
20185
20255
  layout_regions: regionCount(layout),
20186
20256
  stats: {
20187
20257
  total_nodes: canvas.nodes.length,
@@ -20193,6 +20263,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20193
20263
  assets_to_supply: report.elements,
20194
20264
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
20195
20265
  actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
20266
+ placements: report.placements === null ? `Single format (${report.aspect_ratio}). Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 to auto-fan-out to a platform's set.` : `Auto-fans the approved hero out to ${report.placements} \u2192 ${report.output_formats.join(", ")}. The hero ratio (${report.aspect_ratio}) passes through free; each other ratio is a billed render, AI-recomposed (not cropped). Re-read the copy and confirm the brand-color treatment on EVERY ratio before publishing \u2014 adaptation re-generates and can silently mangle text or flatten a color device. Pass --placements none for a single base ad.`,
20196
20267
  note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
20197
20268
  }
20198
20269
  },