@studiomeyer/mcp-video 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  3. package/.github/workflows/ci.yml +34 -0
  4. package/CHANGELOG.md +24 -0
  5. package/CONTRIBUTING.md +75 -0
  6. package/LICENSE +21 -0
  7. package/README.md +198 -0
  8. package/USAGE.md +144 -0
  9. package/dist/handlers/capcut.d.ts +6 -0
  10. package/dist/handlers/capcut.js +229 -0
  11. package/dist/handlers/capcut.js.map +1 -0
  12. package/dist/handlers/editing.d.ts +6 -0
  13. package/dist/handlers/editing.js +242 -0
  14. package/dist/handlers/editing.js.map +1 -0
  15. package/dist/handlers/index.d.ts +2 -0
  16. package/dist/handlers/index.js +33 -0
  17. package/dist/handlers/index.js.map +1 -0
  18. package/dist/handlers/post-production.d.ts +5 -0
  19. package/dist/handlers/post-production.js +109 -0
  20. package/dist/handlers/post-production.js.map +1 -0
  21. package/dist/handlers/smart-screenshot.d.ts +5 -0
  22. package/dist/handlers/smart-screenshot.js +83 -0
  23. package/dist/handlers/smart-screenshot.js.map +1 -0
  24. package/dist/handlers/tts.d.ts +5 -0
  25. package/dist/handlers/tts.js +83 -0
  26. package/dist/handlers/tts.js.map +1 -0
  27. package/dist/handlers/video.d.ts +5 -0
  28. package/dist/handlers/video.js +127 -0
  29. package/dist/handlers/video.js.map +1 -0
  30. package/dist/lib/dual-transport.d.ts +42 -0
  31. package/dist/lib/dual-transport.js +208 -0
  32. package/dist/lib/dual-transport.js.map +1 -0
  33. package/dist/lib/logger.d.ts +12 -0
  34. package/dist/lib/logger.js +42 -0
  35. package/dist/lib/logger.js.map +1 -0
  36. package/dist/lib/types.d.ts +16 -0
  37. package/dist/lib/types.js +15 -0
  38. package/dist/lib/types.js.map +1 -0
  39. package/dist/schemas/capcut.d.ts +608 -0
  40. package/dist/schemas/capcut.js +411 -0
  41. package/dist/schemas/capcut.js.map +1 -0
  42. package/dist/schemas/editing.d.ts +822 -0
  43. package/dist/schemas/editing.js +466 -0
  44. package/dist/schemas/editing.js.map +1 -0
  45. package/dist/schemas/index.d.ts +2366 -0
  46. package/dist/schemas/index.js +15 -0
  47. package/dist/schemas/index.js.map +1 -0
  48. package/dist/schemas/post-production.d.ts +379 -0
  49. package/dist/schemas/post-production.js +268 -0
  50. package/dist/schemas/post-production.js.map +1 -0
  51. package/dist/schemas/smart-screenshot.d.ts +127 -0
  52. package/dist/schemas/smart-screenshot.js +122 -0
  53. package/dist/schemas/smart-screenshot.js.map +1 -0
  54. package/dist/schemas/tts.d.ts +220 -0
  55. package/dist/schemas/tts.js +194 -0
  56. package/dist/schemas/tts.js.map +1 -0
  57. package/dist/schemas/video.d.ts +236 -0
  58. package/dist/schemas/video.js +210 -0
  59. package/dist/schemas/video.js.map +1 -0
  60. package/dist/server.d.ts +11 -0
  61. package/dist/server.js +239 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/server.test.d.ts +1 -0
  64. package/dist/server.test.js +87 -0
  65. package/dist/server.test.js.map +1 -0
  66. package/dist/tools/engine/audio-mixer.d.ts +40 -0
  67. package/dist/tools/engine/audio-mixer.js +169 -0
  68. package/dist/tools/engine/audio-mixer.js.map +1 -0
  69. package/dist/tools/engine/audio.d.ts +22 -0
  70. package/dist/tools/engine/audio.js +73 -0
  71. package/dist/tools/engine/audio.js.map +1 -0
  72. package/dist/tools/engine/beat-sync.d.ts +31 -0
  73. package/dist/tools/engine/beat-sync.js +270 -0
  74. package/dist/tools/engine/beat-sync.js.map +1 -0
  75. package/dist/tools/engine/capture.d.ts +12 -0
  76. package/dist/tools/engine/capture.js +290 -0
  77. package/dist/tools/engine/capture.js.map +1 -0
  78. package/dist/tools/engine/chroma-key.d.ts +27 -0
  79. package/dist/tools/engine/chroma-key.js +154 -0
  80. package/dist/tools/engine/chroma-key.js.map +1 -0
  81. package/dist/tools/engine/concat.d.ts +49 -0
  82. package/dist/tools/engine/concat.js +149 -0
  83. package/dist/tools/engine/concat.js.map +1 -0
  84. package/dist/tools/engine/cursor.d.ts +26 -0
  85. package/dist/tools/engine/cursor.js +185 -0
  86. package/dist/tools/engine/cursor.js.map +1 -0
  87. package/dist/tools/engine/easing.d.ts +15 -0
  88. package/dist/tools/engine/easing.js +100 -0
  89. package/dist/tools/engine/easing.js.map +1 -0
  90. package/dist/tools/engine/editing.d.ts +158 -0
  91. package/dist/tools/engine/editing.js +541 -0
  92. package/dist/tools/engine/editing.js.map +1 -0
  93. package/dist/tools/engine/encoder.d.ts +31 -0
  94. package/dist/tools/engine/encoder.js +154 -0
  95. package/dist/tools/engine/encoder.js.map +1 -0
  96. package/dist/tools/engine/index.d.ts +30 -0
  97. package/dist/tools/engine/index.js +23 -0
  98. package/dist/tools/engine/index.js.map +1 -0
  99. package/dist/tools/engine/lut-presets.d.ts +25 -0
  100. package/dist/tools/engine/lut-presets.js +141 -0
  101. package/dist/tools/engine/lut-presets.js.map +1 -0
  102. package/dist/tools/engine/narrated-video.d.ts +63 -0
  103. package/dist/tools/engine/narrated-video.js +163 -0
  104. package/dist/tools/engine/narrated-video.js.map +1 -0
  105. package/dist/tools/engine/scenes.d.ts +17 -0
  106. package/dist/tools/engine/scenes.js +223 -0
  107. package/dist/tools/engine/scenes.js.map +1 -0
  108. package/dist/tools/engine/smart-screenshot.d.ts +80 -0
  109. package/dist/tools/engine/smart-screenshot.js +744 -0
  110. package/dist/tools/engine/smart-screenshot.js.map +1 -0
  111. package/dist/tools/engine/social-format.d.ts +66 -0
  112. package/dist/tools/engine/social-format.js +107 -0
  113. package/dist/tools/engine/social-format.js.map +1 -0
  114. package/dist/tools/engine/template-renderer.d.ts +45 -0
  115. package/dist/tools/engine/template-renderer.js +233 -0
  116. package/dist/tools/engine/template-renderer.js.map +1 -0
  117. package/dist/tools/engine/templates.d.ts +87 -0
  118. package/dist/tools/engine/templates.js +272 -0
  119. package/dist/tools/engine/templates.js.map +1 -0
  120. package/dist/tools/engine/text-animations.d.ts +33 -0
  121. package/dist/tools/engine/text-animations.js +192 -0
  122. package/dist/tools/engine/text-animations.js.map +1 -0
  123. package/dist/tools/engine/text-overlay.d.ts +27 -0
  124. package/dist/tools/engine/text-overlay.js +84 -0
  125. package/dist/tools/engine/text-overlay.js.map +1 -0
  126. package/dist/tools/engine/tts.d.ts +54 -0
  127. package/dist/tools/engine/tts.js +186 -0
  128. package/dist/tools/engine/tts.js.map +1 -0
  129. package/dist/tools/engine/types.d.ts +166 -0
  130. package/dist/tools/engine/types.js +13 -0
  131. package/dist/tools/engine/types.js.map +1 -0
  132. package/dist/tools/engine/voice-effects.d.ts +18 -0
  133. package/dist/tools/engine/voice-effects.js +215 -0
  134. package/dist/tools/engine/voice-effects.js.map +1 -0
  135. package/dist/tools/index.d.ts +32 -0
  136. package/dist/tools/index.js +23 -0
  137. package/dist/tools/index.js.map +1 -0
  138. package/package.json +56 -0
  139. package/scripts/check-deps.js +39 -0
  140. package/src/handlers/capcut.ts +245 -0
  141. package/src/handlers/editing.ts +260 -0
  142. package/src/handlers/index.ts +34 -0
  143. package/src/handlers/post-production.ts +136 -0
  144. package/src/handlers/smart-screenshot.ts +86 -0
  145. package/src/handlers/tts.ts +103 -0
  146. package/src/handlers/video.ts +137 -0
  147. package/src/lib/dual-transport.ts +272 -0
  148. package/src/lib/logger.ts +59 -0
  149. package/src/lib/types.ts +25 -0
  150. package/src/schemas/capcut.ts +418 -0
  151. package/src/schemas/editing.ts +476 -0
  152. package/src/schemas/index.ts +15 -0
  153. package/src/schemas/post-production.ts +273 -0
  154. package/src/schemas/smart-screenshot.ts +122 -0
  155. package/src/schemas/tts.ts +197 -0
  156. package/src/schemas/video.ts +211 -0
  157. package/src/server.test.ts +99 -0
  158. package/src/server.ts +289 -0
  159. package/src/tools/engine/audio-mixer.ts +244 -0
  160. package/src/tools/engine/audio.ts +115 -0
  161. package/src/tools/engine/beat-sync.ts +356 -0
  162. package/src/tools/engine/capture.ts +360 -0
  163. package/src/tools/engine/chroma-key.ts +202 -0
  164. package/src/tools/engine/concat.ts +242 -0
  165. package/src/tools/engine/cursor.ts +222 -0
  166. package/src/tools/engine/easing.ts +120 -0
  167. package/src/tools/engine/editing.ts +809 -0
  168. package/src/tools/engine/encoder.ts +208 -0
  169. package/src/tools/engine/index.ts +33 -0
  170. package/src/tools/engine/lut-presets.ts +235 -0
  171. package/src/tools/engine/narrated-video.ts +267 -0
  172. package/src/tools/engine/scenes.ts +309 -0
  173. package/src/tools/engine/smart-screenshot.ts +923 -0
  174. package/src/tools/engine/social-format.ts +146 -0
  175. package/src/tools/engine/template-renderer.ts +294 -0
  176. package/src/tools/engine/templates.ts +370 -0
  177. package/src/tools/engine/text-animations.ts +282 -0
  178. package/src/tools/engine/text-overlay.ts +143 -0
  179. package/src/tools/engine/tts.ts +284 -0
  180. package/src/tools/engine/types.ts +191 -0
  181. package/src/tools/engine/voice-effects.ts +258 -0
  182. package/src/tools/index.ts +67 -0
  183. package/tsconfig.json +19 -0
  184. package/vitest.config.ts +7 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Editing tool handlers — speed, color, effects, crop, reverse, extract,
