@koda-sl/baker-cli 0.305.0-dev.4e8ef1cd7 → 0.305.0-dev.582739c3e

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
@@ -16,6 +16,7 @@ import {
16
16
  RunAbortedError,
17
17
  SEEDANCE_DURATIONS,
18
18
  SEEDANCE_PROFILE,
19
+ SPOKEN_CLIP_DURATION_S,
19
20
  SPOKEN_WORDS_PER_SECOND,
20
21
  VIDEO_GENERATE_MODELS,
21
22
  ValidationError,
@@ -65,7 +66,7 @@ import {
65
66
  ulid,
66
67
  validateCanvasDeep,
67
68
  ytDlpBlockSignal
68
- } from "./chunk-7TL4VPK5.js";
69
+ } from "./chunk-JI6QIAD3.js";
69
70
  import {
70
71
  csvOrJson,
71
72
  daysAgoIso,
@@ -38557,6 +38558,7 @@ var CURRENCY_BY_MARKET = {
38557
38558
  "the united states": "$ (US dollars)"
38558
38559
  };
38559
38560
  var MARKERS_TO_NAME_A_PLACE = 3;
38561
+ var WORDS_PER_SECOND2 = 3.06;
38560
38562
  var MARKET_BY_LANGUAGE = {
38561
38563
  es: "Spain",
38562
38564
  "es-es": "Spain",
@@ -38629,9 +38631,10 @@ function marketFor(spec) {
38629
38631
  }
38630
38632
  function beatSeconds(line, videoModel) {
38631
38633
  const words2 = line.trim().split(/\s+/).length;
38632
- const spoken = words2 / 3.06;
38634
+ const spoken = words2 / WORDS_PER_SECOND2;
38633
38635
  const allowed = (videoModel ? clipDurationsFor(videoModel) : void 0) ?? [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
38634
- return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
38636
+ const wanted = Math.max(spoken, SPOKEN_CLIP_DURATION_S);
38637
+ return allowed.find((d) => d >= wanted) ?? allowed[allowed.length - 1];
38635
38638
  }
38636
38639
  var BRAND_PLATE = "A solid flat brand colour plate \u2014 an end card. No people, no objects, no scenery, no text of any kind.";
38637
38640
  var ASKS_FOR_LEGIBLE = /\b(focus on the (number|amount|total|price|figure|text)|readable|legible|clearly (shows?|visible)|highlighted (number|amount|total)|showing the (number|amount|total|price))\b/i;
@@ -38861,6 +38864,22 @@ function adSpecToBlueprint(spec, opts = {}) {
38861
38864
  scenes
38862
38865
  };
38863
38866
  }
38867
+ function beatsTooShortToPlay(spec) {
38868
+ 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);
38869
+ }
38870
+ function withoutCastName(text2, names) {
38871
+ let out = text2;
38872
+ for (const name of names) {
38873
+ const token = name.trim();
38874
+ if (token.length < 3) continue;
38875
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${escapeForPattern(token)}(?![\\p{L}\\p{N}])`, "giu");
38876
+ out = out.replace(pattern, "the person in the reference");
38877
+ }
38878
+ return out;
38879
+ }
38880
+ function escapeForPattern(value) {
38881
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38882
+ }
38864
38883
 
38865
38884
  // src/engine/scaffold/ad-brand-overlay.ts
38866
38885
  var markFallback = (name) => name.trim() ? ` onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:this.className+' brand-wordmark',textContent:${JSON.stringify(name.trim())}}))"` : "";
