@kolbo/mcp 1.77.1 → 1.77.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.77.1",
3
+ "version": "1.77.2",
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": {
@@ -110,6 +110,7 @@ These elevate rich cinematic / reference-anchored sequences. For a short, tight,
110
110
  ## Dialogue & expression
111
111
 
112
112
  - Dialogue goes in quotes and may be in ANY language (Hebrew included). For silent tension, deliver it as expression, not speech: `He does not speak. His expression clearly says: "…"`.
113
+ - **Seedance PERFORMS quoted dialogue natively** — synced voices, lip movement, and room tone come out of the video model itself. Never route scene dialogue through TTS (`generate_speech`) or `generate_lipsync`; write each line in quotes inside its shot beat (`DANIEL says: "…"`) and generate once.
113
114
 
114
115
  ## Content tone
115
116
 
@@ -9,7 +9,7 @@ Load this file when the user wants a **Seedance 2.5** video (they said "2.5" / "
9
9
 
10
10
  **Kolbo MCP routing:** `generate_video` or `generate_elements` (refs / Visual DNA / first-last). Run `list_models({ type: "text_to_video" })` and pick the Seedance 2.5 variant by name.
11
11
 
12
- **Audio:** Seedance 2.5 still emits real synced audio. `list_models` may show `sound_generation_type: none` because there is no in-app toggle (`sound_baked_in: true`). Do not tell the user the model is silent.
12
+ **Audio:** Seedance 2.5 still emits real synced audio. `list_models` may show `sound_generation_type: none` because there is no in-app toggle (`sound_baked_in: true`). Do not tell the user the model is silent. Quoted dialogue in the prompt is PERFORMED — synced voices, lip movement, room tone — so scene dialogue never goes through `generate_speech` or `generate_lipsync`; write the lines in quotes inside their shot beats.
13
13
 
14
14
  ## What's NEW in 2.5 (verified — never hedge)
15
15
 
@@ -668,15 +668,21 @@ function renderAudio(sc, urls) {
668
668
  var titleBase = track.title || sc.title || (TOOL_TITLES[sc.tool] || 'Audio');
669
669
  var title = titleBase + (urls.length > 1 ? ' — Track ' + (i + 1) : '');
670
670
  var duration = track.duration != null ? track.duration : sc.duration;
671
- var artwork = track.thumbnail_url || sc.thumbnail_url;
671
+ // The voice's own portrait is the artwork for speech a generic note glyph
672
+ // told the user nothing about the one thing that defines the take.
673
+ var artwork = track.thumbnail_url || sc.thumbnail_url || sc.voice_thumbnail;
674
+ var placeholder = sc.tool === 'generate_speech' ? ICONS.mic : ICONS.audio;
672
675
  return '<div class="k-audio-row k-generated-audio">' +
673
- (artwork ? '<img class="k-audio-art" src="' + esc(artwork) + '" alt="" loading="lazy">' :
674
- '<div class="k-audio-art k-audio-placeholder">' + ICONS.audio + '</div>') +
676
+ (artwork ? '<img class="k-audio-art" src="' + esc(artwork) + '" alt="" loading="lazy" onerror="this.style.display=\\'none\\'">' :
677
+ '<div class="k-audio-art k-audio-placeholder">' + placeholder + '</div>') +
675
678
  '<div class="k-audio-meta"><div class="k-audio-title">' + esc(title) + '</div>' +
676
- // Resolved name first: a per-track model field is the raw id, and every
677
- // track in one generation came from the same model anyway.
678
- '<div class="k-audio-sub">' + esc(modelLabel(sc) || track.model || '') +
679
- (duration ? ' · ' + fmtDur(duration) : '') + '</div></div>' +
679
+ // Voice FIRST where there is one on a speech row, who is speaking is the
680
+ // thing that defines the take; the engine is secondary. Resolved names
681
+ // only: a per-track model field is the raw id, and every track in one
682
+ // generation came from the same model anyway.
683
+ '<div class="k-audio-sub">' +
684
+ [voiceLabel(sc), modelLabel(sc) || track.model, duration ? fmtDur(duration) : '']
685
+ .filter(Boolean).map(esc).join(' · ') + '</div></div>' +
680
686
  '<button class="k-btn k-audio-download" data-audio-download="' + esc(u) +
681
687
  '" aria-label="Download ' + esc(title) + '">' + ICONS.download + ' Download</button>' +
682
688
  '<audio class="k-audio-player" src="' + esc(u) + '" controls preload="none" aria-label="Play ' +
@@ -659,6 +659,14 @@ async function uiCompleted(p, textPayload, extraContent) {
659
659
  // above which assume everything finished together. Only set when the
660
660
  // caller actually has this shape; every existing caller is unaffected.
661
661
  ...(Array.isArray(p.items) ? { items: p.items } : {}),
662
+ // The voice, by name and portrait. uiGenerating has carried this since the
663
+ // chips were introduced; uiCompleted never did, so it silently dropped a
664
+ // resolved voice its caller had already looked up — every FINISHED speech
665
+ // card fell back to `settings.voice`, printing a raw ElevenLabs id where the
666
+ // name belongs and rendering the generic note placeholder instead of the
667
+ // voice's face. Exactly the "never show a raw id on a card" rule, broken on
668
+ // the one path the user actually ends up looking at.
669
+ ...(p.voice ? { voice_name: p.voice.name, voice_thumbnail: p.voice.thumbnail } : {}),
662
670
  // The RAW generation state, when the caller has one. `phase` above is
663
671
  // hardcoded 'completed' — it means "this tool call finished", not "the
664
672
  // generation finished" — so a status check on a still-running job looked
@@ -855,11 +855,19 @@ function registerGenerateTools(server, client, options = {}) {
855
855
  if (poll.timedOut) return poll.timedOut;
856
856
  const result = poll.result;
857
857
 
858
+ // What ACTUALLY ran, not what was asked for. The status result reports the
859
+ // engine's own choice (`model: "google_tts"`, `voice: "he-IL-Chirp3-HD-Kore"`)
860
+ // and for an omitted model that is the ONLY place the real answer appears —
861
+ // the card was labelling those "Smart Select", which is not even a text-to-
862
+ // speech option, and naming the voice the caller typed rather than the one
863
+ // that spoke. Same resolution addDisplayNames does for the polling path.
864
+ const ranVoice = (await voiceInfo(client, result.result?.voice).catch(() => null)) || voiceRecord;
858
865
  return uiCompleted({
859
- tool: 'generate_speech', kind: 'audio', gen, client, model, prompt: text,
860
- voice: voiceRecord,
866
+ tool: 'generate_speech', kind: 'audio', gen, client, prompt: text,
867
+ model: result.result?.model || model,
868
+ voice: ranVoice,
861
869
  settings: {
862
- voice: voice || 'Rachel',
870
+ voice: (ranVoice && ranVoice.name) || voice || 'Rachel',
863
871
  style: selected_style || emotion || style_instructions_preset_id || style_instructions,
864
872
  speaking_speed,
865
873
  language,
@@ -1181,10 +1189,10 @@ function registerGenerateTools(server, client, options = {}) {
1181
1189
  {
1182
1190
  prompt: z.string().describe('Locked Intro prompt (Seedance/Elements): Total line, [GLOBAL LOOK], [CAST] with @ExactDNAName for every visual_dna_ids entry, [LOCATION], then SHOT N. Not SCENE CONTEXT/OPTICS/ACTION packs. Never substitute "the left man" or "Zohar\'s" for @Name.'),
1183
1191
  model: z.string().optional().describe('Model identifier. If the user already named a family (Grok / Kling / Veo / Seedance / …), pass THAT family — never default to Seedance because Elements often uses it. Use list_models type="elements" for exact ids and elements_max_* caps. Do NOT omit (omitting = Smart Select).'),
1184
- reference_images: z.array(z.string()).optional().describe('Array of image references (product shots, character references, etc.). Accepts a public URL (forwarded as-is, never re-uploaded) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
1185
- reference_videos: z.array(z.string()).optional().describe('Array of reference videos for models that accept video inputs. Accepts a public URL (forwarded as-is, never re-uploaded) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_videos` URLs from list_models — if the cap is 0 the model rejects videos.**'),
1186
- reference_audio_urls: z.array(z.string()).optional().describe('Array of reference audio tracks for models that accept audio inputs. Accepts a public URL (forwarded as-is, never re-uploaded) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_audio` URLs from list_models.** `audio_url` remains supported as the legacy single-track form.'),
1187
- audio_url: z.string().optional().describe('A single reference audio track — legacy form of reference_audio_urls. Accepts a public URL (forwarded as-is, never re-uploaded) OR an absolute local path, which is uploaded for you. **Audio constraints: `elements_max_audio` from list_models gates whether audio is accepted at all; audio duration must fall within `min_audio_duration`-`max_audio_duration`; format must be in `supported_audio_formats` (if specified).**'),
1192
+ reference_images: z.array(z.string()).optional().describe('Array of image references (product shots, character references, etc.). Accepts a public URL (forwarded as-is; if the API rejects an external URL as untrusted, it is auto-rehosted into the media library and retried once) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
1193
+ reference_videos: z.array(z.string()).optional().describe('Array of reference videos for models that accept video inputs. Accepts a public URL (forwarded as-is; if the API rejects an external URL as untrusted, it is auto-rehosted into the media library and retried once) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_videos` URLs from list_models — if the cap is 0 the model rejects videos.**'),
1194
+ reference_audio_urls: z.array(z.string()).optional().describe('Array of reference audio tracks for models that accept audio inputs. Accepts a public URL (forwarded as-is; if the API rejects an external URL as untrusted, it is auto-rehosted into the media library and retried once) OR an absolute local path, which is uploaded for you. **Cap: pass at most `elements_max_audio` URLs from list_models.** `audio_url` remains supported as the legacy single-track form.'),
1195
+ audio_url: z.string().optional().describe('A single reference audio track — legacy form of reference_audio_urls. Accepts a public URL (forwarded as-is; if the API rejects an external URL as untrusted, it is auto-rehosted into the media library and retried once) OR an absolute local path, which is uploaded for you. **Audio constraints: `elements_max_audio` from list_models gates whether audio is accepted at all; audio duration must fall within `min_audio_duration`-`max_audio_duration`; format must be in `supported_audio_formats` (if specified).**'),
1188
1196
  files: z.array(z.string()).optional().describe('Untyped catch-all for mixed media — images, videos AND audio, each a URL or an absolute local path. The kind is detected from the file extension and the item is routed to the matching reference list, so a local .mp4 is sent as a video and a local .mp3 as audio. Prefer the typed lists (reference_images / reference_videos / reference_audio_urls) when you already know the kind; they accept local paths too. URLs given here are forwarded as URLs, never re-uploaded. **Caps still apply per kind: `elements_max_images` / `elements_max_videos` / `elements_max_audio` from list_models. Local uploads are capped at 200MB each.**'),
1189
1197
  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: 5'),
1190
1198
  aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in `supported_aspect_ratios` from list_models. Default: "16:9"'),
@@ -1250,10 +1258,10 @@ function registerGenerateTools(server, client, options = {}) {
1250
1258
  session_name, project_id, session_id
1251
1259
  };
1252
1260
 
1253
- let startResponse;
1254
- if (!locals.length) {
1255
- startResponse = await client.post('/v1/generate/elements', body);
1256
- } else {
1261
+ const startElements = async () => {
1262
+ if (!locals.length) {
1263
+ return client.post('/v1/generate/elements', body);
1264
+ }
1257
1265
  const resolved = await Promise.all(locals.map(({ src, kind }) =>
1258
1266
  resolveToBuffer(src, kind, { maxBytes: ELEMENTS_MAX_UPLOAD_BYTES })));
1259
1267
  const form = new FormData();
@@ -1277,7 +1285,36 @@ function registerGenerateTools(server, client, options = {}) {
1277
1285
  for (const f of resolved) {
1278
1286
  form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
1279
1287
  }
1280
- startResponse = await client.postMultipart('/v1/generate/elements', form);
1288
+ return client.postMultipart('/v1/generate/elements', form);
1289
+ };
1290
+
1291
+ let startResponse;
1292
+ try {
1293
+ startResponse = await startElements();
1294
+ } catch (err) {
1295
+ // The elements trust gate only accepts remote references that are BOTH
1296
+ // Kolbo-hosted AND registered to this caller's library/project — any
1297
+ // other public URL 400s with this code, even though every schema here
1298
+ // says "public URL, forwarded as-is". Agents used to recover by hand
1299
+ // (upload_media, then resend). Do that detour for them: rehost every
1300
+ // remote reference into the caller's library and retry ONCE. The gate
1301
+ // fires before any charge, so the failed first attempt costs nothing.
1302
+ if (err?.code !== 'UNTRUSTED_REFERENCE_MEDIA_URL') throw err;
1303
+ for (const kind of ['image', 'video', 'audio']) {
1304
+ media[kind] = await Promise.all(media[kind].map(async (src) => {
1305
+ if (!isUrlSource(src)) return src;
1306
+ const file = await resolveToBuffer(src, kind, { maxBytes: ELEMENTS_MAX_UPLOAD_BYTES });
1307
+ const form = new FormData();
1308
+ form.append('file', file.buffer, { filename: file.filename, contentType: file.contentType });
1309
+ if (project_id) form.append('project_id', project_id);
1310
+ const uploaded = await client.postMultipart('/v1/media/upload', form);
1311
+ return uploaded?.media?.url || uploaded?.url || src;
1312
+ }));
1313
+ }
1314
+ body.reference_images = some(urlsOf('image'));
1315
+ body.reference_videos = some(urlsOf('video'));
1316
+ body.reference_audio_urls = some(urlsOf('audio'));
1317
+ startResponse = await startElements();
1281
1318
  }
1282
1319
 
1283
1320
  if (ui()) return uiGenerating({
@@ -1576,10 +1613,18 @@ function registerGenerateTools(server, client, options = {}) {
1576
1613
  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.'),
1577
1614
  }).optional(),
1578
1615
  }).optional().describe('VEED Subtitles only: style overrides. Any omitted field keeps the preset default. Best supported by Basic presets.'),
1616
+ enhancement_model: z.string().optional()
1617
+ .describe('Topaz Slow Motion only (model "topaz/interpolate/video"): retiming engine — "Apollo" (smooth motion, default), "Chronos" (complex motion and occlusion), or "Aion" (highest quality for extreme slow motion, and the most expensive).'),
1618
+ target_fps: z.number().optional()
1619
+ .describe('Topaz Slow Motion only: frames per second of the OUTPUT (16-120, default 60). Higher rates generate more frames and cost proportionally more.'),
1620
+ slowdown_factor: z.number().optional()
1621
+ .describe('Topaz Slow Motion only: how many times longer the output runs (1-8, default 1). 4 turns a 5s clip into 20s of slow motion. Billing is on the OUTPUT length, so an 8x pass costs 8x a 1x pass.'),
1622
+ output_format: z.string().optional()
1623
+ .describe('Topaz HDR only (model "topaz/sdr-to-hdr/video"): "mp4" for 10-bit H.265 HDR10 (default) or "prores" for 10-bit ProRes 422 HQ. Both are HDR masters — the in-app player shows a tone-mapped SDR preview and the HDR file is the download.'),
1579
1624
  project_id: projectIdField,
1580
1625
  session_id: sessionIdField
1581
1626
  },
1582
- async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id, session_id }) => {
1627
+ async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, enhancement_model, target_fps, slowdown_factor, output_format, project_id, session_id }) => {
1583
1628
  model = await canonicalModelId(client, model, 'video_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
1584
1629
  if (!source_video) throw new Error('source_video is required');
1585
1630
 
@@ -1589,7 +1634,9 @@ function registerGenerateTools(server, client, options = {}) {
1589
1634
  startResponse = await client.post('/v1/generate/video-from-video', {
1590
1635
  video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled,
1591
1636
  reference_images, reference_videos, elements, preset, source_language, translation_language,
1592
- srt_content, srt_file_url, vocabulary, customization, project_id, session_id
1637
+ srt_content, srt_file_url, vocabulary, customization,
1638
+ enhancement_model, target_fps, slowdown_factor, output_format,
1639
+ project_id, session_id
1593
1640
  });
1594
1641
  } else {
1595
1642
  const resolved = await resolveToBuffer(source_video, 'video');
@@ -1613,6 +1660,10 @@ function registerGenerateTools(server, client, options = {}) {
1613
1660
  if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
1614
1661
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
1615
1662
  if (elements) form.append('elements', JSON.stringify(elements));
1663
+ if (enhancement_model) form.append('enhancement_model', enhancement_model);
1664
+ if (target_fps !== undefined) form.append('target_fps', String(target_fps));
1665
+ if (slowdown_factor !== undefined) form.append('slowdown_factor', String(slowdown_factor));
1666
+ if (output_format) form.append('output_format', output_format);
1616
1667
  if (project_id) form.append('project_id', project_id);
1617
1668
  if (session_id) form.append('session_id', session_id);
1618
1669
  startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
@@ -1822,7 +1873,8 @@ function registerGenerateTools(server, client, options = {}) {
1822
1873
  'camera_angle',
1823
1874
  'split', 'split_upscale',
1824
1875
  'multi_shot',
1825
- 'magic_edit'
1876
+ 'magic_edit',
1877
+ 'enhance'
1826
1878
  ]).describe([
1827
1879
  'Edit operation:',
1828
1880
  '"upscale" — increase resolution by 2×, 3×, or 4× (use `scale`). "clarity_upscale" — AI-powered clarity upscale with detail enhancement (use `resolution`).',
@@ -1831,6 +1883,7 @@ function registerGenerateTools(server, client, options = {}) {
1831
1883
  '"removebg" — remove the image background, output is transparent PNG.',
1832
1884
  '"background_replace" — remove background and replace it with AI-generated content from `prompt`.',
1833
1885
  '"enhance_skin" — portrait skin retouching (use `skin_strength`: "subtle" | "realistic" | "pimple" | "freckle").',
1886
+ '"enhance" — Topaz photo correction at the SOURCE resolution (no resizing). Pick the tool with `model`: "topaz/adjust/image" (exposure, white balance, or colorizing a black-and-white photo), "topaz/sharpen/image" (lens / motion / portrait / wildlife blur, or Super Focus for severely blurred shots), "topaz/denoise/image" (high-ISO and night noise), "topaz/restore/image" (old or damaged photos, dust and scratches). Choose the specific engine with `enhancement_model` — call list_models type="graphics_enhance" to see each model\'s engines. To make an image BIGGER use "upscale" instead.',
1834
1887
  '"inpaint" — paint over a masked area using `mask_image_url` (B&W mask, white = fill area) and optional `prompt`. Add reference images via `additional_images`.',
1835
1888
  '"erase" — erase an object defined by `mask_image_url` (white = erase area).',
1836
1889
  '"face_swap" — swap the face in `image_url` with the face from `mask_image_url` (required).',
@@ -1845,7 +1898,13 @@ function registerGenerateTools(server, client, options = {}) {
1845
1898
 
1846
1899
  // ── upscale ────────────────────────────────────────────
1847
1900
  scale: z.number().optional()
1848
- .describe('Upscale factor: 2, 3, or 4. Used with operation="upscale". Default: 2.'),
1901
+ .describe('Upscale factor: 1, 2, 4 or 8. Used with operation="upscale". Default: 2.'),
1902
+
1903
+ enhancement_model: z.string().optional()
1904
+ .describe('Topaz engine. With operation="upscale" on model "topaz/upscale/image" it selects the engine family: "Standard V2" / "High Fidelity V3" / "CGI" / "Text Refine" (faithful), "Wonder 3.5" (rebuilds natural detail), "Bloom 2" (reinvents detail — most expensive), "Transparent" (keeps the alpha channel). With operation="enhance" it selects the correction engine for the chosen model. Omit for the model default. Call list_models to see the engines a model offers.'),
1905
+
1906
+ output_format: z.string().optional()
1907
+ .describe('Output image format: "jpeg" or "png". Used with Topaz "upscale" and "enhance". Defaults to jpeg (the transparent upscaler always returns png).'),
1849
1908
 
1850
1909
  resolution: z.string().optional()
1851
1910
  .describe('Target output resolution (e.g. "4k", "2k", "1080p"). Used with "clarity_upscale", "split_upscale", "multi_shot".'),
@@ -1902,6 +1961,7 @@ function registerGenerateTools(server, client, options = {}) {
1902
1961
  image_url, operation, model, scale, aspect_ratio, skin_strength, prompt,
1903
1962
  mask_image_url, additional_images, generate_all_angles, resolution, quality, ai_optimize = false,
1904
1963
  zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
1964
+ enhancement_model, output_format,
1905
1965
  project_id, session_id
1906
1966
  }) => {
1907
1967
  // No `type` argument: these are operation-routed tools (upscale / reframe /
@@ -1911,6 +1971,9 @@ function registerGenerateTools(server, client, options = {}) {
1911
1971
 
1912
1972
  // Basic validation
1913
1973
  if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe');
1974
+ if (operation === 'enhance' && !model) {
1975
+ throw new Error('model is required for enhance — pick one of: topaz/adjust/image, topaz/sharpen/image, topaz/denoise/image, topaz/restore/image');
1976
+ }
1914
1977
  if (operation === 'background_replace' && !prompt) throw new Error('prompt is required for background_replace');
1915
1978
  if (operation === 'face_swap' && !mask_image_url && !(additional_images && additional_images.length > 0)) {
1916
1979
  throw new Error('mask_image_url (face reference) is required for face_swap');
@@ -1920,6 +1983,7 @@ function registerGenerateTools(server, client, options = {}) {
1920
1983
  image_url, operation, model, scale, aspect_ratio, skin_strength, prompt,
1921
1984
  mask_image_url, additional_images, generate_all_angles, resolution, quality, ai_optimize,
1922
1985
  zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
1986
+ enhancement_model, output_format,
1923
1987
  project_id, session_id
1924
1988
  });
1925
1989
 
@@ -1992,6 +2056,8 @@ function registerGenerateTools(server, client, options = {}) {
1992
2056
  .describe('Target resolution (e.g. "4k", "2k", "1080p"). Used with "upscale" and "reframe".'),
1993
2057
  target_fps: z.number().optional()
1994
2058
  .describe('Target frame rate (e.g. 24, 30, 60). Used with operation="upscale".'),
2059
+ enhancement_model: z.string().optional()
2060
+ .describe('Topaz enhancement engine for operation="upscale" on model "topaz/upscale/video". Families: Precision (faithful — "Proteus", "Artemis High Quality", "Iris", "Dione TV", "Gaia CG", "Gaia 2"), Denoise ("Nyx", "Nyx Fast"), Generative (rebuilds detail that is not in the source — "Starlight Fast 2", "Starlight HQ", "Starlight Precise 2.6"), Creative ("Astra 2", always renders 4K). Generative and Creative engines cost several times the Precision rate. Omit for the default.'),
1995
2061
 
1996
2062
  // ── reframe ────────────────────────────────────────────
1997
2063
  aspect_ratio: z.string().optional()
@@ -2055,7 +2121,7 @@ function registerGenerateTools(server, client, options = {}) {
2055
2121
  async ({
2056
2122
  video_url, operation, model, aspect_ratio, scale, prompt,
2057
2123
  image_url, audio_url, duration, mode,
2058
- target_fps, resolution,
2124
+ target_fps, resolution, enhancement_model,
2059
2125
  grid_position_x, grid_position_y,
2060
2126
  sound_effect_prompt, background_music_prompt, original_sound, cfg_strength,
2061
2127
  refine_edges, subject_is_person,
@@ -2078,7 +2144,7 @@ function registerGenerateTools(server, client, options = {}) {
2078
2144
  if (operation === 'extend' && !duration) throw new Error('duration is required for extend');
2079
2145
 
2080
2146
  const gen = await client.post('/v1/edit/video', {
2081
- video_url, operation, model, aspect_ratio, scale, prompt,
2147
+ video_url, operation, model, aspect_ratio, scale, prompt, enhancement_model,
2082
2148
  image_url, audio_url, duration, mode,
2083
2149
  target_fps, resolution,
2084
2150
  grid_position_x, grid_position_y,