@kolbo/mcp 1.79.7 → 1.80.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/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/src/apps/bridge.js +2 -0
- package/src/apps/html.js +90 -1
- package/src/apps/index.js +153 -5
- package/src/apps/theme.js +42 -4
- package/src/apps/widgets/generation.js +198 -84
- package/src/apps/widgets/list.js +4 -1
- package/src/apps/widgets/mediaGrid.js +10 -3
- package/src/apps/widgets/transcript.js +4 -1
- package/src/tools/_shared.js +157 -22
- package/src/tools/generate.js +45 -21
- package/src/tools/moodboards.js +1 -0
- package/src/tools/presets.js +37 -36
- package/src/tools/visual_dna.js +12 -4
package/src/tools/_shared.js
CHANGED
|
@@ -372,7 +372,7 @@ const projectIdField = z.string().optional().describe(
|
|
|
372
372
|
// something a caller can rely on — see kolbo-api sdkSessionManager.) Threading
|
|
373
373
|
// the id returned by the FIRST call is the deterministic way to group a batch.
|
|
374
374
|
const sessionIdField = z.string().optional().describe(
|
|
375
|
-
'Existing session to add this generation to, so a related set lands in ONE session instead of a stack of single-item sessions in the Kolbo sidebar. HOW TO USE: omit it on the FIRST call of a
|
|
375
|
+
'Existing session to add this generation to, so a related set lands in ONE session instead of a stack of single-item sessions in the Kolbo sidebar. HOW TO USE: omit it on the FIRST call of a PLAN BUCKET (Cast, Locations, Props, or one Scene NN), read `session_id` off that result, `rename_session` to the plan name, then pass that SAME value on every follow-up in that bucket (another character, shot 2, a retake, "make it darker"). Only omit it again when the plan starts a NEW scene or NEW concept — never per take or per tool call. Image tools and video tools cannot share an id. `list_sessions` also returns ids. When set, `project_id` is ignored — the session\'s own project wins.'
|
|
376
376
|
);
|
|
377
377
|
|
|
378
378
|
// Read-scope variant for list/get tools that can surface a SHARED project's
|
|
@@ -534,6 +534,25 @@ const _dnaChipCache = new Map();
|
|
|
534
534
|
let _dnaChipLoaded = 0;
|
|
535
535
|
const DNA_CHIP_TTL = 5 * 60 * 1000;
|
|
536
536
|
|
|
537
|
+
function dnaThumb(row) {
|
|
538
|
+
if (!row || typeof row !== 'object') return null;
|
|
539
|
+
const first = Array.isArray(row.images) ? row.images[0] : null;
|
|
540
|
+
return row.sheet_url
|
|
541
|
+
|| row.thumbnail_url
|
|
542
|
+
|| row.characterSheet
|
|
543
|
+
|| (typeof first === 'string' ? first : first && first.url)
|
|
544
|
+
|| null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function rememberDna(row, fallbackId) {
|
|
548
|
+
if (!row || typeof row !== 'object') return null;
|
|
549
|
+
const id = String(row.id || row._id || fallbackId || '');
|
|
550
|
+
if (!id) return null;
|
|
551
|
+
const rec = { id, name: row.name || id, thumbnail: dnaThumb(row) };
|
|
552
|
+
_dnaChipCache.set(id, rec);
|
|
553
|
+
return rec;
|
|
554
|
+
}
|
|
555
|
+
|
|
537
556
|
async function resolveVisualDnas(client, ids) {
|
|
538
557
|
const list = Array.isArray(ids) ? ids.filter((id) => typeof id === 'string' && id) : [];
|
|
539
558
|
if (!list.length) return [];
|
|
@@ -543,23 +562,128 @@ async function resolveVisualDnas(client, ids) {
|
|
|
543
562
|
try {
|
|
544
563
|
const res = await client.get('/v1/visual-dna?scope=mine');
|
|
545
564
|
const rows = res?.visual_dnas || res?.data || [];
|
|
546
|
-
for (const row of rows)
|
|
547
|
-
|
|
565
|
+
for (const row of rows) rememberDna(row);
|
|
566
|
+
_dnaChipLoaded = Date.now();
|
|
567
|
+
} catch {
|
|
568
|
+
// Offline / rate-limited — fall through to per-id fetch.
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// scope=mine misses global / shared / teammate DNAs. The generating card
|
|
573
|
+
// then printed "1 Visual DNA" with no face. Fetch the missing ids directly.
|
|
574
|
+
const missing = list.filter((id) => !_dnaChipCache.has(id));
|
|
575
|
+
if (missing.length) {
|
|
576
|
+
await Promise.all(missing.map(async (id) => {
|
|
577
|
+
try {
|
|
578
|
+
const res = await client.get(`/v1/visual-dna/${encodeURIComponent(id)}`);
|
|
579
|
+
rememberDna(res && res.visual_dna ? res.visual_dna : res, id);
|
|
580
|
+
} catch {
|
|
581
|
+
// Leave the bare id — the chip still names it.
|
|
582
|
+
}
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return list.map((id) => _dnaChipCache.get(id) || { id, name: id, thumbnail: null });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const _presetCache = new Map();
|
|
590
|
+
let _presetLoaded = 0;
|
|
591
|
+
const PRESET_TTL = 10 * 60 * 1000;
|
|
592
|
+
|
|
593
|
+
async function resolvePreset(client, presetId) {
|
|
594
|
+
if (!presetId) return null;
|
|
595
|
+
const key = String(presetId);
|
|
596
|
+
const stale = Date.now() - _presetLoaded > PRESET_TTL;
|
|
597
|
+
if (!stale && _presetCache.has(key)) return _presetCache.get(key);
|
|
598
|
+
if (stale || _presetCache.size === 0) {
|
|
599
|
+
try {
|
|
600
|
+
const res = await client.get('/v1/presets');
|
|
601
|
+
for (const row of res?.presets || res?.data || []) {
|
|
602
|
+
const id = row?.id || row?._id || row?.identifier;
|
|
548
603
|
if (!id) continue;
|
|
549
|
-
|
|
604
|
+
_presetCache.set(String(id), {
|
|
550
605
|
id: String(id),
|
|
551
606
|
name: row.name || String(id),
|
|
552
|
-
|
|
553
|
-
thumbnail: row.sheet_url || row.thumbnail_url || (Array.isArray(row.images) ? row.images[0] : null) || null,
|
|
607
|
+
thumbnail: row.thumbnail_url || row.thumbnail || null,
|
|
554
608
|
});
|
|
555
609
|
}
|
|
556
|
-
|
|
610
|
+
_presetLoaded = Date.now();
|
|
557
611
|
} catch {
|
|
558
|
-
// Offline
|
|
612
|
+
// Offline — the chip keeps the word "preset".
|
|
559
613
|
}
|
|
560
614
|
}
|
|
615
|
+
return _presetCache.get(key) || null;
|
|
616
|
+
}
|
|
561
617
|
|
|
562
|
-
|
|
618
|
+
async function decorateSettings(client, settings) {
|
|
619
|
+
const s = { ...(settings || {}) };
|
|
620
|
+
if (!s.preset_id) return s;
|
|
621
|
+
const preset = await resolvePreset(client, s.preset_id);
|
|
622
|
+
if (!preset) return s;
|
|
623
|
+
return {
|
|
624
|
+
...s,
|
|
625
|
+
preset_name: preset.name,
|
|
626
|
+
...(preset.thumbnail ? { preset_thumbnail: preset.thumbnail } : {}),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const _mbChipCache = new Map();
|
|
631
|
+
let _mbChipLoaded = 0;
|
|
632
|
+
|
|
633
|
+
function rememberMoodboard(row, fallbackId) {
|
|
634
|
+
if (!row || typeof row !== 'object') return null;
|
|
635
|
+
const id = String(row.id || row._id || fallbackId || '');
|
|
636
|
+
if (!id) return null;
|
|
637
|
+
const rec = {
|
|
638
|
+
id,
|
|
639
|
+
name: row.name || id,
|
|
640
|
+
thumbnail: row.thumbnail_url || row.thumbnail || row.cover_url || (Array.isArray(row.images) ? row.images[0] : null) || null,
|
|
641
|
+
};
|
|
642
|
+
_mbChipCache.set(id, rec);
|
|
643
|
+
return rec;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function resolveMoodboards(client, ids) {
|
|
647
|
+
const list = Array.isArray(ids) ? ids.filter((id) => typeof id === 'string' && id) : [];
|
|
648
|
+
if (!list.length) return [];
|
|
649
|
+
const stale = Date.now() - _mbChipLoaded > DNA_CHIP_TTL;
|
|
650
|
+
if (stale || list.some((id) => !_mbChipCache.has(id))) {
|
|
651
|
+
try {
|
|
652
|
+
const res = await client.get('/v1/moodboards');
|
|
653
|
+
for (const row of res?.moodboards || res?.data || []) rememberMoodboard(row);
|
|
654
|
+
_mbChipLoaded = Date.now();
|
|
655
|
+
} catch { /* fall through to per-id */ }
|
|
656
|
+
}
|
|
657
|
+
const missing = list.filter((id) => !_mbChipCache.has(id));
|
|
658
|
+
if (missing.length) {
|
|
659
|
+
await Promise.all(missing.map(async (id) => {
|
|
660
|
+
try {
|
|
661
|
+
const res = await client.get(`/v1/moodboards/${encodeURIComponent(id)}`);
|
|
662
|
+
rememberMoodboard(res && res.moodboard ? res.moodboard : res, id);
|
|
663
|
+
} catch { /* bare id */ }
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
return list.map((id) => _mbChipCache.get(id) || { id, name: id, thumbnail: null });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function mediaRefs(p) {
|
|
670
|
+
const images = Array.isArray(p.reference_images)
|
|
671
|
+
? p.reference_images.filter(Boolean)
|
|
672
|
+
: (p.reference_image ? [p.reference_image] : []);
|
|
673
|
+
return {
|
|
674
|
+
reference_images: images,
|
|
675
|
+
reference_image: p.reference_image || images[0],
|
|
676
|
+
...(Array.isArray(p.reference_videos) && p.reference_videos.length
|
|
677
|
+
? { reference_videos: p.reference_videos.filter(Boolean) } : {}),
|
|
678
|
+
...(Array.isArray(p.reference_audio) && p.reference_audio.length
|
|
679
|
+
? { reference_audio: p.reference_audio.filter(Boolean) } : {}),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function moodboardIds(settings) {
|
|
684
|
+
const s = settings || {};
|
|
685
|
+
if (Array.isArray(s.moodboard_ids) && s.moodboard_ids.length) return s.moodboard_ids;
|
|
686
|
+
return s.moodboard_id ? [s.moodboard_id] : [];
|
|
563
687
|
}
|
|
564
688
|
|
|
565
689
|
/**
|
|
@@ -577,6 +701,7 @@ async function resolveVisualDnas(client, ids) {
|
|
|
577
701
|
async function uiGenerating(p) {
|
|
578
702
|
// No ETAs anywhere — just a spinner until the poll flips to completed.
|
|
579
703
|
const chip = await modelChipFields(p.client, p.model);
|
|
704
|
+
const settings = await decorateSettings(p.client, p.settings || {});
|
|
580
705
|
const structured = {
|
|
581
706
|
phase: 'generating',
|
|
582
707
|
widget: 'generation',
|
|
@@ -592,14 +717,12 @@ async function uiGenerating(p) {
|
|
|
592
717
|
...(p.voice ? { voice_name: p.voice.name, voice_thumbnail: p.voice.thumbnail } : {}),
|
|
593
718
|
prompt: p.prompt,
|
|
594
719
|
count: p.count || 1,
|
|
595
|
-
settings
|
|
596
|
-
visual_dnas: await resolveVisualDnas(p.client,
|
|
720
|
+
settings,
|
|
721
|
+
visual_dnas: await resolveVisualDnas(p.client, settings.visual_dna_ids),
|
|
722
|
+
moodboards: await resolveMoodboards(p.client, moodboardIds(settings)),
|
|
597
723
|
// `reference_image` is retained for older widget builds. New widgets render
|
|
598
724
|
// every browser-loadable image supplied to the generation.
|
|
599
|
-
|
|
600
|
-
? p.reference_images.filter(Boolean)
|
|
601
|
-
: (p.reference_image ? [p.reference_image] : []),
|
|
602
|
-
reference_image: p.reference_image || p.reference_images?.find(Boolean),
|
|
725
|
+
...mediaRefs(p),
|
|
603
726
|
open_url: buildOpenUrl(p.tool, p.gen),
|
|
604
727
|
};
|
|
605
728
|
// Batch mode (prompts[] fan-out): ONE widget tracks every id in the set.
|
|
@@ -638,8 +761,22 @@ async function uiGenerating(p) {
|
|
|
638
761
|
*
|
|
639
762
|
* The TEXT is unchanged, so text-only hosts see precisely what they saw before.
|
|
640
763
|
*/
|
|
764
|
+
function preferOwnedUrls(urls) {
|
|
765
|
+
const list = Array.isArray(urls) ? urls.filter((item) => typeof item === 'string' && item) : [];
|
|
766
|
+
const ours = list.filter((item) => {
|
|
767
|
+
try {
|
|
768
|
+
const host = new URL(item).hostname;
|
|
769
|
+
return /(?:^|\.)kolbo\.ai$/.test(host) || /digitaloceanspaces\.com$/.test(host);
|
|
770
|
+
} catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
return ours.length ? ours : (list.length ? list : urls);
|
|
775
|
+
}
|
|
776
|
+
|
|
641
777
|
async function uiCompleted(p, textPayload, extraContent) {
|
|
642
778
|
const chip = await modelChipFields(p.client, p.model);
|
|
779
|
+
const settings = p.settings ? await decorateSettings(p.client, p.settings) : undefined;
|
|
643
780
|
const structured = {
|
|
644
781
|
phase: 'completed',
|
|
645
782
|
widget: 'generation',
|
|
@@ -652,13 +789,11 @@ async function uiCompleted(p, textPayload, extraContent) {
|
|
|
652
789
|
// an incoming status payload over its own state, so an empty-but-present
|
|
653
790
|
// `settings` wiped the resolution / aspect / DNA chips off the finished
|
|
654
791
|
// card. Every reader already does `sc.settings || {}`.
|
|
655
|
-
...(
|
|
656
|
-
visual_dnas: await resolveVisualDnas(p.client, (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
reference_image: p.reference_image || p.reference_images?.find(Boolean),
|
|
661
|
-
urls: p.urls,
|
|
792
|
+
...(settings ? { settings } : {}),
|
|
793
|
+
visual_dnas: await resolveVisualDnas(p.client, (settings || {}).visual_dna_ids),
|
|
794
|
+
moodboards: await resolveMoodboards(p.client, moodboardIds(settings)),
|
|
795
|
+
...mediaRefs(p),
|
|
796
|
+
urls: preferOwnedUrls(p.urls),
|
|
662
797
|
thumbnail_url: p.thumbnail_url,
|
|
663
798
|
title: p.title,
|
|
664
799
|
duration: p.duration,
|
package/src/tools/generate.js
CHANGED
|
@@ -7,7 +7,8 @@ const { z } = require('zod');
|
|
|
7
7
|
const FormData = require('form-data');
|
|
8
8
|
const { pollUntilDone, waitWindowMs } = require('../polling');
|
|
9
9
|
const { resolveToBuffer, pollOrTimedOut, creditFields, projectIdField, sessionIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, uiCompleted, appsEnabled } = require('./_shared');
|
|
10
|
-
const {
|
|
10
|
+
const { ownedUrl } = require('./owned-url');
|
|
11
|
+
const { UI, uiResult, canonicalModelId, modelInfo, voiceInfo, resolveCatalogAspectRatio } = require('../apps');
|
|
11
12
|
|
|
12
13
|
// ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
|
|
13
14
|
// Kolbo's "Cinema mode": eight independent photographic dimensions, each an OPTIONAL
|
|
@@ -114,11 +115,18 @@ async function pollBatch(client, batch, { interval, timeout }, toolName) {
|
|
|
114
115
|
function mediaKind(url) {
|
|
115
116
|
const u = String(url || '').split('?')[0].toLowerCase();
|
|
116
117
|
if (/\.(mp4|mov|webm|mkv)$/.test(u)) return 'video';
|
|
118
|
+
if (/video-elements-results|generated-videos|\/videos?\//i.test(u)) return 'video';
|
|
117
119
|
if (/\.(mp3|wav|m4a|aac|ogg|flac)$/.test(u)) return 'audio';
|
|
118
120
|
if (/\.(glb|gltf|fbx|obj|usdz)$/.test(u)) return '3d';
|
|
119
121
|
return 'image';
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function preferOwned(urls) {
|
|
125
|
+
const list = (urls || []).filter((item) => typeof item === 'string' && item);
|
|
126
|
+
const ours = list.filter(ownedUrl);
|
|
127
|
+
return ours.length ? ours : list;
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
const isUrlSource = (source) => typeof source === 'string' && /^https?:\/\//i.test(source);
|
|
123
131
|
|
|
124
132
|
// Which multipart kind a LOCAL file should upload as. mediaKind answers for the
|
|
@@ -155,6 +163,12 @@ const refSettings = (a = {}) => ({
|
|
|
155
163
|
cinematic: a.cinematic ? true : undefined,
|
|
156
164
|
});
|
|
157
165
|
|
|
166
|
+
const aspectRatioDescribe = (fallback) =>
|
|
167
|
+
'Aspect ratio (e.g. "16:9", "9:16", "1:1"). MCP always snaps unsupported values '
|
|
168
|
+
+ 'to the closest ratio this model accepts — the card and the request both use '
|
|
169
|
+
+ 'the snapped value. Prefer list_models `supported_aspect_ratios`.'
|
|
170
|
+
+ (fallback ? ` Default: "${fallback}".` : '');
|
|
171
|
+
|
|
158
172
|
const imageSettings = (a = {}) => ({
|
|
159
173
|
resolution: a.resolution,
|
|
160
174
|
aspect_ratio: a.aspect_ratio,
|
|
@@ -206,12 +220,12 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
206
220
|
// ─── generate_image ────────────────────────────────────────
|
|
207
221
|
server.tool(
|
|
208
222
|
'generate_image',
|
|
209
|
-
'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), Kolbo image presets, reference images (for composition guidance), batch generation (num_images for variations of ONE prompt, `prompts` for SEVERAL different prompts in one combined widget), and web-search grounding. PRESET CONTRACT:
|
|
223
|
+
'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), Kolbo image presets, reference images (for composition guidance), batch generation (num_images for variations of ONE prompt, `prompts` for SEVERAL different prompts in one combined widget), and web-search grounding. PRESET CONTRACT: resolve with list_presets type="image" AND search="<name>" (headless, bible, character sheet, or a user preset), then pass the exact id as `preset_id`. Custom instructions live on the preset — prefer this over generate_character_sheet. Never list the whole catalog. When the user wants multiple distinct images, pass all their prompts in `prompts` in ONE call — never a series of separate generate_image calls. For EDITING an existing image, use generate_image_edit instead. For a coordinated multi-scene set planned by AI from a single brief (storyboard, ad campaign), use generate_creative_director. Returns the final image URL(s) when complete.',
|
|
210
224
|
{
|
|
211
225
|
prompt: z.string().optional().describe('Text description of the image to generate. Required unless `prompts` is provided.'),
|
|
212
226
|
prompts: promptsField('images'),
|
|
213
227
|
model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "nano-banana-2" (versatile, text rendering, multilingual) or "gpt-image-2" (photoreal, infographics). Call list_models type="text_to_img" to see all options and pick per the user\'s intent.'),
|
|
214
|
-
aspect_ratio: z.string().optional().describe(
|
|
228
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('1:1')),
|
|
215
229
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
216
230
|
num_images: z.number().optional().describe('Number of images to generate in one call. Default: 1. Note: some models (Midjourney etc.) have a fixed `images_per_request` and ignore this — check list_models.'),
|
|
217
231
|
reference_images: z.array(z.string()).optional().describe('STYLE/COMPOSITION inspiration only — does NOT embed reference pixels. Array of image URLs used to guide the look-and-feel of a brand-new generation. The model interprets the references and regenerates approximations conditioned on them. It will NOT copy pixels from these images into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** To embed a specific logo, icon, watermark, or asset pixel-accurately, use generate_image_edit with the asset in source_images. To EDIT an existing image, also use generate_image_edit.'),
|
|
@@ -229,6 +243,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
229
243
|
async ({ prompt, prompts, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id }) => {
|
|
230
244
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
231
245
|
model = await canonicalModelId(client, model, 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
246
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'text_to_img');
|
|
232
247
|
const shared = {
|
|
233
248
|
model, aspect_ratio, enhance_prompt,
|
|
234
249
|
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
@@ -291,7 +306,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
291
306
|
model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Many text-to-image ids double as editors: the server auto-routes a base id to its editing variant when source_images is present (e.g. "gpt-image-2" → gpt-image-2/edit, "nano-banana-2" → nano-banana-2-image-editing) — passing the bare id is fine, no need to hunt for the "/edit" suffix yourself. BUT this only works for models that actually have a registered edit variant. For prompt-driven photoreal photo edits (object removal, keep-this-person/remove-the-rest, crowd cleanup, inpainting) the ONLY auto-pick defaults are "nano-banana-2" or "gpt-image-2" (use GPT Image 2 when the image needs readable text). Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style models; use them only if the user names Flux. If unsure, confirm the model appears in `list_models type="image_editing"` and choose by the strengths summary — Flux edit variants are named-only.'),
|
|
292
307
|
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** Three modes the model auto-detects from input shape: (1) Single image → edit/transform that image. (2) Multiple images, one base + others → composite the others into the base. (3) Multiple images with no clear base → generate a new scene that pixel-accurately embeds the supplied images at positions described in the prompt. Mode 3 is the canonical pattern for thumbnails / branded compositions where exact-pixel logo + face fidelity matter. Refer to source images in the prompt by ordinal position ("FIRST source image", "SECOND source image") or use @image1/@image2 tags. Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
293
308
|
reference_images: z.array(z.string()).optional().describe('STYLE/COMPOSITION inspiration, alongside `source_images` on the same call — does NOT embed reference pixels. Use when the edit should follow a look sampled from other images ("re-light this shot like these references"). The pixels that must survive the edit go in `source_images`; these only steer the look. **Cap: `source_images` + `reference_images` together must not exceed `max_reference_images` from list_models for the chosen model.**'),
|
|
294
|
-
aspect_ratio: z.string().optional().describe(
|
|
309
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('1:1')),
|
|
295
310
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
296
311
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
297
312
|
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs for character / style / product consistency. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.** How DNA works: the server fetches the DNA\'s reference images AND always injects its `description` field into the prompt as plaintext (by design — independent of enhance_prompt). For pixel-accurate face anchoring of a specific person on this tool, the PREFERRED pattern is to pass the face photo directly via source_images and OMIT visual_dna_ids — that way the face pixels anchor the output and no description text competes. Do NOT pass visual_dna_ids if source_images already contains the same person\'s face (face averaging). visual_dna_ids is best here for style / product DNAs.'),
|
|
@@ -308,6 +323,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
308
323
|
async ({ prompt, prompts, model, source_images, reference_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id }) => {
|
|
309
324
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
310
325
|
model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
326
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'image_editing');
|
|
311
327
|
const shared = {
|
|
312
328
|
model, source_images, reference_images, aspect_ratio, enhance_prompt,
|
|
313
329
|
visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
@@ -326,7 +342,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
326
342
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
327
343
|
failed_submissions: batch.failed,
|
|
328
344
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
329
|
-
reference_images: source_images
|
|
345
|
+
reference_images: [...(source_images || []), ...(reference_images || [])]
|
|
330
346
|
});
|
|
331
347
|
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 150000 }, 'generate_image_edit');
|
|
332
348
|
}
|
|
@@ -337,7 +353,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
337
353
|
tool: 'generate_image_edit', kind: 'image', gen, client, model, prompt,
|
|
338
354
|
count: num_images,
|
|
339
355
|
settings,
|
|
340
|
-
reference_images: source_images
|
|
356
|
+
reference_images: [...(source_images || []), ...(reference_images || [])]
|
|
341
357
|
});
|
|
342
358
|
|
|
343
359
|
// Multi-source compositing or DNA-anchored edits routinely exceed 120s
|
|
@@ -355,7 +371,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
355
371
|
return uiCompleted({
|
|
356
372
|
tool: 'generate_image_edit', kind: 'image', gen, client, model, prompt,
|
|
357
373
|
count: num_images, settings: imageSettings(shared),
|
|
358
|
-
reference_images: source_images,
|
|
374
|
+
reference_images: [...(source_images || []), ...(reference_images || [])],
|
|
359
375
|
urls: result.result.urls,
|
|
360
376
|
credits_used: creditFields(result).credits_used,
|
|
361
377
|
}, JSON.stringify({
|
|
@@ -377,7 +393,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
377
393
|
prompt: z.string().describe('Creative brief or concept describing the full set of scenes to generate'),
|
|
378
394
|
scene_count: z.number().optional().describe('Number of scenes/images to generate, 1–8. Default: 4. Use this — NOT num_images — to control how many outputs are created.'),
|
|
379
395
|
model: z.string().optional().describe('Model identifier applied to every scene. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
|
|
380
|
-
aspect_ratio: z.string().optional().describe(
|
|
396
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('1:1')),
|
|
381
397
|
workflow_type: z.string().optional().describe('"image" (default) or "video"'),
|
|
382
398
|
duration: z.number().optional().describe('Duration in seconds per scene (video mode only). Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. E.g., 5 or 10.'),
|
|
383
399
|
enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: false — only pass true if the user explicitly asks to enhance/improve the prompts.'),
|
|
@@ -390,6 +406,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
390
406
|
},
|
|
391
407
|
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt = false, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
|
|
392
408
|
model = await canonicalModelId(client, model, workflow_type === 'video' ? 'text_to_video' : 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
409
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, workflow_type === 'video' ? 'text_to_video' : 'text_to_img');
|
|
393
410
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
394
411
|
prompt, scene_count, model, aspect_ratio, workflow_type, duration,
|
|
395
412
|
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
|
|
@@ -546,7 +563,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
546
563
|
prompt: z.string().optional().describe('Text description of the video to generate. Required unless `prompts` is provided.'),
|
|
547
564
|
prompts: promptsField('videos'),
|
|
548
565
|
model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). If the user already named a model/family (this turn or earlier), pass that name — do not substitute a cheaper default. Only when no model was named: "seedance-2" (versatile) or "veo3" (cinematic + native audio) are reasonable auto-picks; Kling is strongest for motion (list_models type="text_to_video" for exact ids). Call list_models for supported_durations / supported_aspect_ratios.'),
|
|
549
|
-
aspect_ratio: z.string().optional().describe(
|
|
566
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('16:9')),
|
|
550
567
|
duration: z.number().optional().describe('Duration in seconds. Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration` (whichever the model exposes). Default: 5'),
|
|
551
568
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
552
569
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject). **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
|
|
@@ -561,6 +578,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
561
578
|
async ({ prompt, prompts, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, visual_dna_ids, sound_enabled, skip_color_palette, project_id, session_id }) => {
|
|
562
579
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
563
580
|
model = await canonicalModelId(client, model, 'text_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
581
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'text_to_video');
|
|
564
582
|
const shared = {
|
|
565
583
|
model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, visual_dna_ids, sound_enabled, skip_color_palette, project_id, session_id
|
|
566
584
|
};
|
|
@@ -570,7 +588,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
570
588
|
const batch = await submitBatch(prompts, (p) => client.post('/v1/generate/video', { ...shared, prompt: p }));
|
|
571
589
|
if (ui()) return uiGenerating({
|
|
572
590
|
tool: 'generate_video', kind: 'video', gen: batch.ok[0].gen, client, model,
|
|
573
|
-
count: batch.ids.length, settings: videoSettings(
|
|
591
|
+
count: batch.ids.length, settings: videoSettings(shared),
|
|
574
592
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
575
593
|
failed_submissions: batch.failed,
|
|
576
594
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
@@ -583,7 +601,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
583
601
|
|
|
584
602
|
if (ui()) return uiGenerating({
|
|
585
603
|
tool: 'generate_video', kind: 'video', gen, client, model, prompt,
|
|
586
|
-
settings: videoSettings(
|
|
604
|
+
settings: videoSettings(shared),
|
|
587
605
|
reference_images
|
|
588
606
|
});
|
|
589
607
|
|
|
@@ -599,7 +617,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
599
617
|
|
|
600
618
|
return uiCompleted({
|
|
601
619
|
tool: 'generate_video', kind: 'video', gen, client, model, prompt,
|
|
602
|
-
settings: videoSettings(
|
|
620
|
+
settings: videoSettings(shared),
|
|
603
621
|
reference_images,
|
|
604
622
|
urls: result.result.urls,
|
|
605
623
|
thumbnail_url: result.result.thumbnail_url,
|
|
@@ -632,7 +650,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
632
650
|
`BATCH MODE — several DIFFERENT stills (2–${MAX_BATCH_PROMPTS}) animated concurrently in ONE call and rendered together in ONE combined widget. Unlike the \`prompts\` array on generate_image / generate_video, each entry pairs its OWN \`image_url\` with its OWN motion \`prompt\` — the image is what varies, and that is the point. **Hard cap: ${MAX_BATCH_PROMPTS} items per call — more than that is REJECTED with an error (never silently truncated), so split a longer sequence across several calls of at most ${MAX_BATCH_PROMPTS}.** Whenever the user wants several stills animated (a shot sequence, a storyboard, an animatic), ALWAYS pass them all here instead of making several separate calls — separate calls clutter the chat with stacked widgets. All items share the same model / duration / resolution / aspect_ratio / sound_enabled / project_id / session_id. When set, \`image_url\` and \`prompt\` are ignored.`
|
|
633
651
|
),
|
|
634
652
|
model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). If the user already named a model/family (this turn or earlier), pass that name — a text-to-video id remaps to the family\'s image-to-video sibling. Do not substitute a cheaper default (named Grok Imagine → not Seedance). Only when no model was named: "seedance-2" or "veo3" are reasonable auto-picks; Kling is strongest for motion (list_models type="img_to_video" for exact ids).'),
|
|
635
|
-
aspect_ratio: z.string().optional().describe(
|
|
653
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('16:9')),
|
|
636
654
|
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
637
655
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
638
656
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
|
|
@@ -645,6 +663,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
645
663
|
async ({ image_url, prompt, items, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id, session_id }) => {
|
|
646
664
|
if (!(items && items.length) && !(image_url && prompt)) throw new Error('Provide image_url + prompt, or items');
|
|
647
665
|
model = await canonicalModelId(client, model, 'img_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
666
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'img_to_video');
|
|
648
667
|
const shared = {
|
|
649
668
|
model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id, session_id
|
|
650
669
|
};
|
|
@@ -741,7 +760,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
741
760
|
|
|
742
761
|
if (ui()) return uiGenerating({
|
|
743
762
|
tool: 'generate_music', kind: 'audio', gen, client, model: model || 'Suno', prompt,
|
|
744
|
-
settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
|
|
763
|
+
settings: { mode: instrumental ? 'instrumental' : (style || undefined), preset_id },
|
|
745
764
|
});
|
|
746
765
|
|
|
747
766
|
const poll = await pollOrTimedOut(client, gen.generation_id, {
|
|
@@ -753,7 +772,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
753
772
|
|
|
754
773
|
return uiCompleted({
|
|
755
774
|
tool: 'generate_music', kind: 'audio', gen, client, model: model || 'Suno', prompt,
|
|
756
|
-
settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
|
|
775
|
+
settings: { mode: instrumental ? 'instrumental' : (style || undefined), preset_id },
|
|
757
776
|
urls: result.result.urls,
|
|
758
777
|
title: result.result.title,
|
|
759
778
|
duration: result.result.duration,
|
|
@@ -1060,7 +1079,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1060
1079
|
// "No output received / Failed" over a generation that had completed
|
|
1061
1080
|
// and billed. Every card rendered before v1.74 keeps that old iframe JS
|
|
1062
1081
|
// forever, so the flat fields have to live in structuredContent too.
|
|
1063
|
-
const urls = Array.isArray(res.urls) ? res.urls : [];
|
|
1082
|
+
const urls = preferOwned(Array.isArray(res.urls) ? res.urls : []);
|
|
1064
1083
|
const done = single.state === 'completed' && urls.length > 0;
|
|
1065
1084
|
return uiCompleted({
|
|
1066
1085
|
tool: 'get_generation_status', kind: done ? mediaKind(urls[0]) : 'status', client,
|
|
@@ -1195,7 +1214,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1195
1214
|
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).**'),
|
|
1196
1215
|
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.**'),
|
|
1197
1216
|
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'),
|
|
1198
|
-
aspect_ratio: z.string().optional().describe('
|
|
1217
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('16:9')),
|
|
1199
1218
|
motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
|
|
1200
1219
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
|
|
1201
1220
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
@@ -1214,6 +1233,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1214
1233
|
},
|
|
1215
1234
|
async ({ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, keyframes, multi_shots, multi_shot_count, session_name, project_id, session_id }) => {
|
|
1216
1235
|
model = await canonicalModelId(client, model, 'elements'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1236
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'elements');
|
|
1217
1237
|
if (!prompt) throw new Error('prompt is required');
|
|
1218
1238
|
|
|
1219
1239
|
// Elements is the one tool that takes all three modalities, and either a
|
|
@@ -1377,7 +1397,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1377
1397
|
prompt: z.string().optional().describe('Optional description of the desired motion between the two frames (e.g. "smooth camera dolly in")'),
|
|
1378
1398
|
model: z.string().optional().describe('Model identifier. Use list_models type="firstlastgenerations" to see options. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
|
|
1379
1399
|
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
1380
|
-
aspect_ratio: z.string().optional().describe('
|
|
1400
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe('16:9') + ' Auto-detected from the first frame if omitted.'),
|
|
1381
1401
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
1382
1402
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false`, DNA is silently ignored.**'),
|
|
1383
1403
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
@@ -1387,6 +1407,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1387
1407
|
},
|
|
1388
1408
|
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, project_id, session_id }) => {
|
|
1389
1409
|
model = await canonicalModelId(client, model, 'firstlastgenerations'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1410
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'firstlastgenerations');
|
|
1390
1411
|
// One frame per position, whichever arg carried it. The two arg pairs were
|
|
1391
1412
|
// treated as two exclusive MODES, so a URL handed to first_frame/last_frame
|
|
1392
1413
|
// (which their own descriptions invite) took the multipart path and got
|
|
@@ -1586,7 +1607,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1586
1607
|
source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. **Source duration must fall within `min_video_duration`-`max_video_duration` from list_models for the chosen model** — videos outside that range are rejected (or silently truncated by some upstream providers). For models that use reference_videos as their primary input (e.g. WAN 2.6 reference-to-video), pass the first reference video here and also include it in reference_videos.'),
|
|
1587
1608
|
prompt: z.string().optional().describe('Text description of the desired restyle / transformation. Required by most video-to-video models; omit for prompt-less models (VEED Subtitles, Act Two, Wan Animate, Kling Motion Control).'),
|
|
1588
1609
|
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check max_images / max_videos / max_elements / max_video_duration per model. Pick a SPECIFIC model — do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
|
|
1589
|
-
aspect_ratio: z.string().optional().describe(
|
|
1610
|
+
aspect_ratio: z.string().optional().describe(aspectRatioDescribe() + ' Default: matches source.'),
|
|
1590
1611
|
duration: z.number().optional().describe('Output duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: matches source'),
|
|
1591
1612
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
1592
1613
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false`, DNA is silently ignored.**'),
|
|
@@ -1626,6 +1647,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1626
1647
|
},
|
|
1627
1648
|
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 }) => {
|
|
1628
1649
|
model = await canonicalModelId(client, model, 'video_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1650
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio, 'video_to_video');
|
|
1629
1651
|
if (!source_video) throw new Error('source_video is required');
|
|
1630
1652
|
|
|
1631
1653
|
const isUrl = /^https?:\/\//i.test(source_video);
|
|
@@ -1911,7 +1933,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1911
1933
|
|
|
1912
1934
|
// ── reframe ────────────────────────────────────────────
|
|
1913
1935
|
aspect_ratio: z.string().optional()
|
|
1914
|
-
.describe(
|
|
1936
|
+
.describe(aspectRatioDescribe() + ' Required for operation="reframe". Ignored by "zoom_out" — size that expansion with `zoom_out_percentage` or the `expand_*` pixel args.'),
|
|
1915
1937
|
|
|
1916
1938
|
// ── zoom_out (outpaint / expand) ───────────────────────
|
|
1917
1939
|
zoom_out_percentage: z.number().optional()
|
|
@@ -1968,6 +1990,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1968
1990
|
// removebg / …), each operation with its own model family — there is no single
|
|
1969
1991
|
// catalog type to disambiguate against.
|
|
1970
1992
|
model = await canonicalModelId(client, model);
|
|
1993
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio);
|
|
1971
1994
|
|
|
1972
1995
|
// Basic validation
|
|
1973
1996
|
if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe');
|
|
@@ -2061,7 +2084,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
2061
2084
|
|
|
2062
2085
|
// ── reframe ────────────────────────────────────────────
|
|
2063
2086
|
aspect_ratio: z.string().optional()
|
|
2064
|
-
.describe(
|
|
2087
|
+
.describe(aspectRatioDescribe() + ' Required for operation="reframe".'),
|
|
2065
2088
|
grid_position_x: z.number().optional()
|
|
2066
2089
|
.describe('Horizontal position (0.0–1.0) of the original content within the reframed canvas. Used with "reframe". Default: 0.5 (center).'),
|
|
2067
2090
|
grid_position_y: z.number().optional()
|
|
@@ -2134,6 +2157,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
2134
2157
|
// removebg / …), each operation with its own model family — there is no single
|
|
2135
2158
|
// catalog type to disambiguate against.
|
|
2136
2159
|
model = await canonicalModelId(client, model);
|
|
2160
|
+
aspect_ratio = await resolveCatalogAspectRatio(client, model, aspect_ratio);
|
|
2137
2161
|
|
|
2138
2162
|
// Validation
|
|
2139
2163
|
if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit');
|
package/src/tools/moodboards.js
CHANGED
|
@@ -42,6 +42,7 @@ function registerMoodboardTools(server, client, options = {}) {
|
|
|
42
42
|
// API returns thumbnail_url + images[] (sdk listMoodboards) — both
|
|
43
43
|
// previous keys were wrong, so the fallback never fired either.
|
|
44
44
|
thumbnail: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined),
|
|
45
|
+
url: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined),
|
|
45
46
|
media_type: 'image',
|
|
46
47
|
use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.'
|
|
47
48
|
})),
|