@koda-sl/baker-cli 0.306.6 → 0.306.8

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/README.md CHANGED
@@ -3736,6 +3736,8 @@ baker studio animate "she keeps walking, camera stays with her, then she stops a
3736
3736
 
3737
3737
  **A rendered clip is not usable anywhere until it is kept** — `keep` is what ingests it into the video library. Unkept takes are never ingested, which is what makes a rejected batch cheap.
3738
3738
 
3739
+ **One take of a presenter is an asset; an ad is not.** From the second clip in a conversation that carries a scripted line, `animate` warns that an ad is being assembled by hand and names `baker canvas scaffold-ad` instead. Separate takes have no cut between them, no continuous read, no captions and no end card, so three takes of a presenter are three clips and never one ad. It is a hint and never a refusal — splitting one long read into takes you then cut together is a legitimate shape, and building on a previous take (`--from-clip` / `--from-video`) is not flagged at all.
3740
+
3739
3741
  ### `baker studio get` / `list` / `keep` / `skills`
3740
3742
 
3741
3743
  ```bash
package/dist/cli.js CHANGED
@@ -22665,6 +22665,20 @@ var lookalikeSpecSchema = z30.object({
22665
22665
  ratio: z30.number().min(0.01).max(0.2).optional(),
22666
22666
  country: z30.string().length(2).optional()
22667
22667
  });
22668
+ function valuesOfKey(node, key) {
22669
+ if (Array.isArray(node)) return node.flatMap((entry) => valuesOfKey(entry, key));
22670
+ if (!node || typeof node !== "object") return [];
22671
+ const record = node;
22672
+ return [
22673
+ ...key in record ? [record[key]] : [],
22674
+ ...Object.values(record).flatMap((value) => valuesOfKey(value, key))
22675
+ ];
22676
+ }
22677
+ function mixedEventSourceTypes(rule) {
22678
+ const types = valuesOfKey(rule, "event_sources").flatMap((sources) => Array.isArray(sources) ? sources : []).map((source) => source?.type).filter((type) => typeof type === "string");
22679
+ const distinct = [...new Set(types)].sort();
22680
+ return distinct.length > 1 ? distinct : [];
22681
+ }
22668
22682
  var customAudienceCreateSchema = z30.object({
22669
22683
  name: z30.string().min(1).max(META_LIMITS.audience.nameMax),
22670
22684
  subtype: z30.enum(CUSTOM_AUDIENCE_SUBTYPES),
@@ -22681,6 +22695,21 @@ var customAudienceCreateSchema = z30.object({
22681
22695
  if ((p.subtype === "WEBSITE" || p.subtype === "ENGAGEMENT") && !p.rule) {
22682
22696
  ctx.addIssue({ code: "custom", path: ["rule"], message: `${p.subtype} audiences need a rule (use --file)` });
22683
22697
  }
22698
+ if (p.rule && p.retention_days !== void 0) {
22699
+ ctx.addIssue({
22700
+ code: "custom",
22701
+ path: ["retention_days"],
22702
+ message: "a rule carries its own window in retention_seconds, so drop retention_days \u2014 Meta takes it only on a customer-file audience, and sending both leaves the review showing a window the audience will not have"
22703
+ });
22704
+ }
22705
+ const mixed = mixedEventSourceTypes(p.rule);
22706
+ if (mixed.length > 0) {
22707
+ ctx.addIssue({
22708
+ code: "custom",
22709
+ path: ["rule"],
22710
+ message: `one audience covers one kind of source, but this rule mixes ${mixed.join(" and ")} \u2014 split it into one audience per source`
22711
+ });
22712
+ }
22684
22713
  });
22685
22714
  var customAudienceUpdateSchema = z30.object({
22686
22715
  name: z30.string().min(1).max(META_LIMITS.audience.nameMax).optional(),
@@ -23421,6 +23450,7 @@ var audiencesCreateCommand2 = defineCommand57({
23421
23450
  name: "create",
23422
23451
  description: `Stage a new custom audience shell. ${STAGED_NOTE2}
23423
23452
  LOOKALIKE/WEBSITE/ENGAGEMENT specifics go via --file (lookalike_spec / rule). Member-list upload is not staged in v1.
