@koda-sl/baker-cli 0.281.3-dev.1f1c09c80 → 0.281.5-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
@@ -32791,6 +32791,24 @@ var AdSpec = z30.object({
32791
32791
  * is all this needs to carry.
32792
32792
  */
32793
32793
  music: z30.union([z30.string().min(1), z30.literal(false)]).optional(),
32794
+ /**
32795
+ * The place the ad keeps coming back to, held still across every shot.
32796
+ *
32797
+ * The person has an identity sheet; nothing held the SETTING. Each shot's start frame is
32798
+ * generated on its own, so three beats in "her living room" came back as three different
32799
+ * living rooms. The engine already builds `location` reference sheets — its own comment
32800
+ * calls them the strongest cross-scene consistency lock it has — and the ad path simply
32801
+ * never emitted one.
32802
+ *
32803
+ * `image` is a real picture of the place, and it is what makes this work at all: a sheet
32804
+ * fuses source material into a turnaround. Fed nothing, it is not a reference sheet, it
32805
+ * is `image_generate` with extra steps — the model registry says exactly that. Generate
32806
+ * the plate once with gpt-image-2 and pass its URL here.
32807
+ */
32808
+ set: z30.object({
32809
+ description: z30.string().min(1).max(400),
32810
+ image: z30.string().min(1).optional()
32811
+ }).optional(),
32794
32812
  /**
32795
32813
  * How the ad closes.
32796
32814
  *
@@ -32938,28 +32956,55 @@ function endCardCta(spec) {
32938
32956
  return declared.toLowerCase().replace(/[.!?]$/, "") === closing.replace(/[.!?]$/, "") ? null : declared;
32939
32957
  }
32940
32958
  var AD_CAST_ID = "customer";
32959
+ var AD_SET_ID = "set";
32960
+ function castedAvatarOnRoster(cast, roster) {
32961
+ if (!cast || cast.avatar || !cast.description) return null;
32962
+ const words2 = new Set(
32963
+ cast.description.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean)
32964
+ );
32965
+ for (const person of roster) {
32966
+ for (const label of [person.name, person.handle]) {
32967
+ const token = label.trim().toLowerCase();
32968
+ if (!token || token.includes("-") || token.length < 3) continue;
32969
+ if (words2.has(token)) return person.handle;
32970
+ }
32971
+ }
32972
+ return null;
32973
+ }
32941
32974
  function adSpecCastElements(spec, avatar) {
32942
- const description = (avatar?.subjectDescription ?? spec.cast?.description)?.trim();
32943
- if (!description) return [];
32944
32975
  const lastIndex = spec.beats.length - 1;
32976
+ const speakingScenes = spec.beats.map((_, i) => i).filter((i) => !(endCardWanted(spec) && i === lastIndex));
32977
+ const place = spec.set?.description?.trim();
32978
+ const elements = [];
32979
+ if (place && speakingScenes.length > 0) {
32980
+ elements.push({
32981
+ type: "location",
32982
+ label: AD_SET_ID,
32983
+ cast_id: AD_SET_ID,
32984
+ description: place,
32985
+ scenes: speakingScenes,
32986
+ ...spec.set?.image?.trim() ? { source_image: spec.set.image.trim() } : {}
32987
+ });
32988
+ }
32989
+ const description = (avatar?.subjectDescription ?? spec.cast?.description)?.trim();
32990
+ if (!description) return elements;
32945
32991
  const scenes = spec.beats.map((beat, i) => ({ beat, i })).filter(({ beat, i }) => beat.cast !== false && !(endCardWanted(spec) && i === lastIndex)).map(({ i }) => i);
32946
- if (scenes.length === 0) return [];
32992
+ if (scenes.length === 0) return elements;
32947
32993
  const sheet = avatar?.sheetUrl?.trim();
32948
- return [
32949
- {
32950
- type: "person",
32951
- label: AD_CAST_ID,
32952
- // Same id the dialogue names as speaker: that pairing is what lets the engine see
32953
- // the presenter as present in a scene and route it to native audio.
32954
- cast_id: AD_CAST_ID,
32955
- description,
32956
- scenes,
32957
- // An avatar's identity sheet IS the turnaround the engine would otherwise render,
32958
- // so it is handed over as one. Left unmarked, the run paid to generate a sheet OF
32959
- // a sheet and every frame grounded on the copy instead of the avatar.
32960
- ...sheet ? { source_image: sheet, source_is_sheet: true } : {}
32961
- }
32962
- ];
32994
+ elements.push({
32995
+ type: "person",
32996
+ label: AD_CAST_ID,
32997
+ // Same id the dialogue names as speaker: that pairing is what lets the engine see
32998
+ // the presenter as present in a scene and route it to native audio.
32999
+ cast_id: AD_CAST_ID,
33000
+ description,
33001
+ scenes,
33002
+ // An avatar's identity sheet IS the turnaround the engine would otherwise render,
33003
+ // so it is handed over as one. Left unmarked, the run paid to generate a sheet OF
33004
+ // a sheet and every frame grounded on the copy instead of the avatar.
33005
+ ...sheet ? { source_image: sheet, source_is_sheet: true } : {}
33006
+ });
33007
+ return elements;
32963
33008
  }
32964
33009
  function motionPrompt(beat, toCamera, isClosingCard, place, physics) {
32965
33010
  if (isClosingCard) return BRAND_PLATE;
@@ -33398,6 +33443,30 @@ var scaffoldAdCommand = defineCommand105({
33398
33443
  process.exit(1);
33399
33444
  return;
33400
33445
  }
33446
+ let autoCast = null;
33447
+ if (!flagAvatar && !spec.data.cast?.avatar && spec.data.cast?.description) {
33448
+ const roster = await readAvatars(
33449
+ "/api/avatars",
33450
+ { status: "ready" }
33451
+ ).catch(() => null);
33452
+ const named = roster ? castedAvatarOnRoster(spec.data.cast, roster.avatars) : null;
33453
+ if (named) {
33454
+ spec.data.cast = { ...spec.data.cast, avatar: named };
33455
+ autoCast = named;
33456
+ }
33457
+ }
33458
+ if (spec.data.set?.description && !spec.data.set.image?.trim()) {
33459
+ writeJson({
33460
+ ok: false,
33461
+ error: {
33462
+ code: "VALIDATION_ERROR",
33463
+ message: "This ad names a recurring place but gives no picture of it. A location sheet is fused from a real image; with nothing to fuse there is no sheet, and every shot invents the room again.",
33464
+ fix: `Make the plate once, then pass it: \`baker images generate "${spec.data.set.description}" --model openai/gpt-image-2\`, then put its url in the spec as \`"set": { "description": "\u2026", "image": "<url>" }\`. Drop \`set\` entirely if the ad does not keep returning to one place.`
33465
+ }
33466
+ });
33467
+ process.exit(1);
33468
+ return;
33469
+ }
33401
33470
  const handle = flagAvatar || spec.data.cast?.avatar?.trim();
33402
33471
  let avatar = null;
33403
33472
  let avatarError = null;
@@ -33477,7 +33546,7 @@ var scaffoldAdCommand = defineCommand105({
33477
33546
  error: {
33478
33547
  code: "VALIDATION_ERROR",
33479
33548
  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.`
33549
+ fix: `Check the file is there and readable, or put the client's logo in \`${BRAND_DIR}/logos/\` \u2014 the file their designer delivered \u2014 an outlined SVG, or the PNG they gave you.`
33481
33550
  }
33482
33551
  });
33483
33552
  process.exit(1);
@@ -33592,6 +33661,9 @@ var scaffoldAdCommand = defineCommand105({
33592
33661
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
33593
33662
  hints: [
33594
33663
  `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
33664
+ ...autoCast ? [
33665
+ `Cast \`${autoCast}\` \u2014 the spec described this person in words, and they are one of this company's avatars. Every shot now grounds on their identity sheet and they speak in their own pinned voice. Write \`"cast": { "avatar": "` + autoCast + '" }` yourself next time; a written description is for someone the company does not have.'
33666
+ ] : [],
33595
33667
  `These beats run about ${blueprint.estimated_duration_s}s. If the ask was longer, add beats \u2014 the length is the sum of the lines, so a 25s ad needs roughly 25s of script and cannot be stretched by holding shots.`,
33596
33668
  ...logoPath && !staged ? [`Could not read the logo at \`${logoPath}\` \u2014 the ad has no mark on screen. Give the path as it appears in src/brand/BRAND.md, relative to the workspace root.`] : [],
33597
33669
  ...staged ? ["The brand mark is drawn by the composition \u2014 top-left throughout, and large on the closing card. It is never generated as a picture, so it cannot come back garbled."] : [],