@vibeframe/cli 0.113.0 → 0.113.3
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 +25 -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 +16 -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 +41 -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 +1593 -280
- package/dist/tools/manifest/edit.d.ts +6 -6
- package/dist/tools/manifest/index.js +1671 -479
- 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,298 @@ 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
|
+
"keyframe",
|
|
31873
|
+
"motion",
|
|
31874
|
+
"voice",
|
|
31875
|
+
"music",
|
|
31876
|
+
"asset",
|
|
31877
|
+
"characters"
|
|
31878
|
+
];
|
|
31879
|
+
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
31880
|
+
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
31881
|
+
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
31882
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "keyframe", "motion", "voice", "music", "asset"]);
|
|
31883
|
+
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
31884
|
+
}
|
|
31885
|
+
});
|
|
31886
|
+
|
|
31067
31887
|
// src/commands/scene.ts
|
|
31068
31888
|
import { resolve as resolve17, relative as relative6, dirname as dirname4 } from "node:path";
|
|
31069
31889
|
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile12, access as access5, copyFile } from "node:fs/promises";
|
|
@@ -31113,6 +31933,21 @@ async function loadVibeProjectConfig(projectDir) {
|
|
|
31113
31933
|
}
|
|
31114
31934
|
};
|
|
31115
31935
|
}
|
|
31936
|
+
function escapeRegExp3(value) {
|
|
31937
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31938
|
+
}
|
|
31939
|
+
function readClipStart(rootHtml, beatId) {
|
|
31940
|
+
const compositionId = sceneCompositionId(beatId);
|
|
31941
|
+
const match = rootHtml.match(
|
|
31942
|
+
new RegExp(
|
|
31943
|
+
`<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
|
|
31944
|
+
"i"
|
|
31945
|
+
)
|
|
31946
|
+
);
|
|
31947
|
+
if (!match) return void 0;
|
|
31948
|
+
const start = Number.parseFloat(match[1]);
|
|
31949
|
+
return Number.isFinite(start) ? start : void 0;
|
|
31950
|
+
}
|
|
31116
31951
|
async function resolveNarrationText(value) {
|
|
31117
31952
|
if (!value) return void 0;
|
|
31118
31953
|
const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
|
|
@@ -31139,6 +31974,7 @@ async function executeSceneAdd(opts) {
|
|
|
31139
31974
|
const id = slugifySceneName(opts.name);
|
|
31140
31975
|
const sceneRel = `compositions/scene-${id}.html`;
|
|
31141
31976
|
const scenePath = resolve17(projectDir, sceneRel);
|
|
31977
|
+
const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
|
|
31142
31978
|
const errResult = (error) => ({
|
|
31143
31979
|
success: false,
|
|
31144
31980
|
id,
|
|
@@ -31358,6 +32194,27 @@ async function executeSceneAdd(opts) {
|
|
|
31358
32194
|
duration = fallback2;
|
|
31359
32195
|
}
|
|
31360
32196
|
duration = Number(duration.toFixed(2));
|
|
32197
|
+
let storyboardPath2;
|
|
32198
|
+
let storyboardSynced = false;
|
|
32199
|
+
let storyboardAction = "skipped";
|
|
32200
|
+
const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
|
|
32201
|
+
if (shouldSyncStoryboard) {
|
|
32202
|
+
opts.onProgress?.("Syncing STORYBOARD.md...");
|
|
32203
|
+
const before = await readFile13(storyboardAbsPath, "utf-8");
|
|
32204
|
+
const synced = upsertStoryboardBeat(before, {
|
|
32205
|
+
beatId: id,
|
|
32206
|
+
title: opts.headline,
|
|
32207
|
+
duration,
|
|
32208
|
+
narration: narrationText && !opts.skipAudio ? narrationText : void 0,
|
|
32209
|
+
backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
|
|
32210
|
+
});
|
|
32211
|
+
if (synced.markdown !== before) {
|
|
32212
|
+
await writeFile12(storyboardAbsPath, synced.markdown, "utf-8");
|
|
32213
|
+
}
|
|
32214
|
+
storyboardPath2 = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
|
|
32215
|
+
storyboardSynced = true;
|
|
32216
|
+
storyboardAction = synced.action;
|
|
32217
|
+
}
|
|
31361
32218
|
opts.onProgress?.("Emitting scene HTML...");
|
|
31362
32219
|
const sceneHtml = emitSceneHtml({
|
|
31363
32220
|
id,
|
|
@@ -31376,11 +32233,24 @@ async function executeSceneAdd(opts) {
|
|
|
31376
32233
|
await mkdir7(dirname4(scenePath), { recursive: true });
|
|
31377
32234
|
await writeFile12(scenePath, sceneHtml, "utf-8");
|
|
31378
32235
|
opts.onProgress?.("Updating root composition...");
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31382
|
-
|
|
31383
|
-
|
|
32236
|
+
let start = 0;
|
|
32237
|
+
if (storyboardSynced) {
|
|
32238
|
+
const syncPlan = await syncRootComposition({
|
|
32239
|
+
projectDir,
|
|
32240
|
+
rootRel,
|
|
32241
|
+
beats: await loadProjectRootSyncBeats(projectDir)
|
|
32242
|
+
});
|
|
32243
|
+
const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
|
|
32244
|
+
if (blockingIssue) return errResult(blockingIssue.message);
|
|
32245
|
+
const rootHtmlAfterSync = await readFile13(rootPath, "utf-8");
|
|
32246
|
+
start = readClipStart(rootHtmlAfterSync, id) ?? 0;
|
|
32247
|
+
} else {
|
|
32248
|
+
start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
|
|
32249
|
+
const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
|
|
32250
|
+
const trackIndex = existingClipCount % 2 + 1;
|
|
32251
|
+
const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
|
|
32252
|
+
await writeFile12(rootPath, updated, "utf-8");
|
|
32253
|
+
}
|
|
31384
32254
|
const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
|
|
31385
32255
|
return {
|
|
31386
32256
|
success: true,
|
|
@@ -31393,6 +32263,9 @@ async function executeSceneAdd(opts) {
|
|
|
31393
32263
|
audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
|
|
31394
32264
|
imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
|
|
31395
32265
|
lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
|
|
32266
|
+
storyboardPath: storyboardPath2,
|
|
32267
|
+
storyboardSynced,
|
|
32268
|
+
storyboardAction,
|
|
31396
32269
|
transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
|
|
31397
32270
|
transcriptWordCount
|
|
31398
32271
|
};
|
|
@@ -31434,6 +32307,8 @@ var init_scene = __esm({
|
|
|
31434
32307
|
init_compose_prompts();
|
|
31435
32308
|
init_scene_submit();
|
|
31436
32309
|
init_scene_repair();
|
|
32310
|
+
init_root_sync();
|
|
32311
|
+
init_storyboard_edit();
|
|
31437
32312
|
sceneCommand = new Command("scene").description(
|
|
31438
32313
|
"Lower-level scene authoring (add, lint, list-styles). For project flow use `vibe init` / `vibe build` / `vibe render`."
|
|
31439
32314
|
).addHelpText(
|
|
@@ -31718,7 +32593,10 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31718
32593
|
).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
|
|
31719
32594
|
"--visuals <prompt>",
|
|
31720
32595
|
"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(
|
|
32596
|
+
).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(
|
|
32597
|
+
"--no-storyboard",
|
|
32598
|
+
"Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
|
|
32599
|
+
).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
|
|
31722
32600
|
"--tts <provider>",
|
|
31723
32601
|
"TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
|
|
31724
32602
|
"auto"
|
|
@@ -31787,6 +32665,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31787
32665
|
kicker: options.kicker,
|
|
31788
32666
|
project: options.project,
|
|
31789
32667
|
insertInto: options.insertInto,
|
|
32668
|
+
syncStoryboard: options.storyboard !== false,
|
|
31790
32669
|
imageProvider: options.imageProvider,
|
|
31791
32670
|
tts,
|
|
31792
32671
|
audio: options.audio,
|
|
@@ -31814,6 +32693,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31814
32693
|
kicker: options.kicker,
|
|
31815
32694
|
projectDir: options.project,
|
|
31816
32695
|
insertInto: options.insertInto,
|
|
32696
|
+
syncStoryboard: options.storyboard !== false,
|
|
31817
32697
|
imageProvider: options.imageProvider,
|
|
31818
32698
|
tts,
|
|
31819
32699
|
voice: options.voice,
|
|
@@ -31853,6 +32733,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`
|
|
|
31853
32733
|
if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
|
|
31854
32734
|
if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
|
|
31855
32735
|
if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
|
|
32736
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
32737
|
+
console.log(
|
|
32738
|
+
result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
|
|
32739
|
+
result.storyboardPath,
|
|
32740
|
+
source_default.dim(`(${result.storyboardAction})`)
|
|
32741
|
+
);
|
|
32742
|
+
}
|
|
31856
32743
|
console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
|
|
31857
32744
|
console.log();
|
|
31858
32745
|
console.log(source_default.bold.cyan("Composition"));
|
|
@@ -35661,6 +36548,10 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
35661
36548
|
...review?.retryWith ?? [],
|
|
35662
36549
|
...workflow.retryWith
|
|
35663
36550
|
];
|
|
36551
|
+
const nextActions = normalizeReviewActions([
|
|
36552
|
+
...review?.nextActions ?? [],
|
|
36553
|
+
...reviewActionsFromRetryWith([...build?.retryWith ?? [], ...workflow.retryWith])
|
|
36554
|
+
]);
|
|
35664
36555
|
return {
|
|
35665
36556
|
schemaVersion: "1",
|
|
35666
36557
|
kind: "project",
|
|
@@ -35672,6 +36563,7 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
35672
36563
|
review,
|
|
35673
36564
|
jobs,
|
|
35674
36565
|
warnings,
|
|
36566
|
+
nextActions,
|
|
35675
36567
|
retryWith: unique(retryWith)
|
|
35676
36568
|
};
|
|
35677
36569
|
}
|
|
@@ -35965,9 +36857,44 @@ async function readReviewSummary(projectDir) {
|
|
|
35965
36857
|
},
|
|
35966
36858
|
sourceReports: stringArray(report.sourceReports),
|
|
35967
36859
|
updatedAt: info?.mtime.toISOString(),
|
|
36860
|
+
nextActions: reviewActionsFromUnknown(report.nextActions),
|
|
35968
36861
|
retryWith: stringArray(report.retryWith)
|
|
35969
36862
|
});
|
|
35970
36863
|
}
|
|
36864
|
+
function reviewActionsFromUnknown(value) {
|
|
36865
|
+
if (!Array.isArray(value)) return [];
|
|
36866
|
+
return value.flatMap((item) => {
|
|
36867
|
+
if (!item || typeof item !== "object") return [];
|
|
36868
|
+
const record = item;
|
|
36869
|
+
const kind = stringEnum(record.kind, ["command", "agent", "manual"]);
|
|
36870
|
+
const fixOwner = stringEnum(record.fixOwner, ["vibe", "host-agent"]);
|
|
36871
|
+
const costTier = stringEnum(record.costTier, [
|
|
36872
|
+
"free",
|
|
36873
|
+
"low",
|
|
36874
|
+
"high",
|
|
36875
|
+
"very-high",
|
|
36876
|
+
"unknown"
|
|
36877
|
+
]);
|
|
36878
|
+
if (!kind || !fixOwner || !costTier || typeof record.id !== "string" || typeof record.label !== "string" || typeof record.safeToAutoRun !== "boolean" || typeof record.requiresConfirmation !== "boolean" || typeof record.reason !== "string") {
|
|
36879
|
+
return [];
|
|
36880
|
+
}
|
|
36881
|
+
return [
|
|
36882
|
+
stripUndefined2({
|
|
36883
|
+
id: record.id,
|
|
36884
|
+
kind,
|
|
36885
|
+
label: record.label,
|
|
36886
|
+
command: typeof record.command === "string" ? record.command : void 0,
|
|
36887
|
+
agentPrompt: typeof record.agentPrompt === "string" ? record.agentPrompt : void 0,
|
|
36888
|
+
fixOwner,
|
|
36889
|
+
costTier,
|
|
36890
|
+
safeToAutoRun: record.safeToAutoRun,
|
|
36891
|
+
requiresConfirmation: record.requiresConfirmation,
|
|
36892
|
+
reason: record.reason,
|
|
36893
|
+
sourceIssueCodes: stringArray(record.sourceIssueCodes)
|
|
36894
|
+
})
|
|
36895
|
+
];
|
|
36896
|
+
});
|
|
36897
|
+
}
|
|
35971
36898
|
function summarizeJobs(records) {
|
|
35972
36899
|
return {
|
|
35973
36900
|
total: records.length,
|
|
@@ -36179,6 +37106,9 @@ function numberOr(fallback2, value) {
|
|
|
36179
37106
|
function stringArray(value) {
|
|
36180
37107
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
|
|
36181
37108
|
}
|
|
37109
|
+
function stringEnum(value, allowed) {
|
|
37110
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
37111
|
+
}
|
|
36182
37112
|
function isActiveStatus(status) {
|
|
36183
37113
|
return status === "queued" || status === "running";
|
|
36184
37114
|
}
|
|
@@ -36244,6 +37174,7 @@ var init_status_jobs = __esm({
|
|
|
36244
37174
|
init_ai_video();
|
|
36245
37175
|
init_music_status();
|
|
36246
37176
|
init_build_asset_metadata();
|
|
37177
|
+
init_review_report();
|
|
36247
37178
|
init_storyboard_parse();
|
|
36248
37179
|
LOCAL_JOB_ORPHAN_AFTER_MS = 9e4;
|
|
36249
37180
|
}
|
|
@@ -39125,10 +40056,18 @@ Examples:
|
|
|
39125
40056
|
options.ratio = "16:9";
|
|
39126
40057
|
}
|
|
39127
40058
|
if (options.dryRun) {
|
|
40059
|
+
const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
40060
|
+
durationSec: Number(options.duration) || 5,
|
|
40061
|
+
resolution: options.resolution,
|
|
40062
|
+
aspectRatio: options.ratio,
|
|
40063
|
+
fast: options.seedanceModel === "fast",
|
|
40064
|
+
hasVideoReference: Boolean(options.refVideos)
|
|
40065
|
+
}) : void 0;
|
|
39128
40066
|
outputSuccess({
|
|
39129
40067
|
command: "generate video",
|
|
39130
40068
|
startedAt,
|
|
39131
40069
|
dryRun: true,
|
|
40070
|
+
costUsd,
|
|
39132
40071
|
data: {
|
|
39133
40072
|
params: {
|
|
39134
40073
|
prompt: prompt3,
|
|
@@ -44316,208 +45255,6 @@ var init_ai_review = __esm({
|
|
|
44316
45255
|
}
|
|
44317
45256
|
});
|
|
44318
45257
|
|
|
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
45258
|
// src/commands/_shared/scene-inspect.ts
|
|
44522
45259
|
import { existsSync as existsSync34 } from "node:fs";
|
|
44523
45260
|
import { readdir as readdir4, readFile as readFile29, stat as stat5 } from "node:fs/promises";
|
|
@@ -44750,7 +45487,11 @@ async function inspectProject(opts) {
|
|
|
44750
45487
|
);
|
|
44751
45488
|
}
|
|
44752
45489
|
function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
44753
|
-
const
|
|
45490
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
45491
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
45492
|
+
projectDir,
|
|
45493
|
+
retryWith: normalizedRetryWith
|
|
45494
|
+
});
|
|
44754
45495
|
const status = statusFromIssues(normalizedIssues);
|
|
44755
45496
|
return {
|
|
44756
45497
|
schemaVersion: "1",
|
|
@@ -44762,9 +45503,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
|
44762
45503
|
score: scoreIssues(normalizedIssues),
|
|
44763
45504
|
issues: normalizedIssues,
|
|
44764
45505
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
45506
|
+
nextActions: deriveNextReviewActions({
|
|
45507
|
+
issues: normalizedIssues,
|
|
45508
|
+
retryWith: normalizedRetryWith,
|
|
45509
|
+
projectDir
|
|
45510
|
+
}),
|
|
44765
45511
|
sourceReports: projectSourceReports(checks),
|
|
44766
45512
|
checks,
|
|
44767
|
-
retryWith:
|
|
45513
|
+
retryWith: normalizedRetryWith
|
|
44768
45514
|
};
|
|
44769
45515
|
}
|
|
44770
45516
|
function storyboardPlaceholderIssues(storyboard) {
|
|
@@ -45306,6 +46052,8 @@ async function inspectRender(opts) {
|
|
|
45306
46052
|
severity: "error",
|
|
45307
46053
|
code: "RENDER_NOT_FOUND",
|
|
45308
46054
|
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.",
|
|
46055
|
+
beatId: opts.beatId,
|
|
46056
|
+
scene: opts.beatId,
|
|
45309
46057
|
suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
|
|
45310
46058
|
});
|
|
45311
46059
|
retryWith.push(
|
|
@@ -45328,6 +46076,8 @@ async function inspectRender(opts) {
|
|
|
45328
46076
|
code: "EMPTY_RENDER",
|
|
45329
46077
|
message: "Rendered video file is empty.",
|
|
45330
46078
|
file: displayPath(videoPath),
|
|
46079
|
+
beatId: opts.beatId,
|
|
46080
|
+
scene: opts.beatId,
|
|
45331
46081
|
suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
|
|
45332
46082
|
});
|
|
45333
46083
|
retryWith.push(
|
|
@@ -45337,6 +46087,8 @@ async function inspectRender(opts) {
|
|
|
45337
46087
|
const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
|
|
45338
46088
|
const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
|
|
45339
46089
|
if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
|
|
46090
|
+
const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
|
|
46091
|
+
checks.expectedAudio = expectedAudio;
|
|
45340
46092
|
if (!commandExists("ffprobe")) {
|
|
45341
46093
|
issues.push({
|
|
45342
46094
|
severity: "error",
|
|
@@ -45362,7 +46114,7 @@ async function inspectRender(opts) {
|
|
|
45362
46114
|
file: displayPath(videoPath)
|
|
45363
46115
|
});
|
|
45364
46116
|
}
|
|
45365
|
-
if (!audioStream) {
|
|
46117
|
+
if (!audioStream && expectedAudio) {
|
|
45366
46118
|
issues.push({
|
|
45367
46119
|
severity: "warning",
|
|
45368
46120
|
code: "NO_AUDIO_STREAM",
|
|
@@ -45567,8 +46319,86 @@ async function inspectRender(opts) {
|
|
|
45567
46319
|
);
|
|
45568
46320
|
return maybeWriteRenderReport(projectDir, opts, result);
|
|
45569
46321
|
}
|
|
46322
|
+
async function renderExpectsAudio(projectDir, beatId) {
|
|
46323
|
+
const root = resolve43(projectDir);
|
|
46324
|
+
const rootHtml = await readTextIfExists(join19(root, "index.html"));
|
|
46325
|
+
if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
|
|
46326
|
+
const buildReport = await readJsonIfExists(join19(root, "build-report.json"));
|
|
46327
|
+
if (buildReportExpectsAudio(buildReport, beatId)) return true;
|
|
46328
|
+
const storyboard = await readTextIfExists(join19(root, "STORYBOARD.md"));
|
|
46329
|
+
if (!storyboard) return false;
|
|
46330
|
+
try {
|
|
46331
|
+
const parsed = parseStoryboard(storyboard);
|
|
46332
|
+
const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
|
|
46333
|
+
return beats.some(
|
|
46334
|
+
(beat) => [
|
|
46335
|
+
`assets/narration-${beat.id}.mp3`,
|
|
46336
|
+
`assets/narration-${beat.id}.wav`,
|
|
46337
|
+
`assets/music-${beat.id}.mp3`,
|
|
46338
|
+
`assets/music-${beat.id}.wav`
|
|
46339
|
+
].some((asset) => existsSync35(join19(root, asset)))
|
|
46340
|
+
);
|
|
46341
|
+
} catch {
|
|
46342
|
+
return false;
|
|
46343
|
+
}
|
|
46344
|
+
}
|
|
46345
|
+
function rootHtmlExpectsAudio(html, beatId) {
|
|
46346
|
+
const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
|
|
46347
|
+
if (!beatId) return audioTags.length > 0;
|
|
46348
|
+
const escaped = escapeRegExp4(beatId);
|
|
46349
|
+
const beatAudioRe = new RegExp(
|
|
46350
|
+
`(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
|
|
46351
|
+
"i"
|
|
46352
|
+
);
|
|
46353
|
+
return audioTags.some((tag) => beatAudioRe.test(tag));
|
|
46354
|
+
}
|
|
46355
|
+
function buildReportExpectsAudio(report, beatId) {
|
|
46356
|
+
if (!report || typeof report !== "object") return false;
|
|
46357
|
+
const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
|
|
46358
|
+
const selected = beatId === void 0 ? beats : beats.filter((beat) => {
|
|
46359
|
+
if (!beat || typeof beat !== "object") return false;
|
|
46360
|
+
return beat.id === beatId;
|
|
46361
|
+
});
|
|
46362
|
+
return selected.some((beat) => beatReportExpectsAudio(beat));
|
|
46363
|
+
}
|
|
46364
|
+
function beatReportExpectsAudio(beat) {
|
|
46365
|
+
if (!beat || typeof beat !== "object") return false;
|
|
46366
|
+
const record = beat;
|
|
46367
|
+
if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
|
|
46368
|
+
if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
|
|
46369
|
+
if (parseOptionalNumber(record.narrationDurationSec) !== void 0) return true;
|
|
46370
|
+
const narration = record.narration;
|
|
46371
|
+
if (narration && typeof narration === "object") {
|
|
46372
|
+
const n = narration;
|
|
46373
|
+
if (typeof n.path === "string" && n.path.length > 0) return true;
|
|
46374
|
+
if (parseOptionalNumber(n.durationSec) !== void 0) return true;
|
|
46375
|
+
}
|
|
46376
|
+
const music = record.music;
|
|
46377
|
+
if (music && typeof music === "object") {
|
|
46378
|
+
const m = music;
|
|
46379
|
+
if (typeof m.path === "string" && m.path.length > 0) return true;
|
|
46380
|
+
if (parseOptionalNumber(m.durationSec) !== void 0) return true;
|
|
46381
|
+
}
|
|
46382
|
+
return false;
|
|
46383
|
+
}
|
|
46384
|
+
async function readJsonIfExists(path2) {
|
|
46385
|
+
const text = await readTextIfExists(path2);
|
|
46386
|
+
if (!text) return null;
|
|
46387
|
+
try {
|
|
46388
|
+
return JSON.parse(text);
|
|
46389
|
+
} catch {
|
|
46390
|
+
return null;
|
|
46391
|
+
}
|
|
46392
|
+
}
|
|
46393
|
+
function escapeRegExp4(value) {
|
|
46394
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46395
|
+
}
|
|
45570
46396
|
function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
|
|
45571
|
-
const
|
|
46397
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
46398
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
46399
|
+
projectDir,
|
|
46400
|
+
retryWith: normalizedRetryWith
|
|
46401
|
+
});
|
|
45572
46402
|
const status = statusFromIssues(normalizedIssues);
|
|
45573
46403
|
return {
|
|
45574
46404
|
schemaVersion: "1",
|
|
@@ -45581,9 +46411,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
|
|
|
45581
46411
|
score,
|
|
45582
46412
|
issues: normalizedIssues,
|
|
45583
46413
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
46414
|
+
nextActions: deriveNextReviewActions({
|
|
46415
|
+
issues: normalizedIssues,
|
|
46416
|
+
retryWith: normalizedRetryWith,
|
|
46417
|
+
projectDir
|
|
46418
|
+
}),
|
|
45584
46419
|
sourceReports: renderSourceReports(projectDir, videoPath, checks),
|
|
45585
46420
|
checks,
|
|
45586
|
-
retryWith:
|
|
46421
|
+
retryWith: normalizedRetryWith
|
|
45587
46422
|
};
|
|
45588
46423
|
}
|
|
45589
46424
|
async function maybeWriteRenderReport(projectDir, opts, result) {
|
|
@@ -59587,6 +60422,8 @@ var init_build_asset_reference = __esm({
|
|
|
59587
60422
|
EXTENSIONS = {
|
|
59588
60423
|
narration: [".mp3", ".wav", ".m4a"],
|
|
59589
60424
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60425
|
+
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
60426
|
+
keyframe: [".png", ".jpg", ".jpeg", ".webp"],
|
|
59590
60427
|
video: [".mp4", ".mov", ".webm"],
|
|
59591
60428
|
music: [".mp3", ".wav", ".m4a"]
|
|
59592
60429
|
};
|
|
@@ -59624,6 +60461,28 @@ function backdropCacheDescriptor(opts) {
|
|
|
59624
60461
|
ext: "png"
|
|
59625
60462
|
});
|
|
59626
60463
|
}
|
|
60464
|
+
function characterCacheDescriptor(opts) {
|
|
60465
|
+
return cacheAssetDescriptor("character", {
|
|
60466
|
+
name: opts.name,
|
|
60467
|
+
cue: opts.cue,
|
|
60468
|
+
provider: opts.provider,
|
|
60469
|
+
quality: opts.quality,
|
|
60470
|
+
size: opts.size,
|
|
60471
|
+
ext: "png"
|
|
60472
|
+
});
|
|
60473
|
+
}
|
|
60474
|
+
function keyframeCacheDescriptor(opts) {
|
|
60475
|
+
return cacheAssetDescriptor("keyframe", {
|
|
60476
|
+
beatId: opts.beatId,
|
|
60477
|
+
cue: opts.cue,
|
|
60478
|
+
provider: opts.provider,
|
|
60479
|
+
quality: opts.quality,
|
|
60480
|
+
size: opts.size,
|
|
60481
|
+
ratio: opts.ratio,
|
|
60482
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
60483
|
+
ext: "png"
|
|
60484
|
+
});
|
|
60485
|
+
}
|
|
59627
60486
|
function imageRatioForSize(size) {
|
|
59628
60487
|
switch (size) {
|
|
59629
60488
|
case "1024x1024":
|
|
@@ -59643,6 +60502,10 @@ function videoCacheDescriptor(opts) {
|
|
|
59643
60502
|
provider: opts.provider,
|
|
59644
60503
|
duration: normalizeVideoDuration(opts.duration),
|
|
59645
60504
|
ratio: "16:9",
|
|
60505
|
+
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
60506
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
60507
|
+
// Omit when absent so existing (non-keyframe) clips keep their cache key.
|
|
60508
|
+
keyframe: opts.keyframe || void 0,
|
|
59646
60509
|
ext: "mp4"
|
|
59647
60510
|
});
|
|
59648
60511
|
}
|
|
@@ -59793,6 +60656,7 @@ async function createBuildPlan(opts) {
|
|
|
59793
60656
|
const backdropPrompt = stringOrUndefined3(cue.backdrop);
|
|
59794
60657
|
const augmentedBackdropPrompt = backdropPrompt ? augmentBackdropPrompt(backdropPrompt) : null;
|
|
59795
60658
|
const videoPrompt = stringOrUndefined3(cue.video);
|
|
60659
|
+
const keyframePrompt = stringOrUndefined3(cue.keyframe);
|
|
59796
60660
|
const musicPrompt = stringOrUndefined3(cue.music);
|
|
59797
60661
|
const genericReference = resolveGenericAssetReference(projectDir, cue.asset);
|
|
59798
60662
|
const narrationReference = resolveTypedAssetReference(projectDir, "narration", cue.narration);
|
|
@@ -59801,7 +60665,7 @@ async function createBuildPlan(opts) {
|
|
|
59801
60665
|
const musicReference = resolveTypedAssetReference(projectDir, "music", cue.music) ?? (!musicPrompt && genericReference?.kind === "music" ? genericReference : null);
|
|
59802
60666
|
const narrationCue = narrationText ?? narrationReference?.raw;
|
|
59803
60667
|
const backdropCue = backdropPrompt ?? backdropReference?.raw;
|
|
59804
|
-
const videoCue = videoPrompt ?? videoReference?.raw;
|
|
60668
|
+
const videoCue = videoPrompt ?? videoReference?.raw ?? keyframePrompt;
|
|
59805
60669
|
const musicCue = musicPrompt ?? musicReference?.raw;
|
|
59806
60670
|
const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
|
|
59807
60671
|
const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
|
|
@@ -59819,11 +60683,12 @@ async function createBuildPlan(opts) {
|
|
|
59819
60683
|
size: imageSize2,
|
|
59820
60684
|
ratio: imageRatio
|
|
59821
60685
|
}) : null;
|
|
59822
|
-
const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
|
|
60686
|
+
const videoCache = (videoPrompt || keyframePrompt) && !videoReference ? videoCacheDescriptor({
|
|
59823
60687
|
beatId: beat.id,
|
|
59824
|
-
cue: videoPrompt,
|
|
60688
|
+
cue: videoPrompt ?? keyframePrompt,
|
|
59825
60689
|
provider: resolved.video.resolved,
|
|
59826
|
-
duration: beat.duration
|
|
60690
|
+
duration: beat.duration,
|
|
60691
|
+
keyframe: keyframePrompt
|
|
59827
60692
|
}) : null;
|
|
59828
60693
|
const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
|
|
59829
60694
|
beatId: beat.id,
|
|
@@ -59870,7 +60735,11 @@ async function createBuildPlan(opts) {
|
|
|
59870
60735
|
reference: videoReference,
|
|
59871
60736
|
projectDir,
|
|
59872
60737
|
force: opts.force,
|
|
59873
|
-
cost:
|
|
60738
|
+
cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
|
|
60739
|
+
durationSec: normalizeVideoDuration(beat.duration),
|
|
60740
|
+
resolution: "720p",
|
|
60741
|
+
aspectRatio: "16:9"
|
|
60742
|
+
}) : VIDEO_COST_USD,
|
|
59874
60743
|
active: includeAssets
|
|
59875
60744
|
}) : null;
|
|
59876
60745
|
const music = musicCue && !opts.skipMusic ? assetPlan({
|
|
@@ -59914,6 +60783,12 @@ async function createBuildPlan(opts) {
|
|
|
59914
60783
|
if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
59915
60784
|
noteProviderNeed(resolved.image, "Backdrop generation");
|
|
59916
60785
|
if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
|
|
60786
|
+
if (keyframePrompt && video?.willGenerate) {
|
|
60787
|
+
estimatedCostUsd += backdropCostUsd(imageQuality);
|
|
60788
|
+
providers.add(resolved.image.resolved);
|
|
60789
|
+
if (isSupportedBuildImageProvider(resolved.image.resolved))
|
|
60790
|
+
noteProviderNeed(resolved.image, "Keyframe generation");
|
|
60791
|
+
}
|
|
59917
60792
|
if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
|
|
59918
60793
|
if (!compositionExists) missing.add("compositions");
|
|
59919
60794
|
if (includeCompose && !compositionExists && mode !== "agent") {
|
|
@@ -59981,6 +60856,21 @@ async function createBuildPlan(opts) {
|
|
|
59981
60856
|
retryWith.push(skipCommand);
|
|
59982
60857
|
}
|
|
59983
60858
|
}
|
|
60859
|
+
if (validationOk && includeAssets && !opts.skipVideo) {
|
|
60860
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
60861
|
+
for (const beat of sourceBeats) {
|
|
60862
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
60863
|
+
}
|
|
60864
|
+
if (referenced.size > 0) {
|
|
60865
|
+
const toGenerate = resolveCharacters(parsed.frontmatter).filter(
|
|
60866
|
+
(c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync47(join23(projectDir, `assets/character-${c.name}.png`)))
|
|
60867
|
+
);
|
|
60868
|
+
if (toGenerate.length > 0) {
|
|
60869
|
+
estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
|
|
60870
|
+
missing.add("assets");
|
|
60871
|
+
}
|
|
60872
|
+
}
|
|
60873
|
+
}
|
|
59984
60874
|
return finalizeBuildPlan({
|
|
59985
60875
|
projectDir,
|
|
59986
60876
|
config: config2,
|
|
@@ -60345,6 +61235,7 @@ var init_build_plan = __esm({
|
|
|
60345
61235
|
init_schema();
|
|
60346
61236
|
init_build_asset_reference();
|
|
60347
61237
|
init_build_cache();
|
|
61238
|
+
init_dist2();
|
|
60348
61239
|
init_build_asset_metadata();
|
|
60349
61240
|
init_build_backdrop_prompt();
|
|
60350
61241
|
init_composer_resolve();
|
|
@@ -61172,7 +62063,7 @@ __export(scene_build_exports, {
|
|
|
61172
62063
|
executeSceneBuild: () => executeSceneBuild,
|
|
61173
62064
|
resolveSceneBuildMode: () => resolveSceneBuildMode
|
|
61174
62065
|
});
|
|
61175
|
-
import { existsSync as existsSync49 } from "node:fs";
|
|
62066
|
+
import { existsSync as existsSync49, readFileSync as readFileSync3 } from "node:fs";
|
|
61176
62067
|
import { copyFile as copyFile5, mkdir as mkdir26, readFile as readFile46, writeFile as writeFile50 } from "node:fs/promises";
|
|
61177
62068
|
import { dirname as dirname24, join as join25, resolve as resolve65 } from "node:path";
|
|
61178
62069
|
async function executeSceneBuild(opts) {
|
|
@@ -61339,8 +62230,32 @@ async function executeSceneBuild(opts) {
|
|
|
61339
62230
|
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
61340
62231
|
let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
|
|
61341
62232
|
let pendingJobs = [];
|
|
62233
|
+
let characterCostUsd = 0;
|
|
61342
62234
|
if (shouldRunStage(selectedStage, "assets")) {
|
|
61343
62235
|
onProgress({ type: "phase-start", phase: "primitives" });
|
|
62236
|
+
const characterPaths = /* @__PURE__ */ new Map();
|
|
62237
|
+
if (!(opts.skipVideo ?? false)) {
|
|
62238
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
62239
|
+
for (const beat of activeBeats) {
|
|
62240
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
62241
|
+
}
|
|
62242
|
+
if (referenced.size > 0) {
|
|
62243
|
+
const charResult = await buildCharacters(
|
|
62244
|
+
resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
|
|
62245
|
+
{
|
|
62246
|
+
projectDir,
|
|
62247
|
+
imageProvider,
|
|
62248
|
+
imageSize: opts.imageSize ?? "1536x1024",
|
|
62249
|
+
imageQuality: opts.imageQuality ?? "hd",
|
|
62250
|
+
force: opts.force ?? false,
|
|
62251
|
+
onProgress
|
|
62252
|
+
}
|
|
62253
|
+
);
|
|
62254
|
+
for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
|
|
62255
|
+
characterCostUsd = charResult.costUsd;
|
|
62256
|
+
warnings.push(...charResult.failures);
|
|
62257
|
+
}
|
|
62258
|
+
}
|
|
61344
62259
|
const primitiveResults = await mapWithConcurrency(
|
|
61345
62260
|
activeBeats,
|
|
61346
62261
|
BEAT_PRIMITIVES_CONCURRENCY,
|
|
@@ -61358,7 +62273,8 @@ async function executeSceneBuild(opts) {
|
|
|
61358
62273
|
skipVideo: opts.skipVideo ?? false,
|
|
61359
62274
|
skipMusic: opts.skipMusic ?? false,
|
|
61360
62275
|
force: opts.force ?? false,
|
|
61361
|
-
onProgress
|
|
62276
|
+
onProgress,
|
|
62277
|
+
characterPaths
|
|
61362
62278
|
})
|
|
61363
62279
|
);
|
|
61364
62280
|
beatOutcomes = primitiveResults.map((result) => result.outcome);
|
|
@@ -61367,10 +62283,7 @@ async function executeSceneBuild(opts) {
|
|
|
61367
62283
|
(beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
|
|
61368
62284
|
);
|
|
61369
62285
|
stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
|
|
61370
|
-
stageReports.assets.costUsd = estimateActualAssetCost(
|
|
61371
|
-
beatOutcomes,
|
|
61372
|
-
opts.imageQuality ?? "hd"
|
|
61373
|
-
);
|
|
62286
|
+
stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
|
|
61374
62287
|
stageReports.assets.retryWith = pendingJobs.map(
|
|
61375
62288
|
(job) => `vibe status job ${job.id} --project ${projectDir} --json`
|
|
61376
62289
|
);
|
|
@@ -61535,6 +62448,18 @@ async function executeSceneBuild(opts) {
|
|
|
61535
62448
|
stageReports.compose.status = "skipped";
|
|
61536
62449
|
}
|
|
61537
62450
|
if (stageReports.compose.status === "done") {
|
|
62451
|
+
for (const beat of activeBeats) {
|
|
62452
|
+
const videoRel = `assets/video-${beat.id}.mp4`;
|
|
62453
|
+
const compositionAbs = join25(projectDir, "compositions", `scene-${beat.id}.html`);
|
|
62454
|
+
if (!existsSync49(join25(projectDir, videoRel)) || !existsSync49(compositionAbs)) continue;
|
|
62455
|
+
const html = readFileSync3(compositionAbs, "utf-8");
|
|
62456
|
+
if (!html.includes(videoRel)) {
|
|
62457
|
+
warnings.push(
|
|
62458
|
+
`Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
|
|
62459
|
+
);
|
|
62460
|
+
retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
|
|
62461
|
+
}
|
|
62462
|
+
}
|
|
61538
62463
|
const repair = await runBuildSceneRepair(projectDir, "compose", false);
|
|
61539
62464
|
sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
|
|
61540
62465
|
applySceneRepairToStage(stageReports.compose, repair);
|
|
@@ -61769,6 +62694,8 @@ async function buildBeatPrimitives(beat, ctx) {
|
|
|
61769
62694
|
videoMetadataPath: video.metadataPath,
|
|
61770
62695
|
videoFreshness: video.freshness,
|
|
61771
62696
|
videoSourcePath: video.sourcePath,
|
|
62697
|
+
keyframeStatus: video.keyframeStatus,
|
|
62698
|
+
keyframePath: video.keyframePath,
|
|
61772
62699
|
musicStatus: music.status,
|
|
61773
62700
|
musicPath: music.path,
|
|
61774
62701
|
musicJobId: music.job?.id,
|
|
@@ -61955,6 +62882,120 @@ async function dispatchNarration(beat, ctx) {
|
|
|
61955
62882
|
freshness: "fresh"
|
|
61956
62883
|
};
|
|
61957
62884
|
}
|
|
62885
|
+
function characterSheetPrompt(name, description) {
|
|
62886
|
+
return [
|
|
62887
|
+
`Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
|
|
62888
|
+
"Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
|
|
62889
|
+
"with identical outfit, hairstyle, hair color, and body proportions across all three views.",
|
|
62890
|
+
"Plain light-grey studio background, even neutral lighting, no props.",
|
|
62891
|
+
"Photorealistic digital character, professional concept-art character sheet layout."
|
|
62892
|
+
].join(" ");
|
|
62893
|
+
}
|
|
62894
|
+
async function buildCharacters(characters, ctx) {
|
|
62895
|
+
const paths = /* @__PURE__ */ new Map();
|
|
62896
|
+
const failures = [];
|
|
62897
|
+
let costUsd = 0;
|
|
62898
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
62899
|
+
for (const character of characters) {
|
|
62900
|
+
if (character.imagePath) {
|
|
62901
|
+
if (existsSync49(join25(ctx.projectDir, character.imagePath))) {
|
|
62902
|
+
paths.set(character.name, character.imagePath);
|
|
62903
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
|
|
62904
|
+
} else {
|
|
62905
|
+
const error = `character "${character.name}" image not found: ${character.imagePath}`;
|
|
62906
|
+
failures.push(error);
|
|
62907
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error });
|
|
62908
|
+
}
|
|
62909
|
+
continue;
|
|
62910
|
+
}
|
|
62911
|
+
if (!character.prompt) continue;
|
|
62912
|
+
const prompt3 = characterSheetPrompt(character.name, character.prompt);
|
|
62913
|
+
const rel = `assets/character-${character.name}.png`;
|
|
62914
|
+
const abs = join25(ctx.projectDir, rel);
|
|
62915
|
+
const metadataOptions = { quality: ctx.imageQuality, size };
|
|
62916
|
+
const cache = characterCacheDescriptor({
|
|
62917
|
+
name: character.name,
|
|
62918
|
+
cue: prompt3,
|
|
62919
|
+
provider: ctx.imageProvider,
|
|
62920
|
+
quality: ctx.imageQuality,
|
|
62921
|
+
size
|
|
62922
|
+
});
|
|
62923
|
+
if (existsSync49(abs) && !ctx.force) {
|
|
62924
|
+
const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
|
|
62925
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
62926
|
+
projectDir: ctx.projectDir,
|
|
62927
|
+
kind: "character",
|
|
62928
|
+
beatId: character.name,
|
|
62929
|
+
cue: prompt3,
|
|
62930
|
+
provider: ctx.imageProvider,
|
|
62931
|
+
options: metadataOptions,
|
|
62932
|
+
cacheKey: cache.key
|
|
62933
|
+
})) {
|
|
62934
|
+
paths.set(character.name, rel);
|
|
62935
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
62936
|
+
continue;
|
|
62937
|
+
}
|
|
62938
|
+
}
|
|
62939
|
+
const cacheAbs = join25(ctx.projectDir, cache.path);
|
|
62940
|
+
if (existsSync49(cacheAbs) && !ctx.force) {
|
|
62941
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
62942
|
+
await copyFile5(cacheAbs, abs);
|
|
62943
|
+
await writeAssetMetadata({
|
|
62944
|
+
projectDir: ctx.projectDir,
|
|
62945
|
+
kind: "character",
|
|
62946
|
+
beatId: character.name,
|
|
62947
|
+
cue: prompt3,
|
|
62948
|
+
provider: ctx.imageProvider,
|
|
62949
|
+
options: metadataOptions,
|
|
62950
|
+
cacheKey: cache.key,
|
|
62951
|
+
canonicalPath: rel,
|
|
62952
|
+
cachePath: cache.path
|
|
62953
|
+
});
|
|
62954
|
+
paths.set(character.name, rel);
|
|
62955
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
62956
|
+
continue;
|
|
62957
|
+
}
|
|
62958
|
+
const generated = await generateBackdropImage(
|
|
62959
|
+
prompt3,
|
|
62960
|
+
{
|
|
62961
|
+
projectDir: ctx.projectDir,
|
|
62962
|
+
imageProvider: ctx.imageProvider,
|
|
62963
|
+
imageSize: size,
|
|
62964
|
+
imageQuality: ctx.imageQuality
|
|
62965
|
+
},
|
|
62966
|
+
imageRatioForSize(size)
|
|
62967
|
+
);
|
|
62968
|
+
if (!generated.success) {
|
|
62969
|
+
failures.push(`character "${character.name}": ${generated.error}`);
|
|
62970
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
|
|
62971
|
+
continue;
|
|
62972
|
+
}
|
|
62973
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
62974
|
+
await writeFile50(abs, generated.buffer);
|
|
62975
|
+
await mkdir26(dirname24(cacheAbs), { recursive: true });
|
|
62976
|
+
await writeFile50(cacheAbs, generated.buffer);
|
|
62977
|
+
await writeAssetMetadata({
|
|
62978
|
+
projectDir: ctx.projectDir,
|
|
62979
|
+
kind: "character",
|
|
62980
|
+
beatId: character.name,
|
|
62981
|
+
cue: prompt3,
|
|
62982
|
+
provider: ctx.imageProvider,
|
|
62983
|
+
options: metadataOptions,
|
|
62984
|
+
cacheKey: cache.key,
|
|
62985
|
+
canonicalPath: rel,
|
|
62986
|
+
cachePath: cache.path
|
|
62987
|
+
});
|
|
62988
|
+
costUsd += backdropCostUsd(ctx.imageQuality);
|
|
62989
|
+
paths.set(character.name, rel);
|
|
62990
|
+
ctx.onProgress({
|
|
62991
|
+
type: "character-generated",
|
|
62992
|
+
name: character.name,
|
|
62993
|
+
path: rel,
|
|
62994
|
+
provider: ctx.imageProvider
|
|
62995
|
+
});
|
|
62996
|
+
}
|
|
62997
|
+
return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
|
|
62998
|
+
}
|
|
61958
62999
|
async function dispatchBackdrop(beat, ctx) {
|
|
61959
63000
|
const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
|
|
61960
63001
|
if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
|
|
@@ -62099,6 +63140,112 @@ async function generateBackdropImage(prompt3, ctx, ratio) {
|
|
|
62099
63140
|
});
|
|
62100
63141
|
return imageBufferFromResult(result);
|
|
62101
63142
|
}
|
|
63143
|
+
async function editKeyframeImage(prompt3, sheetBuffers, ctx, ratio) {
|
|
63144
|
+
loadSceneBuildEnv(ctx.projectDir);
|
|
63145
|
+
const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
|
|
63146
|
+
const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
|
|
63147
|
+
if (!apiKey) {
|
|
63148
|
+
return {
|
|
63149
|
+
success: false,
|
|
63150
|
+
error: `${keyInfo.envVar} not set \u2014 cannot generate keyframe with ${ctx.imageProvider}`
|
|
63151
|
+
};
|
|
63152
|
+
}
|
|
63153
|
+
if (ctx.imageProvider === "openai") {
|
|
63154
|
+
const provider2 = new OpenAIImageProvider();
|
|
63155
|
+
await provider2.initialize({ apiKey });
|
|
63156
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3);
|
|
63157
|
+
return imageBufferFromResult(result2);
|
|
63158
|
+
}
|
|
63159
|
+
if (ctx.imageProvider === "gemini") {
|
|
63160
|
+
const provider2 = new GeminiProvider();
|
|
63161
|
+
await provider2.initialize({ apiKey });
|
|
63162
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3, {
|
|
63163
|
+
model: "flash",
|
|
63164
|
+
aspectRatio: ratio
|
|
63165
|
+
});
|
|
63166
|
+
return imageBufferFromResult(result2);
|
|
63167
|
+
}
|
|
63168
|
+
const provider = new GrokProvider();
|
|
63169
|
+
await provider.initialize({ apiKey });
|
|
63170
|
+
const result = await provider.editImage(sheetBuffers[0], prompt3, { aspectRatio: ratio });
|
|
63171
|
+
return imageBufferFromResult(result);
|
|
63172
|
+
}
|
|
63173
|
+
async function ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel) {
|
|
63174
|
+
const rel = `assets/keyframe-${beat.id}.png`;
|
|
63175
|
+
const abs = join25(ctx.projectDir, rel);
|
|
63176
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
63177
|
+
const ratio = imageRatioForSize(size);
|
|
63178
|
+
const metadataOptions = { quality: ctx.imageQuality, size, ratio };
|
|
63179
|
+
const cache = keyframeCacheDescriptor({
|
|
63180
|
+
beatId: beat.id,
|
|
63181
|
+
cue: keyframeCue,
|
|
63182
|
+
provider: ctx.imageProvider,
|
|
63183
|
+
quality: ctx.imageQuality,
|
|
63184
|
+
size,
|
|
63185
|
+
ratio,
|
|
63186
|
+
characters: characterRefsRel
|
|
63187
|
+
});
|
|
63188
|
+
if (existsSync49(abs) && !ctx.force) {
|
|
63189
|
+
const metadata = readAssetMetadata(ctx.projectDir, "keyframe", beat.id);
|
|
63190
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
63191
|
+
projectDir: ctx.projectDir,
|
|
63192
|
+
kind: "keyframe",
|
|
63193
|
+
beatId: beat.id,
|
|
63194
|
+
cue: keyframeCue,
|
|
63195
|
+
provider: ctx.imageProvider,
|
|
63196
|
+
options: metadataOptions,
|
|
63197
|
+
cacheKey: cache.key
|
|
63198
|
+
})) {
|
|
63199
|
+
return { status: "cached", rel };
|
|
63200
|
+
}
|
|
63201
|
+
}
|
|
63202
|
+
const cacheAbs = join25(ctx.projectDir, cache.path);
|
|
63203
|
+
if (existsSync49(cacheAbs) && !ctx.force) {
|
|
63204
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
63205
|
+
await copyFile5(cacheAbs, abs);
|
|
63206
|
+
await writeAssetMetadata({
|
|
63207
|
+
projectDir: ctx.projectDir,
|
|
63208
|
+
kind: "keyframe",
|
|
63209
|
+
beatId: beat.id,
|
|
63210
|
+
cue: keyframeCue,
|
|
63211
|
+
provider: ctx.imageProvider,
|
|
63212
|
+
options: metadataOptions,
|
|
63213
|
+
cacheKey: cache.key,
|
|
63214
|
+
canonicalPath: rel,
|
|
63215
|
+
cachePath: cache.path
|
|
63216
|
+
});
|
|
63217
|
+
return { status: "cached", rel };
|
|
63218
|
+
}
|
|
63219
|
+
const sheetBuffers = [];
|
|
63220
|
+
for (const relPath of characterRefsRel) {
|
|
63221
|
+
const sheetAbs = join25(ctx.projectDir, relPath);
|
|
63222
|
+
if (existsSync49(sheetAbs)) sheetBuffers.push(await readFile46(sheetAbs));
|
|
63223
|
+
}
|
|
63224
|
+
const imgCtx = {
|
|
63225
|
+
projectDir: ctx.projectDir,
|
|
63226
|
+
imageProvider: ctx.imageProvider,
|
|
63227
|
+
imageSize: size,
|
|
63228
|
+
imageQuality: ctx.imageQuality
|
|
63229
|
+
};
|
|
63230
|
+
const generated = sheetBuffers.length > 0 ? await editKeyframeImage(keyframeCue, sheetBuffers, imgCtx, ratio) : await generateBackdropImage(keyframeCue, imgCtx, ratio);
|
|
63231
|
+
if (!generated.success) return { status: "failed", error: generated.error };
|
|
63232
|
+
await mkdir26(dirname24(abs), { recursive: true });
|
|
63233
|
+
await writeFile50(abs, generated.buffer);
|
|
63234
|
+
await mkdir26(dirname24(cacheAbs), { recursive: true });
|
|
63235
|
+
await writeFile50(cacheAbs, generated.buffer);
|
|
63236
|
+
await writeAssetMetadata({
|
|
63237
|
+
projectDir: ctx.projectDir,
|
|
63238
|
+
kind: "keyframe",
|
|
63239
|
+
beatId: beat.id,
|
|
63240
|
+
cue: keyframeCue,
|
|
63241
|
+
provider: ctx.imageProvider,
|
|
63242
|
+
options: metadataOptions,
|
|
63243
|
+
cacheKey: cache.key,
|
|
63244
|
+
canonicalPath: rel,
|
|
63245
|
+
cachePath: cache.path
|
|
63246
|
+
});
|
|
63247
|
+
return { status: "generated", rel };
|
|
63248
|
+
}
|
|
62102
63249
|
async function imageBufferFromResult(result) {
|
|
62103
63250
|
const image = result.images?.[0];
|
|
62104
63251
|
if (!result.success || !image) {
|
|
@@ -62122,15 +63269,22 @@ function imageProviderKeyInfo(provider) {
|
|
|
62122
63269
|
async function dispatchVideo(beat, ctx) {
|
|
62123
63270
|
const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
|
|
62124
63271
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
62125
|
-
const
|
|
63272
|
+
const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
|
|
63273
|
+
const prompt3 = stringOrUndefined4(beat.cues?.video) ?? keyframeCue;
|
|
62126
63274
|
if (!prompt3) return { status: "no-cue" };
|
|
63275
|
+
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
63276
|
+
const characterRefsAbs = characterRefsRel.map((p) => join25(ctx.projectDir, p));
|
|
62127
63277
|
const rel = `assets/video-${beat.id}.mp4`;
|
|
62128
63278
|
const abs = join25(ctx.projectDir, rel);
|
|
62129
63279
|
const cache = videoCacheDescriptor({
|
|
62130
63280
|
beatId: beat.id,
|
|
62131
63281
|
cue: prompt3,
|
|
62132
63282
|
provider: ctx.videoProvider,
|
|
62133
|
-
duration: normalizeVideoDuration(beat.duration)
|
|
63283
|
+
duration: normalizeVideoDuration(beat.duration),
|
|
63284
|
+
// Keyframe mode keys the clip to the keyframe prompt; refs feed the keyframe,
|
|
63285
|
+
// not the clip directly.
|
|
63286
|
+
characters: keyframeCue ? void 0 : characterRefsRel,
|
|
63287
|
+
keyframe: keyframeCue
|
|
62134
63288
|
});
|
|
62135
63289
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
62136
63290
|
if (existsSync49(abs) && !ctx.force) {
|
|
@@ -62183,6 +63337,19 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62183
63337
|
};
|
|
62184
63338
|
}
|
|
62185
63339
|
loadSceneBuildEnv(ctx.projectDir);
|
|
63340
|
+
let keyframeStatus;
|
|
63341
|
+
let keyframeRel;
|
|
63342
|
+
let keyframeImageAbs;
|
|
63343
|
+
if (keyframeCue) {
|
|
63344
|
+
const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
|
|
63345
|
+
if (kf.status === "failed") {
|
|
63346
|
+
ctx.onProgress({ type: "video-failed", beatId: beat.id, error: kf.error });
|
|
63347
|
+
return { status: "failed", error: kf.error };
|
|
63348
|
+
}
|
|
63349
|
+
keyframeStatus = kf.status;
|
|
63350
|
+
keyframeRel = kf.rel;
|
|
63351
|
+
keyframeImageAbs = join25(ctx.projectDir, kf.rel);
|
|
63352
|
+
}
|
|
62186
63353
|
const result = await executeVideoGenerate({
|
|
62187
63354
|
prompt: prompt3,
|
|
62188
63355
|
provider: ctx.videoProvider,
|
|
@@ -62190,6 +63357,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62190
63357
|
ratio: "16:9",
|
|
62191
63358
|
output: abs,
|
|
62192
63359
|
wait: false,
|
|
63360
|
+
// Keyframe mode → single init frame (image-to-video); otherwise character
|
|
63361
|
+
// reference-to-video.
|
|
63362
|
+
image: keyframeImageAbs,
|
|
63363
|
+
refImages: keyframeImageAbs ? void 0 : characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
62193
63364
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
62194
63365
|
});
|
|
62195
63366
|
if (!result.success || !result.taskId) {
|
|
@@ -62224,14 +63395,16 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62224
63395
|
cachePath: cache.path,
|
|
62225
63396
|
cacheKey: cache.key,
|
|
62226
63397
|
metadataPath,
|
|
62227
|
-
freshness: "fresh"
|
|
63398
|
+
freshness: "fresh",
|
|
63399
|
+
keyframeStatus,
|
|
63400
|
+
keyframePath: keyframeRel
|
|
62228
63401
|
};
|
|
62229
63402
|
}
|
|
62230
63403
|
const job = await createAndWriteJobRecord({
|
|
62231
63404
|
jobType: "generate-video",
|
|
62232
63405
|
provider: result.provider ?? ctx.videoProvider,
|
|
62233
63406
|
providerTaskId: result.taskId,
|
|
62234
|
-
providerTaskType: "text2video",
|
|
63407
|
+
providerTaskType: keyframeImageAbs ? "image2video" : "text2video",
|
|
62235
63408
|
status: "running",
|
|
62236
63409
|
projectDir: ctx.projectDir,
|
|
62237
63410
|
workingDirectory: ctx.projectDir,
|
|
@@ -62256,7 +63429,9 @@ async function dispatchVideo(beat, ctx) {
|
|
|
62256
63429
|
provider: job.provider,
|
|
62257
63430
|
cachePath: cache.path,
|
|
62258
63431
|
cacheKey: cache.key,
|
|
62259
|
-
metadataPath
|
|
63432
|
+
metadataPath,
|
|
63433
|
+
keyframeStatus,
|
|
63434
|
+
keyframePath: keyframeRel
|
|
62260
63435
|
};
|
|
62261
63436
|
}
|
|
62262
63437
|
async function dispatchMusic(beat, ctx) {
|
|
@@ -62558,7 +63733,14 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
62558
63733
|
for (const outcome of outcomes) {
|
|
62559
63734
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
62560
63735
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
62561
|
-
if (outcome.
|
|
63736
|
+
if (outcome.keyframeStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
63737
|
+
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
63738
|
+
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
63739
|
+
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
|
63740
|
+
resolution: "720p",
|
|
63741
|
+
aspectRatio: "16:9"
|
|
63742
|
+
}) : 5;
|
|
63743
|
+
}
|
|
62562
63744
|
if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
|
|
62563
63745
|
}
|
|
62564
63746
|
return Number(cost.toFixed(2));
|
|
@@ -68455,6 +69637,9 @@ var init_scene2 = __esm({
|
|
|
68455
69637
|
kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
|
|
68456
69638
|
projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
|
|
68457
69639
|
insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
|
|
69640
|
+
syncStoryboard: external_exports.boolean().optional().describe(
|
|
69641
|
+
"Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
|
|
69642
|
+
),
|
|
68458
69643
|
imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
|
|
68459
69644
|
voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
|
|
68460
69645
|
skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
|
|
@@ -68467,7 +69652,7 @@ var init_scene2 = __esm({
|
|
|
68467
69652
|
cost: "low",
|
|
68468
69653
|
title: "Add Scene from Preset",
|
|
68469
69654
|
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
|
|
69655
|
+
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
69656
|
schema: sceneAddSchema,
|
|
68472
69657
|
async execute(args, ctx) {
|
|
68473
69658
|
const projectDir = args.projectDir ? resolve75(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
|
|
@@ -68481,6 +69666,7 @@ var init_scene2 = __esm({
|
|
|
68481
69666
|
kicker: args.kicker,
|
|
68482
69667
|
projectDir,
|
|
68483
69668
|
insertInto: args.insertInto,
|
|
69669
|
+
syncStoryboard: args.syncStoryboard,
|
|
68484
69670
|
imageProvider: args.imageProvider,
|
|
68485
69671
|
voice: args.voice,
|
|
68486
69672
|
skipAudio: args.skipAudio,
|
|
@@ -68499,6 +69685,9 @@ var init_scene2 = __esm({
|
|
|
68499
69685
|
];
|
|
68500
69686
|
if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
|
|
68501
69687
|
if (result.imagePath) lines.push(` image: ${result.imagePath}`);
|
|
69688
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
69689
|
+
lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
|
|
69690
|
+
}
|
|
68502
69691
|
return {
|
|
68503
69692
|
success: true,
|
|
68504
69693
|
data: {
|
|
@@ -68509,7 +69698,10 @@ var init_scene2 = __esm({
|
|
|
68509
69698
|
scenePath: result.scenePath,
|
|
68510
69699
|
rootPath: result.rootPath,
|
|
68511
69700
|
audioPath: result.audioPath,
|
|
68512
|
-
imagePath: result.imagePath
|
|
69701
|
+
imagePath: result.imagePath,
|
|
69702
|
+
storyboardPath: result.storyboardPath,
|
|
69703
|
+
storyboardSynced: result.storyboardSynced,
|
|
69704
|
+
storyboardAction: result.storyboardAction
|
|
68513
69705
|
},
|
|
68514
69706
|
humanLines: lines
|
|
68515
69707
|
};
|
|
@@ -73627,7 +74819,7 @@ init_source();
|
|
|
73627
74819
|
// package.json
|
|
73628
74820
|
var package_default = {
|
|
73629
74821
|
name: "@vibeframe/cli",
|
|
73630
|
-
version: "0.113.
|
|
74822
|
+
version: "0.113.3",
|
|
73631
74823
|
description: "VibeFrame CLI - agentic video workflow runtime",
|
|
73632
74824
|
private: false,
|
|
73633
74825
|
license: "MIT",
|
|
@@ -73692,7 +74884,7 @@ var package_default = {
|
|
|
73692
74884
|
lint: "eslint --ext .ts,.tsx src/",
|
|
73693
74885
|
test: "vitest run",
|
|
73694
74886
|
"test:watch": "vitest",
|
|
73695
|
-
pretest: "node build.js",
|
|
74887
|
+
pretest: "pnpm --dir ../core build && pnpm --dir ../ai-providers build && node build.js",
|
|
73696
74888
|
prepublishOnly: "node build.js"
|
|
73697
74889
|
},
|
|
73698
74890
|
dependencies: {
|
|
@@ -82210,13 +83402,13 @@ function titleizeProjectName(name) {
|
|
|
82210
83402
|
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Video";
|
|
82211
83403
|
}
|
|
82212
83404
|
function describeTheme(brief, product) {
|
|
82213
|
-
const cleaned = brief.replace(/\b\d+[- ]?second\b/gi, "").replace(new RegExp(`\\bfor\\s+${
|
|
83405
|
+
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
83406
|
return cleaned || "focused product story";
|
|
82215
83407
|
}
|
|
82216
83408
|
function isMountainSunriseBrief(brief) {
|
|
82217
83409
|
return /\b(mountain|ridge|sunrise|fog|peak)\b/i.test(brief);
|
|
82218
83410
|
}
|
|
82219
|
-
function
|
|
83411
|
+
function escapeRegExp5(value) {
|
|
82220
83412
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82221
83413
|
}
|
|
82222
83414
|
|
|
@@ -82259,6 +83451,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
|
|
|
82259
83451
|
configKind: "codex-toml",
|
|
82260
83452
|
notes: [
|
|
82261
83453
|
"Codex reads AGENTS.md and can also load project-scoped .codex/config.toml after the project is trusted.",
|
|
83454
|
+
"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
83455
|
"Keep provider/auth keys in VibeFrame config or environment, not in project-local Codex config."
|
|
82263
83456
|
]
|
|
82264
83457
|
},
|
|
@@ -82271,6 +83464,7 @@ function hostDefinitions(_projectDir = process.cwd()) {
|
|
|
82271
83464
|
configKind: "mcp-json",
|
|
82272
83465
|
notes: [
|
|
82273
83466
|
"Claude Code can drive vibe directly through shell plus AGENTS.md/CLAUDE.md.",
|
|
83467
|
+
"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
83468
|
"The MCP entry is optional and gives Claude Code a typed tool surface."
|
|
82275
83469
|
]
|
|
82276
83470
|
},
|
|
@@ -82345,7 +83539,15 @@ async function planHostSetup(host, projectDir, opts) {
|
|
|
82345
83539
|
status: opts.write ? "would-merge" : "would-write",
|
|
82346
83540
|
reason: opts.write ? "dry run" : "snippet only; pass --write to modify"
|
|
82347
83541
|
});
|
|
82348
|
-
return {
|
|
83542
|
+
return {
|
|
83543
|
+
host,
|
|
83544
|
+
snippet,
|
|
83545
|
+
command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
|
|
83546
|
+
configPath,
|
|
83547
|
+
files,
|
|
83548
|
+
warnings,
|
|
83549
|
+
nextSteps
|
|
83550
|
+
};
|
|
82349
83551
|
}
|
|
82350
83552
|
try {
|
|
82351
83553
|
const action = host.configKind === "codex-toml" ? await mergeCodexToml(configPath, { force: opts.force === true }) : await mergeMcpJson(configPath, {
|
|
@@ -82357,7 +83559,15 @@ async function planHostSetup(host, projectDir, opts) {
|
|
|
82357
83559
|
} catch (error) {
|
|
82358
83560
|
warnings.push(error instanceof Error ? error.message : String(error));
|
|
82359
83561
|
}
|
|
82360
|
-
return {
|
|
83562
|
+
return {
|
|
83563
|
+
host,
|
|
83564
|
+
snippet,
|
|
83565
|
+
command: host.id === "claude-code" ? claudeCodeAddCommand() : void 0,
|
|
83566
|
+
configPath,
|
|
83567
|
+
files,
|
|
83568
|
+
warnings,
|
|
83569
|
+
nextSteps
|
|
83570
|
+
};
|
|
82361
83571
|
}
|
|
82362
83572
|
async function inspectHost(host, projectDir) {
|
|
82363
83573
|
const configPath = host.configPath(projectDir);
|
|
@@ -82401,7 +83611,9 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82401
83611
|
try {
|
|
82402
83612
|
root = raw.trim() ? JSON.parse(raw) : {};
|
|
82403
83613
|
} catch (error) {
|
|
82404
|
-
throw new Error(
|
|
83614
|
+
throw new Error(
|
|
83615
|
+
`Invalid JSON in ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
83616
|
+
);
|
|
82405
83617
|
}
|
|
82406
83618
|
}
|
|
82407
83619
|
const mcpServers = typeof root.mcpServers === "object" && root.mcpServers !== null && !Array.isArray(root.mcpServers) ? root.mcpServers : {};
|
|
@@ -82414,10 +83626,18 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82414
83626
|
};
|
|
82415
83627
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82416
83628
|
if (exists && opts.backup) {
|
|
82417
|
-
await writeFile45(
|
|
83629
|
+
await writeFile45(
|
|
83630
|
+
`${configPath}.bak-${timestamp()}`,
|
|
83631
|
+
await readFile39(configPath, "utf-8"),
|
|
83632
|
+
"utf-8"
|
|
83633
|
+
);
|
|
82418
83634
|
}
|
|
82419
83635
|
await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
|
|
82420
|
-
return {
|
|
83636
|
+
return {
|
|
83637
|
+
path: configPath,
|
|
83638
|
+
status: "merged",
|
|
83639
|
+
reason: "anchored existing default vibeframe MCP server"
|
|
83640
|
+
};
|
|
82421
83641
|
}
|
|
82422
83642
|
return {
|
|
82423
83643
|
path: configPath,
|
|
@@ -82428,7 +83648,11 @@ async function mergeMcpJson(configPath, opts) {
|
|
|
82428
83648
|
root.mcpServers = { ...mcpServers, vibeframe: opts.server };
|
|
82429
83649
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82430
83650
|
if (exists && opts.backup) {
|
|
82431
|
-
await writeFile45(
|
|
83651
|
+
await writeFile45(
|
|
83652
|
+
`${configPath}.bak-${timestamp()}`,
|
|
83653
|
+
await readFile39(configPath, "utf-8"),
|
|
83654
|
+
"utf-8"
|
|
83655
|
+
);
|
|
82432
83656
|
}
|
|
82433
83657
|
await writeFile45(configPath, JSON.stringify(root, null, 2) + "\n", "utf-8");
|
|
82434
83658
|
return { path: configPath, status: exists ? "merged" : "wrote" };
|
|
@@ -82439,16 +83663,22 @@ async function mergeCodexToml(configPath, opts) {
|
|
|
82439
83663
|
${CODEX_MCP_BLOCK}${CODEX_MANAGED_END}`;
|
|
82440
83664
|
if (!exists) {
|
|
82441
83665
|
await mkdir23(dirname19(configPath), { recursive: true });
|
|
82442
|
-
await writeFile45(
|
|
83666
|
+
await writeFile45(
|
|
83667
|
+
configPath,
|
|
83668
|
+
`#:schema https://developers.openai.com/codex/config-schema.json
|
|
82443
83669
|
|
|
82444
83670
|
${block}
|
|
82445
|
-
`,
|
|
83671
|
+
`,
|
|
83672
|
+
"utf-8"
|
|
83673
|
+
);
|
|
82446
83674
|
return { path: configPath, status: "wrote" };
|
|
82447
83675
|
}
|
|
82448
83676
|
const raw = await readFile39(configPath, "utf-8");
|
|
82449
83677
|
if (raw.includes(CODEX_MANAGED_START)) {
|
|
82450
83678
|
const updated = raw.replace(
|
|
82451
|
-
new RegExp(
|
|
83679
|
+
new RegExp(
|
|
83680
|
+
`${escapeRegExp6(CODEX_MANAGED_START)}[\\s\\S]*?${escapeRegExp6(CODEX_MANAGED_END)}`
|
|
83681
|
+
),
|
|
82452
83682
|
block
|
|
82453
83683
|
);
|
|
82454
83684
|
await writeFile45(configPath, ensureTrailingNewline(updated), "utf-8");
|
|
@@ -82482,7 +83712,8 @@ function hasVibeframeMcpServer(value) {
|
|
|
82482
83712
|
function readVibeframeMcpServer(value) {
|
|
82483
83713
|
if (!value || typeof value !== "object") return null;
|
|
82484
83714
|
const root = value;
|
|
82485
|
-
if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers))
|
|
83715
|
+
if (!root.mcpServers || typeof root.mcpServers !== "object" || Array.isArray(root.mcpServers))
|
|
83716
|
+
return null;
|
|
82486
83717
|
const server = root.mcpServers.vibeframe;
|
|
82487
83718
|
if (!server || typeof server !== "object" || Array.isArray(server)) return null;
|
|
82488
83719
|
return server;
|
|
@@ -82493,7 +83724,9 @@ function isAnchoredClaudeDesktopServer(server) {
|
|
|
82493
83724
|
return server.args[0] === "-lc" && typeof server.args[1] === "string" && server.args[1].includes("exec npx -y @vibeframe/mcp-server");
|
|
82494
83725
|
}
|
|
82495
83726
|
if (server.command === "cmd.exe" && Array.isArray(server.args)) {
|
|
82496
|
-
return server.args.some(
|
|
83727
|
+
return server.args.some(
|
|
83728
|
+
(arg) => typeof arg === "string" && arg.includes("npx -y @vibeframe/mcp-server")
|
|
83729
|
+
);
|
|
82497
83730
|
}
|
|
82498
83731
|
return false;
|
|
82499
83732
|
}
|
|
@@ -82510,13 +83743,15 @@ function hostNextSteps(host) {
|
|
|
82510
83743
|
if (host.id === "codex") {
|
|
82511
83744
|
return [
|
|
82512
83745
|
"Trust the project in Codex so .codex/config.toml is loaded.",
|
|
82513
|
-
"Run /mcp in Codex to confirm the vibeframe server is connected."
|
|
83746
|
+
"Run /mcp in Codex to confirm the vibeframe server is connected.",
|
|
83747
|
+
"Use /goal with the project AGENTS.md stop rules for long-running video builds."
|
|
82514
83748
|
];
|
|
82515
83749
|
}
|
|
82516
83750
|
if (host.id === "claude-code") {
|
|
82517
83751
|
return [
|
|
82518
83752
|
"Restart Claude Code or run /mcp to approve the project-scoped server.",
|
|
82519
|
-
"Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows."
|
|
83753
|
+
"Use AGENTS.md/CLAUDE.md for shell-driven vibe workflows.",
|
|
83754
|
+
"Use /goal with the project AGENTS.md stop rules for long-running video builds."
|
|
82520
83755
|
];
|
|
82521
83756
|
}
|
|
82522
83757
|
if (host.id === "claude-desktop") {
|
|
@@ -82530,7 +83765,11 @@ function claudeDesktopConfigPath() {
|
|
|
82530
83765
|
return join21(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
82531
83766
|
}
|
|
82532
83767
|
if (process.platform === "win32") {
|
|
82533
|
-
return join21(
|
|
83768
|
+
return join21(
|
|
83769
|
+
process.env.APPDATA ?? join21(home, "AppData", "Roaming"),
|
|
83770
|
+
"Claude",
|
|
83771
|
+
"claude_desktop_config.json"
|
|
83772
|
+
);
|
|
82534
83773
|
}
|
|
82535
83774
|
return join21(home, ".config", "Claude", "claude_desktop_config.json");
|
|
82536
83775
|
}
|
|
@@ -82541,7 +83780,7 @@ function ensureTrailingNewline(value) {
|
|
|
82541
83780
|
return value.endsWith("\n") ? value : `${value}
|
|
82542
83781
|
`;
|
|
82543
83782
|
}
|
|
82544
|
-
function
|
|
83783
|
+
function escapeRegExp6(value) {
|
|
82545
83784
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82546
83785
|
}
|
|
82547
83786
|
function shellQuote(value) {
|
|
@@ -82733,6 +83972,45 @@ Default setup is snippet-only; use \`--write\` when you want VibeFrame to merge
|
|
|
82733
83972
|
the generated config. Keep provider keys in \`.vibeframe/config.yaml\`, user
|
|
82734
83973
|
config, or env files \u2014 not in app host config.
|
|
82735
83974
|
|
|
83975
|
+
## Native host goal loop
|
|
83976
|
+
|
|
83977
|
+
For long-running work, use Codex Goal mode, Claude Code \`/goal\`, Cursor, or
|
|
83978
|
+
another host-native goal feature as the outer loop. VibeFrame should not own a
|
|
83979
|
+
competing project-level goal runner. It provides \`--json\` commands, dry runs,
|
|
83980
|
+
budget caps, \`build-report.json\`, \`review-report.json\`, \`nextActions\`,
|
|
83981
|
+
\`safeToAutoRun\`, \`requiresConfirmation\`, \`fixOwner\`, deterministic repair,
|
|
83982
|
+
and render inspection for the host agent to decide what to do next.
|
|
83983
|
+
|
|
83984
|
+
Copy-paste Codex:
|
|
83985
|
+
|
|
83986
|
+
\`\`\`text
|
|
83987
|
+
/goal Build this VibeFrame project into renders/final.mp4. Use --json for every
|
|
83988
|
+
vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
|
|
83989
|
+
unless the user gives another budget, read build-report.json and
|
|
83990
|
+
review-report.json before deciding the next action, prefer nextActions before
|
|
83991
|
+
guessing, run only safeToAutoRun:true actions automatically, and ask before
|
|
83992
|
+
requiresConfirmation:true actions. Treat retryWith as the compatibility
|
|
83993
|
+
fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
|
|
83994
|
+
STORYBOARD.md, DESIGN.md, or composition edits.
|
|
83995
|
+
Stop only when renders/final.mp4 exists, target duration and aspect ratio are
|
|
83996
|
+
met, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
|
|
83997
|
+
remaining host-agent issue is fixed, accepted with rationale, or reported as
|
|
83998
|
+
blocked.
|
|
83999
|
+
\`\`\`
|
|
84000
|
+
|
|
84001
|
+
Copy-paste Claude Code:
|
|
84002
|
+
|
|
84003
|
+
\`\`\`text
|
|
84004
|
+
/goal Finish this VibeFrame render using Claude Code goal mode as the outer
|
|
84005
|
+
loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
|
|
84006
|
+
paid operations, budget cap via --max-cost 5, nextActions before guessing,
|
|
84007
|
+
build-report.json/review-report.json as loop state, and safeToAutoRun,
|
|
84008
|
+
requiresConfirmation, and fixOwner to decide between vibe scene repair and
|
|
84009
|
+
host-agent edits. Stop only after final MP4,
|
|
84010
|
+
target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
|
|
84011
|
+
no unresolved unacknowledged host-agent issues.
|
|
84012
|
+
\`\`\`
|
|
84013
|
+
|
|
82736
84014
|
### Composition rules (scene HTML)
|
|
82737
84015
|
|
|
82738
84016
|
\`vibe init\` installs local composition rules into your project. The
|
|
@@ -83821,7 +85099,7 @@ init_build_plan();
|
|
|
83821
85099
|
init_storyboard_parse();
|
|
83822
85100
|
init_output();
|
|
83823
85101
|
import { resolve as resolve66, join as join26 } from "node:path";
|
|
83824
|
-
import { readFileSync as
|
|
85102
|
+
import { readFileSync as readFileSync4, existsSync as existsSync50 } from "node:fs";
|
|
83825
85103
|
var VALID_MODES2 = ["agent", "batch", "auto"];
|
|
83826
85104
|
var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
|
|
83827
85105
|
var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
|
|
@@ -84188,7 +85466,7 @@ function estimateBuildCost(params) {
|
|
|
84188
85466
|
if (!existsSync50(storyboardPath2)) return null;
|
|
84189
85467
|
let beats = 0;
|
|
84190
85468
|
try {
|
|
84191
|
-
const md =
|
|
85469
|
+
const md = readFileSync4(storyboardPath2, "utf-8");
|
|
84192
85470
|
beats = parseStoryboard(md).beats.length;
|
|
84193
85471
|
} catch {
|
|
84194
85472
|
return null;
|
|
@@ -84572,6 +85850,18 @@ function printProjectStatus(result) {
|
|
|
84572
85850
|
console.log(` Build: ${result.build ? formatBuild(result.build) : source_default.dim("no build-report.json")}`);
|
|
84573
85851
|
console.log(` Review: ${result.review ? formatReview(result.review) : source_default.dim("no review-report.json")}`);
|
|
84574
85852
|
console.log(` Jobs: ${result.jobs.total} total, ${result.jobs.active} active, ${result.jobs.failed} failed`);
|
|
85853
|
+
if (result.nextActions.length > 0) {
|
|
85854
|
+
console.log();
|
|
85855
|
+
console.log(source_default.bold("Next actions"));
|
|
85856
|
+
for (const action of result.nextActions.slice(0, 5)) {
|
|
85857
|
+
console.log(formatNextAction(action));
|
|
85858
|
+
if (action.command) console.log(source_default.dim(` ${action.command}`));
|
|
85859
|
+
if (action.agentPrompt) console.log(source_default.dim(` ${action.agentPrompt}`));
|
|
85860
|
+
}
|
|
85861
|
+
if (result.nextActions.length > 5) {
|
|
85862
|
+
console.log(source_default.dim(` ... ${result.nextActions.length - 5} more`));
|
|
85863
|
+
}
|
|
85864
|
+
}
|
|
84575
85865
|
if (result.jobs.latest.length > 0) {
|
|
84576
85866
|
console.log();
|
|
84577
85867
|
for (const job of result.jobs.latest.slice(0, 5)) {
|
|
@@ -84592,6 +85882,10 @@ function formatReview(review) {
|
|
|
84592
85882
|
const status = review.status ?? "unknown";
|
|
84593
85883
|
return `${formatStatus(status)} score ${review.score ?? "-"} (${review.issueCount} issue${review.issueCount === 1 ? "" : "s"})`;
|
|
84594
85884
|
}
|
|
85885
|
+
function formatNextAction(action) {
|
|
85886
|
+
const badge = action.safeToAutoRun ? "auto" : action.requiresConfirmation ? "confirm" : action.kind;
|
|
85887
|
+
return ` ${source_default.dim(`[${badge}]`)} ${action.label}`;
|
|
85888
|
+
}
|
|
84595
85889
|
function formatStatus(status) {
|
|
84596
85890
|
if (status === "completed" || status === "pass") return source_default.green(status);
|
|
84597
85891
|
if (status === "failed" || status === "fail") return source_default.red(status);
|
|
@@ -85598,6 +86892,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85598
86892
|
product: "vibeframe",
|
|
85599
86893
|
sourceOfTruth: ["STORYBOARD.md", "DESIGN.md", "vibe.config.json"],
|
|
85600
86894
|
preferredFlow: [
|
|
86895
|
+
"native Codex Goal mode or Claude Code /goal owns persistence, iteration, and stop decisions",
|
|
86896
|
+
"vibe context/schema for command discovery",
|
|
85601
86897
|
"storyboard revise after init when the brief changes",
|
|
85602
86898
|
"storyboard validate",
|
|
85603
86899
|
"plan",
|
|
@@ -85612,6 +86908,19 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85612
86908
|
"inspect render --ai when needed",
|
|
85613
86909
|
"status project when async jobs are involved"
|
|
85614
86910
|
],
|
|
86911
|
+
nativeGoalContract: {
|
|
86912
|
+
owner: "host-native goal mode (Codex /goal, Claude Code /goal, Cursor or other host equivalent)",
|
|
86913
|
+
vibeRole: "video-specific commands, JSON reports, cost gates, deterministic repair, render inspection, nextActions, and retryWith compatibility hints",
|
|
86914
|
+
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",
|
|
86915
|
+
stopRules: [
|
|
86916
|
+
"final MP4 exists at the requested output path",
|
|
86917
|
+
"duration and aspect ratio match the brief or explicitly accepted constraints",
|
|
86918
|
+
"inspect render --cheap reports no errors",
|
|
86919
|
+
"any AI review score meets the goal threshold, commonly >= 90, when AI review is requested",
|
|
86920
|
+
"all fixOwner:vibe issues have been repaired or are blocked with evidence",
|
|
86921
|
+
"all fixOwner:host-agent issues are fixed, intentionally accepted with a written reason, or reported as blocked"
|
|
86922
|
+
]
|
|
86923
|
+
},
|
|
85615
86924
|
mentalModel: {
|
|
85616
86925
|
storyboard: "intent layer; edit or mutate beat cues here",
|
|
85617
86926
|
scene: "generated artifact layer; lint/repair composition HTML here"
|
|
@@ -85645,7 +86954,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85645
86954
|
reviewReportContract: {
|
|
85646
86955
|
kind: "review",
|
|
85647
86956
|
modes: ["project", "render"],
|
|
85648
|
-
fields: ["status", "score", "issues", "summary", "sourceReports", "retryWith"],
|
|
86957
|
+
fields: ["status", "score", "issues", "summary", "sourceReports", "nextActions", "retryWith"],
|
|
86958
|
+
recovery: "prefer nextActions (classified by costTier/safeToAutoRun/requiresConfirmation/fixOwner); retryWith is the compatibility fallback",
|
|
85649
86959
|
issueFixOwner: {
|
|
85650
86960
|
vibe: "deterministic CLI recovery",
|
|
85651
86961
|
"host-agent": "storyboard/design/composition edits the host agent should make"
|
|
@@ -85653,7 +86963,7 @@ var contextCommand = new Command("context").description("Print CLI context/guide
|
|
|
85653
86963
|
},
|
|
85654
86964
|
machineStatusContract: {
|
|
85655
86965
|
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",
|
|
86966
|
+
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
86967
|
reviewSummary: "review.mode/issueCount/errorCount/warningCount/infoCount/fixOwners/sourceReports/retryWith"
|
|
85658
86968
|
},
|
|
85659
86969
|
productSurfaceContract: {
|
|
@@ -85736,12 +87046,13 @@ render. Repair failures return \`code:"SCENE_REPAIR_FAILED"\` with
|
|
|
85736
87046
|
render with \`currentStage:"assets"\` and \`code:"ASSET_REFERENCE_INVALID"\`,
|
|
85737
87047
|
\`code:"MISSING_API_KEY"\`, or \`code:"ASSET_GENERATION_FAILED"\`.
|
|
85738
87048
|
\`build-report.json\` includes \`sceneRepair\`, and \`status project\` carries
|
|
85739
|
-
review issue counts, \`fixOwners\`, \`sourceReports\`,
|
|
85740
|
-
\`
|
|
87049
|
+
review issue counts, \`fixOwners\`, \`sourceReports\`, top-level
|
|
87050
|
+
\`nextActions\` (populated for every workflow state), and \`retryWith\`.
|
|
87051
|
+
Prefer \`nextActions\` and treat \`retryWith\` as the compatibility fallback.
|
|
85741
87052
|
|
|
85742
87053
|
\`review-report.json\` is written by \`inspect project\` and \`inspect render\`
|
|
85743
87054
|
as \`kind:"review"\` with \`mode:"project"|"render"\`, \`summary\`,
|
|
85744
|
-
\`sourceReports\`, \`retryWith\`, and issue-level
|
|
87055
|
+
\`sourceReports\`, \`nextActions\`, \`retryWith\`, and issue-level
|
|
85745
87056
|
\`fixOwner:"vibe"|"host-agent"\`.
|
|
85746
87057
|
\`inspect render --cheap\` checks duration drift, audio presence, black frames,
|
|
85747
87058
|
long silence, and static-frame holds. Static or semantic beat issues are
|
|
@@ -85763,6 +87074,8 @@ Use 'vibe schema --list --surface public' for the small product surface.
|
|
|
85763
87074
|
Use 'vibe schema <command> --json' to get parameter schemas.
|
|
85764
87075
|
Use 'vibe doctor --json' to check configured API keys.
|
|
85765
87076
|
Use '--dry-run --json' before any mutating/costly operation.
|
|
87077
|
+
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.
|
|
87078
|
+
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
87079
|
|
|
85767
87080
|
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
87081
|
|