@vibeframe/cli 0.91.9 → 0.93.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 (3) hide show
  1. package/CONTEXT.md +1 -1
  2. package/dist/index.js +1624 -1000
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -14270,6 +14270,9 @@ function getProvidersFor(kind) {
14270
14270
  function getProviderEnvVars() {
14271
14271
  return Object.fromEntries([...apiKeyRegistry.values()].map((k) => [k.configKey, k.envVar]));
14272
14272
  }
14273
+ function getProviderEnvAliases() {
14274
+ return Object.fromEntries([...apiKeyRegistry.values()].filter((k) => k.envAliases && k.envAliases.length > 0).map((k) => [k.envVar, k.envAliases ?? []]));
14275
+ }
14273
14276
  function getCommandKeyMap() {
14274
14277
  const map = {};
14275
14278
  for (const p of providerRegistry2.values()) {
@@ -14377,7 +14380,8 @@ var init_api_keys = __esm({
14377
14380
  });
14378
14381
  defineApiKey({
14379
14382
  configKey: "fal",
14380
- envVar: "FAL_KEY",
14383
+ envVar: "FAL_API_KEY",
14384
+ envAliases: ["FAL_KEY"],
14381
14385
  label: "fal.ai",
14382
14386
  showInSetup: true,
14383
14387
  setupDescription: "Seedance 2.0 video gen ($$, default since v0.57)",
@@ -14394,7 +14398,7 @@ var init_api_keys = __esm({
14394
14398
  label: "xAI",
14395
14399
  showInSetup: true,
14396
14400
  setupDescription: "Grok \u2014 video gen with audio ($$), image ($), Agent",
14397
- envExampleComment: "xAI API Key (Grok video generation \u2014 fallback when no FAL_KEY)",
14401
+ envExampleComment: "xAI API Key (Grok video generation \u2014 fallback when no FAL_API_KEY)",
14398
14402
  envExampleUrl: "https://console.x.ai/",
14399
14403
  keyFormat: { prefix: /^xai-/, example: "xai-..." }
14400
14404
  });
@@ -14443,8 +14447,8 @@ var init_api_keys = __esm({
14443
14447
  configKey: "imgbb",
14444
14448
  envVar: "IMGBB_API_KEY",
14445
14449
  label: "ImgBB",
14446
- showInSetup: false,
14447
- // not prompted in setup wizard — internal upload host
14450
+ showInSetup: true,
14451
+ setupDescription: "image hosting for Seedance/Kling image-to-video uploads",
14448
14452
  envExampleComment: "ImgBB API Key (image hosting \u2014 used by Kling and Seedance for image-to-video uploads)",
14449
14453
  envExampleUrl: "https://api.imgbb.com/",
14450
14454
  // ImgBB issues 32-character lowercase hex keys. Soft-warn if a pasted
@@ -14453,9 +14457,7 @@ var init_api_keys = __esm({
14453
14457
  keyFormat: { prefix: /^[a-f0-9]{32}$/, example: "32-char hex" },
14454
14458
  // ImgBB has no provider class (envvar-only); doctor still shows what it
14455
14459
  // unlocks at the apiKey level.
14456
- commandsUnlocked: [
14457
- "generate video -p kling/seedance (image-to-video upload host)"
14458
- ]
14460
+ commandsUnlocked: ["generate video -p kling/seedance (image-to-video upload host)"]
14459
14461
  });
14460
14462
  defineProvider({
14461
14463
  id: "openrouter",
@@ -24453,7 +24455,7 @@ var init_FalProvider = __esm({
24453
24455
  return {
24454
24456
  id: "",
24455
24457
  status: "failed",
24456
- error: "fal.ai API key not configured. Set FAL_KEY in .env."
24458
+ error: "fal.ai API key not configured. Set FAL_API_KEY in .env."
24457
24459
  };
24458
24460
  }
24459
24461
  const variant = options?.model ?? DEFAULT_VARIANT;
@@ -25230,6 +25232,7 @@ __export(dist_exports2, {
25230
25232
  getCommandKeyMap: () => getCommandKeyMap,
25231
25233
  getDisplayLabelForApiKey: () => getDisplayLabelForApiKey,
25232
25234
  getKeyFormat: () => getKeyFormat,
25235
+ getProviderEnvAliases: () => getProviderEnvAliases,
25233
25236
  getProviderEnvVars: () => getProviderEnvVars,
25234
25237
  getProvidersFor: () => getProvidersFor,
25235
25238
  getSetupProviders: () => getSetupProviders,
@@ -25677,12 +25680,19 @@ function createDefaultConfig() {
25677
25680
  aspectRatio: "16:9",
25678
25681
  exportQuality: "standard"
25679
25682
  },
25683
+ upload: {
25684
+ provider: "imgbb",
25685
+ ttlSeconds: 3600,
25686
+ s3: {
25687
+ prefix: "vibeframe/tmp"
25688
+ }
25689
+ },
25680
25690
  repl: {
25681
25691
  autoSave: true
25682
25692
  }
25683
25693
  };
25684
25694
  }
25685
- var PROVIDER_NAMES, PROVIDER_ENV_VARS;
25695
+ var PROVIDER_NAMES, PROVIDER_ENV_VARS, PROVIDER_ENV_ALIASES;
25686
25696
  var init_schema = __esm({
25687
25697
  "src/config/schema.ts"() {
25688
25698
  "use strict";
@@ -25696,10 +25706,32 @@ var init_schema = __esm({
25696
25706
  openrouter: "OpenRouter"
25697
25707
  };
25698
25708
  PROVIDER_ENV_VARS = getProviderEnvVars();
25709
+ PROVIDER_ENV_ALIASES = getProviderEnvAliases();
25699
25710
  }
25700
25711
  });
25701
25712
 
25702
25713
  // src/config/index.ts
25714
+ var config_exports = {};
25715
+ __export(config_exports, {
25716
+ CONFIG_DIR: () => CONFIG_DIR,
25717
+ CONFIG_PATH: () => CONFIG_PATH,
25718
+ PROVIDER_ENV_ALIASES: () => PROVIDER_ENV_ALIASES,
25719
+ PROVIDER_ENV_VARS: () => PROVIDER_ENV_VARS,
25720
+ PROVIDER_NAMES: () => PROVIDER_NAMES,
25721
+ USER_CONFIG_DIR: () => USER_CONFIG_DIR,
25722
+ USER_CONFIG_PATH: () => USER_CONFIG_PATH,
25723
+ createDefaultConfig: () => createDefaultConfig,
25724
+ getActiveScope: () => getActiveScope,
25725
+ getApiKeyFromConfig: () => getApiKeyFromConfig,
25726
+ getConfigDir: () => getConfigDir,
25727
+ getConfigPath: () => getConfigPath,
25728
+ getProjectConfigDir: () => getProjectConfigDir,
25729
+ getProjectConfigPath: () => getProjectConfigPath,
25730
+ isConfigured: () => isConfigured,
25731
+ loadConfig: () => loadConfig,
25732
+ saveConfig: () => saveConfig,
25733
+ updateProviderKey: () => updateProviderKey
25734
+ });
25703
25735
  import { resolve as resolve4 } from "node:path";
25704
25736
  import { homedir } from "node:os";
25705
25737
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access as access2 } from "node:fs/promises";
@@ -25734,6 +25766,11 @@ function applyDefaults(parsed) {
25734
25766
  llm: { ...defaults.llm, ...parsed.llm },
25735
25767
  providers: { ...defaults.providers, ...parsed.providers },
25736
25768
  defaults: { ...defaults.defaults, ...parsed.defaults },
25769
+ upload: {
25770
+ ...defaults.upload,
25771
+ ...parsed.upload,
25772
+ s3: { ...defaults.upload.s3, ...parsed.upload?.s3 }
25773
+ },
25737
25774
  repl: { ...defaults.repl, ...parsed.repl }
25738
25775
  };
25739
25776
  }
@@ -25761,6 +25798,11 @@ async function loadConfig(options = {}) {
25761
25798
  llm: { ...user.llm, ...project2.llm },
25762
25799
  providers: { ...user.providers, ...project2.providers },
25763
25800
  defaults: { ...user.defaults, ...project2.defaults },
25801
+ upload: {
25802
+ ...user.upload,
25803
+ ...project2.upload,
25804
+ s3: { ...user.upload.s3, ...project2.upload.s3 }
25805
+ },
25764
25806
  repl: { ...user.repl, ...project2.repl }
25765
25807
  };
25766
25808
  }
@@ -25788,7 +25830,7 @@ async function isConfigured() {
25788
25830
  return true;
25789
25831
  }
25790
25832
  const envVar = PROVIDER_ENV_VARS[providerKey];
25791
- if (envVar && process.env[envVar]) {
25833
+ if (envVar && getEnvValue(envVar)) {
25792
25834
  return true;
25793
25835
  }
25794
25836
  return false;
@@ -25799,10 +25841,20 @@ async function getApiKeyFromConfig(providerKey) {
25799
25841
  return config2.providers[providerKey];
25800
25842
  }
25801
25843
  const envVar = PROVIDER_ENV_VARS[providerKey];
25802
- if (envVar) return process.env[envVar];
25844
+ if (envVar) return getEnvValue(envVar);
25803
25845
  return void 0;
25804
25846
  }
25805
- var import_yaml, USER_CONFIG_DIR, USER_CONFIG_PATH, CONFIG_PATH;
25847
+ function getEnvValue(envVar) {
25848
+ return process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean);
25849
+ }
25850
+ async function updateProviderKey(providerKey, apiKey, options = {}) {
25851
+ const scope = options.scope ?? await getActiveScope(options.cwd);
25852
+ let config2 = await loadConfig({ scope, cwd: options.cwd });
25853
+ if (!config2) config2 = createDefaultConfig();
25854
+ config2.providers[providerKey] = apiKey;
25855
+ await saveConfig(config2, { scope, cwd: options.cwd });
25856
+ }
25857
+ var import_yaml, USER_CONFIG_DIR, USER_CONFIG_PATH, CONFIG_DIR, CONFIG_PATH;
25806
25858
  var init_config = __esm({
25807
25859
  "src/config/index.ts"() {
25808
25860
  "use strict";
@@ -25811,6 +25863,7 @@ var init_config = __esm({
25811
25863
  init_schema();
25812
25864
  USER_CONFIG_DIR = resolve4(homedir(), ".vibeframe");
25813
25865
  USER_CONFIG_PATH = resolve4(USER_CONFIG_DIR, "config.yaml");
25866
+ CONFIG_DIR = USER_CONFIG_DIR;
25814
25867
  CONFIG_PATH = USER_CONFIG_PATH;
25815
25868
  }
25816
25869
  });
@@ -25819,6 +25872,12 @@ var init_config = __esm({
25819
25872
  import { createInterface } from "node:readline";
25820
25873
  import { readFile as readFile3, writeFile as writeFile3, access as access3 } from "node:fs/promises";
25821
25874
  import { resolve as resolve5 } from "node:path";
25875
+ function providerKeyForEnvVar(envVar) {
25876
+ return PROVIDER_KEY_BY_ENV_VAR[envVar];
25877
+ }
25878
+ function getEnvValue2(envVar) {
25879
+ return process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean);
25880
+ }
25822
25881
  function loadEnv() {
25823
25882
  (0, import_dotenv.config)({ path: resolve5(process.cwd(), ".env"), debug: false, quiet: true });
25824
25883
  const monorepoRoot = findMonorepoRoot();
@@ -25881,19 +25940,7 @@ async function getApiKey(envVar, providerName, optionValue) {
25881
25940
  if (optionValue) {
25882
25941
  return optionValue;
25883
25942
  }
25884
- const providerKeyMap = {
25885
- ANTHROPIC_API_KEY: "anthropic",
25886
- OPENAI_API_KEY: "openai",
25887
- GOOGLE_API_KEY: "google",
25888
- XAI_API_KEY: "xai",
25889
- ELEVENLABS_API_KEY: "elevenlabs",
25890
- RUNWAY_API_SECRET: "runway",
25891
- KLING_API_KEY: "kling",
25892
- OPENROUTER_API_KEY: "openrouter",
25893
- IMGBB_API_KEY: "imgbb",
25894
- REPLICATE_API_TOKEN: "replicate"
25895
- };
25896
- const providerKey = providerKeyMap[envVar];
25943
+ const providerKey = providerKeyForEnvVar(envVar);
25897
25944
  if (providerKey) {
25898
25945
  const configKey = await getApiKeyFromConfig(providerKey);
25899
25946
  if (configKey) {
@@ -25901,7 +25948,7 @@ async function getApiKey(envVar, providerName, optionValue) {
25901
25948
  }
25902
25949
  }
25903
25950
  loadEnv();
25904
- const envValue = process.env[envVar];
25951
+ const envValue = getEnvValue2(envVar);
25905
25952
  if (envValue) {
25906
25953
  return envValue;
25907
25954
  }
@@ -25910,7 +25957,9 @@ async function getApiKey(envVar, providerName, optionValue) {
25910
25957
  }
25911
25958
  console.log();
25912
25959
  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.`));
25960
+ console.log(
25961
+ source_default.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`)
25962
+ );
25914
25963
  console.log();
25915
25964
  const apiKey = await prompt(source_default.cyan(`Enter ${providerName} API key: `), true);
25916
25965
  if (!apiKey || apiKey.trim() === "") {
@@ -25925,7 +25974,7 @@ async function getApiKey(envVar, providerName, optionValue) {
25925
25974
  }
25926
25975
  function hasApiKey(envVar) {
25927
25976
  loadEnv();
25928
- return !!process.env[envVar];
25977
+ return !!getEnvValue2(envVar);
25929
25978
  }
25930
25979
  async function requireApiKey(envVar, providerName, cliOverride) {
25931
25980
  const key = await getApiKey(envVar, providerName, cliOverride);
@@ -25954,22 +26003,39 @@ async function saveApiKeyToEnv(envVar, apiKey) {
25954
26003
  }
25955
26004
  await writeFile3(envPath, content, "utf-8");
25956
26005
  }
25957
- var import_dotenv, API_KEY_URLS, ApiKeyError;
26006
+ var import_dotenv, PROVIDER_KEY_BY_ENV_VAR, API_KEY_URLS, ApiKeyError;
25958
26007
  var init_api_key = __esm({
25959
26008
  "src/utils/api-key.ts"() {
25960
26009
  "use strict";
25961
26010
  import_dotenv = __toESM(require_main(), 1);
25962
26011
  init_source();
25963
26012
  init_config();
26013
+ PROVIDER_KEY_BY_ENV_VAR = {
26014
+ ANTHROPIC_API_KEY: "anthropic",
26015
+ OPENAI_API_KEY: "openai",
26016
+ GOOGLE_API_KEY: "google",
26017
+ XAI_API_KEY: "xai",
26018
+ ELEVENLABS_API_KEY: "elevenlabs",
26019
+ FAL_API_KEY: "fal",
26020
+ FAL_KEY: "fal",
26021
+ RUNWAY_API_SECRET: "runway",
26022
+ KLING_API_KEY: "kling",
26023
+ OPENROUTER_API_KEY: "openrouter",
26024
+ IMGBB_API_KEY: "imgbb",
26025
+ REPLICATE_API_TOKEN: "replicate"
26026
+ };
25964
26027
  API_KEY_URLS = {
25965
26028
  GOOGLE_API_KEY: "https://aistudio.google.com/apikey",
25966
26029
  OPENAI_API_KEY: "https://platform.openai.com/api-keys",
25967
26030
  ANTHROPIC_API_KEY: "https://console.anthropic.com/settings/keys",
25968
26031
  XAI_API_KEY: "https://console.x.ai",
25969
26032
  ELEVENLABS_API_KEY: "https://elevenlabs.io/app/settings/api-keys",
26033
+ FAL_API_KEY: "https://fal.ai/dashboard/keys",
26034
+ FAL_KEY: "https://fal.ai/dashboard/keys",
25970
26035
  RUNWAY_API_SECRET: "https://app.runwayml.com/settings/api-keys",
25971
26036
  KLING_API_KEY: "https://klingai.com/dev",
25972
- REPLICATE_API_TOKEN: "https://replicate.com/account/api-tokens"
26037
+ REPLICATE_API_TOKEN: "https://replicate.com/account/api-tokens",
26038
+ IMGBB_API_KEY: "https://api.imgbb.com/"
25973
26039
  };
25974
26040
  ApiKeyError = class extends Error {
25975
26041
  envVar;
@@ -33353,29 +33419,61 @@ var init_video_extend = __esm({
33353
33419
  });
33354
33420
 
33355
33421
  // src/utils/provider-resolver.ts
33422
+ function loadProviderDefaultsFromConfig(config2) {
33423
+ configDefaults = {};
33424
+ if (config2?.defaults.imageProvider) configDefaults.image = config2.defaults.imageProvider;
33425
+ if (config2?.defaults.videoProvider) configDefaults.video = config2.defaults.videoProvider;
33426
+ configProviderKeys = new Set(
33427
+ Object.entries(config2?.providers ?? {}).filter(([, value]) => Boolean(value)).map(([key]) => key)
33428
+ );
33429
+ }
33430
+ async function loadProviderDefaults() {
33431
+ try {
33432
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
33433
+ const config2 = await loadConfig2();
33434
+ loadProviderDefaultsFromConfig(config2);
33435
+ } catch {
33436
+ configDefaults = null;
33437
+ configProviderKeys = null;
33438
+ }
33439
+ }
33440
+ function providerKeyForEnvVar2(envVar) {
33441
+ const envVars = getProviderEnvVars();
33442
+ for (const [providerKey, candidateEnvVar] of Object.entries(envVars)) {
33443
+ if (candidateEnvVar === envVar) return providerKey;
33444
+ }
33445
+ return null;
33446
+ }
33447
+ function hasCandidateKey(candidate) {
33448
+ if (candidate.envVar === null) return true;
33449
+ if (hasApiKey(candidate.envVar)) return true;
33450
+ const providerKey = providerKeyForEnvVar2(candidate.envVar);
33451
+ return providerKey ? Boolean(configProviderKeys?.has(providerKey)) : false;
33452
+ }
33356
33453
  function resolveProvider(category) {
33357
33454
  const candidates = getProvidersFor(category);
33358
33455
  if (candidates.length === 0) return null;
33359
33456
  if (configDefaults?.[category]) {
33360
33457
  const preferred = candidates.find((c) => c.name === configDefaults[category]);
33361
- if (preferred && (preferred.envVar === null || hasApiKey(preferred.envVar))) {
33458
+ if (preferred && hasCandidateKey(preferred)) {
33362
33459
  return { name: preferred.name, label: preferred.label };
33363
33460
  }
33364
33461
  }
33365
33462
  for (const candidate of candidates) {
33366
- if (candidate.envVar === null || hasApiKey(candidate.envVar)) {
33463
+ if (hasCandidateKey(candidate)) {
33367
33464
  return { name: candidate.name, label: candidate.label };
33368
33465
  }
33369
33466
  }
33370
33467
  return null;
33371
33468
  }
33372
- var configDefaults;
33469
+ var configDefaults, configProviderKeys;
33373
33470
  var init_provider_resolver = __esm({
33374
33471
  "src/utils/provider-resolver.ts"() {
33375
33472
  "use strict";
33376
33473
  init_dist2();
33377
33474
  init_api_key();
33378
33475
  configDefaults = null;
33476
+ configProviderKeys = null;
33379
33477
  }
33380
33478
  });
33381
33479
 
@@ -33412,13 +33510,26 @@ import { resolve as resolve23, dirname as dirname7 } from "node:path";
33412
33510
  import { fileURLToPath } from "node:url";
33413
33511
  import { writeFile as writeFile21, mkdir as mkdir11 } from "node:fs/promises";
33414
33512
  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", `
33513
+ 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(
33514
+ "-p, --provider <provider>",
33515
+ "Provider: openai (default when OPENAI_API_KEY set), gemini, grok, runway"
33516
+ ).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(
33517
+ "-r, --ratio <ratio>",
33518
+ "Aspect ratio (gemini: 1:1, 1:4, 1:8, 4:1, 8:1, 16:9, 9:16, 3:4, 4:3, etc.)",
33519
+ "1:1"
33520
+ ).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(
33521
+ "-m, --model <model>",
33522
+ "Model. Gemini: flash, 3.1-flash, latest, pro. OpenAI: 1.5 (default), 2 (gpt-image-2)"
33523
+ ).option("--dry-run", "Preview parameters without executing").addHelpText(
33524
+ "after",
33525
+ `
33416
33526
  Examples:
33417
33527
  $ vibe generate image "a sunset over the ocean" -o sunset.png
33418
33528
  $ vibe gen img "logo design" -o logo.png -p openai
33419
33529
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
33420
33530
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
33421
- $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
33531
+ $ vibe gen img "product shot" --dry-run --json`
33532
+ ).action(async (prompt3, options) => {
33422
33533
  const startedAt = Date.now();
33423
33534
  try {
33424
33535
  if (!prompt3) {
@@ -33429,10 +33540,7 @@ Examples:
33429
33540
  }
33430
33541
  } else {
33431
33542
  exitWithError(
33432
- usageError(
33433
- "Prompt argument is required.",
33434
- "Usage: vibe generate image <prompt>"
33435
- )
33543
+ usageError("Prompt argument is required.", "Usage: vibe generate image <prompt>")
33436
33544
  );
33437
33545
  }
33438
33546
  }
@@ -33440,13 +33548,16 @@ Examples:
33440
33548
  if (options.output) {
33441
33549
  validateOutputPath(options.output);
33442
33550
  }
33551
+ await loadProviderDefaults();
33443
33552
  if (options.count !== void 0) {
33444
33553
  const n = parseInt(options.count, 10);
33445
33554
  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
- ));
33555
+ exitWithError(
33556
+ usageError(
33557
+ `Invalid --count: ${options.count}`,
33558
+ "Must be an integer between 1 and 10."
33559
+ )
33560
+ );
33450
33561
  }
33451
33562
  }
33452
33563
  const imageRegistry = getProvidersFor("image");
@@ -33511,11 +33622,9 @@ Examples:
33511
33622
  const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
33512
33623
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
33513
33624
  if (provider === "openai") {
33514
- const { result, modelLabel } = await executeOpenAIImageGenerate(
33515
- prompt3,
33516
- options,
33517
- { apiKey }
33518
- );
33625
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, {
33626
+ apiKey
33627
+ });
33519
33628
  if (!result.success || !result.images) {
33520
33629
  spinner2.fail(result.error || "Image generation failed");
33521
33630
  exitWithError(apiError(result.error || "Image generation failed", true));
@@ -33737,9 +33846,7 @@ Examples:
33737
33846
  spinner2.fail(result.error || "Image generation failed");
33738
33847
  exitWithError(apiError(result.error || "Image generation failed", true));
33739
33848
  }
33740
- spinner2.succeed(
33741
- source_default.green(`Generated ${result.images.length} image(s) with xAI Grok`)
33742
- );
33849
+ spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with xAI Grok`));
33743
33850
  if (isJsonMode()) {
33744
33851
  const outputPath = options.output ? resolve23(process.cwd(), options.output) : void 0;
33745
33852
  if (outputPath && result.images.length > 0) {
@@ -34972,606 +35079,206 @@ var init_video_utils = __esm({
34972
35079
  }
34973
35080
  });
34974
35081
 
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;
35082
+ // src/utils/upload-host.ts
35083
+ import { createHash as createHash4, createHmac as createHmac2, randomUUID } from "node:crypto";
35084
+ import { extname as extname3 } from "node:path";
35085
+ function envNumber(name) {
35086
+ const value = process.env[name];
35087
+ if (!value) return void 0;
35088
+ const n = Number.parseInt(value, 10);
35089
+ return Number.isFinite(n) && n > 0 ? n : void 0;
35011
35090
  }
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;
35091
+ function safePrefix(prefix) {
35092
+ return (prefix ?? "vibeframe/tmp").replace(/^\/+|\/+$/g, "");
35051
35093
  }
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;
35094
+ function extensionFor(opts) {
35095
+ const fromName = opts?.filename ? extname3(opts.filename).replace(/^\./, "") : "";
35096
+ if (fromName) return fromName.toLowerCase();
35097
+ const mime = opts?.mimeType ?? "image/png";
35098
+ if (mime === "image/jpeg") return "jpg";
35099
+ if (mime === "image/webp") return "webp";
35100
+ if (mime === "image/gif") return "gif";
35101
+ return "png";
35084
35102
  }
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;
35103
+ function hmac(key, value) {
35104
+ return createHmac2("sha256", key).update(value).digest();
35119
35105
  }
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: []
35106
+ function sha256Hex(value) {
35107
+ return createHash4("sha256").update(value).digest("hex");
35108
+ }
35109
+ function awsEncode(value) {
35110
+ return encodeURIComponent(value).replace(
35111
+ /[!'()*]/g,
35112
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
35113
+ );
35114
+ }
35115
+ function presignS3PutUrl(params) {
35116
+ const now = /* @__PURE__ */ new Date();
35117
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
35118
+ const dateStamp = amzDate.slice(0, 8);
35119
+ const host = `${params.bucket}.s3.${params.region}.amazonaws.com`;
35120
+ const credentialScope = `${dateStamp}/${params.region}/s3/aws4_request`;
35121
+ const canonicalUri = `/${params.key.split("/").map(awsEncode).join("/")}`;
35122
+ const query = {
35123
+ "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
35124
+ "X-Amz-Credential": `${params.accessKeyId}/${credentialScope}`,
35125
+ "X-Amz-Date": amzDate,
35126
+ "X-Amz-Expires": String(params.ttlSeconds),
35127
+ "X-Amz-SignedHeaders": "host"
35137
35128
  };
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
- }
35129
+ if (params.sessionToken) {
35130
+ query["X-Amz-Security-Token"] = params.sessionToken;
35131
+ }
35132
+ const canonicalQuery = Object.entries(query).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${awsEncode(key)}=${awsEncode(value)}`).join("&");
35133
+ const canonicalRequest = [
35134
+ "PUT",
35135
+ canonicalUri,
35136
+ canonicalQuery,
35137
+ `host:${host}`,
35138
+ "",
35139
+ "host",
35140
+ "UNSIGNED-PAYLOAD"
35141
+ ].join("\n");
35142
+ const stringToSign = [
35143
+ "AWS4-HMAC-SHA256",
35144
+ amzDate,
35145
+ credentialScope,
35146
+ sha256Hex(canonicalRequest)
35147
+ ].join("\n");
35148
+ const dateKey = hmac(`AWS4${params.secretAccessKey}`, dateStamp);
35149
+ const regionKey = hmac(dateKey, params.region);
35150
+ const serviceKey = hmac(regionKey, "s3");
35151
+ const signingKey = hmac(serviceKey, "aws4_request");
35152
+ const signature = createHmac2("sha256", signingKey).update(stringToSign).digest("hex");
35153
+ return `https://${host}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
35154
+ }
35155
+ function publicS3Url(params) {
35156
+ if (params.publicBaseUrl) {
35157
+ return `${params.publicBaseUrl.replace(/\/+$/g, "")}/${params.key.split("/").map(awsEncode).join("/")}`;
35158
+ }
35159
+ return `https://${params.bucket}.s3.${params.region}.amazonaws.com/${params.key.split("/").map(awsEncode).join("/")}`;
35160
+ }
35161
+ async function resolveUploadSettings() {
35162
+ const config2 = await loadConfig();
35163
+ const provider = process.env.VIBE_UPLOAD_PROVIDER ?? config2?.upload.provider ?? "imgbb";
35164
+ return {
35165
+ provider,
35166
+ ttlSeconds: envNumber("VIBE_UPLOAD_TTL_SECONDS") ?? config2?.upload.ttlSeconds ?? 3600,
35167
+ s3: {
35168
+ bucket: process.env.VIBE_UPLOAD_S3_BUCKET ?? config2?.upload.s3?.bucket,
35169
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? config2?.upload.s3?.region,
35170
+ prefix: process.env.VIBE_UPLOAD_S3_PREFIX ?? config2?.upload.s3?.prefix,
35171
+ publicBaseUrl: process.env.VIBE_UPLOAD_PUBLIC_BASE_URL ?? config2?.upload.s3?.publicBaseUrl
35193
35172
  }
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
- }
35173
+ };
35174
+ }
35175
+ async function resolveUploadHost() {
35176
+ const settings = await resolveUploadSettings();
35177
+ if (settings.provider === "s3") {
35178
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
35179
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
35180
+ const sessionToken = process.env.AWS_SESSION_TOKEN;
35181
+ const { bucket, region } = settings.s3;
35182
+ if (!accessKeyId || !secretAccessKey || !bucket || !region) {
35183
+ throw new Error(
35184
+ "S3 upload host requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and VIBE_UPLOAD_S3_BUCKET."
35185
+ );
35200
35186
  }
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
35187
+ return {
35188
+ provider: "s3",
35189
+ async uploadImage(imageBuffer, opts) {
35190
+ const ext = extensionFor(opts);
35191
+ const prefix = safePrefix(settings.s3.prefix);
35192
+ const key = `${prefix}/${Date.now()}-${randomUUID()}.${ext}`;
35193
+ const presignedUrl = presignS3PutUrl({
35194
+ accessKeyId,
35195
+ secretAccessKey,
35196
+ sessionToken,
35197
+ region,
35198
+ bucket,
35199
+ key,
35200
+ ttlSeconds: settings.ttlSeconds
35215
35201
  });
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
- }
35202
+ const response = await fetch(presignedUrl, {
35203
+ method: "PUT",
35204
+ headers: {
35205
+ "content-type": opts?.mimeType ?? "application/octet-stream"
35206
+ },
35207
+ body: new Uint8Array(imageBuffer)
35208
+ });
35209
+ if (!response.ok) {
35210
+ throw new Error(`S3 upload failed (${response.status}): ${response.statusText}`);
35251
35211
  }
35252
- const dalleImageSizes = {
35253
- "16:9": "1536x1024",
35254
- "9:16": "1024x1536",
35255
- "1:1": "1024x1024"
35212
+ return {
35213
+ provider: "s3",
35214
+ url: publicS3Url({ region, bucket, key, publicBaseUrl: settings.s3.publicBaseUrl }),
35215
+ expiresAt: new Date(Date.now() + settings.ttlSeconds * 1e3).toISOString()
35256
35216
  };
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
35217
  }
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);
35218
+ };
35219
+ }
35220
+ return {
35221
+ provider: "imgbb",
35222
+ async uploadImage(imageBuffer) {
35223
+ const imgbbKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
35224
+ if (!imgbbKey) {
35225
+ throw new Error("IMGBB_API_KEY required for image-to-video uploads.");
35524
35226
  }
35525
- }
35526
- if (storyboardMutated) {
35527
- let currentTime = 0;
35528
- for (const segment of segments) {
35529
- segment.startTime = currentTime;
35530
- currentTime += segment.duration;
35227
+ const result = await uploadToImgbb(imageBuffer, imgbbKey);
35228
+ if (!result.success || !result.url) {
35229
+ throw new Error(`ImgBB upload failed: ${result.error ?? "unknown error"}`);
35531
35230
  }
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");
35231
+ return { provider: "imgbb", url: result.url };
35534
35232
  }
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
- }
35233
+ };
35543
35234
  }
35544
- var import_yaml5;
35545
- var init_ai_script_pipeline = __esm({
35546
- "src/commands/ai-script-pipeline.ts"() {
35235
+ var init_upload_host = __esm({
35236
+ "src/utils/upload-host.ts"() {
35547
35237
  "use strict";
35548
- import_yaml5 = __toESM(require_dist(), 1);
35549
- init_dist2();
35550
- init_api_key();
35551
35238
  init_config();
35552
- init_audio();
35553
- init_ai_helpers();
35554
35239
  init_video_utils();
35555
- init_video_providers();
35556
35240
  }
35557
35241
  });
35558
35242
 
35559
35243
  // 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";
35244
+ import { resolve as resolve25 } from "node:path";
35245
+ import { readFile as readFile13, writeFile as writeFile23 } from "node:fs/promises";
35562
35246
  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(
35247
+ 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(
35248
+ "-p, --provider <provider>",
35249
+ "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."
35250
+ ).option(
35251
+ "-k, --api-key <key>",
35252
+ "API key (or set FAL_API_KEY / XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)"
35253
+ ).option("-o, --output <path>", "Output file path (downloads video)").option("-i, --image <path>", "Reference image for image-to-video").option(
35564
35254
  "-d, --duration <sec>",
35565
35255
  "Duration in seconds. Seedance accepts 4-15; Kling accepts 5 or 10; Veo maps to 6 or 8.",
35566
35256
  "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", `
35257
+ ).option(
35258
+ "-r, --ratio <ratio>",
35259
+ "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)"
35260
+ ).option("--seed <number>", "Random seed for reproducibility (Runway only)").option("--mode <mode>", "Generation mode: std or pro (Kling only)", "std").option(
35261
+ "--seedance-model <model>",
35262
+ "Seedance variant: quality or fast (fal.ai only)",
35263
+ "quality"
35264
+ ).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(
35265
+ "--ref-images <paths...>",
35266
+ "Reference images for character consistency (Veo 3.1 only, max 3)"
35267
+ ).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(
35268
+ "--runway-model <model>",
35269
+ "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)",
35270
+ "gen4.5"
35271
+ ).option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText(
35272
+ "after",
35273
+ `
35568
35274
  Examples:
35569
- $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
35275
+ $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_API_KEY is set
35570
35276
  $ vibe gen vid "cinematic city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
35571
35277
  $ vibe gen vid "city timelapse" -o city.mp4 -p kling # Kling
35572
35278
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
35573
35279
  $ 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) => {
35280
+ $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`
35281
+ ).action(async (prompt3, options) => {
35575
35282
  const startedAt = Date.now();
35576
35283
  try {
35577
35284
  if (!prompt3) {
@@ -35582,10 +35289,7 @@ Examples:
35582
35289
  }
35583
35290
  } else {
35584
35291
  exitWithError(
35585
- usageError(
35586
- "Prompt argument is required.",
35587
- "Usage: vibe generate video <prompt>"
35588
- )
35292
+ usageError("Prompt argument is required.", "Usage: vibe generate video <prompt>")
35589
35293
  );
35590
35294
  }
35591
35295
  }
@@ -35593,13 +35297,16 @@ Examples:
35593
35297
  if (options.output) {
35594
35298
  validateOutputPath(options.output);
35595
35299
  }
35300
+ await loadProviderDefaults();
35596
35301
  if (options.duration !== void 0) {
35597
35302
  const d = parseFloat(options.duration);
35598
35303
  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
- ));
35304
+ exitWithError(
35305
+ usageError(
35306
+ `Invalid --duration: ${options.duration}`,
35307
+ "Must be a positive number \u2264 60 seconds."
35308
+ )
35309
+ );
35603
35310
  }
35604
35311
  }
35605
35312
  const validProviders = ["runway", "kling", "veo", "grok", "seedance", "fal"];
@@ -35608,7 +35315,7 @@ Examples:
35608
35315
  veo: "GOOGLE_API_KEY",
35609
35316
  kling: "KLING_API_KEY",
35610
35317
  runway: "RUNWAY_API_SECRET",
35611
- seedance: "FAL_KEY"
35318
+ seedance: "FAL_API_KEY"
35612
35319
  };
35613
35320
  let provider;
35614
35321
  if (options.provider) {
@@ -35641,10 +35348,12 @@ Examples:
35641
35348
  provider = resolved?.name ?? "grok";
35642
35349
  }
35643
35350
  let referenceImage;
35351
+ let referenceImageBuffer;
35352
+ let referenceImageMimeType;
35644
35353
  let isImageToVideo = false;
35645
35354
  if (options.image) {
35646
- const imagePath = resolve26(process.cwd(), options.image);
35647
- const imageBuffer = await readFile14(imagePath);
35355
+ const imagePath = resolve25(process.cwd(), options.image);
35356
+ const imageBuffer = await readFile13(imagePath);
35648
35357
  const ext = options.image.toLowerCase().split(".").pop();
35649
35358
  const mimeTypes2 = {
35650
35359
  jpg: "image/jpeg",
@@ -35654,6 +35363,8 @@ Examples:
35654
35363
  webp: "image/webp"
35655
35364
  };
35656
35365
  const mimeType = mimeTypes2[ext || "png"] || "image/png";
35366
+ referenceImageBuffer = imageBuffer;
35367
+ referenceImageMimeType = mimeType;
35657
35368
  referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
35658
35369
  isImageToVideo = true;
35659
35370
  if (!options.ratio) {
@@ -35707,7 +35418,7 @@ Examples:
35707
35418
  kling: "KLING_API_KEY",
35708
35419
  veo: "GOOGLE_API_KEY",
35709
35420
  grok: "XAI_API_KEY",
35710
- seedance: "FAL_KEY"
35421
+ seedance: "FAL_API_KEY"
35711
35422
  };
35712
35423
  const providerNameMap = {
35713
35424
  runway: "Runway",
@@ -35778,20 +35489,22 @@ Examples:
35778
35489
  }
35779
35490
  let klingImage = referenceImage;
35780
35491
  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;
35492
+ try {
35493
+ const uploadHost = await resolveUploadHost();
35494
+ spinner2.text = `Uploading image via ${uploadHost.provider} for Kling...`;
35495
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
35496
+ filename: options.image,
35497
+ mimeType: referenceImageMimeType
35498
+ });
35499
+ klingImage = upload.url;
35500
+ } catch (err) {
35501
+ spinner2.fail("Image upload failed");
35502
+ const message = err instanceof Error ? err.message : String(err);
35503
+ if (message.includes("IMGBB_API_KEY")) {
35504
+ exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35505
+ }
35506
+ exitWithError(apiError(message, true));
35507
+ }
35795
35508
  spinner2.text = "Starting video generation...";
35796
35509
  }
35797
35510
  result = await kling.generateVideo(prompt3, {
@@ -35846,8 +35559,8 @@ Examples:
35846
35559
  const veoDuration = parseInt(options.duration) <= 6 ? 6 : 8;
35847
35560
  let lastFrame;
35848
35561
  if (options.lastFrame) {
35849
- const lastFramePath = resolve26(process.cwd(), options.lastFrame);
35850
- const lastFrameBuffer = await readFile14(lastFramePath);
35562
+ const lastFramePath = resolve25(process.cwd(), options.lastFrame);
35563
+ const lastFrameBuffer = await readFile13(lastFramePath);
35851
35564
  const ext = options.lastFrame.toLowerCase().split(".").pop();
35852
35565
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
35853
35566
  lastFrame = `data:${mimeType};base64,${lastFrameBuffer.toString("base64")}`;
@@ -35856,8 +35569,8 @@ Examples:
35856
35569
  if (options.refImages && options.refImages.length > 0) {
35857
35570
  refImages = [];
35858
35571
  for (const refPath of options.refImages.slice(0, 3)) {
35859
- const absRefPath = resolve26(process.cwd(), refPath);
35860
- const refBuffer = await readFile14(absRefPath);
35572
+ const absRefPath = resolve25(process.cwd(), refPath);
35573
+ const refBuffer = await readFile13(absRefPath);
35861
35574
  const ext = refPath.toLowerCase().split(".").pop();
35862
35575
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext || "png"}`;
35863
35576
  refImages.push({ base64: refBuffer.toString("base64"), mimeType });
@@ -35938,20 +35651,22 @@ Examples:
35938
35651
  await fal.initialize({ apiKey });
35939
35652
  let falImage = referenceImage;
35940
35653
  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));
35654
+ try {
35655
+ const uploadHost = await resolveUploadHost();
35656
+ spinner2.text = `Uploading image via ${uploadHost.provider} for Seedance...`;
35657
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
35658
+ filename: options.image,
35659
+ mimeType: referenceImageMimeType
35660
+ });
35661
+ falImage = upload.url;
35662
+ } catch (err) {
35663
+ spinner2.fail("Image upload failed");
35664
+ const message = err instanceof Error ? err.message : String(err);
35665
+ if (message.includes("IMGBB_API_KEY")) {
35666
+ exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
35667
+ }
35668
+ exitWithError(apiError(message, true));
35953
35669
  }
35954
- falImage = uploadResult.url;
35955
35670
  }
35956
35671
  spinner2.text = "Generating video with fal.ai Seedance 2.0 (this may take 1-3 minutes)...";
35957
35672
  const seedanceModel = String(options.seedanceModel ?? "quality").toLowerCase();
@@ -35975,8 +35690,8 @@ Examples:
35975
35690
  let outputPath;
35976
35691
  if (options.output && finalResult.videoUrl) {
35977
35692
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
35978
- outputPath = resolve26(process.cwd(), options.output);
35979
- await writeFile24(outputPath, buffer);
35693
+ outputPath = resolve25(process.cwd(), options.output);
35694
+ await writeFile23(outputPath, buffer);
35980
35695
  }
35981
35696
  outputSuccess({
35982
35697
  command: "generate video",
@@ -36003,14 +35718,12 @@ Examples:
36003
35718
  const downloadSpinner = ora("Downloading video...").start();
36004
35719
  try {
36005
35720
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
36006
- const outputPath = resolve26(process.cwd(), options.output);
36007
- await writeFile24(outputPath, buffer);
35721
+ const outputPath = resolve25(process.cwd(), options.output);
35722
+ await writeFile23(outputPath, buffer);
36008
35723
  downloadSpinner.succeed(source_default.green(`Saved to: ${outputPath}`));
36009
35724
  } catch (err) {
36010
35725
  downloadSpinner.fail(
36011
- source_default.red(
36012
- `Failed to download video: ${err instanceof Error ? err.message : err}`
36013
- )
35726
+ source_default.red(`Failed to download video: ${err instanceof Error ? err.message : err}`)
36014
35727
  );
36015
35728
  }
36016
35729
  }
@@ -36028,11 +35741,10 @@ var init_video = __esm({
36028
35741
  init_dist2();
36029
35742
  init_api_key();
36030
35743
  init_tty();
36031
- init_config();
36032
35744
  init_output();
36033
35745
  init_validate();
36034
35746
  init_provider_resolver();
36035
- init_ai_script_pipeline();
35747
+ init_upload_host();
36036
35748
  init_ai_helpers();
36037
35749
  }
36038
35750
  });
@@ -36085,7 +35797,7 @@ var init_generate = __esm({
36085
35797
  Examples:
36086
35798
  $ vibe generate image "a sunset over the ocean" -o sunset.png
36087
35799
  $ vibe generate image "logo design" -o logo.png -p openai
36088
- $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
35800
+ $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_API_KEY is set
36089
35801
  $ vibe generate video "city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
36090
35802
  $ vibe generate video "city timelapse" -o city.mp4 -p kling # Kling
36091
35803
  $ vibe generate video "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
@@ -36096,7 +35808,7 @@ Examples:
36096
35808
  API Keys (per provider):
36097
35809
  GOOGLE_API_KEY Image (default), Veo video
36098
35810
  OPENAI_API_KEY Image (-p openai)
36099
- FAL_KEY Seedance video (-p seedance, default video)
35811
+ FAL_API_KEY Seedance video (-p seedance, default video)
36100
35812
  XAI_API_KEY Grok image/video
36101
35813
  KLING_API_KEY Kling video (-p kling)
36102
35814
  RUNWAY_API_SECRET Runway video (-p runway)
@@ -36137,8 +35849,8 @@ Run 'vibe schema generate.<command>' for structured parameter info.
36137
35849
  });
36138
35850
 
36139
35851
  // src/commands/_shared/edit/silence-cut.ts
36140
- import { existsSync as existsSync15 } from "node:fs";
36141
- import { readFile as readFile15 } from "node:fs/promises";
35852
+ import { existsSync as existsSync14 } from "node:fs";
35853
+ import { readFile as readFile14 } from "node:fs/promises";
36142
35854
  async function detectSilencePeriods(videoPath, noiseThreshold, minDuration) {
36143
35855
  const totalDuration = await getVideoDuration(videoPath);
36144
35856
  const { stdout, stderr } = await execSafe(
@@ -36188,7 +35900,7 @@ async function detectSilencePeriodsWithGemini(videoPath, minDuration, options) {
36188
35900
  }
36189
35901
  const gemini = new GeminiProvider();
36190
35902
  await gemini.initialize({ apiKey: geminiApiKey });
36191
- const videoBuffer = await readFile15(videoPath);
35903
+ const videoBuffer = await readFile14(videoPath);
36192
35904
  const modelMap = {
36193
35905
  flash: "gemini-3-flash-preview",
36194
35906
  "flash-2.5": "gemini-2.5-flash",
@@ -36269,7 +35981,7 @@ async function executeSilenceCut(options) {
36269
35981
  analyzeOnly = false,
36270
35982
  useGemini = false
36271
35983
  } = options;
36272
- if (!existsSync15(videoPath)) {
35984
+ if (!existsSync14(videoPath)) {
36273
35985
  return { success: false, error: `Video not found: ${videoPath}` };
36274
35986
  }
36275
35987
  if (!commandExists("ffmpeg")) {
@@ -36377,11 +36089,11 @@ var init_silence_cut = __esm({
36377
36089
  });
36378
36090
 
36379
36091
  // 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";
36092
+ import { existsSync as existsSync15 } from "node:fs";
36093
+ import { readFile as readFile15, writeFile as writeFile24, mkdir as mkdir12 } from "node:fs/promises";
36382
36094
  import { join as join8 } from "node:path";
36383
36095
  async function transcribeWithWords(audioPath, apiKey, language) {
36384
- const audioBuffer = await readFile16(audioPath);
36096
+ const audioBuffer = await readFile15(audioPath);
36385
36097
  const audioBlob = new Blob([audioBuffer]);
36386
36098
  const formData = new FormData();
36387
36099
  formData.append("file", audioBlob, "audio.wav");
@@ -36440,7 +36152,7 @@ async function executeJumpCut(options) {
36440
36152
  analyzeOnly = false,
36441
36153
  apiKey
36442
36154
  } = options;
36443
- if (!existsSync16(videoPath)) {
36155
+ if (!existsSync15(videoPath)) {
36444
36156
  return { success: false, error: `Video not found: ${videoPath}` };
36445
36157
  }
36446
36158
  if (!commandExists("ffmpeg")) {
@@ -36537,7 +36249,7 @@ async function executeJumpCut(options) {
36537
36249
  }
36538
36250
  const concatList = segmentPaths.map((p) => `file '${p}'`).join("\n");
36539
36251
  const listPath = join8(tmpDir, "concat.txt");
36540
- await writeFile25(listPath, concatList);
36252
+ await writeFile24(listPath, concatList);
36541
36253
  await execSafe(
36542
36254
  "ffmpeg",
36543
36255
  [
@@ -36726,8 +36438,8 @@ var init_helpers = __esm({
36726
36438
  });
36727
36439
 
36728
36440
  // 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";
36441
+ import { existsSync as existsSync16 } from "node:fs";
36442
+ import { readFile as readFile16, writeFile as writeFile25, mkdir as mkdir13 } from "node:fs/promises";
36731
36443
  import { dirname as dirname8, basename as basename6, extname as extname4, join as join9 } from "node:path";
36732
36444
  function getCaptionForceStyle(style, fontSize, fontColor, position) {
36733
36445
  const alignment = position === "top" ? 8 : position === "center" ? 5 : 2;
@@ -36756,7 +36468,7 @@ async function executeCaption(options) {
36756
36468
  position = "bottom",
36757
36469
  apiKey
36758
36470
  } = options;
36759
- if (!existsSync17(videoPath)) {
36471
+ if (!existsSync16(videoPath)) {
36760
36472
  return { success: false, error: `Video not found: ${videoPath}` };
36761
36473
  }
36762
36474
  if (!commandExists("ffmpeg")) {
@@ -36797,7 +36509,7 @@ async function executeCaption(options) {
36797
36509
  );
36798
36510
  const whisper = new WhisperProvider();
36799
36511
  await whisper.initialize({ apiKey: openaiKey });
36800
- const audioBuffer = await readFile17(audioPath);
36512
+ const audioBuffer = await readFile16(audioPath);
36801
36513
  const audioBlob = new Blob([audioBuffer]);
36802
36514
  const transcriptResult = await whisper.transcribe(audioBlob, language);
36803
36515
  if (transcriptResult.status === "failed" || !transcriptResult.segments || transcriptResult.segments.length === 0) {
@@ -36807,7 +36519,7 @@ async function executeCaption(options) {
36807
36519
  };
36808
36520
  }
36809
36521
  const srtContent = formatSRT(transcriptResult.segments);
36810
- await writeFile26(srtPath, srtContent);
36522
+ await writeFile25(srtPath, srtContent);
36811
36523
  const { width, height } = await getVideoResolution(videoPath);
36812
36524
  const fontSize = customFontSize || Math.round(height / 18);
36813
36525
  let hasSubtitles = false;
@@ -36849,7 +36561,7 @@ async function executeCaption(options) {
36849
36561
  outputDir2,
36850
36562
  basename6(outputPath, extname4(outputPath)) + ".srt"
36851
36563
  );
36852
- await writeFile26(outputSrtPath2, srtContent);
36564
+ await writeFile25(outputSrtPath2, srtContent);
36853
36565
  return { success: false, error: `${remotionErr}
36854
36566
  SRT saved to: ${outputSrtPath2}` };
36855
36567
  }
@@ -36888,7 +36600,7 @@ SRT saved to: ${outputSrtPath2}` };
36888
36600
  outputDir2,
36889
36601
  basename6(outputPath, extname4(outputPath)) + ".srt"
36890
36602
  );
36891
- await writeFile26(outputSrtPath2, srtContent);
36603
+ await writeFile25(outputSrtPath2, srtContent);
36892
36604
  return {
36893
36605
  success: false,
36894
36606
  error: `${renderResult.error}
@@ -36901,7 +36613,7 @@ SRT saved to: ${outputSrtPath2}`
36901
36613
  outputDir,
36902
36614
  basename6(outputPath, extname4(outputPath)) + ".srt"
36903
36615
  );
36904
- await writeFile26(outputSrtPath, srtContent);
36616
+ await writeFile25(outputSrtPath, srtContent);
36905
36617
  return {
36906
36618
  success: true,
36907
36619
  outputPath,
@@ -36934,10 +36646,10 @@ var init_caption = __esm({
36934
36646
  });
36935
36647
 
36936
36648
  // src/commands/_shared/edit/noise-reduce.ts
36937
- import { existsSync as existsSync18 } from "node:fs";
36649
+ import { existsSync as existsSync17 } from "node:fs";
36938
36650
  async function executeNoiseReduce(options) {
36939
36651
  const { inputPath, outputPath, strength = "medium", noiseFloor } = options;
36940
- if (!existsSync18(inputPath)) {
36652
+ if (!existsSync17(inputPath)) {
36941
36653
  return { success: false, error: `File not found: ${inputPath}` };
36942
36654
  }
36943
36655
  if (!commandExists("ffmpeg")) {
@@ -36994,7 +36706,7 @@ var init_noise_reduce = __esm({
36994
36706
  });
36995
36707
 
36996
36708
  // src/commands/_shared/edit/fade.ts
36997
- import { existsSync as existsSync19 } from "node:fs";
36709
+ import { existsSync as existsSync18 } from "node:fs";
36998
36710
  async function executeFade(options) {
36999
36711
  const {
37000
36712
  videoPath,
@@ -37004,7 +36716,7 @@ async function executeFade(options) {
37004
36716
  audioOnly = false,
37005
36717
  videoOnly = false
37006
36718
  } = options;
37007
- if (!existsSync19(videoPath)) {
36719
+ if (!existsSync18(videoPath)) {
37008
36720
  return { success: false, error: `Video not found: ${videoPath}` };
37009
36721
  }
37010
36722
  if (!commandExists("ffmpeg")) {
@@ -37071,8 +36783,8 @@ var init_fade = __esm({
37071
36783
  });
37072
36784
 
37073
36785
  // 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";
36786
+ import { existsSync as existsSync19 } from "node:fs";
36787
+ import { readFile as readFile17, writeFile as writeFile26 } from "node:fs/promises";
37076
36788
  async function executeTranslateSrt(options) {
37077
36789
  const {
37078
36790
  srtPath,
@@ -37082,11 +36794,11 @@ async function executeTranslateSrt(options) {
37082
36794
  sourceLanguage,
37083
36795
  apiKey
37084
36796
  } = options;
37085
- if (!existsSync20(srtPath)) {
36797
+ if (!existsSync19(srtPath)) {
37086
36798
  return { success: false, error: `SRT file not found: ${srtPath}` };
37087
36799
  }
37088
36800
  try {
37089
- const srtContent = await readFile18(srtPath, "utf-8");
36801
+ const srtContent = await readFile17(srtPath, "utf-8");
37090
36802
  const segments = parseSRT(srtContent);
37091
36803
  if (segments.length === 0) {
37092
36804
  return { success: false, error: "No subtitle segments found in SRT file" };
@@ -37176,7 +36888,7 @@ ${textsToTranslate}`;
37176
36888
  }
37177
36889
  }
37178
36890
  const translatedSrt = formatSRT(translatedSegments);
37179
- await writeFile27(outputPath, translatedSrt);
36891
+ await writeFile26(outputPath, translatedSrt);
37180
36892
  return {
37181
36893
  success: true,
37182
36894
  outputPath,
@@ -37199,8 +36911,8 @@ var init_translate_srt = __esm({
37199
36911
  });
37200
36912
 
37201
36913
  // src/commands/_shared/edit/text-overlay.ts
37202
- import { existsSync as existsSync21 } from "node:fs";
37203
- import { resolve as resolve27 } from "node:path";
36914
+ import { existsSync as existsSync20 } from "node:fs";
36915
+ import { resolve as resolve26 } from "node:path";
37204
36916
  function detectSystemFont() {
37205
36917
  const platform3 = process.platform;
37206
36918
  if (platform3 === "darwin") {
@@ -37210,7 +36922,7 @@ function detectSystemFont() {
37210
36922
  "/Library/Fonts/Arial.ttf"
37211
36923
  ];
37212
36924
  for (const f of candidates) {
37213
- if (existsSync21(f)) return f;
36925
+ if (existsSync20(f)) return f;
37214
36926
  }
37215
36927
  } else if (platform3 === "linux") {
37216
36928
  const candidates = [
@@ -37219,7 +36931,7 @@ function detectSystemFont() {
37219
36931
  "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
37220
36932
  ];
37221
36933
  for (const f of candidates) {
37222
- if (existsSync21(f)) return f;
36934
+ if (existsSync20(f)) return f;
37223
36935
  }
37224
36936
  } else if (platform3 === "win32") {
37225
36937
  const candidates = [
@@ -37227,7 +36939,7 @@ function detectSystemFont() {
37227
36939
  "C:\\Windows\\Fonts\\segoeui.ttf"
37228
36940
  ];
37229
36941
  for (const f of candidates) {
37230
- if (existsSync21(f)) return f;
36942
+ if (existsSync20(f)) return f;
37231
36943
  }
37232
36944
  }
37233
36945
  return null;
@@ -37246,9 +36958,9 @@ async function applyTextOverlays(options) {
37246
36958
  if (!texts || texts.length === 0) {
37247
36959
  return { success: false, error: "No texts provided" };
37248
36960
  }
37249
- const absVideoPath = resolve27(process.cwd(), videoPath);
37250
- const absOutputPath = resolve27(process.cwd(), outputPath);
37251
- if (!existsSync21(absVideoPath)) {
36961
+ const absVideoPath = resolve26(process.cwd(), videoPath);
36962
+ const absOutputPath = resolve26(process.cwd(), outputPath);
36963
+ if (!existsSync20(absVideoPath)) {
37252
36964
  return { success: false, error: `Video not found: ${absVideoPath}` };
37253
36965
  }
37254
36966
  if (!commandExists("ffmpeg")) {
@@ -37428,8 +37140,8 @@ var init_ai_edit = __esm({
37428
37140
  });
37429
37141
 
37430
37142
  // 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";
37143
+ import { resolve as resolve27, extname as extname5, basename as basename7 } from "node:path";
37144
+ import { existsSync as existsSync21 } from "node:fs";
37433
37145
  function registerEditCommands(aiCommand) {
37434
37146
  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
37147
  Examples:
@@ -37445,8 +37157,8 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37445
37157
  if (options.output) {
37446
37158
  validateOutputPath(options.output);
37447
37159
  }
37448
- const absVideoPath = resolve28(process.cwd(), videoPath);
37449
- if (!existsSync22(absVideoPath)) {
37160
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37161
+ if (!existsSync21(absVideoPath)) {
37450
37162
  exitWithError(notFoundError(absVideoPath));
37451
37163
  }
37452
37164
  if (!commandExists("ffmpeg")) {
@@ -37485,7 +37197,7 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37485
37197
  const spinner2 = ora(spinnerText).start();
37486
37198
  const result = await executeSilenceCut({
37487
37199
  videoPath: absVideoPath,
37488
- outputPath: resolve28(process.cwd(), outputPath),
37200
+ outputPath: resolve27(process.cwd(), outputPath),
37489
37201
  noiseThreshold: parseFloat(options.noise),
37490
37202
  minDuration: parseFloat(options.minDuration),
37491
37203
  padding: parseFloat(options.padding),
@@ -37553,8 +37265,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37553
37265
  if (options.output) {
37554
37266
  validateOutputPath(options.output);
37555
37267
  }
37556
- const absVideoPath = resolve28(process.cwd(), videoPath);
37557
- if (!existsSync22(absVideoPath)) {
37268
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37269
+ if (!existsSync21(absVideoPath)) {
37558
37270
  exitWithError(notFoundError(absVideoPath));
37559
37271
  }
37560
37272
  if (!commandExists("ffmpeg")) {
@@ -37588,7 +37300,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37588
37300
  const spinner2 = ora("Starting caption process...").start();
37589
37301
  const result = await executeCaption({
37590
37302
  videoPath: absVideoPath,
37591
- outputPath: resolve28(process.cwd(), outputPath),
37303
+ outputPath: resolve27(process.cwd(), outputPath),
37592
37304
  style: options.style,
37593
37305
  fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
37594
37306
  fontColor: options.color,
@@ -37634,8 +37346,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37634
37346
  if (options.output) {
37635
37347
  validateOutputPath(options.output);
37636
37348
  }
37637
- const absInputPath = resolve28(process.cwd(), inputPath);
37638
- if (!existsSync22(absInputPath)) {
37349
+ const absInputPath = resolve27(process.cwd(), inputPath);
37350
+ if (!existsSync21(absInputPath)) {
37639
37351
  exitWithError(notFoundError(absInputPath));
37640
37352
  }
37641
37353
  if (!commandExists("ffmpeg")) {
@@ -37662,7 +37374,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37662
37374
  const spinner2 = ora("Applying noise reduction...").start();
37663
37375
  const result = await executeNoiseReduce({
37664
37376
  inputPath: absInputPath,
37665
- outputPath: resolve28(process.cwd(), outputPath),
37377
+ outputPath: resolve27(process.cwd(), outputPath),
37666
37378
  strength: options.strength,
37667
37379
  noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
37668
37380
  });
@@ -37700,8 +37412,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37700
37412
  if (options.output) {
37701
37413
  validateOutputPath(options.output);
37702
37414
  }
37703
- const absVideoPath = resolve28(process.cwd(), videoPath);
37704
- if (!existsSync22(absVideoPath)) {
37415
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37416
+ if (!existsSync21(absVideoPath)) {
37705
37417
  exitWithError(notFoundError(absVideoPath));
37706
37418
  }
37707
37419
  if (!commandExists("ffmpeg")) {
@@ -37730,7 +37442,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37730
37442
  const spinner2 = ora("Applying fade effects...").start();
37731
37443
  const result = await executeFade({
37732
37444
  videoPath: absVideoPath,
37733
- outputPath: resolve28(process.cwd(), outputPath),
37445
+ outputPath: resolve27(process.cwd(), outputPath),
37734
37446
  fadeIn: parseFloat(options.fadeIn),
37735
37447
  fadeOut: parseFloat(options.fadeOut),
37736
37448
  audioOnly: options.audioOnly || false,
@@ -37775,8 +37487,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37775
37487
  if (!options.target) {
37776
37488
  exitWithError(usageError("Target language required. Use -t or --target"));
37777
37489
  }
37778
- const absSrtPath = resolve28(process.cwd(), srtPath);
37779
- if (!existsSync22(absSrtPath)) {
37490
+ const absSrtPath = resolve27(process.cwd(), srtPath);
37491
+ if (!existsSync21(absSrtPath)) {
37780
37492
  exitWithError(notFoundError(absSrtPath));
37781
37493
  }
37782
37494
  if (options.dryRun) {
@@ -37808,7 +37520,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37808
37520
  const spinner2 = ora(`Translating to ${options.target}...`).start();
37809
37521
  const result = await executeTranslateSrt({
37810
37522
  srtPath: absSrtPath,
37811
- outputPath: resolve28(process.cwd(), outputPath),
37523
+ outputPath: resolve27(process.cwd(), outputPath),
37812
37524
  targetLanguage: options.target,
37813
37525
  provider,
37814
37526
  sourceLanguage: options.source,
@@ -37851,8 +37563,8 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37851
37563
  validateOutputPath(options.output);
37852
37564
  }
37853
37565
  if (options.fillers) rejectControlChars(options.fillers);
37854
- const absVideoPath = resolve28(process.cwd(), videoPath);
37855
- if (!existsSync22(absVideoPath)) {
37566
+ const absVideoPath = resolve27(process.cwd(), videoPath);
37567
+ if (!existsSync21(absVideoPath)) {
37856
37568
  exitWithError(notFoundError(absVideoPath));
37857
37569
  }
37858
37570
  if (!commandExists("ffmpeg")) {
@@ -37887,7 +37599,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37887
37599
  const spinner2 = ora("Transcribing with word-level timestamps...").start();
37888
37600
  const result = await executeJumpCut({
37889
37601
  videoPath: absVideoPath,
37890
- outputPath: resolve28(process.cwd(), outputPath),
37602
+ outputPath: resolve27(process.cwd(), outputPath),
37891
37603
  fillers,
37892
37604
  padding: parseFloat(options.padding),
37893
37605
  language: options.language,
@@ -37952,9 +37664,9 @@ var init_ai_edit_cli = __esm({
37952
37664
  });
37953
37665
 
37954
37666
  // 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";
37667
+ import { existsSync as existsSync22 } from "node:fs";
37668
+ import { readFile as readFile18, writeFile as writeFile27, mkdir as mkdir14, rename as renameFs } from "node:fs/promises";
37669
+ import { resolve as resolve28, dirname as dirname9 } from "node:path";
37958
37670
  function detectVideoGaps(videoClips, totalDuration) {
37959
37671
  const gaps = [];
37960
37672
  const sortedClips = [...videoClips].sort((a, b) => a.startTime - b.startTime);
@@ -38010,7 +37722,7 @@ function analyzeGapFillability(gaps, videoClips, sources) {
38010
37722
  }
38011
37723
  async function uploadFrameToImgbb(framePath, imgbbApiKey) {
38012
37724
  try {
38013
- const frameBuffer = await readFile19(framePath);
37725
+ const frameBuffer = await readFile18(framePath);
38014
37726
  const frameBase64 = frameBuffer.toString("base64");
38015
37727
  const formData = new FormData();
38016
37728
  formData.append("key", imgbbApiKey);
@@ -38035,14 +37747,14 @@ async function executeFillGaps(options) {
38035
37747
  try {
38036
37748
  onProgress("Loading timeline...");
38037
37749
  const filePath = await resolveTimelineFile(options.projectPath);
38038
- if (!existsSync23(filePath)) {
37750
+ if (!existsSync22(filePath)) {
38039
37751
  return {
38040
37752
  success: false,
38041
37753
  error: `Timeline file not found: ${filePath}`,
38042
37754
  humanLines
38043
37755
  };
38044
37756
  }
38045
- const content = await readFile19(filePath, "utf-8");
37757
+ const content = await readFile18(filePath, "utf-8");
38046
37758
  const data = JSON.parse(content);
38047
37759
  const project = Project.fromJSON(data);
38048
37760
  const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
@@ -38169,8 +37881,8 @@ async function executeFillGaps(options) {
38169
37881
  };
38170
37882
  }
38171
37883
  const projectDir = dirname9(filePath);
38172
- const footageDir = options.dir ? resolve29(process.cwd(), options.dir) : resolve29(projectDir, "footage");
38173
- if (!existsSync23(footageDir)) {
37884
+ const footageDir = options.dir ? resolve28(process.cwd(), options.dir) : resolve28(projectDir, "footage");
37885
+ if (!existsSync22(footageDir)) {
38174
37886
  await mkdir14(footageDir, { recursive: true });
38175
37887
  }
38176
37888
  humanLines.push("Generating AI Videos");
@@ -38201,7 +37913,7 @@ async function executeFillGaps(options) {
38201
37913
  }
38202
37914
  onProgress("Extracting frame from preceding clip...");
38203
37915
  const frameOffset = clipBefore.sourceStartOffset + clipBefore.duration - 0.1;
38204
- const framePath = resolve29(footageDir, `frame-${gap.start.toFixed(2)}.png`);
37916
+ const framePath = resolve28(footageDir, `frame-${gap.start.toFixed(2)}.png`);
38205
37917
  try {
38206
37918
  await execSafe("ffmpeg", [
38207
37919
  "-i",
@@ -38262,10 +37974,10 @@ async function executeFillGaps(options) {
38262
37974
  continue;
38263
37975
  }
38264
37976
  const videoFileName = `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}.mp4`;
38265
- const videoPath = resolve29(footageDir, videoFileName);
37977
+ const videoPath = resolve28(footageDir, videoFileName);
38266
37978
  onProgress("Downloading generated video...");
38267
37979
  const videoBuffer = await downloadVideo(finalResult.videoUrl);
38268
- await writeFile28(videoPath, videoBuffer);
37980
+ await writeFile27(videoPath, videoBuffer);
38269
37981
  generatedDuration = finalResult.duration || parseInt(klingDuration);
38270
37982
  generatedVideos.push(videoPath);
38271
37983
  humanLines.push(` Generated: ${videoFileName} (${generatedDuration}s)`);
@@ -38274,7 +37986,7 @@ async function executeFillGaps(options) {
38274
37986
  const remainingNeeded = targetDuration - generatedDuration;
38275
37987
  const segmentDuration = remainingNeeded > 5 ? "10" : "5";
38276
37988
  onProgress(`Generating additional ${segmentDuration}s segment...`);
38277
- const lastFramePath = resolve29(
37989
+ const lastFramePath = resolve28(
38278
37990
  footageDir,
38279
37991
  `frame-extend-${gap.start.toFixed(2)}-${segmentIndex}.png`
38280
37992
  );
@@ -38330,17 +38042,17 @@ async function executeFillGaps(options) {
38330
38042
  );
38331
38043
  break;
38332
38044
  }
38333
- const segVideoPath = resolve29(
38045
+ const segVideoPath = resolve28(
38334
38046
  footageDir,
38335
38047
  `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}-seg${segmentIndex}.mp4`
38336
38048
  );
38337
38049
  const segVideoBuffer = await downloadVideo(segFinalResult.videoUrl);
38338
- await writeFile28(segVideoPath, segVideoBuffer);
38339
- const concatListPath = resolve29(footageDir, `concat-${gap.start.toFixed(2)}.txt`);
38050
+ await writeFile27(segVideoPath, segVideoBuffer);
38051
+ const concatListPath = resolve28(footageDir, `concat-${gap.start.toFixed(2)}.txt`);
38340
38052
  const concatList = generatedVideos.map((v) => `file '${v}'`).join("\n") + `
38341
38053
  file '${segVideoPath}'`;
38342
- await writeFile28(concatListPath, concatList);
38343
- const concatOutputPath = resolve29(
38054
+ await writeFile27(concatListPath, concatList);
38055
+ const concatOutputPath = resolve28(
38344
38056
  footageDir,
38345
38057
  `gap-fill-${gap.start.toFixed(2)}-${gap.end.toFixed(2)}-merged.mp4`
38346
38058
  );
@@ -38396,8 +38108,8 @@ file '${segVideoPath}'`;
38396
38108
  humanLines.push("");
38397
38109
  let outputPath = filePath;
38398
38110
  if (generatedCount > 0) {
38399
- outputPath = options.output ? resolve29(process.cwd(), options.output) : filePath;
38400
- await writeFile28(outputPath, JSON.stringify(project.toJSON(), null, 2));
38111
+ outputPath = options.output ? resolve28(process.cwd(), options.output) : filePath;
38112
+ await writeFile27(outputPath, JSON.stringify(project.toJSON(), null, 2));
38401
38113
  humanLines.push(`\u2714 Filled ${generatedCount} gap(s) with AI-generated video`);
38402
38114
  humanLines.push(`Project saved: ${outputPath}`);
38403
38115
  } else {
@@ -38493,14 +38205,14 @@ __export(edit_cmd_exports, {
38493
38205
  executeSpeedRamp: () => executeSpeedRamp,
38494
38206
  executeUpscale: () => executeUpscale
38495
38207
  });
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";
38208
+ import { resolve as resolve29, dirname as dirname10 } from "node:path";
38209
+ import { readFile as readFile19, writeFile as writeFile28, mkdir as mkdir15 } from "node:fs/promises";
38498
38210
  async function executeGrade(options) {
38499
38211
  const { videoPath, style, preset, output, analyzeOnly, apiKey } = options;
38500
38212
  try {
38501
38213
  if (!style && !preset) return { success: false, error: "Either style or preset is required" };
38502
38214
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38503
- const absPath = resolve30(process.cwd(), videoPath);
38215
+ const absPath = resolve29(process.cwd(), videoPath);
38504
38216
  let gradeResult;
38505
38217
  if (preset) {
38506
38218
  const claude = new ClaudeProvider();
@@ -38515,7 +38227,7 @@ async function executeGrade(options) {
38515
38227
  if (analyzeOnly) {
38516
38228
  return { success: true, style: preset || style, description: gradeResult.description, ffmpegFilter: gradeResult.ffmpegFilter };
38517
38229
  }
38518
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38230
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38519
38231
  await execSafe("ffmpeg", ["-i", absPath, "-vf", gradeResult.ffmpegFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38520
38232
  return { success: true, outputPath, style: preset || style, description: gradeResult.description, ffmpegFilter: gradeResult.ffmpegFilter };
38521
38233
  } catch (error) {
@@ -38530,12 +38242,12 @@ async function executeSpeedRamp(options) {
38530
38242
  const claudeKey = apiKey || process.env.ANTHROPIC_API_KEY;
38531
38243
  if (!openaiKey) return { success: false, error: "OPENAI_API_KEY required for Whisper transcription" };
38532
38244
  if (!claudeKey) return { success: false, error: "ANTHROPIC_API_KEY required for Claude analysis" };
38533
- const absPath = resolve30(process.cwd(), videoPath);
38245
+ const absPath = resolve29(process.cwd(), videoPath);
38534
38246
  const tempAudio = absPath.replace(/(\.[^.]+)$/, "-temp-audio.mp3");
38535
38247
  await execSafe("ffmpeg", ["-i", absPath, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tempAudio, "-y"]);
38536
38248
  const whisper = new WhisperProvider();
38537
38249
  await whisper.initialize({ apiKey: openaiKey });
38538
- const audioBuffer = await readFile20(tempAudio);
38250
+ const audioBuffer = await readFile19(tempAudio);
38539
38251
  const transcript = await whisper.transcribe(new Blob([audioBuffer]), language);
38540
38252
  try {
38541
38253
  const { unlink: unlink5 } = await import("node:fs/promises");
@@ -38555,7 +38267,7 @@ async function executeSpeedRamp(options) {
38555
38267
  if (speedResult.keyframes.length < 2) {
38556
38268
  return { success: false, error: "Not enough keyframes for speed ramping" };
38557
38269
  }
38558
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38270
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38559
38271
  const setpts = `setpts=${(1 / avgSpeed).toFixed(3)}*PTS`;
38560
38272
  const atempo = avgSpeed >= 0.5 && avgSpeed <= 2 ? `atempo=${avgSpeed.toFixed(3)}` : "";
38561
38273
  if (atempo) {
@@ -38572,7 +38284,7 @@ async function executeReframe(options) {
38572
38284
  const { videoPath, aspect = "9:16", output, analyzeOnly } = options;
38573
38285
  try {
38574
38286
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38575
- const absPath = resolve30(process.cwd(), videoPath);
38287
+ const absPath = resolve29(process.cwd(), videoPath);
38576
38288
  const { stdout: probeOut } = await execSafe("ffprobe", [
38577
38289
  "-v",
38578
38290
  "error",
@@ -38607,7 +38319,7 @@ async function executeReframe(options) {
38607
38319
  keyframeCount: 1
38608
38320
  };
38609
38321
  }
38610
- const outputPath = output ? resolve30(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${aspect.replace(":", "x")}$1`);
38322
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${aspect.replace(":", "x")}$1`);
38611
38323
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `crop=${cropWidth}:${cropHeight}:${cropX}:${cropY}`, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38612
38324
  return { success: true, outputPath, sourceAspect: `${sourceWidth}:${sourceHeight}`, targetAspect: aspect };
38613
38325
  } catch (error) {
@@ -38619,7 +38331,7 @@ async function executeInterpolate(options) {
38619
38331
  try {
38620
38332
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38621
38333
  if (![2, 4, 8].includes(factor)) return { success: false, error: "Factor must be 2, 4, or 8" };
38622
- const absPath = resolve30(process.cwd(), videoPath);
38334
+ const absPath = resolve29(process.cwd(), videoPath);
38623
38335
  const { stdout: fpsOut } = await execSafe("ffprobe", [
38624
38336
  "-v",
38625
38337
  "error",
@@ -38635,7 +38347,7 @@ async function executeInterpolate(options) {
38635
38347
  const originalFps = num / (den || 1);
38636
38348
  const targetFps = options.fps || originalFps * factor;
38637
38349
  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`);
38350
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
38639
38351
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
38640
38352
  return { success: true, outputPath, originalFps, targetFps, factor };
38641
38353
  } catch (error) {
@@ -38646,7 +38358,7 @@ async function executeUpscale(options) {
38646
38358
  const { videoPath, output, scale = 2, quality = "quality" } = options;
38647
38359
  try {
38648
38360
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
38649
- const absPath = resolve30(process.cwd(), videoPath);
38361
+ const absPath = resolve29(process.cwd(), videoPath);
38650
38362
  const { stdout: probeOut } = await execSafe("ffprobe", [
38651
38363
  "-v",
38652
38364
  "error",
@@ -38662,7 +38374,7 @@ async function executeUpscale(options) {
38662
38374
  const targetW = w * scale;
38663
38375
  const targetH = h * scale;
38664
38376
  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`);
38377
+ const outputPath = output ? resolve29(process.cwd(), output) : absPath.replace(/(\.[^.]+)$/, `-${scale}x$1`);
38666
38378
  await execSafe("ffmpeg", ["-i", absPath, "-vf", scaleFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38667
38379
  return { success: true, outputPath, originalRes: `${w}x${h}`, targetRes: `${targetW}x${targetH}` };
38668
38380
  } catch (error) {
@@ -38735,7 +38447,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38735
38447
  dryRun: true,
38736
38448
  data: {
38737
38449
  params: {
38738
- videoPath: resolve30(process.cwd(), videoPath),
38450
+ videoPath: resolve29(process.cwd(), videoPath),
38739
38451
  style: options.style || options.preset,
38740
38452
  analyzeOnly: options.analyzeOnly || false
38741
38453
  }
@@ -38762,8 +38474,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38762
38474
  }
38763
38475
  spinner2.succeed(source_default.green("Color grade analyzed"));
38764
38476
  if (isJsonMode()) {
38765
- const absPath2 = resolve30(process.cwd(), videoPath);
38766
- const gradeOutputPath = options.output ? resolve30(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38477
+ const absPath2 = resolve29(process.cwd(), videoPath);
38478
+ const gradeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38767
38479
  outputSuccess({
38768
38480
  command: "edit grade",
38769
38481
  startedAt,
@@ -38789,8 +38501,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38789
38501
  console.log(source_default.dim("Use without --analyze-only to apply the grade."));
38790
38502
  return;
38791
38503
  }
38792
- const absPath = resolve30(process.cwd(), videoPath);
38793
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38504
+ const absPath = resolve29(process.cwd(), videoPath);
38505
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-graded$1");
38794
38506
  spinner2.start("Applying color grade...");
38795
38507
  await execSafe("ffmpeg", ["-i", absPath, "-vf", gradeResult.ffmpegFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
38796
38508
  spinner2.succeed(source_default.green("Color grade applied"));
@@ -38820,7 +38532,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38820
38532
  dryRun: true,
38821
38533
  data: {
38822
38534
  params: {
38823
- videoPath: resolve30(process.cwd(), videoPath),
38535
+ videoPath: resolve29(process.cwd(), videoPath),
38824
38536
  texts: options.text,
38825
38537
  style: options.style,
38826
38538
  fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
@@ -38833,8 +38545,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38833
38545
  });
38834
38546
  return;
38835
38547
  }
38836
- const absPath = resolve30(process.cwd(), videoPath);
38837
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-overlay$1");
38548
+ const absPath = resolve29(process.cwd(), videoPath);
38549
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-overlay$1");
38838
38550
  const spinner2 = ora("Applying text overlays...").start();
38839
38551
  const result = await applyTextOverlays({
38840
38552
  videoPath: absPath,
@@ -38892,7 +38604,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38892
38604
  dryRun: true,
38893
38605
  data: {
38894
38606
  params: {
38895
- videoPath: resolve30(process.cwd(), videoPath),
38607
+ videoPath: resolve29(process.cwd(), videoPath),
38896
38608
  style: options.style,
38897
38609
  minSpeed: parseFloat(options.minSpeed),
38898
38610
  maxSpeed: parseFloat(options.maxSpeed),
@@ -38904,7 +38616,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38904
38616
  }
38905
38617
  const openaiApiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
38906
38618
  const claudeApiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
38907
- const absPath = resolve30(process.cwd(), videoPath);
38619
+ const absPath = resolve29(process.cwd(), videoPath);
38908
38620
  const spinner2 = ora("Extracting audio...").start();
38909
38621
  const { stdout: speedRampProbe } = await execSafe("ffprobe", [
38910
38622
  "-v",
@@ -38926,7 +38638,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38926
38638
  spinner2.text = "Transcribing audio...";
38927
38639
  const whisper = new WhisperProvider();
38928
38640
  await whisper.initialize({ apiKey: openaiApiKey });
38929
- const audioBuffer = await readFile20(tempAudio);
38641
+ const audioBuffer = await readFile19(tempAudio);
38930
38642
  const audioBlob = new Blob([audioBuffer]);
38931
38643
  const transcript = await whisper.transcribe(audioBlob, options.language);
38932
38644
  if (!transcript.segments || transcript.segments.length === 0) {
@@ -38949,7 +38661,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38949
38661
  spinner2.succeed(source_default.green(`Found ${speedResult.keyframes.length} speed keyframes`));
38950
38662
  if (isJsonMode()) {
38951
38663
  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");
38664
+ const speedRampOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38953
38665
  outputSuccess({
38954
38666
  command: "edit speed-ramp",
38955
38667
  startedAt,
@@ -38978,7 +38690,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38978
38690
  return;
38979
38691
  }
38980
38692
  spinner2.start("Applying speed ramps...");
38981
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38693
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
38982
38694
  const avgSpeed = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
38983
38695
  const setpts = `setpts=${(1 / avgSpeed).toFixed(3)}*PTS`;
38984
38696
  const atempo = avgSpeed >= 0.5 && avgSpeed <= 2 ? `atempo=${avgSpeed.toFixed(3)}` : "";
@@ -39012,7 +38724,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39012
38724
  dryRun: true,
39013
38725
  data: {
39014
38726
  params: {
39015
- videoPath: resolve30(process.cwd(), videoPath),
38727
+ videoPath: resolve29(process.cwd(), videoPath),
39016
38728
  aspect: options.aspect,
39017
38729
  focus: options.focus,
39018
38730
  analyzeOnly: options.analyzeOnly || false
@@ -39021,7 +38733,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39021
38733
  });
39022
38734
  return;
39023
38735
  }
39024
- const absPath = resolve30(process.cwd(), videoPath);
38736
+ const absPath = resolve29(process.cwd(), videoPath);
39025
38737
  const spinner2 = ora("Analyzing video...").start();
39026
38738
  const { stdout: probeOut } = await execSafe("ffprobe", [
39027
38739
  "-v",
@@ -39059,7 +38771,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39059
38771
  for (let i = 1; i <= numKeyframes && i <= 30; i++) {
39060
38772
  const framePath = `${tempDir}/frame-${i.toString().padStart(4, "0")}.jpg`;
39061
38773
  try {
39062
- const frameBuffer = await readFile20(framePath);
38774
+ const frameBuffer = await readFile19(framePath);
39063
38775
  const frameBase64 = frameBuffer.toString("base64");
39064
38776
  const result = await claude.analyzeFrameForReframe(frameBase64, options.aspect, {
39065
38777
  focusMode: options.focus,
@@ -39083,7 +38795,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39083
38795
  }
39084
38796
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
39085
38797
  if (isJsonMode()) {
39086
- const reframeOutputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
38798
+ const reframeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39087
38799
  outputSuccess({
39088
38800
  command: "edit reframe",
39089
38801
  startedAt,
@@ -39118,8 +38830,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39118
38830
  }
39119
38831
  console.log();
39120
38832
  if (options.keyframes) {
39121
- const keyframesPath = resolve30(process.cwd(), options.keyframes);
39122
- await writeFile29(keyframesPath, JSON.stringify(cropKeyframes, null, 2));
38833
+ const keyframesPath = resolve29(process.cwd(), options.keyframes);
38834
+ await writeFile28(keyframesPath, JSON.stringify(cropKeyframes, null, 2));
39123
38835
  console.log(source_default.green(`Keyframes saved to: ${keyframesPath}`));
39124
38836
  }
39125
38837
  if (options.analyzeOnly) {
@@ -39130,7 +38842,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39130
38842
  const avgCropY = Math.round(cropKeyframes.reduce((sum, kf) => sum + kf.cropY, 0) / cropKeyframes.length);
39131
38843
  const cropWidth = cropKeyframes[0]?.cropWidth || sourceWidth;
39132
38844
  const cropHeight = cropKeyframes[0]?.cropHeight || sourceHeight;
39133
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
38845
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39134
38846
  spinner2.start("Applying reframe...");
39135
38847
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `crop=${cropWidth}:${cropHeight}:${avgCropX}:${avgCropY}`, "-c:a", "copy", outputPath, "-y"], { timeout: 6e5 });
39136
38848
  spinner2.succeed(source_default.green("Reframe applied"));
@@ -39166,7 +38878,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39166
38878
  dryRun: true,
39167
38879
  data: {
39168
38880
  params: {
39169
- imagePaths: imagePaths.map((p) => resolve30(process.cwd(), p)),
38881
+ imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
39170
38882
  prompt: prompt3,
39171
38883
  provider,
39172
38884
  model: options.model,
@@ -39187,8 +38899,8 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39187
38899
  const spinner2 = ora(`Reading ${imagePaths.length} image(s)...`).start();
39188
38900
  const imageBuffers = [];
39189
38901
  for (const imagePath of imagePaths) {
39190
- const absPath = resolve30(process.cwd(), imagePath);
39191
- const buffer = await readFile20(absPath);
38902
+ const absPath = resolve29(process.cwd(), imagePath);
38903
+ const buffer = await readFile19(absPath);
39192
38904
  imageBuffers.push(buffer);
39193
38905
  }
39194
38906
  let result;
@@ -39236,16 +38948,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39236
38948
  }
39237
38949
  spinner2.succeed(source_default.green("Image edited"));
39238
38950
  const img = result.images[0];
39239
- const outputPath = resolve30(process.cwd(), options.output);
38951
+ const outputPath = resolve29(process.cwd(), options.output);
39240
38952
  const saveImage = async () => {
39241
38953
  await mkdir15(dirname10(outputPath), { recursive: true });
39242
38954
  if (img.base64) {
39243
38955
  const buffer = Buffer.from(img.base64, "base64");
39244
- await writeFile29(outputPath, buffer);
38956
+ await writeFile28(outputPath, buffer);
39245
38957
  } else if (img.url) {
39246
38958
  const resp = await fetch(img.url);
39247
38959
  const arrayBuf = await resp.arrayBuffer();
39248
- await writeFile29(outputPath, Buffer.from(arrayBuf));
38960
+ await writeFile28(outputPath, Buffer.from(arrayBuf));
39249
38961
  }
39250
38962
  };
39251
38963
  const resultModel = result.model;
@@ -39278,7 +38990,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39278
38990
  if (options.output) {
39279
38991
  validateOutputPath(options.output);
39280
38992
  }
39281
- const absPath = resolve30(process.cwd(), videoPath);
38993
+ const absPath = resolve29(process.cwd(), videoPath);
39282
38994
  const factor = parseInt(options.factor);
39283
38995
  if (![2, 4, 8].includes(factor)) {
39284
38996
  exitWithError(usageError("Factor must be 2, 4, or 8"));
@@ -39299,7 +39011,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39299
39011
  });
39300
39012
  return;
39301
39013
  }
39302
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
39014
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
39303
39015
  const spinner2 = ora(`Creating ${factor}x slow motion...`).start();
39304
39016
  try {
39305
39017
  const { stdout: fpsOut } = await execSafe("ffprobe", [
@@ -39360,7 +39072,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39360
39072
  if (options.output) {
39361
39073
  validateOutputPath(options.output);
39362
39074
  }
39363
- const absPath = resolve30(process.cwd(), videoPath);
39075
+ const absPath = resolve29(process.cwd(), videoPath);
39364
39076
  const scale = parseInt(options.scale);
39365
39077
  if (scale !== 2 && scale !== 4) {
39366
39078
  exitWithError(usageError("Scale must be 2 or 4"));
@@ -39382,7 +39094,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39382
39094
  return;
39383
39095
  }
39384
39096
  if (options.ffmpeg) {
39385
- const outputPath = options.output ? resolve30(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-upscaled-${scale}x$1`);
39097
+ const outputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-upscaled-${scale}x$1`);
39386
39098
  const spinner3 = ora(`Upscaling video with FFmpeg (${scale}x)...`).start();
39387
39099
  try {
39388
39100
  const { stdout: probeOut } = await execSafe("ffprobe", [
@@ -39463,9 +39175,9 @@ __export(ai_analyze_exports, {
39463
39175
  executeGeminiVideo: () => executeGeminiVideo,
39464
39176
  registerAnalyzeCommands: () => registerAnalyzeCommands
39465
39177
  });
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";
39178
+ import { readFile as readFile20 } from "node:fs/promises";
39179
+ import { extname as extname6, resolve as resolve30 } from "node:path";
39180
+ import { existsSync as existsSync23 } from "node:fs";
39469
39181
  async function executeGeminiVideo(options) {
39470
39182
  try {
39471
39183
  const apiKey = await getApiKey("GOOGLE_API_KEY", "Google");
@@ -39483,11 +39195,11 @@ async function executeGeminiVideo(options) {
39483
39195
  if (isYouTube) {
39484
39196
  videoData = options.source;
39485
39197
  } else {
39486
- const absPath = resolve31(process.cwd(), options.source);
39487
- if (!existsSync24(absPath)) {
39198
+ const absPath = resolve30(process.cwd(), options.source);
39199
+ if (!existsSync23(absPath)) {
39488
39200
  return { success: false, error: `File not found: ${absPath}` };
39489
39201
  }
39490
- videoData = await readFile21(absPath);
39202
+ videoData = await readFile20(absPath);
39491
39203
  }
39492
39204
  const gemini = new GeminiProvider();
39493
39205
  await gemini.initialize({ apiKey });
@@ -39556,11 +39268,11 @@ async function executeAnalyze(options) {
39556
39268
  }
39557
39269
  imageBuffer = Buffer.from(await response.arrayBuffer());
39558
39270
  } else {
39559
- const absPath = resolve31(process.cwd(), source);
39560
- if (!existsSync24(absPath)) {
39271
+ const absPath = resolve30(process.cwd(), source);
39272
+ if (!existsSync23(absPath)) {
39561
39273
  return { success: false, error: `File not found: ${absPath}` };
39562
39274
  }
39563
- imageBuffer = await readFile21(absPath);
39275
+ imageBuffer = await readFile20(absPath);
39564
39276
  }
39565
39277
  const result2 = await gemini.analyzeImage(imageBuffer, options.prompt, {
39566
39278
  model: modelId,
@@ -39591,11 +39303,11 @@ async function executeAnalyze(options) {
39591
39303
  }
39592
39304
  videoData = Buffer.from(await response.arrayBuffer());
39593
39305
  } else {
39594
- const absPath = resolve31(process.cwd(), source);
39595
- if (!existsSync24(absPath)) {
39306
+ const absPath = resolve30(process.cwd(), source);
39307
+ if (!existsSync23(absPath)) {
39596
39308
  return { success: false, error: `File not found: ${absPath}` };
39597
39309
  }
39598
- videoData = await readFile21(absPath);
39310
+ videoData = await readFile20(absPath);
39599
39311
  }
39600
39312
  const result = await gemini.analyzeVideo(videoData, options.prompt, {
39601
39313
  model: modelId,
@@ -39729,9 +39441,9 @@ __export(ai_review_exports, {
39729
39441
  executeReview: () => executeReview,
39730
39442
  registerReviewCommand: () => registerReviewCommand
39731
39443
  });
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";
39444
+ import { readFile as readFile21, rename as rename2 } from "node:fs/promises";
39445
+ import { resolve as resolve31 } from "node:path";
39446
+ import { existsSync as existsSync24 } from "node:fs";
39735
39447
  function parseReviewFeedback(response) {
39736
39448
  let cleaned = response.trim();
39737
39449
  if (cleaned.startsWith("```json")) {
@@ -39755,8 +39467,8 @@ function parseReviewFeedback(response) {
39755
39467
  }
39756
39468
  async function executeReview(options) {
39757
39469
  const { videoPath, storyboardPath, autoApply = false, verify = false, model = "flash" } = options;
39758
- const absVideoPath = resolve32(process.cwd(), videoPath);
39759
- if (!existsSync25(absVideoPath)) {
39470
+ const absVideoPath = resolve31(process.cwd(), videoPath);
39471
+ if (!existsSync24(absVideoPath)) {
39760
39472
  return { success: false, error: `Video not found: ${absVideoPath}` };
39761
39473
  }
39762
39474
  const apiKey = process.env.GOOGLE_API_KEY || await getApiKey("GOOGLE_API_KEY", "Google");
@@ -39765,9 +39477,9 @@ async function executeReview(options) {
39765
39477
  }
39766
39478
  let storyboardContext = "";
39767
39479
  if (storyboardPath) {
39768
- const absStoryboardPath = resolve32(process.cwd(), storyboardPath);
39769
- if (existsSync25(absStoryboardPath)) {
39770
- const content = await readFile22(absStoryboardPath, "utf-8");
39480
+ const absStoryboardPath = resolve31(process.cwd(), storyboardPath);
39481
+ if (existsSync24(absStoryboardPath)) {
39482
+ const content = await readFile21(absStoryboardPath, "utf-8");
39771
39483
  storyboardContext = `
39772
39484
 
39773
39485
  Original storyboard for reference:
@@ -39800,7 +39512,7 @@ ${content}`;
39800
39512
  Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFixable. Be specific and practical.${storyboardContext}`;
39801
39513
  const gemini = new GeminiProvider();
39802
39514
  await gemini.initialize({ apiKey });
39803
- const videoData = await readFile22(absVideoPath);
39515
+ const videoData = await readFile21(absVideoPath);
39804
39516
  const analysisResult = await gemini.analyzeVideo(videoData, reviewPrompt, {
39805
39517
  model: modelId
39806
39518
  });
@@ -39821,7 +39533,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39821
39533
  };
39822
39534
  if (autoApply && feedback.autoFixable.length > 0) {
39823
39535
  let currentInput = absVideoPath;
39824
- const outputBase = options.outputPath ? resolve32(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
39536
+ const outputBase = options.outputPath ? resolve31(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
39825
39537
  for (const fix of feedback.autoFixable) {
39826
39538
  if (fix.type === "color_grade" && fix.ffmpegFilter) {
39827
39539
  try {
@@ -39838,7 +39550,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39838
39550
  if (currentInput !== absVideoPath) {
39839
39551
  const finalOutput = outputBase;
39840
39552
  try {
39841
- await rename3(currentInput, finalOutput);
39553
+ await rename2(currentInput, finalOutput);
39842
39554
  result.outputPath = finalOutput;
39843
39555
  } catch {
39844
39556
  result.outputPath = currentInput;
@@ -39846,7 +39558,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39846
39558
  }
39847
39559
  }
39848
39560
  if (verify && result.outputPath) {
39849
- const verifyVideoData = await readFile22(result.outputPath);
39561
+ const verifyVideoData = await readFile21(result.outputPath);
39850
39562
  const verifyResult = await gemini.analyzeVideo(
39851
39563
  verifyVideoData,
39852
39564
  'Rate this video overall quality on a scale of 1-10. Return ONLY a JSON object: {"score": <number>}',
@@ -39905,73 +39617,657 @@ function registerReviewCommand(aiCommand) {
39905
39617
  spinner2.fail(result.error || "Video review failed");
39906
39618
  exitWithError(apiError(result.error || "Video review failed", true));
39907
39619
  }
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}`));
39620
+ spinner2.succeed(source_default.green("Video review complete"));
39621
+ console.log();
39622
+ const fb = result.feedback;
39623
+ console.log(source_default.bold.cyan("Video Review"));
39624
+ console.log(source_default.dim("\u2500".repeat(60)));
39625
+ 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`);
39626
+ console.log();
39627
+ const categories = [
39628
+ ["Pacing", fb.categories.pacing],
39629
+ ["Color", fb.categories.color],
39630
+ ["Text Readability", fb.categories.textReadability],
39631
+ ["Audio-Visual Sync", fb.categories.audioVisualSync],
39632
+ ["Composition", fb.categories.composition]
39633
+ ];
39634
+ for (const [name, cat] of categories) {
39635
+ const scoreColor = cat.score >= 7 ? source_default.green : cat.score >= 5 ? source_default.yellow : source_default.red;
39636
+ const fixable = cat.fixable ? source_default.dim(" [fixable]") : "";
39637
+ console.log(` ${name.padEnd(20)} ${scoreColor(String(cat.score).padStart(2))}/10${fixable}`);
39638
+ if (cat.issues.length > 0) {
39639
+ for (const issue of cat.issues) {
39640
+ console.log(source_default.dim(` - ${issue}`));
39641
+ }
39642
+ }
39643
+ }
39644
+ if (result.appliedFixes && result.appliedFixes.length > 0) {
39645
+ console.log();
39646
+ console.log(source_default.bold.green("Applied Fixes:"));
39647
+ for (const fix of result.appliedFixes) {
39648
+ console.log(source_default.green(` + ${fix}`));
39649
+ }
39650
+ if (result.outputPath) {
39651
+ console.log(source_default.green(` Output: ${result.outputPath}`));
39652
+ }
39653
+ }
39654
+ if (result.verificationScore !== void 0) {
39655
+ console.log();
39656
+ console.log(source_default.bold(`Verification Score: ${result.verificationScore}/10`));
39657
+ }
39658
+ if (fb.recommendations.length > 0) {
39659
+ console.log();
39660
+ console.log(source_default.bold("Recommendations:"));
39661
+ for (const rec of fb.recommendations) {
39662
+ console.log(source_default.dim(` * ${rec}`));
39663
+ }
39664
+ }
39665
+ console.log();
39666
+ } catch (error) {
39667
+ exitWithError(generalError(error instanceof Error ? error.message : "Video review failed"));
39668
+ }
39669
+ });
39670
+ }
39671
+ var init_ai_review = __esm({
39672
+ "src/commands/ai-review.ts"() {
39673
+ "use strict";
39674
+ init_source();
39675
+ init_ora();
39676
+ init_dist2();
39677
+ init_api_key();
39678
+ init_exec_safe();
39679
+ init_output();
39680
+ init_validate();
39681
+ }
39682
+ });
39683
+
39684
+ // src/commands/_shared/video-providers.ts
39685
+ async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
39686
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39687
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39688
+ try {
39689
+ const result = await grok.generateVideo(prompt3, {
39690
+ prompt: prompt3,
39691
+ duration: options.duration,
39692
+ aspectRatio: options.aspectRatio,
39693
+ referenceImage: options.referenceImage
39694
+ });
39695
+ if (result.status !== "failed" && result.id) {
39696
+ return { requestId: result.id };
39697
+ }
39698
+ const providerErr = result.error || "Grok returned failed status";
39699
+ if (attempt < maxRetries) {
39700
+ onProgress?.(
39701
+ `\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`
39702
+ );
39703
+ await sleep(RETRY_DELAY_MS);
39704
+ } else {
39705
+ console.error(source_default.dim(`
39706
+ [Grok error: ${providerErr}]`));
39707
+ }
39708
+ } catch (err) {
39709
+ const errMsg = err instanceof Error ? err.message : String(err);
39710
+ if (attempt < maxRetries) {
39711
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39712
+ await sleep(RETRY_DELAY_MS);
39713
+ } else {
39714
+ console.error(source_default.dim(`
39715
+ [Grok error: ${errMsg}]`));
39716
+ }
39717
+ }
39718
+ }
39719
+ return null;
39720
+ }
39721
+ async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
39722
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39723
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39724
+ try {
39725
+ const result = await kling.generateVideo(prompt3, {
39726
+ prompt: prompt3,
39727
+ // Pass reference image (base64 or URL) - KlingProvider handles v1.5 fallback for base64
39728
+ referenceImage: options.referenceImage,
39729
+ duration: options.duration,
39730
+ aspectRatio: options.aspectRatio,
39731
+ mode: "std"
39732
+ // std mode for faster generation
39733
+ });
39734
+ if (result.status !== "failed" && result.id) {
39735
+ return {
39736
+ taskId: result.id,
39737
+ type: options.referenceImage ? "image2video" : "text2video"
39738
+ };
39739
+ }
39740
+ const providerErr = result.error || "Kling returned failed status";
39741
+ if (attempt < maxRetries) {
39742
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39743
+ await sleep(RETRY_DELAY_MS);
39744
+ } else {
39745
+ console.error(source_default.dim(`
39746
+ [Kling error: ${providerErr}]`));
39747
+ }
39748
+ } catch (err) {
39749
+ const errMsg = err instanceof Error ? err.message : String(err);
39750
+ if (attempt < maxRetries) {
39751
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39752
+ await sleep(RETRY_DELAY_MS);
39753
+ } else {
39754
+ console.error(source_default.dim(`
39755
+ [Kling error: ${errMsg}]`));
39756
+ }
39757
+ }
39758
+ }
39759
+ return null;
39760
+ }
39761
+ async function generateVideoWithRetryRunway(runway, segment, referenceImage, options, maxRetries, onProgress) {
39762
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39763
+ try {
39764
+ const result = await runway.generateVideo(segment.visuals, {
39765
+ prompt: segment.visuals,
39766
+ referenceImage,
39767
+ duration: options.duration,
39768
+ aspectRatio: options.aspectRatio
39769
+ });
39770
+ if (result.status !== "failed" && result.id) {
39771
+ return { taskId: result.id };
39772
+ }
39773
+ const providerErr = result.error || "Runway returned failed status";
39774
+ if (attempt < maxRetries) {
39775
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39776
+ await sleep(RETRY_DELAY_MS);
39777
+ } else {
39778
+ console.error(source_default.dim(`
39779
+ [Runway error: ${providerErr}]`));
39780
+ }
39781
+ } catch (err) {
39782
+ const errMsg = err instanceof Error ? err.message : String(err);
39783
+ if (attempt < maxRetries) {
39784
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39785
+ await sleep(RETRY_DELAY_MS);
39786
+ } else {
39787
+ console.error(source_default.dim(`
39788
+ [Runway error: ${errMsg}]`));
39789
+ }
39790
+ }
39791
+ }
39792
+ return null;
39793
+ }
39794
+ async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, onProgress) {
39795
+ const prompt3 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39796
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
39797
+ try {
39798
+ const result = await gemini.generateVideo(prompt3, {
39799
+ prompt: prompt3,
39800
+ referenceImage: options.referenceImage,
39801
+ duration: options.duration,
39802
+ aspectRatio: options.aspectRatio,
39803
+ model: "veo-3.1-fast-generate-preview"
39804
+ });
39805
+ if (result.status !== "failed" && result.id) {
39806
+ return { operationName: result.id };
39807
+ }
39808
+ const providerErr = result.error || "Veo returned failed status";
39809
+ if (attempt < maxRetries) {
39810
+ onProgress?.(`\u26A0 ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39811
+ await sleep(RETRY_DELAY_MS);
39812
+ } else {
39813
+ console.error(source_default.dim(`
39814
+ [Veo error: ${providerErr}]`));
39815
+ }
39816
+ } catch (err) {
39817
+ const errMsg = err instanceof Error ? err.message : String(err);
39818
+ if (attempt < maxRetries) {
39819
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
39820
+ await sleep(RETRY_DELAY_MS);
39821
+ } else {
39822
+ console.error(source_default.dim(`
39823
+ [Veo error: ${errMsg}]`));
39824
+ }
39825
+ }
39826
+ }
39827
+ return null;
39828
+ }
39829
+ var init_video_providers = __esm({
39830
+ "src/commands/_shared/video-providers.ts"() {
39831
+ "use strict";
39832
+ init_source();
39833
+ init_video_utils();
39834
+ }
39835
+ });
39836
+
39837
+ // src/commands/ai-script-pipeline.ts
39838
+ import { readFile as readFile24, writeFile as writeFile31, unlink as unlink2, rename as rename3 } from "node:fs/promises";
39839
+ import { resolve as resolve34, extname as extname8 } from "node:path";
39840
+ import { existsSync as existsSync26 } from "node:fs";
39841
+ async function executeRegenerateScene(options) {
39842
+ const result = {
39843
+ success: false,
39844
+ regeneratedScenes: [],
39845
+ failedScenes: []
39846
+ };
39847
+ try {
39848
+ const outputDir = resolve34(process.cwd(), options.projectDir);
39849
+ if (!existsSync26(outputDir)) {
39850
+ return { ...result, error: `Project directory not found: ${outputDir}` };
39851
+ }
39852
+ const yamlPath = resolve34(outputDir, "storyboard.yaml");
39853
+ const jsonPath = resolve34(outputDir, "storyboard.json");
39854
+ const storyboardPath = existsSync26(yamlPath) ? yamlPath : existsSync26(jsonPath) ? jsonPath : null;
39855
+ if (!storyboardPath) {
39856
+ return { ...result, error: `Storyboard not found in: ${outputDir} (expected storyboard.yaml or storyboard.json)` };
39857
+ }
39858
+ const storyboardContent = await readFile24(storyboardPath, "utf-8");
39859
+ const segments = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.parse)(storyboardContent).scenes : JSON.parse(storyboardContent);
39860
+ for (const sceneNum of options.scenes) {
39861
+ if (sceneNum < 1 || sceneNum > segments.length) {
39862
+ return { ...result, error: `Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.` };
39863
+ }
39864
+ }
39865
+ const regenerateVideo = options.videoOnly || !options.narrationOnly && !options.imageOnly;
39866
+ const regenerateNarration = options.narrationOnly || !options.videoOnly && !options.imageOnly;
39867
+ const regenerateImage = options.imageOnly || !options.videoOnly && !options.narrationOnly;
39868
+ let videoApiKey;
39869
+ if (regenerateVideo) {
39870
+ const generatorKeyMap = {
39871
+ grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
39872
+ kling: { envVar: "KLING_API_KEY", name: "Kling" },
39873
+ runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
39874
+ veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" }
39875
+ };
39876
+ const generator = options.generator || "grok";
39877
+ const genInfo = generatorKeyMap[generator];
39878
+ if (!genInfo) {
39879
+ return { ...result, error: `Invalid generator: ${generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
39880
+ }
39881
+ videoApiKey = await getApiKey(genInfo.envVar, genInfo.name) ?? void 0;
39882
+ if (!videoApiKey) {
39883
+ return { ...result, error: `${genInfo.name} API key required. Run 'vibe setup' or set ${genInfo.envVar} in .env` };
39884
+ }
39885
+ }
39886
+ let imageApiKey;
39887
+ if (regenerateImage) {
39888
+ const imageProvider = options.imageProvider || "openai";
39889
+ const imageKeyMap = {
39890
+ openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
39891
+ gemini: { envVar: "GOOGLE_API_KEY", name: "Google" },
39892
+ grok: { envVar: "XAI_API_KEY", name: "xAI" }
39893
+ };
39894
+ const info = imageKeyMap[imageProvider];
39895
+ if (!info) {
39896
+ return { ...result, error: `Invalid imageProvider: ${imageProvider}` };
39897
+ }
39898
+ imageApiKey = await getApiKey(info.envVar, info.name) ?? void 0;
39899
+ if (!imageApiKey) {
39900
+ return { ...result, error: `${info.name} API key required. Run 'vibe setup' or set ${info.envVar} in .env` };
39901
+ }
39902
+ }
39903
+ let elevenlabsApiKey;
39904
+ if (regenerateNarration) {
39905
+ elevenlabsApiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs") ?? void 0;
39906
+ if (!elevenlabsApiKey) {
39907
+ return { ...result, error: "ElevenLabs API key required. Run 'vibe setup' or set ELEVENLABS_API_KEY in .env" };
39908
+ }
39909
+ }
39910
+ let storyboardMutated = false;
39911
+ for (const sceneNum of options.scenes) {
39912
+ const segment = segments[sceneNum - 1];
39913
+ const narrationPath = resolve34(outputDir, `narration-${sceneNum}.mp3`);
39914
+ const imagePath = resolve34(outputDir, `scene-${sceneNum}.png`);
39915
+ const videoPath = resolve34(outputDir, `scene-${sceneNum}.mp4`);
39916
+ let sceneFailed = false;
39917
+ if (regenerateNarration && elevenlabsApiKey) {
39918
+ options.onProgress?.(`Scene ${sceneNum}: regenerating narration...`);
39919
+ const elevenlabs = new ElevenLabsProvider();
39920
+ await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
39921
+ const narrationText = segment.narration || segment.description;
39922
+ const ttsResult = await elevenlabs.textToSpeech(narrationText, {
39923
+ voiceId: options.voice
39924
+ });
39925
+ if (ttsResult.success && ttsResult.audioBuffer) {
39926
+ await writeFile31(narrationPath, ttsResult.audioBuffer);
39927
+ segment.duration = await getAudioDuration(narrationPath);
39928
+ storyboardMutated = true;
39929
+ } else {
39930
+ sceneFailed = true;
39931
+ }
39932
+ }
39933
+ if (!sceneFailed && regenerateImage && imageApiKey) {
39934
+ options.onProgress?.(`Scene ${sceneNum}: regenerating image...`);
39935
+ const imageProvider = options.imageProvider || "openai";
39936
+ const characterDesc = segment.characterDescription || segments[0]?.characterDescription;
39937
+ let imagePrompt = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
39938
+ if (characterDesc) {
39939
+ imagePrompt = `${imagePrompt}
39940
+
39941
+ IMPORTANT - Character appearance must match exactly: ${characterDesc}`;
39942
+ }
39943
+ let referenceImageBuffer;
39944
+ const refSceneNum = options.referenceScene;
39945
+ if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
39946
+ const refImagePath = resolve34(outputDir, `scene-${refSceneNum}.png`);
39947
+ if (existsSync26(refImagePath)) {
39948
+ referenceImageBuffer = await readFile24(refImagePath);
39949
+ }
39950
+ } else if (!refSceneNum) {
39951
+ for (let i = 1; i <= segments.length; i++) {
39952
+ if (i !== sceneNum) {
39953
+ const otherImagePath = resolve34(outputDir, `scene-${i}.png`);
39954
+ if (existsSync26(otherImagePath)) {
39955
+ referenceImageBuffer = await readFile24(otherImagePath);
39956
+ break;
39957
+ }
39958
+ }
39929
39959
  }
39930
39960
  }
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}`));
39961
+ const dalleImageSizes = {
39962
+ "16:9": "1536x1024",
39963
+ "9:16": "1024x1536",
39964
+ "1:1": "1024x1024"
39965
+ };
39966
+ let imageBuffer;
39967
+ let imageUrl;
39968
+ if (imageProvider === "openai") {
39969
+ const openaiImage = new OpenAIImageProvider();
39970
+ await openaiImage.initialize({ apiKey: imageApiKey });
39971
+ const imageResult = await openaiImage.generateImage(imagePrompt, {
39972
+ size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
39973
+ quality: "standard"
39974
+ });
39975
+ if (imageResult.success && imageResult.images?.[0]) {
39976
+ const img = imageResult.images[0];
39977
+ if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
39978
+ else if (img.url) imageUrl = img.url;
39979
+ }
39980
+ } else if (imageProvider === "gemini") {
39981
+ const gemini = new GeminiProvider();
39982
+ await gemini.initialize({ apiKey: imageApiKey });
39983
+ if (referenceImageBuffer) {
39984
+ const simplifiedVisuals = segment.visuals.split(/[,.]/).find(
39985
+ (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")
39986
+ ) || segment.visuals.split(".")[0];
39987
+ const editPrompt = `Generate a new image showing the SAME SINGLE person from the reference image in a new scene.
39988
+
39989
+ REFERENCE: Look at the person in the reference image - their face, hair, build, and overall appearance.
39990
+
39991
+ NEW SCENE: ${simplifiedVisuals}
39992
+
39993
+ CRITICAL RULES:
39994
+ 1. Show ONLY ONE person - the exact same individual from the reference image
39995
+ 2. The person must have the IDENTICAL face, hair style, and body type
39996
+ 3. Do NOT show multiple people or duplicate the character
39997
+ 4. Create a single moment in time, one pose, one action
39998
+ 5. Match the art style and quality of the reference image
39999
+
40000
+ Generate the single-person scene image now.`;
40001
+ const imageResult = await gemini.editImage([referenceImageBuffer], editPrompt, {
40002
+ aspectRatio: options.aspectRatio || "16:9"
40003
+ });
40004
+ if (imageResult.success && imageResult.images?.[0]?.base64) {
40005
+ imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
40006
+ }
40007
+ } else {
40008
+ const imageResult = await gemini.generateImage(imagePrompt, {
40009
+ aspectRatio: options.aspectRatio || "16:9"
40010
+ });
40011
+ if (imageResult.success && imageResult.images?.[0]?.base64) {
40012
+ imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
40013
+ }
40014
+ }
40015
+ } else if (imageProvider === "grok") {
40016
+ const grok = new GrokProvider();
40017
+ await grok.initialize({ apiKey: imageApiKey });
40018
+ const imageResult = await grok.generateImage(imagePrompt, {
40019
+ aspectRatio: options.aspectRatio || "16:9"
40020
+ });
40021
+ if (imageResult.success && imageResult.images?.[0]) {
40022
+ const img = imageResult.images[0];
40023
+ if (img.base64) imageBuffer = Buffer.from(img.base64, "base64");
40024
+ else if (img.url) imageUrl = img.url;
40025
+ }
39937
40026
  }
39938
- if (result.outputPath) {
39939
- console.log(source_default.green(` Output: ${result.outputPath}`));
40027
+ if (imageBuffer) {
40028
+ await writeFile31(imagePath, imageBuffer);
40029
+ } else if (imageUrl) {
40030
+ const response = await fetch(imageUrl);
40031
+ const buffer = Buffer.from(await response.arrayBuffer());
40032
+ await writeFile31(imagePath, buffer);
40033
+ } else {
40034
+ sceneFailed = true;
39940
40035
  }
39941
40036
  }
39942
- if (result.verificationScore !== void 0) {
39943
- console.log();
39944
- console.log(source_default.bold(`Verification Score: ${result.verificationScore}/10`));
40037
+ if (sceneFailed) {
40038
+ result.failedScenes.push(sceneNum);
40039
+ continue;
39945
40040
  }
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}`));
40041
+ if (regenerateVideo && videoApiKey) {
40042
+ options.onProgress?.(`Scene ${sceneNum}: regenerating video (${options.generator || "grok"})...`);
40043
+ if (!existsSync26(imagePath)) {
40044
+ result.failedScenes.push(sceneNum);
40045
+ continue;
40046
+ }
40047
+ const imageBuffer = await readFile24(imagePath);
40048
+ const videoDuration = segment.duration > 5 ? 10 : 5;
40049
+ const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
40050
+ if (options.generator === "grok") {
40051
+ const grok = new GrokProvider();
40052
+ await grok.initialize({ apiKey: videoApiKey });
40053
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40054
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40055
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40056
+ const grokDuration = Math.min(15, Math.max(1, segment.duration));
40057
+ const taskResult = await generateVideoWithRetryGrok(
40058
+ grok,
40059
+ segment,
40060
+ {
40061
+ duration: grokDuration,
40062
+ aspectRatio: options.aspectRatio || "16:9",
40063
+ referenceImage
40064
+ },
40065
+ maxRetries
40066
+ );
40067
+ if (taskResult) {
40068
+ try {
40069
+ const waitResult = await grok.waitForCompletion(taskResult.requestId, void 0, 3e5);
40070
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40071
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40072
+ await writeFile31(videoPath, buffer);
40073
+ const targetDuration = segment.duration;
40074
+ const actualVideoDuration = await getVideoDuration(videoPath);
40075
+ if (actualVideoDuration < targetDuration - 0.1) {
40076
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40077
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40078
+ await unlink2(videoPath);
40079
+ await rename3(extendedPath, videoPath);
40080
+ }
40081
+ result.regeneratedScenes.push(sceneNum);
40082
+ } else {
40083
+ logSceneFailure("Grok", `scene ${sceneNum}`, waitResult);
40084
+ result.failedScenes.push(sceneNum);
40085
+ }
40086
+ } catch (err) {
40087
+ logSceneFailure("Grok", `scene ${sceneNum}`, err);
40088
+ result.failedScenes.push(sceneNum);
40089
+ }
40090
+ } else {
40091
+ result.failedScenes.push(sceneNum);
40092
+ }
40093
+ } else if (options.generator === "veo") {
40094
+ const veo = new GeminiProvider();
40095
+ await veo.initialize({ apiKey: videoApiKey });
40096
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40097
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40098
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40099
+ const veoDuration = segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4;
40100
+ const taskResult = await generateVideoWithRetryVeo(
40101
+ veo,
40102
+ segment,
40103
+ {
40104
+ duration: veoDuration,
40105
+ aspectRatio: options.aspectRatio || "16:9",
40106
+ referenceImage
40107
+ },
40108
+ maxRetries
40109
+ );
40110
+ if (taskResult) {
40111
+ try {
40112
+ const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, void 0, 3e5);
40113
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40114
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40115
+ await writeFile31(videoPath, buffer);
40116
+ const targetDuration = segment.duration;
40117
+ const actualVideoDuration = await getVideoDuration(videoPath);
40118
+ if (actualVideoDuration < targetDuration - 0.1) {
40119
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40120
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40121
+ await unlink2(videoPath);
40122
+ await rename3(extendedPath, videoPath);
40123
+ }
40124
+ result.regeneratedScenes.push(sceneNum);
40125
+ } else {
40126
+ logSceneFailure("Veo", `scene ${sceneNum}`, waitResult);
40127
+ result.failedScenes.push(sceneNum);
40128
+ }
40129
+ } catch (err) {
40130
+ logSceneFailure("Veo", `scene ${sceneNum}`, err);
40131
+ result.failedScenes.push(sceneNum);
40132
+ }
40133
+ } else {
40134
+ result.failedScenes.push(sceneNum);
40135
+ }
40136
+ } else if (options.generator === "kling" || !options.generator) {
40137
+ const kling = new KlingProvider();
40138
+ await kling.initialize({ apiKey: videoApiKey });
40139
+ if (!kling.isConfigured()) {
40140
+ result.failedScenes.push(sceneNum);
40141
+ continue;
40142
+ }
40143
+ const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
40144
+ let imageUrl;
40145
+ if (imgbbApiKey) {
40146
+ const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
40147
+ if (uploadResult.success && uploadResult.url) {
40148
+ imageUrl = uploadResult.url;
40149
+ }
40150
+ }
40151
+ const taskResult = await generateVideoWithRetryKling(
40152
+ kling,
40153
+ segment,
40154
+ {
40155
+ duration: videoDuration,
40156
+ aspectRatio: options.aspectRatio || "16:9",
40157
+ referenceImage: imageUrl
40158
+ },
40159
+ maxRetries
40160
+ );
40161
+ if (taskResult) {
40162
+ try {
40163
+ const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, void 0, 6e5);
40164
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40165
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40166
+ await writeFile31(videoPath, buffer);
40167
+ await extendVideoToTarget(
40168
+ videoPath,
40169
+ segment.duration,
40170
+ outputDir,
40171
+ `Scene ${sceneNum}`,
40172
+ {
40173
+ kling,
40174
+ videoId: waitResult.videoId,
40175
+ onProgress: options.onProgress
40176
+ }
40177
+ );
40178
+ result.regeneratedScenes.push(sceneNum);
40179
+ } else {
40180
+ logSceneFailure("Kling", `scene ${sceneNum}`, waitResult);
40181
+ result.failedScenes.push(sceneNum);
40182
+ }
40183
+ } catch (err) {
40184
+ logSceneFailure("Kling", `scene ${sceneNum}`, err);
40185
+ result.failedScenes.push(sceneNum);
40186
+ }
40187
+ } else {
40188
+ result.failedScenes.push(sceneNum);
40189
+ }
40190
+ } else {
40191
+ const runway = new RunwayProvider();
40192
+ await runway.initialize({ apiKey: videoApiKey });
40193
+ const ext = extname8(imagePath).toLowerCase().slice(1);
40194
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
40195
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
40196
+ const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio || "16:9";
40197
+ const taskResult = await generateVideoWithRetryRunway(
40198
+ runway,
40199
+ segment,
40200
+ referenceImage,
40201
+ { duration: videoDuration, aspectRatio },
40202
+ maxRetries
40203
+ );
40204
+ if (taskResult) {
40205
+ try {
40206
+ const waitResult = await runway.waitForCompletion(taskResult.taskId, void 0, 3e5);
40207
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
40208
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
40209
+ await writeFile31(videoPath, buffer);
40210
+ const targetDuration = segment.duration;
40211
+ const actualVideoDuration = await getVideoDuration(videoPath);
40212
+ if (actualVideoDuration < targetDuration - 0.1) {
40213
+ const extendedPath = resolve34(outputDir, `scene-${sceneNum}-extended.mp4`);
40214
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
40215
+ await unlink2(videoPath);
40216
+ await rename3(extendedPath, videoPath);
40217
+ }
40218
+ result.regeneratedScenes.push(sceneNum);
40219
+ } else {
40220
+ logSceneFailure("Runway", `scene ${sceneNum}`, waitResult);
40221
+ result.failedScenes.push(sceneNum);
40222
+ }
40223
+ } catch (err) {
40224
+ logSceneFailure("Runway", `scene ${sceneNum}`, err);
40225
+ result.failedScenes.push(sceneNum);
40226
+ }
40227
+ } else {
40228
+ result.failedScenes.push(sceneNum);
40229
+ }
39951
40230
  }
40231
+ } else if (!sceneFailed) {
40232
+ result.regeneratedScenes.push(sceneNum);
39952
40233
  }
39953
- console.log();
39954
- } catch (error) {
39955
- exitWithError(generalError(error instanceof Error ? error.message : "Video review failed"));
39956
40234
  }
39957
- });
40235
+ if (storyboardMutated) {
40236
+ let currentTime = 0;
40237
+ for (const segment of segments) {
40238
+ segment.startTime = currentTime;
40239
+ currentTime += segment.duration;
40240
+ }
40241
+ const serialized = storyboardPath.endsWith(".yaml") ? (0, import_yaml5.stringify)({ scenes: segments }, { indent: 2 }) : JSON.stringify(segments, null, 2);
40242
+ await writeFile31(storyboardPath, serialized, "utf-8");
40243
+ }
40244
+ result.success = result.failedScenes.length === 0;
40245
+ return result;
40246
+ } catch (error) {
40247
+ return {
40248
+ ...result,
40249
+ error: error instanceof Error ? error.message : String(error)
40250
+ };
40251
+ }
39958
40252
  }
39959
- var init_ai_review = __esm({
39960
- "src/commands/ai-review.ts"() {
40253
+ var import_yaml5;
40254
+ var init_ai_script_pipeline = __esm({
40255
+ "src/commands/ai-script-pipeline.ts"() {
39961
40256
  "use strict";
39962
- init_source();
39963
- init_ora();
40257
+ import_yaml5 = __toESM(require_dist(), 1);
39964
40258
  init_dist2();
39965
40259
  init_api_key();
39966
- init_exec_safe();
39967
- init_output();
39968
- init_validate();
40260
+ init_config();
40261
+ init_audio();
40262
+ init_ai_helpers();
40263
+ init_video_utils();
40264
+ init_video_providers();
39969
40265
  }
39970
40266
  });
39971
40267
 
39972
40268
  // src/commands/ai-highlights.ts
39973
40269
  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";
40270
+ import { resolve as resolve36, dirname as dirname12, basename as basename9, extname as extname9 } from "node:path";
39975
40271
  import { existsSync as existsSync28 } from "node:fs";
39976
40272
  function filterHighlights(highlights, options) {
39977
40273
  let filtered = highlights.filter((h) => h.confidence >= options.threshold);
@@ -40015,7 +40311,7 @@ async function executeHighlights(options) {
40015
40311
  if (!existsSync28(absPath)) {
40016
40312
  return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
40017
40313
  }
40018
- const ext = extname8(absPath).toLowerCase();
40314
+ const ext = extname9(absPath).toLowerCase();
40019
40315
  const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
40020
40316
  const isVideo = videoExtensions.includes(ext);
40021
40317
  const targetDuration = options.duration;
@@ -40352,7 +40648,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40352
40648
  };
40353
40649
  for (let i = 0; i < selectedHighlights.length; i++) {
40354
40650
  const h = selectedHighlights[i];
40355
- const baseName = basename9(absPath, extname8(absPath));
40651
+ const baseName = basename9(absPath, extname9(absPath));
40356
40652
  const outputPath = resolve36(outputDir, `${baseName}-short-${i + 1}.mp4`);
40357
40653
  const { stdout: probeOut } = await execSafe("ffprobe", [
40358
40654
  "-v",
@@ -40451,7 +40747,7 @@ function registerHighlightsCommands(aiCommand) {
40451
40747
  });
40452
40748
  return;
40453
40749
  }
40454
- const ext = extname8(absPath).toLowerCase();
40750
+ const ext = extname9(absPath).toLowerCase();
40455
40751
  const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
40456
40752
  const isVideo = videoExtensions.includes(ext);
40457
40753
  console.log();
@@ -40929,11 +41225,11 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40929
41225
  let outputPath;
40930
41226
  if (shortCount === 1 && options.output) {
40931
41227
  outputPath = resolve36(process.cwd(), options.output);
40932
- if (!extname8(outputPath)) {
41228
+ if (!extname9(outputPath)) {
40933
41229
  outputPath += ".mp4";
40934
41230
  }
40935
41231
  } else {
40936
- const baseName = basename9(absPath, extname8(absPath));
41232
+ const baseName = basename9(absPath, extname9(absPath));
40937
41233
  outputPath = resolve36(outputDir, `${baseName}-short-${i + 1}.mp4`);
40938
41234
  }
40939
41235
  const parentDir = dirname12(outputPath);
@@ -51819,7 +52115,7 @@ var init_scene_audio_scan = __esm({
51819
52115
 
51820
52116
  // src/commands/_shared/scene-audio-mux.ts
51821
52117
  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";
52118
+ import { resolve as resolve45, dirname as dirname15, extname as extname12, basename as basename16 } from "node:path";
51823
52119
  function buildAudioMuxFilter(audios) {
51824
52120
  if (audios.length === 0) return null;
51825
52121
  const labels = [];
@@ -51878,7 +52174,7 @@ async function muxAudioIntoVideo(opts) {
51878
52174
  if (!filter) {
51879
52175
  return { success: true, outputPath: opts.videoPath, audioCount: 0 };
51880
52176
  }
51881
- const ext = extname11(opts.videoPath) || `.${opts.format}`;
52177
+ const ext = extname12(opts.videoPath) || `.${opts.format}`;
51882
52178
  const tmpPath = resolve45(
51883
52179
  dirname15(opts.videoPath),
51884
52180
  `.${basename16(opts.videoPath, ext)}.muxing${ext}`
@@ -52530,18 +52826,29 @@ async function executeVideoGenerate(options) {
52530
52826
  runway: "RUNWAY_API_SECRET",
52531
52827
  kling: "KLING_API_KEY",
52532
52828
  veo: "GOOGLE_API_KEY",
52533
- seedance: "FAL_KEY",
52534
- fal: "FAL_KEY"
52829
+ seedance: "FAL_API_KEY",
52830
+ fal: "FAL_API_KEY"
52535
52831
  };
52536
- const key = apiKey || process.env[envKeyMap[provider] || ""];
52832
+ const envKey = envKeyMap[provider] || "";
52833
+ const key = apiKey || (hasApiKey(envKey) ? process.env[envKey] || (envKey === "FAL_API_KEY" ? process.env.FAL_KEY : void 0) : void 0);
52537
52834
  if (!key) return { success: false, error: `${envKeyMap[provider]} required for ${provider}` };
52538
52835
  let referenceImage;
52836
+ let referenceImageBuffer;
52837
+ let referenceImageMimeType;
52539
52838
  if (image) {
52540
52839
  const imagePath = resolve53(process.cwd(), image);
52541
52840
  const imageBuffer = await readFile35(imagePath);
52542
52841
  const ext = image.toLowerCase().split(".").pop();
52543
- const mimeTypes2 = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
52842
+ const mimeTypes2 = {
52843
+ jpg: "image/jpeg",
52844
+ jpeg: "image/jpeg",
52845
+ png: "image/png",
52846
+ gif: "image/gif",
52847
+ webp: "image/webp"
52848
+ };
52544
52849
  const mimeType = mimeTypes2[ext || "png"] || "image/png";
52850
+ referenceImageBuffer = imageBuffer;
52851
+ referenceImageMimeType = mimeType;
52545
52852
  referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
52546
52853
  }
52547
52854
  if (provider === "seedance" || provider === "fal") {
@@ -52549,12 +52856,12 @@ async function executeVideoGenerate(options) {
52549
52856
  await fal.initialize({ apiKey: key });
52550
52857
  let falImage = referenceImage;
52551
52858
  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;
52859
+ const uploadHost = await resolveUploadHost();
52860
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
52861
+ filename: image,
52862
+ mimeType: referenceImageMimeType
52863
+ });
52864
+ falImage = upload.url;
52558
52865
  }
52559
52866
  const model = seedanceModel === "fast" || seedanceModel === "seedance-2.0-fast" ? "seedance-2.0-fast" : "seedance-2.0";
52560
52867
  const result = await fal.generateVideo(prompt3, {
@@ -52565,14 +52872,22 @@ async function executeVideoGenerate(options) {
52565
52872
  negativePrompt: negative,
52566
52873
  model
52567
52874
  });
52568
- if (result.status === "failed") return { success: false, error: result.error || "Seedance generation failed" };
52875
+ if (result.status === "failed")
52876
+ return { success: false, error: result.error || "Seedance generation failed" };
52569
52877
  let outputPath;
52570
52878
  if (output && result.videoUrl) {
52571
52879
  const buffer = await downloadVideo(result.videoUrl, key);
52572
52880
  outputPath = resolve53(process.cwd(), output);
52573
52881
  await writeFile39(outputPath, buffer);
52574
52882
  }
52575
- return { success: true, taskId: result.id, status: "completed", videoUrl: result.videoUrl, outputPath, provider: "seedance" };
52883
+ return {
52884
+ success: true,
52885
+ taskId: result.id,
52886
+ status: "completed",
52887
+ videoUrl: result.videoUrl,
52888
+ outputPath,
52889
+ provider: "seedance"
52890
+ };
52576
52891
  } else if (provider === "runway") {
52577
52892
  const runway = new RunwayProvider();
52578
52893
  await runway.initialize({ apiKey: key });
@@ -52583,30 +52898,41 @@ async function executeVideoGenerate(options) {
52583
52898
  aspectRatio: ratio,
52584
52899
  seed
52585
52900
  });
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" };
52901
+ if (result.status === "failed")
52902
+ return { success: false, error: result.error || "Runway generation failed" };
52903
+ if (!wait)
52904
+ return { success: true, taskId: result.id, status: "processing", provider: "runway" };
52588
52905
  const finalResult = await runway.waitForCompletion(result.id, () => {
52589
52906
  }, 3e5);
52590
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Runway generation failed" };
52907
+ if (finalResult.status !== "completed")
52908
+ return { success: false, error: finalResult.error || "Runway generation failed" };
52591
52909
  let outputPath;
52592
52910
  if (output && finalResult.videoUrl) {
52593
52911
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52594
52912
  outputPath = resolve53(process.cwd(), output);
52595
52913
  await writeFile39(outputPath, buffer);
52596
52914
  }
52597
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "runway" };
52915
+ return {
52916
+ success: true,
52917
+ taskId: result.id,
52918
+ status: "completed",
52919
+ videoUrl: finalResult.videoUrl,
52920
+ duration: finalResult.duration,
52921
+ outputPath,
52922
+ provider: "runway"
52923
+ };
52598
52924
  } else if (provider === "kling") {
52599
52925
  const kling = new KlingProvider();
52600
52926
  await kling.initialize({ apiKey: key });
52601
52927
  if (!kling.isConfigured()) return { success: false, error: "Invalid Kling API key format" };
52602
52928
  let klingImage = referenceImage;
52603
52929
  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;
52930
+ const uploadHost = await resolveUploadHost();
52931
+ const upload = await uploadHost.uploadImage(referenceImageBuffer, {
52932
+ filename: image,
52933
+ mimeType: referenceImageMimeType
52934
+ });
52935
+ klingImage = upload.url;
52610
52936
  }
52611
52937
  const result = await kling.generateVideo(prompt3, {
52612
52938
  prompt: prompt3,
@@ -52616,23 +52942,38 @@ async function executeVideoGenerate(options) {
52616
52942
  negativePrompt: negative,
52617
52943
  mode
52618
52944
  });
52619
- if (result.status === "failed") return { success: false, error: result.error || "Kling generation failed" };
52945
+ if (result.status === "failed")
52946
+ return { success: false, error: result.error || "Kling generation failed" };
52620
52947
  const taskType = referenceImage ? "image2video" : "text2video";
52621
- if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "kling" };
52948
+ if (!wait)
52949
+ return { success: true, taskId: result.id, status: "processing", provider: "kling" };
52622
52950
  const finalResult = await kling.waitForCompletion(result.id, taskType, () => {
52623
52951
  }, 6e5);
52624
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Kling generation failed" };
52952
+ if (finalResult.status !== "completed")
52953
+ return { success: false, error: finalResult.error || "Kling generation failed" };
52625
52954
  let outputPath;
52626
52955
  if (output && finalResult.videoUrl) {
52627
52956
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52628
52957
  outputPath = resolve53(process.cwd(), output);
52629
52958
  await writeFile39(outputPath, buffer);
52630
52959
  }
52631
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "kling" };
52960
+ return {
52961
+ success: true,
52962
+ taskId: result.id,
52963
+ status: "completed",
52964
+ videoUrl: finalResult.videoUrl,
52965
+ duration: finalResult.duration,
52966
+ outputPath,
52967
+ provider: "kling"
52968
+ };
52632
52969
  } else if (provider === "veo") {
52633
52970
  const gemini = new GeminiProvider();
52634
52971
  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" };
52972
+ const veoModelMap = {
52973
+ "3.0": "veo-3.0-generate-preview",
52974
+ "3.1": "veo-3.1-generate-preview",
52975
+ "3.1-fast": "veo-3.1-fast-generate-preview"
52976
+ };
52636
52977
  const model = veoModelMap[veoModel] || "veo-3.1-fast-generate-preview";
52637
52978
  const veoDuration = duration <= 6 ? 6 : 8;
52638
52979
  const result = await gemini.generateVideo(prompt3, {
@@ -52644,18 +52985,27 @@ async function executeVideoGenerate(options) {
52644
52985
  negativePrompt: negative,
52645
52986
  resolution
52646
52987
  });
52647
- if (result.status === "failed") return { success: false, error: result.error || "Veo generation failed" };
52988
+ if (result.status === "failed")
52989
+ return { success: false, error: result.error || "Veo generation failed" };
52648
52990
  if (!wait) return { success: true, taskId: result.id, status: "processing", provider: "veo" };
52649
52991
  const finalResult = await gemini.waitForVideoCompletion(result.id, () => {
52650
52992
  }, 3e5);
52651
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Veo generation failed" };
52993
+ if (finalResult.status !== "completed")
52994
+ return { success: false, error: finalResult.error || "Veo generation failed" };
52652
52995
  let outputPath;
52653
52996
  if (output && finalResult.videoUrl) {
52654
52997
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52655
52998
  outputPath = resolve53(process.cwd(), output);
52656
52999
  await writeFile39(outputPath, buffer);
52657
53000
  }
52658
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, outputPath, provider: "veo" };
53001
+ return {
53002
+ success: true,
53003
+ taskId: result.id,
53004
+ status: "completed",
53005
+ videoUrl: finalResult.videoUrl,
53006
+ outputPath,
53007
+ provider: "veo"
53008
+ };
52659
53009
  } else if (provider === "grok") {
52660
53010
  const grok = new GrokProvider();
52661
53011
  await grok.initialize({ apiKey: key });
@@ -52665,28 +53015,52 @@ async function executeVideoGenerate(options) {
52665
53015
  duration,
52666
53016
  aspectRatio: ratio
52667
53017
  });
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" };
53018
+ if (result.status === "failed")
53019
+ return { success: false, error: result.error || "Grok generation failed" };
53020
+ if (!wait)
53021
+ return { success: true, taskId: result.id, status: "processing", provider: "grok" };
52670
53022
  const finalResult = await grok.waitForCompletion(result.id, () => {
52671
53023
  }, 3e5);
52672
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Grok generation failed" };
53024
+ if (finalResult.status !== "completed")
53025
+ return { success: false, error: finalResult.error || "Grok generation failed" };
52673
53026
  let outputPath;
52674
53027
  if (output && finalResult.videoUrl) {
52675
53028
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52676
53029
  outputPath = resolve53(process.cwd(), output);
52677
53030
  await writeFile39(outputPath, buffer);
52678
53031
  }
52679
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath, provider: "grok" };
53032
+ return {
53033
+ success: true,
53034
+ taskId: result.id,
53035
+ status: "completed",
53036
+ videoUrl: finalResult.videoUrl,
53037
+ duration: finalResult.duration,
53038
+ outputPath,
53039
+ provider: "grok"
53040
+ };
52680
53041
  }
52681
53042
  return { success: false, error: `Unsupported provider: ${provider}` };
52682
53043
  } catch (error) {
52683
- return { success: false, error: `Video generation failed: ${error instanceof Error ? error.message : String(error)}` };
53044
+ return {
53045
+ success: false,
53046
+ error: `Video generation failed: ${error instanceof Error ? error.message : String(error)}`
53047
+ };
52684
53048
  }
52685
53049
  }
52686
53050
  async function executeVideoStatus(options) {
52687
- const { taskId, provider = "runway", taskType = "text2video", wait = false, output, apiKey } = options;
53051
+ const {
53052
+ taskId,
53053
+ provider = "runway",
53054
+ taskType = "text2video",
53055
+ wait = false,
53056
+ output,
53057
+ apiKey
53058
+ } = options;
52688
53059
  try {
52689
- const envKeyMap = { runway: "RUNWAY_API_SECRET", kling: "KLING_API_KEY" };
53060
+ const envKeyMap = {
53061
+ runway: "RUNWAY_API_SECRET",
53062
+ kling: "KLING_API_KEY"
53063
+ };
52690
53064
  const key = apiKey || process.env[envKeyMap[provider] || ""];
52691
53065
  if (!key) return { success: false, error: `${envKeyMap[provider]} required` };
52692
53066
  if (provider === "runway") {
@@ -52703,7 +53077,14 @@ async function executeVideoStatus(options) {
52703
53077
  outputPath = resolve53(process.cwd(), output);
52704
53078
  await writeFile39(outputPath, buffer);
52705
53079
  }
52706
- return { success: true, taskId, status: result.status, progress: result.progress, videoUrl: result.videoUrl, outputPath };
53080
+ return {
53081
+ success: true,
53082
+ taskId,
53083
+ status: result.status,
53084
+ progress: result.progress,
53085
+ videoUrl: result.videoUrl,
53086
+ outputPath
53087
+ };
52707
53088
  } else if (provider === "kling") {
52708
53089
  const kling = new KlingProvider();
52709
53090
  await kling.initialize({ apiKey: key });
@@ -52718,11 +53099,21 @@ async function executeVideoStatus(options) {
52718
53099
  outputPath = resolve53(process.cwd(), output);
52719
53100
  await writeFile39(outputPath, buffer);
52720
53101
  }
52721
- return { success: true, taskId, status: result.status, videoUrl: result.videoUrl, duration: result.duration, outputPath };
53102
+ return {
53103
+ success: true,
53104
+ taskId,
53105
+ status: result.status,
53106
+ videoUrl: result.videoUrl,
53107
+ duration: result.duration,
53108
+ outputPath
53109
+ };
52722
53110
  }
52723
53111
  return { success: false, error: `Unsupported provider: ${provider}` };
52724
53112
  } catch (error) {
52725
- return { success: false, error: `Status check failed: ${error instanceof Error ? error.message : String(error)}` };
53113
+ return {
53114
+ success: false,
53115
+ error: `Status check failed: ${error instanceof Error ? error.message : String(error)}`
53116
+ };
52726
53117
  }
52727
53118
  }
52728
53119
  async function executeVideoCancel(options) {
@@ -52735,11 +53126,24 @@ async function executeVideoCancel(options) {
52735
53126
  const success = await runway.cancelGeneration(taskId);
52736
53127
  return { success };
52737
53128
  } catch (error) {
52738
- return { success: false, error: `Cancel failed: ${error instanceof Error ? error.message : String(error)}` };
53129
+ return {
53130
+ success: false,
53131
+ error: `Cancel failed: ${error instanceof Error ? error.message : String(error)}`
53132
+ };
52739
53133
  }
52740
53134
  }
52741
53135
  async function executeVideoExtend(options) {
52742
- const { videoId, provider = "kling", prompt: prompt3, duration = 5, negative, veoModel = "3.1", output, wait = true, apiKey } = options;
53136
+ const {
53137
+ videoId,
53138
+ provider = "kling",
53139
+ prompt: prompt3,
53140
+ duration = 5,
53141
+ negative,
53142
+ veoModel = "3.1",
53143
+ output,
53144
+ wait = true,
53145
+ apiKey
53146
+ } = options;
52743
53147
  try {
52744
53148
  if (provider === "kling") {
52745
53149
  const key = apiKey || process.env.KLING_API_KEY;
@@ -52752,53 +53156,78 @@ async function executeVideoExtend(options) {
52752
53156
  negativePrompt: negative,
52753
53157
  duration: String(duration)
52754
53158
  });
52755
- if (result.status === "failed") return { success: false, error: result.error || "Kling extension failed" };
53159
+ if (result.status === "failed")
53160
+ return { success: false, error: result.error || "Kling extension failed" };
52756
53161
  if (!wait) return { success: true, taskId: result.id, status: "processing" };
52757
53162
  const finalResult = await kling.waitForExtendCompletion(result.id, () => {
52758
53163
  }, 6e5);
52759
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Kling extension failed" };
53164
+ if (finalResult.status !== "completed")
53165
+ return { success: false, error: finalResult.error || "Kling extension failed" };
52760
53166
  let outputPath;
52761
53167
  if (output && finalResult.videoUrl) {
52762
53168
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52763
53169
  outputPath = resolve53(process.cwd(), output);
52764
53170
  await writeFile39(outputPath, buffer);
52765
53171
  }
52766
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, duration: finalResult.duration, outputPath };
53172
+ return {
53173
+ success: true,
53174
+ taskId: result.id,
53175
+ status: "completed",
53176
+ videoUrl: finalResult.videoUrl,
53177
+ duration: finalResult.duration,
53178
+ outputPath
53179
+ };
52767
53180
  } else if (provider === "veo") {
52768
53181
  const key = apiKey || process.env.GOOGLE_API_KEY;
52769
53182
  if (!key) return { success: false, error: "GOOGLE_API_KEY required" };
52770
53183
  const gemini = new GeminiProvider();
52771
53184
  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" };
53185
+ const veoModelMap = {
53186
+ "3.0": "veo-3.0-generate-preview",
53187
+ "3.1": "veo-3.1-generate-preview",
53188
+ "3.1-fast": "veo-3.1-fast-generate-preview"
53189
+ };
52773
53190
  const model = veoModelMap[veoModel] || "veo-3.1-generate-preview";
52774
53191
  const result = await gemini.extendVideo(videoId, prompt3, {
52775
53192
  duration,
52776
53193
  model
52777
53194
  });
52778
- if (result.status === "failed") return { success: false, error: result.error || "Veo extension failed" };
53195
+ if (result.status === "failed")
53196
+ return { success: false, error: result.error || "Veo extension failed" };
52779
53197
  if (!wait) return { success: true, taskId: result.id, status: "processing" };
52780
53198
  const finalResult = await gemini.waitForVideoCompletion(result.id, () => {
52781
53199
  }, 3e5);
52782
- if (finalResult.status !== "completed") return { success: false, error: finalResult.error || "Veo extension failed" };
53200
+ if (finalResult.status !== "completed")
53201
+ return { success: false, error: finalResult.error || "Veo extension failed" };
52783
53202
  let outputPath;
52784
53203
  if (output && finalResult.videoUrl) {
52785
53204
  const buffer = await downloadVideo(finalResult.videoUrl, key);
52786
53205
  outputPath = resolve53(process.cwd(), output);
52787
53206
  await writeFile39(outputPath, buffer);
52788
53207
  }
52789
- return { success: true, taskId: result.id, status: "completed", videoUrl: finalResult.videoUrl, outputPath };
53208
+ return {
53209
+ success: true,
53210
+ taskId: result.id,
53211
+ status: "completed",
53212
+ videoUrl: finalResult.videoUrl,
53213
+ outputPath
53214
+ };
52790
53215
  }
52791
53216
  return { success: false, error: `Unsupported provider: ${provider}` };
52792
53217
  } catch (error) {
52793
- return { success: false, error: `Video extension failed: ${error instanceof Error ? error.message : String(error)}` };
53218
+ return {
53219
+ success: false,
53220
+ error: `Video extension failed: ${error instanceof Error ? error.message : String(error)}`
53221
+ };
52794
53222
  }
52795
53223
  }
52796
53224
  var init_ai_video = __esm({
52797
53225
  "src/commands/ai-video.ts"() {
52798
53226
  "use strict";
52799
53227
  init_dist2();
52800
- init_ai_script_pipeline();
53228
+ init_upload_host();
52801
53229
  init_ai_helpers();
53230
+ init_api_key();
52802
53231
  }
52803
53232
  });
52804
53233
 
@@ -52811,7 +53240,7 @@ __export(ai_audio_exports, {
52811
53240
  executeTranscribe: () => executeTranscribe,
52812
53241
  executeVoiceClone: () => executeVoiceClone
52813
53242
  });
52814
- import { resolve as resolve54, dirname as dirname19, basename as basename18, extname as extname12 } from "node:path";
53243
+ import { resolve as resolve54, dirname as dirname19, basename as basename18, extname as extname13 } from "node:path";
52815
53244
  import { readFile as readFile36, writeFile as writeFile40 } from "node:fs/promises";
52816
53245
  import { existsSync as existsSync38 } from "node:fs";
52817
53246
  async function executeTranscribe(options) {
@@ -52908,7 +53337,7 @@ async function executeDub(options) {
52908
53337
  if (!openaiKey) return { success: false, error: "OPENAI_API_KEY required for Whisper transcription" };
52909
53338
  if (!anthropicKey) return { success: false, error: "ANTHROPIC_API_KEY required for Claude translation" };
52910
53339
  if (!analyzeOnly && !elevenlabsKey) return { success: false, error: "ELEVENLABS_API_KEY required for TTS" };
52911
- const ext = extname12(absPath).toLowerCase();
53340
+ const ext = extname13(absPath).toLowerCase();
52912
53341
  const isVideo = [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
52913
53342
  let audioPath = absPath;
52914
53343
  if (isVideo) {
@@ -52993,8 +53422,8 @@ ${segmentTexts}`,
52993
53422
  }
52994
53423
  }
52995
53424
  const combinedBuffer = Buffer.concat(dubbedBuffers);
52996
- const outputExt = isVideo ? ".mp3" : extname12(absPath);
52997
- const defaultOutputPath2 = resolve54(dirname19(absPath), `${basename18(absPath, extname12(absPath))}-${language}${outputExt}`);
53425
+ const outputExt = isVideo ? ".mp3" : extname13(absPath);
53426
+ const defaultOutputPath2 = resolve54(dirname19(absPath), `${basename18(absPath, extname13(absPath))}-${language}${outputExt}`);
52998
53427
  const finalOutputPath = resolve54(process.cwd(), output || defaultOutputPath2);
52999
53428
  await writeFile40(finalOutputPath, combinedBuffer);
53000
53429
  if (isVideo && audioPath !== absPath) {
@@ -53031,7 +53460,7 @@ async function executeDuck(options) {
53031
53460
  const absVoicePath = resolve54(process.cwd(), voicePath);
53032
53461
  if (!existsSync38(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
53033
53462
  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)}`);
53463
+ const defaultOutput = resolve54(dirname19(absMusicPath), `${basename18(absMusicPath, extname13(absMusicPath))}-ducked${extname13(absMusicPath)}`);
53035
53464
  const outputPath = resolve54(process.cwd(), output || defaultOutput);
53036
53465
  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
53466
  await execSafe("ffmpeg", [
@@ -59608,11 +60037,17 @@ var init_generate2 = __esm({
59608
60037
  }),
59609
60038
  async execute(args) {
59610
60039
  const result = await executeMotion(args);
59611
- if (!result.success) return { success: false, error: result.error ?? "Motion generation failed" };
60040
+ if (!result.success)
60041
+ return { success: false, error: result.error ?? "Motion generation failed" };
59612
60042
  const out = result.compositedPath ?? result.renderedPath ?? result.codePath;
59613
60043
  return {
59614
60044
  success: true,
59615
- data: { codePath: result.codePath, renderedPath: result.renderedPath, compositedPath: result.compositedPath, componentName: result.componentName },
60045
+ data: {
60046
+ codePath: result.codePath,
60047
+ renderedPath: result.renderedPath,
60048
+ compositedPath: result.compositedPath,
60049
+ componentName: result.componentName
60050
+ },
59616
60051
  humanLines: [`\u2705 Motion generated \u2192 ${out}`]
59617
60052
  };
59618
60053
  }
@@ -59676,7 +60111,9 @@ var init_generate2 = __esm({
59676
60111
  return {
59677
60112
  success: true,
59678
60113
  data: { outputPath: result.outputPath, provider: result.provider, duration: result.duration },
59679
- humanLines: [`\u2705 Music${result.provider ? ` (${result.provider})` : ""} \u2192 ${result.outputPath ?? "(async)"}`]
60114
+ humanLines: [
60115
+ `\u2705 Music${result.provider ? ` (${result.provider})` : ""} \u2192 ${result.outputPath ?? "(async)"}`
60116
+ ]
59680
60117
  };
59681
60118
  }
59682
60119
  });
@@ -59693,7 +60130,12 @@ var init_generate2 = __esm({
59693
60130
  if (!result.success) return { success: false, error: result.error ?? "Music status failed" };
59694
60131
  return {
59695
60132
  success: true,
59696
- data: { taskId: result.taskId, status: result.status, audioUrl: result.audioUrl, error: result.error },
60133
+ data: {
60134
+ taskId: result.taskId,
60135
+ status: result.status,
60136
+ audioUrl: result.audioUrl,
60137
+ error: result.error
60138
+ },
59697
60139
  humanLines: [`Music task ${result.taskId}: ${result.status}`]
59698
60140
  };
59699
60141
  }
@@ -59705,7 +60147,9 @@ var init_generate2 = __esm({
59705
60147
  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
60148
  schema: external_exports.object({
59707
60149
  prompt: external_exports.string().describe("Image description prompt"),
59708
- provider: external_exports.enum(["gemini", "openai", "grok"]).optional().describe("Image provider (default: gemini)"),
60150
+ provider: external_exports.enum(["gemini", "openai", "grok"]).optional().describe(
60151
+ "Image provider (default: openai when OPENAI_API_KEY is configured, otherwise first configured provider)"
60152
+ ),
59709
60153
  output: external_exports.string().optional().describe("Output file path"),
59710
60154
  size: external_exports.string().optional().describe("Image size for OpenAI (1024x1024, 1536x1024, 1024x1536)"),
59711
60155
  ratio: external_exports.string().optional().describe("Aspect ratio for Gemini (1:1, 16:9, 9:16, 4:3, 3:4, etc.)"),
@@ -59715,10 +60159,16 @@ var init_generate2 = __esm({
59715
60159
  }),
59716
60160
  async execute(args) {
59717
60161
  const result = await executeImageGenerate(args);
59718
- if (!result.success) return { success: false, error: result.error ?? "Image generation failed" };
60162
+ if (!result.success)
60163
+ return { success: false, error: result.error ?? "Image generation failed" };
59719
60164
  return {
59720
60165
  success: true,
59721
- data: { outputPath: result.outputPath, provider: result.provider, model: result.model, imageCount: result.images?.length },
60166
+ data: {
60167
+ outputPath: result.outputPath,
60168
+ provider: result.provider,
60169
+ model: result.model,
60170
+ imageCount: result.images?.length
60171
+ },
59722
60172
  humanLines: [`\u2705 Image (${result.provider}) \u2192 ${result.outputPath}`]
59723
60173
  };
59724
60174
  }
@@ -59740,7 +60190,9 @@ var init_generate2 = __esm({
59740
60190
  return {
59741
60191
  success: true,
59742
60192
  data: { segmentCount: result.segmentCount, outputPath: result.outputPath },
59743
- humanLines: [`\u2705 Storyboard: ${result.segmentCount} segments${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`]
60193
+ humanLines: [
60194
+ `\u2705 Storyboard: ${result.segmentCount} segments${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60195
+ ]
59744
60196
  };
59745
60197
  }
59746
60198
  });
@@ -59759,7 +60211,11 @@ var init_generate2 = __esm({
59759
60211
  if (!result.success) return { success: false, error: result.error ?? "Background failed" };
59760
60212
  return {
59761
60213
  success: true,
59762
- data: { imageUrl: result.imageUrl, outputPath: result.outputPath, revisedPrompt: result.revisedPrompt },
60214
+ data: {
60215
+ imageUrl: result.imageUrl,
60216
+ outputPath: result.outputPath,
60217
+ revisedPrompt: result.revisedPrompt
60218
+ },
59763
60219
  humanLines: [`\u2705 Background \u2192 ${result.outputPath ?? result.imageUrl}`]
59764
60220
  };
59765
60221
  }
@@ -59817,7 +60273,9 @@ var init_generate2 = __esm({
59817
60273
  outputPath: result.outputPath,
59818
60274
  provider: result.provider
59819
60275
  },
59820
- humanLines: [`\u2705 Video (${result.provider}, ${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`]
60276
+ humanLines: [
60277
+ `\u2705 Video (${result.provider}, ${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60278
+ ]
59821
60279
  };
59822
60280
  }
59823
60281
  });
@@ -59838,8 +60296,16 @@ var init_generate2 = __esm({
59838
60296
  if (!result.success) return { success: false, error: result.error ?? "Status check failed" };
59839
60297
  return {
59840
60298
  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}%)` : ""}`]
60299
+ data: {
60300
+ taskId: result.taskId,
60301
+ status: result.status,
60302
+ progress: result.progress,
60303
+ videoUrl: result.videoUrl,
60304
+ outputPath: result.outputPath
60305
+ },
60306
+ humanLines: [
60307
+ `Task ${result.taskId}: ${result.status}${result.progress !== void 0 ? ` (${result.progress}%)` : ""}`
60308
+ ]
59843
60309
  };
59844
60310
  }
59845
60311
  });
@@ -59881,8 +60347,16 @@ var init_generate2 = __esm({
59881
60347
  if (!result.success) return { success: false, error: result.error ?? "Extend failed" };
59882
60348
  return {
59883
60349
  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}` : ""}`]
60350
+ data: {
60351
+ taskId: result.taskId,
60352
+ status: result.status,
60353
+ videoUrl: result.videoUrl,
60354
+ duration: result.duration,
60355
+ outputPath: result.outputPath
60356
+ },
60357
+ humanLines: [
60358
+ `\u2705 Video extended (${result.status})${result.outputPath ? ` \u2192 ${result.outputPath}` : ""}`
60359
+ ]
59886
60360
  };
59887
60361
  }
59888
60362
  });
@@ -61813,7 +62287,7 @@ var init_export2 = __esm({
61813
62287
 
61814
62288
  // src/tools/manifest/agent-only.ts
61815
62289
  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";
62290
+ import { resolve as resolve63, join as join16, basename as basename22, extname as extname14 } from "node:path";
61817
62291
  function matchPattern(filename, pattern) {
61818
62292
  const regex2 = new RegExp(
61819
62293
  "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
@@ -61832,7 +62306,7 @@ function formatSize(bytes) {
61832
62306
  return `${size.toFixed(1)}${units2[unit]}`;
61833
62307
  }
61834
62308
  function detectMediaType4(filePath) {
61835
- const ext = extname13(filePath).toLowerCase();
62309
+ const ext = extname14(filePath).toLowerCase();
61836
62310
  if ([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"].includes(ext)) return "video";
61837
62311
  if ([".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"].includes(ext)) return "audio";
61838
62312
  if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"].includes(ext)) return "image";
@@ -61859,7 +62333,7 @@ function isMediaFile2(filePath) {
61859
62333
  ".webp",
61860
62334
  ".bmp"
61861
62335
  ];
61862
- return mediaExts.includes(extname13(filePath).toLowerCase());
62336
+ return mediaExts.includes(extname14(filePath).toLowerCase());
61863
62337
  }
61864
62338
  async function getMediaDuration2(filePath) {
61865
62339
  try {
@@ -62005,7 +62479,7 @@ var init_agent_only = __esm({
62005
62479
  if (entry.isDirectory() && recursive) {
62006
62480
  await scanDir(entryPath);
62007
62481
  } else if (entry.isFile()) {
62008
- const ext = extname13(entry.name).toLowerCase();
62482
+ const ext = extname14(entry.name).toLowerCase();
62009
62483
  const matchesFilter = !filterExts || filterExts.includes(ext);
62010
62484
  if (matchesFilter && isMediaFile2(entryPath)) mediaFiles.push(entryPath);
62011
62485
  }
@@ -62973,7 +63447,7 @@ init_source();
62973
63447
  // package.json
62974
63448
  var package_default = {
62975
63449
  name: "@vibeframe/cli",
62976
- version: "0.91.9",
63450
+ version: "0.93.0",
62977
63451
  description: "VibeFrame CLI - natural language video editing from the terminal",
62978
63452
  private: false,
62979
63453
  license: "MIT",
@@ -64159,8 +64633,8 @@ init_output();
64159
64633
  init_sanitize();
64160
64634
  init_validate();
64161
64635
  init_cost_tier();
64162
- import { readFile as readFile23, writeFile as writeFile30 } from "node:fs/promises";
64163
- import { resolve as resolve33 } from "node:path";
64636
+ import { readFile as readFile22, writeFile as writeFile29 } from "node:fs/promises";
64637
+ import { resolve as resolve32 } from "node:path";
64164
64638
  var inspectCommand = new Command("inspect").description("Inspect media using AI (images, videos, YouTube URLs)").addHelpText(
64165
64639
  "after",
64166
64640
  `
@@ -64357,8 +64831,8 @@ inspectCommand.command("suggest").description("Get AI edit suggestions using Gem
64357
64831
  }
64358
64832
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
64359
64833
  const spinner2 = ora("Initializing Gemini...").start();
64360
- const filePath = resolve33(process.cwd(), projectPath);
64361
- const content = await readFile23(filePath, "utf-8");
64834
+ const filePath = resolve32(process.cwd(), projectPath);
64835
+ const content = await readFile22(filePath, "utf-8");
64362
64836
  const data = JSON.parse(content);
64363
64837
  const project = Project.fromJSON(data);
64364
64838
  const gemini = new GeminiProvider();
@@ -64395,7 +64869,7 @@ inspectCommand.command("suggest").description("Get AI edit suggestions using Gem
64395
64869
  const sug = suggestions[0];
64396
64870
  const applied = applySuggestion(project, sug);
64397
64871
  if (applied) {
64398
- await writeFile30(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
64872
+ await writeFile29(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
64399
64873
  spinner2.succeed(source_default.green("Suggestion applied"));
64400
64874
  } else {
64401
64875
  spinner2.warn(source_default.yellow("Could not apply suggestion automatically"));
@@ -64420,9 +64894,9 @@ init_cost_tier();
64420
64894
  init_ai_helpers();
64421
64895
  init_output();
64422
64896
  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";
64897
+ import { resolve as resolve33, dirname as dirname11, basename as basename8, extname as extname7 } from "node:path";
64898
+ import { readFile as readFile23, writeFile as writeFile30 } from "node:fs/promises";
64899
+ import { existsSync as existsSync25 } from "node:fs";
64426
64900
  var audioCommand = new Command("audio").alias("au").description("Audio operations (transcribe, TTS, voice clone, ducking)").addHelpText(
64427
64901
  "after",
64428
64902
  `
@@ -64455,8 +64929,8 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64455
64929
  const whisper = new WhisperProvider();
64456
64930
  await whisper.initialize({ apiKey });
64457
64931
  spinner2.text = "Reading audio file...";
64458
- const absPath = resolve34(process.cwd(), audioPath);
64459
- const audioBuffer = await readFile24(absPath);
64932
+ const absPath = resolve33(process.cwd(), audioPath);
64933
+ const audioBuffer = await readFile23(absPath);
64460
64934
  const audioBlob = new Blob([audioBuffer]);
64461
64935
  spinner2.text = "Transcribing...";
64462
64936
  const result = await whisper.transcribe(audioBlob, options.language);
@@ -64473,7 +64947,7 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64473
64947
  fullText: result.fullText,
64474
64948
  segments: result.segments,
64475
64949
  language: result.detectedLanguage,
64476
- outputPath: options.output ? resolve34(process.cwd(), options.output) : void 0
64950
+ outputPath: options.output ? resolve33(process.cwd(), options.output) : void 0
64477
64951
  }
64478
64952
  });
64479
64953
  return;
@@ -64493,10 +64967,10 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
64493
64967
  console.log();
64494
64968
  }
64495
64969
  if (options.output) {
64496
- const outputPath = resolve34(process.cwd(), options.output);
64970
+ const outputPath = resolve33(process.cwd(), options.output);
64497
64971
  const format = detectFormat(options.output, options.format);
64498
64972
  const content = formatTranscript(result, format);
64499
- await writeFile31(outputPath, content, "utf-8");
64973
+ await writeFile30(outputPath, content, "utf-8");
64500
64974
  console.log(source_default.green(`Saved ${format.toUpperCase()} to: ${outputPath}`));
64501
64975
  }
64502
64976
  } catch (error) {
@@ -64554,8 +65028,8 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
64554
65028
  }
64555
65029
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
64556
65030
  const spinner2 = ora("Reading audio file...").start();
64557
- const absPath = resolve34(process.cwd(), audioPath);
64558
- const audioBuffer = await readFile24(absPath);
65031
+ const absPath = resolve33(process.cwd(), audioPath);
65032
+ const audioBuffer = await readFile23(absPath);
64559
65033
  spinner2.text = "Isolating vocals...";
64560
65034
  const elevenlabs = new ElevenLabsProvider();
64561
65035
  await elevenlabs.initialize({ apiKey });
@@ -64564,8 +65038,8 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
64564
65038
  spinner2.fail(result.error || "Audio isolation failed");
64565
65039
  exitWithError(apiError(result.error || "Audio isolation failed", true));
64566
65040
  }
64567
- const outputPath = resolve34(process.cwd(), options.output);
64568
- await writeFile31(outputPath, result.audioBuffer);
65041
+ const outputPath = resolve33(process.cwd(), options.output);
65042
+ await writeFile30(outputPath, result.audioBuffer);
64569
65043
  spinner2.succeed(source_default.green("Vocals isolated"));
64570
65044
  if (isJsonMode()) {
64571
65045
  outputSuccess({
@@ -64625,12 +65099,12 @@ audioCommand.command("clone-voice").description("Clone a voice from audio sample
64625
65099
  const spinner2 = ora("Reading audio samples...").start();
64626
65100
  const audioBuffers = [];
64627
65101
  for (const samplePath of samples) {
64628
- const absPath = resolve34(process.cwd(), samplePath);
64629
- if (!existsSync26(absPath)) {
65102
+ const absPath = resolve33(process.cwd(), samplePath);
65103
+ if (!existsSync25(absPath)) {
64630
65104
  spinner2.fail(`File not found: ${samplePath}`);
64631
65105
  exitWithError(notFoundError(samplePath));
64632
65106
  }
64633
- const buffer = await readFile24(absPath);
65107
+ const buffer = await readFile23(absPath);
64634
65108
  audioBuffers.push(buffer);
64635
65109
  }
64636
65110
  spinner2.text = `Cloning voice from ${audioBuffers.length} sample(s)...`;
@@ -64686,8 +65160,8 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64686
65160
  if (!options.language) {
64687
65161
  exitWithError(usageError("Target language is required. Use -l or --language"));
64688
65162
  }
64689
- const absPath = resolve34(process.cwd(), mediaPath);
64690
- if (!existsSync26(absPath)) {
65163
+ const absPath = resolve33(process.cwd(), mediaPath);
65164
+ if (!existsSync25(absPath)) {
64691
65165
  exitWithError(notFoundError(mediaPath));
64692
65166
  }
64693
65167
  const openaiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
@@ -64698,7 +65172,7 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64698
65172
  const isVideo = [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
64699
65173
  let audioPath = absPath;
64700
65174
  if (isVideo) {
64701
- const tempAudioPath = resolve34(dirname11(absPath), `temp-audio-${Date.now()}.mp3`);
65175
+ const tempAudioPath = resolve33(dirname11(absPath), `temp-audio-${Date.now()}.mp3`);
64702
65176
  try {
64703
65177
  execSafeSync("ffmpeg", ["-i", absPath, "-vn", "-acodec", "mp3", "-y", tempAudioPath]);
64704
65178
  audioPath = tempAudioPath;
@@ -64710,7 +65184,7 @@ audioCommand.command("dub").description("Dub audio/video to another language (tr
64710
65184
  spinner2.text = "Transcribing audio...";
64711
65185
  const whisper = new WhisperProvider();
64712
65186
  await whisper.initialize({ apiKey: openaiKey });
64713
- const audioBuffer = await readFile24(audioPath);
65187
+ const audioBuffer = await readFile23(audioPath);
64714
65188
  const audioBlob = new Blob([audioBuffer]);
64715
65189
  const transcriptResult = await whisper.transcribe(audioBlob, options.source);
64716
65190
  if (transcriptResult.status === "failed" || !transcriptResult.segments) {
@@ -64791,7 +65265,7 @@ ${segmentTexts}`,
64791
65265
  console.log();
64792
65266
  console.log(source_default.dim("Use without --analyze-only to generate dubbed audio"));
64793
65267
  if (options.output) {
64794
- const timingPath = resolve34(process.cwd(), options.output);
65268
+ const timingPath = resolve33(process.cwd(), options.output);
64795
65269
  const timingData = {
64796
65270
  sourcePath: absPath,
64797
65271
  sourceLanguage: transcriptResult.detectedLanguage || options.source || "auto",
@@ -64804,7 +65278,7 @@ ${segmentTexts}`,
64804
65278
  translated: translatedSegments[i]?.text || s.text
64805
65279
  }))
64806
65280
  };
64807
- await writeFile31(timingPath, JSON.stringify(timingData, null, 2));
65281
+ await writeFile30(timingPath, JSON.stringify(timingData, null, 2));
64808
65282
  console.log(`Timing saved to: ${source_default.bold(timingPath)}`);
64809
65283
  }
64810
65284
  return;
@@ -64829,12 +65303,12 @@ ${segmentTexts}`,
64829
65303
  spinner2.text = "Combining audio...";
64830
65304
  const combinedBuffer = Buffer.concat(dubbedAudioBuffers.map((a) => a.buffer));
64831
65305
  const outputExt = isVideo ? ".mp3" : extname7(absPath);
64832
- const defaultOutputPath2 = resolve34(
65306
+ const defaultOutputPath2 = resolve33(
64833
65307
  dirname11(absPath),
64834
65308
  `${basename8(absPath, extname7(absPath))}-${options.language}${outputExt}`
64835
65309
  );
64836
- const finalOutputPath = resolve34(process.cwd(), options.output || defaultOutputPath2);
64837
- await writeFile31(finalOutputPath, combinedBuffer);
65310
+ const finalOutputPath = resolve33(process.cwd(), options.output || defaultOutputPath2);
65311
+ await writeFile30(finalOutputPath, combinedBuffer);
64838
65312
  spinner2.succeed(source_default.green("Dubbing complete"));
64839
65313
  if (isJsonMode()) {
64840
65314
  outputSuccess({
@@ -64890,9 +65364,9 @@ audioCommand.command("duck").description("Auto-duck background music when voice
64890
65364
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
64891
65365
  }
64892
65366
  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");
65367
+ const absMusic = resolve33(process.cwd(), musicPath);
65368
+ const absVoice = resolve33(process.cwd(), options.voice);
65369
+ const outputPath = options.output ? resolve33(process.cwd(), options.output) : absMusic.replace(/(\.[^.]+)$/, "-ducked$1");
64896
65370
  const thresholdDb = parseFloat(options.threshold);
64897
65371
  const thresholdLinear = Math.pow(10, thresholdDb / 20);
64898
65372
  const ratio = parseFloat(options.ratio);
@@ -65500,7 +65974,7 @@ init_esm();
65500
65974
  init_source();
65501
65975
  init_ora();
65502
65976
  import { stat as stat4 } from "node:fs/promises";
65503
- import { resolve as resolve39, extname as extname9, basename as basename12 } from "node:path";
65977
+ import { resolve as resolve39, extname as extname10, basename as basename12 } from "node:path";
65504
65978
 
65505
65979
  // ../../node_modules/.pnpm/music-metadata@11.11.1/node_modules/music-metadata/lib/index.js
65506
65980
  init_lib();
@@ -69377,7 +69851,7 @@ mediaCommand.command("info").description("Get media file information").argument(
69377
69851
  const spinner2 = ora("Analyzing media...").start();
69378
69852
  try {
69379
69853
  const filePath = resolve39(process.cwd(), file);
69380
- const ext = extname9(filePath).toLowerCase();
69854
+ const ext = extname10(filePath).toLowerCase();
69381
69855
  const fileName = basename12(filePath);
69382
69856
  const fileStat = await stat4(filePath);
69383
69857
  const mediaType = detectMediaType2(ext);
@@ -69519,10 +69993,10 @@ init_engine();
69519
69993
  init_output();
69520
69994
  init_cost_tier();
69521
69995
  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";
69996
+ import { resolve as resolve40, basename as basename13, extname as extname11, join as join10 } from "node:path";
69523
69997
  var batchCommand = new Command("batch").description("Batch operations for processing multiple items");
69524
69998
  function detectMediaType3(filePath) {
69525
- const ext = extname10(filePath).toLowerCase();
69999
+ const ext = extname11(filePath).toLowerCase();
69526
70000
  const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
69527
70001
  const audioExts = [".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"];
69528
70002
  const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
@@ -69532,7 +70006,7 @@ function detectMediaType3(filePath) {
69532
70006
  return "video";
69533
70007
  }
69534
70008
  function isMediaFile(filePath) {
69535
- const ext = extname10(filePath).toLowerCase();
70009
+ const ext = extname11(filePath).toLowerCase();
69536
70010
  const mediaExts = [
69537
70011
  ".mp4",
69538
70012
  ".mov",
@@ -69590,7 +70064,7 @@ batchCommand.command("import").description("Import multiple media files from a d
69590
70064
  if (entry.isDirectory() && options.recursive) {
69591
70065
  await scanDir(entryPath);
69592
70066
  } else if (entry.isFile()) {
69593
- const ext = extname10(entry.name).toLowerCase();
70067
+ const ext = extname11(entry.name).toLowerCase();
69594
70068
  const matchesFilter = !filterExts || filterExts.includes(ext);
69595
70069
  if (matchesFilter && isMediaFile(entryPath)) {
69596
70070
  mediaFiles.push(entryPath);
@@ -69971,6 +70445,13 @@ var VALID_LLM_PROVIDERS = [
69971
70445
  "openrouter"
69972
70446
  ];
69973
70447
  var VALID_SCOPES = ["user", "project"];
70448
+ function envValueFor(meta) {
70449
+ return process.env[meta.envVar] || meta.envAliases?.map((alias) => process.env[alias]).find(Boolean);
70450
+ }
70451
+ function envValueForName(envVar) {
70452
+ if (envVar === "FAL_API_KEY") return process.env.FAL_API_KEY || process.env.FAL_KEY;
70453
+ return process.env[envVar];
70454
+ }
69974
70455
  function parseScope(raw) {
69975
70456
  const v = String(raw ?? "user");
69976
70457
  if (!VALID_SCOPES.includes(v)) {
@@ -70058,6 +70539,7 @@ var AI_FEATURES = [
70058
70539
  desc: "generate + edit",
70059
70540
  defaultProvider: "OpenAI gpt-image-2 (Artificial Analysis #1, since v0.56)",
70060
70541
  alsoAvailable: "Gemini Nano Banana, Grok Imagine",
70542
+ keyHint: "1 key",
70061
70543
  keys: [
70062
70544
  {
70063
70545
  configKey: "openai",
@@ -70112,10 +70594,11 @@ var AI_FEATURES = [
70112
70594
  desc: "generate + extend",
70113
70595
  defaultProvider: "Seedance 2.0 via fal.ai (Artificial Analysis #2 t2v + i2v, since v0.57)",
70114
70596
  alsoAvailable: "Grok Imagine, Kling, Runway Gen-4.5, Google Veo",
70597
+ keyHint: "1-2 keys",
70115
70598
  keys: [
70116
70599
  {
70117
70600
  configKey: "fal",
70118
- envVar: "FAL_KEY",
70601
+ envVar: "FAL_API_KEY",
70119
70602
  name: "Seedance 2.0 (via fal.ai)",
70120
70603
  url: "https://fal.ai/dashboard/keys",
70121
70604
  what: "ByteDance Seedance 2.0 text-to-video and image-to-video"
@@ -70127,11 +70610,20 @@ var AI_FEATURES = [
70127
70610
  desc: "recommended default, text-to-video + image-to-video",
70128
70611
  key: {
70129
70612
  configKey: "fal",
70130
- envVar: "FAL_KEY",
70613
+ envVar: "FAL_API_KEY",
70131
70614
  name: "Seedance 2.0 (via fal.ai)",
70132
70615
  url: "https://fal.ai/dashboard/keys",
70133
70616
  what: "ByteDance Seedance 2.0 text-to-video and image-to-video"
70134
70617
  },
70618
+ extraKeys: [
70619
+ {
70620
+ configKey: "imgbb",
70621
+ envVar: "IMGBB_API_KEY",
70622
+ name: "ImgBB",
70623
+ url: "https://api.imgbb.com/",
70624
+ what: "Image hosting for Seedance image-to-video uploads"
70625
+ }
70626
+ ],
70135
70627
  defaultFor: { kind: "video", value: "seedance" }
70136
70628
  },
70137
70629
  {
@@ -70156,6 +70648,15 @@ var AI_FEATURES = [
70156
70648
  url: "https://platform.klingai.com/",
70157
70649
  what: "Kling video generation"
70158
70650
  },
70651
+ extraKeys: [
70652
+ {
70653
+ configKey: "imgbb",
70654
+ envVar: "IMGBB_API_KEY",
70655
+ name: "ImgBB",
70656
+ url: "https://api.imgbb.com/",
70657
+ what: "Image hosting for Kling image-to-video uploads"
70658
+ }
70659
+ ],
70159
70660
  defaultFor: { kind: "video", value: "kling" }
70160
70661
  },
70161
70662
  {
@@ -70190,6 +70691,7 @@ var AI_FEATURES = [
70190
70691
  desc: "TTS, SFX, music, voice clone",
70191
70692
  defaultProvider: "ElevenLabs (paid, premium quality) \u2014 falls back to local Kokoro when no key (free, since v0.54)",
70192
70693
  alsoAvailable: "Replicate MusicGen (music only)",
70694
+ keyHint: "0-1 key",
70193
70695
  keys: [
70194
70696
  {
70195
70697
  configKey: "elevenlabs",
@@ -70235,6 +70737,7 @@ var AI_FEATURES = [
70235
70737
  desc: "captions, grade, reframe, motion graphics",
70236
70738
  defaultProvider: "Whisper (transcription) + Claude (reasoning)",
70237
70739
  alsoAvailable: "",
70740
+ keyHint: "2 keys",
70238
70741
  keys: [
70239
70742
  {
70240
70743
  configKey: "openai",
@@ -70268,8 +70771,11 @@ function applyProviderDefault(config2, choice) {
70268
70771
  }
70269
70772
  }
70270
70773
  function isProviderChoiceConfigured(config2, choice) {
70271
- if (choice.key) {
70272
- const value = config2.providers[choice.key.configKey];
70774
+ const keys = [choice.key, ...getChoiceExtraKeys(config2, choice)].filter(
70775
+ (keyDef) => Boolean(keyDef)
70776
+ );
70777
+ for (const keyDef of keys) {
70778
+ const value = config2.providers[keyDef.configKey];
70273
70779
  if (value) return true;
70274
70780
  }
70275
70781
  if (choice.defaultFor?.kind === "image") {
@@ -70280,6 +70786,96 @@ function isProviderChoiceConfigured(config2, choice) {
70280
70786
  }
70281
70787
  return false;
70282
70788
  }
70789
+ function getConfiguredUploadProvider(config2) {
70790
+ return process.env.VIBE_UPLOAD_PROVIDER?.toLowerCase() === "s3" || config2.upload.provider === "s3" ? "s3" : "imgbb";
70791
+ }
70792
+ function hasVideoFeature(features) {
70793
+ return features.some((feature) => feature.label === "Videos");
70794
+ }
70795
+ async function promptOptionalValue(label, currentValue) {
70796
+ const suffix = currentValue ? source_default.dim(` [${currentValue}]`) : "";
70797
+ const value = await prompt2(source_default.cyan(` ${label}${suffix}: `));
70798
+ const trimmed = value.trim();
70799
+ return trimmed || currentValue;
70800
+ }
70801
+ async function configureUploadHost(config2) {
70802
+ console.log(source_default.dim("2b. Upload host"));
70803
+ console.log(source_default.bold("Where should image-to-video upload reference images?"));
70804
+ console.log(
70805
+ source_default.dim(
70806
+ " Seedance and Kling need a temporary HTTPS image URL when you start from a local image."
70807
+ )
70808
+ );
70809
+ console.log();
70810
+ const currentProvider = getConfiguredUploadProvider(config2);
70811
+ const selected = await promptSelect(
70812
+ source_default.cyan(" Upload host [1-3]: "),
70813
+ [
70814
+ `ImgBB ${source_default.dim("- easiest setup, third-party image hosting (default)")}`,
70815
+ `S3 ${source_default.dim("- your own bucket, better control/privacy")}`,
70816
+ `Back ${source_default.dim("(choose different features)")}`
70817
+ ],
70818
+ currentProvider === "s3" ? 1 : 0,
70819
+ { backIndex: 2 }
70820
+ );
70821
+ console.log();
70822
+ if (selected === 2) {
70823
+ return "back";
70824
+ }
70825
+ if (selected === 0) {
70826
+ config2.upload.provider = "imgbb";
70827
+ process.env.VIBE_UPLOAD_PROVIDER = "imgbb";
70828
+ console.log(source_default.dim(" ImgBB selected. Seedance/Kling setup will ask for IMGBB_API_KEY."));
70829
+ console.log();
70830
+ return "continue";
70831
+ }
70832
+ config2.upload.provider = "s3";
70833
+ process.env.VIBE_UPLOAD_PROVIDER = "s3";
70834
+ config2.upload.s3 = config2.upload.s3 ?? {};
70835
+ const envBucket = process.env.VIBE_UPLOAD_S3_BUCKET;
70836
+ const envRegion = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
70837
+ const envPrefix = process.env.VIBE_UPLOAD_S3_PREFIX;
70838
+ const envTtl = process.env.VIBE_UPLOAD_TTL_SECONDS;
70839
+ console.log(source_default.dim(" S3 selected. AWS credentials stay in your shell/.env/AWS profile."));
70840
+ console.log(source_default.dim(" Required at runtime: AWS credentials, region, and bucket."));
70841
+ console.log();
70842
+ const bucket = await promptOptionalValue("S3 bucket", envBucket ?? config2.upload.s3.bucket);
70843
+ const region = await promptOptionalValue("AWS region", envRegion ?? config2.upload.s3.region);
70844
+ const prefix = await promptOptionalValue(
70845
+ "S3 prefix",
70846
+ envPrefix ?? config2.upload.s3.prefix ?? "vibeframe/tmp"
70847
+ );
70848
+ const ttlValue = await promptOptionalValue(
70849
+ "Upload URL TTL seconds",
70850
+ envTtl ?? String(config2.upload.ttlSeconds ?? 3600)
70851
+ );
70852
+ if (bucket) config2.upload.s3.bucket = bucket;
70853
+ if (region) config2.upload.s3.region = region;
70854
+ if (prefix) config2.upload.s3.prefix = prefix;
70855
+ const ttlSeconds = ttlValue ? Number.parseInt(ttlValue, 10) : NaN;
70856
+ if (Number.isFinite(ttlSeconds) && ttlSeconds > 0) {
70857
+ config2.upload.ttlSeconds = ttlSeconds;
70858
+ }
70859
+ if (!config2.upload.s3.bucket || !config2.upload.s3.region) {
70860
+ console.log(
70861
+ source_default.yellow(
70862
+ " \u26A0 S3 selected, but bucket/region is incomplete. Set VIBE_UPLOAD_S3_BUCKET and AWS_REGION before image-to-video."
70863
+ )
70864
+ );
70865
+ } else {
70866
+ console.log(
70867
+ ` ${source_default.green("\u2713")} S3 upload host configured ${source_default.dim(`(${config2.upload.s3.bucket}/${config2.upload.s3.prefix ?? "vibeframe/tmp"})`)}`
70868
+ );
70869
+ }
70870
+ console.log();
70871
+ return "continue";
70872
+ }
70873
+ function getChoiceExtraKeys(config2, choice) {
70874
+ if (config2.upload.provider === "s3") {
70875
+ return [];
70876
+ }
70877
+ return choice.extraKeys ?? [];
70878
+ }
70283
70879
  function isFeatureConfigured(config2, feature) {
70284
70880
  if (feature.providerChoices?.some((choice) => isProviderChoiceConfigured(config2, choice))) {
70285
70881
  return true;
@@ -70323,7 +70919,7 @@ async function runNonInteractiveSetup(opts) {
70323
70919
  if (opts.importEnv) {
70324
70920
  loadEnv();
70325
70921
  for (const meta of getAllApiKeys()) {
70326
- const envValue = process.env[meta.envVar];
70922
+ const envValue = envValueFor(meta);
70327
70923
  if (!envValue) continue;
70328
70924
  const existing = config2.providers[meta.configKey];
70329
70925
  if (existing === envValue) continue;
@@ -70389,6 +70985,7 @@ async function runNonInteractiveSetup(opts) {
70389
70985
  }
70390
70986
  async function runSetupWizard(fullSetup = false, scope = "user") {
70391
70987
  console.log();
70988
+ loadEnv();
70392
70989
  const cfgPath = getConfigPath(scope).replace(homedir5(), "~");
70393
70990
  const scopeNote = scope === "project" ? "applies to this project only \xB7 add .vibeframe/ to .gitignore" : "applies to every project for this user";
70394
70991
  console.log(source_default.bold.magenta("VibeFrame Setup") + source_default.bold(` \u2014 ${scope} scope`));
@@ -70452,7 +71049,7 @@ async function runSetupWizard(fullSetup = false, scope = "user") {
70452
71049
  },
70453
71050
  {
70454
71051
  configKey: "fal",
70455
- envVar: "FAL_KEY",
71052
+ envVar: "FAL_API_KEY",
70456
71053
  name: "fal.ai",
70457
71054
  url: "https://fal.ai/dashboard/keys",
70458
71055
  what: "Seedance 2.0 \u2014 video generation, default since v0.57"
@@ -70540,8 +71137,7 @@ async function runAIFeaturesSetup(config2, _scope) {
70540
71137
  const backIndex = AI_FEATURES.length;
70541
71138
  const featureLabels = [
70542
71139
  ...AI_FEATURES.map((f) => {
70543
- const keyCount = f.keys.length;
70544
- const tag = source_default.dim(`${keyCount} key${keyCount > 1 ? "s" : ""}`);
71140
+ const tag = source_default.dim(f.keyHint ?? `${f.keys.length} key${f.keys.length > 1 ? "s" : ""}`);
70545
71141
  return `${f.label} ${source_default.dim(`(${f.desc})`)} ${tag}`;
70546
71142
  }),
70547
71143
  `Back ${source_default.dim("(return to setup checkpoint)")}`
@@ -70564,6 +71160,13 @@ async function runAIFeaturesSetup(config2, _scope) {
70564
71160
  console.log();
70565
71161
  return { tryCommand: "vibe --help", selectedFeatures: [] };
70566
71162
  }
71163
+ if (hasVideoFeature(selectedFeatures)) {
71164
+ const uploadResult = await configureUploadHost(config2);
71165
+ if (uploadResult === "back") {
71166
+ console.log();
71167
+ continue featureSelection;
71168
+ }
71169
+ }
70567
71170
  const plannedKeys = /* @__PURE__ */ new Map();
70568
71171
  console.log(source_default.dim("3. Providers"));
70569
71172
  console.log(source_default.bold("Choose providers for each selected feature"));
@@ -70586,7 +71189,12 @@ async function runAIFeaturesSetup(config2, _scope) {
70586
71189
  (choice) => isProviderChoiceConfigured(config2, choice)
70587
71190
  );
70588
71191
  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");
71192
+ const keyHint = choice.keyless ? source_default.dim("no key") : choice.key ? source_default.dim(
71193
+ [
71194
+ choice.key.envVar,
71195
+ ...getChoiceExtraKeys(config2, choice).map((keyDef) => keyDef.envVar)
71196
+ ].filter(Boolean).join(" + ")
71197
+ ) : source_default.dim("no key");
70590
71198
  const defaultHint = choice === feature.providerChoices?.[0] ? source_default.dim("recommended default") : "";
70591
71199
  return [choice.label, defaultHint, source_default.dim(`- ${choice.desc}`), keyHint].filter(Boolean).join(" ");
70592
71200
  });
@@ -70612,6 +71220,9 @@ async function runAIFeaturesSetup(config2, _scope) {
70612
71220
  }
70613
71221
  for (const choice of selectedChoices) {
70614
71222
  if (choice.key) addUniqueKey(keysForFeature, choice.key);
71223
+ for (const keyDef of getChoiceExtraKeys(config2, choice)) {
71224
+ addUniqueKey(keysForFeature, keyDef);
71225
+ }
70615
71226
  }
70616
71227
  if (selectedChoices.some((choice) => choice.keyless)) {
70617
71228
  console.log(source_default.dim(" No API key needed for selected local provider(s)."));
@@ -70649,7 +71260,7 @@ async function runAIFeaturesSetup(config2, _scope) {
70649
71260
  }
70650
71261
  loadEnv();
70651
71262
  const configValue = config2.providers[keyDef.configKey];
70652
- const envValue = process.env[keyDef.envVar];
71263
+ const envValue = envValueForName(keyDef.envVar);
70653
71264
  if (configValue || envValue) {
70654
71265
  const value = configValue || envValue;
70655
71266
  const source = configValue ? "config" : ".env";
@@ -70697,7 +71308,7 @@ async function collectKeys(config2, keys) {
70697
71308
  loadEnv();
70698
71309
  for (const keyDef of keys) {
70699
71310
  const configValue = config2.providers[keyDef.configKey];
70700
- const envValue = process.env[keyDef.envVar];
71311
+ const envValue = envValueForName(keyDef.envVar);
70701
71312
  if (configValue) {
70702
71313
  console.log(
70703
71314
  ` ${source_default.green("\u2713")} ${keyDef.name.padEnd(14)} ${maskApiKey(configValue)} ${source_default.dim("(config)")}`
@@ -70958,7 +71569,7 @@ async function showConfig(opts = { verbose: false }) {
70958
71569
  { key: "openai", name: "OpenAI", env: "OPENAI_API_KEY" },
70959
71570
  { key: "google", name: "Google", env: "GOOGLE_API_KEY" },
70960
71571
  { key: "xai", name: "xAI", env: "XAI_API_KEY" },
70961
- { key: "fal", name: "fal.ai", env: "FAL_KEY" },
71572
+ { key: "fal", name: "fal.ai", env: "FAL_API_KEY" },
70962
71573
  { key: "elevenlabs", name: "ElevenLabs", env: "ELEVENLABS_API_KEY" },
70963
71574
  { key: "runway", name: "Runway", env: "RUNWAY_API_SECRET" },
70964
71575
  { key: "kling", name: "Kling", env: "KLING_API_KEY" },
@@ -70969,7 +71580,7 @@ async function showConfig(opts = { verbose: false }) {
70969
71580
  let unsetCount = 0;
70970
71581
  for (const p of providerKeys) {
70971
71582
  const configValue = config2?.providers[p.key];
70972
- const dotenvValue = dotenvKeys[p.env];
71583
+ const dotenvValue = dotenvKeys[p.env] || (p.env === "FAL_API_KEY" ? dotenvKeys.FAL_KEY : void 0);
70973
71584
  if (configValue || dotenvValue) {
70974
71585
  const sources = [];
70975
71586
  if (configValue) sources.push("config");
@@ -71265,11 +71876,22 @@ OPENROUTER_API_KEY= # multiplexes any provider above
71265
71876
 
71266
71877
  # \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
71878
  ELEVENLABS_API_KEY= # paid TTS / SFX / music (Kokoro is the free fallback)
71268
- FAL_KEY= # Seedance 2.0 \u2014 default video provider since v0.57
71879
+ FAL_API_KEY= # Seedance 2.0 \u2014 default video provider since v0.57
71880
+ IMGBB_API_KEY= # image hosting for Seedance/Kling image-to-video
71269
71881
  RUNWAY_API_SECRET= # Runway Gen-4.5 video
71270
71882
  KLING_API_KEY= # Kling video
71271
71883
  REPLICATE_API_TOKEN= # MusicGen, real-esrgan, etc.
71272
- IMGBB_API_KEY= # public image hosting (rare \u2014 some Replicate flows need it)
71884
+
71885
+ # \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
71886
+ # Default is ImgBB. Set these only if you want temporary URLs from your own S3 bucket.
71887
+ # VIBE_UPLOAD_PROVIDER=s3 # imgbb | s3
71888
+ # VIBE_UPLOAD_TTL_SECONDS=3600
71889
+ # VIBE_UPLOAD_S3_BUCKET=
71890
+ # VIBE_UPLOAD_S3_PREFIX=vibeframe/tmp
71891
+ # AWS_REGION=
71892
+ # AWS_ACCESS_KEY_ID=
71893
+ # AWS_SECRET_ACCESS_KEY=
71894
+ # AWS_SESSION_TOKEN=
71273
71895
  `;
71274
71896
  }
71275
71897
  var GITIGNORE_ADDITIONS = `
@@ -72129,7 +72751,9 @@ async function runDiagnostics() {
72129
72751
  let readyCount = FREE_COMMANDS.length;
72130
72752
  let totalCount = FREE_COMMANDS.length;
72131
72753
  for (const [envVar, commands] of Object.entries(COMMAND_KEY_MAP)) {
72132
- const configured = !!process.env[envVar];
72754
+ const configured = Boolean(
72755
+ process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean)
72756
+ );
72133
72757
  const providerName = Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ?? envVar;
72134
72758
  providers[providerName] = { envVar, configured, commands };
72135
72759
  totalCount += commands.length;
@@ -72418,7 +73042,7 @@ async function runLiveKeyTests(results) {
72418
73042
  }
72419
73043
  const { testKey: testKey2 } = await Promise.resolve().then(() => (init_key_live_test(), key_live_test_exports));
72420
73044
  for (const [name, info] of configured) {
72421
- const value = process.env[info.envVar];
73045
+ const value = process.env[info.envVar] || PROVIDER_ENV_ALIASES[info.envVar]?.map((alias) => process.env[alias]).find(Boolean);
72422
73046
  if (!value) continue;
72423
73047
  process.stdout.write(` ${name.padEnd(12)} `);
72424
73048
  const result = await testKey2(name, value);