MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / execute

Method execute

src/tools/media/ffmpeg-tools.ts:79–146  ·  view source on GitHub ↗
(a: z.infer<typeof TransformArgs>)

Source from the content-addressed store, hash-verified

77 isReadOnly = false; isDestructive = true; argsSchema = TransformArgs;
78
79 async execute(a: z.infer<typeof TransformArgs>): Promise<ToolResult> {
80 // ---- concat: join an ordered list of clips into one final video ----
81 // Uses the concat demuxer. Default -c copy is lossless + fast and works when
82 // all clips share codec/resolution (the typical same-preset AI-generated case);
83 // reencode:true normalizes mismatched sources.
84 if (a.operation === 'concat') {
85 const inputs = a.inputs ?? [];
86 if (inputs.length < 2) {
87 return { content: 'concat requires "inputs" with at least 2 clip paths, in play order.', isError: true };
88 }
89 const listLines = inputs.map(f => `file '${path.resolve(f).replace(/'/g, "'\\''")}'`);
90 const listPath = path.join(os.tmpdir(), `qodex-concat-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
91 try {
92 await fs.writeFile(listPath, listLines.join('\n') + '\n', 'utf8');
93 const cargs: string[] = [];
94 if (a.overwrite) cargs.push('-y');
95 cargs.push('-f', 'concat', '-safe', '0', '-i', listPath);
96 if (a.reencode) cargs.push('-c:v', 'libx264', '-crf', '18', '-preset', 'veryfast', '-pix_fmt', 'yuv420p');
97 else cargs.push('-c', 'copy');
98 cargs.push(a.output);
99 const r = await runProcess('ffmpeg', cargs, { timeoutMs: (a.timeout_seconds ?? 600) * 1000 });
100 if (r.notFound) return { content: notInstalledMessage('ffmpeg', FFMPEG_HINT), isError: true };
101 if (r.timedOut) return { content: 'ffmpeg concat timed out. For many/long clips use background_job_start.', isError: true };
102 if (!r.ok) {
103 const hint = a.reencode ? '' : '\nIf clips differ in codec/resolution, retry with reencode:true (or normalize them first with resize/convert).';
104 return { content: `ffmpeg concat failed:\n${r.stderr.trim().slice(-1500)}${hint}`, isError: true };
105 }
106 return { content: `✓ concat ${inputs.length} clips → ${a.output}` };
107 } finally {
108 try { await fs.unlink(listPath); } catch { /* ignore temp cleanup */ }
109 }
110 }
111
112 if (!a.input) return { content: `Operation "${a.operation}" requires "input".`, isError: true };
113 const input: string = a.input;
114 const args: string[] = [];
115 if (a.overwrite) args.push('-y');
116
117 switch (a.operation) {
118 case 'convert':
119 args.push('-i', input, a.output);
120 break;
121 case 'trim':
122 if (!a.start) return { content: 'trim requires "start".', isError: true };
123 args.push('-i', input, '-ss', a.start);
124 if (a.end) args.push('-to', a.end);
125 args.push('-c', 'copy', a.output);
126 break;
127 case 'extract_frame':
128 args.push('-ss', a.start ?? '0', '-i', input, '-frames:v', '1', a.output);
129 break;
130 case 'resize': {
131 if (!a.width && !a.height) return { content: 'resize requires width and/or height.', isError: true };
132 const w = a.width ?? -1, h = a.height ?? -1;
133 args.push('-i', input, '-vf', `scale=${w}:${h}`, a.output);
134 break;
135 }
136 case 'extract_audio':

Callers

nothing calls this directly

Calls 4

runProcessFunction · 0.85
notInstalledMessageFunction · 0.85
resolveMethod · 0.80
pushMethod · 0.45

Tested by

no test coverage detected