ima2-gen 1.1.20 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -25
- package/bin/commands/capabilities.js +2 -2
- package/bin/commands/capabilities.ts +2 -2
- package/bin/commands/defaults.js +2 -2
- package/bin/commands/defaults.ts +2 -2
- package/bin/commands/doctor.js +3 -3
- package/bin/commands/doctor.ts +3 -3
- package/bin/commands/edit.js +1 -1
- package/bin/commands/edit.ts +1 -1
- package/bin/commands/gen.js +1 -1
- package/bin/commands/gen.ts +1 -1
- package/bin/commands/grok.js +16 -11
- package/bin/commands/grok.ts +16 -11
- package/bin/commands/multimode.js +1 -1
- package/bin/commands/multimode.ts +1 -1
- package/bin/commands/observability.js +2 -2
- package/bin/commands/observability.ts +2 -2
- package/bin/commands/video.js +335 -13
- package/bin/commands/video.ts +249 -12
- package/bin/ima2.js +9 -9
- package/bin/ima2.ts +9 -9
- package/bin/lib/error-hints.js +2 -2
- package/bin/lib/error-hints.ts +2 -2
- package/docs/API.md +112 -3
- package/docs/CLI.md +61 -7
- package/docs/FAQ.ko.md +15 -20
- package/docs/FAQ.md +14 -19
- package/docs/NPX_QUICKSTART.md +40 -0
- package/docs/PROMPT_STUDIO.ko.md +1 -1
- package/docs/PROMPT_STUDIO.md +1 -1
- package/docs/README.ja.md +6 -16
- package/docs/README.ko.md +10 -20
- package/docs/README.zh-CN.md +7 -17
- package/docs/migration/runtime-test-inventory.md +8 -1
- package/lib/agentRuntime.js +19 -5
- package/lib/agentRuntime.ts +17 -5
- package/lib/capabilities.js +1 -1
- package/lib/capabilities.ts +1 -1
- package/lib/generationErrors.js +1 -1
- package/lib/generationErrors.ts +1 -1
- package/lib/grokProxyLauncher.js +26 -3
- package/lib/grokProxyLauncher.ts +27 -3
- package/lib/grokVideoAdapter.js +18 -89
- package/lib/grokVideoAdapter.ts +27 -88
- package/lib/grokVideoCanvas.js +25 -0
- package/lib/grokVideoCanvas.ts +26 -0
- package/lib/grokVideoDownload.js +58 -0
- package/lib/grokVideoDownload.ts +59 -0
- package/lib/grokVideoPlannerPrompt.js +64 -0
- package/lib/grokVideoPlannerPrompt.ts +67 -0
- package/lib/historyList.js +7 -1
- package/lib/historyList.ts +5 -1
- package/lib/oauthLauncher.js +21 -6
- package/lib/oauthLauncher.ts +22 -6
- package/lib/videoContinuity.js +149 -0
- package/lib/videoContinuity.ts +180 -0
- package/lib/videoFrameExtract.js +80 -0
- package/lib/videoFrameExtract.ts +78 -0
- package/node_modules/progrok/dist/index.js +187 -88
- package/node_modules/progrok/dist/index.js.map +1 -1
- package/node_modules/progrok/package.json +1 -1
- package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
- package/package.json +2 -2
- package/routes/index.js +4 -0
- package/routes/index.ts +4 -0
- package/routes/quota.js +66 -0
- package/routes/quota.ts +89 -0
- package/routes/video.js +77 -15
- package/routes/video.ts +82 -14
- package/routes/videoExtended.js +293 -0
- package/routes/videoExtended.ts +284 -0
- package/server.js +6 -2
- package/server.ts +5 -2
- package/skills/ima2/SKILL.md +320 -7
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-DS8uvoLI.js → AgentWorkspace-B_hq9CLg.js} +2 -2
- package/ui/dist/assets/{CardNewsWorkspace-CYxMsE67.js → CardNewsWorkspace-wD12J7qk.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-DccIc347.js → NodeCanvas-CI_wuPMf.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-BvxxwSJp.js → PromptBuilderPanel-CUTujJUV.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-u1_BFDRd.js → PromptImportDialog-CUi66jPK.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-C5uvkVSz.js → PromptImportDiscoverySection-Cm3vrjY4.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-D3E_O1SD.js → PromptImportFolderSection-DOtWTD9n.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-4gyf9CB9.js → PromptLibraryPanel-BMjQegRa.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +1 -0
- package/ui/dist/assets/{index-DoKtXbod.js → index-31uVIdt4.js} +1 -1
- package/ui/dist/assets/index-CjgnNtgt.css +1 -0
- package/ui/dist/assets/index-Da2s4_-5.js +36 -0
- package/ui/dist/index.html +2 -2
- package/vendor/progrok-0.2.0.tgz +0 -0
- package/ui/dist/assets/SettingsWorkspace-F3eNu3mJ.js +0 -1
- package/ui/dist/assets/index-B6tcw_UF.css +0 -1
- package/ui/dist/assets/index-DYOh6gQD.js +0 -32
- package/vendor/progrok-0.1.1.tgz +0 -0
package/bin/commands/video.ts
CHANGED
|
@@ -4,11 +4,56 @@ import { streamSse } from "../lib/sse.js";
|
|
|
4
4
|
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
5
5
|
import { config } from "../../config.js";
|
|
6
6
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
8
|
|
|
9
9
|
const VALID_RESOLUTIONS = new Set(["480p", "720p"]);
|
|
10
10
|
const VALID_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "auto"]);
|
|
11
11
|
const VALID_MODELS = new Set(["grok-imagine-video", "grok-imagine-video-1.5-preview"]);
|
|
12
|
+
const ACTIVE_VIDEO_PROMPT_GUIDANCE = "Active video prompt required: describe visual flow, motion flow, sound/no-music intent, dialogue/no-dialogue intent, and the desired ending frame. Pace the scene to naturally fill the selected duration with an opening composition, connected motion/emotion change, and stable ending frame.";
|
|
13
|
+
|
|
14
|
+
function parseIntegerFlag(value: unknown, fallback: number, label: string): number {
|
|
15
|
+
const raw = value === undefined ? String(fallback) : String(value);
|
|
16
|
+
if (!/^\d+$/.test(raw)) die(2, `${label} must be an integer`);
|
|
17
|
+
return Number(raw);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function rejectUnknownFlags(args: { _unknown?: string[] }): void {
|
|
21
|
+
if (args._unknown?.length) die(2, `unknown option: ${args._unknown[0]}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readJsonResponse(res: Response, label: string): Promise<Record<string, unknown>> {
|
|
25
|
+
const text = await res.text();
|
|
26
|
+
try {
|
|
27
|
+
return text ? JSON.parse(text) as Record<string, unknown> : {};
|
|
28
|
+
} catch {
|
|
29
|
+
die(1, `${label} failed: expected JSON response, got ${text.slice(0, 120) || `HTTP ${res.status}`}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseTimeoutSeconds(seconds: unknown): number {
|
|
34
|
+
const sec = parseIntegerFlag(seconds, 600, "--timeout");
|
|
35
|
+
if (sec < 1) die(2, "--timeout must be at least 1");
|
|
36
|
+
return sec;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function timeoutSignal(seconds: unknown): AbortSignal {
|
|
40
|
+
const sec = parseTimeoutSeconds(seconds);
|
|
41
|
+
return AbortSignal.timeout(sec * 1000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function writeBuffer(path: string, buf: Buffer): Promise<void> {
|
|
45
|
+
await mkdir(dirname(path), { recursive: true }).catch(() => {});
|
|
46
|
+
await writeFile(path, buf);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function downloadReturnedVideo(serverBase: string, data: Record<string, unknown>, outPath: string, signal: AbortSignal): Promise<void> {
|
|
50
|
+
const rawUrl = typeof data.url === "string" ? data.url : "";
|
|
51
|
+
const url = rawUrl.startsWith("/") ? `${serverBase}${rawUrl}` : rawUrl;
|
|
52
|
+
if (!url) die(1, "server did not return a video url");
|
|
53
|
+
const res = await fetch(url, { signal });
|
|
54
|
+
if (!res.ok) die(1, `failed to download video: HTTP ${res.status}`);
|
|
55
|
+
await writeBuffer(outPath, Buffer.from(await res.arrayBuffer()));
|
|
56
|
+
}
|
|
12
57
|
|
|
13
58
|
const SPEC = {
|
|
14
59
|
flags: {
|
|
@@ -30,11 +75,24 @@ const SPEC = {
|
|
|
30
75
|
|
|
31
76
|
const HELP = `
|
|
32
77
|
ima2 video <prompt...> [options]
|
|
78
|
+
ima2 video edit <prompt> --video <url|file_id|generated-file>
|
|
79
|
+
ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]
|
|
80
|
+
ima2 video continue <prompt> --video <generated-file>
|
|
81
|
+
ima2 video frame <file> [--last] [-o output.png]
|
|
82
|
+
ima2 video analyze <generated-file>
|
|
33
83
|
|
|
34
|
-
Generate
|
|
84
|
+
Generate, edit, extend, or analyze video via Grok.
|
|
35
85
|
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
Subcommands:
|
|
87
|
+
(default) Generate video (T2V / I2V / Ref2V)
|
|
88
|
+
edit Edit existing video with text prompt (V2V)
|
|
89
|
+
extend Continue video from last frame
|
|
90
|
+
continue Generate a new I2V clip from a generated video's last frame with lineage
|
|
91
|
+
frame Extract a frame from video (requires ffmpeg on server)
|
|
92
|
+
analyze Analyze video with Grok 4.3 vision
|
|
93
|
+
|
|
94
|
+
Options (generate mode):
|
|
95
|
+
--duration <1..15> Duration in seconds. Default: 5. Prompt motion should naturally fill this length
|
|
38
96
|
--resolution <480p|720p> Default: 480p
|
|
39
97
|
--aspect-ratio <ratio|auto> 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto. Default: auto
|
|
40
98
|
--model <name> grok-imagine-video, grok-imagine-video-1.5-preview
|
|
@@ -47,6 +105,10 @@ const HELP = `
|
|
|
47
105
|
--server <url> Override server URL
|
|
48
106
|
--session <id> Session ID
|
|
49
107
|
|
|
108
|
+
Edit/extend subcommands:
|
|
109
|
+
--video <value> HTTPS URL, xAI file_id, data URL, or generated filename
|
|
110
|
+
--duration <2..10> Extension duration only. Default: 6
|
|
111
|
+
|
|
50
112
|
Modes (auto-detected from --ref count):
|
|
51
113
|
0 refs → text-to-video
|
|
52
114
|
1 ref → image-to-video
|
|
@@ -55,17 +117,29 @@ const HELP = `
|
|
|
55
117
|
Examples:
|
|
56
118
|
ima2 video "a cat playing piano"
|
|
57
119
|
ima2 video "animate this" --ref photo.png --duration 10
|
|
58
|
-
ima2 video "
|
|
120
|
+
ima2 video edit "make it sunset" --video https://vidgen.x.ai/.../clip.mp4
|
|
121
|
+
ima2 video extend "camera pulls back" --video https://vidgen.x.ai/.../clip.mp4 --duration 5
|
|
122
|
+
ima2 video continue "she turns back as the music cuts to room tone" --video 1780226256355_50252101.mp4
|
|
123
|
+
ima2 video frame 1780226256355_50252101.mp4 --last -o lastframe.png
|
|
124
|
+
ima2 video analyze 1780226256355_50252101.mp4
|
|
59
125
|
`;
|
|
60
126
|
|
|
61
127
|
export default async function videoCmd(argv: string[]) {
|
|
128
|
+
const sub = argv[0];
|
|
129
|
+
if (sub === "edit") return videoEditCmd(argv.slice(1));
|
|
130
|
+
if (sub === "extend") return videoExtendCmd(argv.slice(1));
|
|
131
|
+
if (sub === "continue") return videoContinueCmd(argv.slice(1));
|
|
132
|
+
if (sub === "frame") return videoFrameCmd(argv.slice(1));
|
|
133
|
+
if (sub === "analyze") return videoAnalyzeCmd(argv.slice(1));
|
|
134
|
+
|
|
62
135
|
const args = parseArgs(argv, SPEC);
|
|
136
|
+
rejectUnknownFlags(args);
|
|
63
137
|
if (args.help) { out(HELP); return; }
|
|
64
138
|
|
|
65
139
|
const prompt = args.positional.join(" ");
|
|
66
|
-
if (!prompt) die(2,
|
|
140
|
+
if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
|
|
67
141
|
|
|
68
|
-
const duration =
|
|
142
|
+
const duration = parseIntegerFlag(args.duration, 5, "--duration");
|
|
69
143
|
if (duration < 1 || duration > 15) die(2, "--duration must be between 1 and 15");
|
|
70
144
|
|
|
71
145
|
const resolution = String(args.resolution);
|
|
@@ -80,6 +154,11 @@ export default async function videoCmd(argv: string[]) {
|
|
|
80
154
|
|
|
81
155
|
const refs = (Array.isArray(args.ref) ? args.ref : []) as string[];
|
|
82
156
|
if (refs.length > 7) die(2, "max 7 --ref attachments for video");
|
|
157
|
+
if (refs.length >= 2 && duration > 10) die(2, "--duration must be between 1 and 10 when using 2 or more --ref attachments");
|
|
158
|
+
|
|
159
|
+
const timeoutSeconds = parseIntegerFlag(args.timeout, 600, "--timeout");
|
|
160
|
+
if (timeoutSeconds < 1) die(2, "--timeout must be at least 1");
|
|
161
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
83
162
|
|
|
84
163
|
let server;
|
|
85
164
|
try { server = await resolveServer({ serverFlag: args.server }); }
|
|
@@ -90,7 +169,6 @@ export default async function videoCmd(argv: string[]) {
|
|
|
90
169
|
return buf.toString("base64");
|
|
91
170
|
}));
|
|
92
171
|
|
|
93
|
-
const timeoutMs = (parseInt(String(args.timeout)) || 600) * 1000;
|
|
94
172
|
const requestId = `req_cli_video_${Date.now().toString(36)}`;
|
|
95
173
|
|
|
96
174
|
const body: Record<string, unknown> = {
|
|
@@ -145,7 +223,7 @@ export default async function videoCmd(argv: string[]) {
|
|
|
145
223
|
break;
|
|
146
224
|
case "error":
|
|
147
225
|
if (!args.json && lastProgress >= 0) process.stdout.write("\n");
|
|
148
|
-
die(1, `video error: ${ev.data.error || ev.data}${ev.data.code ? ` (${ev.data.code})` : ""}`);
|
|
226
|
+
die(1, `video error: ${ev.data.error || ev.data}${ev.data.guidance ? `\n${ev.data.guidance}` : ""}${ev.data.code ? ` (${ev.data.code})` : ""}`);
|
|
149
227
|
}
|
|
150
228
|
}
|
|
151
229
|
} catch (e: unknown) {
|
|
@@ -175,11 +253,10 @@ export default async function videoCmd(argv: string[]) {
|
|
|
175
253
|
|
|
176
254
|
// Download the video file from server
|
|
177
255
|
const videoUrl = `${server.base}${doneData.url || `/generated/${encodeURIComponent(filename)}`}`;
|
|
178
|
-
const dlRes = await fetch(videoUrl, { signal:
|
|
256
|
+
const dlRes = await fetch(videoUrl, { signal: timeoutSignal(args.timeout) });
|
|
179
257
|
if (!dlRes.ok) die(1, `failed to download video: HTTP ${dlRes.status}`);
|
|
180
258
|
const videoBuf = Buffer.from(await dlRes.arrayBuffer());
|
|
181
|
-
await
|
|
182
|
-
await writeFile(target, videoBuf);
|
|
259
|
+
await writeBuffer(target, videoBuf);
|
|
183
260
|
|
|
184
261
|
if (args.json) {
|
|
185
262
|
json({
|
|
@@ -203,3 +280,163 @@ function renderBar(pct: number): string {
|
|
|
203
280
|
const filled = Math.round((pct / 100) * width);
|
|
204
281
|
return color.green("█".repeat(filled)) + color.dim("░".repeat(width - filled));
|
|
205
282
|
}
|
|
283
|
+
|
|
284
|
+
async function runVideoGenerateRequest(serverBase: string, body: Record<string, unknown>, timeout: unknown, silent: boolean): Promise<Record<string, unknown>> {
|
|
285
|
+
let doneData: Record<string, unknown> | null = null;
|
|
286
|
+
let lastProgress = -1;
|
|
287
|
+
for await (const ev of streamSse(`${serverBase}/api/video/generate`, {
|
|
288
|
+
body: { provider: "grok", ...body },
|
|
289
|
+
signal: timeoutSignal(timeout),
|
|
290
|
+
headers: typeof body.requestId === "string" ? { "X-Request-Id": body.requestId } : undefined,
|
|
291
|
+
})) {
|
|
292
|
+
if (ev.event === "progress") {
|
|
293
|
+
const pct = typeof ev.data.progress === "number" ? Math.round(ev.data.progress * 100) : null;
|
|
294
|
+
if (pct !== null && pct !== lastProgress && !silent) {
|
|
295
|
+
process.stdout.write(`\r ${renderBar(pct)} ${pct}%`);
|
|
296
|
+
lastProgress = pct;
|
|
297
|
+
}
|
|
298
|
+
} else if (ev.event === "done") {
|
|
299
|
+
if (!silent && lastProgress >= 0) process.stdout.write("\n");
|
|
300
|
+
doneData = ev.data;
|
|
301
|
+
} else if (ev.event === "error") {
|
|
302
|
+
if (!silent && lastProgress >= 0) process.stdout.write("\n");
|
|
303
|
+
die(1, `video error: ${ev.data.error || ev.data}${ev.data.guidance ? `\n${ev.data.guidance}` : ""}${ev.data.code ? ` (${ev.data.code})` : ""}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!doneData) die(1, "server did not return a video result");
|
|
307
|
+
return doneData;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Subcommands ---
|
|
311
|
+
|
|
312
|
+
async function videoEditCmd(argv: string[]) {
|
|
313
|
+
const spec = { flags: { video: { type: "string" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default: "600" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
314
|
+
const args = parseArgs(argv, spec);
|
|
315
|
+
rejectUnknownFlags(args);
|
|
316
|
+
if (args.help) { out(` ima2 video edit <prompt> --video <url|file_id|generated-file>\n\n Edit existing video with text prompt (real V2V).\n Model: grok-imagine-video only. Input: mp4, max 8.7s.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n -o, --out <file> Download edited video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`); return; }
|
|
317
|
+
const prompt = args.positional.join(" ");
|
|
318
|
+
if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
|
|
319
|
+
if (!args.video) die(2, "--video <url> is required");
|
|
320
|
+
parseTimeoutSeconds(args.timeout);
|
|
321
|
+
let server;
|
|
322
|
+
try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
|
|
323
|
+
const res = await fetch(`${server.base}/api/video/edit`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, videoUrl: args.video }), signal: timeoutSignal(args.timeout) });
|
|
324
|
+
const data = await readJsonResponse(res, "edit");
|
|
325
|
+
if (!res.ok) die(1, `edit failed: ${data.error ?? res.status}`);
|
|
326
|
+
const outPath = (args.out || args.output) as string | undefined;
|
|
327
|
+
if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
|
|
328
|
+
if (args.json) { out(JSON.stringify(data, null, 2)); } else { out(color.green("✓ ") + `Edited video: ${data.url}`); }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function videoExtendCmd(argv: string[]) {
|
|
332
|
+
const spec = { flags: { video: { type: "string" }, duration: { type: "string", default: "6" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default: "600" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
333
|
+
const args = parseArgs(argv, spec);
|
|
334
|
+
rejectUnknownFlags(args);
|
|
335
|
+
if (args.help) { out(` ima2 video extend <prompt> --video <url|file_id|generated-file> [--duration 6]\n\n Extend video from its last frame.\n Model: grok-imagine-video only. Extension: 2-10s.\n\n Options:\n --video <value> Source video HTTPS URL, xAI file_id, data URL, or generated filename (required)\n --duration <2-10> Extension duration (default: 6)\n -o, --out <file> Download extended video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`); return; }
|
|
336
|
+
const prompt = args.positional.join(" ");
|
|
337
|
+
if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
|
|
338
|
+
if (!args.video) die(2, "--video <url> is required");
|
|
339
|
+
const duration = parseIntegerFlag(args.duration, 6, "--duration");
|
|
340
|
+
if (duration < 2 || duration > 10) die(2, "--duration must be between 2 and 10");
|
|
341
|
+
parseTimeoutSeconds(args.timeout);
|
|
342
|
+
let server;
|
|
343
|
+
try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
|
|
344
|
+
const res = await fetch(`${server.base}/api/video/extend`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, videoUrl: args.video, duration }), signal: timeoutSignal(args.timeout) });
|
|
345
|
+
const data = await readJsonResponse(res, "extend");
|
|
346
|
+
if (!res.ok) die(1, `extend failed: ${data.error ?? res.status}`);
|
|
347
|
+
const outPath = (args.out || args.output) as string | undefined;
|
|
348
|
+
if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
|
|
349
|
+
if (args.json) { out(JSON.stringify(data, null, 2)); } else { out(color.green("✓ ") + `Extended video (${data.duration}s): ${data.url}`); }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function videoContinueCmd(argv: string[]) {
|
|
353
|
+
const spec = {
|
|
354
|
+
flags: {
|
|
355
|
+
video: { type: "string" },
|
|
356
|
+
duration: { type: "string", default: "5" },
|
|
357
|
+
resolution: { type: "string", default: "720p" },
|
|
358
|
+
"aspect-ratio": { type: "string", default: "auto" },
|
|
359
|
+
model: { type: "string" },
|
|
360
|
+
out: { short: "o", type: "string" },
|
|
361
|
+
output: { type: "string" },
|
|
362
|
+
json: { type: "boolean" },
|
|
363
|
+
timeout: { type: "string", default: "600" },
|
|
364
|
+
server: { type: "string" },
|
|
365
|
+
help: { short: "h", type: "boolean" },
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
const args = parseArgs(argv, spec);
|
|
369
|
+
rejectUnknownFlags(args);
|
|
370
|
+
if (args.help) {
|
|
371
|
+
out(` ima2 video continue <prompt> --video <generated-file>\n\n Generate a new clip from a generated video's last frame and carry branch-local revisedPrompt lineage.\n\n Prompt must describe visual flow, motion, sound/music/no-music, dialogue/no-dialogue, ending frame, and how the selected duration should feel naturally filled.\n\n Options:\n --video <file> Generated .mp4 filename (required)\n --duration <1..15> Default: 5. Prompt motion should naturally fill this length\n --resolution <480p|720p> Default: 720p\n --aspect-ratio <ratio|auto> Default: auto\n --model <name> grok-imagine-video, grok-imagine-video-1.5-preview\n -o, --out <file> Download continued video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 600\n --server <url> Override server URL`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const prompt = args.positional.join(" ");
|
|
375
|
+
if (!prompt.trim()) die(2, ACTIVE_VIDEO_PROMPT_GUIDANCE);
|
|
376
|
+
if (!args.video) die(2, "--video <generated-file> is required");
|
|
377
|
+
const duration = parseIntegerFlag(args.duration, 5, "--duration");
|
|
378
|
+
if (duration < 1 || duration > 15) die(2, "--duration must be between 1 and 15");
|
|
379
|
+
const resolution = String(args.resolution);
|
|
380
|
+
if (!VALID_RESOLUTIONS.has(resolution)) die(2, "--resolution must be one of: 480p, 720p");
|
|
381
|
+
const aspectRatio = String(args["aspect-ratio"]);
|
|
382
|
+
if (!VALID_ASPECT_RATIOS.has(aspectRatio)) die(2, "--aspect-ratio must be one of: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto");
|
|
383
|
+
if (args.model && !VALID_MODELS.has(String(args.model))) {
|
|
384
|
+
die(2, "--model must be one of: grok-imagine-video, grok-imagine-video-1.5-preview");
|
|
385
|
+
}
|
|
386
|
+
parseTimeoutSeconds(args.timeout);
|
|
387
|
+
let server;
|
|
388
|
+
try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
|
|
389
|
+
const requestId = `req_cli_video_continue_${Date.now().toString(36)}`;
|
|
390
|
+
const body: Record<string, unknown> = {
|
|
391
|
+
prompt,
|
|
392
|
+
requestId,
|
|
393
|
+
duration,
|
|
394
|
+
resolution,
|
|
395
|
+
aspectRatio,
|
|
396
|
+
continueFromVideo: args.video,
|
|
397
|
+
};
|
|
398
|
+
if (args.model) body.model = args.model;
|
|
399
|
+
const data = await runVideoGenerateRequest(server.base, body, args.timeout, Boolean(args.json));
|
|
400
|
+
const outPath = (args.out || args.output) as string | undefined;
|
|
401
|
+
if (outPath) await downloadReturnedVideo(server.base, data, outPath, timeoutSignal(args.timeout));
|
|
402
|
+
if (args.json) out(JSON.stringify(data, null, 2));
|
|
403
|
+
else out(color.green("✓ ") + `Continued video: ${data.url}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function videoFrameCmd(argv: string[]) {
|
|
407
|
+
const spec = { flags: { last: { type: "boolean" }, position: { type: "string" }, out: { type: "string" }, output: { short: "o", type: "string" }, timeout: { type: "string", default: "60" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
408
|
+
const args = parseArgs(argv, spec);
|
|
409
|
+
rejectUnknownFlags(args);
|
|
410
|
+
if (args.help) { out(` ima2 video frame <generated-file> [--last] [--position <sec>] [-o output.png]\n\n Extract a frame from a generated video file.\n\n Options:\n --last Extract last frame (default)\n --position <sec> Extract frame at specific second\n -o, --output <path> Output file path\n --out <path> Alias for --output\n --timeout <sec> Default: 60\n --server <url> Override server URL`); return; }
|
|
411
|
+
const file = args.positional[0];
|
|
412
|
+
if (!file) die(2, "file argument required");
|
|
413
|
+
if (args.last && args.position) die(2, "use either --last or --position, not both");
|
|
414
|
+
const position = args.last ? "last" : (String(args.position || "last"));
|
|
415
|
+
if (position !== "last" && !/^\d+(\.\d+)?$/.test(position)) die(2, "--position must be a non-negative number");
|
|
416
|
+
parseTimeoutSeconds(args.timeout);
|
|
417
|
+
let server;
|
|
418
|
+
try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
|
|
419
|
+
const url = `${server.base}/api/video/frame?file=${encodeURIComponent(file)}&position=${encodeURIComponent(position)}`;
|
|
420
|
+
const res = await fetch(url, { signal: timeoutSignal(args.timeout) });
|
|
421
|
+
if (!res.ok) { const d = await readJsonResponse(res, "frame extraction"); die(1, `frame extraction failed: ${(d as any).error || res.status}`); }
|
|
422
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
423
|
+
const outPath = (args.output || args.out) as string || `frame-${basename(file).replace(/\.[^.]+$/, "")}.png`;
|
|
424
|
+
await writeBuffer(outPath, buf);
|
|
425
|
+
out(color.green("✓ ") + `Frame saved: ${outPath} (${buf.length} bytes)`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function videoAnalyzeCmd(argv: string[]) {
|
|
429
|
+
const spec = { flags: { json: { type: "boolean" }, timeout: { type: "string", default: "180" }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
430
|
+
const args = parseArgs(argv, spec);
|
|
431
|
+
rejectUnknownFlags(args);
|
|
432
|
+
if (args.help) { out(` ima2 video analyze <generated-file>\n\n Analyze first/last frames from a generated .mp4 with Grok 4.3 image understanding. Outputs structured recreation prompt.\n\n Options:\n --json Print JSON result\n --timeout <sec> Default: 180\n --server <url> Override server URL`); return; }
|
|
433
|
+
const videoUrl = args.positional[0];
|
|
434
|
+
if (!videoUrl) die(2, "generated video filename required");
|
|
435
|
+
parseTimeoutSeconds(args.timeout);
|
|
436
|
+
let server;
|
|
437
|
+
try { server = await resolveServer({ serverFlag: args.server }); } catch (e: unknown) { die(exitCodeForError(e), (e as Error).message); throw e; }
|
|
438
|
+
const res = await fetch(`${server.base}/api/video/analyze`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ videoUrl }), signal: timeoutSignal(args.timeout) });
|
|
439
|
+
const data = await readJsonResponse(res, "analyze");
|
|
440
|
+
if (!res.ok) die(1, `analyze failed: ${(data as any).error || res.status}`);
|
|
441
|
+
if (args.json) { out(JSON.stringify(data, null, 2)); } else { out((data as any).analysis); }
|
|
442
|
+
}
|
package/bin/ima2.js
CHANGED
|
@@ -88,7 +88,7 @@ async function setup() {
|
|
|
88
88
|
saveConfig(config);
|
|
89
89
|
console.log("\n Starting Grok OAuth login...\n");
|
|
90
90
|
try {
|
|
91
|
-
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
|
|
91
|
+
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
|
|
92
92
|
}
|
|
93
93
|
catch {
|
|
94
94
|
console.log("\n Grok login failed or cancelled. You can retry with 'ima2 grok login'.\n");
|
|
@@ -121,7 +121,7 @@ async function setup() {
|
|
|
121
121
|
// Grok OAuth
|
|
122
122
|
console.log(" Running Grok OAuth login...\n");
|
|
123
123
|
try {
|
|
124
|
-
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
|
|
124
|
+
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
|
|
125
125
|
}
|
|
126
126
|
catch {
|
|
127
127
|
console.log("\n Grok login failed. You can retry with 'ima2 grok login'.\n");
|
|
@@ -135,7 +135,7 @@ async function setup() {
|
|
|
135
135
|
config.oauth.disableAutoStart = false;
|
|
136
136
|
delete config.apiKey;
|
|
137
137
|
saveConfig(config);
|
|
138
|
-
console.log("\n Starting OAuth login...\n");
|
|
138
|
+
console.log("\n Starting GPT OAuth login...\n");
|
|
139
139
|
const auth = detectCodexAuth();
|
|
140
140
|
const hasAuth = auth.authed;
|
|
141
141
|
if (!hasAuth) {
|
|
@@ -154,10 +154,10 @@ async function setup() {
|
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
156
|
const how = auth.probe === "authed" ? "codex CLI" : "auth file";
|
|
157
|
-
console.log(` Existing OAuth session found (${how}).\n`);
|
|
157
|
+
console.log(` Existing GPT OAuth session found (${how}).\n`);
|
|
158
158
|
}
|
|
159
159
|
saveConfig(config);
|
|
160
|
-
console.log(" OAuth configured. Starting server...\n");
|
|
160
|
+
console.log(" GPT OAuth configured. Starting server...\n");
|
|
161
161
|
}
|
|
162
162
|
rl.close();
|
|
163
163
|
return config;
|
|
@@ -222,7 +222,7 @@ async function showStatus() {
|
|
|
222
222
|
}
|
|
223
223
|
// Check OAuth auth files + codex CLI probe
|
|
224
224
|
const auth = detectCodexAuth();
|
|
225
|
-
console.log(` OAuth sessions:`);
|
|
225
|
+
console.log(` GPT OAuth sessions:`);
|
|
226
226
|
console.log(` ${auth.files.codex} ${auth.fileHits.codex ? "✓" : "✗"}`);
|
|
227
227
|
console.log(` ${auth.files.chatgpt} ${auth.fileHits.chatgpt ? "✓" : "✗"}`);
|
|
228
228
|
if (auth.fileHits.xdgCodex) {
|
|
@@ -255,7 +255,7 @@ function showHelp() {
|
|
|
255
255
|
|
|
256
256
|
Server commands:
|
|
257
257
|
serve [--dev] Start the image generation server
|
|
258
|
-
setup, login Configure API key or OAuth (interactive)
|
|
258
|
+
setup, login Configure API key or GPT OAuth (interactive)
|
|
259
259
|
status Show current configuration status
|
|
260
260
|
doctor Diagnose environment and setup
|
|
261
261
|
open Open web UI in browser
|
|
@@ -283,8 +283,8 @@ function showHelp() {
|
|
|
283
283
|
storage <sub> Storage status / open-dir (ima2 storage --help)
|
|
284
284
|
billing API usage / quota
|
|
285
285
|
providers Configured providers
|
|
286
|
-
oauth <sub> OAuth proxy status (ima2 oauth --help)
|
|
287
|
-
grok <sub> Bundled
|
|
286
|
+
oauth <sub> GPT OAuth proxy status (ima2 oauth --help)
|
|
287
|
+
grok <sub> Bundled Grok auth/status (ima2 grok --help)
|
|
288
288
|
config <sub> Config get/set/ls/path/rm (ima2 config --help)
|
|
289
289
|
defaults <sub> Inspect/change model defaults (ima2 defaults --help)
|
|
290
290
|
capabilities Agent capability metadata (ima2 capabilities --help)
|
package/bin/ima2.ts
CHANGED
|
@@ -91,7 +91,7 @@ async function setup() {
|
|
|
91
91
|
saveConfig(config);
|
|
92
92
|
console.log("\n Starting Grok OAuth login...\n");
|
|
93
93
|
try {
|
|
94
|
-
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
|
|
94
|
+
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
|
|
95
95
|
} catch {
|
|
96
96
|
console.log("\n Grok login failed or cancelled. You can retry with 'ima2 grok login'.\n");
|
|
97
97
|
rl.close();
|
|
@@ -119,7 +119,7 @@ async function setup() {
|
|
|
119
119
|
// Grok OAuth
|
|
120
120
|
console.log(" Running Grok OAuth login...\n");
|
|
121
121
|
try {
|
|
122
|
-
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login`, { stdio: "inherit" });
|
|
122
|
+
execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
|
|
123
123
|
} catch {
|
|
124
124
|
console.log("\n Grok login failed. You can retry with 'ima2 grok login'.\n");
|
|
125
125
|
}
|
|
@@ -131,7 +131,7 @@ async function setup() {
|
|
|
131
131
|
config.oauth.disableAutoStart = false;
|
|
132
132
|
delete config.apiKey;
|
|
133
133
|
saveConfig(config);
|
|
134
|
-
console.log("\n Starting OAuth login...\n");
|
|
134
|
+
console.log("\n Starting GPT OAuth login...\n");
|
|
135
135
|
|
|
136
136
|
const auth = detectCodexAuth();
|
|
137
137
|
const hasAuth = auth.authed;
|
|
@@ -152,11 +152,11 @@ async function setup() {
|
|
|
152
152
|
}
|
|
153
153
|
} else {
|
|
154
154
|
const how = auth.probe === "authed" ? "codex CLI" : "auth file";
|
|
155
|
-
console.log(` Existing OAuth session found (${how}).\n`);
|
|
155
|
+
console.log(` Existing GPT OAuth session found (${how}).\n`);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
saveConfig(config);
|
|
159
|
-
console.log(" OAuth configured. Starting server...\n");
|
|
159
|
+
console.log(" GPT OAuth configured. Starting server...\n");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
rl.close();
|
|
@@ -234,7 +234,7 @@ async function showStatus() {
|
|
|
234
234
|
|
|
235
235
|
// Check OAuth auth files + codex CLI probe
|
|
236
236
|
const auth = detectCodexAuth();
|
|
237
|
-
console.log(` OAuth sessions:`);
|
|
237
|
+
console.log(` GPT OAuth sessions:`);
|
|
238
238
|
console.log(` ${auth.files.codex} ${auth.fileHits.codex ? "✓" : "✗"}`);
|
|
239
239
|
console.log(` ${auth.files.chatgpt} ${auth.fileHits.chatgpt ? "✓" : "✗"}`);
|
|
240
240
|
if (auth.fileHits.xdgCodex) {
|
|
@@ -269,7 +269,7 @@ function showHelp() {
|
|
|
269
269
|
|
|
270
270
|
Server commands:
|
|
271
271
|
serve [--dev] Start the image generation server
|
|
272
|
-
setup, login Configure API key or OAuth (interactive)
|
|
272
|
+
setup, login Configure API key or GPT OAuth (interactive)
|
|
273
273
|
status Show current configuration status
|
|
274
274
|
doctor Diagnose environment and setup
|
|
275
275
|
open Open web UI in browser
|
|
@@ -297,8 +297,8 @@ function showHelp() {
|
|
|
297
297
|
storage <sub> Storage status / open-dir (ima2 storage --help)
|
|
298
298
|
billing API usage / quota
|
|
299
299
|
providers Configured providers
|
|
300
|
-
oauth <sub> OAuth proxy status (ima2 oauth --help)
|
|
301
|
-
grok <sub> Bundled
|
|
300
|
+
oauth <sub> GPT OAuth proxy status (ima2 oauth --help)
|
|
301
|
+
grok <sub> Bundled Grok auth/status (ima2 grok --help)
|
|
302
302
|
config <sub> Config get/set/ls/path/rm (ima2 config --help)
|
|
303
303
|
defaults <sub> Inspect/change model defaults (ima2 defaults --help)
|
|
304
304
|
capabilities Agent capability metadata (ima2 capabilities --help)
|
package/bin/lib/error-hints.js
CHANGED
|
@@ -3,11 +3,11 @@ const HINTS = {
|
|
|
3
3
|
APIKEY_DISABLED: "API-key generation is supported in current builds; switch providers or update the configured API key.",
|
|
4
4
|
IMAGE_MODEL_UNSUPPORTED: "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
|
|
5
5
|
INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
|
|
6
|
-
OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
|
|
6
|
+
OAUTH_UNAVAILABLE: "GPT OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
|
|
7
7
|
NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
|
|
8
8
|
SAFETY_REFUSAL: "The image backend refused this generation.",
|
|
9
9
|
MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
|
|
10
|
-
AUTH_CHATGPT_EXPIRED: "
|
|
10
|
+
AUTH_CHATGPT_EXPIRED: "Re-run `ima2 setup` (option 1), then restart `ima2 serve`.",
|
|
11
11
|
REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
|
|
12
12
|
REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
|
|
13
13
|
};
|
package/bin/lib/error-hints.ts
CHANGED
|
@@ -4,11 +4,11 @@ const HINTS: Record<string, string> = {
|
|
|
4
4
|
IMAGE_MODEL_UNSUPPORTED:
|
|
5
5
|
"This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
|
|
6
6
|
INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
|
|
7
|
-
OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
|
|
7
|
+
OAUTH_UNAVAILABLE: "GPT OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
|
|
8
8
|
NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
|
|
9
9
|
SAFETY_REFUSAL: "The image backend refused this generation.",
|
|
10
10
|
MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
|
|
11
|
-
AUTH_CHATGPT_EXPIRED: "
|
|
11
|
+
AUTH_CHATGPT_EXPIRED: "Re-run `ima2 setup` (option 1), then restart `ima2 serve`.",
|
|
12
12
|
REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
|
|
13
13
|
REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
|
|
14
14
|
};
|
package/docs/API.md
CHANGED
|
@@ -234,6 +234,8 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
|
|
|
234
234
|
"sourceImage": "<base64>",
|
|
235
235
|
"referenceImages": ["<base64>", "<base64>"],
|
|
236
236
|
"referenceFilenames": ["existing-file.png"],
|
|
237
|
+
"continueFromVideo": "1780226256355_50252101.mp4",
|
|
238
|
+
"continuityLineage": { "lineageId": "optional-client-hint", "entries": [] },
|
|
237
239
|
"sessionId": "optional",
|
|
238
240
|
"requestId": "optional-client-id"
|
|
239
241
|
}
|
|
@@ -263,16 +265,67 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
|
|
|
263
265
|
| `sourceFilename` | string | — | Existing generated file for image-to-video |
|
|
264
266
|
| `referenceImages` | string[] | — | Base64 images for reference-to-video |
|
|
265
267
|
| `referenceFilenames` | string[] | — | Existing generated files for reference-to-video |
|
|
268
|
+
| `continueFromVideo` | string | — | Generated `.mp4` parent; server extracts its last frame and rebuilds lineage from sidecar |
|
|
269
|
+
| `continuityLineage` | object | — | Optional client hint; used only when `continueFromVideo` is absent |
|
|
270
|
+
|
|
271
|
+
Blank prompts return `PROMPT_REQUIRED` with a `guidance` string. The active
|
|
272
|
+
prompt should describe visual flow, motion flow, sound/music/no-music,
|
|
273
|
+
dialogue/no-dialogue, ending frame, and duration pacing. The video planner uses
|
|
274
|
+
the selected duration as the full clip runtime and expands short requests into a
|
|
275
|
+
production-level sequence with opening composition, connected motion/emotion
|
|
276
|
+
change, and a stable ending frame suitable for continuation.
|
|
277
|
+
|
|
278
|
+
When `continueFromVideo` is present, the server treats the generated `.mp4`
|
|
279
|
+
sidecar as authoritative. Client `continuityLineage` cannot override it. The
|
|
280
|
+
saved child sidecar includes `videoContinuity`, a branch-local max-4 stack using
|
|
281
|
+
`keep-start-plus-latest-3` retention.
|
|
282
|
+
|
|
283
|
+
`videoContinuity` shape:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"lineageId": "lineage:parent",
|
|
288
|
+
"parentFilename": "parent.mp4",
|
|
289
|
+
"sourceFrame": "last",
|
|
290
|
+
"maxEntries": 4,
|
|
291
|
+
"retention": "keep-start-plus-latest-3",
|
|
292
|
+
"entries": [
|
|
293
|
+
{
|
|
294
|
+
"id": "clip:parent.mp4",
|
|
295
|
+
"ordinal": 1,
|
|
296
|
+
"role": "start",
|
|
297
|
+
"filename": "parent.mp4",
|
|
298
|
+
"userPrompt": "original user prompt",
|
|
299
|
+
"revisedPrompt": "planner prompt actually sent to Grok video",
|
|
300
|
+
"createdAt": 1780300000000
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Entry `role` is `start`, `ancestor`, `parent`, or `current`. The first clip is
|
|
307
|
+
kept as the start anchor; later generations keep only the latest three entries.
|
|
308
|
+
`lineageId` uses the generated video basename without the `.mp4` extension.
|
|
309
|
+
This metadata is stored in the generated `.mp4.json` sidecar and returned in
|
|
310
|
+
history rows and video `done` events; `/generated/*.json` remains private.
|
|
311
|
+
|
|
312
|
+
Grok prompt surfaces used by video APIs:
|
|
313
|
+
|
|
314
|
+
| Surface | Model | Responsibility |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| Video planner | `grok-4.3` | Converts user prompt, search context, refs, and optional continuity lineage into the final English video prompt. It must structure core subject, action/motion, camera/composition, environment/style, dialogue/audio, ending-frame handoff, and constraints. |
|
|
317
|
+
| Video generation | xAI video model | Receives the planner prompt plus `sourceImage` or `referenceImages` when present. |
|
|
318
|
+
| Video analysis | `grok-4.3` | Reads first/last frame images from `/api/video/analyze` and returns recreation/continuation guidance. |
|
|
266
319
|
|
|
267
320
|
**SSE events**:
|
|
268
321
|
|
|
269
322
|
| Event | Data | Description |
|
|
270
323
|
|---|---|---|
|
|
271
324
|
| `planning` | `{ requestId }` | Preparing video generation |
|
|
272
|
-
| `submitted` | `{ requestId, xaiVideoRequestId }` | Submitted to xAI |
|
|
325
|
+
| `submitted` | `{ requestId, xaiVideoRequestId, requestedModel, effectiveModel, modelFallback }` | Submitted to xAI |
|
|
273
326
|
| `progress` | `{ requestId, progress, stalled }` | Progress 0.0–1.0 |
|
|
274
|
-
| `done` | `{ requestId, filename, url, mediaType, revisedPrompt, elapsed, usage, video }` | Video ready |
|
|
275
|
-
| `error` | `{ error, code, status, requestId }` | Generation failed |
|
|
327
|
+
| `done` | `{ requestId, filename, url, mediaType, revisedPrompt, elapsed, usage, requestedModel, effectiveModel, modelFallback, video, videoContinuity }` | Video ready |
|
|
328
|
+
| `error` | `{ error, code, status, requestId, guidance? }` | Generation failed |
|
|
276
329
|
|
|
277
330
|
**Video error codes**:
|
|
278
331
|
|
|
@@ -286,6 +339,57 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
|
|
|
286
339
|
| `INVALID_VIDEO_DURATION` | Duration not 1–15 integer |
|
|
287
340
|
| `GROK_VIDEO_REF_TOO_MANY` | More than 7 reference images |
|
|
288
341
|
| `GROK_VIDEO_FAILED` | Upstream xAI video generation failed |
|
|
342
|
+
| `GROK_VIDEO_FRAME_FAILED` | Server could not extract the parent video's last frame |
|
|
343
|
+
|
|
344
|
+
### `POST /api/video/edit`
|
|
345
|
+
|
|
346
|
+
Edit an existing video via Grok V2V. This is a blocking JSON endpoint that starts the xAI edit job, polls it, downloads the final MP4, and saves it as a generated video artifact.
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
{
|
|
350
|
+
"prompt": "make it sunset",
|
|
351
|
+
"videoUrl": "https://vidgen.x.ai/.../clip.mp4",
|
|
352
|
+
"model": "grok-imagine-video"
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`videoUrl` may be an HTTPS video URL, xAI `file_id`, `data:video/*` URL, or generated `.mp4` filename. Generated-file inputs are restricted to real `.mp4` files under the generated directory.
|
|
357
|
+
|
|
358
|
+
### `POST /api/video/extend`
|
|
359
|
+
|
|
360
|
+
Extend a video from its last frame. This is a blocking JSON endpoint that starts the xAI extension job, polls it, downloads the combined output MP4, and saves it as a generated video artifact.
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"prompt": "camera pulls back",
|
|
365
|
+
"videoUrl": "1780226256355_50252101.mp4",
|
|
366
|
+
"duration": 6,
|
|
367
|
+
"model": "grok-imagine-video"
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`duration` must be an integer from 2 to 10 seconds. Edit and extension support `grok-imagine-video` only; `grok-imagine-video-1.5-preview` is not accepted for these endpoints.
|
|
372
|
+
|
|
373
|
+
### `GET /api/video/frame`
|
|
374
|
+
|
|
375
|
+
Extract a PNG frame from a generated `.mp4` file.
|
|
376
|
+
|
|
377
|
+
| Query | Notes |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `file` | Required generated `.mp4` filename or generated-dir absolute path |
|
|
380
|
+
| `position` | `last` (default) or non-negative seconds |
|
|
381
|
+
|
|
382
|
+
### `POST /api/video/analyze`
|
|
383
|
+
|
|
384
|
+
Analyze first and last frames from a generated `.mp4` using Grok 4.3 image understanding. This does not upload the video as temporal video; it extracts two PNG frames and asks the vision model to infer likely motion.
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"videoUrl": "1780226256355_50252101.mp4"
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Remote URLs and `data:` inputs are intentionally rejected to avoid server-side URL fetching through `ffmpeg`.
|
|
289
393
|
|
|
290
394
|
## History
|
|
291
395
|
|
|
@@ -368,6 +472,11 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
|
|
|
368
472
|
| `POST /api/edit` | `ima2 edit` |
|
|
369
473
|
| `POST /api/generate/multimode` (SSE) | `ima2 multimode` |
|
|
370
474
|
| `POST /api/video/generate` (SSE) | `ima2 video` |
|
|
475
|
+
| `POST /api/video/generate` with `continueFromVideo` | `ima2 video continue` |
|
|
476
|
+
| `POST /api/video/edit` | `ima2 video edit` |
|
|
477
|
+
| `POST /api/video/extend` | `ima2 video extend` |
|
|
478
|
+
| `GET /api/video/frame` | `ima2 video frame` |
|
|
479
|
+
| `POST /api/video/analyze` | `ima2 video analyze` |
|
|
371
480
|
| `POST /api/node/generate` (SSE) / `GET /api/node/:id` | `ima2 node generate` / `ima2 node show` |
|
|
372
481
|
| `GET /api/history` | `ima2 ls` |
|
|
373
482
|
| `DELETE /api/history/:name` / `…/permanent` | `ima2 history rm [--permanent]` |
|