@agimon-ai/video-editor-mcp 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -5
- package/dist/cli.cjs +9 -4
- package/dist/cli.mjs +9 -4
- package/dist/index.cjs +1 -2
- package/dist/index.mjs +1 -2
- package/dist/stdio-CH0Bk0m7.cjs +17 -0
- package/dist/stdio-KMqWPqUm.mjs +17 -0
- package/package.json +12 -4
- package/schemas/main-composition.schema.json +2283 -0
- package/src/remotion/Root.tsx +87 -0
- package/src/remotion/compositions/Main.tsx +51 -0
- package/src/remotion/compositions/Square.tsx +18 -0
- package/src/remotion/compositions/Vertical.tsx +18 -0
- package/src/remotion/compositions/audio/AudioLayer.tsx +130 -0
- package/src/remotion/compositions/captions/CaptionOverlay.tsx +250 -0
- package/src/remotion/compositions/captions/index.ts +1 -0
- package/src/remotion/compositions/clips/AudioClipRenderer.tsx +66 -0
- package/src/remotion/compositions/clips/GifClipRenderer.tsx +71 -0
- package/src/remotion/compositions/clips/ImageClipRenderer.tsx +103 -0
- package/src/remotion/compositions/clips/LottieClipRenderer.tsx +57 -0
- package/src/remotion/compositions/clips/SubtitleClipRenderer.tsx +85 -0
- package/src/remotion/compositions/clips/TextClipRenderer.tsx +131 -0
- package/src/remotion/compositions/clips/VideoClipRenderer.tsx +94 -0
- package/src/remotion/compositions/clips/index.tsx +51 -0
- package/src/remotion/index.ts +10 -0
- package/src/remotion/utils/calculateMainMetadata.ts +76 -0
- package/src/schemas/clips.ts +202 -0
- package/src/schemas/compositions.ts +160 -0
- package/src/schemas/index.ts +19 -0
- package/src/utils/assetPaths.ts +64 -0
- package/src/utils/index.ts +9 -0
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/stdio-B6Hg5voC.mjs +0 -4
- package/dist/stdio-B6Hg5voC.mjs.map +0 -1
- package/dist/stdio-Dwc6SWvA.cjs +0 -4
- package/dist/stdio-Dwc6SWvA.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# video-editor
|
|
1
|
+
# @agimon-ai/video-editor-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for video editing with
|
|
3
|
+
MCP server for video editing with Remotion. Compose, render, and remix videos from JSON props.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -14,6 +14,18 @@ pnpm install
|
|
|
14
14
|
pnpm dev
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
The package also exposes a standalone CLI for direct video analysis:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm build
|
|
23
|
+
node dist/cli.cjs analyze-video \
|
|
24
|
+
--video "$HOME/Downloads/clip.webm" \
|
|
25
|
+
--prompt "What is happening in this video?" \
|
|
26
|
+
--pretty
|
|
27
|
+
```
|
|
28
|
+
|
|
17
29
|
## Build
|
|
18
30
|
|
|
19
31
|
```bash
|
|
@@ -33,10 +45,110 @@ Add to your Claude Code configuration:
|
|
|
33
45
|
```json
|
|
34
46
|
{
|
|
35
47
|
"mcpServers": {
|
|
36
|
-
"video-editor": {
|
|
48
|
+
"video-editor-mcp": {
|
|
37
49
|
"command": "npx",
|
|
38
|
-
"args": ["video-editor", "mcp-serve"]
|
|
50
|
+
"args": ["-y", "@agimon-ai/video-editor-mcp", "mcp-serve"]
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
|
-
```
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### With proxy support
|
|
57
|
+
|
|
58
|
+
Pass a proxy URL for video downloads (HTTP/HTTPS/SOCKS5). Useful for geo-restricted content like TikTok:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"video-editor-mcp": {
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "@agimon-ai/video-editor-mcp", "mcp-serve", "--proxy", "socks5://user:pass@host:port"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or via environment variable:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"video-editor-mcp": {
|
|
77
|
+
"command": "npx",
|
|
78
|
+
"args": ["-y", "@agimon-ai/video-editor-mcp", "mcp-serve"],
|
|
79
|
+
"env": {
|
|
80
|
+
"YTDLP_PROXY": "socks5://user:pass@host:port"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Available Tools
|
|
88
|
+
|
|
89
|
+
### Video Composition
|
|
90
|
+
|
|
91
|
+
| Tool | Description |
|
|
92
|
+
|------|-------------|
|
|
93
|
+
| `list_compositions` | List available compositions (Main 1920x1080, Vertical 1080x1920, Square 1080x1080) |
|
|
94
|
+
| `render_video` | Render a video from JSON props (h264/h265/vp8/vp9) |
|
|
95
|
+
| `preview_frame` | Render a single frame as PNG/JPEG for quick preview |
|
|
96
|
+
|
|
97
|
+
### Media
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| `get_media_info` | Get duration, dimensions, and decodability of video/audio files |
|
|
102
|
+
| `search_videos` | Search YouTube for videos by query, returns metadata without downloading |
|
|
103
|
+
| `download_clip` | Download a clip from YouTube, TikTok, Instagram, or any yt-dlp supported site |
|
|
104
|
+
| `analyze_video` | Analyze one or more local video files with a prompt using Gemini Code Assist |
|
|
105
|
+
| `extract_video_context` | Break a local video into transcript segments and paged filmstrip images |
|
|
106
|
+
|
|
107
|
+
### Captions & Audio
|
|
108
|
+
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `generate_captions` | Create TikTok-style caption pages from a caption array |
|
|
112
|
+
| `import_srt` | Parse SRT subtitle content into structured Caption format |
|
|
113
|
+
| `generate_voiceover` | Generate voiceover audio from text using Kokoro TTS |
|
|
114
|
+
| `generate_music` | Generate background music using a local ACE-Step 1.5 checkout |
|
|
115
|
+
|
|
116
|
+
## Workflow
|
|
117
|
+
|
|
118
|
+
1. **Search** — `search_videos` to find source material on YouTube
|
|
119
|
+
2. **Download** — `download_clip` to grab clips from YouTube, TikTok, Instagram, etc. (saves to Remotion `public/videos/` by default)
|
|
120
|
+
3. **Inspect** — `get_media_info` to check duration/dimensions
|
|
121
|
+
4. **Extract Context** — `extract_video_context` to create transcript + segment filmstrips for non-video models
|
|
122
|
+
5. **Compose** — Build clip JSON with text, video, audio, subtitle, image, gif, or lottie clips
|
|
123
|
+
6. **Preview** — `preview_frame` to check a specific frame before rendering
|
|
124
|
+
7. **Render** — `render_video` to produce the final video
|
|
125
|
+
|
|
126
|
+
## Clip Types
|
|
127
|
+
|
|
128
|
+
- **video** — Video file with volume, playback rate, trimming, looping
|
|
129
|
+
- **image** — Static image with fit modes (contain, cover, fill)
|
|
130
|
+
- **text** — Text overlay with animations (fade, slide, typewriter, highlight)
|
|
131
|
+
- **audio** — Audio track with volume, playback rate, trimming
|
|
132
|
+
- **subtitle** — Subtitle overlay with positioning and animation
|
|
133
|
+
- **gif** — Animated GIF synced to timeline
|
|
134
|
+
- **lottie** — Lottie animation
|
|
135
|
+
|
|
136
|
+
## Compositions
|
|
137
|
+
|
|
138
|
+
| ID | Resolution | Aspect Ratio | Use Case |
|
|
139
|
+
|----|-----------|--------------|----------|
|
|
140
|
+
| Main | 1920x1080 | 16:9 | YouTube, landscape |
|
|
141
|
+
| Vertical | 1080x1920 | 9:16 | TikTok, Instagram Stories, YouTube Shorts |
|
|
142
|
+
| Square | 1080x1080 | 1:1 | Instagram posts |
|
|
143
|
+
|
|
144
|
+
## Prerequisites
|
|
145
|
+
|
|
146
|
+
- **Node.js** >= 18
|
|
147
|
+
- **yt-dlp** — Required for `search_videos` and `download_clip` (`brew install yt-dlp`)
|
|
148
|
+
- **ffmpeg/ffprobe** — Required for `get_media_info` and video processing
|
|
149
|
+
- **Whisper** — Required for `extract_video_context` transcription (`whisper-cli`, `whisper.cpp`, or Python `whisper`)
|
|
150
|
+
- **ACE-Step 1.5** — Optional, required for `generate_music`. Clone `https://github.com/ACE-Step/ACE-Step-1.5`, run `uv sync`, and set `ACE_STEP_15_DIR` or pass `aceStepPath`. The tool defaults to the MLX backend for Apple Silicon and writes to `public/music/`.
|
|
151
|
+
|
|
152
|
+
For whisper.cpp, provide a model with `whisperModelPath` or `WHISPER_MODEL_PATH`; common Homebrew/user model paths are detected automatically.
|
|
153
|
+
|
|
154
|
+
`extract_video_context` writes a transcript-first manifest: `transcript.text` plus `transcript.segments[].filmstrips`. It samples each segment at 3 FPS, downscales each filmstrip tile to 250px wide, and annotates every tile with its absolute millisecond timestamp. Long transcript segments are split into multiple wrapped filmstrip images via the `filmstrips` array. Override `framesPerSecond`, `tileWidth`, `filmstripColumns`, or `filmstripRows` when different density or dimensions are needed.
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require(`./stdio-
|
|
3
|
-
`)),process.stderr.write(s.default.gray(`
|
|
4
|
-
`));let
|
|
5
|
-
|
|
2
|
+
const e=require(`./stdio-CH0Bk0m7.cjs`);let t=require(`node:child_process`),n=require(`node:fs`);n=e.M(n);let r=require(`node:path`);r=e.M(r);let i=require(`commander`),a=require(`@agimon-ai/foundation-port-registry`),o=require(`@agimon-ai/foundation-process-registry`),s=require(`chalk`);s=e.M(s);var c=`0.8.0`;const l=[`pnpm-workspace.yaml`,`nx.json`,`.git`],u=`video-editor-mcp-http`,d=`tool`;function f(e=process.cwd()){let t=r.default.resolve(e);for(;;){for(let e of l)if((0,n.existsSync)(r.default.join(t,e)))return t;let e=r.default.dirname(t);if(e===t)return process.cwd();t=e}}const p=new i.Command(`http-serve`).description(`Start Remotion Studio for video editing`).option(`-p, --port <port>`,`Port to run Remotion Studio on`,String(a.DEFAULT_PORT_RANGE.min)).action(async e=>{let n;try{let i=Number.parseInt(e.port,10),c=new a.PortRegistryService(process.env.PORT_REGISTRY_PATH),l=f(),p=i?{min:i,max:i}:a.DEFAULT_PORT_RANGE,m=await c.reservePort({repositoryPath:l,serviceName:u,serviceType:d,preferredPort:i||a.DEFAULT_PORT_RANGE.min,portRange:p,pid:process.pid,host:`127.0.0.1`,force:!0});if(!m.success||!m.record)throw Error(m.error||`Failed to reserve port ${i}`);let h=m.record.port;n=await(0,o.createProcessLease)({repositoryPath:l,serviceName:u,serviceType:d,pid:process.pid,port:h,host:`127.0.0.1`,command:process.argv[1],args:process.argv.slice(2)});let g=r.default.resolve(__dirname,`..`);console.error(s.default.green(`Starting Remotion Studio on port ${h}...`)),console.error(s.default.gray(` Working directory: ${g}`));let _=(0,t.spawn)(r.default.join(g,`node_modules`,`.bin`,`remotion`),[`studio`,`--port`,String(h)],{stdio:`inherit`,cwd:g});_.on(`error`,e=>{console.error(s.default.red(`Failed to start Remotion Studio: ${e.message}`)),process.exit(1)});let v=async e=>{_.kill(e),await n?.release(),await c.releasePort({repositoryPath:l,serviceName:u,serviceType:d,pid:process.pid}),process.exit(0)};process.on(`SIGINT`,()=>v(`SIGINT`)),process.on(`SIGTERM`,()=>v(`SIGTERM`))}catch(e){await n?.release(),console.error(`Error executing http-serve:`,e),process.exit(1)}}),m=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function h(e=process.cwd()){let t=r.default.resolve(e);for(;;){for(let e of m)if((0,n.existsSync)(r.default.join(t,e)))return t;let e=r.default.dirname(t);if(e===t)return process.cwd();t=e}}async function g(e,t){await e.start();let n=async n=>{console.error(`\nReceived ${n}, shutting down gracefully...`);try{await e.stop(),await t?.(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>n(`SIGINT`)),process.on(`SIGTERM`,()=>n(`SIGTERM`))}const _=new i.Command(`mcp-serve`).description(`Start MCP server with specified transport`).option(`-t, --type <type>`,`Transport type: stdio`,`stdio`).option(`--proxy <url>`,`Proxy URL for yt-dlp (HTTP/HTTPS/SOCKS5, e.g. socks5://user:pass@host:port)`).action(async t=>{let n;try{let r=t.type.toLowerCase(),i=h();if(r===`stdio`){let r=new e.t(e.n(e.r({proxyUrl:t.proxy})));n=await(0,o.createProcessLease)({repositoryPath:i,serviceName:`video-editor-mcp-stdio`,pid:process.pid,metadata:{transport:`stdio`}}),await g(r,async()=>{await n?.release()})}else console.error(`Unknown transport type: ${r}. Use: stdio`),process.exit(1)}catch(e){await n?.release(),console.error(`Failed to start MCP server:`,e),process.exit(1)}}),v=new i.Command(`analyze-video`).description(`Analyze one or more local video files with Gemini Code Assist`).requiredOption(`-v, --video <paths...>`,`Paths to local video files to analyze`).requiredOption(`-p, --prompt <text>`,`Prompt to send with the video(s)`).option(`-m, --model <id>`,`Gemini model to use`,`gemini-2.5-flash-lite`).option(`--pretty`,`Pretty-print the full JSON result`,!1).action(async t=>{try{if(!Array.isArray(t.video)||t.video.length===0)throw Error(`At least one video path is required.`);process.stderr.write(s.default.blue(`🎥 Starting video analysis...
|
|
3
|
+
`)),process.stderr.write(s.default.gray(` Videos: ${t.video.join(`, `)}\n`)),process.stderr.write(s.default.gray(` Prompt: ${t.prompt}\n`)),process.stderr.write(s.default.gray(` Model: ${t.model}\n`));let n=await e.r().get(e.h.VideoAnalysisService).analyzeVideos({videoPaths:t.video,prompt:t.prompt,model:t.model});if(process.stderr.write(s.default.green(`✓ Resolved project: ${n.resolvedProject}\n`)),process.stderr.write(s.default.green(`✓ Videos processed: ${n.videoCount}\n`)),t.pretty){process.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}if(n.responseText){process.stdout.write(`${n.responseText}\n`);return}process.stdout.write(`${JSON.stringify(n.rawResponse,null,2)}\n`)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(s.default.red(`Error analyzing video: ${t}\n`)),process.exit(1)}}),y=e=>Number(e),b=e=>Number.parseInt(e,10),x=()=>(0,n.existsSync)(r.default.resolve(__dirname,`../package.json`))?r.default.resolve(__dirname,`..`):r.default.resolve(__dirname,`../..`),S=e=>{if(e.output)return r.default.resolve(e.output);let t=r.default.resolve(e.outputDir??r.default.join(x(),`public/music`)),n=e.filename??`ace-step-music`;return r.default.join(t,`${n}.${e.audioFormat}`)},C=new i.Command(`generate-music`).description(`Generate background music using ACE-Step 1.5`).requiredOption(`-p, --prompt <text>`,`Music description for ACE-Step text-to-music generation`).option(`-f, --filename <name>`,`Output filename without extension (default: ace-step-music)`).option(`-o, --output <path>`,`Explicit output audio path. Overrides filename/output-dir.`).option(`--output-dir <path>`,`Custom output directory (default: Remotion public/music/)`).option(`--ace-step-path <path>`,`Path to local ACE-Step-1.5 checkout`).option(`--lyrics <text>`,`Lyrics to use. Defaults to [Instrumental] for background music.`).option(`--duration <seconds>`,`Target duration in seconds (default: 30)`,y).option(`--bpm <number>`,`Optional BPM metadata`,y).option(`--keyscale <text>`,`Optional key/scale metadata, e.g. C minor`).option(`--time-signature <text>`,`Optional time signature metadata, e.g. 4/4`).option(`--vocal-language <code>`,`Vocal language code, or unknown`).option(`--seed <number>`,`Optional fixed seed for reproducible generation`,b).option(`--inference-steps <number>`,`Diffusion inference steps (default: 8)`,b).option(`--audio-format <format>`,`Output audio format: wav, mp3, flac`,`wav`).option(`--backend <backend>`,`ACE-Step LM backend: mlx, pt, vllm`,`mlx`).option(`--config-path <name>`,`ACE-Step DiT model config (default: acestep-v15-turbo)`).option(`--lm-model-path <name>`,`ACE-Step LM model (default: acestep-5Hz-lm-0.6B)`).option(`--timeout-ms <number>`,`Generation timeout in milliseconds`,b).option(`--pretty`,`Pretty-print the JSON result`,!1).action(async t=>{try{if(![`wav`,`mp3`,`flac`].includes(t.audioFormat))throw Error(`Unsupported audio format: ${t.audioFormat}`);if(![`mlx`,`pt`,`vllm`].includes(t.backend))throw Error(`Unsupported backend: ${t.backend}`);let n=S(t);process.stderr.write(s.default.blue(`Starting ACE-Step music generation...
|
|
4
|
+
`)),process.stderr.write(s.default.gray(` Prompt: ${t.prompt}\n`)),process.stderr.write(s.default.gray(` Output: ${n}\n`)),process.stderr.write(s.default.gray(` Backend: ${t.backend}\n`));let i=e.r().get(e.h.AceStepMusicService);if(!await i.isAvailable(t.aceStepPath))throw Error(`ACE-Step 1.5 is not available. Clone https://github.com/ACE-Step/ACE-Step-1.5 and set ACE_STEP_15_DIR or pass --ace-step-path.`);let a=await i.generateMusic({prompt:t.prompt,outputPath:n,aceStepPath:t.aceStepPath,lyrics:t.lyrics,duration:t.duration,bpm:t.bpm,keyscale:t.keyscale,timeSignature:t.timeSignature,vocalLanguage:t.vocalLanguage,seed:t.seed,inferenceSteps:t.inferenceSteps,audioFormat:t.audioFormat,backend:t.backend,configPath:t.configPath,lmModelPath:t.lmModelPath,timeoutMs:t.timeoutMs}),o={...a,compositionSrc:r.default.dirname(n)===r.default.resolve(x(),`public/music`)?`/music/${r.default.basename(n)}`:void 0};process.stderr.write(s.default.green(`Music generated successfully: ${a.outputPath}\n`)),process.stdout.write(`${JSON.stringify(o,null,t.pretty?2:0)}\n`)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(s.default.red(`Error generating music: ${t}\n`)),process.exit(1)}}),w=new i.Command(`render`).description(`Render a video from JSON props file`).requiredOption(`-i, --input <path>`,`Path to JSON props file`).requiredOption(`-o, --output <path>`,`Output video file path`).option(`-c, --composition <id>`,`Composition ID`,`Main`).option(`--codec <codec>`,`Video codec (h264, h265, vp8, vp9)`,`h264`).option(`--dry-run`,`Validate props and assets without rendering`).action(async t=>{try{process.stderr.write(s.default.blue(t.dryRun?`🔎 Starting render dry run...
|
|
5
|
+
`:`🎬 Starting video render...
|
|
6
|
+
`)),process.stderr.write(s.default.gray(` Input: ${t.input}\n`)),process.stderr.write(s.default.gray(` Output: ${t.output}\n`)),process.stderr.write(s.default.gray(` Composition: ${t.composition}\n`)),process.stderr.write(s.default.gray(` Codec: ${t.codec}\n`)),process.stderr.write(s.default.gray(` Dry run: ${t.dryRun?`yes`:`no`}\n`)),n.default.existsSync(t.input)||(process.stderr.write(s.default.red(`Error: Input file not found: ${t.input}\n`)),process.exit(1));let r=n.default.readFileSync(t.input,`utf-8`),i=JSON.parse(r),a=e.r().get(e.h.RenderService);process.stderr.write(s.default.blue(t.dryRun?`🔎 Validating render input...
|
|
7
|
+
`:`📦 Bundling Remotion project...
|
|
8
|
+
`));let o=await a.render({compositionId:t.composition,inputProps:i,outputPath:t.output,codec:t.codec,dryRun:t.dryRun});o.dryRun?(process.stdout.write(`${JSON.stringify(o.validation,null,2)}\n`),process.stderr.write(s.default.green(`✓ Render dry run passed
|
|
9
|
+
`))):process.stderr.write(s.default.green(`✓ Video rendered successfully: ${o.outputPath}\n`))}catch(t){t instanceof e._&&(process.stdout.write(`${JSON.stringify({error:`render_validation_failed`,message:t.message,summary:t.validation.summary,issues:t.validation.errors,warnings:t.validation.warnings,suggestedFixes:t.validation.errors.filter(e=>e.fix).map(e=>({code:e.code,path:e.path,...e.fix}))},null,2)}\n`),process.stderr.write(s.default.red(`Render validation failed; see JSON output for actionable fixes.
|
|
10
|
+
`)),process.exit(1)),process.stderr.write(s.default.red(`Error rendering video: ${t}\n`)),process.exit(1)}});async function T(){let e=new i.Command;e.name(`video-editor-mcp`).description(`MCP server for video editing with Remotion`).version(c),e.addCommand(_),e.addCommand(p),e.addCommand(w),e.addCommand(v),e.addCommand(C),await e.parseAsync(process.argv)}T();
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
3
|
-
`)),process.stderr.write(
|
|
4
|
-
`));let
|
|
5
|
-
|
|
2
|
+
import{_ as e,h as t,n,r,t as i}from"./stdio-KMqWPqUm.mjs";import{spawn as a}from"node:child_process";import o,{existsSync as s}from"node:fs";import c from"node:path";import{Command as l}from"commander";import{DEFAULT_PORT_RANGE as u,PortRegistryService as d}from"@agimon-ai/foundation-port-registry";import{createProcessLease as f}from"@agimon-ai/foundation-process-registry";import p from"chalk";var m=`0.8.0`;const h=[`pnpm-workspace.yaml`,`nx.json`,`.git`],g=`video-editor-mcp-http`,_=`tool`;function v(e=process.cwd()){let t=c.resolve(e);for(;;){for(let e of h)if(s(c.join(t,e)))return t;let e=c.dirname(t);if(e===t)return process.cwd();t=e}}const y=new l(`http-serve`).description(`Start Remotion Studio for video editing`).option(`-p, --port <port>`,`Port to run Remotion Studio on`,String(u.min)).action(async e=>{let t;try{let n=Number.parseInt(e.port,10),r=new d(process.env.PORT_REGISTRY_PATH),i=v(),o=n?{min:n,max:n}:u,s=await r.reservePort({repositoryPath:i,serviceName:g,serviceType:_,preferredPort:n||u.min,portRange:o,pid:process.pid,host:`127.0.0.1`,force:!0});if(!s.success||!s.record)throw Error(s.error||`Failed to reserve port ${n}`);let l=s.record.port;t=await f({repositoryPath:i,serviceName:g,serviceType:_,pid:process.pid,port:l,host:`127.0.0.1`,command:process.argv[1],args:process.argv.slice(2)});let m=c.resolve(import.meta.dirname,`..`);console.error(p.green(`Starting Remotion Studio on port ${l}...`)),console.error(p.gray(` Working directory: ${m}`));let h=a(c.join(m,`node_modules`,`.bin`,`remotion`),[`studio`,`--port`,String(l)],{stdio:`inherit`,cwd:m});h.on(`error`,e=>{console.error(p.red(`Failed to start Remotion Studio: ${e.message}`)),process.exit(1)});let y=async e=>{h.kill(e),await t?.release(),await r.releasePort({repositoryPath:i,serviceName:g,serviceType:_,pid:process.pid}),process.exit(0)};process.on(`SIGINT`,()=>y(`SIGINT`)),process.on(`SIGTERM`,()=>y(`SIGTERM`))}catch(e){await t?.release(),console.error(`Error executing http-serve:`,e),process.exit(1)}}),b=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function x(e=process.cwd()){let t=c.resolve(e);for(;;){for(let e of b)if(s(c.join(t,e)))return t;let e=c.dirname(t);if(e===t)return process.cwd();t=e}}async function S(e,t){await e.start();let n=async n=>{console.error(`\nReceived ${n}, shutting down gracefully...`);try{await e.stop(),await t?.(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>n(`SIGINT`)),process.on(`SIGTERM`,()=>n(`SIGTERM`))}const C=new l(`mcp-serve`).description(`Start MCP server with specified transport`).option(`-t, --type <type>`,`Transport type: stdio`,`stdio`).option(`--proxy <url>`,`Proxy URL for yt-dlp (HTTP/HTTPS/SOCKS5, e.g. socks5://user:pass@host:port)`).action(async e=>{let t;try{let a=e.type.toLowerCase(),o=x();if(a===`stdio`){let a=new i(n(r({proxyUrl:e.proxy})));t=await f({repositoryPath:o,serviceName:`video-editor-mcp-stdio`,pid:process.pid,metadata:{transport:`stdio`}}),await S(a,async()=>{await t?.release()})}else console.error(`Unknown transport type: ${a}. Use: stdio`),process.exit(1)}catch(e){await t?.release(),console.error(`Failed to start MCP server:`,e),process.exit(1)}}),w=new l(`analyze-video`).description(`Analyze one or more local video files with Gemini Code Assist`).requiredOption(`-v, --video <paths...>`,`Paths to local video files to analyze`).requiredOption(`-p, --prompt <text>`,`Prompt to send with the video(s)`).option(`-m, --model <id>`,`Gemini model to use`,`gemini-2.5-flash-lite`).option(`--pretty`,`Pretty-print the full JSON result`,!1).action(async e=>{try{if(!Array.isArray(e.video)||e.video.length===0)throw Error(`At least one video path is required.`);process.stderr.write(p.blue(`🎥 Starting video analysis...
|
|
3
|
+
`)),process.stderr.write(p.gray(` Videos: ${e.video.join(`, `)}\n`)),process.stderr.write(p.gray(` Prompt: ${e.prompt}\n`)),process.stderr.write(p.gray(` Model: ${e.model}\n`));let n=await r().get(t.VideoAnalysisService).analyzeVideos({videoPaths:e.video,prompt:e.prompt,model:e.model});if(process.stderr.write(p.green(`✓ Resolved project: ${n.resolvedProject}\n`)),process.stderr.write(p.green(`✓ Videos processed: ${n.videoCount}\n`)),e.pretty){process.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}if(n.responseText){process.stdout.write(`${n.responseText}\n`);return}process.stdout.write(`${JSON.stringify(n.rawResponse,null,2)}\n`)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(p.red(`Error analyzing video: ${t}\n`)),process.exit(1)}}),T=e=>Number(e),E=e=>Number.parseInt(e,10),D=()=>s(c.resolve(import.meta.dirname,`../package.json`))?c.resolve(import.meta.dirname,`..`):c.resolve(import.meta.dirname,`../..`),O=e=>{if(e.output)return c.resolve(e.output);let t=c.resolve(e.outputDir??c.join(D(),`public/music`)),n=e.filename??`ace-step-music`;return c.join(t,`${n}.${e.audioFormat}`)},k=new l(`generate-music`).description(`Generate background music using ACE-Step 1.5`).requiredOption(`-p, --prompt <text>`,`Music description for ACE-Step text-to-music generation`).option(`-f, --filename <name>`,`Output filename without extension (default: ace-step-music)`).option(`-o, --output <path>`,`Explicit output audio path. Overrides filename/output-dir.`).option(`--output-dir <path>`,`Custom output directory (default: Remotion public/music/)`).option(`--ace-step-path <path>`,`Path to local ACE-Step-1.5 checkout`).option(`--lyrics <text>`,`Lyrics to use. Defaults to [Instrumental] for background music.`).option(`--duration <seconds>`,`Target duration in seconds (default: 30)`,T).option(`--bpm <number>`,`Optional BPM metadata`,T).option(`--keyscale <text>`,`Optional key/scale metadata, e.g. C minor`).option(`--time-signature <text>`,`Optional time signature metadata, e.g. 4/4`).option(`--vocal-language <code>`,`Vocal language code, or unknown`).option(`--seed <number>`,`Optional fixed seed for reproducible generation`,E).option(`--inference-steps <number>`,`Diffusion inference steps (default: 8)`,E).option(`--audio-format <format>`,`Output audio format: wav, mp3, flac`,`wav`).option(`--backend <backend>`,`ACE-Step LM backend: mlx, pt, vllm`,`mlx`).option(`--config-path <name>`,`ACE-Step DiT model config (default: acestep-v15-turbo)`).option(`--lm-model-path <name>`,`ACE-Step LM model (default: acestep-5Hz-lm-0.6B)`).option(`--timeout-ms <number>`,`Generation timeout in milliseconds`,E).option(`--pretty`,`Pretty-print the JSON result`,!1).action(async e=>{try{if(![`wav`,`mp3`,`flac`].includes(e.audioFormat))throw Error(`Unsupported audio format: ${e.audioFormat}`);if(![`mlx`,`pt`,`vllm`].includes(e.backend))throw Error(`Unsupported backend: ${e.backend}`);let n=O(e);process.stderr.write(p.blue(`Starting ACE-Step music generation...
|
|
4
|
+
`)),process.stderr.write(p.gray(` Prompt: ${e.prompt}\n`)),process.stderr.write(p.gray(` Output: ${n}\n`)),process.stderr.write(p.gray(` Backend: ${e.backend}\n`));let i=r().get(t.AceStepMusicService);if(!await i.isAvailable(e.aceStepPath))throw Error(`ACE-Step 1.5 is not available. Clone https://github.com/ACE-Step/ACE-Step-1.5 and set ACE_STEP_15_DIR or pass --ace-step-path.`);let a=await i.generateMusic({prompt:e.prompt,outputPath:n,aceStepPath:e.aceStepPath,lyrics:e.lyrics,duration:e.duration,bpm:e.bpm,keyscale:e.keyscale,timeSignature:e.timeSignature,vocalLanguage:e.vocalLanguage,seed:e.seed,inferenceSteps:e.inferenceSteps,audioFormat:e.audioFormat,backend:e.backend,configPath:e.configPath,lmModelPath:e.lmModelPath,timeoutMs:e.timeoutMs}),o={...a,compositionSrc:c.dirname(n)===c.resolve(D(),`public/music`)?`/music/${c.basename(n)}`:void 0};process.stderr.write(p.green(`Music generated successfully: ${a.outputPath}\n`)),process.stdout.write(`${JSON.stringify(o,null,e.pretty?2:0)}\n`)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(p.red(`Error generating music: ${t}\n`)),process.exit(1)}}),A=new l(`render`).description(`Render a video from JSON props file`).requiredOption(`-i, --input <path>`,`Path to JSON props file`).requiredOption(`-o, --output <path>`,`Output video file path`).option(`-c, --composition <id>`,`Composition ID`,`Main`).option(`--codec <codec>`,`Video codec (h264, h265, vp8, vp9)`,`h264`).option(`--dry-run`,`Validate props and assets without rendering`).action(async n=>{try{process.stderr.write(p.blue(n.dryRun?`🔎 Starting render dry run...
|
|
5
|
+
`:`🎬 Starting video render...
|
|
6
|
+
`)),process.stderr.write(p.gray(` Input: ${n.input}\n`)),process.stderr.write(p.gray(` Output: ${n.output}\n`)),process.stderr.write(p.gray(` Composition: ${n.composition}\n`)),process.stderr.write(p.gray(` Codec: ${n.codec}\n`)),process.stderr.write(p.gray(` Dry run: ${n.dryRun?`yes`:`no`}\n`)),o.existsSync(n.input)||(process.stderr.write(p.red(`Error: Input file not found: ${n.input}\n`)),process.exit(1));let e=o.readFileSync(n.input,`utf-8`),i=JSON.parse(e),a=r().get(t.RenderService);process.stderr.write(p.blue(n.dryRun?`🔎 Validating render input...
|
|
7
|
+
`:`📦 Bundling Remotion project...
|
|
8
|
+
`));let s=await a.render({compositionId:n.composition,inputProps:i,outputPath:n.output,codec:n.codec,dryRun:n.dryRun});s.dryRun?(process.stdout.write(`${JSON.stringify(s.validation,null,2)}\n`),process.stderr.write(p.green(`✓ Render dry run passed
|
|
9
|
+
`))):process.stderr.write(p.green(`✓ Video rendered successfully: ${s.outputPath}\n`))}catch(t){t instanceof e&&(process.stdout.write(`${JSON.stringify({error:`render_validation_failed`,message:t.message,summary:t.validation.summary,issues:t.validation.errors,warnings:t.validation.warnings,suggestedFixes:t.validation.errors.filter(e=>e.fix).map(e=>({code:e.code,path:e.path,...e.fix}))},null,2)}\n`),process.stderr.write(p.red(`Render validation failed; see JSON output for actionable fixes.
|
|
10
|
+
`)),process.exit(1)),process.stderr.write(p.red(`Error rendering video: ${t}\n`)),process.exit(1)}});async function j(){let e=new l;e.name(`video-editor-mcp`).description(`MCP server for video editing with Remotion`).version(m),e.addCommand(C),e.addCommand(y),e.addCommand(A),e.addCommand(w),e.addCommand(k),await e.parseAsync(process.argv)}j();export{};
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./stdio-
|
|
2
|
-
//# sourceMappingURL=index.cjs.map
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./stdio-CH0Bk0m7.cjs`);exports.AnimationConfigSchema=e.S,exports.AudioClipSchema=e.C,Object.defineProperty(exports,`BaseTool`,{enumerable:!0,get:function(){return e.p}}),exports.CaptionConfigSchema=e.v,exports.CaptionSchema=e.y,exports.ClipSchema=e.w,Object.defineProperty(exports,`ExtractVideoContextTool`,{enumerable:!0,get:function(){return e.f}}),exports.FontConfigSchema=e.b,Object.defineProperty(exports,`GenerateCaptionsTool`,{enumerable:!0,get:function(){return e.d}}),Object.defineProperty(exports,`GenerateVoiceoverTool`,{enumerable:!0,get:function(){return e.u}}),Object.defineProperty(exports,`GetMediaInfoTool`,{enumerable:!0,get:function(){return e.l}}),exports.GifClipSchema=e.T,exports.ImageClipSchema=e.E,Object.defineProperty(exports,`ImportSrtTool`,{enumerable:!0,get:function(){return e.c}}),Object.defineProperty(exports,`ListCompositionsTool`,{enumerable:!0,get:function(){return e.s}}),exports.LottieClipSchema=e.D,exports.MainCompositionSchema=e.x,Object.defineProperty(exports,`PreviewFrameTool`,{enumerable:!0,get:function(){return e.o}}),Object.defineProperty(exports,`RenderService`,{enumerable:!0,get:function(){return e.g}}),Object.defineProperty(exports,`RenderVideoTool`,{enumerable:!0,get:function(){return e.a}}),exports.StdioTransportHandler=e.t,exports.SubtitleClipSchema=e.O,exports.TYPES=e.h,exports.TextClipSchema=e.k,exports.TransitionConfigSchema=e.A,exports.VideoClipSchema=e.j,Object.defineProperty(exports,`VideoContextService`,{enumerable:!0,get:function(){return e.m}}),Object.defineProperty(exports,`VideoUnderstandingTool`,{enumerable:!0,get:function(){return e.i}}),exports.createContainer=e.r,exports.createServer=e.n;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
import{
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import{A as e,C as t,D as n,E as r,O as i,S as a,T as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,j as h,k as g,l as _,m as v,n as y,o as b,p as x,r as S,s as C,t as w,u as T,v as E,w as D,x as O,y as k}from"./stdio-KMqWPqUm.mjs";export{a as AnimationConfigSchema,t as AudioClipSchema,x as BaseTool,E as CaptionConfigSchema,k as CaptionSchema,D as ClipSchema,d as ExtractVideoContextTool,c as FontConfigSchema,u as GenerateCaptionsTool,T as GenerateVoiceoverTool,_ as GetMediaInfoTool,o as GifClipSchema,r as ImageClipSchema,l as ImportSrtTool,C as ListCompositionsTool,n as LottieClipSchema,O as MainCompositionSchema,b as PreviewFrameTool,f as RenderService,s as RenderVideoTool,w as StdioTransportHandler,i as SubtitleClipSchema,p as TYPES,g as TextClipSchema,e as TransitionConfigSchema,h as VideoClipSchema,v as VideoContextService,m as VideoUnderstandingTool,S as createContainer,y as createServer};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));require(`reflect-metadata`);let c=require(`inversify`),l=require(`node:child_process`),u=require(`node:fs`);u=s(u);let d=require(`node:fs/promises`);d=s(d);let f=require(`node:os`);f=s(f);let p=require(`node:path`);p=s(p);let m=require(`node:util`),h=require(`node:crypto`);h=s(h);let g=require(`node:url`),_=require(`@remotion/bundler`),v=require(`@remotion/renderer`),y=require(`sharp`);y=s(y);let b=require(`zod`),ee=require(`@agimon-ai/foundation-validator`),te=require(`@modelcontextprotocol/sdk/server/index.js`),ne=require(`@modelcontextprotocol/sdk/types.js`),re=require(`@modelcontextprotocol/sdk/server/stdio.js`);function x(e,t,n,r){var i=arguments.length,a=i<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,n):r,o;if(typeof Reflect==`object`&&typeof Reflect.decorate==`function`)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(o=e[s])&&(a=(i<3?o(a):i>3?o(t,n,a):o(t,n))||a);return i>3&&a&&Object.defineProperty(t,n,a),a}const ie=(0,m.promisify)(l.execFile);let S=class{async isAvailable(e){let t=this.resolveAceStepPath(e);if(!this.isAceStepCheckout(t))return!1;try{return await ie(`uv`,[`--version`]),!0}catch{return!1}}async generateMusic(e){let t=this.resolveAceStepPath(e.aceStepPath);if(!this.isAceStepCheckout(t))throw Error(`ACE-Step 1.5 checkout not found at ${t}. Pass aceStepPath or set ACE_STEP_15_DIR.`);let n=e.audioFormat??this.getFormatFromPath(e.outputPath),r=e.backend??`mlx`,i=e.duration??30,a=await d.default.mkdtemp(p.default.join(f.default.tmpdir(),`ace-step-music-`)),o=p.default.join(a,`generation.toml`),s=p.default.join(a,`output`);await d.default.mkdir(s,{recursive:!0}),await d.default.mkdir(p.default.dirname(e.outputPath),{recursive:!0});try{let a=this.buildConfig({...e,aceStepPath:t,audioFormat:n,backend:r,duration:i,outputDir:s});await d.default.writeFile(o,a,`utf-8`);let{stdout:c,stderr:l}=await ie(`uv`,[`run`,`python`,`cli.py`,`-c`,o,`--backend`,r,`--log-level`,`INFO`],{cwd:t,timeout:e.timeoutMs??1200*1e3,maxBuffer:50*1024*1024}),u=await this.findNewestGeneratedAudio(s,n);return await d.default.copyFile(u,e.outputPath),{outputPath:e.outputPath,sourcePath:u,duration:i,audioFormat:n,backend:r,aceStepPath:t,stdout:c,stderr:l}}catch(e){throw Error(`Failed to generate ACE-Step music: ${e instanceof Error?e.message:String(e)}`,{cause:e})}finally{await d.default.rm(a,{recursive:!0,force:!0}).catch(()=>void 0)}}resolveAceStepPath(e){return p.default.resolve(e??process.env.ACE_STEP_15_DIR??process.env.ACESTEP_15_DIR??p.default.join(f.default.homedir(),`workspace`,`ACE-Step-1.5`))}isAceStepCheckout(e){return(0,u.existsSync)(p.default.join(e,`cli.py`))&&(0,u.existsSync)(p.default.join(e,`pyproject.toml`))}getFormatFromPath(e){let t=p.default.extname(e).toLowerCase().replace(`.`,``);return t===`mp3`||t===`flac`||t===`wav`?t:`wav`}async findNewestGeneratedAudio(e,t){let n=await d.default.readdir(e,{withFileTypes:!0}),r=await Promise.all(n.filter(e=>e.isFile()&&e.name.endsWith(`.${t}`)).map(async t=>{let n=p.default.join(e,t.name);return{filePath:n,mtimeMs:(await d.default.stat(n)).mtimeMs}}));if(r.sort((e,t)=>t.mtimeMs-e.mtimeMs),!r[0])throw Error(`ACE-Step completed but no .${t} file was found in ${e}`);return r[0].filePath}buildConfig(e){let t=e.lyrics?.trim()||`[Instrumental]`,n=t.toLowerCase()===`[instrumental]`,r={project_root:e.aceStepPath,checkpoint_dir:p.default.join(e.aceStepPath,`checkpoints`),config_path:e.configPath??`acestep-v15-turbo`,lm_model_path:e.lmModelPath??`acestep-5Hz-lm-0.6B`,backend:e.backend,device:`auto`,offload_to_cpu:!1,offload_dit_to_cpu:!1,save_dir:e.outputDir,audio_format:e.audioFormat,caption:e.prompt,lyrics:t,duration:e.duration,instrumental:n,bpm:e.bpm??null,keyscale:e.keyscale??``,timesignature:e.timeSignature??``,vocal_language:e.vocalLanguage??`unknown`,task_type:`text2music`,sample_mode:!1,sample_query:``,use_format:!1,inference_steps:e.inferenceSteps??8,seed:e.seed??-1,use_random_seed:e.seed===void 0,guidance_scale:7,use_adg:!1,shift:3,infer_method:`ode`,thinking:!0,lm_temperature:.85,lm_cfg_scale:2,lm_top_k:0,lm_top_p:.9,use_cot_metas:!0,use_cot_caption:!0,use_cot_lyrics:!1,use_cot_language:!0,use_constrained_decoding:!0,batch_size:1,allow_lm_batch:!1,lm_batch_chunk_size:8,constrained_decoding_debug:!1,cfg_interval_start:0,cfg_interval_end:1,lm_negative_prompt:`NO USER INPUT`,log_level:`INFO`};return Object.entries(r).filter(e=>e[1]!==null).map(([e,t])=>`${e} = ${this.toTomlValue(t)}`).join(`
|
|
2
|
+
`)}toTomlValue(e){return typeof e==`string`?JSON.stringify(e):String(e)}};S=x([(0,c.injectable)()],S);const ae={tiktok:{top:270,right:140,bottom:480,left:80},reels:{top:220,right:110,bottom:420,left:80},shorts:{top:220,right:100,bottom:420,left:80},universal:{top:240,right:140,bottom:480,left:80}};let C=class{parseSrt(e){let t=[],n=e.split(`
|
|
3
|
+
`),r=0;for(;r<n.length;){if(!n[r]?.trim()){r++;continue}r++;let e=n[r];if(!e?.includes(`-->`)){r++;continue}let[i,a]=e.split(`-->`).map(e=>e.trim());if(!i||!a){r++;continue}let o=this.parseTimestamp(i),s=this.parseTimestamp(a);r++;let c=[];for(;r<n.length&&n[r]?.trim();)c.push(n[r]??``),r++;let l=c.join(`
|
|
4
|
+
`);l&&t.push({text:l,startMs:o,endMs:s})}return t}parseTimestamp(e){let[t,n]=e.split(`,`);if(!t||!n)throw Error(`Invalid timestamp format: ${e}`);let[r,i,a]=t.split(`:`).map(Number);if(r===void 0||i===void 0||a===void 0)throw Error(`Invalid time format: ${t}`);return(r*3600+i*60+a)*1e3+Number(n)}createTikTokCaptions(e,t=100){let n=typeof t==`number`?{combineMs:t}:t,r=n.combineMs??100,i=n.maxWordsPerPage??6,a=n.maxCharsPerLine??22,o=n.platform??`tiktok`,s=n.preset??`tiktok-bold`,c=this.createCaptionPages(e,{combineMs:r,maxWordsPerPage:i});return{pages:c,captionConfig:{captions:e,pages:c,style:s===`karaoke`?`karaoke`:`tiktok`,preset:s,platform:o,combineTokensWithinMilliseconds:r,maxWordsPerPage:i,maxCharsPerLine:a,maxLines:2,maxWidthPct:.86,fontSize:60,minFontSize:42,fontFamily:`Arial, sans-serif`,fontWeight:800,lineHeight:1.08,color:`#ffffff`,inactiveColor:`#ffffff`,highlightColor:`#FFD700`,textColor:`#ffffff`,strokeColor:`#000000`,strokeWidth:5,shadow:!0,backgroundColor:`rgba(0, 0, 0, 0)`,backgroundRadius:8,safeZone:ae[o],position:`bottom`}}}createCaptionPages(e,t={}){let n=[],r=this.normalizeCaptionsToTokens(e),i=t.combineMs??100,a=t.maxWordsPerPage??6;if(r.length===0)return n;let o={startMs:r[0]?.startMs??0,endMs:r[0]?.endMs??0,tokens:[r[0]]};for(let e=1;e<r.length;e++){let t=r[e];if(!t)continue;let s=t.startMs-o.endMs,c=this.countWords(o.tokens.map(e=>e.text).join(` `)),l=this.countWords(t.text);s<=i&&c+l<=a?(o.tokens.push(t),o.endMs=t.endMs):(o.text=this.pageText(o.tokens),n.push(o),o={startMs:t.startMs,endMs:t.endMs,tokens:[t]})}return o.tokens.length>0&&(o.text=this.pageText(o.tokens),n.push(o)),n}normalizeCaptionsToTokens(e){return[...e].sort((e,t)=>e.startMs-t.startMs).flatMap(e=>this.captionToTokens(e))}getPlatformSafeZone(e){return ae[e]}captionToTokens(e){let t=e.text.trim().split(/\s+/).filter(Boolean);if(t.length===0)return[];if(t.length===1)return[{...e,text:t[0]??e.text,emphasis:e.emphasis??`none`}];let n=Math.max(1,e.endMs-e.startMs)/t.length;return t.map((r,i)=>{let a=Math.round(e.startMs+n*i),o=i===t.length-1?e.endMs:Math.round(e.startMs+n*(i+1));return{text:r,startMs:a,endMs:Math.max(o,a+1),timestampMs:a,confidence:e.confidence,emphasis:e.emphasis??`none`}})}pageText(e){return e.map(e=>e.text).join(` `)}countWords(e){return e.trim().split(/\s+/).filter(Boolean).length}};C=x([(0,c.injectable)()],C);const oe=[`Inter`,`Roboto`,`Open Sans`,`Montserrat`,`Lato`,`Poppins`,`Oswald`,`Raleway`,`Playfair Display`,`Merriweather`,`Source Sans Pro`,`PT Sans`,`Ubuntu`,`Nunito`,`Rubik`,`Work Sans`,`Karla`,`Noto Sans`,`Fira Sans`,`DM Sans`];let se=class{getAvailableGoogleFonts(){return[...oe]}validateFontFamily(e){let t=e.toLowerCase();return oe.some(e=>e.toLowerCase()===t)}};se=x([(0,c.injectable)()],se);var ce=class{tail=Promise.resolve();currentActiveCount=0;currentPendingCount=0;async run(e){let t=this.tail,n,r=new Promise(e=>{n=e});this.currentPendingCount+=1,this.tail=t.then(()=>r),await t,--this.currentPendingCount,this.currentActiveCount+=1;try{return await e()}finally{--this.currentActiveCount,n()}}getStatus(){return{activeCount:this.currentActiveCount,pendingCount:this.currentPendingCount}}};const w=(0,m.promisify)(l.execFile);let T=class{async getVideoDuration(e){try{let{stdout:t}=await w(`ffprobe`,[`-v`,`error`,`-show_entries`,`format=duration`,`-of`,`json`,e]),n=JSON.parse(t),r=Number.parseFloat(n.format?.duration??`0`);if(Number.isNaN(r)||r<=0)throw Error(`Invalid duration for video: ${e}`);return r}catch(t){throw Error(`Failed to get video duration for ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}}async getAudioDuration(e){try{let{stdout:t}=await w(`ffprobe`,[`-v`,`error`,`-show_entries`,`format=duration`,`-of`,`json`,e]),n=JSON.parse(t),r=Number.parseFloat(n.format?.duration??`0`);if(Number.isNaN(r)||r<=0)throw Error(`Invalid duration for audio: ${e}`);return r}catch(t){throw Error(`Failed to get audio duration for ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}}async getVideoDimensions(e){try{let{stdout:t}=await w(`ffprobe`,[`-v`,`error`,`-select_streams`,`v:0`,`-show_entries`,`stream=width,height`,`-of`,`json`,e]),n=JSON.parse(t).streams?.[0];if(!n?.width||!n?.height)throw Error(`No video stream found in: ${e}`);return{width:Number(n.width),height:Number(n.height)}}catch(t){throw Error(`Failed to get video dimensions for ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}}async canDecode(e){try{return await w(`ffprobe`,[`-v`,`error`,`-show_entries`,`format=duration`,`-of`,`json`,e]),!0}catch{return!1}}};T=x([(0,c.injectable)()],T);const E=b.z.coerce.number(),D=b.z.coerce.number().int(),O=b.z.preprocess(e=>{if(typeof e!=`string`)return e;let t=e.trim().toLowerCase();return t===`true`?!0:t===`false`?!1:e},b.z.boolean()),k=b.z.string().min(1).refine(e=>/^(\/|file:|https?:|data:|blob:)/i.test(e),{message:`src must be an absolute filesystem path or a file:// / http(s):// / data: / blob: URL — relative paths are not allowed.`}),le=b.z.object({frame:D.nonnegative(),volume:E.min(0).max(1)}),A=b.z.object({type:b.z.enum([`interpolate`,`spring`]).default(`interpolate`),easing:b.z.enum([`linear`,`ease-in`,`ease-out`,`ease-in-out`]).optional(),springConfig:b.z.object({mass:E.positive().default(1),damping:E.positive().default(10),stiffness:E.positive().default(100)}).optional(),delay:D.nonnegative().default(0)}),ue=b.z.object({type:b.z.enum([`fade`,`slide`,`wipe`,`flip`,`clockWipe`,`none`]).default(`fade`),durationInFrames:D.positive().default(15),direction:b.z.enum([`from-left`,`from-right`,`from-top`,`from-bottom`]).optional()}),j={startFrame:D.nonnegative(),durationInFrames:D.positive(),premountFor:D.nonnegative().default(30),entrance:A.optional(),exit:A.optional(),transition:ue.optional()},de=b.z.object({type:b.z.literal(`video`),...j,src:k,volume:E.min(0).max(1).optional(),playbackRate:E.positive().optional(),muted:O.optional(),loop:O.optional(),trimBefore:E.nonnegative().optional(),trimAfter:E.nonnegative().optional(),transparent:O.optional(),noFadeIn:O.optional(),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),fe=b.z.object({type:b.z.literal(`image`),...j,src:k,fit:b.z.enum([`contain`,`cover`,`fill`]).optional(),noFadeIn:O.optional(),baseScale:E.positive().optional(),transformOriginX:b.z.string().optional(),transformOriginY:b.z.string().optional(),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),pe=b.z.object({type:b.z.literal(`text`),...j,text:b.z.string(),fontSize:E.positive().default(80),fontFamily:b.z.string().default(`sans-serif`),fontWeight:b.z.union([b.z.string(),E]).optional(),color:b.z.string().default(`#ffffff`),backgroundColor:b.z.string().optional(),position:b.z.enum([`top`,`center`,`bottom`]).default(`center`),animation:b.z.enum([`none`,`fade`,`slide`,`typewriter`,`highlight`]).default(`none`),highlightColor:b.z.string().optional(),highlightWord:b.z.string().optional(),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),me=b.z.object({type:b.z.literal(`audio`),...j,src:k,volume:E.min(0).max(1).optional(),volumeAutomation:b.z.array(le).optional(),playbackRate:E.positive().optional(),muted:O.optional(),loop:O.optional(),trimBefore:E.nonnegative().optional(),trimAfter:E.nonnegative().optional(),toneFrequency:E.positive().optional()}),he=b.z.object({type:b.z.literal(`subtitle`),...j,text:b.z.string(),position:b.z.enum([`top`,`center`,`bottom`]).default(`bottom`),animation:b.z.enum([`none`,`fade`,`slide`]).default(`fade`),fontSize:E.positive().default(36),fontFamily:b.z.string().default(`Arial, sans-serif`),color:b.z.string().default(`#ffffff`),backgroundColor:b.z.string().default(`rgba(0, 0, 0, 0.7)`),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),ge=b.z.object({type:b.z.literal(`gif`),...j,src:k,width:E.positive().optional(),height:E.positive().optional(),fit:b.z.enum([`contain`,`cover`,`fill`]).optional(),playbackRate:E.positive().optional(),loopBehavior:b.z.enum([`loop`,`pause-after-finish`]).optional(),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),_e=b.z.object({type:b.z.literal(`lottie`),...j,src:k,width:E.positive().optional(),height:E.positive().optional(),loop:O.optional(),style:b.z.record(b.z.string(),b.z.unknown()).optional()}),ve=b.z.discriminatedUnion(`type`,[de,fe,pe,me,he,ge,_e]),ye=b.z.object({text:b.z.string(),startMs:E.nonnegative(),endMs:E.nonnegative(),timestampMs:E.nonnegative().optional(),confidence:E.min(0).max(1).optional(),emphasis:b.z.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).optional()}),be=ye.extend({emphasis:b.z.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).default(`none`)}),xe=b.z.object({x:E.min(0).max(1).optional(),y:E.min(0).max(1).optional(),position:b.z.enum([`top`,`center`,`bottom`]).optional()}),Se=b.z.object({text:b.z.string().optional(),startMs:E.nonnegative(),endMs:E.nonnegative(),tokens:b.z.array(be).min(1),placement:xe.optional()}),Ce=b.z.object({top:E.nonnegative().optional(),right:E.nonnegative().optional(),bottom:E.nonnegative().optional(),left:E.nonnegative().optional()}),we=b.z.object({color:b.z.string().default(`rgba(0, 0, 0, 0.85)`),blur:E.nonnegative().default(8),offsetX:E.default(0),offsetY:E.default(3)}),Te=b.z.object({captions:b.z.array(ye).default([]),pages:b.z.array(Se).optional(),style:b.z.enum([`tiktok`,`standard`,`karaoke`]).default(`tiktok`),preset:b.z.enum([`tiktok-bold`,`karaoke`,`premium-pill`,`minimal`,`ugc`]).default(`tiktok-bold`),platform:b.z.enum([`tiktok`,`reels`,`shorts`,`universal`]).default(`tiktok`),combineTokensWithinMilliseconds:E.nonnegative().default(800),maxWordsPerPage:D.positive().default(6),maxCharsPerLine:D.positive().default(22),maxLines:D.positive().default(2),maxWidthPct:E.positive().max(1).default(.86),fontSize:E.positive().default(60),minFontSize:E.positive().default(42),fontFamily:b.z.string().default(`Arial, sans-serif`),fontWeight:b.z.union([D.positive(),b.z.string()]).default(800),lineHeight:E.positive().default(1.08),color:b.z.string().default(`#ffffff`),inactiveColor:b.z.string().default(`#ffffff`),highlightColor:b.z.string().default(`#FFD700`),textColor:b.z.string().default(`#ffffff`),strokeColor:b.z.string().default(`#000000`),strokeWidth:E.nonnegative().default(5),shadow:b.z.union([O,we]).default(!0),backgroundColor:b.z.string().default(`rgba(0, 0, 0, 0)`),backgroundRadius:E.nonnegative().default(8),safeZone:Ce.optional(),position:b.z.enum([`top`,`center`,`bottom`]).default(`bottom`)}).superRefine((e,t)=>{e.captions.length===0&&(!e.pages||e.pages.length===0)&&t.addIssue({code:`custom`,message:`Caption config must include captions or pages.`,path:[`captions`]})}),Ee={src:k,volume:E.min(0).max(1).default(1),fadeInMs:E.nonnegative().default(0),fadeOutMs:E.nonnegative().default(0),trimBeforeMs:E.nonnegative().default(0),trimAfterMs:E.nonnegative().optional(),playbackRate:E.positive().default(1)},De=b.z.object({...Ee,definesTimeline:O.default(!0),durationMs:E.positive().optional()}),Oe=b.z.object({...Ee,loop:O.default(!0)}),ke=b.z.object({...Ee,atMs:E.nonnegative(),durationMs:E.positive().optional()}),Ae=b.z.object({voiceover:De.optional(),music:Oe.optional(),sfx:b.z.array(ke).default([])}),je=b.z.object({family:b.z.string(),source:b.z.enum([`google`,`local`]).default(`google`),weights:b.z.array(D.positive()).optional(),url:b.z.string().optional()}),Me=b.z.object({clips:b.z.array(ve).default([]),backgroundColor:b.z.string().default(`#000000`),fps:D.positive().default(30),width:D.positive().default(1920),height:D.positive().default(1080),captions:Te.optional(),globalTransition:ue.optional(),fonts:b.z.array(je).optional(),audio:Ae.optional()}).passthrough(),Ne=`${p.default.sep}public${p.default.sep}`,Pe=/^(https?:|data:|blob:)/i,Fe=/\b(undefined|NaN|null)\b|blur\(px\)/i,Ie={Main:{width:1920,height:1080},Vertical:{width:1080,height:1920},Square:{width:1080,height:1080}},Le=new Set([`video`,`image`,`audio`,`gif`,`lottie`]),Re=new Set([`video`,`image`,`gif`,`lottie`,`text`,`subtitle`]),ze={tiktok:480,reels:420,shorts:420,universal:480},Be={tiktok:{top:270,right:140,bottom:480,left:80},reels:{top:220,right:110,bottom:420,left:80},shorts:{top:220,right:100,bottom:420,left:80},universal:{top:240,right:140,bottom:480,left:80}};var Ve=class e extends Error{constructor(t){super(e.formatMessage(t)),this.validation=t,this.name=`RenderValidationError`}static formatMessage(e){return`Render validation failed: ${e.errors.map(e=>`${e.path??e.code}: ${e.message}`).join(`; `)}`}};let M=class{bundlePromise=null;mediaInfoService=new T;projectRoot=(0,u.existsSync)(p.default.resolve(__dirname,`../package.json`))?p.default.resolve(__dirname,`..`):p.default.resolve(__dirname,`../..`);async getServeUrl(){return this.bundlePromise||=(0,_.bundle)({entryPoint:p.default.resolve(this.projectRoot,`src/remotion/index.ts`),publicDir:p.default.resolve(this.projectRoot,`public`),webpackOverride:e=>({...e,resolve:{...e.resolve,extensionAlias:{".js":[`.ts`,`.tsx`,`.js`,`.jsx`]}}})}),this.bundlePromise}isTransientRendererNavigationError(e){let t=e instanceof Error?e.message:String(e);return t.includes(`ERR_EMPTY_RESPONSE`)||t.includes(`ERR_CONNECTION_RESET`)}async withTransientRendererRetry(e){for(let t=1;t<=2;t+=1)try{return await e()}catch(e){if(t===2||!this.isTransientRendererNavigationError(e))throw e;this.bundlePromise=null,await new Promise(e=>setTimeout(e,150))}throw Error(`Renderer retry exhausted unexpectedly.`)}get publicDir(){return p.default.resolve(this.projectRoot,`public`)}toPublicRelativePath(e){let t=p.default.normalize(e),n=t.indexOf(Ne);return n===-1?null:t.slice(n+Ne.length).split(p.default.sep).join(`/`)}toLocalFilesystemPath(e){return e.startsWith(`file://`)?(0,g.fileURLToPath)(e):p.default.isAbsolute(e)?e:null}toPublicFilesystemPath(e){let t=e.replace(/^\/+/,``).replace(/^public\//,``);return p.default.join(this.publicDir,...t.split(`/`))}resolveValidationSource(e){if(Pe.test(e))return{kind:`remote`,src:e};let t=this.toLocalFilesystemPath(e);return t?{kind:`local`,src:e,filePath:t}:{kind:`local`,src:e,filePath:this.toPublicFilesystemPath(e)}}hasExplicitDimension(e,t){return Object.prototype.hasOwnProperty.call(e,t)}voiceoverFrames(e,t){let n=e?.voiceover;if(!n||n.definesTimeline===!1||!n.durationMs)return 0;let r=Math.max(0,(n.trimAfterMs??n.durationMs)-(n.trimBeforeMs??0))/(n.playbackRate??1);return Math.ceil(r/1e3*t)}framesToSeconds(e,t){return Number((e/t).toFixed(3))}canExtendVisualClip(e){return e.type===`image`||e.type===`text`||e.type===`subtitle`||e.type===`lottie`}createVisualGapIssue({gapStart:e,gapEnd:t,fps:n,previous:r,next:i,durationInFrames:a,message:o}){let s=t-e,c={gapStartFrame:e,gapEndFrame:t,gapDurationInFrames:s,gapStartSeconds:this.framesToSeconds(e,n),gapEndSeconds:this.framesToSeconds(t,n),gapDurationSeconds:this.framesToSeconds(s,n),renderDurationInFrames:a};if(r&&(c.previousVisualClip={index:r.clipIndex,type:r.type,startFrame:r.start,endFrame:r.end,durationInFrames:r.durationInFrames,canExtendSafely:r.canExtendSafely}),i&&(c.nextVisualClip={index:i.clipIndex,type:i.type,startFrame:i.start,endFrame:i.end,durationInFrames:i.durationInFrames}),r?.canExtendSafely){let n=r.durationInFrames+s;return{code:`timeline.visual_gap`,message:o,path:`clips`,details:c,fix:{instruction:`Extend clips[${r.clipIndex}].durationInFrames from ${r.durationInFrames} to ${n} so visuals cover frames ${e}..${t}.`,edits:[{path:`inputProps.clips.${r.clipIndex}.durationInFrames`,value:n,reason:`Covers the ${s}-frame visual gap without changing audio timing.`}]}}}return{code:`timeline.visual_gap`,message:o,path:`clips`,details:c,fix:{instruction:r&&!r.canExtendSafely?`Add a still/image/text visual from frame ${e} for ${s} frames, or replace/loop the preceding visual before extending it.`:`Add a visual clip starting at frame ${e} with durationInFrames ${s}.`}}}createSummary(e,t,n){let r=t.clips.reduce((e,t)=>Math.max(e,t.startFrame+t.durationInFrames),0),i=this.voiceoverFrames(t.audio,t.fps),a=Math.max(r,i);return{compositionId:e.compositionId,width:this.hasExplicitDimension(e.inputProps,`width`)?t.width:n.width,height:this.hasExplicitDimension(e.inputProps,`height`)?t.height:n.height,fps:t.fps,durationInFrames:a,clipCount:t.clips.length}}pushBadStringIssues(e,t,n,r){if(typeof e==`string`){Fe.test(e)&&n.push({code:`${r}.invalid_value`,message:`Invalid ${r} value: ${e}`,path:t});return}if(!(!e||typeof e!=`object`)){if(Array.isArray(e)){e.forEach((e,i)=>this.pushBadStringIssues(e,`${t}.${i}`,n,r));return}Object.entries(e).forEach(([e,i])=>{this.pushBadStringIssues(i,`${t}.${e}`,n,r)})}}validateTextClip(e,t,n){`text`in e&&(e.text.trim()||n.push({code:`text.empty`,message:`Text and subtitle clips must not be empty.`,path:`${t}.text`}),this.pushBadStringIssues(e.text,`${t}.text`,n,`text`))}validateStyle(e,t,n){`style`in e&&e.style&&this.pushBadStringIssues(e.style,`${t}.style`,n,`style`)}isSourceClip(e){return Le.has(e.type)}async validateLocalAsset(e,t,n,r,i){if(!t.filePath)return;let a;try{a=await(0,d.stat)(t.filePath)}catch{i.push({code:`asset.missing`,message:`Render asset does not exist: ${t.src}`,path:`${n}.src`});return}if(!a.isFile()){i.push({code:`asset.not_file`,message:`Render asset is not a file: ${t.src}`,path:`${n}.src`});return}try{if(e.type===`image`||e.type===`gif`){let n=await(0,y.default)(t.filePath,{animated:e.type===`gif`}).metadata();if(!n.width||!n.height)throw Error(`Missing image dimensions`)}else if(e.type===`lottie`)JSON.parse(await(0,d.readFile)(t.filePath,`utf-8`));else if(e.type===`video`){await this.mediaInfoService.getVideoDimensions(t.filePath);let a=await this.mediaInfoService.getVideoDuration(t.filePath);this.validateMediaDuration(e,a,r,n,i)}else if(e.type===`audio`){let a=await this.mediaInfoService.getAudioDuration(t.filePath);this.validateMediaDuration(e,a,r,n,i)}}catch(e){i.push({code:`asset.decode_failed`,message:`Render asset cannot be decoded: ${t.src}. ${e instanceof Error?e.message:String(e)}`,path:`${n}.src`})}}validateMediaDuration(e,t,n,r,i){let a=e.trimBefore??0,o=e.trimAfter??0,s=e.playbackRate??1;if(o>0&&o<=a){i.push({code:`media.invalid_trim`,message:`trimAfter must be greater than trimBefore when provided.`,path:r});return}if(e.loop)return;let c=t*n,l=(o>0?o-a:c-a)/s;l+2<e.durationInFrames&&i.push({code:`media.too_short`,message:`Clip duration (${e.durationInFrames} frames) exceeds available media (${Math.floor(l)} frames).`,path:r})}async validateClipAssets(e,t,n){await Promise.all(e.clips.map(async(r,i)=>{let a=`clips.${i}`;if(this.validateTextClip(r,a,n),this.validateStyle(r,a,n),!this.isSourceClip(r))return;let o=this.resolveValidationSource(r.src);if(o.kind===`remote`){t.push({code:`asset.remote_unprobed`,message:`Remote asset will be fetched during render and cannot be decoded in preflight: ${r.src}`,path:`${a}.src`});return}await this.validateLocalAsset(r,o,a,e.fps,n)}))}async validateAudioLayer(e,t,n){if(!e)return;let r=[];e.voiceover&&r.push({src:e.voiceover.src,path:`audio.voiceover.src`}),e.music&&r.push({src:e.music.src,path:`audio.music.src`}),e.sfx?.forEach((e,t)=>{r.push({src:e.src,path:`audio.sfx.${t}.src`})}),await Promise.all(r.map(async({src:e,path:r})=>{let i=this.resolveValidationSource(e);if(i.kind===`remote`){t.push({code:`audio.remote_unprobed`,message:`Remote audio asset will be fetched during render and cannot be decoded in preflight: ${e}`,path:r});return}if(i.filePath)try{if(!(await(0,d.stat)(i.filePath)).isFile()){n.push({code:`audio.not_file`,message:`Audio asset is not a file: ${e}`,path:r});return}await this.mediaInfoService.getAudioDuration(i.filePath)}catch(t){if(t.code===`ENOENT`){n.push({code:`audio.missing`,message:`Audio asset does not exist: ${e}`,path:r});return}n.push({code:`audio.decode_failed`,message:`Audio asset cannot be decoded: ${e}. ${t instanceof Error?t.message:String(t)}`,path:r})}}))}async resolveAudioDurationMs(e){let t=this.resolveValidationSource(e);if(t.kind===`remote`||!t.filePath)return null;try{return await this.mediaInfoService.getAudioDuration(t.filePath)*1e3}catch{return null}}async injectAudioDurations(e){let t=e.audio;if(!t?.voiceover||t.voiceover.durationMs)return e;let n=await this.resolveAudioDurationMs(t.voiceover.src);return n?{...e,audio:{...t,voiceover:{...t.voiceover,durationMs:n}}}:e}validateTimeline(e,t,n){let r=!!(e.audio?.voiceover||e.audio?.music||e.audio?.sfx&&e.audio.sfx.length>0);if(e.clips.length===0){if(r){n.push({code:`timeline.audio_only`,message:`Render has no visual clips; output will show backgroundColor under the audio layer.`,path:`clips`});return}t.push({code:`timeline.empty`,message:`Render must contain at least one clip.`,path:`clips`});return}let i=e.clips.map((e,t)=>({clip:e,clipIndex:t})).filter(({clip:e})=>Re.has(e.type)).map(({clip:e,clipIndex:t})=>({clipIndex:t,type:e.type,start:e.startFrame,end:e.startFrame+e.durationInFrames,durationInFrames:e.durationInFrames,canExtendSafely:this.canExtendVisualClip(e)})).sort((e,t)=>e.start-t.start);if(i.length===0){if(r){n.push({code:`timeline.audio_only`,message:`Render has no visual clips; output will show backgroundColor under the audio layer.`,path:`clips`});return}t.push({code:`timeline.no_visual`,message:`Render must contain at least one visual clip.`,path:`clips`});return}let a=e.clips.reduce((e,t)=>Math.max(e,t.startFrame+t.durationInFrames),0),o=Math.max(a,this.voiceoverFrames(e.audio,e.fps));i[0]&&i[0].start>3&&t.push(this.createVisualGapIssue({gapStart:0,gapEnd:i[0].start,fps:e.fps,next:i[0],durationInFrames:o,message:`Visual timeline starts at frame ${i[0].start}; expected coverage from frame 0.`}));let s=i[0]?.end??0,c=i[0];for(let n of i.slice(1))n.start>s+3&&t.push(this.createVisualGapIssue({gapStart:s,gapEnd:n.start,fps:e.fps,previous:c,next:n,durationInFrames:o,message:`Visual timeline has a gap from frame ${s} to ${n.start}.`})),n.end>s&&(c=n),s=Math.max(s,n.end);s+3<o&&t.push(this.createVisualGapIssue({gapStart:s,gapEnd:o,fps:e.fps,previous:c,durationInFrames:o,message:`Visual timeline ends at frame ${s}; render duration is ${o}.`}))}validateCaptions(e,t,n,r){if(!e.captions)return;let i=e.captions,a=t.durationInFrames/e.fps*1e3,o=-1,s=-1;i.captions.forEach((e,t)=>{let i=`captions.captions.${t}`;e.text.trim()||n.push({code:`captions.empty_text`,message:`Caption text must not be empty.`,path:`${i}.text`}),e.endMs<=e.startMs&&n.push({code:`captions.invalid_timing`,message:`Caption endMs must be greater than startMs.`,path:i}),e.startMs<o&&n.push({code:`captions.unsorted`,message:`Captions must be sorted by startMs.`,path:i}),e.startMs<s&&r.push({code:`captions.overlap`,message:`Caption overlaps the previous caption; prefer page/token timing for intentional karaoke overlap.`,path:i}),e.endMs>a+100&&n.push({code:`captions.out_of_range`,message:`Caption ends after render duration (${Math.round(a)}ms).`,path:i}),o=e.startMs,s=Math.max(s,e.endMs),this.pushBadStringIssues(e.text,`${i}.text`,n,`text`)}),i.pages?.forEach((t,o)=>{let s=`captions.pages.${o}`;t.endMs<=t.startMs&&n.push({code:`captions.page_invalid_timing`,message:`Caption page endMs must be greater than startMs.`,path:s}),t.endMs>a+100&&n.push({code:`captions.page_out_of_range`,message:`Caption page ends after render duration (${Math.round(a)}ms).`,path:s});let c=-1,l=-1;t.tokens.forEach((e,i)=>{let a=`${s}.tokens.${i}`;e.text.trim()||n.push({code:`captions.token_empty_text`,message:`Caption token text must not be empty.`,path:`${a}.text`}),e.endMs<=e.startMs&&n.push({code:`captions.token_invalid_timing`,message:`Caption token endMs must be greater than startMs.`,path:a}),(e.startMs<t.startMs||e.endMs>t.endMs)&&n.push({code:`captions.token_outside_page`,message:`Caption token timing must stay within its page timing.`,path:a}),e.startMs<c&&n.push({code:`captions.token_unsorted`,message:`Caption page tokens must be sorted by startMs.`,path:a}),e.startMs<l&&r.push({code:`captions.token_overlap`,message:`Caption token overlaps the previous token.`,path:a}),e.confidence!==void 0&&e.confidence<.75&&r.push({code:`captions.low_confidence`,message:`Low-confidence caption token should be reviewed before render.`,path:a}),c=e.startMs,l=Math.max(l,e.endMs),this.pushBadStringIssues(e.text,`${a}.text`,n,`text`)});let u=this.countCaptionWords(t.tokens.map(e=>e.text).join(` `));u>i.maxWordsPerPage&&r.push({code:`captions.page_too_dense`,message:`Caption page has ${u} words; configured maxWordsPerPage is ${i.maxWordsPerPage}.`,path:s});let d=t.placement?.x!==void 0,f=t.placement?.y!==void 0;if(d!==f&&r.push({code:`captions.page_partial_placement`,message:`Caption page placement should provide both x and y for predictable focal-target avoidance.`,path:`${s}.placement`}),d||f){let n={...Be[i.platform],...i.safeZone},a=n.left/e.width,o=1-n.right/e.width,c=n.top/e.height,l=1-n.bottom/e.height,u=t.placement?.x??.5,d=t.placement?.y??.5;(u<a||u>o||d<c||d>l)&&r.push({code:`captions.page_placement_safe_zone`,message:`Caption page explicit placement sits outside the configured platform safe zone.`,path:`${s}.placement`})}});let c=ze[i.platform]??480,l=i.safeZone?.bottom??c;i.position===`bottom`&&l<c&&r.push({code:`captions.safe_zone_bottom`,message:`Bottom caption safe zone ${l}px is smaller than ${i.platform} preset ${c}px.`,path:`captions.safeZone.bottom`}),i.style===`karaoke`&&!i.pages&&i.captions.some(e=>/\s/.test(e.text.trim()))&&r.push({code:`captions.karaoke_legacy_timing`,message:`Karaoke legacy captions use proportional word timing; provide pages/tokens for accurate word sync.`,path:`captions.captions`})}countCaptionWords(e){return e.trim().split(/\s+/).filter(Boolean).length}async copyToPublic(e){let t=await(0,d.stat)(e);if(!t.isFile())throw Error(`Render asset is not a file: ${e}`);let n=p.default.extname(e),r=p.default.basename(e,n).replace(/[^a-zA-Z0-9._-]/g,`-`),i=(0,h.createHash)(`sha256`).update(e).update(String(t.mtimeMs)).update(String(t.size)).digest(`hex`).slice(0,16),a=p.default.posix.join(`mcp-render-assets`,`${r}-${i}${n}`),o=p.default.join(this.publicDir,...a.split(`/`));return await(0,d.mkdir)(p.default.dirname(o),{recursive:!0}),await(0,d.copyFile)(e,o),a}async prepareValue(e){if(Array.isArray(e)){let t=!1;return{value:await Promise.all(e.map(async e=>{let n=await this.prepareValue(e);return t||=n.usesLocalAssets,n.value})),usesLocalAssets:t}}if(!e||typeof e!=`object`)return{value:e,usesLocalAssets:!1};let t=!1,n={};for(let[r,i]of Object.entries(e)){if(r===`src`&&typeof i==`string`){let e=this.toLocalFilesystemPath(i);if(e){n[r]=this.toPublicRelativePath(e)??await this.copyToPublic(e),t=!0;continue}}let e=await this.prepareValue(i);n[r]=e.value,t||=e.usesLocalAssets}return{value:n,usesLocalAssets:t}}async prepareInputProps(e){let t=await this.prepareValue(e);return{inputProps:t.value,usesLocalAssets:t.usesLocalAssets}}normalizeProvidedValues(e,t){if(Array.isArray(e)&&Array.isArray(t))return e.map((e,n)=>this.normalizeProvidedValues(e,t[n]));if(e&&t&&typeof e==`object`&&typeof t==`object`&&!Array.isArray(e)&&!Array.isArray(t)){let n={},r=t;for(let[t,i]of Object.entries(e))n[t]=this.normalizeProvidedValues(i,r[t]);return n}return t}normalizeInputProps(e){let t=Me.parse(e);return this.normalizeProvidedValues(e,t)}async validateRender(e){let t=[],n=[],r=Ie[e.compositionId];r||t.push({code:`composition.unknown`,message:`Unknown composition: ${e.compositionId}`,path:`compositionId`});let i=await this.injectAudioDurations(e.inputProps),a=Me.safeParse(i);if(!a.success)return{passed:!1,errors:a.error.issues.map(e=>({code:`schema.invalid`,message:e.message,path:e.path.join(`.`)})),warnings:n,summary:{compositionId:e.compositionId,width:r?.width??0,height:r?.height??0,fps:0,durationInFrames:0,clipCount:0}};let o=a.data,s=r??{width:o.width,height:o.height},c=this.createSummary(e,o,s);return this.hasExplicitDimension(e.inputProps,`width`)&&o.width!==s.width&&t.push({code:`composition.width_mismatch`,message:`${e.compositionId} expects width ${s.width}, received ${o.width}.`,path:`width`}),this.hasExplicitDimension(e.inputProps,`height`)&&o.height!==s.height&&t.push({code:`composition.height_mismatch`,message:`${e.compositionId} expects height ${s.height}, received ${o.height}.`,path:`height`}),this.validateTimeline(o,t,n),this.validateCaptions(o,c,t,n),await this.validateClipAssets(o,n,t),await this.validateAudioLayer(o.audio,n,t),{passed:t.length===0,errors:t,warnings:n,summary:c}}async render(e){let t=await this.validateRender(e);if(!t.passed)throw new Ve(t);if(e.dryRun)return{outputPath:e.outputPath,dryRun:!0,validation:t};let n=await this.injectAudioDurations(e.inputProps),r=this.normalizeInputProps(n),i=await this.prepareInputProps(r);return i.usesLocalAssets&&(this.bundlePromise=null),await this.withTransientRendererRetry(async()=>{let t=await this.getServeUrl();await(0,v.renderMedia)({composition:await(0,v.selectComposition)({serveUrl:t,id:e.compositionId,inputProps:i.inputProps}),serveUrl:t,codec:e.codec??`h264`,outputLocation:e.outputPath,inputProps:i.inputProps})}),{outputPath:e.outputPath,validation:t}}async renderStill(e){let t=await this.injectAudioDurations(e.inputProps),n=this.normalizeInputProps(t),r=await this.prepareInputProps(n);return r.usesLocalAssets&&(this.bundlePromise=null),await this.withTransientRendererRetry(async()=>{let t=await this.getServeUrl();await(0,v.renderStill)({composition:await(0,v.selectComposition)({serveUrl:t,id:e.compositionId,inputProps:r.inputProps}),serveUrl:t,output:e.outputPath,frame:e.frame??0,imageFormat:e.imageFormat??`png`,inputProps:r.inputProps})}),{outputPath:e.outputPath}}};M=x([(0,c.injectable)()],M);const He=(0,m.promisify)(l.execFile),Ue=[`af_sarah`,`af_nicole`,`af_bella`,`af_sky`,`am_adam`,`am_michael`,`bf_emma`,`bf_isabella`,`bm_george`,`bm_lewis`];let N=class{async isAvailable(){try{return await He(`kokoro-tts`,[`--version`]),!0}catch{return!1}}async generateVoiceover(e){let{text:t,voice:n=`af_sarah`,speed:r=1,outputPath:i}=e,a=await d.default.mkdtemp(p.default.join(f.default.tmpdir(),`kokoro-`)),o=p.default.join(a,`input.txt`);try{return await d.default.writeFile(o,t,`utf-8`),await He(`kokoro-tts`,[o,i,`--voice`,n,`--speed`,r.toString()]),{outputPath:i}}catch(e){throw Error(`Failed to generate voiceover: ${e instanceof Error?e.message:String(e)}`,{cause:e})}finally{try{await d.default.rm(a,{recursive:!0,force:!0})}catch{}}}listVoices(){return[...Ue]}};N=x([(0,c.injectable)()],N);const P={AceStepMusicService:Symbol.for(`AceStepMusicService`),RenderService:Symbol.for(`RenderService`),MediaInfoService:Symbol.for(`MediaInfoService`),CaptionService:Symbol.for(`CaptionService`),FontService:Symbol.for(`FontService`),VoiceoverService:Symbol.for(`VoiceoverService`),VideoDownloadService:Symbol.for(`VideoDownloadService`),VideoAnalysisService:Symbol.for(`VideoAnalysisService`),VideoContextService:Symbol.for(`VideoContextService`),ProxyUrl:Symbol.for(`ProxyUrl`),Tool:Symbol.for(`Tool`)};function F(e,t){if(typeof Reflect==`object`&&typeof Reflect.metadata==`function`)return Reflect.metadata(e,t)}function I(e,t){return function(n,r){t(n,r,e)}}const L=(0,m.promisify)(l.execFile);let R=class{constructor(e=null){this.proxyUrl=e}getProxyArgs(){let e=this.proxyUrl||process.env.YTDLP_PROXY;return e?[`--proxy`,e]:[]}async ensureYtDlp(){try{let{stdout:e}=await L(`which`,[`yt-dlp`]);return e.trim()}catch{throw Error(`yt-dlp is not installed. Install it with: brew install yt-dlp`)}}async search(e,t){let n=await this.ensureYtDlp(),r=`ytsearch${t}:${e}`,{stdout:i}=await L(n,[...this.getProxyArgs(),r,`--flat-playlist`,`--print`,`%(id)s %(title)s %(duration)s %(url)s %(channel)s %(view_count)s %(upload_date)s`,`--no-warnings`],{timeout:3e4});return i.trim().split(`
|
|
5
|
+
`).filter(Boolean).map(e=>{let[t,n,r,i,a,o,s]=e.split(` `);return{id:t??``,title:n??``,duration:Number.parseFloat(r??`0`)||0,url:i||`https://www.youtube.com/watch?v=${t}`,channel:a??``,viewCount:Number.parseInt(o??`0`,10)||0,uploadDate:s??``}})}async downloadClip(e,t,n,r,i,a=1080){let o=await this.ensureYtDlp();(0,u.existsSync)(t)||(0,u.mkdirSync)(t,{recursive:!0});let s=p.default.join(t,`${n}.mp4`),c=[...this.getProxyArgs(),`-f`,`bestvideo[height<=${a}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${a}][ext=mp4]/best`,`--merge-output-format`,`mp4`,`-o`,s,`--no-warnings`,`--no-playlist`];if(r&&i?c.push(`--download-sections`,`*${r}-${i}`):r?c.push(`--download-sections`,`*${r}-inf`):i&&c.push(`--download-sections`,`*0:00-${i}`),c.push(e),await L(o,c,{timeout:12e4}),!(0,u.existsSync)(s))throw Error(`Download failed: output file not found at ${s}`);let{width:l,height:d,duration:f}=await this.getMediaInfo(s);return{outputPath:s,publicPath:p.default.relative(t,s),duration:f,width:l,height:d}}async getMediaInfo(e){let{stdout:t}=await L(`ffprobe`,[`-v`,`error`,`-select_streams`,`v:0`,`-show_entries`,`stream=width,height:format=duration`,`-of`,`json`,e]),n=JSON.parse(t),r=n.streams?.[0],i=Number.parseFloat(n.format?.duration??`0`);return{width:Number(r?.width??0),height:Number(r?.height??0),duration:i}}};R=x([(0,c.injectable)(),I(0,(0,c.inject)(P.ProxyUrl)),I(0,(0,c.optional)()),F(`design:paramtypes`,[Object])],R);const We=(0,m.promisify)(l.execFile),Ge=1280,Ke=p.default.join(f.default.homedir(),`.gemini`,`oauth_creds.json`);let z=class{async analyzeVideos(e){let t=e.model||`gemini-2.5-flash-lite`,n=e.videoPaths.map(e=>p.default.resolve(e));if(n.length===0)throw Error(`At least one video path is required.`);let r=this.resolveWorkspaceRoot(process.cwd()),i=h.default.randomUUID().toLowerCase(),a=h.default.randomUUID().toLowerCase(),o=await d.default.mkdtemp(p.default.join(f.default.tmpdir(),`video-editor-mcp-video.`));try{let s=await this.readAccessToken(),c=process.env.CODE_ASSIST_ENDPOINT||`https://cloudcode-pa.googleapis.com`,l=process.env.CODE_ASSIST_API_VERSION||`v1internal`,{resolvedProject:d,projectFromEnv:f}=await this.resolveProject(s,c,l),p=new AbortController,m=setTimeout(()=>p.abort(),6e5);try{let m=await this.prepareVideos(n,o),h={model:t,project:d,user_prompt_id:a,request:{contents:[{role:`user`,parts:[{text:this.buildSessionContext(r)},{text:e.prompt},...m.map(e=>({inlineData:{mimeType:e.mimeType,data:u.default.readFileSync(e.transcodedPath).toString(`base64`)}}))]}],session_id:i}},g=await fetch(`${c}/${l}:generateContent`,{method:`POST`,signal:p.signal,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${s}`,"User-Agent":`GeminiCLI-mcp-video-editor/0.0.0`,...f?{"X-Goog-User-Project":f}:{}},body:JSON.stringify(h)}),_=await g.text(),v=this.safeJsonParse(_);if(!g.ok)throw Error(`generateContent failed with ${g.status}: ${_}`);let y=v,b=this.extractResponseText(v);return{resolvedProject:d,model:t,prompt:e.prompt,videoCount:m.length,videos:m,responseText:b,rawResponse:v??_,traceId:this.getString(v,`traceId`),responseId:this.getString(y?.response,`responseId`)}}finally{clearTimeout(m)}}finally{await d.default.rm(o,{recursive:!0,force:!0})}}resolveWorkspaceRoot(e=process.cwd()){let t=[`pnpm-workspace.yaml`,`nx.json`,`.git`],n=p.default.resolve(e);for(;;){for(let e of t)if((0,u.existsSync)(p.default.join(n,e)))return n;let r=p.default.dirname(n);if(r===n)return p.default.resolve(e);n=r}}async readAccessToken(e=process.env.GEMINI_OAUTH_CREDS_FILE||Ke){if(!u.default.existsSync(e))throw Error(`OAuth credentials file not found: ${e}`);let t=await d.default.readFile(e,`utf8`),n=JSON.parse(t),r=this.parseToken(n);if(r.refreshToken){let e=await fetch(`https://oauth2.googleapis.com/token`,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:new URLSearchParams({client_id:`681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com`,client_secret:`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`,grant_type:`refresh_token`,refresh_token:r.refreshToken})});if(!e.ok){let t=await e.text();throw Error(`Failed to refresh OAuth token: ${e.status} ${t}`)}let t=await e.json();if(typeof t.access_token==`string`&&t.access_token)return t.access_token;throw Error(`OAuth refresh response did not include access_token.`)}if(r.accessToken)return r.accessToken;throw Error(`No access token found in ${e}.`)}parseToken(e){if(!e||typeof e!=`object`)return{};let t=e;return t.token&&typeof t.token==`object`?{accessToken:typeof t.token.accessToken==`string`?t.token.accessToken:void 0,refreshToken:typeof t.token.refreshToken==`string`?t.token.refreshToken:void 0}:{accessToken:typeof t.access_token==`string`?t.access_token:void 0,refreshToken:typeof t.refresh_token==`string`?t.refresh_token:void 0}}async resolveProject(e,t,n){let r=process.env.GOOGLE_CLOUD_PROJECT||process.env.GOOGLE_CLOUD_PROJECT_ID||void 0,i=await fetch(`${t}/${n}:loadCodeAssist`,{method:`POST`,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${e}`,"User-Agent":`GeminiCLI-mcp-video-editor/0.0.0`},body:JSON.stringify({metadata:{ideType:`IDE_UNSPECIFIED`,platform:`PLATFORM_UNSPECIFIED`,pluginType:`GEMINI`,...r?{duetProject:r}:{}},...r?{cloudaicompanionProject:r}:{}})}),a=await i.text();if(!i.ok)throw Error(`loadCodeAssist failed with ${i.status}: ${a}`);return{resolvedProject:this.safeJsonParse(a)?.cloudaicompanionProject||r||``,projectFromEnv:r}}async prepareVideos(e,t){let n=[];for(let[r,i]of e.entries()){if(!u.default.existsSync(i))throw Error(`Video file not found: ${i}`);let e=(await d.default.stat(i)).size,a=p.default.join(t,`${p.default.basename(i,p.default.extname(i))}-${r}.mp4`);await We(`ffmpeg`,[`-y`,`-i`,i,`-map`,`0:v:0`,`-map`,`0:a?`,`-vf`,`scale='if(gt(iw,${Ge}),${Ge},iw)':-2`,`-c:v`,`libx264`,`-preset`,`veryfast`,`-crf`,`28`,`-pix_fmt`,`yuv420p`,`-c:a`,`aac`,`-b:a`,`128k`,`-movflags`,`+faststart`,a],{timeout:600*1e3});let o=(await d.default.stat(a)).size;n.push({sourcePath:i,transcodedPath:a,originalSizeBytes:e,transcodedSizeBytes:o,mimeType:`video/mp4`})}return n}buildSessionContext(e){let t=new Date().toLocaleDateString(void 0,{weekday:`long`,year:`numeric`,month:`long`,day:`numeric`}),n=p.default.join(f.default.homedir(),`.gemini`,`tmp`,p.default.basename(e)),r=this.getWorkspaceTree(e);return`
|
|
6
|
+
<session_context>
|
|
7
|
+
This is the Gemini CLI. We are setting up the context for our chat.
|
|
8
|
+
Today's date is ${t} (formatted according to the user's locale).
|
|
9
|
+
My operating system is: ${process.platform}
|
|
10
|
+
The project's temporary directory is: ${n}
|
|
11
|
+
- **Workspace Directories:**
|
|
12
|
+
- ${e}
|
|
13
|
+
- **Directory Structure:**
|
|
14
|
+
|
|
15
|
+
${r}
|
|
16
|
+
</session_context>`.trim()}getWorkspaceTree(e,t=3,n=200){let r=[`${e}/`],i=0,a=(e,o,s)=>{if(i>=n||s>t)return;let c;try{c=u.default.readdirSync(e,{withFileTypes:!0}).filter(e=>![`.git`,`node_modules`,`.nx-cache`].includes(e.name)).sort((e,t)=>e.name.localeCompare(t.name))}catch{return}for(let l=0;l<c.length&&i<n;l+=1){let n=c[l],u=l===c.length-1,d=u?`└───`:`├───`,f=`${o}${u?` `:`│ `}`,m=p.default.join(e,n.name);if(i+=1,n.isDirectory()){r.push(`${o}${d}${n.name}/`),s<t&&a(m,f,s+1);continue}r.push(`${o}${d}${n.name}`)}};return a(e,``,0),i>=n&&r.push(`...`),r.join(`
|
|
17
|
+
`)}extractResponseText(e){return(e?.response?.candidates?.[0]?.content?.parts??[]).map(e=>e.text??``).filter(Boolean).join(``)}safeJsonParse(e){try{return JSON.parse(e)}catch{return}}getString(e,t){if(!e||typeof e!=`object`)return;let n=e;return typeof n[t]==`string`?n[t]:void 0}};z=x([(0,c.injectable)()],z);const B=(0,m.promisify)(l.execFile);let V=class{constructor(e=new C){this.captionService=e}async extractVideoContext(e){let t=p.default.resolve(e.videoPath);if(!(0,u.existsSync)(t))throw Error(`Video file not found: ${t}`);let n=e.basename??p.default.basename(t,p.default.extname(t)),r=p.default.resolve(e.outputDir??this.defaultOutputDir(t,n)),i=p.default.join(r,`work`),a=p.default.join(r,`manifest.json`);await d.default.mkdir(r,{recursive:!0}),await this.probeVideo(t);let o=e.transcribe===!1?{text:``,segments:[]}:await this.transcribeVideo(t,r,e),s=o.segments.length>0?await this.createProxyVideo(t,i,e):void 0,c={transcript:{...o,segments:s?await this.attachFilmstripsToSegments(s,r,o.segments,e):[]},next_actions:[`Review segments filmstrips for visual inspection`]};return await d.default.writeFile(a,`${JSON.stringify(c,null,2)}\n`,`utf8`),c}async probeVideo(e){let{stdout:t}=await B(`ffprobe`,[`-v`,`error`,`-select_streams`,`v:0`,`-show_entries`,`stream=width,height:format=duration`,`-of`,`json`,e]),n=JSON.parse(t),r=n.streams?.[0],i=Number.parseFloat(n.format?.duration??`0`),a=Number(r?.width??0),o=Number(r?.height??0);if(!i||Number.isNaN(i))throw Error(`Invalid duration for video: ${e}`);if(!a||!o)throw Error(`No video stream found in: ${e}`);return{durationSeconds:i,width:a,height:o}}defaultOutputDir(e,t){let n=this.resolveWorkspaceRoot(p.default.dirname(e)),r=n?p.default.join(n,`.tmp`,`video-editor-mcp`):f.default.tmpdir();return p.default.join(r,`${t}-context`)}resolveWorkspaceRoot(e){let t=[`pnpm-workspace.yaml`,`nx.json`,`.git`],n=p.default.resolve(e);for(;;){if(t.some(e=>(0,u.existsSync)(p.default.join(n,e))))return n;let e=p.default.dirname(n);if(e===n)return;n=e}}async createProxyVideo(e,t,n){let r=p.default.join(t,`proxy.mp4`),i=Math.max(n.tileWidth??250,500);return await d.default.mkdir(t,{recursive:!0}),await B(`ffmpeg`,[`-y`,`-i`,e,`-vf`,`scale='min(${i},iw)':-2`,`-c:v`,`libx264`,`-preset`,`veryfast`,`-crf`,`28`,`-pix_fmt`,`yuv420p`,`-an`,r]),r}async transcribeVideo(e,t,n){let r=await this.resolveWhisperCommand(),i=p.default.join(t,`audio.wav`);await B(`ffmpeg`,[`-y`,`-i`,e,`-vn`,`-ac`,`1`,`-ar`,`16000`,i]),r.engine===`whisper`?await this.runPythonWhisper(r.binary,i,t,n):await this.runWhisperCpp(r.binary,i,t,n);let a=this.firstExistingPath([p.default.join(t,`audio.txt`),p.default.join(t,`transcript.txt`),`${i}.txt`]),o=this.firstExistingPath([p.default.join(t,`audio.srt`),p.default.join(t,`transcript.srt`),`${i}.srt`]),s=a?(await d.default.readFile(a,`utf8`)).trim():``,c=o?this.captionService.parseSrt(await d.default.readFile(o,`utf8`)):[];return{engine:r.engine,text:s,txtPath:a,srtPath:o,segments:c}}async resolveWhisperCommand(){for(let e of[`whisper-cli`,`whisper.cpp`,`whisper`]){let t=await this.findBinary(e);if(t)return{binary:t,engine:e}}throw Error(`No supported Whisper binary found. Install whisper.cpp or OpenAI Whisper, then retry.`)}async findBinary(e){try{let{stdout:t}=await B(`which`,[e]);return t.trim()||void 0}catch{return}}async runPythonWhisper(e,t,n,r){let i=[t,`--model`,r.whisperModel??`base`,`--output_dir`,n,`--output_format`,`all`];r.language&&i.push(`--language`,r.language),await B(e,i,{timeout:1200*1e3})}async runWhisperCpp(e,t,n,r){let i=r.whisperModelPath??process.env.WHISPER_MODEL_PATH??this.resolveDefaultWhisperModelPath();if(!i||!(0,u.existsSync)(i))throw Error(`Whisper.cpp model not found. Provide whisperModelPath or set WHISPER_MODEL_PATH.`);let a=[`-m`,i,`-f`,t,`-otxt`,`-osrt`,`-of`,p.default.join(n,`transcript`)];r.language&&a.push(`-l`,r.language),await B(e,a,{timeout:1200*1e3})}resolveDefaultWhisperModelPath(){return[p.default.join(f.default.homedir(),`.whisper`,`ggml-base.en.bin`),p.default.join(f.default.homedir(),`.whisper`,`ggml-small.en.bin`),`/opt/homebrew/share/whisper-cpp/ggml-base.en.bin`,`/usr/local/share/whisper-cpp/ggml-base.en.bin`].find(e=>(0,u.existsSync)(e))}async attachFilmstripsToSegments(e,t,n,r){let i=p.default.join(t,`slides`);await d.default.mkdir(i,{recursive:!0});let a=[];for(let[t,o]of n.entries()){let n=await this.extractSegmentFilmstrips(e,i,t,o,r);a.push({...o,text:o.text.trim(),filmstrips:n})}return a}async extractSegmentFilmstrips(e,t,n,r,i){let a=(i.imageFormat??`jpeg`)===`png`?`png`:`jpg`,o=i.framesPerSecond??3,s=i.tileWidth??250,c=i.filmstripColumns??6,l=c*(i.filmstripRows??4),u=Math.max((r.endMs-r.startMs)/1e3,1/o),d=Math.max(1,Math.ceil(u*o)),f=[];for(let i=0,u=0;i<d;i+=l,u+=1){let m=Math.min(l,d-i),h=Math.min(c,m),g=Math.ceil(m/h),_=r.startMs+Math.round(i/o*1e3),v=Math.min(m/o,Math.max((r.endMs-_)/1e3,1/o)),y=p.default.join(t,`segment_${String(n+1).padStart(4,`0`)}_filmstrip_${String(u+1).padStart(3,`0`)}.${a}`);await B(`ffmpeg`,[`-y`,`-ss`,(_/1e3).toFixed(3),`-t`,v.toFixed(3),`-i`,e,`-vf`,[`fps=${o}`,`scale='min(${s},iw)':-2`,`pad=iw:ih+32:0:0:white`,`tile=${h}x${g}:padding=8:margin=8:color=white`].join(`,`),`-frames:v`,`1`,y]),await this.annotateFilmstrip(y,_,o,m,h,g),f.push({path:y,index:u,startMs:_,endMs:Math.min(r.endMs,_+Math.round(v*1e3)),frameCount:m,tileWidth:s,columns:h,rows:g})}return f}async annotateFilmstrip(e,t,n,r,i,a){let o=await(0,y.default)(e).metadata();if(!o.width||!o.height)throw Error(`Unable to annotate filmstrip without image dimensions: ${e}`);let s=(o.width-16-8*(i-1))/i,c=(o.height-16-8*(a-1))/a,l=Array.from({length:r},(e,r)=>{let a=r%i,o=Math.floor(r/i),l=t+Math.round(r/n*1e3);return`<text x="${8+a*(s+8)+s/2}" y="${8+o*(c+8)+c-32+22}" text-anchor="middle">${l} ms</text>`}).join(``),u=Buffer.from(`<svg width="${o.width}" height="${o.height}" xmlns="http://www.w3.org/2000/svg"><style>text{font-family:Arial,sans-serif;font-size:18px;fill:#000}</style>${l}</svg>`),f=await(0,y.default)(e).composite([{input:u,top:0,left:0}]).toBuffer();await d.default.writeFile(e,f)}firstExistingPath(e){return e.find(e=>(0,u.existsSync)(e))}};V=x([(0,c.injectable)(),F(`design:paramtypes`,[Object])],V);let H=class{success(e){return{content:[{type:`text`,text:e}]}}successJson(e){return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}error(e){return{content:[{type:`text`,text:`Error: ${e}`}],isError:!0}}async safeExecute(e){try{return await e()}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};H=x([(0,c.injectable)()],H);var qe,Je;const Ye=b.z.object({url:b.z.string().describe(`Video URL from YouTube, TikTok, Instagram, or other yt-dlp supported sites`),filename:b.z.string().describe(`Output filename without extension (e.g., "my_clip")`),startTime:b.z.string().optional().describe(`Start time for clip extraction (e.g., "0:30", "1:15")`),endTime:b.z.string().optional().describe(`End time for clip extraction (e.g., "1:00", "2:30")`),maxHeight:b.z.number().int().positive().default(1080).describe(`Maximum video height in pixels (default: 1080)`),outputDir:b.z.string().optional().describe(`Custom output directory (default: Remotion public/videos/ for direct use in compositions)`)}),Xe=()=>(0,u.existsSync)(p.default.resolve(__dirname,`../package.json`))?p.default.resolve(__dirname,`..`):p.default.resolve(__dirname,`../..`);let Ze=class extends H{static{Je=this}static TOOL_NAME=`download_clip`;constructor(e){super(),this.videoDownloadService=e}getInputSchema(){return Ye}getDefinition(){return{name:Je.TOOL_NAME,description:`Download a video clip from YouTube, TikTok, Instagram, or any yt-dlp supported site with optional time range trimming. Saves to Remotion public/videos/ by default for direct use in compositions.`,inputSchema:b.z.toJSONSchema(Ye,{reused:`inline`})}}async execute(e){try{let t=Ye.parse(e),n=p.default.resolve(Xe(),`public/videos`),r=t.outputDir??n,i=await this.videoDownloadService.downloadClip(t.url,r,t.filename,t.startTime,t.endTime,t.maxHeight),a=t.outputDir?void 0:`/videos/${t.filename}.mp4`;return this.successJson({...i,compositionSrc:a,suggestedComposition:i.height>i.width?`Vertical`:`Main`})}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};Ze=Je=x([(0,c.injectable)(),I(0,(0,c.inject)(P.VideoDownloadService)),F(`design:paramtypes`,[typeof(qe=R!==void 0&&R)==`function`?qe:Object])],Ze);var Qe,$e;const U=b.z.object({videoPath:b.z.string().min(1).describe(`Path to the local video file to extract context from`),outputDir:b.z.string().optional().describe(`Directory to write transcript, filmstrips, and manifest`),basename:b.z.string().optional().describe(`Base name for the generated context directory when outputDir is omitted`),framesPerSecond:b.z.number().positive().default(3).describe(`Frames per second to sample for raw frames and segment slides`),imageFormat:b.z.enum([`jpeg`,`png`]).default(`jpeg`).describe(`Frame image format`),tileWidth:b.z.number().int().positive().default(250).describe(`Width of each filmstrip tile in pixels`),filmstripColumns:b.z.number().int().positive().default(6).describe(`Maximum columns per segment filmstrip image`),filmstripRows:b.z.number().int().positive().default(4).describe(`Maximum rows per segment filmstrip image`),transcribe:b.z.boolean().default(!0).describe(`Run local Whisper transcription`),language:b.z.string().optional().describe(`Optional Whisper language hint`),whisperModel:b.z.string().optional().describe(`Python Whisper model name, default base`),whisperModelPath:b.z.string().optional().describe(`Whisper.cpp model path override`)});let W=class extends H{static{$e=this}static TOOL_NAME=`extract_video_context`;constructor(e){super(),this.videoContextService=e}getInputSchema(){return U}getDefinition(){return{name:$e.TOOL_NAME,description:`Break a local video into timestamped transcript segments with paged filmstrip images for image-understanding tools`,inputSchema:b.z.toJSONSchema(U,{reused:`inline`})}}async execute(e){try{let t=U.parse(e),n=await this.videoContextService.extractVideoContext(t);return this.successJson(n)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};W=$e=x([(0,c.injectable)(),I(0,(0,c.inject)(P.VideoContextService)),F(`design:paramtypes`,[typeof(Qe=V!==void 0&&V)==`function`?Qe:Object])],W);var et,tt;const nt=b.z.object({captions:b.z.array(b.z.object({text:b.z.string(),startMs:b.z.number(),endMs:b.z.number(),confidence:b.z.number().min(0).max(1).optional(),emphasis:b.z.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).optional()})).describe(`Array of caption objects with text, startMs, and endMs`),combineMs:b.z.number().optional().describe(`Milliseconds threshold for combining captions into pages (default: 100)`),maxWordsPerPage:b.z.number().int().positive().optional().describe(`Maximum words per caption page (default: 6)`),maxCharsPerLine:b.z.number().int().positive().optional().describe(`Target maximum characters per caption line (default: 22)`),platform:b.z.enum([`tiktok`,`reels`,`shorts`,`universal`]).optional().describe(`Platform safe-zone preset`),preset:b.z.enum([`tiktok-bold`,`karaoke`,`premium-pill`,`minimal`,`ugc`]).optional().describe(`Caption visual style preset`)});let G=class extends H{static{tt=this}static TOOL_NAME=`generate_captions`;constructor(e){super(),this.captionService=e}getInputSchema(){return nt}getDefinition(){return{name:tt.TOOL_NAME,description:`Create TikTok-style caption pages from a caption array for video overlay`,inputSchema:b.z.toJSONSchema(nt,{reused:`inline`})}}async execute(e){try{let{captions:t,combineMs:n=100,maxWordsPerPage:r,maxCharsPerLine:i,platform:a,preset:o}=nt.parse(e),s=t.map(e=>({text:e.text,startMs:e.startMs,endMs:e.endMs,confidence:e.confidence,emphasis:e.emphasis})),c=this.captionService.createTikTokCaptions(s,{combineMs:n,maxWordsPerPage:r,maxCharsPerLine:i,platform:a,preset:o});return this.successJson(c)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};G=tt=x([(0,c.injectable)(),I(0,(0,c.inject)(P.CaptionService)),F(`design:paramtypes`,[typeof(et=C!==void 0&&C)==`function`?et:Object])],G);var rt,it;const at=b.z.object({prompt:b.z.string().describe(`Music description for ACE-Step text-to-music generation`),filename:b.z.string().optional().describe(`Output filename without extension (default: ace-step-music)`),outputPath:b.z.string().optional().describe(`Explicit output audio path. Overrides filename/outputDir.`),outputDir:b.z.string().optional().describe(`Custom output directory (default: Remotion public/music/ for direct use in compositions)`),aceStepPath:b.z.string().optional().describe(`Path to local ACE-Step-1.5 checkout (default: ACE_STEP_15_DIR or ~/workspace/ACE-Step-1.5)`),lyrics:b.z.string().optional().describe(`Lyrics to use. Defaults to [Instrumental] for background music.`),duration:b.z.number().min(10).max(600).optional().describe(`Target duration in seconds (default: 30)`),bpm:b.z.number().min(30).max(300).optional().describe(`Optional BPM metadata`),keyscale:b.z.string().optional().describe(`Optional key/scale metadata, e.g. C minor`),timeSignature:b.z.string().optional().describe(`Optional time signature metadata, e.g. 4/4`),vocalLanguage:b.z.string().optional().describe(`Vocal language code, or unknown (default: unknown)`),seed:b.z.number().int().optional().describe(`Optional fixed seed for reproducible generation`),inferenceSteps:b.z.number().int().positive().optional().describe(`Diffusion inference steps (default: 8)`),audioFormat:b.z.enum([`wav`,`mp3`,`flac`]).default(`wav`).describe(`Output audio format (default: wav)`),backend:b.z.enum([`mlx`,`pt`,`vllm`]).default(`mlx`).describe(`ACE-Step LM backend (default: mlx for Apple Silicon)`),configPath:b.z.string().optional().describe(`ACE-Step DiT model config (default: acestep-v15-turbo)`),lmModelPath:b.z.string().optional().describe(`ACE-Step LM model (default: acestep-5Hz-lm-0.6B)`),timeoutMs:b.z.number().int().positive().optional().describe(`Generation timeout in milliseconds`)}),ot=()=>(0,u.existsSync)(p.default.resolve(__dirname,`../package.json`))?p.default.resolve(__dirname,`..`):p.default.resolve(__dirname,`../..`);let st=class extends H{static{it=this}static TOOL_NAME=`generate_music`;constructor(e){super(),this.aceStepMusicService=e}getInputSchema(){return at}getDefinition(){return{name:it.TOOL_NAME,description:`Generate background music using a local ACE-Step 1.5 checkout. Defaults to MLX on Apple Silicon and saves to Remotion public/music/.`,inputSchema:b.z.toJSONSchema(at,{reused:`inline`})}}async execute(e){try{let t=at.parse(e);if(!await this.aceStepMusicService.isAvailable(t.aceStepPath))return this.error(`ACE-Step 1.5 is not available. Clone https://github.com/ACE-Step/ACE-Step-1.5 and set ACE_STEP_15_DIR or pass aceStepPath.`);let n=this.resolveOutputPath(t),r=await this.aceStepMusicService.generateMusic({prompt:t.prompt,outputPath:n,aceStepPath:t.aceStepPath,lyrics:t.lyrics,duration:t.duration,bpm:t.bpm,keyscale:t.keyscale,timeSignature:t.timeSignature,vocalLanguage:t.vocalLanguage,seed:t.seed,inferenceSteps:t.inferenceSteps,audioFormat:t.audioFormat,backend:t.backend,configPath:t.configPath,lmModelPath:t.lmModelPath,timeoutMs:t.timeoutMs}),i=p.default.resolve(ot(),`public/music`),a=p.default.dirname(n)===i?`/music/${p.default.basename(n)}`:void 0;return this.successJson({...r,compositionSrc:a,suggestedClip:a?{type:`audio`,src:a,startFrame:0,volume:.35}:void 0})}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}resolveOutputPath(e){if(e.outputPath)return p.default.resolve(e.outputPath);let t=p.default.resolve(e.outputDir??p.default.join(ot(),`public/music`)),n=e.filename??`ace-step-music`,r=e.audioFormat??`wav`;return p.default.join(t,`${n}.${r}`)}};st=it=x([(0,c.injectable)(),I(0,(0,c.inject)(P.AceStepMusicService)),F(`design:paramtypes`,[typeof(rt=S!==void 0&&S)==`function`?rt:Object])],st);var ct,lt;const ut=b.z.object({text:b.z.string().describe(`Text to convert to speech`),voice:b.z.string().optional().describe(`Voice ID (default: af_sarah). Options: af_sarah, af_nicole, af_bella, af_sky, am_adam, am_michael, bf_emma, bf_isabella, bm_george, bm_lewis`),speed:b.z.number().optional().describe(`Speech speed multiplier (default: 1.0)`),outputPath:b.z.string().describe(`Output file path for the generated audio`)});let K=class extends H{static{lt=this}static TOOL_NAME=`generate_voiceover`;constructor(e){super(),this.voiceoverService=e}getInputSchema(){return ut}getDefinition(){return{name:lt.TOOL_NAME,description:`Generate voiceover audio from text using Kokoro TTS local engine`,inputSchema:b.z.toJSONSchema(ut,{reused:`inline`})}}async execute(e){try{let t=ut.parse(e);if(!await this.voiceoverService.isAvailable())return this.error(`Kokoro TTS is not installed. Install it with: pip install kokoro-tts`);let n=await this.voiceoverService.generateVoiceover({text:t.text,voice:t.voice,speed:t.speed,outputPath:t.outputPath});return this.successJson(n)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};K=lt=x([(0,c.injectable)(),I(0,(0,c.inject)(P.VoiceoverService)),F(`design:paramtypes`,[typeof(ct=N!==void 0&&N)==`function`?ct:Object])],K);var dt,ft;const pt=b.z.object({src:b.z.string().describe(`Path to the media file`),type:b.z.enum([`video`,`audio`]).optional().describe(`Media type (default: video)`)});let q=class extends H{static{ft=this}static TOOL_NAME=`get_media_info`;constructor(e){super(),this.mediaInfoService=e}getInputSchema(){return pt}getDefinition(){return{name:ft.TOOL_NAME,description:`Get duration, dimensions, and decodability info for video/audio files`,inputSchema:b.z.toJSONSchema(pt,{reused:`inline`})}}async execute(e){try{let{src:t,type:n=`video`}=pt.parse(e),r=await this.mediaInfoService.canDecode(t);if(n===`audio`){let e=await this.mediaInfoService.getAudioDuration(t);return this.successJson({src:t,type:`audio`,duration:e,canDecode:r})}let[i,a]=await Promise.all([this.mediaInfoService.getVideoDuration(t),this.mediaInfoService.getVideoDimensions(t)]);return this.successJson({src:t,type:`video`,duration:i,width:a.width,height:a.height,canDecode:r})}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};q=ft=x([(0,c.injectable)(),I(0,(0,c.inject)(P.MediaInfoService)),F(`design:paramtypes`,[typeof(dt=T!==void 0&&T)==`function`?dt:Object])],q);var mt,ht;const gt=b.z.object({srtContent:b.z.string().describe(`SRT file content as a string`)});let J=class extends H{static{ht=this}static TOOL_NAME=`import_srt`;constructor(e){super(),this.captionService=e}getInputSchema(){return gt}getDefinition(){return{name:ht.TOOL_NAME,description:`Parse SRT subtitle file content into structured Caption format for video overlay`,inputSchema:b.z.toJSONSchema(gt,{reused:`inline`})}}async execute(e){try{let t=gt.parse(e),n=this.captionService.parseSrt(t.srtContent);return this.successJson({captionCount:n.length,captions:n})}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};J=ht=x([(0,c.injectable)(),I(0,(0,c.inject)(P.CaptionService)),F(`design:paramtypes`,[typeof(mt=C!==void 0&&C)==`function`?mt:Object])],J);var _t;const vt=b.z.object({});let Y=class extends H{static{_t=this}static TOOL_NAME=`list_compositions`;getInputSchema(){return vt}getDefinition(){return{name:_t.TOOL_NAME,description:`List all available Remotion compositions with their schemas, dimensions, and format info`,inputSchema:b.z.toJSONSchema(vt,{reused:`inline`})}}async execute(){let e=[{id:`Main`,description:`Main 16:9 landscape composition`,width:1920,height:1080,fps:30,durationInFrames:null,defaultProps:{clips:[],audioVolume:1,backgroundColor:`#000000`}},{id:`Vertical`,description:`Vertical 9:16 portrait composition (TikTok, Instagram Stories)`,width:1080,height:1920,fps:30,durationInFrames:null,defaultProps:{clips:[],audioVolume:1,backgroundColor:`#000000`}},{id:`Square`,description:`Square 1:1 composition (Instagram posts)`,width:1080,height:1080,fps:30,durationInFrames:null,defaultProps:{clips:[],audioVolume:1,backgroundColor:`#000000`}}];return this.successJson({compositionCount:e.length,compositions:e})}};Y=_t=x([(0,c.injectable)()],Y);var yt,bt;const X=b.z.object({compositionId:b.z.string().describe(`Remotion composition ID (e.g., "Main", "Vertical", "Square")`),inputProps:b.z.record(b.z.string(),b.z.unknown()).describe(`JSON props to pass to the composition`),outputPath:b.z.string().describe(`Output file path for the rendered image`),frame:b.z.number().optional().describe(`Frame number to render (default: 0)`),imageFormat:b.z.enum([`png`,`jpeg`]).optional().describe(`Image format (default: png)`)});let Z=class extends H{static{bt=this}static TOOL_NAME=`preview_frame`;constructor(e){super(),this.renderService=e}getInputSchema(){return X}getDefinition(){return{name:bt.TOOL_NAME,description:`Render a single frame from a composition as a PNG or JPEG image for preview`,inputSchema:b.z.toJSONSchema(X,{reused:`inline`})}}async execute(e){try{let t=X.parse(e),n={compositionId:t.compositionId,inputProps:t.inputProps,outputPath:t.outputPath,frame:t.frame,imageFormat:t.imageFormat},r=await this.renderService.renderStill(n);return this.successJson(r)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};Z=bt=x([(0,c.injectable)(),I(0,(0,c.inject)(P.RenderService)),F(`design:paramtypes`,[typeof(yt=M!==void 0&&M)==`function`?yt:Object])],Z);var xt,St;const Ct=b.z.preprocess(e=>{if(typeof e!=`string`)return e;try{return JSON.parse(e)}catch{return e}},b.z.record(b.z.string(),b.z.unknown())),wt=b.z.preprocess(e=>{if(typeof e!=`string`)return e;let t=e.trim().toLowerCase();return t===`true`?!0:t===`false`?!1:e},b.z.boolean()),Tt=b.z.object({compositionId:b.z.string().describe(`Remotion composition ID (e.g., "Main", "Vertical", "Square")`),inputProps:Ct.describe(`JSON props to pass to the composition`),outputPath:b.z.string().describe(`Output file path for the rendered video`),codec:b.z.enum([`h264`,`h265`,`vp8`,`vp9`]).optional().describe(`Video codec (default: h264)`),dryRun:wt.optional().describe(`Validate props and assets without rendering a video`)});let Q=class extends H{static{St=this}static TOOL_NAME=`render_video`;constructor(e){super(),this.renderService=e}getInputSchema(){return Tt}getDefinition(){return{name:St.TOOL_NAME,description:"Render a video from JSON props using Remotion. `inputProps` must conform to the Main composition schema. Full JSON Schema (generated from `src/schemas/compositions.ts` -> `MainCompositionSchema`): `schemas/main-composition.schema.json` (relative to this package; in the monorepo: `packages/mcp/video-editor-mcp/schemas/main-composition.schema.json`). Validation runs server-side; failures return a `render_validation_failed` error with per-issue paths and suggested fixes.",inputSchema:b.z.toJSONSchema(Tt,{reused:`inline`})}}async execute(e){try{let t=Tt.parse(e),n={compositionId:t.compositionId,inputProps:t.inputProps,outputPath:t.outputPath,codec:t.codec,dryRun:t.dryRun},r=await this.renderService.render(n);return this.successJson(r)}catch(e){return e instanceof Ve?{content:[{type:`text`,text:JSON.stringify({error:`render_validation_failed`,message:e.message,summary:e.validation.summary,issues:e.validation.errors,warnings:e.validation.warnings,suggestedFixes:e.validation.errors.filter(e=>e.fix).map(e=>({code:e.code,path:e.path,...e.fix}))},null,2)}],isError:!0}:this.error(e instanceof Error?e.message:`Unknown error`)}}};Q=St=x([(0,c.injectable)(),I(0,(0,c.inject)(P.RenderService)),F(`design:paramtypes`,[typeof(xt=M!==void 0&&M)==`function`?xt:Object])],Q);var Et,Dt;const Ot=b.z.object({query:b.z.string().describe(`Search query for YouTube videos`),maxResults:b.z.number().int().positive().max(20).default(5).describe(`Maximum number of results (default: 5, max: 20)`)});let kt=class extends H{static{Dt=this}static TOOL_NAME=`search_videos`;constructor(e){super(),this.videoDownloadService=e}getInputSchema(){return Ot}getDefinition(){return{name:Dt.TOOL_NAME,description:`Search YouTube for videos by query. Returns video metadata (title, duration, URL, channel) without downloading.`,inputSchema:b.z.toJSONSchema(Ot,{reused:`inline`})}}async execute(e){try{let t=Ot.parse(e),n=await this.videoDownloadService.search(t.query,t.maxResults);return this.successJson({query:t.query,resultCount:n.length,results:n})}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};kt=Dt=x([(0,c.injectable)(),I(0,(0,c.inject)(P.VideoDownloadService)),F(`design:paramtypes`,[typeof(Et=R!==void 0&&R)==`function`?Et:Object])],kt);var At,jt;const Mt=b.z.object({videoPaths:b.z.array(b.z.string().min(1)).min(1).describe(`Paths to local video files to analyze`),prompt:b.z.string().min(1).describe(`Prompt to send with the video(s)`),model:b.z.string().default(`gemini-2.5-flash-lite`).describe(`Gemini model to use (default: gemini-2.5-flash-lite)`)});let $=class extends H{static{jt=this}static TOOL_NAME=`analyze_video`;constructor(e){super(),this.videoAnalysisService=e}getInputSchema(){return Mt}getDefinition(){return{name:jt.TOOL_NAME,description:`Analyze one or more local video files by transcoding them to a compact MP4 and sending them with a prompt to Gemini Code Assist.`,inputSchema:b.z.toJSONSchema(Mt,{reused:`inline`})}}async execute(e){try{let t=Mt.parse(e),n=await this.videoAnalysisService.analyzeVideos({videoPaths:t.videoPaths,prompt:t.prompt,model:t.model});return this.successJson(n)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};$=jt=x([(0,c.injectable)(),I(0,(0,c.inject)(P.VideoAnalysisService)),F(`design:paramtypes`,[typeof(At=z!==void 0&&z)==`function`?At:Object])],$);const Nt=new c.ContainerModule(e=>{e.bind(P.AceStepMusicService).to(S).inSingletonScope(),e.bind(P.RenderService).to(M).inSingletonScope(),e.bind(P.MediaInfoService).to(T).inSingletonScope(),e.bind(P.CaptionService).to(C).inSingletonScope(),e.bind(P.FontService).to(se).inSingletonScope(),e.bind(P.VoiceoverService).to(N).inSingletonScope(),e.bind(P.VideoDownloadService).to(R).inSingletonScope(),e.bind(P.VideoAnalysisService).to(z).inSingletonScope(),e.bind(P.VideoContextService).to(V).inSingletonScope()}),Pt=new c.ContainerModule(e=>{e.bind(P.Tool).to(Q).inSingletonScope(),e.bind(P.Tool).to(Y).inSingletonScope(),e.bind(P.Tool).to(q).inSingletonScope(),e.bind(P.Tool).to(Z).inSingletonScope(),e.bind(P.Tool).to(G).inSingletonScope(),e.bind(P.Tool).to(J).inSingletonScope(),e.bind(P.Tool).to(st).inSingletonScope(),e.bind(P.Tool).to(K).inSingletonScope(),e.bind(P.Tool).to(kt).inSingletonScope(),e.bind(P.Tool).to(Ze).inSingletonScope(),e.bind(P.Tool).to($).inSingletonScope(),e.bind(P.Tool).to(W).inSingletonScope()});function Ft(e={}){let t=new c.Container;return t.load(Nt,Pt),t.bind(P.ProxyUrl).toConstantValue(e.proxyUrl??null),t}const It=new Set([`render_video`,`preview_frame`,`generate_music`,`generate_voiceover`,`download_clip`,`extract_video_context`,`analyze_video`]);function Lt(e){return It.has(e)}async function Rt(e,t,n,r){return Lt(e)?r.run(()=>t.execute(n)):t.execute(n)}function zt(e){let t=new te.Server({name:`video-editor-mcp`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getAll(P.Tool),r=new ce,i=new Map;for(let e of n)i.set(e.getDefinition().name,e);return t.setRequestHandler(ne.ListToolsRequestSchema,async()=>({tools:n.map(e=>e.getDefinition())})),t.setRequestHandler(ne.CallToolRequestSchema,async e=>{let{name:t,arguments:n}=e.params,a=i.get(t);if(!a)return{content:[{type:`text`,text:`Unknown tool: ${t}`}],isError:!0};let o=(0,ee.coerceArgs)(n??{},a.getInputSchema());try{return await Rt(t,a,o,r)}catch(e){if(e instanceof b.z.ZodError)return{content:[{type:`text`,text:(0,ee.formatZodError)(e,{schemaName:t,schema:a.getInputSchema()})}],isError:!0};throw e}}),t}var Bt=class{server;transport=null;constructor(e){this.server=e}async start(){this.transport=new re.StdioServerTransport,await this.server.connect(this.transport),console.error(`video-editor MCP server started on stdio`)}async stop(){this.transport&&=(await this.transport.close(),null)}};Object.defineProperty(exports,`A`,{enumerable:!0,get:function(){return ue}}),Object.defineProperty(exports,`C`,{enumerable:!0,get:function(){return me}}),Object.defineProperty(exports,`D`,{enumerable:!0,get:function(){return _e}}),Object.defineProperty(exports,`E`,{enumerable:!0,get:function(){return fe}}),Object.defineProperty(exports,`M`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`O`,{enumerable:!0,get:function(){return he}}),Object.defineProperty(exports,`S`,{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,`T`,{enumerable:!0,get:function(){return ge}}),Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return Ve}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return Q}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return je}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return J}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return G}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return W}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return $}}),Object.defineProperty(exports,`j`,{enumerable:!0,get:function(){return de}}),Object.defineProperty(exports,`k`,{enumerable:!0,get:function(){return pe}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return q}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return V}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return zt}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return Z}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return H}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return Ft}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return Y}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return Bt}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return K}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return Te}}),Object.defineProperty(exports,`w`,{enumerable:!0,get:function(){return ve}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return Me}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return ye}});
|