23453
+ A WEBSITE/ENGAGEMENT rule carries its own window in retention_seconds, so --retention-days is refused next to one, and one audience covers one kind of event source (a Page and an Instagram profile are two audiences).
23424
23454
  Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WEBSITE --file rule.json`
23425
23455
  },
23426
23456
  args: {
@@ -23428,7 +23458,10 @@ Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WE
23428
23458
  name: { type: "string", description: "Audience name" },
23429
23459
  subtype: { type: "string", description: "CUSTOM|WEBSITE|ENGAGEMENT|LOOKALIKE" },
23430
23460
  description: { type: "string", description: "Description" },
23431
- "retention-days": { type: "string", description: "Retention window in days" },
23461
+ "retention-days": {
23462
+ type: "string",
23463
+ description: "Retention window in days \u2014 customer-file audiences only; a rule carries its own retention_seconds"
23464
+ },
23432
23465
  file: { type: "string", description: "JSON file with lookalike_spec / rule / full payload" }
23433
23466
  },
23434
23467
  run: async ({ args }) => {
@@ -58456,6 +58489,43 @@ import { defineCommand as defineCommand209 } from "citty";
58456
58489
  // src/commands/studio/animate.ts
58457
58490
  import { defineCommand as defineCommand201 } from "citty";
58458
58491
 
58492
+ // src/commands/studio/spokenPace.ts
58493
+ var WORDS_PER_SECOND3 = SPOKEN_WORDS_PER_SECOND;
58494
+ var spokenWords = spokenWordsIn;
58495
+ function wordsThatFit(seconds) {
58496
+ return Math.floor(seconds * WORDS_PER_SECOND3);
58497
+ }
58498
+ function spokenPaceHint(prompt, seconds, model) {
58499
+ const words2 = spokenWords(prompt);
58500
+ if (words2 === 0) return null;
58501
+ const split = model ? splitScriptFor(words2, model) : null;
58502
+ if (split) {
58503
+ 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).`;
58504
+ }
58505
+ if (!seconds) return null;
58506
+ const fits = wordsThatFit(seconds);
58507
+ if (words2 <= fits) return null;
58508
+ const rate = (words2 / seconds).toFixed(1);
58509
+ 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.`;
58510
+ }
58511
+
58512
+ // src/commands/studio/adByHand.ts
58513
+ function priorScriptedTakes(batches) {
58514
+ return batches.filter(
58515
+ (batch) => batch.kind === "video" && spokenWords(batch.prompt) > 0 && batch.variants.some((variant) => variant.status !== "failed")
58516
+ ).length;
58517
+ }
58518
+ function ordinal(n) {
58519
+ const suffix = n % 100 >= 11 && n % 100 <= 13 ? "th" : ["th", "st", "nd", "rd"][n % 10] ?? "th";
58520
+ return `${n}${suffix}`;
58521
+ }
58522
+ function adByHandHint(input) {
58523
+ if (!input.thisTakeIsScripted || input.buildsOnAClip) return null;
58524
+ if (input.priorScriptedTakes < 1) return null;
58525
+ const which = ordinal(input.priorScriptedTakes + 1);
58526
+ return `This is the ${which} clip with a scripted line in this conversation. Separate takes stay separate \u2014 nothing cuts them together, the audio stops and restarts at every join, and there are no captions and no closing card \u2014 so ${input.priorScriptedTakes + 1} takes of a presenter are ${input.priorScriptedTakes + 1} clips and never one ad. If these are the beats of one ad, stop rendering them one at a time: write them as beats (one line the voice says plus one shot on screen while it is said) and \`baker canvas scaffold-ad\` emits the whole ad \u2014 one canvas, one render, continuous read, synced captions and a brand end card. If they really are consecutive parts of ONE long read you will cut together yourself, carry on.`;
58527
+ }
58528
+
58459
58529
  // src/commands/studio/batch.ts