@@ -39362,6 +39381,13 @@ var scaffoldAdCommand = defineCommand111({
39362
39381
  process.exit(1);
39363
39382
  return;
39364
39383
  }
39384
+ if (handle) {
39385
+ const names = [handle, avatar?.name].filter((value) => Boolean(value));
39386
+ spec.data.beats = spec.data.beats.map((beat) => ({ ...beat, show: withoutCastName(beat.show, names) }));
39387
+ if (spec.data.cast?.description) {
39388
+ spec.data.cast = { ...spec.data.cast, description: withoutCastName(spec.data.cast.description, names) };
39389
+ }
39390
+ }
39365
39391
  const blueprint = adSpecToBlueprint(spec.data, { avatarAccent: avatar?.profile?.accent, videoModel: chosenVideoModel });
39366
39392
  const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
39367
39393
  const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
@@ -39453,6 +39479,9 @@ var scaffoldAdCommand = defineCommand111({
39453
39479
  ok: true,
39454
39480
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
39455
39481
  hints: [
39482
+ // Before anything else: a beat too short to play is a beat to merge, and it is
39483
+ // free to fix here and expensive to discover in a finished ad.
39484
+ ...shortBeatHint(beatsTooShortToPlay(spec.data), spec.data.beats.length),
39456
39485
  ...noBrand ? [
39457
39486
  "Built with NO brand mark and no closing card, because you passed --no-brand. Say that in your reply: the user is getting an unbranded ad and should know it, not discover it. When they do have a logo, put it in `src/brand/logos/` and re-run without the flag."
39458
39487
  ] : [],
@@ -39511,6 +39540,12 @@ var scaffoldAdCommand = defineCommand111({
39511
39540
  });
39512
39541
  }
39513
39542
  });
39543
+ function shortBeatHint(short, total) {
39544
+ if (short.length === 0) return [];
39545
+ return [
39546
+ `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.`
39547
+ ];
39548
+ }
39514
39549
 
39515
39550
  // src/commands/canvas/scaffold-video.ts
39516
39551
  import { access as access2, cp as cp2, mkdir as mkdir8, readFile as readFile19, rm as rm7, writeFile as writeFile10 } from "fs/promises";
@@ -57795,23 +57830,23 @@ function uploadFailure(path47) {
57795
57830
  }
57796
57831
 
57797
57832
  // src/commands/studio/spokenPace.ts
57798
- var WORDS_PER_SECOND2 = SPOKEN_WORDS_PER_SECOND;
57833
+ var WORDS_PER_SECOND3 = SPOKEN_WORDS_PER_SECOND;
57799
57834
  var spokenWords = spokenWordsIn;
57800
57835
  function wordsThatFit(seconds) {
57801
- return Math.floor(seconds * WORDS_PER_SECOND2);
57836
+ return Math.floor(seconds * WORDS_PER_SECOND3);
57802
57837
  }
57803
57838
  function spokenPaceHint(prompt, seconds, model) {
57804
57839
  const words2 = spokenWords(prompt);
57805
57840
  if (words2 === 0) return null;
57806
57841
  const split = model ? splitScriptFor(words2, model) : null;
57807
57842
  if (split) {
57808
- return `The line is ${words2} words \u2014 about ${Math.ceil(words2 / WORDS_PER_SECOND2)}s of speech \u2014 and ${model} renders at most ${split.maxSeconds}s (~${Math.floor(split.maxSeconds * WORDS_PER_SECOND2)} words). It will not refuse and it will not cut: it will RUSH, swallowing syllables. Split the script into ${split.clips} takes of roughly ${split.wordsPerClip} words each, rendered separately and cut together \u2014 break it where the thought breaks, not mid-sentence. Or move to a model that renders longer (bytedance/seedance-2.5 goes to 30s).`;
57843
+ return `The line is ${words2} words \u2014 about ${Math.ceil(words2 / WORDS_PER_SECOND3)}s of speech \u2014 and ${model} renders at most ${split.maxSeconds}s (~${Math.floor(split.maxSeconds * WORDS_PER_SECOND3)} words). It will not refuse and it will not cut: it will RUSH, swallowing syllables. Split the script into ${split.clips} takes of roughly ${split.wordsPerClip} words each, rendered separately and cut together \u2014 break it where the thought breaks, not mid-sentence. Or move to a model that renders longer (bytedance/seedance-2.5 goes to 30s).`;
57809
57844
  }
57810
57845
  if (!seconds) return null;
57811
57846
  const fits = wordsThatFit(seconds);
57812
57847
  if (words2 <= fits) return null;
57813
57848
  const rate = (words2 / seconds).toFixed(1);
57814
- return `The line is ${words2} words and this clip is ${seconds}s \u2014 that is ${rate} words a second against a natural ${WORDS_PER_SECOND2}. The model will not run over, it will RUSH: syllables swallowed, endings clipped, and the take unusable however good the picture. Cut the line to about ${fits} words, or give it the length it needs (${Math.ceil(words2 / WORDS_PER_SECOND2)}s). Do not re-render the same line at the same length and hope.`;
57849
+ return `The line is ${words2} words and this clip is ${seconds}s \u2014 that is ${rate} words a second against a natural ${WORDS_PER_SECOND3}. The model will not run over, it will RUSH: syllables swallowed, endings clipped, and the take unusable however good the picture. Cut the line to about ${fits} words, or give it the length it needs (${Math.ceil(words2 / WORDS_PER_SECOND3)}s). Do not re-render the same line at the same length and hope.`;
57815
57850
  }
57816
57851
 
57817
57852
  // src/commands/studio/animate.ts