@vibeframe/cli 0.113.2 → 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.
- package/dist/commands/_shared/build-cache.d.ts +16 -1
- package/dist/commands/_shared/build-plan.d.ts +2 -0
- package/dist/commands/_shared/scene-build.d.ts +22 -0
- package/dist/commands/_shared/storyboard-edit.d.ts +1 -1
- package/dist/commands/_shared/storyboard-parse.d.ts +7 -0
- package/dist/index.js +203 -15
- package/dist/tools/manifest/index.js +195 -13
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type BuildAssetKind = "narration" | "backdrop" | "character" | "video" | "music";
|
|
1
|
+
export type BuildAssetKind = "narration" | "backdrop" | "character" | "keyframe" | "video" | "music";
|
|
2
2
|
export interface CacheAssetDescriptor {
|
|
3
3
|
key: string;
|
|
4
4
|
path: string;
|
|
@@ -27,6 +27,16 @@ export declare function characterCacheDescriptor(opts: {
|
|
|
27
27
|
quality: "standard" | "hd";
|
|
28
28
|
size: string;
|
|
29
29
|
}): CacheAssetDescriptor;
|
|
30
|
+
export declare function keyframeCacheDescriptor(opts: {
|
|
31
|
+
beatId: string;
|
|
32
|
+
cue: string;
|
|
33
|
+
provider: string;
|
|
34
|
+
quality: "standard" | "hd";
|
|
35
|
+
size: string;
|
|
36
|
+
ratio?: string;
|
|
37
|
+
/** Character sheet paths used as edit references — changing them re-keys the keyframe. */
|
|
38
|
+
characters?: string[];
|
|
39
|
+
}): CacheAssetDescriptor;
|
|
30
40
|
export declare function imageRatioForSize(size: string | undefined): string;
|
|
31
41
|
export declare function videoCacheDescriptor(opts: {
|
|
32
42
|
beatId: string;
|
|
@@ -35,6 +45,11 @@ export declare function videoCacheDescriptor(opts: {
|
|
|
35
45
|
duration: number | undefined;
|
|
36
46
|
/** Character reference image paths — changing them must invalidate the clip. */
|
|
37
47
|
characters?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Keyframe still path for image-to-video mode — changing the keyframe must
|
|
50
|
+
* invalidate the clip. Omitted for text/reference-to-video clips.
|
|
51
|
+
*/
|
|
52
|
+
keyframe?: string;
|
|
38
53
|
}): CacheAssetDescriptor;
|
|
39
54
|
export declare function musicCacheDescriptor(opts: {
|
|
40
55
|
beatId: string;
|
|
@@ -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;
|
|
@@ -168,6 +185,9 @@ export interface BeatBuildOutcome {
|
|
|
168
185
|
videoMetadataPath?: string;
|
|
169
186
|
videoFreshness?: AssetFreshness;
|
|
170
187
|
videoSourcePath?: string;
|
|
188
|
+
/** Keyframe still status when the beat uses keyframe → image-to-video mode. */
|
|
189
|
+
keyframeStatus?: PrimitiveStatus;
|
|
190
|
+
keyframePath?: string;
|
|
171
191
|
musicStatus: PrimitiveStatus;
|
|
172
192
|
musicPath?: string;
|
|
173
193
|
musicJobId?: string;
|
|
@@ -230,6 +250,8 @@ export interface SceneBuildOptions {
|
|
|
230
250
|
musicProvider?: BuildMusicProvider;
|
|
231
251
|
/** Skip AI video generation even when beats declare video cues. */
|
|
232
252
|
skipVideo?: boolean;
|
|
253
|
+
/** Skip keyframe-still generation even when beats declare keyframe cues. */
|
|
254
|
+
skipKeyframe?: boolean;
|
|
233
255
|
/** Skip music generation even when beats declare music cues. */
|
|
234
256
|
skipMusic?: boolean;
|
|
235
257
|
/** OpenAI image quality — see `vibe generate image --quality`. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Beat } from "./storyboard-parse.js";
|
|
2
|
-
export declare const STORYBOARD_CUE_KEYS: readonly ["duration", "narration", "backdrop", "video", "motion", "voice", "music", "asset", "characters"];
|
|
2
|
+
export declare const STORYBOARD_CUE_KEYS: readonly ["duration", "narration", "backdrop", "video", "keyframe", "motion", "voice", "music", "asset", "characters"];
|
|
3
3
|
export type StoryboardCueKey = (typeof STORYBOARD_CUE_KEYS)[number];
|
|
4
4
|
export interface StoryboardValidationIssue {
|
|
5
5
|
severity: "error" | "warning";
|
|
@@ -113,6 +113,13 @@ export interface BeatCues {
|
|
|
113
113
|
* reference-to-video generation. A single name or a list.
|
|
114
114
|
*/
|
|
115
115
|
characters?: string | string[];
|
|
116
|
+
/**
|
|
117
|
+
* Keyframe still prompt. When set, the build generates a keyframe image for
|
|
118
|
+
* this beat (using the character sheet as an edit reference when `characters`
|
|
119
|
+
* is also present) and runs Seedance image-to-video on it. The `video` cue,
|
|
120
|
+
* if present, supplies the motion prompt; otherwise this text is used.
|
|
121
|
+
*/
|
|
122
|
+
keyframe?: string;
|
|
116
123
|
[key: string]: unknown;
|
|
117
124
|
}
|
|
118
125
|
/** Normalize a beat's `characters` cue to a clean string[] (single or list). */
|
package/dist/index.js
CHANGED
|
@@ -31869,6 +31869,7 @@ var init_storyboard_edit = __esm({
|
|
|
31869
31869
|
"narration",
|
|
31870
31870
|
"backdrop",
|
|
31871
31871
|
"video",
|
|
31872
|
+
"keyframe",
|
|
31872
31873
|
"motion",
|
|
31873
31874
|
"voice",
|
|
31874
31875
|
"music",
|
|
@@ -31878,7 +31879,7 @@ var init_storyboard_edit = __esm({
|
|
|
31878
31879
|
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
31879
31880
|
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
31880
31881
|
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
31881
|
-
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
31882
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "keyframe", "motion", "voice", "music", "asset"]);
|
|
31882
31883
|
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
31883
31884
|
}
|
|
31884
31885
|
});
|
|
@@ -60422,6 +60423,7 @@ var init_build_asset_reference = __esm({
|
|
|
60422
60423
|
narration: [".mp3", ".wav", ".m4a"],
|
|
60423
60424
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60424
60425
|
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60426
|
+
keyframe: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60425
60427
|
video: [".mp4", ".mov", ".webm"],
|
|
60426
60428
|
music: [".mp3", ".wav", ".m4a"]
|
|
60427
60429
|
};
|
|
@@ -60469,6 +60471,18 @@ function characterCacheDescriptor(opts) {
|
|
|
60469
60471
|
ext: "png"
|
|
60470
60472
|
});
|
|
60471
60473
|
}
|
|
60474
|
+
function keyframeCacheDescriptor(opts) {
|
|
60475
|
+
return cacheAssetDescriptor("keyframe", {
|
|
60476
|
+
beatId: opts.beatId,
|
|
60477
|
+
cue: opts.cue,
|
|
60478
|
+
provider: opts.provider,
|
|
60479
|
+
quality: opts.quality,
|
|
60480
|
+
size: opts.size,
|
|
60481
|
+
ratio: opts.ratio,
|
|
60482
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
60483
|
+
ext: "png"
|
|
60484
|
+
});
|
|
60485
|
+
}
|
|
60472
60486
|
function imageRatioForSize(size) {
|
|
60473
60487
|
switch (size) {
|
|
60474
60488
|
case "1024x1024":
|
|
@@ -60490,6 +60504,8 @@ function videoCacheDescriptor(opts) {
|
|
|
60490
60504
|
ratio: "16:9",
|
|
60491
60505
|
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
60492
60506
|
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
60507
|
+
// Omit when absent so existing (non-keyframe) clips keep their cache key.
|
|
60508
|
+
keyframe: opts.keyframe || void 0,
|
|
60493
60509
|
ext: "mp4"
|
|
60494
60510
|
});
|
|
60495
60511
|
}
|
|
@@ -60640,6 +60656,7 @@ async function createBuildPlan(opts) {
|
|
|
60640
60656
|
const backdropPrompt = stringOrUndefined3(cue.backdrop);
|
|
60641
60657
|
const augmentedBackdropPrompt = backdropPrompt ? augmentBackdropPrompt(backdropPrompt) : null;
|
|
60642
60658
|
const videoPrompt = stringOrUndefined3(cue.video);
|
|
60659
|
+
const keyframePrompt = stringOrUndefined3(cue.keyframe);
|
|
60643
60660
|
const musicPrompt = stringOrUndefined3(cue.music);
|
|
60644
60661
|
const genericReference = resolveGenericAssetReference(projectDir, cue.asset);
|
|
60645
60662
|
const narrationReference = resolveTypedAssetReference(projectDir, "narration", cue.narration);
|
|
@@ -60648,7 +60665,7 @@ async function createBuildPlan(opts) {
|
|
|
60648
60665
|
const musicReference = resolveTypedAssetReference(projectDir, "music", cue.music) ?? (!musicPrompt && genericReference?.kind === "music" ? genericReference : null);
|
|
60649
60666
|
const narrationCue = narrationText ?? narrationReference?.raw;
|
|
60650
60667
|
const backdropCue = backdropPrompt ?? backdropReference?.raw;
|
|
60651
|
-
const videoCue = videoPrompt ?? videoReference?.raw;
|
|
60668
|
+
const videoCue = videoPrompt ?? videoReference?.raw ?? keyframePrompt;
|
|
60652
60669
|
const musicCue = musicPrompt ?? musicReference?.raw;
|
|
60653
60670
|
const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
|
|
60654
60671
|
const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
|
|
@@ -60666,11 +60683,20 @@ async function createBuildPlan(opts) {
|
|
|
60666
60683
|
size: imageSize2,
|
|
60667
60684
|
ratio: imageRatio
|
|
60668
60685
|
}) : null;
|
|
60669
|
-
const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
|
|
60686
|
+
const videoCache = (videoPrompt || keyframePrompt) && !videoReference ? videoCacheDescriptor({
|
|
60670
60687
|
beatId: beat.id,
|
|
60671
|
-
cue: videoPrompt,
|
|
60688
|
+
cue: videoPrompt ?? keyframePrompt,
|
|
60672
60689
|
provider: resolved.video.resolved,
|
|
60673
|
-
duration: beat.duration
|
|
60690
|
+
duration: beat.duration,
|
|
60691
|
+
keyframe: keyframePrompt
|
|
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
|
|
60674
60700
|
}) : null;
|
|
60675
60701
|
const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
|
|
60676
60702
|
beatId: beat.id,
|
|
@@ -60707,6 +60733,19 @@ async function createBuildPlan(opts) {
|
|
|
60707
60733
|
cost: backdropCostUsd(imageQuality),
|
|
60708
60734
|
active: includeAssets
|
|
60709
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;
|
|
60710
60749
|
const video = videoCue && !opts.skipVideo ? assetPlan({
|
|
60711
60750
|
kind: "video",
|
|
60712
60751
|
beatId: beat.id,
|
|
@@ -60739,7 +60778,7 @@ async function createBuildPlan(opts) {
|
|
|
60739
60778
|
}) : null;
|
|
60740
60779
|
const compositionPath = `compositions/scene-${beat.id}.html`;
|
|
60741
60780
|
const compositionExists = existsSync47(join23(projectDir, compositionPath));
|
|
60742
|
-
for (const asset of [narration, backdrop, video, music]) {
|
|
60781
|
+
for (const asset of [narration, backdrop, keyframe, video, music]) {
|
|
60743
60782
|
if (!asset) continue;
|
|
60744
60783
|
if (asset.referenceError) {
|
|
60745
60784
|
missing.add("assets");
|
|
@@ -60764,6 +60803,8 @@ async function createBuildPlan(opts) {
|
|
|
60764
60803
|
if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
|
|
60765
60804
|
if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
60766
60805
|
noteProviderNeed(resolved.image, "Backdrop generation");
|
|
60806
|
+
if (keyframe?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
60807
|
+
noteProviderNeed(resolved.image, "Keyframe generation");
|
|
60767
60808
|
if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
|
|
60768
60809
|
if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
|
|
60769
60810
|
if (!compositionExists) missing.add("compositions");
|
|
@@ -60777,7 +60818,7 @@ async function createBuildPlan(opts) {
|
|
|
60777
60818
|
heading: beat.heading,
|
|
60778
60819
|
durationSec: beat.duration ?? null,
|
|
60779
60820
|
cues: cue,
|
|
60780
|
-
assets: { narration, backdrop, video, music },
|
|
60821
|
+
assets: { narration, backdrop, keyframe, video, music },
|
|
60781
60822
|
composition: {
|
|
60782
60823
|
path: compositionPath,
|
|
60783
60824
|
exists: compositionExists
|
|
@@ -60993,6 +61034,7 @@ function providerResolutionsForPlan(resolved, beats, opts, includeAssets) {
|
|
|
60993
61034
|
return [
|
|
60994
61035
|
includeAssets && !opts.skipNarration && needsProvider("narration") ? resolved.narration : null,
|
|
60995
61036
|
includeAssets && !opts.skipBackdrop && needsProvider("backdrop") ? resolved.image : null,
|
|
61037
|
+
includeAssets && !opts.skipKeyframe && needsProvider("keyframe") ? resolved.image : null,
|
|
60996
61038
|
includeAssets && !opts.skipVideo && needsProvider("video") ? resolved.video : null,
|
|
60997
61039
|
includeAssets && !opts.skipMusic && needsProvider("music") ? resolved.music : null,
|
|
60998
61040
|
resolved.composer ?? null
|
|
@@ -62062,6 +62104,7 @@ async function executeSceneBuild(opts) {
|
|
|
62062
62104
|
skipNarration: opts.skipNarration,
|
|
62063
62105
|
skipBackdrop: opts.skipBackdrop,
|
|
62064
62106
|
skipVideo: opts.skipVideo,
|
|
62107
|
+
skipKeyframe: opts.skipKeyframe,
|
|
62065
62108
|
skipMusic: opts.skipMusic,
|
|
62066
62109
|
ttsProvider: opts.ttsProvider,
|
|
62067
62110
|
voice: opts.voice,
|
|
@@ -62247,6 +62290,7 @@ async function executeSceneBuild(opts) {
|
|
|
62247
62290
|
skipNarration: opts.skipNarration ?? false,
|
|
62248
62291
|
skipBackdrop: opts.skipBackdrop ?? false,
|
|
62249
62292
|
skipVideo: opts.skipVideo ?? false,
|
|
62293
|
+
skipKeyframe: opts.skipKeyframe ?? false,
|
|
62250
62294
|
skipMusic: opts.skipMusic ?? false,
|
|
62251
62295
|
force: opts.force ?? false,
|
|
62252
62296
|
onProgress,
|
|
@@ -62623,10 +62667,11 @@ async function executeSceneBuild(opts) {
|
|
|
62623
62667
|
});
|
|
62624
62668
|
}
|
|
62625
62669
|
async function buildBeatPrimitives(beat, ctx) {
|
|
62670
|
+
const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
|
|
62626
62671
|
const [narration, backdrop, video, music] = await Promise.all([
|
|
62627
62672
|
ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
|
|
62628
62673
|
ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx),
|
|
62629
|
-
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),
|
|
62630
62675
|
ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
|
|
62631
62676
|
]);
|
|
62632
62677
|
return {
|
|
@@ -62670,6 +62715,8 @@ async function buildBeatPrimitives(beat, ctx) {
|
|
|
62670
62715
|
videoMetadataPath: video.metadataPath,
|
|
62671
62716
|
videoFreshness: video.freshness,
|
|
62672
62717
|
videoSourcePath: video.sourcePath,
|
|
62718
|
+
keyframeStatus: keyframe.status,
|
|
62719
|
+
keyframePath: keyframe.path,
|
|
62673
62720
|
musicStatus: music.status,
|
|
62674
62721
|
musicPath: music.path,
|
|
62675
62722
|
musicJobId: music.job?.id,
|
|
@@ -63114,6 +63161,112 @@ async function generateBackdropImage(prompt3, ctx, ratio) {
|
|
|
63114
63161
|
});
|
|
63115
63162
|
return imageBufferFromResult(result);
|
|
63116
63163
|
}
|
|
63164
|
+
async function editKeyframeImage(prompt3, sheetBuffers, ctx, ratio) {
|
|
63165
|
+
loadSceneBuildEnv(ctx.projectDir);
|
|
63166
|
+
const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
|
|
63167
|
+
const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
|
|
63168
|
+
if (!apiKey) {
|
|
63169
|
+
return {
|
|
63170
|
+
success: false,
|
|
63171
|
+
error: `${keyInfo.envVar} not set \u2014 cannot generate keyframe with ${ctx.imageProvider}`
|
|
63172
|
+
};
|
|
63173
|
+
}
|
|
63174
|
+
if (ctx.imageProvider === "openai") {
|
|
63175
|
+
const provider2 = new OpenAIImageProvider();
|
|
63176
|
+
await provider2.initialize({ apiKey });
|
|
63177
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3);
|
|
63178
|
+
return imageBufferFromResult(result2);
|
|
63179
|
+
}
|
|
63180
|
+
if (ctx.imageProvider === "gemini") {
|
|
63181
|
+
const provider2 = new GeminiProvider();
|
|
63182
|
+
await provider2.initialize({ apiKey });
|
|
63183
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3, {
|
|
63184
|
+
model: "flash",
|
|
63185
|
+
aspectRatio: ratio
|
|
63186
|
+
});
|
|
63187
|
+
return imageBufferFromResult(result2);
|
|
63188
|
+
}
|
|
63189
|
+
const provider = new GrokProvider();
|
|
63190
|
+
await provider.initialize({ apiKey });
|
|
63191
|
+
const result = await provider.editImage(sheetBuffers[0], prompt3, { aspectRatio: ratio });
|
|
63192
|
+
return imageBufferFromResult(result);
|
|
63193
|
+
}
|
|
63194
|
+
async function ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel) {
|
|
63195
|
+
const rel = `assets/keyframe-${beat.id}.png`;
|
|
63196
|
+
const abs = join25(ctx.projectDir, rel);
|
|
63197
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
63198
|
+
const ratio = imageRatioForSize(size);
|
|
63199
|
+
const metadataOptions = { quality: ctx.imageQuality, size, ratio };
|
|
63200
|
+
const cache = keyframeCacheDescriptor({
|
|
63201
|
+
beatId: beat.id,
|
|
63202
|
+
cue: keyframeCue,
|
|
63203
|
+
provider: ctx.imageProvider,
|
|
63204
|
+
quality: ctx.imageQuality,
|
|
63205
|
+
size,
|
|
63206
|
+
ratio,
|
|
63207
|
+
characters: characterRefsRel
|
|
63208
|
+
});
|
|
63209
|
+
if (existsSync49(abs) && !ctx.force) {
|
|
63210
|
+
const metadata = readAssetMetadata(ctx.projectDir, "keyframe", beat.id);
|
|
63211
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
63212
|
+
projectDir: ctx.projectDir,
|
|
63213
|
+
kind: "keyframe",
|
|
63214
|
+
beatId: beat.id,
|
|
63215
|
+
cue: keyframeCue,
|
|
63216
|
+
provider: ctx.imageProvider,
|
|
63217
|
+
options: metadataOptions,
|
|
63218
|
+
cacheKey: cache.key
|
|
63219
|
+
})) {
|
|
63220
|
+
return { status: "cached", rel };
|
|
63221
|
+
}
|
|
63222
|
+
}
|
|
63223
|
+
const cacheAbs = join25(ctx.projectDir, cache.path);
|
|
63224
|
+
if (existsSync49(cacheAbs) && !ctx.force) {
|
|
63225
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
63226
|
+
await copyFile5(cacheAbs, abs);
|
|
63227
|
+
await writeAssetMetadata({
|
|
63228
|
+
projectDir: ctx.projectDir,
|
|
63229
|
+
kind: "keyframe",
|
|
63230
|
+
beatId: beat.id,
|
|
63231
|
+
cue: keyframeCue,
|
|
63232
|
+
provider: ctx.imageProvider,
|
|
63233
|
+
options: metadataOptions,
|
|
63234
|
+
cacheKey: cache.key,
|
|
63235
|
+
canonicalPath: rel,
|
|
63236
|
+
cachePath: cache.path
|
|
63237
|
+
});
|
|
63238
|
+
return { status: "cached", rel };
|
|
63239
|
+
}
|
|
63240
|
+
const sheetBuffers = [];
|
|
63241
|
+
for (const relPath of characterRefsRel) {
|
|
63242
|
+
const sheetAbs = join25(ctx.projectDir, relPath);
|
|
63243
|
+
if (existsSync49(sheetAbs)) sheetBuffers.push(await readFile46(sheetAbs));
|
|
63244
|
+
}
|
|
63245
|
+
const imgCtx = {
|
|
63246
|
+
projectDir: ctx.projectDir,
|
|
63247
|
+
imageProvider: ctx.imageProvider,
|
|
63248
|
+
imageSize: size,
|
|
63249
|
+
imageQuality: ctx.imageQuality
|
|
63250
|
+
};
|
|
63251
|
+
const generated = sheetBuffers.length > 0 ? await editKeyframeImage(keyframeCue, sheetBuffers, imgCtx, ratio) : await generateBackdropImage(keyframeCue, imgCtx, ratio);
|
|
63252
|
+
if (!generated.success) return { status: "failed", error: generated.error };
|
|
63253
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
63254
|
+
await writeFile50(abs, generated.buffer);
|
|
63255
|
+
await mkdir26(dirname24(cacheAbs), { recursive: true });
|
|
63256
|
+
await writeFile50(cacheAbs, generated.buffer);
|
|
63257
|
+
await writeAssetMetadata({
|
|
63258
|
+
projectDir: ctx.projectDir,
|
|
63259
|
+
kind: "keyframe",
|
|
63260
|
+
beatId: beat.id,
|
|
63261
|
+
cue: keyframeCue,
|
|
63262
|
+
provider: ctx.imageProvider,
|
|
63263
|
+
options: metadataOptions,
|
|
63264
|
+
cacheKey: cache.key,
|
|
63265
|
+
canonicalPath: rel,
|
|
63266
|
+
cachePath: cache.path
|
|
63267
|
+
});
|
|
63268
|
+
return { status: "generated", rel };
|
|
63269
|
+
}
|
|
63117
63270
|
async function imageBufferFromResult(result) {
|
|
63118
63271
|
const image = result.images?.[0];
|
|
63119
63272
|
if (!result.success || !image) {
|
|
@@ -63134,10 +63287,25 @@ function imageProviderKeyInfo(provider) {
|
|
|
63134
63287
|
if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
|
|
63135
63288
|
return { configKey: "openai", envVar: "OPENAI_API_KEY" };
|
|
63136
63289
|
}
|
|
63137
|
-
async function
|
|
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) {
|
|
63138
63305
|
const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
|
|
63139
63306
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
63140
|
-
const
|
|
63307
|
+
const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
|
|
63308
|
+
const prompt3 = stringOrUndefined4(beat.cues?.video) ?? keyframeCue;
|
|
63141
63309
|
if (!prompt3) return { status: "no-cue" };
|
|
63142
63310
|
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
63143
63311
|
const characterRefsAbs = characterRefsRel.map((p) => join25(ctx.projectDir, p));
|
|
@@ -63148,7 +63316,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
63148
63316
|
cue: prompt3,
|
|
63149
63317
|
provider: ctx.videoProvider,
|
|
63150
63318
|
duration: normalizeVideoDuration(beat.duration),
|
|
63151
|
-
|
|
63319
|
+
// Keyframe mode keys the clip to the keyframe prompt; refs feed the keyframe,
|
|
63320
|
+
// not the clip directly.
|
|
63321
|
+
characters: keyframeCue ? void 0 : characterRefsRel,
|
|
63322
|
+
keyframe: keyframeCue
|
|
63152
63323
|
});
|
|
63153
63324
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
63154
63325
|
if (existsSync49(abs) && !ctx.force) {
|
|
@@ -63200,6 +63371,13 @@ async function dispatchVideo(beat, ctx) {
|
|
|
63200
63371
|
freshness: "fresh"
|
|
63201
63372
|
};
|
|
63202
63373
|
}
|
|
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" };
|
|
63379
|
+
}
|
|
63380
|
+
const keyframeImageAbs = keyframeImageRel ? join25(ctx.projectDir, keyframeImageRel) : void 0;
|
|
63203
63381
|
loadSceneBuildEnv(ctx.projectDir);
|
|
63204
63382
|
const result = await executeVideoGenerate({
|
|
63205
63383
|
prompt: prompt3,
|
|
@@ -63208,7 +63386,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
63208
63386
|
ratio: "16:9",
|
|
63209
63387
|
output: abs,
|
|
63210
63388
|
wait: false,
|
|
63211
|
-
|
|
63389
|
+
// Keyframe mode → single init frame (image-to-video); otherwise character
|
|
63390
|
+
// reference-to-video.
|
|
63391
|
+
image: keyframeImageAbs,
|
|
63392
|
+
refImages: keyframeImageAbs ? void 0 : characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
63212
63393
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
63213
63394
|
});
|
|
63214
63395
|
if (!result.success || !result.taskId) {
|
|
@@ -63250,7 +63431,7 @@ async function dispatchVideo(beat, ctx) {
|
|
|
63250
63431
|
jobType: "generate-video",
|
|
63251
63432
|
provider: result.provider ?? ctx.videoProvider,
|
|
63252
63433
|
providerTaskId: result.taskId,
|
|
63253
|
-
providerTaskType: "text2video",
|
|
63434
|
+
providerTaskType: keyframeImageAbs ? "image2video" : "text2video",
|
|
63254
63435
|
status: "running",
|
|
63255
63436
|
projectDir: ctx.projectDir,
|
|
63256
63437
|
workingDirectory: ctx.projectDir,
|
|
@@ -63577,6 +63758,7 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
63577
63758
|
for (const outcome of outcomes) {
|
|
63578
63759
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
63579
63760
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
63761
|
+
if (outcome.keyframeStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
63580
63762
|
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
63581
63763
|
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
63582
63764
|
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
|
@@ -74662,7 +74844,7 @@ init_source();
|
|
|
74662
74844
|
// package.json
|
|
74663
74845
|
var package_default = {
|
|
74664
74846
|
name: "@vibeframe/cli",
|
|
74665
|
-
version: "0.113.
|
|
74847
|
+
version: "0.113.4",
|
|
74666
74848
|
description: "VibeFrame CLI - agentic video workflow runtime",
|
|
74667
74849
|
private: false,
|
|
74668
74850
|
license: "MIT",
|
|
@@ -84948,7 +85130,10 @@ var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
|
|
|
84948
85130
|
var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
|
|
84949
85131
|
var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
|
|
84950
85132
|
var VALID_MUSIC_PROVIDERS2 = ["elevenlabs", "replicate"];
|
|
84951
|
-
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(
|
|
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(
|
|
84952
85137
|
"after",
|
|
84953
85138
|
`
|
|
84954
85139
|
Examples:
|
|
@@ -85007,6 +85192,7 @@ Advanced equivalent: \`vibe scene build\`.`
|
|
|
85007
85192
|
skipNarration: options.skipNarration ?? false,
|
|
85008
85193
|
skipBackdrop: options.skipBackdrop ?? false,
|
|
85009
85194
|
skipVideo: options.skipVideo ?? false,
|
|
85195
|
+
skipKeyframe: options.skipKeyframe ?? false,
|
|
85010
85196
|
skipMusic: options.skipMusic ?? false,
|
|
85011
85197
|
skipRender: options.skipRender ?? false,
|
|
85012
85198
|
ttsProvider: options.tts,
|
|
@@ -85027,6 +85213,7 @@ Advanced equivalent: \`vibe scene build\`.`
|
|
|
85027
85213
|
skipNarration: options.skipNarration,
|
|
85028
85214
|
skipBackdrop: options.skipBackdrop,
|
|
85029
85215
|
skipVideo: options.skipVideo,
|
|
85216
|
+
skipKeyframe: options.skipKeyframe,
|
|
85030
85217
|
skipMusic: options.skipMusic,
|
|
85031
85218
|
ttsProvider: options.tts,
|
|
85032
85219
|
voice: options.voice,
|
|
@@ -85117,6 +85304,7 @@ Advanced equivalent: \`vibe scene build\`.`
|
|
|
85117
85304
|
skipNarration: options.skipNarration,
|
|
85118
85305
|
skipBackdrop: options.skipBackdrop,
|
|
85119
85306
|
skipVideo: options.skipVideo,
|
|
85307
|
+
skipKeyframe: options.skipKeyframe,
|
|
85120
85308
|
skipMusic: options.skipMusic,
|
|
85121
85309
|
skipRender: options.skipRender,
|
|
85122
85310
|
ttsProvider: options.tts,
|
|
@@ -32480,6 +32480,7 @@ var init_storyboard_edit = __esm({
|
|
|
32480
32480
|
"narration",
|
|
32481
32481
|
"backdrop",
|
|
32482
32482
|
"video",
|
|
32483
|
+
"keyframe",
|
|
32483
32484
|
"motion",
|
|
32484
32485
|
"voice",
|
|
32485
32486
|
"music",
|
|
@@ -32489,7 +32490,7 @@ var init_storyboard_edit = __esm({
|
|
|
32489
32490
|
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
32490
32491
|
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
32491
32492
|
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
32492
|
-
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
32493
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "keyframe", "motion", "voice", "music", "asset"]);
|
|
32493
32494
|
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
32494
32495
|
}
|
|
32495
32496
|
});
|
|
@@ -33411,6 +33412,7 @@ var init_build_asset_reference = __esm({
|
|
|
33411
33412
|
narration: [".mp3", ".wav", ".m4a"],
|
|
33412
33413
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33413
33414
|
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33415
|
+
keyframe: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33414
33416
|
video: [".mp4", ".mov", ".webm"],
|
|
33415
33417
|
music: [".mp3", ".wav", ".m4a"]
|
|
33416
33418
|
};
|
|
@@ -33458,6 +33460,18 @@ function characterCacheDescriptor(opts) {
|
|
|
33458
33460
|
ext: "png"
|
|
33459
33461
|
});
|
|
33460
33462
|
}
|
|
33463
|
+
function keyframeCacheDescriptor(opts) {
|
|
33464
|
+
return cacheAssetDescriptor("keyframe", {
|
|
33465
|
+
beatId: opts.beatId,
|
|
33466
|
+
cue: opts.cue,
|
|
33467
|
+
provider: opts.provider,
|
|
33468
|
+
quality: opts.quality,
|
|
33469
|
+
size: opts.size,
|
|
33470
|
+
ratio: opts.ratio,
|
|
33471
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
33472
|
+
ext: "png"
|
|
33473
|
+
});
|
|
33474
|
+
}
|
|
33461
33475
|
function imageRatioForSize(size) {
|
|
33462
33476
|
switch (size) {
|
|
33463
33477
|
case "1024x1024":
|
|
@@ -33479,6 +33493,8 @@ function videoCacheDescriptor(opts) {
|
|
|
33479
33493
|
ratio: "16:9",
|
|
33480
33494
|
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
33481
33495
|
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
33496
|
+
// Omit when absent so existing (non-keyframe) clips keep their cache key.
|
|
33497
|
+
keyframe: opts.keyframe || void 0,
|
|
33482
33498
|
ext: "mp4"
|
|
33483
33499
|
});
|
|
33484
33500
|
}
|
|
@@ -33629,6 +33645,7 @@ async function createBuildPlan(opts) {
|
|
|
33629
33645
|
const backdropPrompt = stringOrUndefined3(cue.backdrop);
|
|
33630
33646
|
const augmentedBackdropPrompt = backdropPrompt ? augmentBackdropPrompt(backdropPrompt) : null;
|
|
33631
33647
|
const videoPrompt = stringOrUndefined3(cue.video);
|
|
33648
|
+
const keyframePrompt = stringOrUndefined3(cue.keyframe);
|
|
33632
33649
|
const musicPrompt = stringOrUndefined3(cue.music);
|
|
33633
33650
|
const genericReference = resolveGenericAssetReference(projectDir, cue.asset);
|
|
33634
33651
|
const narrationReference = resolveTypedAssetReference(projectDir, "narration", cue.narration);
|
|
@@ -33637,7 +33654,7 @@ async function createBuildPlan(opts) {
|
|
|
33637
33654
|
const musicReference = resolveTypedAssetReference(projectDir, "music", cue.music) ?? (!musicPrompt && genericReference?.kind === "music" ? genericReference : null);
|
|
33638
33655
|
const narrationCue = narrationText ?? narrationReference?.raw;
|
|
33639
33656
|
const backdropCue = backdropPrompt ?? backdropReference?.raw;
|
|
33640
|
-
const videoCue = videoPrompt ?? videoReference?.raw;
|
|
33657
|
+
const videoCue = videoPrompt ?? videoReference?.raw ?? keyframePrompt;
|
|
33641
33658
|
const musicCue = musicPrompt ?? musicReference?.raw;
|
|
33642
33659
|
const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
|
|
33643
33660
|
const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
|
|
@@ -33655,11 +33672,20 @@ async function createBuildPlan(opts) {
|
|
|
33655
33672
|
size: imageSize2,
|
|
33656
33673
|
ratio: imageRatio
|
|
33657
33674
|
}) : null;
|
|
33658
|
-
const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
|
|
33675
|
+
const videoCache = (videoPrompt || keyframePrompt) && !videoReference ? videoCacheDescriptor({
|
|
33659
33676
|
beatId: beat.id,
|
|
33660
|
-
cue: videoPrompt,
|
|
33677
|
+
cue: videoPrompt ?? keyframePrompt,
|
|
33661
33678
|
provider: resolved.video.resolved,
|
|
33662
|
-
duration: beat.duration
|
|
33679
|
+
duration: beat.duration,
|
|
33680
|
+
keyframe: keyframePrompt
|
|
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
|
|
33663
33689
|
}) : null;
|
|
33664
33690
|
const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
|
|
33665
33691
|
beatId: beat.id,
|
|
@@ -33696,6 +33722,19 @@ async function createBuildPlan(opts) {
|
|
|
33696
33722
|
cost: backdropCostUsd(imageQuality),
|
|
33697
33723
|
active: includeAssets
|
|
33698
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;
|
|
33699
33738
|
const video = videoCue && !opts.skipVideo ? assetPlan({
|
|
33700
33739
|
kind: "video",
|
|
33701
33740
|
beatId: beat.id,
|
|
@@ -33728,7 +33767,7 @@ async function createBuildPlan(opts) {
|
|
|
33728
33767
|
}) : null;
|
|
33729
33768
|
const compositionPath = `compositions/scene-${beat.id}.html`;
|
|
33730
33769
|
const compositionExists = existsSync17(join15(projectDir, compositionPath));
|
|
33731
|
-
for (const asset of [narration, backdrop, video, music]) {
|
|
33770
|
+
for (const asset of [narration, backdrop, keyframe, video, music]) {
|
|
33732
33771
|
if (!asset) continue;
|
|
33733
33772
|
if (asset.referenceError) {
|
|
33734
33773
|
missing.add("assets");
|
|
@@ -33753,6 +33792,8 @@ async function createBuildPlan(opts) {
|
|
|
33753
33792
|
if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
|
|
33754
33793
|
if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
33755
33794
|
noteProviderNeed(resolved.image, "Backdrop generation");
|
|
33795
|
+
if (keyframe?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
33796
|
+
noteProviderNeed(resolved.image, "Keyframe generation");
|
|
33756
33797
|
if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
|
|
33757
33798
|
if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
|
|
33758
33799
|
if (!compositionExists) missing.add("compositions");
|
|
@@ -33766,7 +33807,7 @@ async function createBuildPlan(opts) {
|
|
|
33766
33807
|
heading: beat.heading,
|
|
33767
33808
|
durationSec: beat.duration ?? null,
|
|
33768
33809
|
cues: cue,
|
|
33769
|
-
assets: { narration, backdrop, video, music },
|
|
33810
|
+
assets: { narration, backdrop, keyframe, video, music },
|
|
33770
33811
|
composition: {
|
|
33771
33812
|
path: compositionPath,
|
|
33772
33813
|
exists: compositionExists
|
|
@@ -33982,6 +34023,7 @@ function providerResolutionsForPlan(resolved, beats, opts, includeAssets) {
|
|
|
33982
34023
|
return [
|
|
33983
34024
|
includeAssets && !opts.skipNarration && needsProvider("narration") ? resolved.narration : null,
|
|
33984
34025
|
includeAssets && !opts.skipBackdrop && needsProvider("backdrop") ? resolved.image : null,
|
|
34026
|
+
includeAssets && !opts.skipKeyframe && needsProvider("keyframe") ? resolved.image : null,
|
|
33985
34027
|
includeAssets && !opts.skipVideo && needsProvider("video") ? resolved.video : null,
|
|
33986
34028
|
includeAssets && !opts.skipMusic && needsProvider("music") ? resolved.music : null,
|
|
33987
34029
|
resolved.composer ?? null
|
|
@@ -34556,6 +34598,7 @@ async function executeSceneBuild(opts) {
|
|
|
34556
34598
|
skipNarration: opts.skipNarration,
|
|
34557
34599
|
skipBackdrop: opts.skipBackdrop,
|
|
34558
34600
|
skipVideo: opts.skipVideo,
|
|
34601
|
+
skipKeyframe: opts.skipKeyframe,
|
|
34559
34602
|
skipMusic: opts.skipMusic,
|
|
34560
34603
|
ttsProvider: opts.ttsProvider,
|
|
34561
34604
|
voice: opts.voice,
|
|
@@ -34741,6 +34784,7 @@ async function executeSceneBuild(opts) {
|
|
|
34741
34784
|
skipNarration: opts.skipNarration ?? false,
|
|
34742
34785
|
skipBackdrop: opts.skipBackdrop ?? false,
|
|
34743
34786
|
skipVideo: opts.skipVideo ?? false,
|
|
34787
|
+
skipKeyframe: opts.skipKeyframe ?? false,
|
|
34744
34788
|
skipMusic: opts.skipMusic ?? false,
|
|
34745
34789
|
force: opts.force ?? false,
|
|
34746
34790
|
onProgress,
|
|
@@ -35117,10 +35161,11 @@ async function executeSceneBuild(opts) {
|
|
|
35117
35161
|
});
|
|
35118
35162
|
}
|
|
35119
35163
|
async function buildBeatPrimitives(beat, ctx) {
|
|
35164
|
+
const keyframe = ctx.skipKeyframe ? await skipped("keyframe", beat.id, "--skip-keyframe", ctx) : await dispatchKeyframe(beat, ctx);
|
|
35120
35165
|
const [narration, backdrop, video, music] = await Promise.all([
|
|
35121
35166
|
ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
|
|
35122
35167
|
ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx),
|
|
35123
|
-
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),
|
|
35124
35169
|
ctx.skipMusic ? skipped("music", beat.id, "--skip-music", ctx) : dispatchMusic(beat, ctx)
|
|
35125
35170
|
]);
|
|
35126
35171
|
return {
|
|
@@ -35164,6 +35209,8 @@ async function buildBeatPrimitives(beat, ctx) {
|
|
|
35164
35209
|
videoMetadataPath: video.metadataPath,
|
|
35165
35210
|
videoFreshness: video.freshness,
|
|
35166
35211
|
videoSourcePath: video.sourcePath,
|
|
35212
|
+
keyframeStatus: keyframe.status,
|
|
35213
|
+
keyframePath: keyframe.path,
|
|
35167
35214
|
musicStatus: music.status,
|
|
35168
35215
|
musicPath: music.path,
|
|
35169
35216
|
musicJobId: music.job?.id,
|
|
@@ -35608,6 +35655,112 @@ async function generateBackdropImage(prompt3, ctx, ratio) {
|
|
|
35608
35655
|
});
|
|
35609
35656
|
return imageBufferFromResult(result);
|
|
35610
35657
|
}
|
|
35658
|
+
async function editKeyframeImage(prompt3, sheetBuffers, ctx, ratio) {
|
|
35659
|
+
loadSceneBuildEnv(ctx.projectDir);
|
|
35660
|
+
const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
|
|
35661
|
+
const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
|
|
35662
|
+
if (!apiKey) {
|
|
35663
|
+
return {
|
|
35664
|
+
success: false,
|
|
35665
|
+
error: `${keyInfo.envVar} not set \u2014 cannot generate keyframe with ${ctx.imageProvider}`
|
|
35666
|
+
};
|
|
35667
|
+
}
|
|
35668
|
+
if (ctx.imageProvider === "openai") {
|
|
35669
|
+
const provider2 = new OpenAIImageProvider();
|
|
35670
|
+
await provider2.initialize({ apiKey });
|
|
35671
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3);
|
|
35672
|
+
return imageBufferFromResult(result2);
|
|
35673
|
+
}
|
|
35674
|
+
if (ctx.imageProvider === "gemini") {
|
|
35675
|
+
const provider2 = new GeminiProvider();
|
|
35676
|
+
await provider2.initialize({ apiKey });
|
|
35677
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3, {
|
|
35678
|
+
model: "flash",
|
|
35679
|
+
aspectRatio: ratio
|
|
35680
|
+
});
|
|
35681
|
+
return imageBufferFromResult(result2);
|
|
35682
|
+
}
|
|
35683
|
+
const provider = new GrokProvider();
|
|
35684
|
+
await provider.initialize({ apiKey });
|
|
35685
|
+
const result = await provider.editImage(sheetBuffers[0], prompt3, { aspectRatio: ratio });
|
|
35686
|
+
return imageBufferFromResult(result);
|
|
35687
|
+
}
|
|
35688
|
+
async function ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel) {
|
|
35689
|
+
const rel = `assets/keyframe-${beat.id}.png`;
|
|
35690
|
+
const abs = join16(ctx.projectDir, rel);
|
|
35691
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
35692
|
+
const ratio = imageRatioForSize(size);
|
|
35693
|
+
const metadataOptions = { quality: ctx.imageQuality, size, ratio };
|
|
35694
|
+
const cache = keyframeCacheDescriptor({
|
|
35695
|
+
beatId: beat.id,
|
|
35696
|
+
cue: keyframeCue,
|
|
35697
|
+
provider: ctx.imageProvider,
|
|
35698
|
+
quality: ctx.imageQuality,
|
|
35699
|
+
size,
|
|
35700
|
+
ratio,
|
|
35701
|
+
characters: characterRefsRel
|
|
35702
|
+
});
|
|
35703
|
+
if (existsSync19(abs) && !ctx.force) {
|
|
35704
|
+
const metadata = readAssetMetadata(ctx.projectDir, "keyframe", beat.id);
|
|
35705
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
35706
|
+
projectDir: ctx.projectDir,
|
|
35707
|
+
kind: "keyframe",
|
|
35708
|
+
beatId: beat.id,
|
|
35709
|
+
cue: keyframeCue,
|
|
35710
|
+
provider: ctx.imageProvider,
|
|
35711
|
+
options: metadataOptions,
|
|
35712
|
+
cacheKey: cache.key
|
|
35713
|
+
})) {
|
|
35714
|
+
return { status: "cached", rel };
|
|
35715
|
+
}
|
|
35716
|
+
}
|
|
35717
|
+
const cacheAbs = join16(ctx.projectDir, cache.path);
|
|
35718
|
+
if (existsSync19(cacheAbs) && !ctx.force) {
|
|
35719
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35720
|
+
await copyFile3(cacheAbs, abs);
|
|
35721
|
+
await writeAssetMetadata({
|
|
35722
|
+
projectDir: ctx.projectDir,
|
|
35723
|
+
kind: "keyframe",
|
|
35724
|
+
beatId: beat.id,
|
|
35725
|
+
cue: keyframeCue,
|
|
35726
|
+
provider: ctx.imageProvider,
|
|
35727
|
+
options: metadataOptions,
|
|
35728
|
+
cacheKey: cache.key,
|
|
35729
|
+
canonicalPath: rel,
|
|
35730
|
+
cachePath: cache.path
|
|
35731
|
+
});
|
|
35732
|
+
return { status: "cached", rel };
|
|
35733
|
+
}
|
|
35734
|
+
const sheetBuffers = [];
|
|
35735
|
+
for (const relPath of characterRefsRel) {
|
|
35736
|
+
const sheetAbs = join16(ctx.projectDir, relPath);
|
|
35737
|
+
if (existsSync19(sheetAbs)) sheetBuffers.push(await readFile18(sheetAbs));
|
|
35738
|
+
}
|
|
35739
|
+
const imgCtx = {
|
|
35740
|
+
projectDir: ctx.projectDir,
|
|
35741
|
+
imageProvider: ctx.imageProvider,
|
|
35742
|
+
imageSize: size,
|
|
35743
|
+
imageQuality: ctx.imageQuality
|
|
35744
|
+
};
|
|
35745
|
+
const generated = sheetBuffers.length > 0 ? await editKeyframeImage(keyframeCue, sheetBuffers, imgCtx, ratio) : await generateBackdropImage(keyframeCue, imgCtx, ratio);
|
|
35746
|
+
if (!generated.success) return { status: "failed", error: generated.error };
|
|
35747
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35748
|
+
await writeFile18(abs, generated.buffer);
|
|
35749
|
+
await mkdir10(dirname10(cacheAbs), { recursive: true });
|
|
35750
|
+
await writeFile18(cacheAbs, generated.buffer);
|
|
35751
|
+
await writeAssetMetadata({
|
|
35752
|
+
projectDir: ctx.projectDir,
|
|
35753
|
+
kind: "keyframe",
|
|
35754
|
+
beatId: beat.id,
|
|
35755
|
+
cue: keyframeCue,
|
|
35756
|
+
provider: ctx.imageProvider,
|
|
35757
|
+
options: metadataOptions,
|
|
35758
|
+
cacheKey: cache.key,
|
|
35759
|
+
canonicalPath: rel,
|
|
35760
|
+
cachePath: cache.path
|
|
35761
|
+
});
|
|
35762
|
+
return { status: "generated", rel };
|
|
35763
|
+
}
|
|
35611
35764
|
async function imageBufferFromResult(result) {
|
|
35612
35765
|
const image = result.images?.[0];
|
|
35613
35766
|
if (!result.success || !image) {
|
|
@@ -35628,10 +35781,25 @@ function imageProviderKeyInfo(provider) {
|
|
|
35628
35781
|
if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
|
|
35629
35782
|
return { configKey: "openai", envVar: "OPENAI_API_KEY" };
|
|
35630
35783
|
}
|
|
35631
|
-
async function
|
|
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) {
|
|
35632
35799
|
const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
|
|
35633
35800
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
35634
|
-
const
|
|
35801
|
+
const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
|
|
35802
|
+
const prompt3 = stringOrUndefined4(beat.cues?.video) ?? keyframeCue;
|
|
35635
35803
|
if (!prompt3) return { status: "no-cue" };
|
|
35636
35804
|
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
35637
35805
|
const characterRefsAbs = characterRefsRel.map((p) => join16(ctx.projectDir, p));
|
|
@@ -35642,7 +35810,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
35642
35810
|
cue: prompt3,
|
|
35643
35811
|
provider: ctx.videoProvider,
|
|
35644
35812
|
duration: normalizeVideoDuration(beat.duration),
|
|
35645
|
-
|
|
35813
|
+
// Keyframe mode keys the clip to the keyframe prompt; refs feed the keyframe,
|
|
35814
|
+
// not the clip directly.
|
|
35815
|
+
characters: keyframeCue ? void 0 : characterRefsRel,
|
|
35816
|
+
keyframe: keyframeCue
|
|
35646
35817
|
});
|
|
35647
35818
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
35648
35819
|
if (existsSync19(abs) && !ctx.force) {
|
|
@@ -35694,6 +35865,13 @@ async function dispatchVideo(beat, ctx) {
|
|
|
35694
35865
|
freshness: "fresh"
|
|
35695
35866
|
};
|
|
35696
35867
|
}
|
|
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" };
|
|
35873
|
+
}
|
|
35874
|
+
const keyframeImageAbs = keyframeImageRel ? join16(ctx.projectDir, keyframeImageRel) : void 0;
|
|
35697
35875
|
loadSceneBuildEnv(ctx.projectDir);
|
|
35698
35876
|
const result = await executeVideoGenerate({
|
|
35699
35877
|
prompt: prompt3,
|
|
@@ -35702,7 +35880,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
35702
35880
|
ratio: "16:9",
|
|
35703
35881
|
output: abs,
|
|
35704
35882
|
wait: false,
|
|
35705
|
-
|
|
35883
|
+
// Keyframe mode → single init frame (image-to-video); otherwise character
|
|
35884
|
+
// reference-to-video.
|
|
35885
|
+
image: keyframeImageAbs,
|
|
35886
|
+
refImages: keyframeImageAbs ? void 0 : characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
35706
35887
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
35707
35888
|
});
|
|
35708
35889
|
if (!result.success || !result.taskId) {
|
|
@@ -35744,7 +35925,7 @@ async function dispatchVideo(beat, ctx) {
|
|
|
35744
35925
|
jobType: "generate-video",
|
|
35745
35926
|
provider: result.provider ?? ctx.videoProvider,
|
|
35746
35927
|
providerTaskId: result.taskId,
|
|
35747
|
-
providerTaskType: "text2video",
|
|
35928
|
+
providerTaskType: keyframeImageAbs ? "image2video" : "text2video",
|
|
35748
35929
|
status: "running",
|
|
35749
35930
|
projectDir: ctx.projectDir,
|
|
35750
35931
|
workingDirectory: ctx.projectDir,
|
|
@@ -36071,6 +36252,7 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
36071
36252
|
for (const outcome of outcomes) {
|
|
36072
36253
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
36073
36254
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
36255
|
+
if (outcome.keyframeStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
36074
36256
|
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
36075
36257
|
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
36076
36258
|
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeframe/cli",
|
|
3
|
-
"version": "0.113.
|
|
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/ai-providers": "0.113.
|
|
87
|
-
"@vibeframe/core": "0.113.
|
|
86
|
+
"@vibeframe/ai-providers": "0.113.4",
|
|
87
|
+
"@vibeframe/core": "0.113.4"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
90
|
"node": ">=20"
|