@koda-sl/baker-cli 0.180.0 → 0.181.1

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
@@ -25,6 +25,7 @@ import {
25
25
  isPersistedAssetRef,
26
26
  looksLikeHttpUrl,
27
27
  nearestSupportedAspectRatio,
28
+ nearestSupportedImageSize,
28
29
  parseRefExpr,
29
30
  platformFormats,
30
31
  requireCredentialsFromEnv,
@@ -32,11 +33,12 @@ import {
32
33
  sha256Hex,
33
34
  spineInputFlags,
34
35
  spineInputOps,
36
+ supportsLastFrame,
35
37
  supportsParam,
36
38
  toModelSafeImage,
37
39
  ulid,
38
40
  validateCanvasDeep
39
- } from "./chunk-P2T3IZRE.js";
41
+ } from "./chunk-RWHEFQXI.js";
40
42
  import {
41
43
  csvOrJson,
42
44
  daysAgoIso,
@@ -3412,6 +3414,7 @@ var imageGenerateModelSchema = z11.enum([
3412
3414
  // Legacy — see the registry entry; kept so pre-switch canvases still run.
3413
3415
  "openai/gpt-5.4-image-2",
3414
3416
  "google/gemini-3.1-flash-image-preview",
3417
+ "google/gemini-3.1-flash-lite-image",
3415
3418
  "google/gemini-3-pro-image-preview",
3416
3419
  "recraft/recraft-v4.1-pro-vector"
3417
3420
  ]);
@@ -19770,6 +19773,20 @@ function videoResolutionParam(videoModel, resolution) {
19770
19773
  if (!VIDEO_MODELS_WITH_RESOLUTION.has(videoModel)) return {};
19771
19774
  return { resolution: resolution ?? DEFAULT_VIDEO_RESOLUTION };
19772
19775
  }
19776
+ var VIDEO_MODELS_WITH_DURATION = new Set(
19777
+ Object.entries(MODEL_REGISTRY.video_generate).filter(([, spec]) => "duration" in spec.params).map(([id]) => id)
19778
+ );
19779
+ var VIDEO_MODELS_WITH_AUDIO_TOGGLE = new Set(
19780
+ Object.entries(MODEL_REGISTRY.video_generate).filter(([, spec]) => "generate_audio" in spec.params).map(([id]) => id)
19781
+ );
19782
+ function videoDurationParam(videoModel, duration) {
19783
+ if (!VIDEO_MODELS_WITH_DURATION.has(videoModel)) return {};
19784
+ return { duration };
19785
+ }
19786
+ function videoAudioParam(videoModel, generateAudio) {
19787
+ if (!VIDEO_MODELS_WITH_AUDIO_TOGGLE.has(videoModel)) return {};
19788
+ return { generate_audio: generateAudio };
19789
+ }
19773
19790
  var WORDS_PER_SECOND = 2.5;
19774
19791
  function wordCount(text) {
19775
19792
  return text.trim().split(/\s+/).filter(Boolean).length;
@@ -20374,6 +20391,7 @@ function extendPresenceByPromptMentions(slots, blueprint) {
20374
20391
  });
20375
20392
  }
20376
20393
  var ACTOR_SHEET_MODEL = "google/gemini-3-pro-image-preview";
20394
+ var KEYFRAME_IMAGE_SIZE = "2K";
20377
20395
  var SHEET_SUBJECT_TYPE = {
20378
20396
  person: "person",
20379
20397
  animal: "character",
@@ -20563,8 +20581,11 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
20563
20581
  const genParams = {
20564
20582
  model: ctx.imageModel,
20565
20583
  // gpt-image-2 derives pixel dimensions from the ratio and has no size knob,
20566
- // so asking for 2K there is an `unknown_param` at validate.
20567
- ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: "2K" } : {},
20584
+ // so asking for 2K there is an `unknown_param` at validate. Models that DO take
20585
+ // one still differ in how far up they go — Nano Banana 2 Lite renders 1K only —
20586
+ // so snap to the best tier the chosen model actually offers rather than pinning
20587
+ // a literal that a cheaper model would reject outright.
20588
+ ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: nearestSupportedImageSize("image_generate", ctx.imageModel, KEYFRAME_IMAGE_SIZE) } : {},
20568
20589
  // Per-model image defaults (gpt-image: quality=high — OpenRouter forwards it; we do
