@vibeframe/cli 0.113.0 → 0.113.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +59 -10
- package/dist/commands/_shared/build-cache.d.ts +10 -1
- package/dist/commands/_shared/build-cache.test.d.ts +1 -0
- package/dist/commands/_shared/init-templates.d.ts +1 -1
- package/dist/commands/_shared/render-inspect.d.ts +4 -1
- package/dist/commands/_shared/review-report.d.ts +50 -1
- package/dist/commands/_shared/review-report.test.d.ts +1 -0
- package/dist/commands/_shared/scene-build.d.ts +13 -0
- package/dist/commands/_shared/scene-html-emit.d.ts +4 -2
- package/dist/commands/_shared/scene-inspect.d.ts +2 -1
- package/dist/commands/_shared/status-jobs.d.ts +3 -0
- package/dist/commands/_shared/storyboard-edit.d.ts +15 -1
- package/dist/commands/_shared/storyboard-parse.d.ts +34 -0
- package/dist/commands/error-envelopes.test.d.ts +26 -0
- package/dist/commands/review-actions-cli.test.d.ts +1 -0
- package/dist/commands/scene.d.ts +12 -0
- package/dist/index.js +1428 -272
- package/dist/tools/manifest/edit.d.ts +6 -6
- package/dist/tools/manifest/index.js +1506 -471
- package/dist/tools/manifest/scene.d.ts +3 -0
- package/package.json +4 -4
|
@@ -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,297 @@ 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
|
+
"motion",
|
|
32484
|
+
"voice",
|
|
32485
|
+
"music",
|
|
32486
|
+
"asset",
|
|
32487
|
+
"characters"
|
|
32488
|
+
];
|
|
32489
|
+
HEADING_RE2 = /^##\s+(.+?)\s*$/gm;
|
|
32490
|
+
LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
|
|
32491
|
+
ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
|
|
32492
|
+
STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
|
|
32493
|
+
MAX_RECOMMENDED_BEAT_SEC = 15;
|
|
32494
|
+
}
|
|
32495
|
+
});
|
|
32496
|
+
|
|
31654
32497
|
// src/pipeline/renderers/chrome.ts
|
|
31655
32498
|
import { existsSync as existsSync14 } from "node:fs";
|
|
31656
32499
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -32567,6 +33410,7 @@ var init_build_asset_reference = __esm({
|
|
|
32567
33410
|
EXTENSIONS = {
|
|
32568
33411
|
narration: [".mp3", ".wav", ".m4a"],
|
|
32569
33412
|
backdrop: [".png", ".jpg", ".jpeg", ".webp"],
|
|
33413
|
+
character: [".png", ".jpg", ".jpeg", ".webp"],
|
|
32570
33414
|
video: [".mp4", ".mov", ".webm"],
|
|
32571
33415
|
music: [".mp3", ".wav", ".m4a"]
|
|
32572
33416
|
};
|
|
@@ -32604,6 +33448,16 @@ function backdropCacheDescriptor(opts) {
|
|
|
32604
33448
|
ext: "png"
|
|
32605
33449
|
});
|
|
32606
33450
|
}
|
|
33451
|
+
function characterCacheDescriptor(opts) {
|
|
33452
|
+
return cacheAssetDescriptor("character", {
|
|
33453
|
+
name: opts.name,
|
|
33454
|
+
cue: opts.cue,
|
|
33455
|
+
provider: opts.provider,
|
|
33456
|
+
quality: opts.quality,
|
|
33457
|
+
size: opts.size,
|
|
33458
|
+
ext: "png"
|
|
33459
|
+
});
|
|
33460
|
+
}
|
|
32607
33461
|
function imageRatioForSize(size) {
|
|
32608
33462
|
switch (size) {
|
|
32609
33463
|
case "1024x1024":
|
|
@@ -32623,6 +33477,8 @@ function videoCacheDescriptor(opts) {
|
|
|
32623
33477
|
provider: opts.provider,
|
|
32624
33478
|
duration: normalizeVideoDuration(opts.duration),
|
|
32625
33479
|
ratio: "16:9",
|
|
33480
|
+
// Omit when empty so existing (character-less) clips keep their cache key.
|
|
33481
|
+
characters: opts.characters && opts.characters.length > 0 ? opts.characters : void 0,
|
|
32626
33482
|
ext: "mp4"
|
|
32627
33483
|
});
|
|
32628
33484
|
}
|
|
@@ -32675,208 +33531,6 @@ var init_build_backdrop_prompt = __esm({
|
|
|
32675
33531
|
}
|
|
32676
33532
|
});
|
|
32677
33533
|
|
|
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
33534
|
// src/commands/_shared/build-plan.ts
|
|
32881
33535
|
import { existsSync as existsSync17 } from "node:fs";
|
|
32882
33536
|
import { readFile as readFile17 } from "node:fs/promises";
|
|
@@ -33052,7 +33706,11 @@ async function createBuildPlan(opts) {
|
|
|
33052
33706
|
reference: videoReference,
|
|
33053
33707
|
projectDir,
|
|
33054
33708
|
force: opts.force,
|
|
33055
|
-
cost:
|
|
33709
|
+
cost: resolved.video.resolved === "seedance" || resolved.video.resolved === "fal" ? estimateSeedanceVideoCostUsd({
|
|
33710
|
+
durationSec: normalizeVideoDuration(beat.duration),
|
|
33711
|
+
resolution: "720p",
|
|
33712
|
+
aspectRatio: "16:9"
|
|
33713
|
+
}) : VIDEO_COST_USD,
|
|
33056
33714
|
active: includeAssets
|
|
33057
33715
|
}) : null;
|
|
33058
33716
|
const music = musicCue && !opts.skipMusic ? assetPlan({
|
|
@@ -33163,6 +33821,21 @@ async function createBuildPlan(opts) {
|
|
|
33163
33821
|
retryWith.push(skipCommand);
|
|
33164
33822
|
}
|
|
33165
33823
|
}
|
|
33824
|
+
if (validationOk && includeAssets && !opts.skipVideo) {
|
|
33825
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
33826
|
+
for (const beat of sourceBeats) {
|
|
33827
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
33828
|
+
}
|
|
33829
|
+
if (referenced.size > 0) {
|
|
33830
|
+
const toGenerate = resolveCharacters(parsed.frontmatter).filter(
|
|
33831
|
+
(c) => referenced.has(c.name) && c.prompt && (opts.force || !existsSync17(join15(projectDir, `assets/character-${c.name}.png`)))
|
|
33832
|
+
);
|
|
33833
|
+
if (toGenerate.length > 0) {
|
|
33834
|
+
estimatedCostUsd += toGenerate.length * backdropCostUsd(opts.imageQuality ?? "hd");
|
|
33835
|
+
missing.add("assets");
|
|
33836
|
+
}
|
|
33837
|
+
}
|
|
33838
|
+
}
|
|
33166
33839
|
return finalizeBuildPlan({
|
|
33167
33840
|
projectDir,
|
|
33168
33841
|
config: config2,
|
|
@@ -33527,6 +34200,7 @@ var init_build_plan = __esm({
|
|
|
33527
34200
|
init_schema();
|
|
33528
34201
|
init_build_asset_reference();
|
|
33529
34202
|
init_build_cache();
|
|
34203
|
+
init_dist2();
|
|
33530
34204
|
init_build_asset_metadata();
|
|
33531
34205
|
init_build_backdrop_prompt();
|
|
33532
34206
|
init_composer_resolve();
|
|
@@ -33859,7 +34533,7 @@ __export(scene_build_exports, {
|
|
|
33859
34533
|
executeSceneBuild: () => executeSceneBuild,
|
|
33860
34534
|
resolveSceneBuildMode: () => resolveSceneBuildMode
|
|
33861
34535
|
});
|
|
33862
|
-
import { existsSync as existsSync19 } from "node:fs";
|
|
34536
|
+
import { existsSync as existsSync19, readFileSync as readFileSync3 } from "node:fs";
|
|
33863
34537
|
import { copyFile as copyFile3, mkdir as mkdir10, readFile as readFile18, writeFile as writeFile18 } from "node:fs/promises";
|
|
33864
34538
|
import { dirname as dirname10, join as join16, resolve as resolve25 } from "node:path";
|
|
33865
34539
|
async function executeSceneBuild(opts) {
|
|
@@ -34026,8 +34700,32 @@ async function executeSceneBuild(opts) {
|
|
|
34026
34700
|
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
34027
34701
|
let beatOutcomes = collectExistingBeatOutcomes(parsed.beats, projectDir);
|
|
34028
34702
|
let pendingJobs = [];
|
|
34703
|
+
let characterCostUsd = 0;
|
|
34029
34704
|
if (shouldRunStage(selectedStage, "assets")) {
|
|
34030
34705
|
onProgress({ type: "phase-start", phase: "primitives" });
|
|
34706
|
+
const characterPaths = /* @__PURE__ */ new Map();
|
|
34707
|
+
if (!(opts.skipVideo ?? false)) {
|
|
34708
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
34709
|
+
for (const beat of activeBeats) {
|
|
34710
|
+
for (const name of beatCharacterNames(beat.cues)) referenced.add(name);
|
|
34711
|
+
}
|
|
34712
|
+
if (referenced.size > 0) {
|
|
34713
|
+
const charResult = await buildCharacters(
|
|
34714
|
+
resolveCharacters(parsed.frontmatter).filter((c) => referenced.has(c.name)),
|
|
34715
|
+
{
|
|
34716
|
+
projectDir,
|
|
34717
|
+
imageProvider,
|
|
34718
|
+
imageSize: opts.imageSize ?? "1536x1024",
|
|
34719
|
+
imageQuality: opts.imageQuality ?? "hd",
|
|
34720
|
+
force: opts.force ?? false,
|
|
34721
|
+
onProgress
|
|
34722
|
+
}
|
|
34723
|
+
);
|
|
34724
|
+
for (const [name, path2] of charResult.paths) characterPaths.set(name, path2);
|
|
34725
|
+
characterCostUsd = charResult.costUsd;
|
|
34726
|
+
warnings.push(...charResult.failures);
|
|
34727
|
+
}
|
|
34728
|
+
}
|
|
34031
34729
|
const primitiveResults = await mapWithConcurrency(
|
|
34032
34730
|
activeBeats,
|
|
34033
34731
|
BEAT_PRIMITIVES_CONCURRENCY,
|
|
@@ -34045,7 +34743,8 @@ async function executeSceneBuild(opts) {
|
|
|
34045
34743
|
skipVideo: opts.skipVideo ?? false,
|
|
34046
34744
|
skipMusic: opts.skipMusic ?? false,
|
|
34047
34745
|
force: opts.force ?? false,
|
|
34048
|
-
onProgress
|
|
34746
|
+
onProgress,
|
|
34747
|
+
characterPaths
|
|
34049
34748
|
})
|
|
34050
34749
|
);
|
|
34051
34750
|
beatOutcomes = primitiveResults.map((result) => result.outcome);
|
|
@@ -34054,10 +34753,7 @@ async function executeSceneBuild(opts) {
|
|
|
34054
34753
|
(beat) => beat.narrationStatus === "failed" || beat.backdropStatus === "failed" || beat.videoStatus === "failed" || beat.musicStatus === "failed"
|
|
34055
34754
|
);
|
|
34056
34755
|
stageReports.assets.status = assetFailed ? "failed" : pendingJobs.length > 0 ? "pending-jobs" : "done";
|
|
34057
|
-
stageReports.assets.costUsd = estimateActualAssetCost(
|
|
34058
|
-
beatOutcomes,
|
|
34059
|
-
opts.imageQuality ?? "hd"
|
|
34060
|
-
);
|
|
34756
|
+
stageReports.assets.costUsd = estimateActualAssetCost(beatOutcomes, opts.imageQuality ?? "hd") + characterCostUsd;
|
|
34061
34757
|
stageReports.assets.retryWith = pendingJobs.map(
|
|
34062
34758
|
(job) => `vibe status job ${job.id} --project ${projectDir} --json`
|
|
34063
34759
|
);
|
|
@@ -34222,6 +34918,18 @@ async function executeSceneBuild(opts) {
|
|
|
34222
34918
|
stageReports.compose.status = "skipped";
|
|
34223
34919
|
}
|
|
34224
34920
|
if (stageReports.compose.status === "done") {
|
|
34921
|
+
for (const beat of activeBeats) {
|
|
34922
|
+
const videoRel = `assets/video-${beat.id}.mp4`;
|
|
34923
|
+
const compositionAbs = join16(projectDir, "compositions", `scene-${beat.id}.html`);
|
|
34924
|
+
if (!existsSync19(join16(projectDir, videoRel)) || !existsSync19(compositionAbs)) continue;
|
|
34925
|
+
const html = readFileSync3(compositionAbs, "utf-8");
|
|
34926
|
+
if (!html.includes(videoRel)) {
|
|
34927
|
+
warnings.push(
|
|
34928
|
+
`Beat "${beat.id}" has a generated clip (${videoRel}) but its composition does not reference it, so the clip will not appear. Recompose the beat.`
|
|
34929
|
+
);
|
|
34930
|
+
retryWith.push(`vibe build ${projectDir} --beat ${beat.id} --stage compose --force --json`);
|
|
34931
|
+
}
|
|
34932
|
+
}
|
|
34225
34933
|
const repair = await runBuildSceneRepair(projectDir, "compose", false);
|
|
34226
34934
|
sceneRepair = mergeSceneRepairSummaries(sceneRepair, repair);
|
|
34227
34935
|
applySceneRepairToStage(stageReports.compose, repair);
|
|
@@ -34642,6 +35350,120 @@ async function dispatchNarration(beat, ctx) {
|
|
|
34642
35350
|
freshness: "fresh"
|
|
34643
35351
|
};
|
|
34644
35352
|
}
|
|
35353
|
+
function characterSheetPrompt(name, description) {
|
|
35354
|
+
return [
|
|
35355
|
+
`Character turnaround reference sheet of an original fictional character named ${name}: ${description}.`,
|
|
35356
|
+
"Show three full-body views side by side on one clean canvas \u2014 front view, side profile, and back view \u2014",
|
|
35357
|
+
"with identical outfit, hairstyle, hair color, and body proportions across all three views.",
|
|
35358
|
+
"Plain light-grey studio background, even neutral lighting, no props.",
|
|
35359
|
+
"Photorealistic digital character, professional concept-art character sheet layout."
|
|
35360
|
+
].join(" ");
|
|
35361
|
+
}
|
|
35362
|
+
async function buildCharacters(characters, ctx) {
|
|
35363
|
+
const paths = /* @__PURE__ */ new Map();
|
|
35364
|
+
const failures = [];
|
|
35365
|
+
let costUsd = 0;
|
|
35366
|
+
const size = ctx.imageSize ?? "1536x1024";
|
|
35367
|
+
for (const character of characters) {
|
|
35368
|
+
if (character.imagePath) {
|
|
35369
|
+
if (existsSync19(join16(ctx.projectDir, character.imagePath))) {
|
|
35370
|
+
paths.set(character.name, character.imagePath);
|
|
35371
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: character.imagePath });
|
|
35372
|
+
} else {
|
|
35373
|
+
const error = `character "${character.name}" image not found: ${character.imagePath}`;
|
|
35374
|
+
failures.push(error);
|
|
35375
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error });
|
|
35376
|
+
}
|
|
35377
|
+
continue;
|
|
35378
|
+
}
|
|
35379
|
+
if (!character.prompt) continue;
|
|
35380
|
+
const prompt3 = characterSheetPrompt(character.name, character.prompt);
|
|
35381
|
+
const rel = `assets/character-${character.name}.png`;
|
|
35382
|
+
const abs = join16(ctx.projectDir, rel);
|
|
35383
|
+
const metadataOptions = { quality: ctx.imageQuality, size };
|
|
35384
|
+
const cache = characterCacheDescriptor({
|
|
35385
|
+
name: character.name,
|
|
35386
|
+
cue: prompt3,
|
|
35387
|
+
provider: ctx.imageProvider,
|
|
35388
|
+
quality: ctx.imageQuality,
|
|
35389
|
+
size
|
|
35390
|
+
});
|
|
35391
|
+
if (existsSync19(abs) && !ctx.force) {
|
|
35392
|
+
const metadata = readAssetMetadata(ctx.projectDir, "character", character.name);
|
|
35393
|
+
if (!metadata || isFreshCanonicalAsset({
|
|
35394
|
+
projectDir: ctx.projectDir,
|
|
35395
|
+
kind: "character",
|
|
35396
|
+
beatId: character.name,
|
|
35397
|
+
cue: prompt3,
|
|
35398
|
+
provider: ctx.imageProvider,
|
|
35399
|
+
options: metadataOptions,
|
|
35400
|
+
cacheKey: cache.key
|
|
35401
|
+
})) {
|
|
35402
|
+
paths.set(character.name, rel);
|
|
35403
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
35404
|
+
continue;
|
|
35405
|
+
}
|
|
35406
|
+
}
|
|
35407
|
+
const cacheAbs = join16(ctx.projectDir, cache.path);
|
|
35408
|
+
if (existsSync19(cacheAbs) && !ctx.force) {
|
|
35409
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35410
|
+
await copyFile3(cacheAbs, abs);
|
|
35411
|
+
await writeAssetMetadata({
|
|
35412
|
+
projectDir: ctx.projectDir,
|
|
35413
|
+
kind: "character",
|
|
35414
|
+
beatId: character.name,
|
|
35415
|
+
cue: prompt3,
|
|
35416
|
+
provider: ctx.imageProvider,
|
|
35417
|
+
options: metadataOptions,
|
|
35418
|
+
cacheKey: cache.key,
|
|
35419
|
+
canonicalPath: rel,
|
|
35420
|
+
cachePath: cache.path
|
|
35421
|
+
});
|
|
35422
|
+
paths.set(character.name, rel);
|
|
35423
|
+
ctx.onProgress({ type: "character-cached", name: character.name, path: rel });
|
|
35424
|
+
continue;
|
|
35425
|
+
}
|
|
35426
|
+
const generated = await generateBackdropImage(
|
|
35427
|
+
prompt3,
|
|
35428
|
+
{
|
|
35429
|
+
projectDir: ctx.projectDir,
|
|
35430
|
+
imageProvider: ctx.imageProvider,
|
|
35431
|
+
imageSize: size,
|
|
35432
|
+
imageQuality: ctx.imageQuality
|
|
35433
|
+
},
|
|
35434
|
+
imageRatioForSize(size)
|
|
35435
|
+
);
|
|
35436
|
+
if (!generated.success) {
|
|
35437
|
+
failures.push(`character "${character.name}": ${generated.error}`);
|
|
35438
|
+
ctx.onProgress({ type: "character-failed", name: character.name, error: generated.error });
|
|
35439
|
+
continue;
|
|
35440
|
+
}
|
|
35441
|
+
await mkdir10(dirname10(abs), { recursive: true });
|
|
35442
|
+
await writeFile18(abs, generated.buffer);
|
|
35443
|
+
await mkdir10(dirname10(cacheAbs), { recursive: true });
|
|
35444
|
+
await writeFile18(cacheAbs, generated.buffer);
|
|
35445
|
+
await writeAssetMetadata({
|
|
35446
|
+
projectDir: ctx.projectDir,
|
|
35447
|
+
kind: "character",
|
|
35448
|
+
beatId: character.name,
|
|
35449
|
+
cue: prompt3,
|
|
35450
|
+
provider: ctx.imageProvider,
|
|
35451
|
+
options: metadataOptions,
|
|
35452
|
+
cacheKey: cache.key,
|
|
35453
|
+
canonicalPath: rel,
|
|
35454
|
+
cachePath: cache.path
|
|
35455
|
+
});
|
|
35456
|
+
costUsd += backdropCostUsd(ctx.imageQuality);
|
|
35457
|
+
paths.set(character.name, rel);
|
|
35458
|
+
ctx.onProgress({
|
|
35459
|
+
type: "character-generated",
|
|
35460
|
+
name: character.name,
|
|
35461
|
+
path: rel,
|
|
35462
|
+
provider: ctx.imageProvider
|
|
35463
|
+
});
|
|
35464
|
+
}
|
|
35465
|
+
return { paths, costUsd: Number(costUsd.toFixed(2)), failures };
|
|
35466
|
+
}
|
|
34645
35467
|
async function dispatchBackdrop(beat, ctx) {
|
|
34646
35468
|
const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
|
|
34647
35469
|
if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
|
|
@@ -34811,13 +35633,16 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34811
35633
|
if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
|
|
34812
35634
|
const prompt3 = stringOrUndefined4(beat.cues?.video);
|
|
34813
35635
|
if (!prompt3) return { status: "no-cue" };
|
|
35636
|
+
const characterRefsRel = beatCharacterNames(beat.cues).map((name) => ctx.characterPaths?.get(name)).filter((p) => typeof p === "string");
|
|
35637
|
+
const characterRefsAbs = characterRefsRel.map((p) => join16(ctx.projectDir, p));
|
|
34814
35638
|
const rel = `assets/video-${beat.id}.mp4`;
|
|
34815
35639
|
const abs = join16(ctx.projectDir, rel);
|
|
34816
35640
|
const cache = videoCacheDescriptor({
|
|
34817
35641
|
beatId: beat.id,
|
|
34818
35642
|
cue: prompt3,
|
|
34819
35643
|
provider: ctx.videoProvider,
|
|
34820
|
-
duration: normalizeVideoDuration(beat.duration)
|
|
35644
|
+
duration: normalizeVideoDuration(beat.duration),
|
|
35645
|
+
characters: characterRefsRel
|
|
34821
35646
|
});
|
|
34822
35647
|
const metadataPath = assetMetadataPath("video", beat.id);
|
|
34823
35648
|
if (existsSync19(abs) && !ctx.force) {
|
|
@@ -34877,6 +35702,7 @@ async function dispatchVideo(beat, ctx) {
|
|
|
34877
35702
|
ratio: "16:9",
|
|
34878
35703
|
output: abs,
|
|
34879
35704
|
wait: false,
|
|
35705
|
+
refImages: characterRefsAbs.length > 0 ? characterRefsAbs : void 0,
|
|
34880
35706
|
apiKey: await apiKeyForVideoProvider(ctx.videoProvider, ctx.projectDir)
|
|
34881
35707
|
});
|
|
34882
35708
|
if (!result.success || !result.taskId) {
|
|
@@ -35245,7 +36071,13 @@ function estimateActualAssetCost(outcomes, imageQuality) {
|
|
|
35245
36071
|
for (const outcome of outcomes) {
|
|
35246
36072
|
if (outcome.narrationStatus === "generated") cost += 0.05;
|
|
35247
36073
|
if (outcome.backdropStatus === "generated") cost += backdropCostUsd(imageQuality);
|
|
35248
|
-
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending")
|
|
36074
|
+
if (outcome.videoStatus === "generated" || outcome.videoStatus === "pending") {
|
|
36075
|
+
cost += outcome.videoProvider === "seedance" || outcome.videoProvider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
36076
|
+
durationSec: normalizeVideoDuration(outcome.sceneDurationSec),
|
|
36077
|
+
resolution: "720p",
|
|
36078
|
+
aspectRatio: "16:9"
|
|
36079
|
+
}) : 5;
|
|
36080
|
+
}
|
|
35249
36081
|
if (outcome.musicStatus === "generated" || outcome.musicStatus === "pending") cost += 0.5;
|
|
35250
36082
|
}
|
|
35251
36083
|
return Number(cost.toFixed(2));
|
|
@@ -45366,10 +46198,18 @@ Examples:
|
|
|
45366
46198
|
options.ratio = "16:9";
|
|
45367
46199
|
}
|
|
45368
46200
|
if (options.dryRun) {
|
|
46201
|
+
const costUsd = provider === "seedance" || provider === "fal" ? estimateSeedanceVideoCostUsd({
|
|
46202
|
+
durationSec: Number(options.duration) || 5,
|
|
46203
|
+
resolution: options.resolution,
|
|
46204
|
+
aspectRatio: options.ratio,
|
|
46205
|
+
fast: options.seedanceModel === "fast",
|
|
46206
|
+
hasVideoReference: Boolean(options.refVideos)
|
|
46207
|
+
}) : void 0;
|
|
45369
46208
|
outputSuccess({
|
|
45370
46209
|
command: "generate video",
|
|
45371
46210
|
startedAt,
|
|
45372
46211
|
dryRun: true,
|
|
46212
|
+
costUsd,
|
|
45373
46213
|
data: {
|
|
45374
46214
|
params: {
|
|
45375
46215
|
prompt: prompt3,
|
|
@@ -51847,6 +52687,9 @@ function clean(text) {
|
|
|
51847
52687
|
function humanise(id) {
|
|
51848
52688
|
return id.split(/[-_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
51849
52689
|
}
|
|
52690
|
+
function sceneCompositionId(beatId) {
|
|
52691
|
+
return `scene-${beatId}`;
|
|
52692
|
+
}
|
|
51850
52693
|
function slugifySceneName(name) {
|
|
51851
52694
|
const normalised = name.normalize("NFKD").replace(/[̀-ͯ]/g, "");
|
|
51852
52695
|
const slug = normalised.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -51884,10 +52727,16 @@ function fitKineticFontSize(words) {
|
|
|
51884
52727
|
function kenBurnsTween(scope, dur, endScale = 1.08) {
|
|
51885
52728
|
return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
|
|
51886
52729
|
}
|
|
52730
|
+
function backdropMotionTweens(scope, dur, hasImage, endScale = 1.08) {
|
|
52731
|
+
const sweep = hasImage ? "" : `
|
|
52732
|
+
tl.fromTo('${scope} .ambient-sweep', { xPercent: -75, opacity: 0.18 }, { xPercent: 75, opacity: 0.62, duration: ${dur.toFixed(2)}, ease: 'sine.inOut' }, 0);`;
|
|
52733
|
+
return `${kenBurnsTween(scope, dur, endScale)}${sweep}`;
|
|
52734
|
+
}
|
|
51887
52735
|
function buildPreset(input) {
|
|
51888
52736
|
const id = input.id;
|
|
51889
52737
|
const scope = `[data-composition-id="${id}"]`;
|
|
51890
|
-
const
|
|
52738
|
+
const displayId = input.beatId ?? id;
|
|
52739
|
+
const headline = clean(input.headline) || humanise(displayId);
|
|
51891
52740
|
const subhead = clean(input.subhead);
|
|
51892
52741
|
const kicker = clean(input.kicker);
|
|
51893
52742
|
const dur = input.duration;
|
|
@@ -51910,8 +52759,17 @@ function buildPreset(input) {
|
|
|
51910
52759
|
position: absolute;
|
|
51911
52760
|
inset: 0;
|
|
51912
52761
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 70%);
|
|
52762
|
+
overflow: hidden;
|
|
52763
|
+
}
|
|
52764
|
+
${scope} .ambient-sweep {
|
|
52765
|
+
position: absolute;
|
|
52766
|
+
inset: -22%;
|
|
52767
|
+
display: block;
|
|
52768
|
+
background: linear-gradient(115deg, rgba(255,255,255,0) 20%, rgba(0,201,255,0.34) 48%, rgba(142,45,226,0.26) 58%, rgba(255,255,255,0) 78%);
|
|
52769
|
+
filter: blur(12px);
|
|
52770
|
+
transform: rotate(8deg);
|
|
51913
52771
|
}`;
|
|
51914
|
-
const backdropMarkup = `<div class="backdrop"></div>`;
|
|
52772
|
+
const backdropMarkup = hasImage ? `<div class="backdrop"></div>` : `<div class="backdrop"><div class="ambient-sweep"></div></div>`;
|
|
51915
52773
|
switch (input.preset) {
|
|
51916
52774
|
case "simple": {
|
|
51917
52775
|
const transcript = input.transcript;
|
|
@@ -51943,7 +52801,7 @@ function buildPreset(input) {
|
|
|
51943
52801
|
body: `${backdropMarkup}
|
|
51944
52802
|
<div class="caption" id="caption">${captionInner}</div>`,
|
|
51945
52803
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
51946
|
-
${
|
|
52804
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
51947
52805
|
${captionTween}`
|
|
51948
52806
|
};
|
|
51949
52807
|
}
|
|
@@ -51982,12 +52840,12 @@ function buildPreset(input) {
|
|
|
51982
52840
|
// scene's own fade-in handles the visual transition. Backdrop
|
|
51983
52841
|
// Ken-Burns fills what used to be 80% dead static frame.
|
|
51984
52842
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
51985
|
-
${
|
|
52843
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
51986
52844
|
tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
|
|
51987
52845
|
};
|
|
51988
52846
|
}
|
|
51989
52847
|
case "explainer": {
|
|
51990
|
-
const k = kicker || humanise(
|
|
52848
|
+
const k = kicker || humanise(displayId).toUpperCase();
|
|
51991
52849
|
const sub = subhead || "";
|
|
51992
52850
|
const transcript = input.transcript;
|
|
51993
52851
|
const useWordSync = !!(transcript && transcript.length > 0 && sub);
|
|
@@ -52029,7 +52887,7 @@ function buildPreset(input) {
|
|
|
52029
52887
|
<div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
|
|
52030
52888
|
</div>`,
|
|
52031
52889
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52032
|
-
${
|
|
52890
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
52033
52891
|
tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
|
|
52034
52892
|
tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
|
|
52035
52893
|
${subtitleTween}`
|
|
@@ -52070,12 +52928,12 @@ function buildPreset(input) {
|
|
|
52070
52928
|
body: `${backdropMarkup}
|
|
52071
52929
|
<div class="kinetic">${wordSpans}</div>`,
|
|
52072
52930
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52073
|
-
${
|
|
52931
|
+
${backdropMotionTweens(scope, dur, hasImage)}
|
|
52074
52932
|
${tweens}`
|
|
52075
52933
|
};
|
|
52076
52934
|
}
|
|
52077
52935
|
case "product-shot": {
|
|
52078
|
-
const label = kicker || humanise(
|
|
52936
|
+
const label = kicker || humanise(displayId);
|
|
52079
52937
|
const headlineFontPx = fitFontSize(headline, 72, 40, 50);
|
|
52080
52938
|
return {
|
|
52081
52939
|
css: `${scope} {
|
|
@@ -52109,7 +52967,7 @@ function buildPreset(input) {
|
|
|
52109
52967
|
<div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
|
|
52110
52968
|
<div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
|
|
52111
52969
|
timeline: `${crossfadeTweens(scope, dur)}
|
|
52112
|
-
|
|
52970
|
+
${backdropMotionTweens(scope, dur, hasImage, 1.12)}
|
|
52113
52971
|
tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
|
|
52114
52972
|
tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
|
|
52115
52973
|
tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
|
|
@@ -52174,9 +53032,10 @@ function emitSceneHtml(input) {
|
|
|
52174
53032
|
if (input.width <= 0 || input.height <= 0) {
|
|
52175
53033
|
throw new Error(`Invalid canvas dims: ${input.width}x${input.height}`);
|
|
52176
53034
|
}
|
|
52177
|
-
const
|
|
53035
|
+
const beatId = input.id;
|
|
53036
|
+
const id = sceneCompositionId(beatId);
|
|
52178
53037
|
const dur = Number(input.duration.toFixed(3));
|
|
52179
|
-
const parts = buildPreset({ ...input, id, duration: dur });
|
|
53038
|
+
const parts = buildPreset({ ...input, id, beatId, duration: dur });
|
|
52180
53039
|
const audioBlock = input.audioPath ? `
|
|
52181
53040
|
<audio
|
|
52182
53041
|
id="narration"
|
|
@@ -52192,7 +53051,7 @@ function emitSceneHtml(input) {
|
|
|
52192
53051
|
${lottieLayer.markup}` : "";
|
|
52193
53052
|
const lottieScript = lottieLayer ? `
|
|
52194
53053
|
${lottieLayer.script}` : "";
|
|
52195
|
-
return `<template id="
|
|
53054
|
+
return `<template id="${id}-template">
|
|
52196
53055
|
<div data-composition-id="${id}" data-start="0" data-duration="${dur}" data-width="${input.width}" data-height="${input.height}">
|
|
52197
53056
|
<style>
|
|
52198
53057
|
${parts.css}
|
|
@@ -52228,8 +53087,9 @@ function buildClipReference(opts) {
|
|
|
52228
53087
|
const duration = Number(opts.duration.toFixed(3));
|
|
52229
53088
|
const track = opts.trackIndex ?? 1;
|
|
52230
53089
|
const src = opts.src ?? `compositions/scene-${opts.id}.html`;
|
|
53090
|
+
const compositionId = sceneCompositionId(opts.id);
|
|
52231
53091
|
const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
|
|
52232
|
-
return `<div class="clip" data-composition-id="${esc(
|
|
53092
|
+
return `<div class="clip" data-composition-id="${esc(compositionId)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
|
|
52233
53093
|
}
|
|
52234
53094
|
function insertClipIntoRoot(rootHtml, clip) {
|
|
52235
53095
|
const clipDiv = buildClipReference(clip);
|
|
@@ -52273,9 +53133,9 @@ init_agent_host_detect();
|
|
|
52273
53133
|
// src/commands/_shared/install-skill.ts
|
|
52274
53134
|
init_bundle_content();
|
|
52275
53135
|
init_bundle();
|
|
52276
|
-
import { mkdir as mkdir5, writeFile as
|
|
53136
|
+
import { mkdir as mkdir5, writeFile as writeFile10 } from "node:fs/promises";
|
|
52277
53137
|
import { existsSync as existsSync7 } from "node:fs";
|
|
52278
|
-
import { dirname as dirname4, join as
|
|
53138
|
+
import { dirname as dirname4, join as join7, relative as relative3, resolve as resolve10 } from "node:path";
|
|
52279
53139
|
function universalFiles() {
|
|
52280
53140
|
return [
|
|
52281
53141
|
{ relPath: "SKILL.md", content: SKILL_MD },
|
|
@@ -52325,7 +53185,7 @@ function selectHostFiles(hosts) {
|
|
|
52325
53185
|
return out;
|
|
52326
53186
|
}
|
|
52327
53187
|
async function installHyperframesSkill(opts) {
|
|
52328
|
-
const projectDir =
|
|
53188
|
+
const projectDir = resolve10(opts.projectDir);
|
|
52329
53189
|
const force = opts.force ?? false;
|
|
52330
53190
|
const dryRun = opts.dryRun ?? false;
|
|
52331
53191
|
const files = [...universalFiles(), ...selectHostFiles(opts.hosts)];
|
|
@@ -52334,7 +53194,7 @@ async function installHyperframesSkill(opts) {
|
|
|
52334
53194
|
await mkdir5(projectDir, { recursive: true });
|
|
52335
53195
|
}
|
|
52336
53196
|
for (const file of files) {
|
|
52337
|
-
const absPath =
|
|
53197
|
+
const absPath = join7(projectDir, file.relPath);
|
|
52338
53198
|
const exists = existsSync7(absPath);
|
|
52339
53199
|
const bytes = Buffer.byteLength(file.content, "utf-8");
|
|
52340
53200
|
if (exists && !force) {
|
|
@@ -52350,7 +53210,7 @@ async function installHyperframesSkill(opts) {
|
|
|
52350
53210
|
continue;
|
|
52351
53211
|
}
|
|
52352
53212
|
await mkdir5(dirname4(absPath), { recursive: true });
|
|
52353
|
-
await
|
|
53213
|
+
await writeFile10(absPath, file.content, "utf-8");
|
|
52354
53214
|
actions.push({ path: file.relPath, status: "wrote", bytes });
|
|
52355
53215
|
}
|
|
52356
53216
|
return {
|
|
@@ -52375,10 +53235,10 @@ init_scene_lint();
|
|
|
52375
53235
|
init_storyboard_parse();
|
|
52376
53236
|
init_root_sync();
|
|
52377
53237
|
import { existsSync as existsSync11 } from "node:fs";
|
|
52378
|
-
import { mkdir as mkdir7, readFile as readFile11, writeFile as
|
|
52379
|
-
import { join as
|
|
53238
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile13 } from "node:fs/promises";
|
|
53239
|
+
import { join as join11, resolve as resolve14 } from "node:path";
|
|
52380
53240
|
async function executeSceneSubmit(opts) {
|
|
52381
|
-
const projectDir =
|
|
53241
|
+
const projectDir = resolve14(opts.projectDir);
|
|
52382
53242
|
const beatId = opts.beatId.trim();
|
|
52383
53243
|
const scenePath = `compositions/scene-${beatId}.html`;
|
|
52384
53244
|
const base = {
|
|
@@ -52390,7 +53250,7 @@ async function executeSceneSubmit(opts) {
|
|
|
52390
53250
|
warnings: [],
|
|
52391
53251
|
retryWith: []
|
|
52392
53252
|
};
|
|
52393
|
-
const storyboardPath =
|
|
53253
|
+
const storyboardPath = join11(projectDir, "STORYBOARD.md");
|
|
52394
53254
|
if (!existsSync11(storyboardPath)) {
|
|
52395
53255
|
return {
|
|
52396
53256
|
...base,
|
|
@@ -52468,9 +53328,9 @@ async function executeSceneSubmit(opts) {
|
|
|
52468
53328
|
if (opts.validateOnly) {
|
|
52469
53329
|
return { ...base, success: true };
|
|
52470
53330
|
}
|
|
52471
|
-
const absPath =
|
|
52472
|
-
await mkdir7(
|
|
52473
|
-
await
|
|
53331
|
+
const absPath = join11(projectDir, scenePath);
|
|
53332
|
+
await mkdir7(join11(projectDir, "compositions"), { recursive: true });
|
|
53333
|
+
await writeFile13(absPath, html.endsWith("\n") ? html : `${html}
|
|
52474
53334
|
`, "utf-8");
|
|
52475
53335
|
return {
|
|
52476
53336
|
...base,
|
|
@@ -52485,6 +53345,8 @@ async function executeSceneSubmit(opts) {
|
|
|
52485
53345
|
|
|
52486
53346
|
// src/commands/scene.ts
|
|
52487
53347
|
init_scene_repair();
|
|
53348
|
+
init_root_sync();
|
|
53349
|
+
init_storyboard_edit();
|
|
52488
53350
|
function validateDuration(value) {
|
|
52489
53351
|
const n = parseFloat(value);
|
|
52490
53352
|
if (!Number.isFinite(n) || n <= 0 || n > 3600) {
|
|
@@ -52787,7 +53649,10 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52787
53649
|
).option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option(
|
|
52788
53650
|
"--visuals <prompt>",
|
|
52789
53651
|
"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(
|
|
53652
|
+
).option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option(
|
|
53653
|
+
"--no-storyboard",
|
|
53654
|
+
"Do not sync STORYBOARD.md; insert this scene directly into the root composition only"
|
|
53655
|
+
).option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option(
|
|
52791
53656
|
"--tts <provider>",
|
|
52792
53657
|
"TTS provider: auto, elevenlabs, openai, kokoro (default auto \u2014 ElevenLabs key > OpenAI key > Kokoro local)",
|
|
52793
53658
|
"auto"
|
|
@@ -52856,6 +53721,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52856
53721
|
kicker: options.kicker,
|
|
52857
53722
|
project: options.project,
|
|
52858
53723
|
insertInto: options.insertInto,
|
|
53724
|
+
syncStoryboard: options.storyboard !== false,
|
|
52859
53725
|
imageProvider: options.imageProvider,
|
|
52860
53726
|
tts,
|
|
52861
53727
|
audio: options.audio,
|
|
@@ -52883,6 +53749,7 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52883
53749
|
kicker: options.kicker,
|
|
52884
53750
|
projectDir: options.project,
|
|
52885
53751
|
insertInto: options.insertInto,
|
|
53752
|
+
syncStoryboard: options.storyboard !== false,
|
|
52886
53753
|
imageProvider: options.imageProvider,
|
|
52887
53754
|
tts,
|
|
52888
53755
|
voice: options.voice,
|
|
@@ -52922,6 +53789,13 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
|
|
|
52922
53789
|
if (result.audioPath) console.log(source_default.green(" +"), result.audioPath);
|
|
52923
53790
|
if (result.imagePath) console.log(source_default.green(" +"), result.imagePath);
|
|
52924
53791
|
if (result.transcriptPath) console.log(source_default.green(" +"), result.transcriptPath);
|
|
53792
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
53793
|
+
console.log(
|
|
53794
|
+
result.storyboardAction === "updated" ? source_default.yellow(" ~") : source_default.green(" +"),
|
|
53795
|
+
result.storyboardPath,
|
|
53796
|
+
source_default.dim(`(${result.storyboardAction})`)
|
|
53797
|
+
);
|
|
53798
|
+
}
|
|
52925
53799
|
console.log(source_default.yellow(" ~"), result.rootPath, source_default.dim("(updated)"));
|
|
52926
53800
|
console.log();
|
|
52927
53801
|
console.log(source_default.bold.cyan("Composition"));
|
|
@@ -52968,6 +53842,21 @@ async function loadVibeProjectConfig(projectDir) {
|
|
|
52968
53842
|
}
|
|
52969
53843
|
};
|
|
52970
53844
|
}
|
|
53845
|
+
function escapeRegExp3(value) {
|
|
53846
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
53847
|
+
}
|
|
53848
|
+
function readClipStart(rootHtml, beatId) {
|
|
53849
|
+
const compositionId = sceneCompositionId(beatId);
|
|
53850
|
+
const match = rootHtml.match(
|
|
53851
|
+
new RegExp(
|
|
53852
|
+
`<div\\s+class="clip"[^>]*\\bdata-composition-id="${escapeRegExp3(compositionId)}"[^>]*\\bdata-start="([\\d.]+)"`,
|
|
53853
|
+
"i"
|
|
53854
|
+
)
|
|
53855
|
+
);
|
|
53856
|
+
if (!match) return void 0;
|
|
53857
|
+
const start = Number.parseFloat(match[1]);
|
|
53858
|
+
return Number.isFinite(start) ? start : void 0;
|
|
53859
|
+
}
|
|
52971
53860
|
async function resolveNarrationText(value) {
|
|
52972
53861
|
if (!value) return void 0;
|
|
52973
53862
|
const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
|
|
@@ -52994,6 +53883,7 @@ async function executeSceneAdd(opts) {
|
|
|
52994
53883
|
const id = slugifySceneName(opts.name);
|
|
52995
53884
|
const sceneRel = `compositions/scene-${id}.html`;
|
|
52996
53885
|
const scenePath = resolve17(projectDir, sceneRel);
|
|
53886
|
+
const storyboardAbsPath = resolve17(projectDir, "STORYBOARD.md");
|
|
52997
53887
|
const errResult = (error) => ({
|
|
52998
53888
|
success: false,
|
|
52999
53889
|
id,
|
|
@@ -53213,6 +54103,27 @@ async function executeSceneAdd(opts) {
|
|
|
53213
54103
|
duration = fallback2;
|
|
53214
54104
|
}
|
|
53215
54105
|
duration = Number(duration.toFixed(2));
|
|
54106
|
+
let storyboardPath;
|
|
54107
|
+
let storyboardSynced = false;
|
|
54108
|
+
let storyboardAction = "skipped";
|
|
54109
|
+
const shouldSyncStoryboard = opts.syncStoryboard !== false && await pathExists2(storyboardAbsPath);
|
|
54110
|
+
if (shouldSyncStoryboard) {
|
|
54111
|
+
opts.onProgress?.("Syncing STORYBOARD.md...");
|
|
54112
|
+
const before = await readFile14(storyboardAbsPath, "utf-8");
|
|
54113
|
+
const synced = upsertStoryboardBeat(before, {
|
|
54114
|
+
beatId: id,
|
|
54115
|
+
title: opts.headline,
|
|
54116
|
+
duration,
|
|
54117
|
+
narration: narrationText && !opts.skipAudio ? narrationText : void 0,
|
|
54118
|
+
backdrop: opts.visuals && !opts.skipImage ? opts.visuals : void 0
|
|
54119
|
+
});
|
|
54120
|
+
if (synced.markdown !== before) {
|
|
54121
|
+
await writeFile15(storyboardAbsPath, synced.markdown, "utf-8");
|
|
54122
|
+
}
|
|
54123
|
+
storyboardPath = relative6(process.cwd(), storyboardAbsPath) || storyboardAbsPath;
|
|
54124
|
+
storyboardSynced = true;
|
|
54125
|
+
storyboardAction = synced.action;
|
|
54126
|
+
}
|
|
53216
54127
|
opts.onProgress?.("Emitting scene HTML...");
|
|
53217
54128
|
const sceneHtml = emitSceneHtml({
|
|
53218
54129
|
id,
|
|
@@ -53231,11 +54142,24 @@ async function executeSceneAdd(opts) {
|
|
|
53231
54142
|
await mkdir8(dirname6(scenePath), { recursive: true });
|
|
53232
54143
|
await writeFile15(scenePath, sceneHtml, "utf-8");
|
|
53233
54144
|
opts.onProgress?.("Updating root composition...");
|
|
53234
|
-
|
|
53235
|
-
|
|
53236
|
-
|
|
53237
|
-
|
|
53238
|
-
|
|
54145
|
+
let start = 0;
|
|
54146
|
+
if (storyboardSynced) {
|
|
54147
|
+
const syncPlan = await syncRootComposition({
|
|
54148
|
+
projectDir,
|
|
54149
|
+
rootRel,
|
|
54150
|
+
beats: await loadProjectRootSyncBeats(projectDir)
|
|
54151
|
+
});
|
|
54152
|
+
const blockingIssue = syncPlan.issues.find((issue) => issue.severity === "error");
|
|
54153
|
+
if (blockingIssue) return errResult(blockingIssue.message);
|
|
54154
|
+
const rootHtmlAfterSync = await readFile14(rootPath, "utf-8");
|
|
54155
|
+
start = readClipStart(rootHtmlAfterSync, id) ?? 0;
|
|
54156
|
+
} else {
|
|
54157
|
+
start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
|
|
54158
|
+
const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
|
|
54159
|
+
const trackIndex = existingClipCount % 2 + 1;
|
|
54160
|
+
const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
|
|
54161
|
+
await writeFile15(rootPath, updated, "utf-8");
|
|
54162
|
+
}
|
|
53239
54163
|
const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
|
|
53240
54164
|
return {
|
|
53241
54165
|
success: true,
|
|
@@ -53248,6 +54172,9 @@ async function executeSceneAdd(opts) {
|
|
|
53248
54172
|
audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
|
|
53249
54173
|
imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
|
|
53250
54174
|
lottiePath: lottieAbsPath ? relative6(process.cwd(), lottieAbsPath) || lottieAbsPath : void 0,
|
|
54175
|
+
storyboardPath,
|
|
54176
|
+
storyboardSynced,
|
|
54177
|
+
storyboardAction,
|
|
53251
54178
|
transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
|
|
53252
54179
|
transcriptWordCount
|
|
53253
54180
|
};
|
|
@@ -53545,6 +54472,9 @@ var sceneAddSchema = external_exports.object({
|
|
|
53545
54472
|
kicker: external_exports.string().optional().describe("Small label above the headline (used by 'explainer' and 'product-shot' presets)."),
|
|
53546
54473
|
projectDir: external_exports.string().optional().describe(PROJECT_DIR_DESCRIPTION),
|
|
53547
54474
|
insertInto: external_exports.string().optional().describe("Root composition file (relative to projectDir). Default 'index.html'."),
|
|
54475
|
+
syncStoryboard: external_exports.boolean().optional().describe(
|
|
54476
|
+
"Sync STORYBOARD.md when present, then rebuild root refs from it. Default true; set false for root-only insertion."
|
|
54477
|
+
),
|
|
53548
54478
|
imageProvider: external_exports.enum(["gemini", "openai"]).optional().describe("Image provider for visuals. Default 'gemini'."),
|
|
53549
54479
|
voice: external_exports.string().optional().describe("ElevenLabs voice id or name."),
|
|
53550
54480
|
skipAudio: external_exports.boolean().optional().describe("Skip TTS even if narration is provided."),
|
|
@@ -53557,7 +54487,7 @@ var sceneAddTool = defineTool({
|
|
|
53557
54487
|
cost: "low",
|
|
53558
54488
|
title: "Add Scene from Preset",
|
|
53559
54489
|
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
|
|
54490
|
+
description: "Add a single scene to an existing scene project. Optionally generates narration audio (ElevenLabs) and/or a backdrop image (Gemini/OpenAI), then emits compositions/scene-<id>.html with a paused GSAP timeline. By default, syncs STORYBOARD.md and rebuilds root refs from it; set syncStoryboard:false for root-only insertion. Use skipAudio:true and skipImage:true for text-only scenes that need no API calls.",
|
|
53561
54491
|
schema: sceneAddSchema,
|
|
53562
54492
|
async execute(args, ctx) {
|
|
53563
54493
|
const projectDir = args.projectDir ? resolve26(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
|
|
@@ -53571,6 +54501,7 @@ var sceneAddTool = defineTool({
|
|
|
53571
54501
|
kicker: args.kicker,
|
|
53572
54502
|
projectDir,
|
|
53573
54503
|
insertInto: args.insertInto,
|
|
54504
|
+
syncStoryboard: args.syncStoryboard,
|
|
53574
54505
|
imageProvider: args.imageProvider,
|
|
53575
54506
|
voice: args.voice,
|
|
53576
54507
|
skipAudio: args.skipAudio,
|
|
@@ -53589,6 +54520,9 @@ var sceneAddTool = defineTool({
|
|
|
53589
54520
|
];
|
|
53590
54521
|
if (result.audioPath) lines.push(` audio: ${result.audioPath}`);
|
|
53591
54522
|
if (result.imagePath) lines.push(` image: ${result.imagePath}`);
|
|
54523
|
+
if (result.storyboardPath && result.storyboardSynced) {
|
|
54524
|
+
lines.push(` story: ${result.storyboardPath} (${result.storyboardAction})`);
|
|
54525
|
+
}
|
|
53592
54526
|
return {
|
|
53593
54527
|
success: true,
|
|
53594
54528
|
data: {
|
|
@@ -53599,7 +54533,10 @@ var sceneAddTool = defineTool({
|
|
|
53599
54533
|
scenePath: result.scenePath,
|
|
53600
54534
|
rootPath: result.rootPath,
|
|
53601
54535
|
audioPath: result.audioPath,
|
|
53602
|
-
imagePath: result.imagePath
|
|
54536
|
+
imagePath: result.imagePath,
|
|
54537
|
+
storyboardPath: result.storyboardPath,
|
|
54538
|
+
storyboardSynced: result.storyboardSynced,
|
|
54539
|
+
storyboardAction: result.storyboardAction
|
|
53603
54540
|
},
|
|
53604
54541
|
humanLines: lines
|
|
53605
54542
|
};
|
|
@@ -55260,7 +56197,11 @@ async function inspectProject(opts) {
|
|
|
55260
56197
|
);
|
|
55261
56198
|
}
|
|
55262
56199
|
function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
55263
|
-
const
|
|
56200
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
56201
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
56202
|
+
projectDir,
|
|
56203
|
+
retryWith: normalizedRetryWith
|
|
56204
|
+
});
|
|
55264
56205
|
const status = statusFromIssues(normalizedIssues);
|
|
55265
56206
|
return {
|
|
55266
56207
|
schemaVersion: "1",
|
|
@@ -55272,9 +56213,14 @@ function makeProjectResult(projectDir, issues, checks, retryWith, beatId) {
|
|
|
55272
56213
|
score: scoreIssues(normalizedIssues),
|
|
55273
56214
|
issues: normalizedIssues,
|
|
55274
56215
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
56216
|
+
nextActions: deriveNextReviewActions({
|
|
56217
|
+
issues: normalizedIssues,
|
|
56218
|
+
retryWith: normalizedRetryWith,
|
|
56219
|
+
projectDir
|
|
56220
|
+
}),
|
|
55275
56221
|
sourceReports: projectSourceReports(checks),
|
|
55276
56222
|
checks,
|
|
55277
|
-
retryWith:
|
|
56223
|
+
retryWith: normalizedRetryWith
|
|
55278
56224
|
};
|
|
55279
56225
|
}
|
|
55280
56226
|
function storyboardPlaceholderIssues(storyboard) {
|
|
@@ -55826,6 +56772,8 @@ async function inspectRender(opts) {
|
|
|
55826
56772
|
severity: "error",
|
|
55827
56773
|
code: "RENDER_NOT_FOUND",
|
|
55828
56774
|
message: opts.beatId ? `No rendered video was found for beat "${opts.beatId}". Pass --video or render the beat first.` : "No rendered video was found. Pass --video or render the project first.",
|
|
56775
|
+
beatId: opts.beatId,
|
|
56776
|
+
scene: opts.beatId,
|
|
55829
56777
|
suggestedFix: opts.beatId ? "Run `vibe render <project> --beat <id> --json`." : "Run `vibe build --stage render --json` or `vibe render --json`."
|
|
55830
56778
|
});
|
|
55831
56779
|
retryWith.push(
|
|
@@ -55848,6 +56796,8 @@ async function inspectRender(opts) {
|
|
|
55848
56796
|
code: "EMPTY_RENDER",
|
|
55849
56797
|
message: "Rendered video file is empty.",
|
|
55850
56798
|
file: displayPath(videoPath),
|
|
56799
|
+
beatId: opts.beatId,
|
|
56800
|
+
scene: opts.beatId,
|
|
55851
56801
|
suggestedFix: opts.beatId ? "Render again with `vibe render --beat <id> --json`." : "Render again with `vibe render --json`."
|
|
55852
56802
|
});
|
|
55853
56803
|
retryWith.push(
|
|
@@ -55857,6 +56807,8 @@ async function inspectRender(opts) {
|
|
|
55857
56807
|
const beatTimings = await beatTimingsFromBuildReport(projectDir, opts.beatId);
|
|
55858
56808
|
const expectedDurationSec = beatTimings.length > 0 ? Number(beatTimings.reduce((sum, beat) => sum + beat.sceneDurationSec, 0).toFixed(3)) : await expectedDurationFromBuildReport(projectDir, opts.beatId);
|
|
55859
56809
|
if (expectedDurationSec !== void 0) checks.expectedDurationSec = expectedDurationSec;
|
|
56810
|
+
const expectedAudio = await renderExpectsAudio(projectDir, opts.beatId);
|
|
56811
|
+
checks.expectedAudio = expectedAudio;
|
|
55860
56812
|
if (!commandExists("ffprobe")) {
|
|
55861
56813
|
issues.push({
|
|
55862
56814
|
severity: "error",
|
|
@@ -55882,7 +56834,7 @@ async function inspectRender(opts) {
|
|
|
55882
56834
|
file: displayPath(videoPath)
|
|
55883
56835
|
});
|
|
55884
56836
|
}
|
|
55885
|
-
if (!audioStream) {
|
|
56837
|
+
if (!audioStream && expectedAudio) {
|
|
55886
56838
|
issues.push({
|
|
55887
56839
|
severity: "warning",
|
|
55888
56840
|
code: "NO_AUDIO_STREAM",
|
|
@@ -56087,8 +57039,86 @@ async function inspectRender(opts) {
|
|
|
56087
57039
|
);
|
|
56088
57040
|
return maybeWriteRenderReport(projectDir, opts, result);
|
|
56089
57041
|
}
|
|
57042
|
+
async function renderExpectsAudio(projectDir, beatId) {
|
|
57043
|
+
const root = resolve42(projectDir);
|
|
57044
|
+
const rootHtml = await readTextIfExists(join22(root, "index.html"));
|
|
57045
|
+
if (rootHtml && rootHtmlExpectsAudio(rootHtml, beatId)) return true;
|
|
57046
|
+
const buildReport = await readJsonIfExists(join22(root, "build-report.json"));
|
|
57047
|
+
if (buildReportExpectsAudio(buildReport, beatId)) return true;
|
|
57048
|
+
const storyboard = await readTextIfExists(join22(root, "STORYBOARD.md"));
|
|
57049
|
+
if (!storyboard) return false;
|
|
57050
|
+
try {
|
|
57051
|
+
const parsed = parseStoryboard(storyboard);
|
|
57052
|
+
const beats = beatId ? parsed.beats.filter((beat) => beat.id === beatId) : parsed.beats;
|
|
57053
|
+
return beats.some(
|
|
57054
|
+
(beat) => [
|
|
57055
|
+
`assets/narration-${beat.id}.mp3`,
|
|
57056
|
+
`assets/narration-${beat.id}.wav`,
|
|
57057
|
+
`assets/music-${beat.id}.mp3`,
|
|
57058
|
+
`assets/music-${beat.id}.wav`
|
|
57059
|
+
].some((asset) => existsSync39(join22(root, asset)))
|
|
57060
|
+
);
|
|
57061
|
+
} catch {
|
|
57062
|
+
return false;
|
|
57063
|
+
}
|
|
57064
|
+
}
|
|
57065
|
+
function rootHtmlExpectsAudio(html, beatId) {
|
|
57066
|
+
const audioTags = html.match(/<audio\b[^>]*>/gi) ?? [];
|
|
57067
|
+
if (!beatId) return audioTags.length > 0;
|
|
57068
|
+
const escaped = escapeRegExp4(beatId);
|
|
57069
|
+
const beatAudioRe = new RegExp(
|
|
57070
|
+
`(id="(?:narration|music)-${escaped}"|src="[^"]*(?:narration|music)-${escaped}\\.(?:mp3|wav)")`,
|
|
57071
|
+
"i"
|
|
57072
|
+
);
|
|
57073
|
+
return audioTags.some((tag) => beatAudioRe.test(tag));
|
|
57074
|
+
}
|
|
57075
|
+
function buildReportExpectsAudio(report, beatId) {
|
|
57076
|
+
if (!report || typeof report !== "object") return false;
|
|
57077
|
+
const beats = Array.isArray(report.beats) ? report.beats ?? [] : [];
|
|
57078
|
+
const selected = beatId === void 0 ? beats : beats.filter((beat) => {
|
|
57079
|
+
if (!beat || typeof beat !== "object") return false;
|
|
57080
|
+
return beat.id === beatId;
|
|
57081
|
+
});
|
|
57082
|
+
return selected.some((beat) => beatReportExpectsAudio(beat));
|
|
57083
|
+
}
|
|
57084
|
+
function beatReportExpectsAudio(beat) {
|
|
57085
|
+
if (!beat || typeof beat !== "object") return false;
|
|
57086
|
+
const record = beat;
|
|
57087
|
+
if (typeof record.narrationPath === "string" && record.narrationPath.length > 0) return true;
|
|
57088
|
+
if (typeof record.musicPath === "string" && record.musicPath.length > 0) return true;
|
|
57089
|
+
if (parseOptionalNumber2(record.narrationDurationSec) !== void 0) return true;
|
|
57090
|
+
const narration = record.narration;
|
|
57091
|
+
if (narration && typeof narration === "object") {
|
|
57092
|
+
const n = narration;
|
|
57093
|
+
if (typeof n.path === "string" && n.path.length > 0) return true;
|
|
57094
|
+
if (parseOptionalNumber2(n.durationSec) !== void 0) return true;
|
|
57095
|
+
}
|
|
57096
|
+
const music = record.music;
|
|
57097
|
+
if (music && typeof music === "object") {
|
|
57098
|
+
const m = music;
|
|
57099
|
+
if (typeof m.path === "string" && m.path.length > 0) return true;
|
|
57100
|
+
if (parseOptionalNumber2(m.durationSec) !== void 0) return true;
|
|
57101
|
+
}
|
|
57102
|
+
return false;
|
|
57103
|
+
}
|
|
57104
|
+
async function readJsonIfExists(path2) {
|
|
57105
|
+
const text = await readTextIfExists(path2);
|
|
57106
|
+
if (!text) return null;
|
|
57107
|
+
try {
|
|
57108
|
+
return JSON.parse(text);
|
|
57109
|
+
} catch {
|
|
57110
|
+
return null;
|
|
57111
|
+
}
|
|
57112
|
+
}
|
|
57113
|
+
function escapeRegExp4(value) {
|
|
57114
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57115
|
+
}
|
|
56090
57116
|
function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith, score) {
|
|
56091
|
-
const
|
|
57117
|
+
const normalizedRetryWith = uniqueRetryWith(retryWith);
|
|
57118
|
+
const normalizedIssues = normalizeReviewIssues(issues, {
|
|
57119
|
+
projectDir,
|
|
57120
|
+
retryWith: normalizedRetryWith
|
|
57121
|
+
});
|
|
56092
57122
|
const status = statusFromIssues(normalizedIssues);
|
|
56093
57123
|
return {
|
|
56094
57124
|
schemaVersion: "1",
|
|
@@ -56101,9 +57131,14 @@ function makeRenderResult(projectDir, opts, videoPath, issues, checks, retryWith
|
|
|
56101
57131
|
score,
|
|
56102
57132
|
issues: normalizedIssues,
|
|
56103
57133
|
summary: summarizeReviewIssues(normalizedIssues),
|
|
57134
|
+
nextActions: deriveNextReviewActions({
|
|
57135
|
+
issues: normalizedIssues,
|
|
57136
|
+
retryWith: normalizedRetryWith,
|
|
57137
|
+
projectDir
|
|
57138
|
+
}),
|
|
56104
57139
|
sourceReports: renderSourceReports(projectDir, videoPath, checks),
|
|
56105
57140
|
checks,
|
|
56106
|
-
retryWith:
|
|
57141
|
+
retryWith: normalizedRetryWith
|
|
56107
57142
|
};
|
|
56108
57143
|
}
|
|
56109
57144
|
async function maybeWriteRenderReport(projectDir, opts, result) {
|