@vibeframe/cli 0.91.9 → 0.92.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 +1552 -959
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -14443,8 +14443,8 @@ var init_api_keys = __esm({
14443
14443
  configKey: "imgbb",
14444
14444
  envVar: "IMGBB_API_KEY",
14445
14445
  label: "ImgBB",
14446
- showInSetup: false,
14447
- // not prompted in setup wizard — internal upload host
14446
+ showInSetup: true,
14447
+ setupDescription: "image hosting for Seedance/Kling image-to-video uploads",
14448
14448
  envExampleComment: "ImgBB API Key (image hosting \u2014 used by Kling and Seedance for image-to-video uploads)",
14449
14449
  envExampleUrl: "https://api.imgbb.com/",
14450
14450
  // ImgBB issues 32-character lowercase hex keys. Soft-warn if a pasted
@@ -14453,9 +14453,7 @@ var init_api_keys = __esm({
14453
14453
  keyFormat: { prefix: /^[a-f0-9]{32}$/, example: "32-char hex" },
14454
14454
  // ImgBB has no provider class (envvar-only); doctor still shows what it
14455
14455
  // unlocks at the apiKey level.
14456
- commandsUnlocked: [
14457
- "generate video -p kling/seedance (image-to-video upload host)"
14458
- ]
14456
+ commandsUnlocked: ["generate video -p kling/seedance (image-to-video upload host)"]
14459
14457
  });
14460
14458
  defineProvider({
14461
14459
  id: "openrouter",
@@ -25677,6 +25675,13 @@ function createDefaultConfig() {
25677
25675
  aspectRatio: "16:9",
25678
25676
  exportQuality: "standard"
25679
25677
  },
25678
+ upload: {
25679
+ provider: "imgbb",
25680
+ ttlSeconds: 3600,
25681
+ s3: {
25682
+ prefix: "vibeframe/tmp"
25683
+ }
25684
+ },
25680
25685
  repl: {
25681
25686
  autoSave: true
25682
25687
  }
@@ -25700,6 +25705,26 @@ var init_schema = __esm({
25700
25705
  });
25701
25706
 
25702
25707
  // src/config/index.ts
25708
+ var config_exports = {};
25709
+ __export(config_exports, {
25710
+ CONFIG_DIR: () => CONFIG_DIR,
25711
+ CONFIG_PATH: () => CONFIG_PATH,
25712
+ PROVIDER_ENV_VARS: () => PROVIDER_ENV_VARS,
25713
+ PROVIDER_NAMES: () => PROVIDER_NAMES,
25714
+ USER_CONFIG_DIR: () => USER_CONFIG_DIR,
25715
+ USER_CONFIG_PATH: () => USER_CONFIG_PATH,
25716
+ createDefaultConfig: () => createDefaultConfig,
25717
+ getActiveScope: () => getActiveScope,
25718
+ getApiKeyFromConfig: () => getApiKeyFromConfig,
25719
+ getConfigDir: () => getConfigDir,
25720
+ getConfigPath: () => getConfigPath,
25721
+ getProjectConfigDir: () => getProjectConfigDir,
25722
+ getProjectConfigPath: () => getProjectConfigPath,
25723
+ isConfigured: () => isConfigured,
25724
+ loadConfig: () => loadConfig,
25725
+ saveConfig: () => saveConfig,
25726
+ updateProviderKey: () => updateProviderKey
25727
+ });
25703
25728
  import { resolve as resolve4 } from "node:path";
25704
25729
  import { homedir } from "node:os";
25705
25730
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access as access2 } from "node:fs/promises";
@@ -25734,6 +25759,11 @@ function applyDefaults(parsed) {
25734
25759
  llm: { ...defaults.llm, ...parsed.llm },
25735
25760
  providers: { ...defaults.providers, ...parsed.providers },
25736
25761
  defaults: { ...defaults.defaults, ...parsed.defaults },
25762
+ upload: {
25763
+ ...defaults.upload,
25764
+ ...parsed.upload,
25765
+ s3: { ...defaults.upload.s3, ...parsed.upload?.s3 }
25766
+ },
25737
25767
  repl: { ...defaults.repl, ...parsed.repl }
25738
25768
  };
25739
25769
  }
@@ -25761,6 +25791,11 @@ async function loadConfig(options = {}) {
25761
25791
  llm: { ...user.llm, ...project2.llm },
25762
25792
  providers: { ...user.providers, ...project2.providers },
25763
25793
  defaults: { ...user.defaults, ...project2.defaults },
25794
+ upload: {
25795
+ ...user.upload,
25796
+ ...project2.upload,
25797
+ s3: { ...user.upload.s3, ...project2.upload.s3 }
25798
+ },
25764
25799
  repl: { ...user.repl, ...project2.repl }
25765
25800
  };
25766
25801
  }
@@ -25802,7 +25837,14 @@ async function getApiKeyFromConfig(providerKey) {
25802
25837
  if (envVar) return process.env[envVar];
25803
25838
  return void 0;
25804
25839
  }
25805
- var import_yaml, USER_CONFIG_DIR, USER_CONFIG_PATH, CONFIG_PATH;
25840
+ async function updateProviderKey(providerKey, apiKey, options = {}) {
25841
+ const scope = options.scope ?? await getActiveScope(options.cwd);
25842
+ let config2 = await loadConfig({ scope, cwd: options.cwd });
25843
+ if (!config2) config2 = createDefaultConfig();
25844
+ config2.providers[providerKey] = apiKey;
25845
+ await saveConfig(config2, { scope, cwd: options.cwd });
25846
+ }
25847
+ var import_yaml, USER_CONFIG_DIR, USER_CONFIG_PATH, CONFIG_DIR, CONFIG_PATH;
25806
25848
  var init_config = __esm({
25807
25849
  "src/config/index.ts"() {
25808
25850
  "use strict";
@@ -25811,6 +25853,7 @@ var init_config = __esm({
25811
25853
  init_schema();
25812
25854
  USER_CONFIG_DIR = resolve4(homedir(), ".vibeframe");
25813
25855
  USER_CONFIG_PATH = resolve4(USER_CONFIG_DIR, "config.yaml");
25856
+ CONFIG_DIR = USER_CONFIG_DIR;
25814
25857
  CONFIG_PATH = USER_CONFIG_PATH;
25815
25858
  }
25816
25859
  });
@@ -25910,7 +25953,9 @@ async function getApiKey(envVar, providerName, optionValue) {
25910
25953
  }
25911
25954
  console.log();
25912
25955
  console.log(source_default.yellow(`${providerName} API key not found.`));
25913
- console.log(source_default.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`));
25956
+ console.log(
25957
+ source_default.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`)
25958
+ );
25914
25959
  console.log();
25915
25960
  const apiKey = await prompt(source_default.cyan(`Enter ${providerName} API key: `), true);
25916
25961
  if (!apiKey || apiKey.trim() === "") {
@@ -25969,7 +26014,8 @@ var init_api_key = __esm({
25969
26014
  ELEVENLABS_API_KEY: "https://elevenlabs.io/app/settings/api-keys",
25970
26015
  RUNWAY_API_SECRET: "https://app.runwayml.com/settings/api-keys",
25971
26016
  KLING_API_KEY: "https://klingai.com/dev",
25972
- REPLICATE_API_TOKEN: "https://replicate.com/account/api-tokens"
26017
+ REPLICATE_API_TOKEN: "https://replicate.com/account/api-tokens",
26018
+ IMGBB_API_KEY: "https://api.imgbb.com/"
25973
26019
  };
25974
26020
  ApiKeyError = class extends Error {
25975
26021
  envVar;
@@ -33353,29 +33399,61 @@ var init_video_extend = __esm({
33353
33399
  });
33354
33400
 
33355
33401
  // src/utils/provider-resolver.ts
33402
+ function loadProviderDefaultsFromConfig(config2) {
33403
+ configDefaults = {};
33404
+ if (config2?.defaults.imageProvider) configDefaults.image = config2.defaults.imageProvider;
33405
+ if (config2?.defaults.videoProvider) configDefaults.video = config2.defaults.videoProvider;
33406
+ configProviderKeys = new Set(
33407
+ Object.entries(config2?.providers ?? {}).filter(([, value]) => Boolean(value)).map(([key]) => key)
33408
+ );
33409
+ }
33410
+ async function loadProviderDefaults() {
33411
+ try {
33412
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
33413
+ const config2 = await loadConfig2();
33414
+ loadProviderDefaultsFromConfig(config2);
33415
+ } catch {
33416
+ configDefaults = null;
33417
+ configProviderKeys = null;
33418
+ }
33419
+ }
33420
+ function providerKeyForEnvVar(envVar) {
33421
+ const envVars = getProviderEnvVars();
33422
+ for (const [providerKey, candidateEnvVar] of Object.entries(envVars)) {
33423
+ if (candidateEnvVar === envVar) return providerKey;
33424
+ }
33425
+ return null;
33426
+ }
33427
+ function hasCandidateKey(candidate) {
33428
+ if (candidate.envVar === null) return true;
33429
+ if (hasApiKey(candidate.envVar)) return true;
33430
+ const providerKey = providerKeyForEnvVar(candidate.envVar);
33431
+ return providerKey ? Boolean(configProviderKeys?.has(providerKey)) : false;
33432
+ }
33356
33433
  function resolveProvider(category) {
33357
33434
  const candidates = getProvidersFor(category);
33358
33435
  if (candidates.length === 0) return null;
33359
33436
  if (configDefaults?.[category]) {
33360
33437
  const preferred = candidates.find((c) => c.name === configDefaults[category]);
33361
- if (preferred && (preferred.envVar === null || hasApiKey(preferred.envVar))) {
33438
+ if (preferred && hasCandidateKey(preferred)) {
33362
33439
  return { name: preferred.name, label: preferred.label };
33363
33440
  }
33364
33441
  }
33365
33442
  for (const candidate of candidates) {
33366
- if (candidate.envVar === null || hasApiKey(candidate.envVar)) {
33443
+ if (hasCandidateKey(candidate)) {
33367
33444
  return { name: candidate.name, label: candidate.label };
33368
33445
  }
33369
33446
  }
33370
33447
  return null;
33371
33448
  }
33372
- var configDefaults;
33449
+ var configDefaults, configProviderKeys;
33373
33450
  var init_provider_resolver = __esm({
33374
33451
  "src/utils/provider-resolver.ts"() {
33375
33452
  "use strict";
33376
33453
  init_dist2();
33377
33454
  init_api_key();
33378
33455
  configDefaults = null;
33456
+ configProviderKeys = null;
33379
33457
  }
33380
33458
  });
33381
33459
 
@@ -33412,13 +33490,26 @@ import { resolve as resolve23, dirname as dirname7 } from "node:path";
33412
33490
  import { fileURLToPath } from "node:url";
33413
33491
  import { writeFile as writeFile21, mkdir as mkdir11 } from "node:fs/promises";
33414
33492
  function registerImageCommand(parent) {
33415
- 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", `
33493
+ 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(
33494
+ "-p, --provider <provider>",
33495
+ "Provider: openai (default when OPENAI_API_KEY set), gemini, grok, runway"
33496
+ ).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(
33497
+ "-r, --ratio <ratio>",
33498
+ "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)",
33499
+ "1:1"
33500
+ ).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(
33501
+ "-m, --model <model>",
33502
+ "Model. Gemini: flash, 3.1-flash, latest, pro. OpenAI: 1.5 (default), 2 (gpt-image-2)"
33503
+ ).option("--dry-run", "Preview parameters without executing").addHelpText(
33504
+ "after",
33505
+ `
33416
33506
  Examples:
33417
33507
  $ vibe generate image "a sunset over the ocean" -o sunset.png
33418
33508
  $ vibe gen img "logo design" -o logo.png -p openai
33419
33509
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
33420
33510
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
33421
- $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
33511
+ $ vibe gen img "product shot" --dry-run --json`
33512
+ ).action(async (prompt3, options) => {
33422
33513
  const startedAt = Date.now();
33423
33514
  try {
33424
33515
  if (!prompt3) {
@@ -33429,10 +33520,7 @@ Examples:
33429
33520
  }
33430
33521
  } else {
33431
33522
  exitWithError(
33432
- usageError(
33433
- "Prompt argument is required.",
33434
- "Usage: vibe generate image <prompt>"
33435
- )
33523
+ usageError("Prompt argument is required.", "Usage: vibe generate image <prompt>")
33436
33524
  );
33437
33525
  }
33438
33526
  }
@@ -33440,13 +33528,16 @@ Examples:
33440
33528
  if (options.output) {
33441
33529
  validateOutputPath(options.output);
33442
33530
  }
33531
+ await loadProviderDefaults();
33443
33532
  if (options.count !== void 0) {
33444
33533
  const n = parseInt(options.count, 10);
33445
33534
  if (!Number.isFinite(n) || n < 1 || n > 10) {
33446
- exitWithError(usageError(
33447
- `Invalid --count: ${options.count}`,
33448
- "Must be an integer between 1 and 10."
33449
- ));
33535
+ exitWithError(
33536
+ usageError(
33537
+ `Invalid --count: ${options.count}`,
33538
+ "Must be an integer between 1 and 10."
33539
+ )
33540
+ );
33450
33541
  }
33451
33542
  }
33452
33543
  const imageRegistry = getProvidersFor("image");
@@ -33511,11 +33602,9 @@ Examples:
33511
33602
  const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
33512
33603
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
33513
33604
  if (provider === "openai") {
33514
- const { result, modelLabel } = await executeOpenAIImageGenerate(
33515
- prompt3,
33516
- options,
33517
- { apiKey }
33518
- );
33605
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, {
33606
+ apiKey
33607
+ });
33519
33608
  if (!result.success || !result.images) {
33520
33609
  spinner2.fail(result.error || "Image generation failed");
33521
33610
  exitWithError(apiError(result.error || "Image generation failed", true));
@@ -33737,9 +33826,7 @@ Examples:
33737
33826
  spinner2.fail(result.error || "Image generation failed");
33738
33827
  exitWithError(apiError(result.error || "Image generation failed", true));
33739
33828
  }
33740
- spinner2.succeed(
33741
- source_default.green(`Generated ${result.images.length} image(s) with xAI Grok`)
33742
- );
33829
+ spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with xAI Grok`));
33743
33830
  if (isJsonMode()) {
33744
33831
  const outputPath = options.output ? resolve23(process.cwd(), options.output) : void 0;
33745
33832
  if (outputPath && result.images.length > 0) {
@@ -34972,606 +35059,206 @@ var init_video_utils = __esm({
34972
35059
  }
34973
35060
  });
34974
35061
 
34975
- // src/commands/_shared/video-providers.ts
34976
- async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
34977
- const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
34978
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
34979
- try {
34980
- const result = await grok.generateVideo(prompt3, {
34981
- prompt: prompt3,
34982
- duration: options.duration,
34983
- aspectRatio: options.aspectRatio,
34984
- referenceImage: options.referenceImage
34985
- });
34986
- if (result.status !== "failed" && result.id) {
34987
- return { requestId: result.id };
34988
- }
34989
- const providerErr = result.error || "Grok returned failed status";
34990
- if (attempt < maxRetries) {
34991
- onProgress?.(
34992
- `\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`
34993
- );
34994
- await sleep(RETRY_DELAY_MS);
34995
- } else {
34996
- console.error(source_default.dim(`
34997
- [Grok error: ${providerErr}]`));
34998
- }
34999
- } catch (err) {
35000
- const errMsg = err instanceof Error ? err.message : String(err);
35001
- if (attempt < maxRetries) {
35002
- onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35003
- await sleep(RETRY_DELAY_MS);
35004
- } else {
35005
- console.error(source_default.dim(`
35006
- [Grok error: ${errMsg}]`));
35007
- }
35008
- }
35009
- }
35010
- return null;
35062
+ // src/utils/upload-host.ts
35063
+ import { createHash as createHash4, createHmac as createHmac2, randomUUID } from "node:crypto";
35064
+ import { extname as extname3 } from "node:path";
35065
+ function envNumber(name) {
35066
+ const value = process.env[name];
35067
+ if (!value) return void 0;
35068
+ const n = Number.parseInt(value, 10);
35069
+ return Number.isFinite(n) && n > 0 ? n : void 0;
35011
35070
  }
35012
- async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
35013
- const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
35014
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
35015
- try {
35016
- const result = await kling.generateVideo(prompt3, {
35017
- prompt: prompt3,
35018
- // Pass reference image (base64 or URL) - KlingProvider handles v1.5 fallback for base64
35019
- referenceImage: options.referenceImage,
35020
- duration: options.duration,
35021
- aspectRatio: options.aspectRatio,
35022
- mode: "std"
35023
- // std mode for faster generation
35024
- });
35025
- if (result.status !== "failed" && result.id) {
35026
- return {
35027
- taskId: result.id,
35028
- type: options.referenceImage ? "image2video" : "text2video"
35029
- };
35030
- }
35031
- const providerErr = result.error || "Kling returned failed status";
35032
- if (attempt < maxRetries) {
35033
- onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35034
- await sleep(RETRY_DELAY_MS);
35035
- } else {
35036
- console.error(source_default.dim(`
35037
- [Kling error: ${providerErr}]`));
35038
- }
35039
- } catch (err) {
35040
- const errMsg = err instanceof Error ? err.message : String(err);
35041
- if (attempt < maxRetries) {
35042
- onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35043
- await sleep(RETRY_DELAY_MS);
35044
- } else {
35045
- console.error(source_default.dim(`
35046
- [Kling error: ${errMsg}]`));
35047
- }
35048
- }
35049
- }
35050
- return null;
35071
+ function safePrefix(prefix) {
35072
+ return (prefix ?? "vibeframe/tmp").replace(/^\/+|\/+$/g, "");
35051
35073
  }
35052
- async function generateVideoWithRetryRunway(runway, segment, referenceImage, options, maxRetries, onProgress) {
35053
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
35054
- try {
35055
- const result = await runway.generateVideo(segment.visuals, {
35056
- prompt: segment.visuals,
35057
- referenceImage,
35058
- duration: options.duration,
35059
- aspectRatio: options.aspectRatio
35060
- });
35061
- if (result.status !== "failed" && result.id) {
35062
- return { taskId: result.id };
35063
- }
35064
- const providerErr = result.error || "Runway returned failed status";
35065
- if (attempt < maxRetries) {
35066
- onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35067
- await sleep(RETRY_DELAY_MS);
35068
- } else {
35069
- console.error(source_default.dim(`
35070
- [Runway error: ${providerErr}]`));
35071
- }
35072
- } catch (err) {
35073
- const errMsg = err instanceof Error ? err.message : String(err);
35074
- if (attempt < maxRetries) {
35075
- onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35076
- await sleep(RETRY_DELAY_MS);
35077
- } else {
35078
- console.error(source_default.dim(`
35079
- [Runway error: ${errMsg}]`));
35080
- }
35081
- }
35082
- }
35083
- return null;
35074
+ function extensionFor(opts) {
35075
+ const fromName = opts?.filename ? extname3(opts.filename).replace(/^\./, "") : "";
35076
+ if (fromName) return fromName.toLowerCase();
35077
+ const mime = opts?.mimeType ?? "image/png";
35078
+ if (mime === "image/jpeg") return "jpg";
35079
+ if (mime === "image/webp") return "webp";
35080
+ if (mime === "image/gif") return "gif";
35081
+ return "png";
35084
35082
  }
35085
- async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, onProgress) {
35086
- const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
35087
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
35088
- try {
35089
- const result = await gemini.generateVideo(prompt3, {
35090
- prompt: prompt3,
35091
- referenceImage: options.referenceImage,
35092
- duration: options.duration,
35093
- aspectRatio: options.aspectRatio,
35094
- model: "veo-3.1-fast-generate-preview"
35095
- });
35096
- if (result.status !== "failed" && result.id) {
35097
- return { operationName: result.id };
35098
- }
35099
- const providerErr = result.error || "Veo returned failed status";
35100
- if (attempt < maxRetries) {
35101
- onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35102
- await sleep(RETRY_DELAY_MS);
35103
- } else {
35104
- console.error(source_default.dim(`
35105
- [Veo error: ${providerErr}]`));
35106
- }
35107
- } catch (err) {
35108
- const errMsg = err instanceof Error ? err.message : String(err);
35109
- if (attempt < maxRetries) {
35110
- onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
35111
- await sleep(RETRY_DELAY_MS);
35112
- } else {
35113
- console.error(source_default.dim(`
35114
- [Veo error: ${errMsg}]`));
35115
- }
35116
- }
35117
- }
35118
- return null;
35083
+ function hmac(key, value) {
35084
+ return createHmac2("sha256", key).update(value).digest();
35119
35085
  }
35120
- var init_video_providers = __esm({
35121
- "src/commands/_shared/video-providers.ts"() {
35122
- "use strict";
35123
- init_source();
35124
- init_video_utils();
35125
- }
35126
- });
35127
-
35128
- // src/commands/ai-script-pipeline.ts
35129
- import { readFile as readFile13, writeFile as writeFile23, unlink as unlink2, rename as rename2 } from "node:fs/promises";
35130
- import { resolve as resolve25, extname as extname3 } from "node:path";
35131
- import { existsSync as existsSync14 } from "node:fs";
35132
- async function executeRegenerateScene(options) {
35133
- const result = {
35134
- success: false,
35135
- regeneratedScenes: [],
35136
- failedScenes: []
35086
+ function sha256Hex(value) {
35087
+ return createHash4("sha256").update(value).digest("hex");
35088
+ }
35089
+ function awsEncode(value) {
35090
+ return encodeURIComponent(value).replace(
35091
+ /[!'()*]/g,
35092
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
35093
+ );
35094
+ }
35095
+ function presignS3PutUrl(params) {
35096
+ const now = /* @__PURE__ */ new Date();
35097
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
35098
+ const dateStamp = amzDate.slice(0, 8);
35099
+ const host = `${params.bucket}.s3.${params.region}.amazonaws.com`;
35100
+ const credentialScope = `${dateStamp}/${params.region}/s3/aws4_request`;
35101
+ const canonicalUri = `/${params.key.split("/").map(awsEncode).join("/")}`;
35102
+ const query = {
35103
+ "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
35104
+ "X-Amz-Credential": `${params.accessKeyId}/${credentialScope}`,
35105
+ "X-Amz-Date": amzDate,
35106
+ "X-Amz-Expires": String(params.ttlSeconds),
35107
+ "X-Amz-SignedHeaders": "host"
35137
35108
  };
35138
- try {
35139
- const outputDir = resolve25(process.cwd(), options.projectDir);
35140
- if (!existsSync14(outputDir)) {
35141
- return { ...result, error: `Project directory not found: ${outputDir}` };
35142
- }
35143
- const yamlPath = resolve25(outputDir, "storyboard.yaml");
35144
- const jsonPath = resolve25(outputDir, "storyboard.json");
35145
- const storyboardPath = existsSync14(yamlPath) ? yamlPath : existsSync14(jsonPath) ? jsonPath : null;
35146
- if (!storyboardPath) {
35147
- return { ...result, error: `Storyboard not found in: ${outputDir} (expected storyboard.yaml or storyboard.json)` };
35148
- }
35149
- const storyboardContent = await readFile13(storyboardPath, "utf-8");
35150
- const segments = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.parse)(storyboardContent).scenes : JSON.parse(storyboardContent);
35151
- for (const sceneNum of options.scenes) {
35152
- if (sceneNum < 1 || sceneNum > segments.length) {
35153
- return { ...result, error: `Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.` };
35154
- }
35155
- }
35156
- const regenerateVideo = options.videoOnly || !options.narrationOnly && !options.imageOnly;
35157
- const regenerateNarration = options.narrationOnly || !options.videoOnly && !options.imageOnly;
35158
- const regenerateImage = options.imageOnly || !options.videoOnly && !options.narrationOnly;
35159
- let videoApiKey;
35160
- if (regenerateVideo) {
35161
- const generatorKeyMap = {
35162
- grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
35163
- kling: { envVar: "KLING_API_KEY", name: "Kling" },
35164
- runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
35165
- veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" }
35166
- };
35167
- const generator = options.generator || "grok";
35168
- const genInfo = generatorKeyMap[generator];
35169
- if (!genInfo) {
35170
- return { ...result, error: `Invalid generator: ${generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
35171
- }
35172
- videoApiKey = await getApiKey(genInfo.envVar, genInfo.name) ?? void 0;
35173
- if (!videoApiKey) {
35174
- return { ...result, error: `${genInfo.name} API key required. Run 'vibe setup' or set ${genInfo.envVar} in .env` };
35175
- }
35176
- }
35177
- let imageApiKey;
35178
- if (regenerateImage) {
35179
- const imageProvider = options.imageProvider || "openai";
35180
- const imageKeyMap = {
35181
- openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
35182
- gemini: { envVar: "GOOGLE_API_KEY", name: "Google" },
35183
- grok: { envVar: "XAI_API_KEY", name: "xAI" }
35184
- };
35185
- const info = imageKeyMap[imageProvider];
35186
- if (!info) {
35187
- return { ...result, error: `Invalid imageProvider: ${imageProvider}` };
35188
- }
35189
- imageApiKey = await getApiKey(info.envVar, info.name) ?? void 0;
35190
- if (!imageApiKey) {
35191
- return { ...result, error: `${info.name} API key required. Run 'vibe setup' or set ${info.envVar} in .env` };
35192
- }
35109
+ if (params.sessionToken) {
35110
+ query["X-Amz-Security-Token"] = params.sessionToken;
35111
+ }
35112
+ const canonicalQuery = Object.entries(query).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${awsEncode(key)}=${awsEncode(value)}`).join("&");
35113
+ const canonicalRequest = [
35114
+ "PUT",
35115
+ canonicalUri,
35116
+ canonicalQuery,
35117
+ `host:${host}`,
35118
+ "",
35119
+ "host",
35120
+ "UNSIGNED-PAYLOAD"
35121
+ ].join("\n");
35122
+ const stringToSign = [
35123
+ "AWS4-HMAC-SHA256",
35124
+ amzDate,
35125
+ credentialScope,
35126
+ sha256Hex(canonicalRequest)
35127
+ ].join("\n");
35128
+ const dateKey = hmac(`AWS4${params.secretAccessKey}`, dateStamp);
35129
+ const regionKey = hmac(dateKey, params.region);
35130
+ const serviceKey = hmac(regionKey, "s3");
35131
+ const signingKey = hmac(serviceKey, "aws4_request");
35132
+ const signature = createHmac2("sha256", signingKey).update(stringToSign).digest("hex");
35133
+ return `https://${host}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
35134
+ }
35135
+ function publicS3Url(params) {
35136
+ if (params.publicBaseUrl) {
35137
+ return `${params.publicBaseUrl.replace(/\/+$/g, "")}/${params.key.split("/").map(awsEncode).join("/")}`;
35138
+ }
35139
+ return `https://${params.bucket}.s3.${params.region}.amazonaws.com/${params.key.split("/").map(awsEncode).join("/")}`;
35140
+ }
35141
+ async function resolveUploadSettings() {
35142
+ const config2 = await loadConfig();
35143
+ const provider = process.env.VIBE_UPLOAD_PROVIDER ?? config2?.upload.provider ?? "imgbb";
35144
+ return {
35145
+ provider,
35146
+ ttlSeconds: envNumber("VIBE_UPLOAD_TTL_SECONDS") ?? config2?.upload.ttlSeconds ?? 3600,
35147
+ s3: {
35148
+ bucket: process.env.VIBE_UPLOAD_S3_BUCKET ?? config2?.upload.s3?.bucket,
35149
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? config2?.upload.s3?.region,
35150
+ prefix: process.env.VIBE_UPLOAD_S3_PREFIX ?? config2?.upload.s3?.prefix,
35151
+ publicBaseUrl: process.env.VIBE_UPLOAD_PUBLIC_BASE_URL ?? config2?.upload.s3?.publicBaseUrl
35193
35152
  }
35194
- let elevenlabsApiKey;
35195
- if (regenerateNarration) {
35196
- elevenlabsApiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs") ?? void 0;
35197
- if (!elevenlabsApiKey) {
35198
- return { ...result, error: "ElevenLabs API key required. Run 'vibe setup' or set ELEVENLABS_API_KEY in .env" };
35199
- }
35153
+ };
35154
+ }
35155
+ async function resolveUploadHost() {
35156
+ const settings = await resolveUploadSettings();
35157
+ if (settings.provider === "s3") {
35158
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
35159
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
35160
+ const sessionToken = process.env.AWS_SESSION_TOKEN;
35161
+ const { bucket, region } = settings.s3;
35162
+ if (!accessKeyId || !secretAccessKey || !bucket || !region) {
35163
+ throw new Error(
35164
+ "S3 upload host requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and VIBE_UPLOAD_S3_BUCKET."
35165
+ );
35200
35166
  }
35201
- let storyboardMutated = false;
35202
- for (const sceneNum of options.scenes) {
35203
- const segment = segments[sceneNum - 1];
35204
- const narrationPath = resolve25(outputDir, `narration-${sceneNum}.mp3`);
35205
- const imagePath = resolve25(outputDir, `scene-${sceneNum}.png`);
35206
- const videoPath = resolve25(outputDir, `scene-${sceneNum}.mp4`);
35207
- let sceneFailed = false;
35208
- if (regenerateNarration && elevenlabsApiKey) {
35209
- options.onProgress?.(`Scene ${sceneNum}: regenerating narration...`);
35210
- const elevenlabs = new ElevenLabsProvider();
35211
- await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
35212
- const narrationText = segment.narration || segment.description;
35213
- const ttsResult = await elevenlabs.textToSpeech(narrationText, {
35214
- voiceId: options.voice
35167
+ return {
35168
+ provider: "s3",
35169
+ async uploadImage(imageBuffer, opts) {
35170
+ const ext = extensionFor(opts);
35171
+ const prefix = safePrefix(settings.s3.prefix);
35172
+ const key = `${prefix}/${Date.now()}-${randomUUID()}.${ext}`;
35173
+ const presignedUrl = presignS3PutUrl({
35174
+ accessKeyId,
35175
+ secretAccessKey,
35176
+ sessionToken,
35177
+ region,
35178
+ bucket,
35179
+ key,
35180
+ ttlSeconds: settings.ttlSeconds
35215
35181
  });
35216
- if (ttsResult.success && ttsResult.audioBuffer) {
35217
- await writeFile23(narrationPath, ttsResult.audioBuffer);
35218
- segment.duration = await getAudioDuration(narrationPath);
35219
- storyboardMutated = true;
35220
- } else {
35221
- sceneFailed = true;
35222
- }
35223
- }
35224
- if (!sceneFailed && regenerateImage && imageApiKey) {
35225
- options.onProgress?.(`Scene ${sceneNum}: regenerating image...`);
35226
- const imageProvider = options.imageProvider || "openai";
35227
- const characterDesc = segment.characterDescription || segments[0]?.characterDescription;
35228
- let imagePrompt = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
35229
- if (characterDesc) {
35230
- imagePrompt = `${imagePrompt}
35231
-
35232
- IMPORTANT - Character appearance must match exactly: ${characterDesc}`;
35233
- }
35234
- let referenceImageBuffer;
35235
- const refSceneNum = options.referenceScene;
35236
- if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
35237
- const refImagePath = resolve25(outputDir, `scene-${refSceneNum}.png`);
35238
- if (existsSync14(refImagePath)) {
35239
- referenceImageBuffer = await readFile13(refImagePath);
35240
- }
35241
- } else if (!refSceneNum) {
35242
- for (let i = 1; i <= segments.length; i++) {
35243
- if (i !== sceneNum) {
35244
- const otherImagePath = resolve25(outputDir, `scene-${i}.png`);
35245
- if (existsSync14(otherImagePath)) {
35246
- referenceImageBuffer = await readFile13(otherImagePath);
35247
- break;
35248
- }
35249
- }
35250
- }
35182
+ const response = await fetch(presignedUrl, {
35183
+ method: "PUT",
35184
+ headers: {
35185
+ "content-type": opts?.mimeType ?? "application/octet-stream"
35186
+ },
35187
+ body: new Uint8Array(imageBuffer)
35188
+ });
35189
+ if (!response.ok) {
35190
+ throw new Error(`S3 upload failed (${response.status}): ${response.statusText}`);
35251
35191
  }
35252
- const dalleImageSizes = {
35253
- "16:9": "1536x1024",
35254
- "9:16": "1024x1536",
35255
- "1:1": "1024x1024"
35192
+ return {
35193
+ provider: "s3",
35194
+ url: publicS3Url({ region, bucket, key, publicBaseUrl: settings.s3.publicBaseUrl }),
35195
+ expiresAt: new Date(Date.now() + settings.ttlSeconds * 1e3).toISOString()
35256
35196
  };
35257
- let imageBuffer;
35258
- let imageUrl;
35259
- if (imageProvider === "openai") {
35260
- const openaiImage = new OpenAIImageProvider();
35261
- await openaiImage.initialize({ apiKey: imageApiKey });
35262
- const imageResult = await openaiImage.generateImage(imagePrompt, {
35263
- size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
35264
- quality: "standard"
35265
- });
35266
- if (imageResult.success && imageResult.images?.[0]) {
35267
- const img = imageResult.images[0];
35268
- if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
35269
- else if (img.url) imageUrl = img.url;
35270
- }
35271
- } else if (imageProvider === "gemini") {
35272
- const gemini = new GeminiProvider();
35273
- await gemini.initialize({ apiKey: imageApiKey });
35274
- if (referenceImageBuffer) {
35275
- const simplifiedVisuals = segment.visuals.split(/[,.]/).find(
35276
- (part) => part.includes("standing") || part.includes("sitting") || part.includes("walking") || part.includes("lying") || part.includes("reaching") || part.includes("looking") || part.includes("working") || part.includes("coding") || part.includes("typing")
35277
- ) || segment.visuals.split(".")[0];
35278
- const editPrompt = `Generate a new image showing the SAME SINGLE person from the reference image in a new scene.
35279
-
35280
- REFERENCE: Look at the person in the reference image - their face, hair, build, and overall appearance.
35281
-
35282
- NEW SCENE: ${simplifiedVisuals}
35283
-
35284
- CRITICAL RULES:
35285
- 1. Show ONLY ONE person - the exact same individual from the reference image
35286
- 2. The person must have the IDENTICAL face, hair style, and body type
35287
- 3. Do NOT show multiple people or duplicate the character
35288
- 4. Create a single moment in time, one pose, one action
35289
- 5. Match the art style and quality of the reference image
35290
-
35291
- Generate the single-person scene image now.`;
35292
- const imageResult = await gemini.editImage([referenceImageBuffer], editPrompt, {
35293
- aspectRatio: options.aspectRatio || "16:9"
35294
- });
35295
- if (imageResult.success && imageResult.images?.[0]?.base64) {
35296
- imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
35297
- }
35298
- } else {
35299
- const imageResult = await gemini.generateImage(imagePrompt, {
35300
- aspectRatio: options.aspectRatio || "16:9"
35301
- });
35302
- if (imageResult.success && imageResult.images?.[0]?.base64) {
35303
- imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
35304
- }
35305
- }
35306
- } else if (imageProvider === "grok") {
35307
- const grok = new GrokProvider();
35308
- await grok.initialize({ apiKey: imageApiKey });
35309
- const imageResult = await grok.generateImage(imagePrompt, {
35310
- aspectRatio: options.aspectRatio || "16:9"
35311
- });
35312
- if (imageResult.success && imageResult.images?.[0]) {
35313
- const img = imageResult.images[0];
35314
- if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
35315
- else if (img.url) imageUrl = img.url;
35316
- }
35317
- }
35318
- if (imageBuffer) {
35319
- await writeFile23(imagePath, imageBuffer);
35320
- } else if (imageUrl) {
35321
- const response = await fetch(imageUrl);
35322
- const buffer = Buffer.from(await response.arrayBuffer());
35323
- await writeFile23(imagePath, buffer);
35324
- } else {
35325
- sceneFailed = true;
35326
- }
35327
- }
35328
- if (sceneFailed) {
35329
- result.failedScenes.push(sceneNum);
35330
- continue;
35331
35197
  }
35332
- if (regenerateVideo && videoApiKey) {
35333
- options.onProgress?.(`Scene ${sceneNum}: regenerating video (${options.generator || "grok"})...`);
35334
- if (!existsSync14(imagePath)) {
35335
- result.failedScenes.push(sceneNum);
35336
- continue;
35337
- }
35338
- const imageBuffer = await readFile13(imagePath);
35339
- const videoDuration = segment.duration > 5 ? 10 : 5;
35340
- const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
35341
- if (options.generator === "grok") {
35342
- const grok = new GrokProvider();
35343
- await grok.initialize({ apiKey: videoApiKey });
35344
- const ext = extname3(imagePath).toLowerCase().slice(1);
35345
- const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
35346
- const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
35347
- const grokDuration = Math.min(15, Math.max(1, segment.duration));
35348
- const taskResult = await generateVideoWithRetryGrok(
35349
- grok,
35350
- segment,
35351
- {
35352
- duration: grokDuration,
35353
- aspectRatio: options.aspectRatio || "16:9",
35354
- referenceImage
35355
- },
35356
- maxRetries
35357
- );
35358
- if (taskResult) {
35359
- try {
35360
- const waitResult = await grok.waitForCompletion(taskResult.requestId, void 0, 3e5);
35361
- if (waitResult.status === "completed" && waitResult.videoUrl) {
35362
- const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
35363
- await writeFile23(videoPath, buffer);
35364
- const targetDuration = segment.duration;
35365
- const actualVideoDuration = await getVideoDuration(videoPath);
35366
- if (actualVideoDuration < targetDuration - 0.1) {
35367
- const extendedPath = resolve25(outputDir, `scene-${sceneNum}-extended.mp4`);
35368
- await extendVideoNaturally(videoPath, targetDuration, extendedPath);
35369
- await unlink2(videoPath);
35370
- await rename2(extendedPath, videoPath);
35371
- }
35372
- result.regeneratedScenes.push(sceneNum);
35373
- } else {
35374
- logSceneFailure("Grok", `scene ${sceneNum}`, waitResult);
35375
- result.failedScenes.push(sceneNum);
35376
- }
35377
- } catch (err) {
35378
- logSceneFailure("Grok", `scene ${sceneNum}`, err);
35379
- result.failedScenes.push(sceneNum);
35380
- }
35381
- } else {
35382
- result.failedScenes.push(sceneNum);
35383
- }
35384
- } else if (options.generator === "veo") {
35385
- const veo = new GeminiProvider();
35386
- await veo.initialize({ apiKey: videoApiKey });
35387
- const ext = extname3(imagePath).toLowerCase().slice(1);
35388
- const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
35389
- const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
35390
- const veoDuration = segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4;
35391
- const taskResult = await generateVideoWithRetryVeo(
35392
- veo,
35393
- segment,
35394
- {
35395
- duration: veoDuration,
35396
- aspectRatio: options.aspectRatio || "16:9",
35397
- referenceImage
35398
- },
35399
- maxRetries
35400
- );
35401
- if (taskResult) {
35402
- try {
35403
- const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, void 0, 3e5);
35404
- if (waitResult.status === "completed" && waitResult.videoUrl) {
35405
- const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
35406
- await writeFile23(videoPath, buffer);
35407
- const targetDuration = segment.duration;
35408
- const actualVideoDuration = await getVideoDuration(videoPath);
35409
- if (actualVideoDuration < targetDuration - 0.1) {
35410
- const extendedPath = resolve25(outputDir, `scene-${sceneNum}-extended.mp4`);
35411
- await extendVideoNaturally(videoPath, targetDuration, extendedPath);
35412
- await unlink2(videoPath);
35413
- await rename2(extendedPath, videoPath);
35414
- }
35415
- result.regeneratedScenes.push(sceneNum);
35416
- } else {
35417
- logSceneFailure("Veo", `scene ${sceneNum}`, waitResult);
35418
- result.failedScenes.push(sceneNum);
35419
- }
35420
- } catch (err) {
35421
- logSceneFailure("Veo", `scene ${sceneNum}`, err);
35422
- result.failedScenes.push(sceneNum);
35423
- }
35424
- } else {
35425
- result.failedScenes.push(sceneNum);
35426
- }
35427
- } else if (options.generator === "kling" || !options.generator) {
35428
- const kling = new KlingProvider();
35429
- await kling.initialize({ apiKey: videoApiKey });
35430
- if (!kling.isConfigured()) {
35431
- result.failedScenes.push(sceneNum);
35432
- continue;
35433
- }
35434
- const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
35435
- let imageUrl;
35436
- if (imgbbApiKey) {
35437
- const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
35438
- if (uploadResult.success && uploadResult.url) {
35439
- imageUrl = uploadResult.url;
35440
- }
35441
- }
35442
- const taskResult = await generateVideoWithRetryKling(
35443
- kling,
35444
- segment,
35445
- {
35446
- duration: videoDuration,
35447
- aspectRatio: options.aspectRatio || "16:9",
35448
- referenceImage: imageUrl
35449
- },
35450
- maxRetries
35451
- );
35452
- if (taskResult) {
35453
- try {
35454
- const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, void 0, 6e5);
35455
- if (waitResult.status === "completed" && waitResult.videoUrl) {
35456
- const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
35457
- await writeFile23(videoPath, buffer);
35458
- await extendVideoToTarget(
35459
- videoPath,
35460
- segment.duration,
35461
- outputDir,
35462
- `Scene ${sceneNum}`,
35463
- {
35464
- kling,
35465
- videoId: waitResult.videoId,
35466
- onProgress: options.onProgress
35467
- }
35468
- );
35469
- result.regeneratedScenes.push(sceneNum);
35470
- } else {
35471
- logSceneFailure("Kling", `scene ${sceneNum}`, waitResult);
35472
- result.failedScenes.push(sceneNum);
35473
- }
35474
- } catch (err) {
35475
- logSceneFailure("Kling", `scene ${sceneNum}`, err);
35476
- result.failedScenes.push(sceneNum);
35477
- }
35478
- } else {
35479
- result.failedScenes.push(sceneNum);
35480
- }
35481
- } else {
35482
- const runway = new RunwayProvider();
35483
- await runway.initialize({ apiKey: videoApiKey });
35484
- const ext = extname3(imagePath).toLowerCase().slice(1);
35485
- const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
35486
- const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
35487
- const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio || "16:9";
35488
- const taskResult = await generateVideoWithRetryRunway(
35489
- runway,
35490
- segment,
35491
- referenceImage,
35492
- { duration: videoDuration, aspectRatio },
35493
- maxRetries
35494
- );
35495
- if (taskResult) {
35496
- try {
35497
- const waitResult = await runway.waitForCompletion(taskResult.taskId, void 0, 3e5);
35498
- if (waitResult.status === "completed" && waitResult.videoUrl) {
35499
- const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
35500
- await writeFile23(videoPath, buffer);
35501
- const targetDuration = segment.duration;
35502
- const actualVideoDuration = await getVideoDuration(videoPath);
35503
- if (actualVideoDuration < targetDuration - 0.1) {
35504
- const extendedPath = resolve25(outputDir, `scene-${sceneNum}-extended.mp4`);
35505
- await extendVideoNaturally(videoPath, targetDuration, extendedPath);
35506
- await unlink2(videoPath);
35507
- await rename2(extendedPath, videoPath);
35508
- }
35509
- result.regeneratedScenes.push(sceneNum);
35510
- } else {
35511
- logSceneFailure("Runway", `scene ${sceneNum}`, waitResult);
35512
- result.failedScenes.push(sceneNum);
35513
- }
35514
- } catch (err) {
35515
- logSceneFailure("Runway", `scene ${sceneNum}`, err);
35516
- result.failedScenes.push(sceneNum);
35517
- }
35518
- } else {
35519
- result.failedScenes.push(sceneNum);
35520
- }
35521
- }
35522
- } else if (!sceneFailed) {
35523
- result.regeneratedScenes.push(sceneNum);
35198
+ };
35199
+ }
35200
+ return {
35201
+ provider: "imgbb",
35202
+ async uploadImage(imageBuffer) {
35203
+ const imgbbKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
35204
+ if (!imgbbKey) {
35205
+ throw new Error("IMGBB_API_KEY required for image-to-video uploads.");
35524
35206
  }
35525
- }
35526
- if (storyboardMutated) {
35527
- let currentTime = 0;
35528
- for (const segment of segments) {
35529
- segment.startTime = currentTime;
35530
- currentTime += segment.duration;
35207
+ const result = await uploadToImgbb(imageBuffer, imgbbKey);
35208
+ if (!result.success || !result.url) {
35209
+ throw new Error(`ImgBB upload failed: ${result.error ?? "unknown error"}`);
35531
35210
  }
35532
- const serialized = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.stringify)({ scenes: segments }, { indent: 2 }) : JSON.stringify(segments, null, 2);
35533
- await writeFile23(storyboardPath, serialized, "utf-8");
35211
+ return { provider: "imgbb", url: result.url };
35534
35212
  }
35535
- result.success = result.failedScenes.length === 0;
35536
- return result;
35537
- } catch (error) {
35538
- return {
35539
- ...result,
35540
- error: error instanceof Error ? error.message : String(error)
35541
- };
35542
- }
35213
+ };
35543
35214
  }
35544
- var import_yaml5;
35545
- var init_ai_script_pipeline = __esm({
35546
- "src/commands/ai-script-pipeline.ts"() {
35215
+ var init_upload_host = __esm({
35216
+ "src/utils/upload-host.ts"() {
35547
35217
  "use strict";
35548
- import_yaml5 = __toESM(require_dist(), 1);
35549
- init_dist2();
35550
- init_api_key();
35551
35218
  init_config();
35552
- init_audio();
35553
- init_ai_helpers();
35554
35219
  init_video_utils();
35555
- init_video_providers();
35556
35220
  }
35557
35221
  });
35558
35222
 
35559
35223
  // src/commands/generate/video.ts
35560
- import { resolve as resolve26 } from "node:path";
35561
- import { readFile as readFile14, writeFile as writeFile24 } from "node:fs/promises";
35224
+ import { resolve as resolve25 } from "node:path";
35225
+ import { readFile as readFile13, writeFile as writeFile23 } from "node:fs/promises";
35562
35226
  function registerVideoCommand(parent) {
35563
- parent.command("video").alias("vid").description("Generate video using AI (Seedance, Grok, Kling, Runway, or Veo)").argument("[prompt]", "Text prompt describing the video (interactive if omitted)").option("-p, --provider <provider>", "Provider: seedance (ByteDance Seedance 2.0 via fal.ai), grok, kling, runway, veo. `fal` is a deprecated v0.x alias for seedance and will be removed in 1.0.").option("-k, --api-key <key>", "API key (or set FAL_KEY / XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)").option("-o, --output <path>", "Output file path (downloads video)").option("-i, --image <path>", "Reference image for image-to-video").option(
35227
+ parent.command("video").alias("vid").description("Generate video using AI (Seedance, Grok, Kling, Runway, or Veo)").argument("[prompt]", "Text prompt describing the video (interactive if omitted)").option(
35228
+ "-p, --provider <provider>",
35229
+ "Provider: seedance (ByteDance Seedance 2.0 via fal.ai), grok, kling, runway, veo. `fal` is a deprecated v0.x alias for seedance and will be removed in 1.0."
35230
+ ).option(
35231
+ "-k, --api-key <key>",
35232
+ "API key (or set FAL_KEY / XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)"
35233
+ ).option("-o, --output <path>", "Output file path (downloads video)").option("-i, --image <path>", "Reference image for image-to-video").option(
35564
35234
  "-d, --duration <sec>",
35565
35235
  "Duration in seconds. Seedance accepts 4-15; Kling accepts 5 or 10; Veo maps to 6 or 8.",
35566
35236
  "5"
35567
- ).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", `
35237
+ ).option(
35238
+ "-r, --ratio <ratio>",
35239
+ "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)"
35240
+ ).option("--seed <number>", "Random seed for reproducibility (Runway only)").option("--mode <mode>", "Generation mode: std or pro (Kling only)", "std").option(
35241
+ "--seedance-model <model>",
35242
+ "Seedance variant: quality or fast (fal.ai only)",
35243
+ "quality"
35244
+ ).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(
35245
+ "--ref-images <paths...>",
35246
+ "Reference images for character consistency (Veo 3.1 only, max 3)"
35247
+ ).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(
35248
+ "--runway-model <model>",
35249
+ "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)",
35250
+ "gen4.5"
35251
+ ).option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText(
35252
+ "after",
35253
+ `
35568
35254
  Examples:
35569
35255
  $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
35570
35256
  $ vibe gen vid "cinematic city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
35571
35257
  $ vibe gen vid "city timelapse" -o city.mp4 -p kling # Kling
35572
35258
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
35573
35259
  $ vibe gen vid "ocean waves" -o waves.mp4 -p veo --resolution 1080p # Veo
35574
- $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`).action(async (prompt3, options) => {
35260
+ $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`
35261
+ ).action(async (prompt3, options) => {
35575
35262
  const startedAt = Date.now();
35576
35263
  try {
35577
35264
  if (!prompt3) {
@@ -35582,10 +35269,7 @@ Examples:
35582
35269
  }
35583
35270
  } else {
35584
35271
  exitWithError(
35585
- usageError(
35586
- "Prompt argument is required.",
35587
- "Usage: vibe generate video <prompt>"
35588
- )
35272
+ usageError("Prompt argument is required.", "Usage: vibe generate video <prompt>")
35589
35273
  );
35590
35274
  }
35591
35275
  }
@@ -35593,13 +35277,16 @@ Examples:
35593
35277
  if (options.output) {
35594
35278
  validateOutputPath(options.output);
35595
35279
  }
35280
+ await loadProviderDefaults();
35596
35281
  if (options.duration !== void 0) {
35597
35282
  const d = parseFloat(options.duration);
35598
35283
  if (!Number.isFinite(d) || d <= 0 || d > 60) {
35599
- exitWithError(usageError(
35600
- `Invalid --duration: ${options.duration}`,
35601
- "Must be a positive number \u2264 60 seconds."
35602
- ));
35284
+ exitWithError(
35285
+ usageError(
35286
+ `Invalid --duration: ${options.duration}`,
35287
+ "Must be a positive number \u2264 60 seconds."
35288
+ )
35289
+ );
35603
35290
  }
35604
35291
  }
35605
35292
  const validProviders = ["runway", "kling", "veo", "grok", "seedance", "fal"];
@@ -35641,10 +35328,12 @@ Examples:
35641
35328
  provider = resolved?.name ?? "grok";
35642
35329
  }
35643
35330
  let referenceImage;
35331
+ let referenceImageBuffer;
35332
+ let referenceImageMimeType;
35644
35333
  let isImageToVideo = false;
35645
35334
  if (options.image) {
35646
- const imagePath = resolve26(process.cwd(), options.image);
35647
- const imageBuffer = await readFile14(imagePath);
35335
+ const imagePath = resolve25(process.cwd(), options.image);
35336
+ const imageBuffer = await readFile13(imagePath);
35648
35337
  const ext = options.image.toLowerCase().split(".").pop();
35649
35338
  const mimeTypes2 = {
35650
35339
  jpg: "image/jpeg",
@@ -35654,6 +35343,8 @@ Examples:
35654
35343
  webp: "image/webp"
35655
35344
  };
35656
35345
  const mimeType = mimeTypes2[ext || "png"] || "image/png";
35346
+ referenceImageBuffer = imageBuffer;
35347
+ referenceImageMimeType = mimeType;
35657
35348
  referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
35658
35349
  isImageToVideo = true;
35659
35350
  if (!options.ratio) {
@@ -35778,20 +35469,22 @@ Examples:
35778
35469
  }
35779
35470
  let klingImage = referenceImage;
35780
35471
  if (klingImage && klingImage.startsWith("data:")) {
35781
- spinner2.text = "Uploading image to ImgBB for Kling...";
35782
- const imgbbKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
35783
- if (!imgbbKey) {
35784
- spinner2.fail("ImgBB API key required");
35785
- exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35786
- }
35787
- const base64Data = klingImage.split(",")[1];
35788
- const imageBuffer = Buffer.from(base64Data, "base64");
35789
- const uploadResult = await uploadToImgbb(imageBuffer, imgbbKey);
35790
- if (!uploadResult.success || !uploadResult.url) {
35791
- spinner2.fail("ImgBB upload failed");
35792
- exitWithError(apiError(`ImgBB upload failed: ${uploadResult.error}`, true));
35793
- }
35794
- klingImage = uploadResult.url;
35472
+ try {
35473
+ const uploadHost = await resolveUploadHost();
35474
+ spinner2.text = `Uploading image via ${uploadHost.provider} for Kling...`;
35475
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
35476
+ filename: options.image,
35477
+ mimeType: referenceImageMimeType
35478
+ });
35479
+ klingImage = upload.url;
35480
+ } catch (err) {
35481
+ spinner2.fail("Image upload failed");
35482
+ const message = err instanceof Error ? err.message : String(err);
35483
+ if (message.includes("IMGBB_API_KEY")) {
35484
+ exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35485
+ }
35486
+ exitWithError(apiError(message, true));
35487
+ }
35795
35488
  spinner2.text = "Starting video generation...";
35796
35489
  }
35797
35490
  result = await kling.generateVideo(prompt3, {
@@ -35846,8 +35539,8 @@ Examples:
35846
35539
  const veoDuration = parseInt(options.duration) <= 6 ? 6 : 8;
35847
35540
  let lastFrame;
35848
35541
  if (options.lastFrame) {
35849
- const lastFramePath = resolve26(process.cwd(), options.lastFrame);
35850
- const lastFrameBuffer = await readFile14(lastFramePath);
35542
+ const lastFramePath = resolve25(process.cwd(), options.lastFrame);
35543
+ const lastFrameBuffer = await readFile13(lastFramePath);
35851
35544
  const ext = options.lastFrame.toLowerCase().split(".").pop();
35852
35545
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
35853
35546
  lastFrame = `data:${mimeType};base64,${lastFrameBuffer.toString("base64")}`;
@@ -35856,8 +35549,8 @@ Examples:
35856
35549
  if (options.refImages && options.refImages.length > 0) {
35857
35550
  refImages = [];
35858
35551
  for (const refPath of options.refImages.slice(0, 3)) {
35859
- const absRefPath = resolve26(process.cwd(), refPath);
35860
- const refBuffer = await readFile14(absRefPath);
35552
+ const absRefPath = resolve25(process.cwd(), refPath);
35553
+ const refBuffer = await readFile13(absRefPath);
35861
35554
  const ext = refPath.toLowerCase().split(".").pop();
35862
35555
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
35863
35556
  refImages.push({ base64: refBuffer.toString("base64"), mimeType });
@@ -35938,20 +35631,22 @@ Examples:
35938
35631
  await fal.initialize({ apiKey });
35939
35632
  let falImage = referenceImage;
35940
35633
  if (falImage && falImage.startsWith("data:")) {
35941
- spinner2.text = "Uploading image to ImgBB for Seedance...";
35942
- const imgbbKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
35943
- if (!imgbbKey) {
35944
- spinner2.fail("ImgBB API key required for Seedance image-to-video");
35945
- exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35946
- }
35947
- const base64Data = falImage.split(",")[1];
35948
- const imageBuffer = Buffer.from(base64Data, "base64");
35949
- const uploadResult = await uploadToImgbb(imageBuffer, imgbbKey);
35950
- if (!uploadResult.success || !uploadResult.url) {
35951
- spinner2.fail("ImgBB upload failed");
35952
- exitWithError(apiError(`ImgBB upload failed: ${uploadResult.error}`, true));
35634
+ try {
35635
+ const uploadHost = await resolveUploadHost();
35636
+ spinner2.text = `Uploading image via ${uploadHost.provider} for Seedance...`;
35637
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
35638
+ filename: options.image,
35639
+ mimeType: referenceImageMimeType
35640
+ });
35641
+ falImage = upload.url;
35642
+ } catch (err) {
35643
+ spinner2.fail("Image upload failed");
35644
+ const message = err instanceof Error ? err.message : String(err);
35645
+ if (message.includes("IMGBB_API_KEY")) {
35646
+ exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35647
+ }
35648
+ exitWithError(apiError(message, true));
35953
35649
  }
35954
- falImage = uploadResult.url;
35955
35650
  }
35956
35651
  spinner2.text = "Generating video with fal.ai Seedance 2.0 (this may take 1-3 minutes)...";
35957
35652
  const seedanceModel = String(options.seedanceModel ?? "quality").toLowerCase();
@@ -35975,8 +35670,8 @@ Examples:
35975
35670
  let outputPath;
35976
35671
  if (options.output && finalResult.videoUrl) {
35977
35672
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
35978
- outputPath = resolve26(process.cwd(), options.output);
35979
- await writeFile24(outputPath, buffer);
35673
+ outputPath = resolve25(process.cwd(), options.output);
35674
+ await writeFile23(outputPath, buffer);
35980
35675
  }
35981
35676
  outputSuccess({
35982
35677
  command: "generate video",
@@ -36003,14 +35698,12 @@ Examples:
36003
35698
  const downloadSpinner = ora("Downloading video...").start();
36004
35699
  try {
36005
35700
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
36006
- const outputPath = resolve26(process.cwd(), options.output);
36007
- await writeFile24(outputPath, buffer);
35701
+ const outputPath = resolve25(process.cwd(), options.output);
35702
+ await writeFile23(outputPath, buffer);
36008
35703
  downloadSpinner.succeed(source_default.green(`Saved to: ${outputPath}`));
36009
35704
  } catch (err) {
36010
35705
  downloadSpinner.fail(
36011
- source_default.red(
36012
- `Failed to download video: ${err instanceof Error ? err.message : err}`
36013
- )
35706
+ source_default.red(`Failed to download video: ${err instanceof Error ? err.message : err}`)
36014
35707
  );
36015
35708
  }
36016
35709
  }
@@ -36028,11 +35721,10 @@ var init_video = __esm({
36028
35721
  init_dist2();
36029
35722
  init_api_key();
36030
35723
  init_tty();
36031
- init_config();
36032
35724
  init_output();
36033
35725
  init_validate();
36034
35726
  init_provider_resolver();
36035
- init_ai_script_pipeline();
35727
+ init_upload_host();
36036
35728
  init_ai_helpers();
36037
35729
  }
36038
35730
  });
@@ -36137,8 +35829,8 @@ Run 'vibe schema generate.<command>' for structured parameter info.
36137
35829
  });
36138
35830
 
36139
35831
  // src/commands/_shared/edit/silence-cut.ts
36140
- import { existsSync as existsSync15 } from "node:fs";
36141
- import { readFile as readFile15 } from "node:fs/promises";
35832
+ import { existsSync as existsSync14 } from "node:fs";
35833
+ import { readFile as readFile14 } from "node:fs/promises";
36142
35834
  async function detectSilencePeriods(videoPath, noiseThreshold, minDuration) {
36143
35835
  const totalDuration = await getVideoDuration(videoPath);
36144
35836
  const { stdout, stderr } = await execSafe(
@@ -36188,7 +35880,7 @@ async function detectSilencePeriodsWithGemini(videoPath, minDuration, options) {
36188
35880
  }
36189
35881
  const gemini = new GeminiProvider();
36190
35882
  await gemini.initialize({ apiKey: geminiApiKey });
36191
- const videoBuffer = await readFile15(videoPath);
35883
+ const videoBuffer = await readFile14(videoPath);
36192
35884
  const modelMap = {
36193
35885
  flash: "gemini-3-flash-preview",
36194
35886
  "flash-2.5": "gemini-2.5-flash",
@@ -36269,7 +35961,7 @@ async function executeSilenceCut(options) {
36269
35961
  analyzeOnly = false,
36270
35962
  useGemini = false
36271
35963
  } = options;
36272
- if (!existsSync15(videoPath)) {
35964
+ if (!existsSync14(videoPath)) {
36273
35965
  return { success: false, error: `Video not found: ${videoPath}` };
36274
35966
  }
36275
35967
  if (!commandExists("ffmpeg")) {
@@ -36377,11 +36069,11 @@ var init_silence_cut = __esm({
36377
36069
  });
36378
36070
 
36379
36071
  // src/commands/_shared/edit/jump-cut.ts
36380
- import { existsSync as existsSync16 } from "node:fs";
36381
- import { readFile as readFile16, writeFile as writeFile25, mkdir as mkdir12 } from "node:fs/promises";
36072
+ import { existsSync as existsSync15 } from "node:fs";
36073
+ import { readFile as readFile15, writeFile as writeFile24, mkdir as mkdir12 } from "node:fs/promises";
36382
36074
  import { join as join8 } from "node:path";
36383
36075
  async function transcribeWithWords(audioPath, apiKey, language) {
36384
- const audioBuffer = await readFile16(audioPath);
36076
+ const audioBuffer = await readFile15(audioPath);
36385
36077
  const audioBlob = new Blob([audioBuffer]);
36386
36078
  const formData = new FormData();
36387
36079
  formData.append("file", audioBlob, "audio.wav");
@@ -36440,7 +36132,7 @@ async function executeJumpCut(options) {
36440
36132
  analyzeOnly = false,
36441
36133
  apiKey
36442
36134
  } = options;
36443
- if (!existsSync16(videoPath)) {
36135
+ if (!existsSync15(videoPath)) {
36444
36136
  return { success: false, error: `Video not found: ${videoPath}` };
36445
36137
  }
36446
36138
  if (!commandExists("ffmpeg")) {
@@ -36537,7 +36229,7 @@ async function executeJumpCut(options) {
36537
36229
  }
36538
36230
  const concatList = segmentPaths.map((p) => `file '${p}'`).join("\n");
36539
36231
  const listPath = join8(tmpDir, "concat.txt");
36540
- await writeFile25(listPath, concatList);
36232
+ await writeFile24(listPath, concatList);
36541
36233
  await execSafe(
36542
36234
  "ffmpeg",
36543
36235
  [
@@ -36726,8 +36418,8 @@ var init_helpers = __esm({
36726
36418
  });
36727
36419
 
36728
36420
  // src/commands/_shared/edit/caption.ts
36729
- import { existsSync as existsSync17 } from "node:fs";
36730
- import { readFile as readFile17, writeFile as writeFile26, mkdir as mkdir13 } from "node:fs/promises";
36421
+ import { existsSync as existsSync16 } from "node:fs";
36422
+ import { readFile as readFile16, writeFile as writeFile25, mkdir as mkdir13 } from "node:fs/promises";
36731
36423
  import { dirname as dirname8, basename as basename6, extname as extname4, join as join9 } from "node:path";
36732
36424
  function getCaptionForceStyle(style, fontSize, fontColor, position) {
36733
36425
  const alignment = position === "top" ? 8 : position === "center" ? 5 : 2;
@@ -36756,7 +36448,7 @@ async function executeCaption(options) {
36756
36448
  position = "bottom",
36757
36449
  apiKey
36758
36450
  } = options;
36759
- if (!existsSync17(videoPath)) {
36451
+ if (!existsSync16(videoPath)) {
36760
36452
  return { success: false, error: `Video not found: ${videoPath}` };
36761
36453
  }
36762
36454
  if (!commandExists("ffmpeg")) {
@@ -36797,7 +36489,7 @@ async function executeCaption(options) {
36797
36489
  );
36798
36490
  const whisper = new WhisperProvider();
36799
36491
  await whisper.initialize({ apiKey: openaiKey });
36800
- const audioBuffer = await readFile17(audioPath);
36492
+ const audioBuffer = await readFile16(audioPath);
36801
36493
  const audioBlob = new Blob([audioBuffer]);
36802
36494
  const transcriptResult = await whisper.transcribe(audioBlob, language);
36803
36495
  if (transcriptResult.status === "failed" || !transcriptResult.segments || transcriptResult.segments.length === 0) {
@@ -36807,7 +36499,7 @@ async function executeCaption(options) {
36807
36499
  };
36808
36500
  }
36809
36501
  const srtContent = formatSRT(transcriptResult.segments);
36810
- await writeFile26(srtPath, srtContent);
36502
+ await writeFile25(srtPath, srtContent);
36811
36503
  const { width, height } = await getVideoResolution(videoPath);
36812
36504
  const fontSize = customFontSize || Math.round(height / 18);
36813
36505
  let hasSubtitles = false;
@@ -36849,7 +36541,7 @@ async function executeCaption(options) {
36849
36541
  outputDir2,
36850
36542
  basename6(outputPath, extname4(outputPath)) + ".srt"
36851
36543
  );
36852
- await writeFile26(outputSrtPath2, srtContent);
36544
+ await writeFile25(outputSrtPath2, srtContent);
36853
36545
  return { success: false, error: `${remotionErr}
36854
36546
  SRT saved to: ${outputSrtPath2}` };
36855
36547
  }
@@ -36888,7 +36580,7 @@ SRT saved to: ${outputSrtPath2}` };
36888
36580
  outputDir2,
36889
36581
  basename6(outputPath, extname4(outputPath)) + ".srt"
36890
36582
  );
36891
- await writeFile26(outputSrtPath2, srtContent);
36583
+ await writeFile25(outputSrtPath2, srtContent);
36892
36584
  return {
36893
36585
  success: false,
36894
36586
  error: `${renderResult.error}
@@ -36901,7 +36593,7 @@ SRT saved to: ${outputSrtPath2}`
36901
36593
  outputDir,
36902
36594
  basename6(outputPath, extname4(outputPath)) + ".srt"
36903
36595
  );
36904
- await writeFile26(outputSrtPath, srtContent);
36596
+ await writeFile25(outputSrtPath, srtContent);
36905
36597
  return {
36906
36598
  success: true,
36907
36599
  outputPath,
@@ -36934,10 +36626,10 @@ var init_caption = __esm({
36934
36626
  });
36935
36627
 
36936
36628
  // src/commands/_shared/edit/noise-reduce.ts
36937
- import { existsSync as existsSync18 } from "node:fs";
36629
+ import { existsSync as existsSync17 } from "node:fs";
36938
36630
  async function executeNoiseReduce(options) {
36939
36631
  const { inputPath, outputPath, strength = "medium", noiseFloor } = options;
36940
- if (!existsSync18(inputPath)) {
36632
+ if (!existsSync17(inputPath)) {
36941
36633
  return { success: false, error: `File not found: ${inputPath}` };
36942
36634
  }
36943
36635
  if (!commandExists("ffmpeg")) {
@@ -36994,7 +36686,7 @@ var init_noise_reduce = __esm({
36994
36686
  });
36995
36687
 
36996
36688
  // src/commands/_shared/edit/fade.ts
36997
- import { existsSync as existsSync19 } from "node:fs";
36689
+ import { existsSync as existsSync18 } from "node:fs";
36998
36690
  async function executeFade(options) {
36999
36691
  const {
37000
36692
  videoPath,
@@ -37004,7 +36696,7 @@ async function executeFade(options) {
37004
36696
  audioOnly = false,
37005
36697
  videoOnly = false
37006
36698
  } = options;
37007
- if (!existsSync19(videoPath)) {
36699
+ if (!existsSync18(videoPath)) {
37008
36700
  return { success: false, error: `Video not found: ${videoPath}` };
37009
36701
  }
37010
36702
  if (!commandExists("ffmpeg")) {
@@ -37071,8 +36763,8 @@ var init_fade = __esm({
37071
36763
  });
37072
36764
 
37073
36765
  // src/commands/_shared/edit/translate-srt.ts
37074
- import { existsSync as existsSync20 } from "node:fs";
37075
- import { readFile as readFile18, writeFile as writeFile27 } from "node:fs/promises";
36766
+ import { existsSync as existsSync19 } from "node:fs";
36767
+ import { readFile as readFile17, writeFile as writeFile26 } from "node:fs/promises";
37076
36768
  async function executeTranslateSrt(options) {
37077
36769
  const {
37078
36770
  srtPath,
@@ -37082,11 +36774,11 @@ async function executeTranslateSrt(options) {
37082
36774
  sourceLanguage,
37083
36775
  apiKey
37084
36776
  } = options;
37085
- if (!existsSync20(srtPath)) {
36777
+ if (!existsSync19(srtPath)) {
37086
36778
  return { success: false, error: `SRT file not found: ${srtPath}` };
37087
36779
  }
37088
36780
  try {
37089
- const srtContent = await readFile18(srtPath, "utf-8");
36781
+ const srtContent = await readFile17(srtPath, "utf-8");
37090
36782
  const segments = parseSRT(srtContent);
37091
36783
  if (segments.length === 0) {
37092
36784
  return { success: false, error: "No subtitle segments found in SRT file" };
@@ -37176,7 +36868,7 @@ ${textsToTranslate}`;
37176
36868
  }
37177
36869
  }
37178
36870
  const translatedSrt = formatSRT(translatedSegments);
37179
- await writeFile27(outputPath, translatedSrt);
36871
+ await writeFile26(outputPath, translatedSrt);
37180
36872
  return {
37181
36873
  success: true,
37182
36874
  outputPath,
@@ -37199,8 +36891,8 @@ var init_translate_srt = __esm({
37199
36891
  });
37200
36892
 
37201
36893
  // src/commands/_shared/edit/text-overlay.ts
37202
- import { existsSync as existsSync21 } from "node:fs";
37203
- import { resolve as resolve27 } from "node:path";
36894
+ import { existsSync as existsSync20 } from "node:fs";
36895
+ import { resolve as resolve26 } from "node:path";
37204
36896
  function detectSystemFont() {
37205
36897
  const platform3 = process.platform;
37206
36898
  if (platform3 === "darwin") {
@@ -37210,7 +36902,7 @@ function detectSystemFont() {
37210
36902
  "/Library/Fonts/Arial.ttf"
37211
36903
  ];
37212
36904
  for (const f of candidates) {
37213
- if (existsSync21(f)) return f;
36905
+ if (existsSync20(f)) return f;
37214
36906
  }
37215
36907
  } else if (platform3 === "linux") {
37216
36908
  const candidates = [
@@ -37219,7 +36911,7 @@ function detectSystemFont() {
37219
36911
  "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
37220
36912
  ];
37221
36913
  for (const f of candidates) {
37222
- if (existsSync21(f)) return f;
36914
+ if (existsSync20(f)) return f;
37223
36915
  }
37224
36916
  } else if (platform3 === "win32") {
37225
36917
  const candidates = [
@@ -37227,7 +36919,7 @@ function detectSystemFont() {
37227
36919
  "C:\\Windows\\Fonts\\segoeui.ttf"
37228
36920
  ];
37229
36921
  for (const f of candidates) {
37230
- if (existsSync21(f)) return f;
36922
+ if (existsSync20(f)) return f;
37231
36923
  }
37232
36924
  }
37233
36925
  return null;
@@ -37246,9 +36938,9 @@ async function applyTextOverlays(options) {
37246
36938
  if (!texts || texts.length === 0) {
37247
36939
  return { success: false, error: "No texts provided" };
37248
36940
  }
37249
- const absVideoPath = resolve27(process.cwd(), videoPath);
37250
- const absOutputPath = resolve27(process.cwd(), outputPath);
37251
- if (!existsSync21(absVideoPath)) {
36941
+ const absVideoPath = resolve26(process.cwd(), videoPath);
36942
+ const absOutputPath = resolve26(process.cwd(), outputPath);
36943
+ if (!existsSync20(absVideoPath)) {
37252
36944
  return { success: false, error: `Video not found: ${absVideoPath}` };
37253
36945
  }
37254
36946
  if (!commandExists("ffmpeg")) {
@@ -37428,8 +37120,8 @@ var init_ai_edit = __esm({
37428
37120
  });
37429
37121
 
37430
37122
  // src/commands/ai-edit-cli.ts
37431
- import { resolve as resolve28, extname as extname5, basename as basename7 } from "node:path";
37432
- import { existsSync as existsSync22 } from "node:fs";
37123
+ import { resolve as resolve27, extname as extname5, basename as basename7 } from "node:path";
37124
+ import { existsSync as existsSync21 } from "node:fs";
37433
37125
  function registerEditCommands(aiCommand) {
37434
37126
  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", "(deprecated \u2014 use `vibe detect silence`) 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", `
37435
37127
  Examples:
@@ -37445,8 +37137,8 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37445
37137
  if (options.output) {
37446
37138
  validateOutputPath(options.output);
37447
37139
  }
37448
- const absVideoPath = resolve28(process.cwd(), videoPath);
37449
- if (!existsSync22(absVideoPath)) {
37140
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37141
+ if (!existsSync21(absVideoPath)) {
37450
37142
  exitWithError(notFoundError(absVideoPath));
37451
37143
  }
37452
37144
  if (!commandExists("ffmpeg")) {
@@ -37485,7 +37177,7 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37485
37177
  const spinner2 = ora(spinnerText).start();
37486
37178
  const result = await executeSilenceCut({
37487
37179
  videoPath: absVideoPath,
37488
- outputPath: resolve28(process.cwd(), outputPath),
37180
+ outputPath: resolve27(process.cwd(), outputPath),
37489
37181
  noiseThreshold: parseFloat(options.noise),
37490
37182
  minDuration: parseFloat(options.minDuration),
37491
37183
  padding: parseFloat(options.padding),
@@ -37553,8 +37245,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37553
37245
  if (options.output) {
37554
37246
  validateOutputPath(options.output);
37555
37247
  }
37556
- const absVideoPath = resolve28(process.cwd(), videoPath);
37557
- if (!existsSync22(absVideoPath)) {
37248
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37249
+ if (!existsSync21(absVideoPath)) {
37558
37250
  exitWithError(notFoundError(absVideoPath));
37559
37251
  }
37560
37252
  if (!commandExists("ffmpeg")) {
@@ -37588,7 +37280,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37588
37280
  const spinner2 = ora("Starting caption process...").start();
37589
37281
  const result = await executeCaption({
37590
37282
  videoPath: absVideoPath,
37591
- outputPath: resolve28(process.cwd(), outputPath),
37283
+ outputPath: resolve27(process.cwd(), outputPath),
37592
37284
  style: options.style,
37593
37285
  fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
37594
37286
  fontColor: options.color,
@@ -37634,8 +37326,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37634
37326
  if (options.output) {
37635
37327
  validateOutputPath(options.output);
37636
37328
  }
37637
- const absInputPath = resolve28(process.cwd(), inputPath);
37638
- if (!existsSync22(absInputPath)) {
37329
+ const absInputPath = resolve27(process.cwd(), inputPath);
37330
+ if (!existsSync21(absInputPath)) {
37639
37331
  exitWithError(notFoundError(absInputPath));
37640
37332
  }
37641
37333
  if (!commandExists("ffmpeg")) {
@@ -37662,7 +37354,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37662
37354
  const spinner2 = ora("Applying noise reduction...").start();
37663
37355
  const result = await executeNoiseReduce({
37664
37356
  inputPath: absInputPath,
37665
- outputPath: resolve28(process.cwd(), outputPath),
37357
+ outputPath: resolve27(process.cwd(), outputPath),
37666
37358
  strength: options.strength,
37667
37359
  noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
37668
37360
  });
@@ -37700,8 +37392,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37700
37392
  if (options.output) {
37701
37393
  validateOutputPath(options.output);
37702
37394
  }
37703
- const absVideoPath = resolve28(process.cwd(), videoPath);
37704
- if (!existsSync22(absVideoPath)) {
37395
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37396
+ if (!existsSync21(absVideoPath)) {
37705
37397
  exitWithError(notFoundError(absVideoPath));
37706
37398
  }
37707
37399
  if (!commandExists("ffmpeg")) {
@@ -37730,7 +37422,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37730
37422
  const spinner2 = ora("Applying fade effects...").start();
37731
37423
  const result = await executeFade({
37732
37424
  videoPath: absVideoPath,
37733
- outputPath: resolve28(process.cwd(), outputPath),
37425
+ outputPath: resolve27(process.cwd(), outputPath),
37734
37426
  fadeIn: parseFloat(options.fadeIn),
37735
37427
  fadeOut: parseFloat(options.fadeOut),
37736
37428
  audioOnly: options.audioOnly || false,
@@ -37775,8 +37467,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37775
37467
  if (!options.target) {
37776
37468
  exitWithError(usageError("Target language required. Use -t or --target"));
37777
37469
  }
37778
- const absSrtPath = resolve28(process.cwd(), srtPath);
37779
- if (!existsSync22(absSrtPath)) {
37470
+ const absSrtPath = resolve27(process.cwd(), srtPath);
37471
+ if (!existsSync21(absSrtPath)) {
37780
37472
  exitWithError(notFoundError(absSrtPath));
37781
37473
  }
37782
37474
  if (options.dryRun) {
@@ -37808,7 +37500,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37808
37500
  const spinner2 = ora(`Translating to ${options.target}...`).start();
37809
37501
  const result = await executeTranslateSrt({
37810
37502
  srtPath: absSrtPath,
37811
- outputPath: resolve28(process.cwd(), outputPath),
37503
+ outputPath: resolve27(process.cwd(), outputPath),
37812
37504
  targetLanguage: options.target,
37813
37505
  provider,
37814
37506
  sourceLanguage: options.source,
@@ -37851,8 +37543,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37851
37543
  validateOutputPath(options.output);
37852
37544
  }
37853
37545
  if (options.fillers) rejectControlChars(options.fillers);
37854
- const absVideoPath = resolve28(process.cwd(), videoPath);
37855
- if (!existsSync22(absVideoPath)) {
37546
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37547
+ if (!existsSync21(absVideoPath)) {
37856
37548
  exitWithError(notFoundError(absVideoPath));
37857
37549
  }
37858
37550
  if (!commandExists("ffmpeg")) {
@@ -37887,7 +37579,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37887
37579
  const spinner2 = ora("Transcribing with word-level timestamps...").start();
37888
37580
  const result = await executeJumpCut({
37889
37581
  videoPath: absVideoPath,
37890
- outputPath: resolve28(process.cwd(), outputPath),
37582
+ outputPath: resolve27(process.cwd(), outputPath),
37891
37583
  fillers,
37892
37584
  padding: parseFloat(options.padding),
37893
37585
  language: options.language,
@@ -37952,9 +37644,9 @@ var init_ai_edit_cli = __esm({
37952
37644
  });
37953
37645
 
37954
37646
  // src/commands/_shared/execute-fill-gaps.ts
37955
- import { existsSync as existsSync23 } from "node:fs";
37956
- import { readFile as readFile19, writeFile as writeFile28, mkdir as mkdir14, rename as renameFs } from "node:fs/promises";
37957
- import { resolve as resolve29, dirname as dirname9 } from "node:path";
37647
+ import { existsSync as existsSync22 } from "node:fs";
37648
+ import { readFile as readFile18, writeFile as writeFile27, mkdir as mkdir14, rename as renameFs } from "node:fs/promises";
37649
+ import { resolve as resolve28, dirname as dirname9 } from "node:path";
37958
37650
  function detectVideoGaps(videoClips, totalDuration) {
37959
37651
  const gaps = [];
37960
37652
  const sortedClips = [...videoClips].sort((a, b) => a.startTime - b.startTime);
@@ -38010,7 +37702,7 @@ function analyzeGapFillability(gaps, videoClips, sources) {
38010
37702
  }
38011
37703
  async function uploadFrameToImgbb(framePath, imgbbApiKey) {
38012
37704
  try {
38013
- const frameBuffer = await readFile19(framePath);
37705
+ const frameBuffer = await readFile18(framePath);
38014
37706
  const frameBase64 = frameBuffer.toString("base64");
38015
37707
  const formData = new FormData();
38016
37708
  formData.append("key", imgbbApiKey);
@@ -38035,14 +37727,14 @@ async function executeFillGaps(options) {
38035
37727
  try {
38036
37728
  onProgress("Loading timeline...");
38037
37729
  const filePath = await resolveTimelineFile(options.projectPath);
38038
- if (!existsSync23(filePath)) {
37730
+ if (!existsSync22(filePath)) {
38039
37731
  return {
38040
37732
  success: false,
38041
37733
  error: `Timeline file not found: ${filePath}`,
38042
37734
  humanLines
38043
37735
  };
38044
37736
  }
38045
- const content = await readFile19(filePath, "utf-8");
37737
+ const content = await readFile18(filePath, "utf-8");
38046
37738
  const data = JSON.parse(content);
38047
37739
  const project = Project.fromJSON(data);
38048
37740
  const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
@@ -38169,8 +37861,8 @@ async function executeFillGaps(options) {
38169
37861
  };
38170
37862
  }
38171
37863
  const projectDir = dirname9(filePath);
38172
- const footageDir = options.dir ? resolve29(process.cwd(), options.dir) : resolve29(projectDir, "footage");
38173
- if (!existsSync23(footageDir)) {
37864
+ const footageDir = options.dir ? resolve28(process.cwd(), options.dir) : resolve28(projectDir, "footage");
37865
+ if (!existsSync22(footageDir)) {
38174
37866
  await mkdir14(footageDir, { recursive: true });
38175
37867
  }
38176
37868
  humanLines.push("Generating AI Videos");
@@ -38201,7 +37893,7 @@ async function executeFillGaps(options) {
38201
37893
  }
38202
37894
  onProgress("Extracting frame from preceding clip...");
38203
37895
  const frameOffset = clipBefore.sourceStartOffset + clipBefore.duration - 0.1;
38204
- const framePath = resolve29(footageDir, `frame-${gap.start.toFixed(2)}.png`);
37896
+ const framePath = resolve28(footageDir, `frame-${gap.start.toFixed(2)}.png`);
38205
37897
  try {
38206
37898
  await execSafe("ffmpeg", [
38207
37899
  "-i",
@@ -38262,10 +37954,10 @@ async function executeFillGaps(options) {
38262
37954
  continue;
38263
37955
  }
38264
37956
  const videoFileName = `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}.mp4`;
38265
- const videoPath = resolve29(footageDir, videoFileName);
37957
+ const videoPath = resolve28(footageDir, videoFileName);
38266
37958
  onProgress("Downloading generated video...");
38267
37959
  const videoBuffer = await downloadVideo(finalResult.videoUrl);
38268
- await writeFile28(videoPath, videoBuffer);
37960
+ await writeFile27(videoPath, videoBuffer);
38269
37961
  generatedDuration = finalResult.duration || parseInt(klingDuration);
38270
37962
  generatedVideos.push(videoPath);
38271
37963
  humanLines.push(` Generated: ${videoFileName} (${generatedDuration}s)`);
@@ -38274,7 +37966,7 @@ async function executeFillGaps(options) {
38274
37966
  const remainingNeeded = targetDuration - generatedDuration;
38275
37967
  const segmentDuration = remainingNeeded > 5 ? "10" : "5";
38276
37968
  onProgress(`Generating additional ${segmentDuration}s segment...`);
38277
- const lastFramePath = resolve29(
37969
+ const lastFramePath = resolve28(
38278
37970
  footageDir,
38279
37971
  `frame-extend-${gap.start.toFixed(2)}-${segmentIndex}.png`
38280
37972
  );
@@ -38330,17 +38022,17 @@ async function executeFillGaps(options) {
38330
38022
  );
38331
38023
  break;
38332
38024
  }
38333
- const segVideoPath = resolve29(
38025
+ const segVideoPath = resolve28(
38334
38026
  footageDir,
38335
38027
  `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}-seg${segmentIndex}.mp4`
38336
38028
  );
38337
38029
  const segVideoBuffer = await downloadVideo(segFinalResult.videoUrl);
38338
- await writeFile28(segVideoPath, segVideoBuffer);
38339
- const concatListPath = resolve29(footageDir, `concat-${gap.start.toFixed(2)}.txt`);
38030
+ await writeFile27(segVideoPath, segVideoBuffer);
38031
+ const concatListPath = resolve28(footageDir, `concat-${gap.start.toFixed(2)}.txt`);
38340
38032
  const concatList = generatedVideos.map((v) => `file '${v}'`).join("\n") + `
38341
38033
  file '${segVideoPath}'`;
38342
- await writeFile28(concatListPath, concatList);
38343
- const concatOutputPath = resolve29(
38034
+ await writeFile27(concatListPath, concatList);
38035
+ const concatOutputPath = resolve28(
38344
38036
  footageDir,
38345
38037
  `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}-merged.mp4`
38346
38038
  );
@@ -38396,8 +38088,8 @@ file '${segVideoPath}'`;
38396
38088
  humanLines.push("");
38397
38089
  let outputPath = filePath;
38398
38090
  if (generatedCount > 0) {
38399
- outputPath = options.output ? resolve29(process.cwd(), options.output) : filePath;
38400
- await writeFile28(outputPath, JSON.stringify(project.toJSON(), null, 2));
38091
+ outputPath = options.output ? resolve28(process.cwd(), options.output) : filePath;
38092
+ await writeFile27(outputPath, JSON.stringify(project.toJSON(), null, 2));
38401
38093
  humanLines.push(`\u2714 Filled ${generatedCount} gap(s) with AI-generated video`);
38402
38094
  humanLines.push(`Project saved: ${outputPath}`);
38403
38095
  } else {
@@ -38493,14 +38185,14 @@ __export(edit_cmd_exports, {
38493
38185
  executeSpeedRamp: () => executeSpeedRamp,
38494
38186
  executeUpscale: () => executeUpscale
38495
38187
  });
38496
- import { resolve as resolve30, dirname as dirname10 } from "node:path";
38497
- import { readFile as readFile20, writeFile as writeFile29, mkdir as mkdir15 } from "node:fs/promises";
38188
+ import { resolve as resolve29, dirname as dirname10 } from "node:path";
38189
+ import { readFile as readFile19, writeFile as writeFile28, mkdir as mkdir15 } from "node:fs/promises";
38498
38190
  async function executeGrade(options) {
38499
38191
  const { videoPath, style, preset, output, analyzeOnly, apiKey } = options;
38500
38192
  try {
38501
38193
  if (!style && !preset) return { success: false, error: "Either style or preset is required" };
38502
38194
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38503
- const absPath = resolve30(process.cwd(), videoPath);
38195
+ const absPath = resolve29(process.cwd(), videoPath);
38504
38196
  let gradeResult;
38505
38197
  if (preset) {
38506
38198
  const claude = new ClaudeProvider();
@@ -38515,7 +38207,7 @@ async function executeGrade(options) {
38515
38207
  if (analyzeOnly) {
38516
38208
  return { success: true, style: preset || style, description: gradeResult.description, ffmpegFilter: gradeResult.ffmpegFilter };
38517
38209
  }
38518
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38210
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38519
38211
  await execSafe("ffmpeg", ["-i", absPath, "-vf", gradeResult.ffmpegFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38520
38212
  return { success: true, outputPath, style: preset || style, description: gradeResult.description, ffmpegFilter: gradeResult.ffmpegFilter };
38521
38213
  } catch (error) {
@@ -38530,12 +38222,12 @@ async function executeSpeedRamp(options) {
38530
38222
  const claudeKey = apiKey || process.env.ANTHROPIC_API_KEY;
38531
38223
  if (!openaiKey) return { success: false, error: "OPENAI_API_KEY required for Whisper transcription" };
38532
38224
  if (!claudeKey) return { success: false, error: "ANTHROPIC_API_KEY required for Claude analysis" };
38533
- const absPath = resolve30(process.cwd(), videoPath);
38225
+ const absPath = resolve29(process.cwd(), videoPath);
38534
38226
  const tempAudio = absPath.replace(/(\.[^.]+)$/, "-temp-audio.mp3");
38535
38227
  await execSafe("ffmpeg", ["-i", absPath, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tempAudio, "-y"]);
38536
38228
  const whisper = new WhisperProvider();
38537
38229
  await whisper.initialize({ apiKey: openaiKey });
38538
- const audioBuffer = await readFile20(tempAudio);
38230
+ const audioBuffer = await readFile19(tempAudio);
38539
38231
  const transcript = await whisper.transcribe(new Blob([audioBuffer]), language);
38540
38232
  try {
38541
38233
  const { unlink: unlink5 } = await import("node:fs/promises");
@@ -38555,7 +38247,7 @@ async function executeSpeedRamp(options) {
38555
38247
  if (speedResult.keyframes.length < 2) {
38556
38248
  return { success: false, error: "Not enough keyframes for speed ramping" };
38557
38249
  }
38558
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38250
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38559
38251
  const setpts = `setpts=${(1 / avgSpeed).toFixed(3)}*PTS`;
38560
38252
  const atempo = avgSpeed >= 0.5 && avgSpeed <= 2 ? `atempo=${avgSpeed.toFixed(3)}` : "";
38561
38253
  if (atempo) {
@@ -38572,7 +38264,7 @@ async function executeReframe(options) {
38572
38264
  const { videoPath, aspect = "9:16", output, analyzeOnly } = options;
38573
38265
  try {
38574
38266
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38575
- const absPath = resolve30(process.cwd(), videoPath);
38267
+ const absPath = resolve29(process.cwd(), videoPath);
38576
38268
  const { stdout: probeOut } = await execSafe("ffprobe", [
38577
38269
  "-v",
38578
38270
  "error",
@@ -38607,7 +38299,7 @@ async function executeReframe(options) {
38607
38299
  keyframeCount: 1
38608
38300
  };
38609
38301
  }
38610
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${aspect.replace(":", "x")}$1`);
38302
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${aspect.replace(":", "x")}$1`);
38611
38303
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `crop=${cropWidth}:${cropHeight}:${cropX}:${cropY}`, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38612
38304
  return { success: true, outputPath, sourceAspect: `${sourceWidth}:${sourceHeight}`, targetAspect: aspect };
38613
38305
  } catch (error) {
@@ -38619,7 +38311,7 @@ async function executeInterpolate(options) {
38619
38311
  try {
38620
38312
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38621
38313
  if (![2, 4, 8].includes(factor)) return { success: false, error: "Factor must be 2, 4, or 8" };
38622
- const absPath = resolve30(process.cwd(), videoPath);
38314
+ const absPath = resolve29(process.cwd(), videoPath);
38623
38315
  const { stdout: fpsOut } = await execSafe("ffprobe", [
38624
38316
  "-v",
38625
38317
  "error",
@@ -38635,7 +38327,7 @@ async function executeInterpolate(options) {
38635
38327
  const originalFps = num / (den || 1);
38636
38328
  const targetFps = options.fps || originalFps * factor;
38637
38329
  const mi = quality === "fast" ? "mi_mode=mci" : "mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1";
38638
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
38330
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
38639
38331
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
38640
38332
  return { success: true, outputPath, originalFps, targetFps, factor };
38641
38333
  } catch (error) {
@@ -38646,7 +38338,7 @@ async function executeUpscale(options) {
38646
38338
  const { videoPath, output, scale = 2, quality = "quality" } = options;
38647
38339
  try {
38648
38340
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38649
- const absPath = resolve30(process.cwd(), videoPath);
38341
+ const absPath = resolve29(process.cwd(), videoPath);
38650
38342
  const { stdout: probeOut } = await execSafe("ffprobe", [
38651
38343
  "-v",
38652
38344
  "error",
@@ -38662,7 +38354,7 @@ async function executeUpscale(options) {
38662
38354
  const targetW = w * scale;
38663
38355
  const targetH = h * scale;
38664
38356
  const scaleFilter = quality === "quality" ? `scale=${targetW}:${targetH}:flags=lanczos` : `scale=${targetW}:${targetH}`;
38665
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${scale}x$1`);
38357
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${scale}x$1`);
38666
38358
  await execSafe("ffmpeg", ["-i", absPath, "-vf", scaleFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38667
38359
  return { success: true, outputPath, originalRes: `${w}x${h}`, targetRes: `${targetW}x${targetH}` };
38668
38360
  } catch (error) {
@@ -38735,7 +38427,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38735
38427
  dryRun: true,
38736
38428
  data: {
38737
38429
  params: {
38738
- videoPath: resolve30(process.cwd(), videoPath),
38430
+ videoPath: resolve29(process.cwd(), videoPath),
38739
38431
  style: options.style || options.preset,
38740
38432
  analyzeOnly: options.analyzeOnly || false
38741
38433
  }
@@ -38762,8 +38454,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38762
38454
  }
38763
38455
  spinner2.succeed(source_default.green("Color grade analyzed"));
38764
38456
  if (isJsonMode()) {
38765
- const absPath2 = resolve30(process.cwd(), videoPath);
38766
- const gradeOutputPath = options.output ? resolve30(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38457
+ const absPath2 = resolve29(process.cwd(), videoPath);
38458
+ const gradeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38767
38459
  outputSuccess({
38768
38460
  command: "edit grade",
38769
38461
  startedAt,
@@ -38789,8 +38481,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38789
38481
  console.log(source_default.dim("Use without --analyze-only to apply the grade."));
38790
38482
  return;
38791
38483
  }
38792
- const absPath = resolve30(process.cwd(), videoPath);
38793
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38484
+ const absPath = resolve29(process.cwd(), videoPath);
38485
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38794
38486
  spinner2.start("Applying color grade...");
38795
38487
  await execSafe("ffmpeg", ["-i", absPath, "-vf", gradeResult.ffmpegFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38796
38488
  spinner2.succeed(source_default.green("Color grade applied"));
@@ -38820,7 +38512,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38820
38512
  dryRun: true,
38821
38513
  data: {
38822
38514
  params: {
38823
- videoPath: resolve30(process.cwd(), videoPath),
38515
+ videoPath: resolve29(process.cwd(), videoPath),
38824
38516
  texts: options.text,
38825
38517
  style: options.style,
38826
38518
  fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
@@ -38833,8 +38525,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38833
38525
  });
38834
38526
  return;
38835
38527
  }
38836
- const absPath = resolve30(process.cwd(), videoPath);
38837
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-overlay$1");
38528
+ const absPath = resolve29(process.cwd(), videoPath);
38529
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-overlay$1");
38838
38530
  const spinner2 = ora("Applying text overlays...").start();
38839
38531
  const result = await applyTextOverlays({
38840
38532
  videoPath: absPath,
@@ -38892,7 +38584,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38892
38584
  dryRun: true,
38893
38585
  data: {
38894
38586
  params: {
38895
- videoPath: resolve30(process.cwd(), videoPath),
38587
+ videoPath: resolve29(process.cwd(), videoPath),
38896
38588
  style: options.style,
38897
38589
  minSpeed: parseFloat(options.minSpeed),
38898
38590
  maxSpeed: parseFloat(options.maxSpeed),
@@ -38904,7 +38596,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38904
38596
  }
38905
38597
  const openaiApiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
38906
38598
  const claudeApiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
38907
- const absPath = resolve30(process.cwd(), videoPath);
38599
+ const absPath = resolve29(process.cwd(), videoPath);
38908
38600
  const spinner2 = ora("Extracting audio...").start();
38909
38601
  const { stdout: speedRampProbe } = await execSafe("ffprobe", [
38910
38602
  "-v",
@@ -38926,7 +38618,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38926
38618
  spinner2.text = "Transcribing audio...";
38927
38619
  const whisper = new WhisperProvider();
38928
38620
  await whisper.initialize({ apiKey: openaiApiKey });
38929
- const audioBuffer = await readFile20(tempAudio);
38621
+ const audioBuffer = await readFile19(tempAudio);
38930
38622
  const audioBlob = new Blob([audioBuffer]);
38931
38623
  const transcript = await whisper.transcribe(audioBlob, options.language);
38932
38624
  if (!transcript.segments || transcript.segments.length === 0) {
@@ -38949,7 +38641,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38949
38641
  spinner2.succeed(source_default.green(`Found ${speedResult.keyframes.length} speed keyframes`));
38950
38642
  if (isJsonMode()) {
38951
38643
  const avgSpeed2 = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
38952
- const speedRampOutputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38644
+ const speedRampOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38953
38645
  outputSuccess({
38954
38646
  command: "edit speed-ramp",
38955
38647
  startedAt,
@@ -38978,7 +38670,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38978
38670
  return;
38979
38671
  }
38980
38672
  spinner2.start("Applying speed ramps...");
38981
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38673
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38982
38674
  const avgSpeed = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
38983
38675
  const setpts = `setpts=${(1 / avgSpeed).toFixed(3)}*PTS`;
38984
38676
  const atempo = avgSpeed >= 0.5 && avgSpeed <= 2 ? `atempo=${avgSpeed.toFixed(3)}` : "";
@@ -39012,7 +38704,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39012
38704
  dryRun: true,
39013
38705
  data: {
39014
38706
  params: {
39015
- videoPath: resolve30(process.cwd(), videoPath),
38707
+ videoPath: resolve29(process.cwd(), videoPath),
39016
38708
  aspect: options.aspect,
39017
38709
  focus: options.focus,
39018
38710
  analyzeOnly: options.analyzeOnly || false
@@ -39021,7 +38713,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39021
38713
  });
39022
38714
  return;
39023
38715
  }
39024
- const absPath = resolve30(process.cwd(), videoPath);
38716
+ const absPath = resolve29(process.cwd(), videoPath);
39025
38717
  const spinner2 = ora("Analyzing video...").start();
39026
38718
  const { stdout: probeOut } = await execSafe("ffprobe", [
39027
38719
  "-v",
@@ -39059,7 +38751,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39059
38751
  for (let i = 1; i <= numKeyframes && i <= 30; i++) {
39060
38752
  const framePath = `${tempDir}/frame-${i.toString().padStart(4, "0")}.jpg`;
39061
38753
  try {
39062
- const frameBuffer = await readFile20(framePath);
38754
+ const frameBuffer = await readFile19(framePath);
39063
38755
  const frameBase64 = frameBuffer.toString("base64");
39064
38756
  const result = await claude.analyzeFrameForReframe(frameBase64, options.aspect, {
39065
38757
  focusMode: options.focus,
@@ -39083,7 +38775,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39083
38775
  }
39084
38776
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
39085
38777
  if (isJsonMode()) {
39086
- const reframeOutputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
38778
+ const reframeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39087
38779
  outputSuccess({
39088
38780
  command: "edit reframe",
39089
38781
  startedAt,
@@ -39118,8 +38810,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39118
38810
  }
39119
38811
  console.log();
39120
38812
  if (options.keyframes) {
39121
- const keyframesPath = resolve30(process.cwd(), options.keyframes);
39122
- await writeFile29(keyframesPath, JSON.stringify(cropKeyframes, null, 2));
38813
+ const keyframesPath = resolve29(process.cwd(), options.keyframes);
38814
+ await writeFile28(keyframesPath, JSON.stringify(cropKeyframes, null, 2));
39123
38815
  console.log(source_default.green(`Keyframes saved to: ${keyframesPath}`));
39124
38816
  }
39125
38817
  if (options.analyzeOnly) {
@@ -39130,7 +38822,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39130
38822
  const avgCropY = Math.round(cropKeyframes.reduce((sum, kf) => sum + kf.cropY, 0) / cropKeyframes.length);
39131
38823
  const cropWidth = cropKeyframes[0]?.cropWidth || sourceWidth;
39132
38824
  const cropHeight = cropKeyframes[0]?.cropHeight || sourceHeight;
39133
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
38825
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39134
38826
  spinner2.start("Applying reframe...");
39135
38827
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `crop=${cropWidth}:${cropHeight}:${avgCropX}:${avgCropY}`, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
39136
38828
  spinner2.succeed(source_default.green("Reframe applied"));
@@ -39166,7 +38858,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39166
38858
  dryRun: true,
39167
38859
  data: {
39168
38860
  params: {
39169
- imagePaths: imagePaths.map((p) => resolve30(process.cwd(), p)),
38861
+ imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
39170
38862
  prompt: prompt3,
39171
38863
  provider,
39172
38864
  model: options.model,
@@ -39187,8 +38879,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39187
38879
  const spinner2 = ora(`Reading ${imagePaths.length} image(s)...`).start();
39188
38880
  const imageBuffers = [];
39189
38881
  for (const imagePath of imagePaths) {
39190
- const absPath = resolve30(process.cwd(), imagePath);
39191
- const buffer = await readFile20(absPath);
38882
+ const absPath = resolve29(process.cwd(), imagePath);
38883
+ const buffer = await readFile19(absPath);
39192
38884
  imageBuffers.push(buffer);
39193
38885
  }
39194
38886
  let result;
@@ -39236,16 +38928,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39236
38928
  }
39237
38929
  spinner2.succeed(source_default.green("Image edited"));
39238
38930
  const img = result.images[0];
39239
- const outputPath = resolve30(process.cwd(), options.output);
38931
+ const outputPath = resolve29(process.cwd(), options.output);
39240
38932
  const saveImage = async () => {
39241
38933
  await mkdir15(dirname10(outputPath), { recursive: true });
39242
38934
  if (img.base64) {
39243
38935
  const buffer = Buffer.from(img.base64, "base64");
39244
- await writeFile29(outputPath, buffer);
38936
+ await writeFile28(outputPath, buffer);
39245
38937
  } else if (img.url) {
39246
38938
  const resp = await fetch(img.url);
39247
38939
  const arrayBuf = await resp.arrayBuffer();
39248
- await writeFile29(outputPath, Buffer.from(arrayBuf));
38940
+ await writeFile28(outputPath, Buffer.from(arrayBuf));
39249
38941
  }
39250
38942
  };
39251
38943
  const resultModel = result.model;
@@ -39278,7 +38970,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39278
38970
  if (options.output) {
39279
38971
  validateOutputPath(options.output);
39280
38972
  }
39281
- const absPath = resolve30(process.cwd(), videoPath);
38973
+ const absPath = resolve29(process.cwd(), videoPath);
39282
38974
  const factor = parseInt(options.factor);
39283
38975
  if (![2, 4, 8].includes(factor)) {
39284
38976
  exitWithError(usageError("Factor must be 2, 4, or 8"));
@@ -39299,7 +38991,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39299
38991
  });
39300
38992
  return;
39301
38993
  }
39302
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
38994
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
39303
38995
  const spinner2 = ora(`Creating ${factor}x slow motion...`).start();
39304
38996
  try {
39305
38997
  const { stdout: fpsOut } = await execSafe("ffprobe", [
@@ -39360,7 +39052,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39360
39052
  if (options.output) {
39361
39053
  validateOutputPath(options.output);
39362
39054
  }
39363
- const absPath = resolve30(process.cwd(), videoPath);
39055
+ const absPath = resolve29(process.cwd(), videoPath);
39364
39056
  const scale = parseInt(options.scale);
39365
39057
  if (scale !== 2 && scale !== 4) {
39366
39058
  exitWithError(usageError("Scale must be 2 or 4"));
@@ -39382,7 +39074,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39382
39074
  return;
39383
39075
  }
39384
39076
  if (options.ffmpeg) {
39385
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-upscaled-${scale}x$1`);
39077
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-upscaled-${scale}x$1`);
39386
39078
  const spinner3 = ora(`Upscaling video with FFmpeg (${scale}x)...`).start();
39387
39079
  try {
39388
39080
  const { stdout: probeOut } = await execSafe("ffprobe", [
@@ -39463,9 +39155,9 @@ __export(ai_analyze_exports, {
39463
39155
  executeGeminiVideo: () => executeGeminiVideo,
39464
39156
  registerAnalyzeCommands: () => registerAnalyzeCommands
39465
39157
  });
39466
- import { readFile as readFile21 } from "node:fs/promises";
39467
- import { extname as extname6, resolve as resolve31 } from "node:path";
39468
- import { existsSync as existsSync24 } from "node:fs";
39158
+ import { readFile as readFile20 } from "node:fs/promises";
39159
+ import { extname as extname6, resolve as resolve30 } from "node:path";
39160
+ import { existsSync as existsSync23 } from "node:fs";
39469
39161
  async function executeGeminiVideo(options) {
39470
39162
  try {
39471
39163
  const apiKey = await getApiKey("GOOGLE_API_KEY", "Google");
@@ -39483,11 +39175,11 @@ async function executeGeminiVideo(options) {
39483
39175
  if (isYouTube) {
39484
39176
  videoData = options.source;
39485
39177
  } else {
39486
- const absPath = resolve31(process.cwd(), options.source);
39487
- if (!existsSync24(absPath)) {
39178
+ const absPath = resolve30(process.cwd(), options.source);
39179
+ if (!existsSync23(absPath)) {
39488
39180
  return { success: false, error: `File not found: ${absPath}` };
39489
39181
  }
39490
- videoData = await readFile21(absPath);
39182
+ videoData = await readFile20(absPath);
39491
39183
  }
39492
39184
  const gemini = new GeminiProvider();
39493
39185
  await gemini.initialize({ apiKey });
@@ -39556,11 +39248,11 @@ async function executeAnalyze(options) {
39556
39248
  }
39557
39249
  imageBuffer = Buffer.from(await response.arrayBuffer());
39558
39250
  } else {
39559
- const absPath = resolve31(process.cwd(), source);
39560
- if (!existsSync24(absPath)) {
39251
+ const absPath = resolve30(process.cwd(), source);
39252
+ if (!existsSync23(absPath)) {
39561
39253
  return { success: false, error: `File not found: ${absPath}` };
39562
39254
  }
39563
- imageBuffer = await readFile21(absPath);
39255
+ imageBuffer = await readFile20(absPath);
39564
39256
  }
39565
39257
  const result2 = await gemini.analyzeImage(imageBuffer, options.prompt, {
39566
39258
  model: modelId,
@@ -39591,11 +39283,11 @@ async function executeAnalyze(options) {
39591
39283
  }
39592
39284
  videoData = Buffer.from(await response.arrayBuffer());
39593
39285
  } else {
39594
- const absPath = resolve31(process.cwd(), source);
39595
- if (!existsSync24(absPath)) {
39286
+ const absPath = resolve30(process.cwd(), source);
39287
+ if (!existsSync23(absPath)) {
39596
39288
  return { success: false, error: `File not found: ${absPath}` };
39597
39289
  }
39598
- videoData = await readFile21(absPath);
39290
+ videoData = await readFile20(absPath);
39599
39291
  }
39600
39292
  const result = await gemini.analyzeVideo(videoData, options.prompt, {
39601
39293
  model: modelId,
@@ -39729,9 +39421,9 @@ __export(ai_review_exports, {
39729
39421
  executeReview: () => executeReview,
39730
39422
  registerReviewCommand: () => registerReviewCommand
39731
39423
  });
39732
- import { readFile as readFile22, rename as rename3 } from "node:fs/promises";
39733
- import { resolve as resolve32 } from "node:path";
39734
- import { existsSync as existsSync25 } from "node:fs";
39424
+ import { readFile as readFile21, rename as rename2 } from "node:fs/promises";
39425
+ import { resolve as resolve31 } from "node:path";
39426
+ import { existsSync as existsSync24 } from "node:fs";
39735
39427
  function parseReviewFeedback(response) {
39736
39428
  let cleaned = response.trim();
39737
39429
  if (cleaned.startsWith("```json")) {
@@ -39755,8 +39447,8 @@ function parseReviewFeedback(response) {
39755
39447
  }
39756
39448
  async function executeReview(options) {
39757
39449
  const { videoPath, storyboardPath, autoApply = false, verify = false, model = "flash" } = options;
39758
- const absVideoPath = resolve32(process.cwd(), videoPath);
39759
- if (!existsSync25(absVideoPath)) {
39450
+ const absVideoPath = resolve31(process.cwd(), videoPath);
39451
+ if (!existsSync24(absVideoPath)) {
39760
39452
  return { success: false, error: `Video not found: ${absVideoPath}` };
39761
39453
  }
39762
39454
  const apiKey = process.env.GOOGLE_API_KEY || await getApiKey("GOOGLE_API_KEY", "Google");
@@ -39765,9 +39457,9 @@ async function executeReview(options) {
39765
39457
  }
39766
39458
  let storyboardContext = "";
39767
39459
  if (storyboardPath) {
39768
- const absStoryboardPath = resolve32(process.cwd(), storyboardPath);
39769
- if (existsSync25(absStoryboardPath)) {
39770
- const content = await readFile22(absStoryboardPath, "utf-8");
39460
+ const absStoryboardPath = resolve31(process.cwd(), storyboardPath);
39461
+ if (existsSync24(absStoryboardPath)) {
39462
+ const content = await readFile21(absStoryboardPath, "utf-8");
39771
39463
  storyboardContext = `
39772
39464
 
39773
39465
  Original storyboard for reference:
@@ -39800,7 +39492,7 @@ ${content}`;
39800
39492
  Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFixable. Be specific and practical.${storyboardContext}`;
39801
39493
  const gemini = new GeminiProvider();
39802
39494
  await gemini.initialize({ apiKey });
39803
- const videoData = await readFile22(absVideoPath);
39495
+ const videoData = await readFile21(absVideoPath);
39804
39496
  const analysisResult = await gemini.analyzeVideo(videoData, reviewPrompt, {
39805
39497
  model: modelId
39806
39498
  });
@@ -39821,7 +39513,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39821
39513
  };
39822
39514
  if (autoApply && feedback.autoFixable.length > 0) {
39823
39515
  let currentInput = absVideoPath;
39824
- const outputBase = options.outputPath ? resolve32(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
39516
+ const outputBase = options.outputPath ? resolve31(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
39825
39517
  for (const fix of feedback.autoFixable) {
39826
39518
  if (fix.type === "color_grade" && fix.ffmpegFilter) {
39827
39519
  try {
@@ -39838,7 +39530,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39838
39530
  if (currentInput !== absVideoPath) {
39839
39531
  const finalOutput = outputBase;
39840
39532
  try {
39841
- await rename3(currentInput, finalOutput);
39533
+ await rename2(currentInput, finalOutput);
39842
39534
  result.outputPath = finalOutput;
39843
39535
  } catch {
39844
39536
  result.outputPath = currentInput;
@@ -39846,7 +39538,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39846
39538
  }
39847
39539
  }
39848
39540
  if (verify && result.outputPath) {
39849
- const verifyVideoData = await readFile22(result.outputPath);
39541
+ const verifyVideoData = await readFile21(result.outputPath);
39850
39542
  const verifyResult = await gemini.analyzeVideo(
39851
39543
  verifyVideoData,
39852
39544
  'Rate this video overall quality on a scale of 1-10. Return ONLY a JSON object: {"score": <number>}',
@@ -39905,73 +39597,657 @@ function registerReviewCommand(aiCommand) {
39905
39597
  spinner2.fail(result.error || "Video review failed");
39906
39598
  exitWithError(apiError(result.error || "Video review failed", true));
39907
39599
  }
39908
- spinner2.succeed(source_default.green("Video review complete"));
39909
- console.log();
39910
- const fb = result.feedback;
39911
- console.log(source_default.bold.cyan("Video Review"));
39912
- console.log(source_default.dim("\u2500".repeat(60)));
39913
- console.log(`Overall Score: ${source_default.bold(fb.overallScore >= 7 ? source_default.green(String(fb.overallScore)) : fb.overallScore >= 5 ? source_default.yellow(String(fb.overallScore)) : source_default.red(String(fb.overallScore)))}/10`);
39914
- console.log();
39915
- const categories = [
39916
- ["Pacing", fb.categories.pacing],
39917
- ["Color", fb.categories.color],
39918
- ["Text Readability", fb.categories.textReadability],
39919
- ["Audio-Visual Sync", fb.categories.audioVisualSync],
39920
- ["Composition", fb.categories.composition]
39921
- ];
39922
- for (const [name, cat] of categories) {
39923
- const scoreColor = cat.score >= 7 ? source_default.green : cat.score >= 5 ? source_default.yellow : source_default.red;
39924
- const fixable = cat.fixable ? source_default.dim(" [fixable]") : "";
39925
- console.log(` ${name.padEnd(20)} ${scoreColor(String(cat.score).padStart(2))}/10${fixable}`);
39926
- if (cat.issues.length > 0) {
39927
- for (const issue of cat.issues) {
39928
- console.log(source_default.dim(` - ${issue}`));
39600
+ spinner2.succeed(source_default.green("Video review complete"));
39601
+ console.log();
39602
+ const fb = result.feedback;
39603
+ console.log(source_default.bold.cyan("Video Review"));
39604
+ console.log(source_default.dim("\u2500".repeat(60)));
39605
+ console.log(`Overall Score: ${source_default.bold(fb.overallScore >= 7 ? source_default.green(String(fb.overallScore)) : fb.overallScore >= 5 ? source_default.yellow(String(fb.overallScore)) : source_default.red(String(fb.overallScore)))}/10`);
39606
+ console.log();
39607
+ const categories = [
39608
+ ["Pacing", fb.categories.pacing],
39609
+ ["Color", fb.categories.color],
39610
+ ["Text Readability", fb.categories.textReadability],
39611
+ ["Audio-Visual Sync", fb.categories.audioVisualSync],
39612
+ ["Composition", fb.categories.composition]
39613
+ ];
39614
+ for (const [name, cat] of categories) {
39615
+ const scoreColor = cat.score >= 7 ? source_default.green : cat.score >= 5 ? source_default.yellow : source_default.red;
39616
+ const fixable = cat.fixable ? source_default.dim(" [fixable]") : "";
39617
+ console.log(` ${name.padEnd(20)} ${scoreColor(String(cat.score).padStart(2))}/10${fixable}`);
39618
+ if (cat.issues.length > 0) {
39619
+ for (const issue of cat.issues) {
39620
+ console.log(source_default.dim(` - ${issue}`));
39621
+ }
39622
+ }
39623
+ }
39624
+ if (result.appliedFixes && result.appliedFixes.length > 0) {
39625
+ console.log();
39626
+ console.log(source_default.bold.green("Applied Fixes:"));
39627
+ for (const fix of result.appliedFixes) {
39628
+ console.log(source_default.green(` + ${fix}`));
39629
+ }
39630
+ if (result.outputPath) {
39631
+ console.log(source_default.green(` Output: ${result.outputPath}`));
39632
+ }
39633
+ }
39634
+ if (result.verificationScore !== void 0) {
39635
+ console.log();
39636
+ console.log(source_default.bold(`Verification Score: ${result.verificationScore}/10`));
39637
+ }
39638
+ if (fb.recommendations.length > 0) {
39639
+ console.log();
39640
+ console.log(source_default.bold("Recommendations:"));
39641
+ for (const rec of fb.recommendations) {
39642
+ console.log(source_default.dim(` * ${rec}`));
39643
+ }
39644
+ }
39645
+ console.log();
39646
+ } catch (error) {
39647
+ exitWithError(generalError(error instanceof Error ? error.message : "Video review failed"));
39648
+ }
39649
+ });
39650
+ }
39651
+ var init_ai_review = __esm({
39652
+ "src/commands/ai-review.ts"() {
39653
+ "use strict";
39654
+ init_source();
39655
+ init_ora();
39656
+ init_dist2();
39657
+ init_api_key();
39658
+ init_exec_safe();
39659
+ init_output();
39660
+ init_validate();
39661
+ }
39662
+ });
39663
+
39664
+ // src/commands/_shared/video-providers.ts
39665
+ async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
39666
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39667
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39668
+ try {
39669
+ const result = await grok.generateVideo(prompt3, {
39670
+ prompt: prompt3,
39671
+ duration: options.duration,
39672
+ aspectRatio: options.aspectRatio,
39673
+ referenceImage: options.referenceImage
39674
+ });
39675
+ if (result.status !== "failed" && result.id) {
39676
+ return { requestId: result.id };
39677
+ }
39678
+ const providerErr = result.error || "Grok returned failed status";
39679
+ if (attempt < maxRetries) {
39680
+ onProgress?.(
39681
+ `\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`
39682
+ );
39683
+ await sleep(RETRY_DELAY_MS);
39684
+ } else {
39685
+ console.error(source_default.dim(`
39686
+ [Grok error: ${providerErr}]`));
39687
+ }
39688
+ } catch (err) {
39689
+ const errMsg = err instanceof Error ? err.message : String(err);
39690
+ if (attempt < maxRetries) {
39691
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39692
+ await sleep(RETRY_DELAY_MS);
39693
+ } else {
39694
+ console.error(source_default.dim(`
39695
+ [Grok error: ${errMsg}]`));
39696
+ }
39697
+ }
39698
+ }
39699
+ return null;
39700
+ }
39701
+ async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
39702
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39703
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39704
+ try {
39705
+ const result = await kling.generateVideo(prompt3, {
39706
+ prompt: prompt3,
39707
+ // Pass reference image (base64 or URL) - KlingProvider handles v1.5 fallback for base64
39708
+ referenceImage: options.referenceImage,
39709
+ duration: options.duration,
39710
+ aspectRatio: options.aspectRatio,
39711
+ mode: "std"
39712
+ // std mode for faster generation
39713
+ });
39714
+ if (result.status !== "failed" && result.id) {
39715
+ return {
39716
+ taskId: result.id,
39717
+ type: options.referenceImage ? "image2video" : "text2video"
39718
+ };
39719
+ }
39720
+ const providerErr = result.error || "Kling returned failed status";
39721
+ if (attempt < maxRetries) {
39722
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39723
+ await sleep(RETRY_DELAY_MS);
39724
+ } else {
39725
+ console.error(source_default.dim(`
39726
+ [Kling error: ${providerErr}]`));
39727
+ }
39728
+ } catch (err) {
39729
+ const errMsg = err instanceof Error ? err.message : String(err);
39730
+ if (attempt < maxRetries) {
39731
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39732
+ await sleep(RETRY_DELAY_MS);
39733
+ } else {
39734
+ console.error(source_default.dim(`
39735
+ [Kling error: ${errMsg}]`));
39736
+ }
39737
+ }
39738
+ }
39739
+ return null;
39740
+ }
39741
+ async function generateVideoWithRetryRunway(runway, segment, referenceImage, options, maxRetries, onProgress) {
39742
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39743
+ try {
39744
+ const result = await runway.generateVideo(segment.visuals, {
39745
+ prompt: segment.visuals,
39746
+ referenceImage,
39747
+ duration: options.duration,
39748
+ aspectRatio: options.aspectRatio
39749
+ });
39750
+ if (result.status !== "failed" && result.id) {
39751
+ return { taskId: result.id };
39752
+ }
39753
+ const providerErr = result.error || "Runway returned failed status";
39754
+ if (attempt < maxRetries) {
39755
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39756
+ await sleep(RETRY_DELAY_MS);
39757
+ } else {
39758
+ console.error(source_default.dim(`
39759
+ [Runway error: ${providerErr}]`));
39760
+ }
39761
+ } catch (err) {
39762
+ const errMsg = err instanceof Error ? err.message : String(err);
39763
+ if (attempt < maxRetries) {
39764
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39765
+ await sleep(RETRY_DELAY_MS);
39766
+ } else {
39767
+ console.error(source_default.dim(`
39768
+ [Runway error: ${errMsg}]`));
39769
+ }
39770
+ }
39771
+ }
39772
+ return null;
39773
+ }
39774
+ async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, onProgress) {
39775
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39776
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39777
+ try {
39778
+ const result = await gemini.generateVideo(prompt3, {
39779
+ prompt: prompt3,
39780
+ referenceImage: options.referenceImage,
39781
+ duration: options.duration,
39782
+ aspectRatio: options.aspectRatio,
39783
+ model: "veo-3.1-fast-generate-preview"
39784
+ });
39785
+ if (result.status !== "failed" && result.id) {
39786
+ return { operationName: result.id };
39787
+ }
39788
+ const providerErr = result.error || "Veo returned failed status";
39789
+ if (attempt < maxRetries) {
39790
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39791
+ await sleep(RETRY_DELAY_MS);
39792
+ } else {
39793
+ console.error(source_default.dim(`
39794
+ [Veo error: ${providerErr}]`));
39795
+ }
39796
+ } catch (err) {
39797
+ const errMsg = err instanceof Error ? err.message : String(err);
39798
+ if (attempt < maxRetries) {
39799
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39800
+ await sleep(RETRY_DELAY_MS);
39801
+ } else {
39802
+ console.error(source_default.dim(`
39803
+ [Veo error: ${errMsg}]`));
39804
+ }
39805
+ }
39806
+ }
39807
+ return null;
39808
+ }
39809
+ var init_video_providers = __esm({
39810
+ "src/commands/_shared/video-providers.ts"() {
39811
+ "use strict";
39812
+ init_source();
39813
+ init_video_utils();
39814
+ }
39815
+ });
39816
+
39817
+ // src/commands/ai-script-pipeline.ts
39818
+ import { readFile as readFile24, writeFile as writeFile31, unlink as unlink2, rename as rename3 } from "node:fs/promises";
39819
+ import { resolve as resolve34, extname as extname8 } from "node:path";
39820
+ import { existsSync as existsSync26 } from "node:fs";
39821
+ async function executeRegenerateScene(options) {
39822
+ const result = {
39823
+ success: false,
39824
+ regeneratedScenes: [],
39825
+ failedScenes: []
39826
+ };
39827
+ try {
39828
+ const outputDir = resolve34(process.cwd(), options.projectDir);
39829
+ if (!existsSync26(outputDir)) {
39830
+ return { ...result, error: `Project directory not found: ${outputDir}` };
39831
+ }
39832
+ const yamlPath = resolve34(outputDir, "storyboard.yaml");
39833
+ const jsonPath = resolve34(outputDir, "storyboard.json");
39834
+ const storyboardPath = existsSync26(yamlPath) ? yamlPath : existsSync26(jsonPath) ? jsonPath : null;
39835
+ if (!storyboardPath) {
39836
+ return { ...result, error: `Storyboard not found in: ${outputDir} (expected storyboard.yaml or storyboard.json)` };
39837
+ }
39838
+ const storyboardContent = await readFile24(storyboardPath, "utf-8");
39839
+ const segments = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.parse)(storyboardContent).scenes : JSON.parse(storyboardContent);
39840
+ for (const sceneNum of options.scenes) {
39841
+ if (sceneNum < 1 || sceneNum > segments.length) {
39842
+ return { ...result, error: `Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.` };
39843
+ }
39844
+ }
39845
+ const regenerateVideo = options.videoOnly || !options.narrationOnly && !options.imageOnly;
39846
+ const regenerateNarration = options.narrationOnly || !options.videoOnly && !options.imageOnly;
39847
+ const regenerateImage = options.imageOnly || !options.videoOnly && !options.narrationOnly;
39848
+ let videoApiKey;
39849
+ if (regenerateVideo) {
39850
+ const generatorKeyMap = {
39851
+ grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
39852
+ kling: { envVar: "KLING_API_KEY", name: "Kling" },
39853
+ runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
39854
+ veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" }
39855
+ };
39856
+ const generator = options.generator || "grok";
39857
+ const genInfo = generatorKeyMap[generator];
39858
+ if (!genInfo) {
39859
+ return { ...result, error: `Invalid generator: ${generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
39860
+ }
39861
+ videoApiKey = await getApiKey(genInfo.envVar, genInfo.name) ?? void 0;
39862
+ if (!videoApiKey) {
39863
+ return { ...result, error: `${genInfo.name} API key required. Run 'vibe setup' or set ${genInfo.envVar} in .env` };
39864
+ }
39865
+ }
39866
+ let imageApiKey;
39867
+ if (regenerateImage) {
39868
+ const imageProvider = options.imageProvider || "openai";
39869
+ const imageKeyMap = {
39870
+ openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
39871
+ gemini: { envVar: "GOOGLE_API_KEY", name: "Google" },
39872
+ grok: { envVar: "XAI_API_KEY", name: "xAI" }
39873
+ };
39874
+ const info = imageKeyMap[imageProvider];
39875
+ if (!info) {
39876
+ return { ...result, error: `Invalid imageProvider: ${imageProvider}` };
39877
+ }
39878
+ imageApiKey = await getApiKey(info.envVar, info.name) ?? void 0;
39879
+ if (!imageApiKey) {
39880
+ return { ...result, error: `${info.name} API key required. Run 'vibe setup' or set ${info.envVar} in .env` };
39881
+ }
39882
+ }
39883
+ let elevenlabsApiKey;
39884
+ if (regenerateNarration) {
39885
+ elevenlabsApiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs") ?? void 0;
39886
+ if (!elevenlabsApiKey) {
39887
+ return { ...result, error: "ElevenLabs API key required. Run 'vibe setup' or set ELEVENLABS_API_KEY in .env" };
39888
+ }
39889
+ }
39890
+ let storyboardMutated = false;
39891
+ for (const sceneNum of options.scenes) {
39892
+ const segment = segments[sceneNum - 1];
39893
+ const narrationPath = resolve34(outputDir, `narration-${sceneNum}.mp3`);
39894
+ const imagePath = resolve34(outputDir, `scene-${sceneNum}.png`);
39895
+ const videoPath = resolve34(outputDir, `scene-${sceneNum}.mp4`);
39896
+ let sceneFailed = false;
39897
+ if (regenerateNarration && elevenlabsApiKey) {
39898
+ options.onProgress?.(`Scene ${sceneNum}: regenerating narration...`);
39899
+ const elevenlabs = new ElevenLabsProvider();
39900
+ await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
39901
+ const narrationText = segment.narration || segment.description;
39902
+ const ttsResult = await elevenlabs.textToSpeech(narrationText, {
39903
+ voiceId: options.voice
39904
+ });
39905
+ if (ttsResult.success && ttsResult.audioBuffer) {
39906
+ await writeFile31(narrationPath, ttsResult.audioBuffer);
39907
+ segment.duration = await getAudioDuration(narrationPath);
39908
+ storyboardMutated = true;
39909
+ } else {
39910
+ sceneFailed = true;
39911
+ }
39912
+ }
39913
+ if (!sceneFailed && regenerateImage && imageApiKey) {
39914
+ options.onProgress?.(`Scene ${sceneNum}: regenerating image...`);
39915
+ const imageProvider = options.imageProvider || "openai";
39916
+ const characterDesc = segment.characterDescription || segments[0]?.characterDescription;
39917
+ let imagePrompt = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39918
+ if (characterDesc) {
39919
+ imagePrompt = `${imagePrompt}
39920
+
39921
+ IMPORTANT - Character appearance must match exactly: ${characterDesc}`;
39922
+ }
39923
+ let referenceImageBuffer;
39924
+ const refSceneNum = options.referenceScene;
39925
+ if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
39926
+ const refImagePath = resolve34(outputDir, `scene-${refSceneNum}.png`);
39927
+ if (existsSync26(refImagePath)) {
39928
+ referenceImageBuffer = await readFile24(refImagePath);
39929
+ }
39930
+ } else if (!refSceneNum) {
39931
+ for (let i = 1; i <= segments.length; i++) {
39932
+ if (i !== sceneNum) {
39933
+ const otherImagePath = resolve34(outputDir, `scene-${i}.png`);
39934
+ if (existsSync26(otherImagePath)) {
39935
+ referenceImageBuffer = await readFile24(otherImagePath);
39936
+ break;
39937
+ }
39938
+ }
39929
39939
  }
39930
39940
  }
39931
- }
39932
- if (result.appliedFixes && result.appliedFixes.length > 0) {
39933
- console.log();
39934
- console.log(source_default.bold.green("Applied Fixes:"));
39935
- for (const fix of result.appliedFixes) {
39936
- console.log(source_default.green(` + ${fix}`));
39941
+ const dalleImageSizes = {
39942
+ "16:9": "1536x1024",
39943
+ "9:16": "1024x1536",
39944
+ "1:1": "1024x1024"
39945
+ };
39946
+ let imageBuffer;
39947
+ let imageUrl;
39948
+ if (imageProvider === "openai") {
39949
+ const openaiImage = new OpenAIImageProvider();
39950
+ await openaiImage.initialize({ apiKey: imageApiKey });
39951
+ const imageResult = await openaiImage.generateImage(imagePrompt, {
39952
+ size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
39953
+ quality: "standard"
39954
+ });
39955
+ if (imageResult.success && imageResult.images?.[0]) {
39956
+ const img = imageResult.images[0];
39957
+ if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
39958
+ else if (img.url) imageUrl = img.url;
39959
+ }
39960
+ } else if (imageProvider === "gemini") {
39961
+ const gemini = new GeminiProvider();
39962
+ await gemini.initialize({ apiKey: imageApiKey });
39963
+ if (referenceImageBuffer) {
39964
+ const simplifiedVisuals = segment.visuals.split(/[,.]/).find(
39965
+ (part) => part.includes("standing") || part.includes("sitting") || part.includes("walking") || part.includes("lying") || part.includes("reaching") || part.includes("looking") || part.includes("working") || part.includes("coding") || part.includes("typing")
39966
+ ) || segment.visuals.split(".")[0];
39967
+ const editPrompt = `Generate a new image showing the SAME SINGLE person from the reference image in a new scene.
39968
+
39969
+ REFERENCE: Look at the person in the reference image - their face, hair, build, and overall appearance.
39970
+
39971
+ NEW SCENE: ${simplifiedVisuals}
39972
+
39973
+ CRITICAL RULES:
39974
+ 1. Show ONLY ONE person - the exact same individual from the reference image
39975
+ 2. The person must have the IDENTICAL face, hair style, and body type
39976
+ 3. Do NOT show multiple people or duplicate the character
39977
+ 4. Create a single moment in time, one pose, one action
39978
+ 5. Match the art style and quality of the reference image
39979
+
39980
+ Generate the single-person scene image now.`;
39981
+ const imageResult = await gemini.editImage([referenceImageBuffer], editPrompt, {
39982
+ aspectRatio: options.aspectRatio || "16:9"
39983
+ });
39984
+ if (imageResult.success && imageResult.images?.[0]?.base64) {
39985
+ imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
39986
+ }
39987
+ } else {
39988
+ const imageResult = await gemini.generateImage(imagePrompt, {
39989
+ aspectRatio: options.aspectRatio || "16:9"
39990
+ });
39991
+ if (imageResult.success && imageResult.images?.[0]?.base64) {
39992
+ imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
39993
+ }
39994
+ }
39995
+ } else if (imageProvider === "grok") {
39996
+ const grok = new GrokProvider();
39997
+ await grok.initialize({ apiKey: imageApiKey });
39998
+ const imageResult = await grok.generateImage(imagePrompt, {
39999
+ aspectRatio: options.aspectRatio || "16:9"
40000
+ });
40001
+ if (imageResult.success && imageResult.images?.[0]) {
40002
+ const img = imageResult.images[0];
40003
+ if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
40004
+ else if (img.url) imageUrl = img.url;
40005
+ }
39937
40006
  }
39938
- if (result.outputPath) {
39939
- console.log(source_default.green(` Output: ${result.outputPath}`));
40007
+ if (imageBuffer) {
40008
+ await writeFile31(imagePath, imageBuffer);
40009
+ } else if (imageUrl) {
40010
+ const response = await fetch(imageUrl);
40011
+ const buffer = Buffer.from(await response.arrayBuffer());
40012
+ await writeFile31(imagePath, buffer);
40013
+ } else {
40014
+ sceneFailed = true;
39940
40015
  }
39941
40016
  }
39942
- if (result.verificationScore !== void 0) {
39943
- console.log();
39944
- console.log(source_default.bold(`Verification Score: ${result.verificationScore}/10`));
40017
+ if (sceneFailed) {
40018
+ result.failedScenes.push(sceneNum);
40019
+ continue;
39945
40020
  }
39946
- if (fb.recommendations.length > 0) {
39947
- console.log();
39948
- console.log(source_default.bold("Recommendations:"));
39949
- for (const rec of fb.recommendations) {
39950
- console.log(source_default.dim(` * ${rec}`));
40021
+ if (regenerateVideo && videoApiKey) {
40022
+ options.onProgress?.(`Scene ${sceneNum}: regenerating video (${options.generator || "grok"})...`);
40023
+ if (!existsSync26(imagePath)) {
40024
+ result.failedScenes.push(sceneNum);
40025
+ continue;
40026
+ }
40027
+ const imageBuffer = await readFile24(imagePath);
40028
+ const videoDuration = segment.duration > 5 ? 10 : 5;
40029
+ const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
40030
+ if (options.generator === "grok") {
40031
+ const grok = new GrokProvider();
40032
+ await grok.initialize({ apiKey: videoApiKey });
40033
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40034
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40035
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40036
+ const grokDuration = Math.min(15, Math.max(1, segment.duration));
40037
+ const taskResult = await generateVideoWithRetryGrok(
40038
+ grok,
40039
+ segment,
40040
+ {
40041
+ duration: grokDuration,
40042
+ aspectRatio: options.aspectRatio || "16:9",
40043
+ referenceImage
40044
+ },
40045
+ maxRetries
40046
+ );
40047
+ if (taskResult) {
40048
+ try {
40049
+ const waitResult = await grok.waitForCompletion(taskResult.requestId, void 0, 3e5);
40050
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40051
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40052
+ await writeFile31(videoPath, buffer);
40053
+ const targetDuration = segment.duration;
40054
+ const actualVideoDuration = await getVideoDuration(videoPath);
40055
+ if (actualVideoDuration < targetDuration - 0.1) {
40056
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40057
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40058
+ await unlink2(videoPath);
40059
+ await rename3(extendedPath, videoPath);
40060
+ }
40061
+ result.regeneratedScenes.push(sceneNum);
40062
+ } else {
40063
+ logSceneFailure("Grok", `scene ${sceneNum}`, waitResult);
40064
+ result.failedScenes.push(sceneNum);
40065
+ }
40066
+ } catch (err) {
40067
+ logSceneFailure("Grok", `scene ${sceneNum}`, err);
40068
+ result.failedScenes.push(sceneNum);
40069
+ }
40070
+ } else {
40071
+ result.failedScenes.push(sceneNum);
40072
+ }
40073
+ } else if (options.generator === "veo") {
40074
+ const veo = new GeminiProvider();
40075
+ await veo.initialize({ apiKey: videoApiKey });
40076
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40077
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40078
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40079
+ const veoDuration = segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4;
40080
+ const taskResult = await generateVideoWithRetryVeo(
40081
+ veo,
40082
+ segment,
40083
+ {
40084
+ duration: veoDuration,
40085
+ aspectRatio: options.aspectRatio || "16:9",
40086
+ referenceImage
40087
+ },
40088
+ maxRetries
40089
+ );
40090
+ if (taskResult) {
40091
+ try {
40092
+ const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, void 0, 3e5);
40093
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40094
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40095
+ await writeFile31(videoPath, buffer);
40096
+ const targetDuration = segment.duration;
40097
+ const actualVideoDuration = await getVideoDuration(videoPath);
40098
+ if (actualVideoDuration < targetDuration - 0.1) {
40099
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40100
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40101
+ await unlink2(videoPath);
40102
+ await rename3(extendedPath, videoPath);
40103
+ }
40104
+ result.regeneratedScenes.push(sceneNum);
40105
+ } else {
40106
+ logSceneFailure("Veo", `scene ${sceneNum}`, waitResult);
40107
+ result.failedScenes.push(sceneNum);
40108
+ }
40109
+ } catch (err) {
40110
+ logSceneFailure("Veo", `scene ${sceneNum}`, err);
40111
+ result.failedScenes.push(sceneNum);
40112
+ }
40113
+ } else {
40114
+ result.failedScenes.push(sceneNum);
40115
+ }
40116
+ } else if (options.generator === "kling" || !options.generator) {
40117
+ const kling = new KlingProvider();
40118
+ await kling.initialize({ apiKey: videoApiKey });
40119
+ if (!kling.isConfigured()) {
40120
+ result.failedScenes.push(sceneNum);
40121
+ continue;
40122
+ }
40123
+ const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
40124
+ let imageUrl;
40125
+ if (imgbbApiKey) {
40126
+ const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
40127
+ if (uploadResult.success && uploadResult.url) {
40128
+ imageUrl = uploadResult.url;
40129
+ }
40130
+ }
40131
+ const taskResult = await generateVideoWithRetryKling(
40132
+ kling,
40133
+ segment,
40134
+ {
40135
+ duration: videoDuration,
40136
+ aspectRatio: options.aspectRatio || "16:9",
40137
+ referenceImage: imageUrl
40138
+ },
40139
+ maxRetries
40140
+ );
40141
+ if (taskResult) {
40142
+ try {
40143
+ const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, void 0, 6e5);
40144
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40145
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40146
+ await writeFile31(videoPath, buffer);
40147
+ await extendVideoToTarget(
40148
+ videoPath,
40149
+ segment.duration,
40150
+ outputDir,
40151
+ `Scene ${sceneNum}`,
40152
+ {
40153
+ kling,
40154
+ videoId: waitResult.videoId,
40155
+ onProgress: options.onProgress
40156
+ }
40157
+ );
40158
+ result.regeneratedScenes.push(sceneNum);
40159
+ } else {
40160
+ logSceneFailure("Kling", `scene ${sceneNum}`, waitResult);
40161
+ result.failedScenes.push(sceneNum);
40162
+ }
40163
+ } catch (err) {
40164
+ logSceneFailure("Kling", `scene ${sceneNum}`, err);
40165
+ result.failedScenes.push(sceneNum);
40166
+ }
40167
+ } else {
40168
+ result.failedScenes.push(sceneNum);
40169
+ }
40170
+ } else {
40171
+ const runway = new RunwayProvider();
40172
+ await runway.initialize({ apiKey: videoApiKey });
40173
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40174
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40175
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40176
+ const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio || "16:9";
40177
+ const taskResult = await generateVideoWithRetryRunway(
40178
+ runway,
40179
+ segment,
40180
+ referenceImage,
40181
+ { duration: videoDuration, aspectRatio },
40182
+ maxRetries
40183
+ );
40184
+ if (taskResult) {
40185
+ try {
40186
+ const waitResult = await runway.waitForCompletion(taskResult.taskId, void 0, 3e5);
40187
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40188
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40189
+ await writeFile31(videoPath, buffer);
40190
+ const targetDuration = segment.duration;
40191
+ const actualVideoDuration = await getVideoDuration(videoPath);
40192
+ if (actualVideoDuration < targetDuration - 0.1) {
40193
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40194
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40195
+ await unlink2(videoPath);
40196
+ await rename3(extendedPath, videoPath);
40197
+ }
40198
+ result.regeneratedScenes.push(sceneNum);
40199
+ } else {
40200
+ logSceneFailure("Runway", `scene ${sceneNum}`, waitResult);
40201
+ result.failedScenes.push(sceneNum);
40202
+ }
40203
+ } catch (err) {
40204
+ logSceneFailure("Runway", `scene ${sceneNum}`, err);
40205
+ result.failedScenes.push(sceneNum);
40206
+ }
40207
+ } else {
40208
+ result.failedScenes.push(sceneNum);
40209
+ }
39951
40210
  }
40211
+ } else if (!sceneFailed) {
40212
+ result.regeneratedScenes.push(sceneNum);
39952
40213
  }
39953
- console.log();
39954
- } catch (error) {
39955
- exitWithError(generalError(error instanceof Error ? error.message : "Video review failed"));
39956
40214
  }
39957
- });
40215
+ if (storyboardMutated) {
40216
+ let currentTime = 0;
40217
+ for (const segment of segments) {
40218
+ segment.startTime = currentTime;
40219
+ currentTime += segment.duration;
40220
+ }
40221
+ const serialized = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.stringify)({ scenes: segments }, { indent: 2 }) : JSON.stringify(segments, null, 2);
40222
+ await writeFile31(storyboardPath, serialized, "utf-8");
40223
+ }
40224
+ result.success = result.failedScenes.length === 0;
40225
+ return result;
40226
+ } catch (error) {
40227
+ return {
40228
+ ...result,
40229
+ error: error instanceof Error ? error.message : String(error)
40230
+ };
40231
+ }
39958
40232
  }
39959
- var init_ai_review = __esm({
39960
- "src/commands/ai-review.ts"() {
40233
+ var import_yaml5;
40234
+ var init_ai_script_pipeline = __esm({
40235
+ "src/commands/ai-script-pipeline.ts"() {
39961
40236
  "use strict";
39962
- init_source();
39963
- init_ora();
40237
+ import_yaml5 = __toESM(require_dist(), 1);
39964
40238
  init_dist2();
39965
40239
  init_api_key();
39966
- init_exec_safe();
39967
- init_output();
39968
- init_validate();
40240
+ init_config();
40241
+ init_audio();
40242
+ init_ai_helpers();
40243
+ init_video_utils();
40244
+ init_video_providers();
39969
40245
  }
39970
40246
  });
39971
40247
 
39972
40248
  // src/commands/ai-highlights.ts
39973
40249
  import { readFile as readFile26, writeFile as writeFile33, mkdir as mkdir16 } from "node:fs/promises";
39974
- import { resolve as resolve36, dirname as dirname12, basename as basename9, extname as extname8 } from "node:path";
40250
+ import { resolve as resolve36, dirname as dirname12, basename as basename9, extname as extname9 } from "node:path";
39975
40251
  import { existsSync as existsSync28 } from "node:fs";
39976
40252
  function filterHighlights(highlights, options) {
39977
40253
  let filtered = highlights.filter((h) => h.confidence >= options.threshold);
@@ -40015,7 +40291,7 @@ async function executeHighlights(options) {
40015
40291
  if (!existsSync28(absPath)) {
40016
40292
  return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
40017
40293
  }
40018
- const ext = extname8(absPath).toLowerCase();
40294
+ const ext = extname9(absPath).toLowerCase();
40019
40295
  const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
40020
40296
  const isVideo = videoExtensions.includes(ext);
40021
40297
  const targetDuration = options.duration;
@@ -40352,7 +40628,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40352
40628
  };
40353
40629
  for (let i = 0; i < selectedHighlights.length; i++) {
40354
40630
  const h = selectedHighlights[i];
40355
- const baseName = basename9(absPath, extname8(absPath));
40631
+ const baseName = basename9(absPath, extname9(absPath));
40356
40632
  const outputPath = resolve36(outputDir, `${baseName}-short-${i + 1}.mp4`);
40357
40633
  const { stdout: probeOut } = await execSafe("ffprobe", [
40358
40634
  "-v",
@@ -40451,7 +40727,7 @@ function registerHighlightsCommands(aiCommand) {
40451
40727
  });
40452
40728
  return;
40453
40729
  }
40454
- const ext = extname8(absPath).toLowerCase();
40730
+ const ext = extname9(absPath).toLowerCase();
40455
40731
  const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
40456
40732
  const isVideo = videoExtensions.includes(ext);
40457
40733
  console.log();
@@ -40929,11 +41205,11 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40929
41205
  let outputPath;
40930
41206
  if (shortCount === 1 && options.output) {
40931
41207
  outputPath = resolve36(process.cwd(), options.output);
40932
- if (!extname8(outputPath)) {
41208
+ if (!extname9(outputPath)) {
40933
41209
  outputPath += ".mp4";
40934
41210
  }
40935
41211
  } else {
40936
- const baseName = basename9(absPath, extname8(absPath));
41212
+ const baseName = basename9(absPath, extname9(absPath));
40937
41213
  outputPath = resolve36(outputDir, `${baseName}-short-${i + 1}.mp4`);
40938
41214
  }
40939
41215
  const parentDir = dirname12(outputPath);
@@ -51819,7 +52095,7 @@ var init_scene_audio_scan = __esm({
51819
52095
 
51820
52096
  // src/commands/_shared/scene-audio-mux.ts
51821
52097
  import { rename as rename4, unlink as unlink3 } from "node:fs/promises";
51822
- import { resolve as resolve45, dirname as dirname15, extname as extname11, basename as basename16 } from "node:path";
52098
+ import { resolve as resolve45, dirname as dirname15, extname as extname12, basename as basename16 } from "node:path";
51823
52099
  function buildAudioMuxFilter(audios) {
51824
52100
  if (audios.length === 0) return null;
51825
52101
  const labels = [];
@@ -51878,7 +52154,7 @@ async function muxAudioIntoVideo(opts) {
51878
52154
  if (!filter) {
51879
52155
  return { success: true, outputPath: opts.videoPath, audioCount: 0 };
51880
52156
  }
51881
- const ext = extname11(opts.videoPath) || `.${opts.format}`;
52157
+ const ext = extname12(opts.videoPath) || `.${opts.format}`;
51882
52158
  const tmpPath = resolve45(
51883
52159
  dirname15(opts.videoPath),
51884
52160
  `.${basename16(opts.videoPath, ext)}.muxing${ext}`
@@ -52536,12 +52812,22 @@ async function executeVideoGenerate(options) {
52536
52812
  const key = apiKey || process.env[envKeyMap[provider] || ""];
52537
52813
  if (!key) return { success: false, error: `${envKeyMap[provider]} required for ${provider}` };
52538
52814
  let referenceImage;
52815
+ let referenceImageBuffer;
52816
+ let referenceImageMimeType;
52539
52817
  if (image) {
52540
52818
  const imagePath = resolve53(process.cwd(), image);
52541
52819
  const imageBuffer = await readFile35(imagePath);
52542
52820
  const ext = image.toLowerCase().split(".").pop();
52543
- const mimeTypes2 = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
52821
+ const mimeTypes2 = {
52822
+ jpg: "image/jpeg",
52823
+ jpeg: "image/jpeg",
52824
+ png: "image/png",
52825
+ gif: "image/gif",
52826
+ webp: "image/webp"
52827
+ };
52544
52828
  const mimeType = mimeTypes2[ext || "png"] || "image/png";
52829
+ referenceImageBuffer = imageBuffer;
52830
+ referenceImageMimeType = mimeType;
52545
52831
  referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
52546
52832
  }
52547
52833
  if (provider === "seedance" || provider === "fal") {
@@ -52549,12 +52835,12 @@ async function executeVideoGenerate(options) {
52549
52835
  await fal.initialize({ apiKey: key });
52550
52836
  let falImage = referenceImage;
52551
52837
  if (falImage && falImage.startsWith("data:")) {
52552
- const imgbbKey = process.env.IMGBB_API_KEY;
52553
- if (!imgbbKey) return { success: false, error: "IMGBB_API_KEY required for Seedance image-to-video" };
52554
- const base64Data = falImage.split(",")[1];
52555
- const uploadResult = await uploadToImgbb(Buffer.from(base64Data, "base64"), imgbbKey);
52556
- if (!uploadResult.success || !uploadResult.url) return { success: false, error: `ImgBB upload failed: ${uploadResult.error}` };
52557
- falImage = uploadResult.url;
52838
+ const uploadHost = await resolveUploadHost();
52839
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
52840
+ filename: image,
52841
+ mimeType: referenceImageMimeType
52842
+ });
52843
+ falImage = upload.url;
52558
52844
  }
52559
52845
  const model = seedanceModel === "fast" || seedanceModel === "seedance-2.0-fast" ? "seedance-2.0-fast" : "seedance-2.0";
52560
52846
  const result = await fal.generateVideo(prompt3, {
@@ -52565,14 +52851,22 @@ async function executeVideoGenerate(options) {
52565
52851
  negativePrompt: negative,
52566
52852
  model
52567
52853
  });
52568
- if (result.status === "failed") return { success: false, error: result.error || "Seedance generation failed" };
52854
+ if (result.status === "failed")
52855
+ return { success: false, error: result.error || "Seedance generation failed" };
52569
52856
  let outputPath;
52570
52857
  if (output && result.videoUrl) {
52571
52858
  const buffer = await downloadVideo(result.videoUrl, key);
52572
52859
  outputPath = resolve53(process.cwd(), output);
52573
52860
  await writeFile39(outputPath, buffer);
52574
52861
  }
52575
- return { success: true, taskId: result.id, status: "completed", videoUrl: result.videoUrl, outputPath, provider: "seedance" };
52862
+ return {
52863
+ success: true,
52864
+ taskId: result.id,
52865
+ status: "completed",
52866
+ videoUrl: result.videoUrl,
52867
+ outputPath,
52868
+ provider: "seedance"
52869
+ };
52576
52870
  } else if (provider === "runway") {
52577
52871
  const runway = new RunwayProvider();
52578
52872
  await runway.initialize({ apiKey: key });
@@ -52583,30 +52877,41 @@ async function executeVideoGenerate(options) {
52583
52877
  aspectRatio: ratio,
52584
52878
  seed
52585
52879
  });
52586
- if (result.status === "failed") return { success: false, error: result.error || "Runway generation failed" };
52587
- if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "runway" };
52880
+ if (result.status === "failed")
52881
+ return { success: false, error: result.error || "Runway generation failed" };
52882
+ if (!wait)
52883
+ return { success: true, taskId: result.id, status: "processing", provider: "runway" };
52588
52884
  const finalResult = await runway.waitForCompletion(result.id, () => {
52589
52885
  }, 3e5);
52590
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Runway generation failed" };
52886
+ if (finalResult.status !== "completed")
52887
+ return { success: false, error: finalResult.error || "Runway generation failed" };
52591
52888
  let outputPath;
52592
52889
  if (output && finalResult.videoUrl) {
52593
52890
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52594
52891
  outputPath = resolve53(process.cwd(), output);
52595
52892
  await writeFile39(outputPath, buffer);
52596
52893
  }
52597
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "runway" };
52894
+ return {
52895
+ success: true,
52896
+ taskId: result.id,
52897
+ status: "completed",
52898
+ videoUrl: finalResult.videoUrl,
52899
+ duration: finalResult.duration,
52900
+ outputPath,
52901
+ provider: "runway"
52902
+ };
52598
52903
  } else if (provider === "kling") {
52599
52904
  const kling = new KlingProvider();
52600
52905
  await kling.initialize({ apiKey: key });
52601
52906
  if (!kling.isConfigured()) return { success: false, error: "Invalid Kling API key format" };
52602
52907
  let klingImage = referenceImage;
52603
52908
  if (klingImage && klingImage.startsWith("data:")) {
52604
- const imgbbKey = process.env.IMGBB_API_KEY;
52605
- if (!imgbbKey) return { success: false, error: "IMGBB_API_KEY required for Kling image-to-video" };
52606
- const base64Data = klingImage.split(",")[1];
52607
- const uploadResult = await uploadToImgbb(Buffer.from(base64Data, "base64"), imgbbKey);
52608
- if (!uploadResult.success || !uploadResult.url) return { success: false, error: `ImgBB upload failed: ${uploadResult.error}` };
52609
- klingImage = uploadResult.url;
52909
+ const uploadHost = await resolveUploadHost();
52910
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
52911
+ filename: image,
52912
+ mimeType: referenceImageMimeType
52913
+ });
52914
+ klingImage = upload.url;
52610
52915
  }
52611
52916
  const result = await kling.generateVideo(prompt3, {
52612
52917
  prompt: prompt3,
@@ -52616,23 +52921,38 @@ async function executeVideoGenerate(options) {
52616
52921
  negativePrompt: negative,
52617
52922
  mode
52618
52923
  });
52619
- if (result.status === "failed") return { success: false, error: result.error || "Kling generation failed" };
52924
+ if (result.status === "failed")
52925
+ return { success: false, error: result.error || "Kling generation failed" };
52620
52926
  const taskType = referenceImage ? "image2video" : "text2video";
52621
- if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "kling" };
52927
+ if (!wait)
52928
+ return { success: true, taskId: result.id, status: "processing", provider: "kling" };
52622
52929
  const finalResult = await kling.waitForCompletion(result.id, taskType, () => {
52623
52930
  }, 6e5);
52624
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Kling generation failed" };
52931
+ if (finalResult.status !== "completed")
52932
+ return { success: false, error: finalResult.error || "Kling generation failed" };
52625
52933
  let outputPath;
52626
52934
  if (output && finalResult.videoUrl) {
52627
52935
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52628
52936
  outputPath = resolve53(process.cwd(), output);
52629
52937
  await writeFile39(outputPath, buffer);
52630
52938
  }
52631
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "kling" };
52939
+ return {
52940
+ success: true,
52941
+ taskId: result.id,
52942
+ status: "completed",
52943
+ videoUrl: finalResult.videoUrl,
52944
+ duration: finalResult.duration,
52945
+ outputPath,
52946
+ provider: "kling"
52947
+ };
52632
52948
  } else if (provider === "veo") {
52633
52949
  const gemini = new GeminiProvider();
52634
52950
  await gemini.initialize({ apiKey: key });
52635
- const veoModelMap = { "3.0": "veo-3.0-generate-preview", "3.1": "veo-3.1-generate-preview", "3.1-fast": "veo-3.1-fast-generate-preview" };
52951
+ const veoModelMap = {
52952
+ "3.0": "veo-3.0-generate-preview",
52953
+ "3.1": "veo-3.1-generate-preview",
52954
+ "3.1-fast": "veo-3.1-fast-generate-preview"
52955
+ };
52636
52956
  const model = veoModelMap[veoModel] || "veo-3.1-fast-generate-preview";
52637
52957
  const veoDuration = duration <= 6 ? 6 : 8;
52638
52958
  const result = await gemini.generateVideo(prompt3, {
@@ -52644,18 +52964,27 @@ async function executeVideoGenerate(options) {
52644
52964
  negativePrompt: negative,
52645
52965
  resolution
52646
52966
  });
52647
- if (result.status === "failed") return { success: false, error: result.error || "Veo generation failed" };
52967
+ if (result.status === "failed")
52968
+ return { success: false, error: result.error || "Veo generation failed" };
52648
52969
  if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "veo" };
52649
52970
  const finalResult = await gemini.waitForVideoCompletion(result.id, () => {
52650
52971
  }, 3e5);
52651
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Veo generation failed" };
52972
+ if (finalResult.status !== "completed")
52973
+ return { success: false, error: finalResult.error || "Veo generation failed" };
52652
52974
  let outputPath;
52653
52975
  if (output && finalResult.videoUrl) {
52654
52976
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52655
52977
  outputPath = resolve53(process.cwd(), output);
52656
52978
  await writeFile39(outputPath, buffer);
52657
52979
  }
52658
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, outputPath, provider: "veo" };
52980
+ return {
52981
+ success: true,
52982
+ taskId: result.id,
52983
+ status: "completed",
52984
+ videoUrl: finalResult.videoUrl,
52985
+ outputPath,
52986
+ provider: "veo"
52987
+ };
52659
52988
  } else if (provider === "grok") {
52660
52989
  const grok = new GrokProvider();
52661
52990
  await grok.initialize({ apiKey: key });
@@ -52665,28 +52994,52 @@ async function executeVideoGenerate(options) {
52665
52994
  duration,
52666
52995
  aspectRatio: ratio
52667
52996
  });
52668
- if (result.status === "failed") return { success: false, error: result.error || "Grok generation failed" };
52669
- if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "grok" };
52997
+ if (result.status === "failed")
52998
+ return { success: false, error: result.error || "Grok generation failed" };
52999
+ if (!wait)
53000
+ return { success: true, taskId: result.id, status: "processing", provider: "grok" };
52670
53001
  const finalResult = await grok.waitForCompletion(result.id, () => {
52671
53002
  }, 3e5);
52672
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Grok generation failed" };
53003
+ if (finalResult.status !== "completed")
53004
+ return { success: false, error: finalResult.error || "Grok generation failed" };
52673
53005
  let outputPath;
52674
53006
  if (output && finalResult.videoUrl) {
52675
53007
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52676
53008
  outputPath = resolve53(process.cwd(), output);
52677
53009
  await writeFile39(outputPath, buffer);
52678
53010
  }
52679
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "grok" };
53011
+ return {
53012
+ success: true,
53013
+ taskId: result.id,
53014
+ status: "completed",
53015
+ videoUrl: finalResult.videoUrl,
53016
+ duration: finalResult.duration,
53017
+ outputPath,
53018
+ provider: "grok"
53019
+ };
52680
53020
  }
52681
53021
  return { success: false, error: `Unsupported provider: ${provider}` };
52682
53022
  } catch (error) {
52683
- return { success: false, error: `Video generation failed: ${error instanceof Error ? error.message : String(error)}` };
53023
+ return {
53024
+ success: false,
53025
+ error: `Video generation failed: ${error instanceof Error ? error.message : String(error)}`
53026
+ };
52684
53027
  }
52685
53028
  }
52686
53029
  async function executeVideoStatus(options) {
52687
- const { taskId, provider = "runway", taskType = "text2video", wait = false, output, apiKey } = options;
53030
+ const {
53031
+ taskId,
53032
+ provider = "runway",
53033
+ taskType = "text2video",
53034
+ wait = false,
53035
+ output,
53036
+ apiKey
53037
+ } = options;
52688
53038
  try {
52689
- const envKeyMap = { runway: "RUNWAY_API_SECRET", kling: "KLING_API_KEY" };
53039
+ const envKeyMap = {
53040
+ runway: "RUNWAY_API_SECRET",
53041
+ kling: "KLING_API_KEY"
53042
+ };
52690
53043
  const key = apiKey || process.env[envKeyMap[provider] || ""];
52691
53044
  if (!key) return { success: false, error: `${envKeyMap[provider]} required` };
52692
53045
  if (provider === "runway") {
@@ -52703,7 +53056,14 @@ async function executeVideoStatus(options) {
52703
53056
  outputPath = resolve53(process.cwd(), output);
52704
53057
  await writeFile39(outputPath, buffer);
52705
53058
  }
52706
- return { success: true, taskId, status: result.status, progress: result.progress, videoUrl: result.videoUrl, outputPath };
53059
+ return {
53060
+ success: true,
53061
+ taskId,
53062
+ status: result.status,
53063
+ progress: result.progress,
53064
+ videoUrl: result.videoUrl,
53065
+ outputPath
53066
+ };
52707
53067
  } else if (provider === "kling") {
52708
53068
  const kling = new KlingProvider();
52709
53069
  await kling.initialize({ apiKey: key });
@@ -52718,11 +53078,21 @@ async function executeVideoStatus(options) {
52718
53078
  outputPath = resolve53(process.cwd(), output);
52719
53079
  await writeFile39(outputPath, buffer);
52720
53080
  }
52721
- return { success: true, taskId, status: result.status, videoUrl: result.videoUrl, duration: result.duration, outputPath };
53081
+ return {
53082
+ success: true,
53083
+ taskId,
53084
+ status: result.status,
53085
+ videoUrl: result.videoUrl,
53086
+ duration: result.duration,
53087
+ outputPath
53088
+ };
52722
53089
  }
52723
53090
  return { success: false, error: `Unsupported provider: ${provider}` };
52724
53091
  } catch (error) {
52725
- return { success: false, error: `Status check failed: ${error instanceof Error ? error.message : String(error)}` };
53092
+ return {
53093
+ success: false,
53094
+ error: `Status check failed: ${error instanceof Error ? error.message : String(error)}`
53095
+ };
52726
53096
  }
52727
53097
  }
52728
53098
  async function executeVideoCancel(options) {
@@ -52735,11 +53105,24 @@ async function executeVideoCancel(options) {
52735
53105
  const success = await runway.cancelGeneration(taskId);
52736
53106
  return { success };
52737
53107
  } catch (error) {
52738
- return { success: false, error: `Cancel failed: ${error instanceof Error ? error.message : String(error)}` };
53108
+ return {
53109
+ success: false,
53110
+ error: `Cancel failed: ${error instanceof Error ? error.message : String(error)}`
53111
+ };
52739
53112
  }
52740
53113
  }
52741
53114
  async function executeVideoExtend(options) {
52742
- const { videoId, provider = "kling", prompt: prompt3, duration = 5, negative, veoModel = "3.1", output, wait = true, apiKey } = options;
53115
+ const {
53116
+ videoId,
53117
+ provider = "kling",
53118
+ prompt: prompt3,
53119
+ duration = 5,
53120
+ negative,
53121
+ veoModel = "3.1",
53122
+ output,
53123
+ wait = true,
53124
+ apiKey
53125
+ } = options;
52743
53126
  try {
52744
53127
  if (provider === "kling") {
52745
53128
  const key = apiKey || process.env.KLING_API_KEY;
@@ -52752,52 +53135,76 @@ async function executeVideoExtend(options) {
52752
53135
  negativePrompt: negative,
52753
53136
  duration: String(duration)
52754
53137
  });
52755
- if (result.status === "failed") return { success: false, error: result.error || "Kling extension failed" };
53138
+ if (result.status === "failed")
53139
+ return { success: false, error: result.error || "Kling extension failed" };
52756
53140
  if (!wait) return { success: true, taskId: result.id, status: "processing" };
52757
53141
  const finalResult = await kling.waitForExtendCompletion(result.id, () => {
52758
53142
  }, 6e5);
52759
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Kling extension failed" };
53143
+ if (finalResult.status !== "completed")
53144
+ return { success: false, error: finalResult.error || "Kling extension failed" };
52760
53145
  let outputPath;
52761
53146
  if (output && finalResult.videoUrl) {
52762
53147
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52763
53148
  outputPath = resolve53(process.cwd(), output);
52764
53149
  await writeFile39(outputPath, buffer);
52765
53150
  }
52766
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath };
53151
+ return {
53152
+ success: true,
53153
+ taskId: result.id,
53154
+ status: "completed",
53155
+ videoUrl: finalResult.videoUrl,
53156
+ duration: finalResult.duration,
53157
+ outputPath
53158
+ };
52767
53159
  } else if (provider === "veo") {
52768
53160
  const key = apiKey || process.env.GOOGLE_API_KEY;
52769
53161
  if (!key) return { success: false, error: "GOOGLE_API_KEY required" };
52770
53162
  const gemini = new GeminiProvider();
52771
53163
  await gemini.initialize({ apiKey: key });
52772
- const veoModelMap = { "3.0": "veo-3.0-generate-preview", "3.1": "veo-3.1-generate-preview", "3.1-fast": "veo-3.1-fast-generate-preview" };
53164
+ const veoModelMap = {
53165
+ "3.0": "veo-3.0-generate-preview",
53166
+ "3.1": "veo-3.1-generate-preview",
53167
+ "3.1-fast": "veo-3.1-fast-generate-preview"
53168
+ };
52773
53169
  const model = veoModelMap[veoModel] || "veo-3.1-generate-preview";
52774
53170
  const result = await gemini.extendVideo(videoId, prompt3, {
52775
53171
  duration,
52776
53172
  model
52777
53173
  });
52778
- if (result.status === "failed") return { success: false, error: result.error || "Veo extension failed" };
53174
+ if (result.status === "failed")
53175
+ return { success: false, error: result.error || "Veo extension failed" };
52779
53176
  if (!wait) return { success: true, taskId: result.id, status: "processing" };
52780
53177
  const finalResult = await gemini.waitForVideoCompletion(result.id, () => {
52781
53178
  }, 3e5);
52782
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Veo extension failed" };
53179
+ if (finalResult.status !== "completed")
53180
+ return { success: false, error: finalResult.error || "Veo extension failed" };
52783
53181
  let outputPath;
52784
53182
  if (output && finalResult.videoUrl) {
52785
53183
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52786
53184
  outputPath = resolve53(process.cwd(), output);
52787
53185
  await writeFile39(outputPath, buffer);
52788
53186
  }
52789
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, outputPath };
53187
+ return {
53188
+ success: true,
53189
+ taskId: result.id,
53190
+ status: "completed",
53191
+ videoUrl: finalResult.videoUrl,
53192
+ outputPath
53193
+ };
52790
53194
  }
52791
53195
  return { success: false, error: `Unsupported provider: ${provider}` };
52792
53196
  } catch (error) {
52793
- return { success: false, error: `Video extension failed: ${error instanceof Error ? error.message : String(error)}` };
53197
+ return {
53198
+ success: false,
53199
+ error: `Video extension failed: ${error instanceof Error ? error.message : String(error)}`
53200
+ };
52794
53201
  }
52795
53202
  }
52796
53203
  var init_ai_video = __esm({
52797
53204
  "src/commands/ai-video.ts"() {
52798
53205
  "use strict";
52799
53206
  init_dist2();
52800
- init_ai_script_pipeline();
53207
+ init_upload_host();
52801
53208
  init_ai_helpers();
52802
53209
  }
52803
53210
  });
@@ -52811,7 +53218,7 @@ __export(ai_audio_exports, {
52811
53218
  executeTranscribe: () => executeTranscribe,
52812
53219
  executeVoiceClone: () => executeVoiceClone
52813
53220
  });
52814
- import { resolve as resolve54, dirname as dirname19, basename as basename18, extname as extname12 } from "node:path";
53221
+ import { resolve as resolve54, dirname as dirname19, basename as basename18, extname as extname13 } from "node:path";
52815
53222
  import { readFile as readFile36, writeFile as writeFile40 } from "node:fs/promises";
52816
53223
  import { existsSync as existsSync38 } from "node:fs";
52817
53224
  async function executeTranscribe(options) {
@@ -52908,7 +53315,7 @@ async function executeDub(options) {
52908
53315
  if (!openaiKey) return { success: false, error: "OPENAI_API_KEY required for Whisper transcription" };
52909
53316
  if (!anthropicKey) return { success: false, error: "ANTHROPIC_API_KEY required for Claude translation" };
52910
53317
  if (!analyzeOnly && !elevenlabsKey) return { success: false, error: "ELEVENLABS_API_KEY required for TTS" };
52911
- const ext = extname12(absPath).toLowerCase();
53318
+ const ext = extname13(absPath).toLowerCase();
52912
53319
  const isVideo = [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
52913
53320
  let audioPath = absPath;
52914
53321
  if (isVideo) {
@@ -52993,8 +53400,8 @@ ${segmentTexts}`,
52993
53400
  }
52994
53401
  }
52995
53402
  const combinedBuffer = Buffer.concat(dubbedBuffers);
52996
- const outputExt = isVideo ? ".mp3" : extname12(absPath);
52997
- const defaultOutputPath2 = resolve54(dirname19(absPath), `${basename18(absPath, extname12(absPath))}-${language}${outputExt}`);
53403
+ const outputExt = isVideo ? ".mp3" : extname13(absPath);
53404
+ const defaultOutputPath2 = resolve54(dirname19(absPath), `${basename18(absPath, extname13(absPath))}-${language}${outputExt}`);
52998
53405
  const finalOutputPath = resolve54(process.cwd(), output || defaultOutputPath2);
52999
53406
  await writeFile40(finalOutputPath, combinedBuffer);
53000
53407
  if (isVideo && audioPath !== absPath) {
@@ -53031,7 +53438,7 @@ async function executeDuck(options) {
53031
53438
  const absVoicePath = resolve54(process.cwd(), voicePath);
53032
53439
  if (!existsSync38(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
53033
53440
  if (!existsSync38(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
53034
- const defaultOutput = resolve54(dirname19(absMusicPath), `${basename18(absMusicPath, extname12(absMusicPath))}-ducked${extname12(absMusicPath)}`);
53441
+ const defaultOutput = resolve54(dirname19(absMusicPath), `${basename18(absMusicPath, extname13(absMusicPath))}-ducked${extname13(absMusicPath)}`);
53035
53442
  const outputPath = resolve54(process.cwd(), output || defaultOutput);
53036
53443
  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`;
53037
53444
  await execSafe("ffmpeg", [
@@ -59608,11 +60015,17 @@ var init_generate2 = __esm({
59608
60015
  }),
59609
60016
  async execute(args) {
59610
60017
  const result = await executeMotion(args);
59611
- if (!result.success) return { success: false, error: result.error ?? "Motion generation failed" };
60018
+ if (!result.success)
60019
+ return { success: false, error: result.error ?? "Motion generation failed" };
59612
60020
  const out = result.compositedPath ?? result.renderedPath ?? result.codePath;
59613
60021
  return {
59614
60022
  success: true,
59615
- data: { codePath: result.codePath, renderedPath: result.renderedPath, compositedPath: result.compositedPath, componentName: result.componentName },
60023
+ data: {
60024
+ codePath: result.codePath,
60025
+ renderedPath: result.renderedPath,
60026
+ compositedPath: result.compositedPath,
60027
+ componentName: result.componentName
60028
+ },
59616
60029
  humanLines: [`\u2705 Motion generated \u2192 ${out}`]
59617
60030
  };
59618
60031
  }
@@ -59676,7 +60089,9 @@ var init_generate2 = __esm({
59676
60089
  return {
59677
60090
  success: true,
59678
60091
  data: { outputPath: result.outputPath, provider: result.provider, duration: result.duration },
59679
- humanLines: [`\u2705 Music${result.provider ? ` (${result.provider})` : ""} \u2192 ${result.outputPath ?? "(async)"}`]
60092
+ humanLines: [
60093
+ `\u2705 Music${result.provider ? ` (${result.provider})` : ""} \u2192 ${result.outputPath ?? "(async)"}`
60094
+ ]
59680
60095
  };
59681
60096
  }
59682
60097
  });
@@ -59693,7 +60108,12 @@ var init_generate2 = __esm({
59693
60108
  if (!result.success) return { success: false, error: result.error ?? "Music status failed" };
59694
60109
  return {
59695
60110
  success: true,
59696
- data: { taskId: result.taskId, status: result.status, audioUrl: result.audioUrl, error: result.error },
60111
+ data: {
60112
+ taskId: result.taskId,
60113
+ status: result.status,
60114
+ audioUrl: result.audioUrl,
60115
+ error: result.error
60116
+ },
59697
60117
  humanLines: [`Music task ${result.taskId}: ${result.status}`]
59698
60118
  };
59699
60119
  }
@@ -59705,7 +60125,9 @@ var init_generate2 = __esm({
59705
60125
  description: "Generate an image using AI. Supports Gemini (free), OpenAI GPT Image, or Grok Imagine. Requires GOOGLE_API_KEY (Gemini), OPENAI_API_KEY (OpenAI), or XAI_API_KEY (Grok).",
59706
60126
  schema: external_exports.object({
59707
60127
  prompt: external_exports.string().describe("Image description prompt"),
59708
- provider: external_exports.enum(["gemini", "openai", "grok"]).optional().describe("Image provider (default: gemini)"),
60128
+ provider: external_exports.enum(["gemini", "openai", "grok"]).optional().describe(
60129
+ "Image provider (default: openai when OPENAI_API_KEY is configured, otherwise first configured provider)"
60130
+ ),
59709
60131
  output: external_exports.string().optional().describe("Output file path"),
59710
60132
  size: external_exports.string().optional().describe("Image size for OpenAI (1024x1024, 1536x1024, 1024x1536)"),
59711
60133
  ratio: external_exports.string().optional().describe("Aspect ratio for Gemini (1:1, 16:9, 9:16, 4:3, 3:4, etc.)"),
@@ -59715,10 +60137,16 @@ var init_generate2 = __esm({
59715
60137
  }),
59716
60138
  async execute(args) {
59717
60139
  const result = await executeImageGenerate(args);
59718
- if (!result.success) return { success: false, error: result.error ?? "Image generation failed" };
60140
+ if (!result.success)
60141
+ return { success: false, error: result.error ?? "Image generation failed" };
59719
60142
  return {
59720
60143
  success: true,
59721
- data: { outputPath: result.outputPath, provider: result.provider, model: result.model, imageCount: result.images?.length },
60144
+ data: {
60145
+ outputPath: result.outputPath,
60146
+ provider: result.provider,
60147
+ model: result.model,
60148
+ imageCount: result.images?.length
60149
+ },
59722
60150
  humanLines: [`\u2705 Image (${result.provider}) \u2192 ${result.outputPath}`]
59723
60151
  };
59724
60152
  }
@@ -59740,7 +60168,9 @@ var init_generate2 = __esm({
59740
60168
  return {
59741
60169
  success: true,
59742
60170
  data: { segmentCount: result.segmentCount, outputPath: result.outputPath },
59743
- humanLines: [`\u2705 Storyboard: ${result.segmentCount} segments${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`]
60171
+ humanLines: [
60172
+ `\u2705 Storyboard: ${result.segmentCount} segments${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60173
+ ]
59744
60174
  };
59745
60175
  }
59746
60176
  });
@@ -59759,7 +60189,11 @@ var init_generate2 = __esm({
59759
60189
  if (!result.success) return { success: false, error: result.error ?? "Background failed" };
59760
60190
  return {
59761
60191
  success: true,
59762
- data: { imageUrl: result.imageUrl, outputPath: result.outputPath, revisedPrompt: result.revisedPrompt },
60192
+ data: {
60193
+ imageUrl: result.imageUrl,
60194
+ outputPath: result.outputPath,
60195
+ revisedPrompt: result.revisedPrompt
60196
+ },
59763
60197
  humanLines: [`\u2705 Background \u2192 ${result.outputPath ?? result.imageUrl}`]
59764
60198
  };
59765
60199
  }
@@ -59817,7 +60251,9 @@ var init_generate2 = __esm({
59817
60251
  outputPath: result.outputPath,
59818
60252
  provider: result.provider
59819
60253
  },
59820
- humanLines: [`\u2705 Video (${result.provider}, ${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`]
60254
+ humanLines: [
60255
+ `\u2705 Video (${result.provider}, ${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60256
+ ]
59821
60257
  };
59822
60258
  }
59823
60259
  });
@@ -59838,8 +60274,16 @@ var init_generate2 = __esm({
59838
60274
  if (!result.success) return { success: false, error: result.error ?? "Status check failed" };
59839
60275
  return {
59840
60276
  success: true,
59841
- data: { taskId: result.taskId, status: result.status, progress: result.progress, videoUrl: result.videoUrl, outputPath: result.outputPath },
59842
- humanLines: [`Task ${result.taskId}: ${result.status}${result.progress !== void 0 ? ` (${result.progress}%)` : ""}`]
60277
+ data: {
60278
+ taskId: result.taskId,
60279
+ status: result.status,
60280
+ progress: result.progress,
60281
+ videoUrl: result.videoUrl,
60282
+ outputPath: result.outputPath
60283
+ },
60284
+ humanLines: [
60285
+ `Task ${result.taskId}: ${result.status}${result.progress !== void 0 ? ` (${result.progress}%)` : ""}`
60286
+ ]
59843
60287
  };
59844
60288
  }
59845
60289
  });
@@ -59881,8 +60325,16 @@ var init_generate2 = __esm({
59881
60325
  if (!result.success) return { success: false, error: result.error ?? "Extend failed" };
59882
60326
  return {
59883
60327
  success: true,
59884
- data: { taskId: result.taskId, status: result.status, videoUrl: result.videoUrl, duration: result.duration, outputPath: result.outputPath },
59885
- humanLines: [`\u2705 Video extended (${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`]
60328
+ data: {
60329
+ taskId: result.taskId,
60330
+ status: result.status,
60331
+ videoUrl: result.videoUrl,
60332
+ duration: result.duration,
60333
+ outputPath: result.outputPath
60334
+ },
60335
+ humanLines: [
60336
+ `\u2705 Video extended (${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60337
+ ]
59886
60338
  };
59887
60339
  }
59888
60340
  });
@@ -61813,7 +62265,7 @@ var init_export2 = __esm({
61813
62265
 
61814
62266
  // src/tools/manifest/agent-only.ts
61815
62267
  import { readFile as readFile41, writeFile as writeFile48, readdir as readdir4, stat as stat6, access as access9, unlink as unlink4 } from "node:fs/promises";
61816
- import { resolve as resolve63, join as join16, basename as basename22, extname as extname13 } from "node:path";
62268
+ import { resolve as resolve63, join as join16, basename as basename22, extname as extname14 } from "node:path";
61817
62269
  function matchPattern(filename, pattern) {
61818
62270
  const regex2 = new RegExp(
61819
62271
  "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
@@ -61832,7 +62284,7 @@ function formatSize(bytes) {
61832
62284
  return `${size.toFixed(1)}${units2[unit]}`;
61833
62285
  }
61834
62286
  function detectMediaType4(filePath) {
61835
- const ext = extname13(filePath).toLowerCase();
62287
+ const ext = extname14(filePath).toLowerCase();
61836
62288
  if ([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"].includes(ext)) return "video";
61837
62289
  if ([".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"].includes(ext)) return "audio";
61838
62290
  if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"].includes(ext)) return "image";
@@ -61859,7 +62311,7 @@ function isMediaFile2(filePath) {
61859
62311
  ".webp",
61860
62312
  ".bmp"
61861
62313
  ];
61862
- return mediaExts.includes(extname13(filePath).toLowerCase());
62314
+ return mediaExts.includes(extname14(filePath).toLowerCase());
61863
62315
  }
61864
62316
  async function getMediaDuration2(filePath) {
61865
62317
  try {
@@ -62005,7 +62457,7 @@ var init_agent_only = __esm({
62005
62457
  if (entry.isDirectory() && recursive) {
62006
62458
  await scanDir(entryPath);
62007
62459
  } else if (entry.isFile()) {
62008
- const ext = extname13(entry.name).toLowerCase();
62460
+ const ext = extname14(entry.name).toLowerCase();
62009
62461
  const matchesFilter = !filterExts || filterExts.includes(ext);
62010
62462
  if (matchesFilter && isMediaFile2(entryPath)) mediaFiles.push(entryPath);
62011
62463
  }
@@ -62973,7 +63425,7 @@ init_source();
62973
63425
  // package.json
62974
63426
  var package_default = {
62975
63427
  name: "@vibeframe/cli",
62976
- version: "0.91.9",
63428
+ version: "0.92.0",
62977
63429
  description: "VibeFrame CLI - natural language video editing from the terminal",
62978
63430
  private: false,
62979
63431
  license: "MIT",
@@ -64159,8 +64611,8 @@ init_output();
64159
64611
  init_sanitize();
64160
64612
  init_validate();
64161
64613
  init_cost_tier();
64162
- import { readFile as readFile23, writeFile as writeFile30 } from "node:fs/promises";
64163
- import { resolve as resolve33 } from "node:path";
64614
+ import { readFile as readFile22, writeFile as writeFile29 } from "node:fs/promises";
64615
+ import { resolve as resolve32 } from "node:path";
64164
64616
  var inspectCommand = new Command("inspect").description("Inspect media using AI (images, videos, YouTube URLs)").addHelpText(
64165
64617
  "after",
64166
64618
  `
@@ -64357,8 +64809,8 @@ inspectCommand.command("suggest").description("Get AI edit suggestions using Gem
64357
64809
  }
64358
64810
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
64359
64811
  const spinner2 = ora("Initializing Gemini...").start();
64360
- const filePath = resolve33(process.cwd(), projectPath);
64361
- const content = await readFile23(filePath, "utf-8");
64812
+ const filePath = resolve32(process.cwd(), projectPath);
64813
+ const content = await readFile22(filePath, "utf-8");
64362
64814
  const data = JSON.parse(content);
64363
64815
  const project = Project.fromJSON(data);
64364
64816
  const gemini = new GeminiProvider();
@@ -64395,7 +64847,7 @@ inspectCommand.command("suggest").description("Get AI edit suggestions using Gem
64395
64847
  const sug = suggestions[0];
64396
64848
  const applied = applySuggestion(project, sug);
64397
64849
  if (applied) {
64398
- await writeFile30(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
64850
+ await writeFile29(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
64399
64851
  spinner2.succeed(source_default.green("Suggestion applied"));
64400
64852
  } else {
64401
64853
  spinner2.warn(source_default.yellow("Could not apply suggestion automatically"));
@@ -64420,9 +64872,9 @@ init_cost_tier();
64420
64872
  init_ai_helpers();
64421
64873
  init_output();
64422
64874
  init_validate();
64423
- import { resolve as resolve34, dirname as dirname11, basename as basename8, extname as extname7 } from "node:path";
64424
- import { readFile as readFile24, writeFile as writeFile31 } from "node:fs/promises";
64425
- import { existsSync as existsSync26 } from "node:fs";
64875
+ import { resolve as resolve33, dirname as dirname11, basename as basename8, extname as extname7 } from "node:path";
64876
+ import { readFile as readFile23, writeFile as writeFile30 } from "node:fs/promises";
64877
+ import { existsSync as existsSync25 } from "node:fs";
64426
64878
  var audioCommand = new Command("audio").alias("au").description("Audio operations (transcribe, TTS, voice clone, ducking)").addHelpText(
64427
64879
  "after",
64428
64880
  `
@@ -64455,8 +64907,8 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64455
64907
  const whisper = new WhisperProvider();
64456
64908
  await whisper.initialize({ apiKey });
64457
64909
  spinner2.text = "Reading audio file...";
64458
- const absPath = resolve34(process.cwd(), audioPath);
64459
- const audioBuffer = await readFile24(absPath);
64910
+ const absPath = resolve33(process.cwd(), audioPath);
64911
+ const audioBuffer = await readFile23(absPath);
64460
64912
  const audioBlob = new Blob([audioBuffer]);
64461
64913
  spinner2.text = "Transcribing...";
64462
64914
  const result = await whisper.transcribe(audioBlob, options.language);
@@ -64473,7 +64925,7 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64473
64925
  fullText: result.fullText,
64474
64926
  segments: result.segments,
64475
64927
  language: result.detectedLanguage,
64476
- outputPath: options.output ? resolve34(process.cwd(), options.output) : void 0
64928
+ outputPath: options.output ? resolve33(process.cwd(), options.output) : void 0
64477
64929
  }
64478
64930
  });
64479
64931
  return;
@@ -64493,10 +64945,10 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64493
64945
  console.log();
64494
64946
  }
64495
64947
  if (options.output) {
64496
- const outputPath = resolve34(process.cwd(), options.output);
64948
+ const outputPath = resolve33(process.cwd(), options.output);
64497
64949
  const format = detectFormat(options.output, options.format);
64498
64950
  const content = formatTranscript(result, format);
64499
- await writeFile31(outputPath, content, "utf-8");
64951
+ await writeFile30(outputPath, content, "utf-8");
64500
64952
  console.log(source_default.green(`Saved ${format.toUpperCase()} to: ${outputPath}`));
64501
64953
  }
64502
64954
  } catch (error) {
@@ -64554,8 +65006,8 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
64554
65006
  }
64555
65007
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
64556
65008
  const spinner2 = ora("Reading audio file...").start();
64557
- const absPath = resolve34(process.cwd(), audioPath);
64558
- const audioBuffer = await readFile24(absPath);
65009
+ const absPath = resolve33(process.cwd(), audioPath);
65010
+ const audioBuffer = await readFile23(absPath);
64559
65011
  spinner2.text = "Isolating vocals...";
64560
65012
  const elevenlabs = new ElevenLabsProvider();
64561
65013
  await elevenlabs.initialize({ apiKey });
@@ -64564,8 +65016,8 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
64564
65016
  spinner2.fail(result.error || "Audio isolation failed");
64565
65017
  exitWithError(apiError(result.error || "Audio isolation failed", true));
64566
65018
  }
64567
- const outputPath = resolve34(process.cwd(), options.output);
64568
- await writeFile31(outputPath, result.audioBuffer);
65019
+ const outputPath = resolve33(process.cwd(), options.output);
65020
+ await writeFile30(outputPath, result.audioBuffer);
64569
65021
  spinner2.succeed(source_default.green("Vocals isolated"));
64570
65022
  if (isJsonMode()) {
64571
65023
  outputSuccess({
@@ -64625,12 +65077,12 @@ audioCommand.command("clone-voice").description("Clone a voice from audio sample
64625
65077
  const spinner2 = ora("Reading audio samples...").start();
64626
65078
  const audioBuffers = [];
64627
65079
  for (const samplePath of samples) {
64628
- const absPath = resolve34(process.cwd(), samplePath);
64629
- if (!existsSync26(absPath)) {
65080
+ const absPath = resolve33(process.cwd(), samplePath);
65081
+ if (!existsSync25(absPath)) {
64630
65082
  spinner2.fail(`File not found: ${samplePath}`);
64631
65083
  exitWithError(notFoundError(samplePath));
64632
65084
  }
64633
- const buffer = await readFile24(absPath);
65085
+ const buffer = await readFile23(absPath);
64634
65086
  audioBuffers.push(buffer);
64635
65087
  }
64636
65088
  spinner2.text = `Cloning voice from ${audioBuffers.length} sample(s)...`;
@@ -64686,8 +65138,8 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64686
65138
  if (!options.language) {
64687
65139
  exitWithError(usageError("Target language is required. Use -l or --language"));
64688
65140
  }
64689
- const absPath = resolve34(process.cwd(), mediaPath);
64690
- if (!existsSync26(absPath)) {
65141
+ const absPath = resolve33(process.cwd(), mediaPath);
65142
+ if (!existsSync25(absPath)) {
64691
65143
  exitWithError(notFoundError(mediaPath));
64692
65144
  }
64693
65145
  const openaiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
@@ -64698,7 +65150,7 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64698
65150
  const isVideo = [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
64699
65151
  let audioPath = absPath;
64700
65152
  if (isVideo) {
64701
- const tempAudioPath = resolve34(dirname11(absPath), `temp-audio-${Date.now()}.mp3`);
65153
+ const tempAudioPath = resolve33(dirname11(absPath), `temp-audio-${Date.now()}.mp3`);
64702
65154
  try {
64703
65155
  execSafeSync("ffmpeg", ["-i", absPath, "-vn", "-acodec", "mp3", "-y", tempAudioPath]);
64704
65156
  audioPath = tempAudioPath;
@@ -64710,7 +65162,7 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64710
65162
  spinner2.text = "Transcribing audio...";
64711
65163
  const whisper = new WhisperProvider();
64712
65164
  await whisper.initialize({ apiKey: openaiKey });
64713
- const audioBuffer = await readFile24(audioPath);
65165
+ const audioBuffer = await readFile23(audioPath);
64714
65166
  const audioBlob = new Blob([audioBuffer]);
64715
65167
  const transcriptResult = await whisper.transcribe(audioBlob, options.source);
64716
65168
  if (transcriptResult.status === "failed" || !transcriptResult.segments) {
@@ -64791,7 +65243,7 @@ ${segmentTexts}`,
64791
65243
  console.log();
64792
65244
  console.log(source_default.dim("Use without --analyze-only to generate dubbed audio"));
64793
65245
  if (options.output) {
64794
- const timingPath = resolve34(process.cwd(), options.output);
65246
+ const timingPath = resolve33(process.cwd(), options.output);
64795
65247
  const timingData = {
64796
65248
  sourcePath: absPath,
64797
65249
  sourceLanguage: transcriptResult.detectedLanguage || options.source || "auto",
@@ -64804,7 +65256,7 @@ ${segmentTexts}`,
64804
65256
  translated: translatedSegments[i]?.text || s.text
64805
65257
  }))
64806
65258
  };
64807
- await writeFile31(timingPath, JSON.stringify(timingData, null, 2));
65259
+ await writeFile30(timingPath, JSON.stringify(timingData, null, 2));
64808
65260
  console.log(`Timing saved to: ${source_default.bold(timingPath)}`);
64809
65261
  }
64810
65262
  return;
@@ -64829,12 +65281,12 @@ ${segmentTexts}`,
64829
65281
  spinner2.text = "Combining audio...";
64830
65282
  const combinedBuffer = Buffer.concat(dubbedAudioBuffers.map((a) => a.buffer));
64831
65283
  const outputExt = isVideo ? ".mp3" : extname7(absPath);
64832
- const defaultOutputPath2 = resolve34(
65284
+ const defaultOutputPath2 = resolve33(
64833
65285
  dirname11(absPath),
64834
65286
  `${basename8(absPath, extname7(absPath))}-${options.language}${outputExt}`
64835
65287
  );
64836
- const finalOutputPath = resolve34(process.cwd(), options.output || defaultOutputPath2);
64837
- await writeFile31(finalOutputPath, combinedBuffer);
65288
+ const finalOutputPath = resolve33(process.cwd(), options.output || defaultOutputPath2);
65289
+ await writeFile30(finalOutputPath, combinedBuffer);
64838
65290
  spinner2.succeed(source_default.green("Dubbing complete"));
64839
65291
  if (isJsonMode()) {
64840
65292
  outputSuccess({
@@ -64890,9 +65342,9 @@ audioCommand.command("duck").description("Auto-duck background music when voice
64890
65342
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
64891
65343
  }
64892
65344
  const spinner2 = ora("Processing audio ducking...").start();
64893
- const absMusic = resolve34(process.cwd(), musicPath);
64894
- const absVoice = resolve34(process.cwd(), options.voice);
64895
- const outputPath = options.output ? resolve34(process.cwd(), options.output) : absMusic.replace(/(\.[^.]+)$/, "-ducked$1");
65345
+ const absMusic = resolve33(process.cwd(), musicPath);
65346
+ const absVoice = resolve33(process.cwd(), options.voice);
65347
+ const outputPath = options.output ? resolve33(process.cwd(), options.output) : absMusic.replace(/(\.[^.]+)$/, "-ducked$1");
64896
65348
  const thresholdDb = parseFloat(options.threshold);
64897
65349
  const thresholdLinear = Math.pow(10, thresholdDb / 20);
64898
65350
  const ratio = parseFloat(options.ratio);
@@ -65500,7 +65952,7 @@ init_esm();
65500
65952
  init_source();
65501
65953
  init_ora();
65502
65954
  import { stat as stat4 } from "node:fs/promises";
65503
- import { resolve as resolve39, extname as extname9, basename as basename12 } from "node:path";
65955
+ import { resolve as resolve39, extname as extname10, basename as basename12 } from "node:path";
65504
65956
 
65505
65957
  // ../../node_modules/.pnpm/music-metadata@11.11.1/node_modules/music-metadata/lib/index.js
65506
65958
  init_lib();
@@ -69377,7 +69829,7 @@ mediaCommand.command("info").description("Get media file information").argument(
69377
69829
  const spinner2 = ora("Analyzing media...").start();
69378
69830
  try {
69379
69831
  const filePath = resolve39(process.cwd(), file);
69380
- const ext = extname9(filePath).toLowerCase();
69832
+ const ext = extname10(filePath).toLowerCase();
69381
69833
  const fileName = basename12(filePath);
69382
69834
  const fileStat = await stat4(filePath);
69383
69835
  const mediaType = detectMediaType2(ext);
@@ -69519,10 +69971,10 @@ init_engine();
69519
69971
  init_output();
69520
69972
  init_cost_tier();
69521
69973
  import { readFile as readFile27, writeFile as writeFile35, readdir as readdir2 } from "node:fs/promises";
69522
- import { resolve as resolve40, basename as basename13, extname as extname10, join as join10 } from "node:path";
69974
+ import { resolve as resolve40, basename as basename13, extname as extname11, join as join10 } from "node:path";
69523
69975
  var batchCommand = new Command("batch").description("Batch operations for processing multiple items");
69524
69976
  function detectMediaType3(filePath) {
69525
- const ext = extname10(filePath).toLowerCase();
69977
+ const ext = extname11(filePath).toLowerCase();
69526
69978
  const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
69527
69979
  const audioExts = [".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"];
69528
69980
  const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
@@ -69532,7 +69984,7 @@ function detectMediaType3(filePath) {
69532
69984
  return "video";
69533
69985
  }
69534
69986
  function isMediaFile(filePath) {
69535
- const ext = extname10(filePath).toLowerCase();
69987
+ const ext = extname11(filePath).toLowerCase();
69536
69988
  const mediaExts = [
69537
69989
  ".mp4",
69538
69990
  ".mov",
@@ -69590,7 +70042,7 @@ batchCommand.command("import").description("Import multiple media files from a d
69590
70042
  if (entry.isDirectory() && options.recursive) {
69591
70043
  await scanDir(entryPath);
69592
70044
  } else if (entry.isFile()) {
69593
- const ext = extname10(entry.name).toLowerCase();
70045
+ const ext = extname11(entry.name).toLowerCase();
69594
70046
  const matchesFilter = !filterExts || filterExts.includes(ext);
69595
70047
  if (matchesFilter && isMediaFile(entryPath)) {
69596
70048
  mediaFiles.push(entryPath);
@@ -70058,6 +70510,7 @@ var AI_FEATURES = [
70058
70510
  desc: "generate + edit",
70059
70511
  defaultProvider: "OpenAI gpt-image-2 (Artificial Analysis #1, since v0.56)",
70060
70512
  alsoAvailable: "Gemini Nano Banana, Grok Imagine",
70513
+ keyHint: "1 key",
70061
70514
  keys: [
70062
70515
  {
70063
70516
  configKey: "openai",
@@ -70112,6 +70565,7 @@ var AI_FEATURES = [
70112
70565
  desc: "generate + extend",
70113
70566
  defaultProvider: "Seedance 2.0 via fal.ai (Artificial Analysis #2 t2v + i2v, since v0.57)",
70114
70567
  alsoAvailable: "Grok Imagine, Kling, Runway Gen-4.5, Google Veo",
70568
+ keyHint: "1-2 keys",
70115
70569
  keys: [
70116
70570
  {
70117
70571
  configKey: "fal",
@@ -70132,6 +70586,15 @@ var AI_FEATURES = [
70132
70586
  url: "https://fal.ai/dashboard/keys",
70133
70587
  what: "ByteDance Seedance 2.0 text-to-video and image-to-video"
70134
70588
  },
70589
+ extraKeys: [
70590
+ {
70591
+ configKey: "imgbb",
70592
+ envVar: "IMGBB_API_KEY",
70593
+ name: "ImgBB",
70594
+ url: "https://api.imgbb.com/",
70595
+ what: "Image hosting for Seedance image-to-video uploads"
70596
+ }
70597
+ ],
70135
70598
  defaultFor: { kind: "video", value: "seedance" }
70136
70599
  },
70137
70600
  {
@@ -70156,6 +70619,15 @@ var AI_FEATURES = [
70156
70619
  url: "https://platform.klingai.com/",
70157
70620
  what: "Kling video generation"
70158
70621
  },
70622
+ extraKeys: [
70623
+ {
70624
+ configKey: "imgbb",
70625
+ envVar: "IMGBB_API_KEY",
70626
+ name: "ImgBB",
70627
+ url: "https://api.imgbb.com/",
70628
+ what: "Image hosting for Kling image-to-video uploads"
70629
+ }
70630
+ ],
70159
70631
  defaultFor: { kind: "video", value: "kling" }
70160
70632
  },
70161
70633
  {
@@ -70190,6 +70662,7 @@ var AI_FEATURES = [
70190
70662
  desc: "TTS, SFX, music, voice clone",
70191
70663
  defaultProvider: "ElevenLabs (paid, premium quality) \u2014 falls back to local Kokoro when no key (free, since v0.54)",
70192
70664
  alsoAvailable: "Replicate MusicGen (music only)",
70665
+ keyHint: "0-1 key",
70193
70666
  keys: [
70194
70667
  {
70195
70668
  configKey: "elevenlabs",
@@ -70235,6 +70708,7 @@ var AI_FEATURES = [
70235
70708
  desc: "captions, grade, reframe, motion graphics",
70236
70709
  defaultProvider: "Whisper (transcription) + Claude (reasoning)",
70237
70710
  alsoAvailable: "",
70711
+ keyHint: "2 keys",
70238
70712
  keys: [
70239
70713
  {
70240
70714
  configKey: "openai",
@@ -70268,8 +70742,11 @@ function applyProviderDefault(config2, choice) {
70268
70742
  }
70269
70743
  }
70270
70744
  function isProviderChoiceConfigured(config2, choice) {
70271
- if (choice.key) {
70272
- const value = config2.providers[choice.key.configKey];
70745
+ const keys = [choice.key, ...getChoiceExtraKeys(config2, choice)].filter(
70746
+ (keyDef) => Boolean(keyDef)
70747
+ );
70748
+ for (const keyDef of keys) {
70749
+ const value = config2.providers[keyDef.configKey];
70273
70750
  if (value) return true;
70274
70751
  }
70275
70752
  if (choice.defaultFor?.kind === "image") {
@@ -70280,6 +70757,96 @@ function isProviderChoiceConfigured(config2, choice) {
70280
70757
  }
70281
70758
  return false;
70282
70759
  }
70760
+ function getConfiguredUploadProvider(config2) {
70761
+ return process.env.VIBE_UPLOAD_PROVIDER?.toLowerCase() === "s3" || config2.upload.provider === "s3" ? "s3" : "imgbb";
70762
+ }
70763
+ function hasVideoFeature(features) {
70764
+ return features.some((feature) => feature.label === "Videos");
70765
+ }
70766
+ async function promptOptionalValue(label, currentValue) {
70767
+ const suffix = currentValue ? source_default.dim(` [${currentValue}]`) : "";
70768
+ const value = await prompt2(source_default.cyan(` ${label}${suffix}: `));
70769
+ const trimmed = value.trim();
70770
+ return trimmed || currentValue;
70771
+ }
70772
+ async function configureUploadHost(config2) {
70773
+ console.log(source_default.dim("2b. Upload host"));
70774
+ console.log(source_default.bold("Where should image-to-video upload reference images?"));
70775
+ console.log(
70776
+ source_default.dim(
70777
+ " Seedance and Kling need a temporary HTTPS image URL when you start from a local image."
70778
+ )
70779
+ );
70780
+ console.log();
70781
+ const currentProvider = getConfiguredUploadProvider(config2);
70782
+ const selected = await promptSelect(
70783
+ source_default.cyan(" Upload host [1-3]: "),
70784
+ [
70785
+ `ImgBB ${source_default.dim("- easiest setup, third-party image hosting (default)")}`,
70786
+ `S3 ${source_default.dim("- your own bucket, better control/privacy")}`,
70787
+ `Back ${source_default.dim("(choose different features)")}`
70788
+ ],
70789
+ currentProvider === "s3" ? 1 : 0,
70790
+ { backIndex: 2 }
70791
+ );
70792
+ console.log();
70793
+ if (selected === 2) {
70794
+ return "back";
70795
+ }
70796
+ if (selected === 0) {
70797
+ config2.upload.provider = "imgbb";
70798
+ process.env.VIBE_UPLOAD_PROVIDER = "imgbb";
70799
+ console.log(source_default.dim(" ImgBB selected. Seedance/Kling setup will ask for IMGBB_API_KEY."));
70800
+ console.log();
70801
+ return "continue";
70802
+ }
70803
+ config2.upload.provider = "s3";
70804
+ process.env.VIBE_UPLOAD_PROVIDER = "s3";
70805
+ config2.upload.s3 = config2.upload.s3 ?? {};
70806
+ const envBucket = process.env.VIBE_UPLOAD_S3_BUCKET;
70807
+ const envRegion = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
70808
+ const envPrefix = process.env.VIBE_UPLOAD_S3_PREFIX;
70809
+ const envTtl = process.env.VIBE_UPLOAD_TTL_SECONDS;
70810
+ console.log(source_default.dim(" S3 selected. AWS credentials stay in your shell/.env/AWS profile."));
70811
+ console.log(source_default.dim(" Required at runtime: AWS credentials, region, and bucket."));
70812
+ console.log();
70813
+ const bucket = await promptOptionalValue("S3 bucket", envBucket ?? config2.upload.s3.bucket);
70814
+ const region = await promptOptionalValue("AWS region", envRegion ?? config2.upload.s3.region);
70815
+ const prefix = await promptOptionalValue(
70816
+ "S3 prefix",
70817
+ envPrefix ?? config2.upload.s3.prefix ?? "vibeframe/tmp"
70818
+ );
70819
+ const ttlValue = await promptOptionalValue(
70820
+ "Upload URL TTL seconds",
70821
+ envTtl ?? String(config2.upload.ttlSeconds ?? 3600)
70822
+ );
70823
+ if (bucket) config2.upload.s3.bucket = bucket;
70824
+ if (region) config2.upload.s3.region = region;
70825
+ if (prefix) config2.upload.s3.prefix = prefix;
70826
+ const ttlSeconds = ttlValue ? Number.parseInt(ttlValue, 10) : NaN;
70827
+ if (Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
70828
+ config2.upload.ttlSeconds = ttlSeconds;
70829
+ }
70830
+ if (!config2.upload.s3.bucket || !config2.upload.s3.region) {
70831
+ console.log(
70832
+ source_default.yellow(
70833
+ " \u26A0 S3 selected, but bucket/region is incomplete. Set VIBE_UPLOAD_S3_BUCKET and AWS_REGION before image-to-video."
70834
+ )
70835
+ );
70836
+ } else {
70837
+ console.log(
70838
+ ` ${source_default.green("\u2713")} S3 upload host configured ${source_default.dim(`(${config2.upload.s3.bucket}/${config2.upload.s3.prefix ?? "vibeframe/tmp"})`)}`
70839
+ );
70840
+ }
70841
+ console.log();
70842
+ return "continue";
70843
+ }
70844
+ function getChoiceExtraKeys(config2, choice) {
70845
+ if (config2.upload.provider === "s3") {
70846
+ return [];
70847
+ }
70848
+ return choice.extraKeys ?? [];
70849
+ }
70283
70850
  function isFeatureConfigured(config2, feature) {
70284
70851
  if (feature.providerChoices?.some((choice) => isProviderChoiceConfigured(config2, choice))) {
70285
70852
  return true;
@@ -70389,6 +70956,7 @@ async function runNonInteractiveSetup(opts) {
70389
70956
  }
70390
70957
  async function runSetupWizard(fullSetup = false, scope = "user") {
70391
70958
  console.log();
70959
+ loadEnv();
70392
70960
  const cfgPath = getConfigPath(scope).replace(homedir5(), "~");
70393
70961
  const scopeNote = scope === "project" ? "applies to this project only \xB7 add .vibeframe/ to .gitignore" : "applies to every project for this user";
70394
70962
  console.log(source_default.bold.magenta("VibeFrame Setup") + source_default.bold(` \u2014 ${scope} scope`));
@@ -70540,8 +71108,7 @@ async function runAIFeaturesSetup(config2, _scope) {
70540
71108
  const backIndex = AI_FEATURES.length;
70541
71109
  const featureLabels = [
70542
71110
  ...AI_FEATURES.map((f) => {
70543
- const keyCount = f.keys.length;
70544
- const tag = source_default.dim(`${keyCount} key${keyCount > 1 ? "s" : ""}`);
71111
+ const tag = source_default.dim(f.keyHint ?? `${f.keys.length} key${f.keys.length > 1 ? "s" : ""}`);
70545
71112
  return `${f.label} ${source_default.dim(`(${f.desc})`)} ${tag}`;
70546
71113
  }),
70547
71114
  `Back ${source_default.dim("(return to setup checkpoint)")}`
@@ -70564,6 +71131,13 @@ async function runAIFeaturesSetup(config2, _scope) {
70564
71131
  console.log();
70565
71132
  return { tryCommand: "vibe --help", selectedFeatures: [] };
70566
71133
  }
71134
+ if (hasVideoFeature(selectedFeatures)) {
71135
+ const uploadResult = await configureUploadHost(config2);
71136
+ if (uploadResult === "back") {
71137
+ console.log();
71138
+ continue featureSelection;
71139
+ }
71140
+ }
70567
71141
  const plannedKeys = /* @__PURE__ */ new Map();
70568
71142
  console.log(source_default.dim("3. Providers"));
70569
71143
  console.log(source_default.bold("Choose providers for each selected feature"));
@@ -70586,7 +71160,12 @@ async function runAIFeaturesSetup(config2, _scope) {
70586
71160
  (choice) => isProviderChoiceConfigured(config2, choice)
70587
71161
  );
70588
71162
  const providerLabels = feature.providerChoices.map((choice) => {
70589
- const keyHint = choice.keyless ? source_default.dim("no key") : choice.key ? source_default.dim(choice.key.envVar) : source_default.dim("no key");
71163
+ const keyHint = choice.keyless ? source_default.dim("no key") : choice.key ? source_default.dim(
71164
+ [
71165
+ choice.key.envVar,
71166
+ ...getChoiceExtraKeys(config2, choice).map((keyDef) => keyDef.envVar)
71167
+ ].filter(Boolean).join(" + ")
71168
+ ) : source_default.dim("no key");
70590
71169
  const defaultHint = choice === feature.providerChoices?.[0] ? source_default.dim("recommended default") : "";
70591
71170
  return [choice.label, defaultHint, source_default.dim(`- ${choice.desc}`), keyHint].filter(Boolean).join(" ");
70592
71171
  });
@@ -70612,6 +71191,9 @@ async function runAIFeaturesSetup(config2, _scope) {
70612
71191
  }
70613
71192
  for (const choice of selectedChoices) {
70614
71193
  if (choice.key) addUniqueKey(keysForFeature, choice.key);
71194
+ for (const keyDef of getChoiceExtraKeys(config2, choice)) {
71195
+ addUniqueKey(keysForFeature, keyDef);
71196
+ }
70615
71197
  }
70616
71198
  if (selectedChoices.some((choice) => choice.keyless)) {
70617
71199
  console.log(source_default.dim(" No API key needed for selected local provider(s)."));
@@ -71266,10 +71848,21 @@ OPENROUTER_API_KEY= # multiplexes any provider above
71266
71848
  # \u2500\u2500 Media providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
71267
71849
  ELEVENLABS_API_KEY= # paid TTS / SFX / music (Kokoro is the free fallback)
71268
71850
  FAL_KEY= # Seedance 2.0 \u2014 default video provider since v0.57
71851
+ IMGBB_API_KEY= # image hosting for Seedance/Kling image-to-video
71269
71852
  RUNWAY_API_SECRET= # Runway Gen-4.5 video
71270
71853
  KLING_API_KEY= # Kling video
71271
71854
  REPLICATE_API_TOKEN= # MusicGen, real-esrgan, etc.
71272
- IMGBB_API_KEY= # public image hosting (rare \u2014 some Replicate flows need it)
71855
+
71856
+ # \u2500\u2500 Optional private upload host for image-to-video \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
71857
+ # Default is ImgBB. Set these only if you want temporary URLs from your own S3 bucket.
71858
+ # VIBE_UPLOAD_PROVIDER=s3 # imgbb | s3
71859
+ # VIBE_UPLOAD_TTL_SECONDS=3600
71860
+ # VIBE_UPLOAD_S3_BUCKET=
71861
+ # VIBE_UPLOAD_S3_PREFIX=vibeframe/tmp
71862
+ # AWS_REGION=
71863
+ # AWS_ACCESS_KEY_ID=
71864
+ # AWS_SECRET_ACCESS_KEY=
71865
+ # AWS_SESSION_TOKEN=
71273
71866
  `;
71274
71867
  }
71275
71868
  var GITIGNORE_ADDITIONS = `