@kolbo/mcp 1.22.3 → 1.25.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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/src/index.js +2 -0
- package/src/tools/generate.js +62 -45
- package/src/tools/models.js +25 -0
- package/src/tools/voices.js +49 -0
package/README.md
CHANGED
|
@@ -120,10 +120,10 @@ Without the optional skill, the config block alone already exposes every tool
|
|
|
120
120
|
| `generate_image_edit` | Existing image(s) + prompt → edited image |
|
|
121
121
|
| `generate_video` | Text → video |
|
|
122
122
|
| `generate_video_from_image` | Still image + motion prompt → video |
|
|
123
|
-
| `generate_video_from_video` | Input video
|
|
123
|
+
| `generate_video_from_video` | Input video → restyled video, or burn in subtitles (video-to-video). `prompt` optional — prompt-less models (VEED Subtitles, Act Two, Wan Animate) use `preset` / `source_language` / `translation_language`, plus `srt_content` / `srt_file_url` / `vocabulary` / `customization` for VEED |
|
|
124
124
|
| `generate_elements` | Reference images/videos + prompt → animated video |
|
|
125
125
|
| `generate_first_last_frame` | First frame + last frame → interpolated video |
|
|
126
|
-
| `generate_lipsync` | Source image/video + audio → lipsynced video |
|
|
126
|
+
| `generate_lipsync` | Source image/video + audio → lipsynced video (Sync-3 adds active-speaker selection, emotion, model mode, temperature) |
|
|
127
127
|
| `generate_creative_director` | One brief → N coordinated scenes (image or video) |
|
|
128
128
|
| `generate_music` | Text (+ optional lyrics) → song |
|
|
129
129
|
| `generate_speech` | Text + voice → spoken audio |
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -81,7 +81,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
81
81
|
| `generate_video_from_video` | Restyle/transform an existing video. Keeps original motion. |
|
|
82
82
|
| `generate_elements` | Reference-driven video. **Primary route for DNA → video.** |
|
|
83
83
|
| `generate_first_last_frame` | Keyframe interpolation between two frames. |
|
|
84
|
-
| `generate_lipsync` | Lipsync audio to an image or video face. |
|
|
84
|
+
| `generate_lipsync` | Lipsync audio to an image or video face. Sync-3 adds multi-person speaker selection (`active_speaker_detection`), `emotion`, `model_mode`, `temperature`. |
|
|
85
85
|
| `generate_music` | Music generation (Suno + variants). |
|
|
86
86
|
| `generate_speech` | TTS. Use `list_voices` to pick a voice. |
|
|
87
87
|
| `generate_sound` | Sound effects. |
|
package/src/index.js
CHANGED
|
@@ -69,6 +69,7 @@ const { registerPresetTools } = require('./tools/presets');
|
|
|
69
69
|
const { registerAppBuilderTools } = require('./tools/app_builder');
|
|
70
70
|
const { registerArtifactTools } = require('./tools/artifacts');
|
|
71
71
|
const { registerProjectTools } = require('./tools/projects');
|
|
72
|
+
const { registerVoiceTools } = require('./tools/voices');
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
75
|
* Build a fully-configured Kolbo MCP server (all tool groups registered)
|
|
@@ -95,6 +96,7 @@ function createServer(opts = {}) {
|
|
|
95
96
|
// keep identical text-URL output.
|
|
96
97
|
registerGenerateTools(server, client, { inlineImages: !!opts.inlineImages });
|
|
97
98
|
registerModelTools(server, client);
|
|
99
|
+
registerVoiceTools(server, client);
|
|
98
100
|
registerChatTools(server, client);
|
|
99
101
|
registerVisualDnaTools(server, client);
|
|
100
102
|
registerMoodboardTools(server, client);
|
package/src/tools/generate.js
CHANGED
|
@@ -357,43 +357,6 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
357
357
|
}
|
|
358
358
|
);
|
|
359
359
|
|
|
360
|
-
// ─── list_voices ─────────────────────────────────────────────
|
|
361
|
-
server.tool(
|
|
362
|
-
'list_voices',
|
|
363
|
-
'List available TTS voices for generate_speech. Returns preset voices and the user\'s own cloned/designed voices. Filter by provider, language, or gender to find the right voice. Use the returned `voice_id` as the `voice` parameter in generate_speech.',
|
|
364
|
-
{
|
|
365
|
-
provider: z.string().optional().describe('Filter by provider (e.g., "elevenLabs", "google")'),
|
|
366
|
-
language: z.string().optional().describe('Filter by language name or code (e.g., "English", "en-US")'),
|
|
367
|
-
gender: z.string().optional().describe('Filter by gender (e.g., "Female", "Male")')
|
|
368
|
-
},
|
|
369
|
-
async ({ provider, language, gender }) => {
|
|
370
|
-
const params = new URLSearchParams();
|
|
371
|
-
if (provider) params.set('provider', provider);
|
|
372
|
-
if (language) params.set('language', language);
|
|
373
|
-
if (gender) params.set('gender', gender);
|
|
374
|
-
|
|
375
|
-
const qs = params.toString();
|
|
376
|
-
const result = await client.get(`/v1/voices${qs ? '?' + qs : ''}`);
|
|
377
|
-
|
|
378
|
-
// Summarize for context window efficiency
|
|
379
|
-
const voices = (result.voices || []).map(v => ({
|
|
380
|
-
voice_id: v.voice_id,
|
|
381
|
-
name: v.name,
|
|
382
|
-
provider: v.provider,
|
|
383
|
-
language: v.language,
|
|
384
|
-
gender: v.gender,
|
|
385
|
-
custom: v.custom
|
|
386
|
-
}));
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
content: [{
|
|
390
|
-
type: 'text',
|
|
391
|
-
text: JSON.stringify({ voices, count: result.count }, null, 2)
|
|
392
|
-
}]
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
);
|
|
396
|
-
|
|
397
360
|
// ─── get_generation_status ─────────────────────────────────
|
|
398
361
|
server.tool(
|
|
399
362
|
'get_generation_status',
|
|
@@ -568,12 +531,27 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
568
531
|
{
|
|
569
532
|
source: z.string().describe('URL or absolute local path to the source image or video (the face to animate). For lipsync-video: duration must fall within `min_video_duration`-`max_video_duration` from list_models.'),
|
|
570
533
|
audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to). Duration must fall within `min_audio_duration`-`max_audio_duration` from list_models; format must be in `supported_audio_formats` (when set).'),
|
|
571
|
-
text_prompt: z.string().optional().describe('Optional text prompt (for performance-capable models)'),
|
|
534
|
+
text_prompt: z.string().optional().describe('Optional text prompt (for performance-capable models). For Sync-3 this is the free-text emotion/acting prompt, e.g. "speaking with excitement, calm and serious".'),
|
|
572
535
|
model: z.string().optional().describe('Model identifier. Use list_models type="lipsync-image" or type="lipsync-video" to see options. Omit for Smart Select.'),
|
|
573
536
|
bounding_box_target: z.array(z.number()).optional().describe('Optional bounding box [x, y, w, h] for multi-face inputs (Hedra Character3 style). Leave empty for single-face.'),
|
|
537
|
+
// Sync-3 (fal-ai/sync-lipsync/v3) only; ignored by other models.
|
|
538
|
+
sync_mode: z.enum(['cut_off', 'loop', 'bounce', 'silence', 'remap']).optional().describe('Sync-3 / sync-lipsync family: how to reconcile an audio/video length mismatch. Default cut_off.'),
|
|
539
|
+
model_mode: z.enum(['lips', 'face', 'head', 'lipsync', 'emotion', 'talking_head']).optional().describe('Sync-3 only: which region drives the sync.'),
|
|
540
|
+
emotion: z.enum(['neutral', 'happy', 'sad', 'angry', 'disgusted', 'surprised']).optional().describe('Sync-3 only: quick emotion shortcut. A free-text text_prompt overrides this and gives finer control.'),
|
|
541
|
+
temperature: z.number().min(0).max(1).optional().describe('Sync-3 only: expressiveness 0 (subtle) .. 1 (energetic).'),
|
|
542
|
+
occlusion_detection_enabled: z.boolean().optional().describe('Sync-3 only: handle objects passing in front of the face.'),
|
|
543
|
+
active_speaker_detection: z.object({
|
|
544
|
+
auto_detect: z.boolean().optional().describe('Auto-detect and sync the active speaker.'),
|
|
545
|
+
v3: z.boolean().optional().describe('Use Sync.so v3 detection engine.'),
|
|
546
|
+
frame_number: z.number().int().min(0).optional().describe('Frame index the coordinates refer to.'),
|
|
547
|
+
coordinates: z.array(z.number().int()).length(2).optional().describe('[x, y] PIXEL point on the speaker face (source-video resolution).'),
|
|
548
|
+
bounding_boxes: z.array(z.array(z.number().int())).optional().describe('Per-frame face boxes [x1,y1,x2,y2].'),
|
|
549
|
+
bounding_boxes_url: z.string().optional().describe('URL to a JSON file with per-frame boxes.'),
|
|
550
|
+
face_image: z.string().optional().describe('Base64-encoded reference face image.')
|
|
551
|
+
}).optional().describe('Sync-3 only: choose which speaker gets synced in a multi-person video. Use auto_detect:true for automatic, or coordinates + frame_number to pin a specific face.'),
|
|
574
552
|
project_id: projectIdField
|
|
575
553
|
},
|
|
576
|
-
async ({ source, audio, text_prompt, model, bounding_box_target, project_id }) => {
|
|
554
|
+
async ({ source, audio, text_prompt, model, bounding_box_target, sync_mode, model_mode, emotion, temperature, occlusion_detection_enabled, active_speaker_detection, project_id }) => {
|
|
577
555
|
if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
|
|
578
556
|
if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
|
|
579
557
|
|
|
@@ -589,6 +567,13 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
589
567
|
prompt: text_prompt,
|
|
590
568
|
model,
|
|
591
569
|
bounding_box_target,
|
|
570
|
+
// Sync-3 advanced options (additive; ignored by other models)
|
|
571
|
+
sync_mode,
|
|
572
|
+
model_mode,
|
|
573
|
+
emotion,
|
|
574
|
+
temperature,
|
|
575
|
+
occlusion_detection_enabled,
|
|
576
|
+
active_speaker_detection,
|
|
592
577
|
project_id
|
|
593
578
|
});
|
|
594
579
|
} else {
|
|
@@ -611,6 +596,13 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
611
596
|
if (text_prompt) form.append('prompt', text_prompt);
|
|
612
597
|
if (model) form.append('model', model);
|
|
613
598
|
if (bounding_box_target) form.append('bounding_box_target', JSON.stringify(bounding_box_target));
|
|
599
|
+
// Sync-3 advanced options (additive — ignored by other models)
|
|
600
|
+
if (sync_mode) form.append('sync_mode', sync_mode);
|
|
601
|
+
if (model_mode) form.append('model_mode', model_mode);
|
|
602
|
+
if (emotion) form.append('emotion', emotion);
|
|
603
|
+
if (temperature !== undefined) form.append('temperature', String(temperature));
|
|
604
|
+
if (occlusion_detection_enabled !== undefined) form.append('occlusion_detection_enabled', String(occlusion_detection_enabled));
|
|
605
|
+
if (active_speaker_detection) form.append('active_speaker_detection', JSON.stringify(active_speaker_detection));
|
|
614
606
|
if (project_id) form.append('project_id', project_id);
|
|
615
607
|
startResponse = await client.postMultipart('/v1/generate/lipsync', form);
|
|
616
608
|
}
|
|
@@ -638,10 +630,10 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
638
630
|
// ─── generate_video_from_video ─────────────────────────────
|
|
639
631
|
server.tool(
|
|
640
632
|
'generate_video_from_video',
|
|
641
|
-
'Restyle / transform an existing video
|
|
633
|
+
'Restyle / transform an existing video (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, character replacement, or burning in styled subtitles (VEED Subtitles). Source video can be a URL or absolute local path. `prompt` is OPTIONAL: most models need it, but prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control) ignore it. For VEED Subtitles, pass a `preset` style and optional `source_language` / `translation_language` instead of a prompt. IMPORTANT: different models support different extra inputs — call list_models type="video_to_video" and read max_images / max_videos / max_elements on the chosen model before generating. Pass reference_images for models with max_images > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with max_videos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with max_elements > 0. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
642
634
|
{
|
|
643
635
|
source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. **Source duration must fall within `min_video_duration`-`max_video_duration` from list_models for the chosen model** — videos outside that range are rejected (or silently truncated by some upstream providers). For models that use reference_videos as their primary input (e.g. WAN 2.6 reference-to-video), pass the first reference video here and also include it in reference_videos.'),
|
|
644
|
-
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
636
|
+
prompt: z.string().optional().describe('Text description of the desired restyle / transformation. Required by most video-to-video models; omit for prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control).'),
|
|
645
637
|
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check max_images / max_videos / max_elements / max_video_duration per model. Omit for Smart Select.'),
|
|
646
638
|
aspect_ratio: z.string().optional().describe('Output aspect ratio. Must be in `supported_aspect_ratios` from list_models when set. Default: matches source'),
|
|
647
639
|
duration: z.number().optional().describe('Output duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: matches source'),
|
|
@@ -651,24 +643,49 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
651
643
|
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs. **Cap: pass at most `max_images` URLs from list_models — if `max_images === 0` the model does not accept image refs.** Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit.'),
|
|
652
644
|
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs. **Cap: pass at most `max_videos` URLs from list_models — if `max_videos <= 1` only the source_video is accepted.** Example: WAN 2.6 reference-to-video accepts 1–3 reference videos.'),
|
|
653
645
|
elements: z.array(z.string()).optional().describe('Array of element image URLs. **Cap: pass at most `max_elements` URLs from list_models — if `max_elements === 0` the model does not accept elements.** Elements are style or character reference assets alongside the main video.'),
|
|
646
|
+
// VEED Subtitles (model: veed/subtitles) — burns styled subtitles into the video
|
|
647
|
+
preset: z.string().optional().describe('VEED Subtitles only: caption style preset (e.g. "glass", "whisper", "fusion", "simple", "vegas"). Call list_models type="video_to_video" for the veed/subtitles model. Ignored by other models.'),
|
|
648
|
+
source_language: z.string().optional().describe('VEED Subtitles only: BCP-47 code of the spoken language to improve transcription accuracy (e.g. "en-US", "es-ES", "he-IL"). Omit to auto-detect.'),
|
|
649
|
+
translation_language: z.string().optional().describe('VEED Subtitles only: BCP-47 code to translate the subtitles into (e.g. "en-US", "fr-FR"). Omit to keep the original spoken language.'),
|
|
650
|
+
srt_content: z.string().optional().describe('VEED Subtitles only: raw .srt subtitle text to burn in. When set, auto-transcription is skipped.'),
|
|
651
|
+
srt_file_url: z.string().optional().describe('VEED Subtitles only: URL to a .srt subtitle file. Alternative to srt_content. When set, auto-transcription is skipped.'),
|
|
652
|
+
vocabulary: z.array(z.object({
|
|
653
|
+
word: z.string().describe('Correct spelling to enforce'),
|
|
654
|
+
replaces: z.array(z.string()).describe('Mis-transcriptions to replace with `word`'),
|
|
655
|
+
})).optional().describe('VEED Subtitles only: brand names / jargon to help transcription (e.g. [{"word":"Kolbo","replaces":["colbo","kolboo"]}]). Ignored when srt_content / srt_file_url is set.'),
|
|
656
|
+
customization: z.object({
|
|
657
|
+
position: z.enum(['top', 'center', 'bottom']).optional().describe('Caption vertical position. Ignored by complex animated presets.'),
|
|
658
|
+
shadow: z.enum(['none', 'min', 'mid', 'max']).optional().describe('Text shadow intensity.'),
|
|
659
|
+
text_customizations: z.object({
|
|
660
|
+
baseline: z.object({ font: z.string().optional(), weight: z.number().int().min(100).max(900).optional(), color: z.string().optional() }).optional().describe('All words: Google font name, weight 100-900, hex colour.'),
|
|
661
|
+
highlighted: z.object({ font: z.string().optional(), weight: z.number().int().min(100).max(900).optional(), color: z.string().optional() }).optional().describe('Highlighted word tier styling.'),
|
|
662
|
+
}).optional(),
|
|
663
|
+
}).optional().describe('VEED Subtitles only: style overrides. Any omitted field keeps the preset default. Best supported by Basic presets.'),
|
|
654
664
|
project_id: projectIdField
|
|
655
665
|
},
|
|
656
|
-
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, project_id }) => {
|
|
666
|
+
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
|
|
657
667
|
if (!source_video) throw new Error('source_video is required');
|
|
658
|
-
if (!prompt) throw new Error('prompt is required');
|
|
659
668
|
|
|
660
669
|
const isUrl = /^https?:\/\//i.test(source_video);
|
|
661
670
|
let startResponse;
|
|
662
671
|
if (isUrl) {
|
|
663
672
|
startResponse = await client.post('/v1/generate/video-from-video', {
|
|
664
673
|
video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
|
|
665
|
-
reference_images, reference_videos, elements,
|
|
674
|
+
reference_images, reference_videos, elements, preset, source_language, translation_language,
|
|
675
|
+
srt_content, srt_file_url, vocabulary, customization, project_id
|
|
666
676
|
});
|
|
667
677
|
} else {
|
|
668
678
|
const resolved = await resolveToBuffer(source_video, 'video');
|
|
669
679
|
const form = new FormData();
|
|
670
680
|
form.append('files', resolved.buffer, { filename: resolved.filename, contentType: resolved.contentType });
|
|
671
|
-
form.append('prompt', prompt);
|
|
681
|
+
if (prompt) form.append('prompt', prompt);
|
|
682
|
+
if (preset) form.append('preset', preset);
|
|
683
|
+
if (source_language) form.append('source_language', source_language);
|
|
684
|
+
if (translation_language) form.append('translation_language', translation_language);
|
|
685
|
+
if (srt_content) form.append('srt_content', srt_content);
|
|
686
|
+
if (srt_file_url) form.append('srt_file_url', srt_file_url);
|
|
687
|
+
if (vocabulary) form.append('vocabulary', JSON.stringify(vocabulary));
|
|
688
|
+
if (customization) form.append('customization', JSON.stringify(customization));
|
|
672
689
|
if (model) form.append('model', model);
|
|
673
690
|
if (aspect_ratio) form.append('aspect_ratio', aspect_ratio);
|
|
674
691
|
if (duration !== undefined) form.append('duration', String(duration));
|
package/src/tools/models.js
CHANGED
|
@@ -158,6 +158,31 @@ function registerModelTools(server, client) {
|
|
|
158
158
|
parts.push(`images_per_request: ${m.images_per_request}`);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// Quality tiers (image models that support quality selection)
|
|
162
|
+
if (Array.isArray(m.supported_qualities) && m.supported_qualities.length) {
|
|
163
|
+
const qMult = m.quality_multipliers || {};
|
|
164
|
+
const qParts = m.supported_qualities.map(q =>
|
|
165
|
+
qMult[q] && qMult[q] !== 1 ? `${q}(${qMult[q]}×)` : q
|
|
166
|
+
);
|
|
167
|
+
parts.push(`quality: ${qParts.join(' · ')}${m.default_quality ? ` (default ${m.default_quality})` : ''}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fixed-price override (some models charge a flat rate per resolution instead of per-second)
|
|
171
|
+
if (m.flat_credit_by_resolution && typeof m.flat_credit_by_resolution === 'object' && Object.keys(m.flat_credit_by_resolution).length) {
|
|
172
|
+
const fp = Object.entries(m.flat_credit_by_resolution).map(([k, v]) => `${k}:${v}cr`).join(' · ');
|
|
173
|
+
parts.push(`flat_price: ${fp}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Estimated generation time (wall-clock at base settings)
|
|
177
|
+
if (m.estimated_duration_seconds != null) {
|
|
178
|
+
parts.push(`est_time: ~${m.estimated_duration_seconds}s`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// NSFW flag
|
|
182
|
+
if (m.nsfw_only) {
|
|
183
|
+
parts.push('nsfw: required');
|
|
184
|
+
}
|
|
185
|
+
|
|
161
186
|
return parts.length ? `\n ${parts.join(' | ')}` : '';
|
|
162
187
|
};
|
|
163
188
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
+
* CONTRACT. Never rename, remove, or break an existing tool/arg. Full rules: ../index.js top-of-file. */
|
|
3
|
+
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
|
|
6
|
+
function registerVoiceTools(server, client) {
|
|
7
|
+
// ─── list_voices ──────────────────────────────────────────────
|
|
8
|
+
server.tool(
|
|
9
|
+
'list_voices',
|
|
10
|
+
'List available TTS voices for speech generation. Filter by language, gender, or provider to find the right voice. Returns voice_id, name, provider, language, gender, accent, description, styles, and preview_url for each voice.',
|
|
11
|
+
{
|
|
12
|
+
language: z.string().optional().describe('Filter by language name (e.g. "english", "hebrew", "spanish", "french"). Case-insensitive partial match.'),
|
|
13
|
+
gender: z.enum(['male', 'female']).optional().describe('Filter by gender.'),
|
|
14
|
+
provider: z.string().optional().describe('Filter by provider (e.g. "elevenlabs", "google"). Omit for all providers.')
|
|
15
|
+
},
|
|
16
|
+
async ({ language, gender, provider }) => {
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
if (language) params.set('language', language);
|
|
19
|
+
if (gender) params.set('gender', gender);
|
|
20
|
+
if (provider) params.set('provider', provider);
|
|
21
|
+
|
|
22
|
+
const path = `/v1/voices${params.toString() ? '?' + params.toString() : ''}`;
|
|
23
|
+
const result = await client.get(path);
|
|
24
|
+
|
|
25
|
+
const voices = result.voices || [];
|
|
26
|
+
if (voices.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: 'No voices found matching those filters.' }]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const lines = voices.map(v => {
|
|
33
|
+
const tags = [v.language, v.gender, v.accent].filter(Boolean).join(' · ');
|
|
34
|
+
const styles = Array.isArray(v.styles) && v.styles.length ? ` | styles: ${v.styles.join(', ')}` : '';
|
|
35
|
+
const v3 = v.v3_optimized ? ' [v3]' : '';
|
|
36
|
+
return `${v.voice_id} — ${v.name} (${v.provider})${v3}\n ${tags}${styles}${v.description ? `\n ${v.description}` : ''}`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: `Available voices (${voices.length}):\n\n${lines.join('\n\n')}\n\nUse the "voice_id" value in generate_speech calls.`
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { registerVoiceTools };
|