@vibeframe/mcp-server 0.60.0 → 0.63.0
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/dist/index.js +1121 -1031
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -461339,189 +461339,817 @@ var init_compose_scenes_skills = __esm({
|
|
|
461339
461339
|
}
|
|
461340
461340
|
});
|
|
461341
461341
|
|
|
461342
|
-
// src/
|
|
461343
|
-
import {
|
|
461344
|
-
import {
|
|
461345
|
-
|
|
461346
|
-
|
|
461347
|
-
|
|
461348
|
-
|
|
461349
|
-
|
|
461350
|
-
|
|
461351
|
-
|
|
461352
|
-
|
|
461353
|
-
|
|
461354
|
-
|
|
461355
|
-
|
|
461356
|
-
|
|
461357
|
-
|
|
461358
|
-
async function loadProject(projectPath) {
|
|
461359
|
-
const absPath = resolve(process.cwd(), projectPath);
|
|
461360
|
-
const content = await readFile(absPath, "utf-8");
|
|
461361
|
-
const data = JSON.parse(content);
|
|
461362
|
-
return Project.fromJSON(data);
|
|
461363
|
-
}
|
|
461364
|
-
async function saveProject(projectPath, project) {
|
|
461365
|
-
const absPath = resolve(process.cwd(), projectPath);
|
|
461366
|
-
await writeFile(absPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
461367
|
-
}
|
|
461368
|
-
var projectTools = [
|
|
461369
|
-
{
|
|
461370
|
-
name: "project_create",
|
|
461371
|
-
description: "Create a new VibeFrame project file",
|
|
461372
|
-
inputSchema: {
|
|
461373
|
-
type: "object",
|
|
461374
|
-
properties: {
|
|
461375
|
-
name: { type: "string", description: "Project name" },
|
|
461376
|
-
outputPath: { type: "string", description: "Output file path (defaults to {name}.vibe.json)" },
|
|
461377
|
-
width: { type: "number", description: "Video width in pixels (default: 1920)" },
|
|
461378
|
-
height: { type: "number", description: "Video height in pixels (default: 1080)" },
|
|
461379
|
-
fps: { type: "number", description: "Frames per second (default: 30)" }
|
|
461380
|
-
},
|
|
461381
|
-
required: ["name"]
|
|
461382
|
-
}
|
|
461383
|
-
},
|
|
461384
|
-
{
|
|
461385
|
-
name: "project_info",
|
|
461386
|
-
description: "Get information about a VibeFrame project",
|
|
461387
|
-
inputSchema: {
|
|
461388
|
-
type: "object",
|
|
461389
|
-
properties: {
|
|
461390
|
-
projectPath: { type: "string", description: "Path to the .vibe.json project file" }
|
|
461391
|
-
},
|
|
461392
|
-
required: ["projectPath"]
|
|
461342
|
+
// ../cli/src/commands/_shared/scene-audio-scan.ts
|
|
461343
|
+
import { readFile as readFile22 } from "node:fs/promises";
|
|
461344
|
+
import { resolve as resolve36 } from "node:path";
|
|
461345
|
+
function parseRootClips(rootHtml) {
|
|
461346
|
+
const clips = [];
|
|
461347
|
+
const clipRegex = /<div\b[^>]*class="clip"[^>]*>/gi;
|
|
461348
|
+
let match2;
|
|
461349
|
+
while ((match2 = clipRegex.exec(rootHtml)) !== null) {
|
|
461350
|
+
const tag = match2[0];
|
|
461351
|
+
const compositionId = pickAttr(tag, "data-composition-id");
|
|
461352
|
+
const compositionSrc = pickAttr(tag, "data-composition-src");
|
|
461353
|
+
const start = pickNumberAttr(tag, "data-start");
|
|
461354
|
+
const duration = pickNumberAttr(tag, "data-duration");
|
|
461355
|
+
const trackIndex = pickNumberAttr(tag, "data-track-index") ?? 1;
|
|
461356
|
+
if (!compositionId || !compositionSrc || start === null || duration === null) {
|
|
461357
|
+
continue;
|
|
461393
461358
|
}
|
|
461359
|
+
clips.push({ compositionId, compositionSrc, start, duration, trackIndex });
|
|
461394
461360
|
}
|
|
461395
|
-
|
|
461396
|
-
|
|
461397
|
-
|
|
461398
|
-
|
|
461399
|
-
|
|
461400
|
-
|
|
461401
|
-
|
|
461402
|
-
|
|
461403
|
-
|
|
461404
|
-
|
|
461405
|
-
|
|
461406
|
-
|
|
461361
|
+
return clips;
|
|
461362
|
+
}
|
|
461363
|
+
function parseSceneAudios(compositionHtml) {
|
|
461364
|
+
const out = [];
|
|
461365
|
+
const audioRegex = /<audio\b([^>]*)>/gi;
|
|
461366
|
+
let match2;
|
|
461367
|
+
while ((match2 = audioRegex.exec(compositionHtml)) !== null) {
|
|
461368
|
+
const attrs = match2[1];
|
|
461369
|
+
const src = pickAttr(attrs, "src");
|
|
461370
|
+
if (!src) continue;
|
|
461371
|
+
const localStart = pickNumberAttr(attrs, "data-start") ?? 0;
|
|
461372
|
+
const durationRaw = pickAttr(attrs, "data-duration");
|
|
461373
|
+
const durationHint = !durationRaw || durationRaw === "auto" ? "auto" : Number(durationRaw);
|
|
461374
|
+
const volume = pickNumberAttr(attrs, "data-volume") ?? 1;
|
|
461375
|
+
const trackIndex = pickNumberAttr(attrs, "data-track-index") ?? 2;
|
|
461376
|
+
out.push({ srcRel: src, localStart, durationHint, volume, trackIndex });
|
|
461377
|
+
}
|
|
461378
|
+
return out;
|
|
461379
|
+
}
|
|
461380
|
+
function makeFsCompositionReader(projectDir) {
|
|
461381
|
+
return async (compositionSrcRel) => {
|
|
461382
|
+
const abs = resolve36(projectDir, compositionSrcRel);
|
|
461383
|
+
try {
|
|
461384
|
+
return await readFile22(abs, "utf-8");
|
|
461385
|
+
} catch {
|
|
461386
|
+
return null;
|
|
461407
461387
|
}
|
|
461408
|
-
|
|
461409
|
-
|
|
461410
|
-
|
|
461411
|
-
|
|
461412
|
-
|
|
461413
|
-
|
|
461414
|
-
|
|
461415
|
-
|
|
461416
|
-
|
|
461417
|
-
|
|
461418
|
-
|
|
461419
|
-
|
|
461420
|
-
|
|
461388
|
+
};
|
|
461389
|
+
}
|
|
461390
|
+
async function scanSceneAudio(opts) {
|
|
461391
|
+
const reader = opts.readComposition ?? makeFsCompositionReader(opts.projectDir);
|
|
461392
|
+
const clips = parseRootClips(opts.rootHtml);
|
|
461393
|
+
const out = [];
|
|
461394
|
+
for (const clip of clips) {
|
|
461395
|
+
const html = await reader(clip.compositionSrc);
|
|
461396
|
+
if (!html) continue;
|
|
461397
|
+
const audios = parseSceneAudios(html);
|
|
461398
|
+
for (const audio of audios) {
|
|
461399
|
+
out.push({
|
|
461400
|
+
srcRel: audio.srcRel,
|
|
461401
|
+
srcAbs: resolve36(opts.projectDir, audio.srcRel),
|
|
461402
|
+
absoluteStart: clip.start + audio.localStart,
|
|
461403
|
+
durationHint: audio.durationHint,
|
|
461404
|
+
clipDurationCap: clip.duration - audio.localStart,
|
|
461405
|
+
volume: audio.volume,
|
|
461406
|
+
trackIndex: audio.trackIndex,
|
|
461407
|
+
compositionSrc: clip.compositionSrc
|
|
461408
|
+
});
|
|
461421
461409
|
}
|
|
461422
|
-
default:
|
|
461423
|
-
throw new Error(`Unknown project tool: ${name}`);
|
|
461424
461410
|
}
|
|
461411
|
+
out.sort((a, b) => a.absoluteStart - b.absoluteStart);
|
|
461412
|
+
return out;
|
|
461413
|
+
}
|
|
461414
|
+
function pickAttr(tag, name) {
|
|
461415
|
+
const re = new RegExp(`\\b${escapeRegex3(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`);
|
|
461416
|
+
const m = tag.match(re);
|
|
461417
|
+
if (!m) return null;
|
|
461418
|
+
return m[2] ?? m[3] ?? null;
|
|
461419
|
+
}
|
|
461420
|
+
function pickNumberAttr(tag, name) {
|
|
461421
|
+
const raw2 = pickAttr(tag, name);
|
|
461422
|
+
if (raw2 === null) return null;
|
|
461423
|
+
const n = Number(raw2);
|
|
461424
|
+
return Number.isFinite(n) ? n : null;
|
|
461425
461425
|
}
|
|
461426
|
+
function escapeRegex3(s) {
|
|
461427
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
461428
|
+
}
|
|
461429
|
+
var init_scene_audio_scan = __esm({
|
|
461430
|
+
"../cli/src/commands/_shared/scene-audio-scan.ts"() {
|
|
461431
|
+
"use strict";
|
|
461432
|
+
}
|
|
461433
|
+
});
|
|
461426
461434
|
|
|
461427
|
-
// src/
|
|
461428
|
-
import {
|
|
461429
|
-
|
|
461430
|
-
|
|
461431
|
-
|
|
461432
|
-
|
|
461433
|
-
|
|
461434
|
-
|
|
461435
|
-
|
|
461436
|
-
|
|
461437
|
-
|
|
461438
|
-
|
|
461439
|
-
|
|
461440
|
-
|
|
461441
|
-
|
|
461442
|
-
|
|
461443
|
-
|
|
461444
|
-
|
|
461445
|
-
|
|
461446
|
-
|
|
461447
|
-
|
|
461448
|
-
|
|
461449
|
-
|
|
461450
|
-
|
|
461451
|
-
|
|
461452
|
-
|
|
461453
|
-
|
|
461454
|
-
|
|
461455
|
-
|
|
461456
|
-
|
|
461457
|
-
|
|
461458
|
-
}
|
|
461459
|
-
{
|
|
461460
|
-
|
|
461461
|
-
|
|
461462
|
-
|
|
461463
|
-
|
|
461464
|
-
|
|
461465
|
-
|
|
461466
|
-
|
|
461467
|
-
|
|
461468
|
-
|
|
461469
|
-
|
|
461470
|
-
|
|
461471
|
-
|
|
461472
|
-
|
|
461473
|
-
|
|
461474
|
-
|
|
461475
|
-
|
|
461476
|
-
|
|
461477
|
-
|
|
461478
|
-
|
|
461479
|
-
|
|
461480
|
-
|
|
461481
|
-
|
|
461482
|
-
|
|
461483
|
-
|
|
461484
|
-
}
|
|
461485
|
-
}
|
|
461486
|
-
{
|
|
461487
|
-
|
|
461488
|
-
|
|
461489
|
-
|
|
461490
|
-
|
|
461491
|
-
|
|
461492
|
-
|
|
461493
|
-
|
|
461494
|
-
|
|
461495
|
-
|
|
461496
|
-
|
|
461497
|
-
|
|
461498
|
-
|
|
461499
|
-
|
|
461500
|
-
|
|
461501
|
-
|
|
461502
|
-
|
|
461503
|
-
|
|
461504
|
-
|
|
461505
|
-
|
|
461506
|
-
|
|
461507
|
-
|
|
461508
|
-
|
|
461509
|
-
|
|
461510
|
-
|
|
461511
|
-
|
|
461512
|
-
{
|
|
461513
|
-
|
|
461514
|
-
|
|
461515
|
-
|
|
461516
|
-
|
|
461517
|
-
|
|
461518
|
-
|
|
461519
|
-
|
|
461520
|
-
|
|
461521
|
-
|
|
461522
|
-
|
|
461523
|
-
|
|
461524
|
-
|
|
461435
|
+
// ../cli/src/commands/_shared/scene-audio-mux.ts
|
|
461436
|
+
import { rename as rename6, unlink as unlink5 } from "node:fs/promises";
|
|
461437
|
+
import { resolve as resolve37, dirname as dirname23, extname as extname12, basename as basename15 } from "node:path";
|
|
461438
|
+
function buildAudioMuxFilter(audios) {
|
|
461439
|
+
if (audios.length === 0) return null;
|
|
461440
|
+
const labels = [];
|
|
461441
|
+
const stages = [];
|
|
461442
|
+
audios.forEach((a, i) => {
|
|
461443
|
+
const inputIdx = i + 1;
|
|
461444
|
+
const delayMs = Math.max(0, Math.round(a.absoluteStart * 1e3));
|
|
461445
|
+
const volume = Number.isFinite(a.volume) ? a.volume : 1;
|
|
461446
|
+
const trimSec = Math.max(0, a.clipDurationCap);
|
|
461447
|
+
const label = `a${i}`;
|
|
461448
|
+
const stage = [
|
|
461449
|
+
`[${inputIdx}:a]`,
|
|
461450
|
+
`atrim=duration=${trimSec.toFixed(3)},`,
|
|
461451
|
+
`asetpts=PTS-STARTPTS,`,
|
|
461452
|
+
`adelay=${delayMs}:all=1,`,
|
|
461453
|
+
`volume=${volume}`,
|
|
461454
|
+
`[${label}]`
|
|
461455
|
+
].join("");
|
|
461456
|
+
stages.push(stage);
|
|
461457
|
+
labels.push(`[${label}]`);
|
|
461458
|
+
});
|
|
461459
|
+
if (audios.length === 1) {
|
|
461460
|
+
return {
|
|
461461
|
+
filterComplex: stages.join(";"),
|
|
461462
|
+
outLabel: labels[0],
|
|
461463
|
+
inputCount: 1
|
|
461464
|
+
};
|
|
461465
|
+
}
|
|
461466
|
+
const mix = `${labels.join("")}amix=inputs=${audios.length}:dropout_transition=0:normalize=0[mixed]`;
|
|
461467
|
+
return {
|
|
461468
|
+
filterComplex: `${stages.join(";")};${mix}`,
|
|
461469
|
+
outLabel: "[mixed]",
|
|
461470
|
+
inputCount: audios.length
|
|
461471
|
+
};
|
|
461472
|
+
}
|
|
461473
|
+
function audioCodecForFormat(format4) {
|
|
461474
|
+
if (format4 === "webm") return "libopus";
|
|
461475
|
+
if (format4 === "mov") return "pcm_s16le";
|
|
461476
|
+
return "aac";
|
|
461477
|
+
}
|
|
461478
|
+
async function muxAudioIntoVideo(opts) {
|
|
461479
|
+
if (opts.audios.length === 0) {
|
|
461480
|
+
return { success: true, outputPath: opts.videoPath, audioCount: 0 };
|
|
461481
|
+
}
|
|
461482
|
+
if (!commandExists("ffmpeg")) {
|
|
461483
|
+
return {
|
|
461484
|
+
success: false,
|
|
461485
|
+
outputPath: opts.videoPath,
|
|
461486
|
+
audioCount: opts.audios.length,
|
|
461487
|
+
error: "ffmpeg not found in PATH \u2014 install via `brew install ffmpeg` (mac) or your package manager"
|
|
461488
|
+
};
|
|
461489
|
+
}
|
|
461490
|
+
const filter4 = buildAudioMuxFilter(opts.audios);
|
|
461491
|
+
if (!filter4) {
|
|
461492
|
+
return { success: true, outputPath: opts.videoPath, audioCount: 0 };
|
|
461493
|
+
}
|
|
461494
|
+
const ext = extname12(opts.videoPath) || `.${opts.format}`;
|
|
461495
|
+
const tmpPath = resolve37(
|
|
461496
|
+
dirname23(opts.videoPath),
|
|
461497
|
+
`.${basename15(opts.videoPath, ext)}.muxing${ext}`
|
|
461498
|
+
);
|
|
461499
|
+
const args = ["-y", "-loglevel", "error", "-i", opts.videoPath];
|
|
461500
|
+
for (const a of opts.audios) {
|
|
461501
|
+
args.push("-i", a.srcAbs);
|
|
461502
|
+
}
|
|
461503
|
+
args.push(
|
|
461504
|
+
"-filter_complex",
|
|
461505
|
+
filter4.filterComplex,
|
|
461506
|
+
"-map",
|
|
461507
|
+
"0:v",
|
|
461508
|
+
"-map",
|
|
461509
|
+
filter4.outLabel,
|
|
461510
|
+
"-c:v",
|
|
461511
|
+
"copy",
|
|
461512
|
+
"-c:a",
|
|
461513
|
+
audioCodecForFormat(opts.format),
|
|
461514
|
+
// Cap on the video duration so audio that overruns the producer's render
|
|
461515
|
+
// (e.g. a long Kokoro wav on a short scene) doesn't extend the output.
|
|
461516
|
+
// Video drives the timeline because the producer already counted frames.
|
|
461517
|
+
"-t",
|
|
461518
|
+
opts.totalDuration && opts.totalDuration > 0 ? opts.totalDuration.toFixed(3) : opts.videoDuration?.toFixed(3) ?? ""
|
|
461519
|
+
);
|
|
461520
|
+
if (args[args.length - 1] === "") {
|
|
461521
|
+
args.pop();
|
|
461522
|
+
args.pop();
|
|
461523
|
+
}
|
|
461524
|
+
args.push("-movflags", "+faststart", tmpPath);
|
|
461525
|
+
try {
|
|
461526
|
+
const { stderr } = await execSafe("ffmpeg", args);
|
|
461527
|
+
if (stderr && opts.onProgress) {
|
|
461528
|
+
stderr.split(/\r?\n/).forEach((line) => opts.onProgress?.(line));
|
|
461529
|
+
}
|
|
461530
|
+
} catch (err) {
|
|
461531
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
461532
|
+
try {
|
|
461533
|
+
await unlink5(tmpPath);
|
|
461534
|
+
} catch {
|
|
461535
|
+
}
|
|
461536
|
+
return {
|
|
461537
|
+
success: false,
|
|
461538
|
+
outputPath: opts.videoPath,
|
|
461539
|
+
audioCount: opts.audios.length,
|
|
461540
|
+
error: `ffmpeg mux failed: ${msg}`
|
|
461541
|
+
};
|
|
461542
|
+
}
|
|
461543
|
+
await rename6(tmpPath, opts.videoPath);
|
|
461544
|
+
return {
|
|
461545
|
+
success: true,
|
|
461546
|
+
outputPath: opts.videoPath,
|
|
461547
|
+
audioCount: opts.audios.length
|
|
461548
|
+
};
|
|
461549
|
+
}
|
|
461550
|
+
var init_scene_audio_mux = __esm({
|
|
461551
|
+
"../cli/src/commands/_shared/scene-audio-mux.ts"() {
|
|
461552
|
+
"use strict";
|
|
461553
|
+
init_exec_safe();
|
|
461554
|
+
}
|
|
461555
|
+
});
|
|
461556
|
+
|
|
461557
|
+
// ../cli/src/commands/_shared/scene-render.ts
|
|
461558
|
+
var scene_render_exports = {};
|
|
461559
|
+
__export(scene_render_exports, {
|
|
461560
|
+
buildRenderConfig: () => buildRenderConfig,
|
|
461561
|
+
defaultOutputPath: () => defaultOutputPath,
|
|
461562
|
+
executeSceneRender: () => executeSceneRender,
|
|
461563
|
+
qualityToCrf: () => qualityToCrf2
|
|
461564
|
+
});
|
|
461565
|
+
import { mkdir as mkdir17, readFile as readFile23, stat as stat3 } from "node:fs/promises";
|
|
461566
|
+
import { existsSync as existsSync36 } from "node:fs";
|
|
461567
|
+
import { resolve as resolve38, relative as relative6, dirname as dirname24, basename as basename16 } from "node:path";
|
|
461568
|
+
function qualityToCrf2(quality = "standard") {
|
|
461569
|
+
return quality === "draft" ? 28 : quality === "high" ? 18 : 23;
|
|
461570
|
+
}
|
|
461571
|
+
function defaultOutputPath(opts) {
|
|
461572
|
+
const fmt = opts.format ?? "mp4";
|
|
461573
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
461574
|
+
const stamp = now.toISOString().replace(/[:T]/g, "-").replace(/\..+$/, "");
|
|
461575
|
+
const name = (opts.projectName ?? basename16(resolve38(opts.projectDir))) || "scene";
|
|
461576
|
+
return resolve38(opts.projectDir, "renders", `${name}-${stamp}.${fmt}`);
|
|
461577
|
+
}
|
|
461578
|
+
async function readProjectName(projectDir) {
|
|
461579
|
+
const cfgPath = resolve38(projectDir, "vibe.project.yaml");
|
|
461580
|
+
if (!existsSync36(cfgPath)) return void 0;
|
|
461581
|
+
try {
|
|
461582
|
+
const raw2 = await (await import("node:fs/promises")).readFile(cfgPath, "utf-8");
|
|
461583
|
+
const parsed = (0, import_yaml5.parse)(raw2);
|
|
461584
|
+
return parsed?.name ?? void 0;
|
|
461585
|
+
} catch {
|
|
461586
|
+
return void 0;
|
|
461587
|
+
}
|
|
461588
|
+
}
|
|
461589
|
+
function buildRenderConfig(opts) {
|
|
461590
|
+
const quality = opts.quality ?? "standard";
|
|
461591
|
+
return {
|
|
461592
|
+
fps: opts.fps ?? 30,
|
|
461593
|
+
quality,
|
|
461594
|
+
format: opts.format ?? "mp4",
|
|
461595
|
+
entryFile: opts.entryFile ?? "index.html",
|
|
461596
|
+
crf: qualityToCrf2(quality),
|
|
461597
|
+
workers: opts.workers ?? 1
|
|
461598
|
+
};
|
|
461599
|
+
}
|
|
461600
|
+
async function executeSceneRender(opts = {}) {
|
|
461601
|
+
const projectDir = resolve38(opts.projectDir ?? ".");
|
|
461602
|
+
const root2 = opts.root ?? "index.html";
|
|
461603
|
+
const projectStat = await safeStat(projectDir);
|
|
461604
|
+
if (!projectStat || !projectStat.isDirectory()) {
|
|
461605
|
+
return { success: false, error: `Project directory not found: ${projectDir}` };
|
|
461606
|
+
}
|
|
461607
|
+
if (!await rootExists(projectDir, root2)) {
|
|
461608
|
+
return {
|
|
461609
|
+
success: false,
|
|
461610
|
+
error: `Root composition not found: ${resolve38(projectDir, root2)}. Run \`vibe scene init\` first.`
|
|
461611
|
+
};
|
|
461612
|
+
}
|
|
461613
|
+
const chrome2 = await preflightChrome();
|
|
461614
|
+
if (!chrome2.ok) {
|
|
461615
|
+
return { success: false, error: chrome2.reason };
|
|
461616
|
+
}
|
|
461617
|
+
const projectName = await readProjectName(projectDir);
|
|
461618
|
+
const outputPath = opts.output ? resolve38(projectDir, opts.output) : defaultOutputPath({ projectDir, projectName, format: opts.format });
|
|
461619
|
+
await mkdir17(dirname24(outputPath), { recursive: true });
|
|
461620
|
+
const config4 = buildRenderConfig({
|
|
461621
|
+
fps: opts.fps,
|
|
461622
|
+
quality: opts.quality,
|
|
461623
|
+
format: opts.format,
|
|
461624
|
+
workers: opts.workers,
|
|
461625
|
+
entryFile: root2
|
|
461626
|
+
});
|
|
461627
|
+
const job = createRenderJob(config4);
|
|
461628
|
+
const start = Date.now();
|
|
461629
|
+
try {
|
|
461630
|
+
await executeRenderJob(
|
|
461631
|
+
job,
|
|
461632
|
+
projectDir,
|
|
461633
|
+
outputPath,
|
|
461634
|
+
(j, msg) => opts.onProgress?.(j.progress, j.currentStage ?? msg),
|
|
461635
|
+
opts.signal
|
|
461636
|
+
);
|
|
461637
|
+
} catch (err) {
|
|
461638
|
+
return {
|
|
461639
|
+
success: false,
|
|
461640
|
+
error: err instanceof Error ? err.message : String(err)
|
|
461641
|
+
};
|
|
461642
|
+
}
|
|
461643
|
+
let audioCount = 0;
|
|
461644
|
+
let audioMuxApplied = false;
|
|
461645
|
+
let audioMuxWarning;
|
|
461646
|
+
try {
|
|
461647
|
+
opts.onProgress?.(0.95, "Mixing audio");
|
|
461648
|
+
const rootHtml = await readFile23(resolve38(projectDir, root2), "utf-8");
|
|
461649
|
+
const audios = await scanSceneAudio({ projectDir, rootHtml });
|
|
461650
|
+
audioCount = audios.length;
|
|
461651
|
+
if (audios.length > 0) {
|
|
461652
|
+
const videoDuration = job.totalFrames && config4.fps ? job.totalFrames / config4.fps : void 0;
|
|
461653
|
+
const mux = await muxAudioIntoVideo({
|
|
461654
|
+
videoPath: outputPath,
|
|
461655
|
+
audios,
|
|
461656
|
+
format: config4.format ?? "mp4",
|
|
461657
|
+
videoDuration,
|
|
461658
|
+
onProgress: (line) => {
|
|
461659
|
+
if (line) opts.onProgress?.(0.97, line);
|
|
461660
|
+
}
|
|
461661
|
+
});
|
|
461662
|
+
if (mux.success) {
|
|
461663
|
+
audioMuxApplied = true;
|
|
461664
|
+
} else {
|
|
461665
|
+
audioMuxWarning = mux.error;
|
|
461666
|
+
}
|
|
461667
|
+
}
|
|
461668
|
+
} catch (err) {
|
|
461669
|
+
audioMuxWarning = err instanceof Error ? err.message : String(err);
|
|
461670
|
+
}
|
|
461671
|
+
return {
|
|
461672
|
+
success: true,
|
|
461673
|
+
outputPath: relative6(process.cwd(), outputPath) || outputPath,
|
|
461674
|
+
durationMs: Date.now() - start,
|
|
461675
|
+
framesRendered: job.framesRendered,
|
|
461676
|
+
totalFrames: job.totalFrames,
|
|
461677
|
+
fps: config4.fps,
|
|
461678
|
+
quality: config4.quality,
|
|
461679
|
+
format: config4.format,
|
|
461680
|
+
audioCount,
|
|
461681
|
+
audioMuxApplied,
|
|
461682
|
+
audioMuxWarning
|
|
461683
|
+
};
|
|
461684
|
+
}
|
|
461685
|
+
async function safeStat(p) {
|
|
461686
|
+
try {
|
|
461687
|
+
return await stat3(p);
|
|
461688
|
+
} catch {
|
|
461689
|
+
return null;
|
|
461690
|
+
}
|
|
461691
|
+
}
|
|
461692
|
+
var import_yaml5;
|
|
461693
|
+
var init_scene_render = __esm({
|
|
461694
|
+
"../cli/src/commands/_shared/scene-render.ts"() {
|
|
461695
|
+
"use strict";
|
|
461696
|
+
import_yaml5 = __toESM(require_dist16(), 1);
|
|
461697
|
+
init_dist();
|
|
461698
|
+
init_chrome();
|
|
461699
|
+
init_scene_lint();
|
|
461700
|
+
init_scene_audio_scan();
|
|
461701
|
+
init_scene_audio_mux();
|
|
461702
|
+
}
|
|
461703
|
+
});
|
|
461704
|
+
|
|
461705
|
+
// ../cli/src/commands/_shared/tts-resolve.ts
|
|
461706
|
+
async function resolveTtsProvider(preferred = "auto") {
|
|
461707
|
+
const choice = preferred === "auto" ? hasApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
|
|
461708
|
+
if (choice === "elevenlabs") {
|
|
461709
|
+
return buildElevenLabs();
|
|
461710
|
+
}
|
|
461711
|
+
return buildKokoro();
|
|
461712
|
+
}
|
|
461713
|
+
async function buildElevenLabs() {
|
|
461714
|
+
const key2 = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs");
|
|
461715
|
+
if (!key2) {
|
|
461716
|
+
throw new TtsKeyMissingError("elevenlabs");
|
|
461717
|
+
}
|
|
461718
|
+
const provider = new ElevenLabsProvider();
|
|
461719
|
+
await provider.initialize({ apiKey: key2 });
|
|
461720
|
+
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
461721
|
+
voiceId: opts?.voice,
|
|
461722
|
+
speed: opts?.speed
|
|
461723
|
+
});
|
|
461724
|
+
return { provider: "elevenlabs", audioExtension: "mp3", call };
|
|
461725
|
+
}
|
|
461726
|
+
async function buildKokoro() {
|
|
461727
|
+
const provider = new KokoroProvider();
|
|
461728
|
+
await provider.initialize({});
|
|
461729
|
+
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
461730
|
+
voice: opts?.voice,
|
|
461731
|
+
speed: opts?.speed,
|
|
461732
|
+
onProgress: opts?.onProgress
|
|
461733
|
+
});
|
|
461734
|
+
return { provider: "kokoro", audioExtension: "wav", call };
|
|
461735
|
+
}
|
|
461736
|
+
function parseTtsProviderName(value) {
|
|
461737
|
+
if (!value) return "auto";
|
|
461738
|
+
if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
|
|
461739
|
+
return value;
|
|
461740
|
+
}
|
|
461741
|
+
throw new Error(
|
|
461742
|
+
`Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`
|
|
461743
|
+
);
|
|
461744
|
+
}
|
|
461745
|
+
var TtsKeyMissingError;
|
|
461746
|
+
var init_tts_resolve = __esm({
|
|
461747
|
+
"../cli/src/commands/_shared/tts-resolve.ts"() {
|
|
461748
|
+
"use strict";
|
|
461749
|
+
init_dist2();
|
|
461750
|
+
init_api_key();
|
|
461751
|
+
init_api_key();
|
|
461752
|
+
TtsKeyMissingError = class extends Error {
|
|
461753
|
+
constructor(provider) {
|
|
461754
|
+
super(
|
|
461755
|
+
provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
|
|
461756
|
+
);
|
|
461757
|
+
this.provider = provider;
|
|
461758
|
+
this.name = "TtsKeyMissingError";
|
|
461759
|
+
}
|
|
461760
|
+
};
|
|
461761
|
+
}
|
|
461762
|
+
});
|
|
461763
|
+
|
|
461764
|
+
// ../cli/src/commands/_shared/scene-build.ts
|
|
461765
|
+
var scene_build_exports = {};
|
|
461766
|
+
__export(scene_build_exports, {
|
|
461767
|
+
executeSceneBuild: () => executeSceneBuild
|
|
461768
|
+
});
|
|
461769
|
+
import { existsSync as existsSync37 } from "node:fs";
|
|
461770
|
+
import { mkdir as mkdir18, readFile as readFile24, writeFile as writeFile23 } from "node:fs/promises";
|
|
461771
|
+
import { dirname as dirname25, join as join24, resolve as resolve39 } from "node:path";
|
|
461772
|
+
async function executeSceneBuild(opts) {
|
|
461773
|
+
const startedAt = Date.now();
|
|
461774
|
+
const projectDir = resolve39(opts.projectDir);
|
|
461775
|
+
const onProgress = opts.onProgress ?? (() => {
|
|
461776
|
+
});
|
|
461777
|
+
const storyboardPath = join24(projectDir, "STORYBOARD.md");
|
|
461778
|
+
if (!existsSync37(storyboardPath)) {
|
|
461779
|
+
return failBeforePrimitives(`STORYBOARD.md not found at ${storyboardPath}`, startedAt);
|
|
461780
|
+
}
|
|
461781
|
+
const storyboardMd = await readFile24(storyboardPath, "utf-8");
|
|
461782
|
+
const parsed = parseStoryboard(storyboardMd);
|
|
461783
|
+
if (parsed.beats.length === 0) {
|
|
461784
|
+
return failBeforePrimitives(
|
|
461785
|
+
`STORYBOARD.md at ${storyboardPath} has no \`## Beat \u2026\` headings.`,
|
|
461786
|
+
startedAt
|
|
461787
|
+
);
|
|
461788
|
+
}
|
|
461789
|
+
const ttsProvider = opts.ttsProvider ?? parsed.frontmatter?.providers?.tts ?? "auto";
|
|
461790
|
+
const imageProvider = opts.imageProvider ?? parsed.frontmatter?.providers?.image ?? "openai";
|
|
461791
|
+
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
461792
|
+
onProgress({ type: "phase-start", phase: "primitives" });
|
|
461793
|
+
const beatOutcomes = await Promise.all(
|
|
461794
|
+
parsed.beats.map((beat) => buildBeatPrimitives(beat, {
|
|
461795
|
+
projectDir,
|
|
461796
|
+
ttsProvider,
|
|
461797
|
+
voice,
|
|
461798
|
+
imageProvider,
|
|
461799
|
+
imageQuality: opts.imageQuality ?? "hd",
|
|
461800
|
+
imageSize: opts.imageSize ?? "1536x1024",
|
|
461801
|
+
skipNarration: opts.skipNarration ?? false,
|
|
461802
|
+
skipBackdrop: opts.skipBackdrop ?? false,
|
|
461803
|
+
force: opts.force ?? false,
|
|
461804
|
+
onProgress
|
|
461805
|
+
}))
|
|
461806
|
+
);
|
|
461807
|
+
onProgress({ type: "phase-start", phase: "compose" });
|
|
461808
|
+
const composeResult = await executeComposeScenesWithSkills(
|
|
461809
|
+
{
|
|
461810
|
+
project: ".",
|
|
461811
|
+
effort: opts.effort,
|
|
461812
|
+
cacheDir: opts.cacheDir,
|
|
461813
|
+
onProgress: (e) => onProgress(e)
|
|
461814
|
+
},
|
|
461815
|
+
projectDir
|
|
461816
|
+
);
|
|
461817
|
+
if (!composeResult.success) {
|
|
461818
|
+
return {
|
|
461819
|
+
success: false,
|
|
461820
|
+
error: `compose failed: ${composeResult.error ?? "unknown"}`,
|
|
461821
|
+
beats: beatOutcomes,
|
|
461822
|
+
composeData: composeResult.data,
|
|
461823
|
+
totalLatencyMs: Date.now() - startedAt
|
|
461824
|
+
};
|
|
461825
|
+
}
|
|
461826
|
+
let outputPath;
|
|
461827
|
+
let renderResult;
|
|
461828
|
+
if (!opts.skipRender) {
|
|
461829
|
+
onProgress({ type: "phase-start", phase: "render" });
|
|
461830
|
+
onProgress({ type: "render-start" });
|
|
461831
|
+
renderResult = await executeSceneRender({ projectDir });
|
|
461832
|
+
if (!renderResult.success) {
|
|
461833
|
+
return {
|
|
461834
|
+
success: false,
|
|
461835
|
+
error: `render failed: ${renderResult.error ?? "unknown"}`,
|
|
461836
|
+
beats: beatOutcomes,
|
|
461837
|
+
composeData: composeResult.data,
|
|
461838
|
+
renderResult,
|
|
461839
|
+
totalLatencyMs: Date.now() - startedAt
|
|
461840
|
+
};
|
|
461841
|
+
}
|
|
461842
|
+
outputPath = renderResult.outputPath;
|
|
461843
|
+
if (outputPath) onProgress({ type: "render-done", outputPath });
|
|
461844
|
+
}
|
|
461845
|
+
return {
|
|
461846
|
+
success: true,
|
|
461847
|
+
beats: beatOutcomes,
|
|
461848
|
+
outputPath,
|
|
461849
|
+
composeData: composeResult.data,
|
|
461850
|
+
renderResult,
|
|
461851
|
+
totalLatencyMs: Date.now() - startedAt
|
|
461852
|
+
};
|
|
461853
|
+
}
|
|
461854
|
+
async function buildBeatPrimitives(beat, ctx) {
|
|
461855
|
+
const [narration, backdrop] = await Promise.all([
|
|
461856
|
+
ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
|
|
461857
|
+
ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx)
|
|
461858
|
+
]);
|
|
461859
|
+
return {
|
|
461860
|
+
beatId: beat.id,
|
|
461861
|
+
narrationStatus: narration.status,
|
|
461862
|
+
narrationPath: narration.path,
|
|
461863
|
+
narrationError: narration.error,
|
|
461864
|
+
backdropStatus: backdrop.status,
|
|
461865
|
+
backdropPath: backdrop.path,
|
|
461866
|
+
backdropError: backdrop.error
|
|
461867
|
+
};
|
|
461868
|
+
}
|
|
461869
|
+
async function dispatchNarration(beat, ctx) {
|
|
461870
|
+
const text = beat.cues?.narration;
|
|
461871
|
+
if (!text) return { status: "no-cue" };
|
|
461872
|
+
for (const ext of ["mp3", "wav"]) {
|
|
461873
|
+
const rel2 = `assets/narration-${beat.id}.${ext}`;
|
|
461874
|
+
if (existsSync37(join24(ctx.projectDir, rel2)) && !ctx.force) {
|
|
461875
|
+
ctx.onProgress({ type: "narration-cached", beatId: beat.id, path: rel2 });
|
|
461876
|
+
return { status: "cached", path: rel2 };
|
|
461877
|
+
}
|
|
461878
|
+
}
|
|
461879
|
+
let resolution;
|
|
461880
|
+
try {
|
|
461881
|
+
resolution = await resolveTtsProvider(ctx.ttsProvider);
|
|
461882
|
+
} catch (err) {
|
|
461883
|
+
const error = err instanceof TtsKeyMissingError ? err.message : err.message;
|
|
461884
|
+
ctx.onProgress({ type: "narration-failed", beatId: beat.id, error });
|
|
461885
|
+
return { status: "failed", error };
|
|
461886
|
+
}
|
|
461887
|
+
const result = await resolution.call(text, { voice: ctx.voice });
|
|
461888
|
+
if (!result.success || !result.audioBuffer) {
|
|
461889
|
+
const error = result.error ?? "unknown TTS failure";
|
|
461890
|
+
ctx.onProgress({ type: "narration-failed", beatId: beat.id, error });
|
|
461891
|
+
return { status: "failed", error };
|
|
461892
|
+
}
|
|
461893
|
+
const rel = `assets/narration-${beat.id}.${resolution.audioExtension}`;
|
|
461894
|
+
const abs = join24(ctx.projectDir, rel);
|
|
461895
|
+
await mkdir18(dirname25(abs), { recursive: true });
|
|
461896
|
+
await writeFile23(abs, result.audioBuffer);
|
|
461897
|
+
ctx.onProgress({
|
|
461898
|
+
type: "narration-generated",
|
|
461899
|
+
beatId: beat.id,
|
|
461900
|
+
path: rel,
|
|
461901
|
+
provider: resolution.provider
|
|
461902
|
+
});
|
|
461903
|
+
return { status: "generated", path: rel };
|
|
461904
|
+
}
|
|
461905
|
+
async function dispatchBackdrop(beat, ctx) {
|
|
461906
|
+
const prompt3 = beat.cues?.backdrop;
|
|
461907
|
+
if (!prompt3) return { status: "no-cue" };
|
|
461908
|
+
if (ctx.imageProvider !== "openai") {
|
|
461909
|
+
const error = `image provider "${ctx.imageProvider}" not yet supported (use openai)`;
|
|
461910
|
+
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
461911
|
+
return { status: "failed", error };
|
|
461912
|
+
}
|
|
461913
|
+
const rel = `assets/backdrop-${beat.id}.png`;
|
|
461914
|
+
const abs = join24(ctx.projectDir, rel);
|
|
461915
|
+
if (existsSync37(abs) && !ctx.force) {
|
|
461916
|
+
ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
|
|
461917
|
+
return { status: "cached", path: rel };
|
|
461918
|
+
}
|
|
461919
|
+
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
461920
|
+
if (!apiKey) {
|
|
461921
|
+
const error = "OPENAI_API_KEY not set \u2014 cannot dispatch backdrop";
|
|
461922
|
+
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
461923
|
+
return { status: "failed", error };
|
|
461924
|
+
}
|
|
461925
|
+
const provider = new OpenAIImageProvider();
|
|
461926
|
+
await provider.initialize({ apiKey });
|
|
461927
|
+
const result = await provider.generateImage(prompt3, {
|
|
461928
|
+
model: "gpt-image-2",
|
|
461929
|
+
size: ctx.imageSize,
|
|
461930
|
+
quality: ctx.imageQuality
|
|
461931
|
+
});
|
|
461932
|
+
if (!result.success || !result.images?.[0]?.base64) {
|
|
461933
|
+
const error = result.error ?? "no image data returned";
|
|
461934
|
+
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
461935
|
+
return { status: "failed", error };
|
|
461936
|
+
}
|
|
461937
|
+
await mkdir18(dirname25(abs), { recursive: true });
|
|
461938
|
+
await writeFile23(abs, Buffer.from(result.images[0].base64, "base64"));
|
|
461939
|
+
ctx.onProgress({
|
|
461940
|
+
type: "backdrop-generated",
|
|
461941
|
+
beatId: beat.id,
|
|
461942
|
+
path: rel,
|
|
461943
|
+
provider: "openai"
|
|
461944
|
+
});
|
|
461945
|
+
return { status: "generated", path: rel };
|
|
461946
|
+
}
|
|
461947
|
+
async function skipped(kind, beatId, reason, ctx) {
|
|
461948
|
+
ctx.onProgress({ type: `${kind}-skipped`, beatId, reason });
|
|
461949
|
+
return { status: "skipped" };
|
|
461950
|
+
}
|
|
461951
|
+
function failBeforePrimitives(error, startedAt) {
|
|
461952
|
+
return {
|
|
461953
|
+
success: false,
|
|
461954
|
+
error,
|
|
461955
|
+
beats: [],
|
|
461956
|
+
totalLatencyMs: Date.now() - startedAt
|
|
461957
|
+
};
|
|
461958
|
+
}
|
|
461959
|
+
var init_scene_build = __esm({
|
|
461960
|
+
"../cli/src/commands/_shared/scene-build.ts"() {
|
|
461961
|
+
"use strict";
|
|
461962
|
+
init_dist2();
|
|
461963
|
+
init_compose_scenes_skills();
|
|
461964
|
+
init_scene_render();
|
|
461965
|
+
init_storyboard_parse();
|
|
461966
|
+
init_tts_resolve();
|
|
461967
|
+
}
|
|
461968
|
+
});
|
|
461969
|
+
|
|
461970
|
+
// src/index.ts
|
|
461971
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
461972
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
461973
|
+
import {
|
|
461974
|
+
CallToolRequestSchema,
|
|
461975
|
+
ListToolsRequestSchema,
|
|
461976
|
+
ListResourcesRequestSchema,
|
|
461977
|
+
ReadResourceRequestSchema,
|
|
461978
|
+
ListPromptsRequestSchema,
|
|
461979
|
+
GetPromptRequestSchema
|
|
461980
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
461981
|
+
|
|
461982
|
+
// src/tools/project.ts
|
|
461983
|
+
init_engine();
|
|
461984
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
461985
|
+
import { resolve } from "node:path";
|
|
461986
|
+
async function loadProject(projectPath) {
|
|
461987
|
+
const absPath = resolve(process.cwd(), projectPath);
|
|
461988
|
+
const content = await readFile(absPath, "utf-8");
|
|
461989
|
+
const data = JSON.parse(content);
|
|
461990
|
+
return Project.fromJSON(data);
|
|
461991
|
+
}
|
|
461992
|
+
async function saveProject(projectPath, project) {
|
|
461993
|
+
const absPath = resolve(process.cwd(), projectPath);
|
|
461994
|
+
await writeFile(absPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
461995
|
+
}
|
|
461996
|
+
var projectTools = [
|
|
461997
|
+
{
|
|
461998
|
+
name: "project_create",
|
|
461999
|
+
description: "Create a new VibeFrame project file",
|
|
462000
|
+
inputSchema: {
|
|
462001
|
+
type: "object",
|
|
462002
|
+
properties: {
|
|
462003
|
+
name: { type: "string", description: "Project name" },
|
|
462004
|
+
outputPath: { type: "string", description: "Output file path (defaults to {name}.vibe.json)" },
|
|
462005
|
+
width: { type: "number", description: "Video width in pixels (default: 1920)" },
|
|
462006
|
+
height: { type: "number", description: "Video height in pixels (default: 1080)" },
|
|
462007
|
+
fps: { type: "number", description: "Frames per second (default: 30)" }
|
|
462008
|
+
},
|
|
462009
|
+
required: ["name"]
|
|
462010
|
+
}
|
|
462011
|
+
},
|
|
462012
|
+
{
|
|
462013
|
+
name: "project_info",
|
|
462014
|
+
description: "Get information about a VibeFrame project",
|
|
462015
|
+
inputSchema: {
|
|
462016
|
+
type: "object",
|
|
462017
|
+
properties: {
|
|
462018
|
+
projectPath: { type: "string", description: "Path to the .vibe.json project file" }
|
|
462019
|
+
},
|
|
462020
|
+
required: ["projectPath"]
|
|
462021
|
+
}
|
|
462022
|
+
}
|
|
462023
|
+
];
|
|
462024
|
+
async function handleProjectToolCall(name, args) {
|
|
462025
|
+
switch (name) {
|
|
462026
|
+
case "project_create": {
|
|
462027
|
+
const projectName = args.name;
|
|
462028
|
+
const outputPath = args.outputPath || `${projectName}.vibe.json`;
|
|
462029
|
+
const project = new Project(projectName);
|
|
462030
|
+
if (args.fps) {
|
|
462031
|
+
project.setFrameRate(args.fps);
|
|
462032
|
+
}
|
|
462033
|
+
await saveProject(outputPath, project);
|
|
462034
|
+
return `Created project "${projectName}" at ${outputPath}`;
|
|
462035
|
+
}
|
|
462036
|
+
case "project_info": {
|
|
462037
|
+
const project = await loadProject(args.projectPath);
|
|
462038
|
+
const meta = project.getMeta();
|
|
462039
|
+
const info = {
|
|
462040
|
+
name: meta.name,
|
|
462041
|
+
aspectRatio: meta.aspectRatio,
|
|
462042
|
+
frameRate: meta.frameRate,
|
|
462043
|
+
duration: meta.duration,
|
|
462044
|
+
sources: project.getSources().length,
|
|
462045
|
+
tracks: project.getTracks().length,
|
|
462046
|
+
clips: project.getClips().length
|
|
462047
|
+
};
|
|
462048
|
+
return JSON.stringify(info, null, 2);
|
|
462049
|
+
}
|
|
462050
|
+
default:
|
|
462051
|
+
throw new Error(`Unknown project tool: ${name}`);
|
|
462052
|
+
}
|
|
462053
|
+
}
|
|
462054
|
+
|
|
462055
|
+
// src/tools/timeline.ts
|
|
462056
|
+
import { resolve as resolve2 } from "node:path";
|
|
462057
|
+
var timelineTools = [
|
|
462058
|
+
{
|
|
462059
|
+
name: "timeline_add_source",
|
|
462060
|
+
description: "Add a media source (video, audio, image) to the project",
|
|
462061
|
+
inputSchema: {
|
|
462062
|
+
type: "object",
|
|
462063
|
+
properties: {
|
|
462064
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462065
|
+
mediaPath: { type: "string", description: "Path to the media file" },
|
|
462066
|
+
name: { type: "string", description: "Optional name for the source" },
|
|
462067
|
+
duration: { type: "number", description: "Duration of the media in seconds (default: 10)" }
|
|
462068
|
+
},
|
|
462069
|
+
required: ["projectPath", "mediaPath"]
|
|
462070
|
+
}
|
|
462071
|
+
},
|
|
462072
|
+
{
|
|
462073
|
+
name: "timeline_add_clip",
|
|
462074
|
+
description: "Add a clip to the timeline from an existing source",
|
|
462075
|
+
inputSchema: {
|
|
462076
|
+
type: "object",
|
|
462077
|
+
properties: {
|
|
462078
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462079
|
+
sourceId: { type: "string", description: "ID of the media source" },
|
|
462080
|
+
trackId: { type: "string", description: "ID of the track to add clip to (optional, uses first video track)" },
|
|
462081
|
+
startTime: { type: "number", description: "Start time on timeline in seconds (default: 0)" },
|
|
462082
|
+
duration: { type: "number", description: "Clip duration in seconds (optional, uses source duration)" }
|
|
462083
|
+
},
|
|
462084
|
+
required: ["projectPath", "sourceId"]
|
|
462085
|
+
}
|
|
462086
|
+
},
|
|
462087
|
+
{
|
|
462088
|
+
name: "timeline_split_clip",
|
|
462089
|
+
description: "Split a clip at a specific time",
|
|
462090
|
+
inputSchema: {
|
|
462091
|
+
type: "object",
|
|
462092
|
+
properties: {
|
|
462093
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462094
|
+
clipId: { type: "string", description: "ID of the clip to split" },
|
|
462095
|
+
splitTime: { type: "number", description: "Time to split at (relative to clip start) in seconds" }
|
|
462096
|
+
},
|
|
462097
|
+
required: ["projectPath", "clipId", "splitTime"]
|
|
462098
|
+
}
|
|
462099
|
+
},
|
|
462100
|
+
{
|
|
462101
|
+
name: "timeline_trim_clip",
|
|
462102
|
+
description: "Trim a clip by adjusting its start or end",
|
|
462103
|
+
inputSchema: {
|
|
462104
|
+
type: "object",
|
|
462105
|
+
properties: {
|
|
462106
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462107
|
+
clipId: { type: "string", description: "ID of the clip to trim" },
|
|
462108
|
+
trimStart: { type: "number", description: "New source start offset in seconds" },
|
|
462109
|
+
trimEnd: { type: "number", description: "New duration in seconds" }
|
|
462110
|
+
},
|
|
462111
|
+
required: ["projectPath", "clipId"]
|
|
462112
|
+
}
|
|
462113
|
+
},
|
|
462114
|
+
{
|
|
462115
|
+
name: "timeline_move_clip",
|
|
462116
|
+
description: "Move a clip to a new position or track",
|
|
462117
|
+
inputSchema: {
|
|
462118
|
+
type: "object",
|
|
462119
|
+
properties: {
|
|
462120
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462121
|
+
clipId: { type: "string", description: "ID of the clip to move" },
|
|
462122
|
+
newStartTime: { type: "number", description: "New start time on timeline in seconds" },
|
|
462123
|
+
newTrackId: { type: "string", description: "ID of the target track (optional)" }
|
|
462124
|
+
},
|
|
462125
|
+
required: ["projectPath", "clipId"]
|
|
462126
|
+
}
|
|
462127
|
+
},
|
|
462128
|
+
{
|
|
462129
|
+
name: "timeline_delete_clip",
|
|
462130
|
+
description: "Delete a clip from the timeline",
|
|
462131
|
+
inputSchema: {
|
|
462132
|
+
type: "object",
|
|
462133
|
+
properties: {
|
|
462134
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462135
|
+
clipId: { type: "string", description: "ID of the clip to delete" }
|
|
462136
|
+
},
|
|
462137
|
+
required: ["projectPath", "clipId"]
|
|
462138
|
+
}
|
|
462139
|
+
},
|
|
462140
|
+
{
|
|
462141
|
+
name: "timeline_duplicate_clip",
|
|
462142
|
+
description: "Duplicate a clip",
|
|
462143
|
+
inputSchema: {
|
|
462144
|
+
type: "object",
|
|
462145
|
+
properties: {
|
|
462146
|
+
projectPath: { type: "string", description: "Path to the project file" },
|
|
462147
|
+
clipId: { type: "string", description: "ID of the clip to duplicate" },
|
|
462148
|
+
newStartTime: { type: "number", description: "Start time for the duplicated clip (optional, places after original)" }
|
|
462149
|
+
},
|
|
462150
|
+
required: ["projectPath", "clipId"]
|
|
462151
|
+
}
|
|
462152
|
+
},
|
|
461525
462153
|
{
|
|
461526
462154
|
name: "timeline_add_effect",
|
|
461527
462155
|
description: "Add an effect to a clip",
|
|
@@ -463006,9 +463634,9 @@ async function handleAiAnalysisToolCall(name, args) {
|
|
|
463006
463634
|
|
|
463007
463635
|
// src/tools/ai-pipelines.ts
|
|
463008
463636
|
init_ai_script_pipeline();
|
|
463009
|
-
import { writeFile as
|
|
463637
|
+
import { writeFile as writeFile25 } from "node:fs/promises";
|
|
463010
463638
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
463011
|
-
import { join as
|
|
463639
|
+
import { join as join25 } from "node:path";
|
|
463012
463640
|
|
|
463013
463641
|
// ../cli/src/commands/ai-highlights.ts
|
|
463014
463642
|
import { readFile as readFile13, writeFile as writeFile14, mkdir as mkdir12 } from "node:fs/promises";
|
|
@@ -463452,10 +464080,10 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
|
|
|
463452
464080
|
}
|
|
463453
464081
|
|
|
463454
464082
|
// ../cli/src/pipeline/executor.ts
|
|
463455
|
-
var
|
|
463456
|
-
import { resolve as
|
|
463457
|
-
import { readFile as
|
|
463458
|
-
import { existsSync as
|
|
464083
|
+
var import_yaml6 = __toESM(require_dist16(), 1);
|
|
464084
|
+
import { resolve as resolve40 } from "node:path";
|
|
464085
|
+
import { readFile as readFile25, writeFile as writeFile24, mkdir as mkdir19 } from "node:fs/promises";
|
|
464086
|
+
import { existsSync as existsSync38 } from "node:fs";
|
|
463459
464087
|
|
|
463460
464088
|
// ../cli/src/pipeline/resolver.ts
|
|
463461
464089
|
function resolveStepParams(params, completedSteps) {
|
|
@@ -463555,9 +464183,9 @@ function registerAction(action, handler4) {
|
|
|
463555
464183
|
}
|
|
463556
464184
|
function getOutput(params, outputDir, defaultName) {
|
|
463557
464185
|
if (params.output && typeof params.output === "string") {
|
|
463558
|
-
return
|
|
464186
|
+
return resolve40(outputDir, params.output);
|
|
463559
464187
|
}
|
|
463560
|
-
return
|
|
464188
|
+
return resolve40(outputDir, defaultName);
|
|
463561
464189
|
}
|
|
463562
464190
|
async function ensureActionsRegistered() {
|
|
463563
464191
|
if (Object.keys(ACTION_HANDLERS).length > 0) return;
|
|
@@ -463697,14 +464325,64 @@ async function ensureActionsRegistered() {
|
|
|
463697
464325
|
error: r.error
|
|
463698
464326
|
};
|
|
463699
464327
|
});
|
|
464328
|
+
registerAction("scene-build", async (params, outputDir) => {
|
|
464329
|
+
const { executeSceneBuild: executeSceneBuild2 } = await Promise.resolve().then(() => (init_scene_build(), scene_build_exports));
|
|
464330
|
+
const projectRel = params.project ?? ".";
|
|
464331
|
+
const r = await executeSceneBuild2({
|
|
464332
|
+
projectDir: resolve40(outputDir, projectRel),
|
|
464333
|
+
effort: params.effort,
|
|
464334
|
+
skipNarration: params.skipNarration,
|
|
464335
|
+
skipBackdrop: params.skipBackdrop,
|
|
464336
|
+
skipRender: params.skipRender,
|
|
464337
|
+
ttsProvider: params.tts,
|
|
464338
|
+
voice: params.voice,
|
|
464339
|
+
imageProvider: params.imageProvider,
|
|
464340
|
+
imageQuality: params.quality,
|
|
464341
|
+
force: params.force
|
|
464342
|
+
});
|
|
464343
|
+
return {
|
|
464344
|
+
id: "",
|
|
464345
|
+
action: "scene-build",
|
|
464346
|
+
success: r.success,
|
|
464347
|
+
output: r.outputPath,
|
|
464348
|
+
data: { beats: r.beats, totalLatencyMs: r.totalLatencyMs, composeData: r.composeData ?? null },
|
|
464349
|
+
error: r.error
|
|
464350
|
+
};
|
|
464351
|
+
});
|
|
464352
|
+
registerAction("scene-render", async (params, outputDir) => {
|
|
464353
|
+
const { executeSceneRender: executeSceneRender2 } = await Promise.resolve().then(() => (init_scene_render(), scene_render_exports));
|
|
464354
|
+
const projectRel = params.project ?? ".";
|
|
464355
|
+
const r = await executeSceneRender2({
|
|
464356
|
+
projectDir: resolve40(outputDir, projectRel),
|
|
464357
|
+
root: params.root,
|
|
464358
|
+
output: params.output,
|
|
464359
|
+
fps: params.fps,
|
|
464360
|
+
quality: params.quality,
|
|
464361
|
+
format: params.format,
|
|
464362
|
+
workers: params.workers
|
|
464363
|
+
});
|
|
464364
|
+
return {
|
|
464365
|
+
id: "",
|
|
464366
|
+
action: "scene-render",
|
|
464367
|
+
success: r.success,
|
|
464368
|
+
output: r.outputPath,
|
|
464369
|
+
data: {
|
|
464370
|
+
durationMs: r.durationMs,
|
|
464371
|
+
framesRendered: r.framesRendered,
|
|
464372
|
+
audioCount: r.audioCount,
|
|
464373
|
+
audioMuxApplied: r.audioMuxApplied
|
|
464374
|
+
},
|
|
464375
|
+
error: r.error
|
|
464376
|
+
};
|
|
464377
|
+
});
|
|
463700
464378
|
}
|
|
463701
464379
|
async function loadPipeline(filePath) {
|
|
463702
|
-
const absPath =
|
|
463703
|
-
if (!
|
|
464380
|
+
const absPath = resolve40(process.cwd(), filePath);
|
|
464381
|
+
if (!existsSync38(absPath)) {
|
|
463704
464382
|
throw new Error(`Pipeline file not found: ${absPath}`);
|
|
463705
464383
|
}
|
|
463706
|
-
const content = await
|
|
463707
|
-
const manifest = (0,
|
|
464384
|
+
const content = await readFile25(absPath, "utf-8");
|
|
464385
|
+
const manifest = (0, import_yaml6.parse)(content);
|
|
463708
464386
|
if (!manifest.name) throw new Error("Pipeline missing 'name' field");
|
|
463709
464387
|
if (!manifest.steps || !Array.isArray(manifest.steps)) throw new Error("Pipeline missing 'steps' array");
|
|
463710
464388
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -463719,16 +464397,16 @@ async function loadPipeline(filePath) {
|
|
|
463719
464397
|
var CHECKPOINT_FILE = ".pipeline-state.yaml";
|
|
463720
464398
|
async function executePipeline(manifest, options = {}) {
|
|
463721
464399
|
await ensureActionsRegistered();
|
|
463722
|
-
const outputDir =
|
|
463723
|
-
await
|
|
464400
|
+
const outputDir = resolve40(process.cwd(), options.outputDir || `${manifest.name}-output`);
|
|
464401
|
+
await mkdir19(outputDir, { recursive: true });
|
|
463724
464402
|
const completedSteps = /* @__PURE__ */ new Map();
|
|
463725
464403
|
const results = [];
|
|
463726
464404
|
const startTime = Date.now();
|
|
463727
464405
|
if (options.resume) {
|
|
463728
|
-
const checkpointPath =
|
|
463729
|
-
if (
|
|
463730
|
-
const checkpointContent = await
|
|
463731
|
-
const checkpoint = (0,
|
|
464406
|
+
const checkpointPath = resolve40(outputDir, CHECKPOINT_FILE);
|
|
464407
|
+
if (existsSync38(checkpointPath)) {
|
|
464408
|
+
const checkpointContent = await readFile25(checkpointPath, "utf-8");
|
|
464409
|
+
const checkpoint = (0, import_yaml6.parse)(checkpointContent);
|
|
463732
464410
|
for (const cs of checkpoint.completedSteps) {
|
|
463733
464411
|
completedSteps.set(cs.id, {
|
|
463734
464412
|
id: cs.id,
|
|
@@ -463845,9 +464523,9 @@ async function executePipeline(manifest, options = {}) {
|
|
|
463845
464523
|
data: s.data
|
|
463846
464524
|
}))
|
|
463847
464525
|
};
|
|
463848
|
-
await
|
|
463849
|
-
|
|
463850
|
-
(0,
|
|
464526
|
+
await writeFile24(
|
|
464527
|
+
resolve40(outputDir, CHECKPOINT_FILE),
|
|
464528
|
+
(0, import_yaml6.stringify)(checkpoint, { indent: 2 }),
|
|
463851
464529
|
"utf-8"
|
|
463852
464530
|
);
|
|
463853
464531
|
} else {
|
|
@@ -464094,8 +464772,8 @@ async function handleAiPipelineToolCall(name, args) {
|
|
|
464094
464772
|
let resolvedPath = pipelinePath;
|
|
464095
464773
|
let tempPath;
|
|
464096
464774
|
if (pipelineYaml) {
|
|
464097
|
-
tempPath =
|
|
464098
|
-
await
|
|
464775
|
+
tempPath = join25(tmpdir5(), `vibe-mcp-pipeline-${Date.now()}.yaml`);
|
|
464776
|
+
await writeFile25(tempPath, pipelineYaml, "utf-8");
|
|
464099
464777
|
resolvedPath = tempPath;
|
|
464100
464778
|
}
|
|
464101
464779
|
try {
|
|
@@ -464181,9 +464859,9 @@ init_ai_edit();
|
|
|
464181
464859
|
init_api_key();
|
|
464182
464860
|
init_exec_safe();
|
|
464183
464861
|
init_remotion();
|
|
464184
|
-
import { resolve as
|
|
464185
|
-
import { writeFile as
|
|
464186
|
-
import { existsSync as
|
|
464862
|
+
import { resolve as resolve41, dirname as dirname26, basename as basename17 } from "node:path";
|
|
464863
|
+
import { writeFile as writeFile26, mkdir as mkdir20, rm as rm5 } from "node:fs/promises";
|
|
464864
|
+
import { existsSync as existsSync39 } from "node:fs";
|
|
464187
464865
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
464188
464866
|
var ASS_STYLES = ["karaoke-sweep", "typewriter"];
|
|
464189
464867
|
var SENTENCE_BREAKS = /[.!?]/;
|
|
@@ -464333,9 +465011,9 @@ async function executeAnimatedCaption(options) {
|
|
|
464333
465011
|
} catch {
|
|
464334
465012
|
}
|
|
464335
465013
|
const effectiveFontSize = fontSize ?? Math.round(height * 0.04);
|
|
464336
|
-
const tmpAudioDir =
|
|
464337
|
-
await
|
|
464338
|
-
const audioPath =
|
|
465014
|
+
const tmpAudioDir = resolve41(tmpdir6(), `vf-ac-${Date.now()}`);
|
|
465015
|
+
await mkdir20(tmpAudioDir, { recursive: true });
|
|
465016
|
+
const audioPath = resolve41(tmpAudioDir, "audio.wav");
|
|
464339
465017
|
await execSafe("ffmpeg", [
|
|
464340
465018
|
"-y",
|
|
464341
465019
|
"-i",
|
|
@@ -464362,10 +465040,10 @@ async function executeAnimatedCaption(options) {
|
|
|
464362
465040
|
return { success: false, error: "No words detected in transcription" };
|
|
464363
465041
|
}
|
|
464364
465042
|
const groups = groupWords(transcript.words, { wordsPerGroup, maxChars });
|
|
464365
|
-
const absOutputPath =
|
|
464366
|
-
const outDir =
|
|
464367
|
-
if (!
|
|
464368
|
-
await
|
|
465043
|
+
const absOutputPath = resolve41(process.cwd(), outputPath);
|
|
465044
|
+
const outDir = dirname26(absOutputPath);
|
|
465045
|
+
if (!existsSync39(outDir)) {
|
|
465046
|
+
await mkdir20(outDir, { recursive: true });
|
|
464369
465047
|
}
|
|
464370
465048
|
if (tier === "ass") {
|
|
464371
465049
|
const assContent = generateASS(
|
|
@@ -464373,8 +465051,8 @@ async function executeAnimatedCaption(options) {
|
|
|
464373
465051
|
effectiveStyle,
|
|
464374
465052
|
{ highlightColor, fontSize: effectiveFontSize, position, width, height }
|
|
464375
465053
|
);
|
|
464376
|
-
const assPath =
|
|
464377
|
-
await
|
|
465054
|
+
const assPath = resolve41(tmpAudioDir, "captions.ass");
|
|
465055
|
+
await writeFile26(assPath, assContent, "utf-8");
|
|
464378
465056
|
const escapedAssPath = assPath.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
464379
465057
|
await execSafe("ffmpeg", [
|
|
464380
465058
|
"-y",
|
|
@@ -464396,7 +465074,7 @@ async function executeAnimatedCaption(options) {
|
|
|
464396
465074
|
width,
|
|
464397
465075
|
height,
|
|
464398
465076
|
fps: videoFps,
|
|
464399
|
-
videoFileName:
|
|
465077
|
+
videoFileName: basename17(videoPath)
|
|
464400
465078
|
});
|
|
464401
465079
|
const durationInFrames = Math.ceil(duration * videoFps);
|
|
464402
465080
|
const renderResult = await renderWithEmbeddedVideo({
|
|
@@ -464407,7 +465085,7 @@ async function executeAnimatedCaption(options) {
|
|
|
464407
465085
|
fps: videoFps,
|
|
464408
465086
|
durationInFrames,
|
|
464409
465087
|
videoPath,
|
|
464410
|
-
videoFileName:
|
|
465088
|
+
videoFileName: basename17(videoPath),
|
|
464411
465089
|
outputPath: absOutputPath
|
|
464412
465090
|
});
|
|
464413
465091
|
if (!renderResult.success) {
|
|
@@ -465083,654 +465761,261 @@ var aiAudioTools = [
|
|
|
465083
465761
|
threshold: { type: "string", description: "Sidechain threshold in dB (default: -30)" },
|
|
465084
465762
|
ratio: { type: "string", description: "Compression ratio (default: 3)" }
|
|
465085
465763
|
},
|
|
465086
|
-
required: ["musicPath", "voicePath"]
|
|
465087
|
-
}
|
|
465088
|
-
}
|
|
465089
|
-
];
|
|
465090
|
-
async function handleAiAudioToolCall(name, args) {
|
|
465091
|
-
switch (name) {
|
|
465092
|
-
case "audio_transcribe": {
|
|
465093
|
-
const result = await executeTranscribe({
|
|
465094
|
-
audioPath: args.audioPath,
|
|
465095
|
-
language: args.language,
|
|
465096
|
-
output: args.output,
|
|
465097
|
-
format: args.format
|
|
465098
|
-
});
|
|
465099
|
-
if (!result.success) return `Transcription failed: ${result.error}`;
|
|
465100
|
-
return JSON.stringify({
|
|
465101
|
-
success: true,
|
|
465102
|
-
text: result.text?.slice(0, 500),
|
|
465103
|
-
segmentCount: result.segments?.length,
|
|
465104
|
-
detectedLanguage: result.detectedLanguage,
|
|
465105
|
-
outputPath: result.outputPath
|
|
465106
|
-
});
|
|
465107
|
-
}
|
|
465108
|
-
case "audio_isolate": {
|
|
465109
|
-
const result = await executeIsolate({
|
|
465110
|
-
audioPath: args.audioPath,
|
|
465111
|
-
output: args.output
|
|
465112
|
-
});
|
|
465113
|
-
if (!result.success) return `Audio isolation failed: ${result.error}`;
|
|
465114
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath });
|
|
465115
|
-
}
|
|
465116
|
-
case "audio_voice_clone": {
|
|
465117
|
-
const result = await executeVoiceClone({
|
|
465118
|
-
samplePaths: args.samplePaths,
|
|
465119
|
-
name: args.name,
|
|
465120
|
-
description: args.description,
|
|
465121
|
-
removeNoise: args.removeNoise
|
|
465122
|
-
});
|
|
465123
|
-
if (!result.success) return `Voice cloning failed: ${result.error}`;
|
|
465124
|
-
return JSON.stringify({ success: true, voiceId: result.voiceId, name: result.name });
|
|
465125
|
-
}
|
|
465126
|
-
case "audio_dub": {
|
|
465127
|
-
const result = await executeDub({
|
|
465128
|
-
mediaPath: args.mediaPath,
|
|
465129
|
-
language: args.language,
|
|
465130
|
-
source: args.source,
|
|
465131
|
-
voice: args.voice,
|
|
465132
|
-
analyzeOnly: args.analyzeOnly,
|
|
465133
|
-
output: args.output
|
|
465134
|
-
});
|
|
465135
|
-
if (!result.success) return `Dubbing failed: ${result.error}`;
|
|
465136
|
-
return JSON.stringify({
|
|
465137
|
-
success: true,
|
|
465138
|
-
outputPath: result.outputPath,
|
|
465139
|
-
sourceLanguage: result.sourceLanguage,
|
|
465140
|
-
targetLanguage: result.targetLanguage,
|
|
465141
|
-
segmentCount: result.segmentCount
|
|
465142
|
-
});
|
|
465143
|
-
}
|
|
465144
|
-
case "audio_duck": {
|
|
465145
|
-
const result = await executeDuck({
|
|
465146
|
-
musicPath: args.musicPath,
|
|
465147
|
-
voicePath: args.voicePath,
|
|
465148
|
-
output: args.output,
|
|
465149
|
-
threshold: args.threshold,
|
|
465150
|
-
ratio: args.ratio
|
|
465151
|
-
});
|
|
465152
|
-
if (!result.success) return `Audio ducking failed: ${result.error}`;
|
|
465153
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath });
|
|
465154
|
-
}
|
|
465155
|
-
default:
|
|
465156
|
-
throw new Error(`Unknown AI audio tool: ${name}`);
|
|
465157
|
-
}
|
|
465158
|
-
}
|
|
465159
|
-
|
|
465160
|
-
// src/tools/ai-edit-advanced.ts
|
|
465161
|
-
init_edit_cmd();
|
|
465162
|
-
var aiEditAdvancedTools = [
|
|
465163
|
-
{
|
|
465164
|
-
name: "edit_grade",
|
|
465165
|
-
description: "Apply AI-generated color grading using Claude + FFmpeg. Use preset for free built-in grades, or style for custom AI-generated grades (needs ANTHROPIC_API_KEY).",
|
|
465166
|
-
inputSchema: {
|
|
465167
|
-
type: "object",
|
|
465168
|
-
properties: {
|
|
465169
|
-
videoPath: { type: "string", description: "Input video file path" },
|
|
465170
|
-
style: { type: "string", description: "Custom style description (e.g., 'cinematic warm sunset')" },
|
|
465171
|
-
preset: {
|
|
465172
|
-
type: "string",
|
|
465173
|
-
enum: ["film-noir", "vintage", "cinematic-warm", "cool-tones", "high-contrast", "pastel", "cyberpunk", "horror"],
|
|
465174
|
-
description: "Built-in preset (no API key needed)"
|
|
465175
|
-
},
|
|
465176
|
-
output: { type: "string", description: "Output video file path" },
|
|
465177
|
-
analyzeOnly: { type: "boolean", description: "Show FFmpeg filter without applying" }
|
|
465178
|
-
},
|
|
465179
|
-
required: ["videoPath"]
|
|
465180
|
-
}
|
|
465181
|
-
},
|
|
465182
|
-
{
|
|
465183
|
-
name: "edit_speed_ramp",
|
|
465184
|
-
description: "Apply content-aware speed ramping. Analyzes speech with Whisper, plans speed changes with Claude, applies with FFmpeg. Requires OPENAI_API_KEY + ANTHROPIC_API_KEY.",
|
|
465185
|
-
inputSchema: {
|
|
465186
|
-
type: "object",
|
|
465187
|
-
properties: {
|
|
465188
|
-
videoPath: { type: "string", description: "Input video file path (must have audio)" },
|
|
465189
|
-
output: { type: "string", description: "Output video file path" },
|
|
465190
|
-
style: {
|
|
465191
|
-
type: "string",
|
|
465192
|
-
enum: ["dramatic", "smooth", "action"],
|
|
465193
|
-
description: "Speed ramp style (default: dramatic)"
|
|
465194
|
-
},
|
|
465195
|
-
minSpeed: { type: "number", description: "Minimum speed factor (default: 0.25)" },
|
|
465196
|
-
maxSpeed: { type: "number", description: "Maximum speed factor (default: 4.0)" },
|
|
465197
|
-
analyzeOnly: { type: "boolean", description: "Show keyframes without applying" },
|
|
465198
|
-
language: { type: "string", description: "Language code for transcription" }
|
|
465199
|
-
},
|
|
465200
|
-
required: ["videoPath"]
|
|
465201
|
-
}
|
|
465202
|
-
},
|
|
465203
|
-
{
|
|
465204
|
-
name: "edit_reframe",
|
|
465205
|
-
description: "Auto-reframe video to a different aspect ratio using smart cropping. Free (FFmpeg only).",
|
|
465206
|
-
inputSchema: {
|
|
465207
|
-
type: "object",
|
|
465208
|
-
properties: {
|
|
465209
|
-
videoPath: { type: "string", description: "Input video file path" },
|
|
465210
|
-
aspect: { type: "string", description: "Target aspect ratio: 9:16, 1:1, 4:5 (default: 9:16)" },
|
|
465211
|
-
focus: {
|
|
465212
|
-
type: "string",
|
|
465213
|
-
enum: ["auto", "face", "center", "action"],
|
|
465214
|
-
description: "Focus mode (default: auto)"
|
|
465215
|
-
},
|
|
465216
|
-
output: { type: "string", description: "Output video file path" },
|
|
465217
|
-
analyzeOnly: { type: "boolean", description: "Show crop region without applying" }
|
|
465218
|
-
},
|
|
465219
|
-
required: ["videoPath"]
|
|
465220
|
-
}
|
|
465221
|
-
},
|
|
465222
|
-
{
|
|
465223
|
-
name: "edit_interpolate",
|
|
465224
|
-
description: "Create slow motion with AI frame interpolation using FFmpeg minterpolate. Free, no API key needed.",
|
|
465225
|
-
inputSchema: {
|
|
465226
|
-
type: "object",
|
|
465227
|
-
properties: {
|
|
465228
|
-
videoPath: { type: "string", description: "Input video file path" },
|
|
465229
|
-
output: { type: "string", description: "Output video file path" },
|
|
465230
|
-
factor: { type: "number", description: "Slow motion factor: 2, 4, or 8 (default: 2)" },
|
|
465231
|
-
fps: { type: "number", description: "Target output FPS (default: auto)" },
|
|
465232
|
-
quality: {
|
|
465233
|
-
type: "string",
|
|
465234
|
-
enum: ["fast", "quality"],
|
|
465235
|
-
description: "Interpolation quality (default: quality)"
|
|
465236
|
-
}
|
|
465237
|
-
},
|
|
465238
|
-
required: ["videoPath"]
|
|
465239
|
-
}
|
|
465240
|
-
},
|
|
465241
|
-
{
|
|
465242
|
-
name: "edit_upscale",
|
|
465243
|
-
description: "Upscale video resolution using FFmpeg (Lanczos scaling). Free, no API key needed.",
|
|
465244
|
-
inputSchema: {
|
|
465245
|
-
type: "object",
|
|
465246
|
-
properties: {
|
|
465247
|
-
videoPath: { type: "string", description: "Input video file path" },
|
|
465248
|
-
output: { type: "string", description: "Output video file path" },
|
|
465249
|
-
scale: { type: "number", description: "Scale factor: 2 or 4 (default: 2)" },
|
|
465250
|
-
quality: {
|
|
465251
|
-
type: "string",
|
|
465252
|
-
enum: ["fast", "quality"],
|
|
465253
|
-
description: "Scaling quality (default: quality, uses Lanczos)"
|
|
465254
|
-
}
|
|
465255
|
-
},
|
|
465256
|
-
required: ["videoPath"]
|
|
465257
|
-
}
|
|
465258
|
-
}
|
|
465259
|
-
];
|
|
465260
|
-
async function handleAiEditAdvancedToolCall(name, args) {
|
|
465261
|
-
switch (name) {
|
|
465262
|
-
case "edit_grade": {
|
|
465263
|
-
const result = await executeGrade({
|
|
465264
|
-
videoPath: args.videoPath,
|
|
465265
|
-
style: args.style,
|
|
465266
|
-
preset: args.preset,
|
|
465267
|
-
output: args.output,
|
|
465268
|
-
analyzeOnly: args.analyzeOnly
|
|
465269
|
-
});
|
|
465270
|
-
if (!result.success) return `Color grading failed: ${result.error}`;
|
|
465271
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath, style: result.style, description: result.description, ffmpegFilter: result.ffmpegFilter });
|
|
465272
|
-
}
|
|
465273
|
-
case "edit_speed_ramp": {
|
|
465274
|
-
const result = await executeSpeedRamp({
|
|
465275
|
-
videoPath: args.videoPath,
|
|
465276
|
-
output: args.output,
|
|
465277
|
-
style: args.style,
|
|
465278
|
-
minSpeed: args.minSpeed,
|
|
465279
|
-
maxSpeed: args.maxSpeed,
|
|
465280
|
-
analyzeOnly: args.analyzeOnly,
|
|
465281
|
-
language: args.language
|
|
465282
|
-
});
|
|
465283
|
-
if (!result.success) return `Speed ramping failed: ${result.error}`;
|
|
465284
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath, keyframeCount: result.keyframes?.length, avgSpeed: result.avgSpeed });
|
|
465285
|
-
}
|
|
465286
|
-
case "edit_reframe": {
|
|
465287
|
-
const result = await executeReframe({
|
|
465288
|
-
videoPath: args.videoPath,
|
|
465289
|
-
aspect: args.aspect,
|
|
465290
|
-
focus: args.focus,
|
|
465291
|
-
output: args.output,
|
|
465292
|
-
analyzeOnly: args.analyzeOnly
|
|
465293
|
-
});
|
|
465294
|
-
if (!result.success) return `Reframe failed: ${result.error}`;
|
|
465295
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath, sourceAspect: result.sourceAspect, targetAspect: result.targetAspect });
|
|
465296
|
-
}
|
|
465297
|
-
case "edit_interpolate": {
|
|
465298
|
-
const result = await executeInterpolate({
|
|
465299
|
-
videoPath: args.videoPath,
|
|
465300
|
-
output: args.output,
|
|
465301
|
-
factor: args.factor,
|
|
465302
|
-
fps: args.fps,
|
|
465303
|
-
quality: args.quality
|
|
465304
|
-
});
|
|
465305
|
-
if (!result.success) return `Interpolation failed: ${result.error}`;
|
|
465306
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath, originalFps: result.originalFps, targetFps: result.targetFps, factor: result.factor });
|
|
465307
|
-
}
|
|
465308
|
-
case "edit_upscale": {
|
|
465309
|
-
const result = await executeUpscale({
|
|
465310
|
-
videoPath: args.videoPath,
|
|
465311
|
-
output: args.output,
|
|
465312
|
-
scale: args.scale,
|
|
465313
|
-
quality: args.quality
|
|
465314
|
-
});
|
|
465315
|
-
if (!result.success) return `Upscale failed: ${result.error}`;
|
|
465316
|
-
return JSON.stringify({ success: true, outputPath: result.outputPath, originalRes: result.originalRes, targetRes: result.targetRes });
|
|
465764
|
+
required: ["musicPath", "voicePath"]
|
|
465317
465765
|
}
|
|
465318
|
-
default:
|
|
465319
|
-
throw new Error(`Unknown advanced edit tool: ${name}`);
|
|
465320
465766
|
}
|
|
465321
|
-
|
|
465322
|
-
|
|
465323
|
-
|
|
465324
|
-
|
|
465325
|
-
|
|
465326
|
-
|
|
465327
|
-
|
|
465328
|
-
|
|
465329
|
-
|
|
465330
|
-
|
|
465331
|
-
|
|
465332
|
-
|
|
465333
|
-
|
|
465334
|
-
|
|
465335
|
-
|
|
465336
|
-
|
|
465337
|
-
|
|
465338
|
-
|
|
465339
|
-
import { resolve as resolve38 } from "node:path";
|
|
465340
|
-
function parseRootClips(rootHtml) {
|
|
465341
|
-
const clips = [];
|
|
465342
|
-
const clipRegex = /<div\b[^>]*class="clip"[^>]*>/gi;
|
|
465343
|
-
let match2;
|
|
465344
|
-
while ((match2 = clipRegex.exec(rootHtml)) !== null) {
|
|
465345
|
-
const tag = match2[0];
|
|
465346
|
-
const compositionId = pickAttr(tag, "data-composition-id");
|
|
465347
|
-
const compositionSrc = pickAttr(tag, "data-composition-src");
|
|
465348
|
-
const start = pickNumberAttr(tag, "data-start");
|
|
465349
|
-
const duration = pickNumberAttr(tag, "data-duration");
|
|
465350
|
-
const trackIndex = pickNumberAttr(tag, "data-track-index") ?? 1;
|
|
465351
|
-
if (!compositionId || !compositionSrc || start === null || duration === null) {
|
|
465352
|
-
continue;
|
|
465767
|
+
];
|
|
465768
|
+
async function handleAiAudioToolCall(name, args) {
|
|
465769
|
+
switch (name) {
|
|
465770
|
+
case "audio_transcribe": {
|
|
465771
|
+
const result = await executeTranscribe({
|
|
465772
|
+
audioPath: args.audioPath,
|
|
465773
|
+
language: args.language,
|
|
465774
|
+
output: args.output,
|
|
465775
|
+
format: args.format
|
|
465776
|
+
});
|
|
465777
|
+
if (!result.success) return `Transcription failed: ${result.error}`;
|
|
465778
|
+
return JSON.stringify({
|
|
465779
|
+
success: true,
|
|
465780
|
+
text: result.text?.slice(0, 500),
|
|
465781
|
+
segmentCount: result.segments?.length,
|
|
465782
|
+
detectedLanguage: result.detectedLanguage,
|
|
465783
|
+
outputPath: result.outputPath
|
|
465784
|
+
});
|
|
465353
465785
|
}
|
|
465354
|
-
|
|
465355
|
-
|
|
465356
|
-
|
|
465357
|
-
|
|
465358
|
-
|
|
465359
|
-
|
|
465360
|
-
|
|
465361
|
-
let match2;
|
|
465362
|
-
while ((match2 = audioRegex.exec(compositionHtml)) !== null) {
|
|
465363
|
-
const attrs = match2[1];
|
|
465364
|
-
const src = pickAttr(attrs, "src");
|
|
465365
|
-
if (!src) continue;
|
|
465366
|
-
const localStart = pickNumberAttr(attrs, "data-start") ?? 0;
|
|
465367
|
-
const durationRaw = pickAttr(attrs, "data-duration");
|
|
465368
|
-
const durationHint = !durationRaw || durationRaw === "auto" ? "auto" : Number(durationRaw);
|
|
465369
|
-
const volume = pickNumberAttr(attrs, "data-volume") ?? 1;
|
|
465370
|
-
const trackIndex = pickNumberAttr(attrs, "data-track-index") ?? 2;
|
|
465371
|
-
out.push({ srcRel: src, localStart, durationHint, volume, trackIndex });
|
|
465372
|
-
}
|
|
465373
|
-
return out;
|
|
465374
|
-
}
|
|
465375
|
-
function makeFsCompositionReader(projectDir) {
|
|
465376
|
-
return async (compositionSrcRel) => {
|
|
465377
|
-
const abs = resolve38(projectDir, compositionSrcRel);
|
|
465378
|
-
try {
|
|
465379
|
-
return await readFile23(abs, "utf-8");
|
|
465380
|
-
} catch {
|
|
465381
|
-
return null;
|
|
465786
|
+
case "audio_isolate": {
|
|
465787
|
+
const result = await executeIsolate({
|
|
465788
|
+
audioPath: args.audioPath,
|
|
465789
|
+
output: args.output
|
|
465790
|
+
});
|
|
465791
|
+
if (!result.success) return `Audio isolation failed: ${result.error}`;
|
|
465792
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath });
|
|
465382
465793
|
}
|
|
465383
|
-
|
|
465384
|
-
|
|
465385
|
-
|
|
465386
|
-
|
|
465387
|
-
|
|
465388
|
-
|
|
465389
|
-
for (const clip of clips) {
|
|
465390
|
-
const html = await reader(clip.compositionSrc);
|
|
465391
|
-
if (!html) continue;
|
|
465392
|
-
const audios = parseSceneAudios(html);
|
|
465393
|
-
for (const audio of audios) {
|
|
465394
|
-
out.push({
|
|
465395
|
-
srcRel: audio.srcRel,
|
|
465396
|
-
srcAbs: resolve38(opts.projectDir, audio.srcRel),
|
|
465397
|
-
absoluteStart: clip.start + audio.localStart,
|
|
465398
|
-
durationHint: audio.durationHint,
|
|
465399
|
-
clipDurationCap: clip.duration - audio.localStart,
|
|
465400
|
-
volume: audio.volume,
|
|
465401
|
-
trackIndex: audio.trackIndex,
|
|
465402
|
-
compositionSrc: clip.compositionSrc
|
|
465794
|
+
case "audio_voice_clone": {
|
|
465795
|
+
const result = await executeVoiceClone({
|
|
465796
|
+
samplePaths: args.samplePaths,
|
|
465797
|
+
name: args.name,
|
|
465798
|
+
description: args.description,
|
|
465799
|
+
removeNoise: args.removeNoise
|
|
465403
465800
|
});
|
|
465801
|
+
if (!result.success) return `Voice cloning failed: ${result.error}`;
|
|
465802
|
+
return JSON.stringify({ success: true, voiceId: result.voiceId, name: result.name });
|
|
465404
465803
|
}
|
|
465405
|
-
|
|
465406
|
-
|
|
465407
|
-
|
|
465408
|
-
|
|
465409
|
-
|
|
465410
|
-
|
|
465411
|
-
|
|
465412
|
-
|
|
465413
|
-
|
|
465414
|
-
}
|
|
465415
|
-
|
|
465416
|
-
|
|
465417
|
-
|
|
465418
|
-
|
|
465419
|
-
|
|
465420
|
-
|
|
465421
|
-
|
|
465422
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
465423
|
-
}
|
|
465424
|
-
|
|
465425
|
-
// ../cli/src/commands/_shared/scene-audio-mux.ts
|
|
465426
|
-
init_exec_safe();
|
|
465427
|
-
import { rename as rename6, unlink as unlink5 } from "node:fs/promises";
|
|
465428
|
-
import { resolve as resolve39, dirname as dirname24, extname as extname12, basename as basename16 } from "node:path";
|
|
465429
|
-
function buildAudioMuxFilter(audios) {
|
|
465430
|
-
if (audios.length === 0) return null;
|
|
465431
|
-
const labels = [];
|
|
465432
|
-
const stages = [];
|
|
465433
|
-
audios.forEach((a, i) => {
|
|
465434
|
-
const inputIdx = i + 1;
|
|
465435
|
-
const delayMs = Math.max(0, Math.round(a.absoluteStart * 1e3));
|
|
465436
|
-
const volume = Number.isFinite(a.volume) ? a.volume : 1;
|
|
465437
|
-
const trimSec = Math.max(0, a.clipDurationCap);
|
|
465438
|
-
const label = `a${i}`;
|
|
465439
|
-
const stage = [
|
|
465440
|
-
`[${inputIdx}:a]`,
|
|
465441
|
-
`atrim=duration=${trimSec.toFixed(3)},`,
|
|
465442
|
-
`asetpts=PTS-STARTPTS,`,
|
|
465443
|
-
`adelay=${delayMs}:all=1,`,
|
|
465444
|
-
`volume=${volume}`,
|
|
465445
|
-
`[${label}]`
|
|
465446
|
-
].join("");
|
|
465447
|
-
stages.push(stage);
|
|
465448
|
-
labels.push(`[${label}]`);
|
|
465449
|
-
});
|
|
465450
|
-
if (audios.length === 1) {
|
|
465451
|
-
return {
|
|
465452
|
-
filterComplex: stages.join(";"),
|
|
465453
|
-
outLabel: labels[0],
|
|
465454
|
-
inputCount: 1
|
|
465455
|
-
};
|
|
465456
|
-
}
|
|
465457
|
-
const mix = `${labels.join("")}amix=inputs=${audios.length}:dropout_transition=0:normalize=0[mixed]`;
|
|
465458
|
-
return {
|
|
465459
|
-
filterComplex: `${stages.join(";")};${mix}`,
|
|
465460
|
-
outLabel: "[mixed]",
|
|
465461
|
-
inputCount: audios.length
|
|
465462
|
-
};
|
|
465463
|
-
}
|
|
465464
|
-
function audioCodecForFormat(format4) {
|
|
465465
|
-
if (format4 === "webm") return "libopus";
|
|
465466
|
-
if (format4 === "mov") return "pcm_s16le";
|
|
465467
|
-
return "aac";
|
|
465468
|
-
}
|
|
465469
|
-
async function muxAudioIntoVideo(opts) {
|
|
465470
|
-
if (opts.audios.length === 0) {
|
|
465471
|
-
return { success: true, outputPath: opts.videoPath, audioCount: 0 };
|
|
465472
|
-
}
|
|
465473
|
-
if (!commandExists("ffmpeg")) {
|
|
465474
|
-
return {
|
|
465475
|
-
success: false,
|
|
465476
|
-
outputPath: opts.videoPath,
|
|
465477
|
-
audioCount: opts.audios.length,
|
|
465478
|
-
error: "ffmpeg not found in PATH \u2014 install via `brew install ffmpeg` (mac) or your package manager"
|
|
465479
|
-
};
|
|
465480
|
-
}
|
|
465481
|
-
const filter4 = buildAudioMuxFilter(opts.audios);
|
|
465482
|
-
if (!filter4) {
|
|
465483
|
-
return { success: true, outputPath: opts.videoPath, audioCount: 0 };
|
|
465484
|
-
}
|
|
465485
|
-
const ext = extname12(opts.videoPath) || `.${opts.format}`;
|
|
465486
|
-
const tmpPath = resolve39(
|
|
465487
|
-
dirname24(opts.videoPath),
|
|
465488
|
-
`.${basename16(opts.videoPath, ext)}.muxing${ext}`
|
|
465489
|
-
);
|
|
465490
|
-
const args = ["-y", "-loglevel", "error", "-i", opts.videoPath];
|
|
465491
|
-
for (const a of opts.audios) {
|
|
465492
|
-
args.push("-i", a.srcAbs);
|
|
465493
|
-
}
|
|
465494
|
-
args.push(
|
|
465495
|
-
"-filter_complex",
|
|
465496
|
-
filter4.filterComplex,
|
|
465497
|
-
"-map",
|
|
465498
|
-
"0:v",
|
|
465499
|
-
"-map",
|
|
465500
|
-
filter4.outLabel,
|
|
465501
|
-
"-c:v",
|
|
465502
|
-
"copy",
|
|
465503
|
-
"-c:a",
|
|
465504
|
-
audioCodecForFormat(opts.format),
|
|
465505
|
-
// Cap on the video duration so audio that overruns the producer's render
|
|
465506
|
-
// (e.g. a long Kokoro wav on a short scene) doesn't extend the output.
|
|
465507
|
-
// Video drives the timeline because the producer already counted frames.
|
|
465508
|
-
"-t",
|
|
465509
|
-
opts.totalDuration && opts.totalDuration > 0 ? opts.totalDuration.toFixed(3) : opts.videoDuration?.toFixed(3) ?? ""
|
|
465510
|
-
);
|
|
465511
|
-
if (args[args.length - 1] === "") {
|
|
465512
|
-
args.pop();
|
|
465513
|
-
args.pop();
|
|
465514
|
-
}
|
|
465515
|
-
args.push("-movflags", "+faststart", tmpPath);
|
|
465516
|
-
try {
|
|
465517
|
-
const { stderr } = await execSafe("ffmpeg", args);
|
|
465518
|
-
if (stderr && opts.onProgress) {
|
|
465519
|
-
stderr.split(/\r?\n/).forEach((line) => opts.onProgress?.(line));
|
|
465804
|
+
case "audio_dub": {
|
|
465805
|
+
const result = await executeDub({
|
|
465806
|
+
mediaPath: args.mediaPath,
|
|
465807
|
+
language: args.language,
|
|
465808
|
+
source: args.source,
|
|
465809
|
+
voice: args.voice,
|
|
465810
|
+
analyzeOnly: args.analyzeOnly,
|
|
465811
|
+
output: args.output
|
|
465812
|
+
});
|
|
465813
|
+
if (!result.success) return `Dubbing failed: ${result.error}`;
|
|
465814
|
+
return JSON.stringify({
|
|
465815
|
+
success: true,
|
|
465816
|
+
outputPath: result.outputPath,
|
|
465817
|
+
sourceLanguage: result.sourceLanguage,
|
|
465818
|
+
targetLanguage: result.targetLanguage,
|
|
465819
|
+
segmentCount: result.segmentCount
|
|
465820
|
+
});
|
|
465520
465821
|
}
|
|
465521
|
-
|
|
465522
|
-
|
|
465523
|
-
|
|
465524
|
-
|
|
465525
|
-
|
|
465822
|
+
case "audio_duck": {
|
|
465823
|
+
const result = await executeDuck({
|
|
465824
|
+
musicPath: args.musicPath,
|
|
465825
|
+
voicePath: args.voicePath,
|
|
465826
|
+
output: args.output,
|
|
465827
|
+
threshold: args.threshold,
|
|
465828
|
+
ratio: args.ratio
|
|
465829
|
+
});
|
|
465830
|
+
if (!result.success) return `Audio ducking failed: ${result.error}`;
|
|
465831
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath });
|
|
465526
465832
|
}
|
|
465527
|
-
|
|
465528
|
-
|
|
465529
|
-
outputPath: opts.videoPath,
|
|
465530
|
-
audioCount: opts.audios.length,
|
|
465531
|
-
error: `ffmpeg mux failed: ${msg}`
|
|
465532
|
-
};
|
|
465833
|
+
default:
|
|
465834
|
+
throw new Error(`Unknown AI audio tool: ${name}`);
|
|
465533
465835
|
}
|
|
465534
|
-
await rename6(tmpPath, opts.videoPath);
|
|
465535
|
-
return {
|
|
465536
|
-
success: true,
|
|
465537
|
-
outputPath: opts.videoPath,
|
|
465538
|
-
audioCount: opts.audios.length
|
|
465539
|
-
};
|
|
465540
465836
|
}
|
|
465541
465837
|
|
|
465542
|
-
//
|
|
465543
|
-
|
|
465544
|
-
|
|
465545
|
-
|
|
465546
|
-
|
|
465547
|
-
|
|
465548
|
-
|
|
465549
|
-
|
|
465550
|
-
|
|
465551
|
-
|
|
465552
|
-
}
|
|
465553
|
-
|
|
465554
|
-
|
|
465555
|
-
|
|
465556
|
-
|
|
465557
|
-
|
|
465558
|
-
|
|
465559
|
-
|
|
465560
|
-
|
|
465561
|
-
|
|
465562
|
-
|
|
465563
|
-
}
|
|
465564
|
-
|
|
465565
|
-
|
|
465566
|
-
|
|
465567
|
-
|
|
465568
|
-
|
|
465569
|
-
|
|
465570
|
-
|
|
465571
|
-
|
|
465572
|
-
|
|
465573
|
-
|
|
465574
|
-
|
|
465575
|
-
|
|
465576
|
-
|
|
465577
|
-
|
|
465578
|
-
|
|
465579
|
-
|
|
465580
|
-
|
|
465581
|
-
|
|
465582
|
-
|
|
465583
|
-
|
|
465584
|
-
|
|
465585
|
-
|
|
465586
|
-
|
|
465587
|
-
|
|
465588
|
-
|
|
465589
|
-
|
|
465590
|
-
|
|
465591
|
-
|
|
465592
|
-
|
|
465593
|
-
|
|
465594
|
-
|
|
465595
|
-
|
|
465596
|
-
|
|
465597
|
-
|
|
465598
|
-
|
|
465599
|
-
|
|
465600
|
-
|
|
465601
|
-
|
|
465602
|
-
|
|
465603
|
-
|
|
465604
|
-
|
|
465605
|
-
|
|
465606
|
-
|
|
465607
|
-
|
|
465608
|
-
|
|
465609
|
-
|
|
465610
|
-
|
|
465611
|
-
|
|
465612
|
-
|
|
465613
|
-
|
|
465614
|
-
|
|
465615
|
-
|
|
465616
|
-
|
|
465617
|
-
|
|
465618
|
-
let audioCount = 0;
|
|
465619
|
-
let audioMuxApplied = false;
|
|
465620
|
-
let audioMuxWarning;
|
|
465621
|
-
try {
|
|
465622
|
-
opts.onProgress?.(0.95, "Mixing audio");
|
|
465623
|
-
const rootHtml = await readFile24(resolve40(projectDir, root2), "utf-8");
|
|
465624
|
-
const audios = await scanSceneAudio({ projectDir, rootHtml });
|
|
465625
|
-
audioCount = audios.length;
|
|
465626
|
-
if (audios.length > 0) {
|
|
465627
|
-
const videoDuration = job.totalFrames && config4.fps ? job.totalFrames / config4.fps : void 0;
|
|
465628
|
-
const mux = await muxAudioIntoVideo({
|
|
465629
|
-
videoPath: outputPath,
|
|
465630
|
-
audios,
|
|
465631
|
-
format: config4.format ?? "mp4",
|
|
465632
|
-
videoDuration,
|
|
465633
|
-
onProgress: (line) => {
|
|
465634
|
-
if (line) opts.onProgress?.(0.97, line);
|
|
465838
|
+
// src/tools/ai-edit-advanced.ts
|
|
465839
|
+
init_edit_cmd();
|
|
465840
|
+
var aiEditAdvancedTools = [
|
|
465841
|
+
{
|
|
465842
|
+
name: "edit_grade",
|
|
465843
|
+
description: "Apply AI-generated color grading using Claude + FFmpeg. Use preset for free built-in grades, or style for custom AI-generated grades (needs ANTHROPIC_API_KEY).",
|
|
465844
|
+
inputSchema: {
|
|
465845
|
+
type: "object",
|
|
465846
|
+
properties: {
|
|
465847
|
+
videoPath: { type: "string", description: "Input video file path" },
|
|
465848
|
+
style: { type: "string", description: "Custom style description (e.g., 'cinematic warm sunset')" },
|
|
465849
|
+
preset: {
|
|
465850
|
+
type: "string",
|
|
465851
|
+
enum: ["film-noir", "vintage", "cinematic-warm", "cool-tones", "high-contrast", "pastel", "cyberpunk", "horror"],
|
|
465852
|
+
description: "Built-in preset (no API key needed)"
|
|
465853
|
+
},
|
|
465854
|
+
output: { type: "string", description: "Output video file path" },
|
|
465855
|
+
analyzeOnly: { type: "boolean", description: "Show FFmpeg filter without applying" }
|
|
465856
|
+
},
|
|
465857
|
+
required: ["videoPath"]
|
|
465858
|
+
}
|
|
465859
|
+
},
|
|
465860
|
+
{
|
|
465861
|
+
name: "edit_speed_ramp",
|
|
465862
|
+
description: "Apply content-aware speed ramping. Analyzes speech with Whisper, plans speed changes with Claude, applies with FFmpeg. Requires OPENAI_API_KEY + ANTHROPIC_API_KEY.",
|
|
465863
|
+
inputSchema: {
|
|
465864
|
+
type: "object",
|
|
465865
|
+
properties: {
|
|
465866
|
+
videoPath: { type: "string", description: "Input video file path (must have audio)" },
|
|
465867
|
+
output: { type: "string", description: "Output video file path" },
|
|
465868
|
+
style: {
|
|
465869
|
+
type: "string",
|
|
465870
|
+
enum: ["dramatic", "smooth", "action"],
|
|
465871
|
+
description: "Speed ramp style (default: dramatic)"
|
|
465872
|
+
},
|
|
465873
|
+
minSpeed: { type: "number", description: "Minimum speed factor (default: 0.25)" },
|
|
465874
|
+
maxSpeed: { type: "number", description: "Maximum speed factor (default: 4.0)" },
|
|
465875
|
+
analyzeOnly: { type: "boolean", description: "Show keyframes without applying" },
|
|
465876
|
+
language: { type: "string", description: "Language code for transcription" }
|
|
465877
|
+
},
|
|
465878
|
+
required: ["videoPath"]
|
|
465879
|
+
}
|
|
465880
|
+
},
|
|
465881
|
+
{
|
|
465882
|
+
name: "edit_reframe",
|
|
465883
|
+
description: "Auto-reframe video to a different aspect ratio using smart cropping. Free (FFmpeg only).",
|
|
465884
|
+
inputSchema: {
|
|
465885
|
+
type: "object",
|
|
465886
|
+
properties: {
|
|
465887
|
+
videoPath: { type: "string", description: "Input video file path" },
|
|
465888
|
+
aspect: { type: "string", description: "Target aspect ratio: 9:16, 1:1, 4:5 (default: 9:16)" },
|
|
465889
|
+
focus: {
|
|
465890
|
+
type: "string",
|
|
465891
|
+
enum: ["auto", "face", "center", "action"],
|
|
465892
|
+
description: "Focus mode (default: auto)"
|
|
465893
|
+
},
|
|
465894
|
+
output: { type: "string", description: "Output video file path" },
|
|
465895
|
+
analyzeOnly: { type: "boolean", description: "Show crop region without applying" }
|
|
465896
|
+
},
|
|
465897
|
+
required: ["videoPath"]
|
|
465898
|
+
}
|
|
465899
|
+
},
|
|
465900
|
+
{
|
|
465901
|
+
name: "edit_interpolate",
|
|
465902
|
+
description: "Create slow motion with AI frame interpolation using FFmpeg minterpolate. Free, no API key needed.",
|
|
465903
|
+
inputSchema: {
|
|
465904
|
+
type: "object",
|
|
465905
|
+
properties: {
|
|
465906
|
+
videoPath: { type: "string", description: "Input video file path" },
|
|
465907
|
+
output: { type: "string", description: "Output video file path" },
|
|
465908
|
+
factor: { type: "number", description: "Slow motion factor: 2, 4, or 8 (default: 2)" },
|
|
465909
|
+
fps: { type: "number", description: "Target output FPS (default: auto)" },
|
|
465910
|
+
quality: {
|
|
465911
|
+
type: "string",
|
|
465912
|
+
enum: ["fast", "quality"],
|
|
465913
|
+
description: "Interpolation quality (default: quality)"
|
|
465635
465914
|
}
|
|
465636
|
-
}
|
|
465637
|
-
|
|
465638
|
-
|
|
465639
|
-
|
|
465640
|
-
|
|
465641
|
-
|
|
465915
|
+
},
|
|
465916
|
+
required: ["videoPath"]
|
|
465917
|
+
}
|
|
465918
|
+
},
|
|
465919
|
+
{
|
|
465920
|
+
name: "edit_upscale",
|
|
465921
|
+
description: "Upscale video resolution using FFmpeg (Lanczos scaling). Free, no API key needed.",
|
|
465922
|
+
inputSchema: {
|
|
465923
|
+
type: "object",
|
|
465924
|
+
properties: {
|
|
465925
|
+
videoPath: { type: "string", description: "Input video file path" },
|
|
465926
|
+
output: { type: "string", description: "Output video file path" },
|
|
465927
|
+
scale: { type: "number", description: "Scale factor: 2 or 4 (default: 2)" },
|
|
465928
|
+
quality: {
|
|
465929
|
+
type: "string",
|
|
465930
|
+
enum: ["fast", "quality"],
|
|
465931
|
+
description: "Scaling quality (default: quality, uses Lanczos)"
|
|
465932
|
+
}
|
|
465933
|
+
},
|
|
465934
|
+
required: ["videoPath"]
|
|
465642
465935
|
}
|
|
465643
|
-
} catch (err) {
|
|
465644
|
-
audioMuxWarning = err instanceof Error ? err.message : String(err);
|
|
465645
465936
|
}
|
|
465646
|
-
|
|
465647
|
-
|
|
465648
|
-
|
|
465649
|
-
|
|
465650
|
-
|
|
465651
|
-
|
|
465652
|
-
|
|
465653
|
-
|
|
465654
|
-
|
|
465655
|
-
|
|
465656
|
-
|
|
465657
|
-
|
|
465658
|
-
|
|
465659
|
-
}
|
|
465660
|
-
|
|
465661
|
-
|
|
465662
|
-
|
|
465663
|
-
|
|
465664
|
-
|
|
465937
|
+
];
|
|
465938
|
+
async function handleAiEditAdvancedToolCall(name, args) {
|
|
465939
|
+
switch (name) {
|
|
465940
|
+
case "edit_grade": {
|
|
465941
|
+
const result = await executeGrade({
|
|
465942
|
+
videoPath: args.videoPath,
|
|
465943
|
+
style: args.style,
|
|
465944
|
+
preset: args.preset,
|
|
465945
|
+
output: args.output,
|
|
465946
|
+
analyzeOnly: args.analyzeOnly
|
|
465947
|
+
});
|
|
465948
|
+
if (!result.success) return `Color grading failed: ${result.error}`;
|
|
465949
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath, style: result.style, description: result.description, ffmpegFilter: result.ffmpegFilter });
|
|
465950
|
+
}
|
|
465951
|
+
case "edit_speed_ramp": {
|
|
465952
|
+
const result = await executeSpeedRamp({
|
|
465953
|
+
videoPath: args.videoPath,
|
|
465954
|
+
output: args.output,
|
|
465955
|
+
style: args.style,
|
|
465956
|
+
minSpeed: args.minSpeed,
|
|
465957
|
+
maxSpeed: args.maxSpeed,
|
|
465958
|
+
analyzeOnly: args.analyzeOnly,
|
|
465959
|
+
language: args.language
|
|
465960
|
+
});
|
|
465961
|
+
if (!result.success) return `Speed ramping failed: ${result.error}`;
|
|
465962
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath, keyframeCount: result.keyframes?.length, avgSpeed: result.avgSpeed });
|
|
465963
|
+
}
|
|
465964
|
+
case "edit_reframe": {
|
|
465965
|
+
const result = await executeReframe({
|
|
465966
|
+
videoPath: args.videoPath,
|
|
465967
|
+
aspect: args.aspect,
|
|
465968
|
+
focus: args.focus,
|
|
465969
|
+
output: args.output,
|
|
465970
|
+
analyzeOnly: args.analyzeOnly
|
|
465971
|
+
});
|
|
465972
|
+
if (!result.success) return `Reframe failed: ${result.error}`;
|
|
465973
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath, sourceAspect: result.sourceAspect, targetAspect: result.targetAspect });
|
|
465974
|
+
}
|
|
465975
|
+
case "edit_interpolate": {
|
|
465976
|
+
const result = await executeInterpolate({
|
|
465977
|
+
videoPath: args.videoPath,
|
|
465978
|
+
output: args.output,
|
|
465979
|
+
factor: args.factor,
|
|
465980
|
+
fps: args.fps,
|
|
465981
|
+
quality: args.quality
|
|
465982
|
+
});
|
|
465983
|
+
if (!result.success) return `Interpolation failed: ${result.error}`;
|
|
465984
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath, originalFps: result.originalFps, targetFps: result.targetFps, factor: result.factor });
|
|
465985
|
+
}
|
|
465986
|
+
case "edit_upscale": {
|
|
465987
|
+
const result = await executeUpscale({
|
|
465988
|
+
videoPath: args.videoPath,
|
|
465989
|
+
output: args.output,
|
|
465990
|
+
scale: args.scale,
|
|
465991
|
+
quality: args.quality
|
|
465992
|
+
});
|
|
465993
|
+
if (!result.success) return `Upscale failed: ${result.error}`;
|
|
465994
|
+
return JSON.stringify({ success: true, outputPath: result.outputPath, originalRes: result.originalRes, targetRes: result.targetRes });
|
|
465995
|
+
}
|
|
465996
|
+
default:
|
|
465997
|
+
throw new Error(`Unknown advanced edit tool: ${name}`);
|
|
465665
465998
|
}
|
|
465666
465999
|
}
|
|
465667
466000
|
|
|
466001
|
+
// src/tools/scene.ts
|
|
466002
|
+
init_scene_project();
|
|
466003
|
+
init_scene_lint();
|
|
466004
|
+
init_scene_render();
|
|
466005
|
+
import { resolve as resolve44 } from "node:path";
|
|
466006
|
+
|
|
465668
466007
|
// ../cli/src/commands/scene.ts
|
|
465669
466008
|
init_esm();
|
|
465670
466009
|
init_source();
|
|
465671
466010
|
init_ora();
|
|
465672
466011
|
var import_yaml7 = __toESM(require_dist16(), 1);
|
|
465673
466012
|
init_dist2();
|
|
466013
|
+
init_tts_resolve();
|
|
466014
|
+
init_scene_project();
|
|
465674
466015
|
import { basename as basename18, resolve as resolve43, relative as relative7, dirname as dirname27 } from "node:path";
|
|
465675
466016
|
import { mkdir as mkdir21, readFile as readFile26, writeFile as writeFile27, access as access5, copyFile as copyFile4 } from "node:fs/promises";
|
|
465676
466017
|
import { existsSync as existsSync40 } from "node:fs";
|
|
465677
466018
|
|
|
465678
|
-
// ../cli/src/commands/_shared/tts-resolve.ts
|
|
465679
|
-
init_dist2();
|
|
465680
|
-
init_api_key();
|
|
465681
|
-
init_api_key();
|
|
465682
|
-
async function resolveTtsProvider(preferred = "auto") {
|
|
465683
|
-
const choice = preferred === "auto" ? hasApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
|
|
465684
|
-
if (choice === "elevenlabs") {
|
|
465685
|
-
return buildElevenLabs();
|
|
465686
|
-
}
|
|
465687
|
-
return buildKokoro();
|
|
465688
|
-
}
|
|
465689
|
-
async function buildElevenLabs() {
|
|
465690
|
-
const key2 = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs");
|
|
465691
|
-
if (!key2) {
|
|
465692
|
-
throw new TtsKeyMissingError("elevenlabs");
|
|
465693
|
-
}
|
|
465694
|
-
const provider = new ElevenLabsProvider();
|
|
465695
|
-
await provider.initialize({ apiKey: key2 });
|
|
465696
|
-
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
465697
|
-
voiceId: opts?.voice,
|
|
465698
|
-
speed: opts?.speed
|
|
465699
|
-
});
|
|
465700
|
-
return { provider: "elevenlabs", audioExtension: "mp3", call };
|
|
465701
|
-
}
|
|
465702
|
-
async function buildKokoro() {
|
|
465703
|
-
const provider = new KokoroProvider();
|
|
465704
|
-
await provider.initialize({});
|
|
465705
|
-
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
465706
|
-
voice: opts?.voice,
|
|
465707
|
-
speed: opts?.speed,
|
|
465708
|
-
onProgress: opts?.onProgress
|
|
465709
|
-
});
|
|
465710
|
-
return { provider: "kokoro", audioExtension: "wav", call };
|
|
465711
|
-
}
|
|
465712
|
-
var TtsKeyMissingError = class extends Error {
|
|
465713
|
-
constructor(provider) {
|
|
465714
|
-
super(
|
|
465715
|
-
provider === "elevenlabs" ? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis." : `Provider ${provider} is unavailable.`
|
|
465716
|
-
);
|
|
465717
|
-
this.provider = provider;
|
|
465718
|
-
this.name = "TtsKeyMissingError";
|
|
465719
|
-
}
|
|
465720
|
-
};
|
|
465721
|
-
function parseTtsProviderName(value) {
|
|
465722
|
-
if (!value) return "auto";
|
|
465723
|
-
if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
|
|
465724
|
-
return value;
|
|
465725
|
-
}
|
|
465726
|
-
throw new Error(
|
|
465727
|
-
`Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`
|
|
465728
|
-
);
|
|
465729
|
-
}
|
|
465730
|
-
|
|
465731
|
-
// ../cli/src/commands/scene.ts
|
|
465732
|
-
init_scene_project();
|
|
465733
|
-
|
|
465734
466019
|
// ../cli/src/commands/_shared/visual-styles.ts
|
|
465735
466020
|
var STYLES = [
|
|
465736
466021
|
{
|
|
@@ -465902,203 +466187,8 @@ function visualStyleNames() {
|
|
|
465902
466187
|
// ../cli/src/commands/scene.ts
|
|
465903
466188
|
init_scene_html_emit();
|
|
465904
466189
|
init_scene_lint();
|
|
465905
|
-
|
|
465906
|
-
|
|
465907
|
-
init_dist2();
|
|
465908
|
-
init_compose_scenes_skills();
|
|
465909
|
-
import { existsSync as existsSync39 } from "node:fs";
|
|
465910
|
-
import { mkdir as mkdir20, readFile as readFile25, writeFile as writeFile26 } from "node:fs/promises";
|
|
465911
|
-
import { dirname as dirname26, join as join25, resolve as resolve41 } from "node:path";
|
|
465912
|
-
init_storyboard_parse();
|
|
465913
|
-
async function executeSceneBuild(opts) {
|
|
465914
|
-
const startedAt = Date.now();
|
|
465915
|
-
const projectDir = resolve41(opts.projectDir);
|
|
465916
|
-
const onProgress = opts.onProgress ?? (() => {
|
|
465917
|
-
});
|
|
465918
|
-
const storyboardPath = join25(projectDir, "STORYBOARD.md");
|
|
465919
|
-
if (!existsSync39(storyboardPath)) {
|
|
465920
|
-
return failBeforePrimitives(`STORYBOARD.md not found at ${storyboardPath}`, startedAt);
|
|
465921
|
-
}
|
|
465922
|
-
const storyboardMd = await readFile25(storyboardPath, "utf-8");
|
|
465923
|
-
const parsed = parseStoryboard(storyboardMd);
|
|
465924
|
-
if (parsed.beats.length === 0) {
|
|
465925
|
-
return failBeforePrimitives(
|
|
465926
|
-
`STORYBOARD.md at ${storyboardPath} has no \`## Beat \u2026\` headings.`,
|
|
465927
|
-
startedAt
|
|
465928
|
-
);
|
|
465929
|
-
}
|
|
465930
|
-
const ttsProvider = opts.ttsProvider ?? parsed.frontmatter?.providers?.tts ?? "auto";
|
|
465931
|
-
const imageProvider = opts.imageProvider ?? parsed.frontmatter?.providers?.image ?? "openai";
|
|
465932
|
-
const voice = opts.voice ?? parsed.frontmatter?.voice;
|
|
465933
|
-
onProgress({ type: "phase-start", phase: "primitives" });
|
|
465934
|
-
const beatOutcomes = await Promise.all(
|
|
465935
|
-
parsed.beats.map((beat) => buildBeatPrimitives(beat, {
|
|
465936
|
-
projectDir,
|
|
465937
|
-
ttsProvider,
|
|
465938
|
-
voice,
|
|
465939
|
-
imageProvider,
|
|
465940
|
-
imageQuality: opts.imageQuality ?? "hd",
|
|
465941
|
-
imageSize: opts.imageSize ?? "1536x1024",
|
|
465942
|
-
skipNarration: opts.skipNarration ?? false,
|
|
465943
|
-
skipBackdrop: opts.skipBackdrop ?? false,
|
|
465944
|
-
force: opts.force ?? false,
|
|
465945
|
-
onProgress
|
|
465946
|
-
}))
|
|
465947
|
-
);
|
|
465948
|
-
onProgress({ type: "phase-start", phase: "compose" });
|
|
465949
|
-
const composeResult = await executeComposeScenesWithSkills(
|
|
465950
|
-
{
|
|
465951
|
-
project: ".",
|
|
465952
|
-
effort: opts.effort,
|
|
465953
|
-
cacheDir: opts.cacheDir,
|
|
465954
|
-
onProgress: (e) => onProgress(e)
|
|
465955
|
-
},
|
|
465956
|
-
projectDir
|
|
465957
|
-
);
|
|
465958
|
-
if (!composeResult.success) {
|
|
465959
|
-
return {
|
|
465960
|
-
success: false,
|
|
465961
|
-
error: `compose failed: ${composeResult.error ?? "unknown"}`,
|
|
465962
|
-
beats: beatOutcomes,
|
|
465963
|
-
composeData: composeResult.data,
|
|
465964
|
-
totalLatencyMs: Date.now() - startedAt
|
|
465965
|
-
};
|
|
465966
|
-
}
|
|
465967
|
-
let outputPath;
|
|
465968
|
-
let renderResult;
|
|
465969
|
-
if (!opts.skipRender) {
|
|
465970
|
-
onProgress({ type: "phase-start", phase: "render" });
|
|
465971
|
-
onProgress({ type: "render-start" });
|
|
465972
|
-
renderResult = await executeSceneRender({ projectDir });
|
|
465973
|
-
if (!renderResult.success) {
|
|
465974
|
-
return {
|
|
465975
|
-
success: false,
|
|
465976
|
-
error: `render failed: ${renderResult.error ?? "unknown"}`,
|
|
465977
|
-
beats: beatOutcomes,
|
|
465978
|
-
composeData: composeResult.data,
|
|
465979
|
-
renderResult,
|
|
465980
|
-
totalLatencyMs: Date.now() - startedAt
|
|
465981
|
-
};
|
|
465982
|
-
}
|
|
465983
|
-
outputPath = renderResult.outputPath;
|
|
465984
|
-
if (outputPath) onProgress({ type: "render-done", outputPath });
|
|
465985
|
-
}
|
|
465986
|
-
return {
|
|
465987
|
-
success: true,
|
|
465988
|
-
beats: beatOutcomes,
|
|
465989
|
-
outputPath,
|
|
465990
|
-
composeData: composeResult.data,
|
|
465991
|
-
renderResult,
|
|
465992
|
-
totalLatencyMs: Date.now() - startedAt
|
|
465993
|
-
};
|
|
465994
|
-
}
|
|
465995
|
-
async function buildBeatPrimitives(beat, ctx) {
|
|
465996
|
-
const [narration, backdrop] = await Promise.all([
|
|
465997
|
-
ctx.skipNarration ? skipped("narration", beat.id, "--skip-narration", ctx) : dispatchNarration(beat, ctx),
|
|
465998
|
-
ctx.skipBackdrop ? skipped("backdrop", beat.id, "--skip-backdrop", ctx) : dispatchBackdrop(beat, ctx)
|
|
465999
|
-
]);
|
|
466000
|
-
return {
|
|
466001
|
-
beatId: beat.id,
|
|
466002
|
-
narrationStatus: narration.status,
|
|
466003
|
-
narrationPath: narration.path,
|
|
466004
|
-
narrationError: narration.error,
|
|
466005
|
-
backdropStatus: backdrop.status,
|
|
466006
|
-
backdropPath: backdrop.path,
|
|
466007
|
-
backdropError: backdrop.error
|
|
466008
|
-
};
|
|
466009
|
-
}
|
|
466010
|
-
async function dispatchNarration(beat, ctx) {
|
|
466011
|
-
const text = beat.cues?.narration;
|
|
466012
|
-
if (!text) return { status: "no-cue" };
|
|
466013
|
-
for (const ext of ["mp3", "wav"]) {
|
|
466014
|
-
const rel2 = `assets/narration-${beat.id}.${ext}`;
|
|
466015
|
-
if (existsSync39(join25(ctx.projectDir, rel2)) && !ctx.force) {
|
|
466016
|
-
ctx.onProgress({ type: "narration-cached", beatId: beat.id, path: rel2 });
|
|
466017
|
-
return { status: "cached", path: rel2 };
|
|
466018
|
-
}
|
|
466019
|
-
}
|
|
466020
|
-
let resolution;
|
|
466021
|
-
try {
|
|
466022
|
-
resolution = await resolveTtsProvider(ctx.ttsProvider);
|
|
466023
|
-
} catch (err) {
|
|
466024
|
-
const error = err instanceof TtsKeyMissingError ? err.message : err.message;
|
|
466025
|
-
ctx.onProgress({ type: "narration-failed", beatId: beat.id, error });
|
|
466026
|
-
return { status: "failed", error };
|
|
466027
|
-
}
|
|
466028
|
-
const result = await resolution.call(text, { voice: ctx.voice });
|
|
466029
|
-
if (!result.success || !result.audioBuffer) {
|
|
466030
|
-
const error = result.error ?? "unknown TTS failure";
|
|
466031
|
-
ctx.onProgress({ type: "narration-failed", beatId: beat.id, error });
|
|
466032
|
-
return { status: "failed", error };
|
|
466033
|
-
}
|
|
466034
|
-
const rel = `assets/narration-${beat.id}.${resolution.audioExtension}`;
|
|
466035
|
-
const abs = join25(ctx.projectDir, rel);
|
|
466036
|
-
await mkdir20(dirname26(abs), { recursive: true });
|
|
466037
|
-
await writeFile26(abs, result.audioBuffer);
|
|
466038
|
-
ctx.onProgress({
|
|
466039
|
-
type: "narration-generated",
|
|
466040
|
-
beatId: beat.id,
|
|
466041
|
-
path: rel,
|
|
466042
|
-
provider: resolution.provider
|
|
466043
|
-
});
|
|
466044
|
-
return { status: "generated", path: rel };
|
|
466045
|
-
}
|
|
466046
|
-
async function dispatchBackdrop(beat, ctx) {
|
|
466047
|
-
const prompt3 = beat.cues?.backdrop;
|
|
466048
|
-
if (!prompt3) return { status: "no-cue" };
|
|
466049
|
-
if (ctx.imageProvider !== "openai") {
|
|
466050
|
-
const error = `image provider "${ctx.imageProvider}" not yet supported (use openai)`;
|
|
466051
|
-
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
466052
|
-
return { status: "failed", error };
|
|
466053
|
-
}
|
|
466054
|
-
const rel = `assets/backdrop-${beat.id}.png`;
|
|
466055
|
-
const abs = join25(ctx.projectDir, rel);
|
|
466056
|
-
if (existsSync39(abs) && !ctx.force) {
|
|
466057
|
-
ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
|
|
466058
|
-
return { status: "cached", path: rel };
|
|
466059
|
-
}
|
|
466060
|
-
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
466061
|
-
if (!apiKey) {
|
|
466062
|
-
const error = "OPENAI_API_KEY not set \u2014 cannot dispatch backdrop";
|
|
466063
|
-
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
466064
|
-
return { status: "failed", error };
|
|
466065
|
-
}
|
|
466066
|
-
const provider = new OpenAIImageProvider();
|
|
466067
|
-
await provider.initialize({ apiKey });
|
|
466068
|
-
const result = await provider.generateImage(prompt3, {
|
|
466069
|
-
model: "gpt-image-2",
|
|
466070
|
-
size: ctx.imageSize,
|
|
466071
|
-
quality: ctx.imageQuality
|
|
466072
|
-
});
|
|
466073
|
-
if (!result.success || !result.images?.[0]?.base64) {
|
|
466074
|
-
const error = result.error ?? "no image data returned";
|
|
466075
|
-
ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
|
|
466076
|
-
return { status: "failed", error };
|
|
466077
|
-
}
|
|
466078
|
-
await mkdir20(dirname26(abs), { recursive: true });
|
|
466079
|
-
await writeFile26(abs, Buffer.from(result.images[0].base64, "base64"));
|
|
466080
|
-
ctx.onProgress({
|
|
466081
|
-
type: "backdrop-generated",
|
|
466082
|
-
beatId: beat.id,
|
|
466083
|
-
path: rel,
|
|
466084
|
-
provider: "openai"
|
|
466085
|
-
});
|
|
466086
|
-
return { status: "generated", path: rel };
|
|
466087
|
-
}
|
|
466088
|
-
async function skipped(kind, beatId, reason, ctx) {
|
|
466089
|
-
ctx.onProgress({ type: `${kind}-skipped`, beatId, reason });
|
|
466090
|
-
return { status: "skipped" };
|
|
466091
|
-
}
|
|
466092
|
-
function failBeforePrimitives(error, startedAt) {
|
|
466093
|
-
return {
|
|
466094
|
-
success: false,
|
|
466095
|
-
error,
|
|
466096
|
-
beats: [],
|
|
466097
|
-
totalLatencyMs: Date.now() - startedAt
|
|
466098
|
-
};
|
|
466099
|
-
}
|
|
466100
|
-
|
|
466101
|
-
// ../cli/src/commands/scene.ts
|
|
466190
|
+
init_scene_render();
|
|
466191
|
+
init_scene_build();
|
|
466102
466192
|
init_output();
|
|
466103
466193
|
init_api_key();
|
|
466104
466194
|
init_audio();
|