20569
20590
  // NOT send input_fidelity, which gpt-image-2 forces high automatically).
20570
20591
  ...imageProfile?.paramDefaults ?? {},
@@ -20854,11 +20875,11 @@ function emitSceneClip(i, scene, present2, mode, nativeTurn, ambientBroll, frame
20854
20875
  opts.uiRouted,
20855
20876
  opts.videoModel
20856
20877
  ),
20857
- duration: lengths.genDur,
20878
+ ...videoDurationParam(opts.videoModel, lengths.genDur),
20858
20879
  ...videoResolutionParam(opts.videoModel, opts.resolution),
20859
- // Native talking scene → Seedance generates the spoken audio + lip-sync; an opt-in
20880
+ // Native talking scene → the model generates the spoken audio + lip-sync; an opt-in
20860
20881
  // ambient b-roll beat generates diegetic ambient only; otherwise the clip is silent.
20861
- generate_audio: Boolean(nativeTurn) || ambientBroll,
20882
+ ...videoAudioParam(opts.videoModel, Boolean(nativeTurn) || ambientBroll),
20862
20883
  // Intent-keyed "astonishing default" overrides — merged LAST so a hero/reveal
20863
20884
  // beat can claim the 1080p ceiling over the aspect-derived resolution.
20864
20885
  ...clipParamRecipe(profile, clipIntentOf(scene, i))
@@ -20867,7 +20888,13 @@ function emitSceneClip(i, scene, present2, mode, nativeTurn, ambientBroll, frame
20867
20888
  nodes.push({
20868
20889
  id: `s${i}${tag}_clip`,
20869
20890
  type: "video_generate",
20870
- inputs: { first_frame: frames.first, ...frames.last ? { last_frame: frames.last } : {} },
20891
+ // An end frame can exist purely as the NEXT scene's splice frame, so having one
20892
+ // is not permission to wire it — a model that takes a single conditioning image
20893
+ // silently drops it.
20894
+ inputs: {
20895
+ first_frame: frames.first,
20896
+ ...frames.last && supportsLastFrame(opts.videoModel) ? { last_frame: frames.last } : {}
20897
+ },
20871
20898
  params: clipParams
20872
20899
  });
20873
20900
  const base = `$ref:s${i}${tag}_clip.video`;
@@ -21010,7 +21037,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
21010
21037
  const startPrompt = region.frame_prompt ?? scene.start_frame_prompt;
21011
21038
  const endPrompt = region.frame_prompt ?? scene.end_frame_prompt;
21012
21039
  const first = buildFrameRef("start", void 0, startPrompt, regionSlots, ctx, nodes);
21013
- const last = buildFrameRef("end", void 0, endPrompt, regionSlots, ctx, nodes);
21040
+ const last = supportsLastFrame(opts.videoModel) ? buildFrameRef("end", void 0, endPrompt, regionSlots, ctx, nodes) : void 0;
21014
21041
  const regionNative = isPresenter ? nativeTurn : void 0;
21015
21042
  const regionScene = {
21016
21043
  ...scene,
@@ -21578,14 +21605,16 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
21578
21605
  );
21579
21606
  const lastShown = phrase.shownScenes[phrase.shownScenes.length - 1] ?? anchor;
21580
21607
  const lastScene = env.blueprint.scenes[lastShown] ?? anchorScene;
21581
- const last = buildFrameRef(
21608
+ const modelTakesLastFrame = supportsLastFrame(env.opts.videoModel);
21609
+ const nextSplicesFromEnd = Boolean(env.blueprint.scenes[lastShown + 1]?.continues_previous);
21610
+ const last = modelTakesLastFrame || nextSplicesFromEnd ? buildFrameRef(
21582
21611
  "end",
21583
21612
  lastScene.end_frame_asset?.url,
21584
21613
  lastScene.end_frame_prompt,
21585
21614
  slotsForFrame(env.slots, lastShown, "end"),
21586
21615
  ctx,
21587
21616
  nodes
21588
- );
21617
+ ) : void 0;
21589
21618
  const clipStart = phrase.shownScenes.reduce(
21590
21619
  (m, s) => Math.min(m, env.blueprint.scenes[s]?.start_s ?? phrase.start_s),
21591
21620
  phrase.start_s
@@ -21608,19 +21637,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
21608
21637
  env.uiRouted.has(anchor),
21609
21638
  env.opts.videoModel
21610
21639
  ),
21611
- duration: genDur,
21640
+ ...videoDurationParam(env.opts.videoModel, genDur),
21612
21641
  ...videoResolutionParam(env.opts.videoModel, env.opts.resolution),
21613
- generate_audio: true,
21642
+ ...videoAudioParam(env.opts.videoModel, true),
21614
21643
  ...clipParamRecipe(clipProfile, clipIntentOf(anchorScene, anchor))
21615
21644
  };
