@koda-sl/baker-cli 0.281.0-dev.1f1c09c80 → 0.281.2-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
@@ -32699,9 +32699,17 @@ var Beat = z30.object({
32699
32699
  */
32700
32700
  sound: z30.string().min(1).max(200).optional(),
32701
32701
  /**
32702
- * Set only when this beat's subject is talking to camera. Off by default, and
32703
- * the default is load-bearing: a person visibly speaking under a voice that is
32704
- * not theirs reads as bad dubbing, which is what sank the first cut of this.
32702
+ * Whether this beat's subject is talking to camera.
32703
+ *
32704
+ * Left unset it follows the cast: an ad that cast a presenter, in a shot they are in,
32705
+ * with a line, is that person speaking. Set it to FALSE for the exception — a cutaway,
32706
+ * a product shot, a beat you want a narrator to carry over.
32707
+ *
32708
+ * It used to be opt-in, back when the only route was dubbing and OFF was the safe
32709
+ * default. That inverted the moment on-camera started meaning the video model voices
32710
+ * the clip itself: measured on a real run, a brief that said "talking to camera all the
32711
+ * way through" produced an ad with her face on screen and a narrator cast from a prose
32712
+ * description reading her lines, because nothing set this flag.
32705
32713
  */
32706
32714
  on_camera: z30.boolean().optional(),
32707
32715
  /**
@@ -32770,14 +32778,19 @@ var AdSpec = z30.object({
32770
32778
  message: "give `avatar` (a cast avatar's handle) or `description` \u2014 a cast with neither grounds on nothing"
32771
32779
  }).optional(),
32772
32780
  /**
32773
- * A bed, described in words. Omitted leaves the ad dry the engine only wires
32774
- * 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.
32775
32782
  *
32776
- * The bed never plays over the first beat: the opening line is the
32777
- * scroll-stopper and has to read clean. That is the engine's rule, not this
32778
- * 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.
32779
32792
  */
32780
- music: z30.string().min(1).optional(),
32793
+ music: z30.union([z30.string().min(1), z30.literal(false)]).optional(),
32781
32794
  /**
32782
32795
  * How the ad closes.
32783
32796
  *
@@ -32948,10 +32961,10 @@ function adSpecCastElements(spec, avatar) {
32948
32961
  }
32949
32962
  ];
32950
32963
  }
32951
- function motionPrompt(beat, isClosingCard, place, physics) {
32964
+ function motionPrompt(beat, toCamera, isClosingCard, place, physics) {
32952
32965
  if (isClosingCard) return BRAND_PLATE;
32953
32966
  const shot = `${beat.show}${place}${physics}`;
32954
- if (beat.on_camera) return shot;
32967
+ if (toCamera) return shot;
32955
32968
  return `${shot} Nobody in frame is speaking: mouths CLOSED and STILL throughout, no talking, no lip movement. Whoever is in shot is looking, reacting or working \u2014 never addressing the camera.`;
32956
32969
  }
32957
32970
  function staticTranscript(spec) {
@@ -32973,11 +32986,21 @@ function staticTranscript(spec) {
32973
32986
  }
32974
32987
  return out;
32975
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
+ }
32994
+ function beatToCamera(spec, beat, isClosingCard) {
32995
+ if (isClosingCard) return false;
32996
+ if (beat.on_camera !== void 0) return beat.on_camera;
32997
+ return Boolean(spec.cast) && beat.cast !== false;
32998
+ }
32976
32999
  function wantsNativeSpeech(spec, closesOnCard) {
32977
33000
  if (!spec.cast || spec.voiceover === false) return false;
32978
33001
  const last = spec.beats.length - 1;
32979
33002
  const speaking = spec.beats.filter((_, i) => !(closesOnCard && i === last));
32980
- return speaking.length > 0 && speaking.every((beat) => beat.on_camera === true && beat.cast !== false);
33003
+ return speaking.length > 0 && speaking.every((beat) => beatToCamera(spec, beat, false) && beat.cast !== false);
32981
33004
  }
32982
33005
  function beatDialogue(spec, beat, window2) {
32983
33006
  if (spec.voiceover === false) return [];
@@ -32986,12 +33009,9 @@ function beatDialogue(spec, beat, window2) {
32986
33009
  line: beat.say,
32987
33010
  start_s: window2.start,
32988
33011
  end_s: window2.end,
32989
- // `on_camera` is the engine's own switch for "this line is narration" — absent, it
32990
- // guesses from who is in frame, which is how a still photograph once got lip-synced.
32991
- // Never true on the closing plate: it is a flat colour card with no one in it, and
32992
- // saying someone is on camera there sent the engine looking for a mouth to lip-sync
32993
- // on a picture that has no person.
32994
- on_camera: beat.on_camera === true && !window2.isClosingCard,
33012
+ // Always stated, never left to the engine: absent, it guesses from who is in frame,
33013
+ // which is how a still photograph once got lip-synced.
33014
+ on_camera: beatToCamera(spec, beat, window2.isClosingCard),
32995
33015
  // Naming the CAST as speaker is the other half of what sends this beat down the
32996
33016
  // native path. Left unnamed, the speaker defaults to "voiceover" — a narrator, who is
32997
33017
  // by definition never on camera — so every beat took the dubbed path: a clip generated
@@ -33044,7 +33064,7 @@ function adSpecToBlueprint(spec) {
33044
33064
  isClosingCard,
33045
33065
  nativeSpeech
33046
33066
  }),
33047
- motion_prompt: motionPrompt(beat, isClosingCard, place, physics),
33067
+ motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place, physics),
33048
33068
  start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
33049
33069
  };
33050
33070
  });
@@ -33071,7 +33091,7 @@ function adSpecToBlueprint(spec) {
33071
33091
  }
33072
33092
  } : {},
33073
33093
  ...spec.brand?.palette?.length ? { style: { palette: spec.brand.palette.map((hex) => ({ hex })) } } : {},
33074
- ...spec.music ? { music: { music_prompt: spec.music } } : {}
33094
+ ...musicBed(spec) ? { music: { music_prompt: musicBed(spec) } } : {}
33075
33095
  },
33076
33096
  scenes
33077
33097
  };
@@ -33233,7 +33253,7 @@ function pickBrandMark(files) {
33233
33253
  const score = (f) => {
33234
33254
  const name = f.toLowerCase();
33235
33255
  let s = 0;
33236
- if (name.endsWith(".svg")) s += 4;
33256
+ if (/\.(png|webp)$/.test(name)) s += 1;
33237
33257
  if (/\b(logo|wordmark|lockup)\b/.test(name)) s += 2;
33238
33258
  if (/\b(mono|white|black|inverse|icon|favicon|mark-only|isotipo)\b/.test(name)) s -= 3;
33239
33259
  return s;
@@ -33439,8 +33459,20 @@ var scaffoldAdCommand = defineCommand105({
33439
33459
  return;
33440
33460
  }
33441
33461
  if (resolvedLogo?.toLowerCase().endsWith(".svg")) {
33442
- const svg = await readFile16(resolvedLogo, "utf-8").catch(() => "");
33443
- if (svg && svgIsLiveText(svg)) {
33462
+ const svg = await readFile16(resolvedLogo, "utf-8").catch(() => null);
33463
+ if (svg === null) {
33464
+ writeJson({
33465
+ ok: false,
33466
+ error: {
33467
+ code: "VALIDATION_ERROR",
33468
+ 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.`,
33469
+ 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.`
33470
+ }
33471
+ });
33472
+ process.exit(1);
33473
+ return;
33474
+ }
33475
+ if (svgIsLiveText(svg)) {
33444
33476
  writeJson({
33445
33477
  ok: false,
33446
33478
  error: {