@kolbo/mcp 1.110.3 → 1.111.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 CHANGED
@@ -147,7 +147,7 @@ Without the optional skill, the config block alone already exposes every tool
147
147
  | `generate_lipsync` | Source image/video + audio → lipsynced video (Sync-3 adds active-speaker selection, emotion, model mode, temperature) |
148
148
  | `generate_creative_director` | One brief → N coordinated scenes (image or video) |
149
149
  | `generate_music` | Text (+ optional lyrics) → song. Style, title, negative tags, length, and Suno fine-controls (style weight, weirdness, audio weight, persona / singing voice) |
150
- | `generate_speech` | Text + voice → spoken audio. Full expressive/style control: Google/Gemini named voice-direction presets (`style_instructions_preset_id`: warm/dramatic/whisper/…) or free-form `style_instructions`, preset styles + emotions (DeepDub / MiniMax / Cartesia), speed, accent/`language`, and per-provider voice settings (ElevenLabs similarity/style, DeepDub accent/variance/tempo, MiniMax pitch/volume/intensity/timbre). Status returns the same fields for reuse. |
150
+ | `generate_speech` | Text + voice → spoken audio. Full expressive/style control: Google/Gemini named voice-direction presets (`style_instructions_preset_id`: warm/dramatic/whisper/…) or free-form `style_instructions`, preset styles + emotions (DeepDub / MiniMax / Cartesia), speed, accent/`language`, and per-provider voice settings (ElevenLabs stability/similarity/style/speaker boost/seed, `enforce_language`, `output_format` mp3/wav, `text_normalization`; DeepDub accent/variance/tempo, MiniMax pitch/volume/intensity/timbre), plus `audio_effect` (phone/radio/megaphone/small_room/hall/distant) + `audio_effect_strength` for any voice at no extra credits. Status returns the same fields for reuse. |
151
151
  | `generate_sound` | Text → sound effect. Duration, prompt influence, and per-provider controls (Stable Audio guidance, Kie loop/tempo/key, Seed-Audio voice/speed/volume/pitch + reference audio/image) |
152
152
  | `generate_3d` | Text or reference images → 3D model (GLB/FBX/OBJ/USDZ) |
153
153
  | `transcribe_audio` | Audio/video URL or file → text + SRT subtitles. Language, speaker diarization, audio-event tagging, and SRT formatting (words/line, lines/subtitle, caption stretch) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.110.3",
3
+ "version": "1.111.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -230,6 +230,45 @@ function modelLabel(sc) {
230
230
  }
231
231
  function voiceLabel(sc) { return sc.voice_name || sc.voice || (sc.settings || {}).voice; }
232
232
 