21616
21645
  clipParams.aspect_ratio = env.genAr;
21617
21646
  nodes.push({
21618
21647
  id: `s${anchor}_clip`,
21619
21648
  type: "video_generate",
21620
- inputs: { first_frame: first, last_frame: last },
21649
+ inputs: { first_frame: first, ...last && modelTakesLastFrame ? { last_frame: last } : {} },
21621
21650
  params: clipParams
21622
21651
  });
21623
- out.phraseEnd = { ref: last, scene: lastShown };
21652
+ out.phraseEnd = last ? { ref: last, scene: lastShown } : void 0;
21624
21653
  const clipRef = `$ref:s${anchor}_clip.video`;
21625
21654
  const speechOffset = Math.max(0, phrase.start_s - clipStart);
21626
21655
  const extractLen = Math.min(Math.max(0.5, phrase.end_s - phrase.start_s), Math.max(0.5, genDur - speechOffset));
@@ -21828,14 +21857,15 @@ function brollKeyframes(scene, i, env, ctx, lengths, prevEndFrame, nodes) {
21828
21857
  nodes
21829
21858
  );
21830
21859
  const nextContinues = Boolean(env.blueprint.scenes[i + 1]?.continues_previous);
21831
- const last = lengths.trimTarget <= 4 && !nextContinues ? void 0 : buildFrameRef(
21860
+ const guidesMotion = supportsLastFrame(env.opts.videoModel) && lengths.trimTarget > 4;
21861
+ const last = guidesMotion || nextContinues ? buildFrameRef(
21832
21862
  "end",
21833
21863
  scene.end_frame_asset?.url,
21834
21864
  scene.end_frame_prompt,
21835
21865
  slotsForFrame(env.slots, i, "end"),
21836
21866
  ctx,
21837
21867
  nodes
21838
- );
21868
+ ) : void 0;
21839
21869
  return { first, last, sharesPrevFrame };
21840
21870
  }
21841
21871
  function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
@@ -22685,7 +22715,7 @@ var VIDEO_GUIDE = [
22685
22715
  "1. Edit each frame's prompt IN PLACE. Every `s<i>_start` keyframe node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want. The frame is RECAST to the el_* reference images you drop (the source ad's people are never reused), so describe pose/action/framing here and let the references carry identity.",
22686
22716
  "1b. STORYBOARD FIRST \u2014 align the look on the cheap stills before clips bill. Each scene's keyframe IS your storyboard; `metadata.video.motion_board` lays out each scene's frame, time window, spoken line, and the graphics scheduled in it. Lock the keyframes + check each graphic lands on its spoken beat, THEN run the clips (images are cheap, videos aren't; the cache re-bills only what you change). See references/video-flow.md.",
22687
22717
  "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
22688
- "3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by Seedance itself (the line is in the clip's prompt + `generate_audio`), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
22718
+ "3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by the video model itself (the line goes in the clip's prompt; models with a `generate_audio` toggle get it set, and the default model always renders audio), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
22689
22719
  "4. Voice consistency: every native talking clip's audio is re-voiced to ONE brand voice via `audio_voice_convert` (timing preserved \u2192 lips stay matched). Confirm the `voice_select` casting (one per speaker) \u2014 its `voice_id` is the brand voice; set its gender/language so the voice matches the creator.",
22690
22720
  "5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you source (`baker images icon/sticker/gif/logo`) and drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
22691
22721
  "6. `baker canvas validate` (proves native-audio + timing for free) then `baker canvas run` (generates many billed image/video/audio assets \u2014 not free).",
@@ -22762,9 +22792,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
22762
22792
  voice_description: d.voice_description,
22763
22793
  line: d.line
22764
22794
  })),
