@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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/adapters/index.d.ts +1 -0
  3. package/dist/agent/adapters/index.d.ts.map +1 -1
  4. package/dist/agent/adapters/index.js +5 -0
  5. package/dist/agent/adapters/index.js.map +1 -1
  6. package/dist/agent/adapters/openrouter.d.ts +16 -0
  7. package/dist/agent/adapters/openrouter.d.ts.map +1 -0
  8. package/dist/agent/adapters/openrouter.js +100 -0
  9. package/dist/agent/adapters/openrouter.js.map +1 -0
  10. package/dist/agent/types.d.ts +1 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +3 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/ai-edit-cli.d.ts.map +1 -1
  16. package/dist/commands/ai-edit-cli.js +18 -0
  17. package/dist/commands/ai-edit-cli.js.map +1 -1
  18. package/dist/commands/generate.js +14 -0
  19. package/dist/commands/generate.js.map +1 -1
  20. package/dist/commands/schema.d.ts +1 -0
  21. package/dist/commands/schema.d.ts.map +1 -1
  22. package/dist/commands/schema.js +122 -21
  23. package/dist/commands/schema.js.map +1 -1
  24. package/dist/commands/setup.js +5 -2
  25. package/dist/commands/setup.js.map +1 -1
  26. package/dist/config/schema.d.ts +2 -1
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +2 -0
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/index.js +0 -0
  31. package/package.json +16 -12
  32. package/.turbo/turbo-build.log +0 -4
  33. package/.turbo/turbo-lint.log +0 -21
  34. package/.turbo/turbo-test.log +0 -689
  35. package/src/agent/adapters/claude.ts +0 -143
  36. package/src/agent/adapters/gemini.ts +0 -159
  37. package/src/agent/adapters/index.ts +0 -61
  38. package/src/agent/adapters/ollama.ts +0 -231
  39. package/src/agent/adapters/openai.ts +0 -116
  40. package/src/agent/adapters/xai.ts +0 -119
  41. package/src/agent/index.ts +0 -251
  42. package/src/agent/memory/index.ts +0 -151
  43. package/src/agent/prompts/system.ts +0 -106
  44. package/src/agent/tools/ai-editing.ts +0 -845
  45. package/src/agent/tools/ai-generation.ts +0 -1073
  46. package/src/agent/tools/ai-pipeline.ts +0 -1055
  47. package/src/agent/tools/ai.ts +0 -21
  48. package/src/agent/tools/batch.ts +0 -429
  49. package/src/agent/tools/e2e.test.ts +0 -545
  50. package/src/agent/tools/export.ts +0 -184
  51. package/src/agent/tools/filesystem.ts +0 -237
  52. package/src/agent/tools/index.ts +0 -150
  53. package/src/agent/tools/integration.test.ts +0 -775
  54. package/src/agent/tools/media.ts +0 -697
  55. package/src/agent/tools/project.ts +0 -313
  56. package/src/agent/tools/timeline.ts +0 -951
  57. package/src/agent/types.ts +0 -68
  58. package/src/commands/agent.ts +0 -340
  59. package/src/commands/ai-analyze.ts +0 -429
  60. package/src/commands/ai-animated-caption.ts +0 -390
  61. package/src/commands/ai-audio.ts +0 -941
  62. package/src/commands/ai-broll.ts +0 -490
  63. package/src/commands/ai-edit-cli.ts +0 -658
  64. package/src/commands/ai-edit.ts +0 -1542
  65. package/src/commands/ai-fill-gaps.ts +0 -566
  66. package/src/commands/ai-helpers.ts +0 -65
  67. package/src/commands/ai-highlights.ts +0 -1303
  68. package/src/commands/ai-image.ts +0 -761
  69. package/src/commands/ai-motion.ts +0 -347
  70. package/src/commands/ai-narrate.ts +0 -451
  71. package/src/commands/ai-review.ts +0 -309
  72. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  73. package/src/commands/ai-script-pipeline.ts +0 -1365
  74. package/src/commands/ai-suggest-edit.ts +0 -264
  75. package/src/commands/ai-video-fx.ts +0 -445
  76. package/src/commands/ai-video.ts +0 -915
  77. package/src/commands/ai-viral.ts +0 -595
  78. package/src/commands/ai-visual-fx.ts +0 -601
  79. package/src/commands/ai.test.ts +0 -627
  80. package/src/commands/ai.ts +0 -307
  81. package/src/commands/analyze.ts +0 -282
  82. package/src/commands/audio.ts +0 -644
  83. package/src/commands/batch.test.ts +0 -279
  84. package/src/commands/batch.ts +0 -440
  85. package/src/commands/detect.ts +0 -329
  86. package/src/commands/doctor.ts +0 -237
  87. package/src/commands/edit-cmd.ts +0 -1014
  88. package/src/commands/export.ts +0 -918
  89. package/src/commands/generate.ts +0 -2146
  90. package/src/commands/media.ts +0 -177
  91. package/src/commands/output.ts +0 -142
  92. package/src/commands/pipeline.ts +0 -398
  93. package/src/commands/project.test.ts +0 -127
  94. package/src/commands/project.ts +0 -149
  95. package/src/commands/sanitize.ts +0 -60
  96. package/src/commands/schema.ts +0 -130
  97. package/src/commands/setup.ts +0 -509
  98. package/src/commands/timeline.test.ts +0 -499
  99. package/src/commands/timeline.ts +0 -529
  100. package/src/commands/validate.ts +0 -77
  101. package/src/config/config.test.ts +0 -197
  102. package/src/config/index.ts +0 -125
  103. package/src/config/schema.ts +0 -82
  104. package/src/engine/index.ts +0 -2
  105. package/src/engine/project.test.ts +0 -702
  106. package/src/engine/project.ts +0 -439
  107. package/src/index.ts +0 -146
  108. package/src/utils/api-key.test.ts +0 -41
  109. package/src/utils/api-key.ts +0 -247
  110. package/src/utils/audio.ts +0 -83
  111. package/src/utils/exec-safe.ts +0 -75
  112. package/src/utils/first-run.ts +0 -52
  113. package/src/utils/provider-resolver.ts +0 -56
  114. package/src/utils/remotion.ts +0 -951
  115. package/src/utils/subtitle.test.ts +0 -227
  116. package/src/utils/subtitle.ts +0 -169
  117. package/src/utils/tty.ts +0 -196
  118. package/tsconfig.json +0 -20
