@vibeframe/cli 0.27.0 → 0.30.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/adapters/index.d.ts +1 -0
  3. package/dist/agent/adapters/index.d.ts.map +1 -1
  4. package/dist/agent/adapters/index.js +5 -0
  5. package/dist/agent/adapters/index.js.map +1 -1
  6. package/dist/agent/adapters/openrouter.d.ts +16 -0
  7. package/dist/agent/adapters/openrouter.d.ts.map +1 -0
  8. package/dist/agent/adapters/openrouter.js +100 -0
  9. package/dist/agent/adapters/openrouter.js.map +1 -0
  10. package/dist/agent/types.d.ts +1 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +3 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/ai-edit-cli.d.ts.map +1 -1
  16. package/dist/commands/ai-edit-cli.js +18 -0
  17. package/dist/commands/ai-edit-cli.js.map +1 -1
  18. package/dist/commands/generate.js +14 -0
  19. package/dist/commands/generate.js.map +1 -1
  20. package/dist/commands/schema.d.ts +1 -0
  21. package/dist/commands/schema.d.ts.map +1 -1
  22. package/dist/commands/schema.js +122 -21
  23. package/dist/commands/schema.js.map +1 -1
  24. package/dist/commands/setup.js +5 -2
  25. package/dist/commands/setup.js.map +1 -1
  26. package/dist/config/schema.d.ts +2 -1
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +2 -0
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/index.js +0 -0
  31. package/package.json +16 -12
  32. package/.turbo/turbo-build.log +0 -4
  33. package/.turbo/turbo-lint.log +0 -21
  34. package/.turbo/turbo-test.log +0 -689
  35. package/src/agent/adapters/claude.ts +0 -143
  36. package/src/agent/adapters/gemini.ts +0 -159
  37. package/src/agent/adapters/index.ts +0 -61
  38. package/src/agent/adapters/ollama.ts +0 -231
  39. package/src/agent/adapters/openai.ts +0 -116
  40. package/src/agent/adapters/xai.ts +0 -119
  41. package/src/agent/index.ts +0 -251
  42. package/src/agent/memory/index.ts +0 -151
  43. package/src/agent/prompts/system.ts +0 -106
  44. package/src/agent/tools/ai-editing.ts +0 -845
  45. package/src/agent/tools/ai-generation.ts +0 -1073
  46. package/src/agent/tools/ai-pipeline.ts +0 -1055
  47. package/src/agent/tools/ai.ts +0 -21
  48. package/src/agent/tools/batch.ts +0 -429
  49. package/src/agent/tools/e2e.test.ts +0 -545
  50. package/src/agent/tools/export.ts +0 -184
  51. package/src/agent/tools/filesystem.ts +0 -237
  52. package/src/agent/tools/index.ts +0 -150
  53. package/src/agent/tools/integration.test.ts +0 -775
  54. package/src/agent/tools/media.ts +0 -697
  55. package/src/agent/tools/project.ts +0 -313
  56. package/src/agent/tools/timeline.ts +0 -951
  57. package/src/agent/types.ts +0 -68
  58. package/src/commands/agent.ts +0 -340
  59. package/src/commands/ai-analyze.ts +0 -429
  60. package/src/commands/ai-animated-caption.ts +0 -390
  61. package/src/commands/ai-audio.ts +0 -941
  62. package/src/commands/ai-broll.ts +0 -490
  63. package/src/commands/ai-edit-cli.ts +0 -658
  64. package/src/commands/ai-edit.ts +0 -1542
  65. package/src/commands/ai-fill-gaps.ts +0 -566
  66. package/src/commands/ai-helpers.ts +0 -65
  67. package/src/commands/ai-highlights.ts +0 -1303
  68. package/src/commands/ai-image.ts +0 -761
  69. package/src/commands/ai-motion.ts +0 -347
  70. package/src/commands/ai-narrate.ts +0 -451
  71. package/src/commands/ai-review.ts +0 -309
  72. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  73. package/src/commands/ai-script-pipeline.ts +0 -1365
  74. package/src/commands/ai-suggest-edit.ts +0 -264
  75. package/src/commands/ai-video-fx.ts +0 -445
  76. package/src/commands/ai-video.ts +0 -915
  77. package/src/commands/ai-viral.ts +0 -595
  78. package/src/commands/ai-visual-fx.ts +0 -601
  79. package/src/commands/ai.test.ts +0 -627
  80. package/src/commands/ai.ts +0 -307
  81. package/src/commands/analyze.ts +0 -282
  82. package/src/commands/audio.ts +0 -644
  83. package/src/commands/batch.test.ts +0 -279
  84. package/src/commands/batch.ts +0 -440
  85. package/src/commands/detect.ts +0 -329
  86. package/src/commands/doctor.ts +0 -237
  87. package/src/commands/edit-cmd.ts +0 -1014
  88. package/src/commands/export.ts +0 -918
  89. package/src/commands/generate.ts +0 -2146
  90. package/src/commands/media.ts +0 -177
  91. package/src/commands/output.ts +0 -142
  92. package/src/commands/pipeline.ts +0 -398
  93. package/src/commands/project.test.ts +0 -127
  94. package/src/commands/project.ts +0 -149
  95. package/src/commands/sanitize.ts +0 -60
  96. package/src/commands/schema.ts +0 -130
  97. package/src/commands/setup.ts +0 -509
  98. package/src/commands/timeline.test.ts +0 -499
  99. package/src/commands/timeline.ts +0 -529
  100. package/src/commands/validate.ts +0 -77
  101. package/src/config/config.test.ts +0 -197
  102. package/src/config/index.ts +0 -125
  103. package/src/config/schema.ts +0 -82
  104. package/src/engine/index.ts +0 -2
  105. package/src/engine/project.test.ts +0 -702
  106. package/src/engine/project.ts +0 -439
  107. package/src/index.ts +0 -146
  108. package/src/utils/api-key.test.ts +0 -41
  109. package/src/utils/api-key.ts +0 -247
  110. package/src/utils/audio.ts +0 -83
  111. package/src/utils/exec-safe.ts +0 -75
  112. package/src/utils/first-run.ts +0 -52
  113. package/src/utils/provider-resolver.ts +0 -56
  114. package/src/utils/remotion.ts +0 -951
  115. package/src/utils/subtitle.test.ts +0 -227
  116. package/src/utils/subtitle.ts +0 -169
  117. package/src/utils/tty.ts +0 -196
  118. package/tsconfig.json +0 -20