22765
- talking_head_note: "SHOT-NATIVE: a presenter shot is ONE Seedance clip (its line quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip whose on-camera windows are cut inside the spine's per-input chains (no extra nodes). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
22795
+ talking_head_note: "SHOT-NATIVE: a presenter shot is ONE video clip (its line quoted in s<anchor>_clip's prompt, plus generate_audio on the models that expose it) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip whose on-camera windows are cut inside the spine's per-input chains (no extra nodes). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
22766
22796
  voice_note: "ONE voice per person: a single voice_select is reused across all that person's shots (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Every presenter clip's native audio is extracted and re-voiced to that brand voice through a SINGLE merged audio_voice_convert per speaker (<voice>_conv, eleven_multilingual_sts_v2, timing preserved so lips stay matched) \u2014 so timbre stays consistent across the separate shot clips. Set voice_select.voice_id's gender/language to match the creator.",
22767
- native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 Seedance exposes no word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point). NOTE: a native clip's voice is extracted for the SPOKEN window only (`s<i>_voextract`'s `-t`) \u2014 when the scene runs longer than the line, the picture tail is deliberately silent (extracting the full clip would put post-line breathing/babble on the voice bus); trim the scene or extend the line if the dead air reads wrong.",
22797
+ native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 no video model exposes word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point). NOTE: a native clip's voice is extracted for the SPOKEN window only (`s<i>_voextract`'s `-t`) \u2014 when the scene runs longer than the line, the picture tail is deliberately silent (extracting the full clip would put post-line breathing/babble on the voice bus); trim the scene or extend the line if the dead air reads wrong.",
22768
22798
  craft: {
22769
22799
  note: "Production-craft principles that raise every clip's realism. Full rationale: references/video-craft.md (production craft); references/script-craft.md + meta-ads-playbook for the hook/message layer.",
22770
22800
  principles: [
@@ -24967,6 +24997,7 @@ var SEEDANCE = "bytedance/seedance-2.0";
24967
24997
  var VEO = "google/veo-3.1";
24968
24998
  var VEO_FAST = "google/veo-3.1-fast";
24969
24999
  var KLING = "kwaivgi/kling-v3.0-pro";
25000
+ var GEMINI_OMNI = "google/gemini-omni-flash";
24970
25001
  function routeVideoModel(input) {
24971
25002
  const budget = input.budget ?? "standard";
24972
25003
  const signals = [];
@@ -24979,8 +25010,8 @@ function routeVideoModel(input) {
24979
25010
  if (input.needsIdentity && !input.hasRealFace) {
24980
25011
  signals.push({ model: SEEDANCE, weight: 300, because: "identity/product consistency \u2192 Seedance workhorse" });
24981
25012
  }
24982
- signals.push({ model: SEEDANCE, weight: 100, because: "default identity/product workhorse" });
24983
- const scores = { [SEEDANCE]: 0, [VEO]: 0, [VEO_FAST]: 0, [KLING]: 0 };
25013
+ signals.push({ model: GEMINI_OMNI, weight: 100, because: "default \u2014 native audio in one call" });
25014
+ const scores = { [SEEDANCE]: 0, [VEO]: 0, [VEO_FAST]: 0, [KLING]: 0, [GEMINI_OMNI]: 0 };
24984
25015
  let top = signals[0];
24985
25016
  for (const s of signals) {
24986
25017
  scores[s.model] = (scores[s.model] ?? 0) + s.weight;
@@ -35116,8 +35147,11 @@ Then stage changes (nothing is sent to Tag Manager until publish):
35116
35147
  baker tag-manager tag create --json '{"name":"GA4 Quote","type":"gaawe","firingTriggerId":["gtm_temp_trigger_..."]}'
35117
35148
  baker tag-manager draft list \u2014 review everything staged
35118
35149
 
35119
- On publish, staged changes are written into a new Tag Manager version that is NOT published.
35120
- Tell the user to publish that version in Tag Manager to make it live.
35150
+ On publish, staged changes are written into the container and closed as a new Tag Manager
35151
+ version, and a clean run PUBLISHES that version \u2014 so the changes go live on the user's site.
35152
+ Say they go live when the user publishes the chat; never that they are live already, and never
35153
+ that someone still has to approve them inside Tag Manager. (If the publish call itself fails the
35154
+ version stands unpublished, and \`draft list\` says so and what is left to do.)
35121
35155
 
35122
35156
  This owns the CONTENTS of the container. Installing the container snippet on the site is a
35123
35157
  different job \u2014 that is \`baker tags\` with the googleTagManager tag.