@gobi-ai/cli 0.9.9 → 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
|
@@ -61,6 +61,47 @@ async function downloadVideoToFile(videoId, outputPath) {
|
|
|
61
61
|
await writeFile(outputPath, buffer);
|
|
62
62
|
return { contentType: ct || "video/mp4", size: buffer.length };
|
|
63
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
|
+
}
|
|
64
105
|
function extractImageUrl(data) {
|
|
65
106
|
return (data.downloadUrl || data.download_url || data.url);
|
|
66
107
|
}
|
|
@@ -72,44 +113,15 @@ export function registerMediaCommand(program) {
|
|
|
72
113
|
// Upload
|
|
73
114
|
// ════════════════════════════════════════════════════════════════════
|
|
74
115
|
media
|
|
75
|
-
.command("upload
|
|
76
|
-
.description("
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
.option("--file-size <fileSize>", "File size in bytes")
|
|
80
|
-
.action(async (opts) => {
|
|
81
|
-
const body = {
|
|
82
|
-
fileName: opts.fileName,
|
|
83
|
-
contentType: opts.contentType,
|
|
84
|
-
};
|
|
85
|
-
if (opts.fileSize)
|
|
86
|
-
body.fileSize = parseInt(opts.fileSize, 10);
|
|
87
|
-
const resp = (await apiPost("/media-gen/media/initialize", body));
|
|
88
|
-
const data = unwrapResp(resp);
|
|
89
|
-
if (isJsonMode(media)) {
|
|
90
|
-
jsonOut(data);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
console.log(`Upload initialized!\n` +
|
|
94
|
-
` Media ID: ${data.mediaId}\n` +
|
|
95
|
-
` Upload URL: ${data.uploadUrl}\n\n` +
|
|
96
|
-
`PUT your file to the upload URL, then run:\n` +
|
|
97
|
-
` gobi media upload-finalize --media-id ${data.mediaId}`);
|
|
98
|
-
});
|
|
99
|
-
media
|
|
100
|
-
.command("upload-finalize")
|
|
101
|
-
.description("Confirm that a media upload is complete.")
|
|
102
|
-
.requiredOption("--media-id <mediaId>", "Media ID from upload-init")
|
|
103
|
-
.action(async (opts) => {
|
|
104
|
-
const resp = (await apiPost("/media-gen/media/finalize", {
|
|
105
|
-
mediaId: opts.mediaId,
|
|
106
|
-
}));
|
|
107
|
-
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);
|
|
108
120
|
if (isJsonMode(media)) {
|
|
109
|
-
jsonOut(
|
|
121
|
+
jsonOut({ mediaId });
|
|
110
122
|
return;
|
|
111
123
|
}
|
|
112
|
-
console.log(`
|
|
124
|
+
console.log(`Uploaded → Media ID: ${mediaId}`);
|
|
113
125
|
});
|
|
114
126
|
// ════════════════════════════════════════════════════════════════════
|
|
115
127
|
// Avatars & Voices
|
|
@@ -162,7 +174,7 @@ export function registerMediaCommand(program) {
|
|
|
162
174
|
.requiredOption("--avatar-id <avatarId>", "Avatar to use")
|
|
163
175
|
.requiredOption("--voice-id <voiceId>", "Voice to use")
|
|
164
176
|
.requiredOption("--script <script>", "Script for the avatar to read")
|
|
165
|
-
.option("--background
|
|
177
|
+
.option("--background <file>", "Background image file (auto-uploaded)")
|
|
166
178
|
.option("--wait", "Poll until generation completes")
|
|
167
179
|
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
168
180
|
.action(async (opts) => {
|
|
@@ -174,8 +186,8 @@ export function registerMediaCommand(program) {
|
|
|
174
186
|
voiceId: opts.voiceId,
|
|
175
187
|
script: opts.script,
|
|
176
188
|
};
|
|
177
|
-
if (opts.
|
|
178
|
-
body.backgroundMediaId = opts.
|
|
189
|
+
if (opts.background)
|
|
190
|
+
body.backgroundMediaId = await uploadFile(opts.background);
|
|
179
191
|
const resp = (await apiPost("/media-gen/videos", body));
|
|
180
192
|
let data = unwrapResp(resp);
|
|
181
193
|
const videoId = data.id || data.videoId || data.jobId;
|
|
@@ -336,9 +348,9 @@ export function registerMediaCommand(program) {
|
|
|
336
348
|
.option("--generate-audio", "Generate audio for the video")
|
|
337
349
|
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
338
350
|
.option("--sample-count <count>", "Number of samples (1-4)")
|
|
339
|
-
.option("--first-frame
|
|
340
|
-
.option("--last-frame
|
|
341
|
-
.option("--reference-
|
|
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)")
|
|
342
354
|
.option("--wait", "Poll until generation completes")
|
|
343
355
|
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
344
356
|
.action(async (opts) => {
|
|
@@ -370,12 +382,14 @@ export function registerMediaCommand(program) {
|
|
|
370
382
|
throw new Error("--sample-count must be a number");
|
|
371
383
|
body.sampleCount = v;
|
|
372
384
|
}
|
|
373
|
-
if (opts.
|
|
374
|
-
body.firstFrameImageMediaId = opts.
|
|
375
|
-
if (opts.
|
|
376
|
-
body.lastFrameImageMediaId = opts.
|
|
377
|
-
if (opts.
|
|
378
|
-
|
|
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
|
+
}
|
|
379
393
|
const resp = (await apiPost("/media-gen/videos/cinematic", body));
|
|
380
394
|
let data = unwrapResp(resp);
|
|
381
395
|
const videoId = data.id || data.videoId || data.jobId;
|
|
@@ -413,18 +427,18 @@ export function registerMediaCommand(program) {
|
|
|
413
427
|
media
|
|
414
428
|
.command("avatar-design")
|
|
415
429
|
.description("Start a design-your-avatar job.")
|
|
416
|
-
.
|
|
430
|
+
.option("--name <name>", "Name for the avatar (auto-generated if omitted)")
|
|
417
431
|
.requiredOption("--gender <gender>", "Gender for the avatar design")
|
|
418
432
|
.requiredOption("--age <age>", "Age range for the avatar")
|
|
419
433
|
.requiredOption("--ethnicity <ethnicity>", "Ethnicity for the avatar")
|
|
420
434
|
.requiredOption("--outfit <outfit>", "Outfit description")
|
|
421
435
|
.requiredOption("--background <background>", "Background description")
|
|
422
436
|
.option("--no-portrait", "Generate full-body instead of portrait")
|
|
423
|
-
.option("--audio
|
|
437
|
+
.option("--audio <file>", "Custom voice audio file (auto-uploaded)")
|
|
424
438
|
.option("--wait", "Poll until variants are ready")
|
|
425
439
|
.action(async (opts) => {
|
|
426
440
|
const body = {
|
|
427
|
-
name: opts.name,
|
|
441
|
+
name: opts.name || `avatar-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`,
|
|
428
442
|
gender: opts.gender,
|
|
429
443
|
age: opts.age,
|
|
430
444
|
ethnicity: opts.ethnicity,
|
|
@@ -432,8 +446,8 @@ export function registerMediaCommand(program) {
|
|
|
432
446
|
background: opts.background,
|
|
433
447
|
isPortrait: opts.portrait,
|
|
434
448
|
};
|
|
435
|
-
if (opts.
|
|
436
|
-
body.audioMediaId = opts.
|
|
449
|
+
if (opts.audio)
|
|
450
|
+
body.audioMediaId = await uploadFile(opts.audio);
|
|
437
451
|
const resp = (await apiPost("/media-gen/avatars/design", body));
|
|
438
452
|
let data = unwrapResp(resp);
|
|
439
453
|
const jobId = data.jobId || data.id;
|
|
@@ -479,20 +493,20 @@ export function registerMediaCommand(program) {
|
|
|
479
493
|
media
|
|
480
494
|
.command("avatar-from-selfie")
|
|
481
495
|
.description("Create an avatar from a selfie (instant or enhanced with prompt).")
|
|
482
|
-
.
|
|
483
|
-
.requiredOption("--photo
|
|
496
|
+
.option("--name <name>", "Name for the avatar (auto-generated if omitted)")
|
|
497
|
+
.requiredOption("--photo <file>", "Selfie photo file (auto-uploaded)")
|
|
484
498
|
.option("--prompt <prompt>", "Enhancement prompt (triggers async enhance flow)")
|
|
485
|
-
.option("--audio
|
|
499
|
+
.option("--audio <file>", "Custom voice audio file (auto-uploaded)")
|
|
486
500
|
.option("--wait", "Poll until job completes (only for enhance flow)")
|
|
487
501
|
.action(async (opts) => {
|
|
488
502
|
const body = {
|
|
489
|
-
name: opts.name,
|
|
490
|
-
photoMediaId: opts.
|
|
503
|
+
name: opts.name || `avatar-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`,
|
|
504
|
+
photoMediaId: await uploadFile(opts.photo),
|
|
491
505
|
};
|
|
492
506
|
if (opts.prompt)
|
|
493
507
|
body.prompt = opts.prompt;
|
|
494
|
-
if (opts.
|
|
495
|
-
body.audioMediaId = opts.
|
|
508
|
+
if (opts.audio)
|
|
509
|
+
body.audioMediaId = await uploadFile(opts.audio);
|
|
496
510
|
const resp = (await apiPost("/media-gen/avatars/from-selfie", body));
|
|
497
511
|
let data = unwrapResp(resp);
|
|
498
512
|
const jobId = data.jobId || data.id;
|
|
@@ -548,7 +562,7 @@ export function registerMediaCommand(program) {
|
|
|
548
562
|
.option("--aspect-ratio <aspectRatio>", "Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)")
|
|
549
563
|
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
550
564
|
.option("--seed <seed>", "Random seed for reproducibility")
|
|
551
|
-
.option("--reference-
|
|
565
|
+
.option("--reference-image <file>", "Reference image file (auto-uploaded)")
|
|
552
566
|
.option("--wait", "Poll until generation completes")
|
|
553
567
|
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
554
568
|
.action(async (opts) => {
|
|
@@ -566,8 +580,8 @@ export function registerMediaCommand(program) {
|
|
|
566
580
|
body.negativePrompt = opts.negativePrompt;
|
|
567
581
|
if (opts.seed)
|
|
568
582
|
body.seed = parseInt(opts.seed, 10);
|
|
569
|
-
if (opts.
|
|
570
|
-
body.referenceMediaId = opts.
|
|
583
|
+
if (opts.referenceImage)
|
|
584
|
+
body.referenceMediaId = await uploadFile(opts.referenceImage);
|
|
571
585
|
const resp = (await apiPost("/media-gen/images/generate", body));
|
|
572
586
|
let data = unwrapResp(resp);
|
|
573
587
|
const jobId = data.jobId || data.id;
|
|
@@ -624,19 +638,23 @@ export function registerMediaCommand(program) {
|
|
|
624
638
|
media
|
|
625
639
|
.command("image-edit")
|
|
626
640
|
.description("Edit an existing image with a prompt (image-to-image).")
|
|
627
|
-
.requiredOption("--
|
|
641
|
+
.requiredOption("--image <file>", "Source image file (auto-uploaded)")
|
|
628
642
|
.requiredOption("--prompt <prompt>", "Edit instruction")
|
|
629
|
-
.
|
|
643
|
+
.option("--name <name>", "Name for the edited image (auto-generated if omitted)")
|
|
630
644
|
.option("--wait", "Poll until generation completes")
|
|
645
|
+
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
631
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, "-");
|
|
632
650
|
const resp = (await apiPost("/media-gen/images/edit", {
|
|
633
|
-
mediaId
|
|
651
|
+
mediaId,
|
|
634
652
|
prompt: opts.prompt,
|
|
635
|
-
name:
|
|
653
|
+
name: autoName,
|
|
636
654
|
}));
|
|
637
655
|
let data = unwrapResp(resp);
|
|
638
656
|
const jobId = data.jobId || data.id;
|
|
639
|
-
if (
|
|
657
|
+
if (shouldWait && jobId) {
|
|
640
658
|
console.log(`Image edit job ${jobId} — polling for completion…`);
|
|
641
659
|
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
642
660
|
"completed",
|
|
@@ -645,6 +663,29 @@ export function registerMediaCommand(program) {
|
|
|
645
663
|
"inference_failed",
|
|
646
664
|
]);
|
|
647
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
|
+
}
|
|
648
689
|
if (isJsonMode(media)) {
|
|
649
690
|
jsonOut(data);
|
|
650
691
|
return;
|
|
@@ -660,21 +701,28 @@ export function registerMediaCommand(program) {
|
|
|
660
701
|
media
|
|
661
702
|
.command("image-inpaint")
|
|
662
703
|
.description("Inpaint an image region using a mask.")
|
|
663
|
-
.requiredOption("--
|
|
664
|
-
.requiredOption("--mask
|
|
704
|
+
.requiredOption("--image <file>", "Source image file (auto-uploaded)")
|
|
705
|
+
.requiredOption("--mask <file>", "Mask image file (auto-uploaded)")
|
|
665
706
|
.requiredOption("--prompt <prompt>", "Inpainting prompt")
|
|
666
|
-
.
|
|
707
|
+
.option("--name <name>", "Name for the inpainted image (auto-generated if omitted)")
|
|
667
708
|
.option("--wait", "Poll until generation completes")
|
|
709
|
+
.option("-o, --output <path>", "Download image to this path when done (implies --wait)")
|
|
668
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, "-");
|
|
669
717
|
const resp = (await apiPost("/media-gen/images/inpaint", {
|
|
670
|
-
mediaId
|
|
671
|
-
maskMediaId
|
|
718
|
+
mediaId,
|
|
719
|
+
maskMediaId,
|
|
672
720
|
prompt: opts.prompt,
|
|
673
|
-
name:
|
|
721
|
+
name: autoName,
|
|
674
722
|
}));
|
|
675
723
|
let data = unwrapResp(resp);
|
|
676
724
|
const jobId = data.jobId || data.id;
|
|
677
|
-
if (
|
|
725
|
+
if (shouldWait && jobId) {
|
|
678
726
|
console.log(`Inpaint job ${jobId} — polling for completion…`);
|
|
679
727
|
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
680
728
|
"completed",
|
|
@@ -683,6 +731,28 @@ export function registerMediaCommand(program) {
|
|
|
683
731
|
"inference_failed",
|
|
684
732
|
]);
|
|
685
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
|
+
}
|
|
686
756
|
if (isJsonMode(media)) {
|
|
687
757
|
jsonOut(data);
|
|
688
758
|
return;
|
package/package.json
CHANGED
|
@@ -67,10 +67,10 @@ gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>"
|
|
|
67
67
|
|
|
68
68
|
The `-o` flag implies `--wait` and downloads the video when done.
|
|
69
69
|
|
|
70
|
-
To use a custom image as the **background** of a video,
|
|
70
|
+
To use a custom image as the **background** of a video, pass it directly as `--background <file>` (auto-uploaded):
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" --background
|
|
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
74
|
```
|
|
75
75
|
|
|
76
76
|
## Typical Workflow (Cinematic Video)
|
|
@@ -81,7 +81,17 @@ Generate a cinematic video from a text prompt (no avatar needed):
|
|
|
81
81
|
gobi --json media cinematic-create --prompt "<PROMPT>" --aspect-ratio "<RATIO>" -o media/<NAME>.mp4
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Options: `--duration <4-8>`, `--resolution <720p|1080p>`, `--enhance-prompt`, `--generate-audio`, `--negative-prompt`, `--sample-count <1-4>`, `--first-frame
|
|
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:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
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.
|
|
85
95
|
|
|
86
96
|
## Custom Avatars
|
|
87
97
|
|
|
@@ -90,7 +100,7 @@ Three ways to create custom avatars:
|
|
|
90
100
|
### 1. Design from scratch
|
|
91
101
|
|
|
92
102
|
```bash
|
|
93
|
-
gobi --json media avatar-design --
|
|
103
|
+
gobi --json media avatar-design --gender "<GENDER>" --age "<AGE>" --ethnicity "<ETHNICITY>" --outfit "<OUTFIT>" --background "<BACKGROUND>" --wait
|
|
94
104
|
```
|
|
95
105
|
|
|
96
106
|
When `variants_ready`, confirm with:
|
|
@@ -102,15 +112,13 @@ gobi --json media avatar-confirm --job-id "<JOB_ID>"
|
|
|
102
112
|
### 2. From a selfie (instant)
|
|
103
113
|
|
|
104
114
|
```bash
|
|
105
|
-
gobi --json media avatar-from-selfie --
|
|
115
|
+
gobi --json media avatar-from-selfie --photo media/selfie.png
|
|
106
116
|
```
|
|
107
117
|
|
|
108
|
-
Upload the selfie first via `upload-init` / `upload-finalize` to get the media ID.
|
|
109
|
-
|
|
110
118
|
### 3. From a selfie (enhanced with prompt)
|
|
111
119
|
|
|
112
120
|
```bash
|
|
113
|
-
gobi --json media avatar-from-selfie --
|
|
121
|
+
gobi --json media avatar-from-selfie --photo media/selfie.png --prompt "<ENHANCEMENT>" --wait
|
|
114
122
|
```
|
|
115
123
|
|
|
116
124
|
Check any avatar job status with:
|
|
@@ -131,8 +139,7 @@ Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `
|
|
|
131
139
|
|
|
132
140
|
### Upload
|
|
133
141
|
|
|
134
|
-
- `gobi media upload
|
|
135
|
-
- `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.
|
|
136
143
|
|
|
137
144
|
### Avatars & Voices
|
|
138
145
|
|
|
@@ -161,6 +168,8 @@ Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `
|
|
|
161
168
|
- `gobi media image-edit` — Edit an existing image with a prompt (image-to-image).
|
|
162
169
|
- `gobi media image-inpaint` — Inpaint an image region using a mask.
|
|
163
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.
|
|
164
173
|
|
|
165
174
|
## Reference Documentation
|
|
166
175
|
|
|
@@ -9,8 +9,7 @@ Options:
|
|
|
9
9
|
-h, --help display help for command
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
|
-
upload
|
|
13
|
-
upload-finalize [options] Confirm that a media upload is complete.
|
|
12
|
+
upload <file> Upload a local file and return its media ID.
|
|
14
13
|
avatars List available avatars.
|
|
15
14
|
voices List available voices.
|
|
16
15
|
video-create [options] Create an avatar video generation job.
|
|
@@ -31,30 +30,15 @@ Commands:
|
|
|
31
30
|
help [command] display help for command
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
## upload
|
|
33
|
+
## upload
|
|
35
34
|
|
|
36
35
|
```
|
|
37
|
-
Usage: gobi media upload
|
|
36
|
+
Usage: gobi media upload [options] <file>
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
Upload a local file and return its media ID.
|
|
40
39
|
|
|
41
40
|
Options:
|
|
42
|
-
|
|
43
|
-
--content-type <contentType> MIME type (e.g. image/png, video/mp4)
|
|
44
|
-
--file-size <fileSize> File size in bytes
|
|
45
|
-
-h, --help display help for command
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## upload-finalize
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
Usage: gobi media upload-finalize [options]
|
|
52
|
-
|
|
53
|
-
Confirm that a media upload is complete.
|
|
54
|
-
|
|
55
|
-
Options:
|
|
56
|
-
--media-id <mediaId> Media ID from upload-init
|
|
57
|
-
-h, --help display help for command
|
|
41
|
+
-h, --help display help for command
|
|
58
42
|
```
|
|
59
43
|
|
|
60
44
|
## avatars
|
|
@@ -87,14 +71,14 @@ Usage: gobi media video-create [options]
|
|
|
87
71
|
Create an avatar video generation job.
|
|
88
72
|
|
|
89
73
|
Options:
|
|
90
|
-
--name <name>
|
|
91
|
-
--avatar-id <avatarId>
|
|
92
|
-
--voice-id <voiceId>
|
|
93
|
-
--script <script>
|
|
94
|
-
--background
|
|
95
|
-
--wait
|
|
96
|
-
-o, --output <path>
|
|
97
|
-
-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
|
|
98
82
|
```
|
|
99
83
|
|
|
100
84
|
## video-list
|
|
@@ -161,9 +145,9 @@ Options:
|
|
|
161
145
|
--generate-audio Generate audio for the video
|
|
162
146
|
--negative-prompt <negativePrompt> Negative prompt
|
|
163
147
|
--sample-count <count> Number of samples (1-4)
|
|
164
|
-
--first-frame
|
|
165
|
-
--last-frame
|
|
166
|
-
--reference-
|
|
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)
|
|
167
151
|
--wait Poll until generation completes
|
|
168
152
|
-o, --output <path> Download video to this path when done (implies --wait)
|
|
169
153
|
-h, --help display help for command
|
|
@@ -177,16 +161,16 @@ Usage: gobi media avatar-design [options]
|
|
|
177
161
|
Start a design-your-avatar job.
|
|
178
162
|
|
|
179
163
|
Options:
|
|
180
|
-
--name <name>
|
|
181
|
-
--gender <gender>
|
|
182
|
-
--age <age>
|
|
183
|
-
--ethnicity <ethnicity>
|
|
184
|
-
--outfit <outfit>
|
|
185
|
-
--background <background>
|
|
186
|
-
--no-portrait
|
|
187
|
-
--audio
|
|
188
|
-
--wait
|
|
189
|
-
-h, --help
|
|
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
|
|
190
174
|
```
|
|
191
175
|
|
|
192
176
|
## avatar-confirm
|
|
@@ -210,12 +194,12 @@ Usage: gobi media avatar-from-selfie [options]
|
|
|
210
194
|
Create an avatar from a selfie (instant or enhanced with prompt).
|
|
211
195
|
|
|
212
196
|
Options:
|
|
213
|
-
--name <name>
|
|
214
|
-
--photo
|
|
215
|
-
--prompt <prompt>
|
|
216
|
-
--audio
|
|
217
|
-
--wait
|
|
218
|
-
-h, --help
|
|
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
|
|
219
203
|
```
|
|
220
204
|
|
|
221
205
|
## avatar-job-status
|
|
@@ -238,16 +222,16 @@ Usage: gobi media image-generate [options]
|
|
|
238
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
|
|
239
223
|
|
|
240
224
|
Options:
|
|
241
|
-
--prompt <prompt>
|
|
242
|
-
--name <name>
|
|
243
|
-
--type <type>
|
|
244
|
-
--aspect-ratio <aspectRatio>
|
|
245
|
-
--negative-prompt <negativePrompt>
|
|
246
|
-
--seed <seed>
|
|
247
|
-
--reference-
|
|
248
|
-
--wait
|
|
249
|
-
-o, --output <path>
|
|
250
|
-
-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
|
|
251
235
|
```
|
|
252
236
|
|
|
253
237
|
## image-edit
|
|
@@ -258,11 +242,12 @@ Usage: gobi media image-edit [options]
|
|
|
258
242
|
Edit an existing image with a prompt (image-to-image).
|
|
259
243
|
|
|
260
244
|
Options:
|
|
261
|
-
--
|
|
262
|
-
--prompt <prompt>
|
|
263
|
-
--name <name>
|
|
264
|
-
--wait
|
|
265
|
-
-
|
|
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
|
|
266
251
|
```
|
|
267
252
|
|
|
268
253
|
## image-inpaint
|
|
@@ -273,12 +258,13 @@ Usage: gobi media image-inpaint [options]
|
|
|
273
258
|
Inpaint an image region using a mask.
|
|
274
259
|
|
|
275
260
|
Options:
|
|
276
|
-
--
|
|
277
|
-
--mask
|
|
278
|
-
--prompt <prompt>
|
|
279
|
-
--name <name>
|
|
280
|
-
--wait
|
|
281
|
-
-
|
|
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
|
|
282
268
|
```
|
|
283
269
|
|
|
284
270
|
## image-status
|