@wavespeed/cli 0.4.26 → 0.4.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/history.js +2 -1
- package/dist/lib/media-models.js +6 -5
- package/dist/lib/media-plan.js +84 -9
- package/dist/lib/output.js +78 -2
- package/package.json +1 -1
- package/skills/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ wavespeed extend video "the camera keeps rising" --video @./clip.mp4 --json
|
|
|
120
120
|
|
|
121
121
|
| | Default | Alternatives (`-m`) |
|
|
122
122
|
|---|---|---|
|
|
123
|
-
| Image | `seedream-5-pro` | `gpt-image-2.5`, `nano-banana-2.1`, `nano-banana-pro`, `nano-banana-2`, `gpt-image-2` |
|
|
123
|
+
| Image | `seedream-5-pro` | `gpt-image-2.6`, `gpt-image-2.5`, `nano-banana-2.1`, `nano-banana-pro`, `nano-banana-2`, `gpt-image-2` |
|
|
124
124
|
| Video | `seedance-2.5` | `seedance-2.5-turbo`, `kling-4.0`, `wan-3.0`, `wan-3.0-prime`, `minimax-h3` (cheap and fast, native stereo audio, 768p by default), Seedance 2.0 tiers |
|
|
125
125
|
|
|
126
126
|
For everything else (audio, 3D, upscaling, a specific endpoint), find the model with `wavespeed models <query>` and call it with `wavespeed run <model-id>`.
|
package/dist/commands/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchHistory, PREDICTION_STATUSES } from '../lib/api.js';
|
|
4
4
|
import { toIso } from '../lib/time.js';
|
|
5
|
+
import { describeOutput } from '../lib/output.js';
|
|
5
6
|
function statusBadge(status) {
|
|
6
7
|
const map = {
|
|
7
8
|
completed: chalk.bgGreen.black,
|
|
@@ -40,7 +41,7 @@ export function beyondWindow(since, until) {
|
|
|
40
41
|
function firstOutput(item) {
|
|
41
42
|
const out = item.outputs?.[0];
|
|
42
43
|
if (typeof out === 'string')
|
|
43
|
-
return out;
|
|
44
|
+
return describeOutput(out);
|
|
44
45
|
return '';
|
|
45
46
|
}
|
|
46
47
|
export function registerHistory(program) {
|
package/dist/lib/media-models.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The image and video models the dedicated generate/edit tools offer: the
|
|
2
2
|
// same list, in the same order, as the WaveSpeed /chat page (api-dashboard
|
|
3
|
-
// lib/media-models.ts, checked 2026-
|
|
3
|
+
// lib/media-models.ts, checked 2026-11-05). The first of each is the default.
|
|
4
4
|
// This is a vendored copy: when /chat changes its models, change this file and
|
|
5
5
|
// its test (src/lib/media-models.test.ts) with it. Capabilities mirror the
|
|
6
6
|
// endpoints' input schemas; the tools still fit every request to the live
|
|
@@ -18,6 +18,7 @@ export const IMAGE_MODELS = {
|
|
|
18
18
|
'nano-banana-2.1': image('nano-banana-2.1', 'Nano Banana 2.1', 'google/nano-banana-2.1', 'text-to-image', ['1k', '2k', '4k'], NANO_BANANA_2_RATIOS, 14),
|
|
19
19
|
'nano-banana-2': image('nano-banana-2', 'Nano Banana 2', 'google/nano-banana-2', 'text-to-image', ['0.5k', '1k', '2k', '4k'], NANO_BANANA_2_RATIOS, 14),
|
|
20
20
|
'nano-banana-pro': image('nano-banana-pro', 'Nano Banana Pro', 'google/nano-banana-pro', 'text-to-image', ['1k', '2k', '4k'], NANO_BANANA_RATIOS, 14),
|
|
21
|
+
'gpt-image-2.6': image('gpt-image-2.6', 'GPT Image 2.6', 'openai/gpt-image-2.6-sunburst', 'text-to-image', ['1k', '2k', '4k'], WIDE_RATIOS, 16),
|
|
21
22
|
'gpt-image-2.5': image('gpt-image-2.5', 'GPT Image 2.5', 'openai/gpt-image-2.5-sunburst', 'text-to-image', ['1k', '2k', '4k'], WIDE_RATIOS, 16),
|
|
22
23
|
'gpt-image-2': image('gpt-image-2', 'GPT Image 2', 'openai/gpt-image-2', 'text-to-image', ['1k', '2k', '4k'], WIDE_RATIOS, 16),
|
|
23
24
|
};
|
|
@@ -76,16 +77,16 @@ export function videoModelForEndpoint(modelId) {
|
|
|
76
77
|
return VIDEO_MODEL_KEYS.map((k) => VIDEO_MODELS[k]).find((m) => m.turbo === turbo && modelId.startsWith(`${m.base}/`));
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
|
-
* A model's endpoints.
|
|
80
|
-
* reference-to-video endpoint
|
|
81
|
-
* endpoints have no extend.
|
|
80
|
+
* A model's endpoints. Every family takes references on a dedicated
|
|
81
|
+
* reference-to-video endpoint. Seedance has turbo ones; the others do not.
|
|
82
|
+
* Turbo endpoints have no extend.
|
|
82
83
|
*/
|
|
83
84
|
export function videoEndpoints(m) {
|
|
84
85
|
const suffix = m.turbo ? '-turbo' : '';
|
|
85
86
|
return {
|
|
86
87
|
text: `${m.base}/text-to-video${suffix}`,
|
|
87
88
|
image: `${m.base}/image-to-video${suffix}`,
|
|
88
|
-
|
|
89
|
+
reference: `${m.base}/reference-to-video${m.family === 'seedance-2.5' || m.family === 'seedance-2.0' ? suffix : ''}`,
|
|
89
90
|
edit: `${m.base}/video-edit${suffix}`,
|
|
90
91
|
...(m.modes.includes('extend') ? { extend: `${m.base}/video-extend` } : {}),
|
|
91
92
|
};
|
package/dist/lib/media-plan.js
CHANGED
|
@@ -5,16 +5,73 @@
|
|
|
5
5
|
// the same words give the same model, endpoint and input on both.
|
|
6
6
|
import { DEFAULT_IMAGE_MODEL, DEFAULT_VIDEO_MODEL, IMAGE_MODELS, IMAGE_MODEL_KEYS, VIDEO_MODELS, VIDEO_MODEL_KEYS, referenceFields, videoEndpoints, } from './media-models.js';
|
|
7
7
|
import { renameRefs } from './prompt-refs.js';
|
|
8
|
-
|
|
8
|
+
// Size tiers: 480p, 720p, 1080p (rank = N) and 0.5k, 1k, 2k, 4k (rank = X*1000),
|
|
9
|
+
// so 480p < 720p < 768p < 1080p < 2k < 4k on one scale.
|
|
10
|
+
function tierRank(v) {
|
|
11
|
+
if (typeof v !== 'string')
|
|
12
|
+
return undefined;
|
|
13
|
+
const m = /^(\d+(?:\.\d+)?)([pk])$/i.exec(v.trim());
|
|
14
|
+
if (!m)
|
|
15
|
+
return undefined;
|
|
16
|
+
const n = Number(m[1]);
|
|
17
|
+
return m[2].toLowerCase() === 'k' ? n * 1000 : n;
|
|
18
|
+
}
|
|
19
|
+
// ln(W/H) of a W:H ratio, so 2:1 and 1:2 are equally far from 1:1.
|
|
20
|
+
function ratioLog(v) {
|
|
21
|
+
if (typeof v !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
const m = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/.exec(v.trim());
|
|
24
|
+
if (!m)
|
|
25
|
+
return undefined;
|
|
26
|
+
const w = Number(m[1]);
|
|
27
|
+
const h = Number(m[2]);
|
|
28
|
+
return w > 0 && h > 0 ? Math.log(w / h) : undefined;
|
|
29
|
+
}
|
|
30
|
+
// Distances within this count as a tie (4:3 and 3:4 around 1:1 differ only in the last bit).
|
|
31
|
+
const TIE_EPSILON = 1e-9;
|
|
32
|
+
/** The member whose score is closest to target; on a tie, the one `better` prefers (default: the first). */
|
|
33
|
+
function closestMember(members, score, target, better = () => false) {
|
|
34
|
+
let best;
|
|
35
|
+
for (const v of members) {
|
|
36
|
+
const s = score(v);
|
|
37
|
+
if (s === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const d = Math.abs(s - target);
|
|
40
|
+
if (!best || d < best.d - TIE_EPSILON || (Math.abs(d - best.d) <= TIE_EPSILON && better({ v, s }, best)))
|
|
41
|
+
best = { v, s, d };
|
|
42
|
+
}
|
|
43
|
+
return best?.v;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Keep only fields the model accepts and fit each value to its schema. The
|
|
47
|
+
* rule is shared with the remote MCP (api-server) and the dashboard /chat:
|
|
48
|
+
* - a size tier (resolution, or an enum of Np / Nk values) the model does not
|
|
49
|
+
* offer becomes the closest tier it does; a tie goes to the lower one;
|
|
50
|
+
* - a W:H aspect ratio becomes the closest ratio (in ln(W/H)) among the
|
|
51
|
+
* members that are W:H; a tie goes to the first in enum order;
|
|
52
|
+
* - any other value outside an enum is left out, so the model default applies;
|
|
53
|
+
* - numbers go to the nearest numeric enum value, or are rounded (integers)
|
|
54
|
+
* and clamped to the range.
|
|
55
|
+
* Every value not sent as requested gets a note.
|
|
56
|
+
*/
|
|
9
57
|
export async function fitInput(modelId, wanted, propsOf, showDefaults = []) {
|
|
10
58
|
const properties = await propsOf(modelId);
|
|
11
59
|
if (!properties)
|
|
12
60
|
throw new Error(`No schema for ${modelId} in the model catalog.`);
|
|
13
61
|
const input = {};
|
|
62
|
+
const notes = [];
|
|
63
|
+
// Defaults filled in only to show the price are not the caller's request: no note for them.
|
|
64
|
+
const fromDefault = new Set();
|
|
65
|
+
const used = (key, from, to) => {
|
|
66
|
+
input[key] = to;
|
|
67
|
+
if (to !== from && !fromDefault.has(key))
|
|
68
|
+
notes.push(`${key} ${String(from)} is not offered by ${modelId}; used ${String(to)}.`);
|
|
69
|
+
};
|
|
14
70
|
// Spell out the defaults that set the price, so the estimate shows them.
|
|
15
71
|
for (const key of showDefaults) {
|
|
16
72
|
if (properties[key]?.default !== undefined && (wanted[key] === undefined || wanted[key] === null || wanted[key] === '')) {
|
|
17
73
|
wanted = { ...wanted, [key]: properties[key].default };
|
|
74
|
+
fromDefault.add(key);
|
|
18
75
|
}
|
|
19
76
|
}
|
|
20
77
|
for (const [key, value] of Object.entries(wanted)) {
|
|
@@ -28,11 +85,28 @@ export async function fitInput(modelId, wanted, propsOf, showDefaults = []) {
|
|
|
28
85
|
continue;
|
|
29
86
|
}
|
|
30
87
|
if (prop.enum?.length) {
|
|
31
|
-
|
|
88
|
+
const members = prop.enum;
|
|
89
|
+
if (members.includes(value)) {
|
|
32
90
|
input[key] = value;
|
|
33
|
-
|
|
34
|
-
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'number' && members.every((v) => typeof v === 'number')) {
|
|
94
|
+
used(key, value, members.reduce((a, b) => (Math.abs(b - value) < Math.abs(a - value) ? b : a)));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const rank = tierRank(value);
|
|
98
|
+
const tierMembers = members.filter((v) => tierRank(v) !== undefined);
|
|
99
|
+
if (rank !== undefined && tierMembers.length && (key === 'resolution' || tierMembers.length === members.length)) {
|
|
100
|
+
used(key, value, closestMember(tierMembers, tierRank, rank, (a, b) => a.s < b.s));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const ratio = ratioLog(value);
|
|
104
|
+
if (ratio !== undefined && members.some((v) => ratioLog(v) !== undefined)) {
|
|
105
|
+
used(key, value, closestMember(members, ratioLog, ratio));
|
|
106
|
+
continue;
|
|
35
107
|
}
|
|
108
|
+
if (!fromDefault.has(key))
|
|
109
|
+
notes.push(`${key} ${String(value)} is not accepted by ${modelId}; the model default is used.`);
|
|
36
110
|
continue;
|
|
37
111
|
}
|
|
38
112
|
if (typeof value === 'number' && (prop.type === 'integer' || prop.type === 'number')) {
|
|
@@ -41,12 +115,12 @@ export async function fitInput(modelId, wanted, propsOf, showDefaults = []) {
|
|
|
41
115
|
v = Math.max(prop.minimum, v);
|
|
42
116
|
if (typeof prop.maximum === 'number')
|
|
43
117
|
v = Math.min(prop.maximum, v);
|
|
44
|
-
|
|
118
|
+
used(key, value, v);
|
|
45
119
|
continue;
|
|
46
120
|
}
|
|
47
121
|
input[key] = value;
|
|
48
122
|
}
|
|
49
|
-
return input;
|
|
123
|
+
return { input, notes };
|
|
50
124
|
}
|
|
51
125
|
function imageModelKey(named) {
|
|
52
126
|
if (named === undefined || named === '')
|
|
@@ -64,10 +138,9 @@ export async function planImage(args, propsOf) {
|
|
|
64
138
|
const wanted = { prompt: args.prompt, aspect_ratio: args.aspect_ratio, resolution: args.resolution };
|
|
65
139
|
if (edit)
|
|
66
140
|
wanted.images = images;
|
|
67
|
-
const input = await fitInput(modelId, wanted, propsOf);
|
|
141
|
+
const { input, notes } = await fitInput(modelId, wanted, propsOf);
|
|
68
142
|
if (typeof input.prompt === 'string')
|
|
69
143
|
input.prompt = renameRefs(input.prompt, input, modelId);
|
|
70
|
-
const notes = [];
|
|
71
144
|
if (edit && Array.isArray(input.images) && input.images.length < images.length) {
|
|
72
145
|
notes.push(`${model.label} takes at most ${input.images.length} images, so only the first ${input.images.length} were used.`);
|
|
73
146
|
}
|
|
@@ -167,7 +240,8 @@ export async function planVideo(args, action, propsOf) {
|
|
|
167
240
|
const model = VIDEO_MODELS[chosen.key];
|
|
168
241
|
const modelId = endpoints(chosen.key)[chosen.mode];
|
|
169
242
|
// Image-to-video follows the first frame's shape: no aspect_ratio, not even the schema default.
|
|
170
|
-
const
|
|
243
|
+
const fitted = await fitInput(modelId, chosen.wanted, propsOf, chosen.mode === 'image' ? ['duration', 'resolution'] : ['duration', 'resolution', 'aspect_ratio']);
|
|
244
|
+
const input = fitted.input;
|
|
171
245
|
if (chosen.mode === 'image')
|
|
172
246
|
delete input.aspect_ratio;
|
|
173
247
|
if (typeof input.prompt === 'string')
|
|
@@ -184,5 +258,6 @@ export async function planVideo(args, action, propsOf) {
|
|
|
184
258
|
? `${model.label} does not take ${dropped.join(', ')} in any mode, so those were left out; the model you named was not switched. ${await whoTakes(dropped, propsOf)}`
|
|
185
259
|
: `No available video model takes ${dropped.join(', ')} for this request, so those were left out.`);
|
|
186
260
|
}
|
|
261
|
+
notes.push(...fitted.notes);
|
|
187
262
|
return { modelId, label: model.label, mode: MODE_LABEL[chosen.mode], input, notes };
|
|
188
263
|
}
|
package/dist/lib/output.js
CHANGED
|
@@ -11,6 +11,73 @@ function extFromUrl(url, fallback) {
|
|
|
11
11
|
return m[1].toLowerCase();
|
|
12
12
|
return fallback;
|
|
13
13
|
}
|
|
14
|
+
// With enable_base64_output=true each URL output comes back as naked base64,
|
|
15
|
+
// but an output over 64 MiB or one the API failed to fetch stays a URL, and
|
|
16
|
+
// text/JSON outputs are unchanged — so the kind is decided per output.
|
|
17
|
+
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
18
|
+
const DATA_URI_RE = /^data:([^;,]*)(?:;[^,]*)?;base64,(.*)$/s;
|
|
19
|
+
export function isHttpUrl(value) {
|
|
20
|
+
return /^https?:\/\//i.test(value);
|
|
21
|
+
}
|
|
22
|
+
/** Naked base64 as returned for enable_base64_output (short strings are treated as text). */
|
|
23
|
+
export function isBase64Output(value) {
|
|
24
|
+
return value.length >= 64 && value.length % 4 === 0 && BASE64_RE.test(value);
|
|
25
|
+
}
|
|
26
|
+
const MAGIC = [
|
|
27
|
+
[[0x89, 0x50, 0x4e, 0x47], 'png'],
|
|
28
|
+
[[0xff, 0xd8, 0xff], 'jpg'],
|
|
29
|
+
[[0x47, 0x49, 0x46, 0x38], 'gif'],
|
|
30
|
+
[[0x1a, 0x45, 0xdf, 0xa3], 'webm'],
|
|
31
|
+
[[0x49, 0x44, 0x33], 'mp3'],
|
|
32
|
+
[[0x66, 0x4c, 0x61, 0x43], 'flac'],
|
|
33
|
+
[[0x4f, 0x67, 0x67, 0x53], 'ogg'],
|
|
34
|
+
[[0x67, 0x6c, 0x54, 0x46], 'glb'],
|
|
35
|
+
[[0x50, 0x4b, 0x03, 0x04], 'zip'],
|
|
36
|
+
[[0x25, 0x50, 0x44, 0x46], 'pdf'],
|
|
37
|
+
];
|
|
38
|
+
export function extFromBytes(buf, fallback) {
|
|
39
|
+
for (const [sig, ext] of MAGIC) {
|
|
40
|
+
if (sig.every((b, i) => buf[i] === b))
|
|
41
|
+
return ext;
|
|
42
|
+
}
|
|
43
|
+
const at = (o, s) => buf.subarray(o, o + s.length).toString('latin1') === s;
|
|
44
|
+
if (at(0, 'RIFF') && at(8, 'WEBP'))
|
|
45
|
+
return 'webp';
|
|
46
|
+
if (at(0, 'RIFF') && at(8, 'WAVE'))
|
|
47
|
+
return 'wav';
|
|
48
|
+
if (at(4, 'ftyp'))
|
|
49
|
+
return at(8, 'M4A') ? 'm4a' : at(8, 'qt') ? 'mov' : 'mp4';
|
|
50
|
+
if (buf.length >= 2 && buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0)
|
|
51
|
+
return 'mp3';
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
const EXT_FROM_MIME = {
|
|
55
|
+
'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif',
|
|
56
|
+
'video/mp4': 'mp4', 'video/webm': 'webm', 'audio/mpeg': 'mp3', 'audio/wav': 'wav',
|
|
57
|
+
};
|
|
58
|
+
/** Bytes of an inline (base64 or data URI) output, or null when it is not inline. */
|
|
59
|
+
function inlineBytes(value, fallbackExt) {
|
|
60
|
+
const m = value.match(DATA_URI_RE);
|
|
61
|
+
if (m) {
|
|
62
|
+
const buf = Buffer.from(m[2], 'base64');
|
|
63
|
+
return { buf, ext: EXT_FROM_MIME[m[1].toLowerCase()] ?? extFromBytes(buf, fallbackExt) };
|
|
64
|
+
}
|
|
65
|
+
if (!isBase64Output(value))
|
|
66
|
+
return null;
|
|
67
|
+
const buf = Buffer.from(value, 'base64');
|
|
68
|
+
return { buf, ext: extFromBytes(buf, fallbackExt) };
|
|
69
|
+
}
|
|
70
|
+
/** Short label for an output: URLs as-is, inline data summarised instead of dumped. */
|
|
71
|
+
export function describeOutput(value) {
|
|
72
|
+
if (isHttpUrl(value))
|
|
73
|
+
return value;
|
|
74
|
+
const m = value.match(DATA_URI_RE);
|
|
75
|
+
const b64 = m ? m[2] : isBase64Output(value) ? value : null;
|
|
76
|
+
if (b64 === null)
|
|
77
|
+
return value;
|
|
78
|
+
const bytes = Math.floor((b64.length * 3) / 4);
|
|
79
|
+
return `(base64 data, ${bytes >= 1024 * 1024 ? (bytes / 1024 / 1024).toFixed(1) + ' MB' : Math.ceil(bytes / 1024) + ' KB'})`;
|
|
80
|
+
}
|
|
14
81
|
function slug(input) {
|
|
15
82
|
return input
|
|
16
83
|
.toLowerCase()
|
|
@@ -31,7 +98,8 @@ export async function downloadOutputs(urls, opts = {}) {
|
|
|
31
98
|
const saved = [];
|
|
32
99
|
for (let i = 0; i < urls.length; i++) {
|
|
33
100
|
const url = urls[i];
|
|
34
|
-
const
|
|
101
|
+
const inline = isHttpUrl(url) ? null : inlineBytes(url, opts.ext || 'bin');
|
|
102
|
+
const ext = inline ? inline.ext : extFromUrl(url, opts.ext || 'bin');
|
|
35
103
|
let filepath;
|
|
36
104
|
if (opts.template) {
|
|
37
105
|
const rendered = renderTemplate(opts.template, {
|
|
@@ -53,6 +121,14 @@ export async function downloadOutputs(urls, opts = {}) {
|
|
|
53
121
|
filepath = path.join(baseDir, `${ts}-${tag}${suffix}.${ext}`);
|
|
54
122
|
}
|
|
55
123
|
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
124
|
+
if (inline) {
|
|
125
|
+
fs.writeFileSync(filepath, inline.buf);
|
|
126
|
+
saved.push(filepath);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!isHttpUrl(url)) {
|
|
130
|
+
throw new Error(`Output ${i + 1} is neither a URL nor base64 data, so it cannot be downloaded: ${url.slice(0, 80)}`);
|
|
131
|
+
}
|
|
56
132
|
const res = await fetch(url);
|
|
57
133
|
if (!res.ok || !res.body) {
|
|
58
134
|
throw new Error(`Download failed for ${url}: ${res.status} ${res.statusText}`);
|
|
@@ -72,6 +148,6 @@ export function printOutputs(saved, remote) {
|
|
|
72
148
|
}
|
|
73
149
|
console.log(chalk.gray(saved.length ? 'Source URLs:' : 'URLs:'));
|
|
74
150
|
for (const u of remote)
|
|
75
|
-
console.log(' ' + chalk.cyan(u));
|
|
151
|
+
console.log(' ' + chalk.cyan(describeOutput(u)));
|
|
76
152
|
console.log();
|
|
77
153
|
}
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ They use the same models as the WaveSpeed /chat page and pick the endpoint (text
|
|
|
33
33
|
|
|
34
34
|
| | Default | Alternatives (`-m` / `model`) |
|
|
35
35
|
|---|---|---|
|
|
36
|
-
| Image | `seedream-5-pro` (Seedream 5 Pro) | `gpt-image-2.
|
|
36
|
+
| Image | `seedream-5-pro` (Seedream 5 Pro) | `gpt-image-2.6` (GPT Image 2.6, the model behind ChatGPT Images 2.6; strong at text and photorealism), `gpt-image-2.5`, `nano-banana-2.1`, `nano-banana-pro`, `nano-banana-2`, `gpt-image-2` |
|
|
37
37
|
| Video | `seedance-2.5` (Seedance 2.5) | `seedance-2.5-turbo`, `kling-4.0`, `wan-3.0`, `wan-3.0-prime`, `minimax-h3` (cheap and fast, native stereo audio; defaults to 768p, its counterpart of 720p), older Seedance 2.0 tiers |
|
|
38
38
|
|
|
39
39
|
Video resolution is 720p unless the user asks for another (MiniMax H3: 768p). Offer 480p for a cheaper draft.
|