@vibeframe/cli 0.113.3 → 0.113.4

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.
@@ -15,6 +15,7 @@ export interface BuildPlanBeat {
15
15
  assets: {
16
16
  narration: AssetPlan | null;
17
17
  backdrop: AssetPlan | null;
18
+ keyframe: AssetPlan | null;
18
19
  video: AssetPlan | null;
19
20
  music: AssetPlan | null;
20
21
  };
@@ -93,6 +94,7 @@ export interface CreateBuildPlanOptions {
93
94
  skipNarration?: boolean;
94
95
  skipBackdrop?: boolean;
95
96
  skipVideo?: boolean;
97
+ skipKeyframe?: boolean;
96
98
  skipMusic?: boolean;
97
99
  ttsProvider?: string;
98
100
  voice?: string;
@@ -81,6 +81,23 @@ export type SceneBuildProgressEvent = {
81
81
  type: "backdrop-skipped";
82
82
  beatId: string;
83
83
  reason: string;
84
+ } | {
85
+ type: "keyframe-cached";
86
+ beatId: string;
87
+ path: string;
88
+ } | {
89
+ type: "keyframe-generated";
90
+ beatId: string;
91
+ path: string;
92
+ provider: string;
93
+ } | {
94
+ type: "keyframe-failed";
95
+ beatId: string;
96
+ error: string;
97
+ } | {
98
+ type: "keyframe-skipped";
99
+ beatId: string;
100
+ reason: string;
84
101
  } | {
85
102
  type: "video-cached";
86
103
  beatId: string;
@@ -233,6 +250,8 @@ export interface SceneBuildOptions {
233
250
  musicProvider?: BuildMusicProvider;
234
251
  /** Skip AI video generation even when beats declare video cues. */
235
252
  skipVideo?: boolean;
253
+ /** Skip keyframe-still generation even when beats declare keyframe cues. */
254
+ skipKeyframe?: boolean;
236
255
  /** Skip music generation even when beats declare music cues. */
237
256
  skipMusic?: boolean;
238
257
  /** OpenAI image quality — see `vibe generate image --quality`. */
package/dist/index.js CHANGED
@@ -60690,6 +60690,14 @@ async function createBuildPlan(opts) {
60690
60690
  duration: beat.duration,
60691
60691
  keyframe: keyframePrompt
60692
60692
  }) : null;
60693
+ const keyframeCache = keyframePrompt ? keyframeCacheDescriptor({
60694
+ beatId: beat.id,
60695
+ cue: keyframePrompt,
60696
+ provider: resolved.image.resolved,
60697
+ quality: imageQuality,
60698
+ size: imageSize2,
60699
+ ratio: imageRatio
60700
+ }) : null;
60693
60701
  const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
60694
60702
  beatId: beat.id,
60695
60703
  cue: musicPrompt,
@@ -60725,6 +60733,19 @@ async function createBuildPlan(opts) {
60725
60733
  cost: backdropCostUsd(imageQuality),
60726
60734
  active: includeAssets
60727
60735
  }) : null;
60736
+ const keyframe = keyframePrompt && !opts.skipKeyframe ? assetPlan({
60737
+ kind: "keyframe",
60738
+ beatId: beat.id,
60739
+ cue: keyframePrompt,
60740
+ provider: resolved.image.resolved,
60741
+ path: `assets/keyframe-${beat.id}.png`,
60742
+ cache: keyframeCache,
60743
+ reference: null,
60744
+ projectDir,
60745
+ force: opts.force,
60746
+ cost: backdropCostUsd(imageQuality),
60747
+ active: includeAssets
60748
+ }) : null;
60728
60749
  const video = videoCue && !opts.skipVideo ? assetPlan({
60729
60750
  kind: "video",
60730
60751
  beatId: beat.id,
@@ -60757,7 +60778,7 @@ async function createBuildPlan(opts) {
60757
60778
  }) : null;
60758
60779
  const compositionPath = `compositions/scene-${beat.id}.html`;
60759
60780
  const compositionExists = existsSync47(join23(projectDir, compositionPath));
60760
- for (const asset of [narration, backdrop, video, music]) {
60781
+ for (const asset of [narration, backdrop, keyframe, video, music]) {
60761
60782
  if (!asset) continue;
60762
60783
  if (asset.referenceError) {
60763
60784
  missing.add("assets");
@@ -60782,13 +60803,9 @@ async function createBuildPlan(opts) {
60782
60803
  if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
60783
60804
  if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
60784
60805
  noteProviderNeed(resolved.image, "Backdrop generation");
60806
+ if (keyframe?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
60807
+ noteProviderNeed(resolved.image, "Keyframe generation");
60785
60808
  if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
60786
- if (keyframePrompt && video?.willGenerate) {
60787
- estimatedCostUsd += backdropCostUsd(imageQuality);
60788
- providers.add(resolved.image.resolved);
60789
- if (isSupportedBuildImageProvider(resolved.image.resolved))
60790
- noteProviderNeed(resolved.image, "Keyframe generation");
60791
- }
60792
60809
  if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
60793
60810
  if (!compositionExists) missing.add("compositions");
60794
60811
  if (includeCompose && !compositionExists && mode !== "agent") {
@@ -60801,7 +60818,7 @@ async function createBuildPlan(opts) {
60801
60818
  heading: beat.heading,
60802
60819
  durationSec: beat.duration ?? null,
60803
60820
  cues: cue,
60804
- assets: { narration, backdrop, video, music },
60821
+ assets: { narration, backdrop, keyframe, video, music },
60805
60822
  composition: {
60806
60823
  path: compositionPath,
60807
60824
  exists: compositionExists
@@ -61017,6 +61034,7 @@ function providerResolutionsForPlan(resolved, beats, opts, includeAssets) {
61017
61034
  return [
61018
61035
  includeAssets && !opts.skipNarration && needsProvider("narration") ? resolved.narration : null,
61019
61036
  includeAssets && !opts.skipBackdrop && needsProvider("backdrop") ? resolved.image : null,
61037
+ includeAssets && !opts.skipKeyframe && needsProvider("keyframe") ? resolved.image : null,
61020
61038
  includeAssets && !opts.skipVideo && needsProvider("video") ? resolved.video : null,
61021
61039
  includeAssets && !opts.skipMusic && needsProvider("music") ? resolved.music : null,
61022
61040
  resolved.composer ?? null
@@ -62086,6 +62104,7 @@ async function executeSceneBuild(opts) {
62086
62104
  skipNarration: opts.skipNarration,
62087
62105
  skipBackdrop: opts.skipBackdrop,
62088
62106
  skipVideo: opts.skipVideo,
62107
+ skipKeyframe: opts.skipKeyframe,
62089
62108
  skipMusic: opts.skipMusic,
62090
62109
  ttsProvider: opts.ttsProvider,
62091
62110
  voice: opts.voice,
@@ -62271,6 +62290,7 @@ async function executeSceneBuild(opts) {
62271
62290
  skipNarration: opts.skipNarration ?? false,
62272
62291
  skipBackdrop: opts.skipBackdrop ?? false,
62273
62292
  skipVideo: opts.skipVideo ?? false,
62293
+ skipKeyframe: opts.skipKeyframe ?? false,
62274
62294
  skipMusic: opts.skipMusic ?? false,
62275
62295
  force: opts.force ?? false,
62276
62296
  onProgress,
@@ -62647,10 +62667,11 @@ async function executeSceneBuild(opts) {
62647
62667
  });
62648
62668
  }
62649
62669
  async function buildBeatPrimitives(beat, ctx) {
62670
+ const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
62650
62671
  const [narration, backdrop, video, music] = await Promise.all([
62651
62672
  ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
62652
62673
  ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx),
62653
- ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx),
62674
+ ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx, keyframe),
62654
62675
  ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
62655
62676
  ]);
62656
62677
  return {
@@ -62694,8 +62715,8 @@ async function buildBeatPrimitives(beat, ctx) {
62694
62715
  videoMetadataPath: video.metadataPath,
62695
62716
  videoFreshness: video.freshness,
62696
62717
  videoSourcePath: video.sourcePath,
62697
- keyframeStatus: video.keyframeStatus,
62698
- keyframePath: video.keyframePath,
62718
+ keyframeStatus: keyframe.status,
62719
+ keyframePath: keyframe.path,
62699
62720
  musicStatus: music.status,
62700
62721
  musicPath: music.path,
62701
62722
  musicJobId: music.job?.id,
@@ -63266,7 +63287,21 @@ function imageProviderKeyInfo(provider) {
63266
63287
  if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
63267
63288
  return { configKey: "openai", envVar: "OPENAI_API_KEY" };
63268
63289
  }
63269
- async function dispatchVideo(beat, ctx) {
63290
+ async function dispatchKeyframe(beat, ctx) {
63291
+ const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
63292
+ if (!keyframeCue) return { status: "no-cue" };
63293
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
63294
+ const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
63295
+ if (kf.status === "failed") {
63296
+ ctx.onProgress({ type: "keyframe-failed", beatId: beat.id, error: kf.error });
63297
+ return { status: "failed", error: kf.error };
63298
+ }
63299
+ ctx.onProgress(
63300
+ kf.status === "cached" ? { type: "keyframe-cached", beatId: beat.id, path: kf.rel } : { type: "keyframe-generated", beatId: beat.id, path: kf.rel, provider: ctx.imageProvider }
63301
+ );
63302
+ return { status: kf.status, path: kf.rel, provider: ctx.imageProvider };
63303
+ }
63304
+ async function dispatchVideo(beat, ctx, keyframe) {
63270
63305
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
63271
63306
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
63272
63307
  const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
@@ -63336,20 +63371,14 @@ async function dispatchVideo(beat, ctx) {
63336
63371
  freshness: "fresh"
63337
63372
  };
63338
63373
  }
63339
- loadSceneBuildEnv(ctx.projectDir);
63340
- let keyframeStatus;
63341
- let keyframeRel;
63342
- let keyframeImageAbs;
63343
- if (keyframeCue) {
63344
- const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
63345
- if (kf.status === "failed") {
63346
- ctx.onProgress({ type: "video-failed", beatId: beat.id, error: kf.error });
63347
- return { status: "failed", error: kf.error };
63348
- }
63349
- keyframeStatus = kf.status;
63350
- keyframeRel = kf.rel;
63351
- keyframeImageAbs = join25(ctx.projectDir, kf.rel);
63374
+ const keyframeImageRel = keyframeCue && (keyframe.status === "generated" || keyframe.status === "cached") ? keyframe.path : void 0;
63375
+ if (keyframeCue && !keyframeImageRel) {
63376
+ const reason = keyframe.status === "failed" ? `keyframe unavailable: ${keyframe.error ?? "generation failed"}` : "keyframe not generated (build without --skip-keyframe first)";
63377
+ ctx.onProgress({ type: "video-skipped", beatId: beat.id, reason });
63378
+ return { status: "skipped" };
63352
63379
  }
63380
+ const keyframeImageAbs = keyframeImageRel ? join25(ctx.projectDir, keyframeImageRel) : void 0;
63381
+ loadSceneBuildEnv(ctx.projectDir);
63353
63382
  const result = await executeVideoGenerate({
63354
63383
  prompt: prompt3,
63355
63384
  provider: ctx.videoProvider,
@@ -63395,9 +63424,7 @@ async function dispatchVideo(beat, ctx) {
63395
63424
  cachePath: cache.path,
63396
63425
  cacheKey: cache.key,
63397
63426
  metadataPath,
63398
- freshness: "fresh",
63399
- keyframeStatus,
63400
- keyframePath: keyframeRel
63427
+ freshness: "fresh"
63401
63428
  };
63402
63429
  }
63403
63430
  const job = await createAndWriteJobRecord({
@@ -63429,9 +63456,7 @@ async function dispatchVideo(beat, ctx) {
63429
63456
  provider: job.provider,
63430
63457
  cachePath: cache.path,
63431
63458
  cacheKey: cache.key,
63432
- metadataPath,
63433
- keyframeStatus,
63434
- keyframePath: keyframeRel
63459
+ metadataPath
63435
63460
  };
63436
63461
  }
63437
63462
  async function dispatchMusic(beat, ctx) {
@@ -74819,7 +74844,7 @@ init_source();
74819
74844
  // package.json
74820
74845
  var package_default = {
74821
74846
  name: "@vibeframe/cli",
74822
- version: "0.113.3",
74847
+ version: "0.113.4",
74823
74848
  description: "VibeFrame CLI - agentic video workflow runtime",
74824
74849
  private: false,
74825
74850
  license: "MIT",
@@ -85105,7 +85130,10 @@ var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
85105
85130
  var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
85106
85131
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
85107
85132
  var VALID_MUSIC_PROVIDERS2 = ["elevenlabs", "replicate"];
85108
- var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
85133
+ var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option(
85134
+ "--skip-keyframe",
85135
+ "Don't generate keyframe stills (review keyframes first with --skip-video, then build)"
85136
+ ).option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
85109
85137
  "after",
85110
85138
  `
85111
85139
  Examples:
@@ -85164,6 +85192,7 @@ Advanced equivalent: \`vibe scene build\`.`
85164
85192
  skipNarration: options.skipNarration ?? false,
85165
85193
  skipBackdrop: options.skipBackdrop ?? false,
85166
85194
  skipVideo: options.skipVideo ?? false,
85195
+ skipKeyframe: options.skipKeyframe ?? false,
85167
85196
  skipMusic: options.skipMusic ?? false,
85168
85197
  skipRender: options.skipRender ?? false,
85169
85198
  ttsProvider: options.tts,
@@ -85184,6 +85213,7 @@ Advanced equivalent: \`vibe scene build\`.`
85184
85213
  skipNarration: options.skipNarration,
85185
85214
  skipBackdrop: options.skipBackdrop,
85186
85215
  skipVideo: options.skipVideo,
85216
+ skipKeyframe: options.skipKeyframe,
85187
85217
  skipMusic: options.skipMusic,
85188
85218
  ttsProvider: options.tts,
85189
85219
  voice: options.voice,
@@ -85274,6 +85304,7 @@ Advanced equivalent: \`vibe scene build\`.`
85274
85304
  skipNarration: options.skipNarration,
85275
85305
  skipBackdrop: options.skipBackdrop,
85276
85306
  skipVideo: options.skipVideo,
85307
+ skipKeyframe: options.skipKeyframe,
85277
85308
  skipMusic: options.skipMusic,
85278
85309
  skipRender: options.skipRender,
85279
85310
  ttsProvider: options.tts,
@@ -33679,6 +33679,14 @@ async function createBuildPlan(opts) {
33679
33679
  duration: beat.duration,
33680
33680
  keyframe: keyframePrompt
33681
33681
  }) : null;
33682
+ const keyframeCache = keyframePrompt ? keyframeCacheDescriptor({
33683
+ beatId: beat.id,
33684
+ cue: keyframePrompt,
33685
+ provider: resolved.image.resolved,
33686
+ quality: imageQuality,
33687
+ size: imageSize2,
33688
+ ratio: imageRatio
33689
+ }) : null;
33682
33690
  const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
33683
33691
  beatId: beat.id,
33684
33692
  cue: musicPrompt,
@@ -33714,6 +33722,19 @@ async function createBuildPlan(opts) {
33714
33722
  cost: backdropCostUsd(imageQuality),
33715
33723
  active: includeAssets
33716
33724
  }) : null;
33725
+ const keyframe = keyframePrompt && !opts.skipKeyframe ? assetPlan({
33726
+ kind: "keyframe",
33727
+ beatId: beat.id,
33728
+ cue: keyframePrompt,
33729
+ provider: resolved.image.resolved,
33730
+ path: `assets/keyframe-${beat.id}.png`,
33731
+ cache: keyframeCache,
33732
+ reference: null,
33733
+ projectDir,
33734
+ force: opts.force,
33735
+ cost: backdropCostUsd(imageQuality),
33736
+ active: includeAssets
33737
+ }) : null;
33717
33738
  const video = videoCue && !opts.skipVideo ? assetPlan({
33718
33739
  kind: "video",
33719
33740
  beatId: beat.id,
@@ -33746,7 +33767,7 @@ async function createBuildPlan(opts) {
33746
33767
  }) : null;
33747
33768
  const compositionPath = `compositions/scene-${beat.id}.html`;
33748
33769
  const compositionExists = existsSync17(join15(projectDir, compositionPath));
33749
- for (const asset of [narration, backdrop, video, music]) {
33770
+ for (const asset of [narration, backdrop, keyframe, video, music]) {
33750
33771
  if (!asset) continue;
33751
33772
  if (asset.referenceError) {
33752
33773
  missing.add("assets");
@@ -33771,13 +33792,9 @@ async function createBuildPlan(opts) {
33771
33792
  if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
33772
33793
  if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
33773
33794
  noteProviderNeed(resolved.image, "Backdrop generation");
33795
+ if (keyframe?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
33796
+ noteProviderNeed(resolved.image, "Keyframe generation");
33774
33797
  if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
33775
- if (keyframePrompt && video?.willGenerate) {
33776
- estimatedCostUsd += backdropCostUsd(imageQuality);
33777
- providers.add(resolved.image.resolved);
33778
- if (isSupportedBuildImageProvider(resolved.image.resolved))
33779
- noteProviderNeed(resolved.image, "Keyframe generation");
33780
- }
33781
33798
  if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
33782
33799
  if (!compositionExists) missing.add("compositions");
33783
33800
  if (includeCompose && !compositionExists && mode !== "agent") {
@@ -33790,7 +33807,7 @@ async function createBuildPlan(opts) {
33790
33807
  heading: beat.heading,
33791
33808
  durationSec: beat.duration ?? null,
33792
33809
  cues: cue,
33793
- assets: { narration, backdrop, video, music },
33810
+ assets: { narration, backdrop, keyframe, video, music },
33794
33811
  composition: {
33795
33812
  path: compositionPath,
33796
33813
  exists: compositionExists
@@ -34006,6 +34023,7 @@ function providerResolutionsForPlan(resolved, beats, opts, includeAssets) {
34006
34023
  return [
34007
34024
  includeAssets && !opts.skipNarration && needsProvider("narration") ? resolved.narration : null,
34008
34025
  includeAssets && !opts.skipBackdrop && needsProvider("backdrop") ? resolved.image : null,
34026
+ includeAssets && !opts.skipKeyframe && needsProvider("keyframe") ? resolved.image : null,
34009
34027
  includeAssets && !opts.skipVideo && needsProvider("video") ? resolved.video : null,
34010
34028
  includeAssets && !opts.skipMusic && needsProvider("music") ? resolved.music : null,
34011
34029
  resolved.composer ?? null
@@ -34580,6 +34598,7 @@ async function executeSceneBuild(opts) {
34580
34598
  skipNarration: opts.skipNarration,
34581
34599
  skipBackdrop: opts.skipBackdrop,
34582
34600
  skipVideo: opts.skipVideo,
34601
+ skipKeyframe: opts.skipKeyframe,
34583
34602
  skipMusic: opts.skipMusic,
34584
34603
  ttsProvider: opts.ttsProvider,
34585
34604
  voice: opts.voice,
@@ -34765,6 +34784,7 @@ async function executeSceneBuild(opts) {
34765
34784
  skipNarration: opts.skipNarration ?? false,
34766
34785
  skipBackdrop: opts.skipBackdrop ?? false,
34767
34786
  skipVideo: opts.skipVideo ?? false,
34787
+ skipKeyframe: opts.skipKeyframe ?? false,
34768
34788
  skipMusic: opts.skipMusic ?? false,
34769
34789
  force: opts.force ?? false,
34770
34790
  onProgress,
@@ -35141,10 +35161,11 @@ async function executeSceneBuild(opts) {
35141
35161
  });
35142
35162
  }
35143
35163
  async function buildBeatPrimitives(beat, ctx) {
35164
+ const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
35144
35165
  const [narration, backdrop, video, music] = await Promise.all([
35145
35166
  ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
35146
35167
  ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx),
35147
- ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx),
35168
+ ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx, keyframe),
35148
35169
  ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
35149
35170
  ]);
35150
35171
  return {
@@ -35188,8 +35209,8 @@ async function buildBeatPrimitives(beat, ctx) {
35188
35209
  videoMetadataPath: video.metadataPath,
35189
35210
  videoFreshness: video.freshness,
35190
35211
  videoSourcePath: video.sourcePath,
35191
- keyframeStatus: video.keyframeStatus,
35192
- keyframePath: video.keyframePath,
35212
+ keyframeStatus: keyframe.status,
35213
+ keyframePath: keyframe.path,
35193
35214
  musicStatus: music.status,
35194
35215
  musicPath: music.path,
35195
35216
  musicJobId: music.job?.id,
@@ -35760,7 +35781,21 @@ function imageProviderKeyInfo(provider) {
35760
35781
  if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
35761
35782
  return { configKey: "openai", envVar: "OPENAI_API_KEY" };
35762
35783
  }
35763
- async function dispatchVideo(beat, ctx) {
35784
+ async function dispatchKeyframe(beat, ctx) {
35785
+ const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
35786
+ if (!keyframeCue) return { status: "no-cue" };
35787
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
35788
+ const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
35789
+ if (kf.status === "failed") {
35790
+ ctx.onProgress({ type: "keyframe-failed", beatId: beat.id, error: kf.error });
35791
+ return { status: "failed", error: kf.error };
35792
+ }
35793
+ ctx.onProgress(
35794
+ kf.status === "cached" ? { type: "keyframe-cached", beatId: beat.id, path: kf.rel } : { type: "keyframe-generated", beatId: beat.id, path: kf.rel, provider: ctx.imageProvider }
35795
+ );
35796
+ return { status: kf.status, path: kf.rel, provider: ctx.imageProvider };
35797
+ }
35798
+ async function dispatchVideo(beat, ctx, keyframe) {
35764
35799
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
35765
35800
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
35766
35801
  const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
@@ -35830,20 +35865,14 @@ async function dispatchVideo(beat, ctx) {
35830
35865
  freshness: "fresh"
35831
35866
  };
35832
35867
  }
35833
- loadSceneBuildEnv(ctx.projectDir);
35834
- let keyframeStatus;
35835
- let keyframeRel;
35836
- let keyframeImageAbs;
35837
- if (keyframeCue) {
35838
- const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
35839
- if (kf.status === "failed") {
35840
- ctx.onProgress({ type: "video-failed", beatId: beat.id, error: kf.error });
35841
- return { status: "failed", error: kf.error };
35842
- }
35843
- keyframeStatus = kf.status;
35844
- keyframeRel = kf.rel;
35845
- keyframeImageAbs = join16(ctx.projectDir, kf.rel);
35868
+ const keyframeImageRel = keyframeCue && (keyframe.status === "generated" || keyframe.status === "cached") ? keyframe.path : void 0;
35869
+ if (keyframeCue && !keyframeImageRel) {
35870
+ const reason = keyframe.status === "failed" ? `keyframe unavailable: ${keyframe.error ?? "generation failed"}` : "keyframe not generated (build without --skip-keyframe first)";
35871
+ ctx.onProgress({ type: "video-skipped", beatId: beat.id, reason });
35872
+ return { status: "skipped" };
35846
35873
  }
35874
+ const keyframeImageAbs = keyframeImageRel ? join16(ctx.projectDir, keyframeImageRel) : void 0;
35875
+ loadSceneBuildEnv(ctx.projectDir);
35847
35876
  const result = await executeVideoGenerate({
35848
35877
  prompt: prompt3,
35849
35878
  provider: ctx.videoProvider,
@@ -35889,9 +35918,7 @@ async function dispatchVideo(beat, ctx) {
35889
35918
  cachePath: cache.path,
35890
35919
  cacheKey: cache.key,
35891
35920
  metadataPath,
35892
- freshness: "fresh",
35893
- keyframeStatus,
35894
- keyframePath: keyframeRel
35921
+ freshness: "fresh"
35895
35922
  };
35896
35923
  }
35897
35924
  const job = await createAndWriteJobRecord({
@@ -35923,9 +35950,7 @@ async function dispatchVideo(beat, ctx) {
35923
35950
  provider: job.provider,
35924
35951
  cachePath: cache.path,
35925
35952
  cacheKey: cache.key,
35926
- metadataPath,
35927
- keyframeStatus,
35928
- keyframePath: keyframeRel
35953
+ metadataPath
35929
35954
  };
35930
35955
  }
35931
35956
  async function dispatchMusic(beat, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.113.3",
3
+ "version": "0.113.4",
4
4
  "description": "VibeFrame CLI - agentic video workflow runtime",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -83,8 +83,8 @@
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.3.3",
85
85
  "vitest": "^1.2.2",
86
- "@vibeframe/core": "0.113.3",
87
- "@vibeframe/ai-providers": "0.113.3"
86
+ "@vibeframe/ai-providers": "0.113.4",
87
+ "@vibeframe/core": "0.113.4"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=20"