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.
Files changed (93) hide show
  1. package/README.md +15 -25
  2. package/bin/commands/capabilities.js +2 -2
  3. package/bin/commands/capabilities.ts +2 -2
  4. package/bin/commands/defaults.js +2 -2
  5. package/bin/commands/defaults.ts +2 -2
  6. package/bin/commands/doctor.js +3 -3
  7. package/bin/commands/doctor.ts +3 -3
  8. package/bin/commands/edit.js +1 -1
  9. package/bin/commands/edit.ts +1 -1
  10. package/bin/commands/gen.js +1 -1
  11. package/bin/commands/gen.ts +1 -1
  12. package/bin/commands/grok.js +16 -11
  13. package/bin/commands/grok.ts +16 -11
  14. package/bin/commands/multimode.js +1 -1
  15. package/bin/commands/multimode.ts +1 -1
  16. package/bin/commands/observability.js +2 -2
  17. package/bin/commands/observability.ts +2 -2
  18. package/bin/commands/video.js +335 -13
  19. package/bin/commands/video.ts +249 -12
  20. package/bin/ima2.js +9 -9
  21. package/bin/ima2.ts +9 -9
  22. package/bin/lib/error-hints.js +2 -2
  23. package/bin/lib/error-hints.ts +2 -2
  24. package/docs/API.md +112 -3
  25. package/docs/CLI.md +61 -7
  26. package/docs/FAQ.ko.md +15 -20
  27. package/docs/FAQ.md +14 -19
  28. package/docs/NPX_QUICKSTART.md +40 -0
  29. package/docs/PROMPT_STUDIO.ko.md +1 -1
  30. package/docs/PROMPT_STUDIO.md +1 -1
  31. package/docs/README.ja.md +6 -16
  32. package/docs/README.ko.md +10 -20
  33. package/docs/README.zh-CN.md +7 -17
  34. package/docs/migration/runtime-test-inventory.md +8 -1
  35. package/lib/agentRuntime.js +19 -5
  36. package/lib/agentRuntime.ts +17 -5
  37. package/lib/capabilities.js +1 -1
  38. package/lib/capabilities.ts +1 -1
  39. package/lib/generationErrors.js +1 -1
  40. package/lib/generationErrors.ts +1 -1
  41. package/lib/grokProxyLauncher.js +26 -3
  42. package/lib/grokProxyLauncher.ts +27 -3
  43. package/lib/grokVideoAdapter.js +18 -89
  44. package/lib/grokVideoAdapter.ts +27 -88
  45. package/lib/grokVideoCanvas.js +25 -0
  46. package/lib/grokVideoCanvas.ts +26 -0
  47. package/lib/grokVideoDownload.js +58 -0
  48. package/lib/grokVideoDownload.ts +59 -0
  49. package/lib/grokVideoPlannerPrompt.js +64 -0
  50. package/lib/grokVideoPlannerPrompt.ts +67 -0
  51. package/lib/historyList.js +7 -1
  52. package/lib/historyList.ts +5 -1
  53. package/lib/oauthLauncher.js +21 -6
  54. package/lib/oauthLauncher.ts +22 -6
  55. package/lib/videoContinuity.js +149 -0
  56. package/lib/videoContinuity.ts +180 -0
  57. package/lib/videoFrameExtract.js +80 -0
  58. package/lib/videoFrameExtract.ts +78 -0
  59. package/node_modules/progrok/dist/index.js +187 -88
  60. package/node_modules/progrok/dist/index.js.map +1 -1
  61. package/node_modules/progrok/package.json +1 -1
  62. package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
  63. package/package.json +2 -2
  64. package/routes/index.js +4 -0
  65. package/routes/index.ts +4 -0
  66. package/routes/quota.js +66 -0
  67. package/routes/quota.ts +89 -0
  68. package/routes/video.js +77 -15
  69. package/routes/video.ts +82 -14
  70. package/routes/videoExtended.js +293 -0
  71. package/routes/videoExtended.ts +284 -0
  72. package/server.js +6 -2
  73. package/server.ts +5 -2
  74. package/skills/ima2/SKILL.md +320 -7
  75. package/ui/dist/.vite/manifest.json +12 -12
  76. package/ui/dist/assets/{AgentWorkspace-DS8uvoLI.js → AgentWorkspace-B_hq9CLg.js} +2 -2
  77. package/ui/dist/assets/{CardNewsWorkspace-CYxMsE67.js → CardNewsWorkspace-wD12J7qk.js} +1 -1
  78. package/ui/dist/assets/{NodeCanvas-DccIc347.js → NodeCanvas-CI_wuPMf.js} +1 -1
  79. package/ui/dist/assets/{PromptBuilderPanel-BvxxwSJp.js → PromptBuilderPanel-CUTujJUV.js} +1 -1
  80. package/ui/dist/assets/{PromptImportDialog-u1_BFDRd.js → PromptImportDialog-CUi66jPK.js} +2 -2
  81. package/ui/dist/assets/{PromptImportDiscoverySection-C5uvkVSz.js → PromptImportDiscoverySection-Cm3vrjY4.js} +1 -1
  82. package/ui/dist/assets/{PromptImportFolderSection-D3E_O1SD.js → PromptImportFolderSection-DOtWTD9n.js} +1 -1
  83. package/ui/dist/assets/{PromptLibraryPanel-4gyf9CB9.js → PromptLibraryPanel-BMjQegRa.js} +2 -2
  84. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +1 -0
  85. package/ui/dist/assets/{index-DoKtXbod.js → index-31uVIdt4.js} +1 -1
  86. package/ui/dist/assets/index-CjgnNtgt.css +1 -0
  87. package/ui/dist/assets/index-Da2s4_-5.js +36 -0
  88. package/ui/dist/index.html +2 -2
  89. package/vendor/progrok-0.2.0.tgz +0 -0
  90. package/ui/dist/assets/SettingsWorkspace-F3eNu3mJ.js +0 -1
  91. package/ui/dist/assets/index-B6tcw_UF.css +0 -1
  92. package/ui/dist/assets/index-DYOh6gQD.js +0 -32
  93. package/vendor/progrok-0.1.1.tgz +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progrok",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Activate your xAI OAuth session as a local Grok API proxy and CLI tool surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- # Start: text-to-video, image-to-video (image), or reference-to-video (reference_images)
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
- # Poll until done
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
- Also: `POST /v1/videos/edits` (edit a clip), `POST /v1/videos/extensions` (extend 1-10s).
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.20",
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.1.1.tgz",
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
  }
@@ -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
+ }
@@ -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
- if (typeof prompt !== "string" || !prompt.trim())
69
- return fail(400, "PROMPT_REQUIRED", "Prompt is required");
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", { requestId, xaiVideoRequestId: ev.xaiVideoRequestId });
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${prompt}`
132
- : prompt;
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: prompt,
206
+ prompt: activePrompt,
207
+ userPrompt: activePrompt,
156
208
  revisedPrompt: result.revisedPrompt,
157
209
  provider: "grok",
158
- model: modelCheck.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 writeFile(join(ctx.config.storage.generatedDir, filename), result.videoBuffer);
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
  }