@koda-sl/baker-cli 0.281.1-dev.1f1c09c80 → 0.281.3-dev.1f1c09c80

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
@@ -32778,14 +32778,19 @@ var AdSpec = z30.object({
32778
32778
  message: "give `avatar` (a cast avatar's handle) or `description` \u2014 a cast with neither grounds on nothing"
32779
32779
  }).optional(),
32780
32780
  /**
32781
- * A bed, described in words. Omitted leaves the ad dry the engine only wires
32782
- * music when asked, and silence is a legitimate choice.
32781
+ * The bed, described in words or `false` for an ad that is meant to be dry.
32783
32782
  *
32784
- * The bed never plays over the first beat: the opening line is the
32785
- * scroll-stopper and has to read clean. That is the engine's rule, not this
32786
- * one, and it is why the prompt is all this needs to carry.
32783
+ * Omitted lays a soft, neutral bed. It used to leave the ad silent, on the reasoning
32784
+ * that silence is a legitimate choice; it is, but only as a choice. In practice nothing
32785
+ * ever set this, and finished ads shipped with a voice over nothing which reads as
32786
+ * unfinished, not as restraint. A bed is the ordinary case, so it is the default, and
32787
+ * an ad that wants silence says `false` and means it.
32788
+ *
32789
+ * The bed never plays over the first beat: the opening line is the scroll-stopper and
32790
+ * has to read clean. That is the engine's rule, not this one, and it is why the prompt
32791
+ * is all this needs to carry.
32787
32792
  */
