@vibeframe/cli 0.77.0 → 0.79.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 (2) hide show
  1. package/dist/index.js +279 -202
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -29396,7 +29396,7 @@ var init_scene = __esm({
29396
29396
  init_agent_host_detect();
29397
29397
  init_install_skill();
29398
29398
  init_compose_prompts();
29399
- sceneCommand = new Command("scene").description("Lower-level scene authoring (add, lint, styles). For project flow use `vibe init` / `vibe build` / `vibe render`.").addHelpText("after", `
29399
+ sceneCommand = new Command("scene").description("Lower-level scene authoring (add, lint, list-styles). For project flow use `vibe init` / `vibe build` / `vibe render`.").addHelpText("after", `
29400
29400
  Examples:
29401
29401
  $ vibe scene add intro --style announcement \\
29402
29402
  --headline "Welcome to VibeFrame" # Headline-only scene
@@ -29771,7 +29771,7 @@ __export(remotion_exports, {
29771
29771
  wrapComponentWithVideo: () => wrapComponentWithVideo
29772
29772
  });
29773
29773
  import { writeFile as writeFile10, mkdir as mkdir6, rm, copyFile as copyFile2 } from "node:fs/promises";
29774
- import { existsSync as existsSync8 } from "node:fs";
29774
+ import { existsSync as existsSync9 } from "node:fs";
29775
29775
  import { join as join7 } from "node:path";
29776
29776
  import { tmpdir } from "node:os";
29777
29777
  import { createHash as createHash3 } from "node:crypto";
@@ -29846,7 +29846,7 @@ registerRoot(Root);
29846
29846
  const depsHash = createHash3("md5").update(packageJsonStr).digest("hex").slice(0, 12);
29847
29847
  const cacheMarker = join7(REMOTION_CACHE_DIR, `.deps-${depsHash}`);
29848
29848
  const cachedModules = join7(REMOTION_CACHE_DIR, "node_modules");
29849
- if (existsSync8(cacheMarker) && existsSync8(cachedModules)) {
29849
+ if (existsSync9(cacheMarker) && existsSync9(cachedModules)) {
29850
29850
  const { symlink } = await import("node:fs/promises");
29851
29851
  try {
29852
29852
  await symlink(cachedModules, join7(dir, "node_modules"), "dir");
@@ -29858,7 +29858,7 @@ registerRoot(Root);
29858
29858
  try {
29859
29859
  await mkdir6(REMOTION_CACHE_DIR, { recursive: true });
29860
29860
  await writeFile10(join7(REMOTION_CACHE_DIR, "package.json"), packageJsonStr);
29861
- if (existsSync8(join7(dir, "node_modules"))) {
29861
+ if (existsSync9(join7(dir, "node_modules"))) {
29862
29862
  const { rename: rename5, symlink } = await import("node:fs/promises");
29863
29863
  await rename5(join7(dir, "node_modules"), cachedModules).catch(() => {
29864
29864
  });
@@ -30526,7 +30526,7 @@ __export(ai_motion_exports, {
30526
30526
  registerMotionCommand: () => registerMotionCommand
30527
30527
  });
30528
30528
  import { resolve as resolve12 } from "node:path";
30529
- import { existsSync as existsSync9 } from "node:fs";
30529
+ import { existsSync as existsSync10 } from "node:fs";
30530
30530
  import { readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
30531
30531
  async function executeMotion(options) {
30532
30532
  const modelAlias = options.model || "sonnet";
@@ -30574,7 +30574,7 @@ Use this image analysis to inform the color palette, typography placement, and o
30574
30574
  let result;
30575
30575
  if (options.fromTsx) {
30576
30576
  const tsxPath = resolve12(process.cwd(), options.fromTsx);
30577
- if (!existsSync9(tsxPath)) {
30577
+ if (!existsSync10(tsxPath)) {
30578
30578
  return { success: false, error: `TSX file not found: ${tsxPath}` };
30579
30579
  }
30580
30580
  const existingCode = await readFile10(tsxPath, "utf-8");
@@ -30703,7 +30703,7 @@ Use this image analysis to inform the color palette, typography placement, and o
30703
30703
  return { success: true, codePath, componentName: component.name, renderedPath: renderResult.outputPath };
30704
30704
  }
30705
30705
  function registerMotionCommand(aiCommand) {
30706
- aiCommand.command("motion").description("Generate motion graphics using Claude + Remotion (render & composite)").argument("<description>", "Natural language description of the motion graphic").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output file path", "motion.tsx").option("-d, --duration <sec>", "Duration in seconds", "5").option("-w, --width <px>", "Width in pixels", "1920").option("-h, --height <px>", "Height in pixels", "1080").option("--fps <fps>", "Frame rate", "30").option("-s, --style <style>", "Style preset: minimal, corporate, playful, cinematic").option("--render", "Render the generated code with Remotion (output .webm)").option("--video <path>", "Base video to composite the motion graphic onto").option("--image <path>", "Image to analyze with Gemini \u2014 color/mood fed into Claude prompt").option("--from-tsx <path>", "Refine an existing TSX file instead of generating from scratch").option("-m, --model <alias>", "LLM model: sonnet (default), opus, gemini, gemini-3.1-pro", "sonnet").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
30706
+ aiCommand.command("motion").description("Generate motion graphics using Claude + Remotion (render & composite)").argument("<description>", "Natural language description of the motion graphic").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output file path", "motion.tsx").option("-d, --duration <sec>", "Duration in seconds", "5").option("--width <px>", "Width in pixels", "1920").option("--height <px>", "Height in pixels", "1080").option("--fps <fps>", "Frame rate", "30").option("--style <style>", "Style preset: minimal, corporate, playful, cinematic").option("--render", "Render the generated code with Remotion (output .webm)").option("--video <path>", "Base video to composite the motion graphic onto").option("--image <path>", "Image to analyze with Gemini \u2014 color/mood fed into Claude prompt").option("--from-tsx <path>", "Refine an existing TSX file instead of generating from scratch").option("-m, --model <alias>", "LLM model: sonnet (default), opus, gemini, gemini-3.1-pro", "sonnet").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
30707
30707
  const startedAt = Date.now();
30708
30708
  try {
30709
30709
  if (options.output) {
@@ -31307,7 +31307,7 @@ async function executeStoryboard(options) {
31307
31307
  }
31308
31308
  }
31309
31309
  function registerStoryboardCommand(parent) {
31310
- parent.command("storyboard").description("Generate video storyboard from content using Claude").argument("<content>", "Content to analyze (text or file path)").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output JSON file path").option("-d, --duration <sec>", "Target total duration in seconds").option("-f, --file", "Treat content argument as file path").option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low").option("--dry-run", "Preview parameters without executing").action(async (content, options) => {
31310
+ parent.command("storyboard").description("Generate video storyboard from content using Claude").argument("<content>", "Content to analyze (text or file path)").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output JSON file path").option("-d, --duration <sec>", "Target total duration in seconds").option("--file", "Treat content argument as file path").option("--creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low").option("--dry-run", "Preview parameters without executing").action(async (content, options) => {
31311
31311
  const startedAt = Date.now();
31312
31312
  try {
31313
31313
  rejectControlChars(content);
@@ -31625,7 +31625,7 @@ async function executeSpeech(options) {
31625
31625
  }
31626
31626
  }
31627
31627
  function registerSpeechCommand(parent) {
31628
- parent.command("speech").alias("tts").description("Generate speech from text using ElevenLabs").argument("[text]", "Text to convert to speech (interactive if omitted)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "output.mp3").option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM").option("--list-voices", "List available voices").option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat).option("--dry-run", "Preview parameters without executing").action(async (text, options) => {
31628
+ parent.command("speech").alias("tts").description("Generate speech from text using ElevenLabs").argument("[text]", "Text to convert to speech (interactive if omitted)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "output.mp3").option("--voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM").option("--list-voices", "List available voices").option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat).option("--dry-run", "Preview parameters without executing").action(async (text, options) => {
31629
31629
  const startedAt = Date.now();
31630
31630
  try {
31631
31631
  if (!text) {
@@ -31778,7 +31778,7 @@ var init_speech = __esm({
31778
31778
  // src/commands/generate/music.ts
31779
31779
  import { resolve as resolve17 } from "node:path";
31780
31780
  import { writeFile as writeFile16 } from "node:fs/promises";
31781
- import { existsSync as existsSync10 } from "node:fs";
31781
+ import { existsSync as existsSync11 } from "node:fs";
31782
31782
  async function executeMusic(options) {
31783
31783
  try {
31784
31784
  const provider = options.provider || "elevenlabs";
@@ -31835,7 +31835,7 @@ async function executeMusic(options) {
31835
31835
  }
31836
31836
  }
31837
31837
  function registerMusicCommand(parent) {
31838
- parent.command("music").description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)").argument("<prompt>", "Description of the music to generate").option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs").option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)").option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8").option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)").option("-m, --melody <file>", "Reference melody audio file for conditioning (Replicate only)").option("--model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large").option("-o, --output <path>", "Output audio file path", "music.mp3").option("--no-wait", "Don't wait for generation to complete (Replicate async mode)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
31838
+ parent.command("music").description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)").argument("<prompt>", "Description of the music to generate").option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs").option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)").option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8").option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)").option("--melody <file>", "Reference melody audio file for conditioning (Replicate only)").option("-m, --model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large").option("-o, --output <path>", "Output audio file path", "music.mp3").option("--no-wait", "Don't wait for generation to complete (Replicate async mode)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
31839
31839
  const startedAt = Date.now();
31840
31840
  try {
31841
31841
  rejectControlChars(prompt3);
@@ -31913,7 +31913,7 @@ function registerMusicCommand(parent) {
31913
31913
  if (options.melody) {
31914
31914
  spinner2.text = "Uploading melody reference...";
31915
31915
  const absPath = resolve17(process.cwd(), options.melody);
31916
- if (!existsSync10(absPath)) {
31916
+ if (!existsSync11(absPath)) {
31917
31917
  spinner2.fail(`Melody file not found: ${options.melody}`);
31918
31918
  exitWithError(notFoundError(options.melody));
31919
31919
  }
@@ -32004,7 +32004,7 @@ __export(ai_image_exports, {
32004
32004
  });
32005
32005
  import { resolve as resolve18, dirname as dirname5 } from "node:path";
32006
32006
  import { readFile as readFile12, writeFile as writeFile17, mkdir as mkdir8 } from "node:fs/promises";
32007
- import { existsSync as existsSync11 } from "node:fs";
32007
+ import { existsSync as existsSync12 } from "node:fs";
32008
32008
  async function executeImageGenerate(options) {
32009
32009
  const {
32010
32010
  prompt: prompt3,
@@ -32148,7 +32148,7 @@ async function executeGeminiEdit(options) {
32148
32148
  const imageBuffers = [];
32149
32149
  for (const imagePath of imagePaths) {
32150
32150
  const absPath = resolve18(process.cwd(), imagePath);
32151
- if (!existsSync11(absPath)) {
32151
+ if (!existsSync12(absPath)) {
32152
32152
  return { success: false, error: `Image not found: ${absPath}` };
32153
32153
  }
32154
32154
  const buffer = await readFile12(absPath);
@@ -32196,7 +32196,7 @@ async function executeThumbnailBestFrame(options) {
32196
32196
  model = "flash",
32197
32197
  apiKey
32198
32198
  } = options;
32199
- if (!existsSync11(videoPath)) {
32199
+ if (!existsSync12(videoPath)) {
32200
32200
  return { success: false, error: `Video not found: ${videoPath}` };
32201
32201
  }
32202
32202
  if (!commandExists("ffmpeg")) {
@@ -32236,7 +32236,7 @@ async function executeThumbnailBestFrame(options) {
32236
32236
  reason = reasonMatch[1];
32237
32237
  }
32238
32238
  await execSafe("ffmpeg", ["-ss", String(timestamp), "-i", videoPath, "-frames:v", "1", "-q:v", "2", outputPath, "-y"], { timeout: 6e4, maxBuffer: 50 * 1024 * 1024 });
32239
- if (!existsSync11(outputPath)) {
32239
+ if (!existsSync12(outputPath)) {
32240
32240
  return { success: false, error: "FFmpeg failed to extract frame" };
32241
32241
  }
32242
32242
  return {
@@ -32262,10 +32262,10 @@ var init_ai_image = __esm({
32262
32262
 
32263
32263
  // src/commands/generate/thumbnail.ts
32264
32264
  import { resolve as resolve19, dirname as dirname6, basename as basename3, extname as extname2 } from "node:path";
32265
- import { existsSync as existsSync12 } from "node:fs";
32265
+ import { existsSync as existsSync13 } from "node:fs";
32266
32266
  import { writeFile as writeFile18, mkdir as mkdir9 } from "node:fs/promises";
32267
32267
  function registerThumbnailCommand(parent) {
32268
- parent.command("thumbnail").description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)").argument("[description]", "Thumbnail description (for DALL-E generation)").option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)").option("-o, --output <path>", "Output file path").option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter").option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI").option("--prompt <prompt>", "Custom prompt for best-frame analysis").option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash").action(async (description, options) => {
32268
+ parent.command("thumbnail").description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)").argument("[description]", "Thumbnail description (for DALL-E generation)").option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)").option("-o, --output <path>", "Output file path").option("--style <style>", "Platform style: youtube, instagram, tiktok, twitter").option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI").option("--prompt <prompt>", "Custom prompt for best-frame analysis").option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash").action(async (description, options) => {
32269
32269
  const startedAt = Date.now();
32270
32270
  try {
32271
32271
  if (description) rejectControlChars(description);
@@ -32274,7 +32274,7 @@ function registerThumbnailCommand(parent) {
32274
32274
  }
32275
32275
  if (options.bestFrame) {
32276
32276
  const absVideoPath = resolve19(process.cwd(), options.bestFrame);
32277
- if (!existsSync12(absVideoPath)) {
32277
+ if (!existsSync13(absVideoPath)) {
32278
32278
  exitWithError(notFoundError(absVideoPath));
32279
32279
  }
32280
32280
  if (!commandExists("ffmpeg")) {
@@ -32431,7 +32431,7 @@ function getStatusColor(status) {
32431
32431
  }
32432
32432
  }
32433
32433
  function registerVideoStatusCommand(parent) {
32434
- parent.command("video-status", { hidden: true }).description("Check video generation status (Grok, Runway, or Kling)").argument("<task-id>", "Task ID from video generation").option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)").option("-t, --type <type>", "Task type: text2video or image2video (Kling only)", "text2video").option("-w, --wait", "Wait for completion").option("-o, --output <path>", "Download video when complete").action(async (taskId, options) => {
32434
+ parent.command("video-status", { hidden: true }).description("Check video generation status (Grok, Runway, or Kling)").argument("<task-id>", "Task ID from video generation").option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)").option("--type <type>", "Task type: text2video or image2video (Kling only)", "text2video").option("--wait", "Wait for completion").option("-o, --output <path>", "Download video when complete").action(async (taskId, options) => {
32435
32435
  const startedAt = Date.now();
32436
32436
  try {
32437
32437
  const provider = (options.provider || "grok").toLowerCase();
@@ -32664,7 +32664,7 @@ var init_video_status = __esm({
32664
32664
  import { resolve as resolve21 } from "node:path";
32665
32665
  import { writeFile as writeFile20 } from "node:fs/promises";
32666
32666
  function registerVideoExtendCommand(parent) {
32667
- parent.command("video-extend", { hidden: true }).description("Extend video duration (Kling by video ID, Veo by operation name)").argument("<id>", "Kling video ID or Veo operation name").option("-p, --provider <provider>", "Provider: kling, veo", "kling").option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path").option("--prompt <text>", "Continuation prompt").option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5").option("-n, --negative <prompt>", "Negative prompt (what to avoid, Kling only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1").option("--no-wait", "Start extension and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (id, options) => {
32667
+ parent.command("video-extend", { hidden: true }).description("Extend video duration (Kling by video ID, Veo by operation name)").argument("<id>", "Kling video ID or Veo operation name").option("-p, --provider <provider>", "Provider: kling, veo", "kling").option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path").option("--prompt <text>", "Continuation prompt").option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5").option("--negative <prompt>", "Negative prompt (what to avoid, Kling only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1").option("--no-wait", "Start extension and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (id, options) => {
32668
32668
  const startedAt = Date.now();
32669
32669
  try {
32670
32670
  const provider = (options.provider || "kling").toLowerCase();
@@ -32947,7 +32947,7 @@ import { resolve as resolve22, dirname as dirname7 } from "node:path";
32947
32947
  import { fileURLToPath } from "node:url";
32948
32948
  import { writeFile as writeFile21, mkdir as mkdir10 } from "node:fs/promises";
32949
32949
  function registerImageCommand(parent) {
32950
- parent.command("image").alias("img").description("Generate image using AI (Gemini, OpenAI gpt-image, Grok, or Runway)").argument("[prompt]", "Image description prompt (interactive if omitted)").option("-p, --provider <provider>", "Provider: openai (default when OPENAI_API_KEY set), gemini, grok, runway").option("-k, --api-key <key>", "API key (or set env: OPENAI_API_KEY, GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path (downloads image)").option("-s, --size <size>", "Image size (openai: 1024x1024, 1536x1024, 1024x1536)", "1024x1024").option("-r, --ratio <ratio>", "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)", "1:1").option("--quality <quality>", "Quality: standard, hd (openai only)", "standard").option("--style <style>", "Style: vivid, natural (openai only)", "vivid").option("-n, --count <n>", "Number of images to generate", "1").option("-m, --model <model>", "Model. Gemini: flash, 3.1-flash, latest, pro. OpenAI: 1.5 (default), 2 (gpt-image-2)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
32950
+ parent.command("image").alias("img").description("Generate image using AI (Gemini, OpenAI gpt-image, Grok, or Runway)").argument("[prompt]", "Image description prompt (interactive if omitted)").option("-p, --provider <provider>", "Provider: openai (default when OPENAI_API_KEY set), gemini, grok, runway").option("-k, --api-key <key>", "API key (or set env: OPENAI_API_KEY, GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path (downloads image)").option("--size <size>", "Image size (openai: 1024x1024, 1536x1024, 1024x1536)", "1024x1024").option("-r, --ratio <ratio>", "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)", "1:1").option("--quality <quality>", "Quality: standard, hd (openai only)", "standard").option("--style <style>", "Style: vivid, natural (openai only)", "vivid").option("--count <n>", "Number of images to generate", "1").option("-m, --model <model>", "Model. Gemini: flash, 3.1-flash, latest, pro. OpenAI: 1.5 (default), 2 (gpt-image-2)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
32951
32951
  Examples:
32952
32952
  $ vibe generate image "a sunset over the ocean" -o sunset.png
32953
32953
  $ vibe gen img "logo design" -o logo.png -p openai
@@ -32975,6 +32975,15 @@ Examples:
32975
32975
  if (options.output) {
32976
32976
  validateOutputPath(options.output);
32977
32977
  }
32978
+ if (options.count !== void 0) {
32979
+ const n = parseInt(options.count, 10);
32980
+ if (!Number.isFinite(n) || n < 1 || n > 10) {
32981
+ exitWithError(usageError(
32982
+ `Invalid --count: ${options.count}`,
32983
+ "Must be an integer between 1 and 10."
32984
+ ));
32985
+ }
32986
+ }
32978
32987
  const imageRegistry = getProvidersFor("image");
32979
32988
  const validProviders = [...imageRegistry.map((p) => p.name), "runway"];
32980
32989
  const providerEnvMap = Object.fromEntries(
@@ -34654,7 +34663,7 @@ var init_video_providers = __esm({
34654
34663
  // src/commands/ai-script-pipeline.ts
34655
34664
  import { readFile as readFile13, writeFile as writeFile23, unlink as unlink2, rename as rename2 } from "node:fs/promises";
34656
34665
  import { resolve as resolve24, extname as extname3 } from "node:path";
34657
- import { existsSync as existsSync13 } from "node:fs";
34666
+ import { existsSync as existsSync14 } from "node:fs";
34658
34667
  async function executeRegenerateScene(options) {
34659
34668
  const result = {
34660
34669
  success: false,
@@ -34663,12 +34672,12 @@ async function executeRegenerateScene(options) {
34663
34672
  };
34664
34673
  try {
34665
34674
  const outputDir = resolve24(process.cwd(), options.projectDir);
34666
- if (!existsSync13(outputDir)) {
34675
+ if (!existsSync14(outputDir)) {
34667
34676
  return { ...result, error: `Project directory not found: ${outputDir}` };
34668
34677
  }
34669
34678
  const yamlPath = resolve24(outputDir, "storyboard.yaml");
34670
34679
  const jsonPath = resolve24(outputDir, "storyboard.json");
34671
- const storyboardPath = existsSync13(yamlPath) ? yamlPath : existsSync13(jsonPath) ? jsonPath : null;
34680
+ const storyboardPath = existsSync14(yamlPath) ? yamlPath : existsSync14(jsonPath) ? jsonPath : null;
34672
34681
  if (!storyboardPath) {
34673
34682
  return { ...result, error: `Storyboard not found in: ${outputDir} (expected storyboard.yaml or storyboard.json)` };
34674
34683
  }
@@ -34761,14 +34770,14 @@ IMPORTANT - Character appearance must match exactly: ${characterDesc}`;
34761
34770
  const refSceneNum = options.referenceScene;
34762
34771
  if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
34763
34772
  const refImagePath = resolve24(outputDir, `scene-${refSceneNum}.png`);
34764
- if (existsSync13(refImagePath)) {
34773
+ if (existsSync14(refImagePath)) {
34765
34774
  referenceImageBuffer = await readFile13(refImagePath);
34766
34775
  }
34767
34776
  } else if (!refSceneNum) {
34768
34777
  for (let i = 1; i <= segments.length; i++) {
34769
34778
  if (i !== sceneNum) {
34770
34779
  const otherImagePath = resolve24(outputDir, `scene-${i}.png`);
34771
- if (existsSync13(otherImagePath)) {
34780
+ if (existsSync14(otherImagePath)) {
34772
34781
  referenceImageBuffer = await readFile13(otherImagePath);
34773
34782
  break;
34774
34783
  }
@@ -34857,7 +34866,7 @@ Generate the single-person scene image now.`;
34857
34866
  }
34858
34867
  if (regenerateVideo && videoApiKey) {
34859
34868
  options.onProgress?.(`Scene ${sceneNum}: regenerating video (${options.generator || "grok"})...`);
34860
- if (!existsSync13(imagePath)) {
34869
+ if (!existsSync14(imagePath)) {
34861
34870
  result.failedScenes.push(sceneNum);
34862
34871
  continue;
34863
34872
  }
@@ -35090,7 +35099,7 @@ function registerVideoCommand(parent) {
35090
35099
  "-d, --duration <sec>",
35091
35100
  "Duration in seconds. Seedance accepts 4-15 (`fal` alias supported); Kling accepts 5 or 10; Veo maps to 6 or 8.",
35092
35101
  "5"
35093
- ).option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)").option("-s, --seed <number>", "Random seed for reproducibility (Runway only)").option("-m, --mode <mode>", "Generation mode: std or pro (Kling only)", "std").option("--seedance-model <model>", "Seedance variant: quality or fast (fal.ai only)", "quality").option("-n, --negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)").option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)").option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)").option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)").option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast").option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5").option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
35102
+ ).option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)").option("--seed <number>", "Random seed for reproducibility (Runway only)").option("--mode <mode>", "Generation mode: std or pro (Kling only)", "std").option("--seedance-model <model>", "Seedance variant: quality or fast (fal.ai only)", "quality").option("--negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)").option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)").option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)").option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)").option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast").option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5").option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
35094
35103
  Examples:
35095
35104
  $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
35096
35105
  $ vibe gen vid "cinematic city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
@@ -35119,6 +35128,15 @@ Examples:
35119
35128
  if (options.output) {
35120
35129
  validateOutputPath(options.output);
35121
35130
  }
35131
+ if (options.duration !== void 0) {
35132
+ const d = parseFloat(options.duration);
35133
+ if (!Number.isFinite(d) || d <= 0 || d > 60) {
35134
+ exitWithError(usageError(
35135
+ `Invalid --duration: ${options.duration}`,
35136
+ "Must be a positive number \u2264 60 seconds."
35137
+ ));
35138
+ }
35139
+ }
35122
35140
  const validProviders = ["runway", "kling", "veo", "grok", "seedance", "fal"];
35123
35141
  const videoEnvMap = {
35124
35142
  grok: "XAI_API_KEY",
@@ -35631,7 +35649,7 @@ Run 'vibe schema generate.<command>' for structured parameter info.
35631
35649
  });
35632
35650
 
35633
35651
  // src/commands/_shared/edit/silence-cut.ts
35634
- import { existsSync as existsSync14 } from "node:fs";
35652
+ import { existsSync as existsSync15 } from "node:fs";
35635
35653
  import { readFile as readFile15 } from "node:fs/promises";
35636
35654
  async function detectSilencePeriods(videoPath, noiseThreshold, minDuration) {
35637
35655
  const totalDuration = await getVideoDuration(videoPath);
@@ -35763,7 +35781,7 @@ async function executeSilenceCut(options) {
35763
35781
  analyzeOnly = false,
35764
35782
  useGemini = false
35765
35783
  } = options;
35766
- if (!existsSync14(videoPath)) {
35784
+ if (!existsSync15(videoPath)) {
35767
35785
  return { success: false, error: `Video not found: ${videoPath}` };
35768
35786
  }
35769
35787
  if (!commandExists("ffmpeg")) {
@@ -35871,7 +35889,7 @@ var init_silence_cut = __esm({
35871
35889
  });
35872
35890
 
35873
35891
  // src/commands/_shared/edit/jump-cut.ts
35874
- import { existsSync as existsSync15 } from "node:fs";
35892
+ import { existsSync as existsSync16 } from "node:fs";
35875
35893
  import { readFile as readFile16, writeFile as writeFile25, mkdir as mkdir11 } from "node:fs/promises";
35876
35894
  import { join as join8 } from "node:path";
35877
35895
  async function transcribeWithWords(audioPath, apiKey, language) {
@@ -35934,7 +35952,7 @@ async function executeJumpCut(options) {
35934
35952
  analyzeOnly = false,
35935
35953
  apiKey
35936
35954
  } = options;
35937
- if (!existsSync15(videoPath)) {
35955
+ if (!existsSync16(videoPath)) {
35938
35956
  return { success: false, error: `Video not found: ${videoPath}` };
35939
35957
  }
35940
35958
  if (!commandExists("ffmpeg")) {
@@ -36220,7 +36238,7 @@ var init_helpers = __esm({
36220
36238
  });
36221
36239
 
36222
36240
  // src/commands/_shared/edit/caption.ts
36223
- import { existsSync as existsSync16 } from "node:fs";
36241
+ import { existsSync as existsSync17 } from "node:fs";
36224
36242
  import { readFile as readFile17, writeFile as writeFile26, mkdir as mkdir12 } from "node:fs/promises";
36225
36243
  import { dirname as dirname8, basename as basename5, extname as extname4, join as join9 } from "node:path";
36226
36244
  function getCaptionForceStyle(style, fontSize, fontColor, position) {
@@ -36250,7 +36268,7 @@ async function executeCaption(options) {
36250
36268
  position = "bottom",
36251
36269
  apiKey
36252
36270
  } = options;
36253
- if (!existsSync16(videoPath)) {
36271
+ if (!existsSync17(videoPath)) {
36254
36272
  return { success: false, error: `Video not found: ${videoPath}` };
36255
36273
  }
36256
36274
  if (!commandExists("ffmpeg")) {
@@ -36428,10 +36446,10 @@ var init_caption = __esm({
36428
36446
  });
36429
36447
 
36430
36448
  // src/commands/_shared/edit/noise-reduce.ts
36431
- import { existsSync as existsSync17 } from "node:fs";
36449
+ import { existsSync as existsSync18 } from "node:fs";
36432
36450
  async function executeNoiseReduce(options) {
36433
36451
  const { inputPath, outputPath, strength = "medium", noiseFloor } = options;
36434
- if (!existsSync17(inputPath)) {
36452
+ if (!existsSync18(inputPath)) {
36435
36453
  return { success: false, error: `File not found: ${inputPath}` };
36436
36454
  }
36437
36455
  if (!commandExists("ffmpeg")) {
@@ -36488,7 +36506,7 @@ var init_noise_reduce = __esm({
36488
36506
  });
36489
36507
 
36490
36508
  // src/commands/_shared/edit/fade.ts
36491
- import { existsSync as existsSync18 } from "node:fs";
36509
+ import { existsSync as existsSync19 } from "node:fs";
36492
36510
  async function executeFade(options) {
36493
36511
  const {
36494
36512
  videoPath,
@@ -36498,7 +36516,7 @@ async function executeFade(options) {
36498
36516
  audioOnly = false,
36499
36517
  videoOnly = false
36500
36518
  } = options;
36501
- if (!existsSync18(videoPath)) {
36519
+ if (!existsSync19(videoPath)) {
36502
36520
  return { success: false, error: `Video not found: ${videoPath}` };
36503
36521
  }
36504
36522
  if (!commandExists("ffmpeg")) {
@@ -36565,7 +36583,7 @@ var init_fade = __esm({
36565
36583
  });
36566
36584
 
36567
36585
  // src/commands/_shared/edit/translate-srt.ts
36568
- import { existsSync as existsSync19 } from "node:fs";
36586
+ import { existsSync as existsSync20 } from "node:fs";
36569
36587
  import { readFile as readFile18, writeFile as writeFile27 } from "node:fs/promises";
36570
36588
  async function executeTranslateSrt(options) {
36571
36589
  const {
@@ -36576,7 +36594,7 @@ async function executeTranslateSrt(options) {
36576
36594
  sourceLanguage,
36577
36595
  apiKey
36578
36596
  } = options;
36579
- if (!existsSync19(srtPath)) {
36597
+ if (!existsSync20(srtPath)) {
36580
36598
  return { success: false, error: `SRT file not found: ${srtPath}` };
36581
36599
  }
36582
36600
  try {
@@ -36693,7 +36711,7 @@ var init_translate_srt = __esm({
36693
36711
  });
36694
36712
 
36695
36713
  // src/commands/_shared/edit/text-overlay.ts
36696
- import { existsSync as existsSync20 } from "node:fs";
36714
+ import { existsSync as existsSync21 } from "node:fs";
36697
36715
  import { resolve as resolve26 } from "node:path";
36698
36716
  function detectSystemFont() {
36699
36717
  const platform = process.platform;
@@ -36704,7 +36722,7 @@ function detectSystemFont() {
36704
36722
  "/Library/Fonts/Arial.ttf"
36705
36723
  ];
36706
36724
  for (const f of candidates) {
36707
- if (existsSync20(f)) return f;
36725
+ if (existsSync21(f)) return f;
36708
36726
  }
36709
36727
  } else if (platform === "linux") {
36710
36728
  const candidates = [
@@ -36713,7 +36731,7 @@ function detectSystemFont() {
36713
36731
  "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
36714
36732
  ];
36715
36733
  for (const f of candidates) {
36716
- if (existsSync20(f)) return f;
36734
+ if (existsSync21(f)) return f;
36717
36735
  }
36718
36736
  } else if (platform === "win32") {
36719
36737
  const candidates = [
@@ -36721,7 +36739,7 @@ function detectSystemFont() {
36721
36739
  "C:\\Windows\\Fonts\\segoeui.ttf"
36722
36740
  ];
36723
36741
  for (const f of candidates) {
36724
- if (existsSync20(f)) return f;
36742
+ if (existsSync21(f)) return f;
36725
36743
  }
36726
36744
  }
36727
36745
  return null;
@@ -36742,7 +36760,7 @@ async function applyTextOverlays(options) {
36742
36760
  }
36743
36761
  const absVideoPath = resolve26(process.cwd(), videoPath);
36744
36762
  const absOutputPath = resolve26(process.cwd(), outputPath);
36745
- if (!existsSync20(absVideoPath)) {
36763
+ if (!existsSync21(absVideoPath)) {
36746
36764
  return { success: false, error: `Video not found: ${absVideoPath}` };
36747
36765
  }
36748
36766
  if (!commandExists("ffmpeg")) {
@@ -36923,12 +36941,12 @@ var init_ai_edit = __esm({
36923
36941
 
36924
36942
  // src/commands/ai-edit-cli.ts
36925
36943
  import { resolve as resolve27, extname as extname5, basename as basename6 } from "node:path";
36926
- import { existsSync as existsSync21 } from "node:fs";
36944
+ import { existsSync as existsSync22 } from "node:fs";
36927
36945
  function registerEditCommands(aiCommand) {
36928
- aiCommand.command("silence-cut").alias("sc").description("Remove silent segments from video (FFmpeg default, or Gemini for smart detection)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-cut.<ext>)").option("-n, --noise <dB>", "Silence threshold in dB (default: -30)", "-30").option("-d, --min-duration <seconds>", "Minimum silence duration to cut (default: 0.5)", "0.5").option("--padding <seconds>", "Padding around non-silent segments (default: 0.1)", "0.1").option("--analyze-only", "Only detect silence, don't cut").option("--use-gemini", "Use Gemini Video Understanding for context-aware silence detection").option("-m, --model <model>", "Gemini model (default: flash)").option("--low-res", "Low resolution mode for longer videos (Gemini only)").option("-k, --api-key <key>", "Google API key override (or set GOOGLE_API_KEY env)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
36946
+ aiCommand.command("silence-cut").alias("sc").description("Remove silent segments from video (FFmpeg default, or Gemini for smart detection)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-cut.<ext>)").option("--noise <dB>", "Silence threshold in dB (default: -30)", "-30").option("-d, --min-duration <seconds>", "Minimum silence duration to cut (default: 0.5)", "0.5").option("--padding <seconds>", "Padding around non-silent segments (default: 0.1)", "0.1").option("--analyze-only", "Only detect silence, don't cut").option("--use-gemini", "Use Gemini Video Understanding for context-aware silence detection").option("-m, --model <model>", "Gemini model (default: flash)").option("--low-res", "Low resolution mode for longer videos (Gemini only)").option("-k, --api-key <key>", "Google API key override (or set GOOGLE_API_KEY env)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
36929
36947
  Examples:
36930
36948
  $ vibe edit silence-cut interview.mp4 -o clean.mp4
36931
- $ vibe ed sc podcast.mp4 -o trimmed.mp4 -n -25 -d 1.0
36949
+ $ vibe ed sc podcast.mp4 -o trimmed.mp4 --noise -25 --min-duration 1.0
36932
36950
  $ vibe ed sc video.mp4 --use-gemini -o smart-cut.mp4 # AI-powered detection
36933
36951
  $ vibe ed sc video.mp4 --analyze-only # Detect only, no cut
36934
36952
  $ vibe ed sc video.mp4 --dry-run --json
@@ -36940,7 +36958,7 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
36940
36958
  validateOutputPath(options.output);
36941
36959
  }
36942
36960
  const absVideoPath = resolve27(process.cwd(), videoPath);
36943
- if (!existsSync21(absVideoPath)) {
36961
+ if (!existsSync22(absVideoPath)) {
36944
36962
  exitWithError(notFoundError(absVideoPath));
36945
36963
  }
36946
36964
  if (!commandExists("ffmpeg")) {
@@ -37026,12 +37044,12 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37026
37044
  exitWithError(apiError(`Silence cut failed: ${error instanceof Error ? error.message : String(error)}`, true));
37027
37045
  }
37028
37046
  });
37029
- aiCommand.command("caption").alias("cap").description("Transcribe and burn styled captions onto video (Whisper + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-captioned.<ext>)").option("-s, --style <style>", "Caption style: minimal, bold, outline, karaoke (default: bold)", "bold").option("--font-size <pixels>", "Override auto-calculated font size").option("--color <color>", "Font color (default: white)", "white").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--position <pos>", "Caption position: top, center, bottom (default: bottom)", "bottom").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
37047
+ aiCommand.command("caption").alias("cap").description("Transcribe and burn styled captions onto video (Whisper + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-captioned.<ext>)").option("--style <style>", "Caption style: minimal, bold, outline, karaoke (default: bold)", "bold").option("--font-size <pixels>", "Override auto-calculated font size").option("--color <color>", "Font color (default: white)", "white").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--position <pos>", "Caption position: top, center, bottom (default: bottom)", "bottom").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
37030
37048
  Examples:
37031
37049
  $ vibe edit caption video.mp4 -o captioned.mp4
37032
- $ vibe ed cap video.mp4 -o out.mp4 -s bold --position top
37050
+ $ vibe ed cap video.mp4 -o out.mp4 --style bold --position top
37033
37051
  $ vibe ed cap video.mp4 -o out.mp4 -l ko # Korean transcription
37034
- $ vibe ed cap video.mp4 -o out.mp4 -s karaoke # Karaoke-style
37052
+ $ vibe ed cap video.mp4 -o out.mp4 --style karaoke # Karaoke-style
37035
37053
  $ vibe ed cap video.mp4 --dry-run --json
37036
37054
 
37037
37055
  Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoPath, options) => {
@@ -37041,7 +37059,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37041
37059
  validateOutputPath(options.output);
37042
37060
  }
37043
37061
  const absVideoPath = resolve27(process.cwd(), videoPath);
37044
- if (!existsSync21(absVideoPath)) {
37062
+ if (!existsSync22(absVideoPath)) {
37045
37063
  exitWithError(notFoundError(absVideoPath));
37046
37064
  }
37047
37065
  if (!commandExists("ffmpeg")) {
@@ -37115,14 +37133,14 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37115
37133
  exitWithError(apiError(`Caption failed: ${error instanceof Error ? error.message : String(error)}`, true));
37116
37134
  }
37117
37135
  });
37118
- aiCommand.command("noise-reduce").description("Remove background noise from audio/video using FFmpeg (no API key needed)").argument("<input>", "Audio or video file path").option("-o, --output <path>", "Output file path (default: <name>-denoised.<ext>)").option("-s, --strength <level>", "Noise reduction strength: low, medium, high (default: medium)", "medium").option("-n, --noise-floor <dB>", "Custom noise floor in dB (overrides strength preset)").option("--dry-run", "Preview parameters without executing").action(async (inputPath, options) => {
37136
+ aiCommand.command("noise-reduce").description("Remove background noise from audio/video using FFmpeg (no API key needed)").argument("<input>", "Audio or video file path").option("-o, --output <path>", "Output file path (default: <name>-denoised.<ext>)").option("--strength <level>", "Noise reduction strength: low, medium, high (default: medium)", "medium").option("--noise-floor <dB>", "Custom noise floor in dB (overrides strength preset)").option("--dry-run", "Preview parameters without executing").action(async (inputPath, options) => {
37119
37137
  const startedAt = Date.now();
37120
37138
  try {
37121
37139
  if (options.output) {
37122
37140
  validateOutputPath(options.output);
37123
37141
  }
37124
37142
  const absInputPath = resolve27(process.cwd(), inputPath);
37125
- if (!existsSync21(absInputPath)) {
37143
+ if (!existsSync22(absInputPath)) {
37126
37144
  exitWithError(notFoundError(absInputPath));
37127
37145
  }
37128
37146
  if (!commandExists("ffmpeg")) {
@@ -37188,7 +37206,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37188
37206
  validateOutputPath(options.output);
37189
37207
  }
37190
37208
  const absVideoPath = resolve27(process.cwd(), videoPath);
37191
- if (!existsSync21(absVideoPath)) {
37209
+ if (!existsSync22(absVideoPath)) {
37192
37210
  exitWithError(notFoundError(absVideoPath));
37193
37211
  }
37194
37212
  if (!commandExists("ffmpeg")) {
@@ -37253,7 +37271,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37253
37271
  exitWithError(generalError(`Fade failed: ${error instanceof Error ? error.message : String(error)}`));
37254
37272
  }
37255
37273
  });
37256
- aiCommand.command("translate-srt").description("Translate SRT subtitle file to another language (Claude/OpenAI)").argument("<srt>", "SRT file path").option("-t, --target <language>", "Target language (e.g., ko, es, fr, ja, zh)").option("-o, --output <path>", "Output file path (default: <name>-<target>.srt)").option("-p, --provider <provider>", "Translation provider: claude, openai (default: claude)", "claude").option("--source <language>", "Source language (auto-detected if omitted)").option("-k, --api-key <key>", "API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (srtPath, options) => {
37274
+ aiCommand.command("translate-srt").description("Translate SRT subtitle file to another language (Claude or OpenAI)").argument("<srt>", "SRT file path").option("--target <language>", "Target language (e.g., ko, es, fr, ja, zh)").option("-o, --output <path>", "Output file path (default: <name>-<target>.srt)").option("-p, --provider <provider>", "Translation provider: claude, openai (default: claude)", "claude").option("--source <language>", "Source language (auto-detected if omitted)").option("-k, --api-key <key>", "API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (srtPath, options) => {
37257
37275
  const startedAt = Date.now();
37258
37276
  try {
37259
37277
  if (options.output) {
@@ -37263,7 +37281,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37263
37281
  exitWithError(usageError("Target language required. Use -t or --target"));
37264
37282
  }
37265
37283
  const absSrtPath = resolve27(process.cwd(), srtPath);
37266
- if (!existsSync21(absSrtPath)) {
37284
+ if (!existsSync22(absSrtPath)) {
37267
37285
  exitWithError(notFoundError(absSrtPath));
37268
37286
  }
37269
37287
  if (options.dryRun) {
@@ -37339,7 +37357,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37339
37357
  }
37340
37358
  if (options.fillers) rejectControlChars(options.fillers);
37341
37359
  const absVideoPath = resolve27(process.cwd(), videoPath);
37342
- if (!existsSync21(absVideoPath)) {
37360
+ if (!existsSync22(absVideoPath)) {
37343
37361
  exitWithError(notFoundError(absVideoPath));
37344
37362
  }
37345
37363
  if (!commandExists("ffmpeg")) {
@@ -37439,7 +37457,7 @@ var init_ai_edit_cli = __esm({
37439
37457
  });
37440
37458
 
37441
37459
  // src/commands/_shared/execute-fill-gaps.ts
37442
- import { existsSync as existsSync22 } from "node:fs";
37460
+ import { existsSync as existsSync23 } from "node:fs";
37443
37461
  import { readFile as readFile19, writeFile as writeFile28, mkdir as mkdir13, rename as renameFs } from "node:fs/promises";
37444
37462
  import { resolve as resolve28, dirname as dirname9 } from "node:path";
37445
37463
  function detectVideoGaps(videoClips, totalDuration) {
@@ -37522,7 +37540,7 @@ async function executeFillGaps(options) {
37522
37540
  try {
37523
37541
  onProgress("Loading project...");
37524
37542
  const filePath = resolve28(process.cwd(), options.projectPath);
37525
- if (!existsSync22(filePath)) {
37543
+ if (!existsSync23(filePath)) {
37526
37544
  return {
37527
37545
  success: false,
37528
37546
  error: `Project file not found: ${filePath}`,
@@ -37657,7 +37675,7 @@ async function executeFillGaps(options) {
37657
37675
  }
37658
37676
  const projectDir = dirname9(filePath);
37659
37677
  const footageDir = options.dir ? resolve28(process.cwd(), options.dir) : resolve28(projectDir, "footage");
37660
- if (!existsSync22(footageDir)) {
37678
+ if (!existsSync23(footageDir)) {
37661
37679
  await mkdir13(footageDir, { recursive: true });
37662
37680
  }
37663
37681
  humanLines.push("Generating AI Videos");
@@ -37919,7 +37937,7 @@ var init_execute_fill_gaps = __esm({
37919
37937
 
37920
37938
  // src/commands/ai-fill-gaps.ts
37921
37939
  function registerFillGapsCommand(aiCommand) {
37922
- aiCommand.command("fill-gaps").description("Fill timeline gaps with AI-generated video (Kling image-to-video)").argument("<project>", "Project file path").option("-p, --provider <provider>", "AI provider (kling)", "kling").option("-o, --output <path>", "Output project path (default: overwrite)").option("-d, --dir <path>", "Directory to save generated videos").option("--prompt <text>", "Custom prompt for video generation").option("--dry-run", "Show gaps without generating").option("-m, --mode <mode>", "Generation mode: std or pro (Kling)", "std").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1", "16:9").action(async (projectPath, options) => {
37940
+ aiCommand.command("fill-gaps").description("Fill timeline gaps with AI-generated video (Kling image-to-video)").argument("<project>", "Project file path").option("-p, --provider <provider>", "AI provider (kling)", "kling").option("-o, --output <path>", "Output project path (default: overwrite)").option("-d, --dir <path>", "Directory to save generated videos").option("--prompt <text>", "Custom prompt for video generation").option("--dry-run", "Show gaps without generating").option("--mode <mode>", "Generation mode: std or pro (Kling)", "std").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1", "16:9").action(async (projectPath, options) => {
37923
37941
  try {
37924
37942
  if (options.output) {
37925
37943
  validateOutputPath(options.output);
@@ -38178,12 +38196,12 @@ var init_edit_cmd = __esm({
38178
38196
  `
38179
38197
  Examples:
38180
38198
  $ vibe edit silence-cut interview.mp4 -o clean.mp4
38181
- $ vibe edit caption video.mp4 -o captioned.mp4 -s bold
38199
+ $ vibe edit caption video.mp4 -o captioned.mp4 --style bold
38182
38200
  $ vibe edit grade video.mp4 -o graded.mp4 --preset cinematic-warm
38183
38201
  $ vibe edit reframe landscape.mp4 -o vertical.mp4 -a 9:16
38184
38202
  $ vibe edit image photo.png "add sunset background" -o edited.png
38185
- $ vibe edit text-overlay video.mp4 -t "Title" -s center-bold -o out.mp4
38186
- $ vibe edit noise-reduce noisy.mp4 -o clean.mp4 -s high
38203
+ $ vibe edit text-overlay video.mp4 --text "Title" --style center-bold -o out.mp4
38204
+ $ vibe edit noise-reduce noisy.mp4 -o clean.mp4 --strength high
38187
38205
  $ vibe edit fade video.mp4 -o faded.mp4 --fade-in 1 --fade-out 1
38188
38206
 
38189
38207
  API Keys (varies by subcommand):
@@ -38197,7 +38215,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38197
38215
  );
38198
38216
  registerEditCommands(editCommand);
38199
38217
  registerFillGapsCommand(editCommand);
38200
- editCommand.command("grade").description("Apply AI-generated color grading (Claude + FFmpeg)").argument("<video>", "Video file path").option("-s, --style <prompt>", "Style description (e.g., 'cinematic warm')").option("--preset <name>", "Built-in preset: film-noir, vintage, cinematic-warm, cool-tones, high-contrast, pastel, cyberpunk, horror").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show filter without applying").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38218
+ editCommand.command("grade").description("Apply AI-generated color grading (Claude + FFmpeg)").argument("<video>", "Video file path").option("--style <prompt>", "Style description (e.g., 'cinematic warm')").option("--preset <name>", "Built-in preset: film-noir, vintage, cinematic-warm, cool-tones, high-contrast, pastel, cyberpunk, horror").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show filter without applying").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38201
38219
  const startedAt = Date.now();
38202
38220
  try {
38203
38221
  if (options.style) rejectControlChars(options.style);
@@ -38285,7 +38303,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38285
38303
  exitWithError(apiError(`Color grading failed: ${error.message}`));
38286
38304
  }
38287
38305
  });
38288
- editCommand.command("text-overlay").description("Apply text overlays to video (FFmpeg drawtext)").argument("<video>", "Video file path").option("-t, --text <texts...>", "Text lines to overlay (repeat for multiple)").option("-s, --style <style>", "Overlay style: lower-third, center-bold, subtitle, minimal", "lower-third").option("--font-size <size>", "Font size in pixels (auto-calculated if omitted)").option("--font-color <color>", "Font color (default: white)", "white").option("--fade <seconds>", "Fade in/out duration in seconds", "0.3").option("--start <seconds>", "Start time in seconds", "0").option("--end <seconds>", "End time in seconds (default: video duration)").option("-o, --output <path>", "Output video file path").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38306
+ editCommand.command("text-overlay").description("Apply text overlays to video (FFmpeg drawtext)").argument("<video>", "Video file path").option("--text <texts...>", "Text lines to overlay (repeat for multiple)").option("--style <style>", "Overlay style: lower-third, center-bold, subtitle, minimal", "lower-third").option("--font-size <size>", "Font size in pixels (auto-calculated if omitted)").option("--font-color <color>", "Font color (default: white)", "white").option("--fade <seconds>", "Fade in/out duration in seconds", "0.3").option("--start <seconds>", "Start time in seconds", "0").option("--end <seconds>", "End time in seconds (default: video duration)").option("-o, --output <path>", "Output video file path").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38289
38307
  const startedAt = Date.now();
38290
38308
  try {
38291
38309
  if (!options.text || options.text.length === 0) {
@@ -38361,7 +38379,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38361
38379
  exitWithError(generalError(`Text overlay failed: ${msg}`));
38362
38380
  }
38363
38381
  });
38364
- editCommand.command("speed-ramp").description("Apply content-aware speed ramping (Whisper + Claude + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output video file path").option("-s, --style <style>", "Style: dramatic, smooth, action", "dramatic").option("--min-speed <factor>", "Minimum speed factor", "0.25").option("--max-speed <factor>", "Maximum speed factor", "4.0").option("--analyze-only", "Show keyframes without applying").option("-l, --language <lang>", "Language code for transcription").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38382
+ editCommand.command("speed-ramp").description("Apply content-aware speed ramping (Whisper + Claude + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output video file path").option("--style <style>", "Style: dramatic, smooth, action", "dramatic").option("--min-speed <factor>", "Minimum speed factor", "0.25").option("--max-speed <factor>", "Maximum speed factor", "4.0").option("--analyze-only", "Show keyframes without applying").option("-l, --language <lang>", "Language code for transcription").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38365
38383
  const startedAt = Date.now();
38366
38384
  try {
38367
38385
  if (options.output) {
@@ -38481,7 +38499,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38481
38499
  exitWithError(generalError(`Speed ramping failed: ${msg}`));
38482
38500
  }
38483
38501
  });
38484
- editCommand.command("reframe").description("Auto-reframe video to different aspect ratio (Claude Vision + FFmpeg)").argument("<video>", "Video file path").option("-a, --aspect <ratio>", "Target aspect ratio: 9:16, 1:1, 4:5", "9:16").option("-f, --focus <mode>", "Focus mode: auto, face, center, action", "auto").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show crop regions without applying").option("--keyframes <path>", "Export keyframes to JSON file").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38502
+ editCommand.command("reframe").description("Auto-reframe video to different aspect ratio (Claude Vision + FFmpeg)").argument("<video>", "Video file path").option("-a, --aspect <ratio>", "Target aspect ratio: 9:16, 1:1, 4:5", "9:16").option("--focus <mode>", "Focus mode: auto, face, center, action", "auto").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show crop regions without applying").option("--keyframes <path>", "Export keyframes to JSON file").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38485
38503
  const startedAt = Date.now();
38486
38504
  try {
38487
38505
  if (options.output) {
@@ -38628,7 +38646,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38628
38646
  exitWithError(generalError(`Reframe failed: ${msg}`));
38629
38647
  }
38630
38648
  });
38631
- editCommand.command("image").description("Edit image(s) using AI (Gemini/OpenAI/Grok)").argument("<images...>", "Input image file(s) followed by edit prompt").option("-p, --provider <provider>", "Provider: gemini (default), openai, grok", "gemini").option("-k, --api-key <key>", "API key (or set env variable)").option("-o, --output <path>", "Output file path", "edited.png").option("-m, --model <model>", "Model: flash/3.1-flash/latest/pro (Gemini only)", "flash").option("-r, --ratio <ratio>", "Output aspect ratio").option("-s, --size <resolution>", "Resolution: 1K, 2K, 4K (Gemini Pro only)").option("--dry-run", "Preview parameters without executing").action(async (args, options) => {
38649
+ editCommand.command("image").description("Edit image(s) using AI (Gemini, OpenAI, or Grok)").argument("<images...>", "Input image file(s) followed by edit prompt").option("-p, --provider <provider>", "Provider: gemini (default), openai, grok", "gemini").option("-k, --api-key <key>", "API key (or set env variable)").option("-o, --output <path>", "Output file path", "edited.png").option("-m, --model <model>", "Model: flash/3.1-flash/latest/pro (Gemini only)", "flash").option("-r, --ratio <ratio>", "Output aspect ratio").option("--size <resolution>", "Resolution: 1K, 2K, 4K (Gemini Pro only)").option("--dry-run", "Preview parameters without executing").action(async (args, options) => {
38632
38650
  const startedAt = Date.now();
38633
38651
  try {
38634
38652
  if (args.length < 2) {
@@ -38757,7 +38775,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38757
38775
  exitWithError(apiError(`Image editing failed: ${msg}`, true));
38758
38776
  }
38759
38777
  });
38760
- editCommand.command("interpolate").description("Create slow motion with frame interpolation (FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-f, --factor <number>", "Slow motion factor: 2, 4, or 8", "2").option("--fps <number>", "Target output FPS").option("--quality <mode>", "Quality: fast or quality", "quality").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38778
+ editCommand.command("interpolate").description("Create slow motion with frame interpolation (FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("--factor <number>", "Slow motion factor: 2, 4, or 8", "2").option("--fps <number>", "Target output FPS").option("--mode <mode>", "Speed/quality tradeoff: fast or quality", "quality").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38761
38779
  const startedAt = Date.now();
38762
38780
  try {
38763
38781
  if (options.output) {
@@ -38778,7 +38796,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38778
38796
  videoPath: absPath,
38779
38797
  factor,
38780
38798
  fps: options.fps ? parseInt(options.fps) : void 0,
38781
- quality: options.quality
38799
+ mode: options.mode
38782
38800
  }
38783
38801
  }
38784
38802
  });
@@ -38801,7 +38819,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38801
38819
  const [num, den] = fpsOut.trim().split("/").map(Number);
38802
38820
  const originalFps = num / (den || 1);
38803
38821
  const targetFps = options.fps ? parseInt(options.fps) : originalFps * factor;
38804
- const mi = options.quality === "fast" ? "mi_mode=mci" : "mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1";
38822
+ const mi = options.mode === "fast" ? "mi_mode=mci" : "mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1";
38805
38823
  spinner2.text = `Interpolating frames (${originalFps.toFixed(1)} \u2192 ${targetFps}fps)...`;
38806
38824
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
38807
38825
  spinner2.succeed(source_default.green(`Created ${factor}x slow motion`));
@@ -38839,7 +38857,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38839
38857
  exitWithError(generalError(`Frame interpolation failed: ${msg}`));
38840
38858
  }
38841
38859
  });
38842
- editCommand.command("upscale").description("Upscale video resolution using AI or FFmpeg").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-s, --scale <factor>", "Scale factor: 2 or 4", "2").option("-m, --model <model>", "Model: real-esrgan, topaz", "real-esrgan").option("--ffmpeg", "Use FFmpeg lanczos (free, no API)").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").option("--no-wait", "Start processing and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38860
+ editCommand.command("upscale").description("Upscale video resolution using AI or FFmpeg").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("--scale <factor>", "Scale factor: 2 or 4", "2").option("-m, --model <model>", "Model: real-esrgan, topaz", "real-esrgan").option("--ffmpeg", "Use FFmpeg lanczos (free, no API)").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").option("--no-wait", "Start processing and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38843
38861
  const startedAt = Date.now();
38844
38862
  try {
38845
38863
  if (options.output) {
@@ -38930,7 +38948,7 @@ __export(ai_analyze_exports, {
38930
38948
  });
38931
38949
  import { readFile as readFile21 } from "node:fs/promises";
38932
38950
  import { extname as extname6, resolve as resolve30 } from "node:path";
38933
- import { existsSync as existsSync23 } from "node:fs";
38951
+ import { existsSync as existsSync24 } from "node:fs";
38934
38952
  async function executeGeminiVideo(options) {
38935
38953
  try {
38936
38954
  const apiKey = await getApiKey("GOOGLE_API_KEY", "Google");
@@ -38949,7 +38967,7 @@ async function executeGeminiVideo(options) {
38949
38967
  videoData = options.source;
38950
38968
  } else {
38951
38969
  const absPath = resolve30(process.cwd(), options.source);
38952
- if (!existsSync23(absPath)) {
38970
+ if (!existsSync24(absPath)) {
38953
38971
  return { success: false, error: `File not found: ${absPath}` };
38954
38972
  }
38955
38973
  videoData = await readFile21(absPath);
@@ -39022,7 +39040,7 @@ async function executeAnalyze(options) {
39022
39040
  imageBuffer = Buffer.from(await response.arrayBuffer());
39023
39041
  } else {
39024
39042
  const absPath = resolve30(process.cwd(), source);
39025
- if (!existsSync23(absPath)) {
39043
+ if (!existsSync24(absPath)) {
39026
39044
  return { success: false, error: `File not found: ${absPath}` };
39027
39045
  }
39028
39046
  imageBuffer = await readFile21(absPath);
@@ -39057,7 +39075,7 @@ async function executeAnalyze(options) {
39057
39075
  videoData = Buffer.from(await response.arrayBuffer());
39058
39076
  } else {
39059
39077
  const absPath = resolve30(process.cwd(), source);
39060
- if (!existsSync23(absPath)) {
39078
+ if (!existsSync24(absPath)) {
39061
39079
  return { success: false, error: `File not found: ${absPath}` };
39062
39080
  }
39063
39081
  videoData = await readFile21(absPath);
@@ -39196,7 +39214,7 @@ __export(ai_review_exports, {
39196
39214
  });
39197
39215
  import { readFile as readFile22, rename as rename3 } from "node:fs/promises";
39198
39216
  import { resolve as resolve31 } from "node:path";
39199
- import { existsSync as existsSync24 } from "node:fs";
39217
+ import { existsSync as existsSync25 } from "node:fs";
39200
39218
  function parseReviewFeedback(response) {
39201
39219
  let cleaned = response.trim();
39202
39220
  if (cleaned.startsWith("```json")) {
@@ -39221,7 +39239,7 @@ function parseReviewFeedback(response) {
39221
39239
  async function executeReview(options) {
39222
39240
  const { videoPath, storyboardPath, autoApply = false, verify = false, model = "flash" } = options;
39223
39241
  const absVideoPath = resolve31(process.cwd(), videoPath);
39224
- if (!existsSync24(absVideoPath)) {
39242
+ if (!existsSync25(absVideoPath)) {
39225
39243
  return { success: false, error: `Video not found: ${absVideoPath}` };
39226
39244
  }
39227
39245
  const apiKey = process.env.GOOGLE_API_KEY || await getApiKey("GOOGLE_API_KEY", "Google");
@@ -39231,7 +39249,7 @@ async function executeReview(options) {
39231
39249
  let storyboardContext = "";
39232
39250
  if (storyboardPath) {
39233
39251
  const absStoryboardPath = resolve31(process.cwd(), storyboardPath);
39234
- if (existsSync24(absStoryboardPath)) {
39252
+ if (existsSync25(absStoryboardPath)) {
39235
39253
  const content = await readFile22(absStoryboardPath, "utf-8");
39236
39254
  storyboardContext = `
39237
39255
 
@@ -39332,7 +39350,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39332
39350
  return result;
39333
39351
  }
39334
39352
  function registerReviewCommand(aiCommand) {
39335
- aiCommand.command("review").description("Review video quality using Gemini AI and optionally auto-fix issues").argument("<video>", "Video file path").option("-s, --storyboard <path>", "Storyboard JSON file for context").option("--auto-apply", "Automatically apply fixable corrections").option("--verify", "Run verification pass after applying fixes").option("-m, --model <model>", "Gemini model: flash (default), flash-2.5, pro", "flash").option("-o, --output <path>", "Output video file path (for auto-apply)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39353
+ aiCommand.command("review").description("Review video quality using Gemini AI and optionally auto-fix issues").argument("<source>", "Video file path").option("--storyboard <path>", "Storyboard JSON file for context").option("--auto-apply", "Automatically apply fixable corrections").option("--verify", "Run verification pass after applying fixes").option("-m, --model <model>", "Gemini model: flash (default), flash-2.5, pro", "flash").option("-o, --output <path>", "Output video file path (for auto-apply)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39336
39354
  const startedAt = Date.now();
39337
39355
  try {
39338
39356
  if (options.output) {
@@ -39340,7 +39358,7 @@ function registerReviewCommand(aiCommand) {
39340
39358
  }
39341
39359
  if (options.dryRun) {
39342
39360
  outputSuccess({
39343
- command: "ai review",
39361
+ command: "inspect review",
39344
39362
  startedAt,
39345
39363
  dryRun: true,
39346
39364
  data: {
@@ -39437,7 +39455,7 @@ var init_ai_review = __esm({
39437
39455
  // src/commands/ai-highlights.ts
39438
39456
  import { readFile as readFile26, writeFile as writeFile33, mkdir as mkdir15 } from "node:fs/promises";
39439
39457
  import { resolve as resolve35, dirname as dirname12, basename as basename8, extname as extname8 } from "node:path";
39440
- import { existsSync as existsSync27 } from "node:fs";
39458
+ import { existsSync as existsSync28 } from "node:fs";
39441
39459
  function filterHighlights(highlights, options) {
39442
39460
  let filtered = highlights.filter((h) => h.confidence >= options.threshold);
39443
39461
  filtered.sort((a, b) => b.confidence - a.confidence);
@@ -39477,7 +39495,7 @@ function truncate(text, maxLength) {
39477
39495
  async function executeHighlights(options) {
39478
39496
  try {
39479
39497
  const absPath = resolve35(process.cwd(), options.media);
39480
- if (!existsSync27(absPath)) {
39498
+ if (!existsSync28(absPath)) {
39481
39499
  return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
39482
39500
  }
39483
39501
  const ext = extname8(absPath).toLowerCase();
@@ -39593,7 +39611,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
39593
39611
  const audioBuffer = await readFile26(audioPath);
39594
39612
  const audioBlob = new Blob([audioBuffer]);
39595
39613
  const transcriptResult = await whisper.transcribe(audioBlob, options.language);
39596
- if (tempAudioPath && existsSync27(tempAudioPath)) {
39614
+ if (tempAudioPath && existsSync28(tempAudioPath)) {
39597
39615
  const { unlink: unlinkFile } = await import("node:fs/promises");
39598
39616
  await unlinkFile(tempAudioPath).catch(() => {
39599
39617
  });
@@ -39680,7 +39698,7 @@ async function executeAutoShorts(options) {
39680
39698
  return { success: false, shorts: [], error: "FFmpeg not found" };
39681
39699
  }
39682
39700
  const absPath = resolve35(process.cwd(), options.video);
39683
- if (!existsSync27(absPath)) {
39701
+ if (!existsSync28(absPath)) {
39684
39702
  return { success: false, shorts: [], error: `File not found: ${absPath}` };
39685
39703
  }
39686
39704
  const targetDuration = options.duration ?? 60;
@@ -39808,7 +39826,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
39808
39826
  };
39809
39827
  }
39810
39828
  const outputDir = options.outputDir ? resolve35(process.cwd(), options.outputDir) : dirname12(absPath);
39811
- if (options.outputDir && !existsSync27(outputDir)) {
39829
+ if (options.outputDir && !existsSync28(outputDir)) {
39812
39830
  await mkdir15(outputDir, { recursive: true });
39813
39831
  }
39814
39832
  const result = {
@@ -39884,14 +39902,14 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
39884
39902
  }
39885
39903
  }
39886
39904
  function registerHighlightsCommands(aiCommand) {
39887
- aiCommand.command("highlights").description("Extract highlights from long-form video/audio content").argument("<media>", "Video or audio file path").option("-o, --output <path>", "Output JSON file with highlights").option("--project <path>", "Create project with highlight clips").option("-d, --duration <seconds>", "Target highlight reel duration", "60").option("-n, --count <number>", "Maximum number of highlights").option("-t, --threshold <value>", "Confidence threshold (0-1)", "0.7").option("--criteria <type>", "Selection criteria: emotional | informative | funny | all", "all").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
39905
+ aiCommand.command("highlights").description("Extract highlights from long-form video/audio content").argument("<media>", "Video or audio file path").option("-o, --output <path>", "Output JSON file with highlights").option("--project <path>", "Create project with highlight clips").option("-d, --duration <seconds>", "Target highlight reel duration", "60").option("--count <number>", "Maximum number of highlights").option("--threshold <value>", "Confidence threshold (0-1)", "0.7").option("--criteria <type>", "Selection criteria: emotional | informative | funny | all", "all").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
39888
39906
  const startedAt = Date.now();
39889
39907
  try {
39890
39908
  if (options.output) {
39891
39909
  validateOutputPath(options.output);
39892
39910
  }
39893
39911
  const absPath = resolve35(process.cwd(), mediaPath);
39894
- if (!existsSync27(absPath)) {
39912
+ if (!existsSync28(absPath)) {
39895
39913
  exitWithError(notFoundError(absPath));
39896
39914
  }
39897
39915
  if (options.dryRun) {
@@ -40078,7 +40096,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40078
40096
  const transcriptResult = await whisper.transcribe(audioBlob, options.language);
40079
40097
  if (transcriptResult.status === "failed" || !transcriptResult.segments) {
40080
40098
  transcribeSpinner.fail("Transcription failed");
40081
- if (tempAudioPath && existsSync27(tempAudioPath)) {
40099
+ if (tempAudioPath && existsSync28(tempAudioPath)) {
40082
40100
  const { unlink: unlinkFile } = await import("node:fs/promises");
40083
40101
  await unlinkFile(tempAudioPath).catch(() => {
40084
40102
  });
@@ -40086,7 +40104,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40086
40104
  exitWithError(apiError(`Transcription failed: ${transcriptResult.error}`, true));
40087
40105
  }
40088
40106
  transcribeSpinner.succeed(source_default.green(`Transcribed ${transcriptResult.segments.length} segments`));
40089
- if (tempAudioPath && existsSync27(tempAudioPath)) {
40107
+ if (tempAudioPath && existsSync28(tempAudioPath)) {
40090
40108
  const { unlink: unlinkFile } = await import("node:fs/promises");
40091
40109
  await unlinkFile(tempAudioPath).catch(() => {
40092
40110
  });
@@ -40185,14 +40203,14 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40185
40203
  exitWithError(apiError(`Highlight extraction failed: ${error instanceof Error ? error.message : String(error)}`, true));
40186
40204
  }
40187
40205
  });
40188
- aiCommand.command("auto-shorts").alias("shorts").description("Auto-generate shorts from long-form video").argument("<video>", "Video file path").option("-o, --output <path>", "Output file (single) or directory (multiple)").option("-d, --duration <seconds>", "Target duration in seconds (15-60)", "60").option("-n, --count <number>", "Number of shorts to generate", "1").option("-a, --aspect <ratio>", "Aspect ratio: 9:16, 1:1", "9:16").option("--output-dir <dir>", "Output directory for multiple shorts").option("--add-captions", "Add auto-generated captions").option("--caption-style <style>", "Caption style: minimal, bold, animated", "bold").option("--analyze-only", "Show segments without generating").option("-l, --language <lang>", "Language code for transcription").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
40206
+ aiCommand.command("auto-shorts").alias("shorts").description("Auto-generate shorts from long-form video").argument("<video>", "Video file path").option("-o, --output <path>", "Output file (single) or directory (multiple)").option("-d, --duration <seconds>", "Target duration in seconds (15-60)", "60").option("--count <number>", "Number of shorts to generate", "1").option("-a, --aspect <ratio>", "Aspect ratio: 9:16, 1:1", "9:16").option("--output-dir <dir>", "Output directory for multiple shorts").option("--add-captions", "Add auto-generated captions").option("--caption-style <style>", "Caption style: minimal, bold, animated", "bold").option("--analyze-only", "Show segments without generating").option("-l, --language <lang>", "Language code for transcription").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
40189
40207
  const startedAt = Date.now();
40190
40208
  try {
40191
40209
  if (options.output) {
40192
40210
  validateOutputPath(options.output);
40193
40211
  }
40194
40212
  const absPath = resolve35(process.cwd(), videoPath);
40195
- if (!existsSync27(absPath)) {
40213
+ if (!existsSync28(absPath)) {
40196
40214
  exitWithError(notFoundError(absPath));
40197
40215
  }
40198
40216
  if (options.dryRun) {
@@ -40385,7 +40403,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40385
40403
  return;
40386
40404
  }
40387
40405
  const outputDir = options.outputDir ? resolve35(process.cwd(), options.outputDir) : dirname12(absPath);
40388
- if (options.outputDir && !existsSync27(outputDir)) {
40406
+ if (options.outputDir && !existsSync28(outputDir)) {
40389
40407
  await mkdir15(outputDir, { recursive: true });
40390
40408
  }
40391
40409
  for (let i = 0; i < selectedHighlights.length; i++) {
@@ -40402,7 +40420,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40402
40420
  outputPath = resolve35(outputDir, `${baseName}-short-${i + 1}.mp4`);
40403
40421
  }
40404
40422
  const parentDir = dirname12(outputPath);
40405
- if (!existsSync27(parentDir)) {
40423
+ if (!existsSync28(parentDir)) {
40406
40424
  await mkdir15(parentDir, { recursive: true });
40407
40425
  }
40408
40426
  const { stdout: probeOut } = await execSafe("ffprobe", [
@@ -40477,7 +40495,7 @@ var init_ai_highlights = __esm({
40477
40495
  // src/commands/ai-animated-caption.ts
40478
40496
  import { resolve as resolve36, dirname as dirname13, basename as basename9 } from "node:path";
40479
40497
  import { writeFile as writeFile34, mkdir as mkdir16, rm as rm2 } from "node:fs/promises";
40480
- import { existsSync as existsSync28 } from "node:fs";
40498
+ import { existsSync as existsSync29 } from "node:fs";
40481
40499
  import { tmpdir as tmpdir2 } from "node:os";
40482
40500
  function groupWords(words, options = {}) {
40483
40501
  if (words.length === 0) return [];
@@ -40654,7 +40672,7 @@ async function executeAnimatedCaption(options) {
40654
40672
  const groups = groupWords(transcript.words, { wordsPerGroup, maxChars });
40655
40673
  const absOutputPath = resolve36(process.cwd(), outputPath);
40656
40674
  const outDir = dirname13(absOutputPath);
40657
- if (!existsSync28(outDir)) {
40675
+ if (!existsSync29(outDir)) {
40658
40676
  await mkdir16(outDir, { recursive: true });
40659
40677
  }
40660
40678
  if (tier === "ass") {
@@ -50304,15 +50322,14 @@ var init_detect = __esm({
50304
50322
  "use strict";
50305
50323
  init_esm();
50306
50324
  init_source();
50307
- init_ora();
50308
50325
  init_engine();
50309
50326
  init_exec_safe();
50310
50327
  init_output();
50311
50328
  init_validate();
50312
50329
  detectCommand = new Command("detect").description("Auto-detect scenes, beats, and silences in media");
50313
- detectCommand.command("scenes").description("Detect scene changes in video").argument("<video>", "Video file path").option("-t, --threshold <value>", "Scene change threshold (0-1)", "0.3").option("-o, --output <path>", "Output JSON file with timestamps").option("-p, --project <path>", "Add scenes as clips to project").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
50330
+ detectCommand.command("scenes").description("Detect scene changes in video").argument("<video>", "Video file path").option("--threshold <value>", "Scene change threshold (0-1)", "0.3").option("-o, --output <path>", "Output JSON file with timestamps").option("--project <path>", "Add scenes as clips to project").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
50314
50331
  const startedAt = Date.now();
50315
- const spinner2 = ora("Detecting scenes...").start();
50332
+ const spinner2 = spinner("Detecting scenes...").start();
50316
50333
  try {
50317
50334
  if (options.output) {
50318
50335
  validateOutputPath(options.output);
@@ -50429,9 +50446,9 @@ var init_detect = __esm({
50429
50446
  exitWithError(generalError(`Scene detection failed: ${msg}`));
50430
50447
  }
50431
50448
  });
50432
- detectCommand.command("silence").description("Detect silence in audio/video").argument("<media>", "Media file path").option("-n, --noise <dB>", "Noise threshold in dB", "-30").option("-d, --duration <sec>", "Minimum silence duration", "0.5").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
50449
+ detectCommand.command("silence").description("Detect silence in audio/video").argument("<media>", "Media file path").option("--noise <dB>", "Noise threshold in dB", "-30").option("-d, --duration <sec>", "Minimum silence duration", "0.5").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
50433
50450
  const startedAt = Date.now();
50434
- const spinner2 = ora("Detecting silence...").start();
50451
+ const spinner2 = spinner("Detecting silence...").start();
50435
50452
  try {
50436
50453
  if (options.output) {
50437
50454
  validateOutputPath(options.output);
@@ -50517,7 +50534,7 @@ var init_detect = __esm({
50517
50534
  });
50518
50535
  detectCommand.command("beats").description("Detect beats in audio (for music sync)").argument("<audio>", "Audio file path").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (audioPath, options) => {
50519
50536
  const startedAt = Date.now();
50520
- const spinner2 = ora("Detecting beats...").start();
50537
+ const spinner2 = spinner("Detecting beats...").start();
50521
50538
  try {
50522
50539
  if (options.output) {
50523
50540
  validateOutputPath(options.output);
@@ -50604,7 +50621,7 @@ var init_detect = __esm({
50604
50621
  });
50605
50622
 
50606
50623
  // src/pipeline/renderers/chrome.ts
50607
- import { existsSync as existsSync31 } from "node:fs";
50624
+ import { existsSync as existsSync32 } from "node:fs";
50608
50625
  import { homedir as homedir5 } from "node:os";
50609
50626
  import { join as join11 } from "node:path";
50610
50627
  function findChrome() {
@@ -50630,7 +50647,7 @@ function findChrome() {
50630
50647
  "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
50631
50648
  ];
50632
50649
  for (const c of candidates) {
50633
- if (c && existsSync31(c)) return c;
50650
+ if (c && existsSync32(c)) return c;
50634
50651
  }
50635
50652
  return void 0;
50636
50653
  }
@@ -50885,7 +50902,7 @@ __export(scene_render_exports, {
50885
50902
  qualityToCrf: () => qualityToCrf
50886
50903
  });
50887
50904
  import { mkdir as mkdir18, readFile as readFile32, stat as stat4 } from "node:fs/promises";
50888
- import { existsSync as existsSync32 } from "node:fs";
50905
+ import { existsSync as existsSync33 } from "node:fs";
50889
50906
  import { resolve as resolve45, relative as relative6, dirname as dirname16, basename as basename16 } from "node:path";
50890
50907
  import {
50891
50908
  createRenderJob,
@@ -50903,7 +50920,7 @@ function defaultOutputPath(opts) {
50903
50920
  }
50904
50921
  async function readProjectName(projectDir) {
50905
50922
  const cfgPath = resolve45(projectDir, "vibe.project.yaml");
50906
- if (!existsSync32(cfgPath)) return void 0;
50923
+ if (!existsSync33(cfgPath)) return void 0;
50907
50924
  try {
50908
50925
  const raw = await (await import("node:fs/promises")).readFile(cfgPath, "utf-8");
50909
50926
  const parsed = (0, import_yaml7.parse)(raw);
@@ -51033,7 +51050,7 @@ __export(scene_build_exports, {
51033
51050
  executeSceneBuild: () => executeSceneBuild,
51034
51051
  resolveSceneBuildMode: () => resolveSceneBuildMode
51035
51052
  });
51036
- import { existsSync as existsSync33 } from "node:fs";
51053
+ import { existsSync as existsSync34 } from "node:fs";
51037
51054
  import { mkdir as mkdir19, readFile as readFile33, writeFile as writeFile38 } from "node:fs/promises";
51038
51055
  import { dirname as dirname17, join as join12, resolve as resolve46 } from "node:path";
51039
51056
  async function executeSceneBuild(opts) {
@@ -51043,7 +51060,7 @@ async function executeSceneBuild(opts) {
51043
51060
  });
51044
51061
  const mode = resolveSceneBuildMode(opts);
51045
51062
  const storyboardPath = join12(projectDir, "STORYBOARD.md");
51046
- if (!existsSync33(storyboardPath)) {
51063
+ if (!existsSync34(storyboardPath)) {
51047
51064
  return failBeforePrimitives(
51048
51065
  `STORYBOARD.md not found at ${storyboardPath}. Run \`vibe scene init <dir>\` to create a starter, or add STORYBOARD.md with per-beat cues.`,
51049
51066
  startedAt
@@ -51079,7 +51096,7 @@ async function executeSceneBuild(opts) {
51079
51096
  if (mode === "agent") {
51080
51097
  const compositionsDir = join12(projectDir, "compositions");
51081
51098
  const missingBeats = parsed.beats.filter(
51082
- (b) => !existsSync33(join12(compositionsDir, `scene-${b.id}.html`))
51099
+ (b) => !existsSync34(join12(compositionsDir, `scene-${b.id}.html`))
51083
51100
  );
51084
51101
  if (missingBeats.length > 0) {
51085
51102
  const plan = await getComposePrompts({ projectDir });
@@ -51127,7 +51144,7 @@ async function executeSceneBuild(opts) {
51127
51144
  }
51128
51145
  composeData = composeResult.data;
51129
51146
  }
51130
- if (!existsSync33(join12(projectDir, "index.html"))) {
51147
+ if (!existsSync34(join12(projectDir, "index.html"))) {
51131
51148
  await scaffoldSceneProject({
51132
51149
  dir: projectDir,
51133
51150
  name: projectDir.split(/[\\/]/).filter(Boolean).pop(),
@@ -51187,7 +51204,7 @@ async function dispatchNarration(beat, ctx) {
51187
51204
  if (!text) return { status: "no-cue" };
51188
51205
  for (const ext of ["mp3", "wav"]) {
51189
51206
  const rel2 = `assets/narration-${beat.id}.${ext}`;
51190
- if (existsSync33(join12(ctx.projectDir, rel2)) && !ctx.force) {
51207
+ if (existsSync34(join12(ctx.projectDir, rel2)) && !ctx.force) {
51191
51208
  ctx.onProgress({ type: "narration-cached", beatId: beat.id, path: rel2 });
51192
51209
  return { status: "cached", path: rel2 };
51193
51210
  }
@@ -51228,7 +51245,7 @@ async function dispatchBackdrop(beat, ctx) {
51228
51245
  }
51229
51246
  const rel = `assets/backdrop-${beat.id}.png`;
51230
51247
  const abs = join12(ctx.projectDir, rel);
51231
- if (existsSync33(abs) && !ctx.force) {
51248
+ if (existsSync34(abs) && !ctx.force) {
51232
51249
  ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
51233
51250
  return { status: "cached", path: rel };
51234
51251
  }
@@ -51266,7 +51283,7 @@ function loadSceneBuildEnv(projectDir) {
51266
51283
  (0, import_dotenv3.config)({ path: resolve46(process.cwd(), ".env"), quiet: true });
51267
51284
  let dir = process.cwd();
51268
51285
  while (dir !== dirname17(dir)) {
51269
- if (existsSync33(join12(dir, "pnpm-workspace.yaml"))) {
51286
+ if (existsSync34(join12(dir, "pnpm-workspace.yaml"))) {
51270
51287
  (0, import_dotenv3.config)({ path: join12(dir, ".env"), quiet: true });
51271
51288
  return;
51272
51289
  }
@@ -51297,7 +51314,7 @@ function resolveSceneBuildMode(opts) {
51297
51314
  }
51298
51315
  async function syncRootClipReferences(beats, projectDir, outcomes) {
51299
51316
  const rootPath = join12(projectDir, "index.html");
51300
- if (!existsSync33(rootPath)) return;
51317
+ if (!existsSync34(rootPath)) return;
51301
51318
  const html = await readFile33(rootPath, "utf-8");
51302
51319
  let cursor = 0;
51303
51320
  const clipLines = [];
@@ -51748,14 +51765,14 @@ __export(ai_audio_exports, {
51748
51765
  });
51749
51766
  import { resolve as resolve53, dirname as dirname19, basename as basename17, extname as extname12 } from "node:path";
51750
51767
  import { readFile as readFile36, writeFile as writeFile40 } from "node:fs/promises";
51751
- import { existsSync as existsSync36 } from "node:fs";
51768
+ import { existsSync as existsSync37 } from "node:fs";
51752
51769
  async function executeTranscribe(options) {
51753
51770
  const { audioPath, language, output, format, apiKey } = options;
51754
51771
  try {
51755
51772
  const key = apiKey || process.env.OPENAI_API_KEY;
51756
51773
  if (!key) return { success: false, error: "OPENAI_API_KEY required" };
51757
51774
  const absPath = resolve53(process.cwd(), audioPath);
51758
- if (!existsSync36(absPath)) return { success: false, error: `File not found: ${absPath}` };
51775
+ if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
51759
51776
  const whisper = new WhisperProvider();
51760
51777
  await whisper.initialize({ apiKey: key });
51761
51778
  const audioBuffer = await readFile36(absPath);
@@ -51788,7 +51805,7 @@ async function executeIsolate(options) {
51788
51805
  const key = apiKey || process.env.ELEVENLABS_API_KEY;
51789
51806
  if (!key) return { success: false, error: "ELEVENLABS_API_KEY required" };
51790
51807
  const absPath = resolve53(process.cwd(), audioPath);
51791
- if (!existsSync36(absPath)) return { success: false, error: `File not found: ${absPath}` };
51808
+ if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
51792
51809
  const audioBuffer = await readFile36(absPath);
51793
51810
  const elevenlabs = new ElevenLabsProvider();
51794
51811
  await elevenlabs.initialize({ apiKey: key });
@@ -51814,7 +51831,7 @@ async function executeVoiceClone(options) {
51814
51831
  const audioBuffers = [];
51815
51832
  for (const samplePath of samplePaths) {
51816
51833
  const absPath = resolve53(process.cwd(), samplePath);
51817
- if (!existsSync36(absPath)) return { success: false, error: `File not found: ${samplePath}` };
51834
+ if (!existsSync37(absPath)) return { success: false, error: `File not found: ${samplePath}` };
51818
51835
  const buffer = await readFile36(absPath);
51819
51836
  audioBuffers.push(buffer);
51820
51837
  }
@@ -51836,7 +51853,7 @@ async function executeDub(options) {
51836
51853
  const { mediaPath, language, source, voice, analyzeOnly, output } = options;
51837
51854
  try {
51838
51855
  const absPath = resolve53(process.cwd(), mediaPath);
51839
- if (!existsSync36(absPath)) return { success: false, error: `File not found: ${absPath}` };
51856
+ if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
51840
51857
  const openaiKey = process.env.OPENAI_API_KEY;
51841
51858
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
51842
51859
  const elevenlabsKey = process.env.ELEVENLABS_API_KEY;
@@ -51964,8 +51981,8 @@ async function executeDuck(options) {
51964
51981
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
51965
51982
  const absMusicPath = resolve53(process.cwd(), musicPath);
51966
51983
  const absVoicePath = resolve53(process.cwd(), voicePath);
51967
- if (!existsSync36(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
51968
- if (!existsSync36(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
51984
+ if (!existsSync37(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
51985
+ if (!existsSync37(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
51969
51986
  const defaultOutput = resolve53(dirname19(absMusicPath), `${basename17(absMusicPath, extname12(absMusicPath))}-ducked${extname12(absMusicPath)}`);
51970
51987
  const outputPath = resolve53(process.cwd(), output || defaultOutput);
51971
51988
  const filter = `[1:a]asplit=2[sc][mix];[0:a][sc]sidechaincompress=threshold=${threshold}dB:ratio=${ratio}:attack=${attack}:release=${release}[ducked];[ducked][mix]amix=inputs=2:duration=longest`;
@@ -51979,7 +51996,7 @@ async function executeDuck(options) {
51979
51996
  "-y",
51980
51997
  outputPath
51981
51998
  ], { timeout: 12e4, maxBuffer: 50 * 1024 * 1024 });
51982
- if (!existsSync36(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
51999
+ if (!existsSync37(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
51983
52000
  return { success: true, outputPath };
51984
52001
  } catch (error) {
51985
52002
  return { success: false, error: `Audio ducking failed: ${error instanceof Error ? error.message : String(error)}` };
@@ -51997,7 +52014,7 @@ var init_ai_audio = __esm({
51997
52014
  // src/pipeline/executor.ts
51998
52015
  import { resolve as resolve54 } from "node:path";
51999
52016
  import { readFile as readFile37, writeFile as writeFile41, mkdir as mkdir21 } from "node:fs/promises";
52000
- import { existsSync as existsSync37 } from "node:fs";
52017
+ import { existsSync as existsSync38 } from "node:fs";
52001
52018
  function maxCostFor(action) {
52002
52019
  const cmd = ACTION_METADATA[action]?.command;
52003
52020
  if (!cmd) return 0;
@@ -52214,7 +52231,7 @@ async function ensureActionsRegistered() {
52214
52231
  }
52215
52232
  async function loadPipeline(filePath) {
52216
52233
  const absPath = resolve54(process.cwd(), filePath);
52217
- if (!existsSync37(absPath)) {
52234
+ if (!existsSync38(absPath)) {
52218
52235
  throw new Error(`Pipeline file not found: ${absPath}`);
52219
52236
  }
52220
52237
  const content = await readFile37(absPath, "utf-8");
@@ -52239,7 +52256,7 @@ async function executePipeline(manifest2, options = {}) {
52239
52256
  const startTime = Date.now();
52240
52257
  if (options.resume) {
52241
52258
  const checkpointPath = resolve54(outputDir, CHECKPOINT_FILE);
52242
- if (existsSync37(checkpointPath)) {
52259
+ if (existsSync38(checkpointPath)) {
52243
52260
  const checkpointContent = await readFile37(checkpointPath, "utf-8");
52244
52261
  const checkpoint = (0, import_yaml8.parse)(checkpointContent);
52245
52262
  for (const cs of checkpoint.completedSteps) {
@@ -59645,7 +59662,7 @@ __export(project_builder_exports, {
59645
59662
  resolveSourceUrl: () => resolveSourceUrl
59646
59663
  });
59647
59664
  import { mkdtemp, mkdir as mkdir22, copyFile as copyFile3, writeFile as writeFile46, rm as rm4, readdir as readdir3 } from "node:fs/promises";
59648
- import { existsSync as existsSync38 } from "node:fs";
59665
+ import { existsSync as existsSync39 } from "node:fs";
59649
59666
  import { tmpdir as tmpdir4 } from "node:os";
59650
59667
  import { createRequire } from "node:module";
59651
59668
  import * as path from "node:path";
@@ -59657,7 +59674,7 @@ async function buildTempProject(state, projectFileDir) {
59657
59674
  for (const source of state.sources) {
59658
59675
  if (copied.has(source.id)) continue;
59659
59676
  const resolved = resolveSourceUrl(source.url, projectFileDir);
59660
- if (!existsSync38(resolved)) {
59677
+ if (!existsSync39(resolved)) {
59661
59678
  throw new Error(`Source file not found: ${resolved} (from source id=${source.id})`);
59662
59679
  }
59663
59680
  const dest = path.join(assetsDir, path.basename(resolved));
@@ -60488,18 +60505,18 @@ var init_export = __esm({
60488
60505
  BITRATE_PATTERN = /^\d+(?:\.\d+)?[kKmMgG]?$/;
60489
60506
  RESOLUTION_PATTERN = /^\d+x\d+$/;
60490
60507
  VALID_CODECS = ["h264", "h265", "vp9"];
60491
- exportCommand = new Command("export").description("Export project to video file").argument("<project>", "Project file path").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Output format (mp4, webm, mov, gif)", "mp4").option(
60492
- "-p, --preset <preset>",
60508
+ exportCommand = new Command("export").description("Export project to video file").argument("<project>", "Project file path").option("-o, --output <path>", "Output file path").option("--format <format>", "Output format (mp4, webm, mov, gif)", "mp4").option(
60509
+ "--preset <preset>",
60493
60510
  "Quality preset (draft, standard, high, ultra)",
60494
60511
  "standard"
60495
- ).option("-y, --overwrite", "Overwrite output file if exists", false).option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").option("--backend <name>", "Render backend: ffmpeg (default) | hyperframes (experimental)", "ffmpeg").option("--bitrate <value>", "Video bitrate (e.g. 5000k, 8M) \u2014 overrides preset").option("--fps <number>", "Frames per second (e.g. 24, 30, 60) \u2014 overrides preset").option("--resolution <WxH>", "Output resolution (e.g. 1920x1080) \u2014 overrides preset").option("--codec <codec>", "Video codec: h264 (default) | h265 | vp9 \u2014 overrides preset").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
60512
+ ).option("--overwrite", "Overwrite output file if exists", false).option("--gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").option("--backend <name>", "Render backend: ffmpeg (default) | hyperframes (experimental)", "ffmpeg").option("--bitrate <value>", "Video bitrate (e.g. 5000k, 8M) \u2014 overrides preset").option("--fps <number>", "Frames per second (e.g. 24, 30, 60) \u2014 overrides preset").option("--resolution <WxH>", "Output resolution (e.g. 1920x1080) \u2014 overrides preset").option("--codec <codec>", "Video codec: h264 (default) | h265 | vp9 \u2014 overrides preset").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
60496
60513
  Examples:
60497
60514
  $ vibe export project.vibe.json -o output.mp4
60498
- $ vibe export project.vibe.json -o output.mp4 -p high -y
60499
- $ vibe export project.vibe.json -o output.webm -f webm
60500
- $ vibe export project.vibe.json -o output.gif -f gif
60515
+ $ vibe export project.vibe.json -o output.mp4 --preset high --overwrite
60516
+ $ vibe export project.vibe.json -o output.webm --format webm
60517
+ $ vibe export project.vibe.json -o output.gif --format gif
60501
60518
  $ vibe export project.vibe.json -o out.mp4 --bitrate 5000k --fps 24 --codec h265
60502
- $ vibe export project.vibe.json -o out.mp4 -p high --fps 60
60519
+ $ vibe export project.vibe.json -o out.mp4 --preset high --fps 60
60503
60520
 
60504
60521
  Cost: Free (no API keys needed). Requires FFmpeg.
60505
60522
  GIF format: 15fps, no audio, looping. Good for previews and sharing.
@@ -61635,28 +61652,31 @@ narration / backdrop intent.
61635
61652
  ## Quick-draft path
61636
61653
 
61637
61654
  \`\`\`bash
61638
- vibe scene init my-promo -r 16:9 -d 30
61655
+ vibe init my-promo -r 16:9 -d 30
61639
61656
  vibe scene add intro --style announcement \\
61640
61657
  --headline "Ship videos, not clicks"
61641
61658
  vibe scene lint
61642
61659
  vibe render my-promo
61643
61660
  \`\`\`
61644
61661
 
61645
- \`vibe scene init\` is **idempotent** \u2014 running it on an existing
61646
- Hyperframes directory merges \`hyperframes.json\` instead of clobbering it.
61647
- Safe to invoke on user-provided projects.
61662
+ \`vibe init\` is **idempotent** \u2014 running it on an existing Hyperframes
61663
+ directory merges \`hyperframes.json\` instead of clobbering it. Safe to
61664
+ invoke on user-provided projects.
61648
61665
 
61649
61666
  ## Subcommands
61650
61667
 
61651
61668
  \`\`\`bash
61652
- vibe scene init <dir> [-r 16:9|9:16|1:1|4:5] [-d <sec>] [--visual-style "<name>"]
61669
+ # Project flow (top-level \u2014 preferred entry points)
61670
+ vibe init <dir> [-r 16:9|9:16|1:1|4:5] [-d <sec>] [--visual-style "<name>"]
61671
+ vibe build [<dir>] [--mode agent|batch|auto] # H3 dispatch
61672
+ vibe render [<dir>] [--fps 30] [--quality standard] [--format mp4]
61673
+
61674
+ # Lower-level scene authoring
61653
61675
  vibe scene list-styles [<name>] # list / show vendored visual identities
61654
61676
  vibe scene install-skill [<dir>] [--host all] # retroactive composition-rules install
61655
61677
  vibe scene add <name> --style <preset> [...]
61656
61678
  vibe scene compose-prompts [<dir>] [--beat <id>] # H2: emit plan, no LLM call
61657
61679
  vibe scene lint [<root>] [--json] [--fix]
61658
- vibe scene render [<root>] [--fps 30] [--quality standard] [--format mp4]
61659
- vibe build [<dir>] [--mode agent|batch|auto] # H3 dispatch
61660
61680
  \`\`\`
61661
61681
 
61662
61682
  ## Style presets (for \`vibe scene add --style\`)
@@ -61710,7 +61730,7 @@ and surface the error to the user.
61710
61730
  | Generate narration + image, then author scene | \`vibe scene add\` |
61711
61731
  | Generate a full scenes project from a STORYBOARD | \`vibe build\` |
61712
61732
  | Hand-tweak a single scene's animation | edit \`compositions/<file>.html\` directly |
61713
- | Render the project | \`vibe render\` *or* \`vibe scene render\` for lower-level control |
61733
+ | Render the project | \`vibe render\` (one canonical entry point) |
61714
61734
  | Lint | \`vibe scene lint\` *or* \`npx hyperframes lint\` (equivalent) |
61715
61735
 
61716
61736
  The \`vibe\` CLI adds asset generation, AI orchestration, and pipeline
@@ -61838,12 +61858,12 @@ overlay, etc.) \u2014 useful at the tail of a pipeline.
61838
61858
  ],
61839
61859
  relatedCommands: [
61840
61860
  "vibe init",
61861
+ "vibe build",
61862
+ "vibe render",
61841
61863
  "vibe scene list-styles",
61842
61864
  "vibe scene install-skill",
61843
61865
  "vibe scene compose-prompts",
61844
- "vibe build",
61845
61866
  "vibe scene lint",
61846
- "vibe scene render",
61847
61867
  "vibe scene add"
61848
61868
  ]
61849
61869
  },
@@ -62167,7 +62187,7 @@ init_source();
62167
62187
  // package.json
62168
62188
  var package_default = {
62169
62189
  name: "@vibeframe/cli",
62170
- version: "0.77.0",
62190
+ version: "0.79.0",
62171
62191
  description: "VibeFrame CLI - natural language video editing from the terminal",
62172
62192
  private: false,
62173
62193
  license: "MIT",
@@ -62389,7 +62409,7 @@ async function resolveProjectPath(inputPath) {
62389
62409
  }
62390
62410
  return filePath;
62391
62411
  }
62392
- var projectCommand = new Command("project").description("Project management commands").addHelpText("after", `
62412
+ var projectCommand = new Command("project").description("Manage VibeFrame .vibe.json project files (create, info, set)").addHelpText("after", `
62393
62413
  Examples:
62394
62414
  $ vibe project create "my-video" -o project.vibe.json
62395
62415
  $ vibe project create "shorts" -r 9:16 -o vertical.vibe.json
@@ -62398,7 +62418,7 @@ Examples:
62398
62418
 
62399
62419
  Cost: Free (no API keys needed). Projects are saved as .vibe.json files.
62400
62420
  Run 'vibe schema project.<command>' for structured parameter info.`);
62401
- projectCommand.command("create").description("Create a new project").argument("<name>", "Project name or path (e.g., 'my-project' or 'output/my-project')").option("-o, --output <path>", "Output file path (overrides name-based path)").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)", "16:9").option("-f, --fps <fps>", "Frame rate", "30").option("--dry-run", "Preview parameters without executing").action(async (name, options) => {
62421
+ projectCommand.command("create").description("Create a new project").argument("<name>", "Project name or path (e.g., 'my-project' or 'output/my-project')").option("-o, --output <path>", "Output file path (overrides name-based path)").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)", "16:9").option("--fps <fps>", "Frame rate", "30").option("--dry-run", "Preview parameters without executing").action(async (name, options) => {
62402
62422
  const startedAt = Date.now();
62403
62423
  const spinner2 = ora("Creating project...").start();
62404
62424
  try {
@@ -62512,7 +62532,7 @@ projectCommand.command("info").description("Show project information").argument(
62512
62532
  exitWithError(generalError(`Failed to load project: ${msg}`));
62513
62533
  }
62514
62534
  });
62515
- projectCommand.command("set").description("Update project settings").argument("<file>", "Project file path").option("-n, --name <name>", "Project name").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)").option("-f, --fps <fps>", "Frame rate").option("--dry-run", "Preview parameters without executing").action(async (file, options) => {
62535
+ projectCommand.command("set").description("Update project settings").argument("<file>", "Project file path").option("--name <name>", "Project name").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)").option("--fps <fps>", "Frame rate").option("--dry-run", "Preview parameters without executing").action(async (file, options) => {
62516
62536
  const startedAt = Date.now();
62517
62537
  const spinner2 = ora("Updating project...").start();
62518
62538
  try {
@@ -62579,6 +62599,7 @@ init_engine();
62579
62599
  init_validate();
62580
62600
  init_output();
62581
62601
  import { readFile as readFile9, writeFile as writeFile9 } from "node:fs/promises";
62602
+ import { existsSync as existsSync8 } from "node:fs";
62582
62603
  import { resolve as resolve11, basename as basename2, extname } from "node:path";
62583
62604
  var timelineCommand = new Command("timeline").description("Timeline editing commands").addHelpText("after", `
62584
62605
  Examples:
@@ -62592,7 +62613,7 @@ Examples:
62592
62613
  Typical workflow: create project \u2192 add-source \u2192 add-clip \u2192 trim-clip/split-clip \u2192 export
62593
62614
  Cost: Free (no API keys needed).
62594
62615
  Run 'vibe schema timeline.<command>' for structured parameter info.`);
62595
- timelineCommand.command("add-source").description("Add a media source to the project").argument("<project>", "Project file path").argument("<media>", "Media file path").option("-n, --name <name>", "Source name (defaults to filename)").option("-t, --type <type>", "Media type (video, audio, image, lottie)").option("-d, --duration <seconds>", "Duration in seconds (required for images)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, mediaPath, options) => {
62616
+ timelineCommand.command("add-source").description("Add a media source to the project").argument("<project>", "Project file path").argument("<media>", "Media file path").option("--name <name>", "Source name (defaults to filename)").option("--type <type>", "Media type (video, audio, image, lottie)").option("-d, --duration <seconds>", "Duration in seconds (required for images)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, mediaPath, options) => {
62596
62617
  const startedAt = Date.now();
62597
62618
  const spinner2 = ora("Adding source...").start();
62598
62619
  try {
@@ -62654,7 +62675,7 @@ timelineCommand.command("add-source").description("Add a media source to the pro
62654
62675
  exitWithError(generalError(`Failed to add source: ${msg}`));
62655
62676
  }
62656
62677
  });
62657
- timelineCommand.command("add-clip").description("Add a clip to the timeline").argument("<project>", "Project file path").argument("<source-id>", "Source ID to use").option("-t, --track <id>", "Track ID (defaults to first matching track)").option("-s, --start <seconds>", "Start time in timeline", "0").option("-d, --duration <seconds>", "Clip duration (defaults to source duration)").option("--offset <seconds>", "Source start offset", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, sourceId, options) => {
62678
+ timelineCommand.command("add-clip").description("Add a clip to the timeline").argument("<project>", "Project file path").argument("<source-id>", "Source ID to use").option("--track <id>", "Track ID (defaults to first matching track)").option("--start <seconds>", "Start time in timeline", "0").option("-d, --duration <seconds>", "Clip duration (defaults to source duration)").option("--offset <seconds>", "Source start offset", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, sourceId, options) => {
62658
62679
  const startedAt = Date.now();
62659
62680
  const spinner2 = ora("Adding clip...").start();
62660
62681
  try {
@@ -62736,7 +62757,7 @@ timelineCommand.command("add-clip").description("Add a clip to the timeline").ar
62736
62757
  exitWithError(generalError(`Failed to add clip: ${msg}`));
62737
62758
  }
62738
62759
  });
62739
- timelineCommand.command("add-track").description("Add a new track").argument("<project>", "Project file path").argument("<type>", "Track type (video, audio)").option("-n, --name <name>", "Track name").option("--dry-run", "Preview parameters without executing").action(async (projectPath, type, options) => {
62760
+ timelineCommand.command("add-track").description("Add a new track").argument("<project>", "Project file path").argument("<type>", "Track type (video, audio)").option("--name <name>", "Track name").option("--dry-run", "Preview parameters without executing").action(async (projectPath, type, options) => {
62740
62761
  const startedAt = Date.now();
62741
62762
  const spinner2 = ora("Adding track...").start();
62742
62763
  try {
@@ -62793,7 +62814,7 @@ timelineCommand.command("add-track").description("Add a new track").argument("<p
62793
62814
  exitWithError(generalError(`Failed to add track: ${msg}`));
62794
62815
  }
62795
62816
  });
62796
- timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)").option("-s, --start <seconds>", "Effect start time (relative to clip)", "0").option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)").option("-p, --params <json>", "Effect parameters as JSON", "{}").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, effectType, options) => {
62817
+ timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)").option("--start <seconds>", "Effect start time (relative to clip)", "0").option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)").option("--params <json>", "Effect parameters as JSON", "{}").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, effectType, options) => {
62797
62818
  const startedAt = Date.now();
62798
62819
  const spinner2 = ora("Adding effect...").start();
62799
62820
  try {
@@ -62929,6 +62950,9 @@ timelineCommand.command("list").description("List timeline contents").argument("
62929
62950
  const startedAt = Date.now();
62930
62951
  try {
62931
62952
  const filePath = resolve11(process.cwd(), projectPath);
62953
+ if (!existsSync8(filePath)) {
62954
+ exitWithError(notFoundError(projectPath));
62955
+ }
62932
62956
  const content = await readFile9(filePath, "utf-8");
62933
62957
  const data = JSON.parse(content);
62934
62958
  const project = Project.fromJSON(data);
@@ -63027,7 +63051,7 @@ timelineCommand.command("list").description("List timeline contents").argument("
63027
63051
  exitWithError(generalError(`Failed to list timeline: ${msg}`));
63028
63052
  }
63029
63053
  });
63030
- timelineCommand.command("split-clip").description("Split a clip at a specific time").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to split").option("-t, --time <seconds>", "Split time relative to clip start", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63054
+ timelineCommand.command("split-clip").description("Split a clip at a specific time").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to split").option("--time <seconds>", "Split time relative to clip start", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63031
63055
  const startedAt = Date.now();
63032
63056
  const spinner2 = ora("Splitting clip...").start();
63033
63057
  try {
@@ -63089,7 +63113,7 @@ timelineCommand.command("split-clip").description("Split a clip at a specific ti
63089
63113
  exitWithError(generalError(`Failed to split clip: ${msg}`));
63090
63114
  }
63091
63115
  });
63092
- timelineCommand.command("duplicate-clip").description("Duplicate a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to duplicate").option("-t, --time <seconds>", "Start time for duplicate (default: after original)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63116
+ timelineCommand.command("duplicate-clip").description("Duplicate a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to duplicate").option("--time <seconds>", "Start time for duplicate (default: after original)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63093
63117
  const startedAt = Date.now();
63094
63118
  const spinner2 = ora("Duplicating clip...").start();
63095
63119
  try {
@@ -63200,7 +63224,7 @@ timelineCommand.command("delete-clip").description("Delete a clip from the timel
63200
63224
  exitWithError(generalError(`Failed to delete clip: ${msg}`));
63201
63225
  }
63202
63226
  });
63203
- timelineCommand.command("move-clip").description("Move a clip to a new position").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to move").option("-t, --time <seconds>", "New start time").option("--track <track-id>", "Move to different track").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63227
+ timelineCommand.command("move-clip").description("Move a clip to a new position").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to move").option("--time <seconds>", "New start time").option("--track <track-id>", "Move to different track").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63204
63228
  const startedAt = Date.now();
63205
63229
  const spinner2 = ora("Moving clip...").start();
63206
63230
  try {
@@ -63311,10 +63335,29 @@ Use '--fields response,model' to limit output size.
63311
63335
  Run 'vibe schema inspect.<command>' for structured parameter info.
63312
63336
  `
63313
63337
  );
63314
- analyzeCommand.command("media").description("Analyze any media: images, videos, or YouTube URLs using Gemini").argument("<source>", "Image/video file path, image URL, or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Describe this image', 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second for video (default: 1)").option("--start <seconds>", "Start offset in seconds (video only)").option("--end <seconds>", "End offset in seconds (video only)").option("--low-res", "Use low resolution mode (fewer tokens)").option("-v, --verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").action(async (source, prompt3, options) => {
63338
+ analyzeCommand.command("media").description("Analyze any media: images, videos, or YouTube URLs using Gemini").argument("<source>", "Image/video file path, image URL, or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Describe this image', 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second for video (default: 1)").option("--start <seconds>", "Start offset in seconds (video only)").option("--end <seconds>", "End offset in seconds (video only)").option("--low-res", "Use low resolution mode (fewer tokens)").option("--verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").option("--dry-run", "Preview parameters without executing").action(async (source, prompt3, options) => {
63315
63339
  const startedAt = Date.now();
63316
63340
  try {
63317
63341
  rejectControlChars(prompt3);
63342
+ if (options.dryRun) {
63343
+ outputSuccess({
63344
+ command: "inspect media",
63345
+ startedAt,
63346
+ dryRun: true,
63347
+ data: {
63348
+ params: {
63349
+ source,
63350
+ prompt: prompt3,
63351
+ model: options.model,
63352
+ fps: options.fps,
63353
+ start: options.start,
63354
+ end: options.end,
63355
+ lowRes: options.lowRes ?? false
63356
+ }
63357
+ }
63358
+ });
63359
+ return;
63360
+ }
63318
63361
  if (options.apiKey) {
63319
63362
  process.env.GOOGLE_API_KEY = options.apiKey;
63320
63363
  } else {
@@ -63344,7 +63387,7 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
63344
63387
  data.totalTokens = result.totalTokens;
63345
63388
  }
63346
63389
  outputSuccess({
63347
- command: "analyze media",
63390
+ command: "inspect media",
63348
63391
  startedAt,
63349
63392
  data
63350
63393
  });
@@ -63369,10 +63412,29 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
63369
63412
  exitWithError(apiError(`Analysis failed: ${error.message}`));
63370
63413
  }
63371
63414
  });
63372
- analyzeCommand.command("video").description("Analyze video using Gemini (summarize, Q&A, extract info)").argument("<source>", "Video file path or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second (default: 1, higher for action)").option("--start <seconds>", "Start offset in seconds (for clipping)").option("--end <seconds>", "End offset in seconds (for clipping)").option("--low-res", "Use low resolution mode (fewer tokens, longer videos)").option("-v, --verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").action(async (source, prompt3, options) => {
63415
+ analyzeCommand.command("video").description("Analyze video using Gemini (summarize, Q&A, extract info)").argument("<source>", "Video file path or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second (default: 1, higher for action)").option("--start <seconds>", "Start offset in seconds (for clipping)").option("--end <seconds>", "End offset in seconds (for clipping)").option("--low-res", "Use low resolution mode (fewer tokens, longer videos)").option("--verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").option("--dry-run", "Preview parameters without executing").action(async (source, prompt3, options) => {
63373
63416
  const startedAt = Date.now();
63374
63417
  try {
63375
63418
  rejectControlChars(prompt3);
63419
+ if (options.dryRun) {
63420
+ outputSuccess({
63421
+ command: "inspect video",
63422
+ startedAt,
63423
+ dryRun: true,
63424
+ data: {
63425
+ params: {
63426
+ source,
63427
+ prompt: prompt3,
63428
+ model: options.model,
63429
+ fps: options.fps,
63430
+ start: options.start,
63431
+ end: options.end,
63432
+ lowRes: options.lowRes ?? false
63433
+ }
63434
+ }
63435
+ });
63436
+ return;
63437
+ }
63376
63438
  if (options.apiKey) {
63377
63439
  process.env.GOOGLE_API_KEY = options.apiKey;
63378
63440
  } else {
@@ -63402,7 +63464,7 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
63402
63464
  data.totalTokens = result.totalTokens;
63403
63465
  }
63404
63466
  outputSuccess({
63405
- command: "analyze video",
63467
+ command: "inspect video",
63406
63468
  startedAt,
63407
63469
  data
63408
63470
  });
@@ -63427,10 +63489,25 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
63427
63489
  }
63428
63490
  });
63429
63491
  registerReviewCommand(analyzeCommand);
63430
- analyzeCommand.command("suggest").description("Get AI edit suggestions using Gemini").argument("<project>", "Project file path").argument("<instruction>", "Natural language instruction").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("--apply", "Apply the first suggestion automatically").action(async (projectPath, instruction, options) => {
63492
+ analyzeCommand.command("suggest").description("Get AI edit suggestions using Gemini").argument("<project>", "Project file path").argument("<instruction>", "Natural language instruction").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("--apply", "Apply the first suggestion automatically").option("--dry-run", "Preview parameters without executing").action(async (projectPath, instruction, options) => {
63431
63493
  const startedAt = Date.now();
63432
63494
  try {
63433
63495
  rejectControlChars(instruction);
63496
+ if (options.dryRun) {
63497
+ outputSuccess({
63498
+ command: "inspect suggest",
63499
+ startedAt,
63500
+ dryRun: true,
63501
+ data: {
63502
+ params: {
63503
+ project: projectPath,
63504
+ instruction,
63505
+ apply: options.apply ?? false
63506
+ }
63507
+ }
63508
+ });
63509
+ return;
63510
+ }
63434
63511
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
63435
63512
  const spinner2 = ora("Initializing Gemini...").start();
63436
63513
  const filePath = resolve32(process.cwd(), projectPath);
@@ -63445,7 +63522,7 @@ analyzeCommand.command("suggest").description("Get AI edit suggestions using Gem
63445
63522
  spinner2.succeed(source_default.green(`Found ${suggestions.length} suggestion(s)`));
63446
63523
  if (isJsonMode()) {
63447
63524
  outputSuccess({
63448
- command: "analyze suggest",
63525
+ command: "inspect suggest",
63449
63526
  startedAt,
63450
63527
  data: {
63451
63528
  suggestions: suggestions.map((s) => ({ type: s.type, description: s.description, confidence: s.confidence, clipIds: s.clipIds, params: s.params }))
@@ -63496,16 +63573,16 @@ init_output();
63496
63573
  init_validate();
63497
63574
  import { resolve as resolve33, dirname as dirname11, basename as basename7, extname as extname7 } from "node:path";
63498
63575
  import { readFile as readFile24, writeFile as writeFile31 } from "node:fs/promises";
63499
- import { existsSync as existsSync25 } from "node:fs";
63576
+ import { existsSync as existsSync26 } from "node:fs";
63500
63577
  var audioCommand = new Command("audio").alias("au").description("Audio operations (transcribe, TTS, voice clone, ducking)").addHelpText(
63501
63578
  "after",
63502
63579
  `
63503
63580
  Examples:
63504
- $ vibe audio transcribe interview.mp3 -o transcript.srt -f srt
63581
+ $ vibe audio transcribe interview.mp3 -o transcript.srt --format srt
63505
63582
  $ vibe audio transcribe video.mp4 -l ko # Specify language
63506
63583
  $ vibe audio list-voices # List available voices
63507
63584
  $ vibe audio isolate song.mp3 -o vocals.mp3
63508
- $ vibe audio clone-voice sample.mp3 -n "my-voice"
63585
+ $ vibe audio clone-voice sample.mp3 --name "my-voice"
63509
63586
  $ vibe audio dub video.mp4 -l ko -o dubbed.mp4
63510
63587
  $ vibe audio duck music.mp3 --voice narration.mp3 -o ducked.mp3
63511
63588
 
@@ -63518,7 +63595,7 @@ API Keys:
63518
63595
  Run 'vibe schema audio.<command>' for structured parameter info.
63519
63596
  `
63520
63597
  );
63521
- audioCommand.command("transcribe").description("Transcribe audio using Whisper").argument("<audio>", "Audio file path").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("-l, --language <lang>", "Language code (e.g., en, ko)").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Output format: json, srt, vtt (auto-detected from extension)").action(async (audioPath, options) => {
63598
+ audioCommand.command("transcribe").description("Transcribe audio using Whisper").argument("<audio>", "Audio file path").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("-l, --language <lang>", "Language code (e.g., en, ko)").option("-o, --output <path>", "Output file path").option("--format <format>", "Output format: json, srt, vtt (auto-detected from extension)").action(async (audioPath, options) => {
63522
63599
  const startedAt = Date.now();
63523
63600
  try {
63524
63601
  if (options.output) {
@@ -63656,7 +63733,7 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
63656
63733
  exitWithError(apiError(`Audio isolation failed: ${msg}`, true));
63657
63734
  }
63658
63735
  });
63659
- audioCommand.command("clone-voice").description("Clone a voice from audio samples using ElevenLabs").argument("[samples...]", "Audio sample files (1-25 files)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-n, --name <name>", "Voice name (required)").option("-d, --description <desc>", "Voice description").option("--labels <json>", `Labels as JSON (e.g., '{"accent": "american"}')`).option("--remove-noise", "Remove background noise from samples").option("--list", "List all available voices").option("--dry-run", "Preview parameters without executing").action(async (samples, options) => {
63736
+ audioCommand.command("clone-voice").description("Clone a voice from audio samples using ElevenLabs").argument("[samples...]", "Audio sample files (1-25 files)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("--name <name>", "Voice name (required)").option("-d, --description <desc>", "Voice description").option("--labels <json>", `Labels as JSON (e.g., '{"accent": "american"}')`).option("--remove-noise", "Remove background noise from samples").option("--list", "List all available voices").option("--dry-run", "Preview parameters without executing").action(async (samples, options) => {
63660
63737
  const startedAt = Date.now();
63661
63738
  try {
63662
63739
  if (options.dryRun) {
@@ -63700,7 +63777,7 @@ audioCommand.command("clone-voice").description("Clone a voice from audio sample
63700
63777
  const audioBuffers = [];
63701
63778
  for (const samplePath of samples) {
63702
63779
  const absPath = resolve33(process.cwd(), samplePath);
63703
- if (!existsSync25(absPath)) {
63780
+ if (!existsSync26(absPath)) {
63704
63781
  spinner2.fail(`File not found: ${samplePath}`);
63705
63782
  exitWithError(notFoundError(samplePath));
63706
63783
  }
@@ -63742,7 +63819,7 @@ audioCommand.command("clone-voice").description("Clone a voice from audio sample
63742
63819
  exitWithError(apiError(`Voice cloning failed: ${msg}`, true));
63743
63820
  }
63744
63821
  });
63745
- audioCommand.command("dub").description("Dub audio/video to another language (transcribe, translate, TTS)").argument("<media>", "Input media file (video or audio)").option("-l, --language <lang>", "Target language code (e.g., es, ko, ja) (required)").option("--source <lang>", "Source language code (default: auto-detect)").option("-v, --voice <id>", "ElevenLabs voice ID for output").option("--analyze-only", "Only analyze and show timing, don't generate audio").option("-o, --output <path>", "Output file path").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
63822
+ audioCommand.command("dub").description("Dub audio/video to another language (transcribe, translate, TTS)").argument("<media>", "Input media file (video or audio)").option("-l, --language <lang>", "Target language code (e.g., es, ko, ja) (required)").option("--source <lang>", "Source language code (default: auto-detect)").option("--voice <id>", "ElevenLabs voice ID for output").option("--analyze-only", "Only analyze and show timing, don't generate audio").option("-o, --output <path>", "Output file path").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
63746
63823
  const startedAt = Date.now();
63747
63824
  try {
63748
63825
  if (options.dryRun) {
@@ -63761,7 +63838,7 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
63761
63838
  exitWithError(usageError("Target language is required. Use -l or --language"));
63762
63839
  }
63763
63840
  const absPath = resolve33(process.cwd(), mediaPath);
63764
- if (!existsSync25(absPath)) {
63841
+ if (!existsSync26(absPath)) {
63765
63842
  exitWithError(notFoundError(mediaPath));
63766
63843
  }
63767
63844
  const openaiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
@@ -63938,7 +64015,7 @@ ${segmentTexts}`,
63938
64015
  exitWithError(apiError(`Dubbing failed: ${msg}`, true));
63939
64016
  }
63940
64017
  });
63941
- audioCommand.command("duck").description("Auto-duck background music when voice is present (FFmpeg)").argument("<music>", "Background music file path").option("-v, --voice <path>", "Voice/narration track (required)").option("-o, --output <path>", "Output audio file path").option("-t, --threshold <dB>", "Sidechain threshold in dB", "-30").option("-r, --ratio <ratio>", "Compression ratio", "3").option("-a, --attack <ms>", "Attack time in ms", "20").option("-l, --release <ms>", "Release time in ms", "200").option("--dry-run", "Preview parameters without executing").action(async (musicPath, options) => {
64018
+ audioCommand.command("duck").description("Auto-duck background music when voice is present (FFmpeg)").argument("<music>", "Background music file path").option("--voice <path>", "Voice/narration track (required)").option("-o, --output <path>", "Output audio file path").option("--threshold <dB>", "Sidechain threshold in dB", "-30").option("-r, --ratio <ratio>", "Compression ratio", "3").option("-a, --attack <ms>", "Attack time in ms", "20").option("-l, --release <ms>", "Release time in ms", "200").option("--dry-run", "Preview parameters without executing").action(async (musicPath, options) => {
63942
64019
  const startedAt = Date.now();
63943
64020
  try {
63944
64021
  if (options.dryRun) {
@@ -64004,7 +64081,7 @@ init_esm();
64004
64081
  init_source();
64005
64082
  init_ora();
64006
64083
  import { resolve as resolve37, basename as basename10 } from "node:path";
64007
- import { existsSync as existsSync29 } from "node:fs";
64084
+ import { existsSync as existsSync30 } from "node:fs";
64008
64085
 
64009
64086
  // src/commands/ai-script-pipeline-cli.ts
64010
64087
  init_source();
@@ -64015,19 +64092,19 @@ init_ai_script_pipeline();
64015
64092
  init_output();
64016
64093
  import { readFile as readFile25, writeFile as writeFile32 } from "node:fs/promises";
64017
64094
  import { resolve as resolve34 } from "node:path";
64018
- import { existsSync as existsSync26 } from "node:fs";
64095
+ import { existsSync as existsSync27 } from "node:fs";
64019
64096
  function registerScriptPipelineCommands(aiCommand) {
64020
- aiCommand.command("regenerate-scene").description("Regenerate a specific scene in a script-to-video output directory").argument("<project-dir>", "Path to the script-to-video output directory").requiredOption("--scene <numbers>", "Scene number(s) to regenerate (1-based), e.g., 3 or 3,4,5").option("--video-only", "Only regenerate video").option("--narration-only", "Only regenerate narration").option("--image-only", "Only regenerate image").option("-g, --generator <engine>", "Video generator: grok | kling | runway | veo", "grok").option("-i, --image-provider <provider>", "Image provider: gemini | openai | grok", "gemini").option("-v, --voice <id>", "ElevenLabs voice ID for narration").option("-a, --aspect-ratio <ratio>", "Aspect ratio: 16:9 | 9:16 | 1:1", "16:9").option("--retries <count>", "Number of retries for video generation failures", String(DEFAULT_VIDEO_RETRIES)).option("--reference-scene <num>", "Use another scene's image as reference for character consistency").option("--dry-run", "Preview parameters without executing").action(async (projectDir, options) => {
64097
+ aiCommand.command("regenerate-scene").description("Regenerate a specific scene in a script-to-video output directory").argument("<project-dir>", "Path to the script-to-video output directory").requiredOption("--scene <numbers>", "Scene number(s) to regenerate (1-based), e.g., 3 or 3,4,5").option("--video-only", "Only regenerate video").option("--narration-only", "Only regenerate narration").option("--image-only", "Only regenerate image").option("--generator <engine>", "Video generator: grok | kling | runway | veo", "grok").option("-i, --image-provider <provider>", "Image provider: gemini | openai | grok", "gemini").option("--voice <id>", "ElevenLabs voice ID for narration").option("-a, --aspect-ratio <ratio>", "Aspect ratio: 16:9 | 9:16 | 1:1", "16:9").option("--retries <count>", "Number of retries for video generation failures", String(DEFAULT_VIDEO_RETRIES)).option("--reference-scene <num>", "Use another scene's image as reference for character consistency").option("--dry-run", "Preview parameters without executing").action(async (projectDir, options) => {
64021
64098
  const startedAt = Date.now();
64022
64099
  try {
64023
64100
  const outputDir = resolve34(process.cwd(), projectDir);
64024
64101
  const projectPath = resolve34(outputDir, "project.vibe.json");
64025
- if (!existsSync26(outputDir)) {
64102
+ if (!existsSync27(outputDir)) {
64026
64103
  exitWithError(notFoundError(outputDir));
64027
64104
  }
64028
64105
  const yamlPath = resolve34(outputDir, "storyboard.yaml");
64029
64106
  const jsonPath = resolve34(outputDir, "storyboard.json");
64030
- const storyboardPath = existsSync26(yamlPath) ? yamlPath : existsSync26(jsonPath) ? jsonPath : null;
64107
+ const storyboardPath = existsSync27(yamlPath) ? yamlPath : existsSync27(jsonPath) ? jsonPath : null;
64031
64108
  const storyboardIsYaml = storyboardPath === yamlPath;
64032
64109
  if (!storyboardPath) {
64033
64110
  exitWithError(notFoundError(`${outputDir}/storyboard.{yaml,json}`));
@@ -64125,7 +64202,7 @@ function registerScriptPipelineCommands(aiCommand) {
64125
64202
  exitWithError(apiError(result.error || "Scene regeneration failed", true));
64126
64203
  }
64127
64204
  spinner2.succeed(source_default.green(`Regenerated ${result.regeneratedScenes.length} scene(s)`));
64128
- if (existsSync26(projectPath) && result.regeneratedScenes.length > 0) {
64205
+ if (existsSync27(projectPath) && result.regeneratedScenes.length > 0) {
64129
64206
  const updateSpinner = ora("\u{1F4E6} Updating project file...").start();
64130
64207
  try {
64131
64208
  const updatedContent = await readFile25(storyboardPath, "utf-8");
@@ -64139,7 +64216,7 @@ function registerScriptPipelineCommands(aiCommand) {
64139
64216
  const imagePath = resolve34(outputDir, `scene-${sceneNum}.png`);
64140
64217
  const videoSource = projectData.state.sources.find((s) => s.name === `Scene ${sceneNum}`);
64141
64218
  if (videoSource) {
64142
- const hasVideo = existsSync26(videoPath);
64219
+ const hasVideo = existsSync27(videoPath);
64143
64220
  videoSource.url = hasVideo ? videoPath : imagePath;
64144
64221
  videoSource.type = hasVideo ? "video" : "image";
64145
64222
  videoSource.duration = segment.duration;
@@ -64203,9 +64280,9 @@ Two flows \u2014 pick by intent:
64203
64280
 
64204
64281
  Examples (REMIX):
64205
64282
  $ vibe remix highlights long-video.mp4 -o highlights.json -d 60
64206
- $ vibe remix auto-shorts long-video.mp4 -o shorts/ -n 3 --add-captions
64207
- $ vibe remix animated-caption video.mp4 -o captioned.mp4 -s highlight
64208
- $ vibe remix animated-caption video.mp4 -o out.mp4 -s karaoke-sweep --fast
64283
+ $ vibe remix auto-shorts long-video.mp4 -o shorts/ --count 3 --add-captions
64284
+ $ vibe remix animated-caption video.mp4 -o captioned.mp4 --style highlight
64285
+ $ vibe remix animated-caption video.mp4 -o out.mp4 --style karaoke-sweep --fast
64209
64286
 
64210
64287
  Provider API Keys:
64211
64288
  highlights: GOOGLE_API_KEY (Gemini analysis)
@@ -64225,13 +64302,13 @@ Run 'vibe schema remix.<command>' for structured parameter info.
64225
64302
  registerScriptPipelineCommands(pipelineCommand);
64226
64303
  registerHighlightsCommands(pipelineCommand);
64227
64304
  var ANIMATED_CAPTION_STYLES = ["highlight", "bounce", "pop-in", "neon", "karaoke-sweep", "typewriter"];
64228
- pipelineCommand.command("animated-caption").description("Add animated captions with word-by-word effects (Whisper + Remotion/ASS)").argument("<video>", "Video file path").option("-s, --style <preset>", "Style preset (default: highlight)", "highlight").option("--highlight-color <color>", "Active word highlight color", "#FFFF00").option("--font-size <px>", "Font size (default: auto based on resolution)").option("--position <pos>", "Caption position: top, center, bottom", "bottom").option("--words-per-group <n>", "Words shown at once (default: auto 3-5)").option("--max-chars <n>", "Max characters per group").option("-l, --language <lang>", "Whisper language hint").option("--fast", "Use ASS/FFmpeg only (no Remotion, forces ASS tier styles)").option("-o, --output <path>", "Output file path").option("--dry-run", "Preview parameters without executing").addHelpText(
64305
+ pipelineCommand.command("animated-caption").description("Add animated captions with word-by-word effects (Whisper + Remotion/ASS)").argument("<video>", "Video file path").option("--style <preset>", "Style preset (default: highlight)", "highlight").option("--highlight-color <color>", "Active word highlight color", "#FFFF00").option("--font-size <px>", "Font size (default: auto based on resolution)").option("--position <pos>", "Caption position: top, center, bottom", "bottom").option("--words-per-group <n>", "Words shown at once (default: auto 3-5)").option("--max-chars <n>", "Max characters per group").option("-l, --language <lang>", "Whisper language hint").option("--fast", "Use ASS/FFmpeg only (no Remotion, forces ASS tier styles)").option("-o, --output <path>", "Output file path").option("--dry-run", "Preview parameters without executing").addHelpText(
64229
64306
  "after",
64230
64307
  `
64231
64308
  Examples:
64232
64309
  $ vibe remix animated-caption video.mp4 -o captioned.mp4
64233
- $ vibe remix animated-caption video.mp4 -o out.mp4 -s bounce
64234
- $ vibe remix animated-caption video.mp4 -o out.mp4 -s karaoke-sweep --fast
64310
+ $ vibe remix animated-caption video.mp4 -o out.mp4 --style bounce
64311
+ $ vibe remix animated-caption video.mp4 -o out.mp4 --style karaoke-sweep --fast
64235
64312
 
64236
64313
  Styles:
64237
64314
  highlight (default) TikTok-style background highlight on active word (Remotion)
@@ -64247,7 +64324,7 @@ Required API Key: OPENAI_API_KEY (Whisper transcription)
64247
64324
  const startedAt = Date.now();
64248
64325
  try {
64249
64326
  const absVideoPath = resolve37(process.cwd(), videoPath);
64250
- if (!existsSync29(absVideoPath)) {
64327
+ if (!existsSync30(absVideoPath)) {
64251
64328
  exitWithError(notFoundError(absVideoPath));
64252
64329
  }
64253
64330
  if (!ANIMATED_CAPTION_STYLES.includes(options.style)) {
@@ -68747,7 +68824,7 @@ batchCommand.command("concat").description("Concatenate multiple sources into se
68747
68824
  exitWithError(generalError(`Concat failed: ${msg}`));
68748
68825
  }
68749
68826
  });
68750
- batchCommand.command("apply-effect").description("Apply an effect to multiple clips").argument("<project>", "Project file path").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, etc.)").argument("[clip-ids...]", "Clip IDs to apply effect to (or --all)").option("--all", "Apply to all clips", false).option("-d, --duration <seconds>", "Effect duration", "1").option("-s, --start <seconds>", "Effect start time (relative to clip)", "0").option("--intensity <value>", "Effect intensity (0-1)", "1").option("--dry-run", "Preview parameters without executing").action(
68827
+ batchCommand.command("apply-effect").description("Apply an effect to multiple clips").argument("<project>", "Project file path").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, etc.)").argument("[clip-ids...]", "Clip IDs to apply effect to (or --all)").option("--all", "Apply to all clips", false).option("-d, --duration <seconds>", "Effect duration", "1").option("--start <seconds>", "Effect start time (relative to clip)", "0").option("--intensity <value>", "Effect intensity (0-1)", "1").option("--dry-run", "Preview parameters without executing").action(
68751
68828
  async (projectPath, effectType, clipIds, options) => {
68752
68829
  const startedAt = Date.now();
68753
68830
  const spinner2 = ora("Applying effects...").start();
@@ -69348,7 +69425,7 @@ init_agent_host_detect();
69348
69425
  init_scene_project();
69349
69426
  init_visual_styles();
69350
69427
  init_install_skill();
69351
- import { existsSync as existsSync30 } from "node:fs";
69428
+ import { existsSync as existsSync31 } from "node:fs";
69352
69429
  import { mkdir as mkdir17, readFile as readFile30, writeFile as writeFile37 } from "node:fs/promises";
69353
69430
  import { basename as basename14, dirname as dirname14, resolve as resolve42 } from "node:path";
69354
69431
 
@@ -69639,7 +69716,7 @@ var initCommand = new Command("init").description("Scaffold a VibeFrame project
69639
69716
  const wantsGemini = targetHosts.includes("gemini-cli");
69640
69717
  const wantsCrossTool = targetHosts.some((h) => h !== "claude-code");
69641
69718
  const actions = [];
69642
- if (!existsSync30(projectDir)) {
69719
+ if (!existsSync31(projectDir)) {
69643
69720
  if (options.dryRun) {
69644
69721
  actions.push({ path: projectDir, status: "would-write", reason: "(would create directory)" });
69645
69722
  } else {
@@ -69885,7 +69962,7 @@ function resolveTargets(agent) {
69885
69962
  return [agent];
69886
69963
  }
69887
69964
  async function writeIfMissing(absPath, content, force, dryRun) {
69888
- const exists = existsSync30(absPath);
69965
+ const exists = existsSync31(absPath);
69889
69966
  if (exists && !force) {
69890
69967
  return { path: absPath, status: "skipped-exists", reason: "(use --force to overwrite)" };
69891
69968
  }
@@ -69898,7 +69975,7 @@ async function writeIfMissing(absPath, content, force, dryRun) {
69898
69975
  }
69899
69976
  async function mergeGitignore(absPath, additions, dryRun) {
69900
69977
  const marker = "# VibeFrame";
69901
- if (!existsSync30(absPath)) {
69978
+ if (!existsSync31(absPath)) {
69902
69979
  if (dryRun) return { path: absPath, status: "would-write" };
69903
69980
  await writeFile37(absPath, additions.trimStart(), "utf-8");
69904
69981
  return { path: absPath, status: "wrote" };
@@ -70241,7 +70318,7 @@ init_composer_resolve();
70241
70318
  init_scene_build();
70242
70319
  init_output();
70243
70320
  import { access as access6 } from "node:fs/promises";
70244
- import { existsSync as existsSync34 } from "node:fs";
70321
+ import { existsSync as existsSync35 } from "node:fs";
70245
70322
  import { resolve as resolve49 } from "node:path";
70246
70323
  var COMMAND_KEY_MAP = getCommandKeyMap();
70247
70324
  var FREE_COMMANDS = [
@@ -70355,7 +70432,7 @@ async function runDiagnostics() {
70355
70432
  installed = false;
70356
70433
  }
70357
70434
  } else if (toolName === "chrome") {
70358
- const { existsSync: existsSync39 } = await import("node:fs");
70435
+ const { existsSync: existsSync40 } = await import("node:fs");
70359
70436
  const { homedir: homedir6 } = await import("node:os");
70360
70437
  const { join: join16 } = await import("node:path");
70361
70438
  const chromePaths = [
@@ -70368,7 +70445,7 @@ async function runDiagnostics() {
70368
70445
  "/usr/bin/chromium",
70369
70446
  "/usr/bin/chromium-browser"
70370
70447
  ];
70371
- installed = chromePaths.some((p) => p && existsSync39(p));
70448
+ installed = chromePaths.some((p) => p && existsSync40(p));
70372
70449
  } else {
70373
70450
  installed = commandExists(toolName);
70374
70451
  }
@@ -70399,7 +70476,7 @@ async function runDiagnostics() {
70399
70476
  const cwd = process.cwd();
70400
70477
  const projectFiles = ["AGENTS.md", "CLAUDE.md", "vibe.project.yaml"].map((rel) => ({
70401
70478
  path: rel,
70402
- exists: existsSync34(resolve49(cwd, rel))
70479
+ exists: existsSync35(resolve49(cwd, rel))
70403
70480
  }));
70404
70481
  const projectInitialized = projectFiles.some((f) => f.exists);
70405
70482
  const hosts = detectAgentHosts();
@@ -70414,8 +70491,8 @@ async function runDiagnostics() {
70414
70491
  } catch (err) {
70415
70492
  if (!(err instanceof ComposerResolveError)) throw err;
70416
70493
  }
70417
- const sceneProjectInCwd = existsSync34(resolve49(cwd, "STORYBOARD.md"));
70418
- const skillInstalled = existsSync34(resolve49(cwd, "SKILL.md"));
70494
+ const sceneProjectInCwd = existsSync35(resolve49(cwd, "STORYBOARD.md"));
70495
+ const skillInstalled = existsSync35(resolve49(cwd, "SKILL.md"));
70419
70496
  return {
70420
70497
  system: {
70421
70498
  node: { version: nodeVersion, ok: true },
@@ -70581,7 +70658,7 @@ init_detect();
70581
70658
  init_ai_edit();
70582
70659
  init_output();
70583
70660
  import { resolve as resolve50 } from "node:path";
70584
- import { existsSync as existsSync35 } from "node:fs";
70661
+ import { existsSync as existsSync36 } from "node:fs";
70585
70662
  import { mkdir as mkdir20, rm as rm3 } from "node:fs/promises";
70586
70663
  var DEMO_DIR = resolve50(process.cwd(), ".vibeframe-demo");
70587
70664
  async function generateTestVideo(outputPath) {
@@ -70612,7 +70689,7 @@ async function generateTestVideo(outputPath) {
70612
70689
  "-y",
70613
70690
  outputPath
70614
70691
  ], { timeout: 3e4, maxBuffer: 50 * 1024 * 1024 });
70615
- return existsSync35(outputPath);
70692
+ return existsSync36(outputPath);
70616
70693
  } catch {
70617
70694
  try {
70618
70695
  await execSafe("ffmpeg", [
@@ -70638,7 +70715,7 @@ async function generateTestVideo(outputPath) {
70638
70715
  "-y",
70639
70716
  outputPath
70640
70717
  ], { timeout: 3e4, maxBuffer: 50 * 1024 * 1024 });
70641
- return existsSync35(outputPath);
70718
+ return existsSync36(outputPath);
70642
70719
  } catch {
70643
70720
  return false;
70644
70721
  }