@@ -1,761 +0,0 @@
1
- /**
2
- * @module ai-image
3
- * @description Image generation and editing commands for the VibeFrame CLI.
4
- *
5
- * ## Commands: vibe ai image, vibe ai thumbnail, vibe ai background,
6
- * vibe ai gemini, vibe ai gemini-edit
7
- * ## Dependencies: OpenAI, Gemini, FFmpeg
8
- *
9
- * Extracted from ai.ts as part of modularisation.
10
- * ai.ts calls registerImageCommands(aiCommand).
11
- * @see MODELS.md for AI model configuration
12
- */
13
-
14
- import { type Command } from 'commander';
15
- import { resolve, dirname, basename, extname } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
18
- import { existsSync } from 'node:fs';
19
- import chalk from 'chalk';
20
- import ora from 'ora';
21
- import {
22
- GeminiProvider,
23
- OpenAIImageProvider,
24
- } from '@vibeframe/ai-providers';
25
- import { getApiKey } from '../utils/api-key.js';
26
- import { execSafe, commandExists } from '../utils/exec-safe.js';
27
-
28
- function _registerImageCommands(aiCommand: Command): void {
29
-
30
- aiCommand
31
- .command("image")
32
- .description("Generate image using AI (Gemini or DALL-E)")
33
- .argument("<prompt>", "Image description prompt")
34
- .option("-p, --provider <provider>", "Provider: gemini, openai, runway (dalle is deprecated)", "gemini")
35
- .option("-k, --api-key <key>", "API key (or set env: OPENAI_API_KEY, GOOGLE_API_KEY)")
36
- .option("-o, --output <path>", "Output file path (downloads image)")
37
- .option("-s, --size <size>", "Image size (openai: 1024x1024, 1536x1024, 1024x1536)", "1024x1024")
38
- .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")
39
- .option("-q, --quality <quality>", "Quality: standard, hd (openai only)", "standard")
40
- .option("--style <style>", "Style: vivid, natural (openai only)", "vivid")
41
- .option("-n, --count <n>", "Number of images to generate", "1")
42
- .option("-m, --model <model>", "Gemini model: flash, 3.1-flash, latest (Nano Banana 2), pro (4K)")
43
- .action(async (prompt: string, options) => {
44
- try {
45
- const provider = options.provider.toLowerCase();
46
- const validProviders = ["openai", "dalle", "gemini", "runway"];
47
- if (!validProviders.includes(provider)) {
48
- console.error(chalk.red(`Invalid provider: ${provider}`));
49
- console.error(chalk.dim(`Available providers: openai, gemini, runway`));
50
- process.exit(1);
51
- }
52
-
53
- // Show deprecation warning for "dalle"
54
- if (provider === "dalle") {
55
- console.log(chalk.yellow('Warning: "dalle" is deprecated. Use "openai" instead.'));
56
- }
57
-
58
- // Get API key based on provider
59
- const envKeyMap: Record<string, string> = {
60
- openai: "OPENAI_API_KEY",
61
- dalle: "OPENAI_API_KEY", // backward compatibility
62
- gemini: "GOOGLE_API_KEY",
63
- runway: "RUNWAY_API_SECRET",
64
- };
65
- const providerNameMap: Record<string, string> = {
66
- openai: "OpenAI",
67
- dalle: "OpenAI", // backward compatibility
68
- gemini: "Google",
69
- runway: "Runway",
70
- };
71
- const envKey = envKeyMap[provider];
72
- const providerName = providerNameMap[provider];
73
-
74
- const apiKey = await getApiKey(envKey, providerName, options.apiKey);
75
- if (!apiKey) {
76
- console.error(chalk.red(`${providerName} API key required. Set ${envKey} in .env or run: vibe setup`));
77
- console.error(chalk.dim(`Use --api-key or set ${envKey} environment variable`));
78
- process.exit(1);
79
- }
80
-
81
- const spinner = ora(`Generating image with ${providerName}...`).start();
82
-
83
- if (provider === "dalle" || provider === "openai") {
84
- const openaiImage = new OpenAIImageProvider();
85
- await openaiImage.initialize({ apiKey });
86
-
87
- const result = await openaiImage.generateImage(prompt, {
88
- size: options.size,
89
- quality: options.quality,
90
- style: options.style,
91
- n: parseInt(options.count),
92
- });
93
-
94
- if (!result.success || !result.images) {
95
- spinner.fail(chalk.red(result.error || "Image generation failed"));
96
- process.exit(1);
97
- }
98
-
99
- spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with OpenAI GPT Image 1.5`));
100
-
101
- console.log();
102
- console.log(chalk.bold.cyan("Generated Images"));
103
- console.log(chalk.dim("─".repeat(60)));
104
-
105
- for (let i = 0; i < result.images.length; i++) {
106
- const img = result.images[i];
107
- console.log();
108
- if (img.url) {
109
- console.log(`${chalk.yellow(`[${i + 1}]`)} ${img.url}`);
110
- } else if (img.base64) {
111
- console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image data)`);
112
- }
113
- if (img.revisedPrompt) {
114
- console.log(chalk.dim(` Revised: ${img.revisedPrompt.slice(0, 100)}...`));
115
- }
116
- }
117
- console.log();
118
-
119
- // Save if output specified
120
- if (options.output && result.images.length > 0) {
121
- const img = result.images[0];
122
- const saveSpinner = ora("Saving image...").start();
123
- try {
124
- let buffer: Buffer;
125
- if (img.url) {
126
- // Download from URL
127
- const response = await fetch(img.url);
128
- buffer = Buffer.from(await response.arrayBuffer());
129
- } else if (img.base64) {
130
- // Decode base64
131
- buffer = Buffer.from(img.base64, "base64");
132
- } else {
133
- throw new Error("No image data available");
134
- }
135
- const outputPath = resolve(process.cwd(), options.output);
136
- await mkdir(dirname(outputPath), { recursive: true });
137
- await writeFile(outputPath, buffer);
138
- saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
139
- } catch (err) {
140
- saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
141
- }
142
- }
143
- } else if (provider === "gemini") {
144
- // Validate model name
145
- const validGeminiModels = ["flash", "3.1-flash", "latest", "pro"];
146
- if (options.model && !validGeminiModels.includes(options.model)) {
147
- console.warn(chalk.yellow(`Unknown model "${options.model}", using flash. Valid: ${validGeminiModels.join(", ")}`));
148
- options.model = "flash";
149
- }
150
-
151
- // Validate aspect ratio
152
- 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"];
153
- if (options.ratio && !validRatios.includes(options.ratio)) {
154
- console.error(chalk.red(`Invalid ratio "${options.ratio}". Valid: ${validRatios.join(", ")}`));
155
- process.exit(1);
156
- }
157
-
158
- const gemini = new GeminiProvider();
159
- await gemini.initialize({ apiKey });
160
-
161
- const geminiModelNames: Record<string, string> = {
162
- flash: "Nano Banana",
163
- "3.1-flash": "Nano Banana 2",
164
- latest: "Nano Banana 2",
165
- pro: "Nano Banana Pro",
166
- };
167
- const modelLabel = geminiModelNames[options.model] || "Nano Banana";
168
-
169
- let result = await gemini.generateImage(prompt, {
170
- model: options.model,
171
- 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",
172
- });
173
-
174
- // Auto-fallback: if latest/3.1-flash fails, retry with flash
175
- let usedLabel = modelLabel;
176
- const fallbackModels = ["latest", "3.1-flash"];
177
- if (!result.success && options.model && fallbackModels.includes(options.model)) {
178
- spinner.text = `${chalk.dim(result.error || "Failed")} — retrying with Nano Banana (flash)...`;
179
- result = await gemini.generateImage(prompt, {
180
- model: "flash",
181
- 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",
182
- });
183
- usedLabel = "Nano Banana (fallback)";
184
- }
185
-
186
- if (!result.success || !result.images) {
187
- spinner.fail(chalk.red(result.error || "Image generation failed"));
188
- process.exit(1);
189
- }
190
-
191
- spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with Gemini (${usedLabel})`));
192
-
193
- console.log();
194
- console.log(chalk.bold.cyan("Generated Images"));
195
- console.log(chalk.dim("─".repeat(60)));
196
-
197
- // Gemini returns base64, we need to save or display
198
- for (let i = 0; i < result.images.length; i++) {
199
- const img = result.images[i];
200
- console.log();
201
- console.log(`${chalk.yellow(`[${i + 1}]`)} (base64 image, ${img.mimeType})`);
202
- }
203
- console.log();
204
-
205
- // Save if output specified
206
- if (options.output && result.images.length > 0) {
207
- const saveSpinner = ora("Saving image...").start();
208
- try {
209
- const img = result.images[0];
210
- const buffer = Buffer.from(img.base64, "base64");
211
- const outputPath = resolve(process.cwd(), options.output);
212
- await mkdir(dirname(outputPath), { recursive: true });
213
- await writeFile(outputPath, buffer);
214
- saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
215
- } catch (err) {
216
- saveSpinner.fail(chalk.red(`Failed to save image: ${err instanceof Error ? err.message : err}`));
217
- }
218
- } else {
219
- console.log(chalk.yellow("Use -o to save the generated image to a file"));
220
- }
221
- } else if (provider === "runway") {
222
- // Use Runway's Gemini model for text-to-image (no reference needed)
223
- const { spawn } = await import("child_process");
224
- const __filename = fileURLToPath(import.meta.url);
225
- const __dirname = dirname(__filename);
226
- const scriptPath = resolve(__dirname, "../../../../.claude/skills/runway-video/scripts/image.py");
227
-
228
- if (!options.output) {
229
- spinner.fail(chalk.red("Output path required for Runway. Use -o option."));
230
- process.exit(1);
231
- }
232
-
233
- const outputPath = resolve(process.cwd(), options.output);
234
- const args = [scriptPath, prompt, "-o", outputPath, "-r", options.ratio || "16:9"];
235
-
236
- spinner.text = "Generating image with Runway (gemini_2.5_flash)...";
237
-
238
- await new Promise<void>((resolvePromise, reject) => {
239
- const proc = spawn("python3", args, {
240
- env: { ...process.env, RUNWAY_API_SECRET: apiKey },
241
- stdio: ["ignore", "pipe", "pipe"],
242
- });
243
-
244
- let stdout = "";
245
- let stderr = "";
246
-
247
- proc.stdout.on("data", (data) => {
248
- stdout += data.toString();
249
- });
250
-
251
- proc.stderr.on("data", (data) => {
252
- stderr += data.toString();
253
- });
254
-
255
- proc.on("close", (code) => {
256
- if (code === 0) {
257
- spinner.succeed(chalk.green("Generated image with Runway"));
258
- console.log(chalk.dim(stdout.trim()));
259
- resolvePromise();
260
- } else {
261
- spinner.fail(chalk.red("Runway image generation failed"));
262
- console.error(chalk.red(stderr || stdout));
263
- reject(new Error("Runway generation failed"));
264
- }
265
- });
266
-
267
- proc.on("error", (err) => {
268
- spinner.fail(chalk.red("Failed to run Runway script"));
269
- reject(err);
270
- });
271
- });
272
- }
273
- } catch (error) {
274
- console.error(chalk.red("Image generation failed"));
275
- console.error(error);
276
- process.exit(1);
277
- }
278
- });
279
-
280
- aiCommand
281
- .command("thumbnail")
282
- .description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)")
283
- .argument("[description]", "Thumbnail description (for DALL-E generation)")
284
- .option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)")
285
- .option("-o, --output <path>", "Output file path")
286
- .option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter")
287
- .option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI")
288
- .option("--prompt <prompt>", "Custom prompt for best-frame analysis")
289
- .option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash")
290
- .action(async (description: string | undefined, options) => {
291
- try {
292
- // Best-frame mode: analyze video with Gemini and extract frame
293
- if (options.bestFrame) {
294
- const absVideoPath = resolve(process.cwd(), options.bestFrame);
295
- if (!existsSync(absVideoPath)) {
296
- console.error(chalk.red(`Video not found: ${absVideoPath}`));
297
- process.exit(1);
298
- }
299
-
300
- if (!commandExists("ffmpeg")) {
301
- console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
302
- process.exit(1);
303
- }
304
-
305
- const apiKey = await getApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
306
- if (!apiKey) {
307
- console.error(chalk.red("Google API key required for Gemini video analysis. Set GOOGLE_API_KEY in .env or run: vibe setup"));
308
- console.error(chalk.dim("Use --api-key or set GOOGLE_API_KEY"));
309
- process.exit(1);
310
- }
311
-
312
- const name = basename(options.bestFrame, extname(options.bestFrame));
313
- const outputPath = options.output || `${name}-thumbnail.png`;
314
-
315
- const spinner = ora("Analyzing video for best frame...").start();
316
-
317
- const result = await executeThumbnailBestFrame({
318
- videoPath: absVideoPath,
319
- outputPath: resolve(process.cwd(), outputPath),
320
- prompt: options.prompt,
321
- model: options.model,
322
- apiKey,
323
- });
324
-
325
- if (!result.success) {
326
- spinner.fail(chalk.red(result.error || "Best frame extraction failed"));
327
- process.exit(1);
328
- }
329
-
330
- spinner.succeed(chalk.green("Best frame extracted"));
331
-
332
- console.log();
333
- console.log(chalk.bold.cyan("Best Frame Result"));
334
- console.log(chalk.dim("─".repeat(60)));
335
- console.log(`Timestamp: ${chalk.bold(result.timestamp!.toFixed(2))}s`);
336
- if (result.reason) console.log(`Reason: ${chalk.dim(result.reason)}`);
337
- console.log(`Output: ${chalk.green(result.outputPath!)}`);
338
- console.log();
339
- return;
340
- }
341
-
342
- // Generation mode: create thumbnail with DALL-E
343
- if (!description) {
344
- console.error(chalk.red("Description required for thumbnail generation."));
345
- console.error(chalk.dim("Usage: vibe ai thumbnail <description> or vibe ai thumbnail --best-frame <video>"));
346
- process.exit(1);
347
- }
348
-
349
- const apiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
350
- if (!apiKey) {
351
- console.error(chalk.red("OpenAI API key required. Set OPENAI_API_KEY in .env or run: vibe setup"));
352
- process.exit(1);
353
- }
354
-
355
- const spinner = ora("Generating thumbnail...").start();
356
-
357
- const openaiImage = new OpenAIImageProvider();
358
- await openaiImage.initialize({ apiKey });
359
-
360
- const result = await openaiImage.generateThumbnail(description, options.style);
361
-
362
- if (!result.success || !result.images) {
363
- spinner.fail(chalk.red(result.error || "Thumbnail generation failed"));
364
- process.exit(1);
365
- }
366
-
367
- spinner.succeed(chalk.green("Thumbnail generated"));
368
-
369
- const img = result.images[0];
370
- console.log();
371
- console.log(chalk.bold.cyan("Generated Thumbnail"));
372
- console.log(chalk.dim("─".repeat(60)));
373
- console.log(`URL: ${img.url}`);
374
- if (img.revisedPrompt) {
375
- console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
376
- }
377
- console.log();
378
-
379
- // Save if output specified
380
- if (options.output) {
381
- const saveSpinner = ora("Saving thumbnail...").start();
382
- try {
383
- let buffer: Buffer;
384
- if (img.url) {
385
- const response = await fetch(img.url);
386
- buffer = Buffer.from(await response.arrayBuffer());
387
- } else if (img.base64) {
388
- buffer = Buffer.from(img.base64, "base64");
389
- } else {
390
- throw new Error("No image data available");
391
- }
392
- const outputPath = resolve(process.cwd(), options.output);
393
- await mkdir(dirname(outputPath), { recursive: true });
394
- await writeFile(outputPath, buffer);
395
- saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
396
- } catch (err) {
397
- saveSpinner.fail(chalk.red("Failed to save thumbnail"));
398
- }
399
- }
400
- } catch (error) {
401
- console.error(chalk.red("Thumbnail generation failed"));
402
- console.error(error);
403
- process.exit(1);
404
- }
405
- });
406
-
407
- aiCommand
408
- .command("background")
409
- .description("Generate video background using DALL-E")
410
- .argument("<description>", "Background description")
411
- .option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)")
412
- .option("-o, --output <path>", "Output file path (downloads image)")
413
- .option("-a, --aspect <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9")
414
- .action(async (description: string, options) => {
415
- try {
416
- const apiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
417
- if (!apiKey) {
418
- console.error(chalk.red("OpenAI API key required. Set OPENAI_API_KEY in .env or run: vibe setup"));
419
- process.exit(1);
420
- }
421
-
422
- const spinner = ora("Generating background...").start();
423
-
424
- const openaiImage = new OpenAIImageProvider();
425
- await openaiImage.initialize({ apiKey });
426
-
427
- const result = await openaiImage.generateBackground(description, options.aspect);
428
-
429
- if (!result.success || !result.images) {
430
- spinner.fail(chalk.red(result.error || "Background generation failed"));
431
- process.exit(1);
432
- }
433
-
434
- spinner.succeed(chalk.green("Background generated"));
435
-
436
- const img = result.images[0];
437
- console.log();
438
- console.log(chalk.bold.cyan("Generated Background"));
439
- console.log(chalk.dim("─".repeat(60)));
440
- console.log(`Image: ${img.url || "(base64 data)"}`);
441
- if (img.revisedPrompt) {
442
- console.log(chalk.dim(`Prompt: ${img.revisedPrompt.slice(0, 100)}...`));
443
- }
444
- console.log();
445
-
446
- // Save if output specified
447
- if (options.output) {
448
- const saveSpinner = ora("Saving background...").start();
449
- try {
450
- let buffer: Buffer;
451
- if (img.url) {
452
- const response = await fetch(img.url);
453
- buffer = Buffer.from(await response.arrayBuffer());
454
- } else if (img.base64) {
455
- buffer = Buffer.from(img.base64, "base64");
456
- } else {
457
- throw new Error("No image data available");
458
- }
459
- const outputPath = resolve(process.cwd(), options.output);
460
- await mkdir(dirname(outputPath), { recursive: true });
461
- await writeFile(outputPath, buffer);
462
- saveSpinner.succeed(chalk.green(`Saved to: ${outputPath}`));
463
- } catch (err) {
464
- saveSpinner.fail(chalk.red("Failed to save background"));
465
- }
466
- }
467
- } catch (error) {
468
- console.error(chalk.red("Background generation failed"));
469
- console.error(error);
470
- process.exit(1);
471
- }
472
- });
473
- // Gemini (Nano Banana) commands
474
- aiCommand
475
- .command("gemini")
476
- .description("Generate image using Gemini (Nano Banana)")
477
- .argument("<prompt>", "Text prompt describing the image")
478
- .option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)")
479
- .option("-o, --output <path>", "Output file path", "output.png")
480
- .option("-m, --model <model>", "Model: flash (fast), 3.1-flash / latest (Nano Banana 2), pro (professional, 4K)", "flash")
481
- .option("-r, --ratio <ratio>", "Aspect ratio: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 4:3, 3:4, 21:9, etc.", "1:1")
482
- .option("-s, --size <resolution>", "Resolution: 512px, 1K, 2K, 4K")
483
- .option("--grounding", "Enable Google Search grounding (Pro only)")
484
- .option("--thinking <level>", "Enable thinking mode: minimal or high")
485
- .option("--image-search", "Enable Image Search grounding (3.1 Flash only)")
486
- .action(async (prompt: string, options) => {
487
- try {
488
- const apiKey = await getApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
489
- if (!apiKey) {
490
- console.error(chalk.red("Google API key required. Set GOOGLE_API_KEY in .env or run: vibe setup"));
491
- console.error(chalk.dim("Use --api-key or set GOOGLE_API_KEY environment variable"));
492
- process.exit(1);
493
- }
494
-
495
- const modelNames: Record<string, string> = {
496
- flash: "gemini-2.5-flash-image",
497
- "3.1-flash": "gemini-3.1-flash-image-preview",
498
- latest: "gemini-3.1-flash-image-preview",
499
- pro: "gemini-3-pro-image-preview",
500
- };
501
- const modelName = modelNames[options.model] || modelNames.flash;
502
- const spinner = ora(`Generating image with ${modelName}...`).start();
503
-
504
- const gemini = new GeminiProvider();
505
- await gemini.initialize({ apiKey });
506
-
507
- let result = await gemini.generateImage(prompt, {
508
- model: options.model,
509
- aspectRatio: options.ratio,
510
- resolution: options.size,
511
- grounding: options.grounding,
512
- thinkingConfig: options.thinking ? { thinkingLevel: options.thinking } : undefined,
513
- imageSearchGrounding: options.imageSearch,
514
- });
515
-
516
- // Auto-fallback: if latest/3.1-flash fails, retry with flash
517
- const fallbackModels = ["latest", "3.1-flash"];
518
- if (!result.success && fallbackModels.includes(options.model)) {
519
- spinner.text = `${chalk.dim(result.error || `${modelName} failed`)} — retrying with flash...`;
520
- result = await gemini.generateImage(prompt, {
521
- model: "flash",
522
- aspectRatio: options.ratio,
523
- resolution: options.size,
524
- });
525
- }
526
-
527
- if (!result.success || !result.images || result.images.length === 0) {
528
- spinner.fail(chalk.red(result.error || "Image generation failed"));
529
- process.exit(1);
530
- }
531
-
532
- spinner.succeed(chalk.green("Image generated"));
533
-
534
- if (result.model) {
535
- console.log(chalk.dim(`Model: ${result.model}`));
536
- }
537
-
538
- const img = result.images[0];
539
- if (img.base64) {
540
- const outputPath = resolve(process.cwd(), options.output);
541
- await mkdir(dirname(outputPath), { recursive: true });
542
- const buffer = Buffer.from(img.base64, "base64");
543
- await writeFile(outputPath, buffer);
544
- console.log(chalk.green(`Saved to: ${outputPath}`));
545
- }
546
- } catch (error) {
547
- console.error(chalk.red("Image generation failed"));
548
- console.error(error);
549
- process.exit(1);
550
- }
551
- });
552
-
553
- aiCommand
554
- .command("gemini-edit")
555
- .description("Edit image(s) using Gemini (Nano Banana)")
556
- .argument("<images...>", "Input image file(s) followed by edit prompt")
557
- .option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)")
558
- .option("-o, --output <path>", "Output file path", "edited.png")
559
- .option("-m, --model <model>", "Model: flash (max 3 images), 3.1-flash / latest (max 3 images), pro (max 14 images)", "flash")
560
- .option("-r, --ratio <ratio>", "Output aspect ratio")
561
- .option("-s, --size <resolution>", "Resolution: 1K, 2K, 4K (Pro model only)")
562
- .action(async (args: string[], options) => {
563
- try {
564
- // Last argument is the prompt, rest are image paths
565
- if (args.length < 2) {
566
- console.error(chalk.red("Need at least one image and a prompt"));
567
- process.exit(1);
568
- }
569
-
570
- const prompt = args[args.length - 1];
571
- const imagePaths = args.slice(0, -1);
572
-
573
- const apiKey = await getApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
574
- if (!apiKey) {
575
- console.error(chalk.red("Google API key required. Set GOOGLE_API_KEY in .env or run: vibe setup"));
576
- process.exit(1);
577
- }
578
-
579
- const spinner = ora(`Reading ${imagePaths.length} image(s)...`).start();
580
-
581
- // Load all images
582
- const imageBuffers: Buffer[] = [];
583
- for (const imagePath of imagePaths) {
584
- const absPath = resolve(process.cwd(), imagePath);
585
- const buffer = await readFile(absPath);
586
- imageBuffers.push(buffer);
587
- }
588
-
589
- const editModelNames: Record<string, string> = {
590
- flash: "gemini-2.5-flash-image",
591
- "3.1-flash": "gemini-3.1-flash-image-preview",
592
- latest: "gemini-3.1-flash-image-preview",
593
- pro: "gemini-3-pro-image-preview",
594
- };
595
- const editModelName = editModelNames[options.model] || editModelNames.flash;
596
- spinner.text = `Editing with ${editModelName}...`;
597
-
598
- const gemini = new GeminiProvider();
599
- await gemini.initialize({ apiKey });
600
-
601
- let result = await gemini.editImage(imageBuffers, prompt, {
602
- model: options.model,
603
- aspectRatio: options.ratio,
604
- resolution: options.size,
605
- });
606
-
607
- // Auto-fallback: if latest/3.1-flash fails, retry with flash
608
- const fallbackModels = ["latest", "3.1-flash"];
609
- if (!result.success && fallbackModels.includes(options.model)) {
610
- spinner.text = `${chalk.dim(result.error || `${editModelName} failed`)} — retrying with flash...`;
611
- result = await gemini.editImage(imageBuffers, prompt, {
612
- model: "flash",
613
- aspectRatio: options.ratio,
614
- resolution: options.size,
615
- });
616
- }
617
-
618
- if (!result.success || !result.images || result.images.length === 0) {
619
- spinner.fail(chalk.red(result.error || "Image editing failed"));
620
- process.exit(1);
621
- }
622
-
623
- spinner.succeed(chalk.green("Image edited"));
624
-
625
- if (result.model) {
626
- console.log(chalk.dim(`Model: ${result.model}`));
627
- }
628
-
629
- const img = result.images[0];
630
- if (img.base64) {
631
- const outputPath = resolve(process.cwd(), options.output);
632
- await mkdir(dirname(outputPath), { recursive: true });
633
- const buffer = Buffer.from(img.base64, "base64");
634
- await writeFile(outputPath, buffer);
635
- console.log(chalk.green(`Saved to: ${outputPath}`));
636
- }
637
- } catch (error) {
638
- console.error(chalk.red("Image editing failed"));
639
- console.error(error);
640
- process.exit(1);
641
- }
642
- });
643
-
644
- }
645
-
646
- // ── Exported execute functions ─────────────────────────────────────────────
647
-
648
-
649
- // ============================================================================
650
- // Thumbnail Best Frame
651
- // ============================================================================
652
-
653
- export interface ThumbnailBestFrameOptions {
654
- videoPath: string;
655
- outputPath: string;
656
- prompt?: string;
657
- model?: string;
658
- apiKey?: string;
659
- }
660
-
661
- export interface ThumbnailBestFrameResult {
662
- success: boolean;
663
- outputPath?: string;
664
- timestamp?: number;
665
- reason?: string;
666
- error?: string;
667
- }
668
-
669
- export async function executeThumbnailBestFrame(options: ThumbnailBestFrameOptions): Promise<ThumbnailBestFrameResult> {
670
- const {
671
- videoPath,
672
- outputPath,
673
- prompt,
674
- model = "flash",
675
- apiKey,
676
- } = options;
677
-
678
- if (!existsSync(videoPath)) {
679
- return { success: false, error: `Video not found: ${videoPath}` };
680
- }
681
-
682
- if (!commandExists("ffmpeg")) {
683
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
684
- }
685
-
686
- const googleKey = apiKey || process.env.GOOGLE_API_KEY;
687
- if (!googleKey) {
688
- return { success: false, error: "Google API key required for Gemini video analysis. Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
689
- }
690
-
691
- try {
692
- const gemini = new GeminiProvider();
693
- await gemini.initialize({ apiKey: googleKey });
694
-
695
- const videoData = await readFile(videoPath);
696
-
697
- const analysisPrompt = prompt ||
698
- "Analyze this video and find the single best frame for a thumbnail. " +
699
- "Look for frames that are visually striking, well-composed, emotionally engaging, " +
700
- "and representative of the video content. Avoid blurry frames, transitions, or dark scenes. " +
701
- "Return ONLY a JSON object: {\"timestamp\": <seconds as number>, \"reason\": \"<brief explanation>\"}";
702
-
703
- const modelMap: Record<string, string> = {
704
- flash: "gemini-3-flash-preview",
705
- latest: "gemini-2.5-flash",
706
- "flash-2.5": "gemini-2.5-flash", // backward compat
707
- pro: "gemini-2.5-pro",
708
- };
709
- const modelId = modelMap[model] || "gemini-3-flash-preview";
710
-
711
- const result = await gemini.analyzeVideo(videoData, analysisPrompt, {
712
- model: modelId as "gemini-3-flash-preview" | "gemini-2.5-flash" | "gemini-2.5-pro",
713
- fps: 1,
714
- });
715
-
716
- if (!result.success || !result.response) {
717
- return { success: false, error: result.error || "Gemini analysis failed" };
718
- }
719
-
720
- // Parse timestamp from response
721
- const jsonMatch = result.response.match(/\{[\s\S]*?"timestamp"\s*:\s*([\d.]+)[\s\S]*?\}/);
722
- if (!jsonMatch) {
723
- return { success: false, error: `Could not parse timestamp from Gemini response: ${result.response.slice(0, 200)}` };
724
- }
725
-
726
- const timestamp = parseFloat(jsonMatch[1]);
727
- let reason: string | undefined;
728
- const reasonMatch = result.response.match(/"reason"\s*:\s*"([^"]+)"/);
729
- if (reasonMatch) {
730
- reason = reasonMatch[1];
731
- }
732
-
733
- // Extract frame with FFmpeg
734
- await execSafe("ffmpeg", ["-ss", String(timestamp), "-i", videoPath, "-frames:v", "1", "-q:v", "2", outputPath, "-y"], { timeout: 60000, maxBuffer: 50 * 1024 * 1024 });
735
-
736
- if (!existsSync(outputPath)) {
737
- return { success: false, error: "FFmpeg failed to extract frame" };
738
- }
739
-
740
- return {
741
- success: true,
742
- outputPath,
743
- timestamp,
744
- reason,
745
- };
746
- } catch (error) {
747
- return {
748
- success: false,
749
- error: `Best frame extraction failed: ${error instanceof Error ? error.message : String(error)}`,
750
- };
751
- }
752
- }
753
-
754
-
755
- /**
756
- * Register all image sub-commands on the given parent command.
757
- * Called from ai.ts: registerImageCommands(aiCommand)
758
- */
759
- export function registerImageCommands(aiCommand: Command): void {
760
- _registerImageCommands(aiCommand);
761
- }