@vibeframe/cli 0.112.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7066,8 +7066,8 @@ var init_api_keys = __esm({
7066
7066
  envVar: "OPENAI_API_KEY",
7067
7067
  label: "OpenAI",
7068
7068
  showInSetup: true,
7069
- setupDescription: "gpt-image-2 image gen ($, default since v0.56), Whisper transcribe, Agent",
7070
- envExampleComment: "OpenAI API Key (Whisper transcription, gpt-image-2 \u2014 default text-to-image since v0.56)",
7069
+ setupDescription: "gpt-image-2 image gen ($, default since v0.56), gpt-4o-mini-tts narration, Whisper transcribe, Agent",
7070
+ envExampleComment: "OpenAI API Key (Whisper transcription, gpt-4o-mini-tts narration, gpt-image-2 \u2014 default text-to-image since v0.56)",
7071
7071
  envExampleUrl: "https://platform.openai.com/api-keys",
7072
7072
  keyFormat: { prefix: /^sk-/, example: "sk-..." }
7073
7073
  });
@@ -9174,8 +9174,8 @@ var init_openai = __esm({
9174
9174
  id: "openai",
9175
9175
  label: "OpenAI",
9176
9176
  apiKey: "openai",
9177
- kinds: ["llm", "image", "transcription"],
9178
- resolverPriority: { image: 1 },
9177
+ kinds: ["llm", "image", "transcription", "speech"],
9178
+ resolverPriority: { image: 1, speech: 2 },
9179
9179
  commandsUnlocked: [
9180
9180
  "agent -p openai",
9181
9181
  "generate image -p openai",
@@ -11597,16 +11597,19 @@ var init_elevenlabs = __esm({
11597
11597
  });
11598
11598
 
11599
11599
  // ../ai-providers/dist/kokoro/KokoroProvider.js
11600
- async function loadKokoroFactory() {
11600
+ async function loadKokoroRuntime() {
11601
11601
  if (factoryOverride)
11602
- return factoryOverride;
11602
+ return { factory: factoryOverride, device: "cpu" };
11603
11603
  try {
11604
11604
  const mod = await import("kokoro-js");
11605
- return mod.KokoroTTS;
11605
+ return { factory: mod.KokoroTTS, device: "cpu" };
11606
11606
  } catch (err) {
11607
- const fallback2 = await loadKokoroFromWorkspace();
11608
- if (fallback2)
11609
- return fallback2;
11607
+ const workspace = await loadKokoroFromWorkspace();
11608
+ if (workspace)
11609
+ return { factory: workspace, device: "cpu" };
11610
+ const bundled = await loadBundledKokoroRuntime();
11611
+ if (bundled)
11612
+ return bundled;
11610
11613
  throw mapKokoroImportError(err);
11611
11614
  }
11612
11615
  }
@@ -11626,10 +11629,51 @@ async function loadKokoroFromWorkspace(baseDir = process.cwd()) {
11626
11629
  return null;
11627
11630
  }
11628
11631
  }
11632
+ async function loadBundledKokoroRuntime(runtimeDir = process.env.VIBE_KOKORO_RUNTIME) {
11633
+ const base = runtimeDir?.trim();
11634
+ if (!base)
11635
+ return null;
11636
+ try {
11637
+ const { readFile: readFile56, writeFile: writeFile61, mkdir: mkdir31 } = await import("node:fs/promises");
11638
+ const { join: join30, resolve: resolve86 } = await import("node:path");
11639
+ const { pathToFileURL } = await import("node:url");
11640
+ const { homedir: homedir8 } = await import("node:os");
11641
+ const transformersDir = resolve86(base, "node_modules", "@huggingface", "transformers");
11642
+ const ortDistDir = resolve86(base, "node_modules", "onnxruntime-web", "dist");
11643
+ const transformers = await import(pathToFileURL(join30(transformersDir, "dist", "transformers.web.js")).href);
11644
+ const cacheRoot = join30(homedir8(), ".cache", "vibeframe", "models");
11645
+ transformers.env.allowLocalModels = false;
11646
+ transformers.env.useCustomCache = true;
11647
+ transformers.env.customCache = {
11648
+ async match(key) {
11649
+ try {
11650
+ return new Response(await readFile56(join30(cacheRoot, encodeURIComponent(String(key)))));
11651
+ } catch {
11652
+ return void 0;
11653
+ }
11654
+ },
11655
+ async put(key, response) {
11656
+ try {
11657
+ const buf = Buffer.from(await response.arrayBuffer());
11658
+ await mkdir31(cacheRoot, { recursive: true });
11659
+ await writeFile61(join30(cacheRoot, encodeURIComponent(String(key))), buf);
11660
+ } catch {
11661
+ }
11662
+ }
11663
+ };
11664
+ transformers.env.backends.onnx.wasm.wasmPaths = pathToFileURL(ortDistDir + "/").href;
11665
+ const factory = await loadKokoroFromWorkspace(base);
11666
+ if (!factory)
11667
+ return null;
11668
+ return { factory, device: "cpu" };
11669
+ } catch {
11670
+ return null;
11671
+ }
11672
+ }
11629
11673
  function mapKokoroImportError(err) {
11630
11674
  const code = err instanceof Error && "code" in err ? err.code : void 0;
11631
11675
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
11632
- return new Error("Local Kokoro TTS is unavailable: optional dependency kokoro-js is not installed (its native dependencies may have failed to build on this platform). Install it with `npm i kokoro-js` \u2014 run it inside your workspace folder when using the Claude Desktop extension \u2014 or set ELEVENLABS_API_KEY to use the elevenlabs TTS provider instead.");
11676
+ return new Error("Local Kokoro TTS is unavailable: kokoro-js could not be loaded from the npm install, the workspace node_modules, or the bundled extension runtime. Set OPENAI_API_KEY or ELEVENLABS_API_KEY to use a cloud voice instead (ttsProvider: openai / elevenlabs), or install the engine locally with `npm i kokoro-js` in your workspace folder.");
11633
11677
  }
11634
11678
  return err instanceof Error ? err : new Error(String(err));
11635
11679
  }
@@ -11637,10 +11681,12 @@ function loadModel(progress) {
11637
11681
  if (modelPromise)
11638
11682
  return modelPromise;
11639
11683
  modelPromise = (async () => {
11640
- const factory = await loadKokoroFactory();
11641
- return factory.from_pretrained(KOKORO_MODEL_ID, {
11684
+ const runtime = await loadKokoroRuntime();
11685
+ const override = process.env.VIBE_ONNX_DEVICE;
11686
+ const device = override === "cpu" || override === "wasm" ? override : runtime.device;
11687
+ return runtime.factory.from_pretrained(KOKORO_MODEL_ID, {
11642
11688
  dtype: "q8",
11643
- device: "cpu",
11689
+ device,
11644
11690
  progress_callback: progress ? (raw) => progress(normaliseEvent(raw)) : void 0
11645
11691
  });
11646
11692
  })().catch((err) => {
@@ -11724,7 +11770,7 @@ var init_kokoro = __esm({
11724
11770
  label: "Kokoro (local)",
11725
11771
  apiKey: null,
11726
11772
  kinds: ["speech"],
11727
- resolverPriority: { speech: 2 }
11773
+ resolverPriority: { speech: 3 }
11728
11774
  });
11729
11775
  }
11730
11776
  });
@@ -11996,12 +12042,119 @@ var init_openai_image = __esm({
11996
12042
  }
11997
12043
  });
11998
12044
 
12045
+ // ../ai-providers/dist/openai-tts/OpenAiTtsProvider.js
12046
+ var OPENAI_TTS_VOICES, DEFAULT_VOICE, DEFAULT_MODEL2, TTS_429_MAX_RETRIES2, TTS_429_RETRY_DELAY_MS2, OpenAiTtsProvider, openaiTtsProvider;
12047
+ var init_OpenAiTtsProvider = __esm({
12048
+ "../ai-providers/dist/openai-tts/OpenAiTtsProvider.js"() {
12049
+ "use strict";
12050
+ OPENAI_TTS_VOICES = [
12051
+ "alloy",
12052
+ "ash",
12053
+ "ballad",
12054
+ "cedar",
12055
+ "coral",
12056
+ "echo",
12057
+ "fable",
12058
+ "marin",
12059
+ "nova",
12060
+ "onyx",
12061
+ "sage",
12062
+ "shimmer",
12063
+ "verse"
12064
+ ];
12065
+ DEFAULT_VOICE = "marin";
12066
+ DEFAULT_MODEL2 = "gpt-4o-mini-tts";
12067
+ TTS_429_MAX_RETRIES2 = 1;
12068
+ TTS_429_RETRY_DELAY_MS2 = 2e3;
12069
+ OpenAiTtsProvider = class {
12070
+ constructor() {
12071
+ this.id = "openai";
12072
+ this.name = "OpenAI TTS";
12073
+ this.description = "OpenAI cloud text-to-speech (gpt-4o-mini-tts)";
12074
+ this.capabilities = ["text-to-speech"];
12075
+ this.isAvailable = true;
12076
+ this.baseUrl = "https://api.openai.com/v1";
12077
+ }
12078
+ async initialize(config2) {
12079
+ this.apiKey = config2.apiKey;
12080
+ if (config2.baseUrl) {
12081
+ this.baseUrl = config2.baseUrl;
12082
+ }
12083
+ }
12084
+ isConfigured() {
12085
+ return !!this.apiKey;
12086
+ }
12087
+ async textToSpeech(text, options = {}) {
12088
+ if (!this.apiKey) {
12089
+ return { success: false, error: "OpenAI API key not configured" };
12090
+ }
12091
+ const voice = (options.voice ?? DEFAULT_VOICE).toLowerCase();
12092
+ if (!OPENAI_TTS_VOICES.includes(voice)) {
12093
+ return {
12094
+ success: false,
12095
+ error: `Unknown OpenAI voice "${options.voice}". Available voices: ${OPENAI_TTS_VOICES.join(", ")}.`
12096
+ };
12097
+ }
12098
+ try {
12099
+ let response;
12100
+ for (let attempt = 0; ; attempt++) {
12101
+ response = await fetch(`${this.baseUrl}/audio/speech`, {
12102
+ method: "POST",
12103
+ headers: {
12104
+ Authorization: `Bearer ${this.apiKey}`,
12105
+ "Content-Type": "application/json"
12106
+ },
12107
+ body: JSON.stringify({
12108
+ model: options.model ?? DEFAULT_MODEL2,
12109
+ input: text,
12110
+ voice,
12111
+ response_format: "mp3",
12112
+ ...options.speed !== void 0 && { speed: options.speed },
12113
+ ...options.instructions !== void 0 && { instructions: options.instructions }
12114
+ })
12115
+ });
12116
+ if (response.status === 429 && attempt < TTS_429_MAX_RETRIES2) {
12117
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, TTS_429_RETRY_DELAY_MS2 * (attempt + 1)));
12118
+ continue;
12119
+ }
12120
+ break;
12121
+ }
12122
+ if (!response.ok) {
12123
+ const error = await response.text();
12124
+ return { success: false, error: `OpenAI TTS failed: ${error}` };
12125
+ }
12126
+ const arrayBuffer = await response.arrayBuffer();
12127
+ return {
12128
+ success: true,
12129
+ audioBuffer: Buffer.from(arrayBuffer),
12130
+ characterCount: text.length
12131
+ };
12132
+ } catch (error) {
12133
+ return {
12134
+ success: false,
12135
+ error: error instanceof Error ? error.message : "Unknown error"
12136
+ };
12137
+ }
12138
+ }
12139
+ };
12140
+ openaiTtsProvider = new OpenAiTtsProvider();
12141
+ }
12142
+ });
12143
+
12144
+ // ../ai-providers/dist/openai-tts/index.js
12145
+ var init_openai_tts = __esm({
12146
+ "../ai-providers/dist/openai-tts/index.js"() {
12147
+ "use strict";
12148
+ init_OpenAiTtsProvider();
12149
+ }
12150
+ });
12151
+
11999
12152
  // ../ai-providers/dist/runway/RunwayProvider.js
12000
- var DEFAULT_MODEL2, RunwayProvider, runwayProvider;
12153
+ var DEFAULT_MODEL3, RunwayProvider, runwayProvider;
12001
12154
  var init_RunwayProvider = __esm({
12002
12155
  "../ai-providers/dist/runway/RunwayProvider.js"() {
12003
12156
  "use strict";
12004
- DEFAULT_MODEL2 = "gen4.5";
12157
+ DEFAULT_MODEL3 = "gen4.5";
12005
12158
  RunwayProvider = class _RunwayProvider {
12006
12159
  constructor() {
12007
12160
  this.id = "runway";
@@ -12044,7 +12197,7 @@ var init_RunwayProvider = __esm({
12044
12197
  "1:1": "960:960"
12045
12198
  };
12046
12199
  const apiRatio = ratioMap[options?.aspectRatio || "16:9"] || "1280:720";
12047
- const model = options?.model || DEFAULT_MODEL2;
12200
+ const model = options?.model || DEFAULT_MODEL3;
12048
12201
  if (!options?.referenceImage && model !== "gen4.5") {
12049
12202
  return {
12050
12203
  id: "",
@@ -12306,11 +12459,11 @@ var init_runway = __esm({
12306
12459
 
12307
12460
  // ../ai-providers/dist/kling/KlingProvider.js
12308
12461
  import { createHmac } from "node:crypto";
12309
- var DEFAULT_MODEL3, STD_MODE_MODELS, KlingProvider, klingProvider;
12462
+ var DEFAULT_MODEL4, STD_MODE_MODELS, KlingProvider, klingProvider;
12310
12463
  var init_KlingProvider = __esm({
12311
12464
  "../ai-providers/dist/kling/KlingProvider.js"() {
12312
12465
  "use strict";
12313
- DEFAULT_MODEL3 = "kling-v2-5-turbo";
12466
+ DEFAULT_MODEL4 = "kling-v2-5-turbo";
12314
12467
  STD_MODE_MODELS = ["kling-v2-5-turbo", "kling-v2-6", "kling-v3", "kling-v3-omni"];
12315
12468
  KlingProvider = class {
12316
12469
  constructor() {
@@ -12373,7 +12526,7 @@ var init_KlingProvider = __esm({
12373
12526
  }
12374
12527
  try {
12375
12528
  const token = this.generateToken();
12376
- const model = options?.model || DEFAULT_MODEL3;
12529
+ const model = options?.model || DEFAULT_MODEL4;
12377
12530
  let mode = options?.mode || "std";
12378
12531
  if (!STD_MODE_MODELS.includes(model) && mode === "std") {
12379
12532
  mode = "pro";
@@ -12744,11 +12897,11 @@ var init_kling = __esm({
12744
12897
  });
12745
12898
 
12746
12899
  // ../ai-providers/dist/grok/GrokProvider.js
12747
- var DEFAULT_MODEL4, GrokProvider, grokProvider;
12900
+ var DEFAULT_MODEL5, GrokProvider, grokProvider;
12748
12901
  var init_GrokProvider = __esm({
12749
12902
  "../ai-providers/dist/grok/GrokProvider.js"() {
12750
12903
  "use strict";
12751
- DEFAULT_MODEL4 = "grok-imagine-video";
12904
+ DEFAULT_MODEL5 = "grok-imagine-video";
12752
12905
  GrokProvider = class {
12753
12906
  constructor() {
12754
12907
  this.id = "grok";
@@ -12915,7 +13068,7 @@ var init_GrokProvider = __esm({
12915
13068
  try {
12916
13069
  const duration = Math.round(Math.min(15, Math.max(1, options?.duration || 5)));
12917
13070
  const body = {
12918
- model: DEFAULT_MODEL4,
13071
+ model: DEFAULT_MODEL5,
12919
13072
  prompt: prompt3,
12920
13073
  duration,
12921
13074
  aspect_ratio: options?.aspectRatio || "16:9"
@@ -18122,9 +18275,11 @@ __export(dist_exports2, {
18122
18275
  KOKORO_MODEL_ID: () => KOKORO_MODEL_ID,
18123
18276
  KlingProvider: () => KlingProvider,
18124
18277
  KokoroProvider: () => KokoroProvider,
18278
+ OPENAI_TTS_VOICES: () => OPENAI_TTS_VOICES,
18125
18279
  OllamaProvider: () => OllamaProvider,
18126
18280
  OpenAIImageProvider: () => OpenAIImageProvider,
18127
18281
  OpenAIProvider: () => OpenAIProvider,
18282
+ OpenAiTtsProvider: () => OpenAiTtsProvider,
18128
18283
  ReplicateProvider: () => ReplicateProvider,
18129
18284
  RunwayProvider: () => RunwayProvider,
18130
18285
  WhisperProvider: () => WhisperProvider,
@@ -18148,9 +18303,12 @@ __export(dist_exports2, {
18148
18303
  isGeminiTextModelAlias: () => isGeminiTextModelAlias,
18149
18304
  klingProvider: () => klingProvider,
18150
18305
  kokoroProvider: () => kokoroProvider,
18306
+ loadBundledKokoroRuntime: () => loadBundledKokoroRuntime,
18307
+ loadKokoroFromWorkspace: () => loadKokoroFromWorkspace,
18151
18308
  ollamaProvider: () => ollamaProvider,
18152
18309
  openaiImageProvider: () => openaiImageProvider,
18153
18310
  openaiProvider: () => openaiProvider,
18311
+ openaiTtsProvider: () => openaiTtsProvider,
18154
18312
  providerRegistry: () => providerRegistry,
18155
18313
  replicateProvider: () => replicateProvider,
18156
18314
  resolveGeminiTextModel: () => resolveGeminiTextModel,
@@ -18173,6 +18331,7 @@ var init_dist2 = __esm({
18173
18331
  init_elevenlabs();
18174
18332
  init_kokoro();
18175
18333
  init_openai_image();
18334
+ init_openai_tts();
18176
18335
  init_runway();
18177
18336
  init_kling();
18178
18337
  init_grok();
@@ -26347,10 +26506,22 @@ var init_api_key = __esm({
26347
26506
 
26348
26507
  // src/commands/_shared/tts-resolve.ts
26349
26508
  async function resolveTtsProvider(preferred = "auto") {
26350
- const choice = preferred === "auto" ? await getConfiguredApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
26509
+ let choice = preferred;
26510
+ if (choice === "auto") {
26511
+ if (await getConfiguredApiKey("ELEVENLABS_API_KEY")) {
26512
+ choice = "elevenlabs";
26513
+ } else if (await getConfiguredApiKey("OPENAI_API_KEY")) {
26514
+ choice = "openai";
26515
+ } else {
26516
+ choice = "kokoro";
26517
+ }
26518
+ }
26351
26519
  if (choice === "elevenlabs") {
26352
26520
  return buildElevenLabs();
26353
26521
  }
26522
+ if (choice === "openai") {
26523
+ return buildOpenAi();
26524
+ }
26354
26525
  return buildKokoro();
26355
26526
  }
26356
26527
  async function buildElevenLabs() {
@@ -26366,6 +26537,19 @@ async function buildElevenLabs() {
26366
26537
  });
26367
26538
  return { provider: "elevenlabs", audioExtension: "mp3", call };
26368
26539
  }
26540
+ async function buildOpenAi() {
26541
+ const key = await getApiKey("OPENAI_API_KEY", "OpenAI");
26542
+ if (!key) {
26543
+ throw new TtsKeyMissingError("openai");
26544
+ }
26545
+ const provider = new OpenAiTtsProvider();
26546
+ await provider.initialize({ apiKey: key });
26547
+ const call = async (text, opts) => provider.textToSpeech(text, {
26548
+ voice: opts?.voice,
26549
+ speed: opts?.speed
26550
+ });
26551
+ return { provider: "openai", audioExtension: "mp3", call };
26552
+ }
26369
26553
  async function buildKokoro() {
26370
26554
  const provider = new KokoroProvider();
26371
26555
  await provider.initialize({});
@@ -26378,11 +26562,11 @@ async function buildKokoro() {
26378
26562
  }
26379
26563
  function parseTtsProviderName(value) {
26380
26564
  if (!value) return "auto";
26381
- if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
26565
+ if (value === "auto" || value === "elevenlabs" || value === "openai" || value === "kokoro") {
26382
26566
  return value;
26383
26567
  }
26384
26568
  throw new Error(
26385
- `Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`
26569
+ `Invalid --tts: ${value}. Valid: auto, elevenlabs, openai, kokoro.`
26386
26570
  );
26387
26571
  }
26388
26572
  var TtsKeyMissingError;
@@ -26394,7 +26578,7 @@ var init_tts_resolve = __esm({
26394
26578
  TtsKeyMissingError = class extends Error {
26395
26579
  constructor(provider) {
26396
26580
  super(
26397
- provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
26581
+ provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : provider === "openai" ? "OpenAI API key required (OPENAI_API_KEY). Run 'vibe setup', set OPENAI_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
26398
26582
  );
26399
26583
  this.provider = provider;
26400
26584
  this.name = "TtsKeyMissingError";
@@ -31013,7 +31197,7 @@ async function executeSceneAdd(opts) {
31013
31197
  throw error;
31014
31198
  }
31015
31199
  opts.onProgress?.(
31016
- resolution.provider === "kokoro" ? "Generating narration with Kokoro (local \u2014 first run downloads ~330MB)..." : "Generating narration with ElevenLabs..."
31200
+ resolution.provider === "kokoro" ? "Generating narration with Kokoro (local \u2014 first run downloads a ~90MB model)..." : resolution.provider === "openai" ? "Generating narration with OpenAI (gpt-4o-mini-tts)..." : "Generating narration with ElevenLabs..."
31017
31201
  );
31018
31202
  const tts = await resolution.call(narrationText, {
31019
31203
  voice: opts.voice,
@@ -31536,9 +31720,9 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
31536
31720
  "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider"
31537
31721
  ).option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
31538
31722
  "--tts <provider>",
31539
- "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)",
31723
+ "TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
31540
31724
  "auto"
31541
- ).option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option(
31725
+ ).option("--voice <id>", "Voice id (ElevenLabs name/id, OpenAI voice like marin, or Kokoro id like af_heart)").option(
31542
31726
  "--no-audio",
31543
31727
  "Skip TTS even when --narration is provided (useful for tests/agent dry runs)"
31544
31728
  ).option("--no-image", "Skip image generation even when --visuals is provided").option(
@@ -59515,6 +59699,9 @@ var init_build_backdrop_prompt = __esm({
59515
59699
  import { existsSync as existsSync47 } from "node:fs";
59516
59700
  import { readFile as readFile43 } from "node:fs/promises";
59517
59701
  import { dirname as dirname21, join as join23, resolve as resolve60 } from "node:path";
59702
+ function backdropCostUsd(quality) {
59703
+ return quality === "hd" ? 0.2 : 0.05;
59704
+ }
59518
59705
  async function createBuildPlan(opts) {
59519
59706
  const projectDir = resolve60(opts.projectDir);
59520
59707
  loadPlanEnv(projectDir);
@@ -59616,13 +59803,13 @@ async function createBuildPlan(opts) {
59616
59803
  const backdropCue = backdropPrompt ?? backdropReference?.raw;
59617
59804
  const videoCue = videoPrompt ?? videoReference?.raw;
59618
59805
  const musicCue = musicPrompt ?? musicReference?.raw;
59619
- const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : 0;
59806
+ const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
59620
59807
  const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
59621
59808
  beatId: beat.id,
59622
59809
  cue: narrationText,
59623
59810
  provider: resolved.narration.resolved,
59624
59811
  voice,
59625
- ext: resolved.narration.resolved === "elevenlabs" ? "mp3" : "wav"
59812
+ ext: resolved.narration.resolved === "kokoro" ? "wav" : "mp3"
59626
59813
  }) : null;
59627
59814
  const backdropCache = augmentedBackdropPrompt && !backdropReference ? backdropCacheDescriptor({
59628
59815
  beatId: beat.id,
@@ -59670,7 +59857,7 @@ async function createBuildPlan(opts) {
59670
59857
  reference: backdropReference,
59671
59858
  projectDir,
59672
59859
  force: opts.force,
59673
- cost: BACKDROP_COST_USD,
59860
+ cost: backdropCostUsd(imageQuality),
59674
59861
  active: includeAssets
59675
59862
  }) : null;
59676
59863
  const video = videoCue && !opts.skipVideo ? assetPlan({
@@ -60040,17 +60227,20 @@ function providerInput(opts) {
60040
60227
  async function resolveNarrationProvider(input, projectDir) {
60041
60228
  const requested = input.value.toLowerCase();
60042
60229
  const hasElevenLabs = Boolean(await getApiKeyFromConfig("elevenlabs", { cwd: projectDir }));
60043
- const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : "kokoro";
60230
+ const hasOpenAi = Boolean(await getApiKeyFromConfig("openai", { cwd: projectDir }));
60231
+ const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "openai" ? "openai" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : hasOpenAi ? "openai" : "kokoro";
60232
+ const requiresKey = resolved !== "kokoro";
60233
+ const hasKey = resolved === "elevenlabs" ? hasElevenLabs : resolved === "openai" ? hasOpenAi : true;
60044
60234
  return {
60045
60235
  kind: "narration",
60046
60236
  requested: input.requested,
60047
60237
  resolved,
60048
60238
  source: input.source,
60049
- requiresKey: resolved === "elevenlabs",
60050
- configured: resolved !== "elevenlabs" || hasElevenLabs,
60051
- ...resolved === "elevenlabs" ? { configKey: "elevenlabs" } : {},
60052
- ...resolved === "elevenlabs" && !hasElevenLabs ? { missingKey: providerEnvVar("elevenlabs") } : {},
60053
- retryWith: resolved === "elevenlabs" && !hasElevenLabs ? ["vibe setup --full"] : []
60239
+ requiresKey,
60240
+ configured: hasKey,
60241
+ ...requiresKey ? { configKey: resolved } : {},
60242
+ ...requiresKey && !hasKey ? { missingKey: providerEnvVar(resolved) } : {},
60243
+ retryWith: requiresKey && !hasKey ? ["vibe setup --full"] : []
60054
60244
  };
60055
60245
  }
60056
60246
  async function keyedProviderResolution(opts, projectDir) {
@@ -60146,7 +60336,7 @@ function loadPlanEnv(projectDir) {
60146
60336
  dir = dirname21(dir);
60147
60337
  }
60148
60338
  }
60149
- var import_dotenv3, BACKDROP_COST_USD, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
60339
+ var import_dotenv3, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, OPENAI_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
60150
60340
  var init_build_plan = __esm({
60151
60341
  "src/commands/_shared/build-plan.ts"() {
60152
60342
  "use strict";
@@ -60161,10 +60351,10 @@ var init_build_plan = __esm({
60161
60351
  init_storyboard_parse();
60162
60352
  init_project_config();
60163
60353
  init_storyboard_edit();
60164
- BACKDROP_COST_USD = 3;
60165
60354
  VIDEO_COST_USD = 5;
60166
60355
  MUSIC_COST_USD = 0.5;
60167
60356
  ELEVENLABS_NARRATION_COST_USD = 0.05;
60357
+ OPENAI_NARRATION_COST_USD = 0.01;
60168
60358
  COMPOSE_COST_USD = 0.06;
60169
60359
  SUPPORTED_BUILD_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
60170
60360
  }
@@ -61177,7 +61367,10 @@ async function executeSceneBuild(opts) {
61177
61367
  (beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
61178
61368
  );
61179
61369
  stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
61180
- stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes);
61370
+ stageReports.assets.costUsd = estimateActualAssetCost(
61371
+ beatOutcomes,
61372
+ opts.imageQuality ?? "hd"
61373
+ );
61181
61374
  stageReports.assets.retryWith = pendingJobs.map(
61182
61375
  (job) => `vibe status job ${job.id} --project ${projectDir} --json`
61183
61376
  );
@@ -62360,11 +62553,11 @@ function shouldRunStage(selected, stage) {
62360
62553
  if (selected === "all") return true;
62361
62554
  return selected === stage;
62362
62555
  }
62363
- function estimateActualAssetCost(outcomes) {
62556
+ function estimateActualAssetCost(outcomes, imageQuality) {
62364
62557
  let cost = 0;
62365
62558
  for (const outcome of outcomes) {
62366
62559
  if (outcome.narrationStatus === "generated") cost += 0.05;
62367
- if (outcome.backdropStatus === "generated") cost += 3;
62560
+ if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
62368
62561
  if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") cost += 5;
62369
62562
  if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
62370
62563
  }
@@ -67925,8 +68118,9 @@ function planBuildElicitation(args) {
67925
68118
  description: "Which TTS engine narrates the video?",
67926
68119
  enum: [...NARRATION_CHOICES],
67927
68120
  enumNames: [
67928
- "Kokoro \u2014 free, runs locally on this machine",
67929
- "ElevenLabs \u2014 cloud TTS, uses your ElevenLabs credits"
68121
+ "Kokoro \u2014 free, runs on this machine (first use downloads a ~90 MB voice model; slower)",
68122
+ "OpenAI \u2014 fast cloud voice, uses your OpenAI API key (~$0.02 per video)",
68123
+ "ElevenLabs \u2014 premium cloud voices, uses your ElevenLabs credits"
67930
68124
  ]
67931
68125
  };
67932
68126
  required.push("narration");
@@ -67982,7 +68176,7 @@ var NARRATION_CHOICES, BACKDROP_CHOICES;
67982
68176
  var init_elicit = __esm({
67983
68177
  "src/tools/_shared/elicit.ts"() {
67984
68178
  "use strict";
67985
- NARRATION_CHOICES = ["kokoro", "elevenlabs"];
68179
+ NARRATION_CHOICES = ["kokoro", "openai", "elevenlabs"];
67986
68180
  BACKDROP_CHOICES = ["skip", "openai"];
67987
68181
  }
67988
68182
  });
@@ -68460,7 +68654,9 @@ var init_scene2 = __esm({
68460
68654
  skipVideo: external_exports.boolean().optional().describe("Skip video generation for every beat (use existing MP4 assets if present)."),
68461
68655
  skipMusic: external_exports.boolean().optional().describe("Skip music generation for every beat (use existing audio assets if present)."),
68462
68656
  skipRender: external_exports.boolean().optional().describe("Stop after compose \u2014 produces compositions/*.html but no final MP4."),
68463
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override. Default 'auto'."),
68657
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe(
68658
+ "TTS provider override. Default 'auto' (elevenlabs key > openai key > local kokoro)."
68659
+ ),
68464
68660
  voice: external_exports.string().optional().describe("TTS voice id (provider-specific)."),
68465
68661
  imageProvider: external_exports.enum(["openai"]).optional().describe("Image provider for backdrops. Default 'openai' (gpt-image-2)."),
68466
68662
  videoProvider: external_exports.enum(["seedance", "grok", "kling", "runway", "veo"]).optional().describe("Video provider for per-beat video cues. Default seedance."),
@@ -70315,7 +70511,7 @@ var init_storyboard2 = __esm({
70315
70511
  skipBackdrop: external_exports.boolean().optional().describe("Don't include backdrop image generation in the plan."),
70316
70512
  skipVideo: external_exports.boolean().optional().describe("Don't include video generation in the plan."),
70317
70513
  skipMusic: external_exports.boolean().optional().describe("Don't include music generation in the plan."),
70318
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override."),
70514
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe("TTS provider override."),
70319
70515
  voice: external_exports.string().optional().describe("Voice id."),
70320
70516
  imageProvider: external_exports.string().optional().describe("Image provider override."),
70321
70517
  imageQuality: external_exports.enum(["standard", "hd"]).optional().describe("Image quality override."),
@@ -73431,7 +73627,7 @@ init_source();
73431
73627
  // package.json
73432
73628
  var package_default = {
73433
73629
  name: "@vibeframe/cli",
73434
- version: "0.112.0",
73630
+ version: "0.113.0",
73435
73631
  description: "VibeFrame CLI - agentic video workflow runtime",
73436
73632
  private: false,
73437
73633
  license: "MIT",
@@ -83434,13 +83630,13 @@ init_output();
83434
83630
  import { resolve as resolve61 } from "node:path";
83435
83631
  var VALID_STAGES = ["assets", "compose", "sync", "render", "all"];
83436
83632
  var VALID_MODES = ["agent", "batch", "auto"];
83437
- var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "kokoro"];
83633
+ var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "openai", "kokoro"];
83438
83634
  var VALID_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
83439
83635
  var VALID_VIDEO_PROVIDERS = ["seedance", "grok", "kling", "runway", "veo"];
83440
83636
  var VALID_MUSIC_PROVIDERS = ["elevenlabs", "replicate"];
83441
83637
  var VALID_COMPOSERS = ["claude", "openai", "gemini"];
83442
83638
  var VALID_IMAGE_QUALITIES = ["standard", "hd"];
83443
- var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
83639
+ var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
83444
83640
  const startedAt = Date.now();
83445
83641
  const stage = String(options.stage ?? "all");
83446
83642
  if (!VALID_STAGES.includes(stage)) {
@@ -83631,7 +83827,7 @@ var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
83631
83827
  var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
83632
83828
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
83633
83829
  var VALID_MUSIC_PROVIDERS2 = ["elevenlabs", "replicate"];
83634
- var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
83830
+ var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|openai|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").addHelpText(
83635
83831
  "after",
83636
83832
  `
83637
83833
  Examples:
@@ -11,7 +11,7 @@ import type { ElicitForm } from "../adapters/mcp.js";
11
11
  /** The subset of build args that drive the question set. */
12
12
  export interface BuildChoiceArgs {
13
13
  stage?: "assets" | "compose" | "sync" | "render" | "all";
14
- ttsProvider?: "auto" | "elevenlabs" | "kokoro";
14
+ ttsProvider?: "auto" | "elevenlabs" | "openai" | "kokoro";
15
15
  skipNarration?: boolean;
16
16
  skipBackdrop?: boolean;
17
17
  imageProvider?: string;