@kolbo/mcp 1.108.0 → 1.110.1

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.108.0",
3
+ "version": "1.110.1",
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": {
package/src/apps/html.js CHANGED
@@ -236,9 +236,13 @@ function showPeekItem(time) {
236
236
  var it = peekList[peekIndex];
237
237
  var img = el('peek-img');
238
238
  var vid = el('peek-video');
239
+ var oldNote = el('peek-stage') && el('peek-stage').querySelector('.k-vnote');
240
+ if (oldNote) oldNote.parentNode.removeChild(oldNote);
241
+ vid.removeAttribute('data-unplayable');
239
242
  if (it.kind === 'video') {
240
243
  img.removeAttribute('src');
241
244
  vid.setAttribute('src', it.url);
245
+ guardPlayback(vid);
242
246
  if (time) vid.addEventListener('loadedmetadata', function () { try { vid.currentTime = time; } catch (e) {} }, { once: true });
243
247
  var p = vid.play && vid.play();
244
248
  if (p && p.catch) p.catch(function () {});
@@ -363,6 +367,42 @@ function wireVideoExpand(root) {
363
367
  b.addEventListener('keydown', mark);
364
368
  v.addEventListener('dblclick', function (e) { e.preventDefault(); mark(); b.click(); });
365
369
  v.parentNode.insertBefore(b, v.nextSibling);
370
+ guardPlayback(v);
371
+ });
372
+ }
373
+ // Some outputs cannot be decoded by a browser: 10/12-bit HEVC (Bytedance
374
+ // Upscaler Pro, Seedance 1080p masters) either errors or plays sound over a
375
+ // black frame. Detect both and replace the dead player with a clear note and
376
+ // a Download button instead of leaving the user staring at nothing.
377
+ function guardPlayback(v) {
378
+ if (!v || v.getAttribute('data-guard')) return;
379
+ v.setAttribute('data-guard', '1');
380
+ function fail() {
381
+ if (v.getAttribute('data-unplayable') || !v.parentNode) return;
382
+ v.setAttribute('data-unplayable', '1');
383
+ try { v.pause(); } catch (e) {}
384
+ var url = v.currentSrc || v.getAttribute('src');
385
+ var note = document.createElement('div');
386
+ note.className = 'k-vnote';
387
+ note.innerHTML = '<div class="k-vnote-t"></div><div class="k-vnote-s"></div><button type="button" class="k-btn primary"></button>';
388
+ note.querySelector('.k-vnote-t').textContent = "This video can't play in the browser";
389
+ note.querySelector('.k-vnote-s').textContent = 'It uses a high-quality format (HEVC, 10/12-bit) most browsers do not decode. Download it and open it in a video player.';
390
+ var dl = note.querySelector('button');
391
+ dl.innerHTML = ICONS.download + ' Download video';
392
+ dl.onclick = function (e) { e.stopPropagation(); window.kolbo.openLink(downloadUrl(url)); };
393
+ v.parentNode.insertBefore(note, v.nextSibling);
394
+ if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
395
+ }
396
+ v.addEventListener('error', function () {
397
+ var code = v.error && v.error.code;
398
+ // 3 = decode failure, 4 = format not supported. Network errors (2) are not a format problem.
399
+ if (code === 3 || code === 4) fail();
400
+ });
401
+ // Sound with no picture: time advances but no video frame was ever decoded.
402
+ v.addEventListener('timeupdate', function () {
403
+ if (v.currentTime < 1.2 || v.getAttribute('data-unplayable')) return;
404
+ var q = v.getVideoPlaybackQuality ? v.getVideoPlaybackQuality() : null;
405
+ if ((q && q.totalVideoFrames === 0) || (v.readyState >= 2 && v.videoWidth === 0)) fail();
366
406
  });
367
407
  }