233
+ // generate_speech controls. What the API reports it APPLIED wins: \`speech\`
234
+ // (status / completed payload; a key there is authoritative even when null),
235
+ // then the raw status result's own fields, then what the caller submitted.
236
+ var SPEECH_KEYS = ['turbo', 'speaking_speed', 'stability', 'similarity_boost', 'enforce_language',
237
+ 'output_format', 'audio_effect', 'audio_effect_strength', 'seed'];
238
+ var SPEECH_FORMATS = { mp3_44100_128: 'MP3 128', mp3_44100_192: 'MP3 192', wav_44100: 'WAV' };
239
+ function withSpeech(settings, a) {
240
+ SPEECH_KEYS.forEach(function (k) { if (a && a[k] != null) settings[k] = a[k]; });
241
+ return settings;
242
+ }
243
+ function speechVal(sc, k) {
244
+ if (sc.speech && Object.prototype.hasOwnProperty.call(sc.speech, k)) return sc.speech[k];
245
+ var vs = sc.voice_settings || {};
246
+ if (vs[k] != null) return vs[k];
247
+ if (sc[k] != null) return sc[k];
248
+ return (sc.settings || {})[k];
249
+ }
250
+ function num(v) { return String(Math.round(Number(v) * 100) / 100); }
251
+ function speechChipsHTML(sc) {
252
+ if (sc.tool !== 'generate_speech' && !sc.speech) return '';
253
+ var h = '', v;
254
+ if (speechVal(sc, 'turbo') === true) h += chip('Turbo');
255
+ v = speechVal(sc, 'speaking_speed'); if (v != null) h += chip('Speed ' + esc(num(v)) + '×');
256
+ v = speechVal(sc, 'stability'); if (v != null) h += chip('Stability ' + esc(num(v)));
257
+ v = speechVal(sc, 'similarity_boost'); if (v != null) h += chip('Similarity ' + esc(num(v)));
258
+ v = speechVal(sc, 'enforce_language');
259
+ h += chip('Language ' + (v ? esc(String(v).toUpperCase()) : 'Auto'));
260
+ v = speechVal(sc, 'output_format'); if (v) h += chip(esc(SPEECH_FORMATS[v] || v));
261
+ v = speechVal(sc, 'audio_effect');
262
+ if (v && v !== 'none') {
263
+ var strength = speechVal(sc, 'audio_effect_strength');
264
+ var name = String(v).replace(/_/g, ' ');
265
+ h += chip(esc(name.charAt(0).toUpperCase() + name.slice(1)) +
266
+ (strength != null && Number(strength) < 1 ? ' ' + Math.round(Number(strength) * 100) + '%' : ''));
267
+ }
268
+ v = speechVal(sc, 'seed'); if (v != null) h += chip('Seed ' + esc(String(v)));
269
+ return h;
270
+ }
271
+
233
272
  // @VisualDNA / #Moodboard mentions are the tag syntax the server resolves into
234
273
  // real reference assets — rendering them as flat prose hid the single most
235
274
  // consequential part of the prompt. Escape FIRST, then wrap: the pattern only
@@ -340,6 +379,7 @@ function renderChips(sc) {
340
379
  : ICONS.mic;
341
380
  h += chip(face + ' ' + esc(voice));
342
381
  }
382
+ h += speechChipsHTML(sc);
343
383
  if (s.mode) h += chip(esc(editLabel(s.mode)));
344
384
  if (Array.isArray(s.details)) s.details.forEach(function (d) { if (d) h += chip(esc(String(d))); });
345
385
  if (many && !kind) h += chip(sc.count + ' items');
@@ -1358,7 +1398,7 @@ function preRefSc(toolName, a) {
1358
1398
  reference_videos: vid,
1359
1399
  reference_audio: aud,
1360
1400
  local_references: local,
1361
- settings: {
1401
+ settings: withSpeech({
1362
1402
  duration: a.duration,
1363
1403
  resolution: a.draft === true ? '480p-draft' : a.draft === false ? String(a.resolution || '').replace(/-draft$/i, '') : a.resolution,
1364
1404
  aspect_ratio: a.aspect_ratio,
@@ -1369,7 +1409,7 @@ function preRefSc(toolName, a) {
1369
1409
  moodboard_ids: a.moodboard_ids,
1370
1410
  moodboard_id: a.moodboard_id,
1371
1411
  preset_id: a.preset_id
1372
- }
1412
+ }, a)
1373
1413
  };
1374
1414
  }
1375
1415
 
