@kolbo/mcp 1.22.4 → 1.26.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 +13 -2
- package/package.json +1 -1
- package/skill/SKILL.md +3 -1
- package/skill/references/workflows/music-library.md +32 -0
- package/src/index.js +4 -0
- package/src/tools/generate.js +62 -45
- package/src/tools/models.js +25 -0
- package/src/tools/music_library.js +179 -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 |
|
|
@@ -190,6 +190,17 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
190
190
|
|------|-------------|
|
|
191
191
|
| `publish_html_artifact` | Publish an HTML page, SVG, or Mermaid diagram and get a public shareable URL on `sites.kolbo.ai`. Pass `share_token` from a prior publish to update the same URL in place (old content kept in version history). |
|
|
192
192
|
|
|
193
|
+
**Music Library** (stock / production music)
|
|
194
|
+
| Tool | Description |
|
|
195
|
+
|------|-------------|
|
|
196
|
+
| `search_music_library` | Search the licensed stock-music catalog by keyword + genre/mood/BPM/duration filters. Find a ready-made track (distinct from `generate_music`, which composes a new song). |
|
|
197
|
+
| `analyze_script_for_music` | AI: turn a video/voiceover script into a music search (`query`, `mood`, `genre`, `keywords`). |
|
|
198
|
+
| `browse_music_library` | Browse the catalog without a query (paginated). |
|
|
199
|
+
| `get_music_library_facets` | List available genres, moods, instruments + BPM/duration ranges. |
|
|
200
|
+
| `get_music_track_audio` | Get a track's downloadable 128/320/WAV URLs by id. |
|
|
201
|
+
| `get_music_track_related` | Get stems + alternate versions of a track. |
|
|
202
|
+
| `get_music_track_lyrics` | Get lyrics text, theme, and explicit flag for a track. |
|
|
203
|
+
|
|
193
204
|
**Discovery & Account**
|
|
194
205
|
| Tool | Description |
|
|
195
206
|
|------|-------------|
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -49,6 +49,7 @@ If the user is on a whitelabel build (`sapir`, etc.), they must use their brande
|
|
|
49
49
|
| Generate a **Veo 3 / 3.1** video | `references/models/veo.md` |
|
|
50
50
|
| Build a **multi-scene set** (Creative Director, storyboard, campaign batch, 4+ angles) | `references/models/creative-director.md` |
|
|
51
51
|
| Generate **music** (Suno, song, lyrics, jingle, score) | `references/models/music.md` |
|
|
52
|
+
| Find an **existing / stock / library / royalty-free track** to score a video, ad, or voiceover | `references/workflows/music-library.md` |
|
|
52
53
|
| Build an **HTML presentation / slide deck** | `references/models/html-presentation.md` |
|
|
53
54
|
| Build a **landing page / marketing site** | `references/models/landing-page.md` |
|
|
54
55
|
| Build a **dashboard / data viz / interactive widget / mini-game / UI mockup** | `references/models/visual-code.md` |
|
|
@@ -81,7 +82,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
81
82
|
| `generate_video_from_video` | Restyle/transform an existing video. Keeps original motion. |
|
|
82
83
|
| `generate_elements` | Reference-driven video. **Primary route for DNA → video.** |
|
|
83
84
|
| `generate_first_last_frame` | Keyframe interpolation between two frames. |
|
|
84
|
-
| `generate_lipsync` | Lipsync audio to an image or video face. |
|
|
85
|
+
| `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
86
|
| `generate_music` | Music generation (Suno + variants). |
|
|
86
87
|
| `generate_speech` | TTS. Use `list_voices` to pick a voice. |
|
|
87
88
|
| `generate_sound` | Sound effects. |
|
|
@@ -95,6 +96,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
95
96
|
| `create_visual_dna` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` | Visual DNA — see `workflows/visual-dna.md` |
|
|
96
97
|
| `list_moodboards` / `get_moodboard` / `list_presets` | Style overlays |
|
|
97
98
|
| `chat_send_message` / `chat_list_conversations` / `chat_get_messages` | Kolbo chat with optional `media_urls` (up to 10 per call) |
|
|
99
|
+
| `search_music_library` / `analyze_script_for_music` / `browse_music_library` / `get_music_library_facets` / `get_music_track_audio` / `get_music_track_related` / `get_music_track_lyrics` | **Stock / production music library** — find a licensed ready-made track (NOT `generate_music`, which composes a new song). See `workflows/music-library.md` |
|
|
98
100
|
| `app_builder_*` (9 tools) | Full React app generation — see `workflows/app-builder.md` |
|
|
99
101
|
| `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
|
|
100
102
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Music Library (stock / production music)
|
|
2
|
+
|
|
3
|
+
The music library is Kolbo's catalog of **licensed, ready-made background tracks**. Use it to score a video, ad, or voiceover with an existing track.
|
|
4
|
+
|
|
5
|
+
> **Library vs generation.** `search_music_library` finds an existing track. `generate_music` composes a brand-new song with Suno. If the user wants "a track for my ad", reach for the library first — it's free (no credits) and instant. Use `generate_music` only when they want something original/custom.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | Use |
|
|
10
|
+
|------|-----|
|
|
11
|
+
| `search_music_library` | Keyword search + filters (genre, mood, bpm, duration, has_stems, has_lyrics) + sort. Returns tracks with id, title, artist, duration, BPM, key, genres, moods, preview URL. |
|
|
12
|
+
| `analyze_script_for_music` | Turn a script/scene description into `{ query, mood, genre, keywords }`. |
|
|
13
|
+
| `browse_music_library` | Paginated browse with no query. |
|
|
14
|
+
| `get_music_library_facets` | Valid genres, moods, instruments + BPM/duration ranges. |
|
|
15
|
+
| `get_music_track_audio` | A track's downloadable 128 / 320 / WAV URLs. |
|
|
16
|
+
| `get_music_track_related` | Stems + alternate versions of a master track. |
|
|
17
|
+
| `get_music_track_lyrics` | Lyrics text, theme, explicit flag. |
|
|
18
|
+
|
|
19
|
+
All tools are **free** (no credits) and read-only.
|
|
20
|
+
|
|
21
|
+
## Typical flow
|
|
22
|
+
|
|
23
|
+
1. **From a script** → call `analyze_script_for_music` to derive `query` / `mood` / `genre`.
|
|
24
|
+
2. **Search** → `search_music_library` with that query (+ optional filters). Show the user the top matches by title + vibe; include the `preview` URL so they can listen.
|
|
25
|
+
3. **Pick** → once the user chooses, call `get_music_track_audio` with the track `id` to get the final downloadable URLs (offer WAV for editing, 320 for delivery).
|
|
26
|
+
4. Optionally `get_music_track_related` for an instrumental/stems cut, or `get_music_track_lyrics` if it's a vocal track.
|
|
27
|
+
|
|
28
|
+
## Tips
|
|
29
|
+
|
|
30
|
+
- Don't dump every field — surface title, artist, duration, BPM, mood, and the preview link.
|
|
31
|
+
- If a filtered search returns nothing, call `get_music_library_facets` to use exact valid genre/mood values, then retry.
|
|
32
|
+
- `sort` options: `duration-asc`, `duration-desc`, `bpm-asc`, `bpm-desc`, `title`. Omit for relevance order.
|
package/src/index.js
CHANGED
|
@@ -69,6 +69,8 @@ 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');
|
|
73
|
+
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
76
|
* Build a fully-configured Kolbo MCP server (all tool groups registered)
|
|
@@ -95,6 +97,7 @@ function createServer(opts = {}) {
|
|
|
95
97
|
// keep identical text-URL output.
|
|
96
98
|
registerGenerateTools(server, client, { inlineImages: !!opts.inlineImages });
|
|
97
99
|
registerModelTools(server, client);
|
|
100
|
+
registerVoiceTools(server, client);
|
|
98
101
|
registerChatTools(server, client);
|
|
99
102
|
registerVisualDnaTools(server, client);
|
|
100
103
|
registerMoodboardTools(server, client);
|
|
@@ -103,6 +106,7 @@ function createServer(opts = {}) {
|
|
|
103
106
|
registerAppBuilderTools(server, client);
|
|
104
107
|
registerArtifactTools(server, client);
|
|
105
108
|
registerProjectTools(server, client);
|
|
109
|
+
registerMusicLibraryTools(server, client);
|
|
106
110
|
|
|
107
111
|
return server;
|
|
108
112
|
}
|
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,179 @@
|
|
|
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
|
+
// Format a normalized track into a compact human-readable line.
|
|
7
|
+
function trackLine(t) {
|
|
8
|
+
const meta = [
|
|
9
|
+
t.durationSeconds != null ? `${Math.round(t.durationSeconds)}s` : null,
|
|
10
|
+
t.bpm != null ? `${t.bpm} BPM` : null,
|
|
11
|
+
t.musicalKey || null,
|
|
12
|
+
Array.isArray(t.genres) && t.genres.length ? t.genres.join('/') : t.genre,
|
|
13
|
+
Array.isArray(t.moodTags) && t.moodTags.length ? t.moodTags.slice(0, 3).join(', ') : null,
|
|
14
|
+
].filter(Boolean).join(' · ');
|
|
15
|
+
const flags = [t.hasStems ? 'stems' : null, t.hasLyrics ? 'lyrics' : null].filter(Boolean).join(', ');
|
|
16
|
+
return `${t.id} — ${t.title}${t.artist ? ` by ${t.artist}` : ''}\n ${meta}${flags ? ` [${flags}]` : ''}${t.audioUrl ? `\n preview: ${t.audioUrl}` : ''}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function registerMusicLibraryTools(server, client) {
|
|
20
|
+
// ─── search_music_library ─────────────────────────────────────
|
|
21
|
+
server.tool(
|
|
22
|
+
'search_music_library',
|
|
23
|
+
'Search the Kolbo stock / production music library (licensed background tracks) by keyword with optional filters. Use this to FIND an existing ready-made track to score a video, ad, or voiceover — distinct from generate_music, which composes a brand-new song with Suno. Returns matching tracks with id, title, artist, duration, BPM, key, genres, moods, and preview/download URLs (128/320/wav). To turn a script into a good query first, call analyze_script_for_music.',
|
|
24
|
+
{
|
|
25
|
+
query: z.string().max(200).optional().describe('Keyword search, e.g. "uplifting corporate", "tense cinematic", "lofi hip hop". If omitted, falls back to the mood/genre filter as the search term.'),
|
|
26
|
+
mood: z.string().optional().describe('Mood filter, e.g. "Emotional", "Energetic", "Tense". Use get_music_library_facets to see valid values.'),
|
|
27
|
+
genre: z.string().optional().describe('Genre filter, e.g. "Soundtrack", "Corporate", "Hip Hop". Use get_music_library_facets to see valid values.'),
|
|
28
|
+
bpmMin: z.number().optional().describe('Minimum beats-per-minute.'),
|
|
29
|
+
bpmMax: z.number().optional().describe('Maximum beats-per-minute.'),
|
|
30
|
+
durationMin: z.number().optional().describe('Minimum track duration in seconds.'),
|
|
31
|
+
durationMax: z.number().optional().describe('Maximum track duration in seconds.'),
|
|
32
|
+
hasStems: z.boolean().optional().describe('Only return tracks that have separated stems.'),
|
|
33
|
+
hasLyrics: z.boolean().optional().describe('Only return tracks that have lyrics.'),
|
|
34
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('Optional sort order. Omit for relevance order.'),
|
|
35
|
+
limit: z.number().int().min(1).max(40).optional().describe('Results per page (max 40, default 20).'),
|
|
36
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset for loading more results.')
|
|
37
|
+
},
|
|
38
|
+
async (args) => {
|
|
39
|
+
const result = await client.post('/v1/music-library/search', args);
|
|
40
|
+
const tracks = result.tracks || [];
|
|
41
|
+
if (tracks.length === 0) {
|
|
42
|
+
return { content: [{ type: 'text', text: 'No tracks found matching those filters. Try a broader query or call get_music_library_facets for valid genres/moods.' }] };
|
|
43
|
+
}
|
|
44
|
+
const head = `Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}${result.total ? ` (of ${result.total} sorted)` : ''}:`;
|
|
45
|
+
return { content: [{ type: 'text', text: `${head}\n\n${tracks.map(trackLine).join('\n\n')}\n\nUse the track id with get_music_track_audio to get the downloadable 128/320/wav URLs.` }] };
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ─── analyze_script_for_music ─────────────────────────────────
|
|
50
|
+
server.tool(
|
|
51
|
+
'analyze_script_for_music',
|
|
52
|
+
'AI helper that turns a video or voiceover script into a music search. Returns { query, mood, genre, keywords } you can pass straight into search_music_library to find a fitting background track. Use this first when the user gives you a script/scene description rather than explicit music keywords.',
|
|
53
|
+
{
|
|
54
|
+
script: z.string().min(1).describe('The video or voiceover script / scene description to analyze (up to ~8000 chars).')
|
|
55
|
+
},
|
|
56
|
+
async ({ script }) => {
|
|
57
|
+
const result = await client.post('/v1/music-library/analyze-script', { script });
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: JSON.stringify({
|
|
62
|
+
query: result.query,
|
|
63
|
+
mood: result.mood,
|
|
64
|
+
genre: result.genre,
|
|
65
|
+
keywords: result.keywords,
|
|
66
|
+
_followup_hint: 'Pass query + mood + genre into search_music_library.'
|
|
67
|
+
}, null, 2)
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// ─── browse_music_library ─────────────────────────────────────
|
|
74
|
+
server.tool(
|
|
75
|
+
'browse_music_library',
|
|
76
|
+
'Browse the music library catalog without a search query (stable paginated listing). Use when the user just wants to see what is available. For a targeted search use search_music_library instead.',
|
|
77
|
+
{
|
|
78
|
+
sort: z.enum(['duration-asc', 'duration-desc', 'bpm-asc', 'bpm-desc', 'title']).optional().describe('Optional sort order.'),
|
|
79
|
+
limit: z.number().int().min(1).max(50).optional().describe('Results per page (max 50, default 50).'),
|
|
80
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset for loading more results.')
|
|
81
|
+
},
|
|
82
|
+
async ({ sort, limit, offset }) => {
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
if (sort) params.set('sort', sort);
|
|
85
|
+
if (limit != null) params.set('limit', String(limit));
|
|
86
|
+
if (offset != null) params.set('offset', String(offset));
|
|
87
|
+
const path = `/v1/music-library/catalog${params.toString() ? '?' + params.toString() : ''}`;
|
|
88
|
+
const result = await client.get(path);
|
|
89
|
+
const tracks = result.tracks || [];
|
|
90
|
+
if (tracks.length === 0) {
|
|
91
|
+
return { content: [{ type: 'text', text: 'No tracks returned.' }] };
|
|
92
|
+
}
|
|
93
|
+
return { content: [{ type: 'text', text: `Catalog (${tracks.length} track${tracks.length === 1 ? '' : 's'}):\n\n${tracks.map(trackLine).join('\n\n')}` }] };
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ─── get_music_library_facets ─────────────────────────────────
|
|
98
|
+
server.tool(
|
|
99
|
+
'get_music_library_facets',
|
|
100
|
+
'List the distinct genres, moods, and instruments available in the music library, plus the BPM and duration ranges. Use these values to build precise search_music_library filters.',
|
|
101
|
+
{},
|
|
102
|
+
async () => {
|
|
103
|
+
const result = await client.get('/v1/music-library/facets');
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
genres: result.genres || [],
|
|
109
|
+
moods: result.moods || [],
|
|
110
|
+
instruments: result.instruments || [],
|
|
111
|
+
bpmRange: result.bpmRange || null,
|
|
112
|
+
durationRange: result.durationRange || null
|
|
113
|
+
}, null, 2)
|
|
114
|
+
}]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// ─── get_music_track_audio ────────────────────────────────────
|
|
120
|
+
server.tool(
|
|
121
|
+
'get_music_track_audio',
|
|
122
|
+
'Get the downloadable audio URLs (128 kbps / 320 kbps / WAV) for a single music-library track by id. Call this after the user picks a track from search_music_library or browse_music_library.',
|
|
123
|
+
{
|
|
124
|
+
track_id: z.string().describe('The track id returned by search_music_library / browse_music_library.')
|
|
125
|
+
},
|
|
126
|
+
async ({ track_id }) => {
|
|
127
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/audio`);
|
|
128
|
+
return {
|
|
129
|
+
content: [{
|
|
130
|
+
type: 'text',
|
|
131
|
+
text: JSON.stringify({ id: result.id, urls: result.urls }, null, 2)
|
|
132
|
+
}]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ─── get_music_track_related ──────────────────────────────────
|
|
138
|
+
server.tool(
|
|
139
|
+
'get_music_track_related',
|
|
140
|
+
'Get the stems and alternate versions of a music-library master track by id (e.g. instrumental, 30s cut, looped).',
|
|
141
|
+
{
|
|
142
|
+
track_id: z.string().describe('The master track id.')
|
|
143
|
+
},
|
|
144
|
+
async ({ track_id }) => {
|
|
145
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/related`);
|
|
146
|
+
return {
|
|
147
|
+
content: [{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: JSON.stringify({ stems: result.stems || [], versions: result.versions || [] }, null, 2)
|
|
150
|
+
}]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// ─── get_music_track_lyrics ───────────────────────────────────
|
|
156
|
+
server.tool(
|
|
157
|
+
'get_music_track_lyrics',
|
|
158
|
+
'Get the lyrics text, lyrical theme, and explicit flag for a single music-library track by id.',
|
|
159
|
+
{
|
|
160
|
+
track_id: z.string().describe('The track id.')
|
|
161
|
+
},
|
|
162
|
+
async ({ track_id }) => {
|
|
163
|
+
const result = await client.get(`/v1/music-library/track/${encodeURIComponent(track_id)}/lyrics`);
|
|
164
|
+
return {
|
|
165
|
+
content: [{
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: JSON.stringify({
|
|
168
|
+
hasLyrics: result.hasLyrics,
|
|
169
|
+
lyrics: result.lyrics,
|
|
170
|
+
lyricalTheme: result.lyricalTheme,
|
|
171
|
+
explicit: result.explicit
|
|
172
|
+
}, null, 2)
|
|
173
|
+
}]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { registerMusicLibraryTools };
|
|
@@ -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 };
|