@kolbo/mcp 1.114.2 → 1.116.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
@@ -148,7 +148,7 @@ Without the optional skill, the config block alone already exposes every tool
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
150
  | `generate_speech` | Text + voice → spoken audio. `eleven_model` (eleven_v4 / eleven_v3 / eleven_multilingual_v2) picks which ElevenLabs model speaks an ElevenLabs voice (stock or own clone; omit = the voice's default; same price; `turbo` needs v4). 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
- | `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) |
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, output format, Mirelo SFX 2.0 negative prompt / variations / seed / bitrate) |
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) |
154
154
  | `separate_audio_stems` | Audio/video URL (or a Kolbo `generation_id`) → Dialogue / Music / Effects / without-dialogue (M&E) layers. Kolbo's own masking pipeline with a speech classifier on top, so a centred engine or ambience is not handed back mislabelled as dialogue. 5 credits, runs inline |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.114.2",
3
+ "version": "1.116.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": {
@@ -24,7 +24,11 @@ const { widgetPage } = require('../html');
24
24
  * plans: [{ key, name, interval, credits, price, original_price,
25
25
  * discount_percent, promo_text, currency, top_up_discount }],
26
26
  * credit_packs: [{ …, subscriber_price, is_subscriber }],
27
- * pricing_url
27
+ * pricing_url,
28
+ * // insufficient_credits only — leads the card (packs out-convert plans
29
+ * // for users who just ran out); every URL carries MCP attribution tags:
30
+ * recommended_pack: { …pack, covers_shortfall, url } | null,
31
+ * pack_url, home_url
28
32
  * }
29
33
  */
30
34
 
@@ -52,11 +56,15 @@ var state = null;
52
56
  var interval = 'month';
53
57
 
54
58
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
55
- el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
59
+ function homeUrl() { return (state && state.home_url) || 'https://app.kolbo.ai'; }
60
+ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink(homeUrl()); };
61
+ // Own handler so the shared untagged logo link (html.js) never fires here.
62
+ el('logo').onclick = function (e) { e.stopPropagation(); window.kolbo.openLink(homeUrl()); };
56
63
 
57
64
  function pricingUrl() {
58
65
  return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
59
66
  }
67
+ function packUrl() { return (state && state.pack_url) || pricingUrl(); }
60
68
  function money(v, currency) {
61
69
  if (v == null || isNaN(v)) return '';
62
70
  var sym = { usd: '$', eur: '\u20ac', gbp: '\u00a3', ils: '\u20aa' }[String(currency || 'usd').toLowerCase()] || '';
@@ -76,7 +84,9 @@ function planCard(p, opts) {
76
84
  var note = p.promo_text ? '<div class="k-plan-note">' + esc(p.promo_text) + '</div>'
77
85
  : (opts.pack && p.is_subscriber && p.subscriber_price != null ? '<div class="k-plan-note">Subscriber price applied</div>' : '')
78
86
  + (!opts.pack && p.top_up_discount ? '<div class="k-plan-note">Cheaper credit packs included</div>' : '');
79
- var cta = current ? '' : '<button class="k-btn ' + (opts.pack ? 'ghost' : 'primary') + '" data-open="1">' + (opts.pack ? 'Buy pack' : 'Upgrade') + ' ' + ICONS.open + '</button>';
87
+ if (opts.lead) note += '<div class="k-plan-note">One-time top-up, no subscription</div>';
88
+ var primary = opts.pack ? opts.lead : !state.recommended_pack;
89
+ var cta = current ? '' : '<button class="k-btn ' + (primary ? 'primary' : 'ghost') + '" data-open="' + (opts.pack ? 'pack' : 'plan') + '">' + (opts.pack ? 'Buy pack' : 'Upgrade') + ' ' + ICONS.open + '</button>';
80
90
  return '<div class="k-plan' + (current ? ' current' : '') + '">' +
81
91
  '<div class="k-plan-top"><span class="k-plan-name">' + esc(p.name || p.key || 'Plan') + '</span>' + badge + '</div>' +
82
92
  (p.credits != null ? '<div class="k-plan-credits">' + ICONS.sparkle + ' ' + esc(fmtCredits(p.credits)) + ' credits' + (opts.pack ? '' : ' / ' + (isAnnual(p) ? 'year' : 'month')) + '</div>' : '') +
@@ -85,12 +95,17 @@ function planCard(p, opts) {
85
95
 
86
96
  function renderStage() {
87
97
  var plans = (state.plans || []).filter(function (p) { return p && (p.price != null || p.credits != null); });
88
- var packs = (state.credit_packs || []).filter(function (p) { return p && p.price != null; });
98
+ var lead = state.recommended_pack;
99
+ var packs = (state.credit_packs || []).filter(function (p) { return p && p.price != null && !(lead && lead.key != null && p.key === lead.key); });
89
100
  var monthly = plans.filter(function (p) { return !isAnnual(p); });
90
101
  var annual = plans.filter(isAnnual);
91
102
  var hasToggle = monthly.length && annual.length;
92
103
  var shown = hasToggle ? (interval === 'year' ? annual : monthly) : plans;
93
104
  var html = '';
105
+ if (lead) {
106
+ html += '<div class="k-pack-head">Quickest fix</div><div class="k-plan-grid">' + planCard(lead, { pack: true, lead: true }) + '</div>' +
107
+ '<div class="k-pack-head">Or upgrade your plan</div>';
108
+ }
94
109
  if (hasToggle) {
95
110
  html += '<div class="k-plan-toggle">' +
96
111
  '<button class="k-toggle-btn' + (interval === 'month' ? ' active' : '') + '" data-interval="month">Monthly</button>' +
@@ -98,7 +113,7 @@ function renderStage() {
98
113
  }
99
114
  if (shown.length) html += '<div class="k-plan-grid">' + shown.map(function (p) { return planCard(p, {}); }).join('') + '</div>';
100
115
  if (packs.length) {
101
- html += '<div class="k-pack-head">Credit packs</div><div class="k-plan-grid">' +
116
+ html += '<div class="k-pack-head">' + (lead ? 'Other credit packs' : 'Credit packs') + '</div><div class="k-plan-grid">' +
102
117
  packs.map(function (p) { return planCard(p, { pack: true }); }).join('') + '</div>';
103
118
  }
104
119
  if (!html) html = '<div class="k-plan-note">Live plans and prices are on the Kolbo pricing page.</div>';
@@ -106,7 +121,7 @@ function renderStage() {
106
121
  var btns = el('stage').querySelectorAll('[data-interval]');
107
122
  for (var i = 0; i < btns.length; i++) btns[i].onclick = function () { interval = this.getAttribute('data-interval'); renderStage(); window.kolbo.notifySize(); };
108
123
  var opens = el('stage').querySelectorAll('[data-open]');
109
- for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(pricingUrl()); };
124
+ for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(this.getAttribute('data-open') === 'pack' ? packUrl() : pricingUrl()); };
110
125
  }
111
126
 
112
127
  function boot(sc) {
@@ -1163,12 +1163,44 @@ function compactList(items, { fields, cap = 50, total, extra, note, maxChars = M
1163
1163
  return text;
1164
1164
  }
1165
1165
 
1166
+ // Every link out of the out-of-credits card carries the same tags so MCP
1167
+ // purchases are attributable. kolbo-map's /pricing reads only `category`
1168
+ // (month | annual | one_time) — no per-pack preselect exists, so the pack link
1169
+ // lands on the credit-pack tab.
1170
+ function upgradeCardUrl(path, content, query = {}) {
1171
+ const url = new URL(path, APP_BASE_URL);
1172
+ const params = {
1173
+ ...query,
1174
+ utm_source: 'mcp',
1175
+ utm_medium: 'upgrade_card',
1176
+ utm_campaign: 'out_of_credits',
1177
+ utm_content: content,
1178
+ // kolbo-map's stable funnel-join fields (ANALYTICS_ATTRIBUTION_KEYS).
1179
+ attribution_link_id: `mcp_upgrade_card_${content}`,
1180
+ attribution_trigger: 'mcp_insufficient_credits',
1181
+ upgrade_entry: 'mcp_upgrade_card',
1182
+ };
1183
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
1184
+ return url.toString();
1185
+ }
1186
+
1187
+ // Smallest pack that covers the shortfall; the largest when none does; the
1188
+ // smallest when the shortfall is unknown.
1189
+ function packForShortfall(packs, shortfall) {
1190
+ const sized = (packs || [])
1191
+ .filter((p) => p && Number.isFinite(p.credits) && p.price != null)
1192
+ .sort((a, b) => a.credits - b.credits);
1193
+ if (!sized.length) return null;
1194
+ return sized.find((p) => !(shortfall > 0) || p.credits >= shortfall) || sized[sized.length - 1];
1195
+ }
1196
+
1166
1197
  /**
1167
1198
  * Turn a 403 INSUFFICIENT_CREDITS into the plans/upgrade card.
1168
1199
  *
1169
1200
  * Without this the user just sees the raw API sentence and has to go find the
1170
- * pricing page themselves. The card states the shortfall, shows live
1171
- * promo-adjusted plans, and links to app.kolbo.ai/pricing.
1201
+ * pricing page themselves. The card states the shortfall, leads with the
1202
+ * credit pack that covers it, shows live promo-adjusted plans below, and links
1203
+ * to app.kolbo.ai/pricing with MCP attribution tags.
1172
1204
  *
1173
1205
  * Returns null for any other error so callers can rethrow untouched. The plan
1174
1206
  * fetch is best-effort: if it fails we still return a card carrying the
@@ -1209,30 +1241,56 @@ async function insufficientCreditsResult(client, err, options = {}) {
1209
1241
 
1210
1242
  const balance = err.data?.balance ?? data?.credits?.total;
1211
1243
  const required = err.data?.required;
1244
+ const shortfall = (Number.isFinite(balance) && Number.isFinite(required))
1245
+ ? Math.max(0, required - balance) : undefined;
1246
+ const credit_packs = data?.credit_packs || [];
1247
+ const pack = packForShortfall(credit_packs, shortfall);
1248
+ const pack_url = upgradeCardUrl('/pricing', 'credit_pack', { category: 'one_time' });
1212
1249
  const structured = {
1213
1250
  widget: 'plans',
1214
1251
  reason: 'insufficient_credits',
1215
1252
  balance,
1216
1253
  required,
1217
- shortfall: (Number.isFinite(balance) && Number.isFinite(required))
1218
- ? Math.max(0, required - balance) : undefined,
1254
+ shortfall,
1219
1255
  current_plan: data?.current_plan || null,
1256
+ // Leads the card: out of credits on the web app, packs convert 56% vs
1257
+ // plans 17.5%. Plans stay below as the alternative.
1258
+ recommended_pack: pack ? {
1259
+ ...pack,
1260
+ // Only when the shortfall is known (and `required` is the API's lower
1261
+ // bound, so this is a floor, not a promise).
1262
+ covers_shortfall: shortfall > 0 && pack.credits >= shortfall,
1263
+ url: pack_url,
1264
+ } : null,
1220
1265
  plans: data?.plans || [],
1221
- credit_packs: data?.credit_packs || [],
1222
- pricing_url: err.data?.pricing_url || data?.pricing_url || 'https://app.kolbo.ai/pricing',
1266
+ credit_packs,
1267
+ // MCP-tagged, not the API's sdk-tagged link: every way out of this card
1268
+ // must be attributable to it.
1269
+ pricing_url: upgradeCardUrl('/pricing', 'plans'),
1270
+ pack_url,
1271
+ home_url: upgradeCardUrl('/', 'home'),
1223
1272
  };
1224
1273
 
1225
1274
  // structuredContent shadows the text on widget hosts, so the refusal reason
1226
1275
  // has to be inside the object too — an agent that only reads structured
1227
1276
  // content must still understand that nothing was generated.
1228
1277
  structured.error = err.message;
1229
- structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card; do not retry the generation, and do not attempt to purchase anything for them.';
1278
+ structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card; '
1279
+ + (pack ? 'the quickest fix is the one-time credit pack it leads with (recommended_pack), with plans as the alternative; ' : '')
1280
+ + 'do not retry the generation, and do not attempt to purchase anything for them.';
1230
1281
 
1231
1282
  const text = JSON.stringify({
1232
1283
  error: err.message,
1233
1284
  code: 'INSUFFICIENT_CREDITS',
1234
1285
  balance,
1235
1286
  required,
1287
+ recommended_pack: structured.recommended_pack && {
1288
+ name: pack.name,
1289
+ credits: pack.credits,
1290
+ price: pack.price,
1291
+ currency: pack.currency,
1292
+ url: pack_url,
1293
+ },
1236
1294
  pricing_url: structured.pricing_url,
1237
1295
  _hint: structured._hint,
1238
1296
  }, null, 2);
@@ -1234,7 +1234,7 @@ function registerGenerateTools(server, client, options = {}) {
1234
1234
  // ─── generate_sound ────────────────────────────────────────
1235
1235
  server.tool(
1236
1236
  'generate_sound',
1237
- 'Generate sound effects (not music, not speech) from a text description using Kolbo AI. Use this for ambient sounds, foley, impacts, atmospheres, UI sounds, etc. For music use generate_music; for voice use generate_speech. Beyond the core prompt/duration, per-provider controls are available (Stable Audio guidance, Kie loop/tempo/key, Seed-Audio voice/speed/volume/pitch + reference audio/image); the engine ignores controls that do not apply to the chosen model. Returns the final audio URL when complete.',
1237
+ 'Generate sound effects (not music, not speech) from a text description using Kolbo AI. Use this for ambient sounds, foley, impacts, atmospheres, UI sounds, etc. For music use generate_music; for voice use generate_speech. Beyond the core prompt/duration, per-provider controls are available (Stable Audio guidance, Kie loop/tempo/key, Seed-Audio voice/speed/volume/pitch + reference audio/image, output audio_format, and Mirelo SFX 2.0 negative_prompt / num_samples / seed / audio_bitrate_kbps); the engine ignores controls that do not apply to the chosen model. Mirelo SFX 2.0 (model "mirelo-ai/sfx2.0/text-to-audio") makes up to 10 minutes of layered sound design and up to 4 variations per call (each variation is billed). Returns the final audio URL(s) when complete.',
1238
1238
  {
1239
1239
  prompt: z.string().describe('Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open", "futuristic UI beep")'),
1240
1240
  model: z.string().optional().describe('Model identifier. Use list_models type="text_to_sound" to see options. Default: elevenlabs-sound-effects-v1'),
@@ -1253,21 +1253,30 @@ function registerGenerateTools(server, client, options = {}) {
1253
1253
  seed_pitch: z.number().optional().describe('FAL Seed-Audio: pitch shift in semitones.'),
1254
1254
  seed_reference_audio_urls: z.array(z.string()).optional().describe('FAL Seed-Audio: up to 3 reference audio URLs to condition the sound.'),
1255
1255
  seed_reference_image_url: z.string().optional().describe('FAL Seed-Audio: a reference image URL to condition the sound.'),
1256
+ // ── Output format (models whose list_models entry has output_audio_formats) ──
1257
+ audio_format: z.enum(['wav', 'mp3', 'aac', 'flac']).optional().describe('Output audio container/codec, for models that advertise output_audio_formats in list_models (Sonilo, Mirelo SFX 2.0).'),
1258
+ // ── Mirelo SFX 2.0 (list_models: max_samples / supports_negative_prompt / supports_seed / supports_audio_bitrate) ──
1259
+ negative_prompt: z.string().max(1000).optional().describe('Mirelo SFX 2.0: sounds to keep OUT of the result, e.g. "music, speech".'),
1260
+ num_samples: z.number().int().min(1).max(4).optional().describe('Mirelo SFX 2.0: number of variations (1-4, default 1). Each variation is billed; all URLs are returned.'),
1261
+ seed: z.number().int().min(-1).optional().describe('Mirelo SFX 2.0: seed for repeatable results (-1 or omit = random).'),
1262
+ audio_bitrate_kbps: z.number().int().optional().describe('Mirelo SFX 2.0: bitrate for mp3 (32,40,48,56,64,80,96,112,128,160,192,224,256,320) or aac (32-256). Not allowed with wav/flac.'),
1256
1263
  project_id: projectIdField,
1257
1264
  session_id: sessionIdField
1258
1265
  },
1259
- async ({ prompt, model, duration, prompt_influence, cfg_strength, sound_loop, sound_tempo, sound_key, seed_voice, seed_speed, seed_volume, seed_pitch, seed_reference_audio_urls, seed_reference_image_url, project_id, session_id }) => {
1266
+ async ({ prompt, model, duration, prompt_influence, cfg_strength, sound_loop, sound_tempo, sound_key, seed_voice, seed_speed, seed_volume, seed_pitch, seed_reference_audio_urls, seed_reference_image_url, audio_format, negative_prompt, num_samples, seed, audio_bitrate_kbps, project_id, session_id }) => {
1260
1267
  model = await canonicalModelId(client, model, 'text_to_sound'); // lenient id resolution ("z-image" → "z-image/turbo")
1261
1268
  const gen = await client.post('/v1/generate/sound', {
1262
1269
  prompt, model, duration, prompt_influence,
1263
1270
  cfg_strength, sound_loop, sound_tempo, sound_key,
1264
1271
  seed_voice, seed_speed, seed_volume, seed_pitch,
1265
- seed_reference_audio_urls, seed_reference_image_url, project_id, session_id
1272
+ seed_reference_audio_urls, seed_reference_image_url,
1273
+ audio_format, negative_prompt, num_samples, seed, audio_bitrate_kbps,
1274
+ project_id, session_id
1266
1275
  });
1267
1276
 
1268
1277
  if (returnsImmediately()) return submittedResult({
1269
1278
  tool: 'generate_sound', kind: 'audio', gen, client, model, prompt,
1270
- settings: { duration },
1279
+ settings: { duration, ...(num_samples > 1 && { variations: num_samples }) },
1271
1280
  reference_images: seed_reference_image_url ? [seed_reference_image_url] : []
1272
1281
  });
1273
1282
 
@@ -1280,7 +1289,7 @@ function registerGenerateTools(server, client, options = {}) {
1280
1289
 
1281
1290
  return uiCompleted({
1282
1291
  tool: 'generate_sound', kind: 'audio', gen, client, model, prompt,
1283
- settings: { duration },
1292
+ settings: { duration, ...(num_samples > 1 && { variations: num_samples }) },
1284
1293
  reference_images: seed_reference_image_url ? [seed_reference_image_url] : [],
1285
1294
  urls: result.result.urls,
1286
1295
  playback_urls: result.result.playback_urls,
@@ -2391,6 +2400,10 @@ function registerGenerateTools(server, client, options = {}) {
2391
2400
  const editDetails = (op, a) => {
2392
2401
  const d = [];
2393
2402
  const has = (v) => v !== undefined && v !== null && v !== '';
2403
+ if (op === 'generate_audio') {
2404
+ if (a.num_samples > 1) d.push(`${a.num_samples} variations`);
2405
+ if (a.preserve_speech === false) d.push('no dialogue');
2406
+ }
2394
2407
  if (op === 'upscale' || op === 'split_upscale') {
2395
2408
  if (has(a.scale)) d.push(`${a.scale}×`);
2396
2409
  if (a.face_enhancement === false) d.push('no face enhance');
@@ -2678,7 +2691,7 @@ function registerGenerateTools(server, client, options = {}) {
2678
2691
  '"draft_quote" — check a saved draft’s supported final resolutions, expiry and exact credit cost without starting a render. Use this before draft_enhance.',
2679
2692
  '"upscale" — boost to 4K/2K resolution (use `scale` for factor, `resolution` for target, `target_fps` for frame rate). On model "blackforestlabs/flux-video-upscale" (Flux Video Upscale): `scale` is 1.5-3x (no named `resolution` target — billed on the OUTPUT tier it lands in), `mode` is "precise" (source-faithful, default) or "creative" (reimagines detail — a pricier tier), and `prompt` optionally guides the creative-mode enhancement. On model "bytedance-upscaler/upscale/video" (Bytedance Upscaler): `resolution` is "1080p" | "2k" | "4k" | "6k" | "8k" (OR `scale_ratio` 1.1-10, output ≤4K), plus `target_fps` (24-120), `enhancement_tier`, `enhancement_preset`, `fidelity`, `bit_depth` — see each field. Price = resolution tier (1080p 1 · 2k 2 · 4k 4 · 6k 8 · 8k 16 credits/s) × every started 30 fps (60 = 2×, 120 = 4×) × tier (fast 0.5×, pro 10×). Source-length caps: 6K 120s, 8K 60s, Pro 30s, otherwise 300s.',
2680
2693
  '"reframe" — change aspect ratio (requires `aspect_ratio`; use `grid_position_x`/`grid_position_y` to control where the original sits).',
2681
- '"generate_audio" — add AI-generated audio from `prompt`. Optionally split into `sound_effect_prompt` and `background_music_prompt`. Set `original_sound=true` to keep original audio alongside.',
2694
+ '"generate_audio" — add AI-generated audio from `prompt`. Optionally split into `sound_effect_prompt` and `background_music_prompt`. Set `original_sound=true` to keep original audio alongside. On model "mirelo-ai/sfx2.0/video-to-video" (Mirelo SFX 2.0) the prompt is optional (it watches the clip and builds a full synced sound design, up to 10 minutes): `preserve_speech` keeps the dialogue (default true), `negative_prompt` keeps sounds out, `num_samples` returns 1-4 variations (each billed), `seed` repeats a result, and `duration` makes a shorter soundtrack by TRIMMING the video (omit for the whole clip).',
2682
2695
  '"remove_watermark" — AI-powered watermark removal.',
2683
2696
  '"face_swap" — replace the face in the video with the face from `image_url` (required).',
2684
2697
  '"extend" — lengthen the video at `mode` ("start" or "end") by `duration` seconds. Optional `prompt` and `context` guide the generated content.',
@@ -2742,6 +2755,15 @@ function registerGenerateTools(server, client, options = {}) {
2742
2755
  prompt: z.string()
2743
2756
  })).optional()
2744
2757
  .describe('Optional contiguous Sonilo sound-design ranges. The first start must be 0, each end must equal the next start, and the final end must not exceed the video duration. Omit to let Sonilo detect scenes automatically.'),
2758
+ // Mirelo SFX 2.0 (list_models: max_samples / supports_negative_prompt / supports_seed / supports_preserve_speech)
2759
+ negative_prompt: z.string().max(1000).optional()
2760
+ .describe('Mirelo SFX 2.0 (operation="generate_audio"): sounds to keep out, as a short comma-separated list, e.g. "music, crowd noise".'),
2761
+ preserve_speech: z.boolean().optional()
2762
+ .describe('Mirelo SFX 2.0 (operation="generate_audio"): true (default) keeps the dialogue and builds the soundtrack around it; false makes sound effects only and suppresses speech.'),
2763
+ num_samples: z.number().int().min(1).max(4).optional()
2764
+ .describe('Mirelo SFX 2.0 (operation="generate_audio"): number of variations, 1-4 (default 1). Each is billed; all result URLs are returned.'),
2765
+ seed: z.number().int().min(-1).optional()
2766
+ .describe('Mirelo SFX 2.0 (operation="generate_audio"): seed for repeatable results (-1 or omit = random).'),
2745
2767
 
2746
2768
  // ── face_swap ──────────────────────────────────────────
2747
2769
  image_url: z.string().optional()
@@ -2749,7 +2771,7 @@ function registerGenerateTools(server, client, options = {}) {
2749
2771
 
2750
2772
  // ── extend ─────────────────────────────────────────────
2751
2773
  duration: z.number().optional()
2752
- .describe('Seconds of content to generate. Used with "extend" (required) and "retake" (optional). Typical range: 1–20.'),
2774
+ .describe('Seconds of content to generate. Used with "extend" (required) and "retake" (optional). Typical range: 1–20. With "generate_audio" on Mirelo SFX 2.0: seconds of sound (1-600) — the returned video is TRIMMED to this length; omit for the whole clip.'),
2753
2775
  mode: z.string().optional()
2754
2776
  .describe('Extension direction: "start" or "end". Used with "extend". For "retake": replacement mode (default: "replace_audio_and_video"). For "upscale" on model "blackforestlabs/flux-video-upscale": "precise" (source-faithful, default) or "creative" (reimagines detail — pricier). Default: "end".'),
2755
2777
  context: z.string().optional()
@@ -2790,6 +2812,7 @@ function registerGenerateTools(server, client, options = {}) {
2790
2812
  grid_position_x, grid_position_y,
2791
2813
  sound_effect_prompt, background_music_prompt, original_sound, cfg_strength,
2792
2814
  audio_format, segments,
2815
+ negative_prompt, preserve_speech, num_samples, seed,
2793
2816
  refine_edges, subject_is_person,
2794
2817
  text_prompt, context,
2795
2818
  mask_video_url, object_prompt, video_strength,
@@ -2806,7 +2829,7 @@ function registerGenerateTools(server, client, options = {}) {
2806
2829
  // Validation
2807
2830
  if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit');
2808
2831
  if (operation === 'generate_audio' && !prompt && !sound_effect_prompt && !background_music_prompt
2809
- && !(typeof model === 'string' && model.includes('sonilo'))) {
2832
+ && !(typeof model === 'string' && (model.includes('sonilo') || model.includes('mirelo')))) {
2810
2833
  throw new Error('prompt is required for generate_audio unless dedicated sound/music prompts are provided or the selected model supports automatic captioning');
2811
2834
  }
2812
2835
  if (operation === 'reframe' && !aspect_ratio)throw new Error('aspect_ratio is required for reframe');
@@ -2824,6 +2847,7 @@ function registerGenerateTools(server, client, options = {}) {
2824
2847
  grid_position_x, grid_position_y,
2825
2848
  sound_effect_prompt, background_music_prompt, original_sound, cfg_strength,
2826
2849
  audio_format, segments,
2850
+ negative_prompt, preserve_speech, num_samples, seed,
2827
2851
  refine_edges, subject_is_person,
2828
2852
  text_prompt, context,
2829
2853
  mask_video_url, object_prompt, video_strength,
@@ -2840,7 +2864,7 @@ function registerGenerateTools(server, client, options = {}) {
2840
2864
  if (operation === 'draft_quote') return { content: [{ type: 'text', text: JSON.stringify(gen, null, 2) }] };
2841
2865
  const cardSettings = {
2842
2866
  mode: operation, duration, aspect_ratio, resolution,
2843
- details: editDetails(operation, { scale, enhancement_model, target_fps, mode, enhancement_tier, enhancement_preset, bit_depth, scale_ratio }),
2867
+ details: editDetails(operation, { scale, enhancement_model, target_fps, mode, enhancement_tier, enhancement_preset, bit_depth, scale_ratio, num_samples, preserve_speech }),
2844
2868
  };
2845
2869
  if (returnsImmediately()) return submittedResult({
2846
2870
  tool: 'edit_video', kind: 'video', gen, client, model,