@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.
- 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/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/pipeline.ts
DELETED
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module pipeline
|
|
3
|
-
*
|
|
4
|
-
* Top-level `vibe pipeline` command group for AI video pipelines.
|
|
5
|
-
*
|
|
6
|
-
* Commands:
|
|
7
|
-
* pipeline script-to-video - Full script-to-video pipeline
|
|
8
|
-
* pipeline regenerate-scene - Regenerate specific scene(s)
|
|
9
|
-
* pipeline highlights - Extract highlights from long-form content
|
|
10
|
-
* pipeline auto-shorts - Generate short-form clips from long-form video
|
|
11
|
-
* pipeline viral - Viral optimizer for multi-platform export
|
|
12
|
-
* pipeline b-roll - B-roll matching using Whisper + Claude Vision
|
|
13
|
-
* pipeline narrate - Auto-narration (Gemini + Claude/OpenAI + ElevenLabs)
|
|
14
|
-
*
|
|
15
|
-
* @dependencies Whisper, Claude, Gemini, ElevenLabs, Kling, Runway, FFmpeg
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { Command } from "commander";
|
|
19
|
-
import { resolve, dirname, basename } from "node:path";
|
|
20
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
-
import chalk from "chalk";
|
|
23
|
-
import ora from "ora";
|
|
24
|
-
import { Project, type ProjectFile } from "../engine/index.js";
|
|
25
|
-
import { ffprobeDuration } from "../utils/exec-safe.js";
|
|
26
|
-
import { getAudioDuration } from "../utils/audio.js";
|
|
27
|
-
import { formatTime } from "./ai-helpers.js";
|
|
28
|
-
import { autoNarrate } from "./ai-narrate.js";
|
|
29
|
-
import { registerScriptPipelineCommands } from "./ai-script-pipeline-cli.js";
|
|
30
|
-
import { registerHighlightsCommands } from "./ai-highlights.js";
|
|
31
|
-
import { registerViralCommand } from "./ai-viral.js";
|
|
32
|
-
import { registerBrollCommand } from "./ai-broll.js";
|
|
33
|
-
import { executeAnimatedCaption, type AnimatedCaptionStyle } from "./ai-animated-caption.js";
|
|
34
|
-
import { isJsonMode, outputResult } from "./output.js";
|
|
35
|
-
|
|
36
|
-
export const pipelineCommand = new Command("pipeline")
|
|
37
|
-
.alias("pipe")
|
|
38
|
-
.description(
|
|
39
|
-
"AI video pipelines (script-to-video, highlights, shorts, animated-caption)"
|
|
40
|
-
)
|
|
41
|
-
.addHelpText(
|
|
42
|
-
"after",
|
|
43
|
-
`
|
|
44
|
-
Examples:
|
|
45
|
-
$ vibe pipeline script-to-video "A day in the life..." -o ./output/ -g kling
|
|
46
|
-
$ vibe pipeline script-to-video "..." -o ./output/ --images-only
|
|
47
|
-
$ vibe pipeline highlights long-video.mp4 -o highlights.json -d 60
|
|
48
|
-
$ vibe pipeline auto-shorts long-video.mp4 -o shorts/ -n 3 --add-captions
|
|
49
|
-
$ vibe pipeline animated-caption video.mp4 -o captioned.mp4 -s highlight
|
|
50
|
-
$ vibe pipeline animated-caption video.mp4 -o out.mp4 -s karaoke-sweep --fast
|
|
51
|
-
|
|
52
|
-
Required API Keys (pipelines use multiple providers):
|
|
53
|
-
script-to-video: ANTHROPIC_API_KEY + GOOGLE_API_KEY + ELEVENLABS_API_KEY
|
|
54
|
-
+ video provider key (KLING_API_KEY / RUNWAY_API_SECRET / GOOGLE_API_KEY)
|
|
55
|
-
highlights: GOOGLE_API_KEY (Gemini analysis)
|
|
56
|
-
auto-shorts: GOOGLE_API_KEY + OPENAI_API_KEY (optional captions)
|
|
57
|
-
animated-caption: OPENAI_API_KEY (Whisper transcription)
|
|
58
|
-
|
|
59
|
-
Use '--dry-run' to preview parameters before execution.
|
|
60
|
-
Run 'vibe setup --show' to check API key status.
|
|
61
|
-
`
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// ── pipeline script-to-video & regenerate-scene ────────────────────────
|
|
65
|
-
|
|
66
|
-
registerScriptPipelineCommands(pipelineCommand);
|
|
67
|
-
|
|
68
|
-
// ── pipeline highlights & auto-shorts ──────────────────────────────────
|
|
69
|
-
|
|
70
|
-
registerHighlightsCommands(pipelineCommand);
|
|
71
|
-
|
|
72
|
-
// ── pipeline viral ─────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
registerViralCommand(pipelineCommand);
|
|
75
|
-
|
|
76
|
-
// ── pipeline b-roll ────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
registerBrollCommand(pipelineCommand);
|
|
79
|
-
|
|
80
|
-
// ── pipeline narrate ───────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
pipelineCommand
|
|
83
|
-
.command("narrate")
|
|
84
|
-
.description("Generate AI narration for a video file or project (deprecated)")
|
|
85
|
-
.argument("<input>", "Video file or project file (.vibe.json)")
|
|
86
|
-
.option("-o, --output <dir>", "Output directory for generated files", ".")
|
|
87
|
-
.option("-v, --voice <name>", "ElevenLabs voice name (rachel, adam, josh, etc.)", "rachel")
|
|
88
|
-
.option("-s, --style <style>", "Narration style: informative, energetic, calm, dramatic", "informative")
|
|
89
|
-
.option("-l, --language <lang>", "Language code (e.g., en, ko)", "en")
|
|
90
|
-
.option("-p, --provider <name>", "LLM for script generation: claude (default), openai", "claude")
|
|
91
|
-
.option("--add-to-project", "Add narration to project (only for .vibe.json input)")
|
|
92
|
-
.option("--dry-run", "Preview pipeline parameters without executing")
|
|
93
|
-
.action(async (inputPath: string, options) => {
|
|
94
|
-
try {
|
|
95
|
-
console.warn(chalk.yellow("Warning: 'pipeline narrate' is deprecated. Use individual commands instead:"));
|
|
96
|
-
console.warn(chalk.dim(" vibe analyze video <video> 'describe scenes' → vibe generate speech '<script>'"));
|
|
97
|
-
console.warn();
|
|
98
|
-
|
|
99
|
-
const absPath = resolve(process.cwd(), inputPath);
|
|
100
|
-
if (!existsSync(absPath)) {
|
|
101
|
-
console.error(chalk.red(`File not found: ${absPath}`));
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (options.dryRun) {
|
|
106
|
-
outputResult({ dryRun: true, command: "pipeline narrate", params: { inputPath, voice: options.voice, style: options.style, language: options.language, provider: options.provider } });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log();
|
|
111
|
-
console.log(chalk.bold.cyan("Auto-Narrate Pipeline"));
|
|
112
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
113
|
-
console.log();
|
|
114
|
-
|
|
115
|
-
const isProject = inputPath.endsWith(".vibe.json");
|
|
116
|
-
let videoPath: string;
|
|
117
|
-
let project: Project | null = null;
|
|
118
|
-
let outputDir = resolve(process.cwd(), options.output);
|
|
119
|
-
|
|
120
|
-
if (isProject) {
|
|
121
|
-
// Load project to find video source
|
|
122
|
-
const content = await readFile(absPath, "utf-8");
|
|
123
|
-
const data: ProjectFile = JSON.parse(content);
|
|
124
|
-
project = Project.fromJSON(data);
|
|
125
|
-
const sources = project.getSources();
|
|
126
|
-
const videoSource = sources.find((s) => s.type === "video");
|
|
127
|
-
|
|
128
|
-
if (!videoSource) {
|
|
129
|
-
console.error(chalk.red("No video source found in project"));
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
videoPath = resolve(dirname(absPath), videoSource.url);
|
|
134
|
-
if (!existsSync(videoPath)) {
|
|
135
|
-
console.error(chalk.red(`Video file not found: ${videoPath}`));
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Use project directory as output if not specified
|
|
140
|
-
if (options.output === ".") {
|
|
141
|
-
outputDir = dirname(absPath);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
console.log(`Project: ${chalk.bold(project.getMeta().name)}`);
|
|
145
|
-
} else {
|
|
146
|
-
videoPath = absPath;
|
|
147
|
-
console.log(`Video: ${chalk.bold(basename(videoPath))}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Get video duration
|
|
151
|
-
const durationSpinner = ora("Analyzing video...").start();
|
|
152
|
-
let duration: number;
|
|
153
|
-
try {
|
|
154
|
-
duration = await ffprobeDuration(videoPath);
|
|
155
|
-
durationSpinner.succeed(chalk.green(`Duration: ${formatTime(duration)}`));
|
|
156
|
-
} catch {
|
|
157
|
-
durationSpinner.fail(chalk.red("Failed to get video duration"));
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Validate style option
|
|
162
|
-
const validStyles = ["informative", "energetic", "calm", "dramatic"];
|
|
163
|
-
if (!validStyles.includes(options.style)) {
|
|
164
|
-
console.error(chalk.red(`Invalid style: ${options.style}`));
|
|
165
|
-
console.error(chalk.dim(`Valid styles: ${validStyles.join(", ")}`));
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Generate narration
|
|
170
|
-
const generateSpinner = ora("Generating narration...").start();
|
|
171
|
-
|
|
172
|
-
generateSpinner.text = "Analyzing video with Gemini...";
|
|
173
|
-
const result = await autoNarrate({
|
|
174
|
-
videoPath,
|
|
175
|
-
duration,
|
|
176
|
-
outputDir,
|
|
177
|
-
voice: options.voice,
|
|
178
|
-
style: options.style as "informative" | "energetic" | "calm" | "dramatic",
|
|
179
|
-
language: options.language,
|
|
180
|
-
scriptProvider: options.provider as "claude" | "openai",
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (!result.success) {
|
|
184
|
-
generateSpinner.fail(chalk.red(`Failed: ${result.error}`));
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
generateSpinner.succeed(chalk.green("Narration generated successfully"));
|
|
189
|
-
|
|
190
|
-
if (isJsonMode()) {
|
|
191
|
-
outputResult({ success: true, audioPath: result.audioPath, segments: result.segments?.map(s => ({ startTime: s.startTime, endTime: s.endTime, text: s.text })) });
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Display result
|
|
196
|
-
console.log();
|
|
197
|
-
console.log(chalk.bold.cyan("Generated Files"));
|
|
198
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
199
|
-
console.log(` Audio: ${chalk.green(result.audioPath)}`);
|
|
200
|
-
console.log(` Script: ${chalk.green(resolve(outputDir, "narration-script.txt"))}`);
|
|
201
|
-
|
|
202
|
-
if (result.segments && result.segments.length > 0) {
|
|
203
|
-
console.log();
|
|
204
|
-
console.log(chalk.bold.cyan("Narration Segments"));
|
|
205
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
206
|
-
for (const seg of result.segments.slice(0, 5)) {
|
|
207
|
-
console.log(` [${formatTime(seg.startTime)} - ${formatTime(seg.endTime)}] ${chalk.dim(seg.text.substring(0, 50))}${seg.text.length > 50 ? "..." : ""}`);
|
|
208
|
-
}
|
|
209
|
-
if (result.segments.length > 5) {
|
|
210
|
-
console.log(chalk.dim(` ... and ${result.segments.length - 5} more segments`));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Add to project if requested
|
|
215
|
-
if (options.addToProject && project && isProject) {
|
|
216
|
-
const addSpinner = ora("Adding narration to project...").start();
|
|
217
|
-
|
|
218
|
-
// Get audio duration
|
|
219
|
-
let audioDuration: number;
|
|
220
|
-
try {
|
|
221
|
-
audioDuration = await getAudioDuration(result.audioPath!);
|
|
222
|
-
} catch {
|
|
223
|
-
audioDuration = duration; // Fallback to video duration
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Add audio source
|
|
227
|
-
const audioSource = project.addSource({
|
|
228
|
-
name: "Auto-generated narration",
|
|
229
|
-
url: basename(result.audioPath!),
|
|
230
|
-
type: "audio",
|
|
231
|
-
duration: audioDuration,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Add audio clip to audio track
|
|
235
|
-
const audioTrack = project.getTracks().find((t) => t.type === "audio");
|
|
236
|
-
if (audioTrack) {
|
|
237
|
-
project.addClip({
|
|
238
|
-
sourceId: audioSource.id,
|
|
239
|
-
trackId: audioTrack.id,
|
|
240
|
-
startTime: 0,
|
|
241
|
-
duration: Math.min(audioDuration, duration),
|
|
242
|
-
sourceStartOffset: 0,
|
|
243
|
-
sourceEndOffset: Math.min(audioDuration, duration),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Save updated project
|
|
248
|
-
await writeFile(absPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
249
|
-
addSpinner.succeed(chalk.green("Narration added to project"));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
console.log();
|
|
253
|
-
console.log(chalk.bold.green("Auto-narrate complete!"));
|
|
254
|
-
|
|
255
|
-
if (!options.addToProject && isProject) {
|
|
256
|
-
console.log();
|
|
257
|
-
console.log(chalk.dim("Tip: Use --add-to-project to automatically add the narration to your project"));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
console.log();
|
|
261
|
-
} catch (error) {
|
|
262
|
-
console.error(chalk.red("Auto-narrate failed"));
|
|
263
|
-
console.error(error);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ── pipeline animated-caption ────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
const ANIMATED_CAPTION_STYLES = ["highlight", "bounce", "pop-in", "neon", "karaoke-sweep", "typewriter"];
|
|
271
|
-
|
|
272
|
-
pipelineCommand
|
|
273
|
-
.command("animated-caption")
|
|
274
|
-
.description("Add animated captions with word-by-word effects (Whisper + Remotion/ASS)")
|
|
275
|
-
.argument("<video>", "Video file path")
|
|
276
|
-
.option("-s, --style <preset>", "Style preset (default: highlight)", "highlight")
|
|
277
|
-
.option("--highlight-color <color>", "Active word highlight color", "#FFFF00")
|
|
278
|
-
.option("--font-size <px>", "Font size (default: auto based on resolution)")
|
|
279
|
-
.option("--position <pos>", "Caption position: top, center, bottom", "bottom")
|
|
280
|
-
.option("--words-per-group <n>", "Words shown at once (default: auto 3-5)")
|
|
281
|
-
.option("--max-chars <n>", "Max characters per group")
|
|
282
|
-
.option("-l, --language <lang>", "Whisper language hint")
|
|
283
|
-
.option("--fast", "Use ASS/FFmpeg only (no Remotion, forces ASS tier styles)")
|
|
284
|
-
.option("-o, --output <path>", "Output file path")
|
|
285
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
286
|
-
.addHelpText(
|
|
287
|
-
"after",
|
|
288
|
-
`
|
|
289
|
-
Examples:
|
|
290
|
-
$ vibe pipeline animated-caption video.mp4 -o captioned.mp4
|
|
291
|
-
$ vibe pipeline animated-caption video.mp4 -o out.mp4 -s bounce
|
|
292
|
-
$ vibe pipeline animated-caption video.mp4 -o out.mp4 -s karaoke-sweep --fast
|
|
293
|
-
|
|
294
|
-
Styles:
|
|
295
|
-
highlight (default) TikTok-style background highlight on active word (Remotion)
|
|
296
|
-
bounce Words spring-animate in (Remotion)
|
|
297
|
-
pop-in Words scale-up on entry (Remotion)
|
|
298
|
-
neon Glowing neon effect on active word (Remotion)
|
|
299
|
-
karaoke-sweep Color sweep across active word (ASS/FFmpeg, fast)
|
|
300
|
-
typewriter Words appear one by one (ASS/FFmpeg, fast)
|
|
301
|
-
|
|
302
|
-
Required API Key: OPENAI_API_KEY (Whisper transcription)
|
|
303
|
-
`,
|
|
304
|
-
)
|
|
305
|
-
.action(async (videoPath: string, options) => {
|
|
306
|
-
try {
|
|
307
|
-
const absVideoPath = resolve(process.cwd(), videoPath);
|
|
308
|
-
if (!existsSync(absVideoPath)) {
|
|
309
|
-
console.error(chalk.red(`File not found: ${absVideoPath}`));
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Validate style
|
|
314
|
-
if (!ANIMATED_CAPTION_STYLES.includes(options.style)) {
|
|
315
|
-
console.error(chalk.red(`Invalid style: ${options.style}`));
|
|
316
|
-
console.error(chalk.dim(`Valid styles: ${ANIMATED_CAPTION_STYLES.join(", ")}`));
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const outputFile = options.output || videoPath.replace(/(\.\w+)$/, "-captioned$1");
|
|
321
|
-
|
|
322
|
-
if (options.dryRun) {
|
|
323
|
-
outputResult({
|
|
324
|
-
dryRun: true,
|
|
325
|
-
command: "pipeline animated-caption",
|
|
326
|
-
params: {
|
|
327
|
-
videoPath: absVideoPath,
|
|
328
|
-
outputPath: outputFile,
|
|
329
|
-
style: options.style,
|
|
330
|
-
highlightColor: options.highlightColor,
|
|
331
|
-
fontSize: options.fontSize ? parseInt(options.fontSize) : "auto",
|
|
332
|
-
position: options.position,
|
|
333
|
-
wordsPerGroup: options.wordsPerGroup ? parseInt(options.wordsPerGroup) : "auto",
|
|
334
|
-
maxChars: options.maxChars ? parseInt(options.maxChars) : "auto",
|
|
335
|
-
language: options.language || "auto",
|
|
336
|
-
fast: !!options.fast,
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
console.log();
|
|
343
|
-
console.log(chalk.bold.cyan("Animated Caption Pipeline"));
|
|
344
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
345
|
-
console.log(` Video: ${chalk.bold(basename(absVideoPath))}`);
|
|
346
|
-
console.log(` Style: ${chalk.bold(options.style)}`);
|
|
347
|
-
console.log(` Mode: ${chalk.bold(options.fast ? "ASS (fast)" : "Remotion")}`);
|
|
348
|
-
console.log();
|
|
349
|
-
|
|
350
|
-
const spinner = ora("Processing animated captions...").start();
|
|
351
|
-
|
|
352
|
-
const result = await executeAnimatedCaption({
|
|
353
|
-
videoPath: absVideoPath,
|
|
354
|
-
outputPath: outputFile,
|
|
355
|
-
style: options.style as AnimatedCaptionStyle,
|
|
356
|
-
highlightColor: options.highlightColor,
|
|
357
|
-
fontSize: options.fontSize ? parseInt(options.fontSize) : undefined,
|
|
358
|
-
position: options.position as "top" | "center" | "bottom",
|
|
359
|
-
wordsPerGroup: options.wordsPerGroup ? parseInt(options.wordsPerGroup) : undefined,
|
|
360
|
-
maxChars: options.maxChars ? parseInt(options.maxChars) : undefined,
|
|
361
|
-
language: options.language,
|
|
362
|
-
fast: options.fast,
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
if (!result.success) {
|
|
366
|
-
spinner.fail(chalk.red(result.error || "Animated caption failed"));
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
spinner.succeed(chalk.green("Animated captions applied successfully"));
|
|
371
|
-
|
|
372
|
-
if (isJsonMode()) {
|
|
373
|
-
outputResult({
|
|
374
|
-
success: true,
|
|
375
|
-
outputPath: result.outputPath,
|
|
376
|
-
wordCount: result.wordCount,
|
|
377
|
-
groupCount: result.groupCount,
|
|
378
|
-
style: result.style,
|
|
379
|
-
tier: result.tier,
|
|
380
|
-
});
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
console.log();
|
|
385
|
-
console.log(chalk.bold.cyan("Result"));
|
|
386
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
387
|
-
console.log(` Output: ${chalk.green(result.outputPath)}`);
|
|
388
|
-
console.log(` Words: ${result.wordCount}`);
|
|
389
|
-
console.log(` Groups: ${result.groupCount}`);
|
|
390
|
-
console.log(` Style: ${result.style}`);
|
|
391
|
-
console.log(` Tier: ${result.tier}`);
|
|
392
|
-
console.log();
|
|
393
|
-
} catch (error) {
|
|
394
|
-
console.error(chalk.red("Animated caption failed"));
|
|
395
|
-
console.error(error);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
import { readFileSync, existsSync, mkdtempSync, rmSync } from "fs";
|
|
4
|
-
import { join, resolve } from "path";
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
|
|
7
|
-
const CLI = `npx tsx ${resolve(__dirname, "../index.ts")}`;
|
|
8
|
-
|
|
9
|
-
describe("project commands", () => {
|
|
10
|
-
let tempDir: string;
|
|
11
|
-
let projectFile: string;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
tempDir = mkdtempSync(join(tmpdir(), "vibe-test-"));
|
|
15
|
-
projectFile = join(tempDir, "test.vibe.json");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("project create", () => {
|
|
23
|
-
it("creates a project file with given name", () => {
|
|
24
|
-
execSync(`${CLI} project create "My Project" -o "${projectFile}"`, {
|
|
25
|
-
cwd: process.cwd(),
|
|
26
|
-
encoding: "utf-8",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
expect(existsSync(projectFile)).toBe(true);
|
|
30
|
-
|
|
31
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
32
|
-
expect(content.version).toBe("1.0.0");
|
|
33
|
-
expect(content.state.project.name).toBe("My Project");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("creates project with custom aspect ratio", () => {
|
|
37
|
-
execSync(`${CLI} project create "Vertical" -o "${projectFile}" -r 9:16`, {
|
|
38
|
-
cwd: process.cwd(),
|
|
39
|
-
encoding: "utf-8",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
43
|
-
expect(content.state.project.aspectRatio).toBe("9:16");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("creates project with custom frame rate", () => {
|
|
47
|
-
execSync(`${CLI} project create "HFR" -o "${projectFile}" -f 60`, {
|
|
48
|
-
cwd: process.cwd(),
|
|
49
|
-
encoding: "utf-8",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
53
|
-
expect(content.state.project.frameRate).toBe(60);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("creates project with default tracks", () => {
|
|
57
|
-
execSync(`${CLI} project create "Test" -o "${projectFile}"`, {
|
|
58
|
-
cwd: process.cwd(),
|
|
59
|
-
encoding: "utf-8",
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
63
|
-
expect(content.state.tracks).toHaveLength(2);
|
|
64
|
-
expect(content.state.tracks[0].type).toBe("video");
|
|
65
|
-
expect(content.state.tracks[1].type).toBe("audio");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("project info", () => {
|
|
70
|
-
beforeEach(() => {
|
|
71
|
-
execSync(`${CLI} project create "Info Test" -o "${projectFile}"`, {
|
|
72
|
-
cwd: process.cwd(),
|
|
73
|
-
encoding: "utf-8",
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("displays project information", () => {
|
|
78
|
-
const output = execSync(`${CLI} project info "${projectFile}"`, {
|
|
79
|
-
cwd: process.cwd(),
|
|
80
|
-
encoding: "utf-8",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(output).toContain("Info Test");
|
|
84
|
-
expect(output).toContain("16:9");
|
|
85
|
-
expect(output).toContain("30");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("project set", () => {
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
execSync(`${CLI} project create "Original" -o "${projectFile}"`, {
|
|
92
|
-
cwd: process.cwd(),
|
|
93
|
-
encoding: "utf-8",
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("updates project name", () => {
|
|
98
|
-
execSync(`${CLI} project set "${projectFile}" -n "Updated Name"`, {
|
|
99
|
-
cwd: process.cwd(),
|
|
100
|
-
encoding: "utf-8",
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
104
|
-
expect(content.state.project.name).toBe("Updated Name");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("updates aspect ratio", () => {
|
|
108
|
-
execSync(`${CLI} project set "${projectFile}" -r 1:1`, {
|
|
109
|
-
cwd: process.cwd(),
|
|
110
|
-
encoding: "utf-8",
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
114
|
-
expect(content.state.project.aspectRatio).toBe("1:1");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("updates frame rate", () => {
|
|
118
|
-
execSync(`${CLI} project set "${projectFile}" -f 24`, {
|
|
119
|
-
cwd: process.cwd(),
|
|
120
|
-
encoding: "utf-8",
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const content = JSON.parse(readFileSync(projectFile, "utf-8"));
|
|
124
|
-
expect(content.state.project.frameRate).toBe(24);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
});
|
package/src/commands/project.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { readFile, writeFile, stat } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import ora from "ora";
|
|
6
|
-
import { Project, type ProjectFile } from "../engine/index.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Resolve project file path - handles both file paths and directory paths
|
|
10
|
-
* If path is a directory, looks for project.vibe.json inside
|
|
11
|
-
*/
|
|
12
|
-
async function resolveProjectPath(inputPath: string): Promise<string> {
|
|
13
|
-
const filePath = resolve(process.cwd(), inputPath);
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const stats = await stat(filePath);
|
|
17
|
-
if (stats.isDirectory()) {
|
|
18
|
-
return resolve(filePath, "project.vibe.json");
|
|
19
|
-
}
|
|
20
|
-
} catch {
|
|
21
|
-
// Path doesn't exist or other error - let readFile handle it
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return filePath;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const projectCommand = new Command("project")
|
|
28
|
-
.description("Project management commands");
|
|
29
|
-
|
|
30
|
-
projectCommand
|
|
31
|
-
.command("create")
|
|
32
|
-
.description("Create a new project")
|
|
33
|
-
.argument("<name>", "Project name or path (e.g., 'my-project' or 'output/my-project')")
|
|
34
|
-
.option("-o, --output <path>", "Output file path (overrides name-based path)")
|
|
35
|
-
.option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)", "16:9")
|
|
36
|
-
.option("-f, --fps <fps>", "Frame rate", "30")
|
|
37
|
-
.action(async (name: string, options) => {
|
|
38
|
-
const spinner = ora("Creating project...").start();
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// If name contains a path separator, treat it as a directory path
|
|
42
|
-
const projectName = name.includes("/") ? name.split("/").pop()! : name;
|
|
43
|
-
const project = new Project(projectName);
|
|
44
|
-
project.setAspectRatio(options.ratio);
|
|
45
|
-
project.setFrameRate(parseInt(options.fps, 10));
|
|
46
|
-
|
|
47
|
-
let outputPath: string;
|
|
48
|
-
if (options.output) {
|
|
49
|
-
outputPath = resolve(process.cwd(), options.output);
|
|
50
|
-
} else if (name.includes("/")) {
|
|
51
|
-
// Name contains path — create directory and put project.vibe.json inside
|
|
52
|
-
const dirPath = resolve(process.cwd(), name);
|
|
53
|
-
const { mkdir } = await import("node:fs/promises");
|
|
54
|
-
await mkdir(dirPath, { recursive: true });
|
|
55
|
-
outputPath = resolve(dirPath, "project.vibe.json");
|
|
56
|
-
} else {
|
|
57
|
-
outputPath = resolve(process.cwd(), "project.vibe.json");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const data = JSON.stringify(project.toJSON(), null, 2);
|
|
61
|
-
await writeFile(outputPath, data, "utf-8");
|
|
62
|
-
|
|
63
|
-
spinner.succeed(chalk.green(`Project created: ${outputPath}`));
|
|
64
|
-
console.log();
|
|
65
|
-
console.log(chalk.dim(" Name:"), projectName);
|
|
66
|
-
console.log(chalk.dim(" Aspect Ratio:"), options.ratio);
|
|
67
|
-
console.log(chalk.dim(" Frame Rate:"), options.fps, "fps");
|
|
68
|
-
} catch (error) {
|
|
69
|
-
spinner.fail(chalk.red("Failed to create project"));
|
|
70
|
-
console.error(error);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
projectCommand
|
|
76
|
-
.command("info")
|
|
77
|
-
.description("Show project information")
|
|
78
|
-
.argument("<file>", "Project file path")
|
|
79
|
-
.action(async (file: string) => {
|
|
80
|
-
const spinner = ora("Loading project...").start();
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const filePath = await resolveProjectPath(file);
|
|
84
|
-
const content = await readFile(filePath, "utf-8");
|
|
85
|
-
const data: ProjectFile = JSON.parse(content);
|
|
86
|
-
const project = Project.fromJSON(data);
|
|
87
|
-
|
|
88
|
-
spinner.stop();
|
|
89
|
-
|
|
90
|
-
const summary = project.getSummary();
|
|
91
|
-
const meta = project.getMeta();
|
|
92
|
-
|
|
93
|
-
console.log();
|
|
94
|
-
console.log(chalk.bold.cyan("Project Info"));
|
|
95
|
-
console.log(chalk.dim("─".repeat(40)));
|
|
96
|
-
console.log(chalk.dim(" Name:"), summary.name);
|
|
97
|
-
console.log(chalk.dim(" Duration:"), formatDuration(summary.duration));
|
|
98
|
-
console.log(chalk.dim(" Aspect Ratio:"), summary.aspectRatio);
|
|
99
|
-
console.log(chalk.dim(" Frame Rate:"), summary.frameRate, "fps");
|
|
100
|
-
console.log();
|
|
101
|
-
console.log(chalk.dim(" Tracks:"), summary.trackCount);
|
|
102
|
-
console.log(chalk.dim(" Clips:"), summary.clipCount);
|
|
103
|
-
console.log(chalk.dim(" Sources:"), summary.sourceCount);
|
|
104
|
-
console.log();
|
|
105
|
-
console.log(chalk.dim(" Created:"), meta.createdAt.toLocaleString());
|
|
106
|
-
console.log(chalk.dim(" Updated:"), meta.updatedAt.toLocaleString());
|
|
107
|
-
console.log();
|
|
108
|
-
} catch (error) {
|
|
109
|
-
spinner.fail(chalk.red("Failed to load project"));
|
|
110
|
-
console.error(error);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
projectCommand
|
|
116
|
-
.command("set")
|
|
117
|
-
.description("Update project settings")
|
|
118
|
-
.argument("<file>", "Project file path")
|
|
119
|
-
.option("-n, --name <name>", "Project name")
|
|
120
|
-
.option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)")
|
|
121
|
-
.option("-f, --fps <fps>", "Frame rate")
|
|
122
|
-
.action(async (file: string, options) => {
|
|
123
|
-
const spinner = ora("Updating project...").start();
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const filePath = await resolveProjectPath(file);
|
|
127
|
-
const content = await readFile(filePath, "utf-8");
|
|
128
|
-
const data: ProjectFile = JSON.parse(content);
|
|
129
|
-
const project = Project.fromJSON(data);
|
|
130
|
-
|
|
131
|
-
if (options.name) project.setName(options.name);
|
|
132
|
-
if (options.ratio) project.setAspectRatio(options.ratio);
|
|
133
|
-
if (options.fps) project.setFrameRate(parseInt(options.fps, 10));
|
|
134
|
-
|
|
135
|
-
await writeFile(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
136
|
-
|
|
137
|
-
spinner.succeed(chalk.green("Project updated"));
|
|
138
|
-
} catch (error) {
|
|
139
|
-
spinner.fail(chalk.red("Failed to update project"));
|
|
140
|
-
console.error(error);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
function formatDuration(seconds: number): string {
|
|
146
|
-
const mins = Math.floor(seconds / 60);
|
|
147
|
-
const secs = (seconds % 60).toFixed(1);
|
|
148
|
-
return `${mins}:${secs.padStart(4, "0")}`;
|
|
149
|
-
}
|