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
|
@@ -95,19 +95,48 @@ curl http://127.0.0.1:18645/v1/images/edits \
|
|
|
95
95
|
### Pattern 4: Video generation (async — poll)
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
#
|
|
98
|
+
# Text-to-video
|
|
99
|
+
progrok video "Ocean waves crashing on rocks" --duration 8 --resolution 720p
|
|
100
|
+
|
|
101
|
+
# Image-to-video
|
|
102
|
+
progrok video "Animate this scene" --image photo.jpg --duration 10
|
|
103
|
+
|
|
104
|
+
# Video editing (real V2V — modify existing video, keep motion)
|
|
105
|
+
progrok video edit "Make the water glow neon blue" --video https://vidgen.x.ai/.../clip.mp4
|
|
106
|
+
|
|
107
|
+
# Video extension (continue from last frame)
|
|
108
|
+
progrok video extend "Camera slowly pulls back" --video https://vidgen.x.ai/.../clip.mp4 --duration 5
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**API endpoints** (via proxy at 127.0.0.1:18645):
|
|
112
|
+
```bash
|
|
113
|
+
# Generate: POST /v1/videos/generations
|
|
99
114
|
curl -s http://127.0.0.1:18645/v1/videos/generations \
|
|
100
115
|
-H "Content-Type: application/json" \
|
|
101
116
|
-d '{"model": "grok-imagine-video", "prompt": "Ocean waves", "duration": 8, "resolution": "720p"}'
|
|
102
117
|
# → {"request_id": "abc-123"}
|
|
103
118
|
|
|
104
|
-
#
|
|
119
|
+
# Edit (V2V): POST /v1/videos/edits — grok-imagine-video only
|
|
120
|
+
curl -s http://127.0.0.1:18645/v1/videos/edits \
|
|
121
|
+
-H "Content-Type: application/json" \
|
|
122
|
+
-d '{"model": "grok-imagine-video", "prompt": "Add sunset colors", "video": {"url": "https://..."}}'
|
|
123
|
+
|
|
124
|
+
# Extend: POST /v1/videos/extensions — grok-imagine-video only, 2-10s
|
|
125
|
+
curl -s http://127.0.0.1:18645/v1/videos/extensions \
|
|
126
|
+
-H "Content-Type: application/json" \
|
|
127
|
+
-d '{"model": "grok-imagine-video", "prompt": "Continue scene", "duration": 5, "video": {"url": "https://..."}}'
|
|
128
|
+
|
|
129
|
+
# Poll: GET /v1/videos/{request_id}
|
|
105
130
|
curl http://127.0.0.1:18645/v1/videos/abc-123
|
|
106
|
-
# → {"status": "pending", "progress": 45}
|
|
107
131
|
# → {"status": "done", "video": {"url": "https://...", "duration": 8}}
|
|
108
132
|
```
|
|
109
133
|
|
|
110
|
-
|
|
134
|
+
**Model constraints:**
|
|
135
|
+
- `grok-imagine-video`: T2V, I2V, Ref2V, Edit, Extend — all modes
|
|
136
|
+
- `grok-imagine-video-1.5-preview`: I2V, Ref2V only (no T2V, no Edit, no Extend)
|
|
137
|
+
- Edit/Extend input: mp4, H.264/H.265/AV1, max 8.7s (edit) / 2-15s (extend)
|
|
138
|
+
- Edit output inherits input duration/aspect/resolution (max 720p)
|
|
139
|
+
- Extend duration: 2-10s added to original
|
|
111
140
|
|
|
112
141
|
### Pattern 5: Voice — TTS / STT (HTTP)
|
|
113
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "Local OAuth image generation studio with classic and node workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"express": "^5.1.0",
|
|
72
72
|
"openai": "^5.8.2",
|
|
73
73
|
"openai-oauth": "^1.0.2",
|
|
74
|
-
"progrok": "file:vendor/progrok-0.
|
|
74
|
+
"progrok": "file:vendor/progrok-0.2.0.tgz",
|
|
75
75
|
"sharp": "^0.34.5",
|
|
76
76
|
"trash": "^10.1.1",
|
|
77
77
|
"ulid": "^3.0.2"
|
package/routes/index.js
CHANGED
|
@@ -19,6 +19,8 @@ import { registerPromptBuilderRoutes } from "./promptBuilder.js";
|
|
|
19
19
|
import { registerAgentRoutes } from "./agent.js";
|
|
20
20
|
import { registerGrokRoutes } from "./grok.js";
|
|
21
21
|
import { registerVideoRoutes } from "./video.js";
|
|
22
|
+
import { registerVideoExtendedRoutes } from "./videoExtended.js";
|
|
23
|
+
import { registerQuotaRoutes } from "./quota.js";
|
|
22
24
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
23
25
|
export function configureRoutes(app, ctxRaw) {
|
|
24
26
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
@@ -44,4 +46,6 @@ export function configureRoutes(app, ctxRaw) {
|
|
|
44
46
|
registerPromptImportRoutes(app, ctx);
|
|
45
47
|
registerGrokRoutes(app, ctx);
|
|
46
48
|
registerVideoRoutes(app, ctx);
|
|
49
|
+
registerVideoExtendedRoutes(app, ctx);
|
|
50
|
+
registerQuotaRoutes(app, ctx);
|
|
47
51
|
}
|
package/routes/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { registerPromptBuilderRoutes } from "./promptBuilder.js";
|
|
|
20
20
|
import { registerAgentRoutes } from "./agent.js";
|
|
21
21
|
import { registerGrokRoutes } from "./grok.js";
|
|
22
22
|
import { registerVideoRoutes } from "./video.js";
|
|
23
|
+
import { registerVideoExtendedRoutes } from "./videoExtended.js";
|
|
24
|
+
import { registerQuotaRoutes } from "./quota.js";
|
|
23
25
|
import { type RouteRuntimeContext, requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
24
26
|
|
|
25
27
|
export function configureRoutes(app: Express, ctxRaw: RouteRuntimeContext) {
|
|
@@ -45,4 +47,6 @@ export function configureRoutes(app: Express, ctxRaw: RouteRuntimeContext) {
|
|
|
45
47
|
registerPromptImportRoutes(app, ctx);
|
|
46
48
|
registerGrokRoutes(app, ctx);
|
|
47
49
|
registerVideoRoutes(app, ctx);
|
|
50
|
+
registerVideoExtendedRoutes(app, ctx);
|
|
51
|
+
registerQuotaRoutes(app, ctx);
|
|
48
52
|
}
|
package/routes/quota.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function readCodexTokens() {
|
|
5
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
6
|
+
try {
|
|
7
|
+
const j = JSON.parse(readFileSync(join(codexHome, "auth.json"), "utf8"));
|
|
8
|
+
if (j?.tokens?.access_token) {
|
|
9
|
+
return { access_token: j.tokens.access_token, account_id: j.tokens.account_id ?? "" };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch { }
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
async function fetchCodexUsage(tokens) {
|
|
16
|
+
try {
|
|
17
|
+
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
20
|
+
"ChatGPT-Account-Id": tokens.account_id,
|
|
21
|
+
},
|
|
22
|
+
signal: AbortSignal.timeout(8000),
|
|
23
|
+
});
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
if (resp.status === 401 || resp.status === 403)
|
|
26
|
+
return { provider: "codex", authenticated: false, windows: [] };
|
|
27
|
+
return { provider: "codex", error: true, windows: [] };
|
|
28
|
+
}
|
|
29
|
+
const data = await resp.json();
|
|
30
|
+
const account = { email: data.email ?? null, plan: data.plan_type ?? null };
|
|
31
|
+
const windows = [];
|
|
32
|
+
if (data.rate_limit?.primary_window) {
|
|
33
|
+
windows.push({
|
|
34
|
+
label: "5h",
|
|
35
|
+
percent: Math.round(data.rate_limit.primary_window.used_percent ?? 0),
|
|
36
|
+
resetsAt: data.rate_limit.primary_window.reset_at
|
|
37
|
+
? new Date(data.rate_limit.primary_window.reset_at * 1000).toISOString()
|
|
38
|
+
: null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (data.rate_limit?.secondary_window) {
|
|
42
|
+
windows.push({
|
|
43
|
+
label: "7d",
|
|
44
|
+
percent: Math.round(data.rate_limit.secondary_window.used_percent ?? 0),
|
|
45
|
+
resetsAt: data.rate_limit.secondary_window.reset_at
|
|
46
|
+
? new Date(data.rate_limit.secondary_window.reset_at * 1000).toISOString()
|
|
47
|
+
: null,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { provider: "codex", account, windows };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { provider: "codex", error: true, windows: [] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function registerQuotaRoutes(app, _ctx) {
|
|
57
|
+
app.get("/api/quota", async (_req, res) => {
|
|
58
|
+
const tokens = readCodexTokens();
|
|
59
|
+
if (!tokens) {
|
|
60
|
+
res.json({ codex: { provider: "codex", authenticated: false, windows: [] } });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const codex = await fetchCodexUsage(tokens);
|
|
64
|
+
res.json({ codex });
|
|
65
|
+
});
|
|
66
|
+
}
|
package/routes/quota.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { RouteRuntimeContext } from "../lib/runtimeContext.js";
|
|
6
|
+
|
|
7
|
+
export interface QuotaWindow {
|
|
8
|
+
label: string;
|
|
9
|
+
percent: number;
|
|
10
|
+
resetsAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QuotaResult {
|
|
14
|
+
provider: string;
|
|
15
|
+
account?: { email: string | null; plan: string | null } | null;
|
|
16
|
+
windows: QuotaWindow[];
|
|
17
|
+
error?: boolean;
|
|
18
|
+
authenticated?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readCodexTokens(): { access_token: string; account_id: string } | null {
|
|
22
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
23
|
+
try {
|
|
24
|
+
const j = JSON.parse(readFileSync(join(codexHome, "auth.json"), "utf8"));
|
|
25
|
+
if (j?.tokens?.access_token) {
|
|
26
|
+
return { access_token: j.tokens.access_token, account_id: j.tokens.account_id ?? "" };
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fetchCodexUsage(tokens: { access_token: string; account_id: string }): Promise<QuotaResult> {
|
|
33
|
+
try {
|
|
34
|
+
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
37
|
+
"ChatGPT-Account-Id": tokens.account_id,
|
|
38
|
+
},
|
|
39
|
+
signal: AbortSignal.timeout(8000),
|
|
40
|
+
});
|
|
41
|
+
if (!resp.ok) {
|
|
42
|
+
if (resp.status === 401 || resp.status === 403) return { provider: "codex", authenticated: false, windows: [] };
|
|
43
|
+
return { provider: "codex", error: true, windows: [] };
|
|
44
|
+
}
|
|
45
|
+
const data = await resp.json() as {
|
|
46
|
+
email?: string | null;
|
|
47
|
+
plan_type?: string | null;
|
|
48
|
+
rate_limit?: {
|
|
49
|
+
primary_window?: { used_percent?: number; reset_at?: number };
|
|
50
|
+
secondary_window?: { used_percent?: number; reset_at?: number };
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const account = { email: data.email ?? null, plan: data.plan_type ?? null };
|
|
54
|
+
const windows: QuotaWindow[] = [];
|
|
55
|
+
if (data.rate_limit?.primary_window) {
|
|
56
|
+
windows.push({
|
|
57
|
+
label: "5h",
|
|
58
|
+
percent: Math.round(data.rate_limit.primary_window.used_percent ?? 0),
|
|
59
|
+
resetsAt: data.rate_limit.primary_window.reset_at
|
|
60
|
+
? new Date(data.rate_limit.primary_window.reset_at * 1000).toISOString()
|
|
61
|
+
: null,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (data.rate_limit?.secondary_window) {
|
|
65
|
+
windows.push({
|
|
66
|
+
label: "7d",
|
|
67
|
+
percent: Math.round(data.rate_limit.secondary_window.used_percent ?? 0),
|
|
68
|
+
resetsAt: data.rate_limit.secondary_window.reset_at
|
|
69
|
+
? new Date(data.rate_limit.secondary_window.reset_at * 1000).toISOString()
|
|
70
|
+
: null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { provider: "codex", account, windows };
|
|
74
|
+
} catch {
|
|
75
|
+
return { provider: "codex", error: true, windows: [] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function registerQuotaRoutes(app: Express, _ctx: RouteRuntimeContext) {
|
|
80
|
+
app.get("/api/quota", async (_req, res) => {
|
|
81
|
+
const tokens = readCodexTokens();
|
|
82
|
+
if (!tokens) {
|
|
83
|
+
res.json({ codex: { provider: "codex", authenticated: false, windows: [] } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const codex = await fetchCodexUsage(tokens);
|
|
87
|
+
res.json({ codex });
|
|
88
|
+
});
|
|
89
|
+
}
|
package/routes/video.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
import { startJob, finishJob, registerJobAbortController, isJobCanceled, setJobPhase } from "../lib/inflight.js";
|
|
@@ -7,6 +7,8 @@ import { logEvent, logError } from "../lib/logger.js";
|
|
|
7
7
|
import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
8
8
|
import { generateVideoViaGrok } from "../lib/grokVideoAdapter.js";
|
|
9
9
|
import { getVideoSeriesChain } from "../lib/videoSeriesChain.js";
|
|
10
|
+
import { ACTIVE_VIDEO_PROMPT_GUIDANCE, appendVideoContinuityEntry, lineageFromVideoMetadata, normalizeVideoContinuityLineage, readVideoSidecar, requireActiveVideoPrompt, safeGeneratedVideoFilename, } from "../lib/videoContinuity.js";
|
|
11
|
+
import { extractGeneratedVideoFrameB64 } from "../lib/videoFrameExtract.js";
|
|
10
12
|
import { normalizeGrokVideoModel, normalizeVideoResolution, normalizeVideoAspectRatio, normalizeVideoDuration, deriveVideoMode, clampVideoDuration, MAX_REF2V_REFERENCES, } from "../lib/imageModels.js";
|
|
11
13
|
import { errInfo } from "../lib/errInfo.js";
|
|
12
14
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
@@ -20,11 +22,24 @@ function toArray(v) {
|
|
|
20
22
|
function isNormalizeError(x) {
|
|
21
23
|
return typeof x === "object" && x !== null && typeof x.error === "string";
|
|
22
24
|
}
|
|
25
|
+
export async function saveGeneratedVideoArtifact(ctx, filename, buffer, metadata) {
|
|
26
|
+
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
27
|
+
await writeFile(filePath, buffer);
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(`${filePath}.json`, JSON.stringify(metadata));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
await unlink(filePath).catch(() => { });
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
async function resolveSourceImage(ctx, sourceImage, sourceFilename) {
|
|
24
37
|
if (typeof sourceFilename === "string" && sourceFilename) {
|
|
25
38
|
const safe = sourceFilename.replace(/^\/+/, "");
|
|
26
39
|
if (safe.includes(".."))
|
|
27
40
|
throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "invalid source filename" };
|
|
41
|
+
if (/\.mp4$/i.test(safe))
|
|
42
|
+
throw { status: 400, code: "GROK_VIDEO_INVALID_MODE", message: "use continueFromVideo for generated video continuation" };
|
|
28
43
|
const buf = await readFile(join(ctx.config.storage.generatedDir, safe));
|
|
29
44
|
return { b64: buf.toString("base64"), filename: safe };
|
|
30
45
|
}
|
|
@@ -51,12 +66,12 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
51
66
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
52
67
|
res.setHeader("Connection", "keep-alive");
|
|
53
68
|
res.flushHeaders?.();
|
|
54
|
-
const fail = (status, code, error) => {
|
|
69
|
+
const fail = (status, code, error, extra = {}) => {
|
|
55
70
|
const httpStatus = status ?? 500;
|
|
56
71
|
finishStatus = "error";
|
|
57
72
|
finishHttpStatus = httpStatus;
|
|
58
73
|
finishErrorCode = code;
|
|
59
|
-
sendSse(res, "error", { error, code, status: httpStatus, requestId });
|
|
74
|
+
sendSse(res, "error", { error, code, status: httpStatus, requestId, ...extra });
|
|
60
75
|
};
|
|
61
76
|
try {
|
|
62
77
|
const { prompt, provider = "grok", model: rawModel } = req.body || {};
|
|
@@ -65,8 +80,9 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
65
80
|
const topic = typeof req.body?.topic === "string" ? req.body.topic.trim() : "";
|
|
66
81
|
if (provider !== "grok")
|
|
67
82
|
return fail(400, "VIDEO_PROVIDER_UNSUPPORTED", "video generation requires provider 'grok'");
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
const activePrompt = requireActiveVideoPrompt(prompt);
|
|
84
|
+
if (!activePrompt)
|
|
85
|
+
return fail(400, "PROMPT_REQUIRED", "Prompt is required", { guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
|
|
70
86
|
const modelCheck = normalizeGrokVideoModel(rawModel);
|
|
71
87
|
if (isNormalizeError(modelCheck))
|
|
72
88
|
return fail(modelCheck.status, modelCheck.code, modelCheck.error);
|
|
@@ -80,6 +96,21 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
80
96
|
if (isNormalizeError(aspectCheck))
|
|
81
97
|
return fail(aspectCheck.status, aspectCheck.code, aspectCheck.error);
|
|
82
98
|
// Resolve reference inputs: base64 list + existing-file list + legacy single source.
|
|
99
|
+
let parentLineage = null;
|
|
100
|
+
let continueFromVideoFilename = null;
|
|
101
|
+
if (typeof req.body?.continueFromVideo === "string" && req.body.continueFromVideo.trim()) {
|
|
102
|
+
try {
|
|
103
|
+
continueFromVideoFilename = safeGeneratedVideoFilename(req.body.continueFromVideo);
|
|
104
|
+
const parentMeta = await readVideoSidecar(ctx.config.storage.generatedDir, continueFromVideoFilename);
|
|
105
|
+
parentLineage = lineageFromVideoMetadata(continueFromVideoFilename, parentMeta);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return fail(e?.status || 400, "GROK_VIDEO_INVALID_MODE", e?.message || "invalid continuation video");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
parentLineage = normalizeVideoContinuityLineage(req.body?.continuityLineage);
|
|
113
|
+
}
|
|
83
114
|
const refInputs = [
|
|
84
115
|
...toArray(req.body?.referenceImages).map((image) => ({ image })),
|
|
85
116
|
...toArray(req.body?.referenceFilenames).map((filename) => ({ filename })),
|
|
@@ -87,6 +118,14 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
87
118
|
? [{ image: req.body?.sourceImage, filename: req.body?.sourceFilename }]
|
|
88
119
|
: []),
|
|
89
120
|
];
|
|
121
|
+
if (continueFromVideoFilename && !req.body?.sourceImage && !req.body?.sourceFilename) {
|
|
122
|
+
try {
|
|
123
|
+
refInputs.push({ image: await extractGeneratedVideoFrameB64(ctx.config.storage.generatedDir, continueFromVideoFilename) });
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return fail(e?.status || 500, "GROK_VIDEO_FRAME_FAILED", e?.message || "failed to extract continuation frame");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
90
129
|
let resolved;
|
|
91
130
|
try {
|
|
92
131
|
const all = await Promise.all(refInputs.map((r) => resolveSourceImage(ctx, r.image, r.filename)));
|
|
@@ -105,7 +144,7 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
105
144
|
startJob({
|
|
106
145
|
requestId,
|
|
107
146
|
kind: "video",
|
|
108
|
-
prompt,
|
|
147
|
+
prompt: activePrompt,
|
|
109
148
|
meta: { kind: "video", sessionId, clientNodeId, model: modelCheck.model, mode, duration, resolution: resolutionCheck.resolution },
|
|
110
149
|
});
|
|
111
150
|
registerJobAbortController(requestId, cancelController);
|
|
@@ -115,7 +154,13 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
115
154
|
const onEvent = (ev) => {
|
|
116
155
|
if (ev.phase === "submitted") {
|
|
117
156
|
setJobPhase(requestId, "streaming");
|
|
118
|
-
sendSse(res, "submitted", {
|
|
157
|
+
sendSse(res, "submitted", {
|
|
158
|
+
requestId,
|
|
159
|
+
xaiVideoRequestId: ev.xaiVideoRequestId,
|
|
160
|
+
requestedModel: ev.requestedModel,
|
|
161
|
+
effectiveModel: ev.effectiveModel,
|
|
162
|
+
modelFallback: ev.modelFallback ?? null,
|
|
163
|
+
});
|
|
119
164
|
}
|
|
120
165
|
else if (ev.phase === "progress") {
|
|
121
166
|
sendSse(res, "progress", { requestId, progress: typeof ev.progress === "number" ? ev.progress / 100 : null, stalled: Boolean(ev.stalled) });
|
|
@@ -126,10 +171,10 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
126
171
|
}
|
|
127
172
|
};
|
|
128
173
|
// Build prompt with series chain context
|
|
129
|
-
const chain = topic ? await getVideoSeriesChain(ctx.config.storage.generatedDir, topic) : [];
|
|
174
|
+
const chain = !parentLineage && topic ? await getVideoSeriesChain(ctx.config.storage.generatedDir, topic) : [];
|
|
130
175
|
const effectivePrompt = chain.length > 0
|
|
131
|
-
? `[Series topic: ${topic}]\n[Previous prompts in series:\n${chain.map((p, i) => `${i + 1}. ${p}`).join("\n")}\n]\n\n${
|
|
132
|
-
:
|
|
176
|
+
? `[Series topic: ${topic}]\n[Previous prompts in series:\n${chain.map((p, i) => `${i + 1}. ${p}`).join("\n")}\n]\n\n${activePrompt}`
|
|
177
|
+
: activePrompt;
|
|
133
178
|
const result = await generateVideoViaGrok(effectivePrompt, ctx, {
|
|
134
179
|
model: modelCheck.model,
|
|
135
180
|
mode,
|
|
@@ -140,22 +185,32 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
140
185
|
referenceImages,
|
|
141
186
|
signal: cancelController.signal,
|
|
142
187
|
requestId,
|
|
188
|
+
continuityLineage: parentLineage,
|
|
143
189
|
onEvent,
|
|
144
190
|
});
|
|
145
191
|
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
146
192
|
const filename = `${Date.now()}_${rand}.mp4`;
|
|
147
193
|
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
194
|
+
const videoContinuity = appendVideoContinuityEntry(parentLineage, {
|
|
195
|
+
filename,
|
|
196
|
+
userPrompt: activePrompt,
|
|
197
|
+
revisedPrompt: result.revisedPrompt,
|
|
198
|
+
createdAt: Date.now(),
|
|
199
|
+
});
|
|
148
200
|
const meta = {
|
|
149
201
|
kind: "video",
|
|
150
202
|
mediaType: "video",
|
|
151
203
|
requestId,
|
|
152
204
|
sessionId,
|
|
153
205
|
clientNodeId,
|
|
154
|
-
prompt,
|
|
155
|
-
userPrompt:
|
|
206
|
+
prompt: activePrompt,
|
|
207
|
+
userPrompt: activePrompt,
|
|
156
208
|
revisedPrompt: result.revisedPrompt,
|
|
157
209
|
provider: "grok",
|
|
158
|
-
model:
|
|
210
|
+
model: result.effectiveModel,
|
|
211
|
+
requestedModel: result.requestedModel,
|
|
212
|
+
effectiveModel: result.effectiveModel,
|
|
213
|
+
modelFallback: result.modelFallback,
|
|
159
214
|
createdAt: Date.now(),
|
|
160
215
|
elapsed,
|
|
161
216
|
usage: result.usage,
|
|
@@ -166,11 +221,14 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
166
221
|
aspectRatio: result.aspectRatio,
|
|
167
222
|
sourceImageFilename: sourceFilename,
|
|
168
223
|
xaiVideoRequestId: result.xaiVideoRequestId,
|
|
224
|
+
requestedModel: result.requestedModel,
|
|
225
|
+
effectiveModel: result.effectiveModel,
|
|
226
|
+
modelFallback: result.modelFallback,
|
|
169
227
|
},
|
|
228
|
+
videoContinuity,
|
|
170
229
|
...(topic ? { videoSeries: { topic, chainIndex: chain.length } } : {}),
|
|
171
230
|
};
|
|
172
|
-
await
|
|
173
|
-
await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
|
|
231
|
+
await saveGeneratedVideoArtifact(ctx, filename, result.videoBuffer, meta);
|
|
174
232
|
invalidateHistoryIndex();
|
|
175
233
|
finishMeta = { filename, xaiVideoRequestId: result.xaiVideoRequestId };
|
|
176
234
|
logEvent("video", "saved", { requestId, filename, bytes: result.videoBuffer.length, elapsedMs: Date.now() - startTime });
|
|
@@ -182,7 +240,11 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
182
240
|
revisedPrompt: result.revisedPrompt,
|
|
183
241
|
elapsed,
|
|
184
242
|
usage: result.usage,
|
|
243
|
+
requestedModel: result.requestedModel,
|
|
244
|
+
effectiveModel: result.effectiveModel,
|
|
245
|
+
modelFallback: result.modelFallback,
|
|
185
246
|
video: meta.video,
|
|
247
|
+
videoContinuity,
|
|
186
248
|
...(meta.videoSeries ? { videoSeries: meta.videoSeries } : {}),
|
|
187
249
|
});
|
|
188
250
|
}
|