ima2-gen 1.1.18 → 1.1.20
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 +11 -2
- package/bin/commands/grok.js +19 -21
- package/bin/commands/grok.ts +20 -21
- package/bin/commands/video.js +4 -0
- package/bin/commands/video.ts +3 -0
- package/docs/README.ja.md +2 -2
- package/docs/README.ko.md +15 -3
- package/docs/README.zh-CN.md +2 -2
- package/docs/migration/runtime-test-inventory.md +2 -1
- package/lib/agentGenerationPlanner.js +37 -1
- package/lib/agentGenerationPlanner.ts +45 -1
- package/lib/agentRuntime.js +107 -1
- package/lib/agentRuntime.ts +121 -1
- package/lib/agentTypes.js +1 -0
- package/lib/agentTypes.ts +2 -1
- package/lib/assetLifecycle.js +12 -8
- package/lib/assetLifecycle.ts +12 -8
- package/lib/capabilities.js +1 -1
- package/lib/capabilities.ts +1 -1
- package/lib/grokVideoAdapter.js +30 -2
- package/lib/grokVideoAdapter.ts +36 -2
- package/lib/historyList.js +1 -0
- package/lib/historyList.ts +1 -0
- package/lib/videoSeriesChain.js +24 -0
- package/lib/videoSeriesChain.ts +29 -0
- package/node_modules/progrok/README.md +300 -22
- package/node_modules/progrok/dist/index.js +558 -173
- package/node_modules/progrok/dist/index.js.map +1 -1
- package/node_modules/progrok/package.json +3 -3
- package/node_modules/progrok/skills/progrok/SKILL.md +145 -109
- package/package.json +2 -2
- package/routes/video.js +10 -1
- package/routes/video.ts +11 -1
- package/skills/ima2/SKILL.md +65 -0
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/AgentWorkspace-DS8uvoLI.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-DmqCMnIx.js → CardNewsWorkspace-CYxMsE67.js} +1 -1
- package/ui/dist/assets/NodeCanvas-DccIc347.js +7 -0
- package/ui/dist/assets/{PromptBuilderPanel-CoWjqQZS.js → PromptBuilderPanel-BvxxwSJp.js} +2 -2
- package/ui/dist/assets/{PromptImportDialog-C2zGZkyK.js → PromptImportDialog-u1_BFDRd.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-N0ZxHLYs.js → PromptImportDiscoverySection-C5uvkVSz.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-BC3dCASZ.js → PromptImportFolderSection-D3E_O1SD.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-CcVliYnF.js → PromptLibraryPanel-4gyf9CB9.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-CiB4ux7E.js → SettingsWorkspace-F3eNu3mJ.js} +1 -1
- package/ui/dist/assets/index-B6tcw_UF.css +1 -0
- package/ui/dist/assets/index-DYOh6gQD.js +32 -0
- package/ui/dist/assets/{index-C93CfR9P.js → index-DoKtXbod.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/vendor/progrok-0.1.1.tgz +0 -0
- package/ui/dist/assets/AgentWorkspace-BTuPjlDH.js +0 -3
- package/ui/dist/assets/NodeCanvas-jr9WXfNm.js +0 -7
- package/ui/dist/assets/index-CIhB_ia7.css +0 -1
- package/ui/dist/assets/index-uBEJn5jz.js +0 -32
- package/vendor/progrok-0.1.0.tgz +0 -0
package/lib/agentRuntime.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { detectImageMimeFromB64 } from "./refs.js";
|
|
|
9
9
|
import { resolveProviderOptions } from "./providerOptions.js";
|
|
10
10
|
import { generateViaResponses } from "./responsesImageAdapter.js";
|
|
11
11
|
import { generateViaGrok, type GrokReferenceImage } from "./grokImageAdapter.js";
|
|
12
|
+
import { generateVideoViaGrok } from "./grokVideoAdapter.js";
|
|
13
|
+
import { parseVideoParams } from "./agentGenerationPlanner.js";
|
|
12
14
|
import {
|
|
13
15
|
appendAgentTurn,
|
|
14
16
|
buildImageContextManifest,
|
|
@@ -95,7 +97,7 @@ export async function runAgentGenerationPlan(
|
|
|
95
97
|
const webSearchEnabled = options.provider === "grok" ? true : options.webSearchEnabled ?? session.webSearchEnabled;
|
|
96
98
|
const enabledTools: AgentToolName[] = webSearchEnabled
|
|
97
99
|
? [...AGENT_ALLOWED_TOOLS]
|
|
98
|
-
: ["ima2.get_image_context", "ima2.generate_image"];
|
|
100
|
+
: ["ima2.get_image_context", "ima2.generate_image", "ima2.generate_video"];
|
|
99
101
|
assertAgentAllowedTools(enabledTools);
|
|
100
102
|
if (behavior.appendUserTurn !== false) {
|
|
101
103
|
appendAgentTurn({ sessionId, role: "user", text: prompt, status: "complete" });
|
|
@@ -111,6 +113,13 @@ export async function runAgentGenerationPlan(
|
|
|
111
113
|
});
|
|
112
114
|
return { assistantTurn, imageIds: [], webFindingIds: [] };
|
|
113
115
|
}
|
|
116
|
+
if (plan.mode === "video") {
|
|
117
|
+
return runAgentVideoGeneration(ctx, sessionId, prompt, {
|
|
118
|
+
...options,
|
|
119
|
+
requestId: options.requestId ?? `agent_video_${ulid()}`,
|
|
120
|
+
skipUserTurn: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
114
123
|
const manifest = buildImageContextManifest(sessionId);
|
|
115
124
|
const contextStartedAt = Date.now();
|
|
116
125
|
appendAgentTurn({
|
|
@@ -397,6 +406,117 @@ async function persistAgentImage(
|
|
|
397
406
|
});
|
|
398
407
|
}
|
|
399
408
|
|
|
409
|
+
export async function runAgentVideoGeneration(
|
|
410
|
+
ctx: RuntimeContext,
|
|
411
|
+
sessionId: string,
|
|
412
|
+
prompt: string,
|
|
413
|
+
options: AgentRunOptions & { skipUserTurn?: boolean } = {},
|
|
414
|
+
) {
|
|
415
|
+
const session = getAgentSession(sessionId);
|
|
416
|
+
if (!session) throw notFound(sessionId);
|
|
417
|
+
if (!options.skipUserTurn) {
|
|
418
|
+
appendAgentTurn({ sessionId, role: "user", text: prompt, status: "complete" });
|
|
419
|
+
}
|
|
420
|
+
const requestId = options.requestId ?? `agent_video_${ulid()}`;
|
|
421
|
+
const startedAt = Date.now();
|
|
422
|
+
|
|
423
|
+
// Auto I2V: if session has a last image, use it as source
|
|
424
|
+
let sourceImage: string | undefined;
|
|
425
|
+
let mode: "text-to-video" | "image-to-video" = "text-to-video";
|
|
426
|
+
if (session.lastImageId) {
|
|
427
|
+
const images = getAgentImages(sessionId);
|
|
428
|
+
const lastImage = images.find((img) => img.id === session.lastImageId);
|
|
429
|
+
if (lastImage?.filename && !lastImage.filename.endsWith(".mp4")) {
|
|
430
|
+
try {
|
|
431
|
+
const { loadAssetB64 } = await import("./nodeStore.js");
|
|
432
|
+
sourceImage = await loadAssetB64(ctx.rootDir, lastImage.filename, ctx.config.storage.generatedDir);
|
|
433
|
+
mode = "image-to-video";
|
|
434
|
+
} catch { /* fallback to T2V */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const videoParams = parseVideoParams(prompt);
|
|
439
|
+
|
|
440
|
+
const result = await generateVideoViaGrok(prompt, ctx, {
|
|
441
|
+
model: "grok-imagine-video",
|
|
442
|
+
mode,
|
|
443
|
+
sourceImage,
|
|
444
|
+
duration: videoParams.duration ?? 5,
|
|
445
|
+
resolution: videoParams.resolution ?? "480p",
|
|
446
|
+
aspectRatio: (videoParams.aspectRatio ?? "auto") as "auto" | "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3",
|
|
447
|
+
requestId,
|
|
448
|
+
signal: options.signal ?? undefined,
|
|
449
|
+
});
|
|
450
|
+
const video = await persistAgentVideo(ctx, sessionId, prompt, requestId, result);
|
|
451
|
+
const finishedAt = Date.now();
|
|
452
|
+
const toolCall: AgentToolCallSummary = {
|
|
453
|
+
id: `tc_video_${ulid()}`,
|
|
454
|
+
name: "ima2.generate_video",
|
|
455
|
+
status: "complete",
|
|
456
|
+
startedAt,
|
|
457
|
+
finishedAt,
|
|
458
|
+
durationMs: finishedAt - startedAt,
|
|
459
|
+
requestId,
|
|
460
|
+
inputSummary: prompt,
|
|
461
|
+
outputSummary: `Generated video ${video.filename}.`,
|
|
462
|
+
imageIds: [video.id],
|
|
463
|
+
};
|
|
464
|
+
appendAgentTurn({
|
|
465
|
+
sessionId,
|
|
466
|
+
role: "tool",
|
|
467
|
+
text: "ima2.generate_video",
|
|
468
|
+
imageIds: [video.id],
|
|
469
|
+
status: "complete",
|
|
470
|
+
raw: { toolCalls: [toolCall] },
|
|
471
|
+
});
|
|
472
|
+
const assistantTurn = appendAgentTurn({
|
|
473
|
+
sessionId,
|
|
474
|
+
role: "assistant",
|
|
475
|
+
text: `Generated 1 video artifact. ${result.revisedPrompt}`,
|
|
476
|
+
imageIds: [video.id],
|
|
477
|
+
status: "complete",
|
|
478
|
+
});
|
|
479
|
+
return { assistantTurn, imageIds: [video.id], webFindingIds: [] };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function persistAgentVideo(
|
|
483
|
+
ctx: RuntimeContext,
|
|
484
|
+
sessionId: string,
|
|
485
|
+
prompt: string,
|
|
486
|
+
requestId: string,
|
|
487
|
+
result: { videoBuffer: Buffer; revisedPrompt: string; usage: Record<string, number> | null; webSearchCalls: number },
|
|
488
|
+
) {
|
|
489
|
+
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
490
|
+
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
491
|
+
const filename = `${Date.now()}_${rand}_agent.mp4`;
|
|
492
|
+
const meta = {
|
|
493
|
+
kind: "agent",
|
|
494
|
+
mediaType: "video",
|
|
495
|
+
requestId,
|
|
496
|
+
sessionId,
|
|
497
|
+
prompt,
|
|
498
|
+
userPrompt: prompt,
|
|
499
|
+
revisedPrompt: result.revisedPrompt,
|
|
500
|
+
provider: "grok",
|
|
501
|
+
model: "grok-imagine-video",
|
|
502
|
+
createdAt: Date.now(),
|
|
503
|
+
usage: result.usage,
|
|
504
|
+
webSearchCalls: result.webSearchCalls,
|
|
505
|
+
};
|
|
506
|
+
await writeFile(join(ctx.config.storage.generatedDir, filename), result.videoBuffer);
|
|
507
|
+
await writeFile(join(ctx.config.storage.generatedDir, `${filename}.json`), JSON.stringify(meta)).catch(() => {});
|
|
508
|
+
invalidateHistoryIndex();
|
|
509
|
+
logEvent("agent", "video_saved", { requestId, sessionId, filename });
|
|
510
|
+
return importAgentImage(sessionId, {
|
|
511
|
+
id: `ai_${ulid()}`,
|
|
512
|
+
filename,
|
|
513
|
+
url: `/generated/${filename}`,
|
|
514
|
+
prompt,
|
|
515
|
+
revisedPrompt: result.revisedPrompt,
|
|
516
|
+
createdAt: Date.now(),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
400
520
|
function recordSearchFindings(sessionId: string, prompt: string, count: number, provider: string) {
|
|
401
521
|
if (!count) return [];
|
|
402
522
|
const isGrok = provider === "grok";
|
package/lib/agentTypes.js
CHANGED
package/lib/agentTypes.ts
CHANGED
|
@@ -2,6 +2,7 @@ export const AGENT_ALLOWED_TOOLS = [
|
|
|
2
2
|
"ima2.get_image_context",
|
|
3
3
|
"ima2.web_search",
|
|
4
4
|
"ima2.generate_image",
|
|
5
|
+
"ima2.generate_video",
|
|
5
6
|
] as const;
|
|
6
7
|
|
|
7
8
|
export type AgentToolName = typeof AGENT_ALLOWED_TOOLS[number];
|
|
@@ -11,7 +12,7 @@ export type AgentToolCallStatus = "queued" | "running" | "complete" | "error";
|
|
|
11
12
|
export type AgentQueueStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
|
|
12
13
|
export type AgentSessionRunStatus = "idle" | "queued" | "running" | "error";
|
|
13
14
|
export type AgentGenerationStrategy = "auto" | "manual";
|
|
14
|
-
export type AgentGenerationPlanMode = "single" | "fanout" | "question";
|
|
15
|
+
export type AgentGenerationPlanMode = "single" | "fanout" | "question" | "video";
|
|
15
16
|
export type AgentGenerationPlanSource = "auto-default" | "auto-request" | "manual-settings" | "slash-command" | "question-command";
|
|
16
17
|
export type AgentSlashCommandName = "question" | "help" | "variants" | "generate" | "parallelism";
|
|
17
18
|
|
package/lib/assetLifecycle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDb } from "./db.js";
|
|
2
|
-
import { rename, unlink, access } from "fs/promises";
|
|
2
|
+
import { mkdir, rename, unlink, access } from "fs/promises";
|
|
3
3
|
import { resolve, sep } from "path";
|
|
4
4
|
import { moveToSystemTrash } from "./systemTrash.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
@@ -87,21 +87,25 @@ export async function trashAsset(rootDir, filename) {
|
|
|
87
87
|
paths.push(sidecar);
|
|
88
88
|
}
|
|
89
89
|
catch { }
|
|
90
|
+
let trashMethod = "system";
|
|
90
91
|
try {
|
|
91
92
|
await moveToSystemTrash(paths);
|
|
92
93
|
}
|
|
93
|
-
catch
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
catch {
|
|
95
|
+
trashMethod = "internal";
|
|
96
|
+
const trashDir = resolve(config.storage.trashDir);
|
|
97
|
+
await mkdir(trashDir, { recursive: true });
|
|
98
|
+
const trashId = `${Date.now()}_${filename}`;
|
|
99
|
+
for (const p of paths) {
|
|
100
|
+
const dest = resolve(trashDir, p.endsWith(".json") ? `${trashId}.json` : trashId);
|
|
101
|
+
await rename(p, dest);
|
|
102
|
+
}
|
|
99
103
|
}
|
|
100
104
|
const summary = markNodesAssetMissing(filename);
|
|
101
105
|
return {
|
|
102
106
|
ok: true,
|
|
103
107
|
filename,
|
|
104
|
-
trash:
|
|
108
|
+
trash: trashMethod,
|
|
105
109
|
undoableInApp: false,
|
|
106
110
|
sessionsTouched: summary.sessionsTouched,
|
|
107
111
|
nodesTouched: summary.nodesTouched,
|
package/lib/assetLifecycle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDb } from "./db.js";
|
|
2
|
-
import { rename, unlink, access } from "fs/promises";
|
|
2
|
+
import { mkdir, rename, unlink, access } from "fs/promises";
|
|
3
3
|
import { resolve, sep } from "path";
|
|
4
4
|
import { moveToSystemTrash } from "./systemTrash.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
@@ -84,21 +84,25 @@ export async function trashAsset(rootDir: string, filename: string) {
|
|
|
84
84
|
paths.push(sidecar);
|
|
85
85
|
} catch {}
|
|
86
86
|
|
|
87
|
+
let trashMethod: "system" | "internal" = "system";
|
|
87
88
|
try {
|
|
88
89
|
await moveToSystemTrash(paths);
|
|
89
|
-
} catch
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
} catch {
|
|
91
|
+
trashMethod = "internal";
|
|
92
|
+
const trashDir = resolve(config.storage.trashDir);
|
|
93
|
+
await mkdir(trashDir, { recursive: true });
|
|
94
|
+
const trashId = `${Date.now()}_${filename}`;
|
|
95
|
+
for (const p of paths) {
|
|
96
|
+
const dest = resolve(trashDir, p.endsWith(".json") ? `${trashId}.json` : trashId);
|
|
97
|
+
await rename(p, dest);
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
const summary = markNodesAssetMissing(filename);
|
|
98
102
|
return {
|
|
99
103
|
ok: true,
|
|
100
104
|
filename,
|
|
101
|
-
trash:
|
|
105
|
+
trash: trashMethod,
|
|
102
106
|
undoableInApp: false,
|
|
103
107
|
sessionsTouched: summary.sessionsTouched,
|
|
104
108
|
nodesTouched: summary.nodesTouched,
|
package/lib/capabilities.js
CHANGED
|
@@ -106,7 +106,7 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
|
|
|
106
106
|
i2i: "Use --ref for reference generation, or ima2 edit <file> --prompt \"<text>\" for image edits.",
|
|
107
107
|
defaults: "Use ima2 defaults set model/reasoning for persistent defaults; request flags remain per-call overrides.",
|
|
108
108
|
promptBuilder: "Use ima2 prompt build --message \"...\" to refine prompt intent. Use ima2 gen / ima2 multimode to generate images. Workspace profile settings are UI-only.",
|
|
109
|
-
video: "Use ima2 video \"<prompt>\" to generate video. Supports --ref for image-to-video and reference-to-video modes.",
|
|
109
|
+
video: "Use ima2 video \"<prompt>\" to generate video. Supports --ref for image-to-video and reference-to-video modes. Use --topic for series continuity across multiple generations.",
|
|
110
110
|
},
|
|
111
111
|
};
|
|
112
112
|
}
|
package/lib/capabilities.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function buildIma2Capabilities({
|
|
|
120
120
|
i2i: "Use --ref for reference generation, or ima2 edit <file> --prompt \"<text>\" for image edits.",
|
|
121
121
|
defaults: "Use ima2 defaults set model/reasoning for persistent defaults; request flags remain per-call overrides.",
|
|
122
122
|
promptBuilder: "Use ima2 prompt build --message \"...\" to refine prompt intent. Use ima2 gen / ima2 multimode to generate images. Workspace profile settings are UI-only.",
|
|
123
|
-
video: "Use ima2 video \"<prompt>\" to generate video. Supports --ref for image-to-video and reference-to-video modes.",
|
|
123
|
+
video: "Use ima2 video \"<prompt>\" to generate video. Supports --ref for image-to-video and reference-to-video modes. Use --topic for series continuity across multiple generations.",
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
126
|
}
|
package/lib/grokVideoAdapter.js
CHANGED
|
@@ -45,6 +45,25 @@ function sourceImageUrl(image, mime) {
|
|
|
45
45
|
const detected = mime || detectImageMimeFromB64(image) || "image/png";
|
|
46
46
|
return `data:${detected};base64,${image}`;
|
|
47
47
|
}
|
|
48
|
+
/** Map aspect ratio + resolution to pixel dimensions for white canvas injection. */
|
|
49
|
+
function aspectToCanvas(aspectRatio, resolution) {
|
|
50
|
+
const base = resolution === "720p" ? 720 : 480;
|
|
51
|
+
const ratios = {
|
|
52
|
+
"16:9": [16, 9], "9:16": [9, 16], "4:3": [4, 3], "3:4": [3, 4],
|
|
53
|
+
"3:2": [3, 2], "2:3": [2, 3], "1:1": [1, 1], "auto": [16, 9],
|
|
54
|
+
};
|
|
55
|
+
const [w, h] = ratios[aspectRatio] || [16, 9];
|
|
56
|
+
if (w >= h)
|
|
57
|
+
return { width: Math.round(base * w / h), height: base };
|
|
58
|
+
return { width: base, height: Math.round(base * h / w) };
|
|
59
|
+
}
|
|
60
|
+
/** Generate a minimal white PNG as base64 (no external deps). */
|
|
61
|
+
function generateWhiteCanvasB64() {
|
|
62
|
+
// Minimal valid 1x1 white PNG, scaled conceptually — xAI will accept any valid PNG
|
|
63
|
+
// For simplicity, use a tiny white PNG (the model doesn't use it as a real frame)
|
|
64
|
+
const PNG_1x1_WHITE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/58BAwAHBQKhPX8EPAAAAABJRU5ErkJggg==";
|
|
65
|
+
return PNG_1x1_WHITE;
|
|
66
|
+
}
|
|
48
67
|
const FAILED_CODE_MAP = {
|
|
49
68
|
invalid_argument: { code: "GROK_VIDEO_REQUEST_FAILED", status: 400 },
|
|
50
69
|
permission_denied: { code: "GROK_VIDEO_REQUEST_FAILED", status: 403 },
|
|
@@ -383,11 +402,20 @@ export async function generateVideoViaGrok(prompt, ctx, options = {}) {
|
|
|
383
402
|
const payload = buildVideoGenerationPayload(plan, { model, sourceImageUrl: srcUrl, referenceImageUrls: refUrls });
|
|
384
403
|
let xaiVideoRequestId;
|
|
385
404
|
let effectiveModel = model;
|
|
405
|
+
// grokv1.5 doesn't support T2V — inject a white canvas as source image to use I2V path
|
|
406
|
+
let effectivePayload = payload;
|
|
407
|
+
if (model === "grok-imagine-video-1.5-preview" && !srcUrl && refUrls.length === 0) {
|
|
408
|
+
const { width, height } = aspectToCanvas(plan.aspectRatio, plan.resolution);
|
|
409
|
+
const whiteCanvas = generateWhiteCanvasB64();
|
|
410
|
+
const canvasSrcUrl = `data:image/png;base64,${whiteCanvas}`;
|
|
411
|
+
effectivePayload = buildVideoGenerationPayload({ ...plan, prompt: `${plan.prompt}. This is not a start frame — generate freely as a new video.` }, { model, sourceImageUrl: canvasSrcUrl, referenceImageUrls: [] });
|
|
412
|
+
logEvent("grok", "video:1.5-t2v-canvas", { requestId: options.requestId, width, height });
|
|
413
|
+
}
|
|
386
414
|
try {
|
|
387
|
-
xaiVideoRequestId = await startVideoRequest(ctx,
|
|
415
|
+
xaiVideoRequestId = await startVideoRequest(ctx, effectivePayload, options);
|
|
388
416
|
}
|
|
389
417
|
catch (e) {
|
|
390
|
-
// Fallback: if 1.5-preview fails, retry with base model
|
|
418
|
+
// Fallback: if 1.5-preview still fails, retry with base model
|
|
391
419
|
if (model !== "grok-imagine-video" && e?.status === 400) {
|
|
392
420
|
effectiveModel = "grok-imagine-video";
|
|
393
421
|
const fallbackPayload = buildVideoGenerationPayload(plan, { model: effectiveModel, sourceImageUrl: srcUrl, referenceImageUrls: refUrls });
|
package/lib/grokVideoAdapter.ts
CHANGED
|
@@ -124,6 +124,26 @@ function sourceImageUrl(image: string, mime?: string | null): string {
|
|
|
124
124
|
return `data:${detected};base64,${image}`;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** Map aspect ratio + resolution to pixel dimensions for white canvas injection. */
|
|
128
|
+
function aspectToCanvas(aspectRatio: string, resolution: string): { width: number; height: number } {
|
|
129
|
+
const base = resolution === "720p" ? 720 : 480;
|
|
130
|
+
const ratios: Record<string, [number, number]> = {
|
|
131
|
+
"16:9": [16, 9], "9:16": [9, 16], "4:3": [4, 3], "3:4": [3, 4],
|
|
132
|
+
"3:2": [3, 2], "2:3": [2, 3], "1:1": [1, 1], "auto": [16, 9],
|
|
133
|
+
};
|
|
134
|
+
const [w, h] = ratios[aspectRatio] || [16, 9];
|
|
135
|
+
if (w >= h) return { width: Math.round(base * w / h), height: base };
|
|
136
|
+
return { width: base, height: Math.round(base * h / w) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Generate a minimal white PNG as base64 (no external deps). */
|
|
140
|
+
function generateWhiteCanvasB64(): string {
|
|
141
|
+
// Minimal valid 1x1 white PNG, scaled conceptually — xAI will accept any valid PNG
|
|
142
|
+
// For simplicity, use a tiny white PNG (the model doesn't use it as a real frame)
|
|
143
|
+
const PNG_1x1_WHITE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/58BAwAHBQKhPX8EPAAAAABJRU5ErkJggg==";
|
|
144
|
+
return PNG_1x1_WHITE;
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
const FAILED_CODE_MAP: Record<string, { code: string; status: number }> = {
|
|
128
148
|
invalid_argument: { code: "GROK_VIDEO_REQUEST_FAILED", status: 400 },
|
|
129
149
|
permission_denied: { code: "GROK_VIDEO_REQUEST_FAILED", status: 403 },
|
|
@@ -449,10 +469,24 @@ export async function generateVideoViaGrok(prompt: string, ctx: RouteRuntimeCont
|
|
|
449
469
|
const payload = buildVideoGenerationPayload(plan, { model, sourceImageUrl: srcUrl, referenceImageUrls: refUrls });
|
|
450
470
|
let xaiVideoRequestId: string;
|
|
451
471
|
let effectiveModel = model;
|
|
472
|
+
|
|
473
|
+
// grokv1.5 doesn't support T2V — inject a white canvas as source image to use I2V path
|
|
474
|
+
let effectivePayload = payload;
|
|
475
|
+
if (model === "grok-imagine-video-1.5-preview" && !srcUrl && refUrls.length === 0) {
|
|
476
|
+
const { width, height } = aspectToCanvas(plan.aspectRatio, plan.resolution);
|
|
477
|
+
const whiteCanvas = generateWhiteCanvasB64();
|
|
478
|
+
const canvasSrcUrl = `data:image/png;base64,${whiteCanvas}`;
|
|
479
|
+
effectivePayload = buildVideoGenerationPayload(
|
|
480
|
+
{ ...plan, prompt: `${plan.prompt}. This is not a start frame — generate freely as a new video.` },
|
|
481
|
+
{ model, sourceImageUrl: canvasSrcUrl, referenceImageUrls: [] },
|
|
482
|
+
);
|
|
483
|
+
logEvent("grok", "video:1.5-t2v-canvas", { requestId: options.requestId, width, height });
|
|
484
|
+
}
|
|
485
|
+
|
|
452
486
|
try {
|
|
453
|
-
xaiVideoRequestId = await startVideoRequest(ctx,
|
|
487
|
+
xaiVideoRequestId = await startVideoRequest(ctx, effectivePayload, options);
|
|
454
488
|
} catch (e: any) {
|
|
455
|
-
// Fallback: if 1.5-preview fails, retry with base model
|
|
489
|
+
// Fallback: if 1.5-preview still fails, retry with base model
|
|
456
490
|
if (model !== "grok-imagine-video" && e?.status === 400) {
|
|
457
491
|
effectiveModel = "grok-imagine-video";
|
|
458
492
|
const fallbackPayload = buildVideoGenerationPayload(plan, { model: effectiveModel, sourceImageUrl: srcUrl, referenceImageUrls: refUrls });
|
package/lib/historyList.js
CHANGED
|
@@ -34,6 +34,7 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
34
34
|
url: `/generated/${rel.split("/").map(encodeURIComponent).join("/")}`,
|
|
35
35
|
mediaType: meta?.mediaType || (/\.mp4$/i.test(name) ? "video" : "image"),
|
|
36
36
|
video: meta?.video || null,
|
|
37
|
+
videoSeries: meta?.videoSeries || null,
|
|
37
38
|
createdAt: meta?.createdAt || st?.mtimeMs || 0,
|
|
38
39
|
prompt: meta?.prompt || null,
|
|
39
40
|
userPrompt: meta?.userPrompt || meta?.prompt || null,
|
package/lib/historyList.ts
CHANGED
|
@@ -36,6 +36,7 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
36
36
|
url: `/generated/${rel.split("/").map(encodeURIComponent).join("/")}`,
|
|
37
37
|
mediaType: meta?.mediaType || (/\.mp4$/i.test(name) ? "video" : "image"),
|
|
38
38
|
video: meta?.video || null,
|
|
39
|
+
videoSeries: meta?.videoSeries || null,
|
|
39
40
|
createdAt: meta?.createdAt || st?.mtimeMs || 0,
|
|
40
41
|
prompt: meta?.prompt || null,
|
|
41
42
|
userPrompt: meta?.userPrompt || meta?.prompt || null,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Scan generatedDir for videos with matching topic, return the most recent N revisedPrompts.
|
|
5
|
+
*/
|
|
6
|
+
export async function getVideoSeriesChain(generatedDir, topic, limit = 4) {
|
|
7
|
+
if (!topic.trim())
|
|
8
|
+
return [];
|
|
9
|
+
const entries = await readdir(generatedDir).catch(() => []);
|
|
10
|
+
const sidecars = entries.filter((e) => e.endsWith(".mp4.json"));
|
|
11
|
+
const matches = [];
|
|
12
|
+
for (const sidecar of sidecars) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(join(generatedDir, sidecar), "utf-8");
|
|
15
|
+
const meta = JSON.parse(raw);
|
|
16
|
+
if (meta.videoSeries?.topic === topic && meta.revisedPrompt) {
|
|
17
|
+
matches.push({ revisedPrompt: meta.revisedPrompt, createdAt: meta.createdAt ?? 0 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { /* skip unreadable */ }
|
|
21
|
+
}
|
|
22
|
+
matches.sort((a, b) => b.createdAt - a.createdAt);
|
|
23
|
+
return matches.slice(0, limit).reverse().map((m) => m.revisedPrompt);
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
interface VideoSeriesMeta {
|
|
5
|
+
revisedPrompt?: string;
|
|
6
|
+
createdAt?: number;
|
|
7
|
+
videoSeries?: { topic: string; chainIndex?: number };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scan generatedDir for videos with matching topic, return the most recent N revisedPrompts.
|
|
12
|
+
*/
|
|
13
|
+
export async function getVideoSeriesChain(generatedDir: string, topic: string, limit = 4): Promise<string[]> {
|
|
14
|
+
if (!topic.trim()) return [];
|
|
15
|
+
const entries = await readdir(generatedDir).catch(() => [] as string[]);
|
|
16
|
+
const sidecars = entries.filter((e) => e.endsWith(".mp4.json"));
|
|
17
|
+
const matches: Array<{ revisedPrompt: string; createdAt: number }> = [];
|
|
18
|
+
for (const sidecar of sidecars) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(join(generatedDir, sidecar), "utf-8");
|
|
21
|
+
const meta: VideoSeriesMeta = JSON.parse(raw);
|
|
22
|
+
if (meta.videoSeries?.topic === topic && meta.revisedPrompt) {
|
|
23
|
+
matches.push({ revisedPrompt: meta.revisedPrompt, createdAt: meta.createdAt ?? 0 });
|
|
24
|
+
}
|
|
25
|
+
} catch { /* skip unreadable */ }
|
|
26
|
+
}
|
|
27
|
+
matches.sort((a, b) => b.createdAt - a.createdAt);
|
|
28
|
+
return matches.slice(0, limit).reverse().map((m) => m.revisedPrompt);
|
|
29
|
+
}
|