3
+ * subtitles, auto captions, keyframes, PiP, audio ducking
4
+ */
5
+
6
+ import { jsonResponse, type ToolHandler } from '../lib/types.js';
7
+ import { logger } from '../lib/logger.js';
8
+ import {
9
+ adjustVideoSpeed,
10
+ applyColorGrade,
11
+ applyVideoEffect,
12
+ cropVideo,
13
+ reverseClip,
14
+ extractAudio,
15
+ burnSubtitles,
16
+ autoCaption,
17
+ addKeyframeAnimation,
18
+ composePip,
19
+ addAudioDucking,
20
+ } from '../tools/index.js';
21
+ import type {
22
+ VideoEffect,
23
+ PipPosition,
24
+ Keyframe,
25
+ } from '../tools/index.js';
26
+
27
+ export const editingHandlers: Record<string, ToolHandler> = {
28
+
29
+ adjust_video_speed: async (args) => {
30
+ try {
31
+ const result = await adjustVideoSpeed({
32
+ inputPath: args.inputPath,
33
+ outputPath: args.outputPath,
34
+ speed: args.speed,
35
+ audioMode: args.audioMode ?? 'match',
36
+ });
37
+ return jsonResponse({
38
+ success: true, outputPath: result, speed: args.speed,
39
+ message: `Video speed changed to ${args.speed}x. Audio ${args.audioMode ?? 'match'}ed.`,
40
+ });
41
+ } catch (error) {
42
+ const msg = error instanceof Error ? error.message : String(error);
43
+ logger.error(`adjust_video_speed failed: ${msg}`);
44
+ return jsonResponse({ success: false, error: msg }, true);
45
+ }
46
+ },
47
+
48
+ apply_color_grade: async (args) => {
49
+ try {
50
+ const result = await applyColorGrade({
51
+ inputPath: args.inputPath,
52
+ outputPath: args.outputPath,
53
+ brightness: args.brightness,
54
+ contrast: args.contrast,
55
+ saturation: args.saturation,
56
+ gamma: args.gamma,
57
+ temperature: args.temperature,
58
+ });
59
+ const params = ['brightness', 'contrast', 'saturation', 'gamma', 'temperature']
60
+ .filter(p => args[p] !== undefined)
61
+ .map(p => `${p}=${args[p]}`)
62
+ .join(', ');
63
+ return jsonResponse({ success: true, outputPath: result, applied: params || 'defaults' });
64
+ } catch (error) {
65
+ const msg = error instanceof Error ? error.message : String(error);
66
+ logger.error(`apply_color_grade failed: ${msg}`);
67
+ return jsonResponse({ success: false, error: msg }, true);
68
+ }
69
+ },
70
+
71
+ apply_video_effect: async (args) => {
72
+ try {
73
+ const result = await applyVideoEffect({
74
+ inputPath: args.inputPath,
75
+ outputPath: args.outputPath,
76
+ effect: args.effect as VideoEffect,
77
+ intensity: args.intensity ?? 0.5,
78
+ });
79
+ return jsonResponse({
80
+ success: true, outputPath: result, effect: args.effect,
81
+ intensity: args.intensity ?? 0.5,
82
+ message: `Applied ${args.effect} effect (intensity: ${args.intensity ?? 0.5}).`,
83
+ });
84
+ } catch (error) {
85
+ const msg = error instanceof Error ? error.message : String(error);
86
+ logger.error(`apply_video_effect failed: ${msg}`);
87
+ return jsonResponse({ success: false, error: msg }, true);
88
+ }
89
+ },
90
+
91
+ crop_video: async (args) => {
92
+ try {
93
+ const result = await cropVideo({
94
+ inputPath: args.inputPath,
95
+ outputPath: args.outputPath,
96
+ width: args.width,
97
+ height: args.height,
98
+ x: args.x ?? 'center',
99
+ y: args.y ?? 'center',
100
+ });
101
+ return jsonResponse({
102
+ success: true, outputPath: result,
103
+ crop: { width: args.width, height: args.height, x: args.x ?? 'center', y: args.y ?? 'center' },
104
+ });
105
+ } catch (error) {
106
+ const msg = error instanceof Error ? error.message : String(error);
107
+ logger.error(`crop_video failed: ${msg}`);
108
+ return jsonResponse({ success: false, error: msg }, true);
109
+ }
110
+ },
111
+
112
+ reverse_clip: async (args) => {
113
+ try {
114
+ const result = await reverseClip({
115
+ inputPath: args.inputPath,
116
+ outputPath: args.outputPath,
117
+ reverseAudio: args.reverseAudio ?? true,
118
+ });
119
+ return jsonResponse({
120
+ success: true, outputPath: result,
121
+ message: `Video reversed${(args.reverseAudio ?? true) ? ' (audio also reversed)' : ' (audio kept forward)'}.`,
122
+ });
123
+ } catch (error) {
124
+ const msg = error instanceof Error ? error.message : String(error);
125
+ logger.error(`reverse_clip failed: ${msg}`);
126
+ return jsonResponse({ success: false, error: msg }, true);
127
+ }
128
+ },
129
+
130
+ extract_audio: async (args) => {
131
+ try {
132
+ const result = await extractAudio({
133
+ inputPath: args.inputPath,
134
+ outputPath: args.outputPath,
135
+ format: args.format ?? 'mp3',
136
+ bitrate: args.bitrate ?? '192k',
137
+ });
138
+ return jsonResponse({
139
+ success: true, outputPath: result, format: args.format ?? 'mp3',
140
+ message: `Audio extracted as ${args.format ?? 'mp3'} (${args.bitrate ?? '192k'}).`,
141
+ });
142
+ } catch (error) {
143
+ const msg = error instanceof Error ? error.message : String(error);
144
+ logger.error(`extract_audio failed: ${msg}`);
145
+ return jsonResponse({ success: false, error: msg }, true);
146
+ }
147
+ },
148
+
149
+ burn_subtitles: async (args) => {
150
+ try {
151
+ const result = await burnSubtitles({
152
+ inputPath: args.inputPath,
153
+ outputPath: args.outputPath,
154
+ subtitlePath: args.subtitlePath,
155
+ fontSize: args.fontSize ?? 24,
156
+ fontColor: args.fontColor ?? '&Hffffff',
157
+ outlineColor: args.outlineColor ?? '&H000000',
158
+ outlineWidth: args.outlineWidth ?? 2,
159
+ position: args.position ?? 'bottom',
160
+ });
161
+ return jsonResponse({
162
+ success: true, outputPath: result,
163
+ message: `Subtitles burned into video (${args.position ?? 'bottom'}, size: ${args.fontSize ?? 24}).`,
164
+ });
165
+ } catch (error) {
166
+ const msg = error instanceof Error ? error.message : String(error);
167
+ logger.error(`burn_subtitles failed: ${msg}`);
168
+ return jsonResponse({ success: false, error: msg }, true);
169
+ }
170
+ },
171
+
172
+ auto_caption: async (args) => {
173
+ try {
174
+ const result = await autoCaption({
175
+ inputPath: args.inputPath,
176
+ outputPath: args.outputPath,
177
+ language: args.language,
178
+ fontSize: args.fontSize ?? 28,
179
+ position: args.position ?? 'bottom',
180
+ keepSrt: args.keepSrt ?? true,
181
+ });
182
+ return jsonResponse({
183
+ success: true,
184
+ videoPath: result.videoPath,
185
+ srtPath: result.srtPath,
186
+ message: 'Video captioned with Whisper AI. SRT file also generated.',
187
+ });
188
+ } catch (error) {
189
+ const msg = error instanceof Error ? error.message : String(error);
190
+ logger.error(`auto_caption failed: ${msg}`);
191
+ return jsonResponse({ success: false, error: msg }, true);
192
+ }
193
+ },
194
+
195
+ add_keyframe_animation: async (args) => {
196
+ try {
197
+ const result = await addKeyframeAnimation({
198
+ inputPath: args.inputPath,
199
+ outputPath: args.outputPath,
200
+ keyframes: args.keyframes as Keyframe[],
201
+ outputWidth: args.outputWidth,
202
+ outputHeight: args.outputHeight,
203
+ });
204
+ return jsonResponse({
205
+ success: true,
206
+ outputPath: result,
207
+ keyframeCount: (args.keyframes as Keyframe[]).length,
208
+ });
209
+ } catch (error) {
210
+ const msg = error instanceof Error ? error.message : String(error);
211
+ logger.error(`add_keyframe_animation failed: ${msg}`);
212
+ return jsonResponse({ success: false, error: msg }, true);
213
+ }
214
+ },
215
+
216
+ compose_picture_in_pip: async (args) => {
217
+ try {
218
+ const result = await composePip({
219
+ mainVideo: args.mainVideo,
220
+ overlayVideo: args.overlayVideo,
221
+ outputPath: args.outputPath,
222
+ position: (args.position as PipPosition) ?? 'bottom-right',
223
+ scale: args.scale ?? 0.3,
224
+ startTime: args.startTime ?? 0,
225
+ endTime: args.endTime,
226
+ borderWidth: args.borderWidth ?? 0,
227
+ borderColor: args.borderColor ?? 'white',
228
+ });
229
+ return jsonResponse({
230
+ success: true, outputPath: result,
231
+ pip: { position: args.position ?? 'bottom-right', scale: args.scale ?? 0.3 },
232
+ message: `PiP composed: overlay at ${args.position ?? 'bottom-right'} (${Math.round((args.scale ?? 0.3) * 100)}% size).`,
233
+ });
234
+ } catch (error) {
235
+ const msg = error instanceof Error ? error.message : String(error);
236
+ logger.error(`compose_picture_in_pip failed: ${msg}`);
237
+ return jsonResponse({ success: false, error: msg }, true);
238
+ }
239
+ },
240
+
241
+ add_audio_ducking: async (args) => {
242
+ try {
243
+ const result = await addAudioDucking({
244
+ inputPath: args.inputPath,
245
+ outputPath: args.outputPath,
246
+ duckLevel: args.duckLevel ?? 0.3,
247
+ attack: args.attack ?? 0.5,
248
+ release: args.release ?? 1.0,
249
+ });
250
+ return jsonResponse({
251
+ success: true, outputPath: result,
252
+ message: `Audio ducking applied (reduce to ${Math.round((args.duckLevel ?? 0.3) * 100)}% volume).`,
253
+ });
254
+ } catch (error) {
255
+ const msg = error instanceof Error ? error.message : String(error);
256
+ logger.error(`add_audio_ducking failed: ${msg}`);
257
+ return jsonResponse({ success: false, error: msg }, true);
258
+ }
259
+ },
260
+ };
@@ -0,0 +1,34 @@
1
+ import { type ToolHandler, type ToolResponse } from '../lib/types.js';
2
+ import { logger } from '../lib/logger.js';
3
+ import { videoHandlers } from './video.js';
4
+ import { postProductionHandlers } from './post-production.js';
5
+ import { ttsHandlers } from './tts.js';
6
+ import { smartScreenshotHandlers } from './smart-screenshot.js';
7
+ import { editingHandlers } from './editing.js';
8
+ import { capcutHandlers } from './capcut.js';
9
+
10
+ const HANDLER_REGISTRY: Record<string, ToolHandler> = {
11
+ ...videoHandlers,
12
+ ...postProductionHandlers,
13
+ ...ttsHandlers,
14
+ ...smartScreenshotHandlers,
15
+ ...editingHandlers,
16
+ ...capcutHandlers,
17
+ };
18
+
19
+ export async function handleToolCall(name: string, args: unknown): Promise<ToolResponse> {
20
+ const handler = HANDLER_REGISTRY[name];
21
+ if (!handler) {
22
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
23
+ }
24
+ try {
25
+ return await handler(args);
26
+ } catch (error) {
27
+ logger.logError('Tool execution failed', error, { tool: name });
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: message, code: 'INTERNAL_ERROR' }, null, 2) }],
31
+ isError: true,
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Post-production tool handlers
3
+ */
4
+
5
+ import { jsonResponse, type ToolHandler } from '../lib/types.js';
6
+ import { logger } from '../lib/logger.js';
7
+ import {
8
+ addBackgroundMusic,
9
+ concatenateVideos,
10
+ generateIntro,
11
+ convertToSocialFormat,
12
+ convertToAllFormats,
13
+ addTextOverlays,
14
+ } from '../tools/index.js';
15
+ import type {
16
+ AddMusicConfig,
17
+ ConcatClip,
18
+ TransitionType,
19
+ SocialFormat,
20
+ CropStrategy,
21
+ TextOverlay,
22
+ } from '../tools/index.js';
23
+ import * as path from 'path';
24
+
25
+ const OUTPUT_DIR = process.env.VIDEO_OUTPUT_DIR || './output';
26
+
27
+ export const postProductionHandlers: Record<string, ToolHandler> = {
28
+
29
+ add_background_music: async (args) => {
30
+ try {
31
+ const config: AddMusicConfig = {
32
+ videoPath: args.videoPath,
33
+ musicPath: args.musicPath,
34
+ outputPath: args.outputPath ?? addSuffix(args.videoPath, '-music'),
35
+ musicVolume: args.musicVolume ?? 0.25,
36
+ fadeInDuration: args.fadeInDuration ?? 2,
37
+ fadeOutDuration: args.fadeOutDuration ?? 3,
38
+ loopMusic: args.loopMusic ?? true,
39
+ };
40
+ const result = await addBackgroundMusic(config);
41
+ return jsonResponse({ success: true, outputPath: result });
42
+ } catch (error) {
43
+ const msg = error instanceof Error ? error.message : String(error);
44
+ logger.error(`add_background_music failed: ${msg}`);
45
+ return jsonResponse({ success: false, error: msg }, true);
46
+ }
47
+ },
48
+
49
+ concatenate_videos: async (args) => {
50
+ try {
51
+ const result = await concatenateVideos({
52
+ clips: args.clips as ConcatClip[],
53
+ outputPath: args.outputPath,
54
+ transition: (args.transition as TransitionType) ?? 'fade',
55
+ transitionDuration: args.transitionDuration ?? 1,
56
+ });
57
+ return jsonResponse({ success: true, outputPath: result });
58
+ } catch (error) {
59
+ const msg = error instanceof Error ? error.message : String(error);
60
+ logger.error(`concatenate_videos failed: ${msg}`);
61
+ return jsonResponse({ success: false, error: msg }, true);
62
+ }
63
+ },
64
+
65
+ generate_intro: async (args) => {
66
+ try {
67
+ const result = await generateIntro({
68
+ text: args.text,
69
+ subtitle: args.subtitle,
70
+ duration: args.duration ?? 3,
71
+ backgroundColor: args.backgroundColor ?? '#0a0a0a',
72
+ textColor: args.textColor ?? 'white',
73
+ outputPath: args.outputPath,
74
+ });
75
+ return jsonResponse({ success: true, outputPath: result });
76
+ } catch (error) {
77
+ const msg = error instanceof Error ? error.message : String(error);
78
+ logger.error(`generate_intro failed: ${msg}`);
79
+ return jsonResponse({ success: false, error: msg }, true);
80
+ }
81
+ },
82
+
83
+ convert_social_format: async (args) => {
84
+ try {
85
+ const result = await convertToSocialFormat({
86
+ inputPath: args.inputPath,
87
+ outputPath: args.outputPath,
88
+ format: args.format as SocialFormat,
89
+ strategy: (args.strategy as CropStrategy) ?? 'blur-background',
90
+ });
91
+ return jsonResponse({ success: true, outputPath: result, format: args.format });
92
+ } catch (error) {
93
+ const msg = error instanceof Error ? error.message : String(error);
94
+ logger.error(`convert_social_format failed: ${msg}`);
95
+ return jsonResponse({ success: false, error: msg }, true);
96
+ }
97
+ },
98
+
99
+ convert_all_social_formats: async (args) => {
100
+ try {
101
+ const formats = args.formats as SocialFormat[] | undefined;
102
+ const result = await convertToAllFormats(
103
+ args.inputPath,
104
+ args.outputDir ?? OUTPUT_DIR,
105
+ formats ?? ['instagram-reel', 'instagram-feed', 'youtube', 'tiktok'],
106
+ (args.strategy as CropStrategy) ?? 'blur-background',
107
+ );
108
+ return jsonResponse({ success: true, files: result, totalFormats: Object.keys(result).length });
109
+ } catch (error) {
110
+ const msg = error instanceof Error ? error.message : String(error);
111
+ logger.error(`convert_all_social_formats failed: ${msg}`);
112
+ return jsonResponse({ success: false, error: msg }, true);
113
+ }
114
+ },
115
+
116
+ add_text_overlay: async (args) => {
117
+ try {
118
+ const result = await addTextOverlays(
119
+ args.inputPath,
120
+ args.outputPath,
121
+ args.overlays as TextOverlay[],
122
+ );
123
+ return jsonResponse({ success: true, outputPath: result });
124
+ } catch (error) {
125
+ const msg = error instanceof Error ? error.message : String(error);
126
+ logger.error(`add_text_overlay failed: ${msg}`);
127
+ return jsonResponse({ success: false, error: msg }, true);
128
+ }
129
+ },
130
+ };
131
+
132
+ function addSuffix(filePath: string, suffix: string): string {
133
+ const ext = path.extname(filePath);
134
+ const base = filePath.slice(0, -ext.length);
135
+ return `${base}${suffix}${ext}`;
136
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Smart Screenshot tool handlers
3
+ */
4
+
5
+ import { jsonResponse, type ToolHandler } from '../lib/types.js';
6
+ import { logger } from '../lib/logger.js';
7
+ import { smartScreenshot } from '../tools/engine/smart-screenshot.js';
8
+ import type { SmartScreenshotConfig, SmartTarget } from '../tools/engine/smart-screenshot.js';
9
+
10
+ export const smartScreenshotHandlers: Record<string, ToolHandler> = {
11
+ /**
12
+ * Take element-aware screenshots of specific page features
13
+ */
14
+ screenshot_element: async (args) => {
15
+ try {
16
+ const config: SmartScreenshotConfig = {
17
+ url: args.url,
18
+ targets: normalizeTargetArgs(args.targets),
19
+ outputDir: args.outputDir,
20
+ viewport: args.viewport ?? { width: 1920, height: 1080 },
21
+ deviceScaleFactor: args.deviceScaleFactor ?? 1,
22
+ darkMode: args.darkMode ?? false,
23
+ includeFullPage: args.includeFullPage ?? false,
24
+ };
25
+
26
+ const result = await smartScreenshot(config);
27
+ return jsonResponse(result);
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ logger.error(`screenshot_element failed: ${message}`);
31
+ return jsonResponse({ success: false, error: message }, true);
32
+ }
33
+ },
34
+
35
+ /**
36
+ * Detect page features without taking screenshots
37
+ */
38
+ detect_page_features: async (args) => {
39
+ try {
40
+ const config: SmartScreenshotConfig = {
41
+ url: args.url,
42
+ targets: ['all'],
43
+ viewport: args.viewport ?? { width: 1920, height: 1080 },
44
+ includeFullPage: false,
45
+ };
46
+
47
+ // Use smartScreenshot with includeFullPage=false to just detect
48
+ // We'll capture a quick version that only detects without taking actual screenshots
49
+ const result = await smartScreenshot(config);
50
+ return jsonResponse({
51
+ success: true,
52
+ url: args.url,
53
+ features: result.detected.map(f => ({
54
+ name: f.name,
55
+ selector: f.selector,
56
+ size: `${f.bounds.width}x${f.bounds.height}`,
57
+ position: `(${Math.round(f.bounds.x)}, ${Math.round(f.bounds.y)})`,
58
+ matchMethod: f.matchMethod,
59
+ confidence: f.confidence,
60
+ })),
61
+ total: result.detected.length,
62
+ screenshots: result.screenshots.map(s => ({
63
+ feature: s.feature,
64
+ path: s.path,
65
+ })),
66
+ });
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ logger.error(`detect_page_features failed: ${message}`);
70
+ return jsonResponse({ success: false, error: message }, true);
71
+ }
72
+ },
73
+ };
74
+
75
+ function normalizeTargetArgs(targets: unknown): (string | SmartTarget)[] {
76
+ if (!Array.isArray(targets)) {
77
+ return [String(targets)];
78
+ }
79
+ return targets.map(t => {
80
+ if (typeof t === 'string') return t;
81
+ if (typeof t === 'object' && t !== null && 'feature' in t) {
82
+ return t as SmartTarget;
83
+ }
84
+ return String(t);
85
+ });
86
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * TTS & Narrated Video tool handlers
3
+ */
4
+
5
+ import { jsonResponse, type ToolHandler } from '../lib/types.js';
6
+ import { logger } from '../lib/logger.js';
7
+ import {
8
+ generateSpeech,
9
+ listElevenLabsVoices,
10
+ createNarratedVideo,
11
+ } from '../tools/index.js';
12
+ import type {
13
+ TTSConfig,
14
+ TTSProvider,
15
+ NarrationSegment,
16
+ } from '../tools/index.js';
17
+ import type { ViewportPreset, Scene } from '../tools/engine/types.js';
18
+
19
+ const OUTPUT_DIR = process.env.VIDEO_OUTPUT_DIR || './output';
20
+
21
+ export const ttsHandlers: Record<string, ToolHandler> = {
22
+
23
+ generate_speech: async (args) => {
24
+ try {
25
+ const config: TTSConfig = {
26
+ text: args.text,
27
+ outputPath: args.outputPath ?? `${OUTPUT_DIR}/speech-${Date.now()}.mp3`,
28
+ provider: args.provider as TTSProvider | undefined,
29
+ language: args.language ?? 'en',
30
+ speed: args.speed,
31
+ elevenLabsVoice: args.elevenLabsVoice,
32
+ elevenLabsModel: args.elevenLabsModel,
33
+ openaiVoice: args.openaiVoice,
34
+ openaiModel: args.openaiModel,
35
+ stability: args.stability,
36
+ similarityBoost: args.similarityBoost,
37
+ };
38
+
39
+ const result = await generateSpeech(config);
40
+ return jsonResponse(result);
41
+ } catch (error) {
42
+ const msg = error instanceof Error ? error.message : String(error);
43
+ logger.error(`generate_speech failed: ${msg}`);
44
+ return jsonResponse({ success: false, error: msg }, true);
45
+ }
46
+ },
47
+
48
+ list_voices: async () => {
49
+ try {
50
+ const voices = await listElevenLabsVoices();
51
+ return jsonResponse({
52
+ success: true,
53
+ voices,
54
+ totalVoices: voices.length,
55
+ premade: [
56
+ 'rachel (F)', 'sarah (F)', 'emily (F)', 'charlotte (F)', 'alice (F)',
57
+ 'brian (M)', 'adam (M)', 'daniel (M)', 'josh (M)', 'james (M)', 'liam (M)',
58
+ ],
59
+ });
60
+ } catch (error) {
61
+ const msg = error instanceof Error ? error.message : String(error);
62
+ logger.error(`list_voices failed: ${msg}`);
63
+ return jsonResponse({ success: false, error: msg }, true);
64
+ }
65
+ },
66
+
67
+ create_narrated_video: async (args) => {
68
+ try {
69
+ const segments: NarrationSegment[] = (args.segments as Array<{
70
+ text: string;
71
+ scene: Scene;
72
+ paddingAfter?: number;
73
+ }>).map((s) => ({
74
+ text: s.text,
75
+ scene: s.scene,
76
+ paddingAfter: s.paddingAfter,
77
+ }));
78
+
79
+ const hostname = new URL(args.url).hostname.replace(/^www\./, '').replace(/\./g, '-');
80
+ const defaultOutput = `${OUTPUT_DIR}/narrated-${hostname}-${new Date().toISOString().slice(0, 10)}`;
81
+
82
+ const result = await createNarratedVideo({
83
+ url: args.url,
84
+ segments,
85
+ outputPath: args.outputPath ?? defaultOutput,
86
+ provider: args.provider as TTSProvider | undefined,
87
+ language: args.language ?? 'en',
88
+ viewport: (args.viewport as ViewportPreset) ?? 'desktop',
89
+ elevenLabsVoice: args.elevenLabsVoice,
90
+ openaiVoice: args.openaiVoice,
91
+ speed: args.speed,
92
+ backgroundMusicPath: args.backgroundMusicPath,
93
+ backgroundMusicVolume: args.backgroundMusicVolume,
94
+ });
95
+
96
+ return jsonResponse(result);
97
+ } catch (error) {
98
+ const msg = error instanceof Error ? error.message : String(error);
99
+ logger.error(`create_narrated_video failed: ${msg}`);
100
+ return jsonResponse({ success: false, error: msg }, true);
101
+ }
102
+ },
103
+ };