@gobi-ai/cli 0.9.8 → 0.9.10
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.10",
|
|
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.10",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/commands/media.js
CHANGED
|
@@ -19,6 +19,89 @@ 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
|
+
}
|
|
64
|
+
const MIME_MAP = {
|
|
65
|
+
".png": "image/png",
|
|
66
|
+
".jpg": "image/jpeg",
|
|
67
|
+
".jpeg": "image/jpeg",
|
|
68
|
+
".webp": "image/webp",
|
|
69
|
+
".gif": "image/gif",
|
|
70
|
+
".mp4": "video/mp4",
|
|
71
|
+
".mov": "video/quicktime",
|
|
72
|
+
".mp3": "audio/mpeg",
|
|
73
|
+
".wav": "audio/wav",
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Upload a local file and return its media ID.
|
|
77
|
+
* Handles init → PUT → finalize in one call.
|
|
78
|
+
*/
|
|
79
|
+
async function uploadFile(filePath) {
|
|
80
|
+
const { readFile, stat } = await import("fs/promises");
|
|
81
|
+
const { basename, extname } = await import("path");
|
|
82
|
+
const buffer = await readFile(filePath);
|
|
83
|
+
const fileName = basename(filePath);
|
|
84
|
+
const fileSize = (await stat(filePath)).size;
|
|
85
|
+
const ext = extname(filePath).toLowerCase();
|
|
86
|
+
const contentType = MIME_MAP[ext] || "application/octet-stream";
|
|
87
|
+
const initResp = (await apiPost("/media-gen/media/initialize", {
|
|
88
|
+
fileName, contentType, fileSize,
|
|
89
|
+
}));
|
|
90
|
+
const initData = unwrapResp(initResp);
|
|
91
|
+
const mediaId = initData.mediaId;
|
|
92
|
+
const uploadUrl = initData.uploadUrl;
|
|
93
|
+
if (!mediaId || !uploadUrl)
|
|
94
|
+
throw new Error("Upload init failed: missing mediaId or uploadUrl");
|
|
95
|
+
const putRes = await fetch(uploadUrl, {
|
|
96
|
+
method: "PUT",
|
|
97
|
+
headers: { "Content-Type": contentType },
|
|
98
|
+
body: buffer,
|
|
99
|
+
});
|
|
100
|
+
if (!putRes.ok)
|
|
101
|
+
throw new Error(`Upload PUT failed: ${putRes.status}`);
|
|
102
|
+
await apiPost("/media-gen/media/finalize", { mediaId });
|
|
103
|
+
return mediaId;
|
|
104
|
+
}
|
|
22
105
|
function extractImageUrl(data) {
|
|
23
106
|
return (data.downloadUrl || data.download_url || data.url);
|
|
24
107
|
}
|
|
@@ -30,44 +113,15 @@ export function registerMediaCommand(program) {
|
|
|
30
113
|
// Upload
|
|
31
114
|
// ════════════════════════════════════════════════════════════════════
|
|
32
115
|
media
|
|
33
|
-
.command("upload
|
|
34
|
-
.description("
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
.option("--file-size <fileSize>", "File size in bytes")
|
|
38
|
-
.action(async (opts) => {
|
|
39
|
-
const body = {
|
|
40
|
-
fileName: opts.fileName,
|
|
41
|
-
contentType: opts.contentType,
|
|
42
|
-
};
|
|
43
|
-
if (opts.fileSize)
|
|
44
|
-
body.fileSize = parseInt(opts.fileSize, 10);
|
|
45
|
-
const resp = (await apiPost("/media-gen/media/initialize", body));
|
|
46
|
-
const data = unwrapResp(resp);
|
|
116
|
+
.command("upload <file>")
|
|
117
|
+
.description("Upload a local file and return its media ID.")
|
|
118
|
+
.action(async (file) => {
|
|
119
|
+
const mediaId = await uploadFile(file);
|
|
47
120
|
if (isJsonMode(media)) {
|
|
48
|
-
jsonOut(
|
|
121
|
+
jsonOut({ mediaId });
|
|
49
122
|
return;
|
|
50
123
|
}
|
|
51
|
-
console.log(`
|
|
52
|
-
` Media ID: ${data.mediaId}\n` +
|
|
53
|
-
` Upload URL: ${data.uploadUrl}\n\n` +
|
|
54
|
-
`PUT your file to the upload URL, then run:\n` +
|
|
55
|
-
` gobi media upload-finalize --media-id ${data.mediaId}`);
|
|
56
|
-
});
|
|
57
|
-
media
|
|
58
|
-
.command("upload-finalize")
|
|
59
|
-
.description("Confirm that a media upload is complete.")
|
|
60
|
-
.requiredOption("--media-id <mediaId>", "Media ID from upload-init")
|
|
61
|
-
.action(async (opts) => {
|
|
62
|
-
const resp = (await apiPost("/media-gen/media/finalize", {
|
|
63
|
-
mediaId: opts.mediaId,
|
|
64
|
-
}));
|
|
65
|
-
const data = unwrapResp(resp);
|
|
66
|
-
if (isJsonMode(media)) {
|
|
67
|
-
jsonOut(data);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
console.log(`Upload finalized for media ${opts.mediaId}.`);
|
|
124
|
+
console.log(`Uploaded → Media ID: ${mediaId}`);
|
|
71
125
|
});
|
|
72
126
|
// ════════════════════════════════════════════════════════════════════
|
|
73
127
|
// Avatars & Voices
|
|
@@ -120,7 +174,7 @@ export function registerMediaCommand(program) {
|
|
|
120
174
|
.requiredOption("--avatar-id <avatarId>", "Avatar to use")
|
|
121
175
|
.requiredOption("--voice-id <voiceId>", "Voice to use")
|
|
122
176
|
.requiredOption("--script <script>", "Script for the avatar to read")
|
|
123
|
-
.option("--background
|
|
177
|
+
.option("--background <file>", "Background image file (auto-uploaded)")
|
|
124
178
|
.option("--wait", "Poll until generation completes")
|
|
125
179
|
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
126
180
|
.action(async (opts) => {
|
|
@@ -132,60 +186,26 @@ export function registerMediaCommand(program) {
|
|
|
132
186
|
voiceId: opts.voiceId,
|
|
133
187
|
script: opts.script,
|
|
134
188
|
};
|
|
135
|
-
if (opts.
|
|
136
|
-
body.backgroundMediaId = opts.
|
|
189
|
+
if (opts.background)
|
|
190
|
+
body.backgroundMediaId = await uploadFile(opts.background);
|
|
137
191
|
const resp = (await apiPost("/media-gen/videos", body));
|
|
138
192
|
let data = unwrapResp(resp);
|
|
139
|
-
const videoId = data.id || data.videoId;
|
|
193
|
+
const videoId = data.id || data.videoId || data.jobId;
|
|
140
194
|
if (shouldWait && videoId) {
|
|
141
195
|
console.log(`Video ${videoId} queued — polling for completion…`);
|
|
142
196
|
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
143
197
|
}
|
|
198
|
+
// After polling, the status response may contain the real videoId for download
|
|
199
|
+
const downloadId = data.videoId || data.id || videoId;
|
|
144
200
|
// 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)`);
|
|
201
|
+
if (opts.output && downloadId && data.status === "inference_complete") {
|
|
202
|
+
const { contentType, size } = await downloadVideoToFile(downloadId, opts.output);
|
|
203
|
+
if (isJsonMode(media)) {
|
|
204
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
164
205
|
return;
|
|
165
206
|
}
|
|
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
|
-
}
|
|
207
|
+
console.log(`Video saved to ${opts.output} (${size} bytes)`);
|
|
208
|
+
return;
|
|
189
209
|
}
|
|
190
210
|
if (isJsonMode(media)) {
|
|
191
211
|
jsonOut(data);
|
|
@@ -193,10 +213,10 @@ export function registerMediaCommand(program) {
|
|
|
193
213
|
}
|
|
194
214
|
const status = data.status || "queued";
|
|
195
215
|
console.log(`Video created!\n` +
|
|
196
|
-
` ID: ${
|
|
216
|
+
` ID: ${downloadId}\n` +
|
|
197
217
|
` Status: ${status}`);
|
|
198
218
|
if (status === "inference_complete") {
|
|
199
|
-
console.log(` Download: gobi media video-download ${
|
|
219
|
+
console.log(` Download: gobi media video-download ${downloadId}`);
|
|
200
220
|
}
|
|
201
221
|
});
|
|
202
222
|
media
|
|
@@ -244,49 +264,14 @@ export function registerMediaCommand(program) {
|
|
|
244
264
|
const data = await pollStatus(`/media-gen/videos/${id}/status`, ["inference_complete", "inference_failed"]);
|
|
245
265
|
// Download if -o specified and completed
|
|
246
266
|
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)`);
|
|
267
|
+
const dlId = (data.videoId || data.id || id);
|
|
268
|
+
const { contentType, size } = await downloadVideoToFile(dlId, opts.output);
|
|
269
|
+
if (isJsonMode(media)) {
|
|
270
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
265
271
|
return;
|
|
266
272
|
}
|
|
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
|
-
}
|
|
273
|
+
console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${size} bytes)`);
|
|
274
|
+
return;
|
|
290
275
|
}
|
|
291
276
|
if (isJsonMode(media)) {
|
|
292
277
|
jsonOut(data);
|
|
@@ -312,48 +297,13 @@ export function registerMediaCommand(program) {
|
|
|
312
297
|
const url = `${BASE_URL}/media-gen/videos/${id}/download`;
|
|
313
298
|
// If -o specified, download directly to file
|
|
314
299
|
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)`);
|
|
300
|
+
const { contentType, size } = await downloadVideoToFile(id, opts.output);
|
|
301
|
+
if (isJsonMode(media)) {
|
|
302
|
+
jsonOut({ filename: opts.output, contentType, size });
|
|
331
303
|
return;
|
|
332
304
|
}
|
|
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");
|
|
305
|
+
console.log(`Video saved to ${opts.output} (${size} bytes)`);
|
|
306
|
+
return;
|
|
357
307
|
}
|
|
358
308
|
// No -o: just return the URL (existing behavior)
|
|
359
309
|
const res = await fetch(url, {
|
|
@@ -384,6 +334,223 @@ export function registerMediaCommand(program) {
|
|
|
384
334
|
console.log(`Download URL for video ${id}:\n ${data.url || data.downloadUrl || JSON.stringify(data)}`);
|
|
385
335
|
});
|
|
386
336
|
// ════════════════════════════════════════════════════════════════════
|
|
337
|
+
// Cinematic Video
|
|
338
|
+
// ════════════════════════════════════════════════════════════════════
|
|
339
|
+
media
|
|
340
|
+
.command("cinematic-create")
|
|
341
|
+
.description("Create a cinematic video from a text prompt.")
|
|
342
|
+
.requiredOption("--prompt <prompt>", "Text prompt describing the video")
|
|
343
|
+
.option("--name <name>", "Name for the video (auto-generated if omitted)")
|
|
344
|
+
.option("--aspect-ratio <aspectRatio>", "Aspect ratio: 16:9, 9:16, 1:1")
|
|
345
|
+
.option("--duration <seconds>", "Duration in seconds (4-8)")
|
|
346
|
+
.option("--resolution <resolution>", "Resolution: 720p, 1080p")
|
|
347
|
+
.option("--enhance-prompt", "Enhance the prompt with AI")
|
|
348
|
+
.option("--generate-audio", "Generate audio for the video")
|
|
349
|
+
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
350
|
+
.option("--sample-count <count>", "Number of samples (1-4)")
|
|
351
|
+
.option("--first-frame <file>", "First frame image file (auto-uploaded)")
|
|
352
|
+
.option("--last-frame <file>", "Last frame image file (auto-uploaded)")
|
|
353
|
+
.option("--reference-images <files>", "Comma-separated reference image files (auto-uploaded, max 3)")
|
|
354
|
+
.option("--wait", "Poll until generation completes")
|
|
355
|
+
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
356
|
+
.action(async (opts) => {
|
|
357
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
358
|
+
const autoName = opts.name || `cinematic-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
359
|
+
const body = {
|
|
360
|
+
name: autoName,
|
|
361
|
+
prompt: opts.prompt,
|
|
362
|
+
};
|
|
363
|
+
if (opts.aspectRatio)
|
|
364
|
+
body.aspectRatio = opts.aspectRatio;
|
|
365
|
+
if (opts.duration) {
|
|
366
|
+
const v = parseInt(opts.duration, 10);
|
|
367
|
+
if (Number.isNaN(v))
|
|
368
|
+
throw new Error("--duration must be a number");
|
|
369
|
+
body.durationSeconds = v;
|
|
370
|
+
}
|
|
371
|
+
if (opts.resolution)
|
|
372
|
+
body.resolution = opts.resolution;
|
|
373
|
+
if (opts.enhancePrompt)
|
|
374
|
+
body.enhancePrompt = true;
|
|
375
|
+
if (opts.generateAudio)
|
|
376
|
+
body.generateAudio = true;
|
|
377
|
+
if (opts.negativePrompt)
|
|
378
|
+
body.negativePrompt = opts.negativePrompt;
|
|
379
|
+
if (opts.sampleCount) {
|
|
380
|
+
const v = parseInt(opts.sampleCount, 10);
|
|
381
|
+
if (Number.isNaN(v))
|
|
382
|
+
throw new Error("--sample-count must be a number");
|
|
383
|
+
body.sampleCount = v;
|
|
384
|
+
}
|
|
385
|
+
if (opts.firstFrame)
|
|
386
|
+
body.firstFrameImageMediaId = await uploadFile(opts.firstFrame);
|
|
387
|
+
if (opts.lastFrame)
|
|
388
|
+
body.lastFrameImageMediaId = await uploadFile(opts.lastFrame);
|
|
389
|
+
if (opts.referenceImages) {
|
|
390
|
+
const files = opts.referenceImages.split(",").map((s) => s.trim());
|
|
391
|
+
body.referenceImageMediaIds = await Promise.all(files.map((f) => uploadFile(f)));
|
|
392
|
+
}
|
|
393
|
+
const resp = (await apiPost("/media-gen/videos/cinematic", body));
|
|
394
|
+
let data = unwrapResp(resp);
|
|
395
|
+
const videoId = data.id || data.videoId || data.jobId;
|
|
396
|
+
if (shouldWait && videoId) {
|
|
397
|
+
console.log(`Cinematic video ${videoId} queued — polling for completion…`);
|
|
398
|
+
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
399
|
+
}
|
|
400
|
+
// After polling, the status response may contain the real videoId for download
|
|
401
|
+
const downloadId = data.videoId || data.id || videoId;
|
|
402
|
+
// Download video to file if -o specified
|
|
403
|
+
if (opts.output && downloadId && data.status === "inference_complete") {
|
|
404
|
+
const { contentType, size } = await downloadVideoToFile(downloadId, opts.output);
|
|
405
|
+
if (isJsonMode(media)) {
|
|
406
|
+
jsonOut({ ...data, filename: opts.output, contentType, size });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.log(`Cinematic video saved to ${opts.output} (${size} bytes)`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (isJsonMode(media)) {
|
|
413
|
+
jsonOut(data);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const status = data.status || "queued";
|
|
417
|
+
console.log(`Cinematic video created!\n` +
|
|
418
|
+
` ID: ${downloadId}\n` +
|
|
419
|
+
` Status: ${status}`);
|
|
420
|
+
if (status === "inference_complete") {
|
|
421
|
+
console.log(` Download: gobi media video-download ${downloadId}`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// ════════════════════════════════════════════════════════════════════
|
|
425
|
+
// Custom Avatars
|
|
426
|
+
// ════════════════════════════════════════════════════════════════════
|
|
427
|
+
media
|
|
428
|
+
.command("avatar-design")
|
|
429
|
+
.description("Start a design-your-avatar job.")
|
|
430
|
+
.option("--name <name>", "Name for the avatar (auto-generated if omitted)")
|
|
431
|
+
.requiredOption("--gender <gender>", "Gender for the avatar design")
|
|
432
|
+
.requiredOption("--age <age>", "Age range for the avatar")
|
|
433
|
+
.requiredOption("--ethnicity <ethnicity>", "Ethnicity for the avatar")
|
|
434
|
+
.requiredOption("--outfit <outfit>", "Outfit description")
|
|
435
|
+
.requiredOption("--background <background>", "Background description")
|
|
436
|
+
.option("--no-portrait", "Generate full-body instead of portrait")
|
|
437
|
+
.option("--audio <file>", "Custom voice audio file (auto-uploaded)")
|
|
438
|
+
.option("--wait", "Poll until variants are ready")
|
|
439
|
+
.action(async (opts) => {
|
|
440
|
+
const body = {
|
|
441
|
+
name: opts.name || `avatar-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`,
|
|
442
|
+
gender: opts.gender,
|
|
443
|
+
age: opts.age,
|
|
444
|
+
ethnicity: opts.ethnicity,
|
|
445
|
+
outfit: opts.outfit,
|
|
446
|
+
background: opts.background,
|
|
447
|
+
isPortrait: opts.portrait,
|
|
448
|
+
};
|
|
449
|
+
if (opts.audio)
|
|
450
|
+
body.audioMediaId = await uploadFile(opts.audio);
|
|
451
|
+
const resp = (await apiPost("/media-gen/avatars/design", body));
|
|
452
|
+
let data = unwrapResp(resp);
|
|
453
|
+
const jobId = data.jobId || data.id;
|
|
454
|
+
if (opts.wait && jobId) {
|
|
455
|
+
console.log(`Avatar design job ${jobId} — polling for completion…`);
|
|
456
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
457
|
+
}
|
|
458
|
+
if (isJsonMode(media)) {
|
|
459
|
+
jsonOut(data);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const status = data.status || "queued";
|
|
463
|
+
console.log(`Avatar design started!\n` +
|
|
464
|
+
` Job ID: ${jobId}\n` +
|
|
465
|
+
` Status: ${status}`);
|
|
466
|
+
if (status === "variants_ready") {
|
|
467
|
+
console.log(` Confirm: gobi media avatar-confirm --job-id ${jobId}`);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
media
|
|
471
|
+
.command("avatar-confirm")
|
|
472
|
+
.description("Confirm avatar variant(s) after design.")
|
|
473
|
+
.requiredOption("--job-id <jobId>", "Job ID from avatar-design")
|
|
474
|
+
.option("--variant <variant>", "Variant to confirm (1 or 2); omit to confirm both")
|
|
475
|
+
.action(async (opts) => {
|
|
476
|
+
const body = { jobId: opts.jobId };
|
|
477
|
+
if (opts.variant) {
|
|
478
|
+
const v = parseInt(opts.variant, 10);
|
|
479
|
+
if (Number.isNaN(v))
|
|
480
|
+
throw new Error("--variant must be a number (1 or 2)");
|
|
481
|
+
body.variant = v;
|
|
482
|
+
}
|
|
483
|
+
const resp = (await apiPost("/media-gen/avatars/confirm", body));
|
|
484
|
+
const data = unwrapResp(resp);
|
|
485
|
+
if (isJsonMode(media)) {
|
|
486
|
+
jsonOut(data);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const avatarId = data.avatarId || data.id;
|
|
490
|
+
console.log(`Avatar confirmed!\n` +
|
|
491
|
+
` Avatar ID: ${avatarId || JSON.stringify(data)}`);
|
|
492
|
+
});
|
|
493
|
+
media
|
|
494
|
+
.command("avatar-from-selfie")
|
|
495
|
+
.description("Create an avatar from a selfie (instant or enhanced with prompt).")
|
|
496
|
+
.option("--name <name>", "Name for the avatar (auto-generated if omitted)")
|
|
497
|
+
.requiredOption("--photo <file>", "Selfie photo file (auto-uploaded)")
|
|
498
|
+
.option("--prompt <prompt>", "Enhancement prompt (triggers async enhance flow)")
|
|
499
|
+
.option("--audio <file>", "Custom voice audio file (auto-uploaded)")
|
|
500
|
+
.option("--wait", "Poll until job completes (only for enhance flow)")
|
|
501
|
+
.action(async (opts) => {
|
|
502
|
+
const body = {
|
|
503
|
+
name: opts.name || `avatar-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`,
|
|
504
|
+
photoMediaId: await uploadFile(opts.photo),
|
|
505
|
+
};
|
|
506
|
+
if (opts.prompt)
|
|
507
|
+
body.prompt = opts.prompt;
|
|
508
|
+
if (opts.audio)
|
|
509
|
+
body.audioMediaId = await uploadFile(opts.audio);
|
|
510
|
+
const resp = (await apiPost("/media-gen/avatars/from-selfie", body));
|
|
511
|
+
let data = unwrapResp(resp);
|
|
512
|
+
const jobId = data.jobId || data.id;
|
|
513
|
+
// Enhance flow is async — poll if --wait
|
|
514
|
+
if (opts.wait && opts.prompt && jobId) {
|
|
515
|
+
console.log(`Avatar enhance job ${jobId} — polling for completion…`);
|
|
516
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
517
|
+
}
|
|
518
|
+
if (isJsonMode(media)) {
|
|
519
|
+
jsonOut(data);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (opts.prompt) {
|
|
523
|
+
const status = data.status || "queued";
|
|
524
|
+
console.log(`Avatar enhance started!\n` +
|
|
525
|
+
` Job ID: ${jobId}\n` +
|
|
526
|
+
` Status: ${status}`);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const avatarId = data.avatarId || data.id;
|
|
530
|
+
console.log(`Avatar created from selfie!\n` +
|
|
531
|
+
` Avatar ID: ${avatarId || JSON.stringify(data)}`);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
media
|
|
535
|
+
.command("avatar-job-status <jobId>")
|
|
536
|
+
.description("Check avatar job status.")
|
|
537
|
+
.option("--wait", "Poll until a terminal state is reached")
|
|
538
|
+
.action(async (jobId, opts) => {
|
|
539
|
+
let data;
|
|
540
|
+
if (opts.wait) {
|
|
541
|
+
data = await pollStatus(`/media-gen/avatars/jobs/${jobId}/status`, ["variants_ready", "complete", "failed"]);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const resp = (await apiGet(`/media-gen/avatars/jobs/${jobId}/status`));
|
|
545
|
+
data = unwrapResp(resp);
|
|
546
|
+
}
|
|
547
|
+
if (isJsonMode(media)) {
|
|
548
|
+
jsonOut(data);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
console.log(`Avatar job ${jobId} — status: ${data.status || "unknown"}`);
|
|
552
|
+
});
|
|
553
|
+
// ════════════════════════════════════════════════════════════════════
|
|
387
554
|
// Images
|
|
388
555
|
// ════════════════════════════════════════════════════════════════════
|
|
389
556
|
media
|
|
@@ -395,7 +562,7 @@ export function registerMediaCommand(program) {
|
|
|
395
562
|
.option("--aspect-ratio <aspectRatio>", "Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)")
|
|
396
563
|
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
397
564
|
.option("--seed <seed>", "Random seed for reproducibility")
|
|
398
|
-
.option("--reference-
|
|
565
|
+
.option("--reference-image <file>", "Reference image file (auto-uploaded)")
|
|
399
566
|
.option("--wait", "Poll until generation completes")
|
|
400
567
|
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
401
568
|
.action(async (opts) => {
|
|
@@ -413,8 +580,8 @@ export function registerMediaCommand(program) {
|
|
|
413
580
|
body.negativePrompt = opts.negativePrompt;
|
|
414
581
|
if (opts.seed)
|
|
415
582
|
body.seed = parseInt(opts.seed, 10);
|
|
416
|
-
if (opts.
|
|
417
|
-
body.referenceMediaId = opts.
|
|
583
|
+
if (opts.referenceImage)
|
|
584
|
+
body.referenceMediaId = await uploadFile(opts.referenceImage);
|
|
418
585
|
const resp = (await apiPost("/media-gen/images/generate", body));
|
|
419
586
|
let data = unwrapResp(resp);
|
|
420
587
|
const jobId = data.jobId || data.id;
|
|
@@ -471,19 +638,23 @@ export function registerMediaCommand(program) {
|
|
|
471
638
|
media
|
|
472
639
|
.command("image-edit")
|
|
473
640
|
.description("Edit an existing image with a prompt (image-to-image).")
|
|
474
|
-
.requiredOption("--
|
|
641
|
+
.requiredOption("--image <file>", "Source image file (auto-uploaded)")
|
|
475
642
|
.requiredOption("--prompt <prompt>", "Edit instruction")
|
|
476
|
-
.
|
|
643
|
+
.option("--name <name>", "Name for the edited image (auto-generated if omitted)")
|
|
477
644
|
.option("--wait", "Poll until generation completes")
|
|
645
|
+
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
478
646
|
.action(async (opts) => {
|
|
647
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
648
|
+
const mediaId = await uploadFile(opts.image);
|
|
649
|
+
const autoName = opts.name || opts.prompt.slice(0, 50).replace(/[^a-zA-Z0-9-_ ]/g, "").trim().replace(/\s+/g, "-");
|
|
479
650
|
const resp = (await apiPost("/media-gen/images/edit", {
|
|
480
|
-
mediaId
|
|
651
|
+
mediaId,
|
|
481
652
|
prompt: opts.prompt,
|
|
482
|
-
name:
|
|
653
|
+
name: autoName,
|
|
483
654
|
}));
|
|
484
655
|
let data = unwrapResp(resp);
|
|
485
656
|
const jobId = data.jobId || data.id;
|
|
486
|
-
if (
|
|
657
|
+
if (shouldWait && jobId) {
|
|
487
658
|
console.log(`Image edit job ${jobId} — polling for completion…`);
|
|
488
659
|
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
489
660
|
"completed",
|
|
@@ -492,6 +663,29 @@ export function registerMediaCommand(program) {
|
|
|
492
663
|
"inference_failed",
|
|
493
664
|
]);
|
|
494
665
|
}
|
|
666
|
+
// Download image to file if -o specified
|
|
667
|
+
if (opts.output && jobId) {
|
|
668
|
+
const token = await getValidToken();
|
|
669
|
+
const query = "";
|
|
670
|
+
const url = `${BASE_URL}/media-gen/images/${jobId}/download${query}`;
|
|
671
|
+
const res = await fetch(url, {
|
|
672
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
673
|
+
});
|
|
674
|
+
if (res.ok) {
|
|
675
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
676
|
+
const { dirname } = await import("path");
|
|
677
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
678
|
+
await mkdir(dirname(opts.output), { recursive: true });
|
|
679
|
+
await writeFile(opts.output, buffer);
|
|
680
|
+
const contentType = res.headers.get("content-type") || "image/png";
|
|
681
|
+
if (isJsonMode(media)) {
|
|
682
|
+
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
console.log(`Image saved to ${opts.output} (${buffer.length} bytes)`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
495
689
|
if (isJsonMode(media)) {
|
|
496
690
|
jsonOut(data);
|
|
497
691
|
return;
|
|
@@ -507,21 +701,28 @@ export function registerMediaCommand(program) {
|
|
|
507
701
|
media
|
|
508
702
|
.command("image-inpaint")
|
|
509
703
|
.description("Inpaint an image region using a mask.")
|
|
510
|
-
.requiredOption("--
|
|
511
|
-
.requiredOption("--mask
|
|
704
|
+
.requiredOption("--image <file>", "Source image file (auto-uploaded)")
|
|
705
|
+
.requiredOption("--mask <file>", "Mask image file (auto-uploaded)")
|
|
512
706
|
.requiredOption("--prompt <prompt>", "Inpainting prompt")
|
|
513
|
-
.
|
|
707
|
+
.option("--name <name>", "Name for the inpainted image (auto-generated if omitted)")
|
|
514
708
|
.option("--wait", "Poll until generation completes")
|
|
709
|
+
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
515
710
|
.action(async (opts) => {
|
|
711
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
712
|
+
const [mediaId, maskMediaId] = await Promise.all([
|
|
713
|
+
uploadFile(opts.image),
|
|
714
|
+
uploadFile(opts.mask),
|
|
715
|
+
]);
|
|
716
|
+
const autoName = opts.name || opts.prompt.slice(0, 50).replace(/[^a-zA-Z0-9-_ ]/g, "").trim().replace(/\s+/g, "-");
|
|
516
717
|
const resp = (await apiPost("/media-gen/images/inpaint", {
|
|
517
|
-
mediaId
|
|
518
|
-
maskMediaId
|
|
718
|
+
mediaId,
|
|
719
|
+
maskMediaId,
|
|
519
720
|
prompt: opts.prompt,
|
|
520
|
-
name:
|
|
721
|
+
name: autoName,
|
|
521
722
|
}));
|
|
522
723
|
let data = unwrapResp(resp);
|
|
523
724
|
const jobId = data.jobId || data.id;
|
|
524
|
-
if (
|
|
725
|
+
if (shouldWait && jobId) {
|
|
525
726
|
console.log(`Inpaint job ${jobId} — polling for completion…`);
|
|
526
727
|
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
527
728
|
"completed",
|
|
@@ -530,6 +731,28 @@ export function registerMediaCommand(program) {
|
|
|
530
731
|
"inference_failed",
|
|
531
732
|
]);
|
|
532
733
|
}
|
|
734
|
+
// Download image to file if -o specified
|
|
735
|
+
if (opts.output && jobId) {
|
|
736
|
+
const token = await getValidToken();
|
|
737
|
+
const url = `${BASE_URL}/media-gen/images/${jobId}/download`;
|
|
738
|
+
const res = await fetch(url, {
|
|
739
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
740
|
+
});
|
|
741
|
+
if (res.ok) {
|
|
742
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
743
|
+
const { dirname } = await import("path");
|
|
744
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
745
|
+
await mkdir(dirname(opts.output), { recursive: true });
|
|
746
|
+
await writeFile(opts.output, buffer);
|
|
747
|
+
const contentType = res.headers.get("content-type") || "image/png";
|
|
748
|
+
if (isJsonMode(media)) {
|
|
749
|
+
jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
console.log(`Image saved to ${opts.output} (${buffer.length} bytes)`);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
533
756
|
if (isJsonMode(media)) {
|
|
534
757
|
jsonOut(data);
|
|
535
758
|
return;
|
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,64 @@ 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, pass it directly as `--background <file>` (auto-uploaded):
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
```bash
|
|
73
|
+
gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" --background media/bg.png -o media/<NAME>.mp4
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Typical Workflow (Cinematic Video)
|
|
77
|
+
|
|
78
|
+
Generate a cinematic video from a text prompt (no avatar needed):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gobi --json media cinematic-create --prompt "<PROMPT>" --aspect-ratio "<RATIO>" -o media/<NAME>.mp4
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Options: `--duration <4-8>`, `--resolution <720p|1080p>`, `--enhance-prompt`, `--generate-audio`, `--negative-prompt`, `--sample-count <1-4>`, `--first-frame <file>`, `--last-frame <file>`, `--reference-images <files>`.
|
|
85
|
+
|
|
86
|
+
## Typical Workflow (Image Editing)
|
|
87
|
+
|
|
88
|
+
Edit an existing image with a prompt — single command:
|
|
71
89
|
|
|
72
90
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
gobi --json media image-edit --image media/source.png --prompt "<EDIT_INSTRUCTION>" -o media/<NAME>.png
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
All file arguments (`--image`, `--mask`, `--background`, `--photo`, `--audio`, `--reference-image`, `--first-frame`, `--last-frame`) accept local file paths and auto-upload them. No need to manually upload first.
|
|
95
|
+
|
|
96
|
+
## Custom Avatars
|
|
97
|
+
|
|
98
|
+
Three ways to create custom avatars:
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
### 1. Design from scratch
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
gobi --json media avatar-design --gender "<GENDER>" --age "<AGE>" --ethnicity "<ETHNICITY>" --outfit "<OUTFIT>" --background "<BACKGROUND>" --wait
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
When `variants_ready`, confirm with:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gobi --json media avatar-confirm --job-id "<JOB_ID>"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. From a selfie (instant)
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
gobi --json media avatar-from-selfie --photo media/selfie.png
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. From a selfie (enhanced with prompt)
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
gobi --json media avatar-from-selfie --photo media/selfie.png --prompt "<ENHANCEMENT>" --wait
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Check any avatar job status with:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gobi --json media avatar-job-status <jobId> --wait
|
|
80
128
|
```
|
|
81
129
|
|
|
82
130
|
**IMPORTANT: After downloading, show the video using Obsidian wiki-link syntax EXACTLY like this:**
|
|
@@ -91,8 +139,7 @@ Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `
|
|
|
91
139
|
|
|
92
140
|
### Upload
|
|
93
141
|
|
|
94
|
-
- `gobi media upload
|
|
95
|
-
- `gobi media upload-finalize` — Confirm that a media upload is complete.
|
|
142
|
+
- `gobi media upload <file>` — Upload a local file and return its media ID. Content type is auto-detected.
|
|
96
143
|
|
|
97
144
|
### Avatars & Voices
|
|
98
145
|
|
|
@@ -102,17 +149,27 @@ Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `
|
|
|
102
149
|
### Videos
|
|
103
150
|
|
|
104
151
|
- `gobi media video-create` — Create an avatar video generation job.
|
|
152
|
+
- `gobi media cinematic-create` — Create a cinematic video from a text prompt.
|
|
105
153
|
- `gobi media video-list` — List all videos.
|
|
106
154
|
- `gobi media video-get` — Get video metadata.
|
|
107
155
|
- `gobi media video-status` — Poll video generation status.
|
|
108
156
|
- `gobi media video-download` — Download a completed video (`-o` to save to file).
|
|
109
157
|
|
|
158
|
+
### Custom Avatars
|
|
159
|
+
|
|
160
|
+
- `gobi media avatar-design` — Start a design-your-avatar job.
|
|
161
|
+
- `gobi media avatar-confirm` — Confirm avatar variant(s) after design.
|
|
162
|
+
- `gobi media avatar-from-selfie` — Create an avatar from a selfie (instant or enhanced).
|
|
163
|
+
- `gobi media avatar-job-status` — Check avatar job status.
|
|
164
|
+
|
|
110
165
|
### Images
|
|
111
166
|
|
|
112
167
|
- `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
|
|
113
168
|
- `gobi media image-edit` — Edit an existing image with a prompt (image-to-image).
|
|
114
169
|
- `gobi media image-inpaint` — Inpaint an image region using a mask.
|
|
115
170
|
- `gobi media image-status` — Check image generation job status.
|
|
171
|
+
- `gobi media image-download` — Download a generated image.
|
|
172
|
+
- `gobi media image-status` — Check image generation job status.
|
|
116
173
|
|
|
117
174
|
## Reference Documentation
|
|
118
175
|
|
|
@@ -6,50 +6,39 @@ 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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
video-
|
|
17
|
-
video-
|
|
18
|
-
video-
|
|
19
|
-
video-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
image-
|
|
26
|
-
|
|
12
|
+
upload <file> Upload a local file and return its media ID.
|
|
13
|
+
avatars List available avatars.
|
|
14
|
+
voices List available voices.
|
|
15
|
+
video-create [options] Create an avatar video generation job.
|
|
16
|
+
video-list List all videos.
|
|
17
|
+
video-get <id> Get video metadata.
|
|
18
|
+
video-status [options] <id> Poll video generation status.
|
|
19
|
+
video-download [options] <id> Download a completed video (or get its URL).
|
|
20
|
+
cinematic-create [options] Create a cinematic video from a text prompt.
|
|
21
|
+
avatar-design [options] Start a design-your-avatar job.
|
|
22
|
+
avatar-confirm [options] Confirm avatar variant(s) after design.
|
|
23
|
+
avatar-from-selfie [options] Create an avatar from a selfie (instant or enhanced with prompt).
|
|
24
|
+
avatar-job-status [options] <jobId> Check avatar job status.
|
|
25
|
+
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
|
|
26
|
+
image-edit [options] Edit an existing image with a prompt (image-to-image).
|
|
27
|
+
image-inpaint [options] Inpaint an image region using a mask.
|
|
28
|
+
image-status [options] <jobId> Check image generation job status.
|
|
29
|
+
image-download [options] <jobId> Download a generated image.
|
|
30
|
+
help [command] display help for command
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
## upload
|
|
33
|
+
## upload
|
|
30
34
|
|
|
31
35
|
```
|
|
32
|
-
Usage: gobi media upload
|
|
36
|
+
Usage: gobi media upload [options] <file>
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Upload a local file and return its media ID.
|
|
35
39
|
|
|
36
40
|
Options:
|
|
37
|
-
|
|
38
|
-
--content-type <contentType> MIME type (e.g. image/png, video/mp4)
|
|
39
|
-
--file-size <fileSize> File size in bytes
|
|
40
|
-
-h, --help display help for command
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## upload-finalize
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
Usage: gobi media upload-finalize [options]
|
|
47
|
-
|
|
48
|
-
Confirm that a media upload is complete.
|
|
49
|
-
|
|
50
|
-
Options:
|
|
51
|
-
--media-id <mediaId> Media ID from upload-init
|
|
52
|
-
-h, --help display help for command
|
|
41
|
+
-h, --help display help for command
|
|
53
42
|
```
|
|
54
43
|
|
|
55
44
|
## avatars
|
|
@@ -82,14 +71,14 @@ Usage: gobi media video-create [options]
|
|
|
82
71
|
Create an avatar video generation job.
|
|
83
72
|
|
|
84
73
|
Options:
|
|
85
|
-
--name <name>
|
|
86
|
-
--avatar-id <avatarId>
|
|
87
|
-
--voice-id <voiceId>
|
|
88
|
-
--script <script>
|
|
89
|
-
--background
|
|
90
|
-
--wait
|
|
91
|
-
-o, --output <path>
|
|
92
|
-
-h, --help
|
|
74
|
+
--name <name> Name for the video (auto-generated if omitted)
|
|
75
|
+
--avatar-id <avatarId> Avatar to use
|
|
76
|
+
--voice-id <voiceId> Voice to use
|
|
77
|
+
--script <script> Script for the avatar to read
|
|
78
|
+
--background <file> Background image file (auto-uploaded)
|
|
79
|
+
--wait Poll until generation completes
|
|
80
|
+
-o, --output <path> Download video to this path when done (implies --wait)
|
|
81
|
+
-h, --help display help for command
|
|
93
82
|
```
|
|
94
83
|
|
|
95
84
|
## video-list
|
|
@@ -139,6 +128,92 @@ Options:
|
|
|
139
128
|
-h, --help display help for command
|
|
140
129
|
```
|
|
141
130
|
|
|
131
|
+
## cinematic-create
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Usage: gobi media cinematic-create [options]
|
|
135
|
+
|
|
136
|
+
Create a cinematic video from a text prompt.
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
--prompt <prompt> Text prompt describing the video
|
|
140
|
+
--name <name> Name for the video (auto-generated if omitted)
|
|
141
|
+
--aspect-ratio <aspectRatio> Aspect ratio: 16:9, 9:16, 1:1
|
|
142
|
+
--duration <seconds> Duration in seconds (4-8)
|
|
143
|
+
--resolution <resolution> Resolution: 720p, 1080p
|
|
144
|
+
--enhance-prompt Enhance the prompt with AI
|
|
145
|
+
--generate-audio Generate audio for the video
|
|
146
|
+
--negative-prompt <negativePrompt> Negative prompt
|
|
147
|
+
--sample-count <count> Number of samples (1-4)
|
|
148
|
+
--first-frame <file> First frame image file (auto-uploaded)
|
|
149
|
+
--last-frame <file> Last frame image file (auto-uploaded)
|
|
150
|
+
--reference-images <files> Comma-separated reference image files (auto-uploaded, max 3)
|
|
151
|
+
--wait Poll until generation completes
|
|
152
|
+
-o, --output <path> Download video to this path when done (implies --wait)
|
|
153
|
+
-h, --help display help for command
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## avatar-design
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
Usage: gobi media avatar-design [options]
|
|
160
|
+
|
|
161
|
+
Start a design-your-avatar job.
|
|
162
|
+
|
|
163
|
+
Options:
|
|
164
|
+
--name <name> Name for the avatar (auto-generated if omitted)
|
|
165
|
+
--gender <gender> Gender for the avatar design
|
|
166
|
+
--age <age> Age range for the avatar
|
|
167
|
+
--ethnicity <ethnicity> Ethnicity for the avatar
|
|
168
|
+
--outfit <outfit> Outfit description
|
|
169
|
+
--background <background> Background description
|
|
170
|
+
--no-portrait Generate full-body instead of portrait
|
|
171
|
+
--audio <file> Custom voice audio file (auto-uploaded)
|
|
172
|
+
--wait Poll until variants are ready
|
|
173
|
+
-h, --help display help for command
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## avatar-confirm
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Usage: gobi media avatar-confirm [options]
|
|
180
|
+
|
|
181
|
+
Confirm avatar variant(s) after design.
|
|
182
|
+
|
|
183
|
+
Options:
|
|
184
|
+
--job-id <jobId> Job ID from avatar-design
|
|
185
|
+
--variant <variant> Variant to confirm (1 or 2); omit to confirm both
|
|
186
|
+
-h, --help display help for command
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## avatar-from-selfie
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Usage: gobi media avatar-from-selfie [options]
|
|
193
|
+
|
|
194
|
+
Create an avatar from a selfie (instant or enhanced with prompt).
|
|
195
|
+
|
|
196
|
+
Options:
|
|
197
|
+
--name <name> Name for the avatar (auto-generated if omitted)
|
|
198
|
+
--photo <file> Selfie photo file (auto-uploaded)
|
|
199
|
+
--prompt <prompt> Enhancement prompt (triggers async enhance flow)
|
|
200
|
+
--audio <file> Custom voice audio file (auto-uploaded)
|
|
201
|
+
--wait Poll until job completes (only for enhance flow)
|
|
202
|
+
-h, --help display help for command
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## avatar-job-status
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Usage: gobi media avatar-job-status [options] <jobId>
|
|
209
|
+
|
|
210
|
+
Check avatar job status.
|
|
211
|
+
|
|
212
|
+
Options:
|
|
213
|
+
--wait Poll until a terminal state is reached
|
|
214
|
+
-h, --help display help for command
|
|
215
|
+
```
|
|
216
|
+
|
|
142
217
|
## image-generate
|
|
143
218
|
|
|
144
219
|
```
|
|
@@ -147,16 +222,16 @@ Usage: gobi media image-generate [options]
|
|
|
147
222
|
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
|
|
148
223
|
|
|
149
224
|
Options:
|
|
150
|
-
--prompt <prompt>
|
|
151
|
-
--name <name>
|
|
152
|
-
--type <type>
|
|
153
|
-
--aspect-ratio <aspectRatio>
|
|
154
|
-
--negative-prompt <negativePrompt>
|
|
155
|
-
--seed <seed>
|
|
156
|
-
--reference-
|
|
157
|
-
--wait
|
|
158
|
-
-o, --output <path>
|
|
159
|
-
-h, --help
|
|
225
|
+
--prompt <prompt> Text prompt for image generation
|
|
226
|
+
--name <name> Name for the generated image (auto-generated from prompt if omitted)
|
|
227
|
+
--type <type> Generation type: image (default), thumbnail (YouTube-optimized), asset (logo/product)
|
|
228
|
+
--aspect-ratio <aspectRatio> Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)
|
|
229
|
+
--negative-prompt <negativePrompt> Negative prompt
|
|
230
|
+
--seed <seed> Random seed for reproducibility
|
|
231
|
+
--reference-image <file> Reference image file (auto-uploaded)
|
|
232
|
+
--wait Poll until generation completes
|
|
233
|
+
-o, --output <path> Download image to this path when done (implies --wait)
|
|
234
|
+
-h, --help display help for command
|
|
160
235
|
```
|
|
161
236
|
|
|
162
237
|
## image-edit
|
|
@@ -167,11 +242,12 @@ Usage: gobi media image-edit [options]
|
|
|
167
242
|
Edit an existing image with a prompt (image-to-image).
|
|
168
243
|
|
|
169
244
|
Options:
|
|
170
|
-
--
|
|
171
|
-
--prompt <prompt>
|
|
172
|
-
--name <name>
|
|
173
|
-
--wait
|
|
174
|
-
-
|
|
245
|
+
--image <file> Source image file (auto-uploaded)
|
|
246
|
+
--prompt <prompt> Edit instruction
|
|
247
|
+
--name <name> Name for the edited image (auto-generated if omitted)
|
|
248
|
+
--wait Poll until generation completes
|
|
249
|
+
-o, --output <path> Download image to this path when done (implies --wait)
|
|
250
|
+
-h, --help display help for command
|
|
175
251
|
```
|
|
176
252
|
|
|
177
253
|
## image-inpaint
|
|
@@ -182,12 +258,13 @@ Usage: gobi media image-inpaint [options]
|
|
|
182
258
|
Inpaint an image region using a mask.
|
|
183
259
|
|
|
184
260
|
Options:
|
|
185
|
-
--
|
|
186
|
-
--mask
|
|
187
|
-
--prompt <prompt>
|
|
188
|
-
--name <name>
|
|
189
|
-
--wait
|
|
190
|
-
-
|
|
261
|
+
--image <file> Source image file (auto-uploaded)
|
|
262
|
+
--mask <file> Mask image file (auto-uploaded)
|
|
263
|
+
--prompt <prompt> Inpainting prompt
|
|
264
|
+
--name <name> Name for the inpainted image (auto-generated if omitted)
|
|
265
|
+
--wait Poll until generation completes
|
|
266
|
+
-o, --output <path> Download image to this path when done (implies --wait)
|
|
267
|
+
-h, --help display help for command
|
|
191
268
|
```
|
|
192
269
|
|
|
193
270
|
## image-status
|