@vibeframe/cli 0.27.0 → 0.29.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 (109) 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/setup.js +5 -2
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/config/schema.d.ts +2 -1
  18. package/dist/config/schema.d.ts.map +1 -1
  19. package/dist/config/schema.js +2 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/index.js +0 -0
  22. package/package.json +16 -12
  23. package/.turbo/turbo-build.log +0 -4
  24. package/.turbo/turbo-lint.log +0 -21
  25. package/.turbo/turbo-test.log +0 -689
  26. package/src/agent/adapters/claude.ts +0 -143
  27. package/src/agent/adapters/gemini.ts +0 -159
  28. package/src/agent/adapters/index.ts +0 -61
  29. package/src/agent/adapters/ollama.ts +0 -231
  30. package/src/agent/adapters/openai.ts +0 -116
  31. package/src/agent/adapters/xai.ts +0 -119
  32. package/src/agent/index.ts +0 -251
  33. package/src/agent/memory/index.ts +0 -151
  34. package/src/agent/prompts/system.ts +0 -106
  35. package/src/agent/tools/ai-editing.ts +0 -845
  36. package/src/agent/tools/ai-generation.ts +0 -1073
  37. package/src/agent/tools/ai-pipeline.ts +0 -1055
  38. package/src/agent/tools/ai.ts +0 -21
  39. package/src/agent/tools/batch.ts +0 -429
  40. package/src/agent/tools/e2e.test.ts +0 -545
  41. package/src/agent/tools/export.ts +0 -184
  42. package/src/agent/tools/filesystem.ts +0 -237
  43. package/src/agent/tools/index.ts +0 -150
  44. package/src/agent/tools/integration.test.ts +0 -775
  45. package/src/agent/tools/media.ts +0 -697
  46. package/src/agent/tools/project.ts +0 -313
  47. package/src/agent/tools/timeline.ts +0 -951
  48. package/src/agent/types.ts +0 -68
  49. package/src/commands/agent.ts +0 -340
  50. package/src/commands/ai-analyze.ts +0 -429
  51. package/src/commands/ai-animated-caption.ts +0 -390
  52. package/src/commands/ai-audio.ts +0 -941
  53. package/src/commands/ai-broll.ts +0 -490
  54. package/src/commands/ai-edit-cli.ts +0 -658
  55. package/src/commands/ai-edit.ts +0 -1542
  56. package/src/commands/ai-fill-gaps.ts +0 -566
  57. package/src/commands/ai-helpers.ts +0 -65
  58. package/src/commands/ai-highlights.ts +0 -1303
  59. package/src/commands/ai-image.ts +0 -761
  60. package/src/commands/ai-motion.ts +0 -347
  61. package/src/commands/ai-narrate.ts +0 -451
  62. package/src/commands/ai-review.ts +0 -309
  63. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  64. package/src/commands/ai-script-pipeline.ts +0 -1365
  65. package/src/commands/ai-suggest-edit.ts +0 -264
  66. package/src/commands/ai-video-fx.ts +0 -445
  67. package/src/commands/ai-video.ts +0 -915
  68. package/src/commands/ai-viral.ts +0 -595
  69. package/src/commands/ai-visual-fx.ts +0 -601
  70. package/src/commands/ai.test.ts +0 -627
  71. package/src/commands/ai.ts +0 -307
  72. package/src/commands/analyze.ts +0 -282
  73. package/src/commands/audio.ts +0 -644
  74. package/src/commands/batch.test.ts +0 -279
  75. package/src/commands/batch.ts +0 -440
  76. package/src/commands/detect.ts +0 -329
  77. package/src/commands/doctor.ts +0 -237
  78. package/src/commands/edit-cmd.ts +0 -1014
  79. package/src/commands/export.ts +0 -918
  80. package/src/commands/generate.ts +0 -2146
  81. package/src/commands/media.ts +0 -177
  82. package/src/commands/output.ts +0 -142
  83. package/src/commands/pipeline.ts +0 -398
  84. package/src/commands/project.test.ts +0 -127
  85. package/src/commands/project.ts +0 -149
  86. package/src/commands/sanitize.ts +0 -60
  87. package/src/commands/schema.ts +0 -130
  88. package/src/commands/setup.ts +0 -509
  89. package/src/commands/timeline.test.ts +0 -499
  90. package/src/commands/timeline.ts +0 -529
  91. package/src/commands/validate.ts +0 -77
  92. package/src/config/config.test.ts +0 -197
  93. package/src/config/index.ts +0 -125
  94. package/src/config/schema.ts +0 -82
  95. package/src/engine/index.ts +0 -2
  96. package/src/engine/project.test.ts +0 -702
  97. package/src/engine/project.ts +0 -439
  98. package/src/index.ts +0 -146
  99. package/src/utils/api-key.test.ts +0 -41
  100. package/src/utils/api-key.ts +0 -247
  101. package/src/utils/audio.ts +0 -83
  102. package/src/utils/exec-safe.ts +0 -75
  103. package/src/utils/first-run.ts +0 -52
  104. package/src/utils/provider-resolver.ts +0 -56
  105. package/src/utils/remotion.ts +0 -951
  106. package/src/utils/subtitle.test.ts +0 -227
  107. package/src/utils/subtitle.ts +0 -169
  108. package/src/utils/tty.ts +0 -196
  109. 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
- }