@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/ai-broll.ts
DELETED
|
@@ -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
|
-
}
|