@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
|
@@ -10459,6 +10459,19 @@ var require_src = __commonJS({
|
|
|
10459
10459
|
});
|
|
10460
10460
|
|
|
10461
10461
|
// ../ai-providers/dist/fal/FalProvider.js
|
|
10462
|
+
function estimateSeedanceVideoCostUsd(opts) {
|
|
10463
|
+
const shortSide = SEEDANCE_SHORT_SIDE[opts.resolution ?? "720p"] ?? 720;
|
|
10464
|
+
const [a, b] = (opts.aspectRatio ?? "16:9").split(":").map(Number);
|
|
10465
|
+
const ratioFactor = a > 0 && b > 0 ? Math.max(a, b) / Math.min(a, b) : 16 / 9;
|
|
10466
|
+
const longSide = Math.round(shortSide * ratioFactor);
|
|
10467
|
+
const tokensPerSecond = shortSide * longSide * 24 / 1024;
|
|
10468
|
+
let usdPerSecond = tokensPerSecond / 1e3 * SEEDANCE_USD_PER_1K_TOKENS;
|
|
10469
|
+
if (opts.fast && shortSide <= 720)
|
|
10470
|
+
usdPerSecond *= SEEDANCE_FAST_FACTOR;
|
|
10471
|
+
if (opts.hasVideoReference)
|
|
10472
|
+
usdPerSecond *= SEEDANCE_VIDEO_REF_FACTOR;
|
|
10473
|
+
return Number((usdPerSecond * Math.max(0, opts.durationSec)).toFixed(2));
|
|
10474
|
+
}
|
|
10462
10475
|
function formatFalError(error) {
|
|
10463
10476
|
if (!(error instanceof Error))
|
|
10464
10477
|
return String(error);
|
|
@@ -10568,7 +10581,7 @@ function normaliseDuration(value) {
|
|
|
10568
10581
|
return "auto";
|
|
10569
10582
|
return Math.max(4, Math.min(15, Math.round(value)));
|
|
10570
10583
|
}
|
|
10571
|
-
var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, FalProvider, falProvider;
|
|
10584
|
+
var import_client, ENDPOINT_TEXT_TO_VIDEO, ENDPOINT_IMAGE_TO_VIDEO, ENDPOINT_REFERENCE_TO_VIDEO, DEFAULT_VARIANT, VALID_RESOLUTIONS, VALID_ASPECTS, SEEDANCE_USD_PER_1K_TOKENS, SEEDANCE_FAST_FACTOR, SEEDANCE_VIDEO_REF_FACTOR, SEEDANCE_SHORT_SIDE, FalProvider, falProvider;
|
|
10572
10585
|
var init_FalProvider = __esm({
|
|
10573
10586
|
"../ai-providers/dist/fal/FalProvider.js"() {
|
|
10574
10587
|
"use strict";
|
|
@@ -10588,6 +10601,10 @@ var init_FalProvider = __esm({
|
|
|
10588
10601
|
DEFAULT_VARIANT = "seedance-2.0";
|
|
10589
10602
|
VALID_RESOLUTIONS = ["480p", "720p", "1080p"];
|
|
10590
10603
|
VALID_ASPECTS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "auto"];
|
|
10604
|
+
SEEDANCE_USD_PER_1K_TOKENS = 0.014;
|
|
10605
|
+
SEEDANCE_FAST_FACTOR = 0.8;
|
|
10606
|
+
SEEDANCE_VIDEO_REF_FACTOR = 0.6;
|
|
10607
|
+
SEEDANCE_SHORT_SIDE = { "480p": 480, "720p": 720, "1080p": 1080 };
|
|
10591
10608
|
FalProvider = class {
|
|
10592
10609
|
constructor() {
|
|
10593
10610
|
this.id = "seedance";
|
|
@@ -11412,6 +11429,7 @@ __export(dist_exports2, {
|
|
|
11412
11429
|
defineApiKey: () => defineApiKey,
|
|
11413
11430
|
defineProvider: () => defineProvider,
|
|
11414
11431
|
elevenLabsProvider: () => elevenLabsProvider,
|
|
11432
|
+
estimateSeedanceVideoCostUsd: () => estimateSeedanceVideoCostUsd,
|
|
11415
11433
|
falProvider: () => falProvider,
|
|
11416
11434
|
geminiProvider: () => geminiProvider,
|
|
11417
11435
|
getAllApiKeys: () => getAllApiKeys,
|
|
@@ -24218,7 +24236,534 @@ var init_build_asset_metadata = __esm({
|
|
|
24218
24236
|
}
|
|
24219
24237
|
});
|
|
24220
24238
|
|
|
24239
|
+
// src/commands/_shared/review-report.ts
|
|
24240
|
+
import { writeFile as writeFile6 } from "node:fs/promises";
|
|
24241
|
+
import { join as join2, resolve as resolve5 } from "node:path";
|
|
24242
|
+
function statusFromIssues(issues) {
|
|
24243
|
+
if (issues.some((issue) => issue.severity === "error")) return "fail";
|
|
24244
|
+
if (issues.some((issue) => issue.severity === "warning")) return "warn";
|
|
24245
|
+
return "pass";
|
|
24246
|
+
}
|
|
24247
|
+
function scoreIssues(issues) {
|
|
24248
|
+
let score = 100;
|
|
24249
|
+
for (const issue of issues) {
|
|
24250
|
+
if (issue.severity === "error") score -= 25;
|
|
24251
|
+
else if (issue.severity === "warning") score -= 8;
|
|
24252
|
+
else score -= 2;
|
|
24253
|
+
}
|
|
24254
|
+
return Math.max(0, Math.min(100, score));
|
|
24255
|
+
}
|
|
24256
|
+
function uniqueRetryWith(items) {
|
|
24257
|
+
return [
|
|
24258
|
+
...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
|
|
24259
|
+
];
|
|
24260
|
+
}
|
|
24261
|
+
function commandReviewAction(command, opts) {
|
|
24262
|
+
const classified = classifyReviewCommand(command);
|
|
24263
|
+
return stripUndefined({
|
|
24264
|
+
id: actionId("command", command),
|
|
24265
|
+
kind: "command",
|
|
24266
|
+
label: opts.label ?? command,
|
|
24267
|
+
command,
|
|
24268
|
+
fixOwner: opts.fixOwner ?? classified.fixOwner,
|
|
24269
|
+
costTier: opts.costTier ?? classified.costTier,
|
|
24270
|
+
safeToAutoRun: opts.safeToAutoRun ?? classified.safeToAutoRun,
|
|
24271
|
+
requiresConfirmation: opts.requiresConfirmation ?? classified.requiresConfirmation,
|
|
24272
|
+
reason: opts.reason,
|
|
24273
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
24274
|
+
});
|
|
24275
|
+
}
|
|
24276
|
+
function agentReviewAction(opts) {
|
|
24277
|
+
return stripUndefined({
|
|
24278
|
+
id: actionId("agent", opts.agentPrompt),
|
|
24279
|
+
kind: "agent",
|
|
24280
|
+
label: opts.label,
|
|
24281
|
+
agentPrompt: opts.agentPrompt,
|
|
24282
|
+
fixOwner: "host-agent",
|
|
24283
|
+
costTier: "unknown",
|
|
24284
|
+
safeToAutoRun: false,
|
|
24285
|
+
requiresConfirmation: false,
|
|
24286
|
+
reason: opts.reason,
|
|
24287
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
24288
|
+
});
|
|
24289
|
+
}
|
|
24290
|
+
function manualReviewAction(opts) {
|
|
24291
|
+
return stripUndefined({
|
|
24292
|
+
id: actionId("manual", opts.label),
|
|
24293
|
+
kind: "manual",
|
|
24294
|
+
label: opts.label,
|
|
24295
|
+
fixOwner: opts.fixOwner ?? "host-agent",
|
|
24296
|
+
costTier: "unknown",
|
|
24297
|
+
safeToAutoRun: false,
|
|
24298
|
+
requiresConfirmation: false,
|
|
24299
|
+
reason: opts.reason,
|
|
24300
|
+
sourceIssueCodes: uniqueStrings(opts.sourceIssueCodes ?? [])
|
|
24301
|
+
});
|
|
24302
|
+
}
|
|
24303
|
+
function reviewActionsFromRetryWith(retryWith, sourceIssueCodes = []) {
|
|
24304
|
+
return normalizeReviewActions(
|
|
24305
|
+
retryWith.map((item) => {
|
|
24306
|
+
if (/^codex\s+/i.test(item)) {
|
|
24307
|
+
return agentReviewAction({
|
|
24308
|
+
label: "Ask the host agent to fix review issues",
|
|
24309
|
+
agentPrompt: item,
|
|
24310
|
+
reason: "The review report contains host-agent-owned issues.",
|
|
24311
|
+
sourceIssueCodes
|
|
24312
|
+
});
|
|
24313
|
+
}
|
|
24314
|
+
return commandReviewAction(item, {
|
|
24315
|
+
reason: "Backward-compatible retryWith command.",
|
|
24316
|
+
sourceIssueCodes
|
|
24317
|
+
});
|
|
24318
|
+
})
|
|
24319
|
+
);
|
|
24320
|
+
}
|
|
24321
|
+
function normalizeReviewActions(actions) {
|
|
24322
|
+
const merged = /* @__PURE__ */ new Map();
|
|
24323
|
+
for (const action of actions) {
|
|
24324
|
+
const key = [action.kind, action.command ?? action.agentPrompt ?? action.label].join("\0");
|
|
24325
|
+
const existing = merged.get(key);
|
|
24326
|
+
if (!existing) {
|
|
24327
|
+
merged.set(key, {
|
|
24328
|
+
...action,
|
|
24329
|
+
sourceIssueCodes: uniqueStrings(action.sourceIssueCodes ?? [])
|
|
24330
|
+
});
|
|
24331
|
+
continue;
|
|
24332
|
+
}
|
|
24333
|
+
merged.set(key, {
|
|
24334
|
+
...existing,
|
|
24335
|
+
safeToAutoRun: existing.safeToAutoRun && action.safeToAutoRun,
|
|
24336
|
+
requiresConfirmation: existing.requiresConfirmation || action.requiresConfirmation,
|
|
24337
|
+
sourceIssueCodes: uniqueStrings([
|
|
24338
|
+
...existing.sourceIssueCodes ?? [],
|
|
24339
|
+
...action.sourceIssueCodes ?? []
|
|
24340
|
+
])
|
|
24341
|
+
});
|
|
24342
|
+
}
|
|
24343
|
+
return [...merged.values()].map(
|
|
24344
|
+
(action) => stripUndefined({
|
|
24345
|
+
...action,
|
|
24346
|
+
sourceIssueCodes: action.sourceIssueCodes && action.sourceIssueCodes.length > 0 ? action.sourceIssueCodes : void 0
|
|
24347
|
+
})
|
|
24348
|
+
);
|
|
24349
|
+
}
|
|
24350
|
+
function deriveNextReviewActions(opts) {
|
|
24351
|
+
return normalizeReviewActions([
|
|
24352
|
+
...opts.issues.flatMap((issue) => issue.actions ?? []),
|
|
24353
|
+
...reviewActionsFromRetryWith(opts.retryWith)
|
|
24354
|
+
]);
|
|
24355
|
+
}
|
|
24356
|
+
function fixOwnerForIssue(issue) {
|
|
24357
|
+
if (issue.fixOwner) return issue.fixOwner;
|
|
24358
|
+
if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
|
|
24359
|
+
if (issue.code.startsWith("STORYBOARD_")) return "host-agent";
|
|
24360
|
+
if (issue.code === "PROJECT_NOT_FOUND" || issue.code === "MISSING_STORYBOARD" || issue.code === "BEAT_NOT_FOUND" || issue.code === "MISSING_DESIGN") {
|
|
24361
|
+
return "host-agent";
|
|
24362
|
+
}
|
|
24363
|
+
return "vibe";
|
|
24364
|
+
}
|
|
24365
|
+
function normalizeReviewIssues(issues, opts) {
|
|
24366
|
+
const fallbackOwner = typeof opts === "string" ? opts : opts?.fallbackOwner;
|
|
24367
|
+
const projectDir = typeof opts === "string" ? void 0 : opts?.projectDir;
|
|
24368
|
+
return issues.map((issue) => {
|
|
24369
|
+
const normalized = {
|
|
24370
|
+
...issue,
|
|
24371
|
+
fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
|
|
24372
|
+
};
|
|
24373
|
+
const actions = normalizeReviewActions([
|
|
24374
|
+
...issue.actions ?? [],
|
|
24375
|
+
...projectDir ? defaultActionsForIssue(normalized, projectDir) : []
|
|
24376
|
+
]);
|
|
24377
|
+
return stripUndefined({
|
|
24378
|
+
...normalized,
|
|
24379
|
+
actions: actions.length > 0 ? actions : void 0
|
|
24380
|
+
});
|
|
24381
|
+
});
|
|
24382
|
+
}
|
|
24383
|
+
function summarizeReviewIssues(issues) {
|
|
24384
|
+
const normalized = normalizeReviewIssues(issues);
|
|
24385
|
+
return {
|
|
24386
|
+
issueCount: normalized.length,
|
|
24387
|
+
errorCount: normalized.filter((issue) => issue.severity === "error").length,
|
|
24388
|
+
warningCount: normalized.filter((issue) => issue.severity === "warning").length,
|
|
24389
|
+
infoCount: normalized.filter((issue) => issue.severity === "info").length,
|
|
24390
|
+
fixOwners: {
|
|
24391
|
+
vibe: normalized.filter((issue) => issue.fixOwner === "vibe").length,
|
|
24392
|
+
hostAgent: normalized.filter((issue) => issue.fixOwner === "host-agent").length
|
|
24393
|
+
}
|
|
24394
|
+
};
|
|
24395
|
+
}
|
|
24396
|
+
function buildReviewReport(opts) {
|
|
24397
|
+
const retryWith = uniqueRetryWith(opts.retryWith);
|
|
24398
|
+
const issues = normalizeReviewIssues(opts.issues, {
|
|
24399
|
+
projectDir: opts.project,
|
|
24400
|
+
retryWith
|
|
24401
|
+
});
|
|
24402
|
+
return stripUndefined({
|
|
24403
|
+
schemaVersion: "1",
|
|
24404
|
+
kind: "review",
|
|
24405
|
+
project: resolve5(opts.project),
|
|
24406
|
+
mode: opts.mode,
|
|
24407
|
+
beat: opts.beat,
|
|
24408
|
+
status: opts.status,
|
|
24409
|
+
score: opts.score,
|
|
24410
|
+
issues,
|
|
24411
|
+
summary: summarizeReviewIssues(issues),
|
|
24412
|
+
nextActions: deriveNextReviewActions({ issues, retryWith, projectDir: opts.project }),
|
|
24413
|
+
retryWith,
|
|
24414
|
+
sourceReports: opts.sourceReports ?? [],
|
|
24415
|
+
reportPath: opts.reportPath
|
|
24416
|
+
});
|
|
24417
|
+
}
|
|
24418
|
+
function defaultReviewReportPath(projectDir) {
|
|
24419
|
+
return join2(resolve5(projectDir), "review-report.json");
|
|
24420
|
+
}
|
|
24421
|
+
async function writeReviewReport(path2, report) {
|
|
24422
|
+
await writeFile6(path2, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
24423
|
+
}
|
|
24424
|
+
function stripUndefined(value) {
|
|
24425
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
24426
|
+
}
|
|
24427
|
+
function defaultActionsForIssue(issue, projectDir) {
|
|
24428
|
+
const code = issue.code;
|
|
24429
|
+
const beat = issue.beatId ?? issue.scene;
|
|
24430
|
+
const beatFlag = beat ? ` --beat ${beat}` : "";
|
|
24431
|
+
const sourceIssueCodes = [code];
|
|
24432
|
+
if (code === "PROJECT_NOT_FOUND" || code === "MISSING_STORYBOARD") {
|
|
24433
|
+
return [
|
|
24434
|
+
commandReviewAction(`vibe init ${projectDir} --from "<brief>" --json`, {
|
|
24435
|
+
label: "Initialize the project from a brief",
|
|
24436
|
+
fixOwner: "host-agent",
|
|
24437
|
+
reason: "The project cannot be inspected until the core project files exist.",
|
|
24438
|
+
sourceIssueCodes
|
|
24439
|
+
})
|
|
24440
|
+
];
|
|
24441
|
+
}
|
|
24442
|
+
if (code === "MISSING_DESIGN" || code === "DESIGN_PLACEHOLDER_FIELD") {
|
|
24443
|
+
return [
|
|
24444
|
+
manualReviewAction({
|
|
24445
|
+
label: "Create or complete DESIGN.md",
|
|
24446
|
+
reason: "Visual direction needs host-agent or human input before VibeFrame can repair it.",
|
|
24447
|
+
sourceIssueCodes
|
|
24448
|
+
})
|
|
24449
|
+
];
|
|
24450
|
+
}
|
|
24451
|
+
if (code === "BEAT_NOT_FOUND" || code.startsWith("STORYBOARD_")) {
|
|
24452
|
+
const actions = [
|
|
24453
|
+
commandReviewAction(`vibe storyboard validate ${projectDir} --json`, {
|
|
24454
|
+
label: "Validate the storyboard",
|
|
24455
|
+
fixOwner: "host-agent",
|
|
24456
|
+
reason: "The storyboard issue needs structured validation before editing.",
|
|
24457
|
+
sourceIssueCodes
|
|
24458
|
+
})
|
|
24459
|
+
];
|
|
24460
|
+
if (code === "STORYBOARD_PLACEHOLDER_CUE") {
|
|
24461
|
+
actions.push(
|
|
24462
|
+
commandReviewAction(
|
|
24463
|
+
`vibe storyboard revise ${projectDir} --from "<make cues concrete>" --dry-run --json`,
|
|
24464
|
+
{
|
|
24465
|
+
label: "Preview a storyboard revision",
|
|
24466
|
+
fixOwner: "host-agent",
|
|
24467
|
+
reason: "Placeholder cues should be revised into concrete video intent.",
|
|
24468
|
+
sourceIssueCodes
|
|
24469
|
+
}
|
|
24470
|
+
)
|
|
24471
|
+
);
|
|
24472
|
+
}
|
|
24473
|
+
return actions;
|
|
24474
|
+
}
|
|
24475
|
+
if (code === "MISSING_ROOT_COMPOSITION" || code === "ROOT_SYNC_CHECK_FAILED") {
|
|
24476
|
+
return [
|
|
24477
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
24478
|
+
label: "Sync the project root composition",
|
|
24479
|
+
reason: "The root composition is missing or could not be verified.",
|
|
24480
|
+
sourceIssueCodes
|
|
24481
|
+
})
|
|
24482
|
+
];
|
|
24483
|
+
}
|
|
24484
|
+
if (code.startsWith("ROOT_")) {
|
|
24485
|
+
return issue.fixOwner === "host-agent" ? [
|
|
24486
|
+
agentReviewAction({
|
|
24487
|
+
label: "Repair the root composition shell",
|
|
24488
|
+
agentPrompt: "Fix host-agent-owned root composition issues from review-report.json",
|
|
24489
|
+
reason: "The root composition cannot be repaired deterministically.",
|
|
24490
|
+
sourceIssueCodes
|
|
24491
|
+
})
|
|
24492
|
+
] : [
|
|
24493
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
24494
|
+
label: "Repair deterministic scene/root issues",
|
|
24495
|
+
reason: "The root composition has deterministic sync issues.",
|
|
24496
|
+
sourceIssueCodes
|
|
24497
|
+
}),
|
|
24498
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
24499
|
+
label: "Resync root composition after repair",
|
|
24500
|
+
reason: "The root composition should be synced after deterministic repair.",
|
|
24501
|
+
sourceIssueCodes
|
|
24502
|
+
})
|
|
24503
|
+
];
|
|
24504
|
+
}
|
|
24505
|
+
if (code === "MISSING_COMPOSITION") {
|
|
24506
|
+
return [
|
|
24507
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage compose --json`, {
|
|
24508
|
+
label: beat ? `Compose beat ${beat}` : "Compose missing scenes",
|
|
24509
|
+
reason: "Missing scene compositions must be generated or authored before render.",
|
|
24510
|
+
sourceIssueCodes
|
|
24511
|
+
})
|
|
24512
|
+
];
|
|
24513
|
+
}
|
|
24514
|
+
if (code === "MISSING_BUILD_REPORT") {
|
|
24515
|
+
return [
|
|
24516
|
+
commandReviewAction(`vibe build ${projectDir} --dry-run --json`, {
|
|
24517
|
+
label: "Preview the build plan",
|
|
24518
|
+
reason: "A dry-run build report is needed before choosing a paid or mutating build step.",
|
|
24519
|
+
sourceIssueCodes
|
|
24520
|
+
})
|
|
24521
|
+
];
|
|
24522
|
+
}
|
|
24523
|
+
if (code === "BUILD_REPORT_BEAT_MISSING") {
|
|
24524
|
+
return [
|
|
24525
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
24526
|
+
label: beat ? `Sync build report for beat ${beat}` : "Sync the build report",
|
|
24527
|
+
reason: "The build report does not include the inspected beat.",
|
|
24528
|
+
sourceIssueCodes
|
|
24529
|
+
})
|
|
24530
|
+
];
|
|
24531
|
+
}
|
|
24532
|
+
if (code === "MALFORMED_BUILD_REPORT") {
|
|
24533
|
+
return [
|
|
24534
|
+
commandReviewAction(`vibe build ${projectDir} --json`, {
|
|
24535
|
+
label: "Regenerate the build report",
|
|
24536
|
+
reason: "The existing build report cannot be parsed.",
|
|
24537
|
+
sourceIssueCodes
|
|
24538
|
+
})
|
|
24539
|
+
];
|
|
24540
|
+
}
|
|
24541
|
+
if (code === "MISSING_REPORTED_ASSET" || code === "STALE_ASSET" || code === "UNKNOWN_ASSET_FRESHNESS") {
|
|
24542
|
+
return [
|
|
24543
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --force --json`, {
|
|
24544
|
+
label: beat ? `Regenerate assets for beat ${beat}` : "Regenerate stale or missing assets",
|
|
24545
|
+
reason: "Asset generation may call providers, so the host should confirm first.",
|
|
24546
|
+
sourceIssueCodes
|
|
24547
|
+
})
|
|
24548
|
+
];
|
|
24549
|
+
}
|
|
24550
|
+
if (code === "MUSIC_CUE_NOT_READY") {
|
|
24551
|
+
return [
|
|
24552
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage assets --json`, {
|
|
24553
|
+
label: beat ? `Generate music for beat ${beat}` : "Generate missing music assets",
|
|
24554
|
+
reason: "Music generation may call providers, so the host should confirm first.",
|
|
24555
|
+
sourceIssueCodes
|
|
24556
|
+
}),
|
|
24557
|
+
commandReviewAction(`vibe build ${projectDir} --stage sync --json`, {
|
|
24558
|
+
label: "Sync music into the root composition",
|
|
24559
|
+
reason: "Generated music needs root composition wiring.",
|
|
24560
|
+
sourceIssueCodes
|
|
24561
|
+
})
|
|
24562
|
+
];
|
|
24563
|
+
}
|
|
24564
|
+
if (code.startsWith("SCENE_LINT_")) {
|
|
24565
|
+
return [
|
|
24566
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
24567
|
+
label: "Repair deterministic scene lint issues",
|
|
24568
|
+
reason: "Scene lint reported an issue VibeFrame may be able to repair mechanically.",
|
|
24569
|
+
sourceIssueCodes
|
|
24570
|
+
})
|
|
24571
|
+
];
|
|
24572
|
+
}
|
|
24573
|
+
if (code === "RENDER_NOT_FOUND" || code === "EMPTY_RENDER") {
|
|
24574
|
+
return [
|
|
24575
|
+
commandReviewAction(
|
|
24576
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
24577
|
+
{
|
|
24578
|
+
label: beat ? `Render beat ${beat}` : "Render the project",
|
|
24579
|
+
reason: "A render file is required before render inspection can pass.",
|
|
24580
|
+
sourceIssueCodes
|
|
24581
|
+
}
|
|
24582
|
+
)
|
|
24583
|
+
];
|
|
24584
|
+
}
|
|
24585
|
+
if (code === "NO_AUDIO_STREAM" || code === "DURATION_DRIFT") {
|
|
24586
|
+
return [
|
|
24587
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
24588
|
+
label: beat ? `Sync timing/audio for beat ${beat}` : "Sync timing and audio wiring",
|
|
24589
|
+
reason: "The render output no longer matches synced project state.",
|
|
24590
|
+
sourceIssueCodes
|
|
24591
|
+
}),
|
|
24592
|
+
commandReviewAction(
|
|
24593
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
24594
|
+
{
|
|
24595
|
+
label: beat ? `Rerender beat ${beat}` : "Rerender the project",
|
|
24596
|
+
reason: "The render should be regenerated after sync repair.",
|
|
24597
|
+
sourceIssueCodes
|
|
24598
|
+
}
|
|
24599
|
+
)
|
|
24600
|
+
];
|
|
24601
|
+
}
|
|
24602
|
+
if (code === "BLACK_FRAME_SEGMENT") {
|
|
24603
|
+
return issue.fixOwner === "host-agent" ? [
|
|
24604
|
+
agentReviewAction({
|
|
24605
|
+
label: beat ? `Fix black frames in beat ${beat}` : "Fix black frame segment",
|
|
24606
|
+
agentPrompt: "Fix black-frame issues from review-report.json, then rerender.",
|
|
24607
|
+
reason: "The issue likely needs scene, storyboard, or visual cue changes.",
|
|
24608
|
+
sourceIssueCodes
|
|
24609
|
+
})
|
|
24610
|
+
] : [
|
|
24611
|
+
commandReviewAction(`vibe scene repair ${projectDir} --json`, {
|
|
24612
|
+
label: "Repair deterministic scene timing",
|
|
24613
|
+
reason: "Black frames outside a specific beat may be recoverable with scene repair.",
|
|
24614
|
+
sourceIssueCodes
|
|
24615
|
+
})
|
|
24616
|
+
];
|
|
24617
|
+
}
|
|
24618
|
+
if (code === "STATIC_FRAME_SEGMENT") {
|
|
24619
|
+
return [
|
|
24620
|
+
agentReviewAction({
|
|
24621
|
+
label: beat ? `Add motion to beat ${beat}` : "Fix static visual hold",
|
|
24622
|
+
agentPrompt: "Fix static-frame issues from review-report.json, then rerender.",
|
|
24623
|
+
reason: "Static segments usually need creative scene, storyboard, or asset changes.",
|
|
24624
|
+
sourceIssueCodes
|
|
24625
|
+
})
|
|
24626
|
+
];
|
|
24627
|
+
}
|
|
24628
|
+
if (code === "LONG_SILENCE") {
|
|
24629
|
+
return issue.fixOwner === "host-agent" ? [
|
|
24630
|
+
agentReviewAction({
|
|
24631
|
+
label: beat ? `Fix long silence in beat ${beat}` : "Fix long silence",
|
|
24632
|
+
agentPrompt: "Fix long-silence issues from review-report.json, then rerender.",
|
|
24633
|
+
reason: "The beat duration, narration, or music cue needs host-agent judgment.",
|
|
24634
|
+
sourceIssueCodes
|
|
24635
|
+
})
|
|
24636
|
+
] : [
|
|
24637
|
+
commandReviewAction(`vibe build ${projectDir}${beatFlag} --stage sync --json`, {
|
|
24638
|
+
label: beat ? `Sync audio for beat ${beat}` : "Sync audio wiring",
|
|
24639
|
+
reason: "The silence may be caused by stale narration or music wiring.",
|
|
24640
|
+
sourceIssueCodes
|
|
24641
|
+
})
|
|
24642
|
+
];
|
|
24643
|
+
}
|
|
24644
|
+
if (code.startsWith("AI_REVIEW_")) {
|
|
24645
|
+
return [
|
|
24646
|
+
agentReviewAction({
|
|
24647
|
+
label: "Fix AI review findings",
|
|
24648
|
+
agentPrompt: "Fix host-agent-owned AI review issues from review-report.json, then rerender.",
|
|
24649
|
+
reason: "AI review findings require storyboard, design, or composition judgment.",
|
|
24650
|
+
sourceIssueCodes
|
|
24651
|
+
})
|
|
24652
|
+
];
|
|
24653
|
+
}
|
|
24654
|
+
if (code === "FFPROBE_UNAVAILABLE" || code === "FFMPEG_UNAVAILABLE") {
|
|
24655
|
+
return [
|
|
24656
|
+
manualReviewAction({
|
|
24657
|
+
label: "Install FFmpeg and rerun inspection",
|
|
24658
|
+
reason: "Local media inspection depends on ffmpeg/ffprobe being available.",
|
|
24659
|
+
fixOwner: "host-agent",
|
|
24660
|
+
sourceIssueCodes
|
|
24661
|
+
})
|
|
24662
|
+
];
|
|
24663
|
+
}
|
|
24664
|
+
if (code === "FFPROBE_FAILED" || code === "NO_VIDEO_STREAM") {
|
|
24665
|
+
return [
|
|
24666
|
+
commandReviewAction(
|
|
24667
|
+
beat ? `vibe render ${projectDir} --beat ${beat} --json` : `vibe render ${projectDir} --json`,
|
|
24668
|
+
{
|
|
24669
|
+
label: beat ? `Rerender beat ${beat}` : "Rerender the project",
|
|
24670
|
+
reason: "The rendered media could not be probed as a valid video.",
|
|
24671
|
+
sourceIssueCodes
|
|
24672
|
+
}
|
|
24673
|
+
)
|
|
24674
|
+
];
|
|
24675
|
+
}
|
|
24676
|
+
if (code === "ASPECT_MISMATCH") {
|
|
24677
|
+
return [
|
|
24678
|
+
manualReviewAction({
|
|
24679
|
+
label: "Check project aspect and render settings",
|
|
24680
|
+
reason: "Aspect mismatches can come from either config intent or render settings.",
|
|
24681
|
+
fixOwner: issue.fixOwner,
|
|
24682
|
+
sourceIssueCodes
|
|
24683
|
+
})
|
|
24684
|
+
];
|
|
24685
|
+
}
|
|
24686
|
+
return [];
|
|
24687
|
+
}
|
|
24688
|
+
function classifyReviewCommand(command) {
|
|
24689
|
+
const lower = command.trim().toLowerCase();
|
|
24690
|
+
if (lower.startsWith("vibe status ")) return safe("free");
|
|
24691
|
+
if (lower.startsWith("vibe scene repair")) return safe("free");
|
|
24692
|
+
if (lower.startsWith("vibe storyboard validate") || lower.startsWith("vibe storyboard get") || lower.startsWith("vibe storyboard list")) {
|
|
24693
|
+
return safe("free", "host-agent");
|
|
24694
|
+
}
|
|
24695
|
+
if (lower.startsWith("vibe inspect project")) return safe("free");
|
|
24696
|
+
if (lower.startsWith("vibe inspect render")) {
|
|
24697
|
+
if (lower.includes("--ai")) return unsafe("low");
|
|
24698
|
+
return safe("free");
|
|
24699
|
+
}
|
|
24700
|
+
if (lower.startsWith("vibe render ")) return safe("free");
|
|
24701
|
+
if (lower.startsWith("vibe build ")) {
|
|
24702
|
+
if (lower.includes("--dry-run")) return safe("free");
|
|
24703
|
+
if (lower.includes("--stage sync") || lower.includes("--stage render")) return safe("free");
|
|
24704
|
+
return unsafe("unknown");
|
|
24705
|
+
}
|
|
24706
|
+
if (lower.startsWith("vibe generate video") || lower.startsWith("vibe edit fill-gaps") || lower.startsWith("vibe remix ")) {
|
|
24707
|
+
return unsafe("very-high");
|
|
24708
|
+
}
|
|
24709
|
+
if (lower.startsWith("vibe storyboard revise")) return unsafe("unknown", "host-agent");
|
|
24710
|
+
return unsafe("unknown");
|
|
24711
|
+
}
|
|
24712
|
+
function safe(costTier, fixOwner = "vibe") {
|
|
24713
|
+
return { fixOwner, costTier, safeToAutoRun: true, requiresConfirmation: false };
|
|
24714
|
+
}
|
|
24715
|
+
function unsafe(costTier, fixOwner = "vibe") {
|
|
24716
|
+
return { fixOwner, costTier, safeToAutoRun: false, requiresConfirmation: true };
|
|
24717
|
+
}
|
|
24718
|
+
function actionId(kind, value) {
|
|
24719
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
24720
|
+
return `${kind}:${slug || "action"}`;
|
|
24721
|
+
}
|
|
24722
|
+
function uniqueStrings(items) {
|
|
24723
|
+
return [
|
|
24724
|
+
...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
|
|
24725
|
+
];
|
|
24726
|
+
}
|
|
24727
|
+
var init_review_report = __esm({
|
|
24728
|
+
"src/commands/_shared/review-report.ts"() {
|
|
24729
|
+
"use strict";
|
|
24730
|
+
}
|
|
24731
|
+
});
|
|
24732
|
+
|
|
24221
24733
|
// src/commands/_shared/storyboard-parse.ts
|
|
24734
|
+
function resolveCharacters(frontmatter) {
|
|
24735
|
+
const pool = frontmatter?.characters;
|
|
24736
|
+
if (!pool || typeof pool !== "object") return [];
|
|
24737
|
+
const resolved = [];
|
|
24738
|
+
for (const [name, spec] of Object.entries(pool)) {
|
|
24739
|
+
if (typeof spec === "string") {
|
|
24740
|
+
const prompt3 = spec.trim();
|
|
24741
|
+
if (prompt3) resolved.push({ name, prompt: prompt3 });
|
|
24742
|
+
continue;
|
|
24743
|
+
}
|
|
24744
|
+
if (spec && typeof spec === "object") {
|
|
24745
|
+
const image = typeof spec.image === "string" ? spec.image.trim() : "";
|
|
24746
|
+
if (image) {
|
|
24747
|
+
resolved.push({ name, imagePath: image });
|
|
24748
|
+
continue;
|
|
24749
|
+
}
|
|
24750
|
+
const description = typeof spec.description === "string" ? spec.description.trim() : "";
|
|
24751
|
+
if (description) resolved.push({ name, prompt: description });
|
|
24752
|
+
}
|
|
24753
|
+
}
|
|
24754
|
+
return resolved;
|
|
24755
|
+
}
|
|
24756
|
+
function beatCharacterNames(cues) {
|
|
24757
|
+
const raw = cues?.characters;
|
|
24758
|
+
if (typeof raw === "string") {
|
|
24759
|
+
const name = raw.trim();
|
|
24760
|
+
return name ? [name] : [];
|
|
24761
|
+
}
|
|
24762
|
+
if (Array.isArray(raw)) {
|
|
24763
|
+
return raw.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
|
|
24764
|
+
}
|
|
24765
|
+
return [];
|
|
24766
|
+
}
|
|
24222
24767
|
function parseStoryboard(md) {
|
|
24223
24768
|
const normalized = md.replace(/\r\n/g, "\n");
|
|
24224
24769
|
const { frontmatter, remaining } = extractProjectFrontmatter(normalized);
|
|
@@ -24331,28 +24876,28 @@ var init_storyboard_parse = __esm({
|
|
|
24331
24876
|
// src/commands/_shared/status-jobs.ts
|
|
24332
24877
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
24333
24878
|
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
24334
|
-
import { copyFile, mkdir as mkdir3, readFile as readFile4, readdir, stat, writeFile as
|
|
24335
|
-
import { dirname as dirname3, join as
|
|
24879
|
+
import { copyFile, mkdir as mkdir3, readFile as readFile4, readdir, stat, writeFile as writeFile7 } from "node:fs/promises";
|
|
24880
|
+
import { dirname as dirname3, join as join3, parse as parse2, relative, resolve as resolve6 } from "node:path";
|
|
24336
24881
|
function isLocalJob(record) {
|
|
24337
24882
|
return record.jobType === "build" || record.jobType === "render";
|
|
24338
24883
|
}
|
|
24339
24884
|
function findProjectRoot(start = process.cwd()) {
|
|
24340
|
-
let dir =
|
|
24885
|
+
let dir = resolve6(start);
|
|
24341
24886
|
try {
|
|
24342
24887
|
if (existsSync2(dir) && statSync(dir).isFile()) dir = dirname3(dir);
|
|
24343
24888
|
} catch {
|
|
24344
24889
|
}
|
|
24345
24890
|
for (; ; ) {
|
|
24346
|
-
if (existsSync2(
|
|
24891
|
+
if (existsSync2(join3(dir, "vibe.config.json")) || existsSync2(join3(dir, "STORYBOARD.md"))) {
|
|
24347
24892
|
return dir;
|
|
24348
24893
|
}
|
|
24349
24894
|
const parent = dirname3(dir);
|
|
24350
|
-
if (parent === dir) return
|
|
24895
|
+
if (parent === dir) return resolve6(start);
|
|
24351
24896
|
dir = parent;
|
|
24352
24897
|
}
|
|
24353
24898
|
}
|
|
24354
24899
|
function jobsDir(projectDir) {
|
|
24355
|
-
return
|
|
24900
|
+
return join3(resolve6(projectDir), ".vibeframe", "jobs");
|
|
24356
24901
|
}
|
|
24357
24902
|
function normalizeJobStatus(status) {
|
|
24358
24903
|
const value = String(status ?? "").toLowerCase();
|
|
@@ -24366,10 +24911,10 @@ function normalizeJobStatus(status) {
|
|
|
24366
24911
|
}
|
|
24367
24912
|
function createJobRecord(opts) {
|
|
24368
24913
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
24369
|
-
const projectDir =
|
|
24914
|
+
const projectDir = resolve6(
|
|
24370
24915
|
opts.projectDir ?? findProjectRoot(opts.workingDirectory ?? process.cwd())
|
|
24371
24916
|
);
|
|
24372
|
-
const workingDirectory =
|
|
24917
|
+
const workingDirectory = resolve6(opts.workingDirectory ?? process.cwd());
|
|
24373
24918
|
const id = opts.id ?? makeJobId(opts.providerTaskId, now);
|
|
24374
24919
|
const base = {
|
|
24375
24920
|
schemaVersion: "1",
|
|
@@ -24404,7 +24949,7 @@ function createJobRecord(opts) {
|
|
|
24404
24949
|
message: opts.message
|
|
24405
24950
|
};
|
|
24406
24951
|
base.retryWith = retryWithForJob(base);
|
|
24407
|
-
return
|
|
24952
|
+
return stripUndefined2(base);
|
|
24408
24953
|
}
|
|
24409
24954
|
async function createAndWriteJobRecord(opts) {
|
|
24410
24955
|
const record = createJobRecord(opts);
|
|
@@ -24414,15 +24959,15 @@ async function createAndWriteJobRecord(opts) {
|
|
|
24414
24959
|
async function writeJobRecord(record) {
|
|
24415
24960
|
const dir = jobsDir(record.projectDir);
|
|
24416
24961
|
await mkdir3(dir, { recursive: true });
|
|
24417
|
-
await
|
|
24962
|
+
await writeFile7(
|
|
24418
24963
|
jobRecordPath(record.id, record.projectDir),
|
|
24419
|
-
JSON.stringify(
|
|
24964
|
+
JSON.stringify(stripUndefined2(record), null, 2) + "\n",
|
|
24420
24965
|
"utf-8"
|
|
24421
24966
|
);
|
|
24422
24967
|
}
|
|
24423
24968
|
async function readJobRecord(jobId, projectDir) {
|
|
24424
24969
|
const id = normalizeJobId(jobId);
|
|
24425
|
-
const project =
|
|
24970
|
+
const project = resolve6(projectDir ?? findProjectRoot());
|
|
24426
24971
|
const path2 = jobRecordPath(id, project);
|
|
24427
24972
|
if (!existsSync2(path2)) return null;
|
|
24428
24973
|
return parseJobRecord(await readFile4(path2, "utf-8"));
|
|
@@ -24435,7 +24980,7 @@ async function listJobRecords(projectDir) {
|
|
|
24435
24980
|
for (const entry of entries) {
|
|
24436
24981
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
24437
24982
|
try {
|
|
24438
|
-
const parsed = parseJobRecord(await readFile4(
|
|
24983
|
+
const parsed = parseJobRecord(await readFile4(join3(dir, entry.name), "utf-8"));
|
|
24439
24984
|
if (parsed) records.push(parsed);
|
|
24440
24985
|
} catch {
|
|
24441
24986
|
}
|
|
@@ -24529,9 +25074,9 @@ async function refreshLocalJobRecord(record, opts) {
|
|
|
24529
25074
|
const heartbeatAge = record.heartbeatAt ? Date.now() - new Date(record.heartbeatAt).getTime() : Number.POSITIVE_INFINITY;
|
|
24530
25075
|
const orphaned = !isPidAlive(record.pid) || heartbeatAge > LOCAL_JOB_ORPHAN_AFTER_MS;
|
|
24531
25076
|
if (orphaned) {
|
|
24532
|
-
const hint = `The process that ran this ${record.jobType} job is gone. Check ${
|
|
25077
|
+
const hint = `The process that ran this ${record.jobType} job is gone. Check ${join3(record.projectDir, record.jobType === "render" ? "render-report.json" : "build-report.json")} \u2014 the work may have completed before the process exited.`;
|
|
24533
25078
|
warnings.push(hint);
|
|
24534
|
-
updated =
|
|
25079
|
+
updated = stripUndefined2({
|
|
24535
25080
|
...record,
|
|
24536
25081
|
status: "unknown",
|
|
24537
25082
|
error: record.error ?? hint,
|
|
@@ -24548,7 +25093,7 @@ async function refreshLocalJobRecord(record, opts) {
|
|
|
24548
25093
|
});
|
|
24549
25094
|
}
|
|
24550
25095
|
async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
24551
|
-
const project =
|
|
25096
|
+
const project = resolve6(projectDirArg);
|
|
24552
25097
|
const warnings = [];
|
|
24553
25098
|
let records = await listJobRecords(project);
|
|
24554
25099
|
if (opts.refresh) {
|
|
@@ -24578,6 +25123,10 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
24578
25123
|
...review?.retryWith ?? [],
|
|
24579
25124
|
...workflow.retryWith
|
|
24580
25125
|
];
|
|
25126
|
+
const nextActions = normalizeReviewActions([
|
|
25127
|
+
...review?.nextActions ?? [],
|
|
25128
|
+
...reviewActionsFromRetryWith([...build?.retryWith ?? [], ...workflow.retryWith])
|
|
25129
|
+
]);
|
|
24581
25130
|
return {
|
|
24582
25131
|
schemaVersion: "1",
|
|
24583
25132
|
kind: "project",
|
|
@@ -24589,11 +25138,12 @@ async function inspectProjectStatus(projectDirArg, opts = {}) {
|
|
|
24589
25138
|
review,
|
|
24590
25139
|
jobs,
|
|
24591
25140
|
warnings,
|
|
25141
|
+
nextActions,
|
|
24592
25142
|
retryWith: unique(retryWith)
|
|
24593
25143
|
};
|
|
24594
25144
|
}
|
|
24595
25145
|
function summarizeJob(record) {
|
|
24596
|
-
return
|
|
25146
|
+
return stripUndefined2({
|
|
24597
25147
|
id: record.id,
|
|
24598
25148
|
jobType: record.jobType,
|
|
24599
25149
|
status: record.status,
|
|
@@ -24617,7 +25167,7 @@ function retryWithForJob(record) {
|
|
|
24617
25167
|
]);
|
|
24618
25168
|
}
|
|
24619
25169
|
function makeJobStatusResult(record, opts) {
|
|
24620
|
-
return
|
|
25170
|
+
return stripUndefined2({
|
|
24621
25171
|
schemaVersion: "1",
|
|
24622
25172
|
kind: "job",
|
|
24623
25173
|
id: record.id,
|
|
@@ -24641,7 +25191,7 @@ function jobProgress(record) {
|
|
|
24641
25191
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") {
|
|
24642
25192
|
if (record.progress === void 0 && !record.providerStatus) return void 0;
|
|
24643
25193
|
}
|
|
24644
|
-
return
|
|
25194
|
+
return stripUndefined2({
|
|
24645
25195
|
percent: record.progress,
|
|
24646
25196
|
phase: record.stage ?? record.providerStatus ?? statusPhase(record.status),
|
|
24647
25197
|
providerStatus: record.providerStatus ?? record.message
|
|
@@ -24651,7 +25201,7 @@ function jobResult(record) {
|
|
|
24651
25201
|
if (!record.resultUrl && !record.outputPath && !record.cachePath && !record.error && !record.resultPayload) {
|
|
24652
25202
|
return null;
|
|
24653
25203
|
}
|
|
24654
|
-
return
|
|
25204
|
+
return stripUndefined2({
|
|
24655
25205
|
url: record.resultUrl,
|
|
24656
25206
|
outputPath: record.outputPath,
|
|
24657
25207
|
cachePath: record.cachePath,
|
|
@@ -24687,7 +25237,7 @@ function normalizeJobId(jobId) {
|
|
|
24687
25237
|
return id;
|
|
24688
25238
|
}
|
|
24689
25239
|
function jobRecordPath(jobId, projectDir) {
|
|
24690
|
-
return
|
|
25240
|
+
return join3(jobsDir(projectDir), `${normalizeJobId(jobId)}.json`);
|
|
24691
25241
|
}
|
|
24692
25242
|
function parseJobRecord(raw) {
|
|
24693
25243
|
const parsed = JSON.parse(raw);
|
|
@@ -24712,11 +25262,11 @@ function updateRecordFromProvider(record, result) {
|
|
|
24712
25262
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
24713
25263
|
};
|
|
24714
25264
|
next.retryWith = retryWithForJob(next);
|
|
24715
|
-
return
|
|
25265
|
+
return stripUndefined2(next);
|
|
24716
25266
|
}
|
|
24717
25267
|
function resolveRefreshOutput(record, opts) {
|
|
24718
|
-
if (opts.output) return
|
|
24719
|
-
return record.outputPath ?
|
|
25268
|
+
if (opts.output) return resolve6(opts.workingDirectory ?? process.cwd(), opts.output);
|
|
25269
|
+
return record.outputPath ? resolve6(record.outputPath) : void 0;
|
|
24720
25270
|
}
|
|
24721
25271
|
function liveSupport(record) {
|
|
24722
25272
|
if (record.jobType === "generate-video" && (record.provider === "grok" || record.provider === "runway" || record.provider === "kling" || record.provider === "veo")) {
|
|
@@ -24741,7 +25291,7 @@ async function downloadUrl(url, outputPath) {
|
|
|
24741
25291
|
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
24742
25292
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
24743
25293
|
await mkdir3(dirname3(outputPath), { recursive: true });
|
|
24744
|
-
await
|
|
25294
|
+
await writeFile7(outputPath, buffer);
|
|
24745
25295
|
return outputPath;
|
|
24746
25296
|
}
|
|
24747
25297
|
async function maybeCacheOutput(record) {
|
|
@@ -24773,7 +25323,7 @@ async function maybeWriteCompletedAssetMetadata(record) {
|
|
|
24773
25323
|
}
|
|
24774
25324
|
}
|
|
24775
25325
|
async function refreshBuildReportFromJobs(projectDir, records) {
|
|
24776
|
-
const reportPath =
|
|
25326
|
+
const reportPath = join3(projectDir, "build-report.json");
|
|
24777
25327
|
const report = await readJson(reportPath);
|
|
24778
25328
|
if (!report || !Array.isArray(report.beats)) return;
|
|
24779
25329
|
let changed = false;
|
|
@@ -24801,7 +25351,7 @@ async function refreshBuildReportFromJobs(projectDir, records) {
|
|
|
24801
25351
|
if (kind === "music" && record.assetOptions && typeof record.assetOptions.duration === "number") {
|
|
24802
25352
|
nested.durationSec = record.assetOptions.duration;
|
|
24803
25353
|
}
|
|
24804
|
-
beatRecord[kind] =
|
|
25354
|
+
beatRecord[kind] = stripUndefined2(nested);
|
|
24805
25355
|
beatRecord[`${kind}Path`] = outputPath;
|
|
24806
25356
|
beatRecord[`${kind}Status`] = "generated";
|
|
24807
25357
|
beatRecord[`${kind}JobId`] = record.id;
|
|
@@ -24821,17 +25371,17 @@ async function refreshBuildReportFromJobs(projectDir, records) {
|
|
|
24821
25371
|
}
|
|
24822
25372
|
if (!changed) return;
|
|
24823
25373
|
try {
|
|
24824
|
-
await
|
|
25374
|
+
await writeFile7(reportPath, JSON.stringify(stripUndefined2(report), null, 2) + "\n", "utf-8");
|
|
24825
25375
|
} catch {
|
|
24826
25376
|
}
|
|
24827
25377
|
}
|
|
24828
25378
|
async function readBuildSummary(projectDir) {
|
|
24829
|
-
const reportPath =
|
|
25379
|
+
const reportPath = join3(projectDir, "build-report.json");
|
|
24830
25380
|
const report = await readJson(reportPath);
|
|
24831
25381
|
if (!report) return null;
|
|
24832
25382
|
const info = await statOrNull(reportPath);
|
|
24833
25383
|
const beatSummary = projectBeatReadinessFromUnknown(report.beatSummary);
|
|
24834
|
-
return
|
|
25384
|
+
return stripUndefined2({
|
|
24835
25385
|
reportPath,
|
|
24836
25386
|
kind: typeof report.kind === "string" ? report.kind : void 0,
|
|
24837
25387
|
success: typeof report.success === "boolean" ? report.success : void 0,
|
|
@@ -24849,7 +25399,7 @@ async function readBuildSummary(projectDir) {
|
|
|
24849
25399
|
});
|
|
24850
25400
|
}
|
|
24851
25401
|
async function readReviewSummary(projectDir) {
|
|
24852
|
-
const reportPath =
|
|
25402
|
+
const reportPath = join3(projectDir, "review-report.json");
|
|
24853
25403
|
const report = await readJson(reportPath);
|
|
24854
25404
|
if (!report) return null;
|
|
24855
25405
|
const info = await statOrNull(reportPath);
|
|
@@ -24857,7 +25407,7 @@ async function readReviewSummary(projectDir) {
|
|
|
24857
25407
|
const summary = report.summary && typeof report.summary === "object" ? report.summary : {};
|
|
24858
25408
|
const fixOwners = summary.fixOwners && typeof summary.fixOwners === "object" ? summary.fixOwners : {};
|
|
24859
25409
|
const computedFixOwners = countIssueFixOwners(issues);
|
|
24860
|
-
return
|
|
25410
|
+
return stripUndefined2({
|
|
24861
25411
|
reportPath,
|
|
24862
25412
|
kind: typeof report.kind === "string" ? report.kind : void 0,
|
|
24863
25413
|
mode: typeof report.mode === "string" ? report.mode : void 0,
|
|
@@ -24882,9 +25432,44 @@ async function readReviewSummary(projectDir) {
|
|
|
24882
25432
|
},
|
|
24883
25433
|
sourceReports: stringArray(report.sourceReports),
|
|
24884
25434
|
updatedAt: info?.mtime.toISOString(),
|
|
25435
|
+
nextActions: reviewActionsFromUnknown(report.nextActions),
|
|
24885
25436
|
retryWith: stringArray(report.retryWith)
|
|
24886
25437
|
});
|
|
24887
25438
|
}
|
|
25439
|
+
function reviewActionsFromUnknown(value) {
|
|
25440
|
+
if (!Array.isArray(value)) return [];
|
|
25441
|
+
return value.flatMap((item) => {
|
|
25442
|
+
if (!item || typeof item !== "object") return [];
|
|
25443
|
+
const record = item;
|
|
25444
|
+
const kind = stringEnum(record.kind, ["command", "agent", "manual"]);
|
|
25445
|
+
const fixOwner = stringEnum(record.fixOwner, ["vibe", "host-agent"]);
|
|
25446
|
+
const costTier = stringEnum(record.costTier, [
|
|
25447
|
+
"free",
|
|
25448
|
+
"low",
|
|
25449
|
+
"high",
|
|
25450
|
+
"very-high",
|
|
25451
|
+
"unknown"
|
|
25452
|
+
]);
|
|
25453
|
+
if (!kind || !fixOwner || !costTier || typeof record.id !== "string" || typeof record.label !== "string" || typeof record.safeToAutoRun !== "boolean" || typeof record.requiresConfirmation !== "boolean" || typeof record.reason !== "string") {
|
|
25454
|
+
return [];
|
|
25455
|
+
}
|
|
25456
|
+
return [
|
|
25457
|
+
stripUndefined2({
|
|
25458
|
+
id: record.id,
|
|
25459
|
+
kind,
|
|
25460
|
+
label: record.label,
|
|
25461
|
+
command: typeof record.command === "string" ? record.command : void 0,
|
|
25462
|
+
agentPrompt: typeof record.agentPrompt === "string" ? record.agentPrompt : void 0,
|
|
25463
|
+
fixOwner,
|
|
25464
|
+
costTier,
|
|
25465
|
+
safeToAutoRun: record.safeToAutoRun,
|
|
25466
|
+
requiresConfirmation: record.requiresConfirmation,
|
|
25467
|
+
reason: record.reason,
|
|
25468
|
+
sourceIssueCodes: stringArray(record.sourceIssueCodes)
|
|
25469
|
+
})
|
|
25470
|
+
];
|
|
25471
|
+
});
|
|
25472
|
+
}
|
|
24888
25473
|
function summarizeJobs(records) {
|
|
24889
25474
|
return {
|
|
24890
25475
|
total: records.length,
|
|
@@ -24903,7 +25488,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
|
|
|
24903
25488
|
if (fromBeats) return fromBeats;
|
|
24904
25489
|
if (build.beats) return augmentBeatReadinessFromJobs(build.beats, records, report?.jobs);
|
|
24905
25490
|
}
|
|
24906
|
-
const storyboardPath =
|
|
25491
|
+
const storyboardPath = join3(projectDir, "STORYBOARD.md");
|
|
24907
25492
|
if (!existsSync2(storyboardPath)) {
|
|
24908
25493
|
return { total: 0, assetsReady: 0, compositionsReady: 0, needsAuthor: [] };
|
|
24909
25494
|
}
|
|
@@ -24912,7 +25497,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
|
|
|
24912
25497
|
const needsAuthor = [];
|
|
24913
25498
|
let compositionsReady = 0;
|
|
24914
25499
|
for (const beat of parsed.beats) {
|
|
24915
|
-
if (existsSync2(
|
|
25500
|
+
if (existsSync2(join3(projectDir, "compositions", `scene-${beat.id}.html`))) {
|
|
24916
25501
|
compositionsReady += 1;
|
|
24917
25502
|
} else {
|
|
24918
25503
|
needsAuthor.push(beat.id);
|
|
@@ -24930,7 +25515,7 @@ async function readProjectBeatReadiness(projectDir, build, records) {
|
|
|
24930
25515
|
}
|
|
24931
25516
|
function deriveProjectWorkflow(opts) {
|
|
24932
25517
|
const retryWith = [];
|
|
24933
|
-
const hasStoryboard = existsSync2(
|
|
25518
|
+
const hasStoryboard = existsSync2(join3(opts.project, "STORYBOARD.md"));
|
|
24934
25519
|
if (!hasStoryboard && !opts.build) {
|
|
24935
25520
|
retryWith.push(`vibe init ${opts.project} --from "<brief>" --json`);
|
|
24936
25521
|
return { status: "empty", currentStage: "init", retryWith };
|
|
@@ -25026,7 +25611,7 @@ function beatReadinessFromBuildBeats(projectDir, beats, records = [], reportJobs
|
|
|
25026
25611
|
if (buildBeatAssetsReady(record) || (id ? buildBeatJobsReady(id, records, reportJobs) : false))
|
|
25027
25612
|
assetsReady += 1;
|
|
25028
25613
|
const compositionPath = typeof record.compositionPath === "string" ? record.compositionPath : id ? `compositions/scene-${id}.html` : void 0;
|
|
25029
|
-
if (compositionPath && existsSync2(
|
|
25614
|
+
if (compositionPath && existsSync2(resolve6(projectDir, compositionPath))) {
|
|
25030
25615
|
compositionsReady += 1;
|
|
25031
25616
|
} else if (id) {
|
|
25032
25617
|
needsAuthor.push(id);
|
|
@@ -25096,6 +25681,9 @@ function numberOr(fallback2, value) {
|
|
|
25096
25681
|
function stringArray(value) {
|
|
25097
25682
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
|
|
25098
25683
|
}
|
|
25684
|
+
function stringEnum(value, allowed) {
|
|
25685
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
25686
|
+
}
|
|
25099
25687
|
function isActiveStatus(status) {
|
|
25100
25688
|
return status === "queued" || status === "running";
|
|
25101
25689
|
}
|
|
@@ -25125,7 +25713,7 @@ function assetKindForJob(record) {
|
|
|
25125
25713
|
return void 0;
|
|
25126
25714
|
}
|
|
25127
25715
|
function projectRelativePath(projectDir, path2) {
|
|
25128
|
-
const rel = relative(
|
|
25716
|
+
const rel = relative(resolve6(projectDir), resolve6(path2));
|
|
25129
25717
|
if (!rel || rel.startsWith("..")) return path2;
|
|
25130
25718
|
return rel.replace(/\\/g, "/");
|
|
25131
25719
|
}
|
|
@@ -25143,7 +25731,7 @@ async function statOrNull(path2) {
|
|
|
25143
25731
|
return null;
|
|
25144
25732
|
}
|
|
25145
25733
|
}
|
|
25146
|
-
function
|
|
25734
|
+
function stripUndefined2(value) {
|
|
25147
25735
|
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
25148
25736
|
}
|
|
25149
25737
|
function unique(items) {
|
|
@@ -25161,6 +25749,7 @@ var init_status_jobs = __esm({
|
|
|
25161
25749
|
init_ai_video();
|
|
25162
25750
|
init_music_status();
|
|
25163
25751
|
init_build_asset_metadata();
|
|
25752
|
+
init_review_report();
|
|
25164
25753
|
init_storyboard_parse();
|
|
25165
25754
|
LOCAL_JOB_ORPHAN_AFTER_MS = 9e4;
|
|
25166
25755
|
}
|
|
@@ -25169,7 +25758,7 @@ var init_status_jobs = __esm({
|
|
|
25169
25758
|
// src/commands/_shared/project-config.ts
|
|
25170
25759
|
import { existsSync as existsSync3 } from "node:fs";
|
|
25171
25760
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
25172
|
-
import { resolve as
|
|
25761
|
+
import { resolve as resolve7 } from "node:path";
|
|
25173
25762
|
function defaultProjectConfig(opts) {
|
|
25174
25763
|
return {
|
|
25175
25764
|
schemaVersion: "1",
|
|
@@ -25220,9 +25809,9 @@ function projectConfigJson(opts) {
|
|
|
25220
25809
|
return JSON.stringify(defaultProjectConfig(opts), null, 2) + "\n";
|
|
25221
25810
|
}
|
|
25222
25811
|
async function readProjectConfig(projectDir) {
|
|
25223
|
-
const dir =
|
|
25224
|
-
const canonicalPath =
|
|
25225
|
-
const legacyPath =
|
|
25812
|
+
const dir = resolve7(projectDir);
|
|
25813
|
+
const canonicalPath = resolve7(dir, VIBE_CONFIG_FILENAME);
|
|
25814
|
+
const legacyPath = resolve7(dir, LEGACY_VIBE_PROJECT_FILENAME);
|
|
25226
25815
|
if (existsSync3(canonicalPath)) {
|
|
25227
25816
|
try {
|
|
25228
25817
|
const raw = await readFile5(canonicalPath, "utf-8");
|
|
@@ -25285,8 +25874,8 @@ var init_project_config = __esm({
|
|
|
25285
25874
|
});
|
|
25286
25875
|
|
|
25287
25876
|
// src/commands/_shared/scene-project.ts
|
|
25288
|
-
import { mkdir as mkdir4, readFile as readFile6, writeFile as
|
|
25289
|
-
import { resolve as
|
|
25877
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile8, access as access3 } from "node:fs/promises";
|
|
25878
|
+
import { resolve as resolve8, basename as basename2 } from "node:path";
|
|
25290
25879
|
function aspectToDims(aspect) {
|
|
25291
25880
|
return ASPECT_DIMS[aspect];
|
|
25292
25881
|
}
|
|
@@ -25584,6 +26173,43 @@ vibe host setup all --write # write project/app config
|
|
|
25584
26173
|
vibe host doctor all --json # verify readiness
|
|
25585
26174
|
\`\`\`
|
|
25586
26175
|
|
|
26176
|
+
## Native host goal loop
|
|
26177
|
+
|
|
26178
|
+
For long-running work, use Codex Goal mode, Claude Code \`/goal\`, or the
|
|
26179
|
+
host's equivalent as the outer loop. VibeFrame should not run a competing
|
|
26180
|
+
project-level goal loop; it provides \`--json\` commands, dry runs, cost caps,
|
|
26181
|
+
\`build-report.json\`, \`review-report.json\`, \`nextActions\`, \`safeToAutoRun\`,
|
|
26182
|
+
\`requiresConfirmation\`, \`fixOwner\`, deterministic repair, and render
|
|
26183
|
+
inspection for the host agent to reason over.
|
|
26184
|
+
|
|
26185
|
+
Copy-paste examples:
|
|
26186
|
+
|
|
26187
|
+
\`\`\`text
|
|
26188
|
+
/goal Build this VibeFrame project into renders/final.mp4. Use --json for every
|
|
26189
|
+
vibe command, run --dry-run before paid operations, use --max-cost 5 for builds
|
|
26190
|
+
unless the user sets another budget, read build-report.json and
|
|
26191
|
+
review-report.json before deciding the next action, prefer nextActions before
|
|
26192
|
+
guessing, run only safeToAutoRun:true actions automatically, and ask before
|
|
26193
|
+
requiresConfirmation:true actions. Treat retryWith as the compatibility
|
|
26194
|
+
fallback, fixOwner:"vibe" as CLI repair work, and fixOwner:"host-agent" as
|
|
26195
|
+
STORYBOARD.md, DESIGN.md, or composition edits.
|
|
26196
|
+
Stop only when renders/final.mp4 exists, duration and aspect ratio match the
|
|
26197
|
+
brief, inspect render --cheap has no errors, any AI review score is >= 90 when AI review is requested, and every
|
|
26198
|
+
remaining host-agent issue is fixed, accepted with rationale, or reported as
|
|
26199
|
+
blocked.
|
|
26200
|
+
\`\`\`
|
|
26201
|
+
|
|
26202
|
+
\`\`\`text
|
|
26203
|
+
/goal Finish this VibeFrame render using Claude Code goal mode as the outer
|
|
26204
|
+
loop. Use vibe context/schema when unsure, --json everywhere, dry-run before
|
|
26205
|
+
paid operations, budget cap via --max-cost 5, nextActions before guessing,
|
|
26206
|
+
build-report.json/review-report.json as loop state, and safeToAutoRun,
|
|
26207
|
+
requiresConfirmation, and fixOwner to decide between vibe scene repair and
|
|
26208
|
+
host-agent edits. Stop only after final MP4,
|
|
26209
|
+
target duration/aspect ratio, clean render inspection, any AI review score >= 90 when AI review is requested, and
|
|
26210
|
+
no unresolved unacknowledged host-agent issues.
|
|
26211
|
+
\`\`\`
|
|
26212
|
+
|
|
25587
26213
|
## Skills \u2014 USE THESE FIRST
|
|
25588
26214
|
|
|
25589
26215
|
@SKILL.md
|
|
@@ -25687,35 +26313,35 @@ tmp/
|
|
|
25687
26313
|
`;
|
|
25688
26314
|
}
|
|
25689
26315
|
function describeSceneScaffold(opts) {
|
|
25690
|
-
const dir =
|
|
26316
|
+
const dir = resolve8(opts.dir);
|
|
25691
26317
|
const profile = opts.profile ?? "full";
|
|
25692
26318
|
const groups = {
|
|
25693
26319
|
authoring: [
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
26320
|
+
resolve8(dir, "STORYBOARD.md"),
|
|
26321
|
+
resolve8(dir, "DESIGN.md"),
|
|
26322
|
+
resolve8(dir, VIBE_CONFIG_FILENAME),
|
|
26323
|
+
resolve8(dir, "vibe.project.yaml"),
|
|
26324
|
+
resolve8(dir, ".gitignore")
|
|
25699
26325
|
],
|
|
25700
26326
|
render: [],
|
|
25701
26327
|
agent: []
|
|
25702
26328
|
};
|
|
25703
26329
|
if (profile === "full") {
|
|
25704
26330
|
groups.render = [
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
26331
|
+
resolve8(dir, "index.html"),
|
|
26332
|
+
resolve8(dir, "compositions"),
|
|
26333
|
+
resolve8(dir, "assets"),
|
|
26334
|
+
resolve8(dir, "renders"),
|
|
26335
|
+
resolve8(dir, "hyperframes.json"),
|
|
26336
|
+
resolve8(dir, "meta.json")
|
|
25711
26337
|
];
|
|
25712
26338
|
}
|
|
25713
26339
|
if (profile === "agent" || profile === "full") {
|
|
25714
26340
|
groups.agent = [
|
|
25715
|
-
|
|
25716
|
-
|
|
25717
|
-
|
|
25718
|
-
|
|
26341
|
+
resolve8(dir, "AGENTS.md"),
|
|
26342
|
+
resolve8(dir, "SKILL.md"),
|
|
26343
|
+
resolve8(dir, "references"),
|
|
26344
|
+
resolve8(dir, "CLAUDE.md")
|
|
25719
26345
|
];
|
|
25720
26346
|
}
|
|
25721
26347
|
return groups;
|
|
@@ -25729,7 +26355,7 @@ async function pathExists(p) {
|
|
|
25729
26355
|
}
|
|
25730
26356
|
}
|
|
25731
26357
|
async function scaffoldSceneProject(opts) {
|
|
25732
|
-
const dir =
|
|
26358
|
+
const dir = resolve8(opts.dir);
|
|
25733
26359
|
const name = opts.name ?? basename2(dir);
|
|
25734
26360
|
const aspect = opts.aspect ?? "16:9";
|
|
25735
26361
|
const duration = opts.duration ?? 10;
|
|
@@ -25737,103 +26363,103 @@ async function scaffoldSceneProject(opts) {
|
|
|
25737
26363
|
const profile = opts.profile ?? "full";
|
|
25738
26364
|
await mkdir4(dir, { recursive: true });
|
|
25739
26365
|
if (profile === "full") {
|
|
25740
|
-
await mkdir4(
|
|
25741
|
-
await mkdir4(
|
|
25742
|
-
await mkdir4(
|
|
26366
|
+
await mkdir4(resolve8(dir, "compositions"), { recursive: true });
|
|
26367
|
+
await mkdir4(resolve8(dir, "assets"), { recursive: true });
|
|
26368
|
+
await mkdir4(resolve8(dir, "renders"), { recursive: true });
|
|
25743
26369
|
}
|
|
25744
26370
|
const created = [];
|
|
25745
26371
|
const skipped2 = [];
|
|
25746
26372
|
const merged = [];
|
|
25747
26373
|
if (profile === "full") {
|
|
25748
|
-
const hfPath =
|
|
26374
|
+
const hfPath = resolve8(dir, "hyperframes.json");
|
|
25749
26375
|
const hfDefaults = buildHyperframesConfig();
|
|
25750
26376
|
if (await pathExists(hfPath)) {
|
|
25751
26377
|
const existingRaw = await readFile6(hfPath, "utf-8");
|
|
25752
26378
|
const existing = JSON.parse(existingRaw);
|
|
25753
26379
|
const mergedConfig = mergeHyperframesConfig(existing, hfDefaults);
|
|
25754
|
-
await
|
|
26380
|
+
await writeFile8(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
|
|
25755
26381
|
merged.push(hfPath);
|
|
25756
26382
|
} else {
|
|
25757
|
-
await
|
|
26383
|
+
await writeFile8(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
|
|
25758
26384
|
created.push(hfPath);
|
|
25759
26385
|
}
|
|
25760
|
-
const metaPath =
|
|
26386
|
+
const metaPath = resolve8(dir, "meta.json");
|
|
25761
26387
|
if (await pathExists(metaPath)) {
|
|
25762
26388
|
skipped2.push(metaPath);
|
|
25763
26389
|
} else {
|
|
25764
|
-
await
|
|
26390
|
+
await writeFile8(
|
|
25765
26391
|
metaPath,
|
|
25766
26392
|
JSON.stringify(buildHyperframesMeta(name, now), null, 2) + "\n",
|
|
25767
26393
|
"utf-8"
|
|
25768
26394
|
);
|
|
25769
26395
|
created.push(metaPath);
|
|
25770
26396
|
}
|
|
25771
|
-
const rootPath =
|
|
26397
|
+
const rootPath = resolve8(dir, "index.html");
|
|
25772
26398
|
if (await pathExists(rootPath)) {
|
|
25773
26399
|
skipped2.push(rootPath);
|
|
25774
26400
|
} else {
|
|
25775
|
-
await
|
|
26401
|
+
await writeFile8(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
|
|
25776
26402
|
created.push(rootPath);
|
|
25777
26403
|
}
|
|
25778
26404
|
}
|
|
25779
|
-
const vibeConfigJsonPath =
|
|
26405
|
+
const vibeConfigJsonPath = resolve8(dir, VIBE_CONFIG_FILENAME);
|
|
25780
26406
|
if (await pathExists(vibeConfigJsonPath)) {
|
|
25781
26407
|
skipped2.push(vibeConfigJsonPath);
|
|
25782
26408
|
} else {
|
|
25783
|
-
await
|
|
26409
|
+
await writeFile8(vibeConfigJsonPath, projectConfigJson({ name, aspect }), "utf-8");
|
|
25784
26410
|
created.push(vibeConfigJsonPath);
|
|
25785
26411
|
}
|
|
25786
|
-
const vibePath =
|
|
26412
|
+
const vibePath = resolve8(dir, "vibe.project.yaml");
|
|
25787
26413
|
if (await pathExists(vibePath)) {
|
|
25788
26414
|
skipped2.push(vibePath);
|
|
25789
26415
|
} else {
|
|
25790
26416
|
const cfg = { ...defaultVibeProjectConfig(name), aspect };
|
|
25791
|
-
await
|
|
26417
|
+
await writeFile8(vibePath, (0, import_yaml4.stringify)(cfg), "utf-8");
|
|
25792
26418
|
created.push(vibePath);
|
|
25793
26419
|
}
|
|
25794
26420
|
if (profile === "agent" || profile === "full") {
|
|
25795
|
-
const agentsPath =
|
|
26421
|
+
const agentsPath = resolve8(dir, "AGENTS.md");
|
|
25796
26422
|
if (await pathExists(agentsPath)) {
|
|
25797
26423
|
skipped2.push(agentsPath);
|
|
25798
26424
|
} else {
|
|
25799
|
-
await
|
|
26425
|
+
await writeFile8(agentsPath, buildProjectAgentsMd(name), "utf-8");
|
|
25800
26426
|
created.push(agentsPath);
|
|
25801
26427
|
}
|
|
25802
|
-
const claudePath =
|
|
26428
|
+
const claudePath = resolve8(dir, "CLAUDE.md");
|
|
25803
26429
|
if (await pathExists(claudePath)) {
|
|
25804
26430
|
const existing = await readFile6(claudePath, "utf-8");
|
|
25805
26431
|
if (existing.includes("@AGENTS.md")) {
|
|
25806
26432
|
skipped2.push(claudePath);
|
|
25807
26433
|
} else {
|
|
25808
|
-
await
|
|
26434
|
+
await writeFile8(claudePath, `@AGENTS.md
|
|
25809
26435
|
|
|
25810
26436
|
${existing}`, "utf-8");
|
|
25811
26437
|
merged.push(claudePath);
|
|
25812
26438
|
}
|
|
25813
26439
|
} else {
|
|
25814
|
-
await
|
|
26440
|
+
await writeFile8(claudePath, buildProjectClaudeMd(name), "utf-8");
|
|
25815
26441
|
created.push(claudePath);
|
|
25816
26442
|
}
|
|
25817
26443
|
}
|
|
25818
|
-
const designPath =
|
|
26444
|
+
const designPath = resolve8(dir, "DESIGN.md");
|
|
25819
26445
|
if (await pathExists(designPath)) {
|
|
25820
26446
|
skipped2.push(designPath);
|
|
25821
26447
|
} else {
|
|
25822
|
-
await
|
|
26448
|
+
await writeFile8(designPath, buildDesignMd({ name, style: opts.visualStyle }), "utf-8");
|
|
25823
26449
|
created.push(designPath);
|
|
25824
26450
|
}
|
|
25825
|
-
const storyboardPath =
|
|
26451
|
+
const storyboardPath = resolve8(dir, "STORYBOARD.md");
|
|
25826
26452
|
if (await pathExists(storyboardPath)) {
|
|
25827
26453
|
skipped2.push(storyboardPath);
|
|
25828
26454
|
} else {
|
|
25829
|
-
await
|
|
26455
|
+
await writeFile8(storyboardPath, buildStoryboardMd(name, duration), "utf-8");
|
|
25830
26456
|
created.push(storyboardPath);
|
|
25831
26457
|
}
|
|
25832
|
-
const gitignorePath =
|
|
26458
|
+
const gitignorePath = resolve8(dir, ".gitignore");
|
|
25833
26459
|
if (await pathExists(gitignorePath)) {
|
|
25834
26460
|
skipped2.push(gitignorePath);
|
|
25835
26461
|
} else {
|
|
25836
|
-
await
|
|
26462
|
+
await writeFile8(gitignorePath, buildSceneGitignore(), "utf-8");
|
|
25837
26463
|
created.push(gitignorePath);
|
|
25838
26464
|
}
|
|
25839
26465
|
return { created, skipped: skipped2, merged, groups: describeSceneScaffold({ dir, profile }) };
|
|
@@ -28990,17 +29616,17 @@ var init_cost_tier = __esm({
|
|
|
28990
29616
|
});
|
|
28991
29617
|
|
|
28992
29618
|
// src/commands/_shared/scene-lint.ts
|
|
28993
|
-
import { readdir as readdir2, readFile as readFile7, writeFile as
|
|
29619
|
+
import { readdir as readdir2, readFile as readFile7, writeFile as writeFile9, stat as stat2 } from "node:fs/promises";
|
|
28994
29620
|
import { existsSync as existsSync4 } from "node:fs";
|
|
28995
|
-
import { resolve as
|
|
29621
|
+
import { resolve as resolve9, relative as relative2, join as join4 } from "node:path";
|
|
28996
29622
|
import {
|
|
28997
29623
|
runHyperframeLint
|
|
28998
29624
|
} from "@hyperframes/producer";
|
|
28999
29625
|
async function discoverSceneFiles(opts) {
|
|
29000
|
-
const projectDir =
|
|
29001
|
-
const rootAbs =
|
|
29626
|
+
const projectDir = resolve9(opts.projectDir);
|
|
29627
|
+
const rootAbs = resolve9(projectDir, opts.rootRel ?? "index.html");
|
|
29002
29628
|
const root = existsSync4(rootAbs) ? rootAbs : null;
|
|
29003
|
-
const compsDir =
|
|
29629
|
+
const compsDir = resolve9(projectDir, "compositions");
|
|
29004
29630
|
const subs = [];
|
|
29005
29631
|
if (existsSync4(compsDir)) {
|
|
29006
29632
|
await collectHtmlRecursive(compsDir, subs);
|
|
@@ -29011,7 +29637,7 @@ async function discoverSceneFiles(opts) {
|
|
|
29011
29637
|
async function collectHtmlRecursive(dir, into) {
|
|
29012
29638
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
29013
29639
|
for (const entry of entries) {
|
|
29014
|
-
const full =
|
|
29640
|
+
const full = join4(dir, entry.name);
|
|
29015
29641
|
if (entry.isDirectory()) {
|
|
29016
29642
|
await collectHtmlRecursive(full, into);
|
|
29017
29643
|
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
@@ -29086,7 +29712,7 @@ function addClipClassToTimedElements(html) {
|
|
|
29086
29712
|
});
|
|
29087
29713
|
}
|
|
29088
29714
|
async function runProjectLint(opts) {
|
|
29089
|
-
const projectDir =
|
|
29715
|
+
const projectDir = resolve9(opts.projectDir);
|
|
29090
29716
|
const { root, subs } = await discoverSceneFiles({ projectDir, rootRel: opts.rootRel });
|
|
29091
29717
|
const files = [];
|
|
29092
29718
|
const fixed = [];
|
|
@@ -29110,7 +29736,7 @@ async function runProjectLint(opts) {
|
|
|
29110
29736
|
if (opts.fix && findings.length > 0) {
|
|
29111
29737
|
const { html: nextHtml, fixedCodes } = applyMechanicalFixes(html, findings);
|
|
29112
29738
|
if (fixedCodes.length > 0) {
|
|
29113
|
-
await
|
|
29739
|
+
await writeFile9(abs, nextHtml, "utf-8");
|
|
29114
29740
|
fixed.push({ file: rel, codes: fixedCodes });
|
|
29115
29741
|
const reLinted = runHyperframeLint({ ...prepared, html: nextHtml });
|
|
29116
29742
|
files.push({
|
|
@@ -29147,7 +29773,7 @@ async function runProjectLint(opts) {
|
|
|
29147
29773
|
};
|
|
29148
29774
|
}
|
|
29149
29775
|
async function rootExists(projectDir, rootRel = "index.html") {
|
|
29150
|
-
const abs =
|
|
29776
|
+
const abs = resolve9(projectDir, rootRel);
|
|
29151
29777
|
try {
|
|
29152
29778
|
const s = await stat2(abs);
|
|
29153
29779
|
return s.isFile();
|
|
@@ -29170,7 +29796,7 @@ var init_scene_lint = __esm({
|
|
|
29170
29796
|
// src/utils/agent-host-detect.ts
|
|
29171
29797
|
import { existsSync as existsSync5 } from "node:fs";
|
|
29172
29798
|
import { homedir as homedir3 } from "node:os";
|
|
29173
|
-
import { join as
|
|
29799
|
+
import { join as join5 } from "node:path";
|
|
29174
29800
|
function detectAgentHosts(env2 = process.env) {
|
|
29175
29801
|
const home = homedir3();
|
|
29176
29802
|
return [
|
|
@@ -29230,7 +29856,7 @@ function detectAgentHosts(env2 = process.env) {
|
|
|
29230
29856
|
}
|
|
29231
29857
|
const configDirRel = HOST_CONFIG_DIRS[host.id];
|
|
29232
29858
|
if (configDirRel) {
|
|
29233
|
-
const path2 =
|
|
29859
|
+
const path2 = join5(home, configDirRel);
|
|
29234
29860
|
if (existsSync5(path2)) {
|
|
29235
29861
|
signals2.push({ kind: "configDir", path: path2 });
|
|
29236
29862
|
}
|
|
@@ -29255,8 +29881,8 @@ function isOnPath(binary, env2) {
|
|
|
29255
29881
|
const sep2 = process.platform === "win32" ? ";" : ":";
|
|
29256
29882
|
for (const dir of path2.split(sep2)) {
|
|
29257
29883
|
if (!dir) continue;
|
|
29258
|
-
if (existsSync5(
|
|
29259
|
-
if (process.platform === "win32" && existsSync5(
|
|
29884
|
+
if (existsSync5(join5(dir, binary))) return true;
|
|
29885
|
+
if (process.platform === "win32" && existsSync5(join5(dir, `${binary}.exe`))) return true;
|
|
29260
29886
|
}
|
|
29261
29887
|
return false;
|
|
29262
29888
|
}
|
|
@@ -30093,11 +30719,11 @@ Avoid transitions that create visible repeating geometric patterns \u2014 grids
|
|
|
30093
30719
|
|
|
30094
30720
|
// src/commands/_shared/hf-skill-bundle/bundle.ts
|
|
30095
30721
|
import { readFileSync as readFileSync2, existsSync as existsSync6, statSync as statSync2 } from "node:fs";
|
|
30096
|
-
import { join as
|
|
30722
|
+
import { join as join6 } from "node:path";
|
|
30097
30723
|
import { homedir as homedir4 } from "node:os";
|
|
30098
30724
|
import { createHash as createHash3 } from "node:crypto";
|
|
30099
30725
|
function installedSkillPath() {
|
|
30100
|
-
const candidate =
|
|
30726
|
+
const candidate = join6(homedir4(), ".claude", "skills", "hyperframes");
|
|
30101
30727
|
if (!existsSync6(candidate)) return null;
|
|
30102
30728
|
if (!statSync2(candidate).isDirectory()) return null;
|
|
30103
30729
|
return candidate;
|
|
@@ -30117,11 +30743,11 @@ function buildVendored() {
|
|
|
30117
30743
|
}
|
|
30118
30744
|
function buildInstalled(skillRoot) {
|
|
30119
30745
|
const installedPaths = {
|
|
30120
|
-
"hyperframes/SKILL.md":
|
|
30121
|
-
"hyperframes/house-style.md":
|
|
30122
|
-
"hyperframes/motion-principles.md":
|
|
30123
|
-
"hyperframes/typography.md":
|
|
30124
|
-
"hyperframes/transitions.md":
|
|
30746
|
+
"hyperframes/SKILL.md": join6(skillRoot, "SKILL.md"),
|
|
30747
|
+
"hyperframes/house-style.md": join6(skillRoot, "house-style.md"),
|
|
30748
|
+
"hyperframes/motion-principles.md": join6(skillRoot, "references", "motion-principles.md"),
|
|
30749
|
+
"hyperframes/typography.md": join6(skillRoot, "references", "typography.md"),
|
|
30750
|
+
"hyperframes/transitions.md": join6(skillRoot, "references", "transitions.md")
|
|
30125
30751
|
};
|
|
30126
30752
|
const sections = [];
|
|
30127
30753
|
for (const [label, path2] of Object.entries(installedPaths)) {
|
|
@@ -30175,12 +30801,12 @@ var init_bundle = __esm({
|
|
|
30175
30801
|
|
|
30176
30802
|
// src/commands/_shared/root-sync.ts
|
|
30177
30803
|
import { existsSync as existsSync8 } from "node:fs";
|
|
30178
|
-
import { readFile as readFile8, writeFile as
|
|
30179
|
-
import { join as
|
|
30804
|
+
import { readFile as readFile8, writeFile as writeFile11 } from "node:fs/promises";
|
|
30805
|
+
import { join as join8, resolve as resolve11 } from "node:path";
|
|
30180
30806
|
async function createRootSyncPlan(opts) {
|
|
30181
|
-
const projectDir =
|
|
30807
|
+
const projectDir = resolve11(opts.projectDir);
|
|
30182
30808
|
const rootRel = opts.rootRel ?? "index.html";
|
|
30183
|
-
const rootPath =
|
|
30809
|
+
const rootPath = resolve11(projectDir, rootRel);
|
|
30184
30810
|
if (!existsSync8(rootPath)) {
|
|
30185
30811
|
return { rootPath, rootRel, exists: false, changed: false, fixCodes: [], issues: [] };
|
|
30186
30812
|
}
|
|
@@ -30238,12 +30864,12 @@ async function createProjectRootSyncPlan(opts) {
|
|
|
30238
30864
|
}
|
|
30239
30865
|
async function syncRootComposition(opts) {
|
|
30240
30866
|
const plan = await createRootSyncPlan(opts);
|
|
30241
|
-
if (plan.nextHtml) await
|
|
30867
|
+
if (plan.nextHtml) await writeFile11(plan.rootPath, plan.nextHtml, "utf-8");
|
|
30242
30868
|
return plan;
|
|
30243
30869
|
}
|
|
30244
30870
|
async function loadProjectRootSyncBeats(projectDir) {
|
|
30245
|
-
const root =
|
|
30246
|
-
const storyboardPath =
|
|
30871
|
+
const root = resolve11(projectDir);
|
|
30872
|
+
const storyboardPath = join8(root, "STORYBOARD.md");
|
|
30247
30873
|
if (!existsSync8(storyboardPath)) return [];
|
|
30248
30874
|
const parsed = parseStoryboard(await readFile8(storyboardPath, "utf-8"));
|
|
30249
30875
|
const reportBeats = await readBuildReportBeats(root);
|
|
@@ -30312,14 +30938,14 @@ async function resolveSyncedBeatDuration(opts) {
|
|
|
30312
30938
|
if (!opts.narrationPath) return Number((reported ?? storyboardMin).toFixed(2));
|
|
30313
30939
|
const probe = opts.probeAudioDuration ?? getAudioDuration;
|
|
30314
30940
|
try {
|
|
30315
|
-
const audioDuration = await probe(
|
|
30941
|
+
const audioDuration = await probe(join8(opts.projectDir, opts.narrationPath));
|
|
30316
30942
|
return Number(Math.max(storyboardMin, reported ?? 0, audioDuration + 0.5).toFixed(2));
|
|
30317
30943
|
} catch {
|
|
30318
30944
|
return Number(Math.max(storyboardMin, reported ?? 0).toFixed(2));
|
|
30319
30945
|
}
|
|
30320
30946
|
}
|
|
30321
30947
|
async function resolveProjectBeatDurations(projectDir) {
|
|
30322
|
-
const root =
|
|
30948
|
+
const root = resolve11(projectDir);
|
|
30323
30949
|
const beats = await loadProjectRootSyncBeats(root);
|
|
30324
30950
|
const durations = /* @__PURE__ */ new Map();
|
|
30325
30951
|
for (const beat of beats) {
|
|
@@ -30450,7 +31076,7 @@ function hasExpectedAudioRef(html, ref) {
|
|
|
30450
31076
|
return ref.volume === void 0 || tag.includes(`data-volume="${ref.volume}"`);
|
|
30451
31077
|
}
|
|
30452
31078
|
async function readBuildReportBeats(projectDir) {
|
|
30453
|
-
const reportPath =
|
|
31079
|
+
const reportPath = join8(projectDir, "build-report.json");
|
|
30454
31080
|
if (!existsSync8(reportPath)) return [];
|
|
30455
31081
|
try {
|
|
30456
31082
|
const report = JSON.parse(await readFile8(reportPath, "utf-8"));
|
|
@@ -30460,7 +31086,7 @@ async function readBuildReportBeats(projectDir) {
|
|
|
30460
31086
|
}
|
|
30461
31087
|
}
|
|
30462
31088
|
function firstExisting(projectDir, rels) {
|
|
30463
|
-
return rels.find((rel) => existsSync8(
|
|
31089
|
+
return rels.find((rel) => existsSync8(join8(projectDir, rel)));
|
|
30464
31090
|
}
|
|
30465
31091
|
function stringOrUndefined(value) {
|
|
30466
31092
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -30571,8 +31197,8 @@ import OpenAI from "openai";
|
|
|
30571
31197
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
30572
31198
|
import { createHash as createHash4 } from "node:crypto";
|
|
30573
31199
|
import { existsSync as existsSync9, mkdirSync } from "node:fs";
|
|
30574
|
-
import { mkdir as mkdir6, readFile as readFile9, writeFile as
|
|
30575
|
-
import { dirname as dirname5, join as
|
|
31200
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile12 } from "node:fs/promises";
|
|
31201
|
+
import { dirname as dirname5, join as join9, resolve as resolve12 } from "node:path";
|
|
30576
31202
|
import { runHyperframeLint as runHyperframeLint2 } from "@hyperframes/producer";
|
|
30577
31203
|
function buildSystemPrompt(ctx) {
|
|
30578
31204
|
return `You are a Hyperframes composition author. The skill content below
|
|
@@ -30666,7 +31292,15 @@ ${finalDurationBullet}
|
|
|
30666
31292
|
itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
|
|
30667
31293
|
inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
|
|
30668
31294
|
phase B in, on the same timeline.
|
|
30669
|
-
- If \`assets/
|
|
31295
|
+
- If \`assets/video-${ctx.beat.id}.mp4\` exists, it is an AI-generated clip for
|
|
31296
|
+
this beat \u2014 use it as the FULL-FRAME visual and prefer it over any backdrop
|
|
31297
|
+
image or CSS. Put one full-bleed \`<video>\` inside a full-window \`.clip\`:
|
|
31298
|
+
\`<video src="assets/video-${ctx.beat.id}.mp4" data-start="0" data-duration="${BEAT_DURATION}" data-media-start="0" muted playsinline style="width:100%;height:100%;object-fit:cover"></video>\`
|
|
31299
|
+
Use the exact path (no \`./\` or \`../\`). The framework seeks the clip to each
|
|
31300
|
+
timeline frame, so do NOT add transform tweens to the \`<video>\` or its
|
|
31301
|
+
ancestors; layer text/motion graphics on top instead.
|
|
31302
|
+
- If \`assets/backdrop-${ctx.beat.id}.png\` exists (and no video clip above),
|
|
31303
|
+
use that local file as the
|
|
30670
31304
|
full-frame visual backdrop. The exact path string is
|
|
30671
31305
|
\`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
|
|
30672
31306
|
because fragments are mounted from the project root.
|
|
@@ -30773,7 +31407,7 @@ function computeCacheKey(parts) {
|
|
|
30773
31407
|
return createHash4("sha256").update(parts.provider).update("\u241E").update(parts.model).update("\u241E").update(parts.systemPrompt).update("\u241E").update(parts.userPrompt).digest("hex");
|
|
30774
31408
|
}
|
|
30775
31409
|
function defaultCacheDir() {
|
|
30776
|
-
return
|
|
31410
|
+
return join9(USER_CACHE_DIR, "compose-scenes");
|
|
30777
31411
|
}
|
|
30778
31412
|
function extractHtml(responseText) {
|
|
30779
31413
|
const fenced = responseText.match(/```html\s*\n([\s\S]*?)\n```/);
|
|
@@ -30797,7 +31431,7 @@ async function composeBeatHtml(ctx, overrides) {
|
|
|
30797
31431
|
model: settings.model
|
|
30798
31432
|
});
|
|
30799
31433
|
const cacheDir = ctx.cacheDir ?? defaultCacheDir();
|
|
30800
|
-
const cachePath =
|
|
31434
|
+
const cachePath = join9(cacheDir, `${cacheKey}.html`);
|
|
30801
31435
|
if (existsSync9(cachePath)) {
|
|
30802
31436
|
const html2 = await readFile9(cachePath, "utf-8");
|
|
30803
31437
|
return { html: html2, cached: true, cacheKey, model: settings.model, provider: ctx.provider };
|
|
@@ -30833,7 +31467,7 @@ async function composeBeatHtml(ctx, overrides) {
|
|
|
30833
31467
|
const html = extractHtml(result.text);
|
|
30834
31468
|
try {
|
|
30835
31469
|
mkdirSync(cacheDir, { recursive: true });
|
|
30836
|
-
await
|
|
31470
|
+
await writeFile12(cachePath, html, "utf-8");
|
|
30837
31471
|
} catch {
|
|
30838
31472
|
}
|
|
30839
31473
|
const costUsd = result.inputTokens / 1e6 * settings.costPerMTokIn + result.outputTokens / 1e6 * settings.costPerMTokOut;
|
|
@@ -30932,9 +31566,9 @@ async function resolveComposerConfigAware(explicit, cwd) {
|
|
|
30932
31566
|
return resolveComposer();
|
|
30933
31567
|
}
|
|
30934
31568
|
async function executeComposeScenesWithSkills(params, outputDir, overrides) {
|
|
30935
|
-
const projectRoot = params.project ?
|
|
30936
|
-
const designPath =
|
|
30937
|
-
const storyboardPath =
|
|
31569
|
+
const projectRoot = params.project ? resolve12(outputDir, params.project) : resolve12(outputDir);
|
|
31570
|
+
const designPath = resolve12(projectRoot, params.design ?? "DESIGN.md");
|
|
31571
|
+
const storyboardPath = resolve12(projectRoot, params.storyboard ?? "STORYBOARD.md");
|
|
30938
31572
|
if (!existsSync9(designPath)) {
|
|
30939
31573
|
return { success: false, error: `DESIGN.md not found at ${designPath}` };
|
|
30940
31574
|
}
|
|
@@ -30962,7 +31596,7 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
|
|
|
30962
31596
|
provider = params.composer ?? "claude";
|
|
30963
31597
|
apiKey = "";
|
|
30964
31598
|
}
|
|
30965
|
-
const compositionsDir =
|
|
31599
|
+
const compositionsDir = join9(projectRoot, "compositions");
|
|
30966
31600
|
await mkdir6(compositionsDir, { recursive: true });
|
|
30967
31601
|
let finalDurations = /* @__PURE__ */ new Map();
|
|
30968
31602
|
try {
|
|
@@ -30990,9 +31624,9 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
|
|
|
30990
31624
|
},
|
|
30991
31625
|
overrides
|
|
30992
31626
|
);
|
|
30993
|
-
const compositionPath =
|
|
31627
|
+
const compositionPath = join9(compositionsDir, `scene-${beat.id}.html`);
|
|
30994
31628
|
await mkdir6(dirname5(compositionPath), { recursive: true });
|
|
30995
|
-
await
|
|
31629
|
+
await writeFile12(compositionPath, result.html, "utf-8");
|
|
30996
31630
|
if (result.cached) {
|
|
30997
31631
|
onProgress({
|
|
30998
31632
|
type: "beat-cached",
|
|
@@ -31191,13 +31825,13 @@ var init_compose_scenes_skills = __esm({
|
|
|
31191
31825
|
// src/commands/_shared/compose-prompts.ts
|
|
31192
31826
|
import { existsSync as existsSync10 } from "node:fs";
|
|
31193
31827
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
31194
|
-
import { join as
|
|
31828
|
+
import { join as join10, relative as relative4, resolve as resolve13 } from "node:path";
|
|
31195
31829
|
async function getComposePrompts(opts) {
|
|
31196
|
-
const projectDir =
|
|
31197
|
-
const designPath =
|
|
31198
|
-
const storyboardPath =
|
|
31199
|
-
const skillPath =
|
|
31200
|
-
const compositionsDir =
|
|
31830
|
+
const projectDir = resolve13(opts.projectDir);
|
|
31831
|
+
const designPath = join10(projectDir, "DESIGN.md");
|
|
31832
|
+
const storyboardPath = join10(projectDir, "STORYBOARD.md");
|
|
31833
|
+
const skillPath = join10(projectDir, "SKILL.md");
|
|
31834
|
+
const compositionsDir = join10(projectDir, "compositions");
|
|
31201
31835
|
const warnings = [];
|
|
31202
31836
|
const baseError = (msg) => ({
|
|
31203
31837
|
success: false,
|
|
@@ -31246,7 +31880,7 @@ async function getComposePrompts(opts) {
|
|
|
31246
31880
|
} catch {
|
|
31247
31881
|
}
|
|
31248
31882
|
const result = beats.map((beat) => {
|
|
31249
|
-
const outputPathAbs =
|
|
31883
|
+
const outputPathAbs = join10(compositionsDir, `scene-${beat.id}.html`);
|
|
31250
31884
|
const outputPathRel = relative4(projectDir, outputPathAbs);
|
|
31251
31885
|
const finalDurationSec = finalDurations.get(beat.id);
|
|
31252
31886
|
const userPrompt = buildUserPrompt({
|
|
@@ -31320,7 +31954,7 @@ var init_compose_prompts = __esm({
|
|
|
31320
31954
|
// src/commands/_shared/sub-comp-duration-sync.ts
|
|
31321
31955
|
import { existsSync as existsSync12 } from "node:fs";
|
|
31322
31956
|
import { readFile as readFile12 } from "node:fs/promises";
|
|
31323
|
-
import { join as
|
|
31957
|
+
import { join as join12, resolve as resolve15 } from "node:path";
|
|
31324
31958
|
function syncSubCompositionDurationHtml(html, beatId, newDurationSec, fileRel) {
|
|
31325
31959
|
const compositionId = `scene-${beatId}`;
|
|
31326
31960
|
const file = fileRel ?? `compositions/${compositionId}.html`;
|
|
@@ -31408,11 +32042,11 @@ function rewriteTimelineConst(html, oldDurationSec, newDurationSec) {
|
|
|
31408
32042
|
return { html, rewritten: false, needsManualFix };
|
|
31409
32043
|
}
|
|
31410
32044
|
async function createSubCompDurationSyncPlans(opts) {
|
|
31411
|
-
const projectDir =
|
|
32045
|
+
const projectDir = resolve15(opts.projectDir);
|
|
31412
32046
|
const results = [];
|
|
31413
32047
|
for (const beat of opts.beats) {
|
|
31414
32048
|
const fileRel = `compositions/scene-${beat.id}.html`;
|
|
31415
|
-
const filePath =
|
|
32049
|
+
const filePath = join12(projectDir, fileRel);
|
|
31416
32050
|
if (!existsSync12(filePath)) continue;
|
|
31417
32051
|
const newDurationSec = await resolveSyncedBeatDuration({
|
|
31418
32052
|
projectDir,
|
|
@@ -31443,88 +32077,6 @@ var init_sub_comp_duration_sync = __esm({
|
|
|
31443
32077
|
}
|
|
31444
32078
|
});
|
|
31445
32079
|
|
|
31446
|
-
// src/commands/_shared/review-report.ts
|
|
31447
|
-
import { writeFile as writeFile13 } from "node:fs/promises";
|
|
31448
|
-
import { join as join12, resolve as resolve15 } from "node:path";
|
|
31449
|
-
function statusFromIssues(issues) {
|
|
31450
|
-
if (issues.some((issue) => issue.severity === "error")) return "fail";
|
|
31451
|
-
if (issues.some((issue) => issue.severity === "warning")) return "warn";
|
|
31452
|
-
return "pass";
|
|
31453
|
-
}
|
|
31454
|
-
function scoreIssues(issues) {
|
|
31455
|
-
let score = 100;
|
|
31456
|
-
for (const issue of issues) {
|
|
31457
|
-
if (issue.severity === "error") score -= 25;
|
|
31458
|
-
else if (issue.severity === "warning") score -= 8;
|
|
31459
|
-
else score -= 2;
|
|
31460
|
-
}
|
|
31461
|
-
return Math.max(0, Math.min(100, score));
|
|
31462
|
-
}
|
|
31463
|
-
function uniqueRetryWith(items) {
|
|
31464
|
-
return [
|
|
31465
|
-
...new Set(items.filter((item) => typeof item === "string" && item.length > 0))
|
|
31466
|
-
];
|
|
31467
|
-
}
|
|
31468
|
-
function fixOwnerForIssue(issue) {
|
|
31469
|
-
if (issue.fixOwner) return issue.fixOwner;
|
|
31470
|
-
if (issue.code.startsWith("AI_REVIEW_")) return "host-agent";
|
|
31471
|
-
if (issue.code.startsWith("STORYBOARD_")) return "host-agent";
|
|
31472
|
-
if (issue.code === "PROJECT_NOT_FOUND" || issue.code === "MISSING_STORYBOARD" || issue.code === "BEAT_NOT_FOUND" || issue.code === "MISSING_DESIGN") {
|
|
31473
|
-
return "host-agent";
|
|
31474
|
-
}
|
|
31475
|
-
return "vibe";
|
|
31476
|
-
}
|
|
31477
|
-
function normalizeReviewIssues(issues, fallbackOwner) {
|
|
31478
|
-
return issues.map((issue) => ({
|
|
31479
|
-
...issue,
|
|
31480
|
-
fixOwner: issue.fixOwner ?? fallbackOwner ?? fixOwnerForIssue(issue)
|
|
31481
|
-
}));
|
|
31482
|
-
}
|
|
31483
|
-
function summarizeReviewIssues(issues) {
|
|
31484
|
-
const normalized = normalizeReviewIssues(issues);
|
|
31485
|
-
return {
|
|
31486
|
-
issueCount: normalized.length,
|
|
31487
|
-
errorCount: normalized.filter((issue) => issue.severity === "error").length,
|
|
31488
|
-
warningCount: normalized.filter((issue) => issue.severity === "warning").length,
|
|
31489
|
-
infoCount: normalized.filter((issue) => issue.severity === "info").length,
|
|
31490
|
-
fixOwners: {
|
|
31491
|
-
vibe: normalized.filter((issue) => issue.fixOwner === "vibe").length,
|
|
31492
|
-
hostAgent: normalized.filter((issue) => issue.fixOwner === "host-agent").length
|
|
31493
|
-
}
|
|
31494
|
-
};
|
|
31495
|
-
}
|
|
31496
|
-
function buildReviewReport(opts) {
|
|
31497
|
-
const issues = normalizeReviewIssues(opts.issues);
|
|
31498
|
-
return stripUndefined2({
|
|
31499
|
-
schemaVersion: "1",
|
|
31500
|
-
kind: "review",
|
|
31501
|
-
project: resolve15(opts.project),
|
|
31502
|
-
mode: opts.mode,
|
|
31503
|
-
beat: opts.beat,
|
|
31504
|
-
status: opts.status,
|
|
31505
|
-
score: opts.score,
|
|
31506
|
-
issues,
|
|
31507
|
-
summary: summarizeReviewIssues(issues),
|
|
31508
|
-
retryWith: uniqueRetryWith(opts.retryWith),
|
|
31509
|
-
sourceReports: opts.sourceReports ?? [],
|
|
31510
|
-
reportPath: opts.reportPath
|
|
31511
|
-
});
|
|
31512
|
-
}
|
|
31513
|
-
function defaultReviewReportPath(projectDir) {
|
|
31514
|
-
return join12(resolve15(projectDir), "review-report.json");
|
|
31515
|
-
}
|
|
31516
|
-
async function writeReviewReport(path2, report) {
|
|
31517
|
-
await writeFile13(path2, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
31518
|
-
}
|
|
31519
|
-
function stripUndefined2(value) {
|
|
31520
|
-
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
31521
|
-
}
|
|
31522
|
-
var init_review_report = __esm({
|
|
31523
|
-
"src/commands/_shared/review-report.ts"() {
|
|
31524
|
-
"use strict";
|
|
31525
|
-
}
|
|
31526
|
-
});
|
|
31527
|
-
|
|
31528
32080
|
// src/commands/_shared/scene-repair.ts
|
|
31529
32081
|
import { readFile as readFile13, writeFile as writeFile14 } from "node:fs/promises";
|
|
31530
32082
|
import { relative as relative5, resolve as resolve16 } from "node:path";
|
|
@@ -31651,6 +32203,298 @@ var init_scene_repair = __esm({
|
|
|
31651
32203
|
}
|
|
31652
32204
|
});
|
|
31653
32205
|
|
|
32206
|
+
// src/commands/_shared/storyboard-edit.ts
|
|
32207
|
+
function validateStoryboardMarkdown(markdown) {
|
|
32208
|
+
const parsed = parseStoryboard(markdown);
|
|
32209
|
+
const sections = splitBeatSections(markdown);
|
|
32210
|
+
const issues = [];
|
|
32211
|
+
if (parsed.beats.length === 0) {
|
|
32212
|
+
issues.push({
|
|
32213
|
+
severity: "error",
|
|
32214
|
+
code: "NO_BEATS",
|
|
32215
|
+
message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
|
|
32216
|
+
});
|
|
32217
|
+
}
|
|
32218
|
+
const seen = /* @__PURE__ */ new Map();
|
|
32219
|
+
for (const beat of parsed.beats) {
|
|
32220
|
+
seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
|
|
32221
|
+
}
|
|
32222
|
+
for (const [id, count] of seen.entries()) {
|
|
32223
|
+
if (count > 1) {
|
|
32224
|
+
issues.push({
|
|
32225
|
+
severity: "error",
|
|
32226
|
+
code: "DUPLICATE_BEAT_ID",
|
|
32227
|
+
beatId: id,
|
|
32228
|
+
message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
|
|
32229
|
+
});
|
|
32230
|
+
}
|
|
32231
|
+
}
|
|
32232
|
+
for (const section of sections) {
|
|
32233
|
+
const cueBlock = readLeadingCueBlock(section.body);
|
|
32234
|
+
if (cueBlock?.error) {
|
|
32235
|
+
issues.push({
|
|
32236
|
+
severity: "error",
|
|
32237
|
+
code: "MALFORMED_CUE_YAML",
|
|
32238
|
+
beatId: section.id,
|
|
32239
|
+
message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
|
|
32240
|
+
});
|
|
32241
|
+
}
|
|
32242
|
+
}
|
|
32243
|
+
const characterPool = new Set(
|
|
32244
|
+
Object.keys(
|
|
32245
|
+
parsed.frontmatter?.characters ?? {}
|
|
32246
|
+
)
|
|
32247
|
+
);
|
|
32248
|
+
for (const beat of parsed.beats) {
|
|
32249
|
+
const cues = beat.cues ?? {};
|
|
32250
|
+
for (const [key, value] of Object.entries(cues)) {
|
|
32251
|
+
if (!ALLOWED_CUE_KEYS.has(key)) {
|
|
32252
|
+
issues.push({
|
|
32253
|
+
severity: "warning",
|
|
32254
|
+
code: "UNKNOWN_CUE",
|
|
32255
|
+
beatId: beat.id,
|
|
32256
|
+
message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
|
|
32257
|
+
});
|
|
32258
|
+
continue;
|
|
32259
|
+
}
|
|
32260
|
+
if (key === "characters") {
|
|
32261
|
+
const validShape = typeof value === "string" || Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
32262
|
+
if (!validShape) {
|
|
32263
|
+
issues.push({
|
|
32264
|
+
severity: "error",
|
|
32265
|
+
code: "INVALID_CHARACTERS_VALUE",
|
|
32266
|
+
beatId: beat.id,
|
|
32267
|
+
message: `Beat "${beat.id}" cue "characters" must be a character name or a list of names.`
|
|
32268
|
+
});
|
|
32269
|
+
continue;
|
|
32270
|
+
}
|
|
32271
|
+
for (const name of beatCharacterNames({ characters: value })) {
|
|
32272
|
+
if (!characterPool.has(name)) {
|
|
32273
|
+
issues.push({
|
|
32274
|
+
severity: "warning",
|
|
32275
|
+
code: "UNKNOWN_CHARACTER",
|
|
32276
|
+
beatId: beat.id,
|
|
32277
|
+
message: `Beat "${beat.id}" references character "${name}" not defined in the frontmatter \`characters:\` pool.`
|
|
32278
|
+
});
|
|
32279
|
+
}
|
|
32280
|
+
}
|
|
32281
|
+
continue;
|
|
32282
|
+
}
|
|
32283
|
+
if (key === "duration") {
|
|
32284
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
32285
|
+
issues.push({
|
|
32286
|
+
severity: "error",
|
|
32287
|
+
code: "INVALID_DURATION",
|
|
32288
|
+
beatId: beat.id,
|
|
32289
|
+
message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
|
|
32290
|
+
});
|
|
32291
|
+
}
|
|
32292
|
+
continue;
|
|
32293
|
+
}
|
|
32294
|
+
if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
|
|
32295
|
+
issues.push({
|
|
32296
|
+
severity: "error",
|
|
32297
|
+
code: "INVALID_CUE_VALUE",
|
|
32298
|
+
beatId: beat.id,
|
|
32299
|
+
message: `Beat "${beat.id}" cue "${key}" must be a string.`
|
|
32300
|
+
});
|
|
32301
|
+
}
|
|
32302
|
+
}
|
|
32303
|
+
}
|
|
32304
|
+
for (const beat of parsed.beats) {
|
|
32305
|
+
if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
|
|
32306
|
+
issues.push({
|
|
32307
|
+
severity: "warning",
|
|
32308
|
+
code: "BEAT_DURATION_TOO_LONG",
|
|
32309
|
+
beatId: beat.id,
|
|
32310
|
+
message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
|
|
32311
|
+
});
|
|
32312
|
+
}
|
|
32313
|
+
}
|
|
32314
|
+
return {
|
|
32315
|
+
ok: !issues.some((i) => i.severity === "error"),
|
|
32316
|
+
beats: parsed.beats,
|
|
32317
|
+
issues
|
|
32318
|
+
};
|
|
32319
|
+
}
|
|
32320
|
+
function getStoryboardBeat(markdown, beatId) {
|
|
32321
|
+
return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
|
|
32322
|
+
}
|
|
32323
|
+
function upsertStoryboardBeat(markdown, opts) {
|
|
32324
|
+
const beatId = normalizeBeatId(opts.beatId);
|
|
32325
|
+
const sections = splitBeatSections(markdown);
|
|
32326
|
+
const existing = sections.find((section2) => section2.id === beatId);
|
|
32327
|
+
const cues = storyboardCuesFromInput(opts);
|
|
32328
|
+
if (existing) {
|
|
32329
|
+
let next = markdown;
|
|
32330
|
+
for (const [key, value] of Object.entries(cues)) {
|
|
32331
|
+
next = setStoryboardCue(next, { beatId, key, value });
|
|
32332
|
+
}
|
|
32333
|
+
return { markdown: next, action: "updated" };
|
|
32334
|
+
}
|
|
32335
|
+
const section = buildStoryboardBeatSection({ ...opts, beatId, cues });
|
|
32336
|
+
if (sections.length > 0 && isStarterStoryboard(markdown)) {
|
|
32337
|
+
const global2 = markdown.slice(0, sections[0].start).trimEnd();
|
|
32338
|
+
return {
|
|
32339
|
+
markdown: `${global2}
|
|
32340
|
+
|
|
32341
|
+
${section}`,
|
|
32342
|
+
action: "replaced-starter"
|
|
32343
|
+
};
|
|
32344
|
+
}
|
|
32345
|
+
const prefix = markdown.trimEnd();
|
|
32346
|
+
return {
|
|
32347
|
+
markdown: `${prefix}${prefix ? "\n\n" : ""}${section}`,
|
|
32348
|
+
action: "appended"
|
|
32349
|
+
};
|
|
32350
|
+
}
|
|
32351
|
+
function setStoryboardCue(markdown, opts) {
|
|
32352
|
+
const section = findBeatSection(markdown, opts.beatId);
|
|
32353
|
+
if (!section) {
|
|
32354
|
+
throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
32355
|
+
}
|
|
32356
|
+
if (!ALLOWED_CUE_KEYS.has(opts.key)) {
|
|
32357
|
+
throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
|
|
32358
|
+
}
|
|
32359
|
+
const cue = readLeadingCueBlock(section.body);
|
|
32360
|
+
if (cue?.error) {
|
|
32361
|
+
throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
|
|
32362
|
+
}
|
|
32363
|
+
const cues = cue?.value ? { ...cue.value } : {};
|
|
32364
|
+
if (opts.unset) {
|
|
32365
|
+
delete cues[opts.key];
|
|
32366
|
+
} else {
|
|
32367
|
+
cues[opts.key] = normalizeCueValue(opts.key, opts.value);
|
|
32368
|
+
}
|
|
32369
|
+
const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
|
|
32370
|
+
const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
32371
|
+
const nextBody = cueBlock + remainingBody.trimStart();
|
|
32372
|
+
const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
|
|
32373
|
+
return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
|
|
32374
|
+
}
|
|
32375
|
+
function moveStoryboardBeat(markdown, opts) {
|
|
32376
|
+
if (opts.beatId === opts.afterBeatId) return markdown;
|
|
32377
|
+
const sections = splitBeatSections(markdown);
|
|
32378
|
+
const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
|
|
32379
|
+
if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
32380
|
+
const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
|
|
32381
|
+
if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
|
|
32382
|
+
const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
|
|
32383
|
+
const chunks = sections.map((section) => section.raw);
|
|
32384
|
+
const [moving] = chunks.splice(movingIndex, 1);
|
|
32385
|
+
const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
|
|
32386
|
+
chunks.splice(adjustedAfterIndex + 1, 0, moving);
|
|
32387
|
+
return global2 + normalizeBeatChunks(chunks);
|
|
32388
|
+
}
|
|
32389
|
+
function normalizeBeatChunks(chunks) {
|
|
32390
|
+
return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
|
|
32391
|
+
}
|
|
32392
|
+
function normalizeBeatId(id) {
|
|
32393
|
+
return deriveBeatId(id);
|
|
32394
|
+
}
|
|
32395
|
+
function storyboardCuesFromInput(opts) {
|
|
32396
|
+
const cues = {};
|
|
32397
|
+
if (opts.duration !== void 0) cues.duration = opts.duration;
|
|
32398
|
+
if (opts.narration && opts.narration.trim()) cues.narration = opts.narration.trim();
|
|
32399
|
+
if (opts.backdrop && opts.backdrop.trim()) cues.backdrop = opts.backdrop.trim();
|
|
32400
|
+
return cues;
|
|
32401
|
+
}
|
|
32402
|
+
function buildStoryboardBeatSection(opts) {
|
|
32403
|
+
const title = normalizeTitle(opts.title ?? humanizeBeatId(opts.beatId));
|
|
32404
|
+
const cueBlock = Object.keys(opts.cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(opts.cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
32405
|
+
const body = opts.body?.trim() || "Scene created with `vibe scene add`. Edit this body if the build flow should recompose it.";
|
|
32406
|
+
return `## Beat ${opts.beatId} - ${title}
|
|
32407
|
+
|
|
32408
|
+
${cueBlock}${body.trimEnd()}
|
|
32409
|
+
`;
|
|
32410
|
+
}
|
|
32411
|
+
function normalizeTitle(value) {
|
|
32412
|
+
return value.replace(/\s+/g, " ").trim() || "Scene";
|
|
32413
|
+
}
|
|
32414
|
+
function humanizeBeatId(id) {
|
|
32415
|
+
return id.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || "Scene";
|
|
32416
|
+
}
|
|
32417
|
+
function isStarterStoryboard(markdown) {
|
|
32418
|
+
const parsed = parseStoryboard(markdown);
|
|
32419
|
+
if (parsed.beats.length !== 3) return false;
|
|
32420
|
+
const ids = parsed.beats.map((beat) => beat.id);
|
|
32421
|
+
if (ids.join(",") !== "hook,proof,close") return false;
|
|
32422
|
+
return markdown.includes("Introduce the promise in one crisp sentence.") && markdown.includes("Show the mechanism or proof point that makes the promise believable.") && markdown.includes("Close with the action the viewer should remember.");
|
|
32423
|
+
}
|
|
32424
|
+
function normalizeCueValue(key, value) {
|
|
32425
|
+
if (key === "duration") {
|
|
32426
|
+
const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
|
|
32427
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
32428
|
+
throw new Error("Cue duration must be a positive number.");
|
|
32429
|
+
}
|
|
32430
|
+
return n;
|
|
32431
|
+
}
|
|
32432
|
+
if (typeof value !== "string") return value;
|
|
32433
|
+
return value.trim();
|
|
32434
|
+
}
|
|
32435
|
+
function splitBeatSections(markdown) {
|
|
32436
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
32437
|
+
const headings = [];
|
|
32438
|
+
HEADING_RE2.lastIndex = 0;
|
|
32439
|
+
let match;
|
|
32440
|
+
while ((match = HEADING_RE2.exec(normalized)) !== null) {
|
|
32441
|
+
headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
|
|
32442
|
+
}
|
|
32443
|
+
return headings.map((heading, index) => {
|
|
32444
|
+
const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
|
|
32445
|
+
return {
|
|
32446
|
+
id: deriveBeatId(heading.line),
|
|
32447
|
+
heading: heading.line,
|
|
32448
|
+
start: heading.start,
|
|
32449
|
+
headingEnd: heading.end,
|
|
32450
|
+
end,
|
|
32451
|
+
raw: normalized.slice(heading.start, end),
|
|
32452
|
+
body: normalized.slice(heading.end, end).trim()
|
|
32453
|
+
};
|
|
32454
|
+
});
|
|
32455
|
+
}
|
|
32456
|
+
function findBeatSection(markdown, beatId) {
|
|
32457
|
+
return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
|
|
32458
|
+
}
|
|
32459
|
+
function readLeadingCueBlock(body) {
|
|
32460
|
+
const match = body.match(LEADING_CUE_RE);
|
|
32461
|
+
if (!match) return null;
|
|
32462
|
+
try {
|
|
32463
|
+
const parsed = (0, import_yaml5.parse)(match[2]);
|
|
32464
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32465
|
+
return { full: match[0], error: "cue block must parse to a YAML object" };
|
|
32466
|
+
}
|
|
32467
|
+
return { full: match[0], value: parsed };
|
|
32468
|
+
} catch (error) {
|
|
32469
|
+
return { full: match[0], error: error instanceof Error ? error.message : String(error) };
|
|
32470
|
+
}
|
|
32471
|
+
}
|
|
32472
|
+
var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
|
|
32473
|
+
var init_storyboard_edit = __esm({
|
|
32474
|
+
"src/commands/_shared/storyboard-edit.ts"() {
|
|
32475
|
+
"use strict";
|
|
32476
|
+
import_yaml5 = __toESM(require_dist2(), 1);
|
|
32477
|
+
init_storyboard_parse();
|
|
32478
|
+
STORYBOARD_CUE_KEYS = [
|
|
32479
|
+
"duration",
|
|
32480
|
+
"narration",
|
|
32481
|
+
"backdrop",
|
|
32482
|
+
"video",
|
|
32483
|
+
"keyframe",
|
|
32484
|
+
"motion",
|
|
32485
|
+
"voice",
|
|
32486
|
+
"music",
|
|
32487
|
+
"asset",
|
|
32488
|
+
"characters"
|
|
32489
|
+
];
|
|
32490
|
+
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
32491
|
+
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
32492
|
+
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
32493
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "keyframe", "motion", "voice", "music", "asset"]);
|
|
32494
|
+
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
32495
|
+
}
|
|
32496
|
+
});
|
|
32497
|
+
|
|
31654
32498
|
// src/pipeline/renderers/chrome.ts
|
|
31655
32499
|
import { existsSync as existsSync14 } from "node:fs";
|
|
31656
32500
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -32567,6 +33411,8 @@ var init_build_asset_reference = __esm({
|
|
|
32567
33411
|
EXTENSIONS = {
|
|
32568
33412
|
narration: [".mp3", ".wav", ".m4a"],
|
|
32569
33413
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33414
|
+
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33415
|
+
keyframe: [".png", ".jpg", ".jpeg", ".webp"],
|
|
32570
33416
|
video: [".mp4", ".mov", ".webm"],
|
|
32571
33417
|
music: [".mp3", ".wav", ".m4a"]
|
|
32572
33418
|
};
|
|
@@ -32604,6 +33450,28 @@ function backdropCacheDescriptor(opts) {
|
|
|
32604
33450
|
ext: "png"
|
|
32605
33451
|
});
|
|
32606
33452
|
}
|
|
33453
|
+
function characterCacheDescriptor(opts) {
|
|
33454
|
+
return cacheAssetDescriptor("character", {
|
|
33455
|
+
name: opts.name,
|
|
33456
|
+
cue: opts.cue,
|
|
33457
|
+
provider: opts.provider,
|
|
33458
|
+
quality: opts.quality,
|
|
33459
|
+
size: opts.size,
|
|
33460
|
+
ext: "png"
|
|
33461
|
+
});
|
|
33462
|
+
}
|
|
33463
|
+
function keyframeCacheDescriptor(opts) {
|
|
33464
|
+
return cacheAssetDescriptor("keyframe", {
|
|
33465
|
+
beatId: opts.beatId,
|
|
33466
|
+
cue: opts.cue,
|
|
33467
|
+
provider: opts.provider,
|
|
33468
|
+
quality: opts.quality,
|
|
33469
|
+
size: opts.size,
|
|
33470
|
+
ratio: opts.ratio,
|
|
33471
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
33472
|
+
ext: "png"
|
|
33473
|
+
});
|
|
33474
|
+
}
|
|
32607
33475
|
function imageRatioForSize(size) {
|
|
32608
33476
|
switch (size) {
|
|
32609
33477
|
case "1024x1024":
|
|
@@ -32623,6 +33491,10 @@ function videoCacheDescriptor(opts) {
|
|
|
32623
33491
|
provider: opts.provider,
|
|
32624
33492
|
duration: normalizeVideoDuration(opts.duration),
|
|
32625
33493
|
ratio: "16:9",
|
|
33494
|
+
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
33495
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
33496
|
+
// Omit when absent so existing (non-keyframe) clips keep their cache key.
|
|
33497
|
+
keyframe: opts.keyframe || void 0,
|
|
32626
33498
|
ext: "mp4"
|
|
32627
33499
|
});
|
|
32628
33500
|
}
|
|
@@ -32675,208 +33547,6 @@ var init_build_backdrop_prompt = __esm({
|
|
|
32675
33547
|
}
|
|
32676
33548
|
});
|
|
32677
33549
|
|
|
32678
|
-
// src/commands/_shared/storyboard-edit.ts
|
|
32679
|
-
function validateStoryboardMarkdown(markdown) {
|
|
32680
|
-
const parsed = parseStoryboard(markdown);
|
|
32681
|
-
const sections = splitBeatSections(markdown);
|
|
32682
|
-
const issues = [];
|
|
32683
|
-
if (parsed.beats.length === 0) {
|
|
32684
|
-
issues.push({
|
|
32685
|
-
severity: "error",
|
|
32686
|
-
code: "NO_BEATS",
|
|
32687
|
-
message: "STORYBOARD.md must contain at least one `## Beat ...` heading."
|
|
32688
|
-
});
|
|
32689
|
-
}
|
|
32690
|
-
const seen = /* @__PURE__ */ new Map();
|
|
32691
|
-
for (const beat of parsed.beats) {
|
|
32692
|
-
seen.set(beat.id, (seen.get(beat.id) ?? 0) + 1);
|
|
32693
|
-
}
|
|
32694
|
-
for (const [id, count] of seen.entries()) {
|
|
32695
|
-
if (count > 1) {
|
|
32696
|
-
issues.push({
|
|
32697
|
-
severity: "error",
|
|
32698
|
-
code: "DUPLICATE_BEAT_ID",
|
|
32699
|
-
beatId: id,
|
|
32700
|
-
message: `Beat id "${id}" appears ${count} times. Beat ids must be unique.`
|
|
32701
|
-
});
|
|
32702
|
-
}
|
|
32703
|
-
}
|
|
32704
|
-
for (const section of sections) {
|
|
32705
|
-
const cueBlock = readLeadingCueBlock(section.body);
|
|
32706
|
-
if (cueBlock?.error) {
|
|
32707
|
-
issues.push({
|
|
32708
|
-
severity: "error",
|
|
32709
|
-
code: "MALFORMED_CUE_YAML",
|
|
32710
|
-
beatId: section.id,
|
|
32711
|
-
message: `Beat "${section.id}" has malformed YAML cues: ${cueBlock.error}`
|
|
32712
|
-
});
|
|
32713
|
-
}
|
|
32714
|
-
}
|
|
32715
|
-
for (const beat of parsed.beats) {
|
|
32716
|
-
const cues = beat.cues ?? {};
|
|
32717
|
-
for (const [key, value] of Object.entries(cues)) {
|
|
32718
|
-
if (!ALLOWED_CUE_KEYS.has(key)) {
|
|
32719
|
-
issues.push({
|
|
32720
|
-
severity: "warning",
|
|
32721
|
-
code: "UNKNOWN_CUE",
|
|
32722
|
-
beatId: beat.id,
|
|
32723
|
-
message: `Beat "${beat.id}" uses unknown cue "${key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`
|
|
32724
|
-
});
|
|
32725
|
-
continue;
|
|
32726
|
-
}
|
|
32727
|
-
if (key === "duration") {
|
|
32728
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
32729
|
-
issues.push({
|
|
32730
|
-
severity: "error",
|
|
32731
|
-
code: "INVALID_DURATION",
|
|
32732
|
-
beatId: beat.id,
|
|
32733
|
-
message: `Beat "${beat.id}" cue "duration" must be a positive number of seconds.`
|
|
32734
|
-
});
|
|
32735
|
-
}
|
|
32736
|
-
continue;
|
|
32737
|
-
}
|
|
32738
|
-
if (STRING_CUE_KEYS.has(key) && typeof value !== "string") {
|
|
32739
|
-
issues.push({
|
|
32740
|
-
severity: "error",
|
|
32741
|
-
code: "INVALID_CUE_VALUE",
|
|
32742
|
-
beatId: beat.id,
|
|
32743
|
-
message: `Beat "${beat.id}" cue "${key}" must be a string.`
|
|
32744
|
-
});
|
|
32745
|
-
}
|
|
32746
|
-
}
|
|
32747
|
-
}
|
|
32748
|
-
for (const beat of parsed.beats) {
|
|
32749
|
-
if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
|
|
32750
|
-
issues.push({
|
|
32751
|
-
severity: "warning",
|
|
32752
|
-
code: "BEAT_DURATION_TOO_LONG",
|
|
32753
|
-
beatId: beat.id,
|
|
32754
|
-
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).`
|
|
32755
|
-
});
|
|
32756
|
-
}
|
|
32757
|
-
}
|
|
32758
|
-
return {
|
|
32759
|
-
ok: !issues.some((i) => i.severity === "error"),
|
|
32760
|
-
beats: parsed.beats,
|
|
32761
|
-
issues
|
|
32762
|
-
};
|
|
32763
|
-
}
|
|
32764
|
-
function getStoryboardBeat(markdown, beatId) {
|
|
32765
|
-
return parseStoryboard(markdown).beats.find((beat) => beat.id === beatId) ?? null;
|
|
32766
|
-
}
|
|
32767
|
-
function setStoryboardCue(markdown, opts) {
|
|
32768
|
-
const section = findBeatSection(markdown, opts.beatId);
|
|
32769
|
-
if (!section) {
|
|
32770
|
-
throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
32771
|
-
}
|
|
32772
|
-
if (!ALLOWED_CUE_KEYS.has(opts.key)) {
|
|
32773
|
-
throw new Error(`Unsupported cue "${opts.key}". Supported cues: ${STORYBOARD_CUE_KEYS.join(", ")}.`);
|
|
32774
|
-
}
|
|
32775
|
-
const cue = readLeadingCueBlock(section.body);
|
|
32776
|
-
if (cue?.error) {
|
|
32777
|
-
throw new Error(`Cannot edit malformed YAML cues for beat "${opts.beatId}": ${cue.error}`);
|
|
32778
|
-
}
|
|
32779
|
-
const cues = cue?.value ? { ...cue.value } : {};
|
|
32780
|
-
if (opts.unset) {
|
|
32781
|
-
delete cues[opts.key];
|
|
32782
|
-
} else {
|
|
32783
|
-
cues[opts.key] = normalizeCueValue(opts.key, opts.value);
|
|
32784
|
-
}
|
|
32785
|
-
const remainingBody = cue ? section.body.slice(cue.full.length).replace(/^\s*\n/, "") : section.body.replace(/^\s*\n/, "");
|
|
32786
|
-
const cueBlock = Object.keys(cues).length > 0 ? "```yaml\n" + (0, import_yaml5.stringify)(cues, { lineWidth: 0 }).trimEnd() + "\n```\n\n" : "";
|
|
32787
|
-
const nextBody = cueBlock + remainingBody.trimStart();
|
|
32788
|
-
const nextSection = section.raw.slice(0, section.headingEnd - section.start) + "\n\n" + nextBody.trimEnd() + "\n";
|
|
32789
|
-
return markdown.slice(0, section.start) + nextSection + markdown.slice(section.end);
|
|
32790
|
-
}
|
|
32791
|
-
function moveStoryboardBeat(markdown, opts) {
|
|
32792
|
-
if (opts.beatId === opts.afterBeatId) return markdown;
|
|
32793
|
-
const sections = splitBeatSections(markdown);
|
|
32794
|
-
const movingIndex = sections.findIndex((section) => section.id === opts.beatId);
|
|
32795
|
-
if (movingIndex === -1) throw new Error(`Beat "${opts.beatId}" not found.`);
|
|
32796
|
-
const afterIndex = sections.findIndex((section) => section.id === opts.afterBeatId);
|
|
32797
|
-
if (afterIndex === -1) throw new Error(`Beat "${opts.afterBeatId}" not found.`);
|
|
32798
|
-
const global2 = sections.length > 0 ? markdown.slice(0, sections[0].start) : markdown;
|
|
32799
|
-
const chunks = sections.map((section) => section.raw);
|
|
32800
|
-
const [moving] = chunks.splice(movingIndex, 1);
|
|
32801
|
-
const adjustedAfterIndex = movingIndex < afterIndex ? afterIndex - 1 : afterIndex;
|
|
32802
|
-
chunks.splice(adjustedAfterIndex + 1, 0, moving);
|
|
32803
|
-
return global2 + normalizeBeatChunks(chunks);
|
|
32804
|
-
}
|
|
32805
|
-
function normalizeBeatChunks(chunks) {
|
|
32806
|
-
return chunks.map((chunk) => chunk.trim()).filter(Boolean).join("\n\n") + "\n";
|
|
32807
|
-
}
|
|
32808
|
-
function normalizeCueValue(key, value) {
|
|
32809
|
-
if (key === "duration") {
|
|
32810
|
-
const n = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
|
|
32811
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
32812
|
-
throw new Error("Cue duration must be a positive number.");
|
|
32813
|
-
}
|
|
32814
|
-
return n;
|
|
32815
|
-
}
|
|
32816
|
-
if (typeof value !== "string") return value;
|
|
32817
|
-
return value.trim();
|
|
32818
|
-
}
|
|
32819
|
-
function splitBeatSections(markdown) {
|
|
32820
|
-
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
32821
|
-
const headings = [];
|
|
32822
|
-
HEADING_RE2.lastIndex = 0;
|
|
32823
|
-
let match;
|
|
32824
|
-
while ((match = HEADING_RE2.exec(normalized)) !== null) {
|
|
32825
|
-
headings.push({ start: match.index, end: match.index + match[0].length, line: match[1].trim() });
|
|
32826
|
-
}
|
|
32827
|
-
return headings.map((heading, index) => {
|
|
32828
|
-
const end = index + 1 < headings.length ? headings[index + 1].start : normalized.length;
|
|
32829
|
-
return {
|
|
32830
|
-
id: deriveBeatId(heading.line),
|
|
32831
|
-
heading: heading.line,
|
|
32832
|
-
start: heading.start,
|
|
32833
|
-
headingEnd: heading.end,
|
|
32834
|
-
end,
|
|
32835
|
-
raw: normalized.slice(heading.start, end),
|
|
32836
|
-
body: normalized.slice(heading.end, end).trim()
|
|
32837
|
-
};
|
|
32838
|
-
});
|
|
32839
|
-
}
|
|
32840
|
-
function findBeatSection(markdown, beatId) {
|
|
32841
|
-
return splitBeatSections(markdown).find((section) => section.id === beatId) ?? null;
|
|
32842
|
-
}
|
|
32843
|
-
function readLeadingCueBlock(body) {
|
|
32844
|
-
const match = body.match(LEADING_CUE_RE);
|
|
32845
|
-
if (!match) return null;
|
|
32846
|
-
try {
|
|
32847
|
-
const parsed = (0, import_yaml5.parse)(match[2]);
|
|
32848
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32849
|
-
return { full: match[0], error: "cue block must parse to a YAML object" };
|
|
32850
|
-
}
|
|
32851
|
-
return { full: match[0], value: parsed };
|
|
32852
|
-
} catch (error) {
|
|
32853
|
-
return { full: match[0], error: error instanceof Error ? error.message : String(error) };
|
|
32854
|
-
}
|
|
32855
|
-
}
|
|
32856
|
-
var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
|
|
32857
|
-
var init_storyboard_edit = __esm({
|
|
32858
|
-
"src/commands/_shared/storyboard-edit.ts"() {
|
|
32859
|
-
"use strict";
|
|
32860
|
-
import_yaml5 = __toESM(require_dist2(), 1);
|
|
32861
|
-
init_storyboard_parse();
|
|
32862
|
-
STORYBOARD_CUE_KEYS = [
|
|
32863
|
-
"duration",
|
|
32864
|
-
"narration",
|
|
32865
|
-
"backdrop",
|
|
32866
|
-
"video",
|
|
32867
|
-
"motion",
|
|
32868
|
-
"voice",
|
|
32869
|
-
"music",
|
|
32870
|
-
"asset"
|
|
32871
|
-
];
|
|
32872
|
-
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
32873
|
-
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
32874
|
-
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
32875
|
-
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
32876
|
-
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
32877
|
-
}
|
|
32878
|
-
});
|
|
32879
|
-
|
|
32880
33550
|
// src/commands/_shared/build-plan.ts
|
|
32881
33551
|
import { existsSync as existsSync17 } from "node:fs";
|
|
32882
33552
|
import { readFile as readFile17 } from "node:fs/promises";
|
|
@@ -32975,6 +33645,7 @@ async function createBuildPlan(opts) {
|
|
|
32975
33645
|
const backdropPrompt = stringOrUndefined3(cue.backdrop);
|
|
32976
33646
|
const augmentedBackdropPrompt = backdropPrompt ? augmentBackdropPrompt(backdropPrompt) : null;
|
|
32977
33647
|
const videoPrompt = stringOrUndefined3(cue.video);
|
|
33648
|
+
const keyframePrompt = stringOrUndefined3(cue.keyframe);
|
|
32978
33649
|
const musicPrompt = stringOrUndefined3(cue.music);
|
|
32979
33650
|
const genericReference = resolveGenericAssetReference(projectDir, cue.asset);
|
|
32980
33651
|
const narrationReference = resolveTypedAssetReference(projectDir, "narration", cue.narration);
|
|
@@ -32983,7 +33654,7 @@ async function createBuildPlan(opts) {
|
|
|
32983
33654
|
const musicReference = resolveTypedAssetReference(projectDir, "music", cue.music) ?? (!musicPrompt && genericReference?.kind === "music" ? genericReference : null);
|
|
32984
33655
|
const narrationCue = narrationText ?? narrationReference?.raw;
|
|
32985
33656
|
const backdropCue = backdropPrompt ?? backdropReference?.raw;
|
|
32986
|
-
const videoCue = videoPrompt ?? videoReference?.raw;
|
|
33657
|
+
const videoCue = videoPrompt ?? videoReference?.raw ?? keyframePrompt;
|
|
32987
33658
|
const musicCue = musicPrompt ?? musicReference?.raw;
|
|
32988
33659
|
const narrationCost = resolved.narration.resolved === "elevenlabs" ? ELEVENLABS_NARRATION_COST_USD : resolved.narration.resolved === "openai" ? OPENAI_NARRATION_COST_USD : 0;
|
|
32989
33660
|
const narrationCache = narrationText && !narrationReference ? narrationCacheDescriptor({
|
|
@@ -33001,11 +33672,12 @@ async function createBuildPlan(opts) {
|
|
|
33001
33672
|
size: imageSize2,
|
|
33002
33673
|
ratio: imageRatio
|
|
33003
33674
|
}) : null;
|
|
33004
|
-
const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
|
|
33675
|
+
const videoCache = (videoPrompt || keyframePrompt) && !videoReference ? videoCacheDescriptor({
|
|
33005
33676
|
beatId: beat.id,
|
|
33006
|
-
cue: videoPrompt,
|
|
33677
|
+
cue: videoPrompt ?? keyframePrompt,
|
|
33007
33678
|
provider: resolved.video.resolved,
|
|
33008
|
-
duration: beat.duration
|
|
33679
|
+
duration: beat.duration,
|
|
33680
|
+
keyframe: keyframePrompt
|
|
33009
33681
|
}) : null;
|
|
33010
33682
|
const musicCache = musicPrompt && !musicReference ? musicCacheDescriptor({
|
|
33011
33683
|
beatId: beat.id,
|
|
@@ -33052,7 +33724,11 @@ async function createBuildPlan(opts) {
|
|
|
33052
33724
|
reference: videoReference,
|
|
33053
33725
|
projectDir,
|
|
33054
33726
|
force: opts.force,
|
|
33055
|
-
cost:
|
|
33727
|
+
cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
|
|
33728
|
+
durationSec: normalizeVideoDuration(beat.duration),
|
|
33729
|
+
resolution: "720p",
|
|
33730
|
+
aspectRatio: "16:9"
|
|
33731
|
+
}) : VIDEO_COST_USD,
|
|
33056
33732
|
active: includeAssets
|
|
33057
33733
|
}) : null;
|
|
33058
33734
|
const music = musicCue && !opts.skipMusic ? assetPlan({
|
|
@@ -33096,6 +33772,12 @@ async function createBuildPlan(opts) {
|
|
|
33096
33772
|
if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
|
|
33097
33773
|
noteProviderNeed(resolved.image, "Backdrop generation");
|
|
33098
33774
|
if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
|
|
33775
|
+
if (keyframePrompt && video?.willGenerate) {
|
|
33776
|
+
estimatedCostUsd += backdropCostUsd(imageQuality);
|
|
33777
|
+
providers.add(resolved.image.resolved);
|
|
33778
|
+
if (isSupportedBuildImageProvider(resolved.image.resolved))
|
|
33779
|
+
noteProviderNeed(resolved.image, "Keyframe generation");
|
|
33780
|
+
}
|
|
33099
33781
|
if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
|
|
33100
33782
|
if (!compositionExists) missing.add("compositions");
|
|
33101
33783
|
if (includeCompose && !compositionExists && mode !== "agent") {
|
|
@@ -33163,6 +33845,21 @@ async function createBuildPlan(opts) {
|
|
|
33163
33845
|
retryWith.push(skipCommand);
|
|
33164
33846
|
}
|
|
33165
33847
|
}
|
|
33848
|
+
if (validationOk && includeAssets && !opts.skipVideo) {
|
|
33849
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
33850
|
+
for (const beat of sourceBeats) {
|
|
33851
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
33852
|
+
}
|
|
33853
|
+
if (referenced.size > 0) {
|
|
33854
|
+
const toGenerate = resolveCharacters(parsed.frontmatter).filter(
|
|
33855
|
+
(c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync17(join15(projectDir, `assets/character-${c.name}.png`)))
|
|
33856
|
+
);
|
|
33857
|
+
if (toGenerate.length > 0) {
|
|
33858
|
+
estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
|
|
33859
|
+
missing.add("assets");
|
|
33860
|
+
}
|
|
33861
|
+
}
|
|
33862
|
+
}
|
|
33166
33863
|
return finalizeBuildPlan({
|
|
33167
33864
|
projectDir,
|
|
33168
33865
|
config: config2,
|
|
@@ -33527,6 +34224,7 @@ var init_build_plan = __esm({
|
|
|
33527
34224
|
init_schema();
|
|
33528
34225
|
init_build_asset_reference();
|
|
33529
34226
|
init_build_cache();
|
|
34227
|
+
init_dist2();
|
|
33530
34228
|
init_build_asset_metadata();
|
|
33531
34229
|
init_build_backdrop_prompt();
|
|
33532
34230
|
init_composer_resolve();
|
|
@@ -33859,7 +34557,7 @@ __export(scene_build_exports, {
|
|
|
33859
34557
|
executeSceneBuild: () => executeSceneBuild,
|
|
33860
34558
|
resolveSceneBuildMode: () => resolveSceneBuildMode
|
|
33861
34559
|
});
|
|
33862
|
-
import { existsSync as existsSync19 } from "node:fs";
|
|
34560
|
+
import { existsSync as existsSync19, readFileSync as readFileSync3 } from "node:fs";
|
|
33863
34561
|
import { copyFile as copyFile3, mkdir as mkdir10, readFile as readFile18, writeFile as writeFile18 } from "node:fs/promises";
|
|
33864
34562
|
import { dirname as dirname10, join as join16, resolve as resolve25 } from "node:path";
|
|
33865
34563
|
async function executeSceneBuild(opts) {
|
|
@@ -34026,8 +34724,32 @@ async function executeSceneBuild(opts) {
|
|
|
34026
34724
|
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
34027
34725
|
let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
|
|
34028
34726
|
let pendingJobs = [];
|
|
34727
|
+
let characterCostUsd = 0;
|
|
34029
34728
|
if (shouldRunStage(selectedStage, "assets")) {
|
|
34030
34729
|
onProgress({ type: "phase-start", phase: "primitives" });
|
|
34730
|
+
const characterPaths = /* @__PURE__ */ new Map();
|
|
34731
|
+
if (!(opts.skipVideo ?? false)) {
|
|
34732
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
34733
|
+
for (const beat of activeBeats) {
|
|
34734
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
34735
|
+
}
|
|
34736
|
+
if (referenced.size > 0) {
|
|
34737
|
+
const charResult = await buildCharacters(
|
|
34738
|
+
resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
|
|
34739
|
+
{
|
|
34740
|
+
projectDir,
|
|
34741
|
+
imageProvider,
|
|
34742
|
+
imageSize: opts.imageSize ?? "1536x1024",
|
|
34743
|
+
imageQuality: opts.imageQuality ?? "hd",
|
|
34744
|
+
force: opts.force ?? false,
|
|
34745
|
+
onProgress
|
|
34746
|
+
}
|
|
34747
|
+
);
|
|
34748
|
+
for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
|
|
34749
|
+
characterCostUsd = charResult.costUsd;
|
|
34750
|
+
warnings.push(...charResult.failures);
|
|
34751
|
+
}
|
|
34752
|
+
}
|
|
34031
34753
|
const primitiveResults = await mapWithConcurrency(
|
|
34032
34754
|
activeBeats,
|
|
34033
34755
|
BEAT_PRIMITIVES_CONCURRENCY,
|
|
@@ -34045,7 +34767,8 @@ async function executeSceneBuild(opts) {
|
|
|
34045
34767
|
skipVideo: opts.skipVideo ?? false,
|
|
34046
34768
|
skipMusic: opts.skipMusic ?? false,
|
|
34047
34769
|
force: opts.force ?? false,
|
|
34048
|
-
onProgress
|
|
34770
|
+
onProgress,
|
|
34771
|
+
characterPaths
|
|
34049
34772
|
})
|
|
34050
34773
|
);
|
|
34051
34774
|
beatOutcomes = primitiveResults.map((result) => result.outcome);
|
|
@@ -34054,10 +34777,7 @@ async function executeSceneBuild(opts) {
|
|
|
34054
34777
|
(beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
|
|
34055
34778
|
);
|
|
34056
34779
|
stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
|
|
34057
|
-
stageReports.assets.costUsd = estimateActualAssetCost(
|
|
34058
|
-
beatOutcomes,
|
|
34059
|
-
opts.imageQuality ?? "hd"
|
|
34060
|
-
);
|
|
34780
|
+
stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
|
|
34061
34781
|
stageReports.assets.retryWith = pendingJobs.map(
|
|
34062
34782
|
(job) => `vibe status job ${job.id} --project ${projectDir} --json`
|
|
34063
34783
|
);
|
|
@@ -34222,6 +34942,18 @@ async function executeSceneBuild(opts) {
|
|
|
34222
34942
|
stageReports.compose.status = "skipped";
|
|
34223
34943
|
}
|
|
34224
34944
|
if (stageReports.compose.status === "done") {
|
|
34945
|
+
for (const beat of activeBeats) {
|
|
34946
|
+
const videoRel = `assets/video-${beat.id}.mp4`;
|
|
34947
|
+
const compositionAbs = join16(projectDir, "compositions", `scene-${beat.id}.html`);
|
|
34948
|
+
if (!existsSync19(join16(projectDir, videoRel)) || !existsSync19(compositionAbs)) continue;
|
|
34949
|
+
const html = readFileSync3(compositionAbs, "utf-8");
|
|
34950
|
+
if (!html.includes(videoRel)) {
|
|
34951
|
+
warnings.push(
|
|
34952
|
+
`Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
|
|
34953
|
+
);
|
|
34954
|
+
retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
|
|
34955
|
+
}
|
|
34956
|
+
}
|
|
34225
34957
|
const repair = await runBuildSceneRepair(projectDir, "compose", false);
|
|
34226
34958
|
sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
|
|
34227
34959
|
applySceneRepairToStage(stageReports.compose, repair);
|
|
@@ -34456,6 +35188,8 @@ async function buildBeatPrimitives(beat, ctx) {
|
|
|
34456
35188
|
videoMetadataPath: video.metadataPath,
|
|
34457
35189
|
videoFreshness: video.freshness,
|
|
34458
35190
|
videoSourcePath: video.sourcePath,
|
|
35191
|
+
keyframeStatus: video.keyframeStatus,
|
|
35192
|
+
keyframePath: video.keyframePath,
|
|
34459
35193
|
musicStatus: music.status,
|
|
34460
35194
|
musicPath: music.path,
|
|
34461
35195
|
musicJobId: music.job?.id,
|
|
@@ -34642,6 +35376,120 @@ async function dispatchNarration(beat, ctx) {
|
|
|
34642
35376
|
freshness: "fresh"
|
|
34643
35377
|
};
|
|
34644
35378
|
}
|
|
35379
|
+
function characterSheetPrompt(name, description) {
|
|
35380
|
+
return [
|
|
35381
|
+
`Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
|
|
35382
|
+
"Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
|
|
35383
|
+
"with identical outfit, hairstyle, hair color, and body proportions across all three views.",
|
|
35384
|
+
"Plain light-grey studio background, even neutral lighting, no props.",
|
|
35385
|
+
"Photorealistic digital character, professional concept-art character sheet layout."
|
|
35386
|
+
].join(" ");
|
|
35387
|
+
}
|
|
35388
|
+
async function buildCharacters(characters, ctx) {
|
|
35389
|
+
const paths = /* @__PURE__ */ new Map();
|
|
35390
|
+
const failures = [];
|
|
35391
|
+
let costUsd = 0;
|
|
35392
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
35393
|
+
for (const character of characters) {
|
|
35394
|
+
if (character.imagePath) {
|
|
35395
|
+
if (existsSync19(join16(ctx.projectDir, character.imagePath))) {
|
|
35396
|
+
paths.set(character.name, character.imagePath);
|
|
35397
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
|
|
35398
|
+
} else {
|
|
35399
|
+
const error = `character "${character.name}" image not found: ${character.imagePath}`;
|
|
35400
|
+
failures.push(error);
|
|
35401
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error });
|
|
35402
|
+
}
|
|
35403
|
+
continue;
|
|
35404
|
+
}
|
|
35405
|
+
if (!character.prompt) continue;
|
|
35406
|
+
const prompt3 = characterSheetPrompt(character.name, character.prompt);
|
|
35407
|
+
const rel = `assets/character-${character.name}.png`;
|
|
35408
|
+
const abs = join16(ctx.projectDir, rel);
|
|
35409
|
+
const metadataOptions = { quality: ctx.imageQuality, size };
|
|
35410
|
+
const cache = characterCacheDescriptor({
|
|
35411
|
+
name: character.name,
|
|
35412
|
+
cue: prompt3,
|
|
35413
|
+
provider: ctx.imageProvider,
|
|
35414
|
+
quality: ctx.imageQuality,
|
|
35415
|
+
size
|
|
35416
|
+
});
|
|
35417
|
+
if (existsSync19(abs) && !ctx.force) {
|
|
35418
|
+
const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
|
|
35419
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
35420
|
+
projectDir: ctx.projectDir,
|
|
35421
|
+
kind: "character",
|
|
35422
|
+
beatId: character.name,
|
|
35423
|
+
cue: prompt3,
|
|
35424
|
+
provider: ctx.imageProvider,
|
|
35425
|
+
options: metadataOptions,
|
|
35426
|
+
cacheKey: cache.key
|
|
35427
|
+
})) {
|
|
35428
|
+
paths.set(character.name, rel);
|
|
35429
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
35430
|
+
continue;
|
|
35431
|
+
}
|
|
35432
|
+
}
|
|
35433
|
+
const cacheAbs = join16(ctx.projectDir, cache.path);
|
|
35434
|
+
if (existsSync19(cacheAbs) && !ctx.force) {
|
|
35435
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35436
|
+
await copyFile3(cacheAbs, abs);
|
|
35437
|
+
await writeAssetMetadata({
|
|
35438
|
+
projectDir: ctx.projectDir,
|
|
35439
|
+
kind: "character",
|
|
35440
|
+
beatId: character.name,
|
|
35441
|
+
cue: prompt3,
|
|
35442
|
+
provider: ctx.imageProvider,
|
|
35443
|
+
options: metadataOptions,
|
|
35444
|
+
cacheKey: cache.key,
|
|
35445
|
+
canonicalPath: rel,
|
|
35446
|
+
cachePath: cache.path
|
|
35447
|
+
});
|
|
35448
|
+
paths.set(character.name, rel);
|
|
35449
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
35450
|
+
continue;
|
|
35451
|
+
}
|
|
35452
|
+
const generated = await generateBackdropImage(
|
|
35453
|
+
prompt3,
|
|
35454
|
+
{
|
|
35455
|
+
projectDir: ctx.projectDir,
|
|
35456
|
+
imageProvider: ctx.imageProvider,
|
|
35457
|
+
imageSize: size,
|
|
35458
|
+
imageQuality: ctx.imageQuality
|
|
35459
|
+
},
|
|
35460
|
+
imageRatioForSize(size)
|
|
35461
|
+
);
|
|
35462
|
+
if (!generated.success) {
|
|
35463
|
+
failures.push(`character "${character.name}": ${generated.error}`);
|
|
35464
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
|
|
35465
|
+
continue;
|
|
35466
|
+
}
|
|
35467
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35468
|
+
await writeFile18(abs, generated.buffer);
|
|
35469
|
+
await mkdir10(dirname10(cacheAbs), { recursive: true });
|
|
35470
|
+
await writeFile18(cacheAbs, generated.buffer);
|
|
35471
|
+
await writeAssetMetadata({
|
|
35472
|
+
projectDir: ctx.projectDir,
|
|
35473
|
+
kind: "character",
|
|
35474
|
+
beatId: character.name,
|
|
35475
|
+
cue: prompt3,
|
|
35476
|
+
provider: ctx.imageProvider,
|
|
35477
|
+
options: metadataOptions,
|
|
35478
|
+
cacheKey: cache.key,
|
|
35479
|
+
canonicalPath: rel,
|
|
35480
|
+
cachePath: cache.path
|
|
35481
|
+
});
|
|
35482
|
+
costUsd += backdropCostUsd(ctx.imageQuality);
|
|
35483
|
+
paths.set(character.name, rel);
|
|
35484
|
+
ctx.onProgress({
|
|
35485
|
+
type: "character-generated",
|
|
35486
|
+
name: character.name,
|
|
35487
|
+
path: rel,
|
|
35488
|
+
provider: ctx.imageProvider
|
|
35489
|
+
});
|
|
35490
|
+
}
|
|
35491
|
+
return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
|
|
35492
|
+
}
|
|
34645
35493
|
async function dispatchBackdrop(beat, ctx) {
|
|
34646
35494
|
const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
|
|
34647
35495
|
if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
|
|
@@ -34786,6 +35634,112 @@ async function generateBackdropImage(prompt3, ctx, ratio) {
|
|
|
34786
35634
|
});
|
|
34787
35635
|
return imageBufferFromResult(result);
|
|
34788
35636
|
}
|
|
35637
|
+
async function editKeyframeImage(prompt3, sheetBuffers, ctx, ratio) {
|
|
35638
|
+
loadSceneBuildEnv(ctx.projectDir);
|
|
35639
|
+
const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
|
|
35640
|
+
const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
|
|
35641
|
+
if (!apiKey) {
|
|
35642
|
+
return {
|
|
35643
|
+
success: false,
|
|
35644
|
+
error: `${keyInfo.envVar} not set \u2014 cannot generate keyframe with ${ctx.imageProvider}`
|
|
35645
|
+
};
|
|
35646
|
+
}
|
|
35647
|
+
if (ctx.imageProvider === "openai") {
|
|
35648
|
+
const provider2 = new OpenAIImageProvider();
|
|
35649
|
+
await provider2.initialize({ apiKey });
|
|
35650
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3);
|
|
35651
|
+
return imageBufferFromResult(result2);
|
|
35652
|
+
}
|
|
35653
|
+
if (ctx.imageProvider === "gemini") {
|
|
35654
|
+
const provider2 = new GeminiProvider();
|
|
35655
|
+
await provider2.initialize({ apiKey });
|
|
35656
|
+
const result2 = await provider2.editImage(sheetBuffers, prompt3, {
|
|
35657
|
+
model: "flash",
|
|
35658
|
+
aspectRatio: ratio
|
|
35659
|
+
});
|
|
35660
|
+
return imageBufferFromResult(result2);
|
|
35661
|
+
}
|
|
35662
|
+
const provider = new GrokProvider();
|
|
35663
|
+
await provider.initialize({ apiKey });
|
|
35664
|
+
const result = await provider.editImage(sheetBuffers[0], prompt3, { aspectRatio: ratio });
|
|
35665
|
+
return imageBufferFromResult(result);
|
|
35666
|
+
}
|
|
35667
|
+
async function ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel) {
|
|
35668
|
+
const rel = `assets/keyframe-${beat.id}.png`;
|
|
35669
|
+
const abs = join16(ctx.projectDir, rel);
|
|
35670
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
35671
|
+
const ratio = imageRatioForSize(size);
|
|
35672
|
+
const metadataOptions = { quality: ctx.imageQuality, size, ratio };
|
|
35673
|
+
const cache = keyframeCacheDescriptor({
|
|
35674
|
+
beatId: beat.id,
|
|
35675
|
+
cue: keyframeCue,
|
|
35676
|
+
provider: ctx.imageProvider,
|
|
35677
|
+
quality: ctx.imageQuality,
|
|
35678
|
+
size,
|
|
35679
|
+
ratio,
|
|
35680
|
+
characters: characterRefsRel
|
|
35681
|
+
});
|
|
35682
|
+
if (existsSync19(abs) && !ctx.force) {
|
|
35683
|
+
const metadata = readAssetMetadata(ctx.projectDir, "keyframe", beat.id);
|
|
35684
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
35685
|
+
projectDir: ctx.projectDir,
|
|
35686
|
+
kind: "keyframe",
|
|
35687
|
+
beatId: beat.id,
|
|
35688
|
+
cue: keyframeCue,
|
|
35689
|
+
provider: ctx.imageProvider,
|
|
35690
|
+
options: metadataOptions,
|
|
35691
|
+
cacheKey: cache.key
|
|
35692
|
+
})) {
|
|
35693
|
+
return { status: "cached", rel };
|
|
35694
|
+
}
|
|
35695
|
+
}
|
|
35696
|
+
const cacheAbs = join16(ctx.projectDir, cache.path);
|
|
35697
|
+
if (existsSync19(cacheAbs) && !ctx.force) {
|
|
35698
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35699
|
+
await copyFile3(cacheAbs, abs);
|
|
35700
|
+
await writeAssetMetadata({
|
|
35701
|
+
projectDir: ctx.projectDir,
|
|
35702
|
+
kind: "keyframe",
|
|
35703
|
+
beatId: beat.id,
|
|
35704
|
+
cue: keyframeCue,
|
|
35705
|
+
provider: ctx.imageProvider,
|
|
35706
|
+
options: metadataOptions,
|
|
35707
|
+
cacheKey: cache.key,
|
|
35708
|
+
canonicalPath: rel,
|
|
35709
|
+
cachePath: cache.path
|
|
35710
|
+
});
|
|
35711
|
+
return { status: "cached", rel };
|
|
35712
|
+
}
|
|
35713
|
+
const sheetBuffers = [];
|
|
35714
|
+
for (const relPath of characterRefsRel) {
|
|
35715
|
+
const sheetAbs = join16(ctx.projectDir, relPath);
|
|
35716
|
+
if (existsSync19(sheetAbs)) sheetBuffers.push(await readFile18(sheetAbs));
|
|
35717
|
+
}
|
|
35718
|
+
const imgCtx = {
|
|
35719
|
+
projectDir: ctx.projectDir,
|
|
35720
|
+
imageProvider: ctx.imageProvider,
|
|
35721
|
+
imageSize: size,
|
|
35722
|
+
imageQuality: ctx.imageQuality
|
|
35723
|
+
};
|
|
35724
|
+
const generated = sheetBuffers.length > 0 ? await editKeyframeImage(keyframeCue, sheetBuffers, imgCtx, ratio) : await generateBackdropImage(keyframeCue, imgCtx, ratio);
|
|
35725
|
+
if (!generated.success) return { status: "failed", error: generated.error };
|
|
35726
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35727
|
+
await writeFile18(abs, generated.buffer);
|
|
35728
|
+
await mkdir10(dirname10(cacheAbs), { recursive: true });
|
|
35729
|
+
await writeFile18(cacheAbs, generated.buffer);
|
|
35730
|
+
await writeAssetMetadata({
|
|
35731
|
+
projectDir: ctx.projectDir,
|
|
35732
|
+
kind: "keyframe",
|
|
35733
|
+
beatId: beat.id,
|
|
35734
|
+
cue: keyframeCue,
|
|
35735
|
+
provider: ctx.imageProvider,
|
|
35736
|
+
options: metadataOptions,
|
|
35737
|
+
cacheKey: cache.key,
|
|
35738
|
+
canonicalPath: rel,
|
|
35739
|
+
cachePath: cache.path
|
|
35740
|
+
});
|
|
35741
|
+
return { status: "generated", rel };
|
|
35742
|
+
}
|
|
34789
35743
|
async function imageBufferFromResult(result) {
|
|
34790
35744
|
const image = result.images?.[0];
|
|
34791
35745
|
if (!result.success || !image) {
|
|
@@ -34809,15 +35763,22 @@ function imageProviderKeyInfo(provider) {
|
|
|
34809
35763
|
async function dispatchVideo(beat, ctx) {
|
|
34810
35764
|
const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
|
|
34811
35765
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
34812
|
-
const
|
|
35766
|
+
const keyframeCue = stringOrUndefined4(beat.cues?.keyframe);
|
|
35767
|
+
const prompt3 = stringOrUndefined4(beat.cues?.video) ?? keyframeCue;
|
|
34813
35768
|
if (!prompt3) return { status: "no-cue" };
|
|
35769
|
+
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
35770
|
+
const characterRefsAbs = characterRefsRel.map((p) => join16(ctx.projectDir, p));
|
|
34814
35771
|
const rel = `assets/video-${beat.id}.mp4`;
|
|
34815
35772
|
const abs = join16(ctx.projectDir, rel);
|
|
34816
35773
|
const cache = videoCacheDescriptor({
|
|
34817
35774
|
beatId: beat.id,
|
|
34818
35775
|
cue: prompt3,
|
|
34819
35776
|
provider: ctx.videoProvider,
|
|
34820
|
-
duration: normalizeVideoDuration(beat.duration)
|
|
35777
|
+
duration: normalizeVideoDuration(beat.duration),
|
|
35778
|
+
// Keyframe mode keys the clip to the keyframe prompt; refs feed the keyframe,
|
|
35779
|
+
// not the clip directly.
|
|
35780
|
+
characters: keyframeCue ? void 0 : characterRefsRel,
|
|
35781
|
+
keyframe: keyframeCue
|
|
34821
35782
|
});
|
|
34822
35783
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
34823
35784
|
if (existsSync19(abs) && !ctx.force) {
|
|
@@ -34870,6 +35831,19 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34870
35831
|
};
|
|
34871
35832
|
}
|
|
34872
35833
|
loadSceneBuildEnv(ctx.projectDir);
|
|
35834
|
+
let keyframeStatus;
|
|
35835
|
+
let keyframeRel;
|
|
35836
|
+
let keyframeImageAbs;
|
|
35837
|
+
if (keyframeCue) {
|
|
35838
|
+
const kf = await ensureKeyframe(beat, ctx, keyframeCue, characterRefsRel);
|
|
35839
|
+
if (kf.status === "failed") {
|
|
35840
|
+
ctx.onProgress({ type: "video-failed", beatId: beat.id, error: kf.error });
|
|
35841
|
+
return { status: "failed", error: kf.error };
|
|
35842
|
+
}
|
|
35843
|
+
keyframeStatus = kf.status;
|
|
35844
|
+
keyframeRel = kf.rel;
|
|
35845
|
+
keyframeImageAbs = join16(ctx.projectDir, kf.rel);
|
|
35846
|
+
}
|
|
34873
35847
|
const result = await executeVideoGenerate({
|
|
34874
35848
|
prompt: prompt3,
|
|
34875
35849
|
provider: ctx.videoProvider,
|
|
@@ -34877,6 +35851,10 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34877
35851
|
ratio: "16:9",
|
|
34878
35852
|
output: abs,
|
|
34879
35853
|
wait: false,
|
|
35854
|
+
// Keyframe mode → single init frame (image-to-video); otherwise character
|
|
35855
|
+
// reference-to-video.
|
|
35856
|
+
image: keyframeImageAbs,
|
|
35857
|
+
refImages: keyframeImageAbs ? void 0 : characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
34880
35858
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
34881
35859
|
});
|
|
34882
35860
|
if (!result.success || !result.taskId) {
|
|
@@ -34911,14 +35889,16 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34911
35889
|
cachePath: cache.path,
|
|
34912
35890
|
cacheKey: cache.key,
|
|
34913
35891
|
metadataPath,
|
|
34914
|
-
freshness: "fresh"
|
|
35892
|
+
freshness: "fresh",
|
|
35893
|
+
keyframeStatus,
|
|
35894
|
+
keyframePath: keyframeRel
|
|
34915
35895
|
};
|
|
34916
35896
|
}
|
|
34917
35897
|
const job = await createAndWriteJobRecord({
|
|
34918
35898
|
jobType: "generate-video",
|
|
34919
35899
|
provider: result.provider ?? ctx.videoProvider,
|
|
34920
35900
|
providerTaskId: result.taskId,
|
|
34921
|
-
providerTaskType: "text2video",
|
|
35901
|
+
providerTaskType: keyframeImageAbs ? "image2video" : "text2video",
|
|
34922
35902
|
status: "running",
|
|
34923
35903
|
projectDir: ctx.projectDir,
|
|
34924
35904
|
workingDirectory: ctx.projectDir,
|
|
@@ -34943,7 +35923,9 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34943
35923
|
provider: job.provider,
|
|
34944
35924
|
cachePath: cache.path,
|
|
34945
35925
|
cacheKey: cache.key,
|
|
34946
|
-
metadataPath
|
|
35926
|
+
metadataPath,
|
|
35927
|
+
keyframeStatus,
|
|
35928
|
+
keyframePath: keyframeRel
|
|
34947
35929
|
};
|
|
34948
35930
|
}
|
|
34949
35931
|
async function dispatchMusic(beat, ctx) {
|
|
@@ -35245,7 +36227,14 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
35245
36227
|
for (const outcome of outcomes) {
|
|
35246
36228
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
35247
36229
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
35248
|
-
if (outcome.
|
|
36230
|
+
if (outcome.keyframeStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
36231
|
+
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
36232
|
+
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
36233
|
+
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
|
36234
|
+
resolution: "720p",
|
|
36235
|
+
aspectRatio: "16:9"
|
|
36236
|
+
}) : 5;
|
|
36237
|
+
}
|
|
35249
36238
|
if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
|
|
35250
36239
|
}
|
|
35251
36240
|
return Number(cost.toFixed(2));
|
|
@@ -45366,10 +46355,18 @@ Examples:
|
|
|
45366
46355
|
options.ratio = "16:9";
|
|
45367
46356
|
}
|
|
45368
46357
|
if (options.dryRun) {
|
|
46358
|
+
const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
46359
|
+
durationSec: Number(options.duration) || 5,
|
|
46360
|
+
resolution: options.resolution,
|
|
46361
|
+
aspectRatio: options.ratio,
|
|
46362
|
+
fast: options.seedanceModel === "fast",
|
|
46363
|
+
hasVideoReference: Boolean(options.refVideos)
|
|
46364
|
+
}) : void 0;
|
|
45369
46365
|
outputSuccess({
|
|
45370
46366
|
command: "generate video",
|
|
45371
46367
|
startedAt,
|
|
45372
46368
|
dryRun: true,
|
|
46369
|
+
costUsd,
|
|
45373
46370
|
data: {
|
|
45374
46371
|
params: {
|
|
45375
46372
|
prompt: prompt3,
|
|
@@ -51847,6 +52844,9 @@ function clean(text) {
|
|
|
51847
52844
|
function humanise(id) {
|
|
51848
52845
|
return id.split(/[-_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
51849
52846
|
}
|
|
52847
|
+
function sceneCompositionId(beatId) {
|
|
52848
|
+
return `scene-${beatId}`;
|
|
52849
|
+
}
|
|
51850
52850
|
function slugifySceneName(name) {
|
|
51851
52851
|
const normalised = name.normalize("NFKD").replace(/[̀-ͯ]/g, "");
|
|
51852
52852
|
const slug = normalised.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -51884,10 +52884,16 @@ function fitKineticFontSize(words) {
|
|
|
51884
52884
|
function kenBurnsTween(scope, dur, endScale = 1.08) {
|
|
51885
52885
|
return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
|
|
51886
52886
|
}
|
|
52887
|
+
function backdropMotionTweens(scope, dur, hasImage, endScale = 1.08) {
|
|
52888
|
+
const sweep = hasImage ? "" : `
|
|
52889
|
+
tl.fromTo('${scope} .ambient-sweep', { xPercent: -75, opacity: 0.18 }, { xPercent: 75, opacity: 0.62, duration: ${dur.toFixed(2)}, ease: 'sine.inOut' }, 0);`;
|
|
52890
|
+
return `${kenBurnsTween(scope, dur, endScale)}${sweep}`;
|
|
52891
|
+
}
|
|
51887
52892
|
function buildPreset(input) {
|
|
51888
52893
|
const id = input.id;
|
|
51889
52894
|
const scope = `[data-composition-id="${id}"]`;
|
|
51890
|
-
const
|
|
52895
|
+
const displayId = input.beatId ?? id;
|
|
52896
|
+
const headline = clean(input.headline) || humanise(displayId);
|
|
51891
52897
|
const subhead = clean(input.subhead);
|
|
51892
52898
|
const kicker = clean(input.kicker);
|
|
51893
52899
|
const dur = input.duration;
|
|
@@ -51910,8 +52916,17 @@ function buildPreset(input) {
|
|
|
51910
52916
|
position: absolute;
|
|
51911
52917
|
inset: 0;
|
|
51912
52918
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 70%);
|
|
52919
|
+
overflow: hidden;
|
|
52920
|
+
}
|
|
52921
|
+
${scope} .ambient-sweep {
|
|
52922
|
+
position: absolute;
|
|
52923
|
+
inset: -22%;
|
|
52924
|
+
display: block;
|
|
52925
|
+
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%);
|
|
52926
|
+
filter: blur(12px);
|
|
52927
|
+
transform: rotate(8deg);
|
|
51913
52928
|
}`;
|
|
51914
|
-
const backdropMarkup = `<div class="backdrop"></div>`;
|
|
52929
|
+
const backdropMarkup = hasImage ? `<div class="backdrop"></div>` : `<div class="backdrop"><div class="ambient-sweep"></div></div>`;
|
|
51915
52930
|
switch (input.preset) {
|
|
51916
52931
|
case "simple": {
|
|
51917
52932
|
const transcript = input.transcript;
|
|
@@ -51943,7 +52958,7 @@ function buildPreset(input) {
|
|
|
51943
52958
|
body: `${backdropMarkup}
|
|
51944
52959
|
<div class="caption" id="caption">${captionInner}</div>`,
|
|
51945
52960
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
51946
|
-
${
|
|
52961
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
51947
52962
|
${captionTween}`
|
|
51948
52963
|
};
|
|
51949
52964
|
}
|
|
@@ -51982,12 +52997,12 @@ function buildPreset(input) {
|
|
|
51982
52997
|
// scene's own fade-in handles the visual transition. Backdrop
|
|
51983
52998
|
// Ken-Burns fills what used to be 80% dead static frame.
|
|
51984
52999
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
51985
|
-
${
|
|
53000
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
51986
53001
|
tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
|
|
51987
53002
|
};
|
|
51988
53003
|
}
|
|
51989
53004
|
case "explainer": {
|
|
51990
|
-
const k = kicker || humanise(
|
|
53005
|
+
const k = kicker || humanise(displayId).toUpperCase();
|
|
51991
53006
|
const sub = subhead || "";
|
|
51992
53007
|
const transcript = input.transcript;
|
|
51993
53008
|
const useWordSync = !!(transcript && transcript.length > 0 && sub);
|
|
@@ -52029,7 +53044,7 @@ function buildPreset(input) {
|
|
|
52029
53044
|
<div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
|
|
52030
53045
|
</div>`,
|
|
52031
53046
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52032
|
-
${
|
|
53047
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
52033
53048
|
tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
|
|
52034
53049
|
tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
|
|
52035
53050
|
${subtitleTween}`
|
|
@@ -52070,12 +53085,12 @@ function buildPreset(input) {
|
|
|
52070
53085
|
body: `${backdropMarkup}
|
|
52071
53086
|
<div class="kinetic">${wordSpans}</div>`,
|
|
52072
53087
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52073
|
-
${
|
|
53088
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
52074
53089
|
${tweens}`
|
|
52075
53090
|
};
|
|
52076
53091
|
}
|
|
52077
53092
|
case "product-shot": {
|
|
52078
|
-
const label = kicker || humanise(
|
|
53093
|
+
const label = kicker || humanise(displayId);
|
|
52079
53094
|
const headlineFontPx = fitFontSize(headline, 72, 40, 50);
|
|
52080
53095
|
return {
|
|
52081
53096
|
css: `${scope} {
|
|
@@ -52109,7 +53124,7 @@ function buildPreset(input) {
|
|
|
52109
53124
|
<div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
|
|
52110
53125
|
<div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
|
|
52111
53126
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52112
|
-
|
|
53127
|
+
${backdropMotionTweens(scope, dur, hasImage, 1.12)}
|
|
52113
53128
|
tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
|
|
52114
53129
|
tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
|
|
52115
53130
|
tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
|
|
@@ -52174,9 +53189,10 @@ function emitSceneHtml(input) {
|
|
|
52174
53189
|
if (input.width <= 0 || input.height <= 0) {
|
|
52175
53190
|
throw new Error(`Invalid canvas dims: ${input.width}x${input.height}`);
|
|
52176
53191
|
}
|
|
52177
|
-
const
|
|
53192
|
+
const beatId = input.id;
|
|
53193
|
+
const id = sceneCompositionId(beatId);
|
|
52178
53194
|
const dur = Number(input.duration.toFixed(3));
|
|
52179
|
-
const parts = buildPreset({ ...input, id, duration: dur });
|
|
53195
|
+
const parts = buildPreset({ ...input, id, beatId, duration: dur });
|
|
52180
53196
|
const audioBlock = input.audioPath ? `
|
|
52181
53197
|
<audio
|
|
52182
53198
|
id="narration"
|
|
@@ -52192,7 +53208,7 @@ function emitSceneHtml(input) {
|
|
|
52192
53208
|
${lottieLayer.markup}` : "";
|
|
52193
53209
|
const lottieScript = lottieLayer ? `
|
|
52194
53210
|
${lottieLayer.script}` : "";
|
|
52195
|
-
return `<template id="
|
|
53211
|
+
return `<template id="${id}-template">
|
|
52196
53212
|
<div data-composition-id="${id}" data-start="0" data-duration="${dur}" data-width="${input.width}" data-height="${input.height}">
|
|
52197
53213
|
<style>
|
|
52198
53214
|
${parts.css}
|
|
@@ -52228,8 +53244,9 @@ function buildClipReference(opts) {
|
|
|
52228
53244
|
const duration = Number(opts.duration.toFixed(3));
|
|
52229
53245
|
const track = opts.trackIndex ?? 1;
|
|
52230
53246
|
const src = opts.src ?? `compositions/scene-${opts.id}.html`;
|
|
53247
|
+
const compositionId = sceneCompositionId(opts.id);
|
|
52231
53248
|
const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
|
|
52232
|
-
return `<div class="clip" data-composition-id="${esc(
|
|
53249
|
+
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>`;
|
|
52233
53250
|
}
|
|
52234
53251
|
function insertClipIntoRoot(rootHtml, clip) {
|
|
52235
53252
|
const clipDiv = buildClipReference(clip);
|
|
@@ -52273,9 +53290,9 @@ init_agent_host_detect();
|
|
|
52273
53290
|
// src/commands/_shared/install-skill.ts
|
|
52274
53291
|
init_bundle_content();
|
|
52275
53292
|
init_bundle();
|
|
52276
|
-
import { mkdir as mkdir5, writeFile as
|
|
53293
|
+
import { mkdir as mkdir5, writeFile as writeFile10 } from "node:fs/promises";
|
|
52277
53294
|
import { existsSync as existsSync7 } from "node:fs";
|
|
52278
|
-
import { dirname as dirname4, join as
|
|
53295
|
+
import { dirname as dirname4, join as join7, relative as relative3, resolve as resolve10 } from "node:path";
|
|
52279
53296
|
function universalFiles() {
|
|
52280
53297
|
return [
|
|
52281
53298
|
{ relPath: "SKILL.md", content: SKILL_MD },
|
|
@@ -52325,7 +53342,7 @@ function selectHostFiles(hosts) {
|
|
|
52325
53342
|
return out;
|
|
52326
53343
|
}
|
|
52327
53344
|
async function installHyperframesSkill(opts) {
|
|
52328
|
-
const projectDir =
|
|
53345
|
+
const projectDir = resolve10(opts.projectDir);
|
|
52329
53346
|
const force = opts.force ?? false;
|
|
52330
53347
|
const dryRun = opts.dryRun ?? false;
|
|
52331
53348
|
const files = [...universalFiles(), ...selectHostFiles(opts.hosts)];
|
|
@@ -52334,7 +53351,7 @@ async function installHyperframesSkill(opts) {
|
|
|
52334
53351
|
await mkdir5(projectDir, { recursive: true });
|
|
52335
53352
|
}
|
|
52336
53353
|
for (const file of files) {
|
|
52337
|
-
const absPath =
|
|
53354
|
+
const absPath = join7(projectDir, file.relPath);
|
|
52338
53355
|
const exists = existsSync7(absPath);
|
|
52339
53356
|
const bytes = Buffer.byteLength(file.content, "utf-8");
|
|
52340
53357
|
if (exists && !force) {
|
|
@@ -52350,7 +53367,7 @@ async function installHyperframesSkill(opts) {
|
|
|
52350
53367
|
continue;
|
|
52351
53368
|
}
|
|
52352
53369
|
await mkdir5(dirname4(absPath), { recursive: true });
|
|
52353
|
-
await
|
|
53370
|
+
await writeFile10(absPath, file.content, "utf-8");
|
|
52354
53371
|
actions.push({ path: file.relPath, status: "wrote", bytes });
|
|
52355
53372
|
}
|
|
52356
53373
|
return {
|
|
@@ -52375,10 +53392,10 @@ init_scene_lint();
|
|
|
52375
53392
|
init_storyboard_parse();
|
|
52376
53393
|
init_root_sync();
|
|
52377
53394
|
import { existsSync as existsSync11 } from "node:fs";
|
|
52378
|
-
import { mkdir as mkdir7, readFile as readFile11, writeFile as
|
|
52379
|
-
import { join as
|
|
53395
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile13 } from "node:fs/promises";
|
|
53396
|
+
import { join as join11, resolve as resolve14 } from "node:path";
|
|
52380
53397
|
async function executeSceneSubmit(opts) {
|
|
52381
|
-
const projectDir =
|
|
53398
|
+
const projectDir = resolve14(opts.projectDir);
|
|
52382
53399
|
const beatId = opts.beatId.trim();
|
|
52383
53400
|
const scenePath = `compositions/scene-${beatId}.html`;
|
|
52384
53401
|
const base = {
|
|
@@ -52390,7 +53407,7 @@ async function executeSceneSubmit(opts) {
|
|
|
52390
53407
|
warnings: [],
|
|
52391
53408
|
retryWith: []
|
|
52392
53409
|
};
|
|
52393
|
-
const storyboardPath =
|
|
53410
|
+
const storyboardPath = join11(projectDir, "STORYBOARD.md");
|
|
52394
53411
|
if (!existsSync11(storyboardPath)) {
|
|
52395
53412
|
return {
|
|
52396
53413
|
...base,
|
|
@@ -52468,9 +53485,9 @@ async function executeSceneSubmit(opts) {
|
|
|
52468
53485
|
if (opts.validateOnly) {
|
|
52469
53486
|
return { ...base, success: true };
|
|
52470
53487
|
}
|
|
52471
|
-
const absPath =
|
|
52472
|
-
await mkdir7(
|
|
52473
|
-
await
|
|
53488
|
+
const absPath = join11(projectDir, scenePath);
|
|
53489
|
+
await mkdir7(join11(projectDir, "compositions"), { recursive: true });
|
|
53490
|
+
await writeFile13(absPath, html.endsWith("\n") ? html : `${html}
|
|
52474
53491
|
`, "utf-8");
|
|
52475
53492
|
return {
|
|
52476
53493
|
...base,
|
|
@@ -52485,6 +53502,8 @@ async function executeSceneSubmit(opts) {
|
|
|
52485
53502
|
|
|
52486
53503
|
// src/commands/scene.ts
|
|
52487
53504
|
init_scene_repair();
|
|
53505
|
+
init_root_sync();
|
|
53506
|
+
init_storyboard_edit();
|
|
52488
53507
|
function validateDuration(value) {
|
|
52489
53508
|
const n = parseFloat(value);
|
|
52490
53509
|
if (!Number.isFinite(n) || n <= 0 || n > 3600) {
|
|
@@ -52787,7 +53806,10 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52787
53806
|
).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
|
|
52788
53807
|
"--visuals <prompt>",
|
|
52789
53808
|
"Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider"
|
|
52790
|
-
).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(
|
|
53809
|
+
).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(
|
|
53810
|
+
"--no-storyboard",
|
|
53811
|
+
"Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
|
|
53812
|
+
).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
|
|
52791
53813
|
"--tts <provider>",
|
|
52792
53814
|
"TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
|
|
52793
53815
|
"auto"
|
|
@@ -52856,6 +53878,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52856
53878
|
kicker: options.kicker,
|
|
52857
53879
|
project: options.project,
|
|
52858
53880
|
insertInto: options.insertInto,
|
|
53881
|
+
syncStoryboard: options.storyboard !== false,
|
|
52859
53882
|
imageProvider: options.imageProvider,
|
|
52860
53883
|
tts,
|
|
52861
53884
|
audio: options.audio,
|
|
@@ -52883,6 +53906,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52883
53906
|
kicker: options.kicker,
|
|
52884
53907
|
projectDir: options.project,
|
|
52885
53908
|
insertInto: options.insertInto,
|
|
53909
|
+
syncStoryboard: options.storyboard !== false,
|
|
52886
53910
|
imageProvider: options.imageProvider,
|
|
52887
53911
|
tts,
|
|
52888
53912
|
voice: options.voice,
|
|
@@ -52922,6 +53946,13 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52922
53946
|
if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
|
|
52923
53947
|
if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
|
|
52924
53948
|
if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
|
|
53949
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
53950
|
+
console.log(
|
|
53951
|
+
result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
|
|
53952
|
+
result.storyboardPath,
|
|
53953
|
+
source_default.dim(`(${result.storyboardAction})`)
|
|
53954
|
+
);
|
|
53955
|
+
}
|
|
52925
53956
|
console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
|
|
52926
53957
|
console.log();
|
|
52927
53958
|
console.log(source_default.bold.cyan("Composition"));
|
|
@@ -52968,6 +53999,21 @@ async function loadVibeProjectConfig(projectDir) {
|
|
|
52968
53999
|
}
|
|
52969
54000
|
};
|
|
52970
54001
|
}
|
|
54002
|
+
function escapeRegExp3(value) {
|
|
54003
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54004
|
+
}
|
|
54005
|
+
function readClipStart(rootHtml, beatId) {
|
|
54006
|
+
const compositionId = sceneCompositionId(beatId);
|
|
54007
|
+
const match = rootHtml.match(
|
|
54008
|
+
new RegExp(
|
|
54009
|
+
`<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
|
|
54010
|
+
"i"
|
|
54011
|
+
)
|
|
54012
|
+
);
|
|
54013
|
+
if (!match) return void 0;
|
|
54014
|
+
const start = Number.parseFloat(match[1]);
|
|
54015
|
+
return Number.isFinite(start) ? start : void 0;
|
|
54016
|
+
}
|
|
52971
54017
|
async function resolveNarrationText(value) {
|
|
52972
54018
|
if (!value) return void 0;
|
|
52973
54019
|
const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
|
|
@@ -52994,6 +54040,7 @@ async function executeSceneAdd(opts) {
|
|
|
52994
54040
|
const id = slugifySceneName(opts.name);
|
|
52995
54041
|
const sceneRel = `compositions/scene-${id}.html`;
|
|
52996
54042
|
const scenePath = resolve17(projectDir, sceneRel);
|
|
54043
|
+
const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
|
|
52997
54044
|
const errResult = (error) => ({
|
|
52998
54045
|
success: false,
|
|
52999
54046
|
id,
|
|
@@ -53213,6 +54260,27 @@ async function executeSceneAdd(opts) {
|
|
|
53213
54260
|
duration = fallback2;
|
|
53214
54261
|
}
|
|
53215
54262
|
duration = Number(duration.toFixed(2));
|
|
54263
|
+
let storyboardPath;
|
|
54264
|
+
let storyboardSynced = false;
|
|
54265
|
+
let storyboardAction = "skipped";
|
|
54266
|
+
const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
|
|
54267
|
+
if (shouldSyncStoryboard) {
|
|
54268
|
+
opts.onProgress?.("Syncing STORYBOARD.md...");
|
|
54269
|
+
const before = await readFile14(storyboardAbsPath, "utf-8");
|
|
54270
|
+
const synced = upsertStoryboardBeat(before, {
|
|
54271
|
+
beatId: id,
|
|
54272
|
+
title: opts.headline,
|
|
54273
|
+
duration,
|
|
54274
|
+
narration: narrationText && !opts.skipAudio ? narrationText : void 0,
|
|
54275
|
+
backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
|
|
54276
|
+
});
|
|
54277
|
+
if (synced.markdown !== before) {
|
|
54278
|
+
await writeFile15(storyboardAbsPath, synced.markdown, "utf-8");
|
|
54279
|
+
}
|
|
54280
|
+
storyboardPath = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
|
|
54281
|
+
storyboardSynced = true;
|
|
54282
|
+
storyboardAction = synced.action;
|
|
54283
|
+
}
|
|
53216
54284
|
opts.onProgress?.("Emitting scene HTML...");
|
|
53217
54285
|
const sceneHtml = emitSceneHtml({
|
|
53218
54286
|
id,
|
|
@@ -53231,11 +54299,24 @@ async function executeSceneAdd(opts) {
|
|
|
53231
54299
|
await mkdir8(dirname6(scenePath), { recursive: true });
|
|
53232
54300
|
await writeFile15(scenePath, sceneHtml, "utf-8");
|
|
53233
54301
|
opts.onProgress?.("Updating root composition...");
|
|
53234
|
-
|
|
53235
|
-
|
|
53236
|
-
|
|
53237
|
-
|
|
53238
|
-
|
|
54302
|
+
let start = 0;
|
|
54303
|
+
if (storyboardSynced) {
|
|
54304
|
+
const syncPlan = await syncRootComposition({
|
|
54305
|
+
projectDir,
|
|
54306
|
+
rootRel,
|
|
54307
|
+
beats: await loadProjectRootSyncBeats(projectDir)
|
|
54308
|
+
});
|
|
54309
|
+
const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
|
|
54310
|
+
if (blockingIssue) return errResult(blockingIssue.message);
|
|
54311
|
+
const rootHtmlAfterSync = await readFile14(rootPath, "utf-8");
|
|
54312
|
+
start = readClipStart(rootHtmlAfterSync, id) ?? 0;
|
|
54313
|
+
} else {
|
|
54314
|
+
start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
|
|
54315
|
+
const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
|
|
54316
|
+
const trackIndex = existingClipCount % 2 + 1;
|
|
54317
|
+
const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
|
|
54318
|
+
await writeFile15(rootPath, updated, "utf-8");
|
|
54319
|
+
}
|
|
53239
54320
|
const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
|
|
53240
54321
|
return {
|
|
53241
54322
|
success: true,
|
|
@@ -53248,6 +54329,9 @@ async function executeSceneAdd(opts) {
|
|
|
53248
54329
|
audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
|
|
53249
54330
|
imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
|
|
53250
54331
|
lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
|
|
54332
|
+
storyboardPath,
|
|
54333
|
+
storyboardSynced,
|
|
54334
|
+
storyboardAction,
|
|
53251
54335
|
transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
|
|
53252
54336
|
transcriptWordCount
|
|
53253
54337
|
};
|
|
@@ -53545,6 +54629,9 @@ var sceneAddSchema = external_exports.object({
|
|
|
53545
54629
|
kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
|
|
53546
54630
|
projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
|
|
53547
54631
|
insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
|
|
54632
|
+
syncStoryboard: external_exports.boolean().optional().describe(
|
|
54633
|
+
"Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
|
|
54634
|
+
),
|
|
53548
54635
|
imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
|
|
53549
54636
|
voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
|
|
53550
54637
|
skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
|
|
@@ -53557,7 +54644,7 @@ var sceneAddTool = defineTool({
|
|
|
53557
54644
|
cost: "low",
|
|
53558
54645
|
title: "Add Scene from Preset",
|
|
53559
54646
|
annotations: { readOnly: false, openWorld: true },
|
|
53560
|
-
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
|
|
54647
|
+
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.",
|
|
53561
54648
|
schema: sceneAddSchema,
|
|
53562
54649
|
async execute(args, ctx) {
|
|
53563
54650
|
const projectDir = args.projectDir ? resolve26(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
|
|
@@ -53571,6 +54658,7 @@ var sceneAddTool = defineTool({
|
|
|
53571
54658
|
kicker: args.kicker,
|
|
53572
54659
|
projectDir,
|
|
53573
54660
|
insertInto: args.insertInto,
|
|
54661
|
+
syncStoryboard: args.syncStoryboard,
|
|
53574
54662
|
imageProvider: args.imageProvider,
|
|
53575
54663
|
voice: args.voice,
|
|
53576
54664
|
skipAudio: args.skipAudio,
|
|
@@ -53589,6 +54677,9 @@ var sceneAddTool = defineTool({
|
|
|
53589
54677
|
];
|
|
53590
54678
|
if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
|
|
53591
54679
|
if (result.imagePath) lines.push(` image: ${result.imagePath}`);
|
|
54680
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
54681
|
+
lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
|
|
54682
|
+
}
|
|
53592
54683
|
return {
|
|
53593
54684
|
success: true,
|
|
53594
54685
|
data: {
|
|
@@ -53599,7 +54690,10 @@ var sceneAddTool = defineTool({
|
|
|
53599
54690
|
scenePath: result.scenePath,
|
|
53600
54691
|
rootPath: result.rootPath,
|
|
53601
54692
|
audioPath: result.audioPath,
|
|
53602
|
-
imagePath: result.imagePath
|
|
54693
|
+
imagePath: result.imagePath,
|
|
54694
|
+
storyboardPath: result.storyboardPath,
|
|
54695
|
+
storyboardSynced: result.storyboardSynced,
|
|
54696
|
+
storyboardAction: result.storyboardAction
|
|
53603
54697
|
},
|
|
53604
54698
|
humanLines: lines
|
|
53605
54699
|
};
|
|
@@ -55260,7 +56354,11 @@ async function inspectProject(opts) {
|
|
|
55260
56354
|
);
|
|
55261
56355
|
}
|
|
55262
56356
|
function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
55263
|
-
const
|
|
56357
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
56358
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
56359
|
+
projectDir,
|
|
56360
|
+
retryWith: normalizedRetryWith
|
|
56361
|
+
});
|
|
55264
56362
|
const status = statusFromIssues(normalizedIssues);
|
|
55265
56363
|
return {
|
|
55266
56364
|
schemaVersion: "1",
|
|
@@ -55272,9 +56370,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
|
55272
56370
|
score: scoreIssues(normalizedIssues),
|
|
55273
56371
|
issues: normalizedIssues,
|
|
55274
56372
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
56373
|
+
nextActions: deriveNextReviewActions({
|
|
56374
|
+
issues: normalizedIssues,
|
|
56375
|
+
retryWith: normalizedRetryWith,
|
|
56376
|
+
projectDir
|
|
56377
|
+
}),
|
|
55275
56378
|
sourceReports: projectSourceReports(checks),
|
|
55276
56379
|
checks,
|
|
55277
|
-
retryWith:
|
|
56380
|
+
retryWith: normalizedRetryWith
|
|
55278
56381
|
};
|
|
55279
56382
|
}
|
|
55280
56383
|
function storyboardPlaceholderIssues(storyboard) {
|
|
@@ -55826,6 +56929,8 @@ async function inspectRender(opts) {
|
|
|
55826
56929
|
severity: "error",
|
|
55827
56930
|
code: "RENDER_NOT_FOUND",
|
|
55828
56931
|
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.",
|
|
56932
|
+
beatId: opts.beatId,
|
|
56933
|
+
scene: opts.beatId,
|
|
55829
56934
|
suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
|
|
55830
56935
|
});
|
|
55831
56936
|
retryWith.push(
|
|
@@ -55848,6 +56953,8 @@ async function inspectRender(opts) {
|
|
|
55848
56953
|
code: "EMPTY_RENDER",
|
|
55849
56954
|
message: "Rendered video file is empty.",
|
|
55850
56955
|
file: displayPath(videoPath),
|
|
56956
|
+
beatId: opts.beatId,
|
|
56957
|
+
scene: opts.beatId,
|
|
55851
56958
|
suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
|
|
55852
56959
|
});
|
|
55853
56960
|
retryWith.push(
|
|
@@ -55857,6 +56964,8 @@ async function inspectRender(opts) {
|
|
|
55857
56964
|
const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
|
|
55858
56965
|
const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
|
|
55859
56966
|
if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
|
|
56967
|
+
const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
|
|
56968
|
+
checks.expectedAudio = expectedAudio;
|
|
55860
56969
|
if (!commandExists("ffprobe")) {
|
|
55861
56970
|
issues.push({
|
|
55862
56971
|
severity: "error",
|
|
@@ -55882,7 +56991,7 @@ async function inspectRender(opts) {
|
|
|
55882
56991
|
file: displayPath(videoPath)
|
|
55883
56992
|
});
|
|
55884
56993
|
}
|
|
55885
|
-
if (!audioStream) {
|
|
56994
|
+
if (!audioStream && expectedAudio) {
|
|
55886
56995
|
issues.push({
|
|
55887
56996
|
severity: "warning",
|
|
55888
56997
|
code: "NO_AUDIO_STREAM",
|
|
@@ -56087,8 +57196,86 @@ async function inspectRender(opts) {
|
|
|
56087
57196
|
);
|
|
56088
57197
|
return maybeWriteRenderReport(projectDir, opts, result);
|
|
56089
57198
|
}
|
|
57199
|
+
async function renderExpectsAudio(projectDir, beatId) {
|
|
57200
|
+
const root = resolve42(projectDir);
|
|
57201
|
+
const rootHtml = await readTextIfExists(join22(root, "index.html"));
|
|
57202
|
+
if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
|
|
57203
|
+
const buildReport = await readJsonIfExists(join22(root, "build-report.json"));
|
|
57204
|
+
if (buildReportExpectsAudio(buildReport, beatId)) return true;
|
|
57205
|
+
const storyboard = await readTextIfExists(join22(root, "STORYBOARD.md"));
|
|
57206
|
+
if (!storyboard) return false;
|
|
57207
|
+
try {
|
|
57208
|
+
const parsed = parseStoryboard(storyboard);
|
|
57209
|
+
const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
|
|
57210
|
+
return beats.some(
|
|
57211
|
+
(beat) => [
|
|
57212
|
+
`assets/narration-${beat.id}.mp3`,
|
|
57213
|
+
`assets/narration-${beat.id}.wav`,
|
|
57214
|
+
`assets/music-${beat.id}.mp3`,
|
|
57215
|
+
`assets/music-${beat.id}.wav`
|
|
57216
|
+
].some((asset) => existsSync39(join22(root, asset)))
|
|
57217
|
+
);
|
|
57218
|
+
} catch {
|
|
57219
|
+
return false;
|
|
57220
|
+
}
|
|
57221
|
+
}
|
|
57222
|
+
function rootHtmlExpectsAudio(html, beatId) {
|
|
57223
|
+
const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
|
|
57224
|
+
if (!beatId) return audioTags.length > 0;
|
|
57225
|
+
const escaped = escapeRegExp4(beatId);
|
|
57226
|
+
const beatAudioRe = new RegExp(
|
|
57227
|
+
`(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
|
|
57228
|
+
"i"
|
|
57229
|
+
);
|
|
57230
|
+
return audioTags.some((tag) => beatAudioRe.test(tag));
|
|
57231
|
+
}
|
|
57232
|
+
function buildReportExpectsAudio(report, beatId) {
|
|
57233
|
+
if (!report || typeof report !== "object") return false;
|
|
57234
|
+
const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
|
|
57235
|
+
const selected = beatId === void 0 ? beats : beats.filter((beat) => {
|
|
57236
|
+
if (!beat || typeof beat !== "object") return false;
|
|
57237
|
+
return beat.id === beatId;
|
|
57238
|
+
});
|
|
57239
|
+
return selected.some((beat) => beatReportExpectsAudio(beat));
|
|
57240
|
+
}
|
|
57241
|
+
function beatReportExpectsAudio(beat) {
|
|
57242
|
+
if (!beat || typeof beat !== "object") return false;
|
|
57243
|
+
const record = beat;
|
|
57244
|
+
if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
|
|
57245
|
+
if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
|
|
57246
|
+
if (parseOptionalNumber2(record.narrationDurationSec) !== void 0) return true;
|
|
57247
|
+
const narration = record.narration;
|
|
57248
|
+
if (narration && typeof narration === "object") {
|
|
57249
|
+
const n = narration;
|
|
57250
|
+
if (typeof n.path === "string" && n.path.length > 0) return true;
|
|
57251
|
+
if (parseOptionalNumber2(n.durationSec) !== void 0) return true;
|
|
57252
|
+
}
|
|
57253
|
+
const music = record.music;
|
|
57254
|
+
if (music && typeof music === "object") {
|
|
57255
|
+
const m = music;
|
|
57256
|
+
if (typeof m.path === "string" && m.path.length > 0) return true;
|
|
57257
|
+
if (parseOptionalNumber2(m.durationSec) !== void 0) return true;
|
|
57258
|
+
}
|
|
57259
|
+
return false;
|
|
57260
|
+
}
|
|
57261
|
+
async function readJsonIfExists(path2) {
|
|
57262
|
+
const text = await readTextIfExists(path2);
|
|
57263
|
+
if (!text) return null;
|
|
57264
|
+
try {
|
|
57265
|
+
return JSON.parse(text);
|
|
57266
|
+
} catch {
|
|
57267
|
+
return null;
|
|
57268
|
+
}
|
|
57269
|
+
}
|
|
57270
|
+
function escapeRegExp4(value) {
|
|
57271
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57272
|
+
}
|
|
56090
57273
|
function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
|
|
56091
|
-
const
|
|
57274
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
57275
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
57276
|
+
projectDir,
|
|
57277
|
+
retryWith: normalizedRetryWith
|
|
57278
|
+
});
|
|
56092
57279
|
const status = statusFromIssues(normalizedIssues);
|
|
56093
57280
|
return {
|
|
56094
57281
|
schemaVersion: "1",
|
|
@@ -56101,9 +57288,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
|
|
|
56101
57288
|
score,
|
|
56102
57289
|
issues: normalizedIssues,
|
|
56103
57290
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
57291
|
+
nextActions: deriveNextReviewActions({
|
|
57292
|
+
issues: normalizedIssues,
|
|
57293
|
+
retryWith: normalizedRetryWith,
|
|
57294
|
+
projectDir
|
|
57295
|
+
}),
|
|
56104
57296
|
sourceReports: renderSourceReports(projectDir, videoPath, checks),
|
|
56105
57297
|
checks,
|
|
56106
|
-
retryWith:
|
|
57298
|
+
retryWith: normalizedRetryWith
|
|
56107
57299
|
};
|
|
56108
57300
|
}
|
|
56109
57301
|
async function maybeWriteRenderReport(projectDir, opts, result) {
|