@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.
Files changed (2) hide show
  1. package/dist/index.js +1121 -1031
  2. 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/index.ts
461343
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
461344
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
461345
- import {
461346
- CallToolRequestSchema,
461347
- ListToolsRequestSchema,
461348
- ListResourcesRequestSchema,
461349
- ReadResourceRequestSchema,
461350
- ListPromptsRequestSchema,
461351
- GetPromptRequestSchema
461352
- } from "@modelcontextprotocol/sdk/types.js";
461353
-
461354
- // src/tools/project.ts
461355
- init_engine();
461356
- import { readFile, writeFile } from "node:fs/promises";
461357
- import { resolve } from "node:path";
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
- async function handleProjectToolCall(name, args) {
461397
- switch (name) {
461398
- case "project_create": {
461399
- const projectName = args.name;
461400
- const outputPath = args.outputPath || `${projectName}.vibe.json`;
461401
- const project = new Project(projectName);
461402
- if (args.fps) {
461403
- project.setFrameRate(args.fps);
461404
- }
461405
- await saveProject(outputPath, project);
461406
- return `Created project "${projectName}" at ${outputPath}`;
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
- case "project_info": {
461409
- const project = await loadProject(args.projectPath);
461410
- const meta = project.getMeta();
461411
- const info = {
461412
- name: meta.name,
461413
- aspectRatio: meta.aspectRatio,
461414
- frameRate: meta.frameRate,
461415
- duration: meta.duration,
461416
- sources: project.getSources().length,
461417
- tracks: project.getTracks().length,
461418
- clips: project.getClips().length
461419
- };
461420
- return JSON.stringify(info, null, 2);
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/tools/timeline.ts
461428
- import { resolve as resolve2 } from "node:path";
461429
- var timelineTools = [
461430
- {
461431
- name: "timeline_add_source",
461432
- description: "Add a media source (video, audio, image) to the project",
461433
- inputSchema: {
461434
- type: "object",
461435
- properties: {
461436
- projectPath: { type: "string", description: "Path to the project file" },
461437
- mediaPath: { type: "string", description: "Path to the media file" },
461438
- name: { type: "string", description: "Optional name for the source" },
461439
- duration: { type: "number", description: "Duration of the media in seconds (default: 10)" }
461440
- },
461441
- required: ["projectPath", "mediaPath"]
461442
- }
461443
- },
461444
- {
461445
- name: "timeline_add_clip",
461446
- description: "Add a clip to the timeline from an existing source",
461447
- inputSchema: {
461448
- type: "object",
461449
- properties: {
461450
- projectPath: { type: "string", description: "Path to the project file" },
461451
- sourceId: { type: "string", description: "ID of the media source" },
461452
- trackId: { type: "string", description: "ID of the track to add clip to (optional, uses first video track)" },
461453
- startTime: { type: "number", description: "Start time on timeline in seconds (default: 0)" },
461454
- duration: { type: "number", description: "Clip duration in seconds (optional, uses source duration)" }
461455
- },
461456
- required: ["projectPath", "sourceId"]
461457
- }
461458
- },
461459
- {
461460
- name: "timeline_split_clip",
461461
- description: "Split a clip at a specific time",
461462
- inputSchema: {
461463
- type: "object",
461464
- properties: {
461465
- projectPath: { type: "string", description: "Path to the project file" },
461466
- clipId: { type: "string", description: "ID of the clip to split" },
461467
- splitTime: { type: "number", description: "Time to split at (relative to clip start) in seconds" }
461468
- },
461469
- required: ["projectPath", "clipId", "splitTime"]
461470
- }
461471
- },
461472
- {
461473
- name: "timeline_trim_clip",
461474
- description: "Trim a clip by adjusting its start or end",
461475
- inputSchema: {
461476
- type: "object",
461477
- properties: {
461478
- projectPath: { type: "string", description: "Path to the project file" },
461479
- clipId: { type: "string", description: "ID of the clip to trim" },
461480
- trimStart: { type: "number", description: "New source start offset in seconds" },
461481
- trimEnd: { type: "number", description: "New duration in seconds" }
461482
- },
461483
- required: ["projectPath", "clipId"]
461484
- }
461485
- },
461486
- {
461487
- name: "timeline_move_clip",
461488
- description: "Move a clip to a new position or track",
461489
- inputSchema: {
461490
- type: "object",
461491
- properties: {
461492
- projectPath: { type: "string", description: "Path to the project file" },
461493
- clipId: { type: "string", description: "ID of the clip to move" },
461494
- newStartTime: { type: "number", description: "New start time on timeline in seconds" },
461495
- newTrackId: { type: "string", description: "ID of the target track (optional)" }
461496
- },
461497
- required: ["projectPath", "clipId"]
461498
- }
461499
- },
461500
- {
461501
- name: "timeline_delete_clip",
461502
- description: "Delete a clip from the timeline",
461503
- inputSchema: {
461504
- type: "object",
461505
- properties: {
461506
- projectPath: { type: "string", description: "Path to the project file" },
461507
- clipId: { type: "string", description: "ID of the clip to delete" }
461508
- },
461509
- required: ["projectPath", "clipId"]
461510
- }
461511
- },
461512
- {
461513
- name: "timeline_duplicate_clip",
461514
- description: "Duplicate a clip",
461515
- inputSchema: {
461516
- type: "object",
461517
- properties: {
461518
- projectPath: { type: "string", description: "Path to the project file" },
461519
- clipId: { type: "string", description: "ID of the clip to duplicate" },
461520
- newStartTime: { type: "number", description: "Start time for the duplicated clip (optional, places after original)" }
461521
- },
461522
- required: ["projectPath", "clipId"]
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 writeFile24 } from "node:fs/promises";
463637
+ import { writeFile as writeFile25 } from "node:fs/promises";
463010
463638
  import { tmpdir as tmpdir5 } from "node:os";
463011
- import { join as join24 } from "node:path";
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 import_yaml5 = __toESM(require_dist16(), 1);
463456
- import { resolve as resolve36 } from "node:path";
463457
- import { readFile as readFile22, writeFile as writeFile23, mkdir as mkdir17 } from "node:fs/promises";
463458
- import { existsSync as existsSync36 } from "node:fs";
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 resolve36(outputDir, params.output);
464186
+ return resolve40(outputDir, params.output);
463559
464187
  }
463560
- return resolve36(outputDir, defaultName);
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 = resolve36(process.cwd(), filePath);
463703
- if (!existsSync36(absPath)) {
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 readFile22(absPath, "utf-8");
463707
- const manifest = (0, import_yaml5.parse)(content);
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 = resolve36(process.cwd(), options.outputDir || `${manifest.name}-output`);
463723
- await mkdir17(outputDir, { recursive: true });
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 = resolve36(outputDir, CHECKPOINT_FILE);
463729
- if (existsSync36(checkpointPath)) {
463730
- const checkpointContent = await readFile22(checkpointPath, "utf-8");
463731
- const checkpoint = (0, import_yaml5.parse)(checkpointContent);
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 writeFile23(
463849
- resolve36(outputDir, CHECKPOINT_FILE),
463850
- (0, import_yaml5.stringify)(checkpoint, { indent: 2 }),
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 = join24(tmpdir5(), `vibe-mcp-pipeline-${Date.now()}.yaml`);
464098
- await writeFile24(tempPath, pipelineYaml, "utf-8");
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 resolve37, dirname as dirname23, basename as basename15 } from "node:path";
464185
- import { writeFile as writeFile25, mkdir as mkdir18, rm as rm5 } from "node:fs/promises";
464186
- import { existsSync as existsSync37 } from "node:fs";
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 = resolve37(tmpdir6(), `vf-ac-${Date.now()}`);
464337
- await mkdir18(tmpAudioDir, { recursive: true });
464338
- const audioPath = resolve37(tmpAudioDir, "audio.wav");
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 = resolve37(process.cwd(), outputPath);
464366
- const outDir = dirname23(absOutputPath);
464367
- if (!existsSync37(outDir)) {
464368
- await mkdir18(outDir, { recursive: true });
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 = resolve37(tmpAudioDir, "captions.ass");
464377
- await writeFile25(assPath, assContent, "utf-8");
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: basename15(videoPath)
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: basename15(videoPath),
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
- // src/tools/scene.ts
465324
- init_scene_project();
465325
- init_scene_lint();
465326
- import { resolve as resolve44 } from "node:path";
465327
-
465328
- // ../cli/src/commands/_shared/scene-render.ts
465329
- var import_yaml6 = __toESM(require_dist16(), 1);
465330
- init_dist();
465331
- init_chrome();
465332
- init_scene_lint();
465333
- import { mkdir as mkdir19, readFile as readFile24, stat as stat3 } from "node:fs/promises";
465334
- import { existsSync as existsSync38 } from "node:fs";
465335
- import { resolve as resolve40, relative as relative6, dirname as dirname25, basename as basename17 } from "node:path";
465336
-
465337
- // ../cli/src/commands/_shared/scene-audio-scan.ts
465338
- import { readFile as readFile23 } from "node:fs/promises";
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
- clips.push({ compositionId, compositionSrc, start, duration, trackIndex });
465355
- }
465356
- return clips;
465357
- }
465358
- function parseSceneAudios(compositionHtml) {
465359
- const out = [];
465360
- const audioRegex = /<audio\b([^>]*)>/gi;
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
- async function scanSceneAudio(opts) {
465386
- const reader = opts.readComposition ?? makeFsCompositionReader(opts.projectDir);
465387
- const clips = parseRootClips(opts.rootHtml);
465388
- const out = [];
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
- out.sort((a, b) => a.absoluteStart - b.absoluteStart);
465407
- return out;
465408
- }
465409
- function pickAttr(tag, name) {
465410
- const re = new RegExp(`\\b${escapeRegex3(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`);
465411
- const m = tag.match(re);
465412
- if (!m) return null;
465413
- return m[2] ?? m[3] ?? null;
465414
- }
465415
- function pickNumberAttr(tag, name) {
465416
- const raw2 = pickAttr(tag, name);
465417
- if (raw2 === null) return null;
465418
- const n = Number(raw2);
465419
- return Number.isFinite(n) ? n : null;
465420
- }
465421
- function escapeRegex3(s) {
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
- } catch (err) {
465522
- const msg = err instanceof Error ? err.message : String(err);
465523
- try {
465524
- await unlink5(tmpPath);
465525
- } catch {
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
- return {
465528
- success: false,
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
- // ../cli/src/commands/_shared/scene-render.ts
465543
- function qualityToCrf2(quality = "standard") {
465544
- return quality === "draft" ? 28 : quality === "high" ? 18 : 23;
465545
- }
465546
- function defaultOutputPath(opts) {
465547
- const fmt = opts.format ?? "mp4";
465548
- const now = opts.now ?? /* @__PURE__ */ new Date();
465549
- const stamp = now.toISOString().replace(/[:T]/g, "-").replace(/\..+$/, "");
465550
- const name = (opts.projectName ?? basename17(resolve40(opts.projectDir))) || "scene";
465551
- return resolve40(opts.projectDir, "renders", `${name}-${stamp}.${fmt}`);
465552
- }
465553
- async function readProjectName(projectDir) {
465554
- const cfgPath = resolve40(projectDir, "vibe.project.yaml");
465555
- if (!existsSync38(cfgPath)) return void 0;
465556
- try {
465557
- const raw2 = await (await import("node:fs/promises")).readFile(cfgPath, "utf-8");
465558
- const parsed = (0, import_yaml6.parse)(raw2);
465559
- return parsed?.name ?? void 0;
465560
- } catch {
465561
- return void 0;
465562
- }
465563
- }
465564
- function buildRenderConfig(opts) {
465565
- const quality = opts.quality ?? "standard";
465566
- return {
465567
- fps: opts.fps ?? 30,
465568
- quality,
465569
- format: opts.format ?? "mp4",
465570
- entryFile: opts.entryFile ?? "index.html",
465571
- crf: qualityToCrf2(quality),
465572
- workers: opts.workers ?? 1
465573
- };
465574
- }
465575
- async function executeSceneRender(opts = {}) {
465576
- const projectDir = resolve40(opts.projectDir ?? ".");
465577
- const root2 = opts.root ?? "index.html";
465578
- const projectStat = await safeStat(projectDir);
465579
- if (!projectStat || !projectStat.isDirectory()) {
465580
- return { success: false, error: `Project directory not found: ${projectDir}` };
465581
- }
465582
- if (!await rootExists(projectDir, root2)) {
465583
- return {
465584
- success: false,
465585
- error: `Root composition not found: ${resolve40(projectDir, root2)}. Run \`vibe scene init\` first.`
465586
- };
465587
- }
465588
- const chrome2 = await preflightChrome();
465589
- if (!chrome2.ok) {
465590
- return { success: false, error: chrome2.reason };
465591
- }
465592
- const projectName = await readProjectName(projectDir);
465593
- const outputPath = opts.output ? resolve40(projectDir, opts.output) : defaultOutputPath({ projectDir, projectName, format: opts.format });
465594
- await mkdir19(dirname25(outputPath), { recursive: true });
465595
- const config4 = buildRenderConfig({
465596
- fps: opts.fps,
465597
- quality: opts.quality,
465598
- format: opts.format,
465599
- workers: opts.workers,
465600
- entryFile: root2
465601
- });
465602
- const job = createRenderJob(config4);
465603
- const start = Date.now();
465604
- try {
465605
- await executeRenderJob(
465606
- job,
465607
- projectDir,
465608
- outputPath,
465609
- (j, msg) => opts.onProgress?.(j.progress, j.currentStage ?? msg),
465610
- opts.signal
465611
- );
465612
- } catch (err) {
465613
- return {
465614
- success: false,
465615
- error: err instanceof Error ? err.message : String(err)
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
- if (mux.success) {
465638
- audioMuxApplied = true;
465639
- } else {
465640
- audioMuxWarning = mux.error;
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
- return {
465647
- success: true,
465648
- outputPath: relative6(process.cwd(), outputPath) || outputPath,
465649
- durationMs: Date.now() - start,
465650
- framesRendered: job.framesRendered,
465651
- totalFrames: job.totalFrames,
465652
- fps: config4.fps,
465653
- quality: config4.quality,
465654
- format: config4.format,
465655
- audioCount,
465656
- audioMuxApplied,
465657
- audioMuxWarning
465658
- };
465659
- }
465660
- async function safeStat(p) {
465661
- try {
465662
- return await stat3(p);
465663
- } catch {
465664
- return null;
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
- // ../cli/src/commands/_shared/scene-build.ts
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();