@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.
- package/CONTEXT.md +59 -10
- package/dist/commands/_shared/build-cache.d.ts +10 -1
- package/dist/commands/_shared/build-cache.test.d.ts +1 -0
- package/dist/commands/_shared/init-templates.d.ts +1 -1
- package/dist/commands/_shared/project-config.d.ts +1 -1
- package/dist/commands/_shared/render-inspect.d.ts +4 -1
- package/dist/commands/_shared/review-report.d.ts +50 -1
- package/dist/commands/_shared/review-report.test.d.ts +1 -0
- package/dist/commands/_shared/scene-build.d.ts +14 -1
- package/dist/commands/_shared/scene-html-emit.d.ts +4 -2
- package/dist/commands/_shared/scene-inspect.d.ts +2 -1
- package/dist/commands/_shared/scene-project.d.ts +1 -1
- package/dist/commands/_shared/status-jobs.d.ts +3 -0
- package/dist/commands/_shared/storyboard-edit.d.ts +15 -1
- package/dist/commands/_shared/storyboard-parse.d.ts +34 -0
- package/dist/commands/_shared/tts-resolve.d.ts +11 -7
- package/dist/commands/error-envelopes.test.d.ts +26 -0
- package/dist/commands/review-actions-cli.test.d.ts +1 -0
- package/dist/commands/scene.d.ts +12 -0
- package/dist/index.js +1667 -320
- package/dist/tools/_shared/elicit.d.ts +1 -1
- package/dist/tools/manifest/edit.d.ts +6 -6
- package/dist/tools/manifest/index.js +1742 -516
- package/dist/tools/manifest/scene.d.ts +6 -3
- package/dist/tools/manifest/storyboard.d.ts +3 -3
- package/package.json +4 -4
|
@@ -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
|
|
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
|
|
4733
|
-
if (
|
|
4734
|
-
return
|
|
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:
|
|
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
|
|
4766
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
24176
|
-
import { dirname as dirname3, join as
|
|
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 =
|
|
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(
|
|
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
|
|
24895
|
+
if (parent === dir) return resolve6(start);
|
|
24192
24896
|
dir = parent;
|
|
24193
24897
|
}
|
|
24194
24898
|
}
|
|
24195
24899
|
function jobsDir(projectDir) {
|
|
24196
|
-
return
|
|
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 =
|
|
24914
|
+
const projectDir = resolve6(
|
|
24211
24915
|
opts.projectDir ?? findProjectRoot(opts.workingDirectory ?? process.cwd())
|
|
24212
24916
|
);
|
|
24213
|
-
const workingDirectory =
|
|
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
|
|
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
|
|
24962
|
+
await writeFile7(
|
|
24259
24963
|
jobRecordPath(record.id, record.projectDir),
|
|
24260
|
-
JSON.stringify(
|
|
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 =
|
|
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(
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25265
|
+
return stripUndefined2(next);
|
|
24557
25266
|
}
|
|
24558
25267
|
function resolveRefreshOutput(record, opts) {
|
|
24559
|
-
if (opts.output) return
|
|
24560
|
-
return record.outputPath ?
|
|
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
|
|
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 =
|
|
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] =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
25065
|
-
const canonicalPath =
|
|
25066
|
-
const legacyPath =
|
|
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
|
|
25130
|
-
import { resolve as
|
|
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 =
|
|
26316
|
+
const dir = resolve8(opts.dir);
|
|
25532
26317
|
const profile = opts.profile ?? "full";
|
|
25533
26318
|
const groups = {
|
|
25534
26319
|
authoring: [
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
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
|
-
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
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
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
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 =
|
|
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(
|
|
25582
|
-
await mkdir4(
|
|
25583
|
-
await mkdir4(
|
|
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 =
|
|
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
|
|
26380
|
+
await writeFile8(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
|
|
25596
26381
|
merged.push(hfPath);
|
|
25597
26382
|
} else {
|
|
25598
|
-
await
|
|
26383
|
+
await writeFile8(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
|
|
25599
26384
|
created.push(hfPath);
|
|
25600
26385
|
}
|
|
25601
|
-
const metaPath =
|
|
26386
|
+
const metaPath = resolve8(dir, "meta.json");
|
|
25602
26387
|
if (await pathExists(metaPath)) {
|
|
25603
26388
|
skipped2.push(metaPath);
|
|
25604
26389
|
} else {
|
|
25605
|
-
await
|
|
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 =
|
|
26397
|
+
const rootPath = resolve8(dir, "index.html");
|
|
25613
26398
|
if (await pathExists(rootPath)) {
|
|
25614
26399
|
skipped2.push(rootPath);
|
|
25615
26400
|
} else {
|
|
25616
|
-
await
|
|
26401
|
+
await writeFile8(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
|
|
25617
26402
|
created.push(rootPath);
|
|
25618
26403
|
}
|
|
25619
26404
|
}
|
|
25620
|
-
const vibeConfigJsonPath =
|
|
26405
|
+
const vibeConfigJsonPath = resolve8(dir, VIBE_CONFIG_FILENAME);
|
|
25621
26406
|
if (await pathExists(vibeConfigJsonPath)) {
|
|
25622
26407
|
skipped2.push(vibeConfigJsonPath);
|
|
25623
26408
|
} else {
|
|
25624
|
-
await
|
|
26409
|
+
await writeFile8(vibeConfigJsonPath, projectConfigJson({ name, aspect }), "utf-8");
|
|
25625
26410
|
created.push(vibeConfigJsonPath);
|
|
25626
26411
|
}
|
|
25627
|
-
const vibePath =
|
|
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
|
|
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 =
|
|
26421
|
+
const agentsPath = resolve8(dir, "AGENTS.md");
|
|
25637
26422
|
if (await pathExists(agentsPath)) {
|
|
25638
26423
|
skipped2.push(agentsPath);
|
|
25639
26424
|
} else {
|
|
25640
|
-
await
|
|
26425
|
+
await writeFile8(agentsPath, buildProjectAgentsMd(name), "utf-8");
|
|
25641
26426
|
created.push(agentsPath);
|
|
25642
26427
|
}
|
|
25643
|
-
const claudePath =
|
|
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
|
|
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
|
|
26440
|
+
await writeFile8(claudePath, buildProjectClaudeMd(name), "utf-8");
|
|
25656
26441
|
created.push(claudePath);
|
|
25657
26442
|
}
|
|
25658
26443
|
}
|
|
25659
|
-
const designPath =
|
|
26444
|
+
const designPath = resolve8(dir, "DESIGN.md");
|
|
25660
26445
|
if (await pathExists(designPath)) {
|
|
25661
26446
|
skipped2.push(designPath);
|
|
25662
26447
|
} else {
|
|
25663
|
-
await
|
|
26448
|
+
await writeFile8(designPath, buildDesignMd({ name, style: opts.visualStyle }), "utf-8");
|
|
25664
26449
|
created.push(designPath);
|
|
25665
26450
|
}
|
|
25666
|
-
const storyboardPath =
|
|
26451
|
+
const storyboardPath = resolve8(dir, "STORYBOARD.md");
|
|
25667
26452
|
if (await pathExists(storyboardPath)) {
|
|
25668
26453
|
skipped2.push(storyboardPath);
|
|
25669
26454
|
} else {
|
|
25670
|
-
await
|
|
26455
|
+
await writeFile8(storyboardPath, buildStoryboardMd(name, duration), "utf-8");
|
|
25671
26456
|
created.push(storyboardPath);
|
|
25672
26457
|
}
|
|
25673
|
-
const gitignorePath =
|
|
26458
|
+
const gitignorePath = resolve8(dir, ".gitignore");
|
|
25674
26459
|
if (await pathExists(gitignorePath)) {
|
|
25675
26460
|
skipped2.push(gitignorePath);
|
|
25676
26461
|
} else {
|
|
25677
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
28817
|
-
const rootAbs =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
29075
|
-
if (process.platform === "win32" && existsSync5(
|
|
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
|
|
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 =
|
|
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":
|
|
29937
|
-
"hyperframes/house-style.md":
|
|
29938
|
-
"hyperframes/motion-principles.md":
|
|
29939
|
-
"hyperframes/typography.md":
|
|
29940
|
-
"hyperframes/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
|
|
29995
|
-
import { join as
|
|
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 =
|
|
30807
|
+
const projectDir = resolve11(opts.projectDir);
|
|
29998
30808
|
const rootRel = opts.rootRel ?? "index.html";
|
|
29999
|
-
const rootPath =
|
|
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
|
|
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 =
|
|
30062
|
-
const storyboardPath =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
30391
|
-
import { dirname as dirname5, join as
|
|
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/
|
|
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
|
|
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 =
|
|
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
|
|
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 ?
|
|
30752
|
-
const designPath =
|
|
30753
|
-
const storyboardPath =
|
|
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 =
|
|
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 =
|
|
31627
|
+
const compositionPath = join9(compositionsDir, `scene-${beat.id}.html`);
|
|
30810
31628
|
await mkdir6(dirname5(compositionPath), { recursive: true });
|
|
30811
|
-
await
|
|
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
|
|
31828
|
+
import { join as join10, relative as relative4, resolve as resolve13 } from "node:path";
|
|
31011
31829
|
async function getComposePrompts(opts) {
|
|
31012
|
-
const projectDir =
|
|
31013
|
-
const designPath =
|
|
31014
|
-
const storyboardPath =
|
|
31015
|
-
const skillPath =
|
|
31016
|
-
const compositionsDir =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 === "
|
|
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:
|
|
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
|
|
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
|
|
33235
|
-
configured:
|
|
33236
|
-
...
|
|
33237
|
-
...
|
|
33238
|
-
retryWith:
|
|
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")
|
|
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
|
|
51398
|
-
"
|
|
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
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(
|
|
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
|
-
${
|
|
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
|
-
${
|
|
52931
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
51885
52932
|
${tweens}`
|
|
51886
52933
|
};
|
|
51887
52934
|
}
|
|
51888
52935
|
case "product-shot": {
|
|
51889
|
-
const label = kicker || humanise(
|
|
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
|
-
|
|
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
|
|
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="
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
52190
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
52283
|
-
await mkdir7(
|
|
52284
|
-
await
|
|
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(
|
|
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
|
|
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
|
|
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 ~
|
|
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
|
-
|
|
53046
|
-
|
|
53047
|
-
|
|
53048
|
-
|
|
53049
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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."),
|