58460
58530
  function projectBatch(generation, full) {
58461
58531
  const compact3 = {
@@ -58670,26 +58740,6 @@ function uploadFailure(path48) {
58670
58740
  };
58671
58741
  }
58672
58742
 
58673
- // src/commands/studio/spokenPace.ts
58674
- var WORDS_PER_SECOND3 = SPOKEN_WORDS_PER_SECOND;
58675
- var spokenWords = spokenWordsIn;
58676
- function wordsThatFit(seconds) {
58677
- return Math.floor(seconds * WORDS_PER_SECOND3);
58678
- }
58679
- function spokenPaceHint(prompt, seconds, model) {
58680
- const words2 = spokenWords(prompt);
58681
- if (words2 === 0) return null;
58682
- const split = model ? splitScriptFor(words2, model) : null;
58683
- if (split) {
58684
- 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).`;
58685
- }
58686
- if (!seconds) return null;
58687
- const fits = wordsThatFit(seconds);
58688
- if (words2 <= fits) return null;
58689
- const rate = (words2 / seconds).toFixed(1);
58690
- 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.`;
58691
- }
58692
-
58693
58743
  // src/commands/studio/animate.ts
58694
58744
  var MODEL_LIST = VIDEO_MODEL_IDS;
58695
58745
  var DEFAULT_MAX_WAIT_MS = 6e5;
@@ -58931,6 +58981,25 @@ function costHintsFor(body) {
58931
58981
  }
58932
58982
  return hints2;
58933
58983
  }
58984
+ var PRIOR_TAKE_WINDOW = 20;
58985
+ async function adByHandHintFor(body) {
58986
+ const thisTakeIsScripted = spokenWords(body.prompt) > 0;
58987
+ const buildsOnAClip = (body.clipRefs?.length ?? 0) + (body.videoIds?.length ?? 0) > 0;
58988
+ if (!thisTakeIsScripted || buildsOnAClip) return null;
58989
+ try {
58990
+ const { generations } = await apiGet("/api/studio/generations", {
58991
+ kind: "video",
58992
+ limit: String(PRIOR_TAKE_WINDOW)
58993
+ });
58994
+ return adByHandHint({
58995
+ priorScriptedTakes: priorScriptedTakes(generations),
58996
+ thisTakeIsScripted,
58997
+ buildsOnAClip
58998
+ });
58999
+ } catch {
59000
+ return null;
59001
+ }
59002
+ }
58934
59003
  var animateCommand = defineCommand201({
58935
59004
  meta: {
58936
59005
  name: "animate",
@@ -59020,7 +59089,8 @@ var animateCommand = defineCommand201({
59020
59089
  }
59021
59090
  const { imageIds } = args.image ? await resolveLibraryImageIds(args.image, clipImageLimitFor(args)) : { imageIds: [] };
59022
59091
  const body = buildAnimateBody(args, prompt, imageIds);
59023
- const costHints = costHintsFor(body);
59092
+ const assembling = await adByHandHintFor(body);
59093
+ const preflightHints = [...assembling ? [assembling] : [], ...costHintsFor(body)];
59024
59094
  const start = await apiPost("/api/studio/animate", body);
59025
59095
  const maxWaitMs = resolveMaxWaitMs(args.wait, args["max-wait"], DEFAULT_MAX_WAIT_MS);
59026
59096
  if (maxWaitMs === 0) {
@@ -59029,7 +59099,7 @@ var animateCommand = defineCommand201({
59029
59099
  data: start,
59030
59100
  hints: [
59031
59101
  `Rendering ${start.count} take(s). Read them with \`baker studio get ${start.generationId}\`, then keep the one you want.`,
59032
- ...costHints
59102
+ ...preflightHints
59033
59103
  ]
59034
59104
  });
59035
59105
  return;
@@ -59039,14 +59109,14 @@ var animateCommand = defineCommand201({
59039
59109
  writeJson({
59040
59110
  ok: true,
59041
59111
  data: { ...start, status: "running" },
59042
- hints: [waitingHint(outcome, start.generationId, maxWaitMs), ...costHints]
59112
+ hints: [waitingHint(outcome, start.generationId, maxWaitMs), ...preflightHints]
59043
59113
  });
59044
59114
  return;
59045
59115
  }
59046
59116
  writeJson({
59047
59117
  ok: true,
59048
59118
  data: projectBatch(outcome.generation, args.full === true),
59049
- hints: [...batchHints(outcome.generation), ...costHints]
59119
+ hints: [...batchHints(outcome.generation), ...preflightHints]
59050
59120
  });
59051
59121
  } catch (error) {
59052
59122
  failStudioCommand(error, { withFix: true });