@vibeframe/cli 0.113.0 → 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/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 +13 -0
- 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/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/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 +1428 -272
- package/dist/tools/manifest/edit.d.ts +6 -6
- package/dist/tools/manifest/index.js +1506 -471
- package/dist/tools/manifest/scene.d.ts +3 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -17334,6 +17334,19 @@ var require_src = __commonJS({
|
|
|
17334
17334
|
});
|
|
17335
17335
|
|
|
17336
17336
|
// ../ai-providers/dist/fal/FalProvider.js
|
|
17337
|
+
function estimateSeedanceVideoCostUsd(opts) {
|
|
17338
|
+
const shortSide = SEEDANCE_SHORT_SIDE[opts.resolution ?? "720p"] ?? 720;
|
|
17339
|
+
const [a, b] = (opts.aspectRatio ?? "16:9").split(":").map(Number);
|
|
17340
|
+
const ratioFactor = a > 0 && b > 0 ? Math.max(a, b) / Math.min(a, b) : 16 / 9;
|
|
17341
|
+
const longSide = Math.round(shortSide * ratioFactor);
|
|
17342
|
+
const tokensPerSecond = shortSide * longSide * 24 / 1024;
|
|
17343
|
+
let usdPerSecond = tokensPerSecond / 1e3 * SEEDANCE_USD_PER_1K_TOKENS;
|
|
17344
|
+
if (opts.fast && shortSide <= 720)
|
|
17345
|
+
usdPerSecond *= SEEDANCE_FAST_FACTOR;
|
|
17346
|
+
if (opts.hasVideoReference)
|
|
17347
|
+
usdPerSecond *= SEEDANCE_VIDEO_REF_FACTOR;
|
|
17348
|
+
return Number((usdPerSecond * Math.max(0, opts.durationSec)).toFixed(2));
|
|
17349
|
+
}
|
|
17337
17350
|
function formatFalError(error) {
|
|
17338
17351
|
if (!(error instanceof Error))
|
|
17339
17352
|
return String(error);
|
|
@@ -17443,7 +17456,7 @@ function normaliseDuration(value) {
|
|
|
17443
17456
|
return "auto";
|
|
17444
17457
|
return Math.max(4, Math.min(15, Math.round(value)));
|
|
17445
17458
|
}
|
|
17446
|
-
var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, FalProvider, falProvider;
|
|
17459
|
+
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;
|
|
17447
17460
|
var init_FalProvider = __esm({
|
|
17448
17461
|
"../ai-providers/dist/fal/FalProvider.js"() {
|
|
17449
17462
|
"use strict";
|
|
@@ -17463,6 +17476,10 @@ var init_FalProvider = __esm({
|
|
|
17463
17476
|
DEFAULT_VARIANT = "seedance-2.0";
|
|
17464
17477
|
VALID_RESOLUTIONS = ["480p", "720p", "1080p"];
|
|
17465
17478
|
VALID_ASPECTS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"];
|
|
17479
|
+
SEEDANCE_USD_PER_1K_TOKENS = 0.014;
|
|
17480
|
+
SEEDANCE_FAST_FACTOR = 0.8;
|
|
17481
|
+
SEEDANCE_VIDEO_REF_FACTOR = 0.6;
|
|
17482
|
+
SEEDANCE_SHORT_SIDE = { "480p": 480, "720p": 720, "1080p": 1080 };
|
|
17466
17483
|
FalProvider = class {
|
|
17467
17484
|
constructor() {
|
|
17468
17485
|
this.id = "seedance";
|
|
@@ -18287,6 +18304,7 @@ __export(dist_exports2, {
|
|
|
18287
18304
|
defineApiKey: () => defineApiKey,
|
|
18288
18305
|
defineProvider: () => defineProvider,
|
|
18289
18306
|
elevenLabsProvider: () => elevenLabsProvider,
|
|
18307
|
+
estimateSeedanceVideoCostUsd: () => estimateSeedanceVideoCostUsd,
|
|
18290
18308
|
falProvider: () => falProvider,
|
|
18291
18309
|
geminiProvider: () => geminiProvider,
|
|
18292
18310
|
getAllApiKeys: () => getAllApiKeys,
|
|
@@ -27005,6 +27023,43 @@ vibe host setup all --write # write project/app config
|
|
|
27005
27023
|
vibe host doctor all --json # verify readiness
|
|
27006
27024
|
\`\`\`
|
|
27007
27025
|
|
|
27026
|
+
## Native host goal loop
|
|
27027
|
+
|
|
27028
|
+
For long-running work, use Codex Goal mode, Claude Code \`/goal\`, or the
|
|
27029
|
+
host's equivalent as the outer loop. VibeFrame should not run a competing
|
|
27030
|
+
project-level goal loop; it provides \`--json\` commands, dry runs, cost caps,
|
|
27031
|
+
\`build-report.json\`, \`review-report.json\`, \`nextActions\`, \`safeToAutoRun\`,
|
|
27032
|
+
\`requiresConfirmation\`, \`fixOwner\`, deterministic repair, and render
|
|
27033
|
+
inspection for the host agent to reason over.
|
|
27034
|
+
|
|
27035
|
+
Copy-paste examples:
|
|
27036
|
+
|
|
27037
|
+
\`\`\`text
|
|
27038
|
+
/goal Build this VibeFrame project into renders/final.mp4. Use --json for every
|
|
27039
|
+
vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
|
|
27040
|
+
unless the user sets another budget, read build-report.json and
|
|
27041
|
+
review-report.json before deciding the next action, prefer nextActions before
|
|
27042
|
+
guessing, run only safeToAutoRun:true actions automatically, and ask before
|
|
27043
|
+
requiresConfirmation:true actions. Treat retryWith as the compatibility
|
|
27044
|
+
fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
|
|
27045
|
+
STORYBOARD.md, DESIGN.md, or composition edits.
|
|
27046
|
+
Stop only when renders/final.mp4 exists, duration and aspect ratio match the
|
|
27047
|
+
brief, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
|
|
27048
|
+
remaining host-agent issue is fixed, accepted with rationale, or reported as
|
|
27049
|
+
blocked.
|
|
27050
|
+
\`\`\`
|
|
27051
|
+
|
|
27052
|
+
\`\`\`text
|
|
27053
|
+
/goal Finish this VibeFrame render using Claude Code goal mode as the outer
|
|
27054
|
+
loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
|
|
27055
|
+
paid operations, budget cap via --max-cost 5, nextActions before guessing,
|
|
27056
|
+
build-report.json/review-report.json as loop state, and safeToAutoRun,
|
|
27057
|
+
requiresConfirmation, and fixOwner to decide between vibe scene repair and
|
|
27058
|
+
host-agent edits. Stop only after final MP4,
|
|
27059
|
+
target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
|
|
27060
|
+
no unresolved unacknowledged host-agent issues.
|
|
27061
|
+
\`\`\`
|
|
27062
|
+
|
|
27008
27063
|
## Skills \u2014 USE THESE FIRST
|
|
27009
27064
|
|
|
27010
27065
|
@SKILL.md
|
|
@@ -27500,6 +27555,9 @@ function clean(text) {
|
|
|
27500
27555
|
function humanise(id) {
|
|
27501
27556
|
return id.split(/[-_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
27502
27557
|
}
|
|
27558
|
+
function sceneCompositionId(beatId) {
|
|
27559
|
+
return `scene-${beatId}`;
|
|
27560
|
+
}
|
|
27503
27561
|
function slugifySceneName(name) {
|
|
27504
27562
|
const normalised = name.normalize("NFKD").replace(/[̀-ͯ]/g, "");
|
|
27505
27563
|
const slug = normalised.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -27536,10 +27594,16 @@ function fitKineticFontSize(words) {
|
|
|
27536
27594
|
function kenBurnsTween(scope, dur, endScale = 1.08) {
|
|
27537
27595
|
return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
|
|
27538
27596
|
}
|
|
27597
|
+
function backdropMotionTweens(scope, dur, hasImage, endScale = 1.08) {
|
|
27598
|
+
const sweep = hasImage ? "" : `
|
|
27599
|
+
tl.fromTo('${scope} .ambient-sweep', { xPercent: -75, opacity: 0.18 }, { xPercent: 75, opacity: 0.62, duration: ${dur.toFixed(2)}, ease: 'sine.inOut' }, 0);`;
|
|
27600
|
+
return `${kenBurnsTween(scope, dur, endScale)}${sweep}`;
|
|
27601
|
+
}
|
|
27539
27602
|
function buildPreset(input) {
|
|
27540
27603
|
const id = input.id;
|
|
27541
27604
|
const scope = `[data-composition-id="${id}"]`;
|
|
27542
|
-
const
|
|
27605
|
+
const displayId = input.beatId ?? id;
|
|
27606
|
+
const headline = clean(input.headline) || humanise(displayId);
|
|
27543
27607
|
const subhead = clean(input.subhead);
|
|
27544
27608
|
const kicker = clean(input.kicker);
|
|
27545
27609
|
const dur = input.duration;
|
|
@@ -27562,8 +27626,17 @@ function buildPreset(input) {
|
|
|
27562
27626
|
position: absolute;
|
|
27563
27627
|
inset: 0;
|
|
27564
27628
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 70%);
|
|
27629
|
+
overflow: hidden;
|
|
27630
|
+
}
|
|
27631
|
+
${scope} .ambient-sweep {
|
|
27632
|
+
position: absolute;
|
|
27633
|
+
inset: -22%;
|
|
27634
|
+
display: block;
|
|
27635
|
+
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%);
|
|
27636
|
+
filter: blur(12px);
|
|
27637
|
+
transform: rotate(8deg);
|
|
27565
27638
|
}`;
|
|
27566
|
-
const backdropMarkup = `<div class="backdrop"></div>`;
|
|
27639
|
+
const backdropMarkup = hasImage ? `<div class="backdrop"></div>` : `<div class="backdrop"><div class="ambient-sweep"></div></div>`;
|
|
27567
27640
|
switch (input.preset) {
|
|
27568
27641
|
case "simple": {
|
|
27569
27642
|
const transcript = input.transcript;
|
|
@@ -27595,7 +27668,7 @@ function buildPreset(input) {
|
|
|
27595
27668
|
body: `${backdropMarkup}
|
|
27596
27669
|
<div class="caption" id="caption">${captionInner}</div>`,
|
|
27597
27670
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
27598
|
-
${
|
|
27671
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
27599
27672
|
${captionTween}`
|
|
27600
27673
|
};
|
|
27601
27674
|
}
|
|
@@ -27634,12 +27707,12 @@ function buildPreset(input) {
|
|
|
27634
27707
|
// scene's own fade-in handles the visual transition. Backdrop
|
|
27635
27708
|
// Ken-Burns fills what used to be 80% dead static frame.
|
|
27636
27709
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
27637
|
-
${
|
|
27710
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
27638
27711
|
tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
|
|
27639
27712
|
};
|
|
27640
27713
|
}
|
|
27641
27714
|
case "explainer": {
|
|
27642
|
-
const k = kicker || humanise(
|
|
27715
|
+
const k = kicker || humanise(displayId).toUpperCase();
|
|
27643
27716
|
const sub = subhead || "";
|
|
27644
27717
|
const transcript = input.transcript;
|
|
27645
27718
|
const useWordSync = !!(transcript && transcript.length > 0 && sub);
|
|
@@ -27681,7 +27754,7 @@ function buildPreset(input) {
|
|
|
27681
27754
|
<div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
|
|
27682
27755
|
</div>`,
|
|
27683
27756
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
27684
|
-
${
|
|
27757
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
27685
27758
|
tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
|
|
27686
27759
|
tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
|
|
27687
27760
|
${subtitleTween}`
|
|
@@ -27722,12 +27795,12 @@ function buildPreset(input) {
|
|
|
27722
27795
|
body: `${backdropMarkup}
|
|
27723
27796
|
<div class="kinetic">${wordSpans}</div>`,
|
|
27724
27797
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
27725
|
-
${
|
|
27798
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
27726
27799
|
${tweens}`
|
|
27727
27800
|
};
|
|
27728
27801
|
}
|
|
27729
27802
|
case "product-shot": {
|
|
27730
|
-
const label = kicker || humanise(
|
|
27803
|
+
const label = kicker || humanise(displayId);
|
|
27731
27804
|
const headlineFontPx = fitFontSize(headline, 72, 40, 50);
|
|
27732
27805
|
return {
|
|
27733
27806
|
css: `${scope} {
|
|
@@ -27761,7 +27834,7 @@ function buildPreset(input) {
|
|
|
27761
27834
|
<div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
|
|
27762
27835
|
<div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
|
|
27763
27836
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
27764
|
-
|
|
27837
|
+
${backdropMotionTweens(scope, dur, hasImage, 1.12)}
|
|
27765
27838
|
tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
|
|
27766
27839
|
tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
|
|
27767
27840
|
tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
|
|
@@ -27822,9 +27895,10 @@ function emitSceneHtml(input) {
|
|
|
27822
27895
|
if (input.width <= 0 || input.height <= 0) {
|
|
27823
27896
|
throw new Error(`Invalid canvas dims: ${input.width}x${input.height}`);
|
|
27824
27897
|
}
|
|
27825
|
-
const
|
|
27898
|
+
const beatId = input.id;
|
|
27899
|
+
const id = sceneCompositionId(beatId);
|
|
27826
27900
|
const dur = Number(input.duration.toFixed(3));
|
|
27827
|
-
const parts = buildPreset({ ...input, id, duration: dur });
|
|
27901
|
+
const parts = buildPreset({ ...input, id, beatId, duration: dur });
|
|
27828
27902
|
const audioBlock = input.audioPath ? `
|
|
27829
27903
|
<audio
|
|
27830
27904
|
id="narration"
|
|
@@ -27840,7 +27914,7 @@ function emitSceneHtml(input) {
|
|
|
27840
27914
|
${lottieLayer.markup}` : "";
|
|
27841
27915
|
const lottieScript = lottieLayer ? `
|
|
27842
27916
|
${lottieLayer.script}` : "";
|
|
27843
|
-
return `<template id="
|
|
27917
|
+
return `<template id="${id}-template">
|
|
27844
27918
|
<div data-composition-id="${id}" data-start="0" data-duration="${dur}" data-width="${input.width}" data-height="${input.height}">
|
|
27845
27919
|
<style>
|
|
27846
27920
|
${parts.css}
|
|
@@ -27876,8 +27950,9 @@ function buildClipReference(opts) {
|
|
|
27876
27950
|
const duration = Number(opts.duration.toFixed(3));
|
|
27877
27951
|
const track = opts.trackIndex ?? 1;
|
|
27878
27952
|
const src = opts.src ?? `compositions/scene-${opts.id}.html`;
|
|
27953
|
+
const compositionId = sceneCompositionId(opts.id);
|
|
27879
27954
|
const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
|
|
27880
|
-
return `<div class="clip" data-composition-id="${esc(
|
|
27955
|
+
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>`;
|
|
27881
27956
|
}
|
|
27882
27957
|
function insertClipIntoRoot(rootHtml, clip) {
|
|
27883
27958
|
const clipDiv = buildClipReference(clip);
|
|
@@ -29355,6 +29430,39 @@ var init_install_skill = __esm({
|
|
|
29355
29430
|
});
|
|
29356
29431
|
|
|
29357
29432
|
// src/commands/_shared/storyboard-parse.ts
|
|
29433
|
+
function resolveCharacters(frontmatter) {
|
|
29434
|
+
const pool = frontmatter?.characters;
|
|
29435
|
+
if (!pool || typeof pool !== "object") return [];
|
|
29436
|
+
const resolved = [];
|
|
29437
|
+
for (const [name, spec] of Object.entries(pool)) {
|
|
29438
|
+
if (typeof spec === "string") {
|
|
29439
|
+
const prompt3 = spec.trim();
|
|
29440
|
+
if (prompt3) resolved.push({ name, prompt: prompt3 });
|
|
29441
|
+
continue;
|
|
29442
|
+
}
|
|
29443
|
+
if (spec && typeof spec === "object") {
|
|
29444
|
+
const image = typeof spec.image === "string" ? spec.image.trim() : "";
|
|
29445
|
+
if (image) {
|
|
29446
|
+
resolved.push({ name, imagePath: image });
|
|
29447
|
+
continue;
|
|
29448
|
+
}
|
|
29449
|
+
const description = typeof spec.description === "string" ? spec.description.trim() : "";
|
|
29450
|
+
if (description) resolved.push({ name, prompt: description });
|
|
29451
|
+
}
|
|
29452
|
+
}
|
|
29453
|
+
return resolved;
|
|
29454
|
+
}
|
|
29455
|
+
function beatCharacterNames(cues) {
|
|
29456
|
+
const raw = cues?.characters;
|
|
29457
|
+
if (typeof raw === "string") {
|
|
29458
|
+
const name = raw.trim();
|
|
29459
|
+
return name ? [name] : [];
|
|
29460
|
+
}
|
|
29461
|
+
if (Array.isArray(raw)) {
|
|
29462
|
+
return raw.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
|
|
29463
|
+
}
|
|
29464
|
+
return [];
|
|
29465
|
+
}
|
|
29358
29466
|
function parseStoryboard(md) {
|
|
29359
29467
|
const normalized = md.replace(/\r\n/g, "\n");
|
|
29360
29468
|
const { frontmatter, remaining } = extractProjectFrontmatter(normalized);
|
|
@@ -29960,7 +30068,15 @@ ${finalDurationBullet}
|
|
|
29960
30068
|
itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
|
|
29961
30069
|
inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
|
|
29962
30070
|
phase B in, on the same timeline.
|
|
29963
|
-
- If \`assets/
|
|
30071
|
+
- If \`assets/video-${ctx.beat.id}.mp4\` exists, it is an AI-generated clip for
|
|
30072
|
+
this beat \u2014 use it as the FULL-FRAME visual and prefer it over any backdrop
|
|
30073
|
+
image or CSS. Put one full-bleed \`<video>\` inside a full-window \`.clip\`:
|
|
30074
|
+
\`<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>\`
|
|
30075
|
+
Use the exact path (no \`./\` or \`../\`). The framework seeks the clip to each
|
|
30076
|
+
timeline frame, so do NOT add transform tweens to the \`<video>\` or its
|
|
30077
|
+
ancestors; layer text/motion graphics on top instead.
|
|
30078
|
+
- If \`assets/backdrop-${ctx.beat.id}.png\` exists (and no video clip above),
|
|
30079
|
+
use that local file as the
|
|
29964
30080
|
full-frame visual backdrop. The exact path string is
|
|
29965
30081
|
\`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
|
|
29966
30082
|
because fragments are mounted from the project root.
|
|
@@ -30878,6 +30994,101 @@ function uniqueRetryWith(items) {
|
|
|
30878
30994
|
...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
|
|
30879
30995
|
];
|
|
30880
30996
|
}
|
|
30997
|
+
function commandReviewAction(command, opts) {
|
|
30998
|
+
const classified = classifyReviewCommand(command);
|
|
30999
|
+
return stripUndefined({
|
|
31000
|
+
id: actionId("command", command),
|
|
31001
|
+
kind: "command",
|
|
31002
|
+
label: opts.label ?? command,
|
|
31003
|
+
command,
|
|
31004
|
+
fixOwner: opts.fixOwner ?? classified.fixOwner,
|
|
31005
|
+
costTier: opts.costTier ?? classified.costTier,
|
|
31006
|
+
safeToAutoRun: opts.safeToAutoRun ?? classified.safeToAutoRun,
|
|
31007
|
+
requiresConfirmation: opts.requiresConfirmation ?? classified.requiresConfirmation,
|
|
31008
|
+
reason: opts.reason,
|
|
31009
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
31010
|
+
});
|
|
31011
|
+
}
|
|
31012
|
+
function agentReviewAction(opts) {
|
|
31013
|
+
return stripUndefined({
|
|
31014
|
+
id: actionId("agent", opts.agentPrompt),
|
|
31015
|
+
kind: "agent",
|
|
31016
|
+
label: opts.label,
|
|
31017
|
+
agentPrompt: opts.agentPrompt,
|
|
31018
|
+
fixOwner: "host-agent",
|
|
31019
|
+
costTier: "unknown",
|
|
31020
|
+
safeToAutoRun: false,
|
|
31021
|
+
requiresConfirmation: false,
|
|
31022
|
+
reason: opts.reason,
|
|
31023
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
31024
|
+
});
|
|
31025
|
+
}
|
|
31026
|
+
function manualReviewAction(opts) {
|
|
31027
|
+
return stripUndefined({
|
|
31028
|
+
id: actionId("manual", opts.label),
|
|
31029
|
+
kind: "manual",
|
|
31030
|
+
label: opts.label,
|
|
31031
|
+
fixOwner: opts.fixOwner ?? "host-agent",
|
|
31032
|
+
costTier: "unknown",
|
|
31033
|
+
safeToAutoRun: false,
|
|
31034
|
+
requiresConfirmation: false,
|
|
31035
|
+
reason: opts.reason,
|
|
31036
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
31037
|
+
});
|
|
31038
|
+
}
|
|
31039
|
+
function reviewActionsFromRetryWith(retryWith, sourceIssueCodes = []) {
|
|
31040
|
+
return normalizeReviewActions(
|
|
31041
|
+
retryWith.map((item) => {
|
|
31042
|
+
if (/^codex\s+/i.test(item)) {
|
|
31043
|
+
return agentReviewAction({
|
|
31044
|
+
label: "Ask the host agent to fix review issues",
|
|
31045
|
+
agentPrompt: item,
|
|
31046
|
+
reason: "The review report contains host-agent-owned issues.",
|
|
31047
|
+
sourceIssueCodes
|
|
31048
|
+
});
|
|
31049
|
+
}
|
|
31050
|
+
return commandReviewAction(item, {
|
|
31051
|
+
reason: "Backward-compatible retryWith command.",
|
|
31052
|
+
sourceIssueCodes
|
|
31053
|
+
});
|
|
31054
|
+
})
|
|
31055
|
+
);
|
|
31056
|
+
}
|
|
31057
|
+
function normalizeReviewActions(actions) {
|
|
31058
|
+
const merged = /* @__PURE__ */ new Map();
|
|
31059
|
+
for (const action of actions) {
|
|
31060
|
+
const key = [action.kind, action.command ?? action.agentPrompt ?? action.label].join("\0");
|
|
31061
|
+
const existing = merged.get(key);
|
|
31062
|
+
if (!existing) {
|
|
31063
|
+
merged.set(key, {
|
|
31064
|
+
...action,
|
|
31065
|
+
sourceIssueCodes: uniqueStrings(action.sourceIssueCodes ?? [])
|
|
31066
|
+
});
|
|
31067
|
+
continue;
|
|
31068
|
+
}
|
|
31069
|
+
merged.set(key, {
|
|
31070
|
+
...existing,
|
|
31071
|
+
safeToAutoRun: existing.safeToAutoRun && action.safeToAutoRun,
|
|
31072
|
+
requiresConfirmation: existing.requiresConfirmation || action.requiresConfirmation,
|
|
31073
|
+
sourceIssueCodes: uniqueStrings([
|
|
31074
|
+
...existing.sourceIssueCodes ?? [],
|
|
31075
|
+
...action.sourceIssueCodes ?? []
|
|
31076
|
+
])
|
|
31077
|
+
});
|
|
31078
|
+
}
|
|
31079
|
+
return [...merged.values()].map(
|
|
31080
|
+
(action) => stripUndefined({
|
|
31081
|
+
...action,
|
|
31082
|
+
sourceIssueCodes: action.sourceIssueCodes && action.sourceIssueCodes.length > 0 ? action.sourceIssueCodes : void 0
|
|
31083
|
+
})
|
|
31084
|
+
);
|
|
31085
|
+
}
|
|
31086
|
+
function deriveNextReviewActions(opts) {
|
|
31087
|
+
return normalizeReviewActions([
|
|
31088
|
+
...opts.issues.flatMap((issue) => issue.actions ?? []),
|
|
31089
|
+
...reviewActionsFromRetryWith(opts.retryWith)
|
|
31090
|
+
]);
|
|
31091
|
+
}
|
|
30881
31092
|
function fixOwnerForIssue(issue) {
|
|
30882
31093
|
if (issue.fixOwner) return issue.fixOwner;
|
|
30883
31094
|
if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
|
|
@@ -30887,11 +31098,23 @@ function fixOwnerForIssue(issue) {
|
|
|
30887
31098
|
}
|
|
30888
31099
|
return "vibe";
|
|
30889
31100
|
}
|
|
30890
|
-
function normalizeReviewIssues(issues,
|
|
30891
|
-
|
|
30892
|
-
|
|
30893
|
-
|
|
30894
|
-
|
|
31101
|
+
function normalizeReviewIssues(issues, opts) {
|
|
31102
|
+
const fallbackOwner = typeof opts === "string" ? opts : opts?.fallbackOwner;
|
|
31103
|
+
const projectDir = typeof opts === "string" ? void 0 : opts?.projectDir;
|
|
31104
|
+
return issues.map((issue) => {
|
|
31105
|
+
const normalized = {
|
|
31106
|
+
...issue,
|
|
31107
|
+
fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
|
|
31108
|
+
};
|
|
31109
|
+
const actions = normalizeReviewActions([
|
|
31110
|
+
...issue.actions ?? [],
|
|
31111
|
+
...projectDir ? defaultActionsForIssue(normalized, projectDir) : []
|
|
31112
|
+
]);
|
|
31113
|
+
return stripUndefined({
|
|
31114
|
+
...normalized,
|
|
31115
|
+
actions: actions.length > 0 ? actions : void 0
|
|
31116
|
+
});
|
|
31117
|
+
});
|
|
30895
31118
|
}
|
|
30896
31119
|
function summarizeReviewIssues(issues) {
|
|
30897
31120
|
const normalized = normalizeReviewIssues(issues);
|
|
@@ -30907,7 +31130,11 @@ function summarizeReviewIssues(issues) {
|
|
|
30907
31130
|
};
|
|
30908
31131
|
}
|
|
30909
31132
|
function buildReviewReport(opts) {
|
|
30910
|
-
const
|
|
31133
|
+
const retryWith = uniqueRetryWith(opts.retryWith);
|
|
31134
|
+
const issues = normalizeReviewIssues(opts.issues, {
|
|
31135
|
+
projectDir: opts.project,
|
|
31136
|
+
retryWith
|
|
31137
|
+
});
|
|
30911
31138
|
return stripUndefined({
|
|
30912
31139
|
schemaVersion: "1",
|
|
30913
31140
|
kind: "review",
|
|
@@ -30918,7 +31145,8 @@ function buildReviewReport(opts) {
|
|
|
30918
31145
|
score: opts.score,
|
|
30919
31146
|
issues,
|
|
30920
31147
|
summary: summarizeReviewIssues(issues),
|
|
30921
|
-
retryWith:
|
|
31148
|
+
nextActions: deriveNextReviewActions({ issues, retryWith, projectDir: opts.project }),
|
|
31149
|
+
retryWith,
|
|
30922
31150
|
sourceReports: opts.sourceReports ?? [],
|
|
30923
31151
|
reportPath: opts.reportPath
|
|
30924
31152
|
});
|
|
@@ -30932,6 +31160,306 @@ async function writeReviewReport(path2, report) {
|
|
|
30932
31160
|
function stripUndefined(value) {
|
|
30933
31161
|
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
30934
31162
|
}
|
|
31163
|
+
function defaultActionsForIssue(issue, projectDir) {
|
|
31164
|
+
const code = issue.code;
|
|
31165
|
+
const beat = issue.beatId ?? issue.scene;
|
|
31166
|
+
const beatFlag = beat ? ` --beat ${beat}` : "";
|
|
31167
|
+
const sourceIssueCodes = [code];
|
|
31168
|
+
if (code === "PROJECT_NOT_FOUND" || code === "MISSING_STORYBOARD") {
|
|
31169
|
+
return [
|
|
31170
|
+
commandReviewAction(`vibe init ${projectDir} --from "<brief>" --json`, {
|
|
31171
|
+
label: "Initialize the project from a brief",
|
|
31172
|
+
fixOwner: "host-agent",
|
|
31173
|
+
reason: "The project cannot be inspected until the core project files exist.",
|
|
31174
|
+
sourceIssueCodes
|
|
31175
|
+
})
|
|
31176
|
+
];
|
|
31177
|
+
}
|
|
31178
|
+
if (code === "MISSING_DESIGN" || code === "DESIGN_PLACEHOLDER_FIELD") {
|
|
31179
|
+
return [
|
|
31180
|
+
manualReviewAction({
|
|
31181
|
+
label: "Create or complete DESIGN.md",
|
|
31182
|
+
reason: "Visual direction needs host-agent or human input before VibeFrame can repair it.",
|
|
31183
|
+
sourceIssueCodes
|
|
31184
|
+
})
|
|
31185
|
+
];
|
|
31186
|
+
}
|
|
31187
|
+
if (code === "BEAT_NOT_FOUND" || code.startsWith("STORYBOARD_")) {
|
|
31188
|
+
const actions = [
|
|
31189
|
+
commandReviewAction(`vibe storyboard validate ${projectDir} --json`, {
|
|
31190
|
+
label: "Validate the storyboard",
|
|
31191
|
+
fixOwner: "host-agent",
|
|
31192
|
+
reason: "The storyboard issue needs structured validation before editing.",
|
|
31193
|
+
sourceIssueCodes
|
|
31194
|
+
})
|
|
31195
|
+
];
|
|
31196
|
+
if (code === "STORYBOARD_PLACEHOLDER_CUE") {
|
|
31197
|
+
actions.push(
|
|
31198
|
+
commandReviewAction(
|
|
31199
|
+
`vibe storyboard revise ${projectDir} --from "<make cues concrete>" --dry-run --json`,
|
|
31200
|
+
{
|
|
31201
|
+
label: "Preview a storyboard revision",
|
|
31202
|
+
fixOwner: "host-agent",
|
|
31203
|
+
reason: "Placeholder cues should be revised into concrete video intent.",
|
|
31204
|
+
sourceIssueCodes
|
|
31205
|
+
}
|
|
31206
|
+
)
|
|
31207
|
+
);
|
|
31208
|
+
}
|
|
31209
|
+
return actions;
|
|
31210
|
+
}
|
|
31211
|
+
if (code === "MISSING_ROOT_COMPOSITION" || code === "ROOT_SYNC_CHECK_FAILED") {
|
|
31212
|
+
return [
|
|
31213
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
31214
|
+
label: "Sync the project root composition",
|
|
31215
|
+
reason: "The root composition is missing or could not be verified.",
|
|
31216
|
+
sourceIssueCodes
|
|
31217
|
+
})
|
|
31218
|
+
];
|
|
31219
|
+
}
|
|
31220
|
+
if (code.startsWith("ROOT_")) {
|
|
31221
|
+
return issue.fixOwner === "host-agent" ? [
|
|
31222
|
+
agentReviewAction({
|
|
31223
|
+
label: "Repair the root composition shell",
|
|
31224
|
+
agentPrompt: "Fix host-agent-owned root composition issues from review-report.json",
|
|
31225
|
+
reason: "The root composition cannot be repaired deterministically.",
|
|
31226
|
+
sourceIssueCodes
|
|
31227
|
+
})
|
|
31228
|
+
] : [
|
|
31229
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
31230
|
+
label: "Repair deterministic scene/root issues",
|
|
31231
|
+
reason: "The root composition has deterministic sync issues.",
|
|
31232
|
+
sourceIssueCodes
|
|
31233
|
+
}),
|
|
31234
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
31235
|
+
label: "Resync root composition after repair",
|
|
31236
|
+
reason: "The root composition should be synced after deterministic repair.",
|
|
31237
|
+
sourceIssueCodes
|
|
31238
|
+
})
|
|
31239
|
+
];
|
|
31240
|
+
}
|
|
31241
|
+
if (code === "MISSING_COMPOSITION") {
|
|
31242
|
+
return [
|
|
31243
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage compose --json`, {
|
|
31244
|
+
label: beat ? `Compose beat ${beat}` : "Compose missing scenes",
|
|
31245
|
+
reason: "Missing scene compositions must be generated or authored before render.",
|
|
31246
|
+
sourceIssueCodes
|
|
31247
|
+
})
|
|
31248
|
+
];
|
|
31249
|
+
}
|
|
31250
|
+
if (code === "MISSING_BUILD_REPORT") {
|
|
31251
|
+
return [
|
|
31252
|
+
commandReviewAction(`vibe build ${projectDir} --dry-run --json`, {
|
|
31253
|
+
label: "Preview the build plan",
|
|
31254
|
+
reason: "A dry-run build report is needed before choosing a paid or mutating build step.",
|
|
31255
|
+
sourceIssueCodes
|
|
31256
|
+
})
|
|
31257
|
+
];
|
|
31258
|
+
}
|
|
31259
|
+
if (code === "BUILD_REPORT_BEAT_MISSING") {
|
|
31260
|
+
return [
|
|
31261
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
31262
|
+
label: beat ? `Sync build report for beat ${beat}` : "Sync the build report",
|
|
31263
|
+
reason: "The build report does not include the inspected beat.",
|
|
31264
|
+
sourceIssueCodes
|
|
31265
|
+
})
|
|
31266
|
+
];
|
|
31267
|
+
}
|
|
31268
|
+
if (code === "MALFORMED_BUILD_REPORT") {
|
|
31269
|
+
return [
|
|
31270
|
+
commandReviewAction(`vibe build ${projectDir} --json`, {
|
|
31271
|
+
label: "Regenerate the build report",
|
|
31272
|
+
reason: "The existing build report cannot be parsed.",
|
|
31273
|
+
sourceIssueCodes
|
|
31274
|
+
})
|
|
31275
|
+
];
|
|
31276
|
+
}
|
|
31277
|
+
if (code === "MISSING_REPORTED_ASSET" || code === "STALE_ASSET" || code === "UNKNOWN_ASSET_FRESHNESS") {
|
|
31278
|
+
return [
|
|
31279
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --force --json`, {
|
|
31280
|
+
label: beat ? `Regenerate assets for beat ${beat}` : "Regenerate stale or missing assets",
|
|
31281
|
+
reason: "Asset generation may call providers, so the host should confirm first.",
|
|
31282
|
+
sourceIssueCodes
|
|
31283
|
+
})
|
|
31284
|
+
];
|
|
31285
|
+
}
|
|
31286
|
+
if (code === "MUSIC_CUE_NOT_READY") {
|
|
31287
|
+
return [
|
|
31288
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --json`, {
|
|
31289
|
+
label: beat ? `Generate music for beat ${beat}` : "Generate missing music assets",
|
|
31290
|
+
reason: "Music generation may call providers, so the host should confirm first.",
|
|
31291
|
+
sourceIssueCodes
|
|
31292
|
+
}),
|
|
31293
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
31294
|
+
label: "Sync music into the root composition",
|
|
31295
|
+
reason: "Generated music needs root composition wiring.",
|
|
31296
|
+
sourceIssueCodes
|
|
31297
|
+
})
|
|
31298
|
+
];
|
|
31299
|
+
}
|
|
31300
|
+
if (code.startsWith("SCENE_LINT_")) {
|
|
31301
|
+
return [
|
|
31302
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
31303
|
+
label: "Repair deterministic scene lint issues",
|
|
31304
|
+
reason: "Scene lint reported an issue VibeFrame may be able to repair mechanically.",
|
|
31305
|
+
sourceIssueCodes
|
|
31306
|
+
})
|
|
31307
|
+
];
|
|
31308
|
+
}
|
|
31309
|
+
if (code === "RENDER_NOT_FOUND" || code === "EMPTY_RENDER") {
|
|
31310
|
+
return [
|
|
31311
|
+
commandReviewAction(
|
|
31312
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
31313
|
+
{
|
|
31314
|
+
label: beat ? `Render beat ${beat}` : "Render the project",
|
|
31315
|
+
reason: "A render file is required before render inspection can pass.",
|
|
31316
|
+
sourceIssueCodes
|
|
31317
|
+
}
|
|
31318
|
+
)
|
|
31319
|
+
];
|
|
31320
|
+
}
|
|
31321
|
+
if (code === "NO_AUDIO_STREAM" || code === "DURATION_DRIFT") {
|
|
31322
|
+
return [
|
|
31323
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
31324
|
+
label: beat ? `Sync timing/audio for beat ${beat}` : "Sync timing and audio wiring",
|
|
31325
|
+
reason: "The render output no longer matches synced project state.",
|
|
31326
|
+
sourceIssueCodes
|
|
31327
|
+
}),
|
|
31328
|
+
commandReviewAction(
|
|
31329
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
31330
|
+
{
|
|
31331
|
+
label: beat ? `Rerender beat ${beat}` : "Rerender the project",
|
|
31332
|
+
reason: "The render should be regenerated after sync repair.",
|
|
31333
|
+
sourceIssueCodes
|
|
31334
|
+
}
|
|
31335
|
+
)
|
|
31336
|
+
];
|
|
31337
|
+
}
|
|
31338
|
+
if (code === "BLACK_FRAME_SEGMENT") {
|
|
31339
|
+
return issue.fixOwner === "host-agent" ? [
|
|
31340
|
+
agentReviewAction({
|
|
31341
|
+
label: beat ? `Fix black frames in beat ${beat}` : "Fix black frame segment",
|
|
31342
|
+
agentPrompt: "Fix black-frame issues from review-report.json, then rerender.",
|
|
31343
|
+
reason: "The issue likely needs scene, storyboard, or visual cue changes.",
|
|
31344
|
+
sourceIssueCodes
|
|
31345
|
+
})
|
|
31346
|
+
] : [
|
|
31347
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
31348
|
+
label: "Repair deterministic scene timing",
|
|
31349
|
+
reason: "Black frames outside a specific beat may be recoverable with scene repair.",
|
|
31350
|
+
sourceIssueCodes
|
|
31351
|
+
})
|
|
31352
|
+
];
|
|
31353
|
+
}
|
|
31354
|
+
if (code === "STATIC_FRAME_SEGMENT") {
|
|
31355
|
+
return [
|
|
31356
|
+
agentReviewAction({
|
|
31357
|
+
label: beat ? `Add motion to beat ${beat}` : "Fix static visual hold",
|
|
31358
|
+
agentPrompt: "Fix static-frame issues from review-report.json, then rerender.",
|
|
31359
|
+
reason: "Static segments usually need creative scene, storyboard, or asset changes.",
|
|
31360
|
+
sourceIssueCodes
|
|
31361
|
+
})
|
|
31362
|
+
];
|
|
31363
|
+
}
|
|
31364
|
+
if (code === "LONG_SILENCE") {
|
|
31365
|
+
return issue.fixOwner === "host-agent" ? [
|
|
31366
|
+
agentReviewAction({
|
|
31367
|
+
label: beat ? `Fix long silence in beat ${beat}` : "Fix long silence",
|
|
31368
|
+
agentPrompt: "Fix long-silence issues from review-report.json, then rerender.",
|
|
31369
|
+
reason: "The beat duration, narration, or music cue needs host-agent judgment.",
|
|
31370
|
+
sourceIssueCodes
|
|
31371
|
+
})
|
|
31372
|
+
] : [
|
|
31373
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
31374
|
+
label: beat ? `Sync audio for beat ${beat}` : "Sync audio wiring",
|
|
31375
|
+
reason: "The silence may be caused by stale narration or music wiring.",
|
|
31376
|
+
sourceIssueCodes
|
|
31377
|
+
})
|
|
31378
|
+
];
|
|
31379
|
+
}
|
|
31380
|
+
if (code.startsWith("AI_REVIEW_")) {
|
|
31381
|
+
return [
|
|
31382
|
+
agentReviewAction({
|
|
31383
|
+
label: "Fix AI review findings",
|
|
31384
|
+
agentPrompt: "Fix host-agent-owned AI review issues from review-report.json, then rerender.",
|
|
31385
|
+
reason: "AI review findings require storyboard, design, or composition judgment.",
|
|
31386
|
+
sourceIssueCodes
|
|
31387
|
+
})
|
|
31388
|
+
];
|
|
31389
|
+
}
|
|
31390
|
+
if (code === "FFPROBE_UNAVAILABLE" || code === "FFMPEG_UNAVAILABLE") {
|
|
31391
|
+
return [
|
|
31392
|
+
manualReviewAction({
|
|
31393
|
+
label: "Install FFmpeg and rerun inspection",
|
|
31394
|
+
reason: "Local media inspection depends on ffmpeg/ffprobe being available.",
|
|
31395
|
+
fixOwner: "host-agent",
|
|
31396
|
+
sourceIssueCodes
|
|
31397
|
+
})
|
|
31398
|
+
];
|
|
31399
|
+
}
|
|
31400
|
+
if (code === "FFPROBE_FAILED" || code === "NO_VIDEO_STREAM") {
|
|
31401
|
+
return [
|
|
31402
|
+
commandReviewAction(
|
|
31403
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
31404
|
+
{
|
|
31405
|
+
label: beat ? `Rerender beat ${beat}` : "Rerender the project",
|
|
31406
|
+
reason: "The rendered media could not be probed as a valid video.",
|
|
31407
|
+
sourceIssueCodes
|
|
31408
|
+
}
|
|
31409
|
+
)
|
|
31410
|
+
];
|
|
31411
|
+
}
|
|
31412
|
+
if (code === "ASPECT_MISMATCH") {
|
|
31413
|
+
return [
|
|
31414
|
+
manualReviewAction({
|
|
31415
|
+
label: "Check project aspect and render settings",
|
|
31416
|
+
reason: "Aspect mismatches can come from either config intent or render settings.",
|
|
31417
|
+
fixOwner: issue.fixOwner,
|
|
31418
|
+
sourceIssueCodes
|
|
31419
|
+
})
|
|
31420
|
+
];
|
|
31421
|
+
}
|
|
31422
|
+
return [];
|
|
31423
|
+
}
|
|
31424
|
+
function classifyReviewCommand(command) {
|
|
31425
|
+
const lower = command.trim().toLowerCase();
|
|
31426
|
+
if (lower.startsWith("vibe status ")) return safe("free");
|
|
31427
|
+
if (lower.startsWith("vibe scene repair")) return safe("free");
|
|
31428
|
+
if (lower.startsWith("vibe storyboard validate") || lower.startsWith("vibe storyboard get") || lower.startsWith("vibe storyboard list")) {
|
|
31429
|
+
return safe("free", "host-agent");
|
|
31430
|
+
}
|
|
31431
|
+
if (lower.startsWith("vibe inspect project")) return safe("free");
|
|
31432
|
+
if (lower.startsWith("vibe inspect render")) {
|
|
31433
|
+
if (lower.includes("--ai")) return unsafe("low");
|
|
31434
|
+
return safe("free");
|
|
31435
|
+
}
|
|
31436
|
+
if (lower.startsWith("vibe render ")) return safe("free");
|
|
31437
|
+
if (lower.startsWith("vibe build ")) {
|
|
31438
|
+
if (lower.includes("--dry-run")) return safe("free");
|
|
31439
|
+
if (lower.includes("--stage sync") || lower.includes("--stage render")) return safe("free");
|
|
31440
|
+
return unsafe("unknown");
|
|
31441
|
+
}
|
|
31442
|
+
if (lower.startsWith("vibe generate video") || lower.startsWith("vibe edit fill-gaps") || lower.startsWith("vibe remix ")) {
|
|
31443
|
+
return unsafe("very-high");
|
|
31444
|
+
}
|
|
31445
|
+
if (lower.startsWith("vibe storyboard revise")) return unsafe("unknown", "host-agent");
|
|
31446
|
+
return unsafe("unknown");
|
|
31447
|
+
}
|
|
31448
|
+
function safe(costTier, fixOwner = "vibe") {
|
|
31449
|
+
return { fixOwner, costTier, safeToAutoRun: true, requiresConfirmation: false };
|
|
31450
|
+
}
|
|
31451
|
+
function unsafe(costTier, fixOwner = "vibe") {
|
|
31452
|
+
return { fixOwner, costTier, safeToAutoRun: false, requiresConfirmation: true };
|
|
31453
|
+
}
|
|
31454
|
+
function actionId(kind, value) {
|
|
31455
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
31456
|
+
return `${kind}:${slug || "action"}`;
|
|
31457
|
+
}
|
|
31458
|
+
function uniqueStrings(items) {
|
|
31459
|
+
return [
|
|
31460
|
+
...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
|
|
31461
|
+
];
|
|
31462
|
+
}
|
|
30935
31463
|
var init_review_report = __esm({
|
|
30936
31464
|
"src/commands/_shared/review-report.ts"() {
|
|
30937
31465
|
"use strict";
|
|
@@ -31064,6 +31592,297 @@ var init_scene_repair = __esm({
|
|
|
31064
31592
|
}
|
|
31065
31593
|
});
|
|
31066
31594
|
|
|
31595
|
+
// src/commands/_shared/storyboard-edit.ts
|
|
31596
|
+
function validateStoryboardMarkdown(markdown) {
|
|
31597
|
+
const parsed = parseStoryboard(markdown);
|
|
31598
|
+
const sections = splitBeatSections(markdown);
|
|
31599
|
+
const issues = [];
|
|
31600
|
+
if (parsed.beats.length === 0) {
|
|
31601
|
+
issues.push({
|
|
31602
|
+
severity: "error",
|
|
31603
|
+
code: "NO_BEATS",
|
|
31604
|
+
message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
|
|
31605
|
+
});
|
|
31606
|
+
}
|
|
31607
|
+
const seen = /* @__PURE__ */ new Map();
|
|
31608
|
+
for (const beat of parsed.beats) {
|
|
31609
|
+
seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
|
|
31610
|
+
}
|
|
31611
|
+
for (const [id, count] of seen.entries()) {
|
|
31612
|
+
if (count > 1) {
|
|
31613
|
+
issues.push({
|
|
31614
|
+
severity: "error",
|
|
31615
|
+
code: "DUPLICATE_BEAT_ID",
|
|
31616
|
+
beatId: id,
|
|
31617
|
+
message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
|
|
31618
|
+
});
|
|
31619
|
+
}
|
|
31620
|
+
}
|
|
31621
|
+
for (const section of sections) {
|
|
31622
|
+
const cueBlock = readLeadingCueBlock(section.body);
|
|
31623
|
+
if (cueBlock?.error) {
|
|
31624
|
+
issues.push({
|
|
31625
|
+
severity: "error",
|
|
31626
|
+
code: "MALFORMED_CUE_YAML",
|
|
31627
|
+
beatId: section.id,
|
|
31628
|
+
message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
|
|
31629
|
+
});
|
|
31630
|
+
}
|
|
31631
|
+
}
|
|
31632
|
+
const characterPool = new Set(
|
|
31633
|
+
Object.keys(
|
|
31634
|
+
parsed.frontmatter?.characters ?? {}
|
|
31635
|
+
)
|
|
31636
|
+
);
|
|
31637
|
+
for (const beat of parsed.beats) {
|
|
31638
|
+
const cues = beat.cues ?? {};
|
|
31639
|
+
for (const [key, value] of Object.entries(cues)) {
|
|
31640
|
+
if (!ALLOWED_CUE_KEYS.has(key)) {
|
|
31641
|
+
issues.push({
|
|
31642
|
+
severity: "warning",
|
|
31643
|
+
code: "UNKNOWN_CUE",
|
|
31644
|
+
beatId: beat.id,
|
|
31645
|
+
message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
|
|
31646
|
+
});
|
|
31647
|
+
continue;
|
|
31648
|
+
}
|
|
31649
|
+
if (key === "characters") {
|
|
31650
|
+
const validShape = typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
31651
|
+
if (!validShape) {
|
|
31652
|
+
issues.push({
|
|
31653
|
+
severity: "error",
|
|
31654
|
+
code: "INVALID_CHARACTERS_VALUE",
|
|
31655
|
+
beatId: beat.id,
|
|
31656
|
+
message: `Beat "${beat.id}" cue "characters" must be a character name or a list of names.`
|
|
31657
|
+
});
|
|
31658
|
+
continue;
|
|
31659
|
+
}
|
|
31660
|
+
for (const name of beatCharacterNames({ characters: value })) {
|
|
31661
|
+
if (!characterPool.has(name)) {
|
|
31662
|
+
issues.push({
|
|
31663
|
+
severity: "warning",
|
|
31664
|
+
code: "UNKNOWN_CHARACTER",
|
|
31665
|
+
beatId: beat.id,
|
|
31666
|
+
message: `Beat "${beat.id}" references character "${name}" not defined in the frontmatter \`characters:\` pool.`
|
|
31667
|
+
});
|
|
31668
|
+
}
|
|
31669
|
+
}
|
|
31670
|
+
continue;
|
|
31671
|
+
}
|
|
31672
|
+
if (key === "duration") {
|
|
31673
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
31674
|
+
issues.push({
|
|
31675
|
+
severity: "error",
|
|
31676
|
+
code: "INVALID_DURATION",
|
|
31677
|
+
beatId: beat.id,
|
|
31678
|
+
message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
|
|
31679
|
+
});
|
|
31680
|
+
}
|
|
31681
|
+
continue;
|
|
31682
|
+
}
|
|
31683
|
+
if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
|
|
31684
|
+
issues.push({
|
|
31685
|
+
severity: "error",
|
|
31686
|
+
code: "INVALID_CUE_VALUE",
|
|
31687
|
+
beatId: beat.id,
|
|
31688
|
+
message: `Beat "${beat.id}" cue "${key}" must be a string.`
|
|
31689
|
+
});
|
|
31690
|
+
}
|
|
31691
|
+
}
|
|
31692
|
+
}
|
|
31693
|
+
for (const beat of parsed.beats) {
|
|
31694
|
+
if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
|
|
31695
|
+
issues.push({
|
|
31696
|
+
severity: "warning",
|
|
31697
|
+
code: "BEAT_DURATION_TOO_LONG",
|
|
31698
|
+
beatId: beat.id,
|
|
31699
|
+
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).`
|
|
31700
|
+
});
|
|
31701
|
+
}
|
|
31702
|
+
}
|
|
31703
|
+
return {
|
|
31704
|
+
ok: !issues.some((i) => i.severity === "error"),
|
|
31705
|
+
beats: parsed.beats,
|
|
31706
|
+
issues
|
|
31707
|
+
};
|
|
31708
|
+
}
|
|
31709
|
+
function getStoryboardBeat(markdown, beatId) {
|
|
31710
|
+
return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
|
|
31711
|
+
}
|
|
31712
|
+
function upsertStoryboardBeat(markdown, opts) {
|
|
31713
|
+
const beatId = normalizeBeatId(opts.beatId);
|
|
31714
|
+
const sections = splitBeatSections(markdown);
|
|
31715
|
+
const existing = sections.find((section2) => section2.id === beatId);
|
|
31716
|
+
const cues = storyboardCuesFromInput(opts);
|
|
31717
|
+
if (existing) {
|
|
31718
|
+
let next = markdown;
|
|
31719
|
+
for (const [key, value] of Object.entries(cues)) {
|
|
31720
|
+
next = setStoryboardCue(next, { beatId, key, value });
|
|
31721
|
+
}
|
|
31722
|
+
return { markdown: next, action: "updated" };
|
|
31723
|
+
}
|
|
31724
|
+
const section = buildStoryboardBeatSection({ ...opts, beatId, cues });
|
|
31725
|
+
if (sections.length > 0 && isStarterStoryboard(markdown)) {
|
|
31726
|
+
const global2 = markdown.slice(0, sections[0].start).trimEnd();
|
|
31727
|
+
return {
|
|
31728
|
+
markdown: `${global2}
|
|
31729
|
+
|
|
31730
|
+
${section}`,
|
|
31731
|
+
action: "replaced-starter"
|
|
31732
|
+
};
|
|
31733
|
+
}
|
|
31734
|
+
const prefix = markdown.trimEnd();
|
|
31735
|
+
return {
|
|
31736
|
+
markdown: `${prefix}${prefix ? "\n\n" : ""}${section}`,
|
|
31737
|
+
action: "appended"
|
|
31738
|
+
};
|
|
31739
|
+
}
|
|
31740
|
+
function setStoryboardCue(markdown, opts) {
|
|
31741
|
+
const section = findBeatSection(markdown, opts.beatId);
|
|
31742
|
+
if (!section) {
|
|
31743
|
+
throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
31744
|
+
}
|
|
31745
|
+
if (!ALLOWED_CUE_KEYS.has(opts.key)) {
|
|
31746
|
+
throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
|
|
31747
|
+
}
|
|
31748
|
+
const cue = readLeadingCueBlock(section.body);
|
|
31749
|
+
if (cue?.error) {
|
|
31750
|
+
throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
|
|
31751
|
+
}
|
|
31752
|
+
const cues = cue?.value ? { ...cue.value } : {};
|
|
31753
|
+
if (opts.unset) {
|
|
31754
|
+
delete cues[opts.key];
|
|
31755
|
+
} else {
|
|
31756
|
+
cues[opts.key] = normalizeCueValue(opts.key, opts.value);
|
|
31757
|
+
}
|
|
31758
|
+
const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
|
|
31759
|
+
const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
31760
|
+
const nextBody = cueBlock + remainingBody.trimStart();
|
|
31761
|
+
const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
|
|
31762
|
+
return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
|
|
31763
|
+
}
|
|
31764
|
+
function moveStoryboardBeat(markdown, opts) {
|
|
31765
|
+
if (opts.beatId === opts.afterBeatId) return markdown;
|
|
31766
|
+
const sections = splitBeatSections(markdown);
|
|
31767
|
+
const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
|
|
31768
|
+
if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
31769
|
+
const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
|
|
31770
|
+
if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
|
|
31771
|
+
const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
|
|
31772
|
+
const chunks = sections.map((section) => section.raw);
|
|
31773
|
+
const [moving] = chunks.splice(movingIndex, 1);
|
|
31774
|
+
const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
|
|
31775
|
+
chunks.splice(adjustedAfterIndex + 1, 0, moving);
|
|
31776
|
+
return global2 + normalizeBeatChunks(chunks);
|
|
31777
|
+
}
|
|
31778
|
+
function normalizeBeatChunks(chunks) {
|
|
31779
|
+
return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
|
|
31780
|
+
}
|
|
31781
|
+
function normalizeBeatId(id) {
|
|
31782
|
+
return deriveBeatId(id);
|
|
31783
|
+
}
|
|
31784
|
+
function storyboardCuesFromInput(opts) {
|
|
31785
|
+
const cues = {};
|
|
31786
|
+
if (opts.duration !== void 0) cues.duration = opts.duration;
|
|
31787
|
+
if (opts.narration && opts.narration.trim()) cues.narration = opts.narration.trim();
|
|
31788
|
+
if (opts.backdrop && opts.backdrop.trim()) cues.backdrop = opts.backdrop.trim();
|
|
31789
|
+
return cues;
|
|
31790
|
+
}
|
|
31791
|
+
function buildStoryboardBeatSection(opts) {
|
|
31792
|
+
const title = normalizeTitle(opts.title ?? humanizeBeatId(opts.beatId));
|
|
31793
|
+
const cueBlock = Object.keys(opts.cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(opts.cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
31794
|
+
const body = opts.body?.trim() || "Scene created with `vibe scene add`. Edit this body if the build flow should recompose it.";
|
|
31795
|
+
return `## Beat ${opts.beatId} - ${title}
|
|
31796
|
+
|
|
31797
|
+
${cueBlock}${body.trimEnd()}
|
|
31798
|
+
`;
|
|
31799
|
+
}
|
|
31800
|
+
function normalizeTitle(value) {
|
|
31801
|
+
return value.replace(/\s+/g, " ").trim() || "Scene";
|
|
31802
|
+
}
|
|
31803
|
+
function humanizeBeatId(id) {
|
|
31804
|
+
return id.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Scene";
|
|
31805
|
+
}
|
|
31806
|
+
function isStarterStoryboard(markdown) {
|
|
31807
|
+
const parsed = parseStoryboard(markdown);
|
|
31808
|
+
if (parsed.beats.length !== 3) return false;
|
|
31809
|
+
const ids = parsed.beats.map((beat) => beat.id);
|
|
31810
|
+
if (ids.join(",") !== "hook,proof,close") return false;
|
|
31811
|
+
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.");
|
|
31812
|
+
}
|
|
31813
|
+
function normalizeCueValue(key, value) {
|
|
31814
|
+
if (key === "duration") {
|
|
31815
|
+
const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
|
|
31816
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
31817
|
+
throw new Error("Cue duration must be a positive number.");
|
|
31818
|
+
}
|
|
31819
|
+
return n;
|
|
31820
|
+
}
|
|
31821
|
+
if (typeof value !== "string") return value;
|
|
31822
|
+
return value.trim();
|
|
31823
|
+
}
|
|
31824
|
+
function splitBeatSections(markdown) {
|
|
31825
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
31826
|
+
const headings = [];
|
|
31827
|
+
HEADING_RE2.lastIndex = 0;
|
|
31828
|
+
let match;
|
|
31829
|
+
while ((match = HEADING_RE2.exec(normalized)) !== null) {
|
|
31830
|
+
headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
|
|
31831
|
+
}
|
|
31832
|
+
return headings.map((heading, index) => {
|
|
31833
|
+
const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
|
|
31834
|
+
return {
|
|
31835
|
+
id: deriveBeatId(heading.line),
|
|
31836
|
+
heading: heading.line,
|
|
31837
|
+
start: heading.start,
|
|
31838
|
+
headingEnd: heading.end,
|
|
31839
|
+
end,
|
|
31840
|
+
raw: normalized.slice(heading.start, end),
|
|
31841
|
+
body: normalized.slice(heading.end, end).trim()
|
|
31842
|
+
};
|
|
31843
|
+
});
|
|
31844
|
+
}
|
|
31845
|
+
function findBeatSection(markdown, beatId) {
|
|
31846
|
+
return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
|
|
31847
|
+
}
|
|
31848
|
+
function readLeadingCueBlock(body) {
|
|
31849
|
+
const match = body.match(LEADING_CUE_RE);
|
|
31850
|
+
if (!match) return null;
|
|
31851
|
+
try {
|
|
31852
|
+
const parsed = (0, import_yaml5.parse)(match[2]);
|
|
31853
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31854
|
+
return { full: match[0], error: "cue block must parse to a YAML object" };
|
|
31855
|
+
}
|
|
31856
|
+
return { full: match[0], value: parsed };
|
|
31857
|
+
} catch (error) {
|
|
31858
|
+
return { full: match[0], error: error instanceof Error ? error.message : String(error) };
|
|
31859
|
+
}
|
|
31860
|
+
}
|
|
31861
|
+
var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
|
|
31862
|
+
var init_storyboard_edit = __esm({
|
|
31863
|
+
"src/commands/_shared/storyboard-edit.ts"() {
|
|
31864
|
+
"use strict";
|
|
31865
|
+
import_yaml5 = __toESM(require_dist2(), 1);
|
|
31866
|
+
init_storyboard_parse();
|
|
31867
|
+
STORYBOARD_CUE_KEYS = [
|
|
31868
|
+
"duration",
|
|
31869
|
+
"narration",
|
|
31870
|
+
"backdrop",
|
|
31871
|
+
"video",
|
|
31872
|
+
"motion",
|
|
31873
|
+
"voice",
|
|
31874
|
+
"music",
|
|
31875
|
+
"asset",
|
|
31876
|
+
"characters"
|
|
31877
|
+
];
|
|
31878
|
+
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
31879
|
+
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
31880
|
+
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
31881
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
31882
|
+
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
31883
|
+
}
|
|
31884
|
+
});
|
|
31885
|
+
|
|
31067
31886
|
// src/commands/scene.ts
|
|
31068
31887
|
import { resolve as resolve17, relative as relative6, dirname as dirname4 } from "node:path";
|
|
31069
31888
|
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile12, access as access5, copyFile } from "node:fs/promises";
|
|
@@ -31113,6 +31932,21 @@ async function loadVibeProjectConfig(projectDir) {
|
|
|
31113
31932
|
}
|
|
31114
31933
|
};
|
|
31115
31934
|
}
|
|
31935
|
+
function escapeRegExp3(value) {
|
|
31936
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31937
|
+
}
|
|
31938
|
+
function readClipStart(rootHtml, beatId) {
|
|
31939
|
+
const compositionId = sceneCompositionId(beatId);
|
|
31940
|
+
const match = rootHtml.match(
|
|
31941
|
+
new RegExp(
|
|
31942
|
+
`<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
|
|
31943
|
+
"i"
|
|
31944
|
+
)
|
|
31945
|
+
);
|
|
31946
|
+
if (!match) return void 0;
|
|
31947
|
+
const start = Number.parseFloat(match[1]);
|
|
31948
|
+
return Number.isFinite(start) ? start : void 0;
|
|
31949
|
+
}
|
|
31116
31950
|
async function resolveNarrationText(value) {
|
|
31117
31951
|
if (!value) return void 0;
|
|
31118
31952
|
const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
|
|
@@ -31139,6 +31973,7 @@ async function executeSceneAdd(opts) {
|
|
|
31139
31973
|
const id = slugifySceneName(opts.name);
|
|
31140
31974
|
const sceneRel = `compositions/scene-${id}.html`;
|
|
31141
31975
|
const scenePath = resolve17(projectDir, sceneRel);
|
|
31976
|
+
const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
|
|
31142
31977
|
const errResult = (error) => ({
|
|
31143
31978
|
success: false,
|
|
31144
31979
|
id,
|
|
@@ -31358,6 +32193,27 @@ async function executeSceneAdd(opts) {
|
|
|
31358
32193
|
duration = fallback2;
|
|
31359
32194
|
}
|
|
31360
32195
|
duration = Number(duration.toFixed(2));
|
|
32196
|
+
let storyboardPath2;
|
|
32197
|
+
let storyboardSynced = false;
|
|
32198
|
+
let storyboardAction = "skipped";
|
|
32199
|
+
const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
|
|
32200
|
+
if (shouldSyncStoryboard) {
|
|
32201
|
+
opts.onProgress?.("Syncing STORYBOARD.md...");
|
|
32202
|
+
const before = await readFile13(storyboardAbsPath, "utf-8");
|
|
32203
|
+
const synced = upsertStoryboardBeat(before, {
|
|
32204
|
+
beatId: id,
|
|
32205
|
+
title: opts.headline,
|
|
32206
|
+
duration,
|
|
32207
|
+
narration: narrationText && !opts.skipAudio ? narrationText : void 0,
|
|
32208
|
+
backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
|
|
32209
|
+
});
|
|
32210
|
+
if (synced.markdown !== before) {
|
|
32211
|
+
await writeFile12(storyboardAbsPath, synced.markdown, "utf-8");
|
|
32212
|
+
}
|
|
32213
|
+
storyboardPath2 = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
|
|
32214
|
+
storyboardSynced = true;
|
|
32215
|
+
storyboardAction = synced.action;
|
|
32216
|
+
}
|
|
31361
32217
|
opts.onProgress?.("Emitting scene HTML...");
|
|
31362
32218
|
const sceneHtml = emitSceneHtml({
|
|
31363
32219
|
id,
|
|
@@ -31376,11 +32232,24 @@ async function executeSceneAdd(opts) {
|
|
|
31376
32232
|
await mkdir7(dirname4(scenePath), { recursive: true });
|
|
31377
32233
|
await writeFile12(scenePath, sceneHtml, "utf-8");
|
|
31378
32234
|
opts.onProgress?.("Updating root composition...");
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31382
|
-
|
|
31383
|
-
|
|
32235
|
+
let start = 0;
|
|
32236
|
+
if (storyboardSynced) {
|
|
32237
|
+
const syncPlan = await syncRootComposition({
|
|
32238
|
+
projectDir,
|
|
32239
|
+
rootRel,
|
|
32240
|
+
beats: await loadProjectRootSyncBeats(projectDir)
|
|
32241
|
+
});
|
|
32242
|
+
const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
|
|
32243
|
+
if (blockingIssue) return errResult(blockingIssue.message);
|
|
32244
|
+
const rootHtmlAfterSync = await readFile13(rootPath, "utf-8");
|
|
32245
|
+
start = readClipStart(rootHtmlAfterSync, id) ?? 0;
|
|
32246
|
+
} else {
|
|
32247
|
+
start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
|
|
32248
|
+
const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
|
|
32249
|
+
const trackIndex = existingClipCount % 2 + 1;
|
|
32250
|
+
const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
|
|
32251
|
+
await writeFile12(rootPath, updated, "utf-8");
|
|
32252
|
+
}
|
|
31384
32253
|
const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
|
|
31385
32254
|
return {
|
|
31386
32255
|
success: true,
|
|
@@ -31393,6 +32262,9 @@ async function executeSceneAdd(opts) {
|
|
|
31393
32262
|
audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
|
|
31394
32263
|
imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
|
|
31395
32264
|
lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
|
|
32265
|
+
storyboardPath: storyboardPath2,
|
|
32266
|
+
storyboardSynced,
|
|
32267
|
+
storyboardAction,
|
|
31396
32268
|
transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
|
|
31397
32269
|
transcriptWordCount
|
|
31398
32270
|
};
|
|
@@ -31434,6 +32306,8 @@ var init_scene = __esm({
|
|
|
31434
32306
|
init_compose_prompts();
|
|
31435
32307
|
init_scene_submit();
|
|
31436
32308
|
init_scene_repair();
|
|
32309
|
+
init_root_sync();
|
|
32310
|
+
init_storyboard_edit();
|
|
31437
32311
|
sceneCommand = new Command("scene").description(
|
|
31438
32312
|
"Lower-level scene authoring (add, lint, list-styles). For project flow use `vibe init` / `vibe build` / `vibe render`."
|
|
31439
32313
|
).addHelpText(
|
|
@@ -31718,7 +32592,10 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31718
32592
|
).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
|
|
31719
32593
|
"--visuals <prompt>",
|
|
31720
32594
|
"Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider"
|
|
31721
|
-
).option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option(
|
|
32595
|
+
).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(
|
|
32596
|
+
"--no-storyboard",
|
|
32597
|
+
"Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
|
|
32598
|
+
).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
|
|
31722
32599
|
"--tts <provider>",
|
|
31723
32600
|
"TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
|
|
31724
32601
|
"auto"
|
|
@@ -31787,6 +32664,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31787
32664
|
kicker: options.kicker,
|
|
31788
32665
|
project: options.project,
|
|
31789
32666
|
insertInto: options.insertInto,
|
|
32667
|
+
syncStoryboard: options.storyboard !== false,
|
|
31790
32668
|
imageProvider: options.imageProvider,
|
|
31791
32669
|
tts,
|
|
31792
32670
|
audio: options.audio,
|
|
@@ -31814,6 +32692,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31814
32692
|
kicker: options.kicker,
|
|
31815
32693
|
projectDir: options.project,
|
|
31816
32694
|
insertInto: options.insertInto,
|
|
32695
|
+
syncStoryboard: options.storyboard !== false,
|
|
31817
32696
|
imageProvider: options.imageProvider,
|
|
31818
32697
|
tts,
|
|
31819
32698
|
voice: options.voice,
|
|
@@ -31853,6 +32732,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31853
32732
|
if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
|
|
31854
32733
|
if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
|
|
31855
32734
|
if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
|
|
32735
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
32736
|
+
console.log(
|
|
32737
|
+
result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
|
|
32738
|
+
result.storyboardPath,
|
|
32739
|
+
source_default.dim(`(${result.storyboardAction})`)
|
|
32740
|
+
);
|
|
32741
|
+
}
|
|
31856
32742
|
console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
|
|
31857
32743
|
console.log();
|
|
31858
32744
|
console.log(source_default.bold.cyan("Composition"));
|
|
@@ -35661,6 +36547,10 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
35661
36547
|
...review?.retryWith ?? [],
|
|
35662
36548
|
...workflow.retryWith
|
|
35663
36549
|
];
|
|
36550
|
+
const nextActions = normalizeReviewActions([
|
|
36551
|
+
...review?.nextActions ?? [],
|
|
36552
|
+
...reviewActionsFromRetryWith([...build?.retryWith ?? [], ...workflow.retryWith])
|
|
36553
|
+
]);
|
|
35664
36554
|
return {
|
|
35665
36555
|
schemaVersion: "1",
|
|
35666
36556
|
kind: "project",
|
|
@@ -35672,6 +36562,7 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
35672
36562
|
review,
|
|
35673
36563
|
jobs,
|
|
35674
36564
|
warnings,
|
|
36565
|
+
nextActions,
|
|
35675
36566
|
retryWith: unique(retryWith)
|
|
35676
36567
|
};
|
|
35677
36568
|
}
|
|
@@ -35965,9 +36856,44 @@ async function readReviewSummary(projectDir) {
|
|
|
35965
36856
|
},
|
|
35966
36857
|
sourceReports: stringArray(report.sourceReports),
|
|
35967
36858
|
updatedAt: info?.mtime.toISOString(),
|
|
36859
|
+
nextActions: reviewActionsFromUnknown(report.nextActions),
|
|
35968
36860
|
retryWith: stringArray(report.retryWith)
|
|
35969
36861
|
});
|
|
35970
36862
|
}
|
|
36863
|
+
function reviewActionsFromUnknown(value) {
|
|
36864
|
+
if (!Array.isArray(value)) return [];
|
|
36865
|
+
return value.flatMap((item) => {
|
|
36866
|
+
if (!item || typeof item !== "object") return [];
|
|
36867
|
+
const record = item;
|
|
36868
|
+
const kind = stringEnum(record.kind, ["command", "agent", "manual"]);
|
|
36869
|
+
const fixOwner = stringEnum(record.fixOwner, ["vibe", "host-agent"]);
|
|
36870
|
+
const costTier = stringEnum(record.costTier, [
|
|
36871
|
+
"free",
|
|
36872
|
+
"low",
|
|
36873
|
+
"high",
|
|
36874
|
+
"very-high",
|
|
36875
|
+
"unknown"
|
|
36876
|
+
]);
|
|
36877
|
+
if (!kind || !fixOwner || !costTier || typeof record.id !== "string" || typeof record.label !== "string" || typeof record.safeToAutoRun !== "boolean" || typeof record.requiresConfirmation !== "boolean" || typeof record.reason !== "string") {
|
|
36878
|
+
return [];
|
|
36879
|
+
}
|
|
36880
|
+
return [
|
|
36881
|
+
stripUndefined2({
|
|
36882
|
+
id: record.id,
|
|
36883
|
+
kind,
|
|
36884
|
+
label: record.label,
|
|
36885
|
+
command: typeof record.command === "string" ? record.command : void 0,
|
|
36886
|
+
agentPrompt: typeof record.agentPrompt === "string" ? record.agentPrompt : void 0,
|
|
36887
|
+
fixOwner,
|
|
36888
|
+
costTier,
|
|
36889
|
+
safeToAutoRun: record.safeToAutoRun,
|
|
36890
|
+
requiresConfirmation: record.requiresConfirmation,
|
|
36891
|
+
reason: record.reason,
|
|
36892
|
+
sourceIssueCodes: stringArray(record.sourceIssueCodes)
|
|
36893
|
+
})
|
|
36894
|
+
];
|
|
36895
|
+
});
|
|
36896
|
+
}
|
|
35971
36897
|
function summarizeJobs(records) {
|
|
35972
36898
|
return {
|
|
35973
36899
|
total: records.length,
|
|
@@ -36179,6 +37105,9 @@ function numberOr(fallback2, value) {
|
|
|
36179
37105
|
function stringArray(value) {
|
|
36180
37106
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
|
|
36181
37107
|
}
|
|
37108
|
+
function stringEnum(value, allowed) {
|
|
37109
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
37110
|
+
}
|
|
36182
37111
|
function isActiveStatus(status) {
|
|
36183
37112
|
return status === "queued" || status === "running";
|
|
36184
37113
|
}
|
|
@@ -36244,6 +37173,7 @@ var init_status_jobs = __esm({
|
|
|
36244
37173
|
init_ai_video();
|
|
36245
37174
|
init_music_status();
|
|
36246
37175
|
init_build_asset_metadata();
|
|
37176
|
+
init_review_report();
|
|
36247
37177
|
init_storyboard_parse();
|
|
36248
37178
|
LOCAL_JOB_ORPHAN_AFTER_MS = 9e4;
|
|
36249
37179
|
}
|
|
@@ -39125,10 +40055,18 @@ Examples:
|
|
|
39125
40055
|
options.ratio = "16:9";
|
|
39126
40056
|
}
|
|
39127
40057
|
if (options.dryRun) {
|
|
40058
|
+
const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
40059
|
+
durationSec: Number(options.duration) || 5,
|
|
40060
|
+
resolution: options.resolution,
|
|
40061
|
+
aspectRatio: options.ratio,
|
|
40062
|
+
fast: options.seedanceModel === "fast",
|
|
40063
|
+
hasVideoReference: Boolean(options.refVideos)
|
|
40064
|
+
}) : void 0;
|
|
39128
40065
|
outputSuccess({
|
|
39129
40066
|
command: "generate video",
|
|
39130
40067
|
startedAt,
|
|
39131
40068
|
dryRun: true,
|
|
40069
|
+
costUsd,
|
|
39132
40070
|
data: {
|
|
39133
40071
|
params: {
|
|
39134
40072
|
prompt: prompt3,
|
|
@@ -44316,208 +45254,6 @@ var init_ai_review = __esm({
|
|
|
44316
45254
|
}
|
|
44317
45255
|
});
|
|
44318
45256
|
|
|
44319
|
-
// src/commands/_shared/storyboard-edit.ts
|
|
44320
|
-
function validateStoryboardMarkdown(markdown) {
|
|
44321
|
-
const parsed = parseStoryboard(markdown);
|
|
44322
|
-
const sections = splitBeatSections(markdown);
|
|
44323
|
-
const issues = [];
|
|
44324
|
-
if (parsed.beats.length === 0) {
|
|
44325
|
-
issues.push({
|
|
44326
|
-
severity: "error",
|
|
44327
|
-
code: "NO_BEATS",
|
|
44328
|
-
message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
|
|
44329
|
-
});
|
|
44330
|
-
}
|
|
44331
|
-
const seen = /* @__PURE__ */ new Map();
|
|
44332
|
-
for (const beat of parsed.beats) {
|
|
44333
|
-
seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
|
|
44334
|
-
}
|
|
44335
|
-
for (const [id, count] of seen.entries()) {
|
|
44336
|
-
if (count > 1) {
|
|
44337
|
-
issues.push({
|
|
44338
|
-
severity: "error",
|
|
44339
|
-
code: "DUPLICATE_BEAT_ID",
|
|
44340
|
-
beatId: id,
|
|
44341
|
-
message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
|
|
44342
|
-
});
|
|
44343
|
-
}
|
|
44344
|
-
}
|
|
44345
|
-
for (const section of sections) {
|
|
44346
|
-
const cueBlock = readLeadingCueBlock(section.body);
|
|
44347
|
-
if (cueBlock?.error) {
|
|
44348
|
-
issues.push({
|
|
44349
|
-
severity: "error",
|
|
44350
|
-
code: "MALFORMED_CUE_YAML",
|
|
44351
|
-
beatId: section.id,
|
|
44352
|
-
message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
|
|
44353
|
-
});
|
|
44354
|
-
}
|
|
44355
|
-
}
|
|
44356
|
-
for (const beat of parsed.beats) {
|
|
44357
|
-
const cues = beat.cues ?? {};
|
|
44358
|
-
for (const [key, value] of Object.entries(cues)) {
|
|
44359
|
-
if (!ALLOWED_CUE_KEYS.has(key)) {
|
|
44360
|
-
issues.push({
|
|
44361
|
-
severity: "warning",
|
|
44362
|
-
code: "UNKNOWN_CUE",
|
|
44363
|
-
beatId: beat.id,
|
|
44364
|
-
message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
|
|
44365
|
-
});
|
|
44366
|
-
continue;
|
|
44367
|
-
}
|
|
44368
|
-
if (key === "duration") {
|
|
44369
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
44370
|
-
issues.push({
|
|
44371
|
-
severity: "error",
|
|
44372
|
-
code: "INVALID_DURATION",
|
|
44373
|
-
beatId: beat.id,
|
|
44374
|
-
message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
|
|
44375
|
-
});
|
|
44376
|
-
}
|
|
44377
|
-
continue;
|
|
44378
|
-
}
|
|
44379
|
-
if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
|
|
44380
|
-
issues.push({
|
|
44381
|
-
severity: "error",
|
|
44382
|
-
code: "INVALID_CUE_VALUE",
|
|
44383
|
-
beatId: beat.id,
|
|
44384
|
-
message: `Beat "${beat.id}" cue "${key}" must be a string.`
|
|
44385
|
-
});
|
|
44386
|
-
}
|
|
44387
|
-
}
|
|
44388
|
-
}
|
|
44389
|
-
for (const beat of parsed.beats) {
|
|
44390
|
-
if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
|
|
44391
|
-
issues.push({
|
|
44392
|
-
severity: "warning",
|
|
44393
|
-
code: "BEAT_DURATION_TOO_LONG",
|
|
44394
|
-
beatId: beat.id,
|
|
44395
|
-
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).`
|
|
44396
|
-
});
|
|
44397
|
-
}
|
|
44398
|
-
}
|
|
44399
|
-
return {
|
|
44400
|
-
ok: !issues.some((i) => i.severity === "error"),
|
|
44401
|
-
beats: parsed.beats,
|
|
44402
|
-
issues
|
|
44403
|
-
};
|
|
44404
|
-
}
|
|
44405
|
-
function getStoryboardBeat(markdown, beatId) {
|
|
44406
|
-
return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
|
|
44407
|
-
}
|
|
44408
|
-
function setStoryboardCue(markdown, opts) {
|
|
44409
|
-
const section = findBeatSection(markdown, opts.beatId);
|
|
44410
|
-
if (!section) {
|
|
44411
|
-
throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
44412
|
-
}
|
|
44413
|
-
if (!ALLOWED_CUE_KEYS.has(opts.key)) {
|
|
44414
|
-
throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
|
|
44415
|
-
}
|
|
44416
|
-
const cue = readLeadingCueBlock(section.body);
|
|
44417
|
-
if (cue?.error) {
|
|
44418
|
-
throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
|
|
44419
|
-
}
|
|
44420
|
-
const cues = cue?.value ? { ...cue.value } : {};
|
|
44421
|
-
if (opts.unset) {
|
|
44422
|
-
delete cues[opts.key];
|
|
44423
|
-
} else {
|
|
44424
|
-
cues[opts.key] = normalizeCueValue(opts.key, opts.value);
|
|
44425
|
-
}
|
|
44426
|
-
const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
|
|
44427
|
-
const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
44428
|
-
const nextBody = cueBlock + remainingBody.trimStart();
|
|
44429
|
-
const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
|
|
44430
|
-
return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
|
|
44431
|
-
}
|
|
44432
|
-
function moveStoryboardBeat(markdown, opts) {
|
|
44433
|
-
if (opts.beatId === opts.afterBeatId) return markdown;
|
|
44434
|
-
const sections = splitBeatSections(markdown);
|
|
44435
|
-
const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
|
|
44436
|
-
if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
44437
|
-
const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
|
|
44438
|
-
if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
|
|
44439
|
-
const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
|
|
44440
|
-
const chunks = sections.map((section) => section.raw);
|
|
44441
|
-
const [moving] = chunks.splice(movingIndex, 1);
|
|
44442
|
-
const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
|
|
44443
|
-
chunks.splice(adjustedAfterIndex + 1, 0, moving);
|
|
44444
|
-
return global2 + normalizeBeatChunks(chunks);
|
|
44445
|
-
}
|
|
44446
|
-
function normalizeBeatChunks(chunks) {
|
|
44447
|
-
return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
|
|
44448
|
-
}
|
|
44449
|
-
function normalizeCueValue(key, value) {
|
|
44450
|
-
if (key === "duration") {
|
|
44451
|
-
const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
|
|
44452
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
44453
|
-
throw new Error("Cue duration must be a positive number.");
|
|
44454
|
-
}
|
|
44455
|
-
return n;
|
|
44456
|
-
}
|
|
44457
|
-
if (typeof value !== "string") return value;
|
|
44458
|
-
return value.trim();
|
|
44459
|
-
}
|
|
44460
|
-
function splitBeatSections(markdown) {
|
|
44461
|
-
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
44462
|
-
const headings = [];
|
|
44463
|
-
HEADING_RE2.lastIndex = 0;
|
|
44464
|
-
let match;
|
|
44465
|
-
while ((match = HEADING_RE2.exec(normalized)) !== null) {
|
|
44466
|
-
headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
|
|
44467
|
-
}
|
|
44468
|
-
return headings.map((heading, index) => {
|
|
44469
|
-
const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
|
|
44470
|
-
return {
|
|
44471
|
-
id: deriveBeatId(heading.line),
|
|
44472
|
-
heading: heading.line,
|
|
44473
|
-
start: heading.start,
|
|
44474
|
-
headingEnd: heading.end,
|
|
44475
|
-
end,
|
|
44476
|
-
raw: normalized.slice(heading.start, end),
|
|
44477
|
-
body: normalized.slice(heading.end, end).trim()
|
|
44478
|
-
};
|
|
44479
|
-
});
|
|
44480
|
-
}
|
|
44481
|
-
function findBeatSection(markdown, beatId) {
|
|
44482
|
-
return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
|
|
44483
|
-
}
|
|
44484
|
-
function readLeadingCueBlock(body) {
|
|
44485
|
-
const match = body.match(LEADING_CUE_RE);
|
|
44486
|
-
if (!match) return null;
|
|
44487
|
-
try {
|
|
44488
|
-
const parsed = (0, import_yaml5.parse)(match[2]);
|
|
44489
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
44490
|
-
return { full: match[0], error: "cue block must parse to a YAML object" };
|
|
44491
|
-
}
|
|
44492
|
-
return { full: match[0], value: parsed };
|
|
44493
|
-
} catch (error) {
|
|
44494
|
-
return { full: match[0], error: error instanceof Error ? error.message : String(error) };
|
|
44495
|
-
}
|
|
44496
|
-
}
|
|
44497
|
-
var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
|
|
44498
|
-
var init_storyboard_edit = __esm({
|
|
44499
|
-
"src/commands/_shared/storyboard-edit.ts"() {
|
|
44500
|
-
"use strict";
|
|
44501
|
-
import_yaml5 = __toESM(require_dist2(), 1);
|
|
44502
|
-
init_storyboard_parse();
|
|
44503
|
-
STORYBOARD_CUE_KEYS = [
|
|
44504
|
-
"duration",
|
|
44505
|
-
"narration",
|
|
44506
|
-
"backdrop",
|
|
44507
|
-
"video",
|
|
44508
|
-
"motion",
|
|
44509
|
-
"voice",
|
|
44510
|
-
"music",
|
|
44511
|
-
"asset"
|
|
44512
|
-
];
|
|
44513
|
-
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
44514
|
-
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
44515
|
-
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
44516
|
-
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
44517
|
-
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
44518
|
-
}
|
|
44519
|
-
});
|
|
44520
|
-
|
|
44521
45257
|
// src/commands/_shared/scene-inspect.ts
|
|
44522
45258
|
import { existsSync as existsSync34 } from "node:fs";
|
|
44523
45259
|
import { readdir as readdir4, readFile as readFile29, stat as stat5 } from "node:fs/promises";
|
|
@@ -44750,7 +45486,11 @@ async function inspectProject(opts) {
|
|
|
44750
45486
|
);
|
|
44751
45487
|
}
|
|
44752
45488
|
function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
44753
|
-
const
|
|
45489
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
45490
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
45491
|
+
projectDir,
|
|
45492
|
+
retryWith: normalizedRetryWith
|
|
45493
|
+
});
|
|
44754
45494
|
const status = statusFromIssues(normalizedIssues);
|
|
44755
45495
|
return {
|
|
44756
45496
|
schemaVersion: "1",
|
|
@@ -44762,9 +45502,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
|
44762
45502
|
score: scoreIssues(normalizedIssues),
|
|
44763
45503
|
issues: normalizedIssues,
|
|
44764
45504
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
45505
|
+
nextActions: deriveNextReviewActions({
|
|
45506
|
+
issues: normalizedIssues,
|
|
45507
|
+
retryWith: normalizedRetryWith,
|
|
45508
|
+
projectDir
|
|
45509
|
+
}),
|
|
44765
45510
|
sourceReports: projectSourceReports(checks),
|
|
44766
45511
|
checks,
|
|
44767
|
-
retryWith:
|
|
45512
|
+
retryWith: normalizedRetryWith
|
|
44768
45513
|
};
|
|
44769
45514
|
}
|
|
44770
45515
|
function storyboardPlaceholderIssues(storyboard) {
|
|
@@ -45306,6 +46051,8 @@ async function inspectRender(opts) {
|
|
|
45306
46051
|
severity: "error",
|
|
45307
46052
|
code: "RENDER_NOT_FOUND",
|
|
45308
46053
|
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.",
|
|
46054
|
+
beatId: opts.beatId,
|
|
46055
|
+
scene: opts.beatId,
|
|
45309
46056
|
suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
|
|
45310
46057
|
});
|
|
45311
46058
|
retryWith.push(
|
|
@@ -45328,6 +46075,8 @@ async function inspectRender(opts) {
|
|
|
45328
46075
|
code: "EMPTY_RENDER",
|
|
45329
46076
|
message: "Rendered video file is empty.",
|
|
45330
46077
|
file: displayPath(videoPath),
|
|
46078
|
+
beatId: opts.beatId,
|
|
46079
|
+
scene: opts.beatId,
|
|
45331
46080
|
suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
|
|
45332
46081
|
});
|
|
45333
46082
|
retryWith.push(
|
|
@@ -45337,6 +46086,8 @@ async function inspectRender(opts) {
|
|
|
45337
46086
|
const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
|
|
45338
46087
|
const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
|
|
45339
46088
|
if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
|
|
46089
|
+
const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
|
|
46090
|
+
checks.expectedAudio = expectedAudio;
|
|
45340
46091
|
if (!commandExists("ffprobe")) {
|
|
45341
46092
|
issues.push({
|
|
45342
46093
|
severity: "error",
|
|
@@ -45362,7 +46113,7 @@ async function inspectRender(opts) {
|
|
|
45362
46113
|
file: displayPath(videoPath)
|
|
45363
46114
|
});
|
|
45364
46115
|
}
|
|
45365
|
-
if (!audioStream) {
|
|
46116
|
+
if (!audioStream && expectedAudio) {
|
|
45366
46117
|
issues.push({
|
|
45367
46118
|
severity: "warning",
|
|
45368
46119
|
code: "NO_AUDIO_STREAM",
|
|
@@ -45567,8 +46318,86 @@ async function inspectRender(opts) {
|
|
|
45567
46318
|
);
|
|
45568
46319
|
return maybeWriteRenderReport(projectDir, opts, result);
|
|
45569
46320
|
}
|
|
46321
|
+
async function renderExpectsAudio(projectDir, beatId) {
|
|
46322
|
+
const root = resolve43(projectDir);
|
|
46323
|
+
const rootHtml = await readTextIfExists(join19(root, "index.html"));
|
|
46324
|
+
if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
|
|
46325
|
+
const buildReport = await readJsonIfExists(join19(root, "build-report.json"));
|
|
46326
|
+
if (buildReportExpectsAudio(buildReport, beatId)) return true;
|
|
46327
|
+
const storyboard = await readTextIfExists(join19(root, "STORYBOARD.md"));
|
|
46328
|
+
if (!storyboard) return false;
|
|
46329
|
+
try {
|
|
46330
|
+
const parsed = parseStoryboard(storyboard);
|
|
46331
|
+
const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
|
|
46332
|
+
return beats.some(
|
|
46333
|
+
(beat) => [
|
|
46334
|
+
`assets/narration-${beat.id}.mp3`,
|
|
46335
|
+
`assets/narration-${beat.id}.wav`,
|
|
46336
|
+
`assets/music-${beat.id}.mp3`,
|
|
46337
|
+
`assets/music-${beat.id}.wav`
|
|
46338
|
+
].some((asset) => existsSync35(join19(root, asset)))
|
|
46339
|
+
);
|
|
46340
|
+
} catch {
|
|
46341
|
+
return false;
|
|
46342
|
+
}
|
|
46343
|
+
}
|
|
46344
|
+
function rootHtmlExpectsAudio(html, beatId) {
|
|
46345
|
+
const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
|
|
46346
|
+
if (!beatId) return audioTags.length > 0;
|
|
46347
|
+
const escaped = escapeRegExp4(beatId);
|
|
46348
|
+
const beatAudioRe = new RegExp(
|
|
46349
|
+
`(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
|
|
46350
|
+
"i"
|
|
46351
|
+
);
|
|
46352
|
+
return audioTags.some((tag) => beatAudioRe.test(tag));
|
|
46353
|
+
}
|
|
46354
|
+
function buildReportExpectsAudio(report, beatId) {
|
|
46355
|
+
if (!report || typeof report !== "object") return false;
|
|
46356
|
+
const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
|
|
46357
|
+
const selected = beatId === void 0 ? beats : beats.filter((beat) => {
|
|
46358
|
+
if (!beat || typeof beat !== "object") return false;
|
|
46359
|
+
return beat.id === beatId;
|
|
46360
|
+
});
|
|
46361
|
+
return selected.some((beat) => beatReportExpectsAudio(beat));
|
|
46362
|
+
}
|
|
46363
|
+
function beatReportExpectsAudio(beat) {
|
|
46364
|
+
if (!beat || typeof beat !== "object") return false;
|
|
46365
|
+
const record = beat;
|
|
46366
|
+
if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
|
|
46367
|
+
if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
|
|
46368
|
+
if (parseOptionalNumber(record.narrationDurationSec) !== void 0) return true;
|
|
46369
|
+
const narration = record.narration;
|
|
46370
|
+
if (narration && typeof narration === "object") {
|
|
46371
|
+
const n = narration;
|
|
46372
|
+
if (typeof n.path === "string" && n.path.length > 0) return true;
|
|
46373
|
+
if (parseOptionalNumber(n.durationSec) !== void 0) return true;
|
|
46374
|
+
}
|
|
46375
|
+
const music = record.music;
|
|
46376
|
+
if (music && typeof music === "object") {
|
|
46377
|
+
const m = music;
|
|
46378
|
+
if (typeof m.path === "string" && m.path.length > 0) return true;
|
|
46379
|
+
if (parseOptionalNumber(m.durationSec) !== void 0) return true;
|
|
46380
|
+
}
|
|
46381
|
+
return false;
|
|
46382
|
+
}
|
|
46383
|
+
async function readJsonIfExists(path2) {
|
|
46384
|
+
const text = await readTextIfExists(path2);
|
|
46385
|
+
if (!text) return null;
|
|
46386
|
+
try {
|
|
46387
|
+
return JSON.parse(text);
|
|
46388
|
+
} catch {
|
|
46389
|
+
return null;
|
|
46390
|
+
}
|
|
46391
|
+
}
|
|
46392
|
+
function escapeRegExp4(value) {
|
|
46393
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46394
|
+
}
|
|
45570
46395
|
function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
|
|
45571
|
-
const
|
|
46396
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
46397
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
46398
|
+
projectDir,
|
|
46399
|
+
retryWith: normalizedRetryWith
|
|
46400
|
+
});
|
|
45572
46401
|
const status = statusFromIssues(normalizedIssues);
|
|
45573
46402
|
return {
|
|
45574
46403
|
schemaVersion: "1",
|
|
@@ -45581,9 +46410,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
|
|
|
45581
46410
|
score,
|
|
45582
46411
|
issues: normalizedIssues,
|
|
45583
46412
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
46413
|
+
nextActions: deriveNextReviewActions({
|
|
46414
|
+
issues: normalizedIssues,
|
|
46415
|
+
retryWith: normalizedRetryWith,
|
|
46416
|
+
projectDir
|
|
46417
|
+
}),
|
|
45584
46418
|
sourceReports: renderSourceReports(projectDir, videoPath, checks),
|
|
45585
46419
|
checks,
|
|
45586
|
-
retryWith:
|
|
46420
|
+
retryWith: normalizedRetryWith
|
|
45587
46421
|
};
|
|
45588
46422
|
}
|
|
45589
46423
|
async function maybeWriteRenderReport(projectDir, opts, result) {
|
|
@@ -59587,6 +60421,7 @@ var init_build_asset_reference = __esm({
|
|
|
59587
60421
|
EXTENSIONS = {
|
|
59588
60422
|
narration: [".mp3", ".wav", ".m4a"],
|
|
59589
60423
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60424
|
+
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
59590
60425
|
video: [".mp4", ".mov", ".webm"],
|
|
59591
60426
|
music: [".mp3", ".wav", ".m4a"]
|
|
59592
60427
|
};
|
|
@@ -59624,6 +60459,16 @@ function backdropCacheDescriptor(opts) {
|
|
|
59624
60459
|
ext: "png"
|
|
59625
60460
|
});
|
|
59626
60461
|
}
|
|
60462
|
+
function characterCacheDescriptor(opts) {
|
|
60463
|
+
return cacheAssetDescriptor("character", {
|
|
60464
|
+
name: opts.name,
|
|
60465
|
+
cue: opts.cue,
|
|
60466
|
+
provider: opts.provider,
|
|
60467
|
+
quality: opts.quality,
|
|
60468
|
+
size: opts.size,
|
|
60469
|
+
ext: "png"
|
|
60470
|
+
});
|
|
60471
|
+
}
|
|
59627
60472
|
function imageRatioForSize(size) {
|
|
59628
60473
|
switch (size) {
|
|
59629
60474
|
case "1024x1024":
|
|
@@ -59643,6 +60488,8 @@ function videoCacheDescriptor(opts) {
|
|
|
59643
60488
|
provider: opts.provider,
|
|
59644
60489
|
duration: normalizeVideoDuration(opts.duration),
|
|
59645
60490
|
ratio: "16:9",
|
|
60491
|
+
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
60492
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
59646
60493
|
ext: "mp4"
|
|
59647
60494
|
});
|
|
59648
60495
|
}
|
|
@@ -59870,7 +60717,11 @@ async function createBuildPlan(opts) {
|
|
|
59870
60717
|
reference: videoReference,
|
|
59871
60718
|
projectDir,
|
|
59872
60719
|
force: opts.force,
|
|
59873
|
-
cost:
|
|
60720
|
+
cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
|
|
60721
|
+
durationSec: normalizeVideoDuration(beat.duration),
|
|
60722
|
+
resolution: "720p",
|
|
60723
|
+
aspectRatio: "16:9"
|
|
60724
|
+
}) : VIDEO_COST_USD,
|
|
59874
60725
|
active: includeAssets
|
|
59875
60726
|
}) : null;
|
|
59876
60727
|
const music = musicCue && !opts.skipMusic ? assetPlan({
|
|
@@ -59981,6 +60832,21 @@ async function createBuildPlan(opts) {
|
|
|
59981
60832
|
retryWith.push(skipCommand);
|
|
59982
60833
|
}
|
|
59983
60834
|
}
|
|
60835
|
+
if (validationOk && includeAssets && !opts.skipVideo) {
|
|
60836
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
60837
|
+
for (const beat of sourceBeats) {
|
|
60838
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
60839
|
+
}
|
|
60840
|
+
if (referenced.size > 0) {
|
|
60841
|
+
const toGenerate = resolveCharacters(parsed.frontmatter).filter(
|
|
60842
|
+
(c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync47(join23(projectDir, `assets/character-${c.name}.png`)))
|
|
60843
|
+
);
|
|
60844
|
+
if (toGenerate.length > 0) {
|
|
60845
|
+
estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
|
|
60846
|
+
missing.add("assets");
|
|
60847
|
+
}
|
|
60848
|
+
}
|
|
60849
|
+
}
|
|
59984
60850
|
return finalizeBuildPlan({
|
|
59985
60851
|
projectDir,
|
|
59986
60852
|
config: config2,
|
|
@@ -60345,6 +61211,7 @@ var init_build_plan = __esm({
|
|
|
60345
61211
|
init_schema();
|
|
60346
61212
|
init_build_asset_reference();
|
|
60347
61213
|
init_build_cache();
|
|
61214
|
+
init_dist2();
|
|
60348
61215
|
init_build_asset_metadata();
|
|
60349
61216
|
init_build_backdrop_prompt();
|
|
60350
61217
|
init_composer_resolve();
|
|
@@ -61172,7 +62039,7 @@ __export(scene_build_exports, {
|
|
|
61172
62039
|
executeSceneBuild: () => executeSceneBuild,
|
|
61173
62040
|
resolveSceneBuildMode: () => resolveSceneBuildMode
|
|
61174
62041
|
});
|
|
61175
|
-
import { existsSync as existsSync49 } from "node:fs";
|
|
62042
|
+
import { existsSync as existsSync49, readFileSync as readFileSync3 } from "node:fs";
|
|
61176
62043
|
import { copyFile as copyFile5, mkdir as mkdir26, readFile as readFile46, writeFile as writeFile50 } from "node:fs/promises";
|
|
61177
62044
|
import { dirname as dirname24, join as join25, resolve as resolve65 } from "node:path";
|
|
61178
62045
|
async function executeSceneBuild(opts) {
|
|
@@ -61339,8 +62206,32 @@ async function executeSceneBuild(opts) {
|
|
|
61339
62206
|
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
61340
62207
|
let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
|
|
61341
62208
|
let pendingJobs = [];
|
|
62209
|
+
let characterCostUsd = 0;
|
|
61342
62210
|
if (shouldRunStage(selectedStage, "assets")) {
|
|
61343
62211
|
onProgress({ type: "phase-start", phase: "primitives" });
|
|
62212
|
+
const characterPaths = /* @__PURE__ */ new Map();
|
|
62213
|
+
if (!(opts.skipVideo ?? false)) {
|
|
62214
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
62215
|
+
for (const beat of activeBeats) {
|
|
62216
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
62217
|
+
}
|
|
62218
|
+
if (referenced.size > 0) {
|
|
62219
|
+
const charResult = await buildCharacters(
|
|
62220
|
+
resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
|
|
62221
|
+
{
|
|
62222
|
+
projectDir,
|
|
62223
|
+
imageProvider,
|
|
62224
|
+
imageSize: opts.imageSize ?? "1536x1024",
|
|
62225
|
+
imageQuality: opts.imageQuality ?? "hd",
|
|
62226
|
+
force: opts.force ?? false,
|
|
62227
|
+
onProgress
|
|
62228
|
+
}
|
|
62229
|
+
);
|
|
62230
|
+
for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
|
|
62231
|
+
characterCostUsd = charResult.costUsd;
|
|
62232
|
+
warnings.push(...charResult.failures);
|
|
62233
|
+
}
|
|
62234
|
+
}
|
|
61344
62235
|
const primitiveResults = await mapWithConcurrency(
|
|
61345
62236
|
activeBeats,
|
|
61346
62237
|
BEAT_PRIMITIVES_CONCURRENCY,
|
|
@@ -61358,7 +62249,8 @@ async function executeSceneBuild(opts) {
|
|
|
61358
62249
|
skipVideo: opts.skipVideo ?? false,
|
|
61359
62250
|
skipMusic: opts.skipMusic ?? false,
|
|
61360
62251
|
force: opts.force ?? false,
|
|
61361
|
-
onProgress
|
|
62252
|
+
onProgress,
|
|
62253
|
+
characterPaths
|
|
61362
62254
|
})
|
|
61363
62255
|
);
|
|
61364
62256
|
beatOutcomes = primitiveResults.map((result) => result.outcome);
|
|
@@ -61367,10 +62259,7 @@ async function executeSceneBuild(opts) {
|
|
|
61367
62259
|
(beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
|
|
61368
62260
|
);
|
|
61369
62261
|
stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
|
|
61370
|
-
stageReports.assets.costUsd = estimateActualAssetCost(
|
|
61371
|
-
beatOutcomes,
|
|
61372
|
-
opts.imageQuality ?? "hd"
|
|
61373
|
-
);
|
|
62262
|
+
stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
|
|
61374
62263
|
stageReports.assets.retryWith = pendingJobs.map(
|
|
61375
62264
|
(job) => `vibe status job ${job.id} --project ${projectDir} --json`
|
|
61376
62265
|
);
|
|
@@ -61535,6 +62424,18 @@ async function executeSceneBuild(opts) {
|
|
|
61535
62424
|
stageReports.compose.status = "skipped";
|
|
61536
62425
|
}
|
|
61537
62426
|
if (stageReports.compose.status === "done") {
|
|
62427
|
+
for (const beat of activeBeats) {
|
|
62428
|
+
const videoRel = `assets/video-${beat.id}.mp4`;
|
|
62429
|
+
const compositionAbs = join25(projectDir, "compositions", `scene-${beat.id}.html`);
|
|
62430
|
+
if (!existsSync49(join25(projectDir, videoRel)) || !existsSync49(compositionAbs)) continue;
|
|
62431
|
+
const html = readFileSync3(compositionAbs, "utf-8");
|
|
62432
|
+
if (!html.includes(videoRel)) {
|
|
62433
|
+
warnings.push(
|
|
62434
|
+
`Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
|
|
62435
|
+
);
|
|
62436
|
+
retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
|
|
62437
|
+
}
|
|
62438
|
+
}
|
|
61538
62439
|
const repair = await runBuildSceneRepair(projectDir, "compose", false);
|
|
61539
62440
|
sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
|
|
61540
62441
|
applySceneRepairToStage(stageReports.compose, repair);
|
|
@@ -61955,6 +62856,120 @@ async function dispatchNarration(beat, ctx) {
|
|
|
61955
62856
|
freshness: "fresh"
|
|
61956
62857
|
};
|
|
61957
62858
|
}
|
|
62859
|
+
function characterSheetPrompt(name, description) {
|
|
62860
|
+
return [
|
|
62861
|
+
`Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
|
|
62862
|
+
"Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
|
|
62863
|
+
"with identical outfit, hairstyle, hair color, and body proportions across all three views.",
|
|
62864
|
+
"Plain light-grey studio background, even neutral lighting, no props.",
|
|
62865
|
+
"Photorealistic digital character, professional concept-art character sheet layout."
|
|
62866
|
+
].join(" ");
|
|
62867
|
+
}
|
|
62868
|
+
async function buildCharacters(characters, ctx) {
|
|
62869
|
+
const paths = /* @__PURE__ */ new Map();
|
|
62870
|
+
const failures = [];
|
|
62871
|
+
let costUsd = 0;
|
|
62872
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
62873
|
+
for (const character of characters) {
|
|
62874
|
+
if (character.imagePath) {
|
|
62875
|
+
if (existsSync49(join25(ctx.projectDir, character.imagePath))) {
|
|
62876
|
+
paths.set(character.name, character.imagePath);
|
|
62877
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
|
|
62878
|
+
} else {
|
|
62879
|
+
const error = `character "${character.name}" image not found: ${character.imagePath}`;
|
|
62880
|
+
failures.push(error);
|
|
62881
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error });
|
|
62882
|
+
}
|
|
62883
|
+
continue;
|
|
62884
|
+
}
|
|
62885
|
+
if (!character.prompt) continue;
|
|
62886
|
+
const prompt3 = characterSheetPrompt(character.name, character.prompt);
|
|
62887
|
+
const rel = `assets/character-${character.name}.png`;
|
|
62888
|
+
const abs = join25(ctx.projectDir, rel);
|
|
62889
|
+
const metadataOptions = { quality: ctx.imageQuality, size };
|
|
62890
|
+
const cache = characterCacheDescriptor({
|
|
62891
|
+
name: character.name,
|
|
62892
|
+
cue: prompt3,
|
|
62893
|
+
provider: ctx.imageProvider,
|
|
62894
|
+
quality: ctx.imageQuality,
|
|
62895
|
+
size
|
|
62896
|
+
});
|
|
62897
|
+
if (existsSync49(abs) && !ctx.force) {
|
|
62898
|
+
const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
|
|
62899
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
62900
|
+
projectDir: ctx.projectDir,
|
|
62901
|
+
kind: "character",
|
|
62902
|
+
beatId: character.name,
|
|
62903
|
+
cue: prompt3,
|
|
62904
|
+
provider: ctx.imageProvider,
|
|
62905
|
+
options: metadataOptions,
|
|
62906
|
+
cacheKey: cache.key
|
|
62907
|
+
})) {
|
|
62908
|
+
paths.set(character.name, rel);
|
|
62909
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
62910
|
+
continue;
|
|
62911
|
+
}
|
|
62912
|
+
}
|
|
62913
|
+
const cacheAbs = join25(ctx.projectDir, cache.path);
|
|
62914
|
+
if (existsSync49(cacheAbs) && !ctx.force) {
|
|
62915
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
62916
|
+
await copyFile5(cacheAbs, abs);
|
|
62917
|
+
await writeAssetMetadata({
|
|
62918
|
+
projectDir: ctx.projectDir,
|
|
62919
|
+
kind: "character",
|
|
62920
|
+
beatId: character.name,
|
|
62921
|
+
cue: prompt3,
|
|
62922
|
+
provider: ctx.imageProvider,
|
|
62923
|
+
options: metadataOptions,
|
|
62924
|
+
cacheKey: cache.key,
|
|
62925
|
+
canonicalPath: rel,
|
|
62926
|
+
cachePath: cache.path
|
|
62927
|
+
});
|
|
62928
|
+
paths.set(character.name, rel);
|
|
62929
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
62930
|
+
continue;
|
|
62931
|
+
}
|
|
62932
|
+
const generated = await generateBackdropImage(
|
|
62933
|
+
prompt3,
|
|
62934
|
+
{
|
|
62935
|
+
projectDir: ctx.projectDir,
|
|
62936
|
+
imageProvider: ctx.imageProvider,
|
|
62937
|
+
imageSize: size,
|
|
62938
|
+
imageQuality: ctx.imageQuality
|
|
62939
|
+
},
|
|
62940
|
+
imageRatioForSize(size)
|
|
62941
|
+
);
|
|
62942
|
+
if (!generated.success) {
|
|
62943
|
+
failures.push(`character "${character.name}": ${generated.error}`);
|
|
62944
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
|
|
62945
|
+
continue;
|
|
62946
|
+
}
|
|
62947
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
62948
|
+
await writeFile50(abs, generated.buffer);
|
|
62949
|
+
await mkdir26(dirname24(cacheAbs), { recursive: true });
|
|
62950
|
+
await writeFile50(cacheAbs, generated.buffer);
|
|
62951
|
+
await writeAssetMetadata({
|
|
62952
|
+
projectDir: ctx.projectDir,
|
|
62953
|
+
kind: "character",
|
|
62954
|
+
beatId: character.name,
|
|
62955
|
+
cue: prompt3,
|
|
62956
|
+
provider: ctx.imageProvider,
|
|
62957
|
+
options: metadataOptions,
|
|
62958
|
+
cacheKey: cache.key,
|
|
62959
|
+
canonicalPath: rel,
|
|
62960
|
+
cachePath: cache.path
|
|
62961
|
+
});
|
|
62962
|
+
costUsd += backdropCostUsd(ctx.imageQuality);
|
|
62963
|
+
paths.set(character.name, rel);
|
|
62964
|
+
ctx.onProgress({
|
|
62965
|
+
type: "character-generated",
|
|
62966
|
+
name: character.name,
|
|
62967
|
+
path: rel,
|
|
62968
|
+
provider: ctx.imageProvider
|
|
62969
|
+
});
|
|
62970
|
+
}
|
|
62971
|
+
return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
|
|
62972
|
+
}
|
|
61958
62973
|
async function dispatchBackdrop(beat, ctx) {
|
|
61959
62974
|
const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
|
|
61960
62975
|
if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
|
|
@@ -62124,13 +63139,16 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62124
63139
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
62125
63140
|
const prompt3 = stringOrUndefined4(beat.cues?.video);
|
|
62126
63141
|
if (!prompt3) return { status: "no-cue" };
|
|
63142
|
+
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
63143
|
+
const characterRefsAbs = characterRefsRel.map((p) => join25(ctx.projectDir, p));
|
|
62127
63144
|
const rel = `assets/video-${beat.id}.mp4`;
|
|
62128
63145
|
const abs = join25(ctx.projectDir, rel);
|
|
62129
63146
|
const cache = videoCacheDescriptor({
|
|
62130
63147
|
beatId: beat.id,
|
|
62131
63148
|
cue: prompt3,
|
|
62132
63149
|
provider: ctx.videoProvider,
|
|
62133
|
-
duration: normalizeVideoDuration(beat.duration)
|
|
63150
|
+
duration: normalizeVideoDuration(beat.duration),
|
|
63151
|
+
characters: characterRefsRel
|
|
62134
63152
|
});
|
|
62135
63153
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
62136
63154
|
if (existsSync49(abs) && !ctx.force) {
|
|
@@ -62190,6 +63208,7 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62190
63208
|
ratio: "16:9",
|
|
62191
63209
|
output: abs,
|
|
62192
63210
|
wait: false,
|
|
63211
|
+
refImages: characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
62193
63212
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
62194
63213
|
});
|
|
62195
63214
|
if (!result.success || !result.taskId) {
|
|
@@ -62558,7 +63577,13 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
62558
63577
|
for (const outcome of outcomes) {
|
|
62559
63578
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
62560
63579
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
62561
|
-
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending")
|
|
63580
|
+
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
63581
|
+
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
63582
|
+
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
|
63583
|
+
resolution: "720p",
|
|
63584
|
+
aspectRatio: "16:9"
|
|
63585
|
+
}) : 5;
|
|
63586
|
+
}
|
|
62562
63587
|
if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
|
|
62563
63588
|
}
|
|
62564
63589
|
return Number(cost.toFixed(2));
|
|
@@ -68455,6 +69480,9 @@ var init_scene2 = __esm({
|
|
|
68455
69480
|
kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
|
|
68456
69481
|
projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
|
|
68457
69482
|
insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
|
|
69483
|
+
syncStoryboard: external_exports.boolean().optional().describe(
|
|
69484
|
+
"Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
|
|
69485
|
+
),
|
|
68458
69486
|
imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
|
|
68459
69487
|
voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
|
|
68460
69488
|
skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
|
|
@@ -68467,7 +69495,7 @@ var init_scene2 = __esm({
|
|
|
68467
69495
|
cost: "low",
|
|
68468
69496
|
title: "Add Scene from Preset",
|
|
68469
69497
|
annotations: { readOnly: false, openWorld: true },
|
|
68470
|
-
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
|
|
69498
|
+
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.",
|
|
68471
69499
|
schema: sceneAddSchema,
|
|
68472
69500
|
async execute(args, ctx) {
|
|
68473
69501
|
const projectDir = args.projectDir ? resolve75(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
|
|
@@ -68481,6 +69509,7 @@ var init_scene2 = __esm({
|
|
|
68481
69509
|
kicker: args.kicker,
|
|
68482
69510
|
projectDir,
|
|
68483
69511
|
insertInto: args.insertInto,
|
|
69512
|
+
syncStoryboard: args.syncStoryboard,
|
|
68484
69513
|
imageProvider: args.imageProvider,
|
|
68485
69514
|
voice: args.voice,
|
|
68486
69515
|
skipAudio: args.skipAudio,
|
|
@@ -68499,6 +69528,9 @@ var init_scene2 = __esm({
|
|
|
68499
69528
|
];
|
|
68500
69529
|
if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
|
|
68501
69530
|
if (result.imagePath) lines.push(` image: ${result.imagePath}`);
|
|
69531
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
69532
|
+
lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
|
|
69533
|
+
}
|
|
68502
69534
|
return {
|
|
68503
69535
|
success: true,
|
|
68504
69536
|
data: {
|
|
@@ -68509,7 +69541,10 @@ var init_scene2 = __esm({
|
|
|
68509
69541
|
scenePath: result.scenePath,
|
|
68510
69542
|
rootPath: result.rootPath,
|
|
68511
69543
|
audioPath: result.audioPath,
|
|
68512
|
-
imagePath: result.imagePath
|
|
69544
|
+
imagePath: result.imagePath,
|
|
69545
|
+
storyboardPath: result.storyboardPath,
|
|
69546
|
+
storyboardSynced: result.storyboardSynced,
|
|
69547
|
+
storyboardAction: result.storyboardAction
|
|
68513
69548
|
},
|
|
68514
69549
|
humanLines: lines
|
|
68515
69550
|
};
|
|
@@ -73627,7 +74662,7 @@ init_source();
|
|
|
73627
74662
|
// package.json
|
|
73628
74663
|
var package_default = {
|
|
73629
74664
|
name: "@vibeframe/cli",
|
|
73630
|
-
version: "0.113.
|
|
74665
|
+
version: "0.113.2",
|
|
73631
74666
|
description: "VibeFrame CLI - agentic video workflow runtime",
|
|
73632
74667
|
private: false,
|
|
73633
74668
|
license: "MIT",
|
|
@@ -73692,7 +74727,7 @@ var package_default = {
|
|
|
73692
74727
|
lint: "eslint --ext .ts,.tsx src/",
|
|
73693
74728
|
test: "vitest run",
|
|
73694
74729
|
"test:watch": "vitest",
|
|
73695
|
-
pretest: "node build.js",
|
|
74730
|
+
pretest: "pnpm --dir ../core build && pnpm --dir ../ai-providers build && node build.js",
|
|
73696
74731
|
prepublishOnly: "node build.js"
|
|
73697
74732
|
},
|
|
73698
74733
|
dependencies: {
|
|
@@ -82210,13 +83245,13 @@ function titleizeProjectName(name) {
|
|
|
82210
83245
|
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Video";
|
|
82211
83246
|
}
|
|
82212
83247
|
function describeTheme(brief, product) {
|
|
82213
|
-
const cleaned = brief.replace(/\b\d+[- ]?second\b/gi, "").replace(new RegExp(`\\bfor\\s+${
|
|
83248
|
+
const cleaned = brief.replace(/\b\d+[- ]?second\b/gi, "").replace(new RegExp(`\\bfor\\s+${escapeRegExp5(product)}\\b`, "i"), "").replace(/\blaunch video\b/gi, "launch story").replace(/\s+/g, " ").trim();
|
|
82214
83249
|
return cleaned || "focused product story";
|
|
82215
83250
|
}
|
|
82216
83251
|
function isMountainSunriseBrief(brief) {
|
|
82217
83252
|
return /\b(mountain|ridge|sunrise|fog|peak)\b/i.test(brief);
|
|
82218
83253
|
}
|
|
82219
|
-
function
|
|
83254
|
+
function escapeRegExp5(value) {
|
|
82220
83255
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82221
83256
|
}
|
|
82222
83257
|
|
|
@@ -82259,6 +83294,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
|
|
|
82259
83294
|
configKind: "codex-toml",
|
|
82260
83295
|
notes: [
|
|
82261
83296
|
"Codex reads AGENTS.md and can also load project-scoped .codex/config.toml after the project is trusted.",
|
|
83297
|
+
"Use Codex /goal as the outer loop for long-running VibeFrame projects; vibe supplies JSON reports, nextActions classified by cost/safety (prefer these), retryWith as a compatibility fallback, and repair commands.",
|
|
82262
83298
|
"Keep provider/auth keys in VibeFrame config or environment, not in project-local Codex config."
|
|
82263
83299
|
]
|
|
82264
83300
|
},
|
|
@@ -82271,6 +83307,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
|
|
|
82271
83307
|
configKind: "mcp-json",
|
|
82272
83308
|
notes: [
|
|
82273
83309
|
"Claude Code can drive vibe directly through shell plus AGENTS.md/CLAUDE.md.",
|
|
83310
|
+
"Use Claude Code /goal as the outer loop for long-running VibeFrame projects; vibe supplies JSON reports, nextActions classified by cost/safety (prefer these), retryWith as a compatibility fallback, and repair commands.",
|
|
82274
83311
|
"The MCP entry is optional and gives Claude Code a typed tool surface."
|
|
82275
83312
|
]
|
|
82276
83313
|
},
|
|
@@ -82345,7 +83382,15 @@ async function planHostSetup(host, projectDir, opts) {
|
|
|
82345
83382
|
status: opts.write ? "would-merge" : "would-write",
|
|
82346
83383
|
reason: opts.write ? "dry run" : "snippet only; pass --write to modify"
|
|
82347
83384
|
});
|
|
82348
|
-
return {
|
|
83385
|
+
return {
|
|
83386
|
+
host,
|
|
83387
|
+
snippet,
|
|
83388
|
+
command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
|
|
83389
|
+
configPath,
|
|
83390
|
+
files,
|
|
83391
|
+
warnings,
|
|
83392
|
+
nextSteps
|
|
83393
|
+
};
|
|
82349
83394
|
}
|
|
82350
83395
|
try {
|
|
82351
83396
|
const action = host.configKind === "codex-toml" ? await mergeCodexToml(configPath, { force: opts.force === true }) : await mergeMcpJson(configPath, {
|
|
@@ -82357,7 +83402,15 @@ async function planHostSetup(host, projectDir, opts) {
|
|
|
82357
83402
|
} catch (error) {
|
|
82358
83403
|
warnings.push(error instanceof Error ? error.message : String(error));
|
|
82359
83404
|
}
|
|
82360
|
-
return {
|
|
83405
|
+
return {
|
|
83406
|
+
host,
|
|
83407
|
+
snippet,
|
|
83408
|
+
command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
|
|
83409
|
+
configPath,
|
|
83410
|
+
files,
|
|
83411
|
+
warnings,
|
|
83412
|
+
nextSteps
|
|
83413
|
+
};
|
|
82361
83414
|
}
|
|
82362
83415
|
async function inspectHost(host, projectDir) {
|
|
82363
83416
|
const configPath = host.configPath(projectDir);
|
|
@@ -82401,7 +83454,9 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82401
83454
|
try {
|
|
82402
83455
|
root = raw.trim() ? JSON.parse(raw) : {};
|
|
82403
83456
|
} catch (error) {
|
|
82404
|
-
throw new Error(
|
|
83457
|
+
throw new Error(
|
|
83458
|
+
`Invalid JSON in ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
83459
|
+
);
|
|
82405
83460
|
}
|
|
82406
83461
|
}
|
|
82407
83462
|
const mcpServers = typeof root.mcpServers === "object" && root.mcpServers !== null && !Array.isArray(root.mcpServers) ? root.mcpServers : {};
|
|
@@ -82414,10 +83469,18 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82414
83469
|
};
|
|
82415
83470
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82416
83471
|
if (exists && opts.backup) {
|
|
82417
|
-
await writeFile45(
|
|
83472
|
+
await writeFile45(
|
|
83473
|
+
`${configPath}.bak-${timestamp()}`,
|
|
83474
|
+
await readFile39(configPath, "utf-8"),
|
|
83475
|
+
"utf-8"
|
|
83476
|
+
);
|
|
82418
83477
|
}
|
|
82419
83478
|
await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
|
|
82420
|
-
return {
|
|
83479
|
+
return {
|
|
83480
|
+
path: configPath,
|
|
83481
|
+
status: "merged",
|
|
83482
|
+
reason: "anchored existing default vibeframe MCP server"
|
|
83483
|
+
};
|
|
82421
83484
|
}
|
|
82422
83485
|
return {
|
|
82423
83486
|
path: configPath,
|
|
@@ -82428,7 +83491,11 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82428
83491
|
root.mcpServers = { ...mcpServers, vibeframe: opts.server };
|
|
82429
83492
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82430
83493
|
if (exists && opts.backup) {
|
|
82431
|
-
await writeFile45(
|
|
83494
|
+
await writeFile45(
|
|
83495
|
+
`${configPath}.bak-${timestamp()}`,
|
|
83496
|
+
await readFile39(configPath, "utf-8"),
|
|
83497
|
+
"utf-8"
|
|
83498
|
+
);
|
|
82432
83499
|
}
|
|
82433
83500
|
await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
|
|
82434
83501
|
return { path: configPath, status: exists ? "merged" : "wrote" };
|
|
@@ -82439,16 +83506,22 @@ async function mergeCodexToml(configPath, opts) {
|
|
|
82439
83506
|
${CODEX_MCP_BLOCK}${CODEX_MANAGED_END}`;
|
|
82440
83507
|
if (!exists) {
|
|
82441
83508
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82442
|
-
await writeFile45(
|
|
83509
|
+
await writeFile45(
|
|
83510
|
+
configPath,
|
|
83511
|
+
`#:schema https://developers.openai.com/codex/config-schema.json
|
|
82443
83512
|
|
|
82444
83513
|
${block}
|
|
82445
|
-
`,
|
|
83514
|
+
`,
|
|
83515
|
+
"utf-8"
|
|
83516
|
+
);
|
|
82446
83517
|
return { path: configPath, status: "wrote" };
|
|
82447
83518
|
}
|
|
82448
83519
|
const raw = await readFile39(configPath, "utf-8");
|
|
82449
83520
|
if (raw.includes(CODEX_MANAGED_START)) {
|
|
82450
83521
|
const updated = raw.replace(
|
|
82451
|
-
new RegExp(
|
|
83522
|
+
new RegExp(
|
|
83523
|
+
`${escapeRegExp6(CODEX_MANAGED_START)}[\\s\\S]*?${escapeRegExp6(CODEX_MANAGED_END)}`
|
|
83524
|
+
),
|
|
82452
83525
|
block
|
|
82453
83526
|
);
|
|
82454
83527
|
await writeFile45(configPath, ensureTrailingNewline(updated), "utf-8");
|
|
@@ -82482,7 +83555,8 @@ function hasVibeframeMcpServer(value) {
|
|
|
82482
83555
|
function readVibeframeMcpServer(value) {
|
|
82483
83556
|
if (!value || typeof value !== "object") return null;
|
|
82484
83557
|
const root = value;
|
|
82485
|
-
if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers))
|
|
83558
|
+
if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers))
|
|
83559
|
+
return null;
|
|
82486
83560
|
const server = root.mcpServers.vibeframe;
|
|
82487
83561
|
if (!server || typeof server !== "object" || Array.isArray(server)) return null;
|
|
82488
83562
|
return server;
|
|
@@ -82493,7 +83567,9 @@ function isAnchoredClaudeDesktopServer(server) {
|
|
|
82493
83567
|
return server.args[0] === "-lc" && typeof server.args[1] === "string" && server.args[1].includes("exec npx -y @vibeframe/mcp-server");
|
|
82494
83568
|
}
|
|
82495
83569
|
if (server.command === "cmd.exe" && Array.isArray(server.args)) {
|
|
82496
|
-
return server.args.some(
|
|
83570
|
+
return server.args.some(
|
|
83571
|
+
(arg) => typeof arg === "string" && arg.includes("npx -y @vibeframe/mcp-server")
|
|
83572
|
+
);
|
|
82497
83573
|
}
|
|
82498
83574
|
return false;
|
|
82499
83575
|
}
|
|
@@ -82510,13 +83586,15 @@ function hostNextSteps(host) {
|
|
|
82510
83586
|
if (host.id === "codex") {
|
|
82511
83587
|
return [
|
|
82512
83588
|
"Trust the project in Codex so .codex/config.toml is loaded.",
|
|
82513
|
-
"Run /mcp in Codex to confirm the vibeframe server is connected."
|
|
83589
|
+
"Run /mcp in Codex to confirm the vibeframe server is connected.",
|
|
83590
|
+
"Use /goal with the project AGENTS.md stop rules for long-running video builds."
|
|
82514
83591
|
];
|
|
82515
83592
|
}
|
|
82516
83593
|
if (host.id === "claude-code") {
|
|
82517
83594
|
return [
|
|
82518
83595
|
"Restart Claude Code or run /mcp to approve the project-scoped server.",
|
|
82519
|
-
"Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows."
|
|
83596
|
+
"Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows.",
|
|
83597
|
+
"Use /goal with the project AGENTS.md stop rules for long-running video builds."
|
|
82520
83598
|
];
|
|
82521
83599
|
}
|
|
82522
83600
|
if (host.id === "claude-desktop") {
|
|
@@ -82530,7 +83608,11 @@ function claudeDesktopConfigPath() {
|
|
|
82530
83608
|
return join21(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
82531
83609
|
}
|
|
82532
83610
|
if (process.platform === "win32") {
|
|
82533
|
-
return join21(
|
|
83611
|
+
return join21(
|
|
83612
|
+
process.env.APPDATA ?? join21(home, "AppData", "Roaming"),
|
|
83613
|
+
"Claude",
|
|
83614
|
+
"claude_desktop_config.json"
|
|
83615
|
+
);
|
|
82534
83616
|
}
|
|
82535
83617
|
return join21(home, ".config", "Claude", "claude_desktop_config.json");
|
|
82536
83618
|
}
|
|
@@ -82541,7 +83623,7 @@ function ensureTrailingNewline(value) {
|
|
|
82541
83623
|
return value.endsWith("\n") ? value : `${value}
|
|
82542
83624
|
`;
|
|
82543
83625
|
}
|
|
82544
|
-
function
|
|
83626
|
+
function escapeRegExp6(value) {
|
|
82545
83627
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82546
83628
|
}
|
|
82547
83629
|
function shellQuote(value) {
|
|
@@ -82733,6 +83815,45 @@ Default setup is snippet-only; use \`--write\` when you want VibeFrame to merge
|
|
|
82733
83815
|
the generated config. Keep provider keys in \`.vibeframe/config.yaml\`, user
|
|
82734
83816
|
config, or env files \u2014 not in app host config.
|
|
82735
83817
|
|
|
83818
|
+
## Native host goal loop
|
|
83819
|
+
|
|
83820
|
+
For long-running work, use Codex Goal mode, Claude Code \`/goal\`, Cursor, or
|
|
83821
|
+
another host-native goal feature as the outer loop. VibeFrame should not own a
|
|
83822
|
+
competing project-level goal runner. It provides \`--json\` commands, dry runs,
|
|
83823
|
+
budget caps, \`build-report.json\`, \`review-report.json\`, \`nextActions\`,
|
|
83824
|
+
\`safeToAutoRun\`, \`requiresConfirmation\`, \`fixOwner\`, deterministic repair,
|
|
83825
|
+
and render inspection for the host agent to decide what to do next.
|
|
83826
|
+
|
|
83827
|
+
Copy-paste Codex:
|
|
83828
|
+
|
|
83829
|
+
\`\`\`text
|
|
83830
|
+
/goal Build this VibeFrame project into renders/final.mp4. Use --json for every
|
|
83831
|
+
vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
|
|
83832
|
+
unless the user gives another budget, read build-report.json and
|
|
83833
|
+
review-report.json before deciding the next action, prefer nextActions before
|
|
83834
|
+
guessing, run only safeToAutoRun:true actions automatically, and ask before
|
|
83835
|
+
requiresConfirmation:true actions. Treat retryWith as the compatibility
|
|
83836
|
+
fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
|
|
83837
|
+
STORYBOARD.md, DESIGN.md, or composition edits.
|
|
83838
|
+
Stop only when renders/final.mp4 exists, target duration and aspect ratio are
|
|
83839
|
+
met, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
|
|
83840
|
+
remaining host-agent issue is fixed, accepted with rationale, or reported as
|
|
83841
|
+
blocked.
|
|
83842
|
+
\`\`\`
|
|
83843
|
+
|
|
83844
|
+
Copy-paste Claude Code:
|
|
83845
|
+
|
|
83846
|
+
\`\`\`text
|
|
83847
|
+
/goal Finish this VibeFrame render using Claude Code goal mode as the outer
|
|
83848
|
+
loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
|
|
83849
|
+
paid operations, budget cap via --max-cost 5, nextActions before guessing,
|
|
83850
|
+
build-report.json/review-report.json as loop state, and safeToAutoRun,
|
|
83851
|
+
requiresConfirmation, and fixOwner to decide between vibe scene repair and
|
|
83852
|
+
host-agent edits. Stop only after final MP4,
|
|
83853
|
+
target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
|
|
83854
|
+
no unresolved unacknowledged host-agent issues.
|
|
83855
|
+
\`\`\`
|
|
83856
|
+
|
|
82736
83857
|
### Composition rules (scene HTML)
|
|
82737
83858
|
|
|
82738
83859
|
\`vibe init\` installs local composition rules into your project. The
|
|
@@ -83821,7 +84942,7 @@ init_build_plan();
|
|
|
83821
84942
|
init_storyboard_parse();
|
|
83822
84943
|
init_output();
|
|
83823
84944
|
import { resolve as resolve66, join as join26 } from "node:path";
|
|
83824
|
-
import { readFileSync as
|
|
84945
|
+
import { readFileSync as readFileSync4, existsSync as existsSync50 } from "node:fs";
|
|
83825
84946
|
var VALID_MODES2 = ["agent", "batch", "auto"];
|
|
83826
84947
|
var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
|
|
83827
84948
|
var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
|
|
@@ -84188,7 +85309,7 @@ function estimateBuildCost(params) {
|
|
|
84188
85309
|
if (!existsSync50(storyboardPath2)) return null;
|
|
84189
85310
|
let beats = 0;
|
|
84190
85311
|
try {
|
|
84191
|
-
const md =
|
|
85312
|
+
const md = readFileSync4(storyboardPath2, "utf-8");
|
|
84192
85313
|
beats = parseStoryboard(md).beats.length;
|
|
84193
85314
|
} catch {
|
|
84194
85315
|
return null;
|
|
@@ -84572,6 +85693,18 @@ function printProjectStatus(result) {
|
|
|
84572
85693
|
console.log(` Build: ${result.build ? formatBuild(result.build) : source_default.dim("no build-report.json")}`);
|
|
84573
85694
|
console.log(` Review: ${result.review ? formatReview(result.review) : source_default.dim("no review-report.json")}`);
|
|
84574
85695
|
console.log(` Jobs: ${result.jobs.total} total, ${result.jobs.active} active, ${result.jobs.failed} failed`);
|
|
85696
|
+
if (result.nextActions.length > 0) {
|
|
85697
|
+
console.log();
|
|
85698
|
+
console.log(source_default.bold("Next actions"));
|
|
85699
|
+
for (const action of result.nextActions.slice(0, 5)) {
|
|
85700
|
+
console.log(formatNextAction(action));
|
|
85701
|
+
if (action.command) console.log(source_default.dim(` ${action.command}`));
|
|
85702
|
+
if (action.agentPrompt) console.log(source_default.dim(` ${action.agentPrompt}`));
|
|
85703
|
+
}
|
|
85704
|
+
if (result.nextActions.length > 5) {
|
|
85705
|
+
console.log(source_default.dim(` ... ${result.nextActions.length - 5} more`));
|
|
85706
|
+
}
|
|
85707
|
+
}
|
|
84575
85708
|
if (result.jobs.latest.length > 0) {
|
|
84576
85709
|
console.log();
|
|
84577
85710
|
for (const job of result.jobs.latest.slice(0, 5)) {
|
|
@@ -84592,6 +85725,10 @@ function formatReview(review) {
|
|
|
84592
85725
|
const status = review.status ?? "unknown";
|
|
84593
85726
|
return `${formatStatus(status)} score ${review.score ?? "-"} (${review.issueCount} issue${review.issueCount === 1 ? "" : "s"})`;
|
|
84594
85727
|
}
|
|
85728
|
+
function formatNextAction(action) {
|
|
85729
|
+
const badge = action.safeToAutoRun ? "auto" : action.requiresConfirmation ? "confirm" : action.kind;
|
|
85730
|
+
return ` ${source_default.dim(`[${badge}]`)} ${action.label}`;
|
|
85731
|
+
}
|
|
84595
85732
|
function formatStatus(status) {
|
|
84596
85733
|
if (status === "completed" || status === "pass") return source_default.green(status);
|
|
84597
85734
|
if (status === "failed" || status === "fail") return source_default.red(status);
|
|
@@ -85598,6 +86735,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85598
86735
|
product: "vibeframe",
|
|
85599
86736
|
sourceOfTruth: ["STORYBOARD.md", "DESIGN.md", "vibe.config.json"],
|
|
85600
86737
|
preferredFlow: [
|
|
86738
|
+
"native Codex Goal mode or Claude Code /goal owns persistence, iteration, and stop decisions",
|
|
86739
|
+
"vibe context/schema for command discovery",
|
|
85601
86740
|
"storyboard revise after init when the brief changes",
|
|
85602
86741
|
"storyboard validate",
|
|
85603
86742
|
"plan",
|
|
@@ -85612,6 +86751,19 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85612
86751
|
"inspect render --ai when needed",
|
|
85613
86752
|
"status project when async jobs are involved"
|
|
85614
86753
|
],
|
|
86754
|
+
nativeGoalContract: {
|
|
86755
|
+
owner: "host-native goal mode (Codex /goal, Claude Code /goal, Cursor or other host equivalent)",
|
|
86756
|
+
vibeRole: "video-specific commands, JSON reports, cost gates, deterministic repair, render inspection, nextActions, and retryWith compatibility hints",
|
|
86757
|
+
canonicalLoop: "native host goal -> vibe context/schema -> plan dry-run -> build with budget -> status polling -> inspect project -> render -> inspect render -> repair/edit using nextActions/fixOwner -> repeat",
|
|
86758
|
+
stopRules: [
|
|
86759
|
+
"final MP4 exists at the requested output path",
|
|
86760
|
+
"duration and aspect ratio match the brief or explicitly accepted constraints",
|
|
86761
|
+
"inspect render --cheap reports no errors",
|
|
86762
|
+
"any AI review score meets the goal threshold, commonly >= 90, when AI review is requested",
|
|
86763
|
+
"all fixOwner:vibe issues have been repaired or are blocked with evidence",
|
|
86764
|
+
"all fixOwner:host-agent issues are fixed, intentionally accepted with a written reason, or reported as blocked"
|
|
86765
|
+
]
|
|
86766
|
+
},
|
|
85615
86767
|
mentalModel: {
|
|
85616
86768
|
storyboard: "intent layer; edit or mutate beat cues here",
|
|
85617
86769
|
scene: "generated artifact layer; lint/repair composition HTML here"
|
|
@@ -85645,7 +86797,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85645
86797
|
reviewReportContract: {
|
|
85646
86798
|
kind: "review",
|
|
85647
86799
|
modes: ["project", "render"],
|
|
85648
|
-
fields: ["status", "score", "issues", "summary", "sourceReports", "retryWith"],
|
|
86800
|
+
fields: ["status", "score", "issues", "summary", "sourceReports", "nextActions", "retryWith"],
|
|
86801
|
+
recovery: "prefer nextActions (classified by costTier/safeToAutoRun/requiresConfirmation/fixOwner); retryWith is the compatibility fallback",
|
|
85649
86802
|
issueFixOwner: {
|
|
85650
86803
|
vibe: "deterministic CLI recovery",
|
|
85651
86804
|
"host-agent": "storyboard/design/composition edits the host agent should make"
|
|
@@ -85653,7 +86806,7 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85653
86806
|
},
|
|
85654
86807
|
machineStatusContract: {
|
|
85655
86808
|
buildReport: "kind/status/currentStage/beatSummary/jobs/sceneRepair/stageReports/warnings/retryWith plus beat timing and nested per-beat asset metadata",
|
|
85656
|
-
projectStatus: "kind/status/currentStage/beats/jobs.latest/build/review/warnings/retryWith",
|
|
86809
|
+
projectStatus: "kind/status/currentStage/beats/jobs.latest/build/review/warnings/nextActions/retryWith (nextActions is populated for every workflow state, not only when a review report exists)",
|
|
85657
86810
|
reviewSummary: "review.mode/issueCount/errorCount/warningCount/infoCount/fixOwners/sourceReports/retryWith"
|
|
85658
86811
|
},
|
|
85659
86812
|
productSurfaceContract: {
|
|
@@ -85736,12 +86889,13 @@ render. Repair failures return \`code:"SCENE_REPAIR_FAILED"\` with
|
|
|
85736
86889
|
render with \`currentStage:"assets"\` and \`code:"ASSET_REFERENCE_INVALID"\`,
|
|
85737
86890
|
\`code:"MISSING_API_KEY"\`, or \`code:"ASSET_GENERATION_FAILED"\`.
|
|
85738
86891
|
\`build-report.json\` includes \`sceneRepair\`, and \`status project\` carries
|
|
85739
|
-
review issue counts, \`fixOwners\`, \`sourceReports\`,
|
|
85740
|
-
\`
|
|
86892
|
+
review issue counts, \`fixOwners\`, \`sourceReports\`, top-level
|
|
86893
|
+
\`nextActions\` (populated for every workflow state), and \`retryWith\`.
|
|
86894
|
+
Prefer \`nextActions\` and treat \`retryWith\` as the compatibility fallback.
|
|
85741
86895
|
|
|
85742
86896
|
\`review-report.json\` is written by \`inspect project\` and \`inspect render\`
|
|
85743
86897
|
as \`kind:"review"\` with \`mode:"project"|"render"\`, \`summary\`,
|
|
85744
|
-
\`sourceReports\`, \`retryWith\`, and issue-level
|
|
86898
|
+
\`sourceReports\`, \`nextActions\`, \`retryWith\`, and issue-level
|
|
85745
86899
|
\`fixOwner:"vibe"|"host-agent"\`.
|
|
85746
86900
|
\`inspect render --cheap\` checks duration drift, audio presence, black frames,
|
|
85747
86901
|
long silence, and static-frame holds. Static or semantic beat issues are
|
|
@@ -85763,6 +86917,8 @@ Use 'vibe schema --list --surface public' for the small product surface.
|
|
|
85763
86917
|
Use 'vibe schema <command> --json' to get parameter schemas.
|
|
85764
86918
|
Use 'vibe doctor --json' to check configured API keys.
|
|
85765
86919
|
Use '--dry-run --json' before any mutating/costly operation.
|
|
86920
|
+
Use native Codex Goal mode or Claude Code /goal as the outer loop; VibeFrame provides JSON reports, retryWith, fixOwner, cost gates, and render inspection for that host loop.
|
|
86921
|
+
Stop only when the final MP4 exists, target duration/aspect ratio are met, inspect render has no errors, any AI review score meets the goal threshold when AI review is requested, and every host-agent issue is fixed, accepted with rationale, or reported blocked.
|
|
85766
86922
|
|
|
85767
86923
|
Cost tiers: Free (schema/context/doctor/detect/status/plan/storyboard validate/inspect project/render --cheap, deterministic edits) | Low (generate narration/sound-effect/music, audio transcribe, inspect media, optional AI review) | High (generate image/motion, edit image/reframe/grade/speed-ramp) | Very High (generate video, edit fill-gaps, remix highlights/auto-shorts, build with generated assets)
|
|
85768
86924
|
|