@gobi-ai/cli 0.9.8 → 0.9.9
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.
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"name": "gobi-ai"
|
|
5
5
|
},
|
|
6
6
|
"description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
|
|
7
|
-
"version": "0.9.
|
|
7
|
+
"version": "0.9.9",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "gobi",
|
|
11
11
|
"description": "Manage the Gobi collaborative knowledge platform from the command line. Search and ask brains, publish brain documents, create threads, manage sessions, generate images and videos.",
|
|
12
|
-
"version": "0.9.
|
|
12
|
+
"version": "0.9.9",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/commands/media.js
CHANGED
|
@@ -19,6 +19,48 @@ async function pollStatus(path, terminalStates, intervalMs = 3000) {
|
|
|
19
19
|
}
|
|
20
20
|
throw new Error(`Polling timed out after ${POLL_MAX_DURATION_MS / 1000}s`);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Download a video binary from the media-gen download endpoint.
|
|
24
|
+
* Handles three cases:
|
|
25
|
+
* 1. Direct binary response (redirect: "follow" returns the file)
|
|
26
|
+
* 2. JSON response with downloadUrl (need to fetch that URL)
|
|
27
|
+
* 3. Redirect (302) with Location header
|
|
28
|
+
*/
|
|
29
|
+
async function downloadVideoToFile(videoId, outputPath) {
|
|
30
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
31
|
+
const { dirname } = await import("path");
|
|
32
|
+
const token = await getValidToken();
|
|
33
|
+
const dlUrl = `${BASE_URL}/media-gen/videos/${videoId}/download`;
|
|
34
|
+
// Try following redirects first
|
|
35
|
+
const res = await fetch(dlUrl, {
|
|
36
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
37
|
+
redirect: "follow",
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new ApiError(res.status, `/media-gen/videos/${videoId}/download`, await res.text());
|
|
41
|
+
}
|
|
42
|
+
const ct = res.headers.get("content-type") || "";
|
|
43
|
+
// If the response is JSON, extract downloadUrl and fetch the actual binary
|
|
44
|
+
if (ct.includes("application/json")) {
|
|
45
|
+
const json = (await res.json());
|
|
46
|
+
const inner = (json.data || json);
|
|
47
|
+
const url = (inner.downloadUrl || inner.download_url || inner.url);
|
|
48
|
+
if (!url)
|
|
49
|
+
throw new Error("Download endpoint returned JSON without a downloadUrl");
|
|
50
|
+
const videoRes = await fetch(url);
|
|
51
|
+
if (!videoRes.ok)
|
|
52
|
+
throw new Error(`Failed to fetch video from ${url}: ${videoRes.status}`);
|
|
53
|
+
const buffer = Buffer.from(await videoRes.arrayBuffer());
|
|
54
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
55
|
+
await writeFile(outputPath, buffer);
|
|
56
|
+
return { contentType: videoRes.headers.get("content-type") || "video/mp4", size: buffer.length };
|
|
57
|
+
}
|
|
58
|
+
// Direct binary response
|
|
59
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
60
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
61
|
+
await writeFile(outputPath, buffer);
|
|
62
|
+
return { contentType: ct || "video/mp4", size: buffer.length };
|
|
63
|
+
}
|
|
22
64
|
function extractImageUrl(data) {
|
|
23
65
|
return (data.downloadUrl || data.download_url || data.url);
|
|
24
66
|
}
|
|
@@ -136,56 +178,22 @@ export function registerMediaCommand(program) {
|
|
|
136
178
|
body.backgroundMediaId = opts.backgroundMediaId;
|
|
137
179
|
const resp = (await apiPost("/media-gen/videos", body));
|
|
138
180
|
let data = unwrapResp(resp);
|
|
139
|
-
const videoId = data.id || data.videoId;
|
|
181
|
+
const videoId = data.id || data.videoId || data.jobId;
|
|
140
182
|
if (shouldWait && videoId) {
|
|
141
183
|
console.log(`Video ${videoId} queued — polling for completion…`);
|
|
142
184
|
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
143
185
|
}
|
|
186
|
+
// After polling, the status response may contain the real videoId for download
|
|
187
|
+
const downloadId = data.videoId || data.id || videoId;
|
|
144
188
|
// Download video to file if -o specified
|
|
145
|
-
if (opts.output &&
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
150
|
-
redirect: "follow",
|
|
151
|
-
});
|
|
152
|
-
if (dlRes.ok) {
|
|
153
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
154
|
-
const { dirname } = await import("path");
|
|
155
|
-
const buffer = Buffer.from(await dlRes.arrayBuffer());
|
|
156
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
157
|
-
await writeFile(opts.output, buffer);
|
|
158
|
-
const contentType = dlRes.headers.get("content-type") || "video/mp4";
|
|
159
|
-
if (isJsonMode(media)) {
|
|
160
|
-
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
|
|
189
|
+
if (opts.output && downloadId && data.status === "inference_complete") {
|
|
190
|
+
const { contentType, size } = await downloadVideoToFile(downloadId, opts.output);
|
|
191
|
+
if (isJsonMode(media)) {
|
|
192
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
164
193
|
return;
|
|
165
194
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
169
|
-
redirect: "manual",
|
|
170
|
-
});
|
|
171
|
-
const location = dlRes2.headers.get("location");
|
|
172
|
-
if (location) {
|
|
173
|
-
const videoRes = await fetch(location);
|
|
174
|
-
if (videoRes.ok) {
|
|
175
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
176
|
-
const { dirname } = await import("path");
|
|
177
|
-
const buffer = Buffer.from(await videoRes.arrayBuffer());
|
|
178
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
179
|
-
await writeFile(opts.output, buffer);
|
|
180
|
-
const contentType = videoRes.headers.get("content-type") || "video/mp4";
|
|
181
|
-
if (isJsonMode(media)) {
|
|
182
|
-
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
195
|
+
console.log(`Video saved to ${opts.output} (${size} bytes)`);
|
|
196
|
+
return;
|
|
189
197
|
}
|
|
190
198
|
if (isJsonMode(media)) {
|
|
191
199
|
jsonOut(data);
|
|
@@ -193,10 +201,10 @@ export function registerMediaCommand(program) {
|
|
|
193
201
|
}
|
|
194
202
|
const status = data.status || "queued";
|
|
195
203
|
console.log(`Video created!\n` +
|
|
196
|
-
` ID: ${
|
|
204
|
+
` ID: ${downloadId}\n` +
|
|
197
205
|
` Status: ${status}`);
|
|
198
206
|
if (status === "inference_complete") {
|
|
199
|
-
console.log(` Download: gobi media video-download ${
|
|
207
|
+
console.log(` Download: gobi media video-download ${downloadId}`);
|
|
200
208
|
}
|
|
201
209
|
});
|
|
202
210
|
media
|
|
@@ -244,49 +252,14 @@ export function registerMediaCommand(program) {
|
|
|
244
252
|
const data = await pollStatus(`/media-gen/videos/${id}/status`, ["inference_complete", "inference_failed"]);
|
|
245
253
|
// Download if -o specified and completed
|
|
246
254
|
if (opts.output && data.status === "inference_complete") {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
redirect: "follow",
|
|
252
|
-
});
|
|
253
|
-
if (dlRes.ok) {
|
|
254
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
255
|
-
const { dirname } = await import("path");
|
|
256
|
-
const buffer = Buffer.from(await dlRes.arrayBuffer());
|
|
257
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
258
|
-
await writeFile(opts.output, buffer);
|
|
259
|
-
const contentType = dlRes.headers.get("content-type") || "video/mp4";
|
|
260
|
-
if (isJsonMode(media)) {
|
|
261
|
-
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${buffer.length} bytes)`);
|
|
255
|
+
const dlId = (data.videoId || data.id || id);
|
|
256
|
+
const { contentType, size } = await downloadVideoToFile(dlId, opts.output);
|
|
257
|
+
if (isJsonMode(media)) {
|
|
258
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
265
259
|
return;
|
|
266
260
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
270
|
-
redirect: "manual",
|
|
271
|
-
});
|
|
272
|
-
const location = dlRes2.headers.get("location");
|
|
273
|
-
if (location) {
|
|
274
|
-
const videoRes = await fetch(location);
|
|
275
|
-
if (videoRes.ok) {
|
|
276
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
277
|
-
const { dirname } = await import("path");
|
|
278
|
-
const buffer = Buffer.from(await videoRes.arrayBuffer());
|
|
279
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
280
|
-
await writeFile(opts.output, buffer);
|
|
281
|
-
const contentType = videoRes.headers.get("content-type") || "video/mp4";
|
|
282
|
-
if (isJsonMode(media)) {
|
|
283
|
-
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${buffer.length} bytes)`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
261
|
+
console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${size} bytes)`);
|
|
262
|
+
return;
|
|
290
263
|
}
|
|
291
264
|
if (isJsonMode(media)) {
|
|
292
265
|
jsonOut(data);
|
|
@@ -312,48 +285,13 @@ export function registerMediaCommand(program) {
|
|
|
312
285
|
const url = `${BASE_URL}/media-gen/videos/${id}/download`;
|
|
313
286
|
// If -o specified, download directly to file
|
|
314
287
|
if (opts.output) {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
if (res.ok) {
|
|
320
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
321
|
-
const { dirname } = await import("path");
|
|
322
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
323
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
324
|
-
await writeFile(opts.output, buffer);
|
|
325
|
-
const contentType = res.headers.get("content-type") || "video/mp4";
|
|
326
|
-
if (isJsonMode(media)) {
|
|
327
|
-
jsonOut({ filename: opts.output, contentType, size: buffer.length });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
|
|
288
|
+
const { contentType, size } = await downloadVideoToFile(id, opts.output);
|
|
289
|
+
if (isJsonMode(media)) {
|
|
290
|
+
jsonOut({ filename: opts.output, contentType, size });
|
|
331
291
|
return;
|
|
332
292
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
336
|
-
redirect: "manual",
|
|
337
|
-
});
|
|
338
|
-
const location = res2.headers.get("location");
|
|
339
|
-
if (location) {
|
|
340
|
-
const videoRes = await fetch(location);
|
|
341
|
-
if (videoRes.ok) {
|
|
342
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
343
|
-
const { dirname } = await import("path");
|
|
344
|
-
const buffer = Buffer.from(await videoRes.arrayBuffer());
|
|
345
|
-
await mkdir(dirname(opts.output), { recursive: true });
|
|
346
|
-
await writeFile(opts.output, buffer);
|
|
347
|
-
const contentType = videoRes.headers.get("content-type") || "video/mp4";
|
|
348
|
-
if (isJsonMode(media)) {
|
|
349
|
-
jsonOut({ filename: opts.output, contentType, size: buffer.length });
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
throw new ApiError(res.status, `/media-gen/videos/${id}/download`, "Failed to download video");
|
|
293
|
+
console.log(`Video saved to ${opts.output} (${size} bytes)`);
|
|
294
|
+
return;
|
|
357
295
|
}
|
|
358
296
|
// No -o: just return the URL (existing behavior)
|
|
359
297
|
const res = await fetch(url, {
|
|
@@ -384,6 +322,221 @@ export function registerMediaCommand(program) {
|
|
|
384
322
|
console.log(`Download URL for video ${id}:\n ${data.url || data.downloadUrl || JSON.stringify(data)}`);
|
|
385
323
|
});
|
|
386
324
|
// ════════════════════════════════════════════════════════════════════
|
|
325
|
+
// Cinematic Video
|
|
326
|
+
// ════════════════════════════════════════════════════════════════════
|
|
327
|
+
media
|
|
328
|
+
.command("cinematic-create")
|
|
329
|
+
.description("Create a cinematic video from a text prompt.")
|
|
330
|
+
.requiredOption("--prompt <prompt>", "Text prompt describing the video")
|
|
331
|
+
.option("--name <name>", "Name for the video (auto-generated if omitted)")
|
|
332
|
+
.option("--aspect-ratio <aspectRatio>", "Aspect ratio: 16:9, 9:16, 1:1")
|
|
333
|
+
.option("--duration <seconds>", "Duration in seconds (4-8)")
|
|
334
|
+
.option("--resolution <resolution>", "Resolution: 720p, 1080p")
|
|
335
|
+
.option("--enhance-prompt", "Enhance the prompt with AI")
|
|
336
|
+
.option("--generate-audio", "Generate audio for the video")
|
|
337
|
+
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
338
|
+
.option("--sample-count <count>", "Number of samples (1-4)")
|
|
339
|
+
.option("--first-frame-media-id <mediaId>", "First frame image media ID")
|
|
340
|
+
.option("--last-frame-media-id <mediaId>", "Last frame image media ID")
|
|
341
|
+
.option("--reference-media-ids <ids>", "Comma-separated reference image media IDs (max 3)")
|
|
342
|
+
.option("--wait", "Poll until generation completes")
|
|
343
|
+
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
346
|
+
const autoName = opts.name || `cinematic-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
347
|
+
const body = {
|
|
348
|
+
name: autoName,
|
|
349
|
+
prompt: opts.prompt,
|
|
350
|
+
};
|
|
351
|
+
if (opts.aspectRatio)
|
|
352
|
+
body.aspectRatio = opts.aspectRatio;
|
|
353
|
+
if (opts.duration) {
|
|
354
|
+
const v = parseInt(opts.duration, 10);
|
|
355
|
+
if (Number.isNaN(v))
|
|
356
|
+
throw new Error("--duration must be a number");
|
|
357
|
+
body.durationSeconds = v;
|
|
358
|
+
}
|
|
359
|
+
if (opts.resolution)
|
|
360
|
+
body.resolution = opts.resolution;
|
|
361
|
+
if (opts.enhancePrompt)
|
|
362
|
+
body.enhancePrompt = true;
|
|
363
|
+
if (opts.generateAudio)
|
|
364
|
+
body.generateAudio = true;
|
|
365
|
+
if (opts.negativePrompt)
|
|
366
|
+
body.negativePrompt = opts.negativePrompt;
|
|
367
|
+
if (opts.sampleCount) {
|
|
368
|
+
const v = parseInt(opts.sampleCount, 10);
|
|
369
|
+
if (Number.isNaN(v))
|
|
370
|
+
throw new Error("--sample-count must be a number");
|
|
371
|
+
body.sampleCount = v;
|
|
372
|
+
}
|
|
373
|
+
if (opts.firstFrameMediaId)
|
|
374
|
+
body.firstFrameImageMediaId = opts.firstFrameMediaId;
|
|
375
|
+
if (opts.lastFrameMediaId)
|
|
376
|
+
body.lastFrameImageMediaId = opts.lastFrameMediaId;
|
|
377
|
+
if (opts.referenceMediaIds)
|
|
378
|
+
body.referenceImageMediaIds = opts.referenceMediaIds.split(",").map((s) => s.trim());
|
|
379
|
+
const resp = (await apiPost("/media-gen/videos/cinematic", body));
|
|
380
|
+
let data = unwrapResp(resp);
|
|
381
|
+
const videoId = data.id || data.videoId || data.jobId;
|
|
382
|
+
if (shouldWait && videoId) {
|
|
383
|
+
console.log(`Cinematic video ${videoId} queued — polling for completion…`);
|
|
384
|
+
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
385
|
+
}
|
|
386
|
+
// After polling, the status response may contain the real videoId for download
|
|
387
|
+
const downloadId = data.videoId || data.id || videoId;
|
|
388
|
+
// Download video to file if -o specified
|
|
389
|
+
if (opts.output && downloadId && data.status === "inference_complete") {
|
|
390
|
+
const { contentType, size } = await downloadVideoToFile(downloadId, opts.output);
|
|
391
|
+
if (isJsonMode(media)) {
|
|
392
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(`Cinematic video saved to ${opts.output} (${size} bytes)`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (isJsonMode(media)) {
|
|
399
|
+
jsonOut(data);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const status = data.status || "queued";
|
|
403
|
+
console.log(`Cinematic video created!\n` +
|
|
404
|
+
` ID: ${downloadId}\n` +
|
|
405
|
+
` Status: ${status}`);
|
|
406
|
+
if (status === "inference_complete") {
|
|
407
|
+
console.log(` Download: gobi media video-download ${downloadId}`);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// ════════════════════════════════════════════════════════════════════
|
|
411
|
+
// Custom Avatars
|
|
412
|
+
// ════════════════════════════════════════════════════════════════════
|
|
413
|
+
media
|
|
414
|
+
.command("avatar-design")
|
|
415
|
+
.description("Start a design-your-avatar job.")
|
|
416
|
+
.requiredOption("--name <name>", "Name for the avatar")
|
|
417
|
+
.requiredOption("--gender <gender>", "Gender for the avatar design")
|
|
418
|
+
.requiredOption("--age <age>", "Age range for the avatar")
|
|
419
|
+
.requiredOption("--ethnicity <ethnicity>", "Ethnicity for the avatar")
|
|
420
|
+
.requiredOption("--outfit <outfit>", "Outfit description")
|
|
421
|
+
.requiredOption("--background <background>", "Background description")
|
|
422
|
+
.option("--no-portrait", "Generate full-body instead of portrait")
|
|
423
|
+
.option("--audio-media-id <mediaId>", "Custom voice audio media ID")
|
|
424
|
+
.option("--wait", "Poll until variants are ready")
|
|
425
|
+
.action(async (opts) => {
|
|
426
|
+
const body = {
|
|
427
|
+
name: opts.name,
|
|
428
|
+
gender: opts.gender,
|
|
429
|
+
age: opts.age,
|
|
430
|
+
ethnicity: opts.ethnicity,
|
|
431
|
+
outfit: opts.outfit,
|
|
432
|
+
background: opts.background,
|
|
433
|
+
isPortrait: opts.portrait,
|
|
434
|
+
};
|
|
435
|
+
if (opts.audioMediaId)
|
|
436
|
+
body.audioMediaId = opts.audioMediaId;
|
|
437
|
+
const resp = (await apiPost("/media-gen/avatars/design", body));
|
|
438
|
+
let data = unwrapResp(resp);
|
|
439
|
+
const jobId = data.jobId || data.id;
|
|
440
|
+
if (opts.wait && jobId) {
|
|
441
|
+
console.log(`Avatar design job ${jobId} — polling for completion…`);
|
|
442
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
443
|
+
}
|
|
444
|
+
if (isJsonMode(media)) {
|
|
445
|
+
jsonOut(data);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const status = data.status || "queued";
|
|
449
|
+
console.log(`Avatar design started!\n` +
|
|
450
|
+
` Job ID: ${jobId}\n` +
|
|
451
|
+
` Status: ${status}`);
|
|
452
|
+
if (status === "variants_ready") {
|
|
453
|
+
console.log(` Confirm: gobi media avatar-confirm --job-id ${jobId}`);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
media
|
|
457
|
+
.command("avatar-confirm")
|
|
458
|
+
.description("Confirm avatar variant(s) after design.")
|
|
459
|
+
.requiredOption("--job-id <jobId>", "Job ID from avatar-design")
|
|
460
|
+
.option("--variant <variant>", "Variant to confirm (1 or 2); omit to confirm both")
|
|
461
|
+
.action(async (opts) => {
|
|
462
|
+
const body = { jobId: opts.jobId };
|
|
463
|
+
if (opts.variant) {
|
|
464
|
+
const v = parseInt(opts.variant, 10);
|
|
465
|
+
if (Number.isNaN(v))
|
|
466
|
+
throw new Error("--variant must be a number (1 or 2)");
|
|
467
|
+
body.variant = v;
|
|
468
|
+
}
|
|
469
|
+
const resp = (await apiPost("/media-gen/avatars/confirm", body));
|
|
470
|
+
const data = unwrapResp(resp);
|
|
471
|
+
if (isJsonMode(media)) {
|
|
472
|
+
jsonOut(data);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const avatarId = data.avatarId || data.id;
|
|
476
|
+
console.log(`Avatar confirmed!\n` +
|
|
477
|
+
` Avatar ID: ${avatarId || JSON.stringify(data)}`);
|
|
478
|
+
});
|
|
479
|
+
media
|
|
480
|
+
.command("avatar-from-selfie")
|
|
481
|
+
.description("Create an avatar from a selfie (instant or enhanced with prompt).")
|
|
482
|
+
.requiredOption("--name <name>", "Name for the avatar")
|
|
483
|
+
.requiredOption("--photo-media-id <mediaId>", "Selfie photo media ID")
|
|
484
|
+
.option("--prompt <prompt>", "Enhancement prompt (triggers async enhance flow)")
|
|
485
|
+
.option("--audio-media-id <mediaId>", "Custom voice audio media ID")
|
|
486
|
+
.option("--wait", "Poll until job completes (only for enhance flow)")
|
|
487
|
+
.action(async (opts) => {
|
|
488
|
+
const body = {
|
|
489
|
+
name: opts.name,
|
|
490
|
+
photoMediaId: opts.photoMediaId,
|
|
491
|
+
};
|
|
492
|
+
if (opts.prompt)
|
|
493
|
+
body.prompt = opts.prompt;
|
|
494
|
+
if (opts.audioMediaId)
|
|
495
|
+
body.audioMediaId = opts.audioMediaId;
|
|
496
|
+
const resp = (await apiPost("/media-gen/avatars/from-selfie", body));
|
|
497
|
+
let data = unwrapResp(resp);
|
|
498
|
+
const jobId = data.jobId || data.id;
|
|
499
|
+
// Enhance flow is async — poll if --wait
|
|
500
|
+
if (opts.wait && opts.prompt && jobId) {
|
|
501
|
+
console.log(`Avatar enhance job ${jobId} — polling for completion…`);
|
|
502
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
503
|
+
}
|
|
504
|
+
if (isJsonMode(media)) {
|
|
505
|
+
jsonOut(data);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (opts.prompt) {
|
|
509
|
+
const status = data.status || "queued";
|
|
510
|
+
console.log(`Avatar enhance started!\n` +
|
|
511
|
+
` Job ID: ${jobId}\n` +
|
|
512
|
+
` Status: ${status}`);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
const avatarId = data.avatarId || data.id;
|
|
516
|
+
console.log(`Avatar created from selfie!\n` +
|
|
517
|
+
` Avatar ID: ${avatarId || JSON.stringify(data)}`);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
media
|
|
521
|
+
.command("avatar-job-status <jobId>")
|
|
522
|
+
.description("Check avatar job status.")
|
|
523
|
+
.option("--wait", "Poll until a terminal state is reached")
|
|
524
|
+
.action(async (jobId, opts) => {
|
|
525
|
+
let data;
|
|
526
|
+
if (opts.wait) {
|
|
527
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const resp = (await apiGet(`/media-gen/avatars/jobs/${jobId}/status`));
|
|
531
|
+
data = unwrapResp(resp);
|
|
532
|
+
}
|
|
533
|
+
if (isJsonMode(media)) {
|
|
534
|
+
jsonOut(data);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
console.log(`Avatar job ${jobId} — status: ${data.status || "unknown"}`);
|
|
538
|
+
});
|
|
539
|
+
// ════════════════════════════════════════════════════════════════════
|
|
387
540
|
// Images
|
|
388
541
|
// ════════════════════════════════════════════════════════════════════
|
|
389
542
|
media
|
package/package.json
CHANGED
|
@@ -3,8 +3,10 @@ name: gobi-media
|
|
|
3
3
|
description: >-
|
|
4
4
|
Gobi media generation: generate images from text prompts (thumbnails,
|
|
5
5
|
assets, logos), edit and inpaint images, create avatar videos with voice
|
|
6
|
-
narration,
|
|
7
|
-
|
|
6
|
+
narration, create cinematic videos from prompts, design custom avatars or
|
|
7
|
+
create avatars from selfies, list available avatars and voices, upload
|
|
8
|
+
media files. Use when the user wants to generate images, create videos,
|
|
9
|
+
or manage media.
|
|
8
10
|
allowed-tools: Bash(gobi:*)
|
|
9
11
|
metadata:
|
|
10
12
|
author: gobi-ai
|
|
@@ -65,18 +67,56 @@ gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>"
|
|
|
65
67
|
|
|
66
68
|
The `-o` flag implies `--wait` and downloads the video when done.
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
To use a custom image as the **background** of a video, upload it via `upload-init` / `upload-finalize`, then pass the mediaId as `--background-media-id`:
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
```bash
|
|
73
|
+
gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" --background-media-id "<MEDIA_ID>" -o media/<NAME>.mp4
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Typical Workflow (Cinematic Video)
|
|
77
|
+
|
|
78
|
+
Generate a cinematic video from a text prompt (no avatar needed):
|
|
71
79
|
|
|
72
80
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
curl -T "media/bg.png" -H "Content-Type: image/png" "<UPLOAD_URL>"
|
|
76
|
-
gobi --json media upload-finalize --media-id "<MEDIA_ID>"
|
|
81
|
+
gobi --json media cinematic-create --prompt "<PROMPT>" --aspect-ratio "<RATIO>" -o media/<NAME>.mp4
|
|
82
|
+
```
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
Options: `--duration <4-8>`, `--resolution <720p|1080p>`, `--enhance-prompt`, `--generate-audio`, `--negative-prompt`, `--sample-count <1-4>`, `--first-frame-media-id`, `--last-frame-media-id`, `--reference-media-ids`.
|
|
85
|
+
|
|
86
|
+
## Custom Avatars
|
|
87
|
+
|
|
88
|
+
Three ways to create custom avatars:
|
|
89
|
+
|
|
90
|
+
### 1. Design from scratch
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gobi --json media avatar-design --name "<NAME>" --gender "<GENDER>" --age "<AGE>" --ethnicity "<ETHNICITY>" --outfit "<OUTFIT>" --background "<BACKGROUND>" --wait
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
When `variants_ready`, confirm with:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
gobi --json media avatar-confirm --job-id "<JOB_ID>"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. From a selfie (instant)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
gobi --json media avatar-from-selfie --name "<NAME>" --photo-media-id "<MEDIA_ID>"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Upload the selfie first via `upload-init` / `upload-finalize` to get the media ID.
|
|
109
|
+
|
|
110
|
+
### 3. From a selfie (enhanced with prompt)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
gobi --json media avatar-from-selfie --name "<NAME>" --photo-media-id "<MEDIA_ID>" --prompt "<ENHANCEMENT>" --wait
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Check any avatar job status with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gobi --json media avatar-job-status <jobId> --wait
|
|
80
120
|
```
|
|
81
121
|
|
|
82
122
|
**IMPORTANT: After downloading, show the video using Obsidian wiki-link syntax EXACTLY like this:**
|
|
@@ -102,11 +142,19 @@ Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `
|
|
|
102
142
|
### Videos
|
|
103
143
|
|
|
104
144
|
- `gobi media video-create` — Create an avatar video generation job.
|
|
145
|
+
- `gobi media cinematic-create` — Create a cinematic video from a text prompt.
|
|
105
146
|
- `gobi media video-list` — List all videos.
|
|
106
147
|
- `gobi media video-get` — Get video metadata.
|
|
107
148
|
- `gobi media video-status` — Poll video generation status.
|
|
108
149
|
- `gobi media video-download` — Download a completed video (`-o` to save to file).
|
|
109
150
|
|
|
151
|
+
### Custom Avatars
|
|
152
|
+
|
|
153
|
+
- `gobi media avatar-design` — Start a design-your-avatar job.
|
|
154
|
+
- `gobi media avatar-confirm` — Confirm avatar variant(s) after design.
|
|
155
|
+
- `gobi media avatar-from-selfie` — Create an avatar from a selfie (instant or enhanced).
|
|
156
|
+
- `gobi media avatar-job-status` — Check avatar job status.
|
|
157
|
+
|
|
110
158
|
### Images
|
|
111
159
|
|
|
112
160
|
- `gobi media image-generate` — Generate an image from a text prompt. Types: image (default), thumbnail (YouTube-optimized), asset (logo/product). Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4
|
|
@@ -6,24 +6,29 @@ Usage: gobi media [options] [command]
|
|
|
6
6
|
Media generation commands (videos, images).
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
-h, --help
|
|
9
|
+
-h, --help display help for command
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
|
-
upload-init [options]
|
|
13
|
-
upload-finalize [options]
|
|
14
|
-
avatars
|
|
15
|
-
voices
|
|
16
|
-
video-create [options]
|
|
17
|
-
video-list
|
|
18
|
-
video-get <id>
|
|
19
|
-
video-status [options] <id>
|
|
20
|
-
video-download [options] <id>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
upload-init [options] Get a presigned upload URL for a media file.
|
|
13
|
+
upload-finalize [options] Confirm that a media upload is complete.
|
|
14
|
+
avatars List available avatars.
|
|
15
|
+
voices List available voices.
|
|
16
|
+
video-create [options] Create an avatar video generation job.
|
|
17
|
+
video-list List all videos.
|
|
18
|
+
video-get <id> Get video metadata.
|
|
19
|
+
video-status [options] <id> Poll video generation status.
|
|
20
|
+
video-download [options] <id> Download a completed video (or get its URL).
|
|
21
|
+
cinematic-create [options] Create a cinematic video from a text prompt.
|
|
22
|
+
avatar-design [options] Start a design-your-avatar job.
|
|
23
|
+
avatar-confirm [options] Confirm avatar variant(s) after design.
|
|
24
|
+
avatar-from-selfie [options] Create an avatar from a selfie (instant or enhanced with prompt).
|
|
25
|
+
avatar-job-status [options] <jobId> Check avatar job status.
|
|
26
|
+
image-generate [options] Generate an image from a text prompt. Types: image (default), thumbnail (YouTube-optimized), asset (logo/product). Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4
|
|
27
|
+
image-edit [options] Edit an existing image with a prompt (image-to-image).
|
|
28
|
+
image-inpaint [options] Inpaint an image region using a mask.
|
|
29
|
+
image-status [options] <jobId> Check image generation job status.
|
|
30
|
+
image-download [options] <jobId> Download a generated image.
|
|
31
|
+
help [command] display help for command
|
|
27
32
|
```
|
|
28
33
|
|
|
29
34
|
## upload-init
|
|
@@ -139,6 +144,92 @@ Options:
|
|
|
139
144
|
-h, --help display help for command
|
|
140
145
|
```
|
|
141
146
|
|
|
147
|
+
## cinematic-create
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Usage: gobi media cinematic-create [options]
|
|
151
|
+
|
|
152
|
+
Create a cinematic video from a text prompt.
|
|
153
|
+
|
|
154
|
+
Options:
|
|
155
|
+
--prompt <prompt> Text prompt describing the video
|
|
156
|
+
--name <name> Name for the video (auto-generated if omitted)
|
|
157
|
+
--aspect-ratio <aspectRatio> Aspect ratio: 16:9, 9:16, 1:1
|
|
158
|
+
--duration <seconds> Duration in seconds (4-8)
|
|
159
|
+
--resolution <resolution> Resolution: 720p, 1080p
|
|
160
|
+
--enhance-prompt Enhance the prompt with AI
|
|
161
|
+
--generate-audio Generate audio for the video
|
|
162
|
+
--negative-prompt <negativePrompt> Negative prompt
|
|
163
|
+
--sample-count <count> Number of samples (1-4)
|
|
164
|
+
--first-frame-media-id <mediaId> First frame image media ID
|
|
165
|
+
--last-frame-media-id <mediaId> Last frame image media ID
|
|
166
|
+
--reference-media-ids <ids> Comma-separated reference image media IDs (max 3)
|
|
167
|
+
--wait Poll until generation completes
|
|
168
|
+
-o, --output <path> Download video to this path when done (implies --wait)
|
|
169
|
+
-h, --help display help for command
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## avatar-design
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
Usage: gobi media avatar-design [options]
|
|
176
|
+
|
|
177
|
+
Start a design-your-avatar job.
|
|
178
|
+
|
|
179
|
+
Options:
|
|
180
|
+
--name <name> Name for the avatar
|
|
181
|
+
--gender <gender> Gender for the avatar design
|
|
182
|
+
--age <age> Age range for the avatar
|
|
183
|
+
--ethnicity <ethnicity> Ethnicity for the avatar
|
|
184
|
+
--outfit <outfit> Outfit description
|
|
185
|
+
--background <background> Background description
|
|
186
|
+
--no-portrait Generate full-body instead of portrait
|
|
187
|
+
--audio-media-id <mediaId> Custom voice audio media ID
|
|
188
|
+
--wait Poll until variants are ready
|
|
189
|
+
-h, --help display help for command
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## avatar-confirm
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Usage: gobi media avatar-confirm [options]
|
|
196
|
+
|
|
197
|
+
Confirm avatar variant(s) after design.
|
|
198
|
+
|
|
199
|
+
Options:
|
|
200
|
+
--job-id <jobId> Job ID from avatar-design
|
|
201
|
+
--variant <variant> Variant to confirm (1 or 2); omit to confirm both
|
|
202
|
+
-h, --help display help for command
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## avatar-from-selfie
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Usage: gobi media avatar-from-selfie [options]
|
|
209
|
+
|
|
210
|
+
Create an avatar from a selfie (instant or enhanced with prompt).
|
|
211
|
+
|
|
212
|
+
Options:
|
|
213
|
+
--name <name> Name for the avatar
|
|
214
|
+
--photo-media-id <mediaId> Selfie photo media ID
|
|
215
|
+
--prompt <prompt> Enhancement prompt (triggers async enhance flow)
|
|
216
|
+
--audio-media-id <mediaId> Custom voice audio media ID
|
|
217
|
+
--wait Poll until job completes (only for enhance flow)
|
|
218
|
+
-h, --help display help for command
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## avatar-job-status
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
Usage: gobi media avatar-job-status [options] <jobId>
|
|
225
|
+
|
|
226
|
+
Check avatar job status.
|
|
227
|
+
|
|
228
|
+
Options:
|
|
229
|
+
--wait Poll until a terminal state is reached
|
|
230
|
+
-h, --help display help for command
|
|
231
|
+
```
|
|
232
|
+
|
|
142
233
|
## image-generate
|
|
143
234
|
|
|
144
235
|
```
|