ima2-gen 3.5.2 → 3.5.3
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 +3 -1
- package/bin/commands/video.js +10 -9
- package/bin/lib/videoMcp.js +8 -4
- package/config.js +25 -8
- package/docs/CLI.md +2 -2
- package/docs/migration/runtime-test-inventory.md +3 -1
- package/lib/errors/providerMap.js +1 -0
- package/lib/grokImageAdapter.js +4 -1
- package/lib/grokImageCore.js +4 -1
- package/lib/grokVideoAdapter.js +153 -37
- package/lib/grokVideoDownload.js +1 -1
- package/lib/grokVideoPlannerPrompt.js +46 -0
- package/lib/grokVideoShared.js +14 -3
- package/lib/videoClientTimeouts.js +17 -0
- package/package.json +2 -2
- package/routes/video.js +2 -0
- package/ui/dist/.vite/manifest.json +31 -31
- package/ui/dist/assets/{AgentWorkspace-IQkTyjJH.js → AgentWorkspace-BZuw_82p.js} +1 -1
- package/ui/dist/assets/{AssetGenWorkspace-C8m2ZPqQ.js → AssetGenWorkspace-D-p57I-0.js} +2 -2
- package/ui/dist/assets/{AssetsWorkspace-HFowWrn5.js → AssetsWorkspace-BlVr5Qjq.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-BakVkPHn.js → CardNewsWorkspace-BPYWHZqh.js} +1 -1
- package/ui/dist/assets/{GenerationRequestLogPanel-CYY42FV8.js → GenerationRequestLogPanel-CJYn7vCN.js} +1 -1
- package/ui/dist/assets/{HomeWorkspace-B_CfJ8NM.js → HomeWorkspace-DbWuQI7B.js} +1 -1
- package/ui/dist/assets/{KeyingPanel-D1HFpVPP.js → KeyingPanel-hK4oc51k.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-CmhGjMDf.js → NodeCanvas-CO9FzAfn.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-D9UwkVEV.js → PromptBuilderPanel-DEPrkXcU.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-CFT_1oGB.js → PromptImportDialog-CGxyjU6L.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-DzVY9HM4.js → PromptImportDiscoverySection-C5PY1YV3.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-8AcoiI0c.js → PromptImportFolderSection-DWsWg6nw.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-DDraaZxD.js → PromptLibraryPanel-BPiKLU-z.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-N7JHafTr.js → SettingsWorkspace-D1gLUIPQ.js} +1 -1
- package/ui/dist/assets/{SpriteRecipeWorkspace-d0VeLHp7.js → SpriteRecipeWorkspace-vlnp398m.js} +1 -1
- package/ui/dist/assets/{index-DJimPauj.js → index-DogXlrkz.js} +3 -3
- package/ui/dist/assets/{index-BOSjAXB6.js → index-znBY56O9.js} +3 -3
- package/ui/dist/assets/{pptxgen.es-B4z3xJiJ.js → pptxgen.es-C5xOqzMQ.js} +1 -1
- package/ui/dist/assets/{useAgentDialogFocus-BqrYA0we.js → useAgentDialogFocus-C1ZNYO-9.js} +1 -1
- package/ui/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -314,7 +314,9 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
314
314
|
| `IMA2_GROK_PROXY_PORT` | `18645` | Port for the bundled progrok proxy |
|
|
315
315
|
| `IMA2_NO_GROK_PROXY` | — | Set `1` to disable automatic progrok startup |
|
|
316
316
|
| `IMA2_GROK_PLANNER_MODEL` | `grok-4.5` | Grok search/planner model (also configurable via settings UI or `--planner-model` CLI flag) |
|
|
317
|
-
| `IMA2_GROK_PLANNER_TIMEOUT_MS` | `
|
|
317
|
+
| `IMA2_GROK_PLANNER_TIMEOUT_MS` | `900000` | Timeout for the Grok planner call |
|
|
318
|
+
| `IMA2_GROK_SEARCH_TIMEOUT_MS` | `300000` | Timeout for the Grok web-search brief (degrades instead of failing) |
|
|
319
|
+
| `IMA2_GROK_VIDEO_PLAN_TOTAL_TIMEOUT_MS` | `1500000` | Ceiling on the whole video planning phase (clamped above search + planner) |
|
|
318
320
|
| `IMA2_GROK_IMAGE_MODEL_DEFAULT` | `grok-imagine-image-quality` | Default final Grok image model |
|
|
319
321
|
| `IMA2_GROK_VIDEO_MODEL_DEFAULT` | `grok-imagine-video-1.5` | Default Grok video model |
|
|
320
322
|
| `IMA2_GROK_GENERATION_TIMEOUT_MS` | `120000` | Timeout for the final Grok Images API call |
|
package/bin/commands/video.js
CHANGED
|
@@ -6,6 +6,7 @@ import { writeFile, mkdir } from "node:fs/promises";
|
|
|
6
6
|
import { basename, dirname } from "node:path";
|
|
7
7
|
import { runVideoGenerate } from "../lib/videoMcp.js";
|
|
8
8
|
import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, GROK_VIDEO_MODEL_BASE, validateVideoResolutionForRequest, } from "../../lib/imageModels.js";
|
|
9
|
+
import { VIDEO_CLIENT_TIMEOUT_SEC } from "../../lib/videoClientTimeouts.js";
|
|
9
10
|
const VALID_RESOLUTIONS = new Set(["480p", "720p", "1080p"]);
|
|
10
11
|
const VALID_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "auto"]);
|
|
11
12
|
const VALID_MODELS = new Set([GROK_VIDEO_MODEL_BASE, GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS]);
|
|
@@ -40,7 +41,7 @@ async function readJsonResponse(res, label) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
function parseTimeoutSeconds(seconds) {
|
|
43
|
-
const sec = parseIntegerFlag(seconds,
|
|
44
|
+
const sec = parseIntegerFlag(seconds, VIDEO_CLIENT_TIMEOUT_SEC, "--timeout");
|
|
44
45
|
if (sec < 1)
|
|
45
46
|
die(2, "--timeout must be at least 1");
|
|
46
47
|
return sec;
|
|
@@ -82,7 +83,7 @@ const SPEC = {
|
|
|
82
83
|
out: { short: "o", type: "string" },
|
|
83
84
|
"out-dir": { short: "d", type: "string" },
|
|
84
85
|
json: { type: "boolean" },
|
|
85
|
-
timeout: { type: "string", default:
|
|
86
|
+
timeout: { type: "string", default: String(VIDEO_CLIENT_TIMEOUT_SEC) },
|
|
86
87
|
server: { type: "string" },
|
|
87
88
|
session: { type: "string" },
|
|
88
89
|
help: { short: "h", type: "boolean" },
|
|
@@ -126,7 +127,7 @@ const HELP = `
|
|
|
126
127
|
-o, --out <file> Output file path
|
|
127
128
|
-d, --out-dir <dir> Output directory
|
|
128
129
|
--json Print JSON result to stdout
|
|
129
|
-
--timeout <sec> Default:
|
|
130
|
+
--timeout <sec> Default: 5400
|
|
130
131
|
--server <url> Override server URL
|
|
131
132
|
--session <id> Session ID
|
|
132
133
|
|
|
@@ -210,11 +211,11 @@ async function runVideoGenerateRequest(serverBase, body, timeout, silent) {
|
|
|
210
211
|
}
|
|
211
212
|
// --- Subcommands ---
|
|
212
213
|
async function videoEditCmd(argv) {
|
|
213
|
-
const spec = { flags: { video: { type: "string" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default:
|
|
214
|
+
const spec = { flags: { video: { type: "string" }, out: { short: "o", type: "string" }, output: { type: "string" }, json: { type: "boolean" }, timeout: { type: "string", default: String(VIDEO_CLIENT_TIMEOUT_SEC) }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
214
215
|
const args = parseArgs(argv, spec);
|
|
215
216
|
rejectUnknownFlags(args);
|
|
216
217
|
if (args.help) {
|
|
217
|
-
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:
|
|
218
|
+
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: 5400\n --server <url> Override server URL`);
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
221
|
const prompt = args.positional.join(" ");
|
|
@@ -246,11 +247,11 @@ async function videoEditCmd(argv) {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
async function videoExtendCmd(argv) {
|
|
249
|
-
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:
|
|
250
|
+
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: String(VIDEO_CLIENT_TIMEOUT_SEC) }, server: { type: "string" }, help: { short: "h", type: "boolean" } } };
|
|
250
251
|
const args = parseArgs(argv, spec);
|
|
251
252
|
rejectUnknownFlags(args);
|
|
252
253
|
if (args.help) {
|
|
253
|
-
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:
|
|
254
|
+
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: 5400\n --server <url> Override server URL`);
|
|
254
255
|
return;
|
|
255
256
|
}
|
|
256
257
|
const prompt = args.positional.join(" ");
|
|
@@ -297,7 +298,7 @@ async function videoContinueCmd(argv) {
|
|
|
297
298
|
out: { short: "o", type: "string" },
|
|
298
299
|
output: { type: "string" },
|
|
299
300
|
json: { type: "boolean" },
|
|
300
|
-
timeout: { type: "string", default:
|
|
301
|
+
timeout: { type: "string", default: String(VIDEO_CLIENT_TIMEOUT_SEC) },
|
|
301
302
|
server: { type: "string" },
|
|
302
303
|
help: { short: "h", type: "boolean" },
|
|
303
304
|
},
|
|
@@ -305,7 +306,7 @@ async function videoContinueCmd(argv) {
|
|
|
305
306
|
const args = parseArgs(argv, spec);
|
|
306
307
|
rejectUnknownFlags(args);
|
|
307
308
|
if (args.help) {
|
|
308
|
-
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|1080p> Default: 720p. 1080p requires --model grok-imagine-video-1.5\n --aspect-ratio <ratio|auto> Default: auto\n --model <name> grok-imagine-video, grok-imagine-video-1.5 (preview alias accepted)\n -o, --out <file> Download continued video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default:
|
|
309
|
+
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|1080p> Default: 720p. 1080p requires --model grok-imagine-video-1.5\n --aspect-ratio <ratio|auto> Default: auto\n --model <name> grok-imagine-video, grok-imagine-video-1.5 (preview alias accepted)\n -o, --out <file> Download continued video to file\n --output <file> Alias for --out\n --json Print JSON result\n --timeout <sec> Default: 5400\n --server <url> Override server URL`);
|
|
309
310
|
return;
|
|
310
311
|
}
|
|
311
312
|
const prompt = args.positional.join(" ");
|
package/bin/lib/videoMcp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { config } from "../../config.js";
|
|
4
4
|
import { deriveVideoMode, GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, validateVideoResolutionForRequest, } from "../../lib/imageModels.js";
|
|
5
|
+
import { VIDEO_CLIENT_TIMEOUT_MS } from "../../lib/videoClientTimeouts.js";
|
|
5
6
|
import { wasFlagPassed } from "./argsExplicit.js";
|
|
6
7
|
import { resolveHistoryReference, resolveServer, request } from "./client.js";
|
|
7
8
|
import { loadCliDefaults } from "./config-store.js";
|
|
@@ -13,7 +14,10 @@ import { createCliRequestId } from "./recover-output.js";
|
|
|
13
14
|
import { streamSse } from "./sse.js";
|
|
14
15
|
const VALID_RESOLUTIONS = new Set(["480p", "720p", "1080p"]);
|
|
15
16
|
const VALID_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "auto"]);
|
|
16
|
-
|
|
17
|
+
// Shared client ceiling (see lib/videoClientTimeouts.ts). Also guards the
|
|
18
|
+
// runway/higgsfield MCP lanes, which end on their own terminal events, so a longer
|
|
19
|
+
// ceiling is harmless there.
|
|
20
|
+
const MCP_VIDEO_TIMEOUT_MS = VIDEO_CLIENT_TIMEOUT_MS;
|
|
17
21
|
const MCP_LANES = new Set(["runway", "higgsfield"]);
|
|
18
22
|
function parseInteger(value, fallback, label) {
|
|
19
23
|
const raw = value === undefined ? String(fallback) : String(value);
|
|
@@ -134,7 +138,7 @@ function coreBody(args, context, options, references, requestId) {
|
|
|
134
138
|
async function consumeCoreSse(url, body, args, requestId) {
|
|
135
139
|
const controller = new AbortController();
|
|
136
140
|
let timedOut = false;
|
|
137
|
-
const timeoutMs = parseInteger(args.timeout,
|
|
141
|
+
const timeoutMs = parseInteger(args.timeout, MCP_VIDEO_TIMEOUT_MS / 1000, "--timeout") * 1000;
|
|
138
142
|
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
|
139
143
|
const onSignal = () => { controller.abort(); process.exit(130); };
|
|
140
144
|
process.once("SIGINT", onSignal);
|
|
@@ -192,7 +196,7 @@ async function runCoreVideo(args, context) {
|
|
|
192
196
|
die(1, "server did not return a video filename");
|
|
193
197
|
const filename = String(done.filename);
|
|
194
198
|
const target = args.out ? String(args.out) : args["out-dir"] ? join(String(args["out-dir"]), filename) : join(config.storage.generatedDir, filename);
|
|
195
|
-
const response = await fetch(`${context.server.base}${done.url || `/generated/${encodeURIComponent(filename)}`}`, { signal: AbortSignal.timeout(parseInteger(args.timeout,
|
|
199
|
+
const response = await fetch(`${context.server.base}${done.url || `/generated/${encodeURIComponent(filename)}`}`, { signal: AbortSignal.timeout(parseInteger(args.timeout, MCP_VIDEO_TIMEOUT_MS / 1000, "--timeout") * 1000) });
|
|
196
200
|
if (!response.ok)
|
|
197
201
|
die(1, `failed to download video: HTTP ${response.status}`);
|
|
198
202
|
await writeBuffer(target, Buffer.from(await response.arrayBuffer()));
|
|
@@ -373,7 +377,7 @@ async function runMcpVideo(argv, args, context) {
|
|
|
373
377
|
}
|
|
374
378
|
}
|
|
375
379
|
export async function runVideoGenerate(argv, args, prompt) {
|
|
376
|
-
if (parseInteger(args.timeout,
|
|
380
|
+
if (parseInteger(args.timeout, MCP_VIDEO_TIMEOUT_MS / 1000, "--timeout") < 1)
|
|
377
381
|
die(2, "--timeout must be at least 1");
|
|
378
382
|
const { server, catalog } = await fetchCatalog(args);
|
|
379
383
|
const target = resolveVideoTarget(args, catalog);
|
package/config.js
CHANGED
|
@@ -165,7 +165,12 @@ export const config = {
|
|
|
165
165
|
nodeHexBytes: pickInt(env.IMA2_NODE_HEX_BYTES, fileCfg.ids?.nodeHexBytes, 5),
|
|
166
166
|
},
|
|
167
167
|
inflight: {
|
|
168
|
-
|
|
168
|
+
// Must exceed the longest legal request. Grok video can legitimately run ~70 min
|
|
169
|
+
// (planning + start + poll + poll overshoot + download), and purgeStaleJobs() drops the
|
|
170
|
+
// job row without aborting its worker, so a 10-minute TTL used to erase live jobs
|
|
171
|
+
// mid-flight.
|
|
172
|
+
// devlog/_plan/260817_grok_video_planner_timeout/010_timeout_budgets.md
|
|
173
|
+
ttlMs: pickInt(env.IMA2_INFLIGHT_TTL_MS, fileCfg.inflight?.ttlMs, 90 * 60 * 1000),
|
|
169
174
|
reapMs: pickInt(env.IMA2_INFLIGHT_REAP_MS, fileCfg.inflight?.reapMs, 60 * 1000),
|
|
170
175
|
terminalTtlMs: pickInt(env.IMA2_INFLIGHT_TERMINAL_TTL_MS, fileCfg.inflight?.terminalTtlMs, 5 * 60 * 1000),
|
|
171
176
|
},
|
|
@@ -200,21 +205,33 @@ export const config = {
|
|
|
200
205
|
restartMaxDelayMs: pickInt(env.IMA2_GROK_RESTART_MAX_DELAY_MS, fileCfg.grokProvider?.restartMaxDelayMs, 60_000),
|
|
201
206
|
restartHealthyMs: pickInt(env.IMA2_GROK_RESTART_HEALTHY_MS, fileCfg.grokProvider?.restartHealthyMs, 60_000),
|
|
202
207
|
plannerModel: pickStr(env.IMA2_GROK_PLANNER_MODEL, fileCfg.grokProvider?.plannerModel, DEFAULT_GROK_PLANNER_MODEL),
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
208
|
+
// Measured 260817: the forced web_search brief takes 70-73 s idle / 44-79 s concurrent,
|
|
209
|
+
// and the planner tool call 9-32 s idle / 28-41 s concurrent. But the reported failure
|
|
210
|
+
// was a planner call that STALLED for its entire 300 s budget while an isolated probe
|
|
211
|
+
// of the same payload answered in 32 s. Budgets are therefore calibrated against the
|
|
212
|
+
// stall, not the probe: 900 s is 3x the observed stall.
|
|
213
|
+
// devlog/_plan/260817_grok_video_planner_timeout/010_timeout_budgets.md
|
|
214
|
+
plannerTimeoutMs: pickInt(env.IMA2_GROK_PLANNER_TIMEOUT_MS, fileCfg.grokProvider?.plannerTimeoutMs, 900_000),
|
|
215
|
+
// The web-search brief gets its own bound: it is an enhancement, not a requirement, so
|
|
216
|
+
// it degrades instead of failing the request (lib/grokVideoAdapter.ts).
|
|
217
|
+
searchTimeoutMs: pickInt(env.IMA2_GROK_SEARCH_TIMEOUT_MS, fileCfg.grokProvider?.searchTimeoutMs, 300_000),
|
|
218
|
+
// Wired ceiling on search + planner combined, so the two stages cannot sum unbounded.
|
|
219
|
+
// It must be STRICTLY greater than searchTimeoutMs + plannerTimeoutMs (invariant enforced
|
|
220
|
+
// by tests): if it equals the sum, a slow search followed by a stalled planner lands both
|
|
221
|
+
// timers together, the phase ceiling wins the race, and the local planner fallback never
|
|
222
|
+
// runs. The margin exists so the planner's own timeout always fires first.
|
|
223
|
+
videoPlanTotalTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_PLAN_TOTAL_TIMEOUT_MS, fileCfg.grokProvider?.videoPlanTotalTimeoutMs, 1_500_000),
|
|
207
224
|
defaultImageModel: pickStr(env.IMA2_GROK_IMAGE_MODEL_DEFAULT, fileCfg.grokProvider?.defaultImageModel, "grok-imagine-image-2.0"),
|
|
208
225
|
// grok-imagine-image-2.0 measured 43-97 s per image; 2k/batch requests run longer,
|
|
209
226
|
// so the image call gets the same generous 300 s ceiling as the planner.
|
|
210
227
|
generationTimeoutMs: pickInt(env.IMA2_GROK_GENERATION_TIMEOUT_MS, fileCfg.grokProvider?.generationTimeoutMs, 300_000),
|
|
211
228
|
statusTimeoutMs: pickInt(env.IMA2_GROK_STATUS_TIMEOUT_MS, fileCfg.grokProvider?.statusTimeoutMs, 3000),
|
|
212
229
|
defaultVideoModel: pickStr(env.IMA2_GROK_VIDEO_MODEL_DEFAULT, fileCfg.grokProvider?.defaultVideoModel, "grok-imagine-video-1.5"),
|
|
213
|
-
videoStartTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_START_TIMEOUT_MS, fileCfg.grokProvider?.videoStartTimeoutMs,
|
|
230
|
+
videoStartTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_START_TIMEOUT_MS, fileCfg.grokProvider?.videoStartTimeoutMs, 300_000),
|
|
214
231
|
videoPollIntervalMs: pickInt(env.IMA2_GROK_VIDEO_POLL_INTERVAL_MS, fileCfg.grokProvider?.videoPollIntervalMs, 5_000),
|
|
215
232
|
videoPollMaxConsecutiveErrors: pickInt(env.IMA2_GROK_VIDEO_POLL_MAX_ERRORS, fileCfg.grokProvider?.videoPollMaxConsecutiveErrors, 5),
|
|
216
|
-
videoTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_TIMEOUT_MS, fileCfg.grokProvider?.videoTimeoutMs,
|
|
217
|
-
videoDownloadTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_DOWNLOAD_TIMEOUT_MS, fileCfg.grokProvider?.videoDownloadTimeoutMs,
|
|
233
|
+
videoTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_TIMEOUT_MS, fileCfg.grokProvider?.videoTimeoutMs, 1_800_000),
|
|
234
|
+
videoDownloadTimeoutMs: pickInt(env.IMA2_GROK_VIDEO_DOWNLOAD_TIMEOUT_MS, fileCfg.grokProvider?.videoDownloadTimeoutMs, 300_000),
|
|
218
235
|
},
|
|
219
236
|
// Direct MiniMax image-generation provider (text-to-image / image-to-image).
|
|
220
237
|
// Region selects the global (.io) or China (.minimaxi.com) OpenAI-compatible
|
package/docs/CLI.md
CHANGED
|
@@ -162,7 +162,7 @@ Video generate flags:
|
|
|
162
162
|
| `--ref <file>` | Attach source/reference image (repeatable, max 7) |
|
|
163
163
|
| `-o, --out <file>` | Output file path |
|
|
164
164
|
| `-d, --out-dir <dir>` | Output directory |
|
|
165
|
-
| `--timeout <sec>` | Timeout in seconds (default:
|
|
165
|
+
| `--timeout <sec>` | Timeout in seconds (default: 5400) |
|
|
166
166
|
| `--session <id>` | Session ID |
|
|
167
167
|
|
|
168
168
|
Blank video prompts are rejected. Prompts should include visual flow, camera or
|
|
@@ -181,7 +181,7 @@ Video edit/extend flags:
|
|
|
181
181
|
| `--duration <2..10>` | Extension duration only (default: 6) |
|
|
182
182
|
| `-o, --out <file>` | Download the edited or extended video to a file |
|
|
183
183
|
| `--json` | Print JSON result |
|
|
184
|
-
| `--timeout <sec>` | Timeout in seconds (default:
|
|
184
|
+
| `--timeout <sec>` | Timeout in seconds (default: 5400) |
|
|
185
185
|
|
|
186
186
|
Video continue flags:
|
|
187
187
|
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 362 (runtime: 159, contract: 203)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-mode-auto-planner-contract.test.ts`
|
|
@@ -66,6 +66,8 @@ Total: 360 (runtime: 157, contract: 203)
|
|
|
66
66
|
- `tests/grok-size-mapper.test.ts`
|
|
67
67
|
- `tests/grok-upstream-retry.test.ts`
|
|
68
68
|
- `tests/grokVideoAdapter.test.ts`
|
|
69
|
+
- `tests/grokVideoPlannerFallback.test.ts`
|
|
70
|
+
- `tests/grokVideoTimeoutBudgets.test.ts`
|
|
69
71
|
- `tests/history-metadata-fallback.test.ts`
|
|
70
72
|
- `tests/history-tombstone.test.ts`
|
|
71
73
|
- `tests/history-video-row.test.ts`
|
|
@@ -48,6 +48,7 @@ export const PROVIDER_ERROR_MAP = {
|
|
|
48
48
|
GROK_VIDEO_INVALID_MODE: "CAPABILITY_UNSUPPORTED",
|
|
49
49
|
GROK_VIDEO_MODERATION_BLOCKED: "CONTENT_REJECTED",
|
|
50
50
|
GROK_VIDEO_POLL_FAILED: "NETWORK_FAILURE",
|
|
51
|
+
GROK_VIDEO_PLAN_TIMEOUT: "PROVIDER_TIMEOUT",
|
|
51
52
|
GROK_VIDEO_REF_TOO_MANY: "CAPABILITY_UNSUPPORTED",
|
|
52
53
|
GROK_VIDEO_REQUEST_FAILED: "CAPABILITY_UNSUPPORTED",
|
|
53
54
|
GROK_VIDEO_TIMEOUT: "PROVIDER_TIMEOUT",
|
package/lib/grokImageAdapter.js
CHANGED
|
@@ -182,7 +182,10 @@ export async function searchGrokVisualContext(prompt, ctx, options = {}) {
|
|
|
182
182
|
const plannerModel = options.plannerModel || planner.model;
|
|
183
183
|
const payload = buildGrokSearchPayload(prompt, plannerModel);
|
|
184
184
|
const { url, headers } = getGrokEndpoint(ctx, "/v1/responses", options.directApiKey);
|
|
185
|
-
|
|
185
|
+
// The search brief has its own, smaller budget: it runs BEFORE the planner call inside
|
|
186
|
+
// the same user request, so charging it the full planner budget lets one slow stage eat
|
|
187
|
+
// the other's time. See devlog/_plan/260817_grok_video_planner_timeout/010.
|
|
188
|
+
const { combinedSignal, timer } = withTimeoutSignal(options.signal, planner.searchTimeoutMs);
|
|
186
189
|
logEvent("grok", "search:start", { requestId: options.requestId, plannerModel, promptChars: prompt.length });
|
|
187
190
|
try {
|
|
188
191
|
// Safe to replay: search produces no billable artifact, only a research summary.
|
package/lib/grokImageCore.js
CHANGED
|
@@ -39,7 +39,10 @@ export function getPlannerConfig(ctx) {
|
|
|
39
39
|
const grokCfg = ctx.config.grokProvider || {};
|
|
40
40
|
return {
|
|
41
41
|
model: grokCfg.plannerModel || DEFAULT_GROK_PLANNER_MODEL,
|
|
42
|
-
timeoutMs: grokCfg.plannerTimeoutMs ||
|
|
42
|
+
timeoutMs: grokCfg.plannerTimeoutMs || 900_000,
|
|
43
|
+
// The forced web_search brief is bounded separately: it is an enhancement, so a slow
|
|
44
|
+
// search should cost a less-researched prompt, not the whole generation.
|
|
45
|
+
searchTimeoutMs: grokCfg.searchTimeoutMs || 300_000,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
export function withTimeoutSignal(signal, timeoutMs) {
|
package/lib/grokVideoAdapter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { logEvent } from "./logger.js";
|
|
1
|
+
import { logEvent, logWarn } from "./logger.js";
|
|
2
2
|
import { grokError, searchGrokVisualContext } from "./grokImageAdapter.js";
|
|
3
3
|
import { grokFetchWithRetry } from "./grokUpstreamRetry.js";
|
|
4
4
|
import { detectImageMimeFromB64 } from "./refs.js";
|
|
5
5
|
import { aspectToCanvas, generateWhiteCanvasB64 } from "./grokVideoCanvas.js";
|
|
6
6
|
import { downloadVideo } from "./grokVideoDownload.js";
|
|
7
|
-
import { buildGrokVideoPlannerSystemPrompt, formatDurationPacingGuidance } from "./grokVideoPlannerPrompt.js";
|
|
7
|
+
import { buildGrokVideoPlannerSystemPrompt, composeFallbackVideoPrompt, formatDurationPacingGuidance } from "./grokVideoPlannerPrompt.js";
|
|
8
8
|
import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, GROK_VIDEO_MODEL_BASE, MAX_REF2V_REFERENCES, validateVideoResolutionForRequest, } from "./imageModels.js";
|
|
9
9
|
import { formatVideoContinuityForPlanner } from "./videoContinuity.js";
|
|
10
10
|
import { DEFAULT_GROK_PLANNER_MODEL } from "../config.js";
|
|
@@ -13,6 +13,34 @@ import { pollVideoUntilDone } from "./grokVideoPoll.js";
|
|
|
13
13
|
export { downloadVideo } from "./grokVideoDownload.js";
|
|
14
14
|
export { normalizeVideoPoll } from "./grokVideoShared.js";
|
|
15
15
|
export { isRecoverablePollError, pollVideoOnce, pollVideoUntilDone } from "./grokVideoPoll.js";
|
|
16
|
+
/**
|
|
17
|
+
* A planner failure is degradable when retrying or waiting could plausibly have produced a
|
|
18
|
+
* prompt: a stall, an upstream 5xx, or a network fault.
|
|
19
|
+
*
|
|
20
|
+
* A 4xx is NOT degradable — the request itself is malformed or unauthorized, and generating
|
|
21
|
+
* a video anyway would spend the user's quota on a request the provider already rejected.
|
|
22
|
+
*
|
|
23
|
+
* Neither is a 200 that carries no usable tool call: the planner ANSWERED and declined or
|
|
24
|
+
* malfunctioned, which is a judgment about the request rather than an availability problem.
|
|
25
|
+
* Billing a video start on the back of a refusal is worse for the user than an honest error.
|
|
26
|
+
*/
|
|
27
|
+
export function isDegradablePlannerFailure(err) {
|
|
28
|
+
const message = typeof err?.message === "string" ? err.message : "planner unavailable";
|
|
29
|
+
if (err?.name === "AbortError")
|
|
30
|
+
return { reason: "timeout", message };
|
|
31
|
+
const code = typeof err?.code === "string" ? err.code : "";
|
|
32
|
+
if (code === "GROK_PLANNER_TIMEOUT")
|
|
33
|
+
return { reason: "timeout", message };
|
|
34
|
+
if (code === "GROK_PLANNER_NETWORK_FAILED")
|
|
35
|
+
return { reason: "failed", message };
|
|
36
|
+
const status = typeof err?.status === "number" ? err.status : 0;
|
|
37
|
+
if (code === "GROK_PLANNER_BAD_REQUEST" && status >= 500)
|
|
38
|
+
return { reason: "failed", message };
|
|
39
|
+
// 429 is the same availability class as a stall: the upstream is saturated, not offended.
|
|
40
|
+
if (code === "GROK_PLANNER_BAD_REQUEST" && status === 429)
|
|
41
|
+
return { reason: "failed", message };
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
16
44
|
function canonicalVideoModel(model) {
|
|
17
45
|
return model === GROK_VIDEO_MODEL_15_PREVIEW_ALIAS ? GROK_VIDEO_MODEL_15 : model;
|
|
18
46
|
}
|
|
@@ -117,46 +145,132 @@ export async function planGrokVideo(prompt, ctx, options = {}) {
|
|
|
117
145
|
const aspectRatio = options.aspectRatio || "auto";
|
|
118
146
|
const plannerModel = options.plannerModel || cfg.plannerModel;
|
|
119
147
|
const model = canonicalVideoModel(options.model || cfg.model);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
aspectRatio,
|
|
128
|
-
plannerModel,
|
|
129
|
-
searchSummary: search.summary,
|
|
130
|
-
sourceImageUrl: options.sourceImage ? sourceImageUrl(options.sourceImage, options.sourceMime) : undefined,
|
|
131
|
-
referenceImageUrls,
|
|
132
|
-
continuityLineage: options.continuityLineage,
|
|
133
|
-
backgroundConstraint: options.backgroundConstraint,
|
|
134
|
-
});
|
|
135
|
-
const { url, headers } = videoEndpoint(ctx, "/v1/chat/completions", options.directApiKey);
|
|
136
|
-
const { combinedSignal, timer } = withTimeoutSignal(options.signal, cfg.plannerTimeoutMs);
|
|
137
|
-
logEvent("grok", "video:planner:start", { requestId: options.requestId, mode, duration, resolution });
|
|
148
|
+
// One deadline for the whole planning phase. Both stages run under it, so search and
|
|
149
|
+
// planner can never sum to two independent budgets (the shape of the 360 s failure).
|
|
150
|
+
// devlog/_plan/260817_grok_video_planner_timeout/010_timeout_budgets.md
|
|
151
|
+
const phase = withTimeoutSignal(options.signal, cfg.planTotalTimeoutMs);
|
|
152
|
+
const phaseSignal = phase.combinedSignal;
|
|
153
|
+
/** A phase-ceiling abort is fatal; it must never be mistaken for a degradable failure. */
|
|
154
|
+
const phaseExpired = () => phaseSignal.aborted && !options.signal?.aborted;
|
|
138
155
|
try {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
// The web-search brief is an enhancement, not a requirement: a slow or failing search
|
|
157
|
+
// used to abort the whole video request (GROK_PLANNER_TIMEOUT / GROK_SEARCH_TIMEOUT).
|
|
158
|
+
// Degrade instead, and keep the planner's own budget intact.
|
|
159
|
+
// devlog/_plan/260817_grok_video_planner_timeout/020_search_resilience.md
|
|
160
|
+
let searchSummary;
|
|
161
|
+
let searchDegraded;
|
|
162
|
+
try {
|
|
163
|
+
// Pass the PHASE signal, never a locally wrapped search timer: searchGrokVisualContext
|
|
164
|
+
// reports any abort of its `options.signal` as a user cancellation, so handing it our
|
|
165
|
+
// own deadline would turn a slow search — the exact case 020 exists to degrade — into
|
|
166
|
+
// GENERATION_CANCELED. The stage bound is applied inside that function via
|
|
167
|
+
// getPlannerConfig().searchTimeoutMs.
|
|
168
|
+
const search = await searchGrokVisualContext(prompt, ctx, { signal: phaseSignal, ...(options.requestId ? { requestId: options.requestId } : {}), ...(options.directApiKey ? { directApiKey: options.directApiKey } : {}), plannerModel });
|
|
169
|
+
searchSummary = search.summary;
|
|
145
170
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
catch (e) { // justified: adapter rejections are untyped; the shape is narrowed here
|
|
172
|
+
// A user cancellation is a real cancellation and must never be swallowed. Neither is
|
|
173
|
+
// the planning-phase ceiling: searchGrokVisualContext reports an inherited abort as its
|
|
174
|
+
// own cancellation, so without this check a ceiling hit would "degrade" and then start
|
|
175
|
+
// the planner after the phase had already expired.
|
|
176
|
+
if (options.signal?.aborted)
|
|
177
|
+
throw e;
|
|
178
|
+
if (phaseExpired())
|
|
179
|
+
throw grokError("Grok video planning timed out", 504, "GROK_VIDEO_PLAN_TIMEOUT");
|
|
180
|
+
if (e?.code === "GENERATION_CANCELED")
|
|
181
|
+
throw e;
|
|
182
|
+
searchDegraded = {
|
|
183
|
+
reason: e?.code === "GROK_SEARCH_TIMEOUT" ? "timeout" : "failed",
|
|
184
|
+
message: typeof e?.message === "string" ? e.message : "web search unavailable",
|
|
185
|
+
};
|
|
186
|
+
logWarn("grok", "video:search:degraded", {
|
|
187
|
+
requestId: options.requestId,
|
|
188
|
+
reason: searchDegraded.reason,
|
|
189
|
+
error: searchDegraded.message,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const referenceImageUrls = (options.referenceImages ?? []).map((img) => sourceImageUrl(img, undefined));
|
|
193
|
+
const payload = buildGrokVideoPlannerPayload(prompt, {
|
|
194
|
+
model,
|
|
195
|
+
mode,
|
|
196
|
+
duration,
|
|
197
|
+
resolution,
|
|
198
|
+
aspectRatio,
|
|
199
|
+
plannerModel,
|
|
200
|
+
searchSummary,
|
|
201
|
+
sourceImageUrl: options.sourceImage ? sourceImageUrl(options.sourceImage, options.sourceMime) : undefined,
|
|
202
|
+
referenceImageUrls,
|
|
203
|
+
continuityLineage: options.continuityLineage,
|
|
204
|
+
backgroundConstraint: options.backgroundConstraint,
|
|
205
|
+
});
|
|
206
|
+
const { url, headers } = videoEndpoint(ctx, "/v1/chat/completions", options.directApiKey);
|
|
207
|
+
const { combinedSignal, timer } = withTimeoutSignal(phaseSignal, cfg.plannerTimeoutMs);
|
|
208
|
+
logEvent("grok", "video:planner:start", { requestId: options.requestId, mode, duration, resolution });
|
|
209
|
+
try {
|
|
210
|
+
// Safe to replay: the planner produces no billable artifact, only a prompt.
|
|
211
|
+
const res = await grokFetchWithRetry(() => fetch(url, { method: "POST", headers, body: JSON.stringify(payload), signal: combinedSignal }), { signal: combinedSignal, label: "video-planner" });
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
const text = await res.text().catch(() => "");
|
|
215
|
+
throw grokError(`Grok video planner failed: ${text || `HTTP ${res.status}`}`, res.status >= 500 ? 502 : res.status, "GROK_PLANNER_BAD_REQUEST");
|
|
216
|
+
}
|
|
217
|
+
const planPrompt = parseGrokVideoPlanPrompt(await res.json());
|
|
218
|
+
logEvent("grok", "video:planner:done", { requestId: options.requestId, mode, promptChars: planPrompt.length });
|
|
219
|
+
return {
|
|
220
|
+
prompt: planPrompt,
|
|
221
|
+
mode,
|
|
222
|
+
duration,
|
|
223
|
+
resolution,
|
|
224
|
+
aspectRatio,
|
|
225
|
+
// Honest accounting: no brief means no billable search call was completed.
|
|
226
|
+
webSearchCalls: searchDegraded ? 0 : 1,
|
|
227
|
+
...(searchDegraded ? { searchDegraded } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
// Real cancellation and the planning-phase ceiling are always fatal.
|
|
153
233
|
if (options.signal?.aborted)
|
|
154
234
|
throw grokError("Generation canceled", 499, "GENERATION_CANCELED");
|
|
155
|
-
|
|
235
|
+
if (phaseExpired())
|
|
236
|
+
throw grokError("Grok video planning timed out", 504, "GROK_VIDEO_PLAN_TIMEOUT");
|
|
237
|
+
const normalized = e.name === "AbortError"
|
|
238
|
+
? grokError("Grok video planner timed out", 504, "GROK_PLANNER_TIMEOUT")
|
|
239
|
+
: (e.code && e.status ? e : grokError(`Grok video planner request failed: ${e.message}`, 502, "GROK_PLANNER_NETWORK_FAILED"));
|
|
240
|
+
// A stalled or broken planner must not cost the user their video: the user's own
|
|
241
|
+
// prompt is already usable, so compose one locally and continue. 4xx stays fatal.
|
|
242
|
+
// devlog/_plan/260817_grok_video_planner_timeout/040_planner_fallback.md
|
|
243
|
+
const degradable = isDegradablePlannerFailure(normalized);
|
|
244
|
+
if (!degradable)
|
|
245
|
+
throw normalized;
|
|
246
|
+
const fallbackPrompt = composeFallbackVideoPrompt(prompt, {
|
|
247
|
+
mode,
|
|
248
|
+
duration,
|
|
249
|
+
resolution,
|
|
250
|
+
searchSummary,
|
|
251
|
+
continuityText: formatVideoContinuityForPlanner(options.continuityLineage) || undefined,
|
|
252
|
+
backgroundConstraint: options.backgroundConstraint,
|
|
253
|
+
});
|
|
254
|
+
logWarn("grok", "video:planner:degraded", {
|
|
255
|
+
requestId: options.requestId,
|
|
256
|
+
reason: degradable.reason,
|
|
257
|
+
error: degradable.message,
|
|
258
|
+
promptChars: fallbackPrompt.length,
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
prompt: fallbackPrompt,
|
|
262
|
+
mode,
|
|
263
|
+
duration,
|
|
264
|
+
resolution,
|
|
265
|
+
aspectRatio,
|
|
266
|
+
webSearchCalls: searchDegraded ? 0 : 1,
|
|
267
|
+
...(searchDegraded ? { searchDegraded } : {}),
|
|
268
|
+
plannerDegraded: degradable,
|
|
269
|
+
};
|
|
156
270
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
clearTimeout(phase.timer);
|
|
160
274
|
}
|
|
161
275
|
}
|
|
162
276
|
export function buildVideoGenerationPayload(plan, opts) {
|
|
@@ -288,5 +402,7 @@ export async function generateVideoViaGrok(prompt, ctx, options = {}) {
|
|
|
288
402
|
requestedModel: model,
|
|
289
403
|
effectiveModel,
|
|
290
404
|
modelFallback,
|
|
405
|
+
...(plan.searchDegraded ? { searchDegraded: plan.searchDegraded } : {}),
|
|
406
|
+
...(plan.plannerDegraded ? { plannerDegraded: plan.plannerDegraded } : {}),
|
|
291
407
|
};
|
|
292
408
|
}
|
package/lib/grokVideoDownload.js
CHANGED
|
@@ -3,7 +3,7 @@ import { grokFetchWithRetry } from "./grokUpstreamRetry.js";
|
|
|
3
3
|
const MAX_VIDEO_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
4
4
|
function downloadTimeoutMs(ctx) {
|
|
5
5
|
const g = ctx.config.grokProvider || {};
|
|
6
|
-
return g.videoDownloadTimeoutMs ||
|
|
6
|
+
return g.videoDownloadTimeoutMs || 300_000;
|
|
7
7
|
}
|
|
8
8
|
function withTimeoutSignal(signal, timeoutMs) {
|
|
9
9
|
const timeoutController = new AbortController();
|
|
@@ -153,3 +153,49 @@ export function buildGrokVideoPlannerSystemPrompt(ctx) {
|
|
|
153
153
|
"Call generate_video exactly once. Do not answer with plain text.",
|
|
154
154
|
].join("\n");
|
|
155
155
|
}
|
|
156
|
+
/** Hard cap so a degraded prompt never bloats the generation payload. */
|
|
157
|
+
const FALLBACK_PROMPT_MAX_CHARS = 4000;
|
|
158
|
+
/** The user's own scene text is the one part that must never be cut. */
|
|
159
|
+
const FALLBACK_USER_PROMPT_RESERVE = 2500;
|
|
160
|
+
/**
|
|
161
|
+
* Deterministic, network-free video prompt used when the Grok planner is unavailable
|
|
162
|
+
* (timeout, upstream 5xx/429, or a network fault). A planner that ANSWERS without a tool
|
|
163
|
+
* call is a refusal, not an availability problem, and stays fatal.
|
|
164
|
+
*
|
|
165
|
+
* The user's own prompt is already a legitimate video prompt; this only adds the pacing
|
|
166
|
+
* and mode guidance the planner would have applied, so a stalled planner costs prompt
|
|
167
|
+
* polish instead of the whole generation.
|
|
168
|
+
* devlog/_plan/260817_grok_video_planner_timeout/040_planner_fallback.md
|
|
169
|
+
*/
|
|
170
|
+
export function composeFallbackVideoPrompt(userPrompt, opts) {
|
|
171
|
+
// The caller's prompt may already carry a long storyboard preamble. Truncating from the
|
|
172
|
+
// front would keep the boilerplate and drop the user's actual scene, so the user text is
|
|
173
|
+
// reserved first and everything else is fitted around it.
|
|
174
|
+
const base = userPrompt.trim().slice(0, FALLBACK_USER_PROMPT_RESERVE);
|
|
175
|
+
const modeLine = opts.mode === "image-to-video"
|
|
176
|
+
? "Start from the provided source image as the first frame and preserve the subject's identity and composition while the described motion happens."
|
|
177
|
+
: opts.mode === "reference-to-video"
|
|
178
|
+
? "Keep the referenced subjects recognizable throughout while the described motion happens."
|
|
179
|
+
: "Describe a fresh scene: establish the setting, then carry the described motion and camera work through the shot.";
|
|
180
|
+
// Continuity and background constraints are load-bearing: without them a continue-from
|
|
181
|
+
// clip can drift off-series, or a background preset can be silently ignored.
|
|
182
|
+
const continuity = opts.continuityText?.trim()
|
|
183
|
+
? `Continuation context:\n${opts.continuityText.trim().slice(0, 600)}`
|
|
184
|
+
: "";
|
|
185
|
+
const background = opts.backgroundConstraint?.trim() ?? "";
|
|
186
|
+
const brief = opts.searchSummary?.trim()
|
|
187
|
+
? `Research context: ${opts.searchSummary.trim().slice(0, 600)}`
|
|
188
|
+
: "";
|
|
189
|
+
const composed = [
|
|
190
|
+
base,
|
|
191
|
+
"",
|
|
192
|
+
modeLine,
|
|
193
|
+
continuity,
|
|
194
|
+
background,
|
|
195
|
+
formatDurationPacingGuidance(opts.duration, opts.mode, opts.resolution),
|
|
196
|
+
brief,
|
|
197
|
+
].filter(Boolean).join("\n");
|
|
198
|
+
return composed.length > FALLBACK_PROMPT_MAX_CHARS
|
|
199
|
+
? composed.slice(0, FALLBACK_PROMPT_MAX_CHARS).trimEnd()
|
|
200
|
+
: composed;
|
|
201
|
+
}
|
package/lib/grokVideoShared.js
CHANGED
|
@@ -12,14 +12,25 @@ export const FAILED_CODE_MAP = {
|
|
|
12
12
|
};
|
|
13
13
|
export function videoConfig(ctx) {
|
|
14
14
|
const g = ctx.config.grokProvider || {}; // justified: RouteRuntimeContext.config is a loose runtime bag; every Grok adapter reads grokProvider this way
|
|
15
|
+
const plannerTimeoutMs = g.plannerTimeoutMs || 900_000;
|
|
16
|
+
const searchTimeoutMs = g.searchTimeoutMs || 300_000;
|
|
17
|
+
// The planning ceiling must stay STRICTLY greater than the two stages it contains, or a
|
|
18
|
+
// slow search plus a stalled planner lands both timers together and the fatal ceiling
|
|
19
|
+
// preempts the planner fallback. Operator config can violate that, so clamp at runtime
|
|
20
|
+
// rather than trusting the defaults to survive an env override.
|
|
21
|
+
// devlog/_plan/260817_grok_video_planner_timeout/010_timeout_budgets.md
|
|
22
|
+
const configuredPlanTotal = g.videoPlanTotalTimeoutMs || 1_500_000;
|
|
23
|
+
const minimumPlanTotal = searchTimeoutMs + plannerTimeoutMs + 60_000;
|
|
15
24
|
return {
|
|
16
25
|
model: g.defaultVideoModel || GROK_FALLBACK_VIDEO_MODEL,
|
|
17
|
-
startTimeoutMs: g.videoStartTimeoutMs ||
|
|
26
|
+
startTimeoutMs: g.videoStartTimeoutMs || 300_000,
|
|
18
27
|
pollIntervalMs: g.videoPollIntervalMs || 5_000,
|
|
19
|
-
totalTimeoutMs: g.videoTimeoutMs ||
|
|
28
|
+
totalTimeoutMs: g.videoTimeoutMs || 1_800_000,
|
|
20
29
|
pollMaxConsecutiveErrors: g.videoPollMaxConsecutiveErrors || 5,
|
|
21
30
|
plannerModel: g.plannerModel || DEFAULT_GROK_PLANNER_MODEL,
|
|
22
|
-
plannerTimeoutMs
|
|
31
|
+
plannerTimeoutMs,
|
|
32
|
+
searchTimeoutMs,
|
|
33
|
+
planTotalTimeoutMs: Math.max(configuredPlanTotal, minimumPlanTotal),
|
|
23
34
|
};
|
|
24
35
|
}
|
|
25
36
|
export function videoEndpoint(ctx, path, directApiKey) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side ceilings for a whole video request, shared by the CLI, the MCP lane, and the
|
|
3
|
+
* budget contract test so none of them can drift apart.
|
|
4
|
+
*
|
|
5
|
+
* The server worst case for one Grok video is
|
|
6
|
+
* 1500 s planning + 300 s start + 1800 s poll + 300 s poll overshoot + 300 s download
|
|
7
|
+
* = 4200 s
|
|
8
|
+
* (the poll loop checks its deadline BEFORE each request, so the final poll can exceed the
|
|
9
|
+
* poll budget by one request timeout). Every client must sit ABOVE that with real slack —
|
|
10
|
+
* an equal ceiling is a race the client can win, abandoning a request the server would
|
|
11
|
+
* have completed.
|
|
12
|
+
*
|
|
13
|
+
* MUST stay a leaf module: no imports, so bin/, ui/, and tests can all read it.
|
|
14
|
+
* devlog/_plan/260817_grok_video_planner_timeout/010_timeout_budgets.md
|
|
15
|
+
*/
|
|
16
|
+
export const VIDEO_CLIENT_TIMEOUT_SEC = 5400;
|
|
17
|
+
export const VIDEO_CLIENT_TIMEOUT_MS = VIDEO_CLIENT_TIMEOUT_SEC * 1000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
4
4
|
"packageManager": "npm@11.18.0",
|
|
5
5
|
"description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
|
|
6
6
|
"type": "module",
|
|
@@ -119,5 +119,5 @@
|
|
|
119
119
|
"tsx": "^4.23.12",
|
|
120
120
|
"typescript": "^5.9.3"
|
|
121
121
|
},
|
|
122
|
-
"gitHead": "
|
|
122
|
+
"gitHead": "ff8f95493fc2e2b5130223e6c63f55043d79fcfe"
|
|
123
123
|
}
|