@@ -1427,7 +1467,7 @@ function liveFromTimedOut(sc) {
1427
1467
  kind: kindFromTool(originTool, sc),
1428
1468
  prompt: originArgs.prompt || originArgs.text || '',
1429
1469
  model: sc.model || originArgs.model,
1430
- settings: {
1470
+ settings: withSpeech({
1431
1471
  duration: originArgs.duration,
1432
1472
  is_draft: originArgs.draft,
1433
1473
  resolution: originArgs.draft === true ? '480p-draft' : originArgs.draft === false ? String(originArgs.resolution || '').replace(/-draft$/i, '') : originArgs.resolution,
@@ -1436,7 +1476,7 @@ function liveFromTimedOut(sc) {
1436
1476
  mode: originArgs.operation,
1437
1477
  visual_dna_ids: originArgs.visual_dna_ids,
1438
1478
  moodboard_id: originArgs.moodboard_id
1439
- },
1479
+ }, originArgs),
1440
1480
  count: ids ? ids.length : (originArgs.num_images || 1),
1441
1481
  generation_id: sc.generation_id,
1442
1482
  poll_tool: 'get_generation_status',
@@ -1457,14 +1497,14 @@ function completedFromPlain(sc) {
1457
1497
  kind: kindFromTool(originTool, sc),
1458
1498
  prompt: originArgs.prompt || originArgs.text || sc.prompt_used || '',
1459
1499
  model: sc.model || originArgs.model,
1460
- settings: {
1500
+ settings: withSpeech({
1461
1501
  duration: sc.duration || originArgs.duration,
1462
1502
  is_draft: originArgs.draft,
1463
1503
  resolution: originArgs.draft === true ? '480p-draft' : originArgs.draft === false ? String(originArgs.resolution || '').replace(/-draft$/i, '') : originArgs.resolution,
1464
1504
  aspect_ratio: originArgs.aspect_ratio,
1465
1505
  quality: originArgs.quality,
1466
1506
  mode: originArgs.operation
1467
- },
1507
+ }, originArgs),
1468
1508
  urls: sc.urls || [],
1469
1509
  session_id: sc.session_id || originArgs.session_id,
1470
1510
  project_id: sc.project_id || originArgs.project_id,
@@ -1066,6 +1066,9 @@ async function uiCompleted(p, textPayload, extraContent) {
1066
1066
  // voice's face. Exactly the "never show a raw id on a card" rule, broken on
1067
1067
  // the one path the user actually ends up looking at.
1068
1068
  ...(p.voice ? { voice_name: p.voice.name, voice_thumbnail: p.voice.thumbnail } : {}),
1069
+ // Speech settings the API reports it applied (generate.js speechFromResult);
1070
+ // the card prefers them over the submitted `settings`.
1071
+ ...(p.speech ? { speech: p.speech } : {}),
1069
1072
  // The RAW generation state, when the caller has one. `phase` above is
1070
1073
  // hardcoded 'completed' — it means "this tool call finished", not "the
1071
1074
  // generation finished" — so a status check on a still-running job looked
@@ -151,6 +151,24 @@ function mediaKind(url) {
151
151
  return 'image';
152
152
  }
153
153
 
154
+ // The speech settings the API says it APPLIED (status result, kolbo-api
155
+ // sdk/controller.js extractResult). Keys are always present (null = not
156
+ // applied) so the card prefers these over what the caller asked for; only a
157
+ // speech result carries `enforce_language`, so every other result returns
158
+ // undefined and its card is untouched.
159
+ function speechFromResult(r) {
160
+ if (!r || typeof r !== 'object' || !Object.prototype.hasOwnProperty.call(r, 'enforce_language')) return undefined;
161
+ const vs = r.voice_settings || {};
162
+ const out = {
163
+ stability: vs.stability, similarity_boost: vs.similarity_boost, style: vs.style, use_speaker_boost: vs.use_speaker_boost,
164
+ speaking_speed: r.speaking_speed, enforce_language: r.enforce_language, output_format: r.output_format,
165
+ text_normalization: r.text_normalization, audio_effect: r.audio_effect,
166
+ audio_effect_strength: r.audio_effect_strength, seed: r.seed,
167
+ };
168
+ for (const k of Object.keys(out)) if (out[k] === undefined) out[k] = null;
169
+ return out;
170
+ }
171
+
154
172
  function preferOwned(urls) {
155
173
  const list = (urls || []).filter((item) => typeof item === 'string' && item);
156
174
  const ours = list.filter(ownedUrl);
@@ -1099,14 +1117,20 @@ function registerGenerateTools(server, client, options = {}) {
1099
1117
  emotion: z.string().optional().describe('MiniMax / Cartesia voices. Emotion: happy, sad, angry, fearful, disgusted, surprised, calm, fluent, whisper. Prefer this OR selected_style (both map to the same delivery control).'),
1100
1118
  speaking_speed: z.number().optional().describe('Speech speed 0.5 (slow) – 2.0 (fast); Cartesia clamped 0.6–1.5. Default 1.0. Applies to ElevenLabs / OpenAI / Google / MiniMax / Cartesia / DeepDub (as tempo when tempo omitted).'),
1101
1119
  // ── ElevenLabs voice settings ──
1102
- similarity_boost: z.number().optional().describe('ElevenLabs voice similarity, 0–1. Default 0.75. Higher hews closer to the original voice.'),
1103
- style: z.number().optional().describe('ElevenLabs style exaggeration, 0–1. Default 0.5. Higher = more expressive/dramatic.'),
1104
- use_speaker_boost: z.boolean().optional().describe('ElevenLabs speaker boost. Default true.'),
1120
+ stability: z.number().min(0).max(1).optional().describe('ElevenLabs Stability, 0–1. Default 0.5. Lower = more expressive and varied between takes (Creative); higher = steadier, more consistent delivery (Robust). Supported on V4.'),
1121
+ similarity_boost: z.number().optional().describe('ElevenLabs voice similarity, 0–1. Default 0.75. Higher hews closer to the original voice. Also applies to the user\'s own cloned/character ElevenLabs voices.'),
1122
+ style: z.number().optional().describe('ElevenLabs style exaggeration, 0–1. Default 0. Higher = more expressive/dramatic. V3/V2 voices only — ElevenLabs V4 ignores it (use stability or inline audio tags instead).'),
1123
+ use_speaker_boost: z.boolean().optional().describe('ElevenLabs speaker boost (presence/clarity). Default true.'),
1124
+ enforce_language: z.string().optional().describe('ElevenLabs voices ONLY. Force the spoken language with an ISO 639-1 code ("he", "en", "fr"; a locale like "he-IL" is also accepted). Omit (recommended) and ElevenLabs auto-detects the language from the text. Different from `language`, which stays the accent/locale control.'),
1125
+ output_format: z.enum(['mp3_44100_128', 'mp3_44100_192', 'wav_44100']).optional().describe('ElevenLabs voices ONLY. Audio file format: "mp3_44100_128" (default), "mp3_44100_192" (higher-bitrate MP3) or "wav_44100" (uncompressed; the result is a .wav file). ElevenLabs\' native render is delivered as is. Any other provider rejects it (OUTPUT_FORMAT_UNSUPPORTED).'),
1126
+ text_normalization: z.enum(['auto', 'on', 'off']).optional().describe('ElevenLabs voices. Spell out numbers, dates and abbreviations before speaking: "auto" (default), "on" (always), "off" (read as written).'),
1127
+ audio_effect: z.enum(['none', 'phone', 'radio', 'megaphone', 'small_room', 'hall', 'distant']).optional().describe('Kolbo post-processing effect applied to the finished voice: phone, radio, megaphone, small_room, hall, distant, or "none" (default). Works for ANY voice/provider, no extra credits.'),
1128
+ audio_effect_strength: z.number().min(0).max(1).optional().describe('Mix of audio_effect with the dry voice, 0–1. Default 1 (full effect). Ignored without audio_effect.'),
1105
1129
  // ── DeepDub controls ──
1106
1130
  variance: z.number().optional().describe('DeepDub voice variance, 0–1. Default 0.2. Higher = more takes/variation.'),
1107
1131
  tempo: z.number().optional().describe('DeepDub tempo multiplier, 0–2. Default 1.0.'),
1108
1132
  promptBoost: z.boolean().optional().describe('DeepDub prompt-fidelity boost. Default true.'),
1109
- seed: z.number().optional().describe('Reproducibility seed (DeepDub / Zonos). Same seed + inputs → same output.'),
1133
+ seed: z.number().optional().describe('Reproducibility seed (ElevenLabs / DeepDub / Zonos). Same seed + inputs → same output. ElevenLabs: integer 0–4294967295, best-effort determinism (not guaranteed).'),
1110
1134
  accentControl: z.object({
1111
1135
  accentBaseLocale: z.string().describe('Base accent locale, e.g. "en-US".'),
1112
1136
  accentLocale: z.string().describe('Target accent locale, e.g. "en-GB".'),
@@ -1121,7 +1145,7 @@ function registerGenerateTools(server, client, options = {}) {
1121
1145
  project_id: projectIdField,
1122
1146
  session_id: sessionIdField
1123
1147
  },
1124
- async ({ text, voice, model, turbo, language, style_instructions_preset_id, style_instructions, style_instructions_label, selected_style, emotion, speaking_speed, similarity_boost, style, use_speaker_boost, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id, session_id }) => {
1148
+ async ({ text, voice, model, turbo, language, style_instructions_preset_id, style_instructions, style_instructions_label, selected_style, emotion, speaking_speed, stability, similarity_boost, style, use_speaker_boost, enforce_language, output_format, text_normalization, audio_effect, audio_effect_strength, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id, session_id }) => {
1125
1149
  model = await canonicalModelId(client, model, 'text_to_speech'); // lenient id resolution ("z-image" → "z-image/turbo")
1126
1150
  // Resolve the requested voice against the REAL catalog (cached) so the card
1127
1151
  // can show its display name + portrait instead of a raw id, and so an id
@@ -1133,11 +1157,14 @@ function registerGenerateTools(server, client, options = {}) {
1133
1157
  const unknownVoice = voice && !voiceRecord && !/^custom_/i.test(voice)
1134
1158
  ? `Voice "${voice}" is not in the Kolbo voice catalog. Call list_voices and pass a voice_id it returns — an unrecognised id is NOT rejected, it is silently mapped to a different voice, so the audio will not be the voice you named.`
1135
1159
  : null;
1160
+ // ElevenLabs / effect controls carried on the card (undefined keys drop out of the JSON).
1161
+ const ttsControls = { turbo, stability, similarity_boost, style, use_speaker_boost, enforce_language, output_format, text_normalization, audio_effect, audio_effect_strength, seed };
1136
1162
  const gen = await client.post('/v1/generate/speech', {
1137
1163
  text, voice, model, turbo, language,
1138
1164
  style_instructions_preset_id, style_instructions, style_instructions_label,
1139
1165
  selected_style, emotion, speaking_speed,
1140
- similarity_boost, style, use_speaker_boost,
1166
+ stability, similarity_boost, style, use_speaker_boost,
1167
+ enforce_language, output_format, text_normalization, audio_effect, audio_effect_strength,
1141
1168
  variance, tempo, promptBoost, seed, accentControl, voiceTitle,
1142
1169
  minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre,
1143
1170
  project_id, session_id
@@ -1151,6 +1178,7 @@ function registerGenerateTools(server, client, options = {}) {
1151
1178
  style: selected_style || emotion || style_instructions_preset_id || style_instructions,
1152
1179
  speaking_speed,
1153
1180
  language,
1181
+ ...ttsControls,
1154
1182
  },
1155
1183
  warning: unknownVoice
1156
1184
  });
@@ -1178,7 +1206,9 @@ function registerGenerateTools(server, client, options = {}) {
1178
1206
  style: selected_style || emotion || style_instructions_preset_id || style_instructions,
1179
1207
  speaking_speed,
1180
1208
  language,
1209
+ ...ttsControls,
1181
1210
  },
1211
+ speech: speechFromResult(result.result),
1182
1212
  urls: result.result.urls,
1183
1213
  playback_urls: result.result.playback_urls,
1184
1214
  duration: result.result.duration,
@@ -1420,6 +1450,7 @@ function registerGenerateTools(server, client, options = {}) {
1420
1450
  txt_url: res.txt_url || undefined,
1421
1451
  audio_url: res.audio_url || undefined,
1422
1452
  prompt: res.prompt_used || res.prompt || undefined,
1453
+ speech: speechFromResult(res),
1423
1454
  credits_used: creditFields(single).credits_used,
1424
1455
  items: [{
1425
1456
  id: single.generation_id,