@vibeframe/cli 0.113.3 → 0.113.5

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
@@ -62072,10 +62090,17 @@ async function executeSceneBuild(opts) {
62072
62090
  const onProgress = opts.onProgress ?? (() => {
62073
62091
  });
62074
62092
  const mode = resolveSceneBuildMode(opts);
62075
- const selectedStage = opts.stage ?? (opts.skipRender ? "sync" : "all");
62093
+ let selectedStage = opts.stage ?? (opts.skipRender ? "sync" : "all");
62094
+ const skipVideoStopsRender = (opts.skipVideo ?? false) && (selectedStage === "all" || selectedStage === "render");
62095
+ if (skipVideoStopsRender) selectedStage = "sync";
62076
62096
  const stageReports = createEmptyStageReports();
62077
62097
  const warnings = [];
62078
62098
  const retryWith = [];
62099
+ if (skipVideoStopsRender) {
62100
+ warnings.push(
62101
+ "--skip-video: skipped render (no clips to capture). Review assets/keyframe-*.png, then build without --skip-video to animate them."
62102
+ );
62103
+ }
62079
62104
  let sceneRepair = skippedSceneRepairSummary();
62080
62105
  const storyboardPath2 = join25(projectDir, "STORYBOARD.md");
62081
62106
  const buildPlan = await createBuildPlan({
@@ -62086,6 +62111,7 @@ async function executeSceneBuild(opts) {
62086
62111
  skipNarration: opts.skipNarration,
62087
62112
  skipBackdrop: opts.skipBackdrop,
62088
62113
  skipVideo: opts.skipVideo,
62114
+ skipKeyframe: opts.skipKeyframe,
62089
62115
  skipMusic: opts.skipMusic,
62090
62116
  ttsProvider: opts.ttsProvider,
62091
62117
  voice: opts.voice,
@@ -62234,7 +62260,7 @@ async function executeSceneBuild(opts) {
62234
62260
  if (shouldRunStage(selectedStage, "assets")) {
62235
62261
  onProgress({ type: "phase-start", phase: "primitives" });
62236
62262
  const characterPaths = /* @__PURE__ */ new Map();
62237
- if (!(opts.skipVideo ?? false)) {
62263
+ if (!((opts.skipVideo ?? false) && (opts.skipKeyframe ?? false))) {
62238
62264
  const referenced = /* @__PURE__ */ new Set();
62239
62265
  for (const beat of activeBeats) {
62240
62266
  for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
@@ -62271,6 +62297,7 @@ async function executeSceneBuild(opts) {
62271
62297
  skipNarration: opts.skipNarration ?? false,
62272
62298
  skipBackdrop: opts.skipBackdrop ?? false,
62273
62299
  skipVideo: opts.skipVideo ?? false,
62300
+ skipKeyframe: opts.skipKeyframe ?? false,
62274
62301
  skipMusic: opts.skipMusic ?? false,
62275
62302
  force: opts.force ?? false,
62276
62303
  onProgress,
@@ -62647,10 +62674,11 @@ async function executeSceneBuild(opts) {
62647
62674
  });
62648
62675
  }
62649
62676
  async function buildBeatPrimitives(beat, ctx) {
62677
+ const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
62650
62678
  const [narration, backdrop, video, music] = await Promise.all([
62651
62679
  ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
62652
62680
  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),
62681
+ ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx, keyframe),
62654
62682
  ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
62655
62683
  ]);
62656
62684
  return {
@@ -62694,8 +62722,8 @@ async function buildBeatPrimitives(beat, ctx) {
62694
62722
  videoMetadataPath: video.metadataPath,
62695
62723
  videoFreshness: video.freshness,
62696
62724
  videoSourcePath: video.sourcePath,
62697
- keyframeStatus: video.keyframeStatus,
62698
- keyframePath: video.keyframePath,
62725
+ keyframeStatus: keyframe.status,
62726
+ keyframePath: keyframe.path,
62699
62727
  musicStatus: music.status,
62700
62728
  musicPath: music.path,
62701
62729
  musicJobId: music.job?.id,
@@ -63266,7 +63294,21 @@ function imageProviderKeyInfo(provider) {
63266
63294
  if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
63267
63295
  return { configKey: "openai", envVar: "OPENAI_API_KEY" };
63268
63296
  }
63269
- async function dispatchVideo(beat, ctx) {
63297
+ async function dispatchKeyframe(beat, ctx) {
63298
+ const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
63299
+ if (!keyframeCue) return { status: "no-cue" };
63300
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
63301
+ const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
63302
+ if (kf.status === "failed") {
63303
+ ctx.onProgress({ type: "keyframe-failed", beatId: beat.id, error: kf.error });
63304
+ return { status: "failed", error: kf.error };
63305
+ }
63306
+ ctx.onProgress(
63307
+ kf.status === "cached" ? { type: "keyframe-cached", beatId: beat.id, path: kf.rel } : { type: "keyframe-generated", beatId: beat.id, path: kf.rel, provider: ctx.imageProvider }
63308
+ );
63309
+ return { status: kf.status, path: kf.rel, provider: ctx.imageProvider };
63310
+ }
63311
+ async function dispatchVideo(beat, ctx, keyframe) {
63270
63312
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
63271
63313
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
63272
63314
  const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
@@ -63336,20 +63378,14 @@ async function dispatchVideo(beat, ctx) {
63336
63378
  freshness: "fresh"
63337
63379
  };
63338
63380
  }
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);
63381
+ const keyframeImageRel = keyframeCue && (keyframe.status === "generated" || keyframe.status === "cached") ? keyframe.path : void 0;
63382
+ if (keyframeCue && !keyframeImageRel) {
63383
+ const reason = keyframe.status === "failed" ? `keyframe unavailable: ${keyframe.error ?? "generation failed"}` : "keyframe not generated (build without --skip-keyframe first)";
63384
+ ctx.onProgress({ type: "video-skipped", beatId: beat.id, reason });
63385
+ return { status: "skipped" };
63352
63386
  }
63387
+ const keyframeImageAbs = keyframeImageRel ? join25(ctx.projectDir, keyframeImageRel) : void 0;
63388
+ loadSceneBuildEnv(ctx.projectDir);
63353
63389
  const result = await executeVideoGenerate({
63354
63390
  prompt: prompt3,
63355
63391
  provider: ctx.videoProvider,
@@ -63395,9 +63431,7 @@ async function dispatchVideo(beat, ctx) {
63395
63431
  cachePath: cache.path,
63396
63432
  cacheKey: cache.key,
63397
63433
  metadataPath,
63398
- freshness: "fresh",
63399
- keyframeStatus,
63400
- keyframePath: keyframeRel
63434
+ freshness: "fresh"
63401
63435
  };
63402
63436
  }
63403
63437
  const job = await createAndWriteJobRecord({
@@ -63429,9 +63463,7 @@ async function dispatchVideo(beat, ctx) {
63429
63463
  provider: job.provider,
63430
63464
  cachePath: cache.path,
63431
63465
  cacheKey: cache.key,
63432
- metadataPath,
63433
- keyframeStatus,
63434
- keyframePath: keyframeRel
63466
+ metadataPath
63435
63467
  };
63436
63468
  }
63437
63469
  async function dispatchMusic(beat, ctx) {
@@ -74819,7 +74851,7 @@ init_source();
74819
74851
  // package.json
74820
74852
  var package_default = {
74821
74853
  name: "@vibeframe/cli",
74822
- version: "0.113.3",
74854
+ version: "0.113.5",
74823
74855
  description: "VibeFrame CLI - agentic video workflow runtime",
74824
74856
  private: false,
74825
74857
  license: "MIT",
@@ -85105,7 +85137,10 @@ var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
85105
85137
  var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
85106
85138
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
85107
85139
  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(
85140
+ 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(
85141
+ "--skip-keyframe",
85142
+ "Don't generate keyframe stills (review keyframes first with --skip-video, then build)"
85143
+ ).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
85144
  "after",
85110
85145
  `
85111
85146
  Examples:
@@ -85164,6 +85199,7 @@ Advanced equivalent: \`vibe scene build\`.`
85164
85199
  skipNarration: options.skipNarration ?? false,
85165
85200
  skipBackdrop: options.skipBackdrop ?? false,
85166
85201
  skipVideo: options.skipVideo ?? false,
85202
+ skipKeyframe: options.skipKeyframe ?? false,
85167
85203
  skipMusic: options.skipMusic ?? false,
85168
85204
  skipRender: options.skipRender ?? false,
85169
85205
  ttsProvider: options.tts,
@@ -85184,6 +85220,7 @@ Advanced equivalent: \`vibe scene build\`.`
85184
85220
  skipNarration: options.skipNarration,
85185
85221
  skipBackdrop: options.skipBackdrop,
85186
85222
  skipVideo: options.skipVideo,
85223
+ skipKeyframe: options.skipKeyframe,
85187
85224
  skipMusic: options.skipMusic,
85188
85225
  ttsProvider: options.tts,
85189
85226
  voice: options.voice,
@@ -85274,6 +85311,7 @@ Advanced equivalent: \`vibe scene build\`.`
85274
85311
  skipNarration: options.skipNarration,
85275
85312
  skipBackdrop: options.skipBackdrop,
85276
85313
  skipVideo: options.skipVideo,
85314
+ skipKeyframe: options.skipKeyframe,
85277
85315
  skipMusic: options.skipMusic,
85278
85316
  skipRender: options.skipRender,
85279
85317
  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
@@ -34566,10 +34584,17 @@ async function executeSceneBuild(opts) {
34566
34584
  const onProgress = opts.onProgress ?? (() => {
34567
34585
  });
34568
34586
  const mode = resolveSceneBuildMode(opts);
34569
- const selectedStage = opts.stage ?? (opts.skipRender ? "sync" : "all");
34587
+ let selectedStage = opts.stage ?? (opts.skipRender ? "sync" : "all");
34588
+ const skipVideoStopsRender = (opts.skipVideo ?? false) && (selectedStage === "all" || selectedStage === "render");
34589
+ if (skipVideoStopsRender) selectedStage = "sync";
34570
34590
  const stageReports = createEmptyStageReports();
34571
34591
  const warnings = [];
34572
34592
  const retryWith = [];
34593
+ if (skipVideoStopsRender) {
34594
+ warnings.push(
34595
+ "--skip-video: skipped render (no clips to capture). Review assets/keyframe-*.png, then build without --skip-video to animate them."
34596
+ );
34597
+ }
34573
34598
  let sceneRepair = skippedSceneRepairSummary();
34574
34599
  const storyboardPath = join16(projectDir, "STORYBOARD.md");
34575
34600
  const buildPlan = await createBuildPlan({
@@ -34580,6 +34605,7 @@ async function executeSceneBuild(opts) {
34580
34605
  skipNarration: opts.skipNarration,
34581
34606
  skipBackdrop: opts.skipBackdrop,
34582
34607
  skipVideo: opts.skipVideo,
34608
+ skipKeyframe: opts.skipKeyframe,
34583
34609
  skipMusic: opts.skipMusic,
34584
34610
  ttsProvider: opts.ttsProvider,
34585
34611
  voice: opts.voice,
@@ -34728,7 +34754,7 @@ async function executeSceneBuild(opts) {
34728
34754
  if (shouldRunStage(selectedStage, "assets")) {
34729
34755
  onProgress({ type: "phase-start", phase: "primitives" });
34730
34756
  const characterPaths = /* @__PURE__ */ new Map();
34731
- if (!(opts.skipVideo ?? false)) {
34757
+ if (!((opts.skipVideo ?? false) && (opts.skipKeyframe ?? false))) {
34732
34758
  const referenced = /* @__PURE__ */ new Set();
34733
34759
  for (const beat of activeBeats) {
34734
34760
  for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
@@ -34765,6 +34791,7 @@ async function executeSceneBuild(opts) {
34765
34791
  skipNarration: opts.skipNarration ?? false,
34766
34792
  skipBackdrop: opts.skipBackdrop ?? false,
34767
34793
  skipVideo: opts.skipVideo ?? false,
34794
+ skipKeyframe: opts.skipKeyframe ?? false,
34768
34795
  skipMusic: opts.skipMusic ?? false,
34769
34796
  force: opts.force ?? false,
34770
34797
  onProgress,
@@ -35141,10 +35168,11 @@ async function executeSceneBuild(opts) {
35141
35168
  });
35142
35169
  }
35143
35170
  async function buildBeatPrimitives(beat, ctx) {
35171
+ const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
35144
35172
  const [narration, backdrop, video, music] = await Promise.all([
35145
35173
  ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
35146
35174
  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),
35175
+ ctx.skipVideo ? skipped("video", beat.id, "--skip-video", ctx) : dispatchVideo(beat, ctx, keyframe),
35148
35176
  ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
35149
35177
  ]);
35150
35178
  return {
@@ -35188,8 +35216,8 @@ async function buildBeatPrimitives(beat, ctx) {
35188
35216
  videoMetadataPath: video.metadataPath,
35189
35217
  videoFreshness: video.freshness,
35190
35218
  videoSourcePath: video.sourcePath,
35191
- keyframeStatus: video.keyframeStatus,
35192
- keyframePath: video.keyframePath,
35219
+ keyframeStatus: keyframe.status,
35220
+ keyframePath: keyframe.path,
35193
35221
  musicStatus: music.status,
35194
35222
  musicPath: music.path,
35195
35223
  musicJobId: music.job?.id,
@@ -35760,7 +35788,21 @@ function imageProviderKeyInfo(provider) {
35760
35788
  if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
35761
35789
  return { configKey: "openai", envVar: "OPENAI_API_KEY" };
35762
35790
  }
35763
- async function dispatchVideo(beat, ctx) {
35791
+ async function dispatchKeyframe(beat, ctx) {
35792
+ const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
35793
+ if (!keyframeCue) return { status: "no-cue" };
35794
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
35795
+ const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
35796
+ if (kf.status === "failed") {
35797
+ ctx.onProgress({ type: "keyframe-failed", beatId: beat.id, error: kf.error });
35798
+ return { status: "failed", error: kf.error };
35799
+ }
35800
+ ctx.onProgress(
35801
+ kf.status === "cached" ? { type: "keyframe-cached", beatId: beat.id, path: kf.rel } : { type: "keyframe-generated", beatId: beat.id, path: kf.rel, provider: ctx.imageProvider }
35802
+ );
35803
+ return { status: kf.status, path: kf.rel, provider: ctx.imageProvider };
35804
+ }
35805
+ async function dispatchVideo(beat, ctx, keyframe) {
35764
35806
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
35765
35807
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
35766
35808
  const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
@@ -35830,20 +35872,14 @@ async function dispatchVideo(beat, ctx) {
35830
35872
  freshness: "fresh"
35831
35873
  };
35832
35874
  }
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);
35875
+ const keyframeImageRel = keyframeCue && (keyframe.status === "generated" || keyframe.status === "cached") ? keyframe.path : void 0;
35876
+ if (keyframeCue && !keyframeImageRel) {
35877
+ const reason = keyframe.status === "failed" ? `keyframe unavailable: ${keyframe.error ?? "generation failed"}` : "keyframe not generated (build without --skip-keyframe first)";
35878
+ ctx.onProgress({ type: "video-skipped", beatId: beat.id, reason });
35879
+ return { status: "skipped" };
35846
35880
  }
35881
+ const keyframeImageAbs = keyframeImageRel ? join16(ctx.projectDir, keyframeImageRel) : void 0;
35882
+ loadSceneBuildEnv(ctx.projectDir);
35847
35883
  const result = await executeVideoGenerate({
35848
35884
  prompt: prompt3,
35849
35885
  provider: ctx.videoProvider,
@@ -35889,9 +35925,7 @@ async function dispatchVideo(beat, ctx) {
35889
35925
  cachePath: cache.path,
35890
35926
  cacheKey: cache.key,
35891
35927
  metadataPath,
35892
- freshness: "fresh",
35893
- keyframeStatus,
35894
- keyframePath: keyframeRel
35928
+ freshness: "fresh"
35895
35929
  };
35896
35930
  }
35897
35931
  const job = await createAndWriteJobRecord({
@@ -35923,9 +35957,7 @@ async function dispatchVideo(beat, ctx) {
35923
35957
  provider: job.provider,
35924
35958
  cachePath: cache.path,
35925
35959
  cacheKey: cache.key,
35926
- metadataPath,
35927
- keyframeStatus,
35928
- keyframePath: keyframeRel
35960
+ metadataPath
35929
35961
  };
35930
35962
  }
35931
35963
  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.5",
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.5",
87
+ "@vibeframe/core": "0.113.5"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=20"