@gobi-ai/cli 0.9.5 → 0.9.7
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.7",
|
|
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.7",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/commands/media.js
CHANGED
|
@@ -116,15 +116,18 @@ export function registerMediaCommand(program) {
|
|
|
116
116
|
media
|
|
117
117
|
.command("video-create")
|
|
118
118
|
.description("Create an avatar video generation job.")
|
|
119
|
-
.
|
|
119
|
+
.option("--name <name>", "Name for the video (auto-generated if omitted)")
|
|
120
120
|
.requiredOption("--avatar-id <avatarId>", "Avatar to use")
|
|
121
121
|
.requiredOption("--voice-id <voiceId>", "Voice to use")
|
|
122
122
|
.requiredOption("--script <script>", "Script for the avatar to read")
|
|
123
123
|
.option("--background-media-id <backgroundMediaId>", "Background media ID (from upload)")
|
|
124
124
|
.option("--wait", "Poll until generation completes")
|
|
125
|
+
.option("-o, --output <path>", "Download video to this path when done (implies --wait)")
|
|
125
126
|
.action(async (opts) => {
|
|
127
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
128
|
+
const autoName = opts.name || `video-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
126
129
|
const body = {
|
|
127
|
-
name:
|
|
130
|
+
name: autoName,
|
|
128
131
|
avatarId: opts.avatarId,
|
|
129
132
|
voiceId: opts.voiceId,
|
|
130
133
|
script: opts.script,
|
|
@@ -134,10 +137,56 @@ export function registerMediaCommand(program) {
|
|
|
134
137
|
const resp = (await apiPost("/media-gen/videos", body));
|
|
135
138
|
let data = unwrapResp(resp);
|
|
136
139
|
const videoId = data.id || data.videoId;
|
|
137
|
-
if (
|
|
140
|
+
if (shouldWait && videoId) {
|
|
138
141
|
console.log(`Video ${videoId} queued — polling for completion…`);
|
|
139
142
|
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
140
143
|
}
|
|
144
|
+
// Download video to file if -o specified
|
|
145
|
+
if (opts.output && videoId && data.status === "inference_complete") {
|
|
146
|
+
const token = await getValidToken();
|
|
147
|
+
const dlUrl = `${BASE_URL}/media-gen/videos/${videoId}/download`;
|
|
148
|
+
const dlRes = await fetch(dlUrl, {
|
|
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)`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// If direct download fails, try getting the URL and fetching that
|
|
167
|
+
const dlRes2 = await fetch(dlUrl, {
|
|
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
|
+
}
|
|
189
|
+
}
|
|
141
190
|
if (isJsonMode(media)) {
|
|
142
191
|
jsonOut(data);
|
|
143
192
|
return;
|
|
@@ -188,9 +237,57 @@ export function registerMediaCommand(program) {
|
|
|
188
237
|
.command("video-status <id>")
|
|
189
238
|
.description("Poll video generation status.")
|
|
190
239
|
.option("--wait", "Poll until a terminal state is reached")
|
|
240
|
+
.option("-o, --output <path>", "Download video to this path when complete (implies --wait)")
|
|
191
241
|
.action(async (id, opts) => {
|
|
192
|
-
|
|
242
|
+
const shouldWait = opts.wait || !!opts.output;
|
|
243
|
+
if (shouldWait) {
|
|
193
244
|
const data = await pollStatus(`/media-gen/videos/${id}/status`, ["inference_complete", "inference_failed"]);
|
|
245
|
+
// Download if -o specified and completed
|
|
246
|
+
if (opts.output && data.status === "inference_complete") {
|
|
247
|
+
const token = await getValidToken();
|
|
248
|
+
const dlUrl = `${BASE_URL}/media-gen/videos/${id}/download`;
|
|
249
|
+
const dlRes = await fetch(dlUrl, {
|
|
250
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
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)`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Try manual redirect
|
|
268
|
+
const dlRes2 = await fetch(dlUrl, {
|
|
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
|
+
}
|
|
290
|
+
}
|
|
194
291
|
if (isJsonMode(media)) {
|
|
195
292
|
jsonOut(data);
|
|
196
293
|
return;
|
|
@@ -208,10 +305,57 @@ export function registerMediaCommand(program) {
|
|
|
208
305
|
});
|
|
209
306
|
media
|
|
210
307
|
.command("video-download <id>")
|
|
211
|
-
.description("
|
|
212
|
-
.
|
|
308
|
+
.description("Download a completed video (or get its URL).")
|
|
309
|
+
.option("-o, --output <path>", "Save video to this file path")
|
|
310
|
+
.action(async (id, opts) => {
|
|
213
311
|
const token = await getValidToken();
|
|
214
312
|
const url = `${BASE_URL}/media-gen/videos/${id}/download`;
|
|
313
|
+
// If -o specified, download directly to file
|
|
314
|
+
if (opts.output) {
|
|
315
|
+
const res = await fetch(url, {
|
|
316
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
317
|
+
redirect: "follow",
|
|
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)`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// If direct follow didn't work, try manual redirect
|
|
334
|
+
const res2 = await fetch(url, {
|
|
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");
|
|
357
|
+
}
|
|
358
|
+
// No -o: just return the URL (existing behavior)
|
|
215
359
|
const res = await fetch(url, {
|
|
216
360
|
headers: { Authorization: `Bearer ${token}` },
|
|
217
361
|
redirect: "manual",
|
package/package.json
CHANGED
|
@@ -52,6 +52,40 @@ Do NOT use markdown image syntax `` or `gobi://` URLs. Always use `![[me
|
|
|
52
52
|
- `image-download` takes a **positional** jobId (NOT `--job-id`): `gobi media image-download <jobId>`
|
|
53
53
|
- The `jobId` (or `id`) field is what you pass to `image-download` / `image-status` — NOT `mediaId`.
|
|
54
54
|
|
|
55
|
+
## Typical Workflow (Video Generation)
|
|
56
|
+
|
|
57
|
+
Single command — create and download in one step:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" -o media/<NAME>.mp4
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--name` is optional (auto-generated if omitted). Replace `<NAME>` with a short descriptive slug. Use `gobi media avatars` and `gobi media voices` to list available IDs.
|
|
64
|
+
|
|
65
|
+
The `-o` flag implies `--wait` and downloads the video when done.
|
|
66
|
+
|
|
67
|
+
**IMPORTANT: Avatars are pre-built system avatars ONLY.** You CANNOT create custom avatars from uploaded images. The `gobi media avatars` list is the complete set of available avatars. Do NOT attempt to upload an image and use its mediaId as an avatarId — it will fail.
|
|
68
|
+
|
|
69
|
+
To use a custom image (e.g. a generated image) as the **background** of a video, upload it first via `upload-init` / `upload-finalize`, then pass the mediaId as `--background-media-id`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 1. Upload custom image as background
|
|
73
|
+
gobi --json media upload-init --file-name "bg.png" --content-type "image/png" --file-size <SIZE>
|
|
74
|
+
curl -T "media/bg.png" -H "Content-Type: image/png" "<UPLOAD_URL>"
|
|
75
|
+
gobi --json media upload-finalize --media-id "<MEDIA_ID>"
|
|
76
|
+
|
|
77
|
+
# 2. Create video with pre-built avatar + custom background
|
|
78
|
+
gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" --background-media-id "<MEDIA_ID>" -o media/<NAME>.mp4
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**IMPORTANT: After downloading, show the video using Obsidian wiki-link syntax EXACTLY like this:**
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
![[media/<NAME>.mp4]]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Do NOT use markdown image/link syntax `` or `gobi://` URLs. Always use `![[media/<NAME>.mp4]]`.
|
|
88
|
+
|
|
55
89
|
## Available Commands
|
|
56
90
|
|
|
57
91
|
### Upload
|
|
@@ -70,7 +104,7 @@ Do NOT use markdown image syntax `` or `gobi://` URLs. Always use `![[me
|
|
|
70
104
|
- `gobi media video-list` — List all videos.
|
|
71
105
|
- `gobi media video-get` — Get video metadata.
|
|
72
106
|
- `gobi media video-status` — Poll video generation status.
|
|
73
|
-
- `gobi media video-download` —
|
|
107
|
+
- `gobi media video-download` — Download a completed video (`-o` to save to file).
|
|
74
108
|
|
|
75
109
|
### Images
|
|
76
110
|
|
|
@@ -17,7 +17,7 @@ Commands:
|
|
|
17
17
|
video-list List all videos.
|
|
18
18
|
video-get <id> Get video metadata.
|
|
19
19
|
video-status [options] <id> Poll video generation status.
|
|
20
|
-
video-download <id>
|
|
20
|
+
video-download [options] <id> Download a completed video (or get its URL).
|
|
21
21
|
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
|
|
22
22
|
image-edit [options] Edit an existing image with a prompt (image-to-image).
|
|
23
23
|
image-inpaint [options] Inpaint an image region using a mask.
|
|
@@ -82,12 +82,13 @@ Usage: gobi media video-create [options]
|
|
|
82
82
|
Create an avatar video generation job.
|
|
83
83
|
|
|
84
84
|
Options:
|
|
85
|
-
--name <name> Name for the video
|
|
85
|
+
--name <name> Name for the video (auto-generated if omitted)
|
|
86
86
|
--avatar-id <avatarId> Avatar to use
|
|
87
87
|
--voice-id <voiceId> Voice to use
|
|
88
88
|
--script <script> Script for the avatar to read
|
|
89
89
|
--background-media-id <backgroundMediaId> Background media ID (from upload)
|
|
90
90
|
--wait Poll until generation completes
|
|
91
|
+
-o, --output <path> Download video to this path when done (implies --wait)
|
|
91
92
|
-h, --help display help for command
|
|
92
93
|
```
|
|
93
94
|
|
|
@@ -121,8 +122,9 @@ Usage: gobi media video-status [options] <id>
|
|
|
121
122
|
Poll video generation status.
|
|
122
123
|
|
|
123
124
|
Options:
|
|
124
|
-
--wait
|
|
125
|
-
-
|
|
125
|
+
--wait Poll until a terminal state is reached
|
|
126
|
+
-o, --output <path> Download video to this path when complete (implies --wait)
|
|
127
|
+
-h, --help display help for command
|
|
126
128
|
```
|
|
127
129
|
|
|
128
130
|
## video-download
|
|
@@ -130,10 +132,11 @@ Options:
|
|
|
130
132
|
```
|
|
131
133
|
Usage: gobi media video-download [options] <id>
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
Download a completed video (or get its URL).
|
|
134
136
|
|
|
135
137
|
Options:
|
|
136
|
-
-
|
|
138
|
+
-o, --output <path> Save video to this file path
|
|
139
|
+
-h, --help display help for command
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
## image-generate
|