@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,490 +0,0 @@
1
- /**
2
- * @module ai-broll
3
- * @description B-Roll Matcher command. Matches B-roll footage to narration
4
- * content using Whisper transcription and Claude Vision analysis.
5
- *
6
- * ## Commands: vibe ai b-roll
7
- * ## Dependencies: Whisper, Claude
8
- *
9
- * Extracted from ai.ts as part of modularisation.
10
- * ai.ts calls registerBrollCommand(aiCommand).
11
- * @see MODELS.md for AI model configuration
12
- */
13
-
14
- import { type Command } from "commander";
15
- import { readFile, writeFile, readdir } from "node:fs/promises";
16
- import { resolve, basename, extname } from "node:path";
17
- import { existsSync } from "node:fs";
18
- import chalk from "chalk";
19
- import ora from "ora";
20
- import {
21
- WhisperProvider,
22
- ClaudeProvider,
23
- type BrollClipInfo,
24
- type BrollMatch,
25
- type BrollMatchResult,
26
- } from "@vibeframe/ai-providers";
27
- import { Project } from "../engine/index.js";
28
- import { getApiKey } from "../utils/api-key.js";
29
- import { execSafe, commandExists, ffprobeDuration } from "../utils/exec-safe.js";
30
- import { formatTime } from "./ai-helpers.js";
31
-
32
- function truncate(text: string, maxLength: number): string {
33
- if (text.length <= maxLength) return text;
34
- return text.slice(0, maxLength - 3) + "...";
35
- }
36
-
37
- /**
38
- * Check if a file path looks like an audio or video file
39
- */
40
- function isAudioOrVideoFile(path: string): boolean {
41
- const mediaExtensions = [
42
- ".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac",
43
- ".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v",
44
- ];
45
- const ext = extname(path).toLowerCase();
46
- return mediaExtensions.includes(ext);
47
- }
48
-
49
- /**
50
- * Discover B-roll video files from paths or directory
51
- */
52
- async function discoverBrollFiles(
53
- paths?: string,
54
- directory?: string
55
- ): Promise<string[]> {
56
- const files: string[] = [];
57
- const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
58
-
59
- if (paths) {
60
- const pathList = paths.split(",").map((p) => resolve(process.cwd(), p.trim()));
61
- for (const path of pathList) {
62
- if (existsSync(path)) {
63
- files.push(path);
64
- }
65
- }
66
- }
67
-
68
- if (directory) {
69
- const dir = resolve(process.cwd(), directory);
70
- if (existsSync(dir)) {
71
- const entries = await readdir(dir);
72
- for (const entry of entries) {
73
- const ext = extname(entry).toLowerCase();
74
- if (videoExtensions.includes(ext)) {
75
- files.push(resolve(dir, entry));
76
- }
77
- }
78
- }
79
- }
80
-
81
- return files;
82
- }
83
-
84
- /**
85
- * Extract a key frame from video as base64 JPEG
86
- */
87
- async function extractKeyFrame(videoPath: string, timestamp: number): Promise<string> {
88
- const tempPath = `/tmp/vibe_frame_${Date.now()}.jpg`;
89
- await execSafe("ffmpeg", ["-ss", String(timestamp), "-i", videoPath, "-frames:v", "1", "-q:v", "2", tempPath, "-y"], { maxBuffer: 10 * 1024 * 1024 });
90
- const buffer = await readFile(tempPath);
91
- const { unlink } = await import("node:fs/promises");
92
- await unlink(tempPath).catch(() => {});
93
- return buffer.toString("base64");
94
- }
95
-
96
- // ── B-Roll Matcher command ──────────────────────────────────────────────────
97
-
98
- export function registerBrollCommand(ai: Command): void {
99
- ai
100
- .command("b-roll")
101
- .description("Match B-roll footage to narration content (deprecated)")
102
- .argument("<narration>", "Narration audio file or script text")
103
- .option("-b, --broll <paths>", "B-roll video files (comma-separated)")
104
- .option("--broll-dir <dir>", "Directory containing B-roll files")
105
- .option("-o, --output <path>", "Output project file", "broll-matched.vibe.json")
106
- .option("-t, --threshold <value>", "Match confidence threshold (0-1)", "0.6")
107
- .option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)")
108
- .option("-f, --file", "Treat narration as file path (script file)")
109
- .option("--analyze-only", "Only analyze, don't create project")
110
- .action(async (narration: string, options) => {
111
- try {
112
- console.warn(chalk.yellow("Warning: 'pipeline b-roll' is deprecated. Use individual commands instead:"));
113
- console.warn(chalk.dim(" vibe analyze video <video> 'identify scenes needing b-roll' → vibe generate video '<prompt>'"));
114
- console.warn();
115
-
116
- // Validate B-roll input
117
- if (!options.broll && !options.brollDir) {
118
- console.error(chalk.red("B-roll files required. Use -b or --broll-dir"));
119
- process.exit(1);
120
- }
121
-
122
- // Check API keys
123
- const openaiApiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
124
- if (!openaiApiKey) {
125
- console.error(chalk.red("OpenAI API key required for Whisper transcription. Set OPENAI_API_KEY in .env or run: vibe setup"));
126
- console.error(chalk.dim("Set OPENAI_API_KEY environment variable"));
127
- process.exit(1);
128
- }
129
-
130
- const claudeApiKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic");
131
- if (!claudeApiKey) {
132
- console.error(chalk.red("Anthropic API key required for B-roll analysis. Set ANTHROPIC_API_KEY in .env or run: vibe setup"));
133
- console.error(chalk.dim("Set ANTHROPIC_API_KEY environment variable"));
134
- process.exit(1);
135
- }
136
-
137
- // Check FFmpeg availability
138
- if (!commandExists("ffmpeg")) {
139
- console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
140
- process.exit(1);
141
- }
142
-
143
- console.log();
144
- console.log(chalk.bold.cyan("🎬 B-Roll Matcher Pipeline"));
145
- console.log(chalk.dim("─".repeat(60)));
146
- console.log();
147
-
148
- // Step 1: Discover B-roll files
149
- const discoverSpinner = ora("🎥 Discovering B-roll files...").start();
150
- const brollFiles = await discoverBrollFiles(options.broll, options.brollDir);
151
-
152
- if (brollFiles.length === 0) {
153
- discoverSpinner.fail(chalk.red("No B-roll video files found"));
154
- process.exit(1);
155
- }
156
-
157
- discoverSpinner.succeed(chalk.green(`Found ${brollFiles.length} B-roll file(s)`));
158
-
159
- // Step 2: Parse narration (audio file or script text)
160
- const narrationSpinner = ora("📝 Processing narration...").start();
161
-
162
- let narrationSegments: Array<{ startTime: number; endTime: number; text: string }> = [];
163
- let totalDuration = 0;
164
- let narrationFile = "";
165
-
166
- const isScriptFile = options.file;
167
- const isAudioFile = !isScriptFile && isAudioOrVideoFile(narration);
168
-
169
- if (isAudioFile) {
170
- // Transcribe audio with Whisper
171
- narrationFile = resolve(process.cwd(), narration);
172
- if (!existsSync(narrationFile)) {
173
- narrationSpinner.fail(chalk.red(`Narration file not found: ${narrationFile}`));
174
- process.exit(1);
175
- }
176
-
177
- narrationSpinner.text = "📝 Transcribing narration with Whisper...";
178
-
179
- const whisper = new WhisperProvider();
180
- await whisper.initialize({ apiKey: openaiApiKey });
181
-
182
- // Extract audio if it's a video file
183
- let audioPath = narrationFile;
184
- let tempAudioPath: string | null = null;
185
-
186
- const ext = extname(narrationFile).toLowerCase();
187
- const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
188
- if (videoExtensions.includes(ext)) {
189
- narrationSpinner.text = "📝 Extracting audio from video...";
190
- tempAudioPath = `/tmp/vibe_broll_audio_${Date.now()}.wav`;
191
- await execSafe("ffmpeg", ["-i", narrationFile, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", tempAudioPath, "-y"], { maxBuffer: 50 * 1024 * 1024 });
192
- audioPath = tempAudioPath;
193
- }
194
-
195
- const audioBuffer = await readFile(audioPath);
196
- const audioBlob = new Blob([audioBuffer]);
197
-
198
- narrationSpinner.text = "📝 Transcribing with Whisper...";
199
- const transcriptResult = await whisper.transcribe(audioBlob, options.language);
200
-
201
- // Cleanup temp file
202
- if (tempAudioPath && existsSync(tempAudioPath)) {
203
- const { unlink } = await import("node:fs/promises");
204
- await unlink(tempAudioPath).catch(() => {});
205
- }
206
-
207
- if (transcriptResult.status === "failed" || !transcriptResult.segments) {
208
- narrationSpinner.fail(chalk.red(`Transcription failed: ${transcriptResult.error}`));
209
- process.exit(1);
210
- }
211
-
212
- narrationSegments = transcriptResult.segments.map((seg) => ({
213
- startTime: seg.startTime,
214
- endTime: seg.endTime,
215
- text: seg.text,
216
- }));
217
-
218
- totalDuration = transcriptResult.segments.length > 0
219
- ? transcriptResult.segments[transcriptResult.segments.length - 1].endTime
220
- : 0;
221
- } else {
222
- // Use script text (direct or from file)
223
- let scriptContent = narration;
224
- if (isScriptFile) {
225
- const scriptPath = resolve(process.cwd(), narration);
226
- if (!existsSync(scriptPath)) {
227
- narrationSpinner.fail(chalk.red(`Script file not found: ${scriptPath}`));
228
- process.exit(1);
229
- }
230
- scriptContent = await readFile(scriptPath, "utf-8");
231
- narrationFile = scriptPath;
232
- } else {
233
- narrationFile = "text-input";
234
- }
235
-
236
- // Split script into segments (by paragraph or sentence)
237
- const paragraphs = scriptContent
238
- .split(/\n\n+/)
239
- .map((p) => p.trim())
240
- .filter((p) => p.length > 0);
241
-
242
- // Estimate timing (rough: ~150 words per minute)
243
- let currentTime = 0;
244
- narrationSegments = paragraphs.map((text) => {
245
- const wordCount = text.split(/\s+/).length;
246
- const duration = Math.max((wordCount / 150) * 60, 3); // Min 3 seconds per segment
247
- const segment = {
248
- startTime: currentTime,
249
- endTime: currentTime + duration,
250
- text,
251
- };
252
- currentTime += duration;
253
- return segment;
254
- });
255
-
256
- totalDuration = currentTime;
257
- }
258
-
259
- narrationSpinner.succeed(chalk.green(`Processed ${narrationSegments.length} narration segments (${formatTime(totalDuration)} total)`));
260
-
261
- // Step 3: Analyze B-roll clips with Claude Vision
262
- const brollSpinner = ora("🎥 Analyzing B-roll content with Claude Vision...").start();
263
-
264
- const claude = new ClaudeProvider();
265
- await claude.initialize({ apiKey: claudeApiKey });
266
-
267
- const brollClips: BrollClipInfo[] = [];
268
-
269
- for (let i = 0; i < brollFiles.length; i++) {
270
- const filePath = brollFiles[i];
271
- const fileName = basename(filePath);
272
- brollSpinner.text = `🎥 Analyzing B-roll ${i + 1}/${brollFiles.length}: ${fileName}`;
273
-
274
- try {
275
- // Get video duration
276
- const duration = await ffprobeDuration(filePath);
277
-
278
- // Extract a key frame (middle of video)
279
- const frameTime = Math.min(duration / 2, 5);
280
- const frameBase64 = await extractKeyFrame(filePath, frameTime);
281
-
282
- // Analyze with Claude Vision
283
- const analysis = await claude.analyzeBrollContent(frameBase64, fileName, "image/jpeg");
284
-
285
- brollClips.push({
286
- id: `broll-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
287
- filePath,
288
- duration,
289
- description: analysis.description,
290
- tags: analysis.tags,
291
- });
292
- } catch (error) {
293
- console.log(chalk.yellow(`\n ⚠ Could not analyze ${fileName}: ${error}`));
294
- }
295
- }
296
-
297
- brollSpinner.succeed(chalk.green(`Analyzed ${brollClips.length} B-roll clips`));
298
-
299
- // Display analyzed B-roll
300
- for (const clip of brollClips) {
301
- console.log(chalk.dim(` → ${basename(clip.filePath)}: "${clip.description}"`));
302
- console.log(chalk.dim(` [${clip.tags.join(", ")}]`));
303
- }
304
- console.log();
305
-
306
- // Step 4: Analyze narration for visual requirements
307
- const visualSpinner = ora("🔍 Analyzing narration for visual needs...").start();
308
-
309
- const analyzedNarration = await claude.analyzeNarrationForVisuals(narrationSegments);
310
-
311
- visualSpinner.succeed(chalk.green("Narration analysis complete"));
312
-
313
- // Step 5: Match B-roll to narration
314
- const matchSpinner = ora("🔗 Matching B-roll to narration...").start();
315
-
316
- const matches = await claude.matchBrollToNarration(analyzedNarration, brollClips);
317
-
318
- const threshold = parseFloat(options.threshold);
319
- const filteredMatches = matches.filter((m) => m.confidence >= threshold);
320
-
321
- // Remove duplicate assignments (keep highest confidence for each segment)
322
- const uniqueMatches: BrollMatch[] = [];
323
- const matchedSegments = new Set<number>();
324
-
325
- // Sort by confidence descending
326
- filteredMatches.sort((a, b) => b.confidence - a.confidence);
327
-
328
- for (const match of filteredMatches) {
329
- if (!matchedSegments.has(match.narrationSegmentIndex)) {
330
- matchedSegments.add(match.narrationSegmentIndex);
331
- uniqueMatches.push(match);
332
- }
333
- }
334
-
335
- // Sort back by segment index
336
- uniqueMatches.sort((a, b) => a.narrationSegmentIndex - b.narrationSegmentIndex);
337
-
338
- const coverage = (uniqueMatches.length / narrationSegments.length) * 100;
339
- matchSpinner.succeed(chalk.green(`Found ${uniqueMatches.length} matches (${coverage.toFixed(0)}% coverage)`));
340
-
341
- // Find unmatched segments
342
- const unmatchedSegments: number[] = [];
343
- for (let i = 0; i < narrationSegments.length; i++) {
344
- if (!matchedSegments.has(i)) {
345
- unmatchedSegments.push(i);
346
- }
347
- }
348
-
349
- // Display match summary
350
- console.log();
351
- console.log(chalk.bold.cyan("📊 Match Summary"));
352
- console.log(chalk.dim("─".repeat(60)));
353
-
354
- for (const match of uniqueMatches) {
355
- const segment = analyzedNarration[match.narrationSegmentIndex];
356
- const clip = brollClips.find((c) => c.id === match.brollClipId);
357
- const startFormatted = formatTime(segment.startTime);
358
- const endFormatted = formatTime(segment.endTime);
359
- const confidencePercent = (match.confidence * 100).toFixed(0);
360
-
361
- console.log();
362
- console.log(` ${chalk.yellow(`Segment ${match.narrationSegmentIndex + 1}`)} [${startFormatted} - ${endFormatted}]`);
363
- console.log(` ${chalk.dim(truncate(segment.text, 60))}`);
364
- console.log(` ${chalk.green("→")} ${basename(clip?.filePath || "unknown")} ${chalk.dim(`(${confidencePercent}%)`)}`);
365
- console.log(` ${chalk.dim(match.reason)}`);
366
- }
367
-
368
- if (unmatchedSegments.length > 0) {
369
- console.log();
370
- console.log(chalk.yellow(` ⚠ ${unmatchedSegments.length} unmatched segment(s): [${unmatchedSegments.map((i) => i + 1).join(", ")}]`));
371
- }
372
-
373
- console.log();
374
- console.log(chalk.dim("─".repeat(60)));
375
- console.log(`Total: ${chalk.bold(uniqueMatches.length)}/${narrationSegments.length} segments matched, ${chalk.bold(coverage.toFixed(0))}% coverage`);
376
- console.log();
377
-
378
- // Prepare result object
379
- const result: BrollMatchResult = {
380
- narrationFile,
381
- totalDuration,
382
- brollClips,
383
- narrationSegments: analyzedNarration,
384
- matches: uniqueMatches,
385
- unmatchedSegments,
386
- };
387
-
388
- // Step 6: Create project (unless analyze-only)
389
- if (!options.analyzeOnly) {
390
- const projectSpinner = ora("📦 Creating project...").start();
391
-
392
- const project = new Project("B-Roll Matched Project");
393
-
394
- // Add B-roll sources
395
- const sourceMap = new Map<string, string>();
396
- for (const clip of brollClips) {
397
- const source = project.addSource({
398
- name: basename(clip.filePath),
399
- url: clip.filePath,
400
- type: "video",
401
- duration: clip.duration,
402
- });
403
- sourceMap.set(clip.id, source.id);
404
- }
405
-
406
- // Add narration audio source if it's an audio file
407
- let narrationSourceId: string | null = null;
408
- if (isAudioFile && narrationFile && existsSync(narrationFile)) {
409
- const narrationSource = project.addSource({
410
- name: basename(narrationFile),
411
- url: narrationFile,
412
- type: "audio",
413
- duration: totalDuration,
414
- });
415
- narrationSourceId = narrationSource.id;
416
- }
417
-
418
- // Get tracks
419
- const videoTrack = project.getTracks().find((t) => t.type === "video");
420
- const audioTrack = project.getTracks().find((t) => t.type === "audio");
421
- if (!videoTrack) {
422
- projectSpinner.fail(chalk.red("Failed to create project"));
423
- process.exit(1);
424
- }
425
-
426
- // Add narration audio clip to audio track
427
- if (narrationSourceId && audioTrack) {
428
- project.addClip({
429
- sourceId: narrationSourceId,
430
- trackId: audioTrack.id,
431
- startTime: 0,
432
- duration: totalDuration,
433
- sourceStartOffset: 0,
434
- sourceEndOffset: totalDuration,
435
- });
436
- }
437
-
438
- // Add clips for each match
439
- for (const match of uniqueMatches) {
440
- const segment = analyzedNarration[match.narrationSegmentIndex];
441
- const sourceId = sourceMap.get(match.brollClipId);
442
- const clip = brollClips.find((c) => c.id === match.brollClipId);
443
-
444
- if (!sourceId || !clip) continue;
445
-
446
- const clipDuration = Math.min(
447
- match.suggestedDuration || segment.endTime - segment.startTime,
448
- clip.duration - match.suggestedStartOffset
449
- );
450
-
451
- project.addClip({
452
- sourceId,
453
- trackId: videoTrack.id,
454
- startTime: segment.startTime,
455
- duration: clipDuration,
456
- sourceStartOffset: match.suggestedStartOffset,
457
- sourceEndOffset: match.suggestedStartOffset + clipDuration,
458
- });
459
- }
460
-
461
- const outputPath = resolve(process.cwd(), options.output);
462
- await writeFile(outputPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
463
-
464
- projectSpinner.succeed(chalk.green(`Created project: ${outputPath}`));
465
-
466
- // Save JSON result alongside project
467
- const jsonOutputPath = outputPath.replace(/\.vibe\.json$/, "-analysis.json");
468
- await writeFile(jsonOutputPath, JSON.stringify(result, null, 2), "utf-8");
469
- console.log(chalk.dim(` → Analysis saved: ${jsonOutputPath}`));
470
- }
471
-
472
- console.log();
473
- console.log(chalk.bold.green("✅ B-Roll matching complete!"));
474
- console.log();
475
- console.log(chalk.dim("Next steps:"));
476
- if (!options.analyzeOnly) {
477
- console.log(chalk.dim(` vibe project info ${options.output}`));
478
- console.log(chalk.dim(` vibe export ${options.output} -o final.mp4`));
479
- }
480
- if (unmatchedSegments.length > 0) {
481
- console.log(chalk.dim(" Consider adding more B-roll clips for unmatched segments"));
482
- }
483
- console.log();
484
- } catch (error) {
485
- console.error(chalk.red("B-Roll matching failed"));
486
- console.error(error);
487
- process.exit(1);
488
- }
489
- });
490
- }