@agimon-ai/video-editor-mcp 0.7.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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import"reflect-metadata";import{Container as e,ContainerModule as t,inject as n,injectable as r,optional as i}from"inversify";import{execFile as a}from"node:child_process";import o,{existsSync as s,mkdirSync as c}from"node:fs";import l,{copyFile as u,mkdir as d,readFile as f,stat as p}from"node:fs/promises";import m from"node:os";import h from"node:path";import{promisify as g}from"node:util";import _,{createHash as v}from"node:crypto";import{fileURLToPath as y}from"node:url";import{bundle as ee}from"@remotion/bundler";import{renderMedia as te,renderStill as ne,selectComposition as re}from"@remotion/renderer";import ie from"sharp";import{z as b}from"zod";import{coerceArgs as ae,formatZodError as oe}from"@agimon-ai/foundation-validator";import{Server as se}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as ce,ListToolsRequestSchema as le}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as ue}from"@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 de=g(a);let S=class{async isAvailable(e){let t=this.resolveAceStepPath(e);if(!this.isAceStepCheckout(t))return!1;try{return await de(`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 l.mkdtemp(h.join(m.tmpdir(),`ace-step-music-`)),o=h.join(a,`generation.toml`),s=h.join(a,`output`);await l.mkdir(s,{recursive:!0}),await l.mkdir(h.dirname(e.outputPath),{recursive:!0});try{let a=this.buildConfig({...e,aceStepPath:t,audioFormat:n,backend:r,duration:i,outputDir:s});await l.writeFile(o,a,`utf-8`);let{stdout:c,stderr:u}=await de(`uv`,[`run`,`python`,`cli.py`,`-c`,o,`--backend`,r,`--log-level`,`INFO`],{cwd:t,timeout:e.timeoutMs??1200*1e3,maxBuffer:50*1024*1024}),d=await this.findNewestGeneratedAudio(s,n);return await l.copyFile(d,e.outputPath),{outputPath:e.outputPath,sourcePath:d,duration:i,audioFormat:n,backend:r,aceStepPath:t,stdout:c,stderr:u}}catch(e){throw Error(`Failed to generate ACE-Step music: ${e instanceof Error?e.message:String(e)}`,{cause:e})}finally{await l.rm(a,{recursive:!0,force:!0}).catch(()=>void 0)}}resolveAceStepPath(e){return h.resolve(e??process.env.ACE_STEP_15_DIR??process.env.ACESTEP_15_DIR??h.join(m.homedir(),`workspace`,`ACE-Step-1.5`))}isAceStepCheckout(e){return s(h.join(e,`cli.py`))&&s(h.join(e,`pyproject.toml`))}getFormatFromPath(e){let t=h.extname(e).toLowerCase().replace(`.`,``);return t===`mp3`||t===`flac`||t===`wav`?t:`wav`}async findNewestGeneratedAudio(e,t){let n=await l.readdir(e,{withFileTypes:!0}),r=await Promise.all(n.filter(e=>e.isFile()&&e.name.endsWith(`.${t}`)).map(async t=>{let n=h.join(e,t.name);return{filePath:n,mtimeMs:(await l.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:h.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([r()],S);const fe={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:fe[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 fe[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([r()],C);const pe=[`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 me=class{getAvailableGoogleFonts(){return[...pe]}validateFontFamily(e){let t=e.toLowerCase();return pe.some(e=>e.toLowerCase()===t)}};me=x([r()],me);var he=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=g(a);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([r()],T);const E=b.coerce.number(),D=b.coerce.number().int(),O=b.preprocess(e=>{if(typeof e!=`string`)return e;let t=e.trim().toLowerCase();return t===`true`?!0:t===`false`?!1:e},b.boolean()),k=b.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.`}),ge=b.object({frame:D.nonnegative(),volume:E.min(0).max(1)}),_e=b.object({type:b.enum([`interpolate`,`spring`]).default(`interpolate`),easing:b.enum([`linear`,`ease-in`,`ease-out`,`ease-in-out`]).optional(),springConfig:b.object({mass:E.positive().default(1),damping:E.positive().default(10),stiffness:E.positive().default(100)}).optional(),delay:D.nonnegative().default(0)}),ve=b.object({type:b.enum([`fade`,`slide`,`wipe`,`flip`,`clockWipe`,`none`]).default(`fade`),durationInFrames:D.positive().default(15),direction:b.enum([`from-left`,`from-right`,`from-top`,`from-bottom`]).optional()}),A={startFrame:D.nonnegative(),durationInFrames:D.positive(),premountFor:D.nonnegative().default(30),entrance:_e.optional(),exit:_e.optional(),transition:ve.optional()},ye=b.object({type:b.literal(`video`),...A,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.record(b.string(),b.unknown()).optional()}),be=b.object({type:b.literal(`image`),...A,src:k,fit:b.enum([`contain`,`cover`,`fill`]).optional(),noFadeIn:O.optional(),baseScale:E.positive().optional(),transformOriginX:b.string().optional(),transformOriginY:b.string().optional(),style:b.record(b.string(),b.unknown()).optional()}),xe=b.object({type:b.literal(`text`),...A,text:b.string(),fontSize:E.positive().default(80),fontFamily:b.string().default(`sans-serif`),fontWeight:b.union([b.string(),E]).optional(),color:b.string().default(`#ffffff`),backgroundColor:b.string().optional(),position:b.enum([`top`,`center`,`bottom`]).default(`center`),animation:b.enum([`none`,`fade`,`slide`,`typewriter`,`highlight`]).default(`none`),highlightColor:b.string().optional(),highlightWord:b.string().optional(),style:b.record(b.string(),b.unknown()).optional()}),Se=b.object({type:b.literal(`audio`),...A,src:k,volume:E.min(0).max(1).optional(),volumeAutomation:b.array(ge).optional(),playbackRate:E.positive().optional(),muted:O.optional(),loop:O.optional(),trimBefore:E.nonnegative().optional(),trimAfter:E.nonnegative().optional(),toneFrequency:E.positive().optional()}),Ce=b.object({type:b.literal(`subtitle`),...A,text:b.string(),position:b.enum([`top`,`center`,`bottom`]).default(`bottom`),animation:b.enum([`none`,`fade`,`slide`]).default(`fade`),fontSize:E.positive().default(36),fontFamily:b.string().default(`Arial, sans-serif`),color:b.string().default(`#ffffff`),backgroundColor:b.string().default(`rgba(0, 0, 0, 0.7)`),style:b.record(b.string(),b.unknown()).optional()}),we=b.object({type:b.literal(`gif`),...A,src:k,width:E.positive().optional(),height:E.positive().optional(),fit:b.enum([`contain`,`cover`,`fill`]).optional(),playbackRate:E.positive().optional(),loopBehavior:b.enum([`loop`,`pause-after-finish`]).optional(),style:b.record(b.string(),b.unknown()).optional()}),Te=b.object({type:b.literal(`lottie`),...A,src:k,width:E.positive().optional(),height:E.positive().optional(),loop:O.optional(),style:b.record(b.string(),b.unknown()).optional()}),Ee=b.discriminatedUnion(`type`,[ye,be,xe,Se,Ce,we,Te]),j=b.object({text:b.string(),startMs:E.nonnegative(),endMs:E.nonnegative(),timestampMs:E.nonnegative().optional(),confidence:E.min(0).max(1).optional(),emphasis:b.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).optional()}),De=j.extend({emphasis:b.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).default(`none`)}),Oe=b.object({x:E.min(0).max(1).optional(),y:E.min(0).max(1).optional(),position:b.enum([`top`,`center`,`bottom`]).optional()}),ke=b.object({text:b.string().optional(),startMs:E.nonnegative(),endMs:E.nonnegative(),tokens:b.array(De).min(1),placement:Oe.optional()}),Ae=b.object({top:E.nonnegative().optional(),right:E.nonnegative().optional(),bottom:E.nonnegative().optional(),left:E.nonnegative().optional()}),je=b.object({color:b.string().default(`rgba(0, 0, 0, 0.85)`),blur:E.nonnegative().default(8),offsetX:E.default(0),offsetY:E.default(3)}),Me=b.object({captions:b.array(j).default([]),pages:b.array(ke).optional(),style:b.enum([`tiktok`,`standard`,`karaoke`]).default(`tiktok`),preset:b.enum([`tiktok-bold`,`karaoke`,`premium-pill`,`minimal`,`ugc`]).default(`tiktok-bold`),platform:b.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.string().default(`Arial, sans-serif`),fontWeight:b.union([D.positive(),b.string()]).default(800),lineHeight:E.positive().default(1.08),color:b.string().default(`#ffffff`),inactiveColor:b.string().default(`#ffffff`),highlightColor:b.string().default(`#FFD700`),textColor:b.string().default(`#ffffff`),strokeColor:b.string().default(`#000000`),strokeWidth:E.nonnegative().default(5),shadow:b.union([O,je]).default(!0),backgroundColor:b.string().default(`rgba(0, 0, 0, 0)`),backgroundRadius:E.nonnegative().default(8),safeZone:Ae.optional(),position:b.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`]})}),M={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)},Ne=b.object({...M,definesTimeline:O.default(!0),durationMs:E.positive().optional()}),Pe=b.object({...M,loop:O.default(!0)}),Fe=b.object({...M,atMs:E.nonnegative(),durationMs:E.positive().optional()}),Ie=b.object({voiceover:Ne.optional(),music:Pe.optional(),sfx:b.array(Fe).default([])}),Le=b.object({family:b.string(),source:b.enum([`google`,`local`]).default(`google`),weights:b.array(D.positive()).optional(),url:b.string().optional()}),Re=b.object({clips:b.array(Ee).default([]),backgroundColor:b.string().default(`#000000`),fps:D.positive().default(30),width:D.positive().default(1920),height:D.positive().default(1080),captions:Me.optional(),globalTransition:ve.optional(),fonts:b.array(Le).optional(),audio:Ie.optional()}).passthrough(),ze=`${h.sep}public${h.sep}`,Be=/^(https?:|data:|blob:)/i,Ve=/\b(undefined|NaN|null)\b|blur\(px\)/i,He={Main:{width:1920,height:1080},Vertical:{width:1080,height:1920},Square:{width:1080,height:1080}},Ue=new Set([`video`,`image`,`audio`,`gif`,`lottie`]),We=new Set([`video`,`image`,`gif`,`lottie`,`text`,`subtitle`]),Ge={tiktok:480,reels:420,shorts:420,universal:480},Ke={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 qe=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 N=class{bundlePromise=null;mediaInfoService=new T;projectRoot=s(h.resolve(import.meta.dirname,`../package.json`))?h.resolve(import.meta.dirname,`..`):h.resolve(import.meta.dirname,`../..`);async getServeUrl(){return this.bundlePromise||=ee({entryPoint:h.resolve(this.projectRoot,`src/remotion/index.ts`),publicDir:h.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 h.resolve(this.projectRoot,`public`)}toPublicRelativePath(e){let t=h.normalize(e),n=t.indexOf(ze);return n===-1?null:t.slice(n+ze.length).split(h.sep).join(`/`)}toLocalFilesystemPath(e){return e.startsWith(`file://`)?y(e):h.isAbsolute(e)?e:null}toPublicFilesystemPath(e){let t=e.replace(/^\/+/,``).replace(/^public\//,``);return h.join(this.publicDir,...t.split(`/`))}resolveValidationSource(e){if(Be.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`){Ve.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 Ue.has(e.type)}async validateLocalAsset(e,t,n,r,i){if(!t.filePath)return;let a;try{a=await p(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 ie(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 f(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 p(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})=>We.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={...Ke[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=Ge[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 p(e);if(!t.isFile())throw Error(`Render asset is not a file: ${e}`);let n=h.extname(e),r=h.basename(e,n).replace(/[^a-zA-Z0-9._-]/g,`-`),i=v(`sha256`).update(e).update(String(t.mtimeMs)).update(String(t.size)).digest(`hex`).slice(0,16),a=h.posix.join(`mcp-render-assets`,`${r}-${i}${n}`),o=h.join(this.publicDir,...a.split(`/`));return await d(h.dirname(o),{recursive:!0}),await u(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=Re.parse(e);return this.normalizeProvidedValues(e,t)}async validateRender(e){let t=[],n=[],r=He[e.compositionId];r||t.push({code:`composition.unknown`,message:`Unknown composition: ${e.compositionId}`,path:`compositionId`});let i=await this.injectAudioDurations(e.inputProps),a=Re.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 qe(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 te({composition:await re({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 ne({composition:await re({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}}};N=x([r()],N);const Je=g(a),Ye=[`af_sarah`,`af_nicole`,`af_bella`,`af_sky`,`am_adam`,`am_michael`,`bf_emma`,`bf_isabella`,`bm_george`,`bm_lewis`];let P=class{async isAvailable(){try{return await Je(`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 l.mkdtemp(h.join(m.tmpdir(),`kokoro-`)),o=h.join(a,`input.txt`);try{return await l.writeFile(o,t,`utf-8`),await Je(`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 l.rm(a,{recursive:!0,force:!0})}catch{}}}listVoices(){return[...Ye]}};P=x([r()],P);const F={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 I(e,t){if(typeof Reflect==`object`&&typeof Reflect.metadata==`function`)return Reflect.metadata(e,t)}function L(e,t){return function(n,r){t(n,r,e)}}const R=g(a);let z=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 R(`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 R(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();s(t)||c(t,{recursive:!0});let l=h.join(t,`${n}.mp4`),u=[...this.getProxyArgs(),`-f`,`bestvideo[height<=${a}][ext=mp4]+bestaudio[ext=m4a]/best[height<=${a}][ext=mp4]/best`,`--merge-output-format`,`mp4`,`-o`,l,`--no-warnings`,`--no-playlist`];if(r&&i?u.push(`--download-sections`,`*${r}-${i}`):r?u.push(`--download-sections`,`*${r}-inf`):i&&u.push(`--download-sections`,`*0:00-${i}`),u.push(e),await R(o,u,{timeout:12e4}),!s(l))throw Error(`Download failed: output file not found at ${l}`);let{width:d,height:f,duration:p}=await this.getMediaInfo(l);return{outputPath:l,publicPath:h.relative(t,l),duration:p,width:d,height:f}}async getMediaInfo(e){let{stdout:t}=await R(`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}}};z=x([r(),L(0,n(F.ProxyUrl)),L(0,i()),I(`design:paramtypes`,[Object])],z);const Xe=g(a),Ze=1280,Qe=h.join(m.homedir(),`.gemini`,`oauth_creds.json`);let B=class{async analyzeVideos(e){let t=e.model||`gemini-2.5-flash-lite`,n=e.videoPaths.map(e=>h.resolve(e));if(n.length===0)throw Error(`At least one video path is required.`);let r=this.resolveWorkspaceRoot(process.cwd()),i=_.randomUUID().toLowerCase(),a=_.randomUUID().toLowerCase(),s=await l.mkdtemp(h.join(m.tmpdir(),`video-editor-mcp-video.`));try{let c=await this.readAccessToken(),l=process.env.CODE_ASSIST_ENDPOINT||`https://cloudcode-pa.googleapis.com`,u=process.env.CODE_ASSIST_API_VERSION||`v1internal`,{resolvedProject:d,projectFromEnv:f}=await this.resolveProject(c,l,u),p=new AbortController,m=setTimeout(()=>p.abort(),6e5);try{let m=await this.prepareVideos(n,s),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:o.readFileSync(e.transcodedPath).toString(`base64`)}}))]}],session_id:i}},g=await fetch(`${l}/${u}:generateContent`,{method:`POST`,signal:p.signal,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${c}`,"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,ee=this.extractResponseText(v);return{resolvedProject:d,model:t,prompt:e.prompt,videoCount:m.length,videos:m,responseText:ee,rawResponse:v??_,traceId:this.getString(v,`traceId`),responseId:this.getString(y?.response,`responseId`)}}finally{clearTimeout(m)}}finally{await l.rm(s,{recursive:!0,force:!0})}}resolveWorkspaceRoot(e=process.cwd()){let t=[`pnpm-workspace.yaml`,`nx.json`,`.git`],n=h.resolve(e);for(;;){for(let e of t)if(s(h.join(n,e)))return n;let r=h.dirname(n);if(r===n)return h.resolve(e);n=r}}async readAccessToken(e=process.env.GEMINI_OAUTH_CREDS_FILE||Qe){if(!o.existsSync(e))throw Error(`OAuth credentials file not found: ${e}`);let t=await l.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(!o.existsSync(i))throw Error(`Video file not found: ${i}`);let e=(await l.stat(i)).size,a=h.join(t,`${h.basename(i,h.extname(i))}-${r}.mp4`);await Xe(`ffmpeg`,[`-y`,`-i`,i,`-map`,`0:v:0`,`-map`,`0:a?`,`-vf`,`scale='if(gt(iw,${Ze}),${Ze},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 s=(await l.stat(a)).size;n.push({sourcePath:i,transcodedPath:a,originalSizeBytes:e,transcodedSizeBytes:s,mimeType:`video/mp4`})}return n}buildSessionContext(e){let t=new Date().toLocaleDateString(void 0,{weekday:`long`,year:`numeric`,month:`long`,day:`numeric`}),n=h.join(m.homedir(),`.gemini`,`tmp`,h.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,s,c)=>{if(i>=n||c>t)return;let l;try{l=o.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 o=0;o<l.length&&i<n;o+=1){let n=l[o],u=o===l.length-1,d=u?`└───`:`├───`,f=`${s}${u?` `:`│ `}`,p=h.join(e,n.name);if(i+=1,n.isDirectory()){r.push(`${s}${d}${n.name}/`),c<t&&a(p,f,c+1);continue}r.push(`${s}${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}};B=x([r()],B);const V=g(a);let H=class{constructor(e=new C){this.captionService=e}async extractVideoContext(e){let t=h.resolve(e.videoPath);if(!s(t))throw Error(`Video file not found: ${t}`);let n=e.basename??h.basename(t,h.extname(t)),r=h.resolve(e.outputDir??this.defaultOutputDir(t,n)),i=h.join(r,`work`),a=h.join(r,`manifest.json`);await l.mkdir(r,{recursive:!0}),await this.probeVideo(t);let o=e.transcribe===!1?{text:``,segments:[]}:await this.transcribeVideo(t,r,e),c=o.segments.length>0?await this.createProxyVideo(t,i,e):void 0,u={transcript:{...o,segments:c?await this.attachFilmstripsToSegments(c,r,o.segments,e):[]},next_actions:[`Review segments filmstrips for visual inspection`]};return await l.writeFile(a,`${JSON.stringify(u,null,2)}\n`,`utf8`),u}async probeVideo(e){let{stdout:t}=await V(`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(h.dirname(e)),r=n?h.join(n,`.tmp`,`video-editor-mcp`):m.tmpdir();return h.join(r,`${t}-context`)}resolveWorkspaceRoot(e){let t=[`pnpm-workspace.yaml`,`nx.json`,`.git`],n=h.resolve(e);for(;;){if(t.some(e=>s(h.join(n,e))))return n;let e=h.dirname(n);if(e===n)return;n=e}}async createProxyVideo(e,t,n){let r=h.join(t,`proxy.mp4`),i=Math.max(n.tileWidth??250,500);return await l.mkdir(t,{recursive:!0}),await V(`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=h.join(t,`audio.wav`);await V(`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([h.join(t,`audio.txt`),h.join(t,`transcript.txt`),`${i}.txt`]),o=this.firstExistingPath([h.join(t,`audio.srt`),h.join(t,`transcript.srt`),`${i}.srt`]),s=a?(await l.readFile(a,`utf8`)).trim():``,c=o?this.captionService.parseSrt(await l.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 V(`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 V(e,i,{timeout:1200*1e3})}async runWhisperCpp(e,t,n,r){let i=r.whisperModelPath??process.env.WHISPER_MODEL_PATH??this.resolveDefaultWhisperModelPath();if(!i||!s(i))throw Error(`Whisper.cpp model not found. Provide whisperModelPath or set WHISPER_MODEL_PATH.`);let a=[`-m`,i,`-f`,t,`-otxt`,`-osrt`,`-of`,h.join(n,`transcript`)];r.language&&a.push(`-l`,r.language),await V(e,a,{timeout:1200*1e3})}resolveDefaultWhisperModelPath(){return[h.join(m.homedir(),`.whisper`,`ggml-base.en.bin`),h.join(m.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=>s(e))}async attachFilmstripsToSegments(e,t,n,r){let i=h.join(t,`slides`);await l.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 p=Math.min(l,d-i),m=Math.min(c,p),g=Math.ceil(p/m),_=r.startMs+Math.round(i/o*1e3),v=Math.min(p/o,Math.max((r.endMs-_)/1e3,1/o)),y=h.join(t,`segment_${String(n+1).padStart(4,`0`)}_filmstrip_${String(u+1).padStart(3,`0`)}.${a}`);await V(`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=${m}x${g}:padding=8:margin=8:color=white`].join(`,`),`-frames:v`,`1`,y]),await this.annotateFilmstrip(y,_,o,p,m,g),f.push({path:y,index:u,startMs:_,endMs:Math.min(r.endMs,_+Math.round(v*1e3)),frameCount:p,tileWidth:s,columns:m,rows:g})}return f}async annotateFilmstrip(e,t,n,r,i,a){let o=await ie(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,u=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(``),d=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>${u}</svg>`),f=await ie(e).composite([{input:d,top:0,left:0}]).toBuffer();await l.writeFile(e,f)}firstExistingPath(e){return e.find(e=>s(e))}};H=x([r(),I(`design:paramtypes`,[Object])],H);let U=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`)}}};U=x([r()],U);var $e,et;const tt=b.object({url:b.string().describe(`Video URL from YouTube, TikTok, Instagram, or other yt-dlp supported sites`),filename:b.string().describe(`Output filename without extension (e.g., "my_clip")`),startTime:b.string().optional().describe(`Start time for clip extraction (e.g., "0:30", "1:15")`),endTime:b.string().optional().describe(`End time for clip extraction (e.g., "1:00", "2:30")`),maxHeight:b.number().int().positive().default(1080).describe(`Maximum video height in pixels (default: 1080)`),outputDir:b.string().optional().describe(`Custom output directory (default: Remotion public/videos/ for direct use in compositions)`)}),nt=()=>s(h.resolve(import.meta.dirname,`../package.json`))?h.resolve(import.meta.dirname,`..`):h.resolve(import.meta.dirname,`../..`);let W=class extends U{static{et=this}static TOOL_NAME=`download_clip`;constructor(e){super(),this.videoDownloadService=e}getInputSchema(){return tt}getDefinition(){return{name:et.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.toJSONSchema(tt,{reused:`inline`})}}async execute(e){try{let t=tt.parse(e),n=h.resolve(nt(),`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`)}}};W=et=x([r(),L(0,n(F.VideoDownloadService)),I(`design:paramtypes`,[typeof($e=z!==void 0&&z)==`function`?$e:Object])],W);var rt,it;const at=b.object({videoPath:b.string().min(1).describe(`Path to the local video file to extract context from`),outputDir:b.string().optional().describe(`Directory to write transcript, filmstrips, and manifest`),basename:b.string().optional().describe(`Base name for the generated context directory when outputDir is omitted`),framesPerSecond:b.number().positive().default(3).describe(`Frames per second to sample for raw frames and segment slides`),imageFormat:b.enum([`jpeg`,`png`]).default(`jpeg`).describe(`Frame image format`),tileWidth:b.number().int().positive().default(250).describe(`Width of each filmstrip tile in pixels`),filmstripColumns:b.number().int().positive().default(6).describe(`Maximum columns per segment filmstrip image`),filmstripRows:b.number().int().positive().default(4).describe(`Maximum rows per segment filmstrip image`),transcribe:b.boolean().default(!0).describe(`Run local Whisper transcription`),language:b.string().optional().describe(`Optional Whisper language hint`),whisperModel:b.string().optional().describe(`Python Whisper model name, default base`),whisperModelPath:b.string().optional().describe(`Whisper.cpp model path override`)});let G=class extends U{static{it=this}static TOOL_NAME=`extract_video_context`;constructor(e){super(),this.videoContextService=e}getInputSchema(){return at}getDefinition(){return{name:it.TOOL_NAME,description:`Break a local video into timestamped transcript segments with paged filmstrip images for image-understanding tools`,inputSchema:b.toJSONSchema(at,{reused:`inline`})}}async execute(e){try{let t=at.parse(e),n=await this.videoContextService.extractVideoContext(t);return this.successJson(n)}catch(e){return this.error(e instanceof Error?e.message:`Unknown error`)}}};G=it=x([r(),L(0,n(F.VideoContextService)),I(`design:paramtypes`,[typeof(rt=H!==void 0&&H)==`function`?rt:Object])],G);var ot,st;const ct=b.object({captions:b.array(b.object({text:b.string(),startMs:b.number(),endMs:b.number(),confidence:b.number().min(0).max(1).optional(),emphasis:b.enum([`none`,`keyword`,`stat`,`negation`,`cta`]).optional()})).describe(`Array of caption objects with text, startMs, and endMs`),combineMs:b.number().optional().describe(`Milliseconds threshold for combining captions into pages (default: 100)`),maxWordsPerPage:b.number().int().positive().optional().describe(`Maximum words per caption page (default: 6)`),maxCharsPerLine:b.number().int().positive().optional().describe(`Target maximum characters per caption line (default: 22)`),platform:b.enum([`tiktok`,`reels`,`shorts`,`universal`]).optional().describe(`Platform safe-zone preset`),preset:b.enum([`tiktok-bold`,`karaoke`,`premium-pill`,`minimal`,`ugc`]).optional().describe(`Caption visual style preset`)});let K=class extends U{static{st=this}static TOOL_NAME=`generate_captions`;constructor(e){super(),this.captionService=e}getInputSchema(){return ct}getDefinition(){return{name:st.TOOL_NAME,description:`Create TikTok-style caption pages from a caption array for video overlay`,inputSchema:b.toJSONSchema(ct,{reused:`inline`})}}async execute(e){try{let{captions:t,combineMs:n=100,maxWordsPerPage:r,maxCharsPerLine:i,platform:a,preset:o}=ct.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`)}}};K=st=x([r(),L(0,n(F.CaptionService)),I(`design:paramtypes`,[typeof(ot=C!==void 0&&C)==`function`?ot:Object])],K);var lt,ut;const dt=b.object({prompt:b.string().describe(`Music description for ACE-Step text-to-music generation`),filename:b.string().optional().describe(`Output filename without extension (default: ace-step-music)`),outputPath:b.string().optional().describe(`Explicit output audio path. Overrides filename/outputDir.`),outputDir:b.string().optional().describe(`Custom output directory (default: Remotion public/music/ for direct use in compositions)`),aceStepPath:b.string().optional().describe(`Path to local ACE-Step-1.5 checkout (default: ACE_STEP_15_DIR or ~/workspace/ACE-Step-1.5)`),lyrics:b.string().optional().describe(`Lyrics to use. Defaults to [Instrumental] for background music.`),duration:b.number().min(10).max(600).optional().describe(`Target duration in seconds (default: 30)`),bpm:b.number().min(30).max(300).optional().describe(`Optional BPM metadata`),keyscale:b.string().optional().describe(`Optional key/scale metadata, e.g. C minor`),timeSignature:b.string().optional().describe(`Optional time signature metadata, e.g. 4/4`),vocalLanguage:b.string().optional().describe(`Vocal language code, or unknown (default: unknown)`),seed:b.number().int().optional().describe(`Optional fixed seed for reproducible generation`),inferenceSteps:b.number().int().positive().optional().describe(`Diffusion inference steps (default: 8)`),audioFormat:b.enum([`wav`,`mp3`,`flac`]).default(`wav`).describe(`Output audio format (default: wav)`),backend:b.enum([`mlx`,`pt`,`vllm`]).default(`mlx`).describe(`ACE-Step LM backend (default: mlx for Apple Silicon)`),configPath:b.string().optional().describe(`ACE-Step DiT model config (default: acestep-v15-turbo)`),lmModelPath:b.string().optional().describe(`ACE-Step LM model (default: acestep-5Hz-lm-0.6B)`),timeoutMs:b.number().int().positive().optional().describe(`Generation timeout in milliseconds`)}),ft=()=>s(h.resolve(import.meta.dirname,`../package.json`))?h.resolve(import.meta.dirname,`..`):h.resolve(import.meta.dirname,`../..`);let pt=class extends U{static{ut=this}static TOOL_NAME=`generate_music`;constructor(e){super(),this.aceStepMusicService=e}getInputSchema(){return dt}getDefinition(){return{name:ut.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.toJSONSchema(dt,{reused:`inline`})}}async execute(e){try{let t=dt.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=h.resolve(ft(),`public/music`),a=h.dirname(n)===i?`/music/${h.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 h.resolve(e.outputPath);let t=h.resolve(e.outputDir??h.join(ft(),`public/music`)),n=e.filename??`ace-step-music`,r=e.audioFormat??`wav`;return h.join(t,`${n}.${r}`)}};pt=ut=x([r(),L(0,n(F.AceStepMusicService)),I(`design:paramtypes`,[typeof(lt=S!==void 0&&S)==`function`?lt:Object])],pt);var mt,ht;const gt=b.object({text:b.string().describe(`Text to convert to speech`),voice:b.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.number().optional().describe(`Speech speed multiplier (default: 1.0)`),outputPath:b.string().describe(`Output file path for the generated audio`)});let q=class extends U{static{ht=this}static TOOL_NAME=`generate_voiceover`;constructor(e){super(),this.voiceoverService=e}getInputSchema(){return gt}getDefinition(){return{name:ht.TOOL_NAME,description:`Generate voiceover audio from text using Kokoro TTS local engine`,inputSchema:b.toJSONSchema(gt,{reused:`inline`})}}async execute(e){try{let t=gt.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`)}}};q=ht=x([r(),L(0,n(F.VoiceoverService)),I(`design:paramtypes`,[typeof(mt=P!==void 0&&P)==`function`?mt:Object])],q);var _t,vt;const yt=b.object({src:b.string().describe(`Path to the media file`),type:b.enum([`video`,`audio`]).optional().describe(`Media type (default: video)`)});let J=class extends U{static{vt=this}static TOOL_NAME=`get_media_info`;constructor(e){super(),this.mediaInfoService=e}getInputSchema(){return yt}getDefinition(){return{name:vt.TOOL_NAME,description:`Get duration, dimensions, and decodability info for video/audio files`,inputSchema:b.toJSONSchema(yt,{reused:`inline`})}}async execute(e){try{let{src:t,type:n=`video`}=yt.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`)}}};J=vt=x([r(),L(0,n(F.MediaInfoService)),I(`design:paramtypes`,[typeof(_t=T!==void 0&&T)==`function`?_t:Object])],J);var bt,xt;const St=b.object({srtContent:b.string().describe(`SRT file content as a string`)});let Y=class extends U{static{xt=this}static TOOL_NAME=`import_srt`;constructor(e){super(),this.captionService=e}getInputSchema(){return St}getDefinition(){return{name:xt.TOOL_NAME,description:`Parse SRT subtitle file content into structured Caption format for video overlay`,inputSchema:b.toJSONSchema(St,{reused:`inline`})}}async execute(e){try{let t=St.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`)}}};Y=xt=x([r(),L(0,n(F.CaptionService)),I(`design:paramtypes`,[typeof(bt=C!==void 0&&C)==`function`?bt:Object])],Y);var Ct;const wt=b.object({});let X=class extends U{static{Ct=this}static TOOL_NAME=`list_compositions`;getInputSchema(){return wt}getDefinition(){return{name:Ct.TOOL_NAME,description:`List all available Remotion compositions with their schemas, dimensions, and format info`,inputSchema:b.toJSONSchema(wt,{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})}};X=Ct=x([r()],X);var Tt,Et;const Dt=b.object({compositionId:b.string().describe(`Remotion composition ID (e.g., "Main", "Vertical", "Square")`),inputProps:b.record(b.string(),b.unknown()).describe(`JSON props to pass to the composition`),outputPath:b.string().describe(`Output file path for the rendered image`),frame:b.number().optional().describe(`Frame number to render (default: 0)`),imageFormat:b.enum([`png`,`jpeg`]).optional().describe(`Image format (default: png)`)});let Z=class extends U{static{Et=this}static TOOL_NAME=`preview_frame`;constructor(e){super(),this.renderService=e}getInputSchema(){return Dt}getDefinition(){return{name:Et.TOOL_NAME,description:`Render a single frame from a composition as a PNG or JPEG image for preview`,inputSchema:b.toJSONSchema(Dt,{reused:`inline`})}}async execute(e){try{let t=Dt.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=Et=x([r(),L(0,n(F.RenderService)),I(`design:paramtypes`,[typeof(Tt=N!==void 0&&N)==`function`?Tt:Object])],Z);var Ot,kt;const At=b.preprocess(e=>{if(typeof e!=`string`)return e;try{return JSON.parse(e)}catch{return e}},b.record(b.string(),b.unknown())),jt=b.preprocess(e=>{if(typeof e!=`string`)return e;let t=e.trim().toLowerCase();return t===`true`?!0:t===`false`?!1:e},b.boolean()),Mt=b.object({compositionId:b.string().describe(`Remotion composition ID (e.g., "Main", "Vertical", "Square")`),inputProps:At.describe(`JSON props to pass to the composition`),outputPath:b.string().describe(`Output file path for the rendered video`),codec:b.enum([`h264`,`h265`,`vp8`,`vp9`]).optional().describe(`Video codec (default: h264)`),dryRun:jt.optional().describe(`Validate props and assets without rendering a video`)});let Q=class extends U{static{kt=this}static TOOL_NAME=`render_video`;constructor(e){super(),this.renderService=e}getInputSchema(){return Mt}getDefinition(){return{name:kt.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.toJSONSchema(Mt,{reused:`inline`})}}async execute(e){try{let t=Mt.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 qe?{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=kt=x([r(),L(0,n(F.RenderService)),I(`design:paramtypes`,[typeof(Ot=N!==void 0&&N)==`function`?Ot:Object])],Q);var Nt,Pt;const Ft=b.object({query:b.string().describe(`Search query for YouTube videos`),maxResults:b.number().int().positive().max(20).default(5).describe(`Maximum number of results (default: 5, max: 20)`)});let It=class extends U{static{Pt=this}static TOOL_NAME=`search_videos`;constructor(e){super(),this.videoDownloadService=e}getInputSchema(){return Ft}getDefinition(){return{name:Pt.TOOL_NAME,description:`Search YouTube for videos by query. Returns video metadata (title, duration, URL, channel) without downloading.`,inputSchema:b.toJSONSchema(Ft,{reused:`inline`})}}async execute(e){try{let t=Ft.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`)}}};It=Pt=x([r(),L(0,n(F.VideoDownloadService)),I(`design:paramtypes`,[typeof(Nt=z!==void 0&&z)==`function`?Nt:Object])],It);var Lt,Rt;const zt=b.object({videoPaths:b.array(b.string().min(1)).min(1).describe(`Paths to local video files to analyze`),prompt:b.string().min(1).describe(`Prompt to send with the video(s)`),model:b.string().default(`gemini-2.5-flash-lite`).describe(`Gemini model to use (default: gemini-2.5-flash-lite)`)});let $=class extends U{static{Rt=this}static TOOL_NAME=`analyze_video`;constructor(e){super(),this.videoAnalysisService=e}getInputSchema(){return zt}getDefinition(){return{name:Rt.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.toJSONSchema(zt,{reused:`inline`})}}async execute(e){try{let t=zt.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`)}}};$=Rt=x([r(),L(0,n(F.VideoAnalysisService)),I(`design:paramtypes`,[typeof(Lt=B!==void 0&&B)==`function`?Lt:Object])],$);const Bt=new t(e=>{e.bind(F.AceStepMusicService).to(S).inSingletonScope(),e.bind(F.RenderService).to(N).inSingletonScope(),e.bind(F.MediaInfoService).to(T).inSingletonScope(),e.bind(F.CaptionService).to(C).inSingletonScope(),e.bind(F.FontService).to(me).inSingletonScope(),e.bind(F.VoiceoverService).to(P).inSingletonScope(),e.bind(F.VideoDownloadService).to(z).inSingletonScope(),e.bind(F.VideoAnalysisService).to(B).inSingletonScope(),e.bind(F.VideoContextService).to(H).inSingletonScope()}),Vt=new t(e=>{e.bind(F.Tool).to(Q).inSingletonScope(),e.bind(F.Tool).to(X).inSingletonScope(),e.bind(F.Tool).to(J).inSingletonScope(),e.bind(F.Tool).to(Z).inSingletonScope(),e.bind(F.Tool).to(K).inSingletonScope(),e.bind(F.Tool).to(Y).inSingletonScope(),e.bind(F.Tool).to(pt).inSingletonScope(),e.bind(F.Tool).to(q).inSingletonScope(),e.bind(F.Tool).to(It).inSingletonScope(),e.bind(F.Tool).to(W).inSingletonScope(),e.bind(F.Tool).to($).inSingletonScope(),e.bind(F.Tool).to(G).inSingletonScope()});function Ht(t={}){let n=new e;return n.load(Bt,Vt),n.bind(F.ProxyUrl).toConstantValue(t.proxyUrl??null),n}const Ut=new Set([`render_video`,`preview_frame`,`generate_music`,`generate_voiceover`,`download_clip`,`extract_video_context`,`analyze_video`]);function Wt(e){return Ut.has(e)}async function Gt(e,t,n,r){return Wt(e)?r.run(()=>t.execute(n)):t.execute(n)}function Kt(e){let t=new se({name:`video-editor-mcp`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getAll(F.Tool),r=new he,i=new Map;for(let e of n)i.set(e.getDefinition().name,e);return t.setRequestHandler(le,async()=>({tools:n.map(e=>e.getDefinition())})),t.setRequestHandler(ce,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=ae(n??{},a.getInputSchema());try{return await Gt(t,a,o,r)}catch(e){if(e instanceof b.ZodError)return{content:[{type:`text`,text:oe(e,{schemaName:t,schema:a.getInputSchema()})}],isError:!0};throw e}}),t}var qt=class{server;transport=null;constructor(e){this.server=e}async start(){this.transport=new ue,await this.server.connect(this.transport),console.error(`video-editor MCP server started on stdio`)}async stop(){this.transport&&=(await this.transport.close(),null)}};export{ve as A,Se as C,Te as D,be as E,Ce as O,_e as S,we as T,qe as _,Q as a,Le as b,Y as c,K as d,G as f,N as g,F as h,$ as i,ye as j,xe as k,J as l,H as m,Kt as n,Z as o,U as p,Ht as r,X as s,qt as t,q as u,Me as v,Ee as w,Re as x,j as y};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/video-editor-mcp",
|
|
3
3
|
"description": "MCP server for video editing with Remotion",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.2",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"module": "./dist/index.mjs",
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"schemas",
|
|
22
|
+
"src/remotion",
|
|
23
|
+
"src/schemas",
|
|
24
|
+
"src/utils",
|
|
21
25
|
"README.md"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
@@ -39,15 +43,17 @@
|
|
|
39
43
|
"react-dom": "19.2.5",
|
|
40
44
|
"reflect-metadata": "0.2.2",
|
|
41
45
|
"remotion": "4.0.447",
|
|
46
|
+
"sharp": "0.34.5",
|
|
42
47
|
"zod": "4.3.6",
|
|
43
|
-
"@agimon-ai/foundation-
|
|
44
|
-
"@agimon-ai/foundation-
|
|
45
|
-
"@agimon-ai/foundation-validator": "0.
|
|
48
|
+
"@agimon-ai/foundation-port-registry": "0.8.2",
|
|
49
|
+
"@agimon-ai/foundation-process-registry": "0.8.2",
|
|
50
|
+
"@agimon-ai/foundation-validator": "0.5.2"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@types/node": "25.6.0",
|
|
49
54
|
"@types/react": "19.2.14",
|
|
50
55
|
"tsdown": "0.21.7",
|
|
56
|
+
"tsx": "4.21.0",
|
|
51
57
|
"typescript": "6.0.2",
|
|
52
58
|
"vitest": "4.1.4"
|
|
53
59
|
},
|
|
@@ -65,6 +71,8 @@
|
|
|
65
71
|
},
|
|
66
72
|
"scripts": {
|
|
67
73
|
"dev": "node --loader ts-node/esm src/cli.ts mcp-serve",
|
|
74
|
+
"generate-schemas": "tsx scripts/generate-schemas.ts",
|
|
75
|
+
"prebuild": "tsx scripts/generate-schemas.ts",
|
|
68
76
|
"build": "tsdown",
|
|
69
77
|
"test": "vitest --run",
|
|
70
78
|
"lint": "eslint src",
|