32788
- music: z30.string().min(1).optional(),
32793
+ music: z30.union([z30.string().min(1), z30.literal(false)]).optional(),
32789
32794
  /**
32790
32795
  * How the ad closes.
32791
32796
  *
@@ -32981,6 +32986,11 @@ function staticTranscript(spec) {
32981
32986
  }
32982
32987
  return out;
32983
32988
  }
32989
+ function musicBed(spec) {
32990
+ if (spec.music === false) return null;
32991
+ if (typeof spec.music === "string") return spec.music;
32992
+ return "A soft, warm instrumental bed that sits well under a spoken voiceover: gentle acoustic guitar or soft piano with light pads, steady and unobtrusive, no vocals, no lyrics, no drop, no build to a climax, nothing that pulls attention off the voice.";
32993
+ }
32984
32994
  function beatToCamera(spec, beat, isClosingCard) {
32985
32995
  if (isClosingCard) return false;
32986
32996
  if (beat.on_camera !== void 0) return beat.on_camera;
@@ -33081,7 +33091,7 @@ function adSpecToBlueprint(spec) {
33081
33091
  }
33082
33092
  } : {},
33083
33093
  ...spec.brand?.palette?.length ? { style: { palette: spec.brand.palette.map((hex) => ({ hex })) } } : {},
33084
- ...spec.music ? { music: { music_prompt: spec.music } } : {}
33094
+ ...musicBed(spec) ? { music: { music_prompt: musicBed(spec) } } : {}
33085
33095
  },
33086
33096
  scenes
33087
33097
  };
@@ -33243,7 +33253,7 @@ function pickBrandMark(files) {
33243
33253
  const score = (f) => {
33244
33254
  const name = f.toLowerCase();
33245
33255
  let s = 0;
33246
- if (name.endsWith(".svg")) s += 4;
33256
+ if (/\.(png|webp)$/.test(name)) s += 1;
33247
33257
  if (/\b(logo|wordmark|lockup)\b/.test(name)) s += 2;
33248
33258
  if (/\b(mono|white|black|inverse|icon|favicon|mark-only|isotipo)\b/.test(name)) s -= 3;
33249
33259
  return s;
@@ -33268,6 +33278,17 @@ async function readBrandFromWorkspace(root = ".") {
33268
33278
  function svgIsLiveText(svg) {
33269
33279
  return /<text[\s>]/i.test(svg);
33270
33280
  }
33281
+ async function markDirectoryIsReal(dir) {
33282
+ const files = await readdir8(dir).catch(() => null);
33283
+ if (!files) return false;
33284
+ const marks = files.filter((f) => /\.(svg|png|webp)$/i.test(f));
33285
+ if (marks.length === 0) return false;
33286
+ for (const f of marks.filter((f2) => /\.svg$/i.test(f2))) {
33287
+ const svg = await readFile15(path22.join(dir, f), "utf-8").catch(() => null);
33288
+ if (svg === null || svgIsLiveText(svg)) return false;
33289
+ }
33290
+ return true;
33291
+ }
33271
33292
 
33272
33293
  // src/commands/canvas/composition-path.ts
33273
33294
  import { existsSync as existsSync4 } from "fs";
@@ -33449,20 +33470,44 @@ var scaffoldAdCommand = defineCommand105({
33449
33470
  return;
33450
33471
  }
33451
33472
  if (resolvedLogo?.toLowerCase().endsWith(".svg")) {
33452
- const svg = await readFile16(resolvedLogo, "utf-8").catch(() => "");
33453
- if (svg && svgIsLiveText(svg)) {
33473
+ const svg = await readFile16(resolvedLogo, "utf-8").catch(() => null);
33474
+ if (svg === null) {
33475
+ writeJson({
33476
+ ok: false,
33477
+ error: {
33478
+ code: "VALIDATION_ERROR",
33479
+ message: `The brand mark at \`${resolvedLogo}\` could not be read, so there is no way to tell whether it is a real logo or a typed stand-in. An ad is not shipped on a mark nobody could open.`,
33480
+ fix: `Check the file is there and readable, or put the client's logo in \`${BRAND_DIR}/logos/\` \u2014 a PNG at 1000px or wider, or an outlined SVG.`
33481
+ }
33482
+ });
33483
+ process.exit(1);
33484
+ return;
33485
+ }
33486
+ if (svgIsLiveText(svg)) {
33454
33487
  writeJson({
33455
33488
  ok: false,
33456
33489
  error: {
33457
33490
  code: "VALIDATION_ERROR",
33458
33491
  message: `The mark at \`${resolvedLogo}\` draws its wordmark with live \`<text>\`, not outlines. That is a typed stand-in, not a logo: it renders in whatever font the machine happens to have, and the one that shipped this way came out reading "greenle\xE1" in every frame.`,
33459
- fix: `Ask the client for their real logo file and put it in \`${BRAND_DIR}/logos/\` \u2014 an outlined SVG, or a PNG at 1000px or wider. Do not redraw it.`
33492
+ fix: `Ask the client for their real logo file and put it in \`${BRAND_DIR}/logos/\` \u2014 the one their designer delivered, as an outlined SVG or a PNG they gave you. Do not redraw it, and do not export this file to PNG: rasterising it only bakes the invented letters into pixels, where nothing downstream can tell they are wrong.`
33460
33493
  }
33461
33494
  });
33462
33495
  process.exit(1);
33463
33496
  return;
33464
33497
  }
33465
33498
  }
33499
+ if (resolvedLogo && !await markDirectoryIsReal(path24.dirname(resolvedLogo))) {
33500
+ writeJson({
33501
+ ok: false,
33502
+ error: {
33503
+ code: "VALIDATION_ERROR",
33504
+ message: `There is no real logo in \`${BRAND_DIR}/logos/\` \u2014 what is there is a typed stand-in, whose wordmark is drawn as live text in a font the renderer does not have. An ad built on it wears letters the machine invented, and converting it to a PNG only bakes them in.`,
33505
+ fix: `Ask the client for their actual logo file and put it in \`${BRAND_DIR}/logos/\` \u2014 the one their designer delivered, as an outlined SVG or a PNG they gave you. Do not redraw it and do not export the stand-in to another format.`
33506
+ }
33507
+ });
33508
+ process.exit(1);
33509
+ return;
33510
+ }
33466
33511
  const blueprint = adSpecToBlueprint(spec.data);
33467
33512
  const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
33468
33513
  const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));