@koda-sl/baker-cli 0.305.0-dev.881741ff3 → 0.306.0-dev.2b6124eb2

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
@@ -67,7 +67,7 @@ import {
67
67
  validateCanvasDeep,
68
68
  withoutCastName,
69
69
  ytDlpBlockSignal
70
- } from "./chunk-ZILPAKE6.js";
70
+ } from "./chunk-UBNLCVGN.js";
71
71
  import {
72
72
  csvOrJson,
73
73
  daysAgoIso,
@@ -29920,7 +29920,7 @@ import { defineCommand as defineCommand101 } from "citty";
29920
29920
  import { defineCommand as defineCommand96 } from "citty";
29921
29921
 
29922
29922
  // src/commands/avatars/casting.ts
29923
- var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-ad`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their pinned VOICE and how they speak into a clip; without it the video model invents a different voice, and no later step can put theirs back.";
29923
+ var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-ad`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their ACCENT and delivery into a clip. An avatar has no pinned voice by design (ADR 0005): the video model renders the voice, and the region on their profile is the only thing steering it \u2014 without the flag the clip is performed flat, in whatever accent the model picks on its own.";
29924
29924
  var VERBATIM_RULE = "When you do write the description yourself \u2014 a hand-built canvas node, a landing image, anywhere `--avatar` does not exist \u2014 copy `subjectDescription` VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set.";
29925
29925
  var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included. Scaffolding a video creative: `baker canvas scaffold-video \u2026 --real-face`.";
29926
29926
  function castingHints(avatar) {
@@ -30020,7 +30020,7 @@ function creationHints(created, profile) {
30020
30020
  ...casting,
30021
30021
  ...thin ? [thin] : [],
30022
30022
  ...castingHints({ handle: created.handle, status: created.status }),
30023
- `Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what makes it speak in this avatar's own voice.`
30023
+ `Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what carries their accent and delivery into the read.`
30024
30024
  ];
30025
30025
  if (created.likeness === "licensed") {
30026
30026
  hints2.push(
@@ -30224,8 +30224,6 @@ registerSchema({
30224
30224
  description: "The company's statement that it may use this real person's image in advertising. Required with --source-image. Only the user can give this \u2014 ask them, never assume it.",
30225
30225
  required: false
30226
30226
  },
30227
- "voice-id": { type: "string", description: "Voice id to speak with in video", required: false },
30228
- "voice-description": { type: "string", description: "How the voice should sound", required: false },
30229
30227
  face: {
30230
30228
  type: "string",
30231
30229
  description: "Who picks the face, for an invented avatar: 'ask' (default) blocks on the user through `request_avatar_casting`, 'auto' keeps the one this build casts and asks nobody. Use 'auto' only when there is no one to answer \u2014 a Schedule, a batch, an explicit \"do it yourself\" \u2014 because a person who is there almost always wants this choice.",
@@ -30270,8 +30268,6 @@ var createCommand2 = defineCommand96({
30270
30268
  required: false,
30271
30269
  default: false
30272
30270
  },
30273
- "voice-id": { type: "string", description: "Voice id", required: false },
30274
- "voice-description": { type: "string", description: "How the voice should sound", required: false },
30275
30271
  face: {
30276
30272
  type: "string",
30277
30273
  description: "Who picks the face of an invented avatar: 'ask' (default, blocks on the user) or 'auto' (asks nobody)",
@@ -30339,8 +30335,6 @@ var createCommand2 = defineCommand96({
30339
30335
  ...handle ? { handle } : {},
30340
30336
  profile: { ...profile, subjectDescription },
30341
30337
  ...sourceImageIds ? { sourceImageIds, likenessConfirmed: true } : {},
30342
- ...args["voice-id"] ? { voiceId: args["voice-id"] } : {},
30343
- ...args["voice-description"] ? { voiceDescription: args["voice-description"] } : {},
30344
30338
  ...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {},
30345
30339
  // Sent so the backend can hold the sheet until the chosen portrait exists —
30346
30340
  // otherwise it fuses one from a face nobody picked and pays for it twice.
@@ -36963,6 +36957,22 @@ async function reviewRenderedVideo(opts) {
36963
36957
  await rm5(dir, { recursive: true, force: true });
36964
36958
  }
36965
36959
  }
36960
+ async function reviewContextBeside(canvasPath) {
36961
+ const raw = await readFile11(path15.join(path15.dirname(canvasPath), "prompt.json"), "utf-8").catch(() => null);
36962
+ if (!raw) return {};
36963
+ try {
36964
+ const blueprint = JSON.parse(raw);
36965
+ const global = blueprint.global ?? {};
36966
+ const text2 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
36967
+ return {
36968
+ ...text2(global.market) ? { market: text2(global.market) } : {},
36969
+ ...text2(global.branding?.name) ? { subject: text2(global.branding?.name) } : {},
36970
+ ...text2(global.transcript) ? { script: text2(global.transcript) } : {}
36971
+ };
36972
+ } catch {
36973
+ return {};
36974
+ }
36975
+ }
36966
36976
  async function sceneWindowsBeside(canvasPath) {
36967
36977
  const dir = path15.join(path15.dirname(canvasPath), "scenes");
36968
36978
  const files = await readdir6(dir).catch(() => null);
@@ -37321,6 +37331,7 @@ ${describeRewrites(healed.rewrites)}
37321
37331
  const review = reviewable ? await reviewRenderedVideo({
37322
37332
  video: reviewable,
37323
37333
  windows: await sceneWindowsBeside(filePath),
37334
+ ...await reviewContextBeside(filePath),
37324
37335
  brief: opts.brief?.trim() || void 0,
37325
37336
  onSpend: (usage) => reviewUsage.push(usage)
37326
37337
  }) : null;
@@ -38644,7 +38655,11 @@ var AdSpec = z37.object({
38644
38655
  end_card: z37.union([
38645
38656
  z37.literal(false),
38646
38657
  z37.object({
38647
- /** The words on the button. Defaults to the last beat's line. */
38658
+ /**
38659
+ * The words on the button. No default — with none the card carries the mark
38660
+ * alone, because falling back to the last beat's line printed that sentence
38661
+ * twice on the closing frame. See `endCardCta`.
38662
+ */
38648
38663
  cta: z37.string().min(1).optional()
38649
38664
  })
38650
38665
  ]).optional(),
@@ -38971,6 +38986,9 @@ function adSpecToBlueprint(spec, opts = {}) {
38971
38986
  estimated_duration_s: Math.round(clock * 100) / 100,
38972
38987
  global: {
38973
38988
  transcript: spec.beats.map((b) => b.say).join(" "),
38989
+ // Resolved, not declared: `marketFor` falls back to the voice language and then to
38990
+ // the script itself, so an ad that never named a place still says where it is set.
38991
+ ...market ? { market } : {},
38974
38992
  voiceover: {
38975
38993
  mode: voiceoverMode(spec, nativeSpeech),
38976
38994
  ...spokenVariety(spec, avatarAccent) ? { accent: spokenVariety(spec, avatarAccent) } : {},
@@ -38996,8 +39014,9 @@ function adSpecToBlueprint(spec, opts = {}) {
38996
39014
  scenes
38997
39015
  };
38998
39016
  }
39017
+ var MIN_BEAT_WORDS = 4;
38999
39018
  function beatsTooShortToPlay(spec) {
39000
- return spec.beats.map((beat, i) => ({ i, spoken: beat.say.trim().split(/\s+/).filter(Boolean).length / WORDS_PER_SECOND2 })).filter(({ spoken }) => spoken < SPOKEN_CLIP_DURATION_S).map(({ i }) => i + 1);
39019
+ return spec.beats.map((beat, i) => ({ i, words: beat.say.trim().split(/\s+/).filter(Boolean).length })).filter(({ words: words2 }) => words2 < MIN_BEAT_WORDS).map(({ i }) => i + 1);
39001
39020
  }
39002
39021
 
39003
39022
  // src/engine/scaffold/ad-brand-overlay.ts
@@ -39248,7 +39267,7 @@ registerSchema({
39248
39267
  spec: {
39249
39268
  type: "string",
39250
39269
  required: true,
39251
- description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. A beat\'s `sound` is one sound EFFECT for that shot \u2014 and MOST BEATS SHOULD NOT HAVE ONE. Add it only where the picture shows a specific physical event the viewer expects to hear (a drill tightening a bolt, a door closing, rain on glass, a graphic snapping in). On a talking head or a calm lifestyle shot it reads as a stock sting, and one per beat turns a quiet ad into a noisy one. A music bed plus two well-placed effects beats eight. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
39270
+ description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. A beat\'s `sound` is one sound EFFECT for that shot \u2014 and MOST BEATS SHOULD NOT HAVE ONE. Add it only where the picture shows a specific physical event the viewer expects to hear (a drill tightening a bolt, a door closing, rain on glass, a graphic snapping in). On a talking head or a calm lifestyle shot it reads as a stock sting, and one per beat turns a quiet ad into a noisy one. A music bed plus two well-placed effects beats eight. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy \u2014 there is no default, so with no `cta` the card carries the mark alone, and a `cta` repeating the closing line is dropped rather than printed twice. `false` keeps the footage.'
39252
39271
  },
39253
39272
  avatar: {
39254
39273
  type: "string",
@@ -39468,14 +39487,15 @@ var scaffoldAdCommand = defineCommand111({
39468
39487
  process.exit(1);
39469
39488
  return;
39470
39489
  }
39471
- if (!noBrand && resolvedLogo?.toLowerCase().endsWith(".svg")) {
39472
- const svg = await readFile16(resolvedLogo, "utf-8").catch(() => null);
39490
+ const markPath = spec.data.brand?.logo?.trim();
39491
+ if (!noBrand && markPath?.toLowerCase().endsWith(".svg")) {
39492
+ const svg = await readFile16(markPath, "utf-8").catch(() => null);
39473
39493
  if (svg === null) {
39474
39494
  writeJson({
39475
39495
  ok: false,
39476
39496
  error: {
39477
39497
  code: "VALIDATION_ERROR",
39478
- 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.`,
39498
+ message: `The brand mark at \`${markPath}\` 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.`,
39479
39499
  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.`
39480
39500
  }
39481
39501
  });
@@ -39487,7 +39507,7 @@ var scaffoldAdCommand = defineCommand111({
39487
39507
  ok: false,
39488
39508
  error: {
39489
39509
  code: "VALIDATION_ERROR",
39490
- 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.`,
39510
+ message: `The mark at \`${markPath}\` 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.`,
39491
39511
  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.`
39492
39512
  }
39493
39513
  });
@@ -39495,7 +39515,7 @@ var scaffoldAdCommand = defineCommand111({
39495
39515
  return;
39496
39516
  }
39497
39517
  }
39498
- if (resolvedLogo && !await markIsReal(resolvedLogo)) {
39518
+ if (!noBrand && markPath && !await markIsReal(markPath)) {
39499
39519
  writeJson({
39500
39520
  ok: false,
39501
39521
  error: {
@@ -39621,7 +39641,7 @@ var scaffoldAdCommand = defineCommand111({
39621
39641
  // stops, the frames are shown, and continuing pays for none of them twice.
39622
39642
  handle || staged ? `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --until frames --brief "<what they asked for, their words>"\`, SHOW the frames it returns and ask whether to carry on \u2014 then continue with the \`--run-id\` it gives you. This ad carries ${handle ? "a person's face" : "the client's mark"}, and a wrong look costs a whole ad to discover after the clips.` : `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --brief "<what they asked for, their words>"\`.`,
39623
39643
  ...autoCast ? [
39624
- `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.'
39644
+ `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 carries their accent and delivery. Write \`"cast": { "avatar": "` + autoCast + '" }` yourself next time; a written description is for someone the company does not have.'
39625
39645
  ] : [],
39626
39646
  `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.`,
39627
39647
  ...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.`] : [],
@@ -39669,7 +39689,7 @@ var scaffoldAdCommand = defineCommand111({
39669
39689
  function shortBeatHint(short, total) {
39670
39690
  if (short.length === 0) return [];
39671
39691
  return [
39672
- `Beat${short.length === 1 ? "" : "s"} ${short.join(", ")} of ${total} ${short.length === 1 ? "is" : "are"} under ${SPOKEN_CLIP_DURATION_S}s \u2014 a beat lasts exactly as long as its line takes to read, so a two-word line is a three-second shot, and a run of those plays as a slideshow rather than an ad. Do NOT pad them: that buys seconds of somebody standing still. Merge them, so each beat carries a whole thought \u2014 an ad of this length usually wants two or three beats, not six. Edit the spec and re-run this command.`
39692
+ `Beat${short.length === 1 ? "" : "s"} ${short.join(", ")} of ${total} ${short.length === 1 ? "is" : "are"} a fragment rather than a line \u2014 a few words with a whole shot to themselves. Every beat's picture holds for at least ${SPOKEN_CLIP_DURATION_S}s, so a two-word beat buys six seconds of somebody standing still, and a run of those plays as a slideshow rather than an ad. Do NOT pad them. Merge them into the beat before or after, so each one carries a whole thought. Edit the spec and re-run this command.`
39673
39693
  ];
39674
39694
  }
39675
39695
  async function fetchClientLogo() {