@wji.ai/cli 0.7.0 → 0.8.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/README.md +4 -1
- package/dist/index.js +7162 -76
- package/package.json +6 -9
- package/skill/short-drama/SKILL.md +22 -10
- package/skill/short-drama/reference/bgm-sfx.md +124 -0
- package/skill/short-drama/reference/canvas-recipes.md +19 -2
- package/skill/short-drama/reference/film-mixing-guideline.md +447 -0
- package/skill/short-drama/reference/mix-over-video.md +89 -0
- package/skill/short-drama/reference/operations-cli.md +1 -1
- package/skill/short-drama/reference/schema.md +4 -2
- package/skill/short-drama/reference/stage-characters.md +2 -2
- package/skill/short-drama/reference/stage-storyboard.md +29 -6
- package/skill/short-drama/reference/workspace-layout.md +2 -2
- package/skill/short-drama/scripts/selftest.mjs +54 -9
- package/skill/short-drama/scripts/short-drama.mjs +150 -12
- package/dist/commands/agent.js +0 -294
- package/dist/commands/asset.js +0 -241
- package/dist/commands/auth.js +0 -216
- package/dist/commands/backreel-enquiry.js +0 -171
- package/dist/commands/backreel-episode.js +0 -243
- package/dist/commands/backreel-fund.js +0 -299
- package/dist/commands/backreel.js +0 -443
- package/dist/commands/canvas.js +0 -1706
- package/dist/commands/class.js +0 -395
- package/dist/commands/config.js +0 -113
- package/dist/commands/crew.js +0 -205
- package/dist/commands/fsnode.js +0 -288
- package/dist/commands/gen.js +0 -356
- package/dist/commands/model.js +0 -136
- package/dist/commands/npc-album.js +0 -201
- package/dist/commands/npc-live.js +0 -316
- package/dist/commands/npc.js +0 -437
- package/dist/commands/org.js +0 -85
- package/dist/commands/preset.js +0 -176
- package/dist/commands/profile.js +0 -129
- package/dist/commands/skill.js +0 -272
- package/dist/commands/workspace.js +0 -470
- package/dist/core/archive.js +0 -95
- package/dist/core/backreel.js +0 -23
- package/dist/core/client.js +0 -269
- package/dist/core/config.js +0 -95
- package/dist/core/crew.js +0 -546
- package/dist/core/image.js +0 -106
- package/dist/core/members.js +0 -54
- package/dist/core/npc.js +0 -108
- package/dist/core/output.js +0 -174
- package/dist/core/prompt.js +0 -118
- package/dist/core/vod.js +0 -149
- package/dist/core/zip.js +0 -87
package/dist/commands/gen.js
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { WjiClient } from '../core/client.js';
|
|
3
|
-
import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
|
|
4
|
-
import { color, detail, detailLevel, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
|
|
5
|
-
const READY = 1;
|
|
6
|
-
const CANCELLED = -1;
|
|
7
|
-
/** `--ref abc123`, `--ref abc123:hero`, `--ref abc123:hero:0.6` */
|
|
8
|
-
function parseReference(token) {
|
|
9
|
-
const [assetId, label, weight] = token.split(':');
|
|
10
|
-
if (!assetId)
|
|
11
|
-
throw new Error(`Invalid reference "${token}"`);
|
|
12
|
-
return {
|
|
13
|
-
assetId,
|
|
14
|
-
label: label || undefined,
|
|
15
|
-
weight: weight ? Number(weight) : undefined,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
const refs = (tokens) => (tokens?.length ? tokens.map(parseReference) : undefined);
|
|
19
|
-
function assetIdsOf(history) {
|
|
20
|
-
const images = history?.images;
|
|
21
|
-
const list = Array.isArray(images) ? images : Array.isArray(images?.items) ? images.items : [];
|
|
22
|
-
return list.map((a) => (typeof a === 'string' ? a : a?.id)).filter(Boolean);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* A history entry only carries asset *ids* — `images` is `[{id}]` with no
|
|
26
|
-
* status or url — so the real state has to be fetched separately. Polling on
|
|
27
|
-
* the history alone would report "complete" the instant the entry exists.
|
|
28
|
-
*/
|
|
29
|
-
export async function resolveAssets(client, history) {
|
|
30
|
-
const ids = assetIdsOf(history);
|
|
31
|
-
if (!ids.length)
|
|
32
|
-
return [];
|
|
33
|
-
return client.checkAssetStatus(ids);
|
|
34
|
-
}
|
|
35
|
-
export const stateOf = (a) => a?.status === CANCELLED || a?.error ? 'failed' : a?.status === READY ? 'ready' : 'pending';
|
|
36
|
-
export async function pollHistory(client, id, { timeoutMs, intervalMs }) {
|
|
37
|
-
const started = Date.now();
|
|
38
|
-
let lastReport = 0;
|
|
39
|
-
while (Date.now() - started < timeoutMs) {
|
|
40
|
-
const history = await client.get(`/ai/assets/history/list/${id}`);
|
|
41
|
-
const assets = await resolveAssets(client, history);
|
|
42
|
-
const pending = assets.filter((a) => stateOf(a) === 'pending');
|
|
43
|
-
if (assets.length && !pending.length) {
|
|
44
|
-
const failed = assets.filter((a) => stateOf(a) === 'failed');
|
|
45
|
-
if (failed.length === assets.length) {
|
|
46
|
-
throw new Error(`Generation failed: ${failed
|
|
47
|
-
.map((a) => a.error?.message || a.error || 'cancelled')
|
|
48
|
-
.join('; ')}`);
|
|
49
|
-
}
|
|
50
|
-
if (failed.length) {
|
|
51
|
-
warn(`${failed.length}/${assets.length} outputs failed`);
|
|
52
|
-
}
|
|
53
|
-
return { history, assets };
|
|
54
|
-
}
|
|
55
|
-
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
56
|
-
if (elapsed - lastReport >= 10) {
|
|
57
|
-
lastReport = elapsed;
|
|
58
|
-
const ready = assets.length - pending.length;
|
|
59
|
-
info(color.dim(` …${elapsed}s (${ready}/${assets.length || '?'} ready)`));
|
|
60
|
-
}
|
|
61
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
62
|
-
}
|
|
63
|
-
throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s. The task may still finish — check \`wji history get ${id}\`.`);
|
|
64
|
-
}
|
|
65
|
-
export function reportHistory(history, assets) {
|
|
66
|
-
detail([
|
|
67
|
-
['history', history.id],
|
|
68
|
-
['type', history.type],
|
|
69
|
-
['prompt', truncate(history.prompt, 70)],
|
|
70
|
-
['credits', history.chargedCredits],
|
|
71
|
-
['assets', assets.length || assetIdsOf(history).length],
|
|
72
|
-
]);
|
|
73
|
-
if (!assets.length)
|
|
74
|
-
return;
|
|
75
|
-
table(assets, [
|
|
76
|
-
{ header: 'assetId', value: (a) => a.id },
|
|
77
|
-
{ header: 'state', value: (a) => stateOf(a) },
|
|
78
|
-
{ header: 'name', value: (a) => truncate(a.name, 28) },
|
|
79
|
-
{ header: 'url', value: (a) => a.url || a.error?.message },
|
|
80
|
-
]);
|
|
81
|
-
}
|
|
82
|
-
async function submit(client, kind, body, opts) {
|
|
83
|
-
// Always the history endpoint: the raw one silently drops fsnode, scope,
|
|
84
|
-
// scopeId and tags, and returns no id to poll with.
|
|
85
|
-
const result = await client.post(`/ai/assets/history/${kind}/generations`, body);
|
|
86
|
-
if (!opts.wait) {
|
|
87
|
-
if (isJsonMode())
|
|
88
|
-
return printJson(result);
|
|
89
|
-
success(`Submitted ${kind} generation`);
|
|
90
|
-
reportHistory(result, await resolveAssets(client, result));
|
|
91
|
-
info(color.dim(`Poll with: wji history get ${result.id} --wait`));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
info(color.dim(`Submitted ${result.id}, waiting…`));
|
|
95
|
-
const { history, assets } = await pollHistory(client, result.id, {
|
|
96
|
-
timeoutMs: Number(opts.timeout) * 1000,
|
|
97
|
-
intervalMs: Number(opts.interval) * 1000,
|
|
98
|
-
});
|
|
99
|
-
if (isJsonMode())
|
|
100
|
-
return printJson({ ...history, images: assets });
|
|
101
|
-
success(`${kind} generation complete`);
|
|
102
|
-
reportHistory(history, assets);
|
|
103
|
-
}
|
|
104
|
-
function withCommonOptions(cmd) {
|
|
105
|
-
return cmd
|
|
106
|
-
.requiredOption('-m, --model <name>', 'model name (see `wji models list`)')
|
|
107
|
-
.option('-p, --prompt <text>', 'prompt text (@file / - for stdin)')
|
|
108
|
-
.option('-n, --n <count>', 'number of outputs', '1')
|
|
109
|
-
.option('-w, --workspace <id>', 'workspace to bill and file the result under')
|
|
110
|
-
.option('--fsnode <id>', 'file the history entry under an fsnode (does not add the asset to its images)')
|
|
111
|
-
.option('--scope <scope>', 'history scope, e.g. workspace | canvas')
|
|
112
|
-
.option('--scope-id <id>', 'history scope id')
|
|
113
|
-
.option('-r, --ratio <ratio>', 'aspect ratio, e.g. 16:9')
|
|
114
|
-
.option('--tags <tag...>', 'tags for the history entry')
|
|
115
|
-
.option('--values <json>', 'model-specific form values (@file / - supported)')
|
|
116
|
-
.option('--set <key=value...>', 'model-specific form values as key=value')
|
|
117
|
-
.option('--wait', 'block until the task finishes')
|
|
118
|
-
.option('--timeout <seconds>', 'wait timeout', '900')
|
|
119
|
-
.option('--interval <seconds>', 'poll interval', '5');
|
|
120
|
-
}
|
|
121
|
-
export function genCommand(globals) {
|
|
122
|
-
const gen = new Command('gen').description('Generate images, video and audio');
|
|
123
|
-
withCommonOptions(gen
|
|
124
|
-
.command('image')
|
|
125
|
-
.description('Generate images')
|
|
126
|
-
.option('--ref <ref...>', 'reference images as assetId[:label[:weight]]')
|
|
127
|
-
.option('--omni <ref...>', 'omni reference images as assetId[:label[:weight]]')
|
|
128
|
-
.option('--style-ref <ref...>', 'style reference images as assetId[:label[:weight]]')
|
|
129
|
-
.option('--reference-weight <n>', 'weight for --ref')
|
|
130
|
-
.option('--stylize <n>', 'stylize strength')
|
|
131
|
-
.option('--model-options <json>', 'raw modelOptions JSON')).action(async (opts) => {
|
|
132
|
-
const client = new WjiClient(globals());
|
|
133
|
-
await submit(client, 'image', {
|
|
134
|
-
model: opts.model,
|
|
135
|
-
prompt: opts.prompt ? await readValue(opts.prompt) : '',
|
|
136
|
-
n: Number(opts.n),
|
|
137
|
-
workspace: client.workspace(opts.workspace),
|
|
138
|
-
fsnode: opts.fsnode,
|
|
139
|
-
scope: opts.scope,
|
|
140
|
-
scopeId: opts.scopeId,
|
|
141
|
-
aspectRatio: opts.ratio,
|
|
142
|
-
tags: opts.tags,
|
|
143
|
-
referenceImages: refs(opts.ref),
|
|
144
|
-
omniReferenceImages: refs(opts.omni),
|
|
145
|
-
styleReferenceImages: refs(opts.styleRef),
|
|
146
|
-
referenceWeight: opts.referenceWeight ? Number(opts.referenceWeight) : undefined,
|
|
147
|
-
stylize: opts.stylize ? Number(opts.stylize) : undefined,
|
|
148
|
-
modelOptions: await readJson(opts.modelOptions),
|
|
149
|
-
values: { ...((await readJson(opts.values)) || {}), ...parseKeyValues(opts.set) },
|
|
150
|
-
}, opts);
|
|
151
|
-
});
|
|
152
|
-
withCommonOptions(gen
|
|
153
|
-
.command('video')
|
|
154
|
-
.description('Generate video')
|
|
155
|
-
.option('-d, --duration <seconds>', 'clip duration')
|
|
156
|
-
.option('--start-image <ref>', 'first frame as assetId[:label]')
|
|
157
|
-
.option('--end-image <ref>', 'last frame as assetId[:label]')
|
|
158
|
-
.option('--ref <ref...>', 'reference images as assetId[:label[:weight]]')
|
|
159
|
-
.option('--ref-video <ref...>', 'motion reference videos as assetId[:label[:weight]]')
|
|
160
|
-
.option('--ref-audio <ref...>', 'audio references as assetId[:label[:weight]]')).action(async (opts) => {
|
|
161
|
-
const client = new WjiClient(globals());
|
|
162
|
-
await submit(client, 'video', {
|
|
163
|
-
model: opts.model,
|
|
164
|
-
prompt: opts.prompt ? await readValue(opts.prompt) : '',
|
|
165
|
-
n: Number(opts.n),
|
|
166
|
-
workspace: client.workspace(opts.workspace),
|
|
167
|
-
fsnode: opts.fsnode,
|
|
168
|
-
scope: opts.scope,
|
|
169
|
-
scopeId: opts.scopeId,
|
|
170
|
-
aspectRatio: opts.ratio,
|
|
171
|
-
tags: opts.tags,
|
|
172
|
-
duration: opts.duration ? Number(opts.duration) : undefined,
|
|
173
|
-
startImage: opts.startImage ? parseReference(opts.startImage) : undefined,
|
|
174
|
-
endImage: opts.endImage ? parseReference(opts.endImage) : undefined,
|
|
175
|
-
referenceImages: refs(opts.ref),
|
|
176
|
-
referenceVideos: refs(opts.refVideo),
|
|
177
|
-
referenceAudios: refs(opts.refAudio),
|
|
178
|
-
values: { ...((await readJson(opts.values)) || {}), ...parseKeyValues(opts.set) },
|
|
179
|
-
}, opts);
|
|
180
|
-
});
|
|
181
|
-
withCommonOptions(gen
|
|
182
|
-
.command('audio')
|
|
183
|
-
.description('Generate audio')
|
|
184
|
-
.option('-d, --duration <seconds>', 'duration')
|
|
185
|
-
.option('--ref-audio <ref...>', 'audio references as assetId[:label[:weight]]')
|
|
186
|
-
.option('--ref <ref...>', 'reference image, e.g. a headshot for a Seed Audio voice sample (not with --ref-audio)')).action(async (opts) => {
|
|
187
|
-
if (opts.ref?.length && opts.refAudio?.length)
|
|
188
|
-
throw new Error('Use --ref or --ref-audio, not both');
|
|
189
|
-
const client = new WjiClient(globals());
|
|
190
|
-
await submit(client, 'audio', {
|
|
191
|
-
model: opts.model,
|
|
192
|
-
prompt: opts.prompt ? await readValue(opts.prompt) : '',
|
|
193
|
-
n: Number(opts.n),
|
|
194
|
-
workspace: client.workspace(opts.workspace),
|
|
195
|
-
fsnode: opts.fsnode,
|
|
196
|
-
scope: opts.scope,
|
|
197
|
-
scopeId: opts.scopeId,
|
|
198
|
-
tags: opts.tags,
|
|
199
|
-
referenceAudios: refs(opts.refAudio),
|
|
200
|
-
referenceImages: refs(opts.ref),
|
|
201
|
-
values: {
|
|
202
|
-
...((await readJson(opts.values)) || {}),
|
|
203
|
-
...parseKeyValues(opts.set),
|
|
204
|
-
...(opts.duration ? { duration: Number(opts.duration) } : {}),
|
|
205
|
-
},
|
|
206
|
-
}, opts);
|
|
207
|
-
});
|
|
208
|
-
gen
|
|
209
|
-
.command('cost')
|
|
210
|
-
.description('Estimate the credit cost of a generation before running it')
|
|
211
|
-
.requiredOption('-t, --type <type>', 'image | video | audio')
|
|
212
|
-
.requiredOption('-m, --model <name>', 'model name')
|
|
213
|
-
.option('-n, --n <count>', 'number of outputs', '1')
|
|
214
|
-
.option('-p, --prompt <text>', 'prompt (some models price by length)')
|
|
215
|
-
.option('-d, --duration <seconds>', 'duration for video/audio')
|
|
216
|
-
.option('-r, --ratio <ratio>', 'aspect ratio')
|
|
217
|
-
.option('--refs <n>', 'number of reference images')
|
|
218
|
-
.option('--values <json>', 'model-specific form values')
|
|
219
|
-
.action(async (opts) => {
|
|
220
|
-
const client = new WjiClient(globals());
|
|
221
|
-
const result = await client.post('/ai/assets/preComputeCost', {
|
|
222
|
-
type: opts.type,
|
|
223
|
-
model: opts.model,
|
|
224
|
-
n: Number(opts.n),
|
|
225
|
-
prompt: opts.prompt,
|
|
226
|
-
duration: opts.duration ? Number(opts.duration) : undefined,
|
|
227
|
-
aspectRatio: opts.ratio,
|
|
228
|
-
referenceImageCount: opts.refs ? Number(opts.refs) : undefined,
|
|
229
|
-
values: await readJson(opts.values),
|
|
230
|
-
});
|
|
231
|
-
render(result, (r) => typeof r === 'object' ? detail(Object.entries(r)) : process.stdout.write(`${r}\n`));
|
|
232
|
-
});
|
|
233
|
-
return gen;
|
|
234
|
-
}
|
|
235
|
-
export function historyCommand(globals) {
|
|
236
|
-
const history = new Command('history').description('Browse generation history');
|
|
237
|
-
history
|
|
238
|
-
.command('list', { isDefault: true })
|
|
239
|
-
.description('Search generation history')
|
|
240
|
-
.option('-w, --workspace <id>', 'workspace scope')
|
|
241
|
-
.option('--mode <mode>', 'image | video | audio')
|
|
242
|
-
.option('--fsnode <id>', 'filter by fsnode')
|
|
243
|
-
.option('--scope <scope>', 'workspace | canvas | …')
|
|
244
|
-
.option('--scope-id <id>', 'scope id')
|
|
245
|
-
.option('--tags <tag...>', 'filter by tags')
|
|
246
|
-
.option('--bookmarked', 'only bookmarked entries')
|
|
247
|
-
.option('--page <n>', 'page number', '1')
|
|
248
|
-
.option('--limit <n>', 'page size', '20')
|
|
249
|
-
.action(async (opts) => {
|
|
250
|
-
const client = new WjiClient(globals());
|
|
251
|
-
const result = await client.get('/ai/assets/history/search', {
|
|
252
|
-
workspace: client.workspace(opts.workspace),
|
|
253
|
-
mode: opts.mode,
|
|
254
|
-
fsnodeId: opts.fsnode,
|
|
255
|
-
scope: opts.scope,
|
|
256
|
-
scopeId: opts.scopeId,
|
|
257
|
-
tags: opts.tags,
|
|
258
|
-
bookmarked: opts.bookmarked ? true : undefined,
|
|
259
|
-
page: Number(opts.page),
|
|
260
|
-
limit: Number(opts.limit),
|
|
261
|
-
detail: detailLevel(),
|
|
262
|
-
});
|
|
263
|
-
render(result, (r) => {
|
|
264
|
-
table(r.data || [], [
|
|
265
|
-
{ header: 'id', value: (h) => h.id },
|
|
266
|
-
{ header: 'type', value: (h) => h.type },
|
|
267
|
-
{ header: 'assets', value: (h) => assetIdsOf(h).length },
|
|
268
|
-
{ header: 'prompt', value: (h) => truncate(h.prompt, 48) },
|
|
269
|
-
{ header: 'created', value: (h) => h.createdAt?.slice?.(0, 19)?.replace('T', ' ') },
|
|
270
|
-
]);
|
|
271
|
-
info(color.dim(`${r.pagination?.total ?? 0} total`));
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
history
|
|
275
|
-
.command('get <id>')
|
|
276
|
-
.description('Show one history entry')
|
|
277
|
-
.option('--wait', 'poll until the task finishes')
|
|
278
|
-
.option('--timeout <seconds>', 'wait timeout', '900')
|
|
279
|
-
.option('--interval <seconds>', 'poll interval', '5')
|
|
280
|
-
.action(async (id, opts) => {
|
|
281
|
-
const client = new WjiClient(globals());
|
|
282
|
-
let history;
|
|
283
|
-
let assets;
|
|
284
|
-
if (opts.wait) {
|
|
285
|
-
;
|
|
286
|
-
({ history, assets } = await pollHistory(client, id, {
|
|
287
|
-
timeoutMs: Number(opts.timeout) * 1000,
|
|
288
|
-
intervalMs: Number(opts.interval) * 1000,
|
|
289
|
-
}));
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
history = await client.get(`/ai/assets/history/list/${id}`, { detail: detailLevel() });
|
|
293
|
-
assets = await resolveAssets(client, history);
|
|
294
|
-
}
|
|
295
|
-
render({ ...history, images: assets }, () => reportHistory(history, assets));
|
|
296
|
-
});
|
|
297
|
-
history
|
|
298
|
-
.command('bookmark <id>')
|
|
299
|
-
.description('Bookmark or unbookmark an entry')
|
|
300
|
-
.option('--off', 'remove the bookmark')
|
|
301
|
-
.action(async (id, opts) => {
|
|
302
|
-
const client = new WjiClient(globals());
|
|
303
|
-
const result = await client.put(`/ai/assets/history/list/${id}/bookmark`, {
|
|
304
|
-
bookmarked: !opts.off,
|
|
305
|
-
});
|
|
306
|
-
render(result, () => success(opts.off ? 'Bookmark removed' : 'Bookmarked'));
|
|
307
|
-
});
|
|
308
|
-
history
|
|
309
|
-
.command('rm <id>')
|
|
310
|
-
.description('Delete a history entry')
|
|
311
|
-
.option('-y, --yes', 'skip confirmation')
|
|
312
|
-
.action(async (id, opts) => {
|
|
313
|
-
const client = new WjiClient(globals());
|
|
314
|
-
const h = await client.get(`/ai/assets/history/list/${id}`).catch(() => null);
|
|
315
|
-
const about = [h?.mode, h?.model, h?.prompt ? `"${truncate(h.prompt, 40)}"` : undefined]
|
|
316
|
-
.filter(Boolean)
|
|
317
|
-
.join(' · ');
|
|
318
|
-
if (!(await confirm(`Delete history entry ${id}${about ? ` (${about})` : ''}?`, opts.yes))) {
|
|
319
|
-
return info('Aborted');
|
|
320
|
-
}
|
|
321
|
-
const result = await client.delete(`/ai/assets/history/list/${id}`);
|
|
322
|
-
render(result, () => success(`Deleted ${id}`));
|
|
323
|
-
});
|
|
324
|
-
history
|
|
325
|
-
.command('download <id>')
|
|
326
|
-
.description('Download every finished asset in a history entry')
|
|
327
|
-
.option('-o, --out <dir>', 'output directory', '.')
|
|
328
|
-
.action(async (id, opts) => {
|
|
329
|
-
const client = new WjiClient(globals());
|
|
330
|
-
const entry = await client.get(`/ai/assets/history/list/${id}`);
|
|
331
|
-
const ready = (await resolveAssets(client, entry)).filter((a) => a.url && stateOf(a) === 'ready');
|
|
332
|
-
if (!ready.length)
|
|
333
|
-
return warn('Nothing to download yet');
|
|
334
|
-
const { createWriteStream } = await import('node:fs');
|
|
335
|
-
const { Readable } = await import('node:stream');
|
|
336
|
-
const { pipeline } = await import('node:stream/promises');
|
|
337
|
-
const { join } = await import('node:path');
|
|
338
|
-
const saved = [];
|
|
339
|
-
for (const a of ready) {
|
|
340
|
-
const res = await fetch(a.url);
|
|
341
|
-
if (!res.ok || !res.body) {
|
|
342
|
-
warn(`Skipped ${a.id}: ${res.status}`);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
const ext = /\.([a-z0-9]{2,5})(?:\?|$)/i.exec(a.url)?.[1] || 'bin';
|
|
346
|
-
const target = join(opts.out, `${a.id}.${ext}`);
|
|
347
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(target));
|
|
348
|
-
saved.push(target);
|
|
349
|
-
if (!isJsonMode())
|
|
350
|
-
success(`Saved ${target}`);
|
|
351
|
-
}
|
|
352
|
-
if (isJsonMode())
|
|
353
|
-
printJson({ id, files: saved });
|
|
354
|
-
});
|
|
355
|
-
return history;
|
|
356
|
-
}
|
package/dist/commands/model.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { WjiClient } from '../core/client.js';
|
|
3
|
-
import { color, detail, detailLevel, info, render, table, truncate, warn } from '../core/output.js';
|
|
4
|
-
const FEATURES = [
|
|
5
|
-
'text',
|
|
6
|
-
'image',
|
|
7
|
-
'video',
|
|
8
|
-
'audio',
|
|
9
|
-
'tts',
|
|
10
|
-
'web',
|
|
11
|
-
'thinking',
|
|
12
|
-
'function_calling',
|
|
13
|
-
'image_vision',
|
|
14
|
-
'video_vision',
|
|
15
|
-
'structured_output',
|
|
16
|
-
'digital_host',
|
|
17
|
-
'upscale_video',
|
|
18
|
-
];
|
|
19
|
-
/**
|
|
20
|
-
* The endpoint filters with `features.some(...)`, so an empty feature list
|
|
21
|
-
* matches nothing. Asking for every feature is the only way to "list all".
|
|
22
|
-
*/
|
|
23
|
-
export async function fetchModels(client, opts) {
|
|
24
|
-
const features = opts.features?.length ? opts.features : FEATURES;
|
|
25
|
-
const list = await client.get(`/ai/model/list/${features.join(',')}`, {
|
|
26
|
-
workspaceId: client.workspace(opts.workspace),
|
|
27
|
-
detail: opts.detail,
|
|
28
|
-
});
|
|
29
|
-
return Array.isArray(list) ? list : list?.data || [];
|
|
30
|
-
}
|
|
31
|
-
export function modelCommand(globals) {
|
|
32
|
-
const models = new Command('models').aliases(['model']).description('List available AI models');
|
|
33
|
-
models
|
|
34
|
-
.command('list', { isDefault: true })
|
|
35
|
-
.description('List models, optionally filtered by feature')
|
|
36
|
-
.option('-f, --feature <feature...>', `filter: ${FEATURES.join(', ')}`)
|
|
37
|
-
.option('-w, --workspace <id>', 'resolve pricing/access for a workspace')
|
|
38
|
-
.option('-k, --keyword <text>', 'match name, nickname or description')
|
|
39
|
-
.option('--all', 'include hidden and disabled models')
|
|
40
|
-
.action(async (opts) => {
|
|
41
|
-
const client = new WjiClient(globals());
|
|
42
|
-
let rows = await fetchModels(client, {
|
|
43
|
-
features: opts.feature,
|
|
44
|
-
workspace: opts.workspace,
|
|
45
|
-
detail: detailLevel(),
|
|
46
|
-
});
|
|
47
|
-
if (!opts.all)
|
|
48
|
-
rows = rows.filter((m) => m.enabled !== false && !m.hidden);
|
|
49
|
-
if (opts.keyword) {
|
|
50
|
-
const needle = opts.keyword.toLowerCase();
|
|
51
|
-
rows = rows.filter((m) => [m.name, m.nickname, m.description].some((v) => String(v || '').toLowerCase().includes(needle)));
|
|
52
|
-
}
|
|
53
|
-
// The client endpoint returns a curated model view — no rate, tags or id —
|
|
54
|
-
// so the useful columns come out of the form schema instead.
|
|
55
|
-
render(rows, () => {
|
|
56
|
-
table(rows, [
|
|
57
|
-
{ header: 'model', value: (m) => m.nickname },
|
|
58
|
-
{ header: 'name', value: (m) => (m.name === m.nickname ? '' : m.name) },
|
|
59
|
-
{ header: 'features', value: (m) => truncate((m.features || []).join(','), 22) },
|
|
60
|
-
{ header: 'ratios', value: (m) => truncate((m.form?.aspectRatio || []).join(' '), 30) },
|
|
61
|
-
{ header: 'max n', value: (m) => m.form?.batch?.max },
|
|
62
|
-
{ header: 'refs', value: (m) => (m.form?.reference?.enable ? m.form.reference.max : '') },
|
|
63
|
-
{ header: 'duration', value: (m) => durationLabel(m.form?.video?.duration) },
|
|
64
|
-
{ header: 'params', value: (m) => truncate(Object.keys(m.form?.other || {}).join(','), 34) },
|
|
65
|
-
]);
|
|
66
|
-
info(color.dim(`${rows.length} models · pass the MODEL column to \`-m\``));
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
models
|
|
70
|
-
.command('get <name>')
|
|
71
|
-
.description('Show a model, including its form parameter schema')
|
|
72
|
-
.option('-w, --workspace <id>', 'resolve pricing/access for a workspace')
|
|
73
|
-
.action(async (name, opts) => {
|
|
74
|
-
const client = new WjiClient(globals());
|
|
75
|
-
const rows = await fetchModels(client, { workspace: opts.workspace, detail: detailLevel() });
|
|
76
|
-
// `name` is not unique across providers; `nickname` is, so prefer it.
|
|
77
|
-
const matches = rows.filter((m) => m.nickname === name || m.name === name);
|
|
78
|
-
if (!matches.length)
|
|
79
|
-
throw new Error(`No model named "${name}". Try \`wji models list\`.`);
|
|
80
|
-
const model = matches.find((m) => m.nickname === name) ?? matches[0];
|
|
81
|
-
if (matches.length > 1) {
|
|
82
|
-
warn(`${matches.length} models share the name "${name}" — showing "${model.nickname}". ` +
|
|
83
|
-
`Others: ${matches
|
|
84
|
-
.filter((m) => m !== model)
|
|
85
|
-
.map((m) => m.nickname)
|
|
86
|
-
.join(', ')}`);
|
|
87
|
-
}
|
|
88
|
-
render(model, (m) => {
|
|
89
|
-
const form = m.form || {};
|
|
90
|
-
detail([
|
|
91
|
-
['name', m.name],
|
|
92
|
-
['nickname', m.nickname],
|
|
93
|
-
['description', m.description],
|
|
94
|
-
['features', (m.features || []).join(', ')],
|
|
95
|
-
['aspectRatio', (form.aspectRatio || []).join(' ')],
|
|
96
|
-
['n (--n)', form.batch ? `${form.batch.min}-${form.batch.max} (default ${form.batch.default})` : null],
|
|
97
|
-
['references', form.reference?.enable ? `up to ${form.reference.max}` : 'not supported'],
|
|
98
|
-
['duration (-d)', durationLabel(form.video?.duration)],
|
|
99
|
-
['video frames', (form.video?.support || []).join(', ')],
|
|
100
|
-
['accepted', (form.props?.filter || []).join(', ')],
|
|
101
|
-
['required', (form.props?.required || []).join(', ')],
|
|
102
|
-
]);
|
|
103
|
-
printOtherParams(form.other);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
models
|
|
107
|
-
.command('features')
|
|
108
|
-
.description('List the feature flags accepted by --feature')
|
|
109
|
-
.action(() => render(FEATURES, (f) => f.forEach((x) => process.stdout.write(x + '\n'))));
|
|
110
|
-
return models;
|
|
111
|
-
}
|
|
112
|
-
function durationLabel(duration) {
|
|
113
|
-
if (!duration?.enable)
|
|
114
|
-
return null;
|
|
115
|
-
return `${duration.min}-${duration.max}s (default ${duration.default})`;
|
|
116
|
-
}
|
|
117
|
-
/** `form.other` holds the model-specific knobs reachable through `--set`. */
|
|
118
|
-
function printOtherParams(other) {
|
|
119
|
-
const entries = Object.entries(other || {});
|
|
120
|
-
if (!entries.length)
|
|
121
|
-
return;
|
|
122
|
-
info('');
|
|
123
|
-
info(color.bold('model parameters (pass as --set <key>=<value>):'));
|
|
124
|
-
for (const [key, raw] of entries) {
|
|
125
|
-
const spec = raw;
|
|
126
|
-
const options = spec?.options
|
|
127
|
-
? ` [${spec.options.map((o) => o?.value ?? o).join(' | ')}]`
|
|
128
|
-
: spec?.type === 'boolean'
|
|
129
|
-
? ' [true | false]'
|
|
130
|
-
: '';
|
|
131
|
-
const fallback = spec?.default != null ? ` (default ${spec.default})` : '';
|
|
132
|
-
process.stdout.write(` ${color.cyan(key)}${options}${color.dim(fallback)}\n`);
|
|
133
|
-
if (spec?.description)
|
|
134
|
-
process.stdout.write(` ${color.dim(spec.description)}\n`);
|
|
135
|
-
}
|
|
136
|
-
}
|