@vibeframe/cli 0.112.1 → 0.113.2

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.
@@ -191,8 +191,8 @@ var init_api_keys = __esm({
191
191
  envVar: "OPENAI_API_KEY",
192
192
  label: "OpenAI",
193
193
  showInSetup: true,
194
- setupDescription: "gpt-image-2 image gen ($, default since v0.56), Whisper transcribe, Agent",
195
- envExampleComment: "OpenAI API Key (Whisper transcription, gpt-image-2 \u2014 default text-to-image since v0.56)",
194
+ setupDescription: "gpt-image-2 image gen ($, default since v0.56), gpt-4o-mini-tts narration, Whisper transcribe, Agent",
195
+ envExampleComment: "OpenAI API Key (Whisper transcription, gpt-4o-mini-tts narration, gpt-image-2 \u2014 default text-to-image since v0.56)",
196
196
  envExampleUrl: "https://platform.openai.com/api-keys",
197
197
  keyFormat: { prefix: /^sk-/, example: "sk-..." }
198
198
  });
@@ -2299,8 +2299,8 @@ var init_openai = __esm({
2299
2299
  id: "openai",
2300
2300
  label: "OpenAI",
2301
2301
  apiKey: "openai",
2302
- kinds: ["llm", "image", "transcription"],
2303
- resolverPriority: { image: 1 },
2302
+ kinds: ["llm", "image", "transcription", "speech"],
2303
+ resolverPriority: { image: 1, speech: 2 },
2304
2304
  commandsUnlocked: [
2305
2305
  "agent -p openai",
2306
2306
  "generate image -p openai",
@@ -4722,16 +4722,19 @@ var init_elevenlabs = __esm({
4722
4722
  });
4723
4723
 
4724
4724
  // ../ai-providers/dist/kokoro/KokoroProvider.js
4725
- async function loadKokoroFactory() {
4725
+ async function loadKokoroRuntime() {
4726
4726
  if (factoryOverride)
4727
- return factoryOverride;
4727
+ return { factory: factoryOverride, device: "cpu" };
4728
4728
  try {
4729
4729
  const mod = await import("kokoro-js");
4730
- return mod.KokoroTTS;
4730
+ return { factory: mod.KokoroTTS, device: "cpu" };
4731
4731
  } catch (err) {
4732
- const fallback2 = await loadKokoroFromWorkspace();
4733
- if (fallback2)
4734
- return fallback2;
4732
+ const workspace = await loadKokoroFromWorkspace();
4733
+ if (workspace)
4734
+ return { factory: workspace, device: "cpu" };
4735
+ const bundled = await loadBundledKokoroRuntime();
4736
+ if (bundled)
4737
+ return bundled;
4735
4738
  throw mapKokoroImportError(err);
4736
4739
  }
4737
4740
  }
@@ -4751,10 +4754,51 @@ async function loadKokoroFromWorkspace(baseDir = process.cwd()) {
4751
4754
  return null;
4752
4755
  }
4753
4756
  }
4757
+ async function loadBundledKokoroRuntime(runtimeDir = process.env.VIBE_KOKORO_RUNTIME) {
4758
+ const base = runtimeDir?.trim();
4759
+ if (!base)
4760
+ return null;
4761
+ try {
4762
+ const { readFile: readFile45, writeFile: writeFile51, mkdir: mkdir26 } = await import("node:fs/promises");
4763
+ const { join: join27, resolve: resolve66 } = await import("node:path");
4764
+ const { pathToFileURL } = await import("node:url");
4765
+ const { homedir: homedir6 } = await import("node:os");
4766
+ const transformersDir = resolve66(base, "node_modules", "@huggingface", "transformers");
4767
+ const ortDistDir = resolve66(base, "node_modules", "onnxruntime-web", "dist");
4768
+ const transformers = await import(pathToFileURL(join27(transformersDir, "dist", "transformers.web.js")).href);
4769
+ const cacheRoot = join27(homedir6(), ".cache", "vibeframe", "models");
4770
+ transformers.env.allowLocalModels = false;
4771
+ transformers.env.useCustomCache = true;
4772
+ transformers.env.customCache = {
4773
+ async match(key) {
4774
+ try {
4775
+ return new Response(await readFile45(join27(cacheRoot, encodeURIComponent(String(key)))));
4776
+ } catch {
4777
+ return void 0;
4778
+ }
4779
+ },
4780
+ async put(key, response) {
4781
+ try {
4782
+ const buf = Buffer.from(await response.arrayBuffer());
4783
+ await mkdir26(cacheRoot, { recursive: true });
4784
+ await writeFile51(join27(cacheRoot, encodeURIComponent(String(key))), buf);
4785
+ } catch {
4786
+ }
4787
+ }
4788
+ };
4789
+ transformers.env.backends.onnx.wasm.wasmPaths = pathToFileURL(ortDistDir + "/").href;
4790
+ const factory = await loadKokoroFromWorkspace(base);
4791
+ if (!factory)
4792
+ return null;
4793
+ return { factory, device: "cpu" };
4794
+ } catch {
4795
+ return null;
4796
+ }
4797
+ }
4754
4798
  function mapKokoroImportError(err) {
4755
4799
  const code = err instanceof Error && "code" in err ? err.code : void 0;
4756
4800
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
4757
- 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.");
4801
+ 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.");
4758
4802
  }
4759
4803
  return err instanceof Error ? err : new Error(String(err));
4760
4804
  }
@@ -4762,10 +4806,12 @@ function loadModel(progress) {
4762
4806
  if (modelPromise)
4763
4807
  return modelPromise;
4764
4808
  modelPromise = (async () => {
4765
- const factory = await loadKokoroFactory();
4766
- return factory.from_pretrained(KOKORO_MODEL_ID, {
4809
+ const runtime = await loadKokoroRuntime();
4810
+ const override = process.env.VIBE_ONNX_DEVICE;
4811
+ const device = override === "cpu" || override === "wasm" ? override : runtime.device;
4812
+ return runtime.factory.from_pretrained(KOKORO_MODEL_ID, {
4767
4813
  dtype: "q8",
4768
- device: "cpu",
4814
+ device,
4769
4815
  progress_callback: progress ? (raw) => progress(normaliseEvent(raw)) : void 0
4770
4816
  });
4771
4817
  })().catch((err) => {
@@ -4849,7 +4895,7 @@ var init_kokoro = __esm({
4849
4895
  label: "Kokoro (local)",
4850
4896
  apiKey: null,
4851
4897
  kinds: ["speech"],
4852
- resolverPriority: { speech: 2 }
4898
+ resolverPriority: { speech: 3 }
4853
4899
  });
4854
4900
  }
4855
4901
  });
@@ -5121,12 +5167,119 @@ var init_openai_image = __esm({
5121
5167
  }
5122
5168
  });
5123
5169
 
5170
+ // ../ai-providers/dist/openai-tts/OpenAiTtsProvider.js
5171
+ var OPENAI_TTS_VOICES, DEFAULT_VOICE, DEFAULT_MODEL2, TTS_429_MAX_RETRIES2, TTS_429_RETRY_DELAY_MS2, OpenAiTtsProvider, openaiTtsProvider;
5172
+ var init_OpenAiTtsProvider = __esm({
5173
+ "../ai-providers/dist/openai-tts/OpenAiTtsProvider.js"() {
5174
+ "use strict";
5175
+ OPENAI_TTS_VOICES = [
5176
+ "alloy",
5177
+ "ash",
5178
+ "ballad",
5179
+ "cedar",
5180
+ "coral",
5181
+ "echo",
5182
+ "fable",
5183
+ "marin",
5184
+ "nova",
5185
+ "onyx",
5186
+ "sage",
5187
+ "shimmer",
5188
+ "verse"
5189
+ ];
5190
+ DEFAULT_VOICE = "marin";
5191
+ DEFAULT_MODEL2 = "gpt-4o-mini-tts";
5192
+ TTS_429_MAX_RETRIES2 = 1;
5193
+ TTS_429_RETRY_DELAY_MS2 = 2e3;
5194
+ OpenAiTtsProvider = class {
5195
+ constructor() {
5196
+ this.id = "openai";
5197
+ this.name = "OpenAI TTS";
5198
+ this.description = "OpenAI cloud text-to-speech (gpt-4o-mini-tts)";
5199
+ this.capabilities = ["text-to-speech"];
5200
+ this.isAvailable = true;
5201
+ this.baseUrl = "https://api.openai.com/v1";
5202
+ }
5203
+ async initialize(config2) {
5204
+ this.apiKey = config2.apiKey;
5205
+ if (config2.baseUrl) {
5206
+ this.baseUrl = config2.baseUrl;
5207
+ }
5208
+ }
5209
+ isConfigured() {
5210
+ return !!this.apiKey;
5211
+ }
5212
+ async textToSpeech(text, options = {}) {
5213
+ if (!this.apiKey) {
5214
+ return { success: false, error: "OpenAI API key not configured" };
5215
+ }
5216
+ const voice = (options.voice ?? DEFAULT_VOICE).toLowerCase();
5217
+ if (!OPENAI_TTS_VOICES.includes(voice)) {
5218
+ return {
5219
+ success: false,
5220
+ error: `Unknown OpenAI voice "${options.voice}". Available voices: ${OPENAI_TTS_VOICES.join(", ")}.`
5221
+ };
5222
+ }
5223
+ try {
5224
+ let response;
5225
+ for (let attempt = 0; ; attempt++) {
5226
+ response = await fetch(`${this.baseUrl}/audio/speech`, {
5227
+ method: "POST",
5228
+ headers: {
5229
+ Authorization: `Bearer ${this.apiKey}`,
5230
+ "Content-Type": "application/json"
5231
+ },
5232
+ body: JSON.stringify({
5233
+ model: options.model ?? DEFAULT_MODEL2,
5234
+ input: text,
5235
+ voice,
5236
+ response_format: "mp3",
5237
+ ...options.speed !== void 0 && { speed: options.speed },
5238
+ ...options.instructions !== void 0 && { instructions: options.instructions }
5239
+ })
5240
+ });
5241
+ if (response.status === 429 && attempt < TTS_429_MAX_RETRIES2) {
5242
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, TTS_429_RETRY_DELAY_MS2 * (attempt + 1)));
5243
+ continue;
5244
+ }
5245
+ break;
5246
+ }
5247
+ if (!response.ok) {
5248
+ const error = await response.text();
5249
+ return { success: false, error: `OpenAI TTS failed: ${error}` };
5250
+ }
5251
+ const arrayBuffer = await response.arrayBuffer();
5252
+ return {
5253
+ success: true,
5254
+ audioBuffer: Buffer.from(arrayBuffer),
5255
+ characterCount: text.length
5256
+ };
5257
+ } catch (error) {
5258
+ return {
5259
+ success: false,
5260
+ error: error instanceof Error ? error.message : "Unknown error"
5261
+ };
5262
+ }
5263
+ }
5264
+ };
5265
+ openaiTtsProvider = new OpenAiTtsProvider();
5266
+ }
5267
+ });
5268
+
5269
+ // ../ai-providers/dist/openai-tts/index.js
5270
+ var init_openai_tts = __esm({
5271
+ "../ai-providers/dist/openai-tts/index.js"() {
5272
+ "use strict";
5273
+ init_OpenAiTtsProvider();
5274
+ }
5275
+ });
5276
+
5124
5277
  // ../ai-providers/dist/runway/RunwayProvider.js
5125
- var DEFAULT_MODEL2, RunwayProvider, runwayProvider;
5278
+ var DEFAULT_MODEL3, RunwayProvider, runwayProvider;
5126
5279
  var init_RunwayProvider = __esm({
5127
5280
  "../ai-providers/dist/runway/RunwayProvider.js"() {
5128
5281
  "use strict";
5129
- DEFAULT_MODEL2 = "gen4.5";
5282
+ DEFAULT_MODEL3 = "gen4.5";
5130
5283
  RunwayProvider = class _RunwayProvider {
5131
5284
  constructor() {
5132
5285
  this.id = "runway";
@@ -5169,7 +5322,7 @@ var init_RunwayProvider = __esm({
5169
5322
  "1:1": "960:960"
5170
5323
  };
5171
5324
  const apiRatio = ratioMap[options?.aspectRatio || "16:9"] || "1280:720";
5172
- const model = options?.model || DEFAULT_MODEL2;
5325
+ const model = options?.model || DEFAULT_MODEL3;
5173
5326
  if (!options?.referenceImage && model !== "gen4.5") {
5174
5327
  return {
5175
5328
  id: "",
@@ -5431,11 +5584,11 @@ var init_runway = __esm({
5431
5584
 
5432
5585
  // ../ai-providers/dist/kling/KlingProvider.js
5433
5586
  import { createHmac } from "node:crypto";
5434
- var DEFAULT_MODEL3, STD_MODE_MODELS, KlingProvider, klingProvider;
5587
+ var DEFAULT_MODEL4, STD_MODE_MODELS, KlingProvider, klingProvider;
5435
5588
  var init_KlingProvider = __esm({
5436
5589
  "../ai-providers/dist/kling/KlingProvider.js"() {
5437
5590
  "use strict";
5438
- DEFAULT_MODEL3 = "kling-v2-5-turbo";
5591
+ DEFAULT_MODEL4 = "kling-v2-5-turbo";
5439
5592
  STD_MODE_MODELS = ["kling-v2-5-turbo", "kling-v2-6", "kling-v3", "kling-v3-omni"];
5440
5593
  KlingProvider = class {
5441
5594
  constructor() {
@@ -5498,7 +5651,7 @@ var init_KlingProvider = __esm({
5498
5651
  }
5499
5652
  try {
5500
5653
  const token = this.generateToken();
5501
- const model = options?.model || DEFAULT_MODEL3;
5654
+ const model = options?.model || DEFAULT_MODEL4;
5502
5655
  let mode = options?.mode || "std";
5503
5656
  if (!STD_MODE_MODELS.includes(model) && mode === "std") {
5504
5657
  mode = "pro";
@@ -5869,11 +6022,11 @@ var init_kling = __esm({
5869
6022
  });
5870
6023
 
5871
6024
  // ../ai-providers/dist/grok/GrokProvider.js
5872
- var DEFAULT_MODEL4, GrokProvider, grokProvider;
6025
+ var DEFAULT_MODEL5, GrokProvider, grokProvider;
5873
6026
  var init_GrokProvider = __esm({
5874
6027
  "../ai-providers/dist/grok/GrokProvider.js"() {
5875
6028
  "use strict";
5876
- DEFAULT_MODEL4 = "grok-imagine-video";
6029
+ DEFAULT_MODEL5 = "grok-imagine-video";
5877
6030
  GrokProvider = class {
5878
6031
  constructor() {
5879
6032
  this.id = "grok";
@@ -6040,7 +6193,7 @@ var init_GrokProvider = __esm({
6040
6193
  try {
6041
6194
  const duration = Math.round(Math.min(15, Math.max(1, options?.duration || 5)));
6042
6195
  const body = {
6043
- model: DEFAULT_MODEL4,
6196
+ model: DEFAULT_MODEL5,
6044
6197
  prompt: prompt3,
6045
6198
  duration,
6046
6199
  aspect_ratio: options?.aspectRatio || "16:9"
@@ -10306,6 +10459,19 @@ var require_src = __commonJS({
10306
10459
  });
10307
10460
 
10308
10461
  // ../ai-providers/dist/fal/FalProvider.js
10462
+ function estimateSeedanceVideoCostUsd(opts) {
10463
+ const shortSide = SEEDANCE_SHORT_SIDE[opts.resolution ?? "720p"] ?? 720;
10464
+ const [a, b] = (opts.aspectRatio ?? "16:9").split(":").map(Number);
10465
+ const ratioFactor = a > 0 && b > 0 ? Math.max(a, b) / Math.min(a, b) : 16 / 9;
10466
+ const longSide = Math.round(shortSide * ratioFactor);
10467
+ const tokensPerSecond = shortSide * longSide * 24 / 1024;
10468
+ let usdPerSecond = tokensPerSecond / 1e3 * SEEDANCE_USD_PER_1K_TOKENS;
10469
+ if (opts.fast && shortSide <= 720)
10470
+ usdPerSecond *= SEEDANCE_FAST_FACTOR;
10471
+ if (opts.hasVideoReference)
10472
+ usdPerSecond *= SEEDANCE_VIDEO_REF_FACTOR;
10473
+ return Number((usdPerSecond * Math.max(0, opts.durationSec)).toFixed(2));
10474
+ }
10309
10475
  function formatFalError(error) {
10310
10476
  if (!(error instanceof Error))
10311
10477
  return String(error);
@@ -10415,7 +10581,7 @@ function normaliseDuration(value) {
10415
10581
  return "auto";
10416
10582
  return Math.max(4, Math.min(15, Math.round(value)));
10417
10583
  }
10418
- var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, FalProvider, falProvider;
10584
+ var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, SEEDANCE_USD_PER_1K_TOKENS, SEEDANCE_FAST_FACTOR, SEEDANCE_VIDEO_REF_FACTOR, SEEDANCE_SHORT_SIDE, FalProvider, falProvider;
10419
10585
  var init_FalProvider = __esm({
10420
10586
  "../ai-providers/dist/fal/FalProvider.js"() {
10421
10587
  "use strict";
@@ -10435,6 +10601,10 @@ var init_FalProvider = __esm({
10435
10601
  DEFAULT_VARIANT = "seedance-2.0";
10436
10602
  VALID_RESOLUTIONS = ["480p", "720p", "1080p"];
10437
10603
  VALID_ASPECTS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"];
10604
+ SEEDANCE_USD_PER_1K_TOKENS = 0.014;
10605
+ SEEDANCE_FAST_FACTOR = 0.8;
10606
+ SEEDANCE_VIDEO_REF_FACTOR = 0.6;
10607
+ SEEDANCE_SHORT_SIDE = { "480p": 480, "720p": 720, "1080p": 1080 };
10438
10608
  FalProvider = class {
10439
10609
  constructor() {
10440
10610
  this.id = "seedance";
@@ -11247,9 +11417,11 @@ __export(dist_exports2, {
11247
11417
  KOKORO_MODEL_ID: () => KOKORO_MODEL_ID,
11248
11418
  KlingProvider: () => KlingProvider,
11249
11419
  KokoroProvider: () => KokoroProvider,
11420
+ OPENAI_TTS_VOICES: () => OPENAI_TTS_VOICES,
11250
11421
  OllamaProvider: () => OllamaProvider,
11251
11422
  OpenAIImageProvider: () => OpenAIImageProvider,
11252
11423
  OpenAIProvider: () => OpenAIProvider,
11424
+ OpenAiTtsProvider: () => OpenAiTtsProvider,
11253
11425
  ReplicateProvider: () => ReplicateProvider,
11254
11426
  RunwayProvider: () => RunwayProvider,
11255
11427
  WhisperProvider: () => WhisperProvider,
@@ -11257,6 +11429,7 @@ __export(dist_exports2, {
11257
11429
  defineApiKey: () => defineApiKey,
11258
11430
  defineProvider: () => defineProvider,
11259
11431
  elevenLabsProvider: () => elevenLabsProvider,
11432
+ estimateSeedanceVideoCostUsd: () => estimateSeedanceVideoCostUsd,
11260
11433
  falProvider: () => falProvider,
11261
11434
  geminiProvider: () => geminiProvider,
11262
11435
  getAllApiKeys: () => getAllApiKeys,
@@ -11273,9 +11446,12 @@ __export(dist_exports2, {
11273
11446
  isGeminiTextModelAlias: () => isGeminiTextModelAlias,
11274
11447
  klingProvider: () => klingProvider,
11275
11448
  kokoroProvider: () => kokoroProvider,
11449
+ loadBundledKokoroRuntime: () => loadBundledKokoroRuntime,
11450
+ loadKokoroFromWorkspace: () => loadKokoroFromWorkspace,
11276
11451
  ollamaProvider: () => ollamaProvider,
11277
11452
  openaiImageProvider: () => openaiImageProvider,
11278
11453
  openaiProvider: () => openaiProvider,
11454
+ openaiTtsProvider: () => openaiTtsProvider,
11279
11455
  providerRegistry: () => providerRegistry,
11280
11456
  replicateProvider: () => replicateProvider,
11281
11457
  resolveGeminiTextModel: () => resolveGeminiTextModel,
@@ -11298,6 +11474,7 @@ var init_dist2 = __esm({
11298
11474
  init_elevenlabs();
11299
11475
  init_kokoro();
11300
11476
  init_openai_image();
11477
+ init_openai_tts();
11301
11478
  init_runway();
11302
11479
  init_kling();
11303
11480
  init_grok();
@@ -24059,7 +24236,534 @@ var init_build_asset_metadata = __esm({
24059
24236
  }
24060
24237
  });
24061
24238
 
24239
+ // src/commands/_shared/review-report.ts
24240
+ import { writeFile as writeFile6 } from "node:fs/promises";
24241
+ import { join as join2, resolve as resolve5 } from "node:path";
24242
+ function statusFromIssues(issues) {
24243
+ if (issues.some((issue) => issue.severity === "error")) return "fail";
24244
+ if (issues.some((issue) => issue.severity === "warning")) return "warn";
24245
+ return "pass";
24246
+ }
24247
+ function scoreIssues(issues) {
24248
+ let score = 100;
24249
+ for (const issue of issues) {
24250
+ if (issue.severity === "error") score -= 25;
24251
+ else if (issue.severity === "warning") score -= 8;
24252
+ else score -= 2;
24253
+ }
24254
+ return Math.max(0, Math.min(100, score));
24255
+ }
24256
+ function uniqueRetryWith(items) {
24257
+ return [
24258
+ ...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
24259
+ ];
24260
+ }
24261
+ function commandReviewAction(command, opts) {
24262
+ const classified = classifyReviewCommand(command);
24263
+ return stripUndefined({
24264
+ id: actionId("command", command),
24265
+ kind: "command",
24266
+ label: opts.label ?? command,
24267
+ command,
24268
+ fixOwner: opts.fixOwner ?? classified.fixOwner,
24269
+ costTier: opts.costTier ?? classified.costTier,
24270
+ safeToAutoRun: opts.safeToAutoRun ?? classified.safeToAutoRun,
24271
+ requiresConfirmation: opts.requiresConfirmation ?? classified.requiresConfirmation,
24272
+ reason: opts.reason,
24273
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
24274
+ });
24275
+ }
24276
+ function agentReviewAction(opts) {
24277
+ return stripUndefined({
24278
+ id: actionId("agent", opts.agentPrompt),
24279
+ kind: "agent",
24280
+ label: opts.label,
24281
+ agentPrompt: opts.agentPrompt,
24282
+ fixOwner: "host-agent",
24283
+ costTier: "unknown",
24284
+ safeToAutoRun: false,
24285
+ requiresConfirmation: false,
24286
+ reason: opts.reason,
24287
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
24288
+ });
24289
+ }
24290
+ function manualReviewAction(opts) {
24291
+ return stripUndefined({
24292
+ id: actionId("manual", opts.label),
24293
+ kind: "manual",
24294
+ label: opts.label,
24295
+ fixOwner: opts.fixOwner ?? "host-agent",
24296
+ costTier: "unknown",
24297
+ safeToAutoRun: false,
24298
+ requiresConfirmation: false,
24299
+ reason: opts.reason,
24300
+ sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
24301
+ });
24302
+ }
24303
+ function reviewActionsFromRetryWith(retryWith, sourceIssueCodes = []) {
24304
+ return normalizeReviewActions(
24305
+ retryWith.map((item) => {
24306
+ if (/^codex\s+/i.test(item)) {
24307
+ return agentReviewAction({
24308
+ label: "Ask the host agent to fix review issues",
24309
+ agentPrompt: item,
24310
+ reason: "The review report contains host-agent-owned issues.",
24311
+ sourceIssueCodes
24312
+ });
24313
+ }
24314
+ return commandReviewAction(item, {
24315
+ reason: "Backward-compatible retryWith command.",
24316
+ sourceIssueCodes
24317
+ });
24318
+ })
24319
+ );
24320
+ }
24321
+ function normalizeReviewActions(actions) {
24322
+ const merged = /* @__PURE__ */ new Map();
24323
+ for (const action of actions) {
24324
+ const key = [action.kind, action.command ?? action.agentPrompt ?? action.label].join("\0");
24325
+ const existing = merged.get(key);
24326
+ if (!existing) {
24327
+ merged.set(key, {
24328
+ ...action,
24329
+ sourceIssueCodes: uniqueStrings(action.sourceIssueCodes ?? [])
24330
+ });
24331
+ continue;
24332
+ }
24333
+ merged.set(key, {
24334
+ ...existing,
24335
+ safeToAutoRun: existing.safeToAutoRun && action.safeToAutoRun,
24336
+ requiresConfirmation: existing.requiresConfirmation || action.requiresConfirmation,
24337
+ sourceIssueCodes: uniqueStrings([
24338
+ ...existing.sourceIssueCodes ?? [],
24339
+ ...action.sourceIssueCodes ?? []
24340
+ ])
24341
+ });
24342
+ }
24343
+ return [...merged.values()].map(
24344
+ (action) => stripUndefined({
24345
+ ...action,
24346
+ sourceIssueCodes: action.sourceIssueCodes && action.sourceIssueCodes.length > 0 ? action.sourceIssueCodes : void 0
24347
+ })
24348
+ );
24349
+ }
24350
+ function deriveNextReviewActions(opts) {
24351
+ return normalizeReviewActions([
24352
+ ...opts.issues.flatMap((issue) => issue.actions ?? []),
24353
+ ...reviewActionsFromRetryWith(opts.retryWith)
24354
+ ]);
24355
+ }
24356
+ function fixOwnerForIssue(issue) {
24357
+ if (issue.fixOwner) return issue.fixOwner;
24358
+ if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
24359
+ if (issue.code.startsWith("STORYBOARD_")) return "host-agent";
24360
+ if (issue.code === "PROJECT_NOT_FOUND" || issue.code === "MISSING_STORYBOARD" || issue.code === "BEAT_NOT_FOUND" || issue.code === "MISSING_DESIGN") {
24361
+ return "host-agent";
24362
+ }
24363
+ return "vibe";
24364
+ }
24365
+ function normalizeReviewIssues(issues, opts) {
24366
+ const fallbackOwner = typeof opts === "string" ? opts : opts?.fallbackOwner;
24367
+ const projectDir = typeof opts === "string" ? void 0 : opts?.projectDir;
24368
+ return issues.map((issue) => {
24369
+ const normalized = {
24370
+ ...issue,
24371
+ fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
24372
+ };
24373
+ const actions = normalizeReviewActions([
24374
+ ...issue.actions ?? [],
24375
+ ...projectDir ? defaultActionsForIssue(normalized, projectDir) : []
24376
+ ]);
24377
+ return stripUndefined({
24378
+ ...normalized,
24379
+ actions: actions.length > 0 ? actions : void 0
24380
+ });
24381
+ });
24382
+ }
24383
+ function summarizeReviewIssues(issues) {
24384
+ const normalized = normalizeReviewIssues(issues);
24385
+ return {
24386
+ issueCount: normalized.length,
24387
+ errorCount: normalized.filter((issue) => issue.severity === "error").length,
24388
+ warningCount: normalized.filter((issue) => issue.severity === "warning").length,
24389
+ infoCount: normalized.filter((issue) => issue.severity === "info").length,
24390
+ fixOwners: {
24391
+ vibe: normalized.filter((issue) => issue.fixOwner === "vibe").length,
24392
+ hostAgent: normalized.filter((issue) => issue.fixOwner === "host-agent").length
24393
+ }
24394
+ };
24395
+ }
24396
+ function buildReviewReport(opts) {
24397
+ const retryWith = uniqueRetryWith(opts.retryWith);
24398
+ const issues = normalizeReviewIssues(opts.issues, {
24399
+ projectDir: opts.project,
24400
+ retryWith
24401
+ });
24402
+ return stripUndefined({
24403
+ schemaVersion: "1",
24404
+ kind: "review",
24405
+ project: resolve5(opts.project),
24406
+ mode: opts.mode,
24407
+ beat: opts.beat,
24408
+ status: opts.status,
24409
+ score: opts.score,
24410
+ issues,
24411
+ summary: summarizeReviewIssues(issues),
24412
+ nextActions: deriveNextReviewActions({ issues, retryWith, projectDir: opts.project }),
24413
+ retryWith,
24414
+ sourceReports: opts.sourceReports ?? [],
24415
+ reportPath: opts.reportPath
24416
+ });
24417
+ }
24418
+ function defaultReviewReportPath(projectDir) {
24419
+ return join2(resolve5(projectDir), "review-report.json");
24420
+ }
24421
+ async function writeReviewReport(path2, report) {
24422
+ await writeFile6(path2, JSON.stringify(report, null, 2) + "\n", "utf-8");
24423
+ }
24424
+ function stripUndefined(value) {
24425
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
24426
+ }
24427
+ function defaultActionsForIssue(issue, projectDir) {
24428
+ const code = issue.code;
24429
+ const beat = issue.beatId ?? issue.scene;
24430
+ const beatFlag = beat ? ` --beat ${beat}` : "";
24431
+ const sourceIssueCodes = [code];
24432
+ if (code === "PROJECT_NOT_FOUND" || code === "MISSING_STORYBOARD") {
24433
+ return [
24434
+ commandReviewAction(`vibe init ${projectDir} --from "<brief>" --json`, {
24435
+ label: "Initialize the project from a brief",
24436
+ fixOwner: "host-agent",
24437
+ reason: "The project cannot be inspected until the core project files exist.",
24438
+ sourceIssueCodes
24439
+ })
24440
+ ];
24441
+ }
24442
+ if (code === "MISSING_DESIGN" || code === "DESIGN_PLACEHOLDER_FIELD") {
24443
+ return [
24444
+ manualReviewAction({
24445
+ label: "Create or complete DESIGN.md",
24446
+ reason: "Visual direction needs host-agent or human input before VibeFrame can repair it.",
24447
+ sourceIssueCodes
24448
+ })
24449
+ ];
24450
+ }
24451
+ if (code === "BEAT_NOT_FOUND" || code.startsWith("STORYBOARD_")) {
24452
+ const actions = [
24453
+ commandReviewAction(`vibe storyboard validate ${projectDir} --json`, {
24454
+ label: "Validate the storyboard",
24455
+ fixOwner: "host-agent",
24456
+ reason: "The storyboard issue needs structured validation before editing.",
24457
+ sourceIssueCodes
24458
+ })
24459
+ ];
24460
+ if (code === "STORYBOARD_PLACEHOLDER_CUE") {
24461
+ actions.push(
24462
+ commandReviewAction(
24463
+ `vibe storyboard revise ${projectDir} --from "<make cues concrete>" --dry-run --json`,
24464
+ {
24465
+ label: "Preview a storyboard revision",
24466
+ fixOwner: "host-agent",
24467
+ reason: "Placeholder cues should be revised into concrete video intent.",
24468
+ sourceIssueCodes
24469
+ }
24470
+ )
24471
+ );
24472
+ }
24473
+ return actions;
24474
+ }
24475
+ if (code === "MISSING_ROOT_COMPOSITION" || code === "ROOT_SYNC_CHECK_FAILED") {
24476
+ return [
24477
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
24478
+ label: "Sync the project root composition",
24479
+ reason: "The root composition is missing or could not be verified.",
24480
+ sourceIssueCodes
24481
+ })
24482
+ ];
24483
+ }
24484
+ if (code.startsWith("ROOT_")) {
24485
+ return issue.fixOwner === "host-agent" ? [
24486
+ agentReviewAction({
24487
+ label: "Repair the root composition shell",
24488
+ agentPrompt: "Fix host-agent-owned root composition issues from review-report.json",
24489
+ reason: "The root composition cannot be repaired deterministically.",
24490
+ sourceIssueCodes
24491
+ })
24492
+ ] : [
24493
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
24494
+ label: "Repair deterministic scene/root issues",
24495
+ reason: "The root composition has deterministic sync issues.",
24496
+ sourceIssueCodes
24497
+ }),
24498
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
24499
+ label: "Resync root composition after repair",
24500
+ reason: "The root composition should be synced after deterministic repair.",
24501
+ sourceIssueCodes
24502
+ })
24503
+ ];
24504
+ }
24505
+ if (code === "MISSING_COMPOSITION") {
24506
+ return [
24507
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage compose --json`, {
24508
+ label: beat ? `Compose beat ${beat}` : "Compose missing scenes",
24509
+ reason: "Missing scene compositions must be generated or authored before render.",
24510
+ sourceIssueCodes
24511
+ })
24512
+ ];
24513
+ }
24514
+ if (code === "MISSING_BUILD_REPORT") {
24515
+ return [
24516
+ commandReviewAction(`vibe build ${projectDir} --dry-run --json`, {
24517
+ label: "Preview the build plan",
24518
+ reason: "A dry-run build report is needed before choosing a paid or mutating build step.",
24519
+ sourceIssueCodes
24520
+ })
24521
+ ];
24522
+ }
24523
+ if (code === "BUILD_REPORT_BEAT_MISSING") {
24524
+ return [
24525
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
24526
+ label: beat ? `Sync build report for beat ${beat}` : "Sync the build report",
24527
+ reason: "The build report does not include the inspected beat.",
24528
+ sourceIssueCodes
24529
+ })
24530
+ ];
24531
+ }
24532
+ if (code === "MALFORMED_BUILD_REPORT") {
24533
+ return [
24534
+ commandReviewAction(`vibe build ${projectDir} --json`, {
24535
+ label: "Regenerate the build report",
24536
+ reason: "The existing build report cannot be parsed.",
24537
+ sourceIssueCodes
24538
+ })
24539
+ ];
24540
+ }
24541
+ if (code === "MISSING_REPORTED_ASSET" || code === "STALE_ASSET" || code === "UNKNOWN_ASSET_FRESHNESS") {
24542
+ return [
24543
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --force --json`, {
24544
+ label: beat ? `Regenerate assets for beat ${beat}` : "Regenerate stale or missing assets",
24545
+ reason: "Asset generation may call providers, so the host should confirm first.",
24546
+ sourceIssueCodes
24547
+ })
24548
+ ];
24549
+ }
24550
+ if (code === "MUSIC_CUE_NOT_READY") {
24551
+ return [
24552
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --json`, {
24553
+ label: beat ? `Generate music for beat ${beat}` : "Generate missing music assets",
24554
+ reason: "Music generation may call providers, so the host should confirm first.",
24555
+ sourceIssueCodes
24556
+ }),
24557
+ commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
24558
+ label: "Sync music into the root composition",
24559
+ reason: "Generated music needs root composition wiring.",
24560
+ sourceIssueCodes
24561
+ })
24562
+ ];
24563
+ }
24564
+ if (code.startsWith("SCENE_LINT_")) {
24565
+ return [
24566
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
24567
+ label: "Repair deterministic scene lint issues",
24568
+ reason: "Scene lint reported an issue VibeFrame may be able to repair mechanically.",
24569
+ sourceIssueCodes
24570
+ })
24571
+ ];
24572
+ }
24573
+ if (code === "RENDER_NOT_FOUND" || code === "EMPTY_RENDER") {
24574
+ return [
24575
+ commandReviewAction(
24576
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
24577
+ {
24578
+ label: beat ? `Render beat ${beat}` : "Render the project",
24579
+ reason: "A render file is required before render inspection can pass.",
24580
+ sourceIssueCodes
24581
+ }
24582
+ )
24583
+ ];
24584
+ }
24585
+ if (code === "NO_AUDIO_STREAM" || code === "DURATION_DRIFT") {
24586
+ return [
24587
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
24588
+ label: beat ? `Sync timing/audio for beat ${beat}` : "Sync timing and audio wiring",
24589
+ reason: "The render output no longer matches synced project state.",
24590
+ sourceIssueCodes
24591
+ }),
24592
+ commandReviewAction(
24593
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
24594
+ {
24595
+ label: beat ? `Rerender beat ${beat}` : "Rerender the project",
24596
+ reason: "The render should be regenerated after sync repair.",
24597
+ sourceIssueCodes
24598
+ }
24599
+ )
24600
+ ];
24601
+ }
24602
+ if (code === "BLACK_FRAME_SEGMENT") {
24603
+ return issue.fixOwner === "host-agent" ? [
24604
+ agentReviewAction({
24605
+ label: beat ? `Fix black frames in beat ${beat}` : "Fix black frame segment",
24606
+ agentPrompt: "Fix black-frame issues from review-report.json, then rerender.",
24607
+ reason: "The issue likely needs scene, storyboard, or visual cue changes.",
24608
+ sourceIssueCodes
24609
+ })
24610
+ ] : [
24611
+ commandReviewAction(`vibe scene repair ${projectDir} --json`, {
24612
+ label: "Repair deterministic scene timing",
24613
+ reason: "Black frames outside a specific beat may be recoverable with scene repair.",
24614
+ sourceIssueCodes
24615
+ })
24616
+ ];
24617
+ }
24618
+ if (code === "STATIC_FRAME_SEGMENT") {
24619
+ return [
24620
+ agentReviewAction({
24621
+ label: beat ? `Add motion to beat ${beat}` : "Fix static visual hold",
24622
+ agentPrompt: "Fix static-frame issues from review-report.json, then rerender.",
24623
+ reason: "Static segments usually need creative scene, storyboard, or asset changes.",
24624
+ sourceIssueCodes
24625
+ })
24626
+ ];
24627
+ }
24628
+ if (code === "LONG_SILENCE") {
24629
+ return issue.fixOwner === "host-agent" ? [
24630
+ agentReviewAction({
24631
+ label: beat ? `Fix long silence in beat ${beat}` : "Fix long silence",
24632
+ agentPrompt: "Fix long-silence issues from review-report.json, then rerender.",
24633
+ reason: "The beat duration, narration, or music cue needs host-agent judgment.",
24634
+ sourceIssueCodes
24635
+ })
24636
+ ] : [
24637
+ commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
24638
+ label: beat ? `Sync audio for beat ${beat}` : "Sync audio wiring",
24639
+ reason: "The silence may be caused by stale narration or music wiring.",
24640
+ sourceIssueCodes
24641
+ })
24642
+ ];
24643
+ }
24644
+ if (code.startsWith("AI_REVIEW_")) {
24645
+ return [
24646
+ agentReviewAction({
24647
+ label: "Fix AI review findings",
24648
+ agentPrompt: "Fix host-agent-owned AI review issues from review-report.json, then rerender.",
24649
+ reason: "AI review findings require storyboard, design, or composition judgment.",
24650
+ sourceIssueCodes
24651
+ })
24652
+ ];
24653
+ }
24654
+ if (code === "FFPROBE_UNAVAILABLE" || code === "FFMPEG_UNAVAILABLE") {
24655
+ return [
24656
+ manualReviewAction({
24657
+ label: "Install FFmpeg and rerun inspection",
24658
+ reason: "Local media inspection depends on ffmpeg/ffprobe being available.",
24659
+ fixOwner: "host-agent",
24660
+ sourceIssueCodes
24661
+ })
24662
+ ];
24663
+ }
24664
+ if (code === "FFPROBE_FAILED" || code === "NO_VIDEO_STREAM") {
24665
+ return [
24666
+ commandReviewAction(
24667
+ beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
24668
+ {
24669
+ label: beat ? `Rerender beat ${beat}` : "Rerender the project",
24670
+ reason: "The rendered media could not be probed as a valid video.",
24671
+ sourceIssueCodes
24672
+ }
24673
+ )
24674
+ ];
24675
+ }
24676
+ if (code === "ASPECT_MISMATCH") {
24677
+ return [
24678
+ manualReviewAction({
24679
+ label: "Check project aspect and render settings",
24680
+ reason: "Aspect mismatches can come from either config intent or render settings.",
24681
+ fixOwner: issue.fixOwner,
24682
+ sourceIssueCodes
24683
+ })
24684
+ ];
24685
+ }
24686
+ return [];
24687
+ }
24688
+ function classifyReviewCommand(command) {
24689
+ const lower = command.trim().toLowerCase();
24690
+ if (lower.startsWith("vibe status ")) return safe("free");
24691
+ if (lower.startsWith("vibe scene repair")) return safe("free");
24692
+ if (lower.startsWith("vibe storyboard validate") || lower.startsWith("vibe storyboard get") || lower.startsWith("vibe storyboard list")) {
24693
+ return safe("free", "host-agent");
24694
+ }
24695
+ if (lower.startsWith("vibe inspect project")) return safe("free");
24696
+ if (lower.startsWith("vibe inspect render")) {
24697
+ if (lower.includes("--ai")) return unsafe("low");
24698
+ return safe("free");
24699
+ }
24700
+ if (lower.startsWith("vibe render ")) return safe("free");
24701
+ if (lower.startsWith("vibe build ")) {
24702
+ if (lower.includes("--dry-run")) return safe("free");
24703
+ if (lower.includes("--stage sync") || lower.includes("--stage render")) return safe("free");
24704
+ return unsafe("unknown");
24705
+ }
24706
+ if (lower.startsWith("vibe generate video") || lower.startsWith("vibe edit fill-gaps") || lower.startsWith("vibe remix ")) {
24707
+ return unsafe("very-high");
24708
+ }
24709
+ if (lower.startsWith("vibe storyboard revise")) return unsafe("unknown", "host-agent");
24710
+ return unsafe("unknown");
24711
+ }
24712
+ function safe(costTier, fixOwner = "vibe") {
24713
+ return { fixOwner, costTier, safeToAutoRun: true, requiresConfirmation: false };
24714
+ }
24715
+ function unsafe(costTier, fixOwner = "vibe") {
24716
+ return { fixOwner, costTier, safeToAutoRun: false, requiresConfirmation: true };
24717
+ }
24718
+ function actionId(kind, value) {
24719
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
24720
+ return `${kind}:${slug || "action"}`;
24721
+ }
24722
+ function uniqueStrings(items) {
24723
+ return [
24724
+ ...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
24725
+ ];
24726
+ }
24727
+ var init_review_report = __esm({
24728
+ "src/commands/_shared/review-report.ts"() {
24729
+ "use strict";
24730
+ }
24731
+ });
24732
+
24062
24733
  // src/commands/_shared/storyboard-parse.ts
24734
+ function resolveCharacters(frontmatter) {
24735
+ const pool = frontmatter?.characters;
24736
+ if (!pool || typeof pool !== "object") return [];
24737
+ const resolved = [];
24738
+ for (const [name, spec] of Object.entries(pool)) {
24739
+ if (typeof spec === "string") {
24740
+ const prompt3 = spec.trim();
24741
+ if (prompt3) resolved.push({ name, prompt: prompt3 });
24742
+ continue;
24743
+ }
24744
+ if (spec && typeof spec === "object") {
24745
+ const image = typeof spec.image === "string" ? spec.image.trim() : "";
24746
+ if (image) {
24747
+ resolved.push({ name, imagePath: image });
24748
+ continue;
24749
+ }
24750
+ const description = typeof spec.description === "string" ? spec.description.trim() : "";
24751
+ if (description) resolved.push({ name, prompt: description });
24752
+ }
24753
+ }
24754
+ return resolved;
24755
+ }
24756
+ function beatCharacterNames(cues) {
24757
+ const raw = cues?.characters;
24758
+ if (typeof raw === "string") {
24759
+ const name = raw.trim();
24760
+ return name ? [name] : [];
24761
+ }
24762
+ if (Array.isArray(raw)) {
24763
+ return raw.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
24764
+ }
24765
+ return [];
24766
+ }
24063
24767
  function parseStoryboard(md) {
24064
24768
  const normalized = md.replace(/\r\n/g, "\n");
24065
24769
  const { frontmatter, remaining } = extractProjectFrontmatter(normalized);
@@ -24172,28 +24876,28 @@ var init_storyboard_parse = __esm({
24172
24876
  // src/commands/_shared/status-jobs.ts
24173
24877
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
24174
24878
  import { existsSync as existsSync2, statSync } from "node:fs";
24175
- import { copyFile, mkdir as mkdir3, readFile as readFile4, readdir, stat, writeFile as writeFile6 } from "node:fs/promises";
24176
- import { dirname as dirname3, join as join2, parse as parse2, relative, resolve as resolve5 } from "node:path";
24879
+ import { copyFile, mkdir as mkdir3, readFile as readFile4, readdir, stat, writeFile as writeFile7 } from "node:fs/promises";
24880
+ import { dirname as dirname3, join as join3, parse as parse2, relative, resolve as resolve6 } from "node:path";
24177
24881
  function isLocalJob(record) {
24178
24882
  return record.jobType === "build" || record.jobType === "render";
24179
24883
  }
24180
24884
  function findProjectRoot(start = process.cwd()) {
24181
- let dir = resolve5(start);
24885
+ let dir = resolve6(start);
24182
24886
  try {
24183
24887
  if (existsSync2(dir) && statSync(dir).isFile()) dir = dirname3(dir);
24184
24888
  } catch {
24185
24889
  }
24186
24890
  for (; ; ) {
24187
- if (existsSync2(join2(dir, "vibe.config.json")) || existsSync2(join2(dir, "STORYBOARD.md"))) {
24891
+ if (existsSync2(join3(dir, "vibe.config.json")) || existsSync2(join3(dir, "STORYBOARD.md"))) {
24188
24892
  return dir;
24189
24893
  }
24190
24894
  const parent = dirname3(dir);
24191
- if (parent === dir) return resolve5(start);
24895
+ if (parent === dir) return resolve6(start);
24192
24896
  dir = parent;
24193
24897
  }
24194
24898
  }
24195
24899
  function jobsDir(projectDir) {
24196
- return join2(resolve5(projectDir), ".vibeframe", "jobs");
24900
+ return join3(resolve6(projectDir), ".vibeframe", "jobs");
24197
24901
  }
24198
24902
  function normalizeJobStatus(status) {
24199
24903
  const value = String(status ?? "").toLowerCase();
@@ -24207,10 +24911,10 @@ function normalizeJobStatus(status) {
24207
24911
  }
24208
24912
  function createJobRecord(opts) {
24209
24913
  const now = opts.now ?? /* @__PURE__ */ new Date();
24210
- const projectDir = resolve5(
24914
+ const projectDir = resolve6(
24211
24915
  opts.projectDir ?? findProjectRoot(opts.workingDirectory ?? process.cwd())
24212
24916
  );
24213
- const workingDirectory = resolve5(opts.workingDirectory ?? process.cwd());
24917
+ const workingDirectory = resolve6(opts.workingDirectory ?? process.cwd());
24214
24918
  const id = opts.id ?? makeJobId(opts.providerTaskId, now);
24215
24919
  const base = {
24216
24920
  schemaVersion: "1",
@@ -24245,7 +24949,7 @@ function createJobRecord(opts) {
24245
24949
  message: opts.message
24246
24950
  };
24247
24951
  base.retryWith = retryWithForJob(base);
24248
- return stripUndefined(base);
24952
+ return stripUndefined2(base);
24249
24953
  }
24250
24954
  async function createAndWriteJobRecord(opts) {
24251
24955
  const record = createJobRecord(opts);
@@ -24255,15 +24959,15 @@ async function createAndWriteJobRecord(opts) {
24255
24959
  async function writeJobRecord(record) {
24256
24960
  const dir = jobsDir(record.projectDir);
24257
24961
  await mkdir3(dir, { recursive: true });
24258
- await writeFile6(
24962
+ await writeFile7(
24259
24963
  jobRecordPath(record.id, record.projectDir),
24260
- JSON.stringify(stripUndefined(record), null, 2) + "\n",
24964
+ JSON.stringify(stripUndefined2(record), null, 2) + "\n",
24261
24965
  "utf-8"
24262
24966
  );
24263
24967
  }
24264
24968
  async function readJobRecord(jobId, projectDir) {
24265
24969
  const id = normalizeJobId(jobId);
24266
- const project = resolve5(projectDir ?? findProjectRoot());
24970
+ const project = resolve6(projectDir ?? findProjectRoot());
24267
24971
  const path2 = jobRecordPath(id, project);
24268
24972
  if (!existsSync2(path2)) return null;
24269
24973
  return parseJobRecord(await readFile4(path2, "utf-8"));
@@ -24276,7 +24980,7 @@ async function listJobRecords(projectDir) {
24276
24980
  for (const entry of entries) {
24277
24981
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
24278
24982
  try {
24279
- const parsed = parseJobRecord(await readFile4(join2(dir, entry.name), "utf-8"));
24983
+ const parsed = parseJobRecord(await readFile4(join3(dir, entry.name), "utf-8"));
24280
24984
  if (parsed) records.push(parsed);
24281
24985
  } catch {
24282
24986
  }
@@ -24370,9 +25074,9 @@ async function refreshLocalJobRecord(record, opts) {
24370
25074
  const heartbeatAge = record.heartbeatAt ? Date.now() - new Date(record.heartbeatAt).getTime() : Number.POSITIVE_INFINITY;
24371
25075
  const orphaned = !isPidAlive(record.pid) || heartbeatAge > LOCAL_JOB_ORPHAN_AFTER_MS;
24372
25076
  if (orphaned) {
24373
- const hint = `The process that ran this ${record.jobType} job is gone. Check ${join2(record.projectDir, record.jobType === "render" ? "render-report.json" : "build-report.json")} \u2014 the work may have completed before the process exited.`;
25077
+ const hint = `The process that ran this ${record.jobType} job is gone. Check ${join3(record.projectDir, record.jobType === "render" ? "render-report.json" : "build-report.json")} \u2014 the work may have completed before the process exited.`;
24374
25078
  warnings.push(hint);
24375
- updated = stripUndefined({
25079
+ updated = stripUndefined2({
24376
25080
  ...record,
24377
25081
  status: "unknown",
24378
25082
  error: record.error ?? hint,
@@ -24389,7 +25093,7 @@ async function refreshLocalJobRecord(record, opts) {
24389
25093
  });
24390
25094
  }
24391
25095
  async function inspectProjectStatus(projectDirArg, opts = {}) {
24392
- const project = resolve5(projectDirArg);
25096
+ const project = resolve6(projectDirArg);
24393
25097
  const warnings = [];
24394
25098
  let records = await listJobRecords(project);
24395
25099
  if (opts.refresh) {
@@ -24419,6 +25123,10 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
24419
25123
  ...review?.retryWith ?? [],
24420
25124
  ...workflow.retryWith
24421
25125
  ];
25126
+ const nextActions = normalizeReviewActions([
25127
+ ...review?.nextActions ?? [],
25128
+ ...reviewActionsFromRetryWith([...build?.retryWith ?? [], ...workflow.retryWith])
25129
+ ]);
24422
25130
  return {
24423
25131
  schemaVersion: "1",
24424
25132
  kind: "project",
@@ -24430,11 +25138,12 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
24430
25138
  review,
24431
25139
  jobs,
24432
25140
  warnings,
25141
+ nextActions,
24433
25142
  retryWith: unique(retryWith)
24434
25143
  };
24435
25144
  }
24436
25145
  function summarizeJob(record) {
24437
- return stripUndefined({
25146
+ return stripUndefined2({
24438
25147
  id: record.id,
24439
25148
  jobType: record.jobType,
24440
25149
  status: record.status,
@@ -24458,7 +25167,7 @@ function retryWithForJob(record) {
24458
25167
  ]);
24459
25168
  }
24460
25169
  function makeJobStatusResult(record, opts) {
24461
- return stripUndefined({
25170
+ return stripUndefined2({
24462
25171
  schemaVersion: "1",
24463
25172
  kind: "job",
24464
25173
  id: record.id,
@@ -24482,7 +25191,7 @@ function jobProgress(record) {
24482
25191
  if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") {
24483
25192
  if (record.progress === void 0 && !record.providerStatus) return void 0;
24484
25193
  }
24485
- return stripUndefined({
25194
+ return stripUndefined2({
24486
25195
  percent: record.progress,
24487
25196
  phase: record.stage ?? record.providerStatus ?? statusPhase(record.status),
24488
25197
  providerStatus: record.providerStatus ?? record.message
@@ -24492,7 +25201,7 @@ function jobResult(record) {
24492
25201
  if (!record.resultUrl && !record.outputPath && !record.cachePath && !record.error && !record.resultPayload) {
24493
25202
  return null;
24494
25203
  }
24495
- return stripUndefined({
25204
+ return stripUndefined2({
24496
25205
  url: record.resultUrl,
24497
25206
  outputPath: record.outputPath,
24498
25207
  cachePath: record.cachePath,
@@ -24528,7 +25237,7 @@ function normalizeJobId(jobId) {
24528
25237
  return id;
24529
25238
  }
24530
25239
  function jobRecordPath(jobId, projectDir) {
24531
- return join2(jobsDir(projectDir), `${normalizeJobId(jobId)}.json`);
25240
+ return join3(jobsDir(projectDir), `${normalizeJobId(jobId)}.json`);
24532
25241
  }
24533
25242
  function parseJobRecord(raw) {
24534
25243
  const parsed = JSON.parse(raw);
@@ -24553,11 +25262,11 @@ function updateRecordFromProvider(record, result) {
24553
25262
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24554
25263
  };
24555
25264
  next.retryWith = retryWithForJob(next);
24556
- return stripUndefined(next);
25265
+ return stripUndefined2(next);
24557
25266
  }
24558
25267
  function resolveRefreshOutput(record, opts) {
24559
- if (opts.output) return resolve5(opts.workingDirectory ?? process.cwd(), opts.output);
24560
- return record.outputPath ? resolve5(record.outputPath) : void 0;
25268
+ if (opts.output) return resolve6(opts.workingDirectory ?? process.cwd(), opts.output);
25269
+ return record.outputPath ? resolve6(record.outputPath) : void 0;
24561
25270
  }
24562
25271
  function liveSupport(record) {
24563
25272
  if (record.jobType === "generate-video" && (record.provider === "grok" || record.provider === "runway" || record.provider === "kling" || record.provider === "veo")) {
@@ -24582,7 +25291,7 @@ async function downloadUrl(url, outputPath) {
24582
25291
  if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
24583
25292
  const buffer = Buffer.from(await response.arrayBuffer());
24584
25293
  await mkdir3(dirname3(outputPath), { recursive: true });
24585
- await writeFile6(outputPath, buffer);
25294
+ await writeFile7(outputPath, buffer);
24586
25295
  return outputPath;
24587
25296
  }
24588
25297
  async function maybeCacheOutput(record) {
@@ -24614,7 +25323,7 @@ async function maybeWriteCompletedAssetMetadata(record) {
24614
25323
  }
24615
25324
  }
24616
25325
  async function refreshBuildReportFromJobs(projectDir, records) {
24617
- const reportPath = join2(projectDir, "build-report.json");
25326
+ const reportPath = join3(projectDir, "build-report.json");
24618
25327
  const report = await readJson(reportPath);
24619
25328
  if (!report || !Array.isArray(report.beats)) return;
24620
25329
  let changed = false;
@@ -24642,7 +25351,7 @@ async function refreshBuildReportFromJobs(projectDir, records) {
24642
25351
  if (kind === "music" && record.assetOptions && typeof record.assetOptions.duration === "number") {
24643
25352
  nested.durationSec = record.assetOptions.duration;
24644
25353
  }
24645
- beatRecord[kind] = stripUndefined(nested);
25354
+ beatRecord[kind] = stripUndefined2(nested);
24646
25355
  beatRecord[`${kind}Path`] = outputPath;
24647
25356
  beatRecord[`${kind}Status`] = "generated";
24648
25357
  beatRecord[`${kind}JobId`] = record.id;
@@ -24662,17 +25371,17 @@ async function refreshBuildReportFromJobs(projectDir, records) {
24662
25371
  }
24663
25372
  if (!changed) return;
24664
25373
  try {
24665
- await writeFile6(reportPath, JSON.stringify(stripUndefined(report), null, 2) + "\n", "utf-8");
25374
+ await writeFile7(reportPath, JSON.stringify(stripUndefined2(report), null, 2) + "\n", "utf-8");
24666
25375
  } catch {
24667
25376
  }
24668
25377
  }
24669
25378
  async function readBuildSummary(projectDir) {
24670
- const reportPath = join2(projectDir, "build-report.json");
25379
+ const reportPath = join3(projectDir, "build-report.json");
24671
25380
  const report = await readJson(reportPath);
24672
25381
  if (!report) return null;
24673
25382
  const info = await statOrNull(reportPath);
24674
25383
  const beatSummary = projectBeatReadinessFromUnknown(report.beatSummary);
24675
- return stripUndefined({
25384
+ return stripUndefined2({
24676
25385
  reportPath,
24677
25386
  kind: typeof report.kind === "string" ? report.kind : void 0,
24678
25387
  success: typeof report.success === "boolean" ? report.success : void 0,
@@ -24690,7 +25399,7 @@ async function readBuildSummary(projectDir) {
24690
25399
  });
24691
25400
  }
24692
25401
  async function readReviewSummary(projectDir) {
24693
- const reportPath = join2(projectDir, "review-report.json");
25402
+ const reportPath = join3(projectDir, "review-report.json");
24694
25403
  const report = await readJson(reportPath);
24695
25404
  if (!report) return null;
24696
25405
  const info = await statOrNull(reportPath);
@@ -24698,7 +25407,7 @@ async function readReviewSummary(projectDir) {
24698
25407
  const summary = report.summary && typeof report.summary === "object" ? report.summary : {};
24699
25408
  const fixOwners = summary.fixOwners && typeof summary.fixOwners === "object" ? summary.fixOwners : {};
24700
25409
  const computedFixOwners = countIssueFixOwners(issues);
24701
- return stripUndefined({
25410
+ return stripUndefined2({
24702
25411
  reportPath,
24703
25412
  kind: typeof report.kind === "string" ? report.kind : void 0,
24704
25413
  mode: typeof report.mode === "string" ? report.mode : void 0,
@@ -24723,9 +25432,44 @@ async function readReviewSummary(projectDir) {
24723
25432
  },
24724
25433
  sourceReports: stringArray(report.sourceReports),
24725
25434
  updatedAt: info?.mtime.toISOString(),
25435
+ nextActions: reviewActionsFromUnknown(report.nextActions),
24726
25436
  retryWith: stringArray(report.retryWith)
24727
25437
  });
24728
25438
  }
25439
+ function reviewActionsFromUnknown(value) {
25440
+ if (!Array.isArray(value)) return [];
25441
+ return value.flatMap((item) => {
25442
+ if (!item || typeof item !== "object") return [];
25443
+ const record = item;
25444
+ const kind = stringEnum(record.kind, ["command", "agent", "manual"]);
25445
+ const fixOwner = stringEnum(record.fixOwner, ["vibe", "host-agent"]);
25446
+ const costTier = stringEnum(record.costTier, [
25447
+ "free",
25448
+ "low",
25449
+ "high",
25450
+ "very-high",
25451
+ "unknown"
25452
+ ]);
25453
+ if (!kind || !fixOwner || !costTier || typeof record.id !== "string" || typeof record.label !== "string" || typeof record.safeToAutoRun !== "boolean" || typeof record.requiresConfirmation !== "boolean" || typeof record.reason !== "string") {
25454
+ return [];
25455
+ }
25456
+ return [
25457
+ stripUndefined2({
25458
+ id: record.id,
25459
+ kind,
25460
+ label: record.label,
25461
+ command: typeof record.command === "string" ? record.command : void 0,
25462
+ agentPrompt: typeof record.agentPrompt === "string" ? record.agentPrompt : void 0,
25463
+ fixOwner,
25464
+ costTier,
25465
+ safeToAutoRun: record.safeToAutoRun,
25466
+ requiresConfirmation: record.requiresConfirmation,
25467
+ reason: record.reason,
25468
+ sourceIssueCodes: stringArray(record.sourceIssueCodes)
25469
+ })
25470
+ ];
25471
+ });
25472
+ }
24729
25473
  function summarizeJobs(records) {
24730
25474
  return {
24731
25475
  total: records.length,
@@ -24744,7 +25488,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
24744
25488
  if (fromBeats) return fromBeats;
24745
25489
  if (build.beats) return augmentBeatReadinessFromJobs(build.beats, records, report?.jobs);
24746
25490
  }
24747
- const storyboardPath = join2(projectDir, "STORYBOARD.md");
25491
+ const storyboardPath = join3(projectDir, "STORYBOARD.md");
24748
25492
  if (!existsSync2(storyboardPath)) {
24749
25493
  return { total: 0, assetsReady: 0, compositionsReady: 0, needsAuthor: [] };
24750
25494
  }
@@ -24753,7 +25497,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
24753
25497
  const needsAuthor = [];
24754
25498
  let compositionsReady = 0;
24755
25499
  for (const beat of parsed.beats) {
24756
- if (existsSync2(join2(projectDir, "compositions", `scene-${beat.id}.html`))) {
25500
+ if (existsSync2(join3(projectDir, "compositions", `scene-${beat.id}.html`))) {
24757
25501
  compositionsReady += 1;
24758
25502
  } else {
24759
25503
  needsAuthor.push(beat.id);
@@ -24771,7 +25515,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
24771
25515
  }
24772
25516
  function deriveProjectWorkflow(opts) {
24773
25517
  const retryWith = [];
24774
- const hasStoryboard = existsSync2(join2(opts.project, "STORYBOARD.md"));
25518
+ const hasStoryboard = existsSync2(join3(opts.project, "STORYBOARD.md"));
24775
25519
  if (!hasStoryboard && !opts.build) {
24776
25520
  retryWith.push(`vibe init ${opts.project} --from "<brief>" --json`);
24777
25521
  return { status: "empty", currentStage: "init", retryWith };
@@ -24867,7 +25611,7 @@ function beatReadinessFromBuildBeats(projectDir, beats, records = [], reportJobs
24867
25611
  if (buildBeatAssetsReady(record) || (id ? buildBeatJobsReady(id, records, reportJobs) : false))
24868
25612
  assetsReady += 1;
24869
25613
  const compositionPath = typeof record.compositionPath === "string" ? record.compositionPath : id ? `compositions/scene-${id}.html` : void 0;
24870
- if (compositionPath && existsSync2(resolve5(projectDir, compositionPath))) {
25614
+ if (compositionPath && existsSync2(resolve6(projectDir, compositionPath))) {
24871
25615
  compositionsReady += 1;
24872
25616
  } else if (id) {
24873
25617
  needsAuthor.push(id);
@@ -24937,6 +25681,9 @@ function numberOr(fallback2, value) {
24937
25681
  function stringArray(value) {
24938
25682
  return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
24939
25683
  }
25684
+ function stringEnum(value, allowed) {
25685
+ return typeof value === "string" && allowed.includes(value) ? value : void 0;
25686
+ }
24940
25687
  function isActiveStatus(status) {
24941
25688
  return status === "queued" || status === "running";
24942
25689
  }
@@ -24966,7 +25713,7 @@ function assetKindForJob(record) {
24966
25713
  return void 0;
24967
25714
  }
24968
25715
  function projectRelativePath(projectDir, path2) {
24969
- const rel = relative(resolve5(projectDir), resolve5(path2));
25716
+ const rel = relative(resolve6(projectDir), resolve6(path2));
24970
25717
  if (!rel || rel.startsWith("..")) return path2;
24971
25718
  return rel.replace(/\\/g, "/");
24972
25719
  }
@@ -24984,7 +25731,7 @@ async function statOrNull(path2) {
24984
25731
  return null;
24985
25732
  }
24986
25733
  }
24987
- function stripUndefined(value) {
25734
+ function stripUndefined2(value) {
24988
25735
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
24989
25736
  }
24990
25737
  function unique(items) {
@@ -25002,6 +25749,7 @@ var init_status_jobs = __esm({
25002
25749
  init_ai_video();
25003
25750
  init_music_status();
25004
25751
  init_build_asset_metadata();
25752
+ init_review_report();
25005
25753
  init_storyboard_parse();
25006
25754
  LOCAL_JOB_ORPHAN_AFTER_MS = 9e4;
25007
25755
  }
@@ -25010,7 +25758,7 @@ var init_status_jobs = __esm({
25010
25758
  // src/commands/_shared/project-config.ts
25011
25759
  import { existsSync as existsSync3 } from "node:fs";
25012
25760
  import { readFile as readFile5 } from "node:fs/promises";
25013
- import { resolve as resolve6 } from "node:path";
25761
+ import { resolve as resolve7 } from "node:path";
25014
25762
  function defaultProjectConfig(opts) {
25015
25763
  return {
25016
25764
  schemaVersion: "1",
@@ -25061,9 +25809,9 @@ function projectConfigJson(opts) {
25061
25809
  return JSON.stringify(defaultProjectConfig(opts), null, 2) + "\n";
25062
25810
  }
25063
25811
  async function readProjectConfig(projectDir) {
25064
- const dir = resolve6(projectDir);
25065
- const canonicalPath = resolve6(dir, VIBE_CONFIG_FILENAME);
25066
- const legacyPath = resolve6(dir, LEGACY_VIBE_PROJECT_FILENAME);
25812
+ const dir = resolve7(projectDir);
25813
+ const canonicalPath = resolve7(dir, VIBE_CONFIG_FILENAME);
25814
+ const legacyPath = resolve7(dir, LEGACY_VIBE_PROJECT_FILENAME);
25067
25815
  if (existsSync3(canonicalPath)) {
25068
25816
  try {
25069
25817
  const raw = await readFile5(canonicalPath, "utf-8");
@@ -25126,8 +25874,8 @@ var init_project_config = __esm({
25126
25874
  });
25127
25875
 
25128
25876
  // src/commands/_shared/scene-project.ts
25129
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile7, access as access3 } from "node:fs/promises";
25130
- import { resolve as resolve7, basename as basename2 } from "node:path";
25877
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile8, access as access3 } from "node:fs/promises";
25878
+ import { resolve as resolve8, basename as basename2 } from "node:path";
25131
25879
  function aspectToDims(aspect) {
25132
25880
  return ASPECT_DIMS[aspect];
25133
25881
  }
@@ -25425,6 +26173,43 @@ vibe host setup all --write # write project/app config
25425
26173
  vibe host doctor all --json # verify readiness
25426
26174
  \`\`\`
25427
26175
 
26176
+ ## Native host goal loop
26177
+
26178
+ For long-running work, use Codex Goal mode, Claude Code \`/goal\`, or the
26179
+ host's equivalent as the outer loop. VibeFrame should not run a competing
26180
+ project-level goal loop; it provides \`--json\` commands, dry runs, cost caps,
26181
+ \`build-report.json\`, \`review-report.json\`, \`nextActions\`, \`safeToAutoRun\`,
26182
+ \`requiresConfirmation\`, \`fixOwner\`, deterministic repair, and render
26183
+ inspection for the host agent to reason over.
26184
+
26185
+ Copy-paste examples:
26186
+
26187
+ \`\`\`text
26188
+ /goal Build this VibeFrame project into renders/final.mp4. Use --json for every
26189
+ vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
26190
+ unless the user sets another budget, read build-report.json and
26191
+ review-report.json before deciding the next action, prefer nextActions before
26192
+ guessing, run only safeToAutoRun:true actions automatically, and ask before
26193
+ requiresConfirmation:true actions. Treat retryWith as the compatibility
26194
+ fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
26195
+ STORYBOARD.md, DESIGN.md, or composition edits.
26196
+ Stop only when renders/final.mp4 exists, duration and aspect ratio match the
26197
+ brief, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
26198
+ remaining host-agent issue is fixed, accepted with rationale, or reported as
26199
+ blocked.
26200
+ \`\`\`
26201
+
26202
+ \`\`\`text
26203
+ /goal Finish this VibeFrame render using Claude Code goal mode as the outer
26204
+ loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
26205
+ paid operations, budget cap via --max-cost 5, nextActions before guessing,
26206
+ build-report.json/review-report.json as loop state, and safeToAutoRun,
26207
+ requiresConfirmation, and fixOwner to decide between vibe scene repair and
26208
+ host-agent edits. Stop only after final MP4,
26209
+ target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
26210
+ no unresolved unacknowledged host-agent issues.
26211
+ \`\`\`
26212
+
25428
26213
  ## Skills \u2014 USE THESE FIRST
25429
26214
 
25430
26215
  @SKILL.md
@@ -25528,35 +26313,35 @@ tmp/
25528
26313
  `;
25529
26314
  }
25530
26315
  function describeSceneScaffold(opts) {
25531
- const dir = resolve7(opts.dir);
26316
+ const dir = resolve8(opts.dir);
25532
26317
  const profile = opts.profile ?? "full";
25533
26318
  const groups = {
25534
26319
  authoring: [
25535
- resolve7(dir, "STORYBOARD.md"),
25536
- resolve7(dir, "DESIGN.md"),
25537
- resolve7(dir, VIBE_CONFIG_FILENAME),
25538
- resolve7(dir, "vibe.project.yaml"),
25539
- resolve7(dir, ".gitignore")
26320
+ resolve8(dir, "STORYBOARD.md"),
26321
+ resolve8(dir, "DESIGN.md"),
26322
+ resolve8(dir, VIBE_CONFIG_FILENAME),
26323
+ resolve8(dir, "vibe.project.yaml"),
26324
+ resolve8(dir, ".gitignore")
25540
26325
  ],
25541
26326
  render: [],
25542
26327
  agent: []
25543
26328
  };
25544
26329
  if (profile === "full") {
25545
26330
  groups.render = [
25546
- resolve7(dir, "index.html"),
25547
- resolve7(dir, "compositions"),
25548
- resolve7(dir, "assets"),
25549
- resolve7(dir, "renders"),
25550
- resolve7(dir, "hyperframes.json"),
25551
- resolve7(dir, "meta.json")
26331
+ resolve8(dir, "index.html"),
26332
+ resolve8(dir, "compositions"),
26333
+ resolve8(dir, "assets"),
26334
+ resolve8(dir, "renders"),
26335
+ resolve8(dir, "hyperframes.json"),
26336
+ resolve8(dir, "meta.json")
25552
26337
  ];
25553
26338
  }
25554
26339
  if (profile === "agent" || profile === "full") {
25555
26340
  groups.agent = [
25556
- resolve7(dir, "AGENTS.md"),
25557
- resolve7(dir, "SKILL.md"),
25558
- resolve7(dir, "references"),
25559
- resolve7(dir, "CLAUDE.md")
26341
+ resolve8(dir, "AGENTS.md"),
26342
+ resolve8(dir, "SKILL.md"),
26343
+ resolve8(dir, "references"),
26344
+ resolve8(dir, "CLAUDE.md")
25560
26345
  ];
25561
26346
  }
25562
26347
  return groups;
@@ -25570,7 +26355,7 @@ async function pathExists(p) {
25570
26355
  }
25571
26356
  }
25572
26357
  async function scaffoldSceneProject(opts) {
25573
- const dir = resolve7(opts.dir);
26358
+ const dir = resolve8(opts.dir);
25574
26359
  const name = opts.name ?? basename2(dir);
25575
26360
  const aspect = opts.aspect ?? "16:9";
25576
26361
  const duration = opts.duration ?? 10;
@@ -25578,103 +26363,103 @@ async function scaffoldSceneProject(opts) {
25578
26363
  const profile = opts.profile ?? "full";
25579
26364
  await mkdir4(dir, { recursive: true });
25580
26365
  if (profile === "full") {
25581
- await mkdir4(resolve7(dir, "compositions"), { recursive: true });
25582
- await mkdir4(resolve7(dir, "assets"), { recursive: true });
25583
- await mkdir4(resolve7(dir, "renders"), { recursive: true });
26366
+ await mkdir4(resolve8(dir, "compositions"), { recursive: true });
26367
+ await mkdir4(resolve8(dir, "assets"), { recursive: true });
26368
+ await mkdir4(resolve8(dir, "renders"), { recursive: true });
25584
26369
  }
25585
26370
  const created = [];
25586
26371
  const skipped2 = [];
25587
26372
  const merged = [];
25588
26373
  if (profile === "full") {
25589
- const hfPath = resolve7(dir, "hyperframes.json");
26374
+ const hfPath = resolve8(dir, "hyperframes.json");
25590
26375
  const hfDefaults = buildHyperframesConfig();
25591
26376
  if (await pathExists(hfPath)) {
25592
26377
  const existingRaw = await readFile6(hfPath, "utf-8");
25593
26378
  const existing = JSON.parse(existingRaw);
25594
26379
  const mergedConfig = mergeHyperframesConfig(existing, hfDefaults);
25595
- await writeFile7(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
26380
+ await writeFile8(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
25596
26381
  merged.push(hfPath);
25597
26382
  } else {
25598
- await writeFile7(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
26383
+ await writeFile8(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
25599
26384
  created.push(hfPath);
25600
26385
  }
25601
- const metaPath = resolve7(dir, "meta.json");
26386
+ const metaPath = resolve8(dir, "meta.json");
25602
26387
  if (await pathExists(metaPath)) {
25603
26388
  skipped2.push(metaPath);
25604
26389
  } else {
25605
- await writeFile7(
26390
+ await writeFile8(
25606
26391
  metaPath,
25607
26392
  JSON.stringify(buildHyperframesMeta(name, now), null, 2) + "\n",
25608
26393
  "utf-8"
25609
26394
  );
25610
26395
  created.push(metaPath);
25611
26396
  }
25612
- const rootPath = resolve7(dir, "index.html");
26397
+ const rootPath = resolve8(dir, "index.html");
25613
26398
  if (await pathExists(rootPath)) {
25614
26399
  skipped2.push(rootPath);
25615
26400
  } else {
25616
- await writeFile7(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
26401
+ await writeFile8(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
25617
26402
  created.push(rootPath);
25618
26403
  }
25619
26404
  }
25620
- const vibeConfigJsonPath = resolve7(dir, VIBE_CONFIG_FILENAME);
26405
+ const vibeConfigJsonPath = resolve8(dir, VIBE_CONFIG_FILENAME);
25621
26406
  if (await pathExists(vibeConfigJsonPath)) {
25622
26407
  skipped2.push(vibeConfigJsonPath);
25623
26408
  } else {
25624
- await writeFile7(vibeConfigJsonPath, projectConfigJson({ name, aspect }), "utf-8");
26409
+ await writeFile8(vibeConfigJsonPath, projectConfigJson({ name, aspect }), "utf-8");
25625
26410
  created.push(vibeConfigJsonPath);
25626
26411
  }
25627
- const vibePath = resolve7(dir, "vibe.project.yaml");
26412
+ const vibePath = resolve8(dir, "vibe.project.yaml");
25628
26413
  if (await pathExists(vibePath)) {
25629
26414
  skipped2.push(vibePath);
25630
26415
  } else {
25631
26416
  const cfg = { ...defaultVibeProjectConfig(name), aspect };
25632
- await writeFile7(vibePath, (0, import_yaml4.stringify)(cfg), "utf-8");
26417
+ await writeFile8(vibePath, (0, import_yaml4.stringify)(cfg), "utf-8");
25633
26418
  created.push(vibePath);
25634
26419
  }
25635
26420
  if (profile === "agent" || profile === "full") {
25636
- const agentsPath = resolve7(dir, "AGENTS.md");
26421
+ const agentsPath = resolve8(dir, "AGENTS.md");
25637
26422
  if (await pathExists(agentsPath)) {
25638
26423
  skipped2.push(agentsPath);
25639
26424
  } else {
25640
- await writeFile7(agentsPath, buildProjectAgentsMd(name), "utf-8");
26425
+ await writeFile8(agentsPath, buildProjectAgentsMd(name), "utf-8");
25641
26426
  created.push(agentsPath);
25642
26427
  }
25643
- const claudePath = resolve7(dir, "CLAUDE.md");
26428
+ const claudePath = resolve8(dir, "CLAUDE.md");
25644
26429
  if (await pathExists(claudePath)) {
25645
26430
  const existing = await readFile6(claudePath, "utf-8");
25646
26431
  if (existing.includes("@AGENTS.md")) {
25647
26432
  skipped2.push(claudePath);
25648
26433
  } else {
25649
- await writeFile7(claudePath, `@AGENTS.md
26434
+ await writeFile8(claudePath, `@AGENTS.md
25650
26435
 
25651
26436
  ${existing}`, "utf-8");
25652
26437
  merged.push(claudePath);
25653
26438
  }
25654
26439
  } else {
25655
- await writeFile7(claudePath, buildProjectClaudeMd(name), "utf-8");
26440
+ await writeFile8(claudePath, buildProjectClaudeMd(name), "utf-8");
25656
26441
  created.push(claudePath);
25657
26442
  }
25658
26443
  }
25659
- const designPath = resolve7(dir, "DESIGN.md");
26444
+ const designPath = resolve8(dir, "DESIGN.md");
25660
26445
  if (await pathExists(designPath)) {
25661
26446
  skipped2.push(designPath);
25662
26447
  } else {
25663
- await writeFile7(designPath, buildDesignMd({ name, style: opts.visualStyle }), "utf-8");
26448
+ await writeFile8(designPath, buildDesignMd({ name, style: opts.visualStyle }), "utf-8");
25664
26449
  created.push(designPath);
25665
26450
  }
25666
- const storyboardPath = resolve7(dir, "STORYBOARD.md");
26451
+ const storyboardPath = resolve8(dir, "STORYBOARD.md");
25667
26452
  if (await pathExists(storyboardPath)) {
25668
26453
  skipped2.push(storyboardPath);
25669
26454
  } else {
25670
- await writeFile7(storyboardPath, buildStoryboardMd(name, duration), "utf-8");
26455
+ await writeFile8(storyboardPath, buildStoryboardMd(name, duration), "utf-8");
25671
26456
  created.push(storyboardPath);
25672
26457
  }
25673
- const gitignorePath = resolve7(dir, ".gitignore");
26458
+ const gitignorePath = resolve8(dir, ".gitignore");
25674
26459
  if (await pathExists(gitignorePath)) {
25675
26460
  skipped2.push(gitignorePath);
25676
26461
  } else {
25677
- await writeFile7(gitignorePath, buildSceneGitignore(), "utf-8");
26462
+ await writeFile8(gitignorePath, buildSceneGitignore(), "utf-8");
25678
26463
  created.push(gitignorePath);
25679
26464
  }
25680
26465
  return { created, skipped: skipped2, merged, groups: describeSceneScaffold({ dir, profile }) };
@@ -28713,10 +29498,22 @@ var init_esm = __esm({
28713
29498
 
28714
29499
  // src/commands/_shared/tts-resolve.ts
28715
29500
  async function resolveTtsProvider(preferred = "auto") {
28716
- const choice = preferred === "auto" ? await getConfiguredApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
29501
+ let choice = preferred;
29502
+ if (choice === "auto") {
29503
+ if (await getConfiguredApiKey("ELEVENLABS_API_KEY")) {
29504
+ choice = "elevenlabs";
29505
+ } else if (await getConfiguredApiKey("OPENAI_API_KEY")) {
29506
+ choice = "openai";
29507
+ } else {
29508
+ choice = "kokoro";
29509
+ }
29510
+ }
28717
29511
  if (choice === "elevenlabs") {
28718
29512
  return buildElevenLabs();
28719
29513
  }
29514
+ if (choice === "openai") {
29515
+ return buildOpenAi();
29516
+ }
28720
29517
  return buildKokoro();
28721
29518
  }
28722
29519
  async function buildElevenLabs() {
@@ -28732,6 +29529,19 @@ async function buildElevenLabs() {
28732
29529
  });
28733
29530
  return { provider: "elevenlabs", audioExtension: "mp3", call };
28734
29531
  }
29532
+ async function buildOpenAi() {
29533
+ const key = await getApiKey("OPENAI_API_KEY", "OpenAI");
29534
+ if (!key) {
29535
+ throw new TtsKeyMissingError("openai");
29536
+ }
29537
+ const provider = new OpenAiTtsProvider();
29538
+ await provider.initialize({ apiKey: key });
29539
+ const call = async (text, opts) => provider.textToSpeech(text, {
29540
+ voice: opts?.voice,
29541
+ speed: opts?.speed
29542
+ });
29543
+ return { provider: "openai", audioExtension: "mp3", call };
29544
+ }
28735
29545
  async function buildKokoro() {
28736
29546
  const provider = new KokoroProvider();
28737
29547
  await provider.initialize({});
@@ -28744,11 +29554,11 @@ async function buildKokoro() {
28744
29554
  }
28745
29555
  function parseTtsProviderName(value) {
28746
29556
  if (!value) return "auto";
28747
- if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
29557
+ if (value === "auto" || value === "elevenlabs" || value === "openai" || value === "kokoro") {
28748
29558
  return value;
28749
29559
  }
28750
29560
  throw new Error(
28751
- `Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`
29561
+ `Invalid --tts: ${value}. Valid: auto, elevenlabs, openai, kokoro.`
28752
29562
  );
28753
29563
  }
28754
29564
  var TtsKeyMissingError;
@@ -28760,7 +29570,7 @@ var init_tts_resolve = __esm({
28760
29570
  TtsKeyMissingError = class extends Error {
28761
29571
  constructor(provider) {
28762
29572
  super(
28763
- 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.`
29573
+ 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.`
28764
29574
  );
28765
29575
  this.provider = provider;
28766
29576
  this.name = "TtsKeyMissingError";
@@ -28806,17 +29616,17 @@ var init_cost_tier = __esm({
28806
29616
  });
28807
29617
 
28808
29618
  // src/commands/_shared/scene-lint.ts
28809
- import { readdir as readdir2, readFile as readFile7, writeFile as writeFile8, stat as stat2 } from "node:fs/promises";
29619
+ import { readdir as readdir2, readFile as readFile7, writeFile as writeFile9, stat as stat2 } from "node:fs/promises";
28810
29620
  import { existsSync as existsSync4 } from "node:fs";
28811
- import { resolve as resolve8, relative as relative2, join as join3 } from "node:path";
29621
+ import { resolve as resolve9, relative as relative2, join as join4 } from "node:path";
28812
29622
  import {
28813
29623
  runHyperframeLint
28814
29624
  } from "@hyperframes/producer";
28815
29625
  async function discoverSceneFiles(opts) {
28816
- const projectDir = resolve8(opts.projectDir);
28817
- const rootAbs = resolve8(projectDir, opts.rootRel ?? "index.html");
29626
+ const projectDir = resolve9(opts.projectDir);
29627
+ const rootAbs = resolve9(projectDir, opts.rootRel ?? "index.html");
28818
29628
  const root = existsSync4(rootAbs) ? rootAbs : null;
28819
- const compsDir = resolve8(projectDir, "compositions");
29629
+ const compsDir = resolve9(projectDir, "compositions");
28820
29630
  const subs = [];
28821
29631
  if (existsSync4(compsDir)) {
28822
29632
  await collectHtmlRecursive(compsDir, subs);
@@ -28827,7 +29637,7 @@ async function discoverSceneFiles(opts) {
28827
29637
  async function collectHtmlRecursive(dir, into) {
28828
29638
  const entries = await readdir2(dir, { withFileTypes: true });
28829
29639
  for (const entry of entries) {
28830
- const full = join3(dir, entry.name);
29640
+ const full = join4(dir, entry.name);
28831
29641
  if (entry.isDirectory()) {
28832
29642
  await collectHtmlRecursive(full, into);
28833
29643
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
@@ -28902,7 +29712,7 @@ function addClipClassToTimedElements(html) {
28902
29712
  });
28903
29713
  }
28904
29714
  async function runProjectLint(opts) {
28905
- const projectDir = resolve8(opts.projectDir);
29715
+ const projectDir = resolve9(opts.projectDir);
28906
29716
  const { root, subs } = await discoverSceneFiles({ projectDir, rootRel: opts.rootRel });
28907
29717
  const files = [];
28908
29718
  const fixed = [];
@@ -28926,7 +29736,7 @@ async function runProjectLint(opts) {
28926
29736
  if (opts.fix && findings.length > 0) {
28927
29737
  const { html: nextHtml, fixedCodes } = applyMechanicalFixes(html, findings);
28928
29738
  if (fixedCodes.length > 0) {
28929
- await writeFile8(abs, nextHtml, "utf-8");
29739
+ await writeFile9(abs, nextHtml, "utf-8");
28930
29740
  fixed.push({ file: rel, codes: fixedCodes });
28931
29741
  const reLinted = runHyperframeLint({ ...prepared, html: nextHtml });
28932
29742
  files.push({
@@ -28963,7 +29773,7 @@ async function runProjectLint(opts) {
28963
29773
  };
28964
29774
  }
28965
29775
  async function rootExists(projectDir, rootRel = "index.html") {
28966
- const abs = resolve8(projectDir, rootRel);
29776
+ const abs = resolve9(projectDir, rootRel);
28967
29777
  try {
28968
29778
  const s = await stat2(abs);
28969
29779
  return s.isFile();
@@ -28986,7 +29796,7 @@ var init_scene_lint = __esm({
28986
29796
  // src/utils/agent-host-detect.ts
28987
29797
  import { existsSync as existsSync5 } from "node:fs";
28988
29798
  import { homedir as homedir3 } from "node:os";
28989
- import { join as join4 } from "node:path";
29799
+ import { join as join5 } from "node:path";
28990
29800
  function detectAgentHosts(env2 = process.env) {
28991
29801
  const home = homedir3();
28992
29802
  return [
@@ -29046,7 +29856,7 @@ function detectAgentHosts(env2 = process.env) {
29046
29856
  }
29047
29857
  const configDirRel = HOST_CONFIG_DIRS[host.id];
29048
29858
  if (configDirRel) {
29049
- const path2 = join4(home, configDirRel);
29859
+ const path2 = join5(home, configDirRel);
29050
29860
  if (existsSync5(path2)) {
29051
29861
  signals2.push({ kind: "configDir", path: path2 });
29052
29862
  }
@@ -29071,8 +29881,8 @@ function isOnPath(binary, env2) {
29071
29881
  const sep2 = process.platform === "win32" ? ";" : ":";
29072
29882
  for (const dir of path2.split(sep2)) {
29073
29883
  if (!dir) continue;
29074
- if (existsSync5(join4(dir, binary))) return true;
29075
- if (process.platform === "win32" && existsSync5(join4(dir, `${binary}.exe`))) return true;
29884
+ if (existsSync5(join5(dir, binary))) return true;
29885
+ if (process.platform === "win32" && existsSync5(join5(dir, `${binary}.exe`))) return true;
29076
29886
  }
29077
29887
  return false;
29078
29888
  }
@@ -29909,11 +30719,11 @@ Avoid transitions that create visible repeating geometric patterns \u2014 grids
29909
30719
 
29910
30720
  // src/commands/_shared/hf-skill-bundle/bundle.ts
29911
30721
  import { readFileSync as readFileSync2, existsSync as existsSync6, statSync as statSync2 } from "node:fs";
29912
- import { join as join5 } from "node:path";
30722
+ import { join as join6 } from "node:path";
29913
30723
  import { homedir as homedir4 } from "node:os";
29914
30724
  import { createHash as createHash3 } from "node:crypto";
29915
30725
  function installedSkillPath() {
29916
- const candidate = join5(homedir4(), ".claude", "skills", "hyperframes");
30726
+ const candidate = join6(homedir4(), ".claude", "skills", "hyperframes");
29917
30727
  if (!existsSync6(candidate)) return null;
29918
30728
  if (!statSync2(candidate).isDirectory()) return null;
29919
30729
  return candidate;
@@ -29933,11 +30743,11 @@ function buildVendored() {
29933
30743
  }
29934
30744
  function buildInstalled(skillRoot) {
29935
30745
  const installedPaths = {
29936
- "hyperframes/SKILL.md": join5(skillRoot, "SKILL.md"),
29937
- "hyperframes/house-style.md": join5(skillRoot, "house-style.md"),
29938
- "hyperframes/motion-principles.md": join5(skillRoot, "references", "motion-principles.md"),
29939
- "hyperframes/typography.md": join5(skillRoot, "references", "typography.md"),
29940
- "hyperframes/transitions.md": join5(skillRoot, "references", "transitions.md")
30746
+ "hyperframes/SKILL.md": join6(skillRoot, "SKILL.md"),
30747
+ "hyperframes/house-style.md": join6(skillRoot, "house-style.md"),
30748
+ "hyperframes/motion-principles.md": join6(skillRoot, "references", "motion-principles.md"),
30749
+ "hyperframes/typography.md": join6(skillRoot, "references", "typography.md"),
30750
+ "hyperframes/transitions.md": join6(skillRoot, "references", "transitions.md")
29941
30751
  };
29942
30752
  const sections = [];
29943
30753
  for (const [label, path2] of Object.entries(installedPaths)) {
@@ -29991,12 +30801,12 @@ var init_bundle = __esm({
29991
30801
 
29992
30802
  // src/commands/_shared/root-sync.ts
29993
30803
  import { existsSync as existsSync8 } from "node:fs";
29994
- import { readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
29995
- import { join as join7, resolve as resolve10 } from "node:path";
30804
+ import { readFile as readFile8, writeFile as writeFile11 } from "node:fs/promises";
30805
+ import { join as join8, resolve as resolve11 } from "node:path";
29996
30806
  async function createRootSyncPlan(opts) {
29997
- const projectDir = resolve10(opts.projectDir);
30807
+ const projectDir = resolve11(opts.projectDir);
29998
30808
  const rootRel = opts.rootRel ?? "index.html";
29999
- const rootPath = resolve10(projectDir, rootRel);
30809
+ const rootPath = resolve11(projectDir, rootRel);
30000
30810
  if (!existsSync8(rootPath)) {
30001
30811
  return { rootPath, rootRel, exists: false, changed: false, fixCodes: [], issues: [] };
30002
30812
  }
@@ -30054,12 +30864,12 @@ async function createProjectRootSyncPlan(opts) {
30054
30864
  }
30055
30865
  async function syncRootComposition(opts) {
30056
30866
  const plan = await createRootSyncPlan(opts);
30057
- if (plan.nextHtml) await writeFile10(plan.rootPath, plan.nextHtml, "utf-8");
30867
+ if (plan.nextHtml) await writeFile11(plan.rootPath, plan.nextHtml, "utf-8");
30058
30868
  return plan;
30059
30869
  }
30060
30870
  async function loadProjectRootSyncBeats(projectDir) {
30061
- const root = resolve10(projectDir);
30062
- const storyboardPath = join7(root, "STORYBOARD.md");
30871
+ const root = resolve11(projectDir);
30872
+ const storyboardPath = join8(root, "STORYBOARD.md");
30063
30873
  if (!existsSync8(storyboardPath)) return [];
30064
30874
  const parsed = parseStoryboard(await readFile8(storyboardPath, "utf-8"));
30065
30875
  const reportBeats = await readBuildReportBeats(root);
@@ -30128,14 +30938,14 @@ async function resolveSyncedBeatDuration(opts) {
30128
30938
  if (!opts.narrationPath) return Number((reported ?? storyboardMin).toFixed(2));
30129
30939
  const probe = opts.probeAudioDuration ?? getAudioDuration;
30130
30940
  try {
30131
- const audioDuration = await probe(join7(opts.projectDir, opts.narrationPath));
30941
+ const audioDuration = await probe(join8(opts.projectDir, opts.narrationPath));
30132
30942
  return Number(Math.max(storyboardMin, reported ?? 0, audioDuration + 0.5).toFixed(2));
30133
30943
  } catch {
30134
30944
  return Number(Math.max(storyboardMin, reported ?? 0).toFixed(2));
30135
30945
  }
30136
30946
  }
30137
30947
  async function resolveProjectBeatDurations(projectDir) {
30138
- const root = resolve10(projectDir);
30948
+ const root = resolve11(projectDir);
30139
30949
  const beats = await loadProjectRootSyncBeats(root);
30140
30950
  const durations = /* @__PURE__ */ new Map();
30141
30951
  for (const beat of beats) {
@@ -30266,7 +31076,7 @@ function hasExpectedAudioRef(html, ref) {
30266
31076
  return ref.volume === void 0 || tag.includes(`data-volume="${ref.volume}"`);
30267
31077
  }
30268
31078
  async function readBuildReportBeats(projectDir) {
30269
- const reportPath = join7(projectDir, "build-report.json");
31079
+ const reportPath = join8(projectDir, "build-report.json");
30270
31080
  if (!existsSync8(reportPath)) return [];
30271
31081
  try {
30272
31082
  const report = JSON.parse(await readFile8(reportPath, "utf-8"));
@@ -30276,7 +31086,7 @@ async function readBuildReportBeats(projectDir) {
30276
31086
  }
30277
31087
  }
30278
31088
  function firstExisting(projectDir, rels) {
30279
- return rels.find((rel) => existsSync8(join7(projectDir, rel)));
31089
+ return rels.find((rel) => existsSync8(join8(projectDir, rel)));
30280
31090
  }
30281
31091
  function stringOrUndefined(value) {
30282
31092
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -30387,8 +31197,8 @@ import OpenAI from "openai";
30387
31197
  import { GoogleGenerativeAI } from "@google/generative-ai";
30388
31198
  import { createHash as createHash4 } from "node:crypto";
30389
31199
  import { existsSync as existsSync9, mkdirSync } from "node:fs";
30390
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
30391
- import { dirname as dirname5, join as join8, resolve as resolve11 } from "node:path";
31200
+ import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile12 } from "node:fs/promises";
31201
+ import { dirname as dirname5, join as join9, resolve as resolve12 } from "node:path";
30392
31202
  import { runHyperframeLint as runHyperframeLint2 } from "@hyperframes/producer";
30393
31203
  function buildSystemPrompt(ctx) {
30394
31204
  return `You are a Hyperframes composition author. The skill content below
@@ -30482,7 +31292,15 @@ ${finalDurationBullet}
30482
31292
  itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
30483
31293
  inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
30484
31294
  phase B in, on the same timeline.
30485
- - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
31295
+ - If \`assets/video-${ctx.beat.id}.mp4\` exists, it is an AI-generated clip for
31296
+ this beat \u2014 use it as the FULL-FRAME visual and prefer it over any backdrop
31297
+ image or CSS. Put one full-bleed \`<video>\` inside a full-window \`.clip\`:
31298
+ \`<video src="assets/video-${ctx.beat.id}.mp4" data-start="0" data-duration="${BEAT_DURATION}" data-media-start="0" muted playsinline style="width:100%;height:100%;object-fit:cover"></video>\`
31299
+ Use the exact path (no \`./\` or \`../\`). The framework seeks the clip to each
31300
+ timeline frame, so do NOT add transform tweens to the \`<video>\` or its
31301
+ ancestors; layer text/motion graphics on top instead.
31302
+ - If \`assets/backdrop-${ctx.beat.id}.png\` exists (and no video clip above),
31303
+ use that local file as the
30486
31304
  full-frame visual backdrop. The exact path string is
30487
31305
  \`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
30488
31306
  because fragments are mounted from the project root.
@@ -30589,7 +31407,7 @@ function computeCacheKey(parts) {
30589
31407
  return createHash4("sha256").update(parts.provider).update("\u241E").update(parts.model).update("\u241E").update(parts.systemPrompt).update("\u241E").update(parts.userPrompt).digest("hex");
30590
31408
  }
30591
31409
  function defaultCacheDir() {
30592
- return join8(USER_CACHE_DIR, "compose-scenes");
31410
+ return join9(USER_CACHE_DIR, "compose-scenes");
30593
31411
  }
30594
31412
  function extractHtml(responseText) {
30595
31413
  const fenced = responseText.match(/```html\s*\n([\s\S]*?)\n```/);
@@ -30613,7 +31431,7 @@ async function composeBeatHtml(ctx, overrides) {
30613
31431
  model: settings.model
30614
31432
  });
30615
31433
  const cacheDir = ctx.cacheDir ?? defaultCacheDir();
30616
- const cachePath = join8(cacheDir, `${cacheKey}.html`);
31434
+ const cachePath = join9(cacheDir, `${cacheKey}.html`);
30617
31435
  if (existsSync9(cachePath)) {
30618
31436
  const html2 = await readFile9(cachePath, "utf-8");
30619
31437
  return { html: html2, cached: true, cacheKey, model: settings.model, provider: ctx.provider };
@@ -30649,7 +31467,7 @@ async function composeBeatHtml(ctx, overrides) {
30649
31467
  const html = extractHtml(result.text);
30650
31468
  try {
30651
31469
  mkdirSync(cacheDir, { recursive: true });
30652
- await writeFile11(cachePath, html, "utf-8");
31470
+ await writeFile12(cachePath, html, "utf-8");
30653
31471
  } catch {
30654
31472
  }
30655
31473
  const costUsd = result.inputTokens / 1e6 * settings.costPerMTokIn + result.outputTokens / 1e6 * settings.costPerMTokOut;
@@ -30748,9 +31566,9 @@ async function resolveComposerConfigAware(explicit, cwd) {
30748
31566
  return resolveComposer();
30749
31567
  }
30750
31568
  async function executeComposeScenesWithSkills(params, outputDir, overrides) {
30751
- const projectRoot = params.project ? resolve11(outputDir, params.project) : resolve11(outputDir);
30752
- const designPath = resolve11(projectRoot, params.design ?? "DESIGN.md");
30753
- const storyboardPath = resolve11(projectRoot, params.storyboard ?? "STORYBOARD.md");
31569
+ const projectRoot = params.project ? resolve12(outputDir, params.project) : resolve12(outputDir);
31570
+ const designPath = resolve12(projectRoot, params.design ?? "DESIGN.md");
31571
+ const storyboardPath = resolve12(projectRoot, params.storyboard ?? "STORYBOARD.md");
30754
31572
  if (!existsSync9(designPath)) {
30755
31573
  return { success: false, error: `DESIGN.md not found at ${designPath}` };
30756
31574
  }
@@ -30778,7 +31596,7 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
30778
31596
  provider = params.composer ?? "claude";
30779
31597
  apiKey = "";
30780
31598
  }
30781
- const compositionsDir = join8(projectRoot, "compositions");
31599
+ const compositionsDir = join9(projectRoot, "compositions");
30782
31600
  await mkdir6(compositionsDir, { recursive: true });
30783
31601
  let finalDurations = /* @__PURE__ */ new Map();
30784
31602
  try {
@@ -30806,9 +31624,9 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
30806
31624
  },
30807
31625
  overrides
30808
31626
  );
30809
- const compositionPath = join8(compositionsDir, `scene-${beat.id}.html`);
31627
+ const compositionPath = join9(compositionsDir, `scene-${beat.id}.html`);
30810
31628
  await mkdir6(dirname5(compositionPath), { recursive: true });
30811
- await writeFile11(compositionPath, result.html, "utf-8");
31629
+ await writeFile12(compositionPath, result.html, "utf-8");
30812
31630
  if (result.cached) {
30813
31631
  onProgress({
30814
31632
  type: "beat-cached",
@@ -31007,13 +31825,13 @@ var init_compose_scenes_skills = __esm({
31007
31825
  // src/commands/_shared/compose-prompts.ts
31008
31826
  import { existsSync as existsSync10 } from "node:fs";
31009
31827
  import { readFile as readFile10 } from "node:fs/promises";
31010
- import { join as join9, relative as relative4, resolve as resolve12 } from "node:path";
31828
+ import { join as join10, relative as relative4, resolve as resolve13 } from "node:path";
31011
31829
  async function getComposePrompts(opts) {
31012
- const projectDir = resolve12(opts.projectDir);
31013
- const designPath = join9(projectDir, "DESIGN.md");
31014
- const storyboardPath = join9(projectDir, "STORYBOARD.md");
31015
- const skillPath = join9(projectDir, "SKILL.md");
31016
- const compositionsDir = join9(projectDir, "compositions");
31830
+ const projectDir = resolve13(opts.projectDir);
31831
+ const designPath = join10(projectDir, "DESIGN.md");
31832
+ const storyboardPath = join10(projectDir, "STORYBOARD.md");
31833
+ const skillPath = join10(projectDir, "SKILL.md");
31834
+ const compositionsDir = join10(projectDir, "compositions");
31017
31835
  const warnings = [];
31018
31836
  const baseError = (msg) => ({
31019
31837
  success: false,
@@ -31062,7 +31880,7 @@ async function getComposePrompts(opts) {
31062
31880
  } catch {
31063
31881
  }
31064
31882
  const result = beats.map((beat) => {
31065
- const outputPathAbs = join9(compositionsDir, `scene-${beat.id}.html`);
31883
+ const outputPathAbs = join10(compositionsDir, `scene-${beat.id}.html`);
31066
31884
  const outputPathRel = relative4(projectDir, outputPathAbs);
31067
31885
  const finalDurationSec = finalDurations.get(beat.id);
31068
31886
  const userPrompt = buildUserPrompt({
@@ -31136,7 +31954,7 @@ var init_compose_prompts = __esm({
31136
31954
  // src/commands/_shared/sub-comp-duration-sync.ts
31137
31955
  import { existsSync as existsSync12 } from "node:fs";
31138
31956
  import { readFile as readFile12 } from "node:fs/promises";
31139
- import { join as join11, resolve as resolve14 } from "node:path";
31957
+ import { join as join12, resolve as resolve15 } from "node:path";
31140
31958
  function syncSubCompositionDurationHtml(html, beatId, newDurationSec, fileRel) {
31141
31959
  const compositionId = `scene-${beatId}`;
31142
31960
  const file = fileRel ?? `compositions/${compositionId}.html`;
@@ -31224,11 +32042,11 @@ function rewriteTimelineConst(html, oldDurationSec, newDurationSec) {
31224
32042
  return { html, rewritten: false, needsManualFix };
31225
32043
  }
31226
32044
  async function createSubCompDurationSyncPlans(opts) {
31227
- const projectDir = resolve14(opts.projectDir);
32045
+ const projectDir = resolve15(opts.projectDir);
31228
32046
  const results = [];
31229
32047
  for (const beat of opts.beats) {
31230
32048
  const fileRel = `compositions/scene-${beat.id}.html`;
31231
- const filePath = join11(projectDir, fileRel);
32049
+ const filePath = join12(projectDir, fileRel);
31232
32050
  if (!existsSync12(filePath)) continue;
31233
32051
  const newDurationSec = await resolveSyncedBeatDuration({
31234
32052
  projectDir,
@@ -31259,88 +32077,6 @@ var init_sub_comp_duration_sync = __esm({
31259
32077
  }
31260
32078
  });
31261
32079
 
31262
- // src/commands/_shared/review-report.ts
31263
- import { writeFile as writeFile13 } from "node:fs/promises";
31264
- import { join as join12, resolve as resolve15 } from "node:path";
31265
- function statusFromIssues(issues) {
31266
- if (issues.some((issue) => issue.severity === "error")) return "fail";
31267
- if (issues.some((issue) => issue.severity === "warning")) return "warn";
31268
- return "pass";
31269
- }
31270
- function scoreIssues(issues) {
31271
- let score = 100;
31272
- for (const issue of issues) {
31273
- if (issue.severity === "error") score -= 25;
31274
- else if (issue.severity === "warning") score -= 8;
31275
- else score -= 2;
31276
- }
31277
- return Math.max(0, Math.min(100, score));
31278
- }
31279
- function uniqueRetryWith(items) {
31280
- return [
31281
- ...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
31282
- ];
31283
- }
31284
- function fixOwnerForIssue(issue) {
31285
- if (issue.fixOwner) return issue.fixOwner;
31286
- if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
31287
- if (issue.code.startsWith("STORYBOARD_")) return "host-agent";
31288
- if (issue.code === "PROJECT_NOT_FOUND" || issue.code === "MISSING_STORYBOARD" || issue.code === "BEAT_NOT_FOUND" || issue.code === "MISSING_DESIGN") {
31289
- return "host-agent";
31290
- }
31291
- return "vibe";
31292
- }
31293
- function normalizeReviewIssues(issues, fallbackOwner) {
31294
- return issues.map((issue) => ({
31295
- ...issue,
31296
- fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
31297
- }));
31298
- }
31299
- function summarizeReviewIssues(issues) {
31300
- const normalized = normalizeReviewIssues(issues);
31301
- return {
31302
- issueCount: normalized.length,
31303
- errorCount: normalized.filter((issue) => issue.severity === "error").length,
31304
- warningCount: normalized.filter((issue) => issue.severity === "warning").length,
31305
- infoCount: normalized.filter((issue) => issue.severity === "info").length,
31306
- fixOwners: {
31307
- vibe: normalized.filter((issue) => issue.fixOwner === "vibe").length,
31308
- hostAgent: normalized.filter((issue) => issue.fixOwner === "host-agent").length
31309
- }
31310
- };
31311
- }
31312
- function buildReviewReport(opts) {
31313
- const issues = normalizeReviewIssues(opts.issues);
31314
- return stripUndefined2({
31315
- schemaVersion: "1",
31316
- kind: "review",
31317
- project: resolve15(opts.project),
31318
- mode: opts.mode,
31319
- beat: opts.beat,
31320
- status: opts.status,
31321
- score: opts.score,
31322
- issues,
31323
- summary: summarizeReviewIssues(issues),
31324
- retryWith: uniqueRetryWith(opts.retryWith),
31325
- sourceReports: opts.sourceReports ?? [],
31326
- reportPath: opts.reportPath
31327
- });
31328
- }
31329
- function defaultReviewReportPath(projectDir) {
31330
- return join12(resolve15(projectDir), "review-report.json");
31331
- }
31332
- async function writeReviewReport(path2, report) {
31333
- await writeFile13(path2, JSON.stringify(report, null, 2) + "\n", "utf-8");
31334
- }
31335
- function stripUndefined2(value) {
31336
- return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
31337
- }
31338
- var init_review_report = __esm({
31339
- "src/commands/_shared/review-report.ts"() {
31340
- "use strict";
31341
- }
31342
- });
31343
-
31344
32080
  // src/commands/_shared/scene-repair.ts
31345
32081
  import { readFile as readFile13, writeFile as writeFile14 } from "node:fs/promises";
31346
32082
  import { relative as relative5, resolve as resolve16 } from "node:path";
@@ -31467,6 +32203,297 @@ var init_scene_repair = __esm({
31467
32203
  }
31468
32204
  });
31469
32205
 
32206
+ // src/commands/_shared/storyboard-edit.ts
32207
+ function validateStoryboardMarkdown(markdown) {
32208
+ const parsed = parseStoryboard(markdown);
32209
+ const sections = splitBeatSections(markdown);
32210
+ const issues = [];
32211
+ if (parsed.beats.length === 0) {
32212
+ issues.push({
32213
+ severity: "error",
32214
+ code: "NO_BEATS",
32215
+ message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
32216
+ });
32217
+ }
32218
+ const seen = /* @__PURE__ */ new Map();
32219
+ for (const beat of parsed.beats) {
32220
+ seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
32221
+ }
32222
+ for (const [id, count] of seen.entries()) {
32223
+ if (count > 1) {
32224
+ issues.push({
32225
+ severity: "error",
32226
+ code: "DUPLICATE_BEAT_ID",
32227
+ beatId: id,
32228
+ message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
32229
+ });
32230
+ }
32231
+ }
32232
+ for (const section of sections) {
32233
+ const cueBlock = readLeadingCueBlock(section.body);
32234
+ if (cueBlock?.error) {
32235
+ issues.push({
32236
+ severity: "error",
32237
+ code: "MALFORMED_CUE_YAML",
32238
+ beatId: section.id,
32239
+ message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
32240
+ });
32241
+ }
32242
+ }
32243
+ const characterPool = new Set(
32244
+ Object.keys(
32245
+ parsed.frontmatter?.characters ?? {}
32246
+ )
32247
+ );
32248
+ for (const beat of parsed.beats) {
32249
+ const cues = beat.cues ?? {};
32250
+ for (const [key, value] of Object.entries(cues)) {
32251
+ if (!ALLOWED_CUE_KEYS.has(key)) {
32252
+ issues.push({
32253
+ severity: "warning",
32254
+ code: "UNKNOWN_CUE",
32255
+ beatId: beat.id,
32256
+ message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
32257
+ });
32258
+ continue;
32259
+ }
32260
+ if (key === "characters") {
32261
+ const validShape = typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
32262
+ if (!validShape) {
32263
+ issues.push({
32264
+ severity: "error",
32265
+ code: "INVALID_CHARACTERS_VALUE",
32266
+ beatId: beat.id,
32267
+ message: `Beat "${beat.id}" cue "characters" must be a character name or a list of names.`
32268
+ });
32269
+ continue;
32270
+ }
32271
+ for (const name of beatCharacterNames({ characters: value })) {
32272
+ if (!characterPool.has(name)) {
32273
+ issues.push({
32274
+ severity: "warning",
32275
+ code: "UNKNOWN_CHARACTER",
32276
+ beatId: beat.id,
32277
+ message: `Beat "${beat.id}" references character "${name}" not defined in the frontmatter \`characters:\` pool.`
32278
+ });
32279
+ }
32280
+ }
32281
+ continue;
32282
+ }
32283
+ if (key === "duration") {
32284
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
32285
+ issues.push({
32286
+ severity: "error",
32287
+ code: "INVALID_DURATION",
32288
+ beatId: beat.id,
32289
+ message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
32290
+ });
32291
+ }
32292
+ continue;
32293
+ }
32294
+ if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
32295
+ issues.push({
32296
+ severity: "error",
32297
+ code: "INVALID_CUE_VALUE",
32298
+ beatId: beat.id,
32299
+ message: `Beat "${beat.id}" cue "${key}" must be a string.`
32300
+ });
32301
+ }
32302
+ }
32303
+ }
32304
+ for (const beat of parsed.beats) {
32305
+ if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
32306
+ issues.push({
32307
+ severity: "warning",
32308
+ code: "BEAT_DURATION_TOO_LONG",
32309
+ beatId: beat.id,
32310
+ message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
32311
+ });
32312
+ }
32313
+ }
32314
+ return {
32315
+ ok: !issues.some((i) => i.severity === "error"),
32316
+ beats: parsed.beats,
32317
+ issues
32318
+ };
32319
+ }
32320
+ function getStoryboardBeat(markdown, beatId) {
32321
+ return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
32322
+ }
32323
+ function upsertStoryboardBeat(markdown, opts) {
32324
+ const beatId = normalizeBeatId(opts.beatId);
32325
+ const sections = splitBeatSections(markdown);
32326
+ const existing = sections.find((section2) => section2.id === beatId);
32327
+ const cues = storyboardCuesFromInput(opts);
32328
+ if (existing) {
32329
+ let next = markdown;
32330
+ for (const [key, value] of Object.entries(cues)) {
32331
+ next = setStoryboardCue(next, { beatId, key, value });
32332
+ }
32333
+ return { markdown: next, action: "updated" };
32334
+ }
32335
+ const section = buildStoryboardBeatSection({ ...opts, beatId, cues });
32336
+ if (sections.length > 0 && isStarterStoryboard(markdown)) {
32337
+ const global2 = markdown.slice(0, sections[0].start).trimEnd();
32338
+ return {
32339
+ markdown: `${global2}
32340
+
32341
+ ${section}`,
32342
+ action: "replaced-starter"
32343
+ };
32344
+ }
32345
+ const prefix = markdown.trimEnd();
32346
+ return {
32347
+ markdown: `${prefix}${prefix ? "\n\n" : ""}${section}`,
32348
+ action: "appended"
32349
+ };
32350
+ }
32351
+ function setStoryboardCue(markdown, opts) {
32352
+ const section = findBeatSection(markdown, opts.beatId);
32353
+ if (!section) {
32354
+ throw new Error(`Beat "${opts.beatId}" not found.`);
32355
+ }
32356
+ if (!ALLOWED_CUE_KEYS.has(opts.key)) {
32357
+ throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
32358
+ }
32359
+ const cue = readLeadingCueBlock(section.body);
32360
+ if (cue?.error) {
32361
+ throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
32362
+ }
32363
+ const cues = cue?.value ? { ...cue.value } : {};
32364
+ if (opts.unset) {
32365
+ delete cues[opts.key];
32366
+ } else {
32367
+ cues[opts.key] = normalizeCueValue(opts.key, opts.value);
32368
+ }
32369
+ const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
32370
+ const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
32371
+ const nextBody = cueBlock + remainingBody.trimStart();
32372
+ const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
32373
+ return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
32374
+ }
32375
+ function moveStoryboardBeat(markdown, opts) {
32376
+ if (opts.beatId === opts.afterBeatId) return markdown;
32377
+ const sections = splitBeatSections(markdown);
32378
+ const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
32379
+ if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
32380
+ const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
32381
+ if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
32382
+ const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
32383
+ const chunks = sections.map((section) => section.raw);
32384
+ const [moving] = chunks.splice(movingIndex, 1);
32385
+ const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
32386
+ chunks.splice(adjustedAfterIndex + 1, 0, moving);
32387
+ return global2 + normalizeBeatChunks(chunks);
32388
+ }
32389
+ function normalizeBeatChunks(chunks) {
32390
+ return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
32391
+ }
32392
+ function normalizeBeatId(id) {
32393
+ return deriveBeatId(id);
32394
+ }
32395
+ function storyboardCuesFromInput(opts) {
32396
+ const cues = {};
32397
+ if (opts.duration !== void 0) cues.duration = opts.duration;
32398
+ if (opts.narration && opts.narration.trim()) cues.narration = opts.narration.trim();
32399
+ if (opts.backdrop && opts.backdrop.trim()) cues.backdrop = opts.backdrop.trim();
32400
+ return cues;
32401
+ }
32402
+ function buildStoryboardBeatSection(opts) {
32403
+ const title = normalizeTitle(opts.title ?? humanizeBeatId(opts.beatId));
32404
+ const cueBlock = Object.keys(opts.cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(opts.cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
32405
+ const body = opts.body?.trim() || "Scene created with `vibe scene add`. Edit this body if the build flow should recompose it.";
32406
+ return `## Beat ${opts.beatId} - ${title}
32407
+
32408
+ ${cueBlock}${body.trimEnd()}
32409
+ `;
32410
+ }
32411
+ function normalizeTitle(value) {
32412
+ return value.replace(/\s+/g, " ").trim() || "Scene";
32413
+ }
32414
+ function humanizeBeatId(id) {
32415
+ return id.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Scene";
32416
+ }
32417
+ function isStarterStoryboard(markdown) {
32418
+ const parsed = parseStoryboard(markdown);
32419
+ if (parsed.beats.length !== 3) return false;
32420
+ const ids = parsed.beats.map((beat) => beat.id);
32421
+ if (ids.join(",") !== "hook,proof,close") return false;
32422
+ return markdown.includes("Introduce the promise in one crisp sentence.") && markdown.includes("Show the mechanism or proof point that makes the promise believable.") && markdown.includes("Close with the action the viewer should remember.");
32423
+ }
32424
+ function normalizeCueValue(key, value) {
32425
+ if (key === "duration") {
32426
+ const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
32427
+ if (!Number.isFinite(n) || n <= 0) {
32428
+ throw new Error("Cue duration must be a positive number.");
32429
+ }
32430
+ return n;
32431
+ }
32432
+ if (typeof value !== "string") return value;
32433
+ return value.trim();
32434
+ }
32435
+ function splitBeatSections(markdown) {
32436
+ const normalized = markdown.replace(/\r\n/g, "\n");
32437
+ const headings = [];
32438
+ HEADING_RE2.lastIndex = 0;
32439
+ let match;
32440
+ while ((match = HEADING_RE2.exec(normalized)) !== null) {
32441
+ headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
32442
+ }
32443
+ return headings.map((heading, index) => {
32444
+ const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
32445
+ return {
32446
+ id: deriveBeatId(heading.line),
32447
+ heading: heading.line,
32448
+ start: heading.start,
32449
+ headingEnd: heading.end,
32450
+ end,
32451
+ raw: normalized.slice(heading.start, end),
32452
+ body: normalized.slice(heading.end, end).trim()
32453
+ };
32454
+ });
32455
+ }
32456
+ function findBeatSection(markdown, beatId) {
32457
+ return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
32458
+ }
32459
+ function readLeadingCueBlock(body) {
32460
+ const match = body.match(LEADING_CUE_RE);
32461
+ if (!match) return null;
32462
+ try {
32463
+ const parsed = (0, import_yaml5.parse)(match[2]);
32464
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32465
+ return { full: match[0], error: "cue block must parse to a YAML object" };
32466
+ }
32467
+ return { full: match[0], value: parsed };
32468
+ } catch (error) {
32469
+ return { full: match[0], error: error instanceof Error ? error.message : String(error) };
32470
+ }
32471
+ }
32472
+ var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
32473
+ var init_storyboard_edit = __esm({
32474
+ "src/commands/_shared/storyboard-edit.ts"() {
32475
+ "use strict";
32476
+ import_yaml5 = __toESM(require_dist2(), 1);
32477
+ init_storyboard_parse();
32478
+ STORYBOARD_CUE_KEYS = [
32479
+ "duration",
32480
+ "narration",
32481
+ "backdrop",
32482
+ "video",
32483
+ "motion",
32484
+ "voice",
32485
+ "music",
32486
+ "asset",
32487
+ "characters"
32488
+ ];
32489
+ HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
32490
+ LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
32491
+ ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
32492
+ STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
32493
+ MAX_RECOMMENDED_BEAT_SEC = 15;
32494
+ }
32495
+ });
32496
+
31470
32497
  // src/pipeline/renderers/chrome.ts
31471
32498
  import { existsSync as existsSync14 } from "node:fs";
31472
32499
  import { homedir as homedir5 } from "node:os";
@@ -32383,6 +33410,7 @@ var init_build_asset_reference = __esm({
32383
33410
  EXTENSIONS = {
32384
33411
  narration: [".mp3", ".wav", ".m4a"],
32385
33412
  backdrop: [".png", ".jpg", ".jpeg", ".webp"],
33413
+ character: [".png", ".jpg", ".jpeg", ".webp"],
32386
33414
  video: [".mp4", ".mov", ".webm"],
32387
33415
  music: [".mp3", ".wav", ".m4a"]
32388
33416
  };
@@ -32420,6 +33448,16 @@ function backdropCacheDescriptor(opts) {
32420
33448
  ext: "png"
32421
33449
  });
32422
33450
  }
33451
+ function characterCacheDescriptor(opts) {
33452
+ return cacheAssetDescriptor("character", {
33453
+ name: opts.name,
33454
+ cue: opts.cue,
33455
+ provider: opts.provider,
33456
+ quality: opts.quality,
33457
+ size: opts.size,
33458
+ ext: "png"
33459
+ });
33460
+ }
32423
33461
  function imageRatioForSize(size) {
32424
33462
  switch (size) {
32425
33463
  case "1024x1024":
@@ -32439,6 +33477,8 @@ function videoCacheDescriptor(opts) {
32439
33477
  provider: opts.provider,
32440
33478
  duration: normalizeVideoDuration(opts.duration),
32441
33479
  ratio: "16:9",
33480
+ // Omit when empty so existing (character-less) clips keep their cache key.
33481
+ characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
32442
33482
  ext: "mp4"
32443
33483
  });
32444
33484
  }
@@ -32491,208 +33531,6 @@ var init_build_backdrop_prompt = __esm({
32491
33531
  }
32492
33532
  });
32493
33533
 
32494
- // src/commands/_shared/storyboard-edit.ts
32495
- function validateStoryboardMarkdown(markdown) {
32496
- const parsed = parseStoryboard(markdown);
32497
- const sections = splitBeatSections(markdown);
32498
- const issues = [];
32499
- if (parsed.beats.length === 0) {
32500
- issues.push({
32501
- severity: "error",
32502
- code: "NO_BEATS",
32503
- message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
32504
- });
32505
- }
32506
- const seen = /* @__PURE__ */ new Map();
32507
- for (const beat of parsed.beats) {
32508
- seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
32509
- }
32510
- for (const [id, count] of seen.entries()) {
32511
- if (count > 1) {
32512
- issues.push({
32513
- severity: "error",
32514
- code: "DUPLICATE_BEAT_ID",
32515
- beatId: id,
32516
- message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
32517
- });
32518
- }
32519
- }
32520
- for (const section of sections) {
32521
- const cueBlock = readLeadingCueBlock(section.body);
32522
- if (cueBlock?.error) {
32523
- issues.push({
32524
- severity: "error",
32525
- code: "MALFORMED_CUE_YAML",
32526
- beatId: section.id,
32527
- message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
32528
- });
32529
- }
32530
- }
32531
- for (const beat of parsed.beats) {
32532
- const cues = beat.cues ?? {};
32533
- for (const [key, value] of Object.entries(cues)) {
32534
- if (!ALLOWED_CUE_KEYS.has(key)) {
32535
- issues.push({
32536
- severity: "warning",
32537
- code: "UNKNOWN_CUE",
32538
- beatId: beat.id,
32539
- message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
32540
- });
32541
- continue;
32542
- }
32543
- if (key === "duration") {
32544
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
32545
- issues.push({
32546
- severity: "error",
32547
- code: "INVALID_DURATION",
32548
- beatId: beat.id,
32549
- message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
32550
- });
32551
- }
32552
- continue;
32553
- }
32554
- if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
32555
- issues.push({
32556
- severity: "error",
32557
- code: "INVALID_CUE_VALUE",
32558
- beatId: beat.id,
32559
- message: `Beat "${beat.id}" cue "${key}" must be a string.`
32560
- });
32561
- }
32562
- }
32563
- }
32564
- for (const beat of parsed.beats) {
32565
- if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
32566
- issues.push({
32567
- severity: "warning",
32568
- code: "BEAT_DURATION_TOO_LONG",
32569
- beatId: beat.id,
32570
- message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
32571
- });
32572
- }
32573
- }
32574
- return {
32575
- ok: !issues.some((i) => i.severity === "error"),
32576
- beats: parsed.beats,
32577
- issues
32578
- };
32579
- }
32580
- function getStoryboardBeat(markdown, beatId) {
32581
- return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
32582
- }
32583
- function setStoryboardCue(markdown, opts) {
32584
- const section = findBeatSection(markdown, opts.beatId);
32585
- if (!section) {
32586
- throw new Error(`Beat "${opts.beatId}" not found.`);
32587
- }
32588
- if (!ALLOWED_CUE_KEYS.has(opts.key)) {
32589
- throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
32590
- }
32591
- const cue = readLeadingCueBlock(section.body);
32592
- if (cue?.error) {
32593
- throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
32594
- }
32595
- const cues = cue?.value ? { ...cue.value } : {};
32596
- if (opts.unset) {
32597
- delete cues[opts.key];
32598
- } else {
32599
- cues[opts.key] = normalizeCueValue(opts.key, opts.value);
32600
- }
32601
- const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
32602
- const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
32603
- const nextBody = cueBlock + remainingBody.trimStart();
32604
- const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
32605
- return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
32606
- }
32607
- function moveStoryboardBeat(markdown, opts) {
32608
- if (opts.beatId === opts.afterBeatId) return markdown;
32609
- const sections = splitBeatSections(markdown);
32610
- const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
32611
- if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
32612
- const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
32613
- if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
32614
- const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
32615
- const chunks = sections.map((section) => section.raw);
32616
- const [moving] = chunks.splice(movingIndex, 1);
32617
- const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
32618
- chunks.splice(adjustedAfterIndex + 1, 0, moving);
32619
- return global2 + normalizeBeatChunks(chunks);
32620
- }
32621
- function normalizeBeatChunks(chunks) {
32622
- return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
32623
- }
32624
- function normalizeCueValue(key, value) {
32625
- if (key === "duration") {
32626
- const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
32627
- if (!Number.isFinite(n) || n <= 0) {
32628
- throw new Error("Cue duration must be a positive number.");
32629
- }
32630
- return n;
32631
- }
32632
- if (typeof value !== "string") return value;
32633
- return value.trim();
32634
- }
32635
- function splitBeatSections(markdown) {
32636
- const normalized = markdown.replace(/\r\n/g, "\n");
32637
- const headings = [];
32638
- HEADING_RE2.lastIndex = 0;
32639
- let match;
32640
- while ((match = HEADING_RE2.exec(normalized)) !== null) {
32641
- headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
32642
- }
32643
- return headings.map((heading, index) => {
32644
- const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
32645
- return {
32646
- id: deriveBeatId(heading.line),
32647
- heading: heading.line,
32648
- start: heading.start,
32649
- headingEnd: heading.end,
32650
- end,
32651
- raw: normalized.slice(heading.start, end),
32652
- body: normalized.slice(heading.end, end).trim()
32653
- };
32654
- });
32655
- }
32656
- function findBeatSection(markdown, beatId) {
32657
- return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
32658
- }
32659
- function readLeadingCueBlock(body) {
32660
- const match = body.match(LEADING_CUE_RE);
32661
- if (!match) return null;
32662
- try {
32663
- const parsed = (0, import_yaml5.parse)(match[2]);
32664
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32665
- return { full: match[0], error: "cue block must parse to a YAML object" };
32666
- }
32667
- return { full: match[0], value: parsed };
32668
- } catch (error) {
32669
- return { full: match[0], error: error instanceof Error ? error.message : String(error) };
32670
- }
32671
- }
32672
- var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
32673
- var init_storyboard_edit = __esm({
32674
- "src/commands/_shared/storyboard-edit.ts"() {
32675
- "use strict";
32676
- import_yaml5 = __toESM(require_dist2(), 1);
32677
- init_storyboard_parse();
32678
- STORYBOARD_CUE_KEYS = [
32679
- "duration",
32680
- "narration",
32681
- "backdrop",
32682
- "video",
32683
- "motion",
32684
- "voice",
32685
- "music",
32686
- "asset"
32687
- ];
32688
- HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
32689
- LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
32690
- ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
32691
- STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
32692
- MAX_RECOMMENDED_BEAT_SEC = 15;
32693
- }
32694
- });
32695
-
32696
33534
  // src/commands/_shared/build-plan.ts
32697
33535
  import { existsSync as existsSync17 } from "node:fs";
32698
33536
  import { readFile as readFile17 } from "node:fs/promises";
@@ -32801,13 +33639,13 @@ async function createBuildPlan(opts) {
32801
33639
  const backdropCue = backdropPrompt ?? backdropReference?.raw;
32802
33640
  const videoCue = videoPrompt ?? videoReference?.raw;
32803
33641
  const musicCue = musicPrompt ?? musicReference?.raw;
32804
- const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : 0;
33642
+ const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
32805
33643
  const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
32806
33644
  beatId: beat.id,
32807
33645
  cue: narrationText,
32808
33646
  provider: resolved.narration.resolved,
32809
33647
  voice,
32810
- ext: resolved.narration.resolved === "elevenlabs" ? "mp3" : "wav"
33648
+ ext: resolved.narration.resolved === "kokoro" ? "wav" : "mp3"
32811
33649
  }) : null;
32812
33650
  const backdropCache = augmentedBackdropPrompt && !backdropReference ? backdropCacheDescriptor({
32813
33651
  beatId: beat.id,
@@ -32868,7 +33706,11 @@ async function createBuildPlan(opts) {
32868
33706
  reference: videoReference,
32869
33707
  projectDir,
32870
33708
  force: opts.force,
32871
- cost: VIDEO_COST_USD,
33709
+ cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
33710
+ durationSec: normalizeVideoDuration(beat.duration),
33711
+ resolution: "720p",
33712
+ aspectRatio: "16:9"
33713
+ }) : VIDEO_COST_USD,
32872
33714
  active: includeAssets
32873
33715
  }) : null;
32874
33716
  const music = musicCue && !opts.skipMusic ? assetPlan({
@@ -32979,6 +33821,21 @@ async function createBuildPlan(opts) {
32979
33821
  retryWith.push(skipCommand);
32980
33822
  }
32981
33823
  }
33824
+ if (validationOk && includeAssets && !opts.skipVideo) {
33825
+ const referenced = /* @__PURE__ */ new Set();
33826
+ for (const beat of sourceBeats) {
33827
+ for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
33828
+ }
33829
+ if (referenced.size > 0) {
33830
+ const toGenerate = resolveCharacters(parsed.frontmatter).filter(
33831
+ (c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync17(join15(projectDir, `assets/character-${c.name}.png`)))
33832
+ );
33833
+ if (toGenerate.length > 0) {
33834
+ estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
33835
+ missing.add("assets");
33836
+ }
33837
+ }
33838
+ }
32982
33839
  return finalizeBuildPlan({
32983
33840
  projectDir,
32984
33841
  config: config2,
@@ -33225,17 +34082,20 @@ function providerInput(opts) {
33225
34082
  async function resolveNarrationProvider(input, projectDir) {
33226
34083
  const requested = input.value.toLowerCase();
33227
34084
  const hasElevenLabs = Boolean(await getApiKeyFromConfig("elevenlabs", { cwd: projectDir }));
33228
- const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : "kokoro";
34085
+ const hasOpenAi = Boolean(await getApiKeyFromConfig("openai", { cwd: projectDir }));
34086
+ const resolved = requested === "elevenlabs" ? "elevenlabs" : requested === "openai" ? "openai" : requested === "kokoro" ? "kokoro" : hasElevenLabs ? "elevenlabs" : hasOpenAi ? "openai" : "kokoro";
34087
+ const requiresKey = resolved !== "kokoro";
34088
+ const hasKey = resolved === "elevenlabs" ? hasElevenLabs : resolved === "openai" ? hasOpenAi : true;
33229
34089
  return {
33230
34090
  kind: "narration",
33231
34091
  requested: input.requested,
33232
34092
  resolved,
33233
34093
  source: input.source,
33234
- requiresKey: resolved === "elevenlabs",
33235
- configured: resolved !== "elevenlabs" || hasElevenLabs,
33236
- ...resolved === "elevenlabs" ? { configKey: "elevenlabs" } : {},
33237
- ...resolved === "elevenlabs" && !hasElevenLabs ? { missingKey: providerEnvVar("elevenlabs") } : {},
33238
- retryWith: resolved === "elevenlabs" && !hasElevenLabs ? ["vibe setup --full"] : []
34094
+ requiresKey,
34095
+ configured: hasKey,
34096
+ ...requiresKey ? { configKey: resolved } : {},
34097
+ ...requiresKey && !hasKey ? { missingKey: providerEnvVar(resolved) } : {},
34098
+ retryWith: requiresKey && !hasKey ? ["vibe setup --full"] : []
33239
34099
  };
33240
34100
  }
33241
34101
  async function keyedProviderResolution(opts, projectDir) {
@@ -33331,7 +34191,7 @@ function loadPlanEnv(projectDir) {
33331
34191
  dir = dirname9(dir);
33332
34192
  }
33333
34193
  }
33334
- var import_dotenv2, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
34194
+ var import_dotenv2, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, OPENAI_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
33335
34195
  var init_build_plan = __esm({
33336
34196
  "src/commands/_shared/build-plan.ts"() {
33337
34197
  "use strict";
@@ -33340,6 +34200,7 @@ var init_build_plan = __esm({
33340
34200
  init_schema();
33341
34201
  init_build_asset_reference();
33342
34202
  init_build_cache();
34203
+ init_dist2();
33343
34204
  init_build_asset_metadata();
33344
34205
  init_build_backdrop_prompt();
33345
34206
  init_composer_resolve();
@@ -33349,6 +34210,7 @@ var init_build_plan = __esm({
33349
34210
  VIDEO_COST_USD = 5;
33350
34211
  MUSIC_COST_USD = 0.5;
33351
34212
  ELEVENLABS_NARRATION_COST_USD = 0.05;
34213
+ OPENAI_NARRATION_COST_USD = 0.01;
33352
34214
  COMPOSE_COST_USD = 0.06;
33353
34215
  SUPPORTED_BUILD_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
33354
34216
  }
@@ -33671,7 +34533,7 @@ __export(scene_build_exports, {
33671
34533
  executeSceneBuild: () => executeSceneBuild,
33672
34534
  resolveSceneBuildMode: () => resolveSceneBuildMode
33673
34535
  });
33674
- import { existsSync as existsSync19 } from "node:fs";
34536
+ import { existsSync as existsSync19, readFileSync as readFileSync3 } from "node:fs";
33675
34537
  import { copyFile as copyFile3, mkdir as mkdir10, readFile as readFile18, writeFile as writeFile18 } from "node:fs/promises";
33676
34538
  import { dirname as dirname10, join as join16, resolve as resolve25 } from "node:path";
33677
34539
  async function executeSceneBuild(opts) {
@@ -33838,8 +34700,32 @@ async function executeSceneBuild(opts) {
33838
34700
  const voice = opts.voice ?? parsed.frontmatter?.voice;
33839
34701
  let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
33840
34702
  let pendingJobs = [];
34703
+ let characterCostUsd = 0;
33841
34704
  if (shouldRunStage(selectedStage, "assets")) {
33842
34705
  onProgress({ type: "phase-start", phase: "primitives" });
34706
+ const characterPaths = /* @__PURE__ */ new Map();
34707
+ if (!(opts.skipVideo ?? false)) {
34708
+ const referenced = /* @__PURE__ */ new Set();
34709
+ for (const beat of activeBeats) {
34710
+ for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
34711
+ }
34712
+ if (referenced.size > 0) {
34713
+ const charResult = await buildCharacters(
34714
+ resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
34715
+ {
34716
+ projectDir,
34717
+ imageProvider,
34718
+ imageSize: opts.imageSize ?? "1536x1024",
34719
+ imageQuality: opts.imageQuality ?? "hd",
34720
+ force: opts.force ?? false,
34721
+ onProgress
34722
+ }
34723
+ );
34724
+ for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
34725
+ characterCostUsd = charResult.costUsd;
34726
+ warnings.push(...charResult.failures);
34727
+ }
34728
+ }
33843
34729
  const primitiveResults = await mapWithConcurrency(
33844
34730
  activeBeats,
33845
34731
  BEAT_PRIMITIVES_CONCURRENCY,
@@ -33857,7 +34743,8 @@ async function executeSceneBuild(opts) {
33857
34743
  skipVideo: opts.skipVideo ?? false,
33858
34744
  skipMusic: opts.skipMusic ?? false,
33859
34745
  force: opts.force ?? false,
33860
- onProgress
34746
+ onProgress,
34747
+ characterPaths
33861
34748
  })
33862
34749
  );
33863
34750
  beatOutcomes = primitiveResults.map((result) => result.outcome);
@@ -33866,10 +34753,7 @@ async function executeSceneBuild(opts) {
33866
34753
  (beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
33867
34754
  );
33868
34755
  stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
33869
- stageReports.assets.costUsd = estimateActualAssetCost(
33870
- beatOutcomes,
33871
- opts.imageQuality ?? "hd"
33872
- );
34756
+ stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
33873
34757
  stageReports.assets.retryWith = pendingJobs.map(
33874
34758
  (job) => `vibe status job ${job.id} --project ${projectDir} --json`
33875
34759
  );
@@ -34034,6 +34918,18 @@ async function executeSceneBuild(opts) {
34034
34918
  stageReports.compose.status = "skipped";
34035
34919
  }
34036
34920
  if (stageReports.compose.status === "done") {
34921
+ for (const beat of activeBeats) {
34922
+ const videoRel = `assets/video-${beat.id}.mp4`;
34923
+ const compositionAbs = join16(projectDir, "compositions", `scene-${beat.id}.html`);
34924
+ if (!existsSync19(join16(projectDir, videoRel)) || !existsSync19(compositionAbs)) continue;
34925
+ const html = readFileSync3(compositionAbs, "utf-8");
34926
+ if (!html.includes(videoRel)) {
34927
+ warnings.push(
34928
+ `Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
34929
+ );
34930
+ retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
34931
+ }
34932
+ }
34037
34933
  const repair = await runBuildSceneRepair(projectDir, "compose", false);
34038
34934
  sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
34039
34935
  applySceneRepairToStage(stageReports.compose, repair);
@@ -34454,6 +35350,120 @@ async function dispatchNarration(beat, ctx) {
34454
35350
  freshness: "fresh"
34455
35351
  };
34456
35352
  }
35353
+ function characterSheetPrompt(name, description) {
35354
+ return [
35355
+ `Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
35356
+ "Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
35357
+ "with identical outfit, hairstyle, hair color, and body proportions across all three views.",
35358
+ "Plain light-grey studio background, even neutral lighting, no props.",
35359
+ "Photorealistic digital character, professional concept-art character sheet layout."
35360
+ ].join(" ");
35361
+ }
35362
+ async function buildCharacters(characters, ctx) {
35363
+ const paths = /* @__PURE__ */ new Map();
35364
+ const failures = [];
35365
+ let costUsd = 0;
35366
+ const size = ctx.imageSize ?? "1536x1024";
35367
+ for (const character of characters) {
35368
+ if (character.imagePath) {
35369
+ if (existsSync19(join16(ctx.projectDir, character.imagePath))) {
35370
+ paths.set(character.name, character.imagePath);
35371
+ ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
35372
+ } else {
35373
+ const error = `character "${character.name}" image not found: ${character.imagePath}`;
35374
+ failures.push(error);
35375
+ ctx.onProgress({ type: "character-failed", name: character.name, error });
35376
+ }
35377
+ continue;
35378
+ }
35379
+ if (!character.prompt) continue;
35380
+ const prompt3 = characterSheetPrompt(character.name, character.prompt);
35381
+ const rel = `assets/character-${character.name}.png`;
35382
+ const abs = join16(ctx.projectDir, rel);
35383
+ const metadataOptions = { quality: ctx.imageQuality, size };
35384
+ const cache = characterCacheDescriptor({
35385
+ name: character.name,
35386
+ cue: prompt3,
35387
+ provider: ctx.imageProvider,
35388
+ quality: ctx.imageQuality,
35389
+ size
35390
+ });
35391
+ if (existsSync19(abs) && !ctx.force) {
35392
+ const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
35393
+ if (!metadata || isFreshCanonicalAsset({
35394
+ projectDir: ctx.projectDir,
35395
+ kind: "character",
35396
+ beatId: character.name,
35397
+ cue: prompt3,
35398
+ provider: ctx.imageProvider,
35399
+ options: metadataOptions,
35400
+ cacheKey: cache.key
35401
+ })) {
35402
+ paths.set(character.name, rel);
35403
+ ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
35404
+ continue;
35405
+ }
35406
+ }
35407
+ const cacheAbs = join16(ctx.projectDir, cache.path);
35408
+ if (existsSync19(cacheAbs) && !ctx.force) {
35409
+ await mkdir10(dirname10(abs), { recursive: true });
35410
+ await copyFile3(cacheAbs, abs);
35411
+ await writeAssetMetadata({
35412
+ projectDir: ctx.projectDir,
35413
+ kind: "character",
35414
+ beatId: character.name,
35415
+ cue: prompt3,
35416
+ provider: ctx.imageProvider,
35417
+ options: metadataOptions,
35418
+ cacheKey: cache.key,
35419
+ canonicalPath: rel,
35420
+ cachePath: cache.path
35421
+ });
35422
+ paths.set(character.name, rel);
35423
+ ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
35424
+ continue;
35425
+ }
35426
+ const generated = await generateBackdropImage(
35427
+ prompt3,
35428
+ {
35429
+ projectDir: ctx.projectDir,
35430
+ imageProvider: ctx.imageProvider,
35431
+ imageSize: size,
35432
+ imageQuality: ctx.imageQuality
35433
+ },
35434
+ imageRatioForSize(size)
35435
+ );
35436
+ if (!generated.success) {
35437
+ failures.push(`character "${character.name}": ${generated.error}`);
35438
+ ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
35439
+ continue;
35440
+ }
35441
+ await mkdir10(dirname10(abs), { recursive: true });
35442
+ await writeFile18(abs, generated.buffer);
35443
+ await mkdir10(dirname10(cacheAbs), { recursive: true });
35444
+ await writeFile18(cacheAbs, generated.buffer);
35445
+ await writeAssetMetadata({
35446
+ projectDir: ctx.projectDir,
35447
+ kind: "character",
35448
+ beatId: character.name,
35449
+ cue: prompt3,
35450
+ provider: ctx.imageProvider,
35451
+ options: metadataOptions,
35452
+ cacheKey: cache.key,
35453
+ canonicalPath: rel,
35454
+ cachePath: cache.path
35455
+ });
35456
+ costUsd += backdropCostUsd(ctx.imageQuality);
35457
+ paths.set(character.name, rel);
35458
+ ctx.onProgress({
35459
+ type: "character-generated",
35460
+ name: character.name,
35461
+ path: rel,
35462
+ provider: ctx.imageProvider
35463
+ });
35464
+ }
35465
+ return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
35466
+ }
34457
35467
  async function dispatchBackdrop(beat, ctx) {
34458
35468
  const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
34459
35469
  if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
@@ -34623,13 +35633,16 @@ async function dispatchVideo(beat, ctx) {
34623
35633
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
34624
35634
  const prompt3 = stringOrUndefined4(beat.cues?.video);
34625
35635
  if (!prompt3) return { status: "no-cue" };
35636
+ const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
35637
+ const characterRefsAbs = characterRefsRel.map((p) => join16(ctx.projectDir, p));
34626
35638
  const rel = `assets/video-${beat.id}.mp4`;
34627
35639
  const abs = join16(ctx.projectDir, rel);
34628
35640
  const cache = videoCacheDescriptor({
34629
35641
  beatId: beat.id,
34630
35642
  cue: prompt3,
34631
35643
  provider: ctx.videoProvider,
34632
- duration: normalizeVideoDuration(beat.duration)
35644
+ duration: normalizeVideoDuration(beat.duration),
35645
+ characters: characterRefsRel
34633
35646
  });
34634
35647
  const metadataPath = assetMetadataPath("video", beat.id);
34635
35648
  if (existsSync19(abs) && !ctx.force) {
@@ -34689,6 +35702,7 @@ async function dispatchVideo(beat, ctx) {
34689
35702
  ratio: "16:9",
34690
35703
  output: abs,
34691
35704
  wait: false,
35705
+ refImages: characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
34692
35706
  apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
34693
35707
  });
34694
35708
  if (!result.success || !result.taskId) {
@@ -35057,7 +36071,13 @@ function estimateActualAssetCost(outcomes, imageQuality) {
35057
36071
  for (const outcome of outcomes) {
35058
36072
  if (outcome.narrationStatus === "generated") cost += 0.05;
35059
36073
  if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
35060
- if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") cost += 5;
36074
+ if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
36075
+ cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
36076
+ durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
36077
+ resolution: "720p",
36078
+ aspectRatio: "16:9"
36079
+ }) : 5;
36080
+ }
35061
36081
  if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
35062
36082
  }
35063
36083
  return Number(cost.toFixed(2));
@@ -45178,10 +46198,18 @@ Examples:
45178
46198
  options.ratio = "16:9";
45179
46199
  }
45180
46200
  if (options.dryRun) {
46201
+ const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
46202
+ durationSec: Number(options.duration) || 5,
46203
+ resolution: options.resolution,
46204
+ aspectRatio: options.ratio,
46205
+ fast: options.seedanceModel === "fast",
46206
+ hasVideoReference: Boolean(options.refVideos)
46207
+ }) : void 0;
45181
46208
  outputSuccess({
45182
46209
  command: "generate video",
45183
46210
  startedAt,
45184
46211
  dryRun: true,
46212
+ costUsd,
45185
46213
  data: {
45186
46214
  params: {
45187
46215
  prompt: prompt3,
@@ -51379,7 +52407,7 @@ function pickOutputPath(data) {
51379
52407
  }
51380
52408
 
51381
52409
  // src/tools/_shared/elicit.ts
51382
- var NARRATION_CHOICES = ["kokoro", "elevenlabs"];
52410
+ var NARRATION_CHOICES = ["kokoro", "openai", "elevenlabs"];
51383
52411
  var BACKDROP_CHOICES = ["skip", "openai"];
51384
52412
  function planBuildElicitation(args) {
51385
52413
  const assetsWillRun = args.stage === void 0 || args.stage === "all" || args.stage === "assets";
@@ -51394,8 +52422,9 @@ function planBuildElicitation(args) {
51394
52422
  description: "Which TTS engine narrates the video?",
51395
52423
  enum: [...NARRATION_CHOICES],
51396
52424
  enumNames: [
51397
- "Kokoro \u2014 free, runs locally on this machine",
51398
- "ElevenLabs \u2014 cloud TTS, uses your ElevenLabs credits"
52425
+ "Kokoro \u2014 free, runs on this machine (first use downloads a ~90 MB voice model; slower)",
52426
+ "OpenAI \u2014 fast cloud voice, uses your OpenAI API key (~$0.02 per video)",
52427
+ "ElevenLabs \u2014 premium cloud voices, uses your ElevenLabs credits"
51399
52428
  ]
51400
52429
  };
51401
52430
  required.push("narration");
@@ -51658,6 +52687,9 @@ function clean(text) {
51658
52687
  function humanise(id) {
51659
52688
  return id.split(/[-_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
51660
52689
  }
52690
+ function sceneCompositionId(beatId) {
52691
+ return `scene-${beatId}`;
52692
+ }
51661
52693
  function slugifySceneName(name) {
51662
52694
  const normalised = name.normalize("NFKD").replace(/[̀-ͯ]/g, "");
51663
52695
  const slug = normalised.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -51695,10 +52727,16 @@ function fitKineticFontSize(words) {
51695
52727
  function kenBurnsTween(scope, dur, endScale = 1.08) {
51696
52728
  return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
51697
52729
  }
52730
+ function backdropMotionTweens(scope, dur, hasImage, endScale = 1.08) {
52731
+ const sweep = hasImage ? "" : `
52732
+ tl.fromTo('${scope} .ambient-sweep', { xPercent: -75, opacity: 0.18 }, { xPercent: 75, opacity: 0.62, duration: ${dur.toFixed(2)}, ease: 'sine.inOut' }, 0);`;
52733
+ return `${kenBurnsTween(scope, dur, endScale)}${sweep}`;
52734
+ }
51698
52735
  function buildPreset(input) {
51699
52736
  const id = input.id;
51700
52737
  const scope = `[data-composition-id="${id}"]`;
51701
- const headline = clean(input.headline) || humanise(id);
52738
+ const displayId = input.beatId ?? id;
52739
+ const headline = clean(input.headline) || humanise(displayId);
51702
52740
  const subhead = clean(input.subhead);
51703
52741
  const kicker = clean(input.kicker);
51704
52742
  const dur = input.duration;
@@ -51721,8 +52759,17 @@ function buildPreset(input) {
51721
52759
  position: absolute;
51722
52760
  inset: 0;
51723
52761
  background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 70%);
52762
+ overflow: hidden;
52763
+ }
52764
+ ${scope} .ambient-sweep {
52765
+ position: absolute;
52766
+ inset: -22%;
52767
+ display: block;
52768
+ background: linear-gradient(115deg, rgba(255,255,255,0) 20%, rgba(0,201,255,0.34) 48%, rgba(142,45,226,0.26) 58%, rgba(255,255,255,0) 78%);
52769
+ filter: blur(12px);
52770
+ transform: rotate(8deg);
51724
52771
  }`;
51725
- const backdropMarkup = `<div class="backdrop"></div>`;
52772
+ const backdropMarkup = hasImage ? `<div class="backdrop"></div>` : `<div class="backdrop"><div class="ambient-sweep"></div></div>`;
51726
52773
  switch (input.preset) {
51727
52774
  case "simple": {
51728
52775
  const transcript = input.transcript;
@@ -51754,7 +52801,7 @@ function buildPreset(input) {
51754
52801
  body: `${backdropMarkup}
51755
52802
  <div class="caption" id="caption">${captionInner}</div>`,
51756
52803
  timeline: `${crossfadeTweens(scope, dur)}
51757
- ${kenBurnsTween(scope, dur)}
52804
+ ${backdropMotionTweens(scope, dur, hasImage)}
51758
52805
  ${captionTween}`
51759
52806
  };
51760
52807
  }
@@ -51793,12 +52840,12 @@ function buildPreset(input) {
51793
52840
  // scene's own fade-in handles the visual transition. Backdrop
51794
52841
  // Ken-Burns fills what used to be 80% dead static frame.
51795
52842
  timeline: `${crossfadeTweens(scope, dur)}
51796
- ${kenBurnsTween(scope, dur)}
52843
+ ${backdropMotionTweens(scope, dur, hasImage)}
51797
52844
  tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
51798
52845
  };
51799
52846
  }
51800
52847
  case "explainer": {
51801
- const k = kicker || humanise(id).toUpperCase();
52848
+ const k = kicker || humanise(displayId).toUpperCase();
51802
52849
  const sub = subhead || "";
51803
52850
  const transcript = input.transcript;
51804
52851
  const useWordSync = !!(transcript && transcript.length > 0 && sub);
@@ -51840,7 +52887,7 @@ function buildPreset(input) {
51840
52887
  <div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
51841
52888
  </div>`,
51842
52889
  timeline: `${crossfadeTweens(scope, dur)}
51843
- ${kenBurnsTween(scope, dur)}
52890
+ ${backdropMotionTweens(scope, dur, hasImage)}
51844
52891
  tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
51845
52892
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
51846
52893
  ${subtitleTween}`
@@ -51881,12 +52928,12 @@ function buildPreset(input) {
51881
52928
  body: `${backdropMarkup}
51882
52929
  <div class="kinetic">${wordSpans}</div>`,
51883
52930
  timeline: `${crossfadeTweens(scope, dur)}
51884
- ${kenBurnsTween(scope, dur)}
52931
+ ${backdropMotionTweens(scope, dur, hasImage)}
51885
52932
  ${tweens}`
51886
52933
  };
51887
52934
  }
51888
52935
  case "product-shot": {
51889
- const label = kicker || humanise(id);
52936
+ const label = kicker || humanise(displayId);
51890
52937
  const headlineFontPx = fitFontSize(headline, 72, 40, 50);
51891
52938
  return {
51892
52939
  css: `${scope} {
@@ -51920,7 +52967,7 @@ function buildPreset(input) {
51920
52967
  <div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
51921
52968
  <div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
51922
52969
  timeline: `${crossfadeTweens(scope, dur)}
51923
- tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.12, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
52970
+ ${backdropMotionTweens(scope, dur, hasImage, 1.12)}
51924
52971
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
51925
52972
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
51926
52973
  tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
@@ -51985,9 +53032,10 @@ function emitSceneHtml(input) {
51985
53032
  if (input.width <= 0 || input.height <= 0) {
51986
53033
  throw new Error(`Invalid canvas dims: ${input.width}x${input.height}`);
51987
53034
  }
51988
- const id = input.id;
53035
+ const beatId = input.id;
53036
+ const id = sceneCompositionId(beatId);
51989
53037
  const dur = Number(input.duration.toFixed(3));
51990
- const parts = buildPreset({ ...input, id, duration: dur });
53038
+ const parts = buildPreset({ ...input, id, beatId, duration: dur });
51991
53039
  const audioBlock = input.audioPath ? `
51992
53040
  <audio
51993
53041
  id="narration"
@@ -52003,7 +53051,7 @@ function emitSceneHtml(input) {
52003
53051
  ${lottieLayer.markup}` : "";
52004
53052
  const lottieScript = lottieLayer ? `
52005
53053
  ${lottieLayer.script}` : "";
52006
- return `<template id="scene-${id}-template">
53054
+ return `<template id="${id}-template">
52007
53055
  <div data-composition-id="${id}" data-start="0" data-duration="${dur}" data-width="${input.width}" data-height="${input.height}">
52008
53056
  <style>
52009
53057
  ${parts.css}
@@ -52039,8 +53087,9 @@ function buildClipReference(opts) {
52039
53087
  const duration = Number(opts.duration.toFixed(3));
52040
53088
  const track = opts.trackIndex ?? 1;
52041
53089
  const src = opts.src ?? `compositions/scene-${opts.id}.html`;
53090
+ const compositionId = sceneCompositionId(opts.id);
52042
53091
  const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
52043
- return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
53092
+ return `<div class="clip" data-composition-id="${esc(compositionId)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
52044
53093
  }
52045
53094
  function insertClipIntoRoot(rootHtml, clip) {
52046
53095
  const clipDiv = buildClipReference(clip);
@@ -52084,9 +53133,9 @@ init_agent_host_detect();
52084
53133
  // src/commands/_shared/install-skill.ts
52085
53134
  init_bundle_content();
52086
53135
  init_bundle();
52087
- import { mkdir as mkdir5, writeFile as writeFile9 } from "node:fs/promises";
53136
+ import { mkdir as mkdir5, writeFile as writeFile10 } from "node:fs/promises";
52088
53137
  import { existsSync as existsSync7 } from "node:fs";
52089
- import { dirname as dirname4, join as join6, relative as relative3, resolve as resolve9 } from "node:path";
53138
+ import { dirname as dirname4, join as join7, relative as relative3, resolve as resolve10 } from "node:path";
52090
53139
  function universalFiles() {
52091
53140
  return [
52092
53141
  { relPath: "SKILL.md", content: SKILL_MD },
@@ -52136,7 +53185,7 @@ function selectHostFiles(hosts) {
52136
53185
  return out;
52137
53186
  }
52138
53187
  async function installHyperframesSkill(opts) {
52139
- const projectDir = resolve9(opts.projectDir);
53188
+ const projectDir = resolve10(opts.projectDir);
52140
53189
  const force = opts.force ?? false;
52141
53190
  const dryRun = opts.dryRun ?? false;
52142
53191
  const files = [...universalFiles(), ...selectHostFiles(opts.hosts)];
@@ -52145,7 +53194,7 @@ async function installHyperframesSkill(opts) {
52145
53194
  await mkdir5(projectDir, { recursive: true });
52146
53195
  }
52147
53196
  for (const file of files) {
52148
- const absPath = join6(projectDir, file.relPath);
53197
+ const absPath = join7(projectDir, file.relPath);
52149
53198
  const exists = existsSync7(absPath);
52150
53199
  const bytes = Buffer.byteLength(file.content, "utf-8");
52151
53200
  if (exists && !force) {
@@ -52161,7 +53210,7 @@ async function installHyperframesSkill(opts) {
52161
53210
  continue;
52162
53211
  }
52163
53212
  await mkdir5(dirname4(absPath), { recursive: true });
52164
- await writeFile9(absPath, file.content, "utf-8");
53213
+ await writeFile10(absPath, file.content, "utf-8");
52165
53214
  actions.push({ path: file.relPath, status: "wrote", bytes });
52166
53215
  }
52167
53216
  return {
@@ -52186,10 +53235,10 @@ init_scene_lint();
52186
53235
  init_storyboard_parse();
52187
53236
  init_root_sync();
52188
53237
  import { existsSync as existsSync11 } from "node:fs";
52189
- import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile12 } from "node:fs/promises";
52190
- import { join as join10, resolve as resolve13 } from "node:path";
53238
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile13 } from "node:fs/promises";
53239
+ import { join as join11, resolve as resolve14 } from "node:path";
52191
53240
  async function executeSceneSubmit(opts) {
52192
- const projectDir = resolve13(opts.projectDir);
53241
+ const projectDir = resolve14(opts.projectDir);
52193
53242
  const beatId = opts.beatId.trim();
52194
53243
  const scenePath = `compositions/scene-${beatId}.html`;
52195
53244
  const base = {
@@ -52201,7 +53250,7 @@ async function executeSceneSubmit(opts) {
52201
53250
  warnings: [],
52202
53251
  retryWith: []
52203
53252
  };
52204
- const storyboardPath = join10(projectDir, "STORYBOARD.md");
53253
+ const storyboardPath = join11(projectDir, "STORYBOARD.md");
52205
53254
  if (!existsSync11(storyboardPath)) {
52206
53255
  return {
52207
53256
  ...base,
@@ -52279,9 +53328,9 @@ async function executeSceneSubmit(opts) {
52279
53328
  if (opts.validateOnly) {
52280
53329
  return { ...base, success: true };
52281
53330
  }
52282
- const absPath = join10(projectDir, scenePath);
52283
- await mkdir7(join10(projectDir, "compositions"), { recursive: true });
52284
- await writeFile12(absPath, html.endsWith("\n") ? html : `${html}
53331
+ const absPath = join11(projectDir, scenePath);
53332
+ await mkdir7(join11(projectDir, "compositions"), { recursive: true });
53333
+ await writeFile13(absPath, html.endsWith("\n") ? html : `${html}
52285
53334
  `, "utf-8");
52286
53335
  return {
52287
53336
  ...base,
@@ -52296,6 +53345,8 @@ async function executeSceneSubmit(opts) {
52296
53345
 
52297
53346
  // src/commands/scene.ts
52298
53347
  init_scene_repair();
53348
+ init_root_sync();
53349
+ init_storyboard_edit();
52299
53350
  function validateDuration(value) {
52300
53351
  const n = parseFloat(value);
52301
53352
  if (!Number.isFinite(n) || n <= 0 || n > 3600) {
@@ -52598,11 +53649,14 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
52598
53649
  ).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
52599
53650
  "--visuals <prompt>",
52600
53651
  "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider"
52601
- ).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(
53652
+ ).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(
53653
+ "--no-storyboard",
53654
+ "Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
53655
+ ).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
52602
53656
  "--tts <provider>",
52603
- "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)",
53657
+ "TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
52604
53658
  "auto"
52605
- ).option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option(
53659
+ ).option("--voice <id>", "Voice id (ElevenLabs name/id, OpenAI voice like marin, or Kokoro id like af_heart)").option(
52606
53660
  "--no-audio",
52607
53661
  "Skip TTS even when --narration is provided (useful for tests/agent dry runs)"
52608
53662
  ).option("--no-image", "Skip image generation even when --visuals is provided").option(
@@ -52667,6 +53721,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
52667
53721
  kicker: options.kicker,
52668
53722
  project: options.project,
52669
53723
  insertInto: options.insertInto,
53724
+ syncStoryboard: options.storyboard !== false,
52670
53725
  imageProvider: options.imageProvider,
52671
53726
  tts,
52672
53727
  audio: options.audio,
@@ -52694,6 +53749,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
52694
53749
  kicker: options.kicker,
52695
53750
  projectDir: options.project,
52696
53751
  insertInto: options.insertInto,
53752
+ syncStoryboard: options.storyboard !== false,
52697
53753
  imageProvider: options.imageProvider,
52698
53754
  tts,
52699
53755
  voice: options.voice,
@@ -52733,6 +53789,13 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
52733
53789
  if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
52734
53790
  if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
52735
53791
  if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
53792
+ if (result.storyboardPath && result.storyboardSynced) {
53793
+ console.log(
53794
+ result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
53795
+ result.storyboardPath,
53796
+ source_default.dim(`(${result.storyboardAction})`)
53797
+ );
53798
+ }
52736
53799
  console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
52737
53800
  console.log();
52738
53801
  console.log(source_default.bold.cyan("Composition"));
@@ -52779,6 +53842,21 @@ async function loadVibeProjectConfig(projectDir) {
52779
53842
  }
52780
53843
  };
52781
53844
  }
53845
+ function escapeRegExp3(value) {
53846
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
53847
+ }
53848
+ function readClipStart(rootHtml, beatId) {
53849
+ const compositionId = sceneCompositionId(beatId);
53850
+ const match = rootHtml.match(
53851
+ new RegExp(
53852
+ `<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
53853
+ "i"
53854
+ )
53855
+ );
53856
+ if (!match) return void 0;
53857
+ const start = Number.parseFloat(match[1]);
53858
+ return Number.isFinite(start) ? start : void 0;
53859
+ }
52782
53860
  async function resolveNarrationText(value) {
52783
53861
  if (!value) return void 0;
52784
53862
  const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
@@ -52805,6 +53883,7 @@ async function executeSceneAdd(opts) {
52805
53883
  const id = slugifySceneName(opts.name);
52806
53884
  const sceneRel = `compositions/scene-${id}.html`;
52807
53885
  const scenePath = resolve17(projectDir, sceneRel);
53886
+ const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
52808
53887
  const errResult = (error) => ({
52809
53888
  success: false,
52810
53889
  id,
@@ -52863,7 +53942,7 @@ async function executeSceneAdd(opts) {
52863
53942
  throw error;
52864
53943
  }
52865
53944
  opts.onProgress?.(
52866
- resolution.provider === "kokoro" ? "Generating narration with Kokoro (local \u2014 first run downloads ~330MB)..." : "Generating narration with ElevenLabs..."
53945
+ 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..."
52867
53946
  );
52868
53947
  const tts = await resolution.call(narrationText, {
52869
53948
  voice: opts.voice,
@@ -53024,6 +54103,27 @@ async function executeSceneAdd(opts) {
53024
54103
  duration = fallback2;
53025
54104
  }
53026
54105
  duration = Number(duration.toFixed(2));
54106
+ let storyboardPath;
54107
+ let storyboardSynced = false;
54108
+ let storyboardAction = "skipped";
54109
+ const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
54110
+ if (shouldSyncStoryboard) {
54111
+ opts.onProgress?.("Syncing STORYBOARD.md...");
54112
+ const before = await readFile14(storyboardAbsPath, "utf-8");
54113
+ const synced = upsertStoryboardBeat(before, {
54114
+ beatId: id,
54115
+ title: opts.headline,
54116
+ duration,
54117
+ narration: narrationText && !opts.skipAudio ? narrationText : void 0,
54118
+ backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
54119
+ });
54120
+ if (synced.markdown !== before) {
54121
+ await writeFile15(storyboardAbsPath, synced.markdown, "utf-8");
54122
+ }
54123
+ storyboardPath = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
54124
+ storyboardSynced = true;
54125
+ storyboardAction = synced.action;
54126
+ }
53027
54127
  opts.onProgress?.("Emitting scene HTML...");
53028
54128
  const sceneHtml = emitSceneHtml({
53029
54129
  id,
@@ -53042,11 +54142,24 @@ async function executeSceneAdd(opts) {
53042
54142
  await mkdir8(dirname6(scenePath), { recursive: true });
53043
54143
  await writeFile15(scenePath, sceneHtml, "utf-8");
53044
54144
  opts.onProgress?.("Updating root composition...");
53045
- const start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
53046
- const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
53047
- const trackIndex = existingClipCount % 2 + 1;
53048
- const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
53049
- await writeFile15(rootPath, updated, "utf-8");
54145
+ let start = 0;
54146
+ if (storyboardSynced) {
54147
+ const syncPlan = await syncRootComposition({
54148
+ projectDir,
54149
+ rootRel,
54150
+ beats: await loadProjectRootSyncBeats(projectDir)
54151
+ });
54152
+ const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
54153
+ if (blockingIssue) return errResult(blockingIssue.message);
54154
+ const rootHtmlAfterSync = await readFile14(rootPath, "utf-8");
54155
+ start = readClipStart(rootHtmlAfterSync, id) ?? 0;
54156
+ } else {
54157
+ start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
54158
+ const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
54159
+ const trackIndex = existingClipCount % 2 + 1;
54160
+ const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
54161
+ await writeFile15(rootPath, updated, "utf-8");
54162
+ }
53050
54163
  const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
53051
54164
  return {
53052
54165
  success: true,
@@ -53059,6 +54172,9 @@ async function executeSceneAdd(opts) {
53059
54172
  audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
53060
54173
  imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
53061
54174
  lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
54175
+ storyboardPath,
54176
+ storyboardSynced,
54177
+ storyboardAction,
53062
54178
  transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
53063
54179
  transcriptWordCount
53064
54180
  };
@@ -53356,6 +54472,9 @@ var sceneAddSchema = external_exports.object({
53356
54472
  kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
53357
54473
  projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
53358
54474
  insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
54475
+ syncStoryboard: external_exports.boolean().optional().describe(
54476
+ "Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
54477
+ ),
53359
54478
  imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
53360
54479
  voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
53361
54480
  skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
@@ -53368,7 +54487,7 @@ var sceneAddTool = defineTool({
53368
54487
  cost: "low",
53369
54488
  title: "Add Scene from Preset",
53370
54489
  annotations: { readOnly: false, openWorld: true },
53371
- description: "Add a single scene to an existing scene project. Optionally generates narration audio (ElevenLabs) and/or a backdrop image (Gemini/OpenAI), then emits compositions/scene-<id>.html with a paused GSAP timeline and splices a clip reference into the root index.html. Use skipAudio:true and skipImage:true for text-only scenes that need no API calls.",
54490
+ description: "Add a single scene to an existing scene project. Optionally generates narration audio (ElevenLabs) and/or a backdrop image (Gemini/OpenAI), then emits compositions/scene-<id>.html with a paused GSAP timeline. By default, syncs STORYBOARD.md and rebuilds root refs from it; set syncStoryboard:false for root-only insertion. Use skipAudio:true and skipImage:true for text-only scenes that need no API calls.",
53372
54491
  schema: sceneAddSchema,
53373
54492
  async execute(args, ctx) {
53374
54493
  const projectDir = args.projectDir ? resolve26(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
@@ -53382,6 +54501,7 @@ var sceneAddTool = defineTool({
53382
54501
  kicker: args.kicker,
53383
54502
  projectDir,
53384
54503
  insertInto: args.insertInto,
54504
+ syncStoryboard: args.syncStoryboard,
53385
54505
  imageProvider: args.imageProvider,
53386
54506
  voice: args.voice,
53387
54507
  skipAudio: args.skipAudio,
@@ -53400,6 +54520,9 @@ var sceneAddTool = defineTool({
53400
54520
  ];
53401
54521
  if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
53402
54522
  if (result.imagePath) lines.push(` image: ${result.imagePath}`);
54523
+ if (result.storyboardPath && result.storyboardSynced) {
54524
+ lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
54525
+ }
53403
54526
  return {
53404
54527
  success: true,
53405
54528
  data: {
@@ -53410,7 +54533,10 @@ var sceneAddTool = defineTool({
53410
54533
  scenePath: result.scenePath,
53411
54534
  rootPath: result.rootPath,
53412
54535
  audioPath: result.audioPath,
53413
- imagePath: result.imagePath
54536
+ imagePath: result.imagePath,
54537
+ storyboardPath: result.storyboardPath,
54538
+ storyboardSynced: result.storyboardSynced,
54539
+ storyboardAction: result.storyboardAction
53414
54540
  },
53415
54541
  humanLines: lines
53416
54542
  };
@@ -53623,7 +54749,9 @@ var sceneBuildSchema = external_exports.object({
53623
54749
  skipVideo: external_exports.boolean().optional().describe("Skip video generation for every beat (use existing MP4 assets if present)."),
53624
54750
  skipMusic: external_exports.boolean().optional().describe("Skip music generation for every beat (use existing audio assets if present)."),
53625
54751
  skipRender: external_exports.boolean().optional().describe("Stop after compose \u2014 produces compositions/*.html but no final MP4."),
53626
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override. Default 'auto'."),
54752
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe(
54753
+ "TTS provider override. Default 'auto' (elevenlabs key > openai key > local kokoro)."
54754
+ ),
53627
54755
  voice: external_exports.string().optional().describe("TTS voice id (provider-specific)."),
53628
54756
  imageProvider: external_exports.enum(["openai"]).optional().describe("Image provider for backdrops. Default 'openai' (gpt-image-2)."),
53629
54757
  videoProvider: external_exports.enum(["seedance", "grok", "kling", "runway", "veo"]).optional().describe("Video provider for per-beat video cues. Default seedance."),
@@ -55069,7 +56197,11 @@ async function inspectProject(opts) {
55069
56197
  );
55070
56198
  }
55071
56199
  function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
55072
- const normalizedIssues = normalizeReviewIssues(issues);
56200
+ const normalizedRetryWith = uniqueRetryWith(retryWith);
56201
+ const normalizedIssues = normalizeReviewIssues(issues, {
56202
+ projectDir,
56203
+ retryWith: normalizedRetryWith
56204
+ });
55073
56205
  const status = statusFromIssues(normalizedIssues);
55074
56206
  return {
55075
56207
  schemaVersion: "1",
@@ -55081,9 +56213,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
55081
56213
  score: scoreIssues(normalizedIssues),
55082
56214
  issues: normalizedIssues,
55083
56215
  summary: summarizeReviewIssues(normalizedIssues),
56216
+ nextActions: deriveNextReviewActions({
56217
+ issues: normalizedIssues,
56218
+ retryWith: normalizedRetryWith,
56219
+ projectDir
56220
+ }),
55084
56221
  sourceReports: projectSourceReports(checks),
55085
56222
  checks,
55086
- retryWith: uniqueRetryWith(retryWith)
56223
+ retryWith: normalizedRetryWith
55087
56224
  };
55088
56225
  }
55089
56226
  function storyboardPlaceholderIssues(storyboard) {
@@ -55635,6 +56772,8 @@ async function inspectRender(opts) {
55635
56772
  severity: "error",
55636
56773
  code: "RENDER_NOT_FOUND",
55637
56774
  message: opts.beatId ? `No rendered video was found for beat "${opts.beatId}". Pass --video or render the beat first.` : "No rendered video was found. Pass --video or render the project first.",
56775
+ beatId: opts.beatId,
56776
+ scene: opts.beatId,
55638
56777
  suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
55639
56778
  });
55640
56779
  retryWith.push(
@@ -55657,6 +56796,8 @@ async function inspectRender(opts) {
55657
56796
  code: "EMPTY_RENDER",
55658
56797
  message: "Rendered video file is empty.",
55659
56798
  file: displayPath(videoPath),
56799
+ beatId: opts.beatId,
56800
+ scene: opts.beatId,
55660
56801
  suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
55661
56802
  });
55662
56803
  retryWith.push(
@@ -55666,6 +56807,8 @@ async function inspectRender(opts) {
55666
56807
  const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
55667
56808
  const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
55668
56809
  if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
56810
+ const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
56811
+ checks.expectedAudio = expectedAudio;
55669
56812
  if (!commandExists("ffprobe")) {
55670
56813
  issues.push({
55671
56814
  severity: "error",
@@ -55691,7 +56834,7 @@ async function inspectRender(opts) {
55691
56834
  file: displayPath(videoPath)
55692
56835
  });
55693
56836
  }
55694
- if (!audioStream) {
56837
+ if (!audioStream && expectedAudio) {
55695
56838
  issues.push({
55696
56839
  severity: "warning",
55697
56840
  code: "NO_AUDIO_STREAM",
@@ -55896,8 +57039,86 @@ async function inspectRender(opts) {
55896
57039
  );
55897
57040
  return maybeWriteRenderReport(projectDir, opts, result);
55898
57041
  }
57042
+ async function renderExpectsAudio(projectDir, beatId) {
57043
+ const root = resolve42(projectDir);
57044
+ const rootHtml = await readTextIfExists(join22(root, "index.html"));
57045
+ if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
57046
+ const buildReport = await readJsonIfExists(join22(root, "build-report.json"));
57047
+ if (buildReportExpectsAudio(buildReport, beatId)) return true;
57048
+ const storyboard = await readTextIfExists(join22(root, "STORYBOARD.md"));
57049
+ if (!storyboard) return false;
57050
+ try {
57051
+ const parsed = parseStoryboard(storyboard);
57052
+ const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
57053
+ return beats.some(
57054
+ (beat) => [
57055
+ `assets/narration-${beat.id}.mp3`,
57056
+ `assets/narration-${beat.id}.wav`,
57057
+ `assets/music-${beat.id}.mp3`,
57058
+ `assets/music-${beat.id}.wav`
57059
+ ].some((asset) => existsSync39(join22(root, asset)))
57060
+ );
57061
+ } catch {
57062
+ return false;
57063
+ }
57064
+ }
57065
+ function rootHtmlExpectsAudio(html, beatId) {
57066
+ const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
57067
+ if (!beatId) return audioTags.length > 0;
57068
+ const escaped = escapeRegExp4(beatId);
57069
+ const beatAudioRe = new RegExp(
57070
+ `(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
57071
+ "i"
57072
+ );
57073
+ return audioTags.some((tag) => beatAudioRe.test(tag));
57074
+ }
57075
+ function buildReportExpectsAudio(report, beatId) {
57076
+ if (!report || typeof report !== "object") return false;
57077
+ const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
57078
+ const selected = beatId === void 0 ? beats : beats.filter((beat) => {
57079
+ if (!beat || typeof beat !== "object") return false;
57080
+ return beat.id === beatId;
57081
+ });
57082
+ return selected.some((beat) => beatReportExpectsAudio(beat));
57083
+ }
57084
+ function beatReportExpectsAudio(beat) {
57085
+ if (!beat || typeof beat !== "object") return false;
57086
+ const record = beat;
57087
+ if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
57088
+ if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
57089
+ if (parseOptionalNumber2(record.narrationDurationSec) !== void 0) return true;
57090
+ const narration = record.narration;
57091
+ if (narration && typeof narration === "object") {
57092
+ const n = narration;
57093
+ if (typeof n.path === "string" && n.path.length > 0) return true;
57094
+ if (parseOptionalNumber2(n.durationSec) !== void 0) return true;
57095
+ }
57096
+ const music = record.music;
57097
+ if (music && typeof music === "object") {
57098
+ const m = music;
57099
+ if (typeof m.path === "string" && m.path.length > 0) return true;
57100
+ if (parseOptionalNumber2(m.durationSec) !== void 0) return true;
57101
+ }
57102
+ return false;
57103
+ }
57104
+ async function readJsonIfExists(path2) {
57105
+ const text = await readTextIfExists(path2);
57106
+ if (!text) return null;
57107
+ try {
57108
+ return JSON.parse(text);
57109
+ } catch {
57110
+ return null;
57111
+ }
57112
+ }
57113
+ function escapeRegExp4(value) {
57114
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57115
+ }
55899
57116
  function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
55900
- const normalizedIssues = normalizeReviewIssues(issues);
57117
+ const normalizedRetryWith = uniqueRetryWith(retryWith);
57118
+ const normalizedIssues = normalizeReviewIssues(issues, {
57119
+ projectDir,
57120
+ retryWith: normalizedRetryWith
57121
+ });
55901
57122
  const status = statusFromIssues(normalizedIssues);
55902
57123
  return {
55903
57124
  schemaVersion: "1",
@@ -55910,9 +57131,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
55910
57131
  score,
55911
57132
  issues: normalizedIssues,
55912
57133
  summary: summarizeReviewIssues(normalizedIssues),
57134
+ nextActions: deriveNextReviewActions({
57135
+ issues: normalizedIssues,
57136
+ retryWith: normalizedRetryWith,
57137
+ projectDir
57138
+ }),
55913
57139
  sourceReports: renderSourceReports(projectDir, videoPath, checks),
55914
57140
  checks,
55915
- retryWith: uniqueRetryWith(retryWith)
57141
+ retryWith: normalizedRetryWith
55916
57142
  };
55917
57143
  }
55918
57144
  async function maybeWriteRenderReport(projectDir, opts, result) {
@@ -57461,7 +58687,7 @@ var planTool = defineTool({
57461
58687
  skipBackdrop: external_exports.boolean().optional().describe("Don't include backdrop image generation in the plan."),
57462
58688
  skipVideo: external_exports.boolean().optional().describe("Don't include video generation in the plan."),
57463
58689
  skipMusic: external_exports.boolean().optional().describe("Don't include music generation in the plan."),
57464
- ttsProvider: external_exports.enum(["auto", "elevenlabs", "kokoro"]).optional().describe("TTS provider override."),
58690
+ ttsProvider: external_exports.enum(["auto", "elevenlabs", "openai", "kokoro"]).optional().describe("TTS provider override."),
57465
58691
  voice: external_exports.string().optional().describe("Voice id."),
57466
58692
  imageProvider: external_exports.string().optional().describe("Image provider override."),
57467
58693
  imageQuality: external_exports.enum(["standard", "hd"]).optional().describe("Image quality override."),