@vibeframe/cli 0.27.0 → 0.30.0
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/LICENSE +21 -0
- package/dist/agent/adapters/index.d.ts +1 -0
- package/dist/agent/adapters/index.d.ts.map +1 -1
- package/dist/agent/adapters/index.js +5 -0
- package/dist/agent/adapters/index.js.map +1 -1
- package/dist/agent/adapters/openrouter.d.ts +16 -0
- package/dist/agent/adapters/openrouter.d.ts.map +1 -0
- package/dist/agent/adapters/openrouter.js +100 -0
- package/dist/agent/adapters/openrouter.js.map +1 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +3 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/ai-edit-cli.d.ts.map +1 -1
- package/dist/commands/ai-edit-cli.js +18 -0
- package/dist/commands/ai-edit-cli.js.map +1 -1
- package/dist/commands/generate.js +14 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/schema.d.ts +1 -0
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +122 -21
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/setup.js +5 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/config/schema.d.ts +2 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index.js +0 -0
- package/package.json +16 -12
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-lint.log +0 -21
- package/.turbo/turbo-test.log +0 -689
- package/src/agent/adapters/claude.ts +0 -143
- package/src/agent/adapters/gemini.ts +0 -159
- package/src/agent/adapters/index.ts +0 -61
- package/src/agent/adapters/ollama.ts +0 -231
- package/src/agent/adapters/openai.ts +0 -116
- package/src/agent/adapters/xai.ts +0 -119
- package/src/agent/index.ts +0 -251
- package/src/agent/memory/index.ts +0 -151
- package/src/agent/prompts/system.ts +0 -106
- package/src/agent/tools/ai-editing.ts +0 -845
- package/src/agent/tools/ai-generation.ts +0 -1073
- package/src/agent/tools/ai-pipeline.ts +0 -1055
- package/src/agent/tools/ai.ts +0 -21
- package/src/agent/tools/batch.ts +0 -429
- package/src/agent/tools/e2e.test.ts +0 -545
- package/src/agent/tools/export.ts +0 -184
- package/src/agent/tools/filesystem.ts +0 -237
- package/src/agent/tools/index.ts +0 -150
- package/src/agent/tools/integration.test.ts +0 -775
- package/src/agent/tools/media.ts +0 -697
- package/src/agent/tools/project.ts +0 -313
- package/src/agent/tools/timeline.ts +0 -951
- package/src/agent/types.ts +0 -68
- package/src/commands/agent.ts +0 -340
- package/src/commands/ai-analyze.ts +0 -429
- package/src/commands/ai-animated-caption.ts +0 -390
- package/src/commands/ai-audio.ts +0 -941
- package/src/commands/ai-broll.ts +0 -490
- package/src/commands/ai-edit-cli.ts +0 -658
- package/src/commands/ai-edit.ts +0 -1542
- package/src/commands/ai-fill-gaps.ts +0 -566
- package/src/commands/ai-helpers.ts +0 -65
- package/src/commands/ai-highlights.ts +0 -1303
- package/src/commands/ai-image.ts +0 -761
- package/src/commands/ai-motion.ts +0 -347
- package/src/commands/ai-narrate.ts +0 -451
- package/src/commands/ai-review.ts +0 -309
- package/src/commands/ai-script-pipeline-cli.ts +0 -1710
- package/src/commands/ai-script-pipeline.ts +0 -1365
- package/src/commands/ai-suggest-edit.ts +0 -264
- package/src/commands/ai-video-fx.ts +0 -445
- package/src/commands/ai-video.ts +0 -915
- package/src/commands/ai-viral.ts +0 -595
- package/src/commands/ai-visual-fx.ts +0 -601
- package/src/commands/ai.test.ts +0 -627
- package/src/commands/ai.ts +0 -307
- package/src/commands/analyze.ts +0 -282
- package/src/commands/audio.ts +0 -644
- package/src/commands/batch.test.ts +0 -279
- package/src/commands/batch.ts +0 -440
- package/src/commands/detect.ts +0 -329
- package/src/commands/doctor.ts +0 -237
- package/src/commands/edit-cmd.ts +0 -1014
- package/src/commands/export.ts +0 -918
- package/src/commands/generate.ts +0 -2146
- package/src/commands/media.ts +0 -177
- package/src/commands/output.ts +0 -142
- package/src/commands/pipeline.ts +0 -398
- package/src/commands/project.test.ts +0 -127
- package/src/commands/project.ts +0 -149
- package/src/commands/sanitize.ts +0 -60
- package/src/commands/schema.ts +0 -130
- package/src/commands/setup.ts +0 -509
- package/src/commands/timeline.test.ts +0 -499
- package/src/commands/timeline.ts +0 -529
- package/src/commands/validate.ts +0 -77
- package/src/config/config.test.ts +0 -197
- package/src/config/index.ts +0 -125
- package/src/config/schema.ts +0 -82
- package/src/engine/index.ts +0 -2
- package/src/engine/project.test.ts +0 -702
- package/src/engine/project.ts +0 -439
- package/src/index.ts +0 -146
- package/src/utils/api-key.test.ts +0 -41
- package/src/utils/api-key.ts +0 -247
- package/src/utils/audio.ts +0 -83
- package/src/utils/exec-safe.ts +0 -75
- package/src/utils/first-run.ts +0 -52
- package/src/utils/provider-resolver.ts +0 -56
- package/src/utils/remotion.ts +0 -951
- package/src/utils/subtitle.test.ts +0 -227
- package/src/utils/subtitle.ts +0 -169
- package/src/utils/tty.ts +0 -196
- package/tsconfig.json +0 -20
package/src/commands/generate.ts
DELETED
|
@@ -1,2146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module generate
|
|
3
|
-
*
|
|
4
|
-
* Top-level `vibe generate` command group for AI asset generation.
|
|
5
|
-
*
|
|
6
|
-
* Commands:
|
|
7
|
-
* generate image - Generate image (Gemini, OpenAI, Grok, Runway)
|
|
8
|
-
* generate video - Generate video (Kling, Runway, Veo, Grok)
|
|
9
|
-
* generate speech - Text-to-speech (ElevenLabs)
|
|
10
|
-
* generate sound-effect - Sound effects (ElevenLabs)
|
|
11
|
-
* generate music - Music generation (ElevenLabs default, Replicate MusicGen)
|
|
12
|
-
* generate music-status - Check music generation status
|
|
13
|
-
* generate storyboard - Script-to-storyboard (Claude)
|
|
14
|
-
* generate motion - Motion graphics (Claude/Gemini + Remotion)
|
|
15
|
-
* generate thumbnail - Thumbnail generation/extraction
|
|
16
|
-
* generate background - AI background generation (OpenAI)
|
|
17
|
-
* generate video-status - Check video generation status (Grok/Runway/Kling)
|
|
18
|
-
* generate video-cancel - Cancel video generation (Grok/Runway)
|
|
19
|
-
* generate video-extend - Extend video (Kling/Veo)
|
|
20
|
-
*
|
|
21
|
-
* @dependencies OpenAI, Gemini, Runway, Kling, ElevenLabs, Replicate, Claude, FFmpeg
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { Command } from "commander";
|
|
25
|
-
import { resolve, dirname, basename, extname } from "node:path";
|
|
26
|
-
import { fileURLToPath } from "node:url";
|
|
27
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
28
|
-
import { existsSync } from "node:fs";
|
|
29
|
-
import chalk from "chalk";
|
|
30
|
-
import ora from "ora";
|
|
31
|
-
import imageSize from "image-size";
|
|
32
|
-
import {
|
|
33
|
-
GeminiProvider,
|
|
34
|
-
OpenAIImageProvider,
|
|
35
|
-
KlingProvider,
|
|
36
|
-
RunwayProvider,
|
|
37
|
-
ElevenLabsProvider,
|
|
38
|
-
ReplicateProvider,
|
|
39
|
-
ClaudeProvider,
|
|
40
|
-
GrokProvider,
|
|
41
|
-
} from "@vibeframe/ai-providers";
|
|
42
|
-
import { requireApiKey, hasApiKey } from "../utils/api-key.js";
|
|
43
|
-
import { hasTTY, prompt as promptText } from "../utils/tty.js";
|
|
44
|
-
import { getApiKeyFromConfig } from "../config/index.js";
|
|
45
|
-
import { sanitizeLLMResponse } from "./sanitize.js";
|
|
46
|
-
import { isJsonMode, outputResult, log, exitWithError, usageError, apiError } from "./output.js";
|
|
47
|
-
import { commandExists } from "../utils/exec-safe.js";
|
|
48
|
-
import { uploadToImgbb } from "./ai-script-pipeline.js";
|
|
49
|
-
import { downloadVideo, formatTime } from "./ai-helpers.js";
|
|
50
|
-
import { rejectControlChars } from "./validate.js";
|
|
51
|
-
import { resolveProvider } from "../utils/provider-resolver.js";
|
|
52
|
-
import { executeThumbnailBestFrame } from "./ai-image.js";
|
|
53
|
-
import { registerMotionCommand } from "./ai-motion.js";
|
|
54
|
-
|
|
55
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
function getStatusColor(status: string): string {
|
|
58
|
-
switch (status) {
|
|
59
|
-
case "completed":
|
|
60
|
-
return chalk.green(status);
|
|
61
|
-
case "processing":
|
|
62
|
-
case "running":
|
|
63
|
-
case "in_progress":
|
|
64
|
-
return chalk.yellow(status);
|
|
65
|
-
case "failed":
|
|
66
|
-
case "error":
|
|
67
|
-
return chalk.red(status);
|
|
68
|
-
default:
|
|
69
|
-
return chalk.gray(status);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Command group ────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
export const generateCommand = new Command("generate")
|
|
76
|
-
.alias("gen")
|
|
77
|
-
.description(
|
|
78
|
-
"Generate assets using AI (images, videos, speech, music, motion)"
|
|
79
|
-
)
|
|
80
|
-
.addHelpText(
|
|
81
|
-
"after",
|
|
82
|
-
`
|
|
83
|
-
Examples:
|
|
84
|
-
$ vibe generate image "a sunset over the ocean" -o sunset.png
|
|
85
|
-
$ vibe generate image "logo design" -o logo.png -p openai
|
|
86
|
-
$ vibe generate video "dancing cat" -o cat.mp4 # Grok (default, native audio)
|
|
87
|
-
$ vibe generate video "city timelapse" -o city.mp4 -p kling # Kling
|
|
88
|
-
$ vibe generate video "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
|
|
89
|
-
$ vibe generate speech "Hello world" -o hello.mp3
|
|
90
|
-
$ vibe generate music "upbeat jazz" -o jazz.mp3 -d 30
|
|
91
|
-
$ vibe generate motion "animated logo intro" -o intro.mp4 --render
|
|
92
|
-
|
|
93
|
-
API Keys (per provider):
|
|
94
|
-
GOOGLE_API_KEY Image (default), Veo video
|
|
95
|
-
OPENAI_API_KEY Image (-p openai)
|
|
96
|
-
XAI_API_KEY Grok image/video (default video)
|
|
97
|
-
KLING_API_KEY Kling video (-p kling)
|
|
98
|
-
RUNWAY_API_SECRET Runway video (-p runway)
|
|
99
|
-
ELEVENLABS_API_KEY Speech, sound effects, music
|
|
100
|
-
ANTHROPIC_API_KEY Storyboard, motion graphics
|
|
101
|
-
|
|
102
|
-
Run 'vibe setup --show' to check API key status.
|
|
103
|
-
Run 'vibe schema generate.<command>' for structured parameter info.
|
|
104
|
-
`
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// 1. Image
|
|
109
|
-
// ============================================================================
|
|
110
|
-
|
|
111
|
-
generateCommand
|
|
112
|
-
.command("image")
|
|
113
|
-
.alias("img")
|
|
114
|
-
.description("Generate image using AI (Gemini, DALL-E, or Runway)")
|
|
115
|
-
.argument("[prompt]", "Image description prompt (interactive if omitted)")
|
|
116
|
-
.option("-p, --provider <provider>", "Provider: gemini, openai, grok, runway (dalle is deprecated)", "gemini")
|
|
117
|
-
.option("-k, --api-key <key>", "API key (or set env: OPENAI_API_KEY, GOOGLE_API_KEY)")
|
|
118
|
-
.option("-o, --output <path>", "Output file path (downloads image)")
|
|
119
|
-
.option("-s, --size <size>", "Image size (openai: 1024x1024, 1536x1024, 1024x1536)", "1024x1024")
|
|
120
|
-
.option("-r, --ratio <ratio>", "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)", "1:1")
|
|
121
|
-
.option("-q, --quality <quality>", "Quality: standard, hd (openai only)", "standard")
|
|
122
|
-
.option("--style <style>", "Style: vivid, natural (openai only)", "vivid")
|
|
123
|
-
.option("-n, --count <n>", "Number of images to generate", "1")
|
|
124
|
-
.option("-m, --model <model>", "Gemini model: flash, 3.1-flash, latest (Nano Banana 2), pro (4K)")
|
|
125
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
126
|
-
.action(async (prompt: string | undefined, options) => {
|
|
127
|
-
try {
|
|
128
|
-
// Interactive prompt if no argument provided
|
|
129
|
-
if (!prompt) {
|
|
130
|
-
if (hasTTY()) {
|
|
131
|
-
prompt = await promptText(chalk.cyan("What would you like to generate? "));
|
|
132
|
-
if (!prompt?.trim()) {
|
|
133
|
-
console.error(chalk.red("Prompt is required."));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
console.error(chalk.red("Prompt argument is required."));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
rejectControlChars(prompt);
|
|
142
|
-
|
|
143
|
-
// Auto-resolve provider if user didn't explicitly set one
|
|
144
|
-
let provider = options.provider.toLowerCase();
|
|
145
|
-
const validProviders = ["openai", "dalle", "gemini", "grok", "runway"];
|
|
146
|
-
if (!validProviders.includes(provider)) {
|
|
147
|
-
exitWithError(usageError(`Invalid provider: ${provider}`, `Available providers: openai, gemini, grok, runway`));
|
|
148
|
-
}
|
|
149
|
-
// Auto-fallback: if default provider's key is missing, find one that works
|
|
150
|
-
const providerEnvMap: Record<string, string> = {
|
|
151
|
-
gemini: "GOOGLE_API_KEY", openai: "OPENAI_API_KEY", grok: "XAI_API_KEY",
|
|
152
|
-
};
|
|
153
|
-
if (providerEnvMap[provider] && !hasApiKey(providerEnvMap[provider]) && !options.apiKey) {
|
|
154
|
-
const resolved = resolveProvider("image");
|
|
155
|
-
if (resolved) {
|
|
156
|
-
log(chalk.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
|
|
157
|
-
provider = resolved.name;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Show deprecation warning for "dalle"
|
|
162
|
-
if (provider === "dalle") {
|
|
163
|
-
console.log(chalk.yellow('Warning: "dalle" is deprecated. Use "openai" instead.'));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Dry-run check
|
|
167
|
-
if (options.dryRun) {
|
|
168
|
-
outputResult({ dryRun: true, command: "generate image", params: { prompt, provider, model: options.model, ratio: options.ratio, size: options.size, quality: options.quality, count: options.count, output: options.output } });
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Get API key based on provider
|
|
173
|
-
const envKeyMap: Record<string, string> = {
|
|
174
|
-
openai: "OPENAI_API_KEY",
|
|
175
|
-
dalle: "OPENAI_API_KEY",
|
|
176
|
-
gemini: "GOOGLE_API_KEY",
|
|
177
|
-
grok: "XAI_API_KEY",
|
|
178
|
-
runway: "RUNWAY_API_SECRET",
|
|
179
|
-
};
|
|
180
|
-
const providerNameMap: Record<string, string> = {
|
|
181
|
-
openai: "OpenAI",
|
|
182
|
-
dalle: "OpenAI",
|
|
183
|
-
gemini: "Google",
|
|
184
|
-
grok: "xAI Grok",
|
|
185
|
-
runway: "Runway",
|
|
186
|
-
};
|
|
187
|
-
const envKey = envKeyMap[provider];
|
|
188
|
-
const providerName = providerNameMap[provider];
|
|
189
|
-
|
|
190
|
-
const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
|
|
191
|
-
|
|
192
|
-
const spinner = ora(`Generating image with ${providerName}...`).start();
|
|
193
|
-
|
|
194
|
-
if (provider === "dalle" || provider === "openai") {
|
|
195
|
-
const openaiImage = new OpenAIImageProvider();
|
|
196
|
-
await openaiImage.initialize({ apiKey });
|
|
197
|
-
|
|
198
|
-
const result = await openaiImage.generateImage(prompt, {
|
|
199
|
-
size: options.size,
|
|
200
|
-
quality: options.quality,
|
|
201
|
-
style: options.style,
|
|
202
|
-
n: parseInt(options.count),
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
if (!result.success || !result.images) {
|
|
206
|
-
spinner.fail(chalk.red(result.error || "Image generation failed"));
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with OpenAI GPT Image 1.5`));
|
|
211
|
-
|
|
212
|
-
if (isJsonMode()) {
|
|
213
|
-
const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
|
|
214
|
-
// Still save the file in JSON mode
|
|
215
|
-
if (outputPath && result.images.length > 0) {
|
|
216
|
-
const img = result.images[0];
|
|
217
|
-
let buffer: Buffer;
|
|
218
|
-
if (img.url) {
|
|
219
|
-
const response = await fetch(img.url);
|
|
220
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
221
|
-
} else if (img.base64) {
|
|
222
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
223
|
-
} else {
|
|
224
|
-
throw new Error("No image data available");
|
|
225
|
-
}
|
|
226
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
227
|
-
await writeFile(outputPath, buffer);
|
|
228
|
-
}
|
|
229
|
-
outputResult({ success: true, provider: "openai", images: result.images.map(img => ({ url: img.url, revisedPrompt: img.revisedPrompt })), outputPath });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
console.log();
|
|
234
|
-
console.log(chalk.bold.cyan("Generated Images"));
|
|
235
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
236
|
-
|
|
237
|
-
for (let i = 0; i < result.images.length; i++) {
|
|
238
|
-
const img = result.images[i];
|
|
239
|
-
console.log();
|
|
240
|
-
if (img.url) {
|
|
241
|
-
console.log(`${chalk.yellow(`[${i + 1}]`)} ${img.url}`);
|
|
242
|
-
} else if (img.base64) {
|
|
243
|
-
console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image data)`);
|
|
244
|
-
}
|
|
245
|
-
if (img.revisedPrompt) {
|
|
246
|
-
console.log(chalk.dim(` Revised: ${img.revisedPrompt.slice(0, 100)}...`));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
console.log();
|
|
250
|
-
|
|
251
|
-
// Save if output specified
|
|
252
|
-
if (options.output && result.images.length > 0) {
|
|
253
|
-
const img = result.images[0];
|
|
254
|
-
const saveSpinner = ora("Saving image...").start();
|
|
255
|
-
try {
|
|
256
|
-
let buffer: Buffer;
|
|
257
|
-
if (img.url) {
|
|
258
|
-
const response = await fetch(img.url);
|
|
259
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
260
|
-
} else if (img.base64) {
|
|
261
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
262
|
-
} else {
|
|
263
|
-
throw new Error("No image data available");
|
|
264
|
-
}
|
|
265
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
266
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
267
|
-
await writeFile(outputPath, buffer);
|
|
268
|
-
saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
269
|
-
} catch (err) {
|
|
270
|
-
saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} else if (provider === "gemini") {
|
|
274
|
-
// Validate model name
|
|
275
|
-
const validGeminiModels = ["flash", "3.1-flash", "latest", "pro"];
|
|
276
|
-
if (options.model && !validGeminiModels.includes(options.model)) {
|
|
277
|
-
console.warn(chalk.yellow(`Unknown model "${options.model}", using flash. Valid: ${validGeminiModels.join(", ")}`));
|
|
278
|
-
options.model = "flash";
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Validate aspect ratio
|
|
282
|
-
const validRatios = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"];
|
|
283
|
-
if (options.ratio && !validRatios.includes(options.ratio)) {
|
|
284
|
-
console.error(chalk.red(`Invalid ratio "${options.ratio}". Valid: ${validRatios.join(", ")}`));
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const gemini = new GeminiProvider();
|
|
289
|
-
await gemini.initialize({ apiKey });
|
|
290
|
-
|
|
291
|
-
const geminiModelNames: Record<string, string> = {
|
|
292
|
-
flash: "Nano Banana",
|
|
293
|
-
"3.1-flash": "Nano Banana 2",
|
|
294
|
-
latest: "Nano Banana 2",
|
|
295
|
-
pro: "Nano Banana Pro",
|
|
296
|
-
};
|
|
297
|
-
const modelLabel = geminiModelNames[options.model] || "Nano Banana";
|
|
298
|
-
|
|
299
|
-
let result = await gemini.generateImage(prompt, {
|
|
300
|
-
model: options.model,
|
|
301
|
-
aspectRatio: options.ratio as "1:1" | "1:4" | "1:8" | "2:3" | "3:2" | "3:4" | "4:1" | "4:3" | "4:5" | "5:4" | "8:1" | "9:16" | "16:9" | "21:9",
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Auto-fallback: if latest/3.1-flash fails, retry with flash
|
|
305
|
-
let usedLabel = modelLabel;
|
|
306
|
-
const fallbackModels = ["latest", "3.1-flash"];
|
|
307
|
-
if (!result.success && options.model && fallbackModels.includes(options.model)) {
|
|
308
|
-
spinner.text = `${chalk.dim(result.error || "Failed")} — retrying with Nano Banana (flash)...`;
|
|
309
|
-
result = await gemini.generateImage(prompt, {
|
|
310
|
-
model: "flash",
|
|
311
|
-
aspectRatio: options.ratio as "1:1" | "1:4" | "1:8" | "2:3" | "3:2" | "3:4" | "4:1" | "4:3" | "4:5" | "5:4" | "8:1" | "9:16" | "16:9" | "21:9",
|
|
312
|
-
});
|
|
313
|
-
usedLabel = "Nano Banana (fallback)";
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!result.success || !result.images) {
|
|
317
|
-
spinner.fail(chalk.red(result.error || "Image generation failed"));
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with Gemini (${usedLabel})`));
|
|
322
|
-
|
|
323
|
-
if (isJsonMode()) {
|
|
324
|
-
const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
|
|
325
|
-
if (outputPath && result.images.length > 0) {
|
|
326
|
-
const img = result.images[0];
|
|
327
|
-
const buffer = Buffer.from(img.base64, "base64");
|
|
328
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
329
|
-
await writeFile(outputPath, buffer);
|
|
330
|
-
}
|
|
331
|
-
outputResult({ success: true, provider: "gemini", images: result.images.map((img: { mimeType?: string }) => ({ mimeType: img.mimeType })), outputPath });
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
console.log();
|
|
336
|
-
console.log(chalk.bold.cyan("Generated Images"));
|
|
337
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
338
|
-
|
|
339
|
-
for (let i = 0; i < result.images.length; i++) {
|
|
340
|
-
const img = result.images[i];
|
|
341
|
-
console.log();
|
|
342
|
-
console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image, ${img.mimeType})`);
|
|
343
|
-
}
|
|
344
|
-
console.log();
|
|
345
|
-
|
|
346
|
-
// Save if output specified
|
|
347
|
-
if (options.output && result.images.length > 0) {
|
|
348
|
-
const saveSpinner = ora("Saving image...").start();
|
|
349
|
-
try {
|
|
350
|
-
const img = result.images[0];
|
|
351
|
-
const buffer = Buffer.from(img.base64, "base64");
|
|
352
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
353
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
354
|
-
await writeFile(outputPath, buffer);
|
|
355
|
-
saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
356
|
-
} catch (err) {
|
|
357
|
-
saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
|
|
358
|
-
}
|
|
359
|
-
} else {
|
|
360
|
-
console.log(chalk.yellow("Use -o to save the generated image to a file"));
|
|
361
|
-
}
|
|
362
|
-
} else if (provider === "grok") {
|
|
363
|
-
const grok = new GrokProvider();
|
|
364
|
-
await grok.initialize({ apiKey });
|
|
365
|
-
|
|
366
|
-
// Validate aspect ratio for Grok
|
|
367
|
-
const validGrokRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "2:1", "1:2", "19.5:9", "9:19.5", "20:9", "9:20", "auto"];
|
|
368
|
-
if (options.ratio && !validGrokRatios.includes(options.ratio)) {
|
|
369
|
-
console.warn(chalk.yellow(`Unknown ratio "${options.ratio}" for Grok, using 1:1. Valid: ${validGrokRatios.join(", ")}`));
|
|
370
|
-
options.ratio = "1:1";
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = await grok.generateImage(prompt, {
|
|
374
|
-
aspectRatio: options.ratio || "1:1",
|
|
375
|
-
n: parseInt(options.count),
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
if (!result.success || !result.images) {
|
|
379
|
-
spinner.fail(chalk.red(result.error || "Image generation failed"));
|
|
380
|
-
process.exit(1);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with xAI Grok`));
|
|
384
|
-
|
|
385
|
-
if (isJsonMode()) {
|
|
386
|
-
const outputPath = options.output ? resolve(process.cwd(), options.output) : undefined;
|
|
387
|
-
if (outputPath && result.images.length > 0) {
|
|
388
|
-
const img = result.images[0];
|
|
389
|
-
let buffer: Buffer;
|
|
390
|
-
if (img.url) {
|
|
391
|
-
const response = await fetch(img.url);
|
|
392
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
393
|
-
} else if (img.base64) {
|
|
394
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
395
|
-
} else {
|
|
396
|
-
throw new Error("No image data available");
|
|
397
|
-
}
|
|
398
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
399
|
-
await writeFile(outputPath, buffer);
|
|
400
|
-
}
|
|
401
|
-
outputResult({ success: true, provider: "grok", images: result.images.map(img => ({ url: img.url })), outputPath });
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
console.log();
|
|
406
|
-
console.log(chalk.bold.cyan("Generated Images"));
|
|
407
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
408
|
-
|
|
409
|
-
for (let i = 0; i < result.images.length; i++) {
|
|
410
|
-
const img = result.images[i];
|
|
411
|
-
console.log();
|
|
412
|
-
if (img.url) {
|
|
413
|
-
console.log(`${chalk.yellow(`[${i + 1}]`)} ${img.url}`);
|
|
414
|
-
} else if (img.base64) {
|
|
415
|
-
console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image data)`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
console.log();
|
|
419
|
-
|
|
420
|
-
// Save if output specified
|
|
421
|
-
if (options.output && result.images.length > 0) {
|
|
422
|
-
const img = result.images[0];
|
|
423
|
-
const saveSpinner = ora("Saving image...").start();
|
|
424
|
-
try {
|
|
425
|
-
let buffer: Buffer;
|
|
426
|
-
if (img.url) {
|
|
427
|
-
const response = await fetch(img.url);
|
|
428
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
429
|
-
} else if (img.base64) {
|
|
430
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
431
|
-
} else {
|
|
432
|
-
throw new Error("No image data available");
|
|
433
|
-
}
|
|
434
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
435
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
436
|
-
await writeFile(outputPath, buffer);
|
|
437
|
-
saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
438
|
-
} catch (err) {
|
|
439
|
-
saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
} else if (provider === "runway") {
|
|
443
|
-
const { spawn } = await import("child_process");
|
|
444
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
445
|
-
const __dirname = dirname(__filename);
|
|
446
|
-
const scriptPath = resolve(__dirname, "../../../../.claude/skills/runway-video/scripts/image.py");
|
|
447
|
-
|
|
448
|
-
if (!options.output) {
|
|
449
|
-
spinner.fail(chalk.red("Output path required for Runway. Use -o option."));
|
|
450
|
-
process.exit(1);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
454
|
-
const args = [scriptPath, prompt, "-o", outputPath, "-r", options.ratio || "16:9"];
|
|
455
|
-
|
|
456
|
-
spinner.text = "Generating image with Runway (gemini_2.5_flash)...";
|
|
457
|
-
|
|
458
|
-
await new Promise<void>((resolvePromise, reject) => {
|
|
459
|
-
const proc = spawn("python3", args, {
|
|
460
|
-
env: { ...process.env, RUNWAY_API_SECRET: apiKey },
|
|
461
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
let stdout = "";
|
|
465
|
-
let stderr = "";
|
|
466
|
-
|
|
467
|
-
proc.stdout.on("data", (data) => {
|
|
468
|
-
stdout += data.toString();
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
proc.stderr.on("data", (data) => {
|
|
472
|
-
stderr += data.toString();
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
proc.on("close", (code) => {
|
|
476
|
-
if (code === 0) {
|
|
477
|
-
if (isJsonMode()) {
|
|
478
|
-
outputResult({ success: true, provider: "runway", images: [{ format: "file" }], outputPath });
|
|
479
|
-
} else {
|
|
480
|
-
spinner.succeed(chalk.green("Generated image with Runway"));
|
|
481
|
-
console.log(chalk.dim(stdout.trim()));
|
|
482
|
-
}
|
|
483
|
-
resolvePromise();
|
|
484
|
-
} else {
|
|
485
|
-
spinner.fail(chalk.red("Runway image generation failed"));
|
|
486
|
-
console.error(chalk.red(stderr || stdout));
|
|
487
|
-
reject(new Error("Runway generation failed"));
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
proc.on("error", (err) => {
|
|
492
|
-
spinner.fail(chalk.red("Failed to run Runway script"));
|
|
493
|
-
reject(err);
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
} catch (error) {
|
|
498
|
-
exitWithError(apiError(`Image generation failed: ${(error as Error).message}`));
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// ============================================================================
|
|
503
|
-
// 2. Video (merged: ai video + ai kling, unified via --provider)
|
|
504
|
-
// ============================================================================
|
|
505
|
-
|
|
506
|
-
generateCommand
|
|
507
|
-
.command("video")
|
|
508
|
-
.alias("vid")
|
|
509
|
-
.description("Generate video using AI (Kling, Runway, Veo, or Grok)")
|
|
510
|
-
.argument("[prompt]", "Text prompt describing the video (interactive if omitted)")
|
|
511
|
-
.option("-p, --provider <provider>", "Provider: grok (default), kling, runway, veo", "grok")
|
|
512
|
-
.option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)")
|
|
513
|
-
.option("-o, --output <path>", "Output file path (downloads video)")
|
|
514
|
-
.option("-i, --image <path>", "Reference image for image-to-video")
|
|
515
|
-
.option("-d, --duration <sec>", "Duration: 5 or 10 seconds", "5")
|
|
516
|
-
.option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)")
|
|
517
|
-
.option("-s, --seed <number>", "Random seed for reproducibility (Runway only)")
|
|
518
|
-
.option("-m, --mode <mode>", "Generation mode: std or pro (Kling only)", "std")
|
|
519
|
-
.option("-n, --negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)")
|
|
520
|
-
.option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)")
|
|
521
|
-
.option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)")
|
|
522
|
-
.option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)")
|
|
523
|
-
.option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)")
|
|
524
|
-
.option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast")
|
|
525
|
-
.option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5")
|
|
526
|
-
.option("--no-wait", "Start generation and return task ID without waiting")
|
|
527
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
528
|
-
.action(async (prompt: string | undefined, options) => {
|
|
529
|
-
try {
|
|
530
|
-
// Interactive prompt if no argument provided
|
|
531
|
-
if (!prompt) {
|
|
532
|
-
if (hasTTY()) {
|
|
533
|
-
prompt = await promptText(chalk.cyan("Describe your video: "));
|
|
534
|
-
if (!prompt?.trim()) {
|
|
535
|
-
console.error(chalk.red("Prompt is required."));
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
console.error(chalk.red("Prompt argument is required."));
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
rejectControlChars(prompt);
|
|
544
|
-
|
|
545
|
-
let provider = options.provider.toLowerCase();
|
|
546
|
-
const validProviders = ["runway", "kling", "veo", "grok"];
|
|
547
|
-
if (!validProviders.includes(provider)) {
|
|
548
|
-
exitWithError(usageError(`Invalid provider: ${provider}`, `Available providers: ${validProviders.join(", ")}`));
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Auto-fallback: if default provider's key is missing, find one that works
|
|
552
|
-
const videoEnvMap: Record<string, string> = {
|
|
553
|
-
grok: "XAI_API_KEY", veo: "GOOGLE_API_KEY", kling: "KLING_API_KEY", runway: "RUNWAY_API_SECRET",
|
|
554
|
-
};
|
|
555
|
-
if (videoEnvMap[provider] && !hasApiKey(videoEnvMap[provider]) && !options.apiKey) {
|
|
556
|
-
const resolved = resolveProvider("video");
|
|
557
|
-
if (resolved) {
|
|
558
|
-
log(chalk.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
|
|
559
|
-
provider = resolved.name;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Read image early so we can auto-detect aspect ratio before dry-run
|
|
564
|
-
let referenceImage: string | undefined;
|
|
565
|
-
let isImageToVideo = false;
|
|
566
|
-
if (options.image) {
|
|
567
|
-
const imagePath = resolve(process.cwd(), options.image);
|
|
568
|
-
const imageBuffer = await readFile(imagePath);
|
|
569
|
-
const ext = options.image.toLowerCase().split(".").pop();
|
|
570
|
-
const mimeTypes: Record<string, string> = {
|
|
571
|
-
jpg: "image/jpeg",
|
|
572
|
-
jpeg: "image/jpeg",
|
|
573
|
-
png: "image/png",
|
|
574
|
-
gif: "image/gif",
|
|
575
|
-
webp: "image/webp",
|
|
576
|
-
};
|
|
577
|
-
const mimeType = mimeTypes[ext || "png"] || "image/png";
|
|
578
|
-
referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
579
|
-
isImageToVideo = true;
|
|
580
|
-
|
|
581
|
-
// Auto-detect aspect ratio from image dimensions when not explicitly set
|
|
582
|
-
if (!options.ratio) {
|
|
583
|
-
const dimensions = imageSize(imageBuffer);
|
|
584
|
-
if (dimensions.width && dimensions.height) {
|
|
585
|
-
const ratio = dimensions.width / dimensions.height;
|
|
586
|
-
if (ratio > 1.2) {
|
|
587
|
-
options.ratio = "16:9";
|
|
588
|
-
} else if (ratio < 0.8) {
|
|
589
|
-
options.ratio = "9:16";
|
|
590
|
-
} else {
|
|
591
|
-
options.ratio = "1:1";
|
|
592
|
-
}
|
|
593
|
-
log(`Auto-detected aspect ratio: ${options.ratio} (${dimensions.width}x${dimensions.height})`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Default to 16:9 when no image and no explicit ratio
|
|
599
|
-
if (!options.ratio) {
|
|
600
|
-
options.ratio = "16:9";
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Veo and Runway only support 16:9 and 9:16 — clamp 1:1 to 16:9
|
|
604
|
-
if ((provider === "veo" || provider === "runway") && options.ratio === "1:1") {
|
|
605
|
-
log(`${provider} does not support 1:1 — falling back to 16:9`);
|
|
606
|
-
options.ratio = "16:9";
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (options.dryRun) {
|
|
610
|
-
outputResult({ dryRun: true, command: "generate video", params: { prompt, provider, duration: options.duration, ratio: options.ratio, image: options.image, mode: options.mode, negative: options.negative, resolution: options.resolution, veoModel: options.veoModel } });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const envKeyMap: Record<string, string> = {
|
|
615
|
-
runway: "RUNWAY_API_SECRET",
|
|
616
|
-
kling: "KLING_API_KEY",
|
|
617
|
-
veo: "GOOGLE_API_KEY",
|
|
618
|
-
grok: "XAI_API_KEY",
|
|
619
|
-
};
|
|
620
|
-
const providerNameMap: Record<string, string> = {
|
|
621
|
-
runway: "Runway",
|
|
622
|
-
kling: "Kling",
|
|
623
|
-
veo: "Veo",
|
|
624
|
-
grok: "Grok",
|
|
625
|
-
};
|
|
626
|
-
const envKey = envKeyMap[provider];
|
|
627
|
-
const providerName = providerNameMap[provider];
|
|
628
|
-
const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
|
|
629
|
-
|
|
630
|
-
// Runway gen4_turbo requires an input image; gen4.5 supports text-to-video
|
|
631
|
-
const runwayModel = (options.runwayModel as string) || "gen4.5";
|
|
632
|
-
if (provider === "runway" && !options.image && runwayModel !== "gen4.5") {
|
|
633
|
-
console.error(chalk.red(`Runway ${runwayModel} requires an input image. Use -i <image> or use gen4.5 for text-to-video.`));
|
|
634
|
-
console.error(chalk.dim("Example: vibe generate video \"prompt\" -p runway -i image.png -o out.mp4"));
|
|
635
|
-
process.exit(1);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const spinner = ora(`Initializing ${providerName}...`).start();
|
|
639
|
-
|
|
640
|
-
spinner.text = "Starting video generation...";
|
|
641
|
-
|
|
642
|
-
let result;
|
|
643
|
-
let finalResult;
|
|
644
|
-
|
|
645
|
-
if (provider === "runway") {
|
|
646
|
-
const runway = new RunwayProvider();
|
|
647
|
-
await runway.initialize({ apiKey });
|
|
648
|
-
|
|
649
|
-
result = await runway.generateVideo(prompt, {
|
|
650
|
-
prompt,
|
|
651
|
-
referenceImage,
|
|
652
|
-
model: runwayModel,
|
|
653
|
-
duration: parseInt(options.duration),
|
|
654
|
-
aspectRatio: options.ratio as "16:9" | "9:16",
|
|
655
|
-
seed: options.seed ? parseInt(options.seed) : undefined,
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
if (result.status === "failed") {
|
|
659
|
-
spinner.fail(chalk.red(result.error || "Failed to start generation"));
|
|
660
|
-
process.exit(1);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
console.log();
|
|
664
|
-
console.log(chalk.bold.cyan("Video Generation Started"));
|
|
665
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
666
|
-
console.log(`Provider: ${chalk.bold(`Runway ${runwayModel}`)}`);
|
|
667
|
-
console.log(`Task ID: ${chalk.bold(result.id)}`);
|
|
668
|
-
|
|
669
|
-
if (!options.wait) {
|
|
670
|
-
spinner.succeed(chalk.green("Generation started"));
|
|
671
|
-
console.log();
|
|
672
|
-
console.log(chalk.dim("Check status with:"));
|
|
673
|
-
console.log(chalk.dim(` vibe generate video-status ${result.id} -p runway`));
|
|
674
|
-
console.log();
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
spinner.text = "Generating video (this may take 1-2 minutes)...";
|
|
679
|
-
|
|
680
|
-
finalResult = await runway.waitForCompletion(
|
|
681
|
-
result.id,
|
|
682
|
-
(status) => {
|
|
683
|
-
if (status.progress !== undefined) {
|
|
684
|
-
spinner.text = `Generating video... ${status.progress}%`;
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
300000
|
|
688
|
-
);
|
|
689
|
-
} else if (provider === "kling") {
|
|
690
|
-
const kling = new KlingProvider();
|
|
691
|
-
await kling.initialize({ apiKey });
|
|
692
|
-
|
|
693
|
-
if (!kling.isConfigured()) {
|
|
694
|
-
spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
|
|
695
|
-
process.exit(1);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Kling v2.x requires image URL, not base64 — auto-upload to ImgBB
|
|
699
|
-
let klingImage = referenceImage;
|
|
700
|
-
if (klingImage && klingImage.startsWith("data:")) {
|
|
701
|
-
spinner.text = "Uploading image to ImgBB for Kling...";
|
|
702
|
-
const imgbbKey = (await getApiKeyFromConfig("imgbb")) || process.env.IMGBB_API_KEY;
|
|
703
|
-
if (!imgbbKey) {
|
|
704
|
-
spinner.fail(chalk.red("Kling requires image URL. Set IMGBB_API_KEY for auto-upload."));
|
|
705
|
-
console.error(chalk.dim("Run: vibe setup --full to configure ImgBB"));
|
|
706
|
-
process.exit(1);
|
|
707
|
-
}
|
|
708
|
-
// Extract raw base64 from data URI
|
|
709
|
-
const base64Data = klingImage.split(",")[1];
|
|
710
|
-
const imageBuffer = Buffer.from(base64Data, "base64");
|
|
711
|
-
const uploadResult = await uploadToImgbb(imageBuffer, imgbbKey);
|
|
712
|
-
if (!uploadResult.success || !uploadResult.url) {
|
|
713
|
-
spinner.fail(chalk.red(`ImgBB upload failed: ${uploadResult.error}`));
|
|
714
|
-
process.exit(1);
|
|
715
|
-
}
|
|
716
|
-
klingImage = uploadResult.url;
|
|
717
|
-
spinner.text = "Starting video generation...";
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
result = await kling.generateVideo(prompt, {
|
|
721
|
-
prompt,
|
|
722
|
-
referenceImage: klingImage,
|
|
723
|
-
duration: parseInt(options.duration) as 5 | 10,
|
|
724
|
-
aspectRatio: options.ratio as "16:9" | "9:16" | "1:1",
|
|
725
|
-
negativePrompt: options.negative,
|
|
726
|
-
mode: options.mode as "std" | "pro",
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
if (result.status === "failed") {
|
|
730
|
-
spinner.fail(chalk.red(result.error || "Failed to start generation"));
|
|
731
|
-
process.exit(1);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
console.log();
|
|
735
|
-
console.log(chalk.bold.cyan("Video Generation Started"));
|
|
736
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
737
|
-
console.log(`Provider: ${chalk.bold("Kling AI")}`);
|
|
738
|
-
console.log(`Task ID: ${chalk.bold(result.id)}`);
|
|
739
|
-
console.log(`Type: ${isImageToVideo ? "image2video" : "text2video"}`);
|
|
740
|
-
|
|
741
|
-
if (!options.wait) {
|
|
742
|
-
spinner.succeed(chalk.green("Generation started"));
|
|
743
|
-
console.log();
|
|
744
|
-
console.log(chalk.dim("Check status with:"));
|
|
745
|
-
console.log(chalk.dim(` vibe generate video-status ${result.id} -p kling${isImageToVideo ? " --type image2video" : ""}`));
|
|
746
|
-
console.log();
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
spinner.text = "Generating video (this may take 2-5 minutes)...";
|
|
751
|
-
|
|
752
|
-
const taskType = isImageToVideo ? "image2video" : "text2video";
|
|
753
|
-
finalResult = await kling.waitForCompletion(
|
|
754
|
-
result.id,
|
|
755
|
-
taskType,
|
|
756
|
-
(status) => {
|
|
757
|
-
spinner.text = `Generating video... ${status.status}`;
|
|
758
|
-
},
|
|
759
|
-
600000
|
|
760
|
-
);
|
|
761
|
-
} else if (provider === "veo") {
|
|
762
|
-
const gemini = new GeminiProvider();
|
|
763
|
-
await gemini.initialize({ apiKey });
|
|
764
|
-
|
|
765
|
-
// Map Veo model alias to full model ID
|
|
766
|
-
const veoModelMap: Record<string, string> = {
|
|
767
|
-
"3.0": "veo-3.0-generate-preview",
|
|
768
|
-
"3.1": "veo-3.1-generate-preview",
|
|
769
|
-
"3.1-fast": "veo-3.1-fast-generate-preview",
|
|
770
|
-
};
|
|
771
|
-
const veoModel = veoModelMap[options.veoModel] || "veo-3.1-fast-generate-preview";
|
|
772
|
-
|
|
773
|
-
const veoDuration = parseInt(options.duration) <= 6 ? 6 : 8;
|
|
774
|
-
|
|
775
|
-
// Prepare last frame if provided
|
|
776
|
-
let lastFrame: string | undefined;
|
|
777
|
-
if (options.lastFrame) {
|
|
778
|
-
const lastFramePath = resolve(process.cwd(), options.lastFrame);
|
|
779
|
-
const lastFrameBuffer = await readFile(lastFramePath);
|
|
780
|
-
const ext = options.lastFrame.toLowerCase().split(".").pop();
|
|
781
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
|
|
782
|
-
lastFrame = `data:${mimeType};base64,${lastFrameBuffer.toString("base64")}`;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Prepare reference images if provided
|
|
786
|
-
let refImages: Array<{ base64: string; mimeType: string }> | undefined;
|
|
787
|
-
if (options.refImages && options.refImages.length > 0) {
|
|
788
|
-
refImages = [];
|
|
789
|
-
for (const refPath of options.refImages.slice(0, 3)) {
|
|
790
|
-
const absRefPath = resolve(process.cwd(), refPath);
|
|
791
|
-
const refBuffer = await readFile(absRefPath);
|
|
792
|
-
const ext = refPath.toLowerCase().split(".").pop();
|
|
793
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
|
|
794
|
-
refImages.push({ base64: refBuffer.toString("base64"), mimeType });
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
result = await gemini.generateVideo(prompt, {
|
|
799
|
-
prompt,
|
|
800
|
-
referenceImage,
|
|
801
|
-
duration: veoDuration,
|
|
802
|
-
aspectRatio: options.ratio as "16:9" | "9:16" | "1:1",
|
|
803
|
-
model: veoModel as "veo-3.0-generate-preview" | "veo-3.1-generate-preview" | "veo-3.1-fast-generate-preview",
|
|
804
|
-
negativePrompt: options.negative,
|
|
805
|
-
resolution: options.resolution as "720p" | "1080p" | "4k" | undefined,
|
|
806
|
-
lastFrame,
|
|
807
|
-
referenceImages: refImages,
|
|
808
|
-
personGeneration: options.person as "allow_all" | "allow_adult" | undefined,
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
if (result.status === "failed") {
|
|
812
|
-
spinner.fail(chalk.red(result.error || "Failed to start generation"));
|
|
813
|
-
process.exit(1);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
console.log();
|
|
817
|
-
console.log(chalk.bold.cyan("Video Generation Started"));
|
|
818
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
819
|
-
console.log(`Provider: ${chalk.bold("Google Veo 3.1")}`);
|
|
820
|
-
console.log(`Task ID: ${chalk.bold(result.id)}`);
|
|
821
|
-
|
|
822
|
-
if (!options.wait) {
|
|
823
|
-
spinner.succeed(chalk.green("Generation started"));
|
|
824
|
-
console.log();
|
|
825
|
-
console.log(chalk.dim("Veo generation is synchronous - video URL available above"));
|
|
826
|
-
console.log();
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
spinner.text = "Generating video (this may take 1-3 minutes)...";
|
|
831
|
-
finalResult = await gemini.waitForVideoCompletion(
|
|
832
|
-
result.id,
|
|
833
|
-
(status) => {
|
|
834
|
-
spinner.text = `Generating video... ${status.status}`;
|
|
835
|
-
},
|
|
836
|
-
300000
|
|
837
|
-
);
|
|
838
|
-
} else if (provider === "grok") {
|
|
839
|
-
const grok = new GrokProvider();
|
|
840
|
-
await grok.initialize({ apiKey });
|
|
841
|
-
|
|
842
|
-
result = await grok.generateVideo(prompt, {
|
|
843
|
-
prompt,
|
|
844
|
-
referenceImage,
|
|
845
|
-
duration: parseInt(options.duration),
|
|
846
|
-
aspectRatio: options.ratio as "16:9" | "9:16" | "1:1",
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
if (result.status === "failed") {
|
|
850
|
-
spinner.fail(chalk.red(result.error || "Failed to start generation"));
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
console.log();
|
|
855
|
-
console.log(chalk.bold.cyan("Video Generation Started"));
|
|
856
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
857
|
-
console.log(`Provider: ${chalk.bold("xAI Grok Imagine")}`);
|
|
858
|
-
console.log(`Task ID: ${chalk.bold(result.id)}`);
|
|
859
|
-
|
|
860
|
-
if (!options.wait) {
|
|
861
|
-
spinner.succeed(chalk.green("Generation started"));
|
|
862
|
-
console.log();
|
|
863
|
-
console.log(chalk.dim("Check status with:"));
|
|
864
|
-
console.log(chalk.dim(` vibe generate video-status ${result.id} -p grok`));
|
|
865
|
-
console.log();
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
spinner.text = "Generating video (this may take 1-3 minutes)...";
|
|
870
|
-
finalResult = await grok.waitForCompletion(
|
|
871
|
-
result.id,
|
|
872
|
-
(status) => {
|
|
873
|
-
spinner.text = `Generating video... ${status.status}`;
|
|
874
|
-
},
|
|
875
|
-
300000
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (!finalResult || finalResult.status !== "completed") {
|
|
880
|
-
spinner.fail(chalk.red(finalResult?.error || "Generation failed"));
|
|
881
|
-
process.exit(1);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
spinner.succeed(chalk.green("Video generated"));
|
|
885
|
-
|
|
886
|
-
if (isJsonMode()) {
|
|
887
|
-
let outputPath: string | undefined;
|
|
888
|
-
if (options.output && finalResult.videoUrl) {
|
|
889
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
890
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
891
|
-
await writeFile(outputPath, buffer);
|
|
892
|
-
}
|
|
893
|
-
outputResult({ success: true, provider, taskId: result?.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
console.log();
|
|
898
|
-
if (finalResult.videoUrl) {
|
|
899
|
-
console.log(`Video URL: ${finalResult.videoUrl}`);
|
|
900
|
-
}
|
|
901
|
-
if (finalResult.duration) {
|
|
902
|
-
console.log(`Duration: ${finalResult.duration}s`);
|
|
903
|
-
}
|
|
904
|
-
console.log();
|
|
905
|
-
|
|
906
|
-
if (options.output && finalResult.videoUrl) {
|
|
907
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
908
|
-
try {
|
|
909
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
910
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
911
|
-
await writeFile(outputPath, buffer);
|
|
912
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
913
|
-
} catch (err) {
|
|
914
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
} catch (error) {
|
|
918
|
-
exitWithError(apiError(`Video generation failed: ${(error as Error).message}`));
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// ============================================================================
|
|
923
|
-
// 3. Speech (was: ai tts)
|
|
924
|
-
// ============================================================================
|
|
925
|
-
|
|
926
|
-
generateCommand
|
|
927
|
-
.command("speech")
|
|
928
|
-
.alias("tts")
|
|
929
|
-
.description("Generate speech from text using ElevenLabs")
|
|
930
|
-
.argument("[text]", "Text to convert to speech (interactive if omitted)")
|
|
931
|
-
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
932
|
-
.option("-o, --output <path>", "Output audio file path", "output.mp3")
|
|
933
|
-
.option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM")
|
|
934
|
-
.option("--list-voices", "List available voices")
|
|
935
|
-
.option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat)
|
|
936
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
937
|
-
.action(async (text: string | undefined, options) => {
|
|
938
|
-
try {
|
|
939
|
-
// Interactive prompt if no argument provided
|
|
940
|
-
if (!text) {
|
|
941
|
-
if (hasTTY()) {
|
|
942
|
-
text = await promptText(chalk.cyan("What text to speak? "));
|
|
943
|
-
if (!text?.trim()) {
|
|
944
|
-
console.error(chalk.red("Text is required."));
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
} else {
|
|
948
|
-
console.error(chalk.red("Text argument is required."));
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
rejectControlChars(text);
|
|
953
|
-
|
|
954
|
-
if (options.dryRun) {
|
|
955
|
-
outputResult({ dryRun: true, command: "generate speech", params: { text, voice: options.voice, output: options.output } });
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
960
|
-
|
|
961
|
-
const elevenlabs = new ElevenLabsProvider();
|
|
962
|
-
await elevenlabs.initialize({ apiKey });
|
|
963
|
-
|
|
964
|
-
// List voices mode
|
|
965
|
-
if (options.listVoices) {
|
|
966
|
-
const spinner = ora("Fetching voices...").start();
|
|
967
|
-
const voices = await elevenlabs.getVoices();
|
|
968
|
-
spinner.succeed(chalk.green(`Found ${voices.length} voices`));
|
|
969
|
-
|
|
970
|
-
console.log();
|
|
971
|
-
console.log(chalk.bold.cyan("Available Voices"));
|
|
972
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
973
|
-
|
|
974
|
-
for (const voice of voices) {
|
|
975
|
-
console.log();
|
|
976
|
-
console.log(`${chalk.bold(voice.name)} ${chalk.dim(`(${voice.voice_id})`)}`);
|
|
977
|
-
console.log(` Category: ${voice.category}`);
|
|
978
|
-
if (voice.labels) {
|
|
979
|
-
const labels = Object.entries(voice.labels)
|
|
980
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
981
|
-
.join(", ");
|
|
982
|
-
console.log(` ${chalk.dim(labels)}`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
console.log();
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const spinner = ora("Generating speech...").start();
|
|
990
|
-
|
|
991
|
-
const result = await elevenlabs.textToSpeech(text, {
|
|
992
|
-
voiceId: options.voice,
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
if (!result.success || !result.audioBuffer) {
|
|
996
|
-
spinner.fail(chalk.red(result.error || "TTS generation failed"));
|
|
997
|
-
process.exit(1);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1001
|
-
await writeFile(outputPath, result.audioBuffer);
|
|
1002
|
-
|
|
1003
|
-
spinner.succeed(chalk.green("Speech generated"));
|
|
1004
|
-
|
|
1005
|
-
// Post-process: fit to target duration via atempo
|
|
1006
|
-
if (options.fitDuration && options.fitDuration > 0) {
|
|
1007
|
-
const { ffprobeDuration, execSafe } = await import("../utils/exec-safe.js");
|
|
1008
|
-
const actualDuration = await ffprobeDuration(outputPath);
|
|
1009
|
-
|
|
1010
|
-
if (actualDuration > options.fitDuration) {
|
|
1011
|
-
const tempo = actualDuration / options.fitDuration;
|
|
1012
|
-
if (tempo > 2.0) {
|
|
1013
|
-
log(chalk.yellow(`Warning: Audio is ${tempo.toFixed(1)}x longer than target — would sound unnatural. Skipping tempo adjustment.`));
|
|
1014
|
-
} else {
|
|
1015
|
-
const fitSpinner = ora(`Adjusting tempo (${tempo.toFixed(3)}x) to fit ${options.fitDuration}s...`).start();
|
|
1016
|
-
const tempPath = outputPath.replace(/(\.\w+)$/, `.tempo$1`);
|
|
1017
|
-
try {
|
|
1018
|
-
await execSafe("ffmpeg", [
|
|
1019
|
-
"-y", "-i", outputPath,
|
|
1020
|
-
"-filter:a", `atempo=${tempo.toFixed(4)}`,
|
|
1021
|
-
"-vn", tempPath,
|
|
1022
|
-
]);
|
|
1023
|
-
const { rename } = await import("node:fs/promises");
|
|
1024
|
-
await rename(tempPath, outputPath);
|
|
1025
|
-
fitSpinner.succeed(chalk.green(`Adjusted to fit ${options.fitDuration}s (${tempo.toFixed(3)}x speed)`));
|
|
1026
|
-
} catch (err) {
|
|
1027
|
-
fitSpinner.fail(chalk.yellow("Tempo adjustment failed — keeping original audio"));
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
} else {
|
|
1031
|
-
log(chalk.dim(`Audio (${actualDuration.toFixed(2)}s) already fits within ${options.fitDuration}s`));
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (isJsonMode()) {
|
|
1036
|
-
outputResult({ success: true, characterCount: result.characterCount, outputPath });
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
console.log();
|
|
1041
|
-
console.log(chalk.dim(`Characters: ${result.characterCount}`));
|
|
1042
|
-
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
1043
|
-
console.log();
|
|
1044
|
-
} catch (error) {
|
|
1045
|
-
console.error(chalk.red("TTS generation failed"));
|
|
1046
|
-
console.error(error);
|
|
1047
|
-
process.exit(1);
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
// ============================================================================
|
|
1052
|
-
// 4. Sound Effect (was: ai sfx)
|
|
1053
|
-
// Note: -p is reserved for --provider; --prompt-influence is long-only
|
|
1054
|
-
// ============================================================================
|
|
1055
|
-
|
|
1056
|
-
generateCommand
|
|
1057
|
-
.command("sound-effect")
|
|
1058
|
-
.description("Generate sound effect using ElevenLabs")
|
|
1059
|
-
.argument("<prompt>", "Description of the sound effect")
|
|
1060
|
-
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
1061
|
-
.option("-o, --output <path>", "Output audio file path", "sound-effect.mp3")
|
|
1062
|
-
.option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)")
|
|
1063
|
-
.option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)")
|
|
1064
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
1065
|
-
.action(async (prompt: string, options) => {
|
|
1066
|
-
try {
|
|
1067
|
-
rejectControlChars(prompt);
|
|
1068
|
-
|
|
1069
|
-
if (options.dryRun) {
|
|
1070
|
-
outputResult({ dryRun: true, command: "generate sound-effect", params: { prompt, duration: options.duration, promptInfluence: options.promptInfluence, output: options.output } });
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
1075
|
-
|
|
1076
|
-
const spinner = ora("Generating sound effect...").start();
|
|
1077
|
-
|
|
1078
|
-
const elevenlabs = new ElevenLabsProvider();
|
|
1079
|
-
await elevenlabs.initialize({ apiKey });
|
|
1080
|
-
|
|
1081
|
-
const result = await elevenlabs.generateSoundEffect(prompt, {
|
|
1082
|
-
duration: options.duration ? parseFloat(options.duration) : undefined,
|
|
1083
|
-
promptInfluence: options.promptInfluence ? parseFloat(options.promptInfluence) : undefined,
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
if (!result.success || !result.audioBuffer) {
|
|
1087
|
-
spinner.fail(chalk.red(result.error || "Sound effect generation failed"));
|
|
1088
|
-
process.exit(1);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1092
|
-
await writeFile(outputPath, result.audioBuffer);
|
|
1093
|
-
|
|
1094
|
-
spinner.succeed(chalk.green("Sound effect generated"));
|
|
1095
|
-
|
|
1096
|
-
if (isJsonMode()) {
|
|
1097
|
-
outputResult({ success: true, outputPath });
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
1102
|
-
console.log();
|
|
1103
|
-
} catch (error) {
|
|
1104
|
-
console.error(chalk.red("Sound effect generation failed"));
|
|
1105
|
-
console.error(error);
|
|
1106
|
-
process.exit(1);
|
|
1107
|
-
}
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
// ============================================================================
|
|
1111
|
-
// 5. Music
|
|
1112
|
-
// ============================================================================
|
|
1113
|
-
|
|
1114
|
-
generateCommand
|
|
1115
|
-
.command("music")
|
|
1116
|
-
.description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)")
|
|
1117
|
-
.argument("<prompt>", "Description of the music to generate")
|
|
1118
|
-
.option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs")
|
|
1119
|
-
.option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)")
|
|
1120
|
-
.option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8")
|
|
1121
|
-
.option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)")
|
|
1122
|
-
.option("-m, --melody <file>", "Reference melody audio file for conditioning (Replicate only)")
|
|
1123
|
-
.option("--model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large")
|
|
1124
|
-
.option("-o, --output <path>", "Output audio file path", "music.mp3")
|
|
1125
|
-
.option("--no-wait", "Don't wait for generation to complete (Replicate async mode)")
|
|
1126
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
1127
|
-
.action(async (prompt: string, options) => {
|
|
1128
|
-
try {
|
|
1129
|
-
rejectControlChars(prompt);
|
|
1130
|
-
|
|
1131
|
-
const provider = (options.provider || "elevenlabs").toLowerCase();
|
|
1132
|
-
|
|
1133
|
-
if (options.dryRun) {
|
|
1134
|
-
outputResult({ dryRun: true, command: "generate music", params: { prompt, provider, duration: options.duration, model: options.model, output: options.output, instrumental: options.instrumental } });
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
if (provider === "elevenlabs") {
|
|
1139
|
-
// ElevenLabs Music API — synchronous, up to 10 minutes
|
|
1140
|
-
const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
1141
|
-
|
|
1142
|
-
const elevenlabs = new ElevenLabsProvider();
|
|
1143
|
-
await elevenlabs.initialize({ apiKey });
|
|
1144
|
-
|
|
1145
|
-
const duration = Math.max(3, Math.min(600, parseFloat(options.duration)));
|
|
1146
|
-
const spinner = ora(`Generating music (${duration}s)...`).start();
|
|
1147
|
-
|
|
1148
|
-
const result = await elevenlabs.generateMusic(prompt, {
|
|
1149
|
-
duration,
|
|
1150
|
-
forceInstrumental: options.instrumental || false,
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
if (!result.success || !result.audioBuffer) {
|
|
1154
|
-
spinner.fail(chalk.red(result.error || "Music generation failed"));
|
|
1155
|
-
process.exit(1);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1159
|
-
await writeFile(outputPath, result.audioBuffer);
|
|
1160
|
-
|
|
1161
|
-
spinner.succeed(chalk.green("Music generated successfully"));
|
|
1162
|
-
|
|
1163
|
-
if (isJsonMode()) {
|
|
1164
|
-
outputResult({ success: true, provider: "elevenlabs", outputPath, duration });
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
console.log();
|
|
1169
|
-
console.log(`Saved to: ${chalk.bold(outputPath)}`);
|
|
1170
|
-
console.log(`Duration: ${duration}s`);
|
|
1171
|
-
console.log(`Provider: ElevenLabs (music_v1)`);
|
|
1172
|
-
if (options.instrumental) console.log(`Mode: Instrumental`);
|
|
1173
|
-
console.log();
|
|
1174
|
-
} else {
|
|
1175
|
-
// Replicate MusicGen — async, max 30 seconds
|
|
1176
|
-
const apiKey = await requireApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
1177
|
-
|
|
1178
|
-
const replicate = new ReplicateProvider();
|
|
1179
|
-
await replicate.initialize({ apiKey });
|
|
1180
|
-
|
|
1181
|
-
const spinner = ora("Starting music generation...").start();
|
|
1182
|
-
|
|
1183
|
-
const duration = Math.max(1, Math.min(30, parseFloat(options.duration)));
|
|
1184
|
-
|
|
1185
|
-
// If melody file provided, upload it first
|
|
1186
|
-
if (options.melody) {
|
|
1187
|
-
spinner.text = "Uploading melody reference...";
|
|
1188
|
-
const absPath = resolve(process.cwd(), options.melody);
|
|
1189
|
-
if (!existsSync(absPath)) {
|
|
1190
|
-
spinner.fail(chalk.red(`Melody file not found: ${options.melody}`));
|
|
1191
|
-
process.exit(1);
|
|
1192
|
-
}
|
|
1193
|
-
console.log(chalk.yellow("Note: Melody conditioning requires a publicly accessible URL"));
|
|
1194
|
-
console.log(chalk.yellow("Please upload your melody file and provide the URL"));
|
|
1195
|
-
process.exit(1);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const result = await replicate.generateMusic(prompt, {
|
|
1199
|
-
duration,
|
|
1200
|
-
model: options.model as "large" | "stereo-large" | "melody-large" | "stereo-melody-large",
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
if (!result.success || !result.taskId) {
|
|
1204
|
-
spinner.fail(chalk.red(result.error || "Music generation failed"));
|
|
1205
|
-
process.exit(1);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
if (!options.wait) {
|
|
1209
|
-
spinner.succeed(chalk.green("Music generation started"));
|
|
1210
|
-
console.log();
|
|
1211
|
-
console.log(`Task ID: ${chalk.bold(result.taskId)}`);
|
|
1212
|
-
console.log(chalk.dim("Check status with: vibe generate music-status " + result.taskId));
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
spinner.text = "Generating music (this may take a few minutes)...";
|
|
1217
|
-
|
|
1218
|
-
const finalResult = await replicate.waitForMusic(result.taskId);
|
|
1219
|
-
|
|
1220
|
-
if (!finalResult.success || !finalResult.audioUrl) {
|
|
1221
|
-
spinner.fail(chalk.red(finalResult.error || "Music generation failed"));
|
|
1222
|
-
process.exit(1);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
spinner.text = "Downloading generated audio...";
|
|
1226
|
-
|
|
1227
|
-
const response = await fetch(finalResult.audioUrl);
|
|
1228
|
-
if (!response.ok) {
|
|
1229
|
-
spinner.fail(chalk.red("Failed to download generated audio"));
|
|
1230
|
-
process.exit(1);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const audioBuffer = Buffer.from(await response.arrayBuffer());
|
|
1234
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1235
|
-
await writeFile(outputPath, audioBuffer);
|
|
1236
|
-
|
|
1237
|
-
spinner.succeed(chalk.green("Music generated successfully"));
|
|
1238
|
-
|
|
1239
|
-
if (isJsonMode()) {
|
|
1240
|
-
outputResult({ success: true, provider: "replicate", taskId: result.taskId, audioUrl: finalResult.audioUrl, outputPath });
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
console.log();
|
|
1245
|
-
console.log(`Saved to: ${chalk.bold(outputPath)}`);
|
|
1246
|
-
console.log(`Duration: ${duration}s`);
|
|
1247
|
-
console.log(`Model: ${options.model}`);
|
|
1248
|
-
console.log();
|
|
1249
|
-
}
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
console.error(chalk.red("Music generation failed"));
|
|
1252
|
-
console.error(error);
|
|
1253
|
-
process.exit(1);
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
// ============================================================================
|
|
1258
|
-
// 6. Music Status
|
|
1259
|
-
// ============================================================================
|
|
1260
|
-
|
|
1261
|
-
generateCommand
|
|
1262
|
-
.command("music-status", { hidden: true })
|
|
1263
|
-
.description("Check music generation status")
|
|
1264
|
-
.argument("<task-id>", "Task ID from music generation")
|
|
1265
|
-
.option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
|
|
1266
|
-
.action(async (taskId: string, options) => {
|
|
1267
|
-
try {
|
|
1268
|
-
const apiKey = await requireApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
1269
|
-
|
|
1270
|
-
const replicate = new ReplicateProvider();
|
|
1271
|
-
await replicate.initialize({ apiKey });
|
|
1272
|
-
|
|
1273
|
-
const result = await replicate.getMusicStatus(taskId);
|
|
1274
|
-
|
|
1275
|
-
if (isJsonMode()) {
|
|
1276
|
-
const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
|
|
1277
|
-
outputResult({ success: true, taskId, status, audioUrl: result.audioUrl, error: result.error });
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
console.log();
|
|
1282
|
-
console.log(chalk.bold.cyan("Music Generation Status"));
|
|
1283
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1284
|
-
console.log(`Task ID: ${taskId}`);
|
|
1285
|
-
|
|
1286
|
-
if (result.audioUrl) {
|
|
1287
|
-
console.log(`Status: ${chalk.green("completed")}`);
|
|
1288
|
-
console.log(`Audio URL: ${result.audioUrl}`);
|
|
1289
|
-
} else if (result.error) {
|
|
1290
|
-
console.log(`Status: ${chalk.red("failed")}`);
|
|
1291
|
-
console.log(`Error: ${result.error}`);
|
|
1292
|
-
} else {
|
|
1293
|
-
console.log(`Status: ${chalk.yellow("processing")}`);
|
|
1294
|
-
}
|
|
1295
|
-
console.log();
|
|
1296
|
-
} catch (error) {
|
|
1297
|
-
console.error(chalk.red("Failed to get music status"));
|
|
1298
|
-
console.error(error);
|
|
1299
|
-
process.exit(1);
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
// ============================================================================
|
|
1304
|
-
// 7. Storyboard
|
|
1305
|
-
// ============================================================================
|
|
1306
|
-
|
|
1307
|
-
generateCommand
|
|
1308
|
-
.command("storyboard")
|
|
1309
|
-
.description("Generate video storyboard from content using Claude")
|
|
1310
|
-
.argument("<content>", "Content to analyze (text or file path)")
|
|
1311
|
-
.option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)")
|
|
1312
|
-
.option("-o, --output <path>", "Output JSON file path")
|
|
1313
|
-
.option("-d, --duration <sec>", "Target total duration in seconds")
|
|
1314
|
-
.option("-f, --file", "Treat content argument as file path")
|
|
1315
|
-
.option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low")
|
|
1316
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
1317
|
-
.action(async (content: string, options) => {
|
|
1318
|
-
try {
|
|
1319
|
-
rejectControlChars(content);
|
|
1320
|
-
|
|
1321
|
-
// Validate creativity level
|
|
1322
|
-
const creativity = options.creativity?.toLowerCase();
|
|
1323
|
-
if (creativity && creativity !== "low" && creativity !== "high") {
|
|
1324
|
-
console.error(chalk.red("Invalid creativity level. Use 'low' or 'high'."));
|
|
1325
|
-
process.exit(1);
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
let textContent = content;
|
|
1329
|
-
if (options.file) {
|
|
1330
|
-
const filePath = resolve(process.cwd(), content);
|
|
1331
|
-
textContent = await readFile(filePath, "utf-8");
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
if (options.dryRun) {
|
|
1335
|
-
outputResult({ dryRun: true, command: "generate storyboard", params: { content: textContent.substring(0, 200), duration: options.duration, creativity } });
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
const apiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
|
|
1340
|
-
|
|
1341
|
-
const spinnerText = creativity === "high"
|
|
1342
|
-
? "Analyzing content with high creativity..."
|
|
1343
|
-
: "Analyzing content...";
|
|
1344
|
-
const spinner = ora(spinnerText).start();
|
|
1345
|
-
|
|
1346
|
-
const claude = new ClaudeProvider();
|
|
1347
|
-
await claude.initialize({ apiKey });
|
|
1348
|
-
|
|
1349
|
-
const segments = await claude.analyzeContent(
|
|
1350
|
-
textContent,
|
|
1351
|
-
options.duration ? parseFloat(options.duration) : undefined,
|
|
1352
|
-
{ creativity: creativity as "low" | "high" | undefined }
|
|
1353
|
-
);
|
|
1354
|
-
|
|
1355
|
-
if (segments.length === 0) {
|
|
1356
|
-
spinner.fail(chalk.red("Could not generate storyboard"));
|
|
1357
|
-
process.exit(1);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
spinner.succeed(chalk.green(`Generated ${segments.length} segments`));
|
|
1361
|
-
|
|
1362
|
-
for (const seg of segments) {
|
|
1363
|
-
seg.description = sanitizeLLMResponse(seg.description);
|
|
1364
|
-
if (seg.visuals) seg.visuals = sanitizeLLMResponse(seg.visuals);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
if (options.output) {
|
|
1368
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1369
|
-
await writeFile(outputPath, JSON.stringify(segments, null, 2), "utf-8");
|
|
1370
|
-
if (isJsonMode()) {
|
|
1371
|
-
outputResult({ success: true, segmentCount: segments.length, segments, outputPath });
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
} else if (isJsonMode()) {
|
|
1375
|
-
outputResult({ success: true, segmentCount: segments.length, segments });
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
console.log();
|
|
1380
|
-
console.log(chalk.bold.cyan("Storyboard"));
|
|
1381
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1382
|
-
|
|
1383
|
-
for (const seg of segments) {
|
|
1384
|
-
console.log();
|
|
1385
|
-
console.log(chalk.yellow(`[${seg.index + 1}] ${formatTime(seg.startTime)} - ${formatTime(seg.startTime + seg.duration)}`));
|
|
1386
|
-
console.log(` ${seg.description}`);
|
|
1387
|
-
console.log(chalk.dim(` Visuals: ${seg.visuals}`));
|
|
1388
|
-
if (seg.audio) {
|
|
1389
|
-
console.log(chalk.dim(` Audio: ${seg.audio}`));
|
|
1390
|
-
}
|
|
1391
|
-
if (seg.textOverlays && seg.textOverlays.length > 0) {
|
|
1392
|
-
console.log(chalk.dim(` Text: ${seg.textOverlays.join(", ")}`));
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
console.log();
|
|
1396
|
-
|
|
1397
|
-
if (options.output) {
|
|
1398
|
-
console.log(chalk.green(`Saved to: ${resolve(process.cwd(), options.output)}`));
|
|
1399
|
-
}
|
|
1400
|
-
} catch (error) {
|
|
1401
|
-
console.error(chalk.red("Storyboard generation failed"));
|
|
1402
|
-
console.error(error);
|
|
1403
|
-
process.exit(1);
|
|
1404
|
-
}
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
// ============================================================================
|
|
1408
|
-
// 8. Motion (delegated to registerMotionCommand)
|
|
1409
|
-
// ============================================================================
|
|
1410
|
-
|
|
1411
|
-
registerMotionCommand(generateCommand);
|
|
1412
|
-
|
|
1413
|
-
// ============================================================================
|
|
1414
|
-
// 9. Thumbnail
|
|
1415
|
-
// ============================================================================
|
|
1416
|
-
|
|
1417
|
-
generateCommand
|
|
1418
|
-
.command("thumbnail")
|
|
1419
|
-
.description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)")
|
|
1420
|
-
.argument("[description]", "Thumbnail description (for DALL-E generation)")
|
|
1421
|
-
.option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)")
|
|
1422
|
-
.option("-o, --output <path>", "Output file path")
|
|
1423
|
-
.option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter")
|
|
1424
|
-
.option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI")
|
|
1425
|
-
.option("--prompt <prompt>", "Custom prompt for best-frame analysis")
|
|
1426
|
-
.option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash")
|
|
1427
|
-
.action(async (description: string | undefined, options) => {
|
|
1428
|
-
try {
|
|
1429
|
-
if (description) rejectControlChars(description);
|
|
1430
|
-
|
|
1431
|
-
// Best-frame mode: analyze video with Gemini and extract frame
|
|
1432
|
-
if (options.bestFrame) {
|
|
1433
|
-
const absVideoPath = resolve(process.cwd(), options.bestFrame);
|
|
1434
|
-
if (!existsSync(absVideoPath)) {
|
|
1435
|
-
console.error(chalk.red(`Video not found: ${absVideoPath}`));
|
|
1436
|
-
process.exit(1);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
if (!commandExists("ffmpeg")) {
|
|
1440
|
-
console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
|
|
1441
|
-
process.exit(1);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
|
|
1445
|
-
|
|
1446
|
-
const name = basename(options.bestFrame, extname(options.bestFrame));
|
|
1447
|
-
const outputPath = options.output || `${name}-thumbnail.png`;
|
|
1448
|
-
|
|
1449
|
-
const spinner = ora("Analyzing video for best frame...").start();
|
|
1450
|
-
|
|
1451
|
-
const result = await executeThumbnailBestFrame({
|
|
1452
|
-
videoPath: absVideoPath,
|
|
1453
|
-
outputPath: resolve(process.cwd(), outputPath),
|
|
1454
|
-
prompt: options.prompt,
|
|
1455
|
-
model: options.model,
|
|
1456
|
-
apiKey,
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
if (!result.success) {
|
|
1460
|
-
spinner.fail(chalk.red(result.error || "Best frame extraction failed"));
|
|
1461
|
-
process.exit(1);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
spinner.succeed(chalk.green("Best frame extracted"));
|
|
1465
|
-
|
|
1466
|
-
if (isJsonMode()) {
|
|
1467
|
-
outputResult({ success: true, timestamp: result.timestamp, reason: result.reason, outputPath: result.outputPath });
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
console.log();
|
|
1472
|
-
console.log(chalk.bold.cyan("Best Frame Result"));
|
|
1473
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1474
|
-
console.log(`Timestamp: ${chalk.bold(result.timestamp!.toFixed(2))}s`);
|
|
1475
|
-
if (result.reason) console.log(`Reason: ${chalk.dim(result.reason)}`);
|
|
1476
|
-
console.log(`Output: ${chalk.green(result.outputPath!)}`);
|
|
1477
|
-
console.log();
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
// Generation mode: create thumbnail with DALL-E
|
|
1482
|
-
if (!description) {
|
|
1483
|
-
console.error(chalk.red("Description required for thumbnail generation."));
|
|
1484
|
-
console.error(chalk.dim("Usage: vibe generate thumbnail <description> or vibe generate thumbnail --best-frame <video>"));
|
|
1485
|
-
process.exit(1);
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
const apiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
|
|
1489
|
-
|
|
1490
|
-
const spinner = ora("Generating thumbnail...").start();
|
|
1491
|
-
|
|
1492
|
-
const openaiImage = new OpenAIImageProvider();
|
|
1493
|
-
await openaiImage.initialize({ apiKey });
|
|
1494
|
-
|
|
1495
|
-
const result = await openaiImage.generateThumbnail(description, options.style);
|
|
1496
|
-
|
|
1497
|
-
if (!result.success || !result.images) {
|
|
1498
|
-
spinner.fail(chalk.red(result.error || "Thumbnail generation failed"));
|
|
1499
|
-
process.exit(1);
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
spinner.succeed(chalk.green("Thumbnail generated"));
|
|
1503
|
-
|
|
1504
|
-
const img = result.images[0];
|
|
1505
|
-
|
|
1506
|
-
if (isJsonMode()) {
|
|
1507
|
-
let outputPath: string | undefined;
|
|
1508
|
-
if (options.output) {
|
|
1509
|
-
let buffer: Buffer;
|
|
1510
|
-
if (img.url) {
|
|
1511
|
-
const response = await fetch(img.url);
|
|
1512
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
1513
|
-
} else if (img.base64) {
|
|
1514
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
1515
|
-
} else {
|
|
1516
|
-
throw new Error("No image data available");
|
|
1517
|
-
}
|
|
1518
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
1519
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
1520
|
-
await writeFile(outputPath, buffer);
|
|
1521
|
-
}
|
|
1522
|
-
outputResult({ success: true, imageUrl: img.url, outputPath });
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
console.log();
|
|
1527
|
-
console.log(chalk.bold.cyan("Generated Thumbnail"));
|
|
1528
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1529
|
-
console.log(`URL: ${img.url}`);
|
|
1530
|
-
if (img.revisedPrompt) {
|
|
1531
|
-
console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
|
|
1532
|
-
}
|
|
1533
|
-
console.log();
|
|
1534
|
-
|
|
1535
|
-
// Save if output specified
|
|
1536
|
-
if (options.output) {
|
|
1537
|
-
const saveSpinner = ora("Saving thumbnail...").start();
|
|
1538
|
-
try {
|
|
1539
|
-
let buffer: Buffer;
|
|
1540
|
-
if (img.url) {
|
|
1541
|
-
const response = await fetch(img.url);
|
|
1542
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
1543
|
-
} else if (img.base64) {
|
|
1544
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
1545
|
-
} else {
|
|
1546
|
-
throw new Error("No image data available");
|
|
1547
|
-
}
|
|
1548
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1549
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
1550
|
-
await writeFile(outputPath, buffer);
|
|
1551
|
-
saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
1552
|
-
} catch (err) {
|
|
1553
|
-
saveSpinner.fail(chalk.red("Failed to save thumbnail"));
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
} catch (error) {
|
|
1557
|
-
console.error(chalk.red("Thumbnail generation failed"));
|
|
1558
|
-
console.error(error);
|
|
1559
|
-
process.exit(1);
|
|
1560
|
-
}
|
|
1561
|
-
});
|
|
1562
|
-
|
|
1563
|
-
// ============================================================================
|
|
1564
|
-
// 10. Background
|
|
1565
|
-
// ============================================================================
|
|
1566
|
-
|
|
1567
|
-
generateCommand
|
|
1568
|
-
.command("background")
|
|
1569
|
-
.description("Generate video background using DALL-E")
|
|
1570
|
-
.argument("<description>", "Background description")
|
|
1571
|
-
.option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)")
|
|
1572
|
-
.option("-o, --output <path>", "Output file path (downloads image)")
|
|
1573
|
-
.option("-a, --aspect <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9")
|
|
1574
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
1575
|
-
.action(async (description: string, options) => {
|
|
1576
|
-
try {
|
|
1577
|
-
rejectControlChars(description);
|
|
1578
|
-
|
|
1579
|
-
if (options.dryRun) {
|
|
1580
|
-
outputResult({ dryRun: true, command: "generate background", params: { description, aspect: options.aspect, output: options.output } });
|
|
1581
|
-
return;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
const apiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
|
|
1585
|
-
|
|
1586
|
-
const spinner = ora("Generating background...").start();
|
|
1587
|
-
|
|
1588
|
-
const openaiImage = new OpenAIImageProvider();
|
|
1589
|
-
await openaiImage.initialize({ apiKey });
|
|
1590
|
-
|
|
1591
|
-
const result = await openaiImage.generateBackground(description, options.aspect);
|
|
1592
|
-
|
|
1593
|
-
if (!result.success || !result.images) {
|
|
1594
|
-
spinner.fail(chalk.red(result.error || "Background generation failed"));
|
|
1595
|
-
process.exit(1);
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
spinner.succeed(chalk.green("Background generated"));
|
|
1599
|
-
|
|
1600
|
-
const img = result.images[0];
|
|
1601
|
-
|
|
1602
|
-
if (isJsonMode()) {
|
|
1603
|
-
let outputPath: string | undefined;
|
|
1604
|
-
if (options.output) {
|
|
1605
|
-
let buffer: Buffer;
|
|
1606
|
-
if (img.url) {
|
|
1607
|
-
const response = await fetch(img.url);
|
|
1608
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
1609
|
-
} else if (img.base64) {
|
|
1610
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
1611
|
-
} else {
|
|
1612
|
-
throw new Error("No image data available");
|
|
1613
|
-
}
|
|
1614
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
1615
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
1616
|
-
await writeFile(outputPath, buffer);
|
|
1617
|
-
}
|
|
1618
|
-
outputResult({ success: true, imageUrl: img.url, outputPath });
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
console.log();
|
|
1623
|
-
console.log(chalk.bold.cyan("Generated Background"));
|
|
1624
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1625
|
-
console.log(`Image: ${img.url || "(base64 data)"}`);
|
|
1626
|
-
if (img.revisedPrompt) {
|
|
1627
|
-
console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
|
|
1628
|
-
}
|
|
1629
|
-
console.log();
|
|
1630
|
-
|
|
1631
|
-
// Save if output specified
|
|
1632
|
-
if (options.output) {
|
|
1633
|
-
const saveSpinner = ora("Saving background...").start();
|
|
1634
|
-
try {
|
|
1635
|
-
let buffer: Buffer;
|
|
1636
|
-
if (img.url) {
|
|
1637
|
-
const response = await fetch(img.url);
|
|
1638
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
1639
|
-
} else if (img.base64) {
|
|
1640
|
-
buffer = Buffer.from(img.base64, "base64");
|
|
1641
|
-
} else {
|
|
1642
|
-
throw new Error("No image data available");
|
|
1643
|
-
}
|
|
1644
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1645
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
1646
|
-
await writeFile(outputPath, buffer);
|
|
1647
|
-
saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
1648
|
-
} catch (err) {
|
|
1649
|
-
saveSpinner.fail(chalk.red("Failed to save background"));
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
} catch (error) {
|
|
1653
|
-
console.error(chalk.red("Background generation failed"));
|
|
1654
|
-
console.error(error);
|
|
1655
|
-
process.exit(1);
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
1658
|
-
|
|
1659
|
-
// ============================================================================
|
|
1660
|
-
// 11. Video Status (merged: ai video-status + ai kling-status)
|
|
1661
|
-
// ============================================================================
|
|
1662
|
-
|
|
1663
|
-
generateCommand
|
|
1664
|
-
.command("video-status", { hidden: true })
|
|
1665
|
-
.description("Check video generation status (Grok, Runway, or Kling)")
|
|
1666
|
-
.argument("<task-id>", "Task ID from video generation")
|
|
1667
|
-
.option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok")
|
|
1668
|
-
.option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)")
|
|
1669
|
-
.option("-t, --type <type>", "Task type: text2video or image2video (Kling only)", "text2video")
|
|
1670
|
-
.option("-w, --wait", "Wait for completion")
|
|
1671
|
-
.option("-o, --output <path>", "Download video when complete")
|
|
1672
|
-
.action(async (taskId: string, options) => {
|
|
1673
|
-
try {
|
|
1674
|
-
const provider = (options.provider || "grok").toLowerCase();
|
|
1675
|
-
|
|
1676
|
-
if (provider === "grok") {
|
|
1677
|
-
const apiKey = await requireApiKey("XAI_API_KEY", "xAI", options.apiKey);
|
|
1678
|
-
|
|
1679
|
-
const spinner = ora("Checking status...").start();
|
|
1680
|
-
|
|
1681
|
-
const grok = new GrokProvider();
|
|
1682
|
-
await grok.initialize({ apiKey });
|
|
1683
|
-
|
|
1684
|
-
let result = await grok.getGenerationStatus(taskId);
|
|
1685
|
-
|
|
1686
|
-
if (options.wait && result.status !== "completed" && result.status !== "failed") {
|
|
1687
|
-
spinner.text = "Waiting for completion...";
|
|
1688
|
-
result = await grok.waitForCompletion(
|
|
1689
|
-
taskId,
|
|
1690
|
-
(status) => {
|
|
1691
|
-
spinner.text = `Generating... ${status.status}`;
|
|
1692
|
-
}
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
spinner.stop();
|
|
1697
|
-
|
|
1698
|
-
if (isJsonMode()) {
|
|
1699
|
-
let outputPath: string | undefined;
|
|
1700
|
-
if (options.output && result.videoUrl) {
|
|
1701
|
-
const buffer = await downloadVideo(result.videoUrl);
|
|
1702
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
1703
|
-
await writeFile(outputPath, buffer);
|
|
1704
|
-
}
|
|
1705
|
-
outputResult({ success: true, taskId, provider: "grok", status: result.status, videoUrl: result.videoUrl, error: result.error, outputPath });
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
console.log();
|
|
1710
|
-
console.log(chalk.bold.cyan("Generation Status"));
|
|
1711
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1712
|
-
console.log(`Task ID: ${taskId}`);
|
|
1713
|
-
console.log(`Provider: Grok Imagine`);
|
|
1714
|
-
console.log(`Status: ${getStatusColor(result.status)}`);
|
|
1715
|
-
if (result.videoUrl) {
|
|
1716
|
-
console.log(`Video URL: ${result.videoUrl}`);
|
|
1717
|
-
}
|
|
1718
|
-
if (result.error) {
|
|
1719
|
-
console.log(`Error: ${chalk.red(result.error)}`);
|
|
1720
|
-
}
|
|
1721
|
-
console.log();
|
|
1722
|
-
|
|
1723
|
-
if (options.output && result.videoUrl) {
|
|
1724
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
1725
|
-
try {
|
|
1726
|
-
const buffer = await downloadVideo(result.videoUrl);
|
|
1727
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1728
|
-
await writeFile(outputPath, buffer);
|
|
1729
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
1730
|
-
} catch (err) {
|
|
1731
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
} else if (provider === "runway") {
|
|
1735
|
-
const apiKey = await requireApiKey("RUNWAY_API_SECRET", "Runway", options.apiKey);
|
|
1736
|
-
|
|
1737
|
-
const spinner = ora("Checking status...").start();
|
|
1738
|
-
|
|
1739
|
-
const runway = new RunwayProvider();
|
|
1740
|
-
await runway.initialize({ apiKey });
|
|
1741
|
-
|
|
1742
|
-
let result = await runway.getGenerationStatus(taskId);
|
|
1743
|
-
|
|
1744
|
-
if (options.wait && result.status !== "completed" && result.status !== "failed" && result.status !== "cancelled") {
|
|
1745
|
-
spinner.text = "Waiting for completion...";
|
|
1746
|
-
result = await runway.waitForCompletion(
|
|
1747
|
-
taskId,
|
|
1748
|
-
(status) => {
|
|
1749
|
-
if (status.progress !== undefined) {
|
|
1750
|
-
spinner.text = `Generating... ${status.progress}%`;
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
);
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
spinner.stop();
|
|
1757
|
-
|
|
1758
|
-
if (isJsonMode()) {
|
|
1759
|
-
let outputPath: string | undefined;
|
|
1760
|
-
if (options.output && result.videoUrl) {
|
|
1761
|
-
const buffer = await downloadVideo(result.videoUrl, apiKey);
|
|
1762
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
1763
|
-
await writeFile(outputPath, buffer);
|
|
1764
|
-
}
|
|
1765
|
-
outputResult({ success: true, taskId, provider: "runway", status: result.status, videoUrl: result.videoUrl, progress: result.progress, error: result.error, outputPath });
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
console.log();
|
|
1770
|
-
console.log(chalk.bold.cyan("Generation Status"));
|
|
1771
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1772
|
-
console.log(`Task ID: ${taskId}`);
|
|
1773
|
-
console.log(`Provider: Runway`);
|
|
1774
|
-
console.log(`Status: ${getStatusColor(result.status)}`);
|
|
1775
|
-
if (result.progress !== undefined) {
|
|
1776
|
-
console.log(`Progress: ${result.progress}%`);
|
|
1777
|
-
}
|
|
1778
|
-
if (result.videoUrl) {
|
|
1779
|
-
console.log(`Video URL: ${result.videoUrl}`);
|
|
1780
|
-
}
|
|
1781
|
-
if (result.error) {
|
|
1782
|
-
console.log(`Error: ${chalk.red(result.error)}`);
|
|
1783
|
-
}
|
|
1784
|
-
console.log();
|
|
1785
|
-
|
|
1786
|
-
if (options.output && result.videoUrl) {
|
|
1787
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
1788
|
-
try {
|
|
1789
|
-
const buffer = await downloadVideo(result.videoUrl, apiKey);
|
|
1790
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1791
|
-
await writeFile(outputPath, buffer);
|
|
1792
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
1793
|
-
} catch (err) {
|
|
1794
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
} else if (provider === "kling") {
|
|
1798
|
-
const apiKey = await requireApiKey("KLING_API_KEY", "Kling", options.apiKey);
|
|
1799
|
-
|
|
1800
|
-
const spinner = ora("Checking status...").start();
|
|
1801
|
-
|
|
1802
|
-
const kling = new KlingProvider();
|
|
1803
|
-
await kling.initialize({ apiKey });
|
|
1804
|
-
|
|
1805
|
-
const taskType = options.type as "text2video" | "image2video";
|
|
1806
|
-
let result = await kling.getGenerationStatus(taskId, taskType);
|
|
1807
|
-
|
|
1808
|
-
if (options.wait && result.status !== "completed" && result.status !== "failed" && result.status !== "cancelled") {
|
|
1809
|
-
spinner.text = "Waiting for completion...";
|
|
1810
|
-
result = await kling.waitForCompletion(
|
|
1811
|
-
taskId,
|
|
1812
|
-
taskType,
|
|
1813
|
-
(status) => {
|
|
1814
|
-
spinner.text = `Generating... ${status.status}`;
|
|
1815
|
-
}
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
spinner.stop();
|
|
1820
|
-
|
|
1821
|
-
if (isJsonMode()) {
|
|
1822
|
-
let outputPath: string | undefined;
|
|
1823
|
-
if (options.output && result.videoUrl) {
|
|
1824
|
-
const buffer = await downloadVideo(result.videoUrl, apiKey);
|
|
1825
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
1826
|
-
await writeFile(outputPath, buffer);
|
|
1827
|
-
}
|
|
1828
|
-
outputResult({ success: true, taskId, provider: "kling", status: result.status, videoUrl: result.videoUrl, duration: result.duration, error: result.error, outputPath });
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
console.log();
|
|
1833
|
-
console.log(chalk.bold.cyan("Generation Status"));
|
|
1834
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1835
|
-
console.log(`Task ID: ${taskId}`);
|
|
1836
|
-
console.log(`Provider: Kling`);
|
|
1837
|
-
console.log(`Type: ${taskType}`);
|
|
1838
|
-
console.log(`Status: ${getStatusColor(result.status)}`);
|
|
1839
|
-
if (result.videoUrl) {
|
|
1840
|
-
console.log(`Video URL: ${result.videoUrl}`);
|
|
1841
|
-
}
|
|
1842
|
-
if (result.duration) {
|
|
1843
|
-
console.log(`Duration: ${result.duration}s`);
|
|
1844
|
-
}
|
|
1845
|
-
if (result.error) {
|
|
1846
|
-
console.log(`Error: ${chalk.red(result.error)}`);
|
|
1847
|
-
}
|
|
1848
|
-
console.log();
|
|
1849
|
-
|
|
1850
|
-
if (options.output && result.videoUrl) {
|
|
1851
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
1852
|
-
try {
|
|
1853
|
-
const buffer = await downloadVideo(result.videoUrl, apiKey);
|
|
1854
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
1855
|
-
await writeFile(outputPath, buffer);
|
|
1856
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
1857
|
-
} catch (err) {
|
|
1858
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
} else {
|
|
1862
|
-
console.error(chalk.red(`Invalid provider: ${provider}. Use grok, runway, or kling.`));
|
|
1863
|
-
process.exit(1);
|
|
1864
|
-
}
|
|
1865
|
-
} catch (error) {
|
|
1866
|
-
console.error(chalk.red("Failed to get status"));
|
|
1867
|
-
console.error(error);
|
|
1868
|
-
process.exit(1);
|
|
1869
|
-
}
|
|
1870
|
-
});
|
|
1871
|
-
|
|
1872
|
-
// ============================================================================
|
|
1873
|
-
// 12. Video Cancel
|
|
1874
|
-
// ============================================================================
|
|
1875
|
-
|
|
1876
|
-
generateCommand
|
|
1877
|
-
.command("video-cancel", { hidden: true })
|
|
1878
|
-
.description("Cancel video generation (Grok or Runway)")
|
|
1879
|
-
.argument("<task-id>", "Task ID to cancel")
|
|
1880
|
-
.option("-p, --provider <provider>", "Provider: grok, runway", "grok")
|
|
1881
|
-
.option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET env)")
|
|
1882
|
-
.action(async (taskId: string, options) => {
|
|
1883
|
-
try {
|
|
1884
|
-
const provider = (options.provider || "grok").toLowerCase();
|
|
1885
|
-
|
|
1886
|
-
let success = false;
|
|
1887
|
-
|
|
1888
|
-
if (provider === "grok") {
|
|
1889
|
-
const apiKey = await requireApiKey("XAI_API_KEY", "xAI", options.apiKey);
|
|
1890
|
-
|
|
1891
|
-
const spinner = ora("Cancelling generation...").start();
|
|
1892
|
-
const grok = new GrokProvider();
|
|
1893
|
-
await grok.initialize({ apiKey });
|
|
1894
|
-
success = await grok.cancelGeneration(taskId);
|
|
1895
|
-
|
|
1896
|
-
if (success) {
|
|
1897
|
-
spinner.succeed(chalk.green("Generation cancelled"));
|
|
1898
|
-
if (isJsonMode()) {
|
|
1899
|
-
outputResult({ success: true, taskId, provider: "grok", cancelled: true });
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
} else {
|
|
1903
|
-
spinner.fail(chalk.red("Failed to cancel generation"));
|
|
1904
|
-
process.exit(1);
|
|
1905
|
-
}
|
|
1906
|
-
} else if (provider === "runway") {
|
|
1907
|
-
const apiKey = await requireApiKey("RUNWAY_API_SECRET", "Runway", options.apiKey);
|
|
1908
|
-
|
|
1909
|
-
const spinner = ora("Cancelling generation...").start();
|
|
1910
|
-
const runway = new RunwayProvider();
|
|
1911
|
-
await runway.initialize({ apiKey });
|
|
1912
|
-
success = await runway.cancelGeneration(taskId);
|
|
1913
|
-
|
|
1914
|
-
if (success) {
|
|
1915
|
-
spinner.succeed(chalk.green("Generation cancelled"));
|
|
1916
|
-
if (isJsonMode()) {
|
|
1917
|
-
outputResult({ success: true, taskId, provider: "runway", cancelled: true });
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
} else {
|
|
1921
|
-
spinner.fail(chalk.red("Failed to cancel generation"));
|
|
1922
|
-
process.exit(1);
|
|
1923
|
-
}
|
|
1924
|
-
} else {
|
|
1925
|
-
console.error(chalk.red(`Invalid provider: ${provider}. Use grok or runway.`));
|
|
1926
|
-
process.exit(1);
|
|
1927
|
-
}
|
|
1928
|
-
} catch (error) {
|
|
1929
|
-
console.error(chalk.red("Failed to cancel"));
|
|
1930
|
-
console.error(error);
|
|
1931
|
-
process.exit(1);
|
|
1932
|
-
}
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
// ============================================================================
|
|
1936
|
-
// 13. Video Extend (merged: ai video-extend + ai veo-extend)
|
|
1937
|
-
// Note: --prompt is long-only (-p is reserved for --provider)
|
|
1938
|
-
// ============================================================================
|
|
1939
|
-
|
|
1940
|
-
generateCommand
|
|
1941
|
-
.command("video-extend", { hidden: true })
|
|
1942
|
-
.description("Extend video duration (Kling by video ID, Veo by operation name)")
|
|
1943
|
-
.argument("<id>", "Kling video ID or Veo operation name")
|
|
1944
|
-
.option("-p, --provider <provider>", "Provider: kling, veo", "kling")
|
|
1945
|
-
.option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)")
|
|
1946
|
-
.option("-o, --output <path>", "Output file path")
|
|
1947
|
-
.option("--prompt <text>", "Continuation prompt")
|
|
1948
|
-
.option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5")
|
|
1949
|
-
.option("-n, --negative <prompt>", "Negative prompt (what to avoid, Kling only)")
|
|
1950
|
-
.option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1")
|
|
1951
|
-
.option("--no-wait", "Start extension and return task ID without waiting")
|
|
1952
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
1953
|
-
.action(async (id: string, options) => {
|
|
1954
|
-
try {
|
|
1955
|
-
const provider = (options.provider || "kling").toLowerCase();
|
|
1956
|
-
|
|
1957
|
-
if (options.dryRun) {
|
|
1958
|
-
outputResult({ dryRun: true, command: "generate video-extend", params: { id, provider, prompt: options.prompt, duration: options.duration, negative: options.negative, veoModel: options.veoModel } });
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
if (provider === "kling") {
|
|
1963
|
-
const apiKey = await requireApiKey("KLING_API_KEY", "Kling", options.apiKey);
|
|
1964
|
-
|
|
1965
|
-
const spinner = ora("Initializing Kling AI...").start();
|
|
1966
|
-
|
|
1967
|
-
const kling = new KlingProvider();
|
|
1968
|
-
await kling.initialize({ apiKey });
|
|
1969
|
-
|
|
1970
|
-
if (!kling.isConfigured()) {
|
|
1971
|
-
spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
|
|
1972
|
-
process.exit(1);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
spinner.text = "Starting video extension...";
|
|
1976
|
-
|
|
1977
|
-
const result = await kling.extendVideo(id, {
|
|
1978
|
-
prompt: options.prompt,
|
|
1979
|
-
negativePrompt: options.negative,
|
|
1980
|
-
duration: options.duration as "5" | "10",
|
|
1981
|
-
});
|
|
1982
|
-
|
|
1983
|
-
if (result.status === "failed") {
|
|
1984
|
-
spinner.fail(chalk.red(result.error || "Failed to start extension"));
|
|
1985
|
-
process.exit(1);
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
console.log();
|
|
1989
|
-
console.log(chalk.bold.cyan("Video Extension Started"));
|
|
1990
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1991
|
-
console.log(`Provider: Kling`);
|
|
1992
|
-
console.log(`Task ID: ${chalk.bold(result.id)}`);
|
|
1993
|
-
|
|
1994
|
-
if (!options.wait) {
|
|
1995
|
-
spinner.succeed(chalk.green("Extension started"));
|
|
1996
|
-
console.log();
|
|
1997
|
-
console.log(chalk.dim("Check status with:"));
|
|
1998
|
-
console.log(chalk.dim(` vibe generate video-status ${result.id} -p kling`));
|
|
1999
|
-
console.log();
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
spinner.text = "Extending video (this may take 2-5 minutes)...";
|
|
2004
|
-
|
|
2005
|
-
const finalResult = await kling.waitForExtendCompletion(
|
|
2006
|
-
result.id,
|
|
2007
|
-
(status) => {
|
|
2008
|
-
spinner.text = `Extending video... ${status.status}`;
|
|
2009
|
-
},
|
|
2010
|
-
600000
|
|
2011
|
-
);
|
|
2012
|
-
|
|
2013
|
-
if (finalResult.status !== "completed") {
|
|
2014
|
-
spinner.fail(chalk.red(finalResult.error || "Extension failed"));
|
|
2015
|
-
process.exit(1);
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
spinner.succeed(chalk.green("Video extended"));
|
|
2019
|
-
|
|
2020
|
-
if (isJsonMode()) {
|
|
2021
|
-
let outputPath: string | undefined;
|
|
2022
|
-
if (options.output && finalResult.videoUrl) {
|
|
2023
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
2024
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
2025
|
-
await writeFile(outputPath, buffer);
|
|
2026
|
-
}
|
|
2027
|
-
outputResult({ success: true, provider: "kling", taskId: result.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
console.log();
|
|
2032
|
-
if (finalResult.videoUrl) {
|
|
2033
|
-
console.log(`Video URL: ${finalResult.videoUrl}`);
|
|
2034
|
-
}
|
|
2035
|
-
if (finalResult.duration) {
|
|
2036
|
-
console.log(`Duration: ${finalResult.duration}s`);
|
|
2037
|
-
}
|
|
2038
|
-
console.log();
|
|
2039
|
-
|
|
2040
|
-
if (options.output && finalResult.videoUrl) {
|
|
2041
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
2042
|
-
try {
|
|
2043
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
2044
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
2045
|
-
await writeFile(outputPath, buffer);
|
|
2046
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
2047
|
-
} catch (err) {
|
|
2048
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
} else if (provider === "veo") {
|
|
2052
|
-
const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
|
|
2053
|
-
|
|
2054
|
-
const spinner = ora("Initializing Veo...").start();
|
|
2055
|
-
|
|
2056
|
-
const gemini = new GeminiProvider();
|
|
2057
|
-
await gemini.initialize({ apiKey });
|
|
2058
|
-
|
|
2059
|
-
const veoModelMap: Record<string, string> = {
|
|
2060
|
-
"3.0": "veo-3.0-generate-preview",
|
|
2061
|
-
"3.1": "veo-3.1-generate-preview",
|
|
2062
|
-
"3.1-fast": "veo-3.1-fast-generate-preview",
|
|
2063
|
-
};
|
|
2064
|
-
const veoModel = veoModelMap[options.veoModel] || "veo-3.1-generate-preview";
|
|
2065
|
-
|
|
2066
|
-
spinner.text = "Starting video extension...";
|
|
2067
|
-
|
|
2068
|
-
const result = await gemini.extendVideo(id, options.prompt, {
|
|
2069
|
-
duration: parseInt(options.duration) as 4 | 6 | 8,
|
|
2070
|
-
model: veoModel as "veo-3.0-generate-preview" | "veo-3.1-generate-preview" | "veo-3.1-fast-generate-preview",
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
if (result.status === "failed") {
|
|
2074
|
-
spinner.fail(chalk.red(result.error || "Failed to start extension"));
|
|
2075
|
-
process.exit(1);
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
console.log();
|
|
2079
|
-
console.log(chalk.bold.cyan("Veo Video Extension Started"));
|
|
2080
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
2081
|
-
console.log(`Provider: Veo`);
|
|
2082
|
-
console.log(`Operation: ${chalk.bold(result.id)}`);
|
|
2083
|
-
|
|
2084
|
-
if (!options.wait) {
|
|
2085
|
-
spinner.succeed(chalk.green("Extension started"));
|
|
2086
|
-
console.log();
|
|
2087
|
-
console.log(chalk.dim("Check status or wait with:"));
|
|
2088
|
-
console.log(chalk.dim(` vibe generate video-extend ${result.id} -p veo`));
|
|
2089
|
-
console.log();
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
spinner.text = "Extending video (this may take 1-3 minutes)...";
|
|
2094
|
-
const finalResult = await gemini.waitForVideoCompletion(
|
|
2095
|
-
result.id,
|
|
2096
|
-
(status) => {
|
|
2097
|
-
spinner.text = `Extending video... ${status.status}`;
|
|
2098
|
-
},
|
|
2099
|
-
300000
|
|
2100
|
-
);
|
|
2101
|
-
|
|
2102
|
-
if (finalResult.status !== "completed") {
|
|
2103
|
-
spinner.fail(chalk.red(finalResult.error || "Extension failed"));
|
|
2104
|
-
process.exit(1);
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
spinner.succeed(chalk.green("Video extended"));
|
|
2108
|
-
|
|
2109
|
-
if (isJsonMode()) {
|
|
2110
|
-
let outputPath: string | undefined;
|
|
2111
|
-
if (options.output && finalResult.videoUrl) {
|
|
2112
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
2113
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
2114
|
-
await writeFile(outputPath, buffer);
|
|
2115
|
-
}
|
|
2116
|
-
outputResult({ success: true, provider: "veo", taskId: result.id, videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath });
|
|
2117
|
-
return;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
console.log();
|
|
2121
|
-
if (finalResult.videoUrl) {
|
|
2122
|
-
console.log(`Video URL: ${finalResult.videoUrl}`);
|
|
2123
|
-
}
|
|
2124
|
-
console.log();
|
|
2125
|
-
|
|
2126
|
-
if (options.output && finalResult.videoUrl) {
|
|
2127
|
-
const downloadSpinner = ora("Downloading video...").start();
|
|
2128
|
-
try {
|
|
2129
|
-
const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
|
|
2130
|
-
const outputPath = resolve(process.cwd(), options.output);
|
|
2131
|
-
await writeFile(outputPath, buffer);
|
|
2132
|
-
downloadSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
|
|
2133
|
-
} catch (err) {
|
|
2134
|
-
downloadSpinner.fail(chalk.red(`Failed to download video: ${err instanceof Error ? err.message : err}`));
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
} else {
|
|
2138
|
-
console.error(chalk.red(`Invalid provider: ${provider}. Video extend supports: kling, veo`));
|
|
2139
|
-
process.exit(1);
|
|
2140
|
-
}
|
|
2141
|
-
} catch (error) {
|
|
2142
|
-
console.error(chalk.red("Video extension failed"));
|
|
2143
|
-
console.error(error);
|
|
2144
|
-
process.exit(1);
|
|
2145
|
-
}
|
|
2146
|
-
});
|