@@ -1,845 +0,0 @@
1
- /**
2
- * @module ai-editing
3
- * @description Agent tools for post-production editing (text overlay, review,
4
- * silence cut, jump cut, captions, noise reduction, fade, thumbnail,
5
- * SRT translation). FFmpeg-based and AI-assisted editing tools for agent use.
6
- * Most tools work without API keys (FFmpeg-only), some use Gemini or OpenAI.
7
- *
8
- * ## Tools: edit_text_overlay, analyze_review, edit_silence_cut, edit_jump_cut, edit_caption,
9
- * edit_noise_reduce, edit_fade, generate_thumbnail, edit_translate_srt
10
- * ## Dependencies: FFmpeg, Gemini (optional), OpenAI/Whisper (optional)
11
- * @see MODELS.md for the Single Source of Truth (SSOT) on supported providers/models
12
- */
13
-
14
- import { resolve } from "node:path";
15
- import type { ToolRegistry, ToolHandler } from "./index.js";
16
- import type { ToolDefinition, ToolResult } from "../types.js";
17
- import {
18
- executeTextOverlay,
19
- executeSilenceCut,
20
- executeJumpCut,
21
- executeCaption,
22
- executeNoiseReduce,
23
- executeFade,
24
- executeTranslateSrt,
25
- type TextOverlayStyle,
26
- type CaptionStyle,
27
- } from "../../commands/ai-edit.js";
28
- import { executeReview } from "../../commands/ai-review.js";
29
- import { executeThumbnailBestFrame } from "../../commands/ai-image.js";
30
- import { sanitizeAIResult } from "../../commands/sanitize.js";
31
-
32
- // ============================================================================
33
- // Tool Definitions
34
- // ============================================================================
35
-
36
- const textOverlayDef: ToolDefinition = {
37
- name: "edit_text_overlay",
38
- description: "Apply text overlays to a video using FFmpeg drawtext. Supports 4 style presets: lower-third, center-bold, subtitle, minimal. Auto-detects font and scales based on video resolution.",
39
- parameters: {
40
- type: "object",
41
- properties: {
42
- videoPath: {
43
- type: "string",
44
- description: "Path to input video file",
45
- },
46
- texts: {
47
- type: "array",
48
- items: { type: "string", description: "Text line to overlay" },
49
- description: "Text lines to overlay (multiple lines stack vertically)",
50
- },
51
- outputPath: {
52
- type: "string",
53
- description: "Output video file path",
54
- },
55
- style: {
56
- type: "string",
57
- description: "Overlay style preset",
58
- enum: ["lower-third", "center-bold", "subtitle", "minimal"],
59
- },
60
- fontSize: {
61
- type: "number",
62
- description: "Font size in pixels (auto-calculated if omitted)",
63
- },
64
- fontColor: {
65
- type: "string",
66
- description: "Font color (default: white)",
67
- },
68
- fadeDuration: {
69
- type: "number",
70
- description: "Fade in/out duration in seconds (default: 0.3)",
71
- },
72
- startTime: {
73
- type: "number",
74
- description: "Start time for overlay in seconds (default: 0)",
75
- },
76
- endTime: {
77
- type: "number",
78
- description: "End time for overlay in seconds (default: video duration)",
79
- },
80
- },
81
- required: ["videoPath", "texts", "outputPath"],
82
- },
83
- };
84
-
85
- const reviewDef: ToolDefinition = {
86
- name: "analyze_review",
87
- description: "Review video quality using Gemini AI. Analyzes pacing, color, text readability, audio-visual sync, and composition. Can auto-apply fixable corrections (color grading). Returns structured feedback with scores and recommendations.",
88
- parameters: {
89
- type: "object",
90
- properties: {
91
- videoPath: {
92
- type: "string",
93
- description: "Path to video file to review",
94
- },
95
- storyboardPath: {
96
- type: "string",
97
- description: "Optional path to storyboard JSON for context",
98
- },
99
- autoApply: {
100
- type: "boolean",
101
- description: "Automatically apply fixable corrections (default: false)",
102
- },
103
- verify: {
104
- type: "boolean",
105
- description: "Run verification pass after applying fixes (default: false)",
106
- },
107
- model: {
108
- type: "string",
109
- description: "Gemini model: flash (default), flash-2.5, pro",
110
- enum: ["flash", "flash-2.5", "pro"],
111
- },
112
- outputPath: {
113
- type: "string",
114
- description: "Output path for corrected video (when autoApply is true)",
115
- },
116
- },
117
- required: ["videoPath"],
118
- },
119
- };
120
-
121
- const silenceCutDef: ToolDefinition = {
122
- name: "edit_silence_cut",
123
- description: "Remove silent segments from a video. Default uses FFmpeg silencedetect (free, no API key). Use useGemini=true for smart context-aware detection via Gemini Video Understanding — distinguishes dead air from intentional pauses using visual+audio analysis.",
124
- parameters: {
125
- type: "object",
126
- properties: {
127
- videoPath: {
128
- type: "string",
129
- description: "Path to input video file",
130
- },
131
- outputPath: {
132
- type: "string",
133
- description: "Output file path (default: <name>-cut.<ext>)",
134
- },
135
- noiseThreshold: {
136
- type: "number",
137
- description: "Silence threshold in dB (default: -30). Lower = more sensitive. FFmpeg mode only.",
138
- },
139
- minDuration: {
140
- type: "number",
141
- description: "Minimum silence duration in seconds to cut (default: 0.5)",
142
- },
143
- padding: {
144
- type: "number",
145
- description: "Padding around non-silent segments in seconds (default: 0.1)",
146
- },
147
- analyzeOnly: {
148
- type: "boolean",
149
- description: "Only detect silence without cutting (default: false)",
150
- },
151
- useGemini: {
152
- type: "boolean",
153
- description: "Use Gemini Video Understanding for context-aware silence detection (default: false). Requires GOOGLE_API_KEY.",
154
- },
155
- model: {
156
- type: "string",
157
- description: "Gemini model to use (default: flash). Options: flash, flash-2.5, pro",
158
- },
159
- lowRes: {
160
- type: "boolean",
161
- description: "Low resolution mode for longer videos (Gemini only)",
162
- },
163
- },
164
- required: ["videoPath"],
165
- },
166
- };
167
-
168
- const jumpCutDef: ToolDefinition = {
169
- name: "edit_jump_cut",
170
- description: "Remove filler words (um, uh, like, etc.) from video using Whisper word-level timestamps + FFmpeg concat. Requires OpenAI API key. Detects filler words, cuts them out, and stitches remaining segments with stream copy (fast, no re-encode).",
171
- parameters: {
172
- type: "object",
173
- properties: {
174
- videoPath: {
175
- type: "string",
176
- description: "Path to input video file",
177
- },
178
- outputPath: {
179
- type: "string",
180
- description: "Output file path (default: <name>-jumpcut.<ext>)",
181
- },
182
- fillers: {
183
- type: "array",
184
- items: { type: "string", description: "A filler word to detect" },
185
- description: "Custom filler words to detect (default: um, uh, like, you know, etc.)",
186
- },
187
- padding: {
188
- type: "number",
189
- description: "Padding around cuts in seconds (default: 0.05)",
190
- },
191
- language: {
192
- type: "string",
193
- description: "Language code for transcription (e.g., en, ko)",
194
- },
195
- analyzeOnly: {
196
- type: "boolean",
197
- description: "Only detect fillers without cutting (default: false)",
198
- },
199
- },
200
- required: ["videoPath"],
201
- },
202
- };
203
-
204
- const captionDef: ToolDefinition = {
205
- name: "edit_caption",
206
- description: "Transcribe video with Whisper and burn styled captions using FFmpeg. Requires OpenAI API key. 4 style presets: minimal, bold (default), outline, karaoke. Auto-sizes font based on video resolution.",
207
- parameters: {
208
- type: "object",
209
- properties: {
210
- videoPath: {
211
- type: "string",
212
- description: "Path to input video file",
213
- },
214
- outputPath: {
215
- type: "string",
216
- description: "Output file path (default: <name>-captioned.<ext>)",
217
- },
218
- style: {
219
- type: "string",
220
- description: "Caption style preset",
221
- enum: ["minimal", "bold", "outline", "karaoke"],
222
- },
223
- fontSize: {
224
- type: "number",
225
- description: "Font size in pixels (auto-calculated based on resolution if omitted)",
226
- },
227
- fontColor: {
228
- type: "string",
229
- description: "Font color (default: white)",
230
- },
231
- language: {
232
- type: "string",
233
- description: "Language code for transcription (e.g., en, ko)",
234
- },
235
- position: {
236
- type: "string",
237
- description: "Caption position",
238
- enum: ["top", "center", "bottom"],
239
- },
240
- },
241
- required: ["videoPath"],
242
- },
243
- };
244
-
245
- const noiseReduceDef: ToolDefinition = {
246
- name: "edit_noise_reduce",
247
- description: "Remove background noise from audio/video using FFmpeg afftdn filter. No API key needed. Three strength presets: low, medium (default), high. High adds bandpass filtering.",
248
- parameters: {
249
- type: "object",
250
- properties: {
251
- inputPath: {
252
- type: "string",
253
- description: "Path to input audio or video file",
254
- },
255
- outputPath: {
256
- type: "string",
257
- description: "Output file path (default: <name>-denoised.<ext>)",
258
- },
259
- strength: {
260
- type: "string",
261
- description: "Noise reduction strength",
262
- enum: ["low", "medium", "high"],
263
- },
264
- noiseFloor: {
265
- type: "number",
266
- description: "Custom noise floor in dB (overrides strength preset)",
267
- },
268
- },
269
- required: ["inputPath"],
270
- },
271
- };
272
-
273
- const fadeDef: ToolDefinition = {
274
- name: "edit_fade",
275
- description: "Apply fade in/out effects to video using FFmpeg. No API key needed. Supports video-only, audio-only, or both. Configurable fade durations.",
276
- parameters: {
277
- type: "object",
278
- properties: {
279
- videoPath: {
280
- type: "string",
281
- description: "Path to input video file",
282
- },
283
- outputPath: {
284
- type: "string",
285
- description: "Output file path (default: <name>-faded.<ext>)",
286
- },
287
- fadeIn: {
288
- type: "number",
289
- description: "Fade-in duration in seconds (default: 1)",
290
- },
291
- fadeOut: {
292
- type: "number",
293
- description: "Fade-out duration in seconds (default: 1)",
294
- },
295
- audioOnly: {
296
- type: "boolean",
297
- description: "Apply fade to audio only (default: false)",
298
- },
299
- videoOnly: {
300
- type: "boolean",
301
- description: "Apply fade to video only (default: false)",
302
- },
303
- },
304
- required: ["videoPath"],
305
- },
306
- };
307
-
308
- const thumbnailBestFrameDef: ToolDefinition = {
309
- name: "generate_thumbnail",
310
- description: "Extract the best thumbnail frame from a video using Gemini AI analysis + FFmpeg frame extraction. Requires GOOGLE_API_KEY. Finds visually striking, well-composed frames.",
311
- parameters: {
312
- type: "object",
313
- properties: {
314
- videoPath: {
315
- type: "string",
316
- description: "Path to input video file",
317
- },
318
- outputPath: {
319
- type: "string",
320
- description: "Output image path (default: <name>-thumbnail.png)",
321
- },
322
- prompt: {
323
- type: "string",
324
- description: "Custom prompt for frame selection analysis",
325
- },
326
- model: {
327
- type: "string",
328
- description: "Gemini model to use",
329
- enum: ["flash", "flash-2.5", "pro"],
330
- },
331
- },
332
- required: ["videoPath"],
333
- },
334
- };
335
-
336
- const translateSrtDef: ToolDefinition = {
337
- name: "edit_translate_srt",
338
- description: "Translate SRT subtitle file to another language using Claude or OpenAI. Preserves timestamps. Batches segments for efficiency.",
339
- parameters: {
340
- type: "object",
341
- properties: {
342
- srtPath: {
343
- type: "string",
344
- description: "Path to input SRT file",
345
- },
346
- outputPath: {
347
- type: "string",
348
- description: "Output file path (default: <name>-<target>.srt)",
349
- },
350
- targetLanguage: {
351
- type: "string",
352
- description: "Target language (e.g., ko, es, fr, ja, zh)",
353
- },
354
- provider: {
355
- type: "string",
356
- description: "Translation provider",
357
- enum: ["claude", "openai"],
358
- },
359
- sourceLanguage: {
360
- type: "string",
361
- description: "Source language (auto-detected if omitted)",
362
- },
363
- },
364
- required: ["srtPath", "targetLanguage"],
365
- },
366
- };
367
-
368
- // ============================================================================
369
- // Tool Handlers
370
- // ============================================================================
371
-
372
- const textOverlayHandler: ToolHandler = async (args) => {
373
- const { videoPath, texts, outputPath, style, fontSize, fontColor, fadeDuration, startTime, endTime } = args as {
374
- videoPath: string;
375
- texts: string[];
376
- outputPath: string;
377
- style?: TextOverlayStyle;
378
- fontSize?: number;
379
- fontColor?: string;
380
- fadeDuration?: number;
381
- startTime?: number;
382
- endTime?: number;
383
- };
384
-
385
- if (!videoPath || !texts || texts.length === 0 || !outputPath) {
386
- return {
387
- toolCallId: "",
388
- success: false,
389
- output: "",
390
- error: "videoPath, texts (non-empty array), and outputPath are required",
391
- };
392
- }
393
-
394
- const result = await executeTextOverlay({
395
- videoPath,
396
- texts,
397
- outputPath,
398
- style,
399
- fontSize,
400
- fontColor,
401
- fadeDuration,
402
- startTime,
403
- endTime,
404
- });
405
-
406
- if (!result.success) {
407
- return {
408
- toolCallId: "",
409
- success: false,
410
- output: "",
411
- error: result.error || "Text overlay failed",
412
- };
413
- }
414
-
415
- return {
416
- toolCallId: "",
417
- success: true,
418
- output: `Text overlay applied: ${result.outputPath}`,
419
- };
420
- };
421
-
422
- const reviewHandler: ToolHandler = async (args) => {
423
- const { videoPath, storyboardPath, autoApply, verify, model, outputPath } = args as {
424
- videoPath: string;
425
- storyboardPath?: string;
426
- autoApply?: boolean;
427
- verify?: boolean;
428
- model?: "flash" | "flash-2.5" | "pro";
429
- outputPath?: string;
430
- };
431
-
432
- if (!videoPath) {
433
- return {
434
- toolCallId: "",
435
- success: false,
436
- output: "",
437
- error: "videoPath is required",
438
- };
439
- }
440
-
441
- const result = await executeReview({
442
- videoPath,
443
- storyboardPath,
444
- autoApply,
445
- verify,
446
- model,
447
- outputPath,
448
- });
449
-
450
- if (!result.success) {
451
- return {
452
- toolCallId: "",
453
- success: false,
454
- output: "",
455
- error: result.error || "Video review failed",
456
- };
457
- }
458
-
459
- const fb = sanitizeAIResult(result.feedback!);
460
- let output = `Video Review: ${fb.overallScore}/10\n`;
461
- output += `Pacing: ${fb.categories.pacing.score}/10, Color: ${fb.categories.color.score}/10, `;
462
- output += `Text: ${fb.categories.textReadability.score}/10, AV Sync: ${fb.categories.audioVisualSync.score}/10, `;
463
- output += `Composition: ${fb.categories.composition.score}/10\n`;
464
-
465
- if (result.appliedFixes && result.appliedFixes.length > 0) {
466
- output += `Applied fixes: ${sanitizeAIResult(result.appliedFixes).join("; ")}\n`;
467
- }
468
- if (result.verificationScore !== undefined) {
469
- output += `Verification score: ${result.verificationScore}/10\n`;
470
- }
471
- if (fb.recommendations.length > 0) {
472
- output += `Recommendations: ${fb.recommendations.join("; ")}`;
473
- }
474
-
475
- return {
476
- toolCallId: "",
477
- success: true,
478
- output,
479
- };
480
- };
481
-
482
- const silenceCutHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
483
- const videoPath = resolve(context.workingDirectory, args.videoPath as string);
484
- const ext = videoPath.split(".").pop() || "mp4";
485
- const name = videoPath.replace(/\.[^.]+$/, "");
486
- const outputPath = args.outputPath
487
- ? resolve(context.workingDirectory, args.outputPath as string)
488
- : `${name}-cut.${ext}`;
489
-
490
- try {
491
- const result = await executeSilenceCut({
492
- videoPath,
493
- outputPath,
494
- noiseThreshold: args.noiseThreshold as number | undefined,
495
- minDuration: args.minDuration as number | undefined,
496
- padding: args.padding as number | undefined,
497
- analyzeOnly: args.analyzeOnly as boolean | undefined,
498
- useGemini: args.useGemini as boolean | undefined,
499
- model: args.model as string | undefined,
500
- lowRes: args.lowRes as boolean | undefined,
501
- });
502
-
503
- if (!result.success) {
504
- return {
505
- toolCallId: "",
506
- success: false,
507
- output: "",
508
- error: result.error || "Silence cut failed",
509
- };
510
- }
511
-
512
- const lines: string[] = [];
513
- lines.push(`Detection method: ${result.method === "gemini" ? "Gemini Video Understanding" : "FFmpeg silencedetect"}`);
514
- lines.push(`Total duration: ${result.totalDuration!.toFixed(1)}s`);
515
- lines.push(`Silent periods: ${result.silentPeriods!.length}`);
516
- lines.push(`Silent duration: ${result.silentDuration!.toFixed(1)}s`);
517
- lines.push(`Non-silent duration: ${(result.totalDuration! - result.silentDuration!).toFixed(1)}s`);
518
-
519
- if (result.outputPath) {
520
- lines.push(`Output: ${result.outputPath}`);
521
- }
522
-
523
- return {
524
- toolCallId: "",
525
- success: true,
526
- output: lines.join("\n"),
527
- };
528
- } catch (error) {
529
- return {
530
- toolCallId: "",
531
- success: false,
532
- output: "",
533
- error: `Silence cut failed: ${error instanceof Error ? error.message : String(error)}`,
534
- };
535
- }
536
- };
537
-
538
- const jumpCutHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
539
- const videoPath = resolve(context.workingDirectory, args.videoPath as string);
540
- const ext = videoPath.split(".").pop() || "mp4";
541
- const name = videoPath.replace(/\.[^.]+$/, "");
542
- const outputPath = args.outputPath
543
- ? resolve(context.workingDirectory, args.outputPath as string)
544
- : `${name}-jumpcut.${ext}`;
545
-
546
- try {
547
- const result = await executeJumpCut({
548
- videoPath,
549
- outputPath,
550
- fillers: args.fillers as string[] | undefined,
551
- padding: args.padding as number | undefined,
552
- language: args.language as string | undefined,
553
- analyzeOnly: args.analyzeOnly as boolean | undefined,
554
- });
555
-
556
- if (!result.success) {
557
- return {
558
- toolCallId: "",
559
- success: false,
560
- output: "",
561
- error: result.error || "Jump cut failed",
562
- };
563
- }
564
-
565
- const lines: string[] = [];
566
- lines.push(`Total duration: ${result.totalDuration!.toFixed(1)}s`);
567
- lines.push(`Filler words found: ${result.fillerCount}`);
568
- lines.push(`Filler duration: ${result.fillerDuration!.toFixed(1)}s`);
569
- lines.push(`Clean duration: ${(result.totalDuration! - result.fillerDuration!).toFixed(1)}s`);
570
-
571
- if (result.fillers && result.fillers.length > 0) {
572
- lines.push("");
573
- lines.push("Detected fillers:");
574
- for (const filler of result.fillers) {
575
- lines.push(` "${filler.word}" at ${filler.start.toFixed(2)}s - ${filler.end.toFixed(2)}s`);
576
- }
577
- }
578
-
579
- if (result.outputPath) {
580
- lines.push(`Output: ${result.outputPath}`);
581
- }
582
-
583
- return {
584
- toolCallId: "",
585
- success: true,
586
- output: lines.join("\n"),
587
- };
588
- } catch (error) {
589
- return {
590
- toolCallId: "",
591
- success: false,
592
- output: "",
593
- error: `Jump cut failed: ${error instanceof Error ? error.message : String(error)}`,
594
- };
595
- }
596
- };
597
-
598
- const captionHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
599
- const videoPath = resolve(context.workingDirectory, args.videoPath as string);
600
- const ext = videoPath.split(".").pop() || "mp4";
601
- const name = videoPath.replace(/\.[^.]+$/, "");
602
- const outputPath = args.outputPath
603
- ? resolve(context.workingDirectory, args.outputPath as string)
604
- : `${name}-captioned.${ext}`;
605
-
606
- try {
607
- const result = await executeCaption({
608
- videoPath,
609
- outputPath,
610
- style: args.style as CaptionStyle | undefined,
611
- fontSize: args.fontSize as number | undefined,
612
- fontColor: args.fontColor as string | undefined,
613
- language: args.language as string | undefined,
614
- position: args.position as "top" | "center" | "bottom" | undefined,
615
- });
616
-
617
- if (!result.success) {
618
- return {
619
- toolCallId: "",
620
- success: false,
621
- output: "",
622
- error: result.error || "Caption failed",
623
- };
624
- }
625
-
626
- const lines: string[] = [];
627
- lines.push(`Captions applied: ${result.outputPath}`);
628
- lines.push(`Segments transcribed: ${result.segmentCount}`);
629
- if (result.srtPath) {
630
- lines.push(`SRT file: ${result.srtPath}`);
631
- }
632
-
633
- return {
634
- toolCallId: "",
635
- success: true,
636
- output: lines.join("\n"),
637
- };
638
- } catch (error) {
639
- return {
640
- toolCallId: "",
641
- success: false,
642
- output: "",
643
- error: `Caption failed: ${error instanceof Error ? error.message : String(error)}`,
644
- };
645
- }
646
- };
647
-
648
- const noiseReduceHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
649
- const inputPath = resolve(context.workingDirectory, args.inputPath as string);
650
- const ext = inputPath.split(".").pop() || "mp4";
651
- const name = inputPath.replace(/\.[^.]+$/, "");
652
- const outputPath = args.outputPath
653
- ? resolve(context.workingDirectory, args.outputPath as string)
654
- : `${name}-denoised.${ext}`;
655
-
656
- try {
657
- const result = await executeNoiseReduce({
658
- inputPath,
659
- outputPath,
660
- strength: args.strength as "low" | "medium" | "high" | undefined,
661
- noiseFloor: args.noiseFloor as number | undefined,
662
- });
663
-
664
- if (!result.success) {
665
- return {
666
- toolCallId: "",
667
- success: false,
668
- output: "",
669
- error: result.error || "Noise reduction failed",
670
- };
671
- }
672
-
673
- const lines: string[] = [];
674
- lines.push(`Noise reduction applied: ${result.outputPath}`);
675
- lines.push(`Input duration: ${result.inputDuration!.toFixed(1)}s`);
676
-
677
- return {
678
- toolCallId: "",
679
- success: true,
680
- output: lines.join("\n"),
681
- };
682
- } catch (error) {
683
- return {
684
- toolCallId: "",
685
- success: false,
686
- output: "",
687
- error: `Noise reduction failed: ${error instanceof Error ? error.message : String(error)}`,
688
- };
689
- }
690
- };
691
-
692
- const fadeHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
693
- const videoPath = resolve(context.workingDirectory, args.videoPath as string);
694
- const ext = videoPath.split(".").pop() || "mp4";
695
- const name = videoPath.replace(/\.[^.]+$/, "");
696
- const outputPath = args.outputPath
697
- ? resolve(context.workingDirectory, args.outputPath as string)
698
- : `${name}-faded.${ext}`;
699
-
700
- try {
701
- const result = await executeFade({
702
- videoPath,
703
- outputPath,
704
- fadeIn: args.fadeIn as number | undefined,
705
- fadeOut: args.fadeOut as number | undefined,
706
- audioOnly: args.audioOnly as boolean | undefined,
707
- videoOnly: args.videoOnly as boolean | undefined,
708
- });
709
-
710
- if (!result.success) {
711
- return {
712
- toolCallId: "",
713
- success: false,
714
- output: "",
715
- error: result.error || "Fade failed",
716
- };
717
- }
718
-
719
- const lines: string[] = [];
720
- lines.push(`Fade effects applied: ${result.outputPath}`);
721
- lines.push(`Total duration: ${result.totalDuration!.toFixed(1)}s`);
722
- if (result.fadeInApplied) lines.push(`Fade-in applied`);
723
- if (result.fadeOutApplied) lines.push(`Fade-out applied`);
724
-
725
- return {
726
- toolCallId: "",
727
- success: true,
728
- output: lines.join("\n"),
729
- };
730
- } catch (error) {
731
- return {
732
- toolCallId: "",
733
- success: false,
734
- output: "",
735
- error: `Fade failed: ${error instanceof Error ? error.message : String(error)}`,
736
- };
737
- }
738
- };
739
-
740
- const thumbnailBestFrameHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
741
- const videoPath = resolve(context.workingDirectory, args.videoPath as string);
742
- const name = videoPath.replace(/\.[^.]+$/, "");
743
- const outputPath = args.outputPath
744
- ? resolve(context.workingDirectory, args.outputPath as string)
745
- : `${name}-thumbnail.png`;
746
-
747
- try {
748
- const result = await executeThumbnailBestFrame({
749
- videoPath,
750
- outputPath,
751
- prompt: args.prompt as string | undefined,
752
- model: args.model as string | undefined,
753
- });
754
-
755
- if (!result.success) {
756
- return {
757
- toolCallId: "",
758
- success: false,
759
- output: "",
760
- error: result.error || "Best frame extraction failed",
761
- };
762
- }
763
-
764
- const lines: string[] = [];
765
- lines.push(`Best frame extracted: ${result.outputPath}`);
766
- lines.push(`Timestamp: ${result.timestamp!.toFixed(2)}s`);
767
- if (result.reason) lines.push(`Reason: ${sanitizeAIResult(result.reason)}`);
768
-
769
- return {
770
- toolCallId: "",
771
- success: true,
772
- output: lines.join("\n"),
773
- };
774
- } catch (error) {
775
- return {
776
- toolCallId: "",
777
- success: false,
778
- output: "",
779
- error: `Best frame extraction failed: ${error instanceof Error ? error.message : String(error)}`,
780
- };
781
- }
782
- };
783
-
784
- const translateSrtHandler: ToolHandler = async (args, context): Promise<ToolResult> => {
785
- const srtPath = resolve(context.workingDirectory, args.srtPath as string);
786
- const target = args.targetLanguage as string;
787
- const ext = srtPath.split(".").pop() || "srt";
788
- const name = srtPath.replace(/\.[^.]+$/, "");
789
- const outputPath = args.outputPath
790
- ? resolve(context.workingDirectory, args.outputPath as string)
791
- : `${name}-${target}.${ext}`;
792
-
793
- try {
794
- const result = await executeTranslateSrt({
795
- srtPath,
796
- outputPath,
797
- targetLanguage: target,
798
- provider: args.provider as "claude" | "openai" | undefined,
799
- sourceLanguage: args.sourceLanguage as string | undefined,
800
- });
801
-
802
- if (!result.success) {
803
- return {
804
- toolCallId: "",
805
- success: false,
806
- output: "",
807
- error: result.error || "Translation failed",
808
- };
809
- }
810
-
811
- const lines: string[] = [];
812
- lines.push(`Translation complete: ${result.outputPath}`);
813
- lines.push(`Segments translated: ${result.segmentCount}`);
814
- lines.push(`Target language: ${result.targetLanguage}`);
815
-
816
- return {
817
- toolCallId: "",
818
- success: true,
819
- output: lines.join("\n"),
820
- };
821
- } catch (error) {
822
- return {
823
- toolCallId: "",
824
- success: false,
825
- output: "",
826
- error: `Translation failed: ${error instanceof Error ? error.message : String(error)}`,
827
- };
828
- }
829
- };
830
-
831
- // ============================================================================
832
- // Registration
833
- // ============================================================================
834
-
835
- export function registerEditingTools(registry: ToolRegistry): void {
836
- registry.register(textOverlayDef, textOverlayHandler);
837
- registry.register(reviewDef, reviewHandler);
838
- registry.register(silenceCutDef, silenceCutHandler);
839
- registry.register(jumpCutDef, jumpCutHandler);
840
- registry.register(captionDef, captionHandler);
841
- registry.register(noiseReduceDef, noiseReduceHandler);
842
- registry.register(fadeDef, fadeHandler);
843
- registry.register(thumbnailBestFrameDef, thumbnailBestFrameHandler);
844
- registry.register(translateSrtDef, translateSrtHandler);
845
- }