@kolbo/mcp 1.6.5 → 1.6.8
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/src/client.js +4 -0
- package/src/tools/app_builder.js +1 -2
- package/src/tools/generate.js +39 -27
- package/src/tools/models.js +57 -1
package/package.json
CHANGED
package/src/client.js
CHANGED
package/src/tools/app_builder.js
CHANGED
|
@@ -119,8 +119,7 @@ function registerAppBuilderTools(server, client) {
|
|
|
119
119
|
edit_prompt: z.string().describe('Natural language instruction describing the change to make.')
|
|
120
120
|
},
|
|
121
121
|
async ({ session_id, generation_id, edit_prompt }) => {
|
|
122
|
-
await client.
|
|
123
|
-
'PUT',
|
|
122
|
+
await client.put(
|
|
124
123
|
`/app-builder/generation/${encodeURIComponent(session_id)}/${encodeURIComponent(generation_id)}`,
|
|
125
124
|
{ editPrompt: edit_prompt }
|
|
126
125
|
);
|
package/src/tools/generate.js
CHANGED
|
@@ -19,11 +19,11 @@ function registerGenerateTools(server, client) {
|
|
|
19
19
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
20
20
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
21
21
|
num_images: z.number().optional().describe('Number of images to generate in one call. Default: 1'),
|
|
22
|
-
reference_images: z.array(z.string()).optional().describe('Array of image URLs used
|
|
23
|
-
visual_dna_ids: z.array(z.string()).optional().describe('
|
|
22
|
+
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. 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.'),
|
|
23
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs (from create_visual_dna / list_visual_dnas) for character / style / product / scene consistency. How DNA works: the server fetches the DNA\'s reference images AND always injects its `description` field into the prompt as plaintext (this is by design — the description carries the identity signal, independent of enhance_prompt). Practical implication: do NOT also write physical descriptors of the same subject in your own prompt — they will compete with the DNA description text. For pixel-accurate face anchoring of a specific person, prefer passing the DNA\'s reference image directly via source_images on generate_image_edit and OMIT visual_dna_ids. visual_dna_ids is best for style / scene / product DNAs and for soft consistency across a set.'),
|
|
24
24
|
moodboard_id: z.string().optional().describe('Moodboard ID (from list_moodboards / get_moodboard) whose master_prompt and style_guide should be applied to this generation.'),
|
|
25
25
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false'),
|
|
26
|
-
resolution: z.string().optional().describe('Image resolution tier: "1K" (~1024px), "2K" (Full HD), "3K" (QHD), or "4K" (UHD). Model-dependent — call list_models and read supported_resolutions on the chosen model. Read
|
|
26
|
+
resolution: z.string().optional().describe('Image resolution tier: "1K" (~1024px), "2K" (Full HD), "3K" (QHD), or "4K" (UHD). Model-dependent — call list_models and read supported_resolutions on the chosen model. Read resolution_multipliers on the same model to predict credit cost. Omit to use the model default.'),
|
|
27
27
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="image" to apply a saved style preset to this generation.')
|
|
28
28
|
},
|
|
29
29
|
async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id }) => {
|
|
@@ -43,7 +43,8 @@ function registerGenerateTools(server, client) {
|
|
|
43
43
|
text: JSON.stringify({
|
|
44
44
|
urls: result.result.urls,
|
|
45
45
|
model: result.result.model,
|
|
46
|
-
prompt_used: result.result.prompt_used
|
|
46
|
+
prompt_used: result.result.prompt_used,
|
|
47
|
+
_followup_hint: 'If the user asks to edit/change/modify this image next, pass urls[0] to generate_image_edit (free-form edits) or edit_image (upscale/reframe/removebg/enhance_skin/magic_edit). Do NOT call generate_image again.'
|
|
47
48
|
}, null, 2)
|
|
48
49
|
}]
|
|
49
50
|
};
|
|
@@ -57,11 +58,11 @@ function registerGenerateTools(server, client) {
|
|
|
57
58
|
{
|
|
58
59
|
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
59
60
|
model: z.string().optional().describe('Model identifier. Use list_models type="image_editing" to see options. Omit for Smart Select.'),
|
|
60
|
-
source_images: z.array(z.string()).describe('Array of source image URLs
|
|
61
|
+
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. 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"). Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
61
62
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
62
63
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
63
64
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
64
|
-
visual_dna_ids: z.array(z.string()).optional().describe('
|
|
65
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Visual DNA profile IDs for character / style / product consistency. 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.'),
|
|
65
66
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
66
67
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
67
68
|
resolution: z.string().optional().describe('Image resolution tier: "1K" / "2K" / "3K" / "4K". Model-dependent — call list_models and read supported_resolutions. Default: "1K" for most edit models.')
|
|
@@ -72,9 +73,13 @@ function registerGenerateTools(server, client) {
|
|
|
72
73
|
visual_dna_ids, moodboard_id, enable_web_search, resolution
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
// Multi-source compositing or DNA-anchored edits routinely exceed 120s
|
|
77
|
+
// server-side. Extend the polling window in those cases to avoid forcing
|
|
78
|
+
// every call into the timeout-and-recover path via get_generation_status.
|
|
79
|
+
const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
|
|
75
80
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
76
81
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
77
|
-
timeout: 120000
|
|
82
|
+
timeout: heavy ? 240000 : 120000
|
|
78
83
|
});
|
|
79
84
|
|
|
80
85
|
return {
|
|
@@ -83,7 +88,8 @@ function registerGenerateTools(server, client) {
|
|
|
83
88
|
text: JSON.stringify({
|
|
84
89
|
urls: result.result.urls,
|
|
85
90
|
model: result.result.model,
|
|
86
|
-
prompt_used: result.result.prompt_used
|
|
91
|
+
prompt_used: result.result.prompt_used,
|
|
92
|
+
_followup_hint: 'If the user asks for another edit on this output, pass urls[0] back into generate_image_edit as source_images. For targeted ops (upscale/reframe/removebg/enhance_skin) use edit_image instead. Do NOT call generate_image from scratch.'
|
|
87
93
|
}, null, 2)
|
|
88
94
|
}]
|
|
89
95
|
};
|
|
@@ -135,7 +141,8 @@ function registerGenerateTools(server, client) {
|
|
|
135
141
|
text: JSON.stringify({
|
|
136
142
|
scenes,
|
|
137
143
|
total_scenes: result.scenes?.length || 0,
|
|
138
|
-
completed_scenes: scenes.length
|
|
144
|
+
completed_scenes: scenes.length,
|
|
145
|
+
_followup_hint: 'Each scene is a separate asset. If the user asks to edit one scene, find that scene by scene_number/title and pass its image_urls[0] (or video_urls[0]) to generate_image_edit / edit_image / edit_video / generate_video_from_video. Do NOT re-run generate_creative_director unless the user explicitly wants a brand-new set.'
|
|
139
146
|
}, null, 2)
|
|
140
147
|
}]
|
|
141
148
|
};
|
|
@@ -143,9 +150,13 @@ function registerGenerateTools(server, client) {
|
|
|
143
150
|
);
|
|
144
151
|
|
|
145
152
|
// ─── generate_video ────────────────────────────────────────
|
|
153
|
+
// NOTE: text-to-video does NOT support Visual DNA — the textToVideoGeneration
|
|
154
|
+
// controller in kolbo-api never reads visualDnaIds. For character-consistent
|
|
155
|
+
// video, use generate_elements (which DOES honor visual_dna_ids) or animate a
|
|
156
|
+
// DNA-locked still via generate_video_from_image.
|
|
146
157
|
server.tool(
|
|
147
158
|
'generate_video',
|
|
148
|
-
'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports Visual DNA
|
|
159
|
+
'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance). Does NOT support Visual DNA — for character-consistent video use generate_elements or animate a DNA-locked still via generate_video_from_image. Returns the final video URL when complete.',
|
|
149
160
|
{
|
|
150
161
|
prompt: z.string().describe('Text description of the video to generate'),
|
|
151
162
|
model: z.string().optional().describe('Model identifier. Use list_models type="text_to_video" to see options. Check supported_durations and supported_aspect_ratios.'),
|
|
@@ -153,13 +164,12 @@ function registerGenerateTools(server, client) {
|
|
|
153
164
|
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports — check supported_durations from list_models. Default: 5'),
|
|
154
165
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
155
166
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
|
|
156
|
-
|
|
157
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolutionMultipliers to predict cost.'),
|
|
167
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
|
|
158
168
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
|
|
159
169
|
},
|
|
160
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
170
|
+
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id }) => {
|
|
161
171
|
const gen = await client.post('/v1/generate/video', {
|
|
162
|
-
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images,
|
|
172
|
+
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id
|
|
163
173
|
});
|
|
164
174
|
|
|
165
175
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -175,7 +185,8 @@ function registerGenerateTools(server, client) {
|
|
|
175
185
|
model: result.result.model,
|
|
176
186
|
duration: result.result.duration,
|
|
177
187
|
thumbnail_url: result.result.thumbnail_url,
|
|
178
|
-
prompt_used: result.result.prompt_used
|
|
188
|
+
prompt_used: result.result.prompt_used,
|
|
189
|
+
_followup_hint: 'If the user asks to edit/restyle/extend this video next, pass urls[0] to edit_video (upscale/reframe/face_swap/extend/generate_audio/lipsync/magic_edit) or generate_video_from_video (restyle). Do NOT call generate_video from scratch.'
|
|
179
190
|
}, null, 2)
|
|
180
191
|
}]
|
|
181
192
|
};
|
|
@@ -194,7 +205,7 @@ function registerGenerateTools(server, client) {
|
|
|
194
205
|
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports. Default: 5'),
|
|
195
206
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
196
207
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.'),
|
|
197
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read
|
|
208
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.')
|
|
198
209
|
},
|
|
199
210
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
200
211
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
@@ -213,7 +224,8 @@ function registerGenerateTools(server, client) {
|
|
|
213
224
|
urls: result.result.urls,
|
|
214
225
|
model: result.result.model,
|
|
215
226
|
duration: result.result.duration,
|
|
216
|
-
thumbnail_url: result.result.thumbnail_url
|
|
227
|
+
thumbnail_url: result.result.thumbnail_url,
|
|
228
|
+
_followup_hint: 'If the user asks to edit/restyle/extend this video next, pass urls[0] to edit_video or generate_video_from_video. Do NOT re-run generate_video_from_image unless they want a fresh animation from a different source image.'
|
|
217
229
|
}, null, 2)
|
|
218
230
|
}]
|
|
219
231
|
};
|
|
@@ -386,13 +398,13 @@ function registerGenerateTools(server, client) {
|
|
|
386
398
|
// ─── generate_elements ─────────────────────────────────────
|
|
387
399
|
server.tool(
|
|
388
400
|
'generate_elements',
|
|
389
|
-
'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read
|
|
401
|
+
'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elements_max_images / elements_max_videos / elements_max_audio on the chosen model before generating. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
|
|
390
402
|
{
|
|
391
403
|
prompt: z.string().describe('Text description of the desired video / animation'),
|
|
392
|
-
model: z.string().optional().describe('Model identifier. Use list_models type="elements" to see options (Seedance 2, Kling O3 Reference, Grok Imagine, Veo 3.1, etc.). Check
|
|
393
|
-
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). Check
|
|
394
|
-
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs (
|
|
395
|
-
audio_url: z.string().optional().describe('URL of a reference audio track for models that accept audio inputs (
|
|
404
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="elements" to see options (Seedance 2, Kling O3 Reference, Grok Imagine, Veo 3.1, etc.). Check elements_max_images / elements_max_videos / elements_max_audio on the model. Omit for Smart Select.'),
|
|
405
|
+
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). Check elements_max_images on the chosen model — pass at most that many URLs.'),
|
|
406
|
+
reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs (elements_max_videos > 0). Check elements_max_videos on the chosen model from list_models before passing.'),
|
|
407
|
+
audio_url: z.string().optional().describe('URL of a reference audio track for models that accept audio inputs (elements_max_audio > 0). Check elements_max_audio on the chosen model from list_models before passing.'),
|
|
396
408
|
files: z.array(z.string()).optional().describe('Array of URLs or absolute local paths — alternative to reference_images. Use this when you have local files to upload. Each item can be a URL OR a local path.'),
|
|
397
409
|
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
398
410
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
@@ -594,19 +606,19 @@ function registerGenerateTools(server, client) {
|
|
|
594
606
|
// ─── generate_video_from_video ─────────────────────────────
|
|
595
607
|
server.tool(
|
|
596
608
|
'generate_video_from_video',
|
|
597
|
-
'Restyle / transform an existing video using a text prompt (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, or character replacement. Source video can be a URL or absolute local path. IMPORTANT: different models support different extra inputs — call list_models type="video_to_video" and read
|
|
609
|
+
'Restyle / transform an existing video using a text prompt (video-to-video). Use for style transfer, scene restyling, subject swap, motion transfer, or character replacement. Source video can be a URL or absolute local path. IMPORTANT: different models support different extra inputs — call list_models type="video_to_video" and read max_images / max_videos / max_elements on the chosen model before generating. Pass reference_images for models with max_images > 0 (e.g. Kling O1/O3, Aleph, WAN VACE), reference_videos for models with max_videos > 1 (e.g. WAN 2.6 reference-to-video accepts up to 3), and elements for models with max_elements > 0. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
598
610
|
{
|
|
599
611
|
source_video: z.string().describe('URL or absolute local path to the primary source video to restyle. 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.'),
|
|
600
612
|
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
601
|
-
model: z.string().optional().describe('Model identifier. Use list_models type="video_to_video" to see options and check
|
|
613
|
+
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 per model. Omit for Smart Select.'),
|
|
602
614
|
aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
|
|
603
615
|
duration: z.number().optional().describe('Duration in seconds (default: matches source)'),
|
|
604
616
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
605
617
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.'),
|
|
606
618
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
607
|
-
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs (
|
|
608
|
-
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs (
|
|
609
|
-
elements: z.array(z.string()).optional().describe('Array of element image URLs for models with
|
|
619
|
+
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs (max_images > 0). Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit. Check max_images on the model from list_models before passing.'),
|
|
620
|
+
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs (max_videos > 1). Example: WAN 2.6 reference-to-video accepts 1–3 reference videos. Check max_videos on the model from list_models before passing.'),
|
|
621
|
+
elements: z.array(z.string()).optional().describe('Array of element image URLs for models with max_elements > 0. Elements are used as style or character reference assets alongside the main video. Check max_elements on the model from list_models before passing.')
|
|
610
622
|
},
|
|
611
623
|
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
|
|
612
624
|
if (!source_video) throw new Error('source_video is required');
|
package/src/tools/models.js
CHANGED
|
@@ -21,8 +21,64 @@ function registerModelTools(server, client) {
|
|
|
21
21
|
const withSummary = result.models.filter(m => m.summary && m.summary.trim() !== '');
|
|
22
22
|
const withoutSummary = result.models.filter(m => !m.summary || m.summary.trim() === '');
|
|
23
23
|
|
|
24
|
+
// Format the per-model spec line. The agent NEEDS this — without it,
|
|
25
|
+
// it has to guess `supported_resolutions`/`supported_durations` and
|
|
26
|
+
// either invents values (then the API silently substitutes) or asks
|
|
27
|
+
// the user to clarify what's only knowable from this list.
|
|
28
|
+
const formatSpecs = m => {
|
|
29
|
+
const parts = [];
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(m.supported_resolutions) && m.supported_resolutions.length) {
|
|
32
|
+
const mult = m.resolution_multipliers || {};
|
|
33
|
+
parts.push(
|
|
34
|
+
'resolutions: ' +
|
|
35
|
+
m.supported_resolutions
|
|
36
|
+
.map(r => (mult[r] != null && mult[r] !== 1 ? `${r} (${mult[r]}×)` : r))
|
|
37
|
+
.join(' · ')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(m.supported_durations) && m.supported_durations.length) {
|
|
42
|
+
const ds = m.supported_durations;
|
|
43
|
+
// Compact ranges like 4-15 if it's a contiguous run.
|
|
44
|
+
const sorted = [...ds].sort((a, b) => a - b);
|
|
45
|
+
const isRange = sorted.length > 2 && sorted.every((v, i) => i === 0 || v - sorted[i - 1] === 1);
|
|
46
|
+
parts.push(`durations: ${isRange ? `${sorted[0]}-${sorted[sorted.length - 1]}s` : sorted.join('/') + 's'}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(m.supported_aspect_ratios) && m.supported_aspect_ratios.length) {
|
|
50
|
+
parts.push(`aspect: ${m.supported_aspect_ratios.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Elements-type caps (only show when at least one is non-zero)
|
|
54
|
+
const eImg = m.elements_max_images, eVid = m.elements_max_videos, eAud = m.elements_max_audio;
|
|
55
|
+
if ((eImg ?? 0) > 0 || (eVid ?? 0) > 0 || (eAud ?? 0) > 0) {
|
|
56
|
+
parts.push(`elements: ${eImg ?? 0} imgs / ${eVid ?? 0} vids / ${eAud ?? 0} audio`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Video-to-video / multi-input caps
|
|
60
|
+
const mImg = m.max_images, mVid = m.max_videos, mElm = m.max_elements;
|
|
61
|
+
if ((mImg ?? 0) > 0 || (mVid ?? 0) > 0 || (mElm ?? 0) > 0) {
|
|
62
|
+
parts.push(`refs: ${mImg ?? 0} imgs / ${mVid ?? 0} vids / ${mElm ?? 0} elms`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ((m.max_visual_dna ?? 0) > 0) parts.push(`max_dna: ${m.max_visual_dna}`);
|
|
66
|
+
|
|
67
|
+
// Sound (only show when sound costs more or is generated natively)
|
|
68
|
+
if (m.sound_generation_type === 'native') {
|
|
69
|
+
const mult = m.sound_credit_multiplier && m.sound_credit_multiplier !== 1
|
|
70
|
+
? ` (${m.sound_credit_multiplier}×)`
|
|
71
|
+
: '';
|
|
72
|
+
parts.push(`sound: native${mult}${m.sound_enabled_by_default ? ' on-by-default' : ''}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (m.max_audio_duration != null) parts.push(`audio_max: ${m.max_audio_duration}s`);
|
|
76
|
+
|
|
77
|
+
return parts.length ? `\n ${parts.join(' | ')}` : '';
|
|
78
|
+
};
|
|
79
|
+
|
|
24
80
|
const formatModel = m =>
|
|
25
|
-
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${m.summary}` : ''}`;
|
|
81
|
+
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${m.summary}` : ''}${formatSpecs(m)}`;
|
|
26
82
|
|
|
27
83
|
const sections = [];
|
|
28
84
|
if (withSummary.length > 0) {
|