368
408
  if (window.MutationObserver) {
package/src/apps/index.js CHANGED
@@ -358,8 +358,20 @@ async function modelInfoMap(client) {
358
358
  }
359
359
 
360
360
  /** Resolve one model's { icon, eta, name }; missing → all null. */
361
+ // Engines hidden from /v1/models that still reach a card, shown as the public
362
+ // model the user actually knows. draft_enhance finalizes a Seedance 2.5 Draft;
363
+ // its engine id is internal, so the card showed "Smart Select" / a raw id.
364
+ const DISPLAY_ALIASES = {
365
+ 'flux-3-draft-enhance': { base: 'seedance-2-5', name: 'Seedance 2.5 Draft Enhance' },
366
+ 'video-draft-enhance': { base: 'seedance-2-5', name: 'Seedance 2.5 Draft Enhance' },
367
+ };
368
+ // Operations whose engine is picked server-side when no model is passed.
369
+ const OPERATION_ENGINES = { draft_enhance: 'flux-3-draft-enhance' };
370
+
361
371
  async function modelInfo(client, modelName) {
362
372
  if (!modelName) return { icon: null, eta: null, name: null };
373
+ const alias = DISPLAY_ALIASES[String(modelName).toLowerCase()];
374
+ if (alias) return { ...(await modelInfo(client, alias.base)), name: alias.name };
363
375
  const catalog = await modelCatalog(client);
364
376
  const exact = catalog.byKey.get(String(modelName).toLowerCase());
365
377
  if (exact) return exact;
@@ -827,6 +839,7 @@ module.exports = {
827
839
  appsEnabled,
828
840
  modelIcon,
829
841
  modelInfo,
842
+ OPERATION_ENGINES,
830
843
  modelInfoMap,
831
844
  voiceInfo,
832
845
  canonicalModelId,
package/src/apps/theme.js CHANGED
@@ -222,6 +222,17 @@ html.k-peek-fs .k-card { min-height: 0; backdrop-filter: none; -webkit-backdrop-
222
222
  display: inline-flex; align-items: center; justify-content: center;
223
223
  }
224
224
  .k-vexpand:hover { background: var(--brand); border-color: var(--brand); }
225
+ /* Undecodable video (10/12-bit HEVC): the dead player is replaced by a note. */
226
+ video[data-unplayable] { display: none !important; }
227
+ video[data-unplayable] ~ .k-vexpand { display: none; }
228
+ .k-vnote { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
229
+ width: 100%; min-height: 160px; padding: 18px 16px; border-radius: 10px; text-align: center;
230
+ background: rgba(255, 255, 255, 0.05); }
231
+ .k-vnote-t { font-size: 13px; font-weight: 600; color: var(--text); }
232
+ .k-vnote-s { font-size: 12px; color: var(--text-muted); max-width: 360px; margin-bottom: 4px; }
233
+ .k-peek-stage .k-vnote { max-width: 440px; color: #fff; }
234
+ .k-peek-stage .k-vnote-t { color: #fff; }
235
+ .k-peek-stage .k-vnote-s { color: rgba(255, 255, 255, 0.7); }
225
236
  /* Recent Chromium still draws the native button despite controlslist. */
226
237
  video::-webkit-media-controls-fullscreen-button { display: none !important; }
227
238
  /* ---- Plans / upgrade card ---- */
@@ -340,7 +340,8 @@ function renderChips(sc) {
340
340
  : ICONS.mic;
341
341
  h += chip(face + ' ' + esc(voice));
342
342
  }
343
- if (s.mode) h += chip(esc(s.mode));
343
+ if (s.mode) h += chip(esc(editLabel(s.mode)));
344
+ if (Array.isArray(s.details)) s.details.forEach(function (d) { if (d) h += chip(esc(String(d))); });
344
345
  if (many && !kind) h += chip(sc.count + ' items');
345
346
  h += referenceHTML(sc);
346
347
  el('chips').innerHTML = h;
@@ -460,6 +461,17 @@ function capLabel(inner) {
460
461
  return String(inner).replace(/(^|>)(\\s*)([a-z])/, function (m, gt, sp, ch) { return gt + sp + ch.toUpperCase(); });
461
462
  }
462
463
  function chip(inner) { return '<span class="k-chip">' + capLabel(inner) + '</span>'; }
464
+ // edit_image / edit_video operation ids -> the words a user reads. Unknown ids
465
+ // fall back to the id with spaces, never the raw snake_case.
466
+ var EDIT_LABELS = {
467
+ upscale: 'Upscale', clarity_upscale: 'Clarity upscale', reframe: 'Reframe', zoom_out: 'Expand',
468
+ removebg: 'Remove background', remove_background: 'Remove background', background_replace: 'Replace background',
469
+ enhance_skin: 'Skin enhance', enhance: 'Photo enhance', inpaint: 'Inpaint', erase: 'Erase', face_swap: 'Face swap',
470
+ camera_angle: 'Camera angle', split: 'Split grid', split_upscale: 'Split + upscale', multi_shot: 'Multi-shot',
471
+ magic_edit: 'Magic edit', generate_audio: 'Add audio', remove_watermark: 'Remove watermark', extend: 'Extend',
472
+ lipsync: 'Lipsync', retake: 'Retake', draft_enhance: 'Finalize draft', draft_quote: 'Draft quote'
473
+ };
474
+ function editLabel(mode) { return EDIT_LABELS[mode] || String(mode).split('_').join(' '); }
463
475
  // Same chip with a hover title — used to surface the asset id behind a
464
476
  // "2 Visual DNA" / "preset" label without spending chip width on it.
465
477
  function chipT(inner, title) {
@@ -1246,6 +1258,7 @@ function renderActions(sc) {
1246
1258
  bind('btn-open', function () { window.kolbo.openLink(kolboUrl(state)); });
1247
1259
  bind('btn-recreate', function () {
1248
1260
  window.kolbo.sendMessage('Recreate this with the same settings' +
1261
+ (state.settings && state.settings.mode ? '\\nOperation: ' + state.settings.mode : '') +
1249
1262
  (state.model ? '\\nModel: ' + state.model : '') +
1250
1263
  (state.prompt ? '\\nPrompt: ' + state.prompt : '') +
1251
1264
  '\\n(from the ' + (TOOL_TITLES[state.tool] || 'generation') + ' widget)');
@@ -1335,7 +1348,7 @@ function preRefSc(toolName, a) {
1335
1348
  resolution: a.draft === true ? '480p-draft' : a.draft === false ? String(a.resolution || '').replace(/-draft$/i, '') : a.resolution,
1336
1349
  aspect_ratio: a.aspect_ratio,
1337
1350
  quality: a.quality,
1338
- mode: a.mode,
1351
+ mode: a.operation || a.mode,
1339
1352
  cinematic: a.cinematic,
1340
1353
  visual_dna_ids: a.visual_dna_ids,
1341
1354
  moodboard_ids: a.moodboard_ids,
@@ -1405,6 +1418,7 @@ function liveFromTimedOut(sc) {
1405
1418
  resolution: originArgs.draft === true ? '480p-draft' : originArgs.draft === false ? String(originArgs.resolution || '').replace(/-draft$/i, '') : originArgs.resolution,
1406
1419
  aspect_ratio: originArgs.aspect_ratio,
1407
1420
  quality: originArgs.quality,
1421
+ mode: originArgs.operation,
1408
1422
  visual_dna_ids: originArgs.visual_dna_ids,
1409
1423
  moodboard_id: originArgs.moodboard_id
1410
1424
  },
@@ -1433,7 +1447,8 @@ function completedFromPlain(sc) {
1433
1447
  is_draft: originArgs.draft,
1434
1448
  resolution: originArgs.draft === true ? '480p-draft' : originArgs.draft === false ? String(originArgs.resolution || '').replace(/-draft$/i, '') : originArgs.resolution,
1435
1449
  aspect_ratio: originArgs.aspect_ratio,
1436
- quality: originArgs.quality
1450
+ quality: originArgs.quality,
1451
+ mode: originArgs.operation
1437
1452
  },
1438
1453
  urls: sc.urls || [],
1439
1454
  session_id: sc.session_id || originArgs.session_id,
package/src/index.js CHANGED
@@ -141,7 +141,8 @@ function createServer(opts = {}) {
141
141
  '9. MODEL SELECTION — NAMED MODEL WINS, THEN STRENGTHS SUMMARY: ALWAYS pass a specific `model` on every generation tool — do NOT omit it (omitting falls back to "Smart Select" auto-routing, which hides the choice from the user; use it ONLY if the user explicitly asks you to auto-pick). If the user named a model this turn OR earlier in the conversation (including a compaction "Locked choices" / summary), that name is a FAMILY LOCK: pass it (or its display name) on every follow-up, including when the tool changes (text-to-video → image-to-video). Identifier resolution remaps a t2v id to the family\'s i2v sibling automatically. NEVER substitute a different brand because it is cheaper, faster, or "best balance" (Grok Imagine named → do not fire Seedance). If the named family has no variant for this modality, ASK — do not silently switch. Cheapest-summary routing applies ONLY when no model was named on this task: call `list_models` with the matching `type` and read each model\'s STRENGTHS SUMMARY — the "— …" clause printed after the credit cost — then pick the CHEAPEST model whose summary covers the task. `[NEW]` and `[RECOMMENDED]` badges, a high credit number, and "flagship"/"most intelligent" wording are NOT selection signals — never pick a model because it is newest, biggest or most expensive. Escalate to a premium/frontier model only when the user explicitly asks for maximum quality, or when no cheaper summary covers the requirement. Models printed under "Named-only" (no summary) are opt-in: use them only when the user names them. TEXT/CHAT: `chat_send_message` bills PER TOKEN, so the listed credit number is not the cost — a frontier text model (Claude Fable 5, GPT-5.6 Sol, Pro-class) costs 5-30x a mid-tier one per reply. Default ordinary chat (writing, brainstorming, Q&A, summarising) to a balanced mid-tier model and reserve the frontier tier for hard reasoning or long-form code the user asked for.',
142
142
  '10. IMAGE EDITING: for ANY prompt-driven / content edit of an existing image — "make it night", changing scene/lighting/colors, adding/removing/replacing objects, restyling — use `generate_image_edit`. Auto-pick only Nano Banana 2 (`nano-banana-2` / `nano-banana-2-image-editing`) or GPT Image 2 (`gpt-image-2` / `gpt-image-2/edit`) for photoreal photo edits, object removal, keep-subject/remove-others, or crowd cleanup. Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style, named-only for editing. Do NOT use `edit_image` for content edits — `edit_image` is ONLY for mechanical enhancements (upscale, expand/outpaint, remove-background, skin retouch). Its `magic_edit` operation is deprecated in favor of `generate_image_edit`. EXPANDING AN IMAGE: to widen/extend/uncrop an image or fit it into a wider frame while KEEPING the existing artwork, use `edit_image` with operation="zoom_out" (outpainting — original pixels preserved; size it with `zoom_out_percentage` or the `expand_left/right/top/bottom` pixel args). The "reframe" operation is NOT this: it re-generates the whole picture at a new aspect ratio and the subject comes back re-imagined. Only pick "reframe" when the user wants the shot re-taken, never when they want their image extended.',
143
143
  '11. PRESET CONTRACT: if the user asks for a preset, names a preset, or says to use one of their/Kolbo presets, you MUST call `list_presets` with the matching type before generation, resolve the named or closest matching preset, and pass its exact returned `id` as `preset_id`. Use type="image" for generate_image and type="image_edit" for generate_image_edit. Never silently ignore a preset request, never invent an id, and never claim a preset was applied unless `preset_id` was present in the generation call.',
144
- '12. DRAFT IS A TIER, NOT A RESOLUTION: when the user says "draft" (Hebrew "טיוטה") for a video — "480 draft", "draft mode", "480p draft" — pass `draft: true` on the generate_* call (or `resolution: "480p-draft"`) with the SAME model, e.g. `seedance-2-5`. Plain `resolution: "480p"` is a regular full-price render and is NEVER Draft. Draft exists only where list_models `supported_resolutions` includes "480p-draft"; if the chosen model lacks it, say so instead of silently rendering regular 480p.'
144
+ '12. DRAFT IS A TIER, NOT A RESOLUTION: when the user says "draft" (Hebrew "טיוטה") for a video — "480 draft", "draft mode", "480p draft" — pass `draft: true` on the generate_* call (or `resolution: "480p-draft"`) with the SAME model, e.g. `seedance-2-5`. Plain `resolution: "480p"` is a regular full-price render and is NEVER Draft. Draft exists only where list_models `supported_resolutions` includes "480p-draft"; if the chosen model lacks it, say so instead of silently rendering regular 480p.',
145
+ '13. VIDEO UPSCALE SETTINGS: when the user asks to upscale a video with specific quality controls (6K/8K, Pro/fast, 10/12-bit, film restoration, AI-footage tuning, custom scale), use `edit_video` operation="upscale" with model "bytedance-upscaler/upscale/video" and pass the matching fields (`resolution` 1080p-8k or `scale_ratio`, `target_fps`, `enhancement_tier`, `enhancement_preset`, `fidelity`, `bit_depth`). Pro costs 10x and caps the source at 30s; 6K/8K cap at 120s/60s; 10/12-bit needs Pro. State the credit cost before running Pro or 6K/8K.'
145
146
  ].join('\n')
146
147
  });
147
148
  const progress = require('./progress');
@@ -698,7 +698,7 @@ function buildProjectUrl(projectId, opts = {}) {
698
698
  // ui://kolbo/generation.html widget takes over: live progress, inline result,
699
699
  // action buttons. Text-only hosts never enter this path — their blocking
700
700
  // behavior and response bytes are UNCHANGED.
701
- const { UI, uiResult, appsEnabled, modelInfo } = require('../apps');
701
+ const { UI, uiResult, appsEnabled, modelInfo, OPERATION_ENGINES } = require('../apps');
702
702
 
703
703
  /**
704
704
  * Chip identity for a model: the CLEAN display name + its icon, resolved from
@@ -706,8 +706,13 @@ const { UI, uiResult, appsEnabled, modelInfo } = require('../apps');
706
706
  * user/LLM supplied (an identifier like `google_tts`, `fal-ai/…/omnihuman/v1.5`,
707
707
  * or a display name) — the card must never show the raw id.
708
708
  */
709
- async function modelChipFields(client, model) {
710
- const info = await modelInfo(client, model).catch(() => null);
709
+ // `operation`: an edit whose engine the server picks when no model is passed
710
+ // (draft_enhance) still names that engine on the chip, not "Smart Select".
711
+ // `model` itself stays unset so Recreate never pins a hidden engine id.
712
+ async function modelChipFields(client, model, operation) {
713
+ const lookup = model || (operation && OPERATION_ENGINES[operation]);
714
+ const info = await modelInfo(client, lookup).catch(() => null);
715
+ if (!model && info && info.name) return { model: 'Smart Select', model_name: info.name, model_icon: info.icon || null };
711
716
  return {
712
717
  model: model || 'Smart Select',
713
718
  model_name: (info && info.name) || model || 'Smart Select',
@@ -898,7 +903,7 @@ function moodboardIds(settings) {
898
903
  */
899
904
  async function uiGenerating(p) {
900
905
  // No ETAs anywhere — just a spinner until the poll flips to completed.
901
- const chip = await modelChipFields(p.client, p.model);
906
+ const chip = await modelChipFields(p.client, p.model, p.settings && p.settings.mode);
902
907
  const settings = await decorateSettings(p.client, p.settings || {});
903
908
  const structured = {
904
909
  phase: 'generating',
@@ -1009,7 +1014,7 @@ function preferOwnedUrls(urls) {
1009
1014
  }
1010
1015
 
1011
1016
  async function uiCompleted(p, textPayload, extraContent) {
1012
- const chip = await modelChipFields(p.client, p.model);
1017
+ const chip = await modelChipFields(p.client, p.model, p.settings && p.settings.mode);
1013
1018
  const settings = p.settings ? await decorateSettings(p.client, p.settings) : undefined;
1014
1019
  const structured = {
1015
1020
  phase: 'completed',
@@ -131,6 +131,11 @@
131
131
  "upscaleMode",
132
132
  "targetResolution",
133
133
  "targetFps",
134
+ "scaleRatio",
135
+ "enhancementTier",
136
+ "enhancementPreset",
137
+ "fidelity",
138
+ "bitDepth",
134
139
  "extendMode",
135
140
  "extendDuration",
136
141
  "presetId",
@@ -20,7 +20,7 @@ const voice = z.union([z.string().max(256), z.object({
20
20
  name: z.string().max(500).optional(), type: z.string().max(128).optional(),
21
21
  }).strict()]);
22
22
  const booleanConfig = new Set('instrumental preservePitch resolved enhancePrompt soundEnabled sound_enabled showLyrics includeAudio loop reverse'.split(' '));
23
- const numberConfig = new Set('scale frameCount quantity startTime endTime speed timestamp speaking_speed promptInfluence creativity resemblance movement upscaleFactor targetFps extendDuration skinIntensity start end cropX cropY cropWidth cropHeight x y width height fps frameTime seed temperature maxTokens volume'.split(' '));
23
+ const numberConfig = new Set('scale frameCount quantity startTime endTime speed timestamp speaking_speed promptInfluence creativity resemblance movement upscaleFactor targetFps scaleRatio bitDepth extendDuration skinIntensity start end cropX cropY cropWidth cropHeight x y width height fps frameTime seed temperature maxTokens volume'.split(' '));
24
24
  const modelConfig = new Set('model t2iModel i2iModel t2vModel i2vModel flModel modelId engineId'.split(' '));
25
25
  const configShape = Object.fromEntries(contract.config_fields.map(field => {
26
26
  let schema = text;
@@ -2325,6 +2325,50 @@ function registerGenerateTools(server, client, options = {}) {
2325
2325
  }, null, 2));
2326
2326
  }
2327
2327
  );
2328
+ // Short human chips for the settings an edit actually ran with — the card
2329
+ // shows these next to the operation label (raw ids never reach the card).
2330
+ const editDetails = (op, a) => {
2331
+ const d = [];
2332
+ const has = (v) => v !== undefined && v !== null && v !== '';
2333
+ if (op === 'upscale' || op === 'split_upscale') {
2334
+ if (has(a.scale)) d.push(`${a.scale}×`);
2335
+ if (a.face_enhancement === false) d.push('no face enhance');
2336
+ else if (has(a.face_enhancement_strength)) d.push(`face ${a.face_enhancement_strength}`);
2337
+ }
2338
+ if (op === 'clarity_upscale') {
2339
+ if (has(a.scale)) d.push(`${a.scale}×`);
2340
+ if (has(a.creativity)) d.push(`creativity ${a.creativity}`);
2341
+ if (has(a.resemblance)) d.push(`resemblance ${a.resemblance}`);
2342
+ }
2343
+ if (op === 'enhance_skin') {
2344
+ d.push(a.skin_strength || 'realistic');
2345
+ if (a.fix_lighting) d.push('fix lighting');
2346
+ if (has(a.freckle_intensity) && a.skin_strength === 'freckle') d.push(`freckles ${a.freckle_intensity}`);
2347
+ }
2348
+ if (op === 'camera_angle') {
2349
+ if (a.generate_all_angles) d.push('all angles');
2350
+ else {
2351
+ if (has(a.horizontal_angle)) d.push(`${a.horizontal_angle}° turn`);
2352
+ if (has(a.vertical_angle)) d.push(`${a.vertical_angle}° tilt`);
2353
+ if (has(a.camera_zoom)) d.push(`zoom ${a.camera_zoom}`);
2354
+ }
2355
+ }
2356
+ if (op === 'zoom_out') {
2357
+ if ([a.expand_left, a.expand_right, a.expand_top, a.expand_bottom].some(has)) d.push('custom expand');
2358
+ else if (has(a.zoom_out_percentage)) d.push(`${a.zoom_out_percentage}%`);
2359
+ }
2360
+ if (op === 'split' && has(a.split_layout)) d.push(a.split_layout);
2361
+ if (op === 'background_replace' && a.background_reference_image) d.push('reference bg');
2362
+ if (has(a.enhancement_model)) d.push(a.enhancement_model);
2363
+ if (has(a.scale_ratio)) d.push(`${a.scale_ratio}×`);
2364
+ if (has(a.enhancement_tier) && a.enhancement_tier !== 'standard') d.push(a.enhancement_tier);
2365
+ if (has(a.enhancement_preset) && a.enhancement_preset !== 'general') d.push(a.enhancement_preset);
2366
+ if (has(a.bit_depth) && Number(a.bit_depth) > 8) d.push(`${a.bit_depth}-bit`);
2367
+ if (has(a.target_fps)) d.push(`${a.target_fps} fps`);
2368
+ if (op === 'extend' && has(a.mode)) d.push(`at ${a.mode}`);
2369
+ return d.length ? d : undefined;
2370
+ };
2371
+
2328
2372
  // ─── edit_image ────────────────────────────────────────────
2329
2373
  server.tool(
2330
2374
  'edit_image',
@@ -2351,12 +2395,12 @@ function registerGenerateTools(server, client, options = {}) {
2351
2395
  '"reframe" — RE-GENERATES the entire picture at a new aspect ratio (Luma Photon). The subject is re-imagined, not preserved — expect a different-looking image. Use ONLY when the user explicitly wants the shot re-taken in another format. If they said "expand", "extend", "widen", "uncrop", "add space", "fill the sides", or "keep it the same but 16:9", they want "zoom_out" instead. Requires `aspect_ratio`.',
2352
2396
  '"removebg" — remove the image background, output is transparent PNG.',
2353
2397
  '"background_replace" — remove background and replace it with AI-generated content from `prompt`.',
2354
- '"enhance_skin" — portrait skin retouching (use `skin_strength`: "subtle" | "realistic" | "pimple" | "freckle").',
2398
+ '"enhance_skin" — realistic skin texture on portraits (Enhancor). `skin_strength`: "subtle" (barely visible — avoid unless asked), "realistic" (default, visible pore/texture detail; add `fix_lighting=true` to also even out lighting), "pimple" (removes blemishes), "freckle" (adds freckles — set `freckle_intensity`). Works on visible faces: on a full-body shot or a multi-panel character sheet the face is small and the change is hard to see — run it on a face close-up or crop instead.',
2355
2399
  '"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.',
2356
2400
  '"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`.',
2357
2401
  '"erase" — erase an object defined by `mask_image_url` (white = erase area).',
2358
2402
  '"face_swap" — swap the face in `image_url` with the face from `mask_image_url` (required).',
2359
- '"camera_angle" — generate the image from a different camera angle. Set `generate_all_angles=true` for a full set. Use `prompt` to guide the angle.',
2403
+ '"camera_angle" — generate the image from a different camera angle. Set the view with `horizontal_angle` / `vertical_angle` / `camera_zoom` (preferred — exact), or describe it in `prompt` ("left side", "back", "low angle", "bird\'s eye", "close up", "wide"); with neither, the view barely changes. Set `generate_all_angles=true` for the 12-angle set.',
2360
2404
  '"split" — split the image into a 3×3 grid of tiles. "split_upscale" — split into a grid and upscale each tile.',
2361
2405
  '"multi_shot" — generate a 3×3 multi-shot grid of scenes (uses `additional_images` as reference shots). Use `resolution` for output quality.',
2362
2406
  '"magic_edit" (DEPRECATED) — prompt-driven content edit. Prefer `generate_image_edit` for better results.',
@@ -2400,7 +2444,45 @@ function registerGenerateTools(server, client, options = {}) {
2400
2444
 
2401
2445
  // ── enhance_skin ───────────────────────────────────────
2402
2446
  skin_strength: z.enum(['subtle', 'realistic', 'pimple', 'freckle']).optional()
2403
- .describe('Skin enhancement preset. Used with operation="enhance_skin". Default: "realistic".'),
2447
+ .describe('Skin enhancement preset. Used with operation="enhance_skin". Default: "realistic". "subtle" changes very little.'),
2448
+ freckle_intensity: z.union([z.literal(0), z.literal(50), z.literal(100)]).optional()
2449
+ .describe('Freckle amount: 0, 50 or 100. Used with operation="enhance_skin" and skin_strength="freckle". Default: 50.'),
2450
+ fix_lighting: z.boolean().optional()
2451
+ .describe('Also correct uneven / flat lighting on the face. Used with operation="enhance_skin" and skin_strength="realistic". Default: false.'),
2452
+
2453
+ // ── camera_angle ───────────────────────────────────────
2454
+ horizontal_angle: z.number().min(0).max(360).optional()
2455
+ .describe('Camera rotation around the subject in degrees (0-360; 0 = unchanged, 90 = right side, 180 = behind). Used with operation="camera_angle" when generate_all_angles is not set.'),
2456
+ vertical_angle: z.number().min(-30).max(90).optional()
2457
+ .describe('Camera tilt in degrees (-30 = from below, 0 = eye level, 90 = straight down). Used with operation="camera_angle".'),
2458
+ camera_zoom: z.number().min(0).max(10).optional()
2459
+ .describe('Camera distance (0 = far, 5 = unchanged, 10 = close-up). Used with operation="camera_angle". Default: 5.'),
2460
+
2461
+ // ── upscale (Topaz) / clarity_upscale ──────────────────
2462
+ face_enhancement: z.boolean().optional()
2463
+ .describe('Topaz face recovery. Used with operation="upscale" on Topaz precision/generative engines. Default: true — set false to keep faces exactly as in the source.'),
2464
+ face_enhancement_strength: z.number().min(0).max(1).optional()
2465
+ .describe('How strongly faces are recovered (0-1). Used with operation="upscale" (Topaz). Default: 0.8.'),
2466
+ face_enhancement_creativity: z.number().min(0).max(1).optional()
2467
+ .describe('How much new facial detail Topaz may invent (0-1; 0 = faithful). Used with operation="upscale". Default: 0.'),
2468
+ subject_detection: z.enum(['All', 'Foreground', 'Background']).optional()
2469
+ .describe('Which part Topaz enhances. Used with operation="upscale". Default: "All".'),
2470
+ crop_to_fill: z.boolean().optional()
2471
+ .describe('Crop to fill the target size instead of keeping the source framing. Used with operation="upscale" (Topaz). Default: false.'),
2472
+ creativity: z.number().optional()
2473
+ .describe('How much new detail is invented. operation="clarity_upscale": 0-1 (default 0.35). operation="upscale" with the Bloom (creative) engine: 1-9.'),
2474
+ resemblance: z.number().min(0).max(1).optional()
2475
+ .describe('How closely the result must match the source (0-1). Used with operation="clarity_upscale". Default: 0.6.'),
2476
+
2477
+ // ── background_replace / split / erase ─────────────────
2478
+ background_reference_image: z.string().optional()
2479
+ .describe('URL or absolute local path of an image to use as the new background. Used with operation="background_replace" (prompt still required).'),
2480
+ negative_prompt: z.string().optional()
2481
+ .describe('What must NOT appear. Used with operation="background_replace" and "clarity_upscale".'),
2482
+ split_layout: z.string().optional()
2483
+ .describe('Grid to split into: "auto" (detect), "2x2", "3x3" or "4x4". Used with operation="split" / "split_upscale". Default: "auto".'),
2484
+ mask_expansion: z.number().min(0).max(100).optional()
2485
+ .describe('Pixels to grow the erase mask so edges blend. Used with operation="erase". Default: 15.'),
2404
2486
 
2405
2487
  // ── inpaint / erase / face_swap / background_replace / zoom_out / camera_angle / magic_edit ──
2406
2488
  prompt: z.string().optional()
@@ -2427,10 +2509,14 @@ function registerGenerateTools(server, client, options = {}) {
2427
2509
  session_id: sessionIdField
2428
2510
  },
2429
2511
  async ({
2430
- image_url, operation, model, scale, aspect_ratio, skin_strength, prompt,
2512
+ image_url, operation, model, scale, aspect_ratio, skin_strength, freckle_intensity, fix_lighting, prompt,
2431
2513
  mask_image_url, additional_images, generate_all_angles, resolution, quality, ai_optimize = false,
2432
2514
  zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
2433
2515
  enhancement_model, output_format,
2516
+ horizontal_angle, vertical_angle, camera_zoom,
2517
+ face_enhancement, face_enhancement_strength, face_enhancement_creativity,
2518
+ subject_detection, crop_to_fill, creativity, resemblance,
2519
+ background_reference_image, negative_prompt, split_layout, mask_expansion,
2434
2520
  project_id, session_id
2435
2521
  }) => {
2436
2522
  const editModelType = modelTypeForEditOperation('image', operation);
@@ -2454,23 +2540,37 @@ function registerGenerateTools(server, client, options = {}) {
2454
2540
  }
2455
2541
 
2456
2542
  // Local paths → CDN URLs (image_url / mask_image_url / additional_images).
2457
- [image_url, mask_image_url, additional_images] = await Promise.all([
2543
+ [image_url, mask_image_url, additional_images, background_reference_image] = await Promise.all([
2458
2544
  rehostLocal(image_url, 'image', project_id),
2459
2545
  rehostLocal(mask_image_url, 'image', project_id),
2460
2546
  additional_images ? Promise.all(additional_images.map((s) => rehostLocal(s, 'image', project_id))) : additional_images,
2547
+ rehostLocal(background_reference_image, 'image', project_id),
2461
2548
  ]);
2462
2549
  const gen = await client.post('/v1/edit/image', {
2463
- image_url, operation, model, scale, aspect_ratio, skin_strength, prompt,
2550
+ image_url, operation, model, scale, aspect_ratio, skin_strength, freckle_intensity, fix_lighting, prompt,
2464
2551
  mask_image_url, additional_images, generate_all_angles, resolution, quality, ai_optimize,
2465
2552
  zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
2466
2553
  enhancement_model, output_format,
2554
+ horizontal_angle, vertical_angle, camera_zoom,
2555
+ face_enhancement, face_enhancement_strength, face_enhancement_creativity,
2556
+ subject_detection, crop_to_fill, creativity, resemblance,
2557
+ background_reference_image, negative_prompt, split_layout, mask_expansion,
2467
2558
  project_id, session_id
2468
2559
  });
2560
+ const cardSettings = {
2561
+ mode: operation, aspect_ratio, resolution,
2562
+ details: editDetails(operation, {
2563
+ scale, skin_strength, freckle_intensity, fix_lighting, generate_all_angles,
2564
+ horizontal_angle, vertical_angle, camera_zoom, face_enhancement, face_enhancement_strength,
2565
+ creativity, resemblance, zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
2566
+ split_layout, background_reference_image, enhancement_model,
2567
+ }),
2568
+ };
2469
2569
 
2470
2570
  if (returnsImmediately()) return submittedResult({
2471
2571
  tool: 'edit_image', kind: 'image', gen, client, model,
2472
- prompt: prompt || operation,
2473
- settings: { mode: operation, aspect_ratio, scale, resolution },
2572
+ prompt: prompt || '',
2573
+ settings: cardSettings,
2474
2574
  reference_images: [image_url, mask_image_url, ...(additional_images || [])].filter(Boolean)
2475
2575
  });
2476
2576
 
@@ -2483,8 +2583,8 @@ function registerGenerateTools(server, client, options = {}) {
2483
2583
 
2484
2584
  return uiCompleted({
2485
2585
  tool: 'edit_image', kind: 'image', gen, client, model,
2486
- prompt: prompt || operation,
2487
- settings: { mode: operation, aspect_ratio, scale, resolution },
2586
+ prompt: prompt || '',
2587
+ settings: cardSettings,
2488
2588
  reference_images: [image_url, mask_image_url, ...(additional_images || [])].filter(Boolean),
2489
2589
  urls: result.result?.urls || [],
2490
2590
  credits_used: creditFields(result).credits_used,
@@ -2515,7 +2615,7 @@ function registerGenerateTools(server, client, options = {}) {
2515
2615
  'Edit operation:',
2516
2616
  '"draft_enhance" — render an owned saved draft at full quality in its original project. Pass `resolution` from the draft model capabilities; the server selects its finalization engine. No prompt or provider task ID is needed.',
2517
2617
  '"draft_quote" — check a saved draft’s supported final resolutions, expiry and exact credit cost without starting a render. Use this before draft_enhance.',
2518
- '"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.',
2618
+ '"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.',
2519
2619
  '"reframe" — change aspect ratio (requires `aspect_ratio`; use `grid_position_x`/`grid_position_y` to control where the original sits).',
2520
2620
  '"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.',
2521
2621
  '"remove_watermark" — AI-powered watermark removal.',
@@ -2535,12 +2635,25 @@ function registerGenerateTools(server, client, options = {}) {
2535
2635
  scale: z.number().optional()
2536
2636
  .describe('Upscale factor (e.g. 2, 4). Used with operation="upscale". On "blackforestlabs/flux-video-upscale" the valid range is 1.5-3 (default 2).'),
2537
2637
  resolution: z.string().optional()
2538
- .describe('Target resolution (e.g. "4k", "2k", "1080p"). Used with "upscale" and "reframe". Not used by "blackforestlabs/flux-video-upscale" — use `scale` instead.'),
2638
+ .describe('Target resolution (e.g. "4k", "2k", "1080p"; Bytedance Upscaler also "6k", "8k"). Used with "upscale" and "reframe". Not used by "blackforestlabs/flux-video-upscale" — use `scale` instead.'),
2539
2639
  target_fps: z.number().optional()
2540
- .describe('Target frame rate (e.g. 24, 30, 60). Used with operation="upscale".'),
2640
+ .describe('Target frame rate (e.g. 24, 30, 60). Used with operation="upscale". Bytedance Upscaler: 24-120, default 30; price multiplies per started 30 fps.'),
2541
2641
  enhancement_model: z.string().optional()
2542
2642
  .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.'),
2543
2643
 
2644
+ // Bytedance Upscaler ("bytedance-upscaler/upscale/video") controls. Validated, priced and
2645
+ // duration-capped server-side (kolbo-api services/globalVideoEditing/bytedanceUpscale.js).
2646
+ enhancement_tier: z.enum(['fast', 'standard', 'pro']).optional()
2647
+ .describe('Bytedance Upscaler quality tier for operation="upscale". "fast" = half price, lighter enhancement; "standard" (default); "pro" = large-model restoration at 10× credits, source ≤30s, required for 10/12-bit.'),
2648
+ enhancement_preset: z.enum(['general', 'ugc', 'short_series', 'aigc', 'old_film']).optional()
2649
+ .describe('Bytedance Upscaler content type for operation="upscale": "general" (default), "ugc" (phone/social shorts), "short_series" (short dramas), "aigc" (AI-generated footage), "old_film" (classic film restoration). Same price for all.'),
2650
+ fidelity: z.enum(['high', 'medium']).optional()
2651
+ .describe('Bytedance Upscaler enhancement strength for operation="upscale": "high" (default, mild and source-faithful) or "medium" (stronger, balanced). Same price.'),
2652
+ bit_depth: z.union([z.literal(8), z.literal(10), z.literal(12)]).optional()
2653
+ .describe('Bytedance Upscaler output bit depth for operation="upscale": 8 (default), 10 or 12. 10/12 require enhancement_tier="pro" and are delivered as HEVC.'),
2654
+ scale_ratio: z.number().min(1.1).max(10).optional()
2655
+ .describe('Bytedance Upscaler custom scale (1.1-10) for operation="upscale", used INSTEAD of `resolution`. Output must stay within 4K; billed on the 1080p/2k/4k tier it lands in. For 6K/8K use `resolution`.'),
2656
+
2544
2657
  // ── reframe ────────────────────────────────────────────
2545
2658
  aspect_ratio: z.string().optional()
2546
2659
  .describe(aspectRatioDescribe() + ' Required for operation="reframe".'),
@@ -2612,6 +2725,7 @@ function registerGenerateTools(server, client, options = {}) {
2612
2725
  video_url, operation, model, aspect_ratio, scale, prompt,
2613
2726
  image_url, audio_url, duration, mode,
2614
2727
  target_fps, resolution, enhancement_model,
2728
+ enhancement_tier, enhancement_preset, fidelity, bit_depth, scale_ratio,
2615
2729
  grid_position_x, grid_position_y,
2616
2730
  sound_effect_prompt, background_music_prompt, original_sound, cfg_strength,
2617
2731
  audio_format, segments,
@@ -2645,6 +2759,7 @@ function registerGenerateTools(server, client, options = {}) {
2645
2759
  video_url, operation, model, aspect_ratio, scale, prompt, enhancement_model,
2646
2760
  image_url, audio_url, duration, mode,
2647
2761
  target_fps, resolution,
2762
+ enhancement_tier, enhancement_preset, fidelity, bit_depth, scale_ratio,
2648
2763
  grid_position_x, grid_position_y,
2649
2764
  sound_effect_prompt, background_music_prompt, original_sound, cfg_strength,
2650
2765
  audio_format, segments,
@@ -2662,10 +2777,14 @@ function registerGenerateTools(server, client, options = {}) {
2662
2777
  };
2663
2778
 
2664
2779
  if (operation === 'draft_quote') return { content: [{ type: 'text', text: JSON.stringify(gen, null, 2) }] };
2780
+ const cardSettings = {
2781
+ mode: operation, duration, aspect_ratio, resolution,
2782
+ details: editDetails(operation, { scale, enhancement_model, target_fps, mode, enhancement_tier, enhancement_preset, bit_depth, scale_ratio }),
2783
+ };
2665
2784
  if (returnsImmediately()) return submittedResult({
2666
2785
  tool: 'edit_video', kind: 'video', gen, client, model,
2667
- prompt: prompt || operation,
2668
- settings: { mode: operation, duration, aspect_ratio, resolution },
2786
+ prompt: prompt || '',
2787
+ settings: cardSettings,
2669
2788
  ...editRefs
2670
2789
  });
2671
2790
 
@@ -2678,8 +2797,8 @@ function registerGenerateTools(server, client, options = {}) {
2678
2797
 
2679
2798
  return uiCompleted({
2680
2799
  tool: 'edit_video', kind: 'video', gen, client, model,
2681
- prompt: prompt || operation,
2682
- settings: { mode: operation, duration, aspect_ratio, resolution },
2800
+ prompt: prompt || '',
2801
+ settings: cardSettings,
2683
2802
  ...editRefs,
2684
2803
  urls: result.result?.urls || [],
2685
2804
  duration: result.result?.duration || null,