ima2-gen 1.1.20 → 1.1.22
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 +42 -26
- package/bin/commands/capabilities.js +2 -2
- package/bin/commands/defaults.js +2 -2
- package/bin/commands/doctor.js +3 -3
- package/bin/commands/edit.js +1 -1
- package/bin/commands/gen.js +1 -1
- package/bin/commands/grok.js +16 -11
- package/bin/commands/multimode.js +1 -1
- package/bin/commands/observability.js +2 -2
- package/bin/commands/video.js +335 -13
- package/bin/ima2.js +23 -13
- package/bin/lib/error-hints.js +2 -2
- package/bin/lib/platform.js +34 -5
- package/docs/API.md +112 -3
- package/docs/CLI.md +61 -7
- package/docs/FAQ.ko.md +15 -20
- package/docs/FAQ.md +14 -19
- package/docs/NPX_QUICKSTART.md +40 -0
- package/docs/PROMPT_STUDIO.ko.md +1 -1
- package/docs/PROMPT_STUDIO.md +1 -1
- package/docs/README.ja.md +6 -16
- package/docs/README.ko.md +35 -14
- package/docs/README.zh-CN.md +7 -17
- package/docs/migration/runtime-test-inventory.md +8 -1
- package/lib/agentQueueWorker.js +6 -0
- package/lib/agentRuntime.js +20 -5
- package/lib/atomicWrite.js +14 -0
- package/lib/capabilities.js +1 -1
- package/lib/generationErrors.js +1 -1
- package/lib/grokProxyLauncher.js +31 -6
- package/lib/grokVideoAdapter.js +18 -89
- package/lib/grokVideoCanvas.js +25 -0
- package/lib/grokVideoDownload.js +58 -0
- package/lib/grokVideoPlannerPrompt.js +64 -0
- package/lib/historyList.js +7 -1
- package/lib/inflight.js +1 -1
- package/lib/oauthLauncher.js +26 -6
- package/lib/videoContinuity.js +149 -0
- package/lib/videoFrameExtract.js +80 -0
- package/node_modules/progrok/dist/index.js +187 -88
- package/node_modules/progrok/dist/index.js.map +1 -1
- package/node_modules/progrok/package.json +1 -1
- package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
- package/package.json +6 -8
- package/routes/edit.js +2 -1
- package/routes/generate.js +4 -3
- package/routes/health.js +4 -3
- package/routes/index.js +4 -0
- package/routes/multimode.js +2 -1
- package/routes/quota.js +66 -0
- package/routes/video.js +80 -16
- package/routes/videoExtended.js +293 -0
- package/server.js +35 -4
- package/skills/ima2/SKILL.md +320 -7
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-DS8uvoLI.js → AgentWorkspace-COxQ5TjU.js} +2 -2
- package/ui/dist/assets/{CardNewsWorkspace-CYxMsE67.js → CardNewsWorkspace-B0OkcuVz.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-DccIc347.js → NodeCanvas-BSsclEBh.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-BvxxwSJp.js → PromptBuilderPanel-DpC9A5Rz.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-u1_BFDRd.js → PromptImportDialog-CVwT0rLd.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-C5uvkVSz.js → PromptImportDiscoverySection-BDCkRCRs.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-D3E_O1SD.js → PromptImportFolderSection-QoKbZD83.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-4gyf9CB9.js → PromptLibraryPanel-BhFgeKnY.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-CfjrlH5R.js +1 -0
- package/ui/dist/assets/index-C-mur7pa.css +1 -0
- package/ui/dist/assets/index-CCP5nUOj.js +42 -0
- package/ui/dist/assets/{index-DoKtXbod.js → index-Cxhzi3bs.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/vendor/progrok-0.2.0.tgz +0 -0
- package/bin/commands/annotate.ts +0 -119
- package/bin/commands/cancel.ts +0 -48
- package/bin/commands/canvas-versions.ts +0 -80
- package/bin/commands/capabilities.ts +0 -110
- package/bin/commands/cardnews.ts +0 -249
- package/bin/commands/comfy.ts +0 -54
- package/bin/commands/config.ts +0 -186
- package/bin/commands/defaults.ts +0 -192
- package/bin/commands/doctor.ts +0 -202
- package/bin/commands/edit.ts +0 -150
- package/bin/commands/gen.ts +0 -214
- package/bin/commands/grok.ts +0 -85
- package/bin/commands/history.ts +0 -146
- package/bin/commands/ls.ts +0 -64
- package/bin/commands/metadata.ts +0 -39
- package/bin/commands/multimode.ts +0 -196
- package/bin/commands/node.ts +0 -166
- package/bin/commands/observability.ts +0 -176
- package/bin/commands/ping.ts +0 -31
- package/bin/commands/prompt-sub/build.ts +0 -101
- package/bin/commands/prompt.ts +0 -492
- package/bin/commands/ps.ts +0 -81
- package/bin/commands/session.ts +0 -266
- package/bin/commands/show.ts +0 -72
- package/bin/commands/skill.ts +0 -70
- package/bin/commands/video.ts +0 -205
- package/bin/ima2.ts +0 -430
- package/bin/lib/args.ts +0 -92
- package/bin/lib/browser-id.ts +0 -16
- package/bin/lib/client.ts +0 -122
- package/bin/lib/config-store.ts +0 -120
- package/bin/lib/destructive-confirm.ts +0 -19
- package/bin/lib/doctor-checks.ts +0 -91
- package/bin/lib/error-hints.ts +0 -23
- package/bin/lib/files.ts +0 -39
- package/bin/lib/output.ts +0 -73
- package/bin/lib/platform.ts +0 -99
- package/bin/lib/recover-output.ts +0 -139
- package/bin/lib/sse.ts +0 -73
- package/bin/lib/star-prompt.ts +0 -97
- package/bin/lib/storage-doctor.ts +0 -39
- package/bin/lib/ui-build.ts +0 -85
- package/config.ts +0 -354
- package/lib/agentCommandParser.ts +0 -69
- package/lib/agentGenerationPlanner.ts +0 -273
- package/lib/agentQuestionResponder.ts +0 -266
- package/lib/agentQueueStore.ts +0 -270
- package/lib/agentQueueWorker.ts +0 -89
- package/lib/agentRuntime.ts +0 -592
- package/lib/agentSettings.ts +0 -72
- package/lib/agentStore.ts +0 -422
- package/lib/agentStoreRows.ts +0 -136
- package/lib/agentTypes.ts +0 -154
- package/lib/apiCachePolicy.ts +0 -11
- package/lib/assetLifecycle.ts +0 -146
- package/lib/canvasVersionStore.ts +0 -223
- package/lib/capabilities.ts +0 -126
- package/lib/cardNewsGenerator.ts +0 -271
- package/lib/cardNewsJobStore.ts +0 -142
- package/lib/cardNewsManifestStore.ts +0 -154
- package/lib/cardNewsPlanner.ts +0 -236
- package/lib/cardNewsPlannerClient.ts +0 -155
- package/lib/cardNewsPlannerPrompt.ts +0 -62
- package/lib/cardNewsPlannerSchema.ts +0 -321
- package/lib/cardNewsRoleTemplateStore.ts +0 -47
- package/lib/cardNewsTemplateStore.ts +0 -252
- package/lib/codexDetect.ts +0 -71
- package/lib/comfyBridge.ts +0 -235
- package/lib/composerSnapshot.ts +0 -33
- package/lib/configKeys.ts +0 -62
- package/lib/db.ts +0 -295
- package/lib/errInfo.ts +0 -43
- package/lib/errorClassify.ts +0 -100
- package/lib/generationCancel.ts +0 -28
- package/lib/generationErrors.ts +0 -238
- package/lib/grokImageAdapter.ts +0 -513
- package/lib/grokMultimodeAdapter.ts +0 -84
- package/lib/grokProxyLauncher.ts +0 -129
- package/lib/grokRuntime.ts +0 -23
- package/lib/grokSizeMapper.ts +0 -71
- package/lib/grokVideoAdapter.ts +0 -519
- package/lib/historyIndex.ts +0 -51
- package/lib/historyList.ts +0 -177
- package/lib/imageMetadata.ts +0 -113
- package/lib/imageMetadataStore.ts +0 -67
- package/lib/imageModels.ts +0 -165
- package/lib/inflight.ts +0 -281
- package/lib/localImportStore.ts +0 -114
- package/lib/logger.ts +0 -161
- package/lib/nodeStore.ts +0 -91
- package/lib/oauthLauncher.ts +0 -78
- package/lib/oauthNormalize.ts +0 -30
- package/lib/oauthProxy/errors.ts +0 -128
- package/lib/oauthProxy/generators.ts +0 -494
- package/lib/oauthProxy/index.ts +0 -28
- package/lib/oauthProxy/prompts.ts +0 -123
- package/lib/oauthProxy/references.ts +0 -45
- package/lib/oauthProxy/runtime.ts +0 -115
- package/lib/oauthProxy/streams.ts +0 -232
- package/lib/oauthProxy/types.ts +0 -9
- package/lib/oauthProxy.ts +0 -3
- package/lib/openDirectory.ts +0 -47
- package/lib/pngInfo.ts +0 -26
- package/lib/promptBuilder/attachments.ts +0 -74
- package/lib/promptBuilder/client.ts +0 -130
- package/lib/promptBuilder/constants.ts +0 -9
- package/lib/promptBuilder/context.ts +0 -36
- package/lib/promptBuilder/errors.ts +0 -12
- package/lib/promptBuilder/requestSchema.ts +0 -56
- package/lib/promptBuilder/responseParser.ts +0 -219
- package/lib/promptBuilder/systemPrompt.ts +0 -135
- package/lib/promptBuilder/transport.ts +0 -94
- package/lib/promptBuilder/types.ts +0 -109
- package/lib/promptImport/curatedSources.ts +0 -141
- package/lib/promptImport/discoveryRegistry.ts +0 -329
- package/lib/promptImport/errors.ts +0 -18
- package/lib/promptImport/githubDiscovery.ts +0 -309
- package/lib/promptImport/githubFolder.ts +0 -397
- package/lib/promptImport/githubSource.ts +0 -257
- package/lib/promptImport/gptImageHints.ts +0 -70
- package/lib/promptImport/parsePromptCandidates.ts +0 -179
- package/lib/promptImport/promptIndex.ts +0 -326
- package/lib/promptImport/rankPromptCandidates.ts +0 -65
- package/lib/promptImport/types.ts +0 -103
- package/lib/promptSafetyPolicy.ts +0 -5
- package/lib/providerOptions.ts +0 -56
- package/lib/referenceImageCompress.ts +0 -84
- package/lib/refs.ts +0 -133
- package/lib/requestLogger.ts +0 -49
- package/lib/responsesDoctor.ts +0 -456
- package/lib/responsesErrors.ts +0 -83
- package/lib/responsesFallback.ts +0 -114
- package/lib/responsesImageAdapter.ts +0 -466
- package/lib/responsesParse.ts +0 -452
- package/lib/responsesTools.ts +0 -28
- package/lib/runtimeContext.ts +0 -146
- package/lib/runtimePorts.ts +0 -105
- package/lib/sessionStore.ts +0 -308
- package/lib/storageMigration.ts +0 -310
- package/lib/styleSheet.ts +0 -139
- package/lib/systemTrash.ts +0 -20
- package/lib/videoSeriesChain.ts +0 -29
- package/lib/visibleTextLanguagePolicy.ts +0 -7
- package/routes/agent.ts +0 -308
- package/routes/annotations.ts +0 -118
- package/routes/canvasVersions.ts +0 -69
- package/routes/capabilities.ts +0 -18
- package/routes/cardNews.ts +0 -211
- package/routes/comfy.ts +0 -43
- package/routes/edit.ts +0 -352
- package/routes/generate.ts +0 -492
- package/routes/grok.ts +0 -24
- package/routes/health.ts +0 -123
- package/routes/history.ts +0 -221
- package/routes/imageImport.ts +0 -37
- package/routes/index.ts +0 -48
- package/routes/metadata.ts +0 -77
- package/routes/multimode.ts +0 -499
- package/routes/nodes.ts +0 -578
- package/routes/promptBuilder.ts +0 -37
- package/routes/promptImport.ts +0 -379
- package/routes/prompts.ts +0 -428
- package/routes/sessions.ts +0 -317
- package/routes/storage.ts +0 -47
- package/routes/video.ts +0 -232
- package/server.ts +0 -290
- package/ui/dist/assets/SettingsWorkspace-F3eNu3mJ.js +0 -1
- package/ui/dist/assets/index-B6tcw_UF.css +0 -1
- package/ui/dist/assets/index-DYOh6gQD.js +0 -32
- package/vendor/progrok-0.1.1.tgz +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
5
|
+
import { getGrokProxyUrl } from "../lib/grokRuntime.js";
|
|
6
|
+
import { logEvent, logError } from "../lib/logger.js";
|
|
7
|
+
import { downloadVideo, pollVideoUntilDone } from "../lib/grokVideoAdapter.js";
|
|
8
|
+
import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
9
|
+
import { ACTIVE_VIDEO_PROMPT_GUIDANCE, appendVideoContinuityEntry, lineageFromVideoMetadata, readVideoSidecar } from "../lib/videoContinuity.js";
|
|
10
|
+
import { assertLocalMp4, extractVideoFrame, safeGeneratedFilePath } from "../lib/videoFrameExtract.js";
|
|
11
|
+
function videoProxyUrl(ctx, path) {
|
|
12
|
+
return { url: getGrokProxyUrl(ctx, path), headers: { "Content-Type": "application/json", Authorization: "Bearer dummy" } };
|
|
13
|
+
}
|
|
14
|
+
function routeError(message, status = 400) {
|
|
15
|
+
return Object.assign(new Error(message), { status });
|
|
16
|
+
}
|
|
17
|
+
function sendError(res, err) {
|
|
18
|
+
res.status(typeof err?.status === "number" ? err.status : 500).json({ error: err?.message || String(err) });
|
|
19
|
+
}
|
|
20
|
+
async function safeGeneratedFile(ctx, file, options = {}) {
|
|
21
|
+
return safeGeneratedFilePath(ctx.config.storage.generatedDir, file, options);
|
|
22
|
+
}
|
|
23
|
+
function summarizeSource(input) {
|
|
24
|
+
if (input.startsWith("data:video/")) {
|
|
25
|
+
const encoded = input.split(",", 2)[1] || "";
|
|
26
|
+
return { kind: "data-url", approximateBytes: Math.floor(encoded.length * 0.75) };
|
|
27
|
+
}
|
|
28
|
+
if (/^https?:\/\//i.test(input)) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(input);
|
|
31
|
+
return { kind: "url", origin: parsed.origin, pathname: basename(parsed.pathname) };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { kind: "url" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (/^file[-_][A-Za-z0-9._-]+$/.test(input))
|
|
38
|
+
return { kind: "file_id" };
|
|
39
|
+
return { kind: "generated-file", filename: basename(input) };
|
|
40
|
+
}
|
|
41
|
+
async function resolveVideoInput(ctx, input) {
|
|
42
|
+
if (/^https?:\/\//i.test(input) || input.startsWith("data:video/"))
|
|
43
|
+
return { url: input };
|
|
44
|
+
if (/^file[-_][A-Za-z0-9._-]+$/.test(input))
|
|
45
|
+
return { file_id: input };
|
|
46
|
+
const inputPath = await safeGeneratedFile(ctx, input, { requireMp4: true });
|
|
47
|
+
await assertLocalMp4(inputPath);
|
|
48
|
+
const buf = await readFile(inputPath);
|
|
49
|
+
return { url: `data:video/mp4;base64,${buf.toString("base64")}` };
|
|
50
|
+
}
|
|
51
|
+
function validateEditModel(model) {
|
|
52
|
+
if (typeof model !== "string")
|
|
53
|
+
throw routeError("model must be a string", 400);
|
|
54
|
+
if (model !== "grok-imagine-video")
|
|
55
|
+
throw routeError("Video edit/extension only supports grok-imagine-video", 400);
|
|
56
|
+
return model;
|
|
57
|
+
}
|
|
58
|
+
async function saveVideoResult(ctx, options) {
|
|
59
|
+
const { buffer, contentType } = await downloadVideo(ctx, options.videoUrl, options.signal);
|
|
60
|
+
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
61
|
+
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
62
|
+
const filename = `${Date.now()}_${rand}.mp4`;
|
|
63
|
+
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
64
|
+
const sourceFilename = /^https?:\/\//i.test(options.source) || options.source.startsWith("data:") || /^file[-_]/.test(options.source)
|
|
65
|
+
? null
|
|
66
|
+
: basename(options.source);
|
|
67
|
+
const sourceMeta = sourceFilename ? await readVideoSidecar(ctx.config.storage.generatedDir, sourceFilename) : null;
|
|
68
|
+
const parentLineage = sourceFilename ? lineageFromVideoMetadata(sourceFilename, sourceMeta) : null;
|
|
69
|
+
const videoContinuity = appendVideoContinuityEntry(parentLineage, {
|
|
70
|
+
filename,
|
|
71
|
+
userPrompt: options.prompt,
|
|
72
|
+
revisedPrompt: options.prompt,
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
await writeFile(filePath, buffer);
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(`${filePath}.json`, JSON.stringify({
|
|
78
|
+
kind: "video",
|
|
79
|
+
mediaType: "video",
|
|
80
|
+
requestId: options.requestId,
|
|
81
|
+
prompt: options.prompt,
|
|
82
|
+
userPrompt: options.prompt,
|
|
83
|
+
provider: "grok",
|
|
84
|
+
model: options.model,
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
usage: options.usage ?? null,
|
|
87
|
+
revisedPrompt: options.prompt,
|
|
88
|
+
videoContinuity,
|
|
89
|
+
video: {
|
|
90
|
+
operation: options.operation,
|
|
91
|
+
duration: options.duration,
|
|
92
|
+
source: summarizeSource(options.source),
|
|
93
|
+
sourceUrl: summarizeSource(options.videoUrl),
|
|
94
|
+
contentType,
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
await unlink(filePath).catch(() => { });
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
invalidateHistoryIndex();
|
|
103
|
+
return { filename, url: `/generated/${encodeURIComponent(filename)}`, sourceUrl: options.videoUrl };
|
|
104
|
+
}
|
|
105
|
+
function requestSignal(req, res) {
|
|
106
|
+
const ac = new AbortController();
|
|
107
|
+
const abort = () => {
|
|
108
|
+
if (!res.writableEnded)
|
|
109
|
+
ac.abort();
|
|
110
|
+
};
|
|
111
|
+
req.on("aborted", abort);
|
|
112
|
+
res.on("close", abort);
|
|
113
|
+
return ac.signal;
|
|
114
|
+
}
|
|
115
|
+
function requirePrompt(value) {
|
|
116
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
117
|
+
}
|
|
118
|
+
function extractOutputText(data) {
|
|
119
|
+
const output = Array.isArray(data.output) ? data.output : [];
|
|
120
|
+
const texts = [];
|
|
121
|
+
for (const item of output) {
|
|
122
|
+
const content = item?.content;
|
|
123
|
+
if (!Array.isArray(content))
|
|
124
|
+
continue;
|
|
125
|
+
for (const part of content) {
|
|
126
|
+
if (part?.type === "output_text" && typeof part.text === "string")
|
|
127
|
+
texts.push(part.text);
|
|
128
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
129
|
+
texts.push(part.text);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return texts.join("\n").trim();
|
|
133
|
+
}
|
|
134
|
+
export function registerVideoExtendedRoutes(app, ctxRaw) {
|
|
135
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
136
|
+
// --- Video Edit (V2V) ---
|
|
137
|
+
app.post("/api/video/edit", async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const { prompt: rawPrompt, videoUrl, model = "grok-imagine-video" } = req.body ?? {};
|
|
140
|
+
const prompt = requirePrompt(rawPrompt);
|
|
141
|
+
if (!prompt)
|
|
142
|
+
return res.status(400).json({ error: "prompt required", code: "PROMPT_REQUIRED", guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
|
|
143
|
+
if (!videoUrl || typeof videoUrl !== "string")
|
|
144
|
+
return res.status(400).json({ error: "videoUrl required" });
|
|
145
|
+
const validModel = validateEditModel(model);
|
|
146
|
+
const signal = requestSignal(req, res);
|
|
147
|
+
const { url, headers } = videoProxyUrl(ctx, "/v1/videos/edits");
|
|
148
|
+
const video = await resolveVideoInput(ctx, videoUrl);
|
|
149
|
+
const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, video }), signal });
|
|
150
|
+
if (!apiRes.ok) {
|
|
151
|
+
const t = await apiRes.text();
|
|
152
|
+
return res.status(apiRes.status).json({ error: t });
|
|
153
|
+
}
|
|
154
|
+
const { request_id } = (await apiRes.json());
|
|
155
|
+
if (!request_id)
|
|
156
|
+
return res.status(502).json({ error: "No request_id in response" });
|
|
157
|
+
logEvent("video", "edit:start", { requestId: request_id, model: validModel });
|
|
158
|
+
const result = await pollVideoUntilDone(ctx, request_id, { signal });
|
|
159
|
+
if (result.respectModeration === false)
|
|
160
|
+
return res.status(502).json({ error: "Grok video blocked by moderation" });
|
|
161
|
+
if (!result.videoUrl)
|
|
162
|
+
return res.status(502).json({ error: "No video URL in response" });
|
|
163
|
+
const saved = await saveVideoResult(ctx, { requestId: request_id, prompt, model: validModel, operation: "edit", source: videoUrl, duration: result.duration ?? null, videoUrl: result.videoUrl, usage: result.usage, signal });
|
|
164
|
+
logEvent("video", "edit:done", { requestId: request_id });
|
|
165
|
+
res.json({ requestId: request_id, url: saved.url, filename: saved.filename, sourceUrl: saved.sourceUrl, duration: result.duration, model: validModel });
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
logError("video", "edit:error", err);
|
|
169
|
+
sendError(res, err);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// --- Video Extension ---
|
|
173
|
+
app.post("/api/video/extend", async (req, res) => {
|
|
174
|
+
try {
|
|
175
|
+
const { prompt: rawPrompt, videoUrl, duration = 6, model = "grok-imagine-video" } = req.body ?? {};
|
|
176
|
+
const prompt = requirePrompt(rawPrompt);
|
|
177
|
+
if (!prompt)
|
|
178
|
+
return res.status(400).json({ error: "prompt required", code: "PROMPT_REQUIRED", guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
|
|
179
|
+
if (!videoUrl || typeof videoUrl !== "string")
|
|
180
|
+
return res.status(400).json({ error: "videoUrl required" });
|
|
181
|
+
const validModel = validateEditModel(model);
|
|
182
|
+
const dur = Number(duration);
|
|
183
|
+
if (!Number.isInteger(dur) || dur < 2 || dur > 10)
|
|
184
|
+
return res.status(400).json({ error: "duration must be an integer between 2 and 10" });
|
|
185
|
+
const signal = requestSignal(req, res);
|
|
186
|
+
const { url, headers } = videoProxyUrl(ctx, "/v1/videos/extensions");
|
|
187
|
+
const video = await resolveVideoInput(ctx, videoUrl);
|
|
188
|
+
const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, duration: dur, video }), signal });
|
|
189
|
+
if (!apiRes.ok) {
|
|
190
|
+
const t = await apiRes.text();
|
|
191
|
+
return res.status(apiRes.status).json({ error: t });
|
|
192
|
+
}
|
|
193
|
+
const { request_id } = (await apiRes.json());
|
|
194
|
+
if (!request_id)
|
|
195
|
+
return res.status(502).json({ error: "No request_id in response" });
|
|
196
|
+
logEvent("video", "extend:start", { requestId: request_id, model: validModel, duration: dur });
|
|
197
|
+
const result = await pollVideoUntilDone(ctx, request_id, { signal });
|
|
198
|
+
if (result.respectModeration === false)
|
|
199
|
+
return res.status(502).json({ error: "Grok video blocked by moderation" });
|
|
200
|
+
if (!result.videoUrl)
|
|
201
|
+
return res.status(502).json({ error: "No video URL in response" });
|
|
202
|
+
const saved = await saveVideoResult(ctx, { requestId: request_id, prompt, model: validModel, operation: "extend", source: videoUrl, duration: result.duration ?? null, videoUrl: result.videoUrl, usage: result.usage, signal });
|
|
203
|
+
logEvent("video", "extend:done", { requestId: request_id, totalDuration: result.duration });
|
|
204
|
+
res.json({ requestId: request_id, url: saved.url, filename: saved.filename, sourceUrl: saved.sourceUrl, duration: result.duration, model: validModel });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
logError("video", "extend:error", err);
|
|
208
|
+
sendError(res, err);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// --- Video Frame Extraction ---
|
|
212
|
+
app.get("/api/video/frame", async (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
const file = req.query.file;
|
|
215
|
+
const position = req.query.position || "last";
|
|
216
|
+
if (!file)
|
|
217
|
+
return res.status(400).json({ error: "file query param required" });
|
|
218
|
+
const inputPath = await safeGeneratedFile(ctx, file, { requireMp4: true });
|
|
219
|
+
await assertLocalMp4(inputPath);
|
|
220
|
+
const tmpOut = join(ctx.config.storage.generatedDir, `frame_tmp_${randomBytes(4).toString("hex")}.png`);
|
|
221
|
+
try {
|
|
222
|
+
await extractVideoFrame(inputPath, tmpOut, position);
|
|
223
|
+
const frame = await readFile(tmpOut);
|
|
224
|
+
res.type("png").send(frame);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return res.status(500).json({ error: "ffmpeg failed" });
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await unlink(tmpOut).catch(() => { });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
logError("video", "frame:error", err);
|
|
235
|
+
sendError(res, err);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
// --- Video Analysis (Grok 4.3 Vision) ---
|
|
239
|
+
app.post("/api/video/analyze", async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
const { videoUrl } = req.body ?? {};
|
|
242
|
+
if (!videoUrl || typeof videoUrl !== "string")
|
|
243
|
+
return res.status(400).json({ error: "videoUrl required" });
|
|
244
|
+
if (/^https?:\/\//i.test(videoUrl) || videoUrl.startsWith("data:")) {
|
|
245
|
+
return res.status(400).json({ error: "videoUrl must be a generated .mp4 filename" });
|
|
246
|
+
}
|
|
247
|
+
const input = await safeGeneratedFile(ctx, videoUrl, { requireMp4: true });
|
|
248
|
+
await assertLocalMp4(input);
|
|
249
|
+
const firstFrame = join(ctx.config.storage.generatedDir, `analyze_first_${randomBytes(4).toString("hex")}.png`);
|
|
250
|
+
const lastFrame = join(ctx.config.storage.generatedDir, `analyze_last_${randomBytes(4).toString("hex")}.png`);
|
|
251
|
+
try {
|
|
252
|
+
await extractVideoFrame(input, firstFrame, "0");
|
|
253
|
+
await extractVideoFrame(input, lastFrame, "last");
|
|
254
|
+
const first = (await readFile(firstFrame)).toString("base64");
|
|
255
|
+
const last = (await readFile(lastFrame)).toString("base64");
|
|
256
|
+
const { url, headers } = videoProxyUrl(ctx, "/v1/responses");
|
|
257
|
+
const apiRes = await fetch(url, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers,
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
model: "grok-4.3",
|
|
262
|
+
input: [{
|
|
263
|
+
role: "user",
|
|
264
|
+
content: [
|
|
265
|
+
{ type: "input_image", image_url: `data:image/png;base64,${first}`, detail: "high" },
|
|
266
|
+
{ type: "input_image", image_url: `data:image/png;base64,${last}`, detail: "high" },
|
|
267
|
+
{ type: "input_text", text: "Analyze these first and last frames from a video for recreation. Infer likely motion between them. Include shot type, camera movement, lighting, color palette, subjects, motion direction/speed, mood, and audio/sound prompt suggestions. Be specific and cinematic." },
|
|
268
|
+
],
|
|
269
|
+
}],
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
if (!apiRes.ok) {
|
|
273
|
+
const t = await apiRes.text();
|
|
274
|
+
return res.status(apiRes.status).json({ error: t });
|
|
275
|
+
}
|
|
276
|
+
const data = (await apiRes.json());
|
|
277
|
+
const text = extractOutputText(data);
|
|
278
|
+
if (!text)
|
|
279
|
+
return res.status(502).json({ error: "No analysis text in response" });
|
|
280
|
+
logEvent("video", "analyze:done", { videoUrl, chars: text.length });
|
|
281
|
+
res.json({ analysis: text, model: "grok-4.3", method: "first-last-frame" });
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
await unlink(firstFrame).catch(() => { });
|
|
285
|
+
await unlink(lastFrame).catch(() => { });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
logError("video", "analyze:error", err);
|
|
290
|
+
sendError(res, err);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
package/server.js
CHANGED
|
@@ -16,6 +16,10 @@ import { configureApiCachePolicy } from "./lib/apiCachePolicy.js";
|
|
|
16
16
|
import { configureRoutes } from "./routes/index.js";
|
|
17
17
|
import { config } from "./config.js";
|
|
18
18
|
import { getServerPort, listenWithPortFallback } from "./lib/runtimePorts.js";
|
|
19
|
+
import { closeDb } from "./lib/db.js";
|
|
20
|
+
import { stopAgentQueueWorker } from "./lib/agentQueueWorker.js";
|
|
21
|
+
import { reapCardNewsJobs } from "./lib/cardNewsJobStore.js";
|
|
22
|
+
import { reapTerminalJobs } from "./lib/inflight.js";
|
|
19
23
|
import { errInfo } from "./lib/errInfo.js";
|
|
20
24
|
const rootDir = dirname(fileURLToPath(import.meta.url));
|
|
21
25
|
async function loadApiKey() {
|
|
@@ -74,7 +78,11 @@ export function buildApp(ctx) {
|
|
|
74
78
|
app.use("/assets", (_req, res) => {
|
|
75
79
|
res.status(404).type("text/plain").send("Asset not found");
|
|
76
80
|
});
|
|
77
|
-
app.use("/generated",
|
|
81
|
+
app.use("/generated", (req, res, next) => {
|
|
82
|
+
if (req.path.endsWith(".json"))
|
|
83
|
+
return res.status(404).type("text/plain").send("Generated metadata is not public");
|
|
84
|
+
return next();
|
|
85
|
+
}, express.static(ctx.config.storage.generatedDir, {
|
|
78
86
|
maxAge: ctx.config.storage.staticMaxAge,
|
|
79
87
|
immutable: true,
|
|
80
88
|
}));
|
|
@@ -224,7 +232,9 @@ export async function startServer(overrides = {}) {
|
|
|
224
232
|
},
|
|
225
233
|
})
|
|
226
234
|
: null;
|
|
227
|
-
|
|
235
|
+
let server;
|
|
236
|
+
let reapTimer;
|
|
237
|
+
onShutdown(async () => {
|
|
228
238
|
unadvertise(ctx);
|
|
229
239
|
try {
|
|
230
240
|
oauthChild?.stop?.();
|
|
@@ -242,9 +252,18 @@ export async function startServer(overrides = {}) {
|
|
|
242
252
|
grokChild?.kill?.();
|
|
243
253
|
}
|
|
244
254
|
catch { }
|
|
255
|
+
stopAgentQueueWorker();
|
|
256
|
+
clearInterval(reapTimer);
|
|
257
|
+
await new Promise((resolve) => {
|
|
258
|
+
if (server)
|
|
259
|
+
server.close(() => resolve());
|
|
260
|
+
else
|
|
261
|
+
resolve();
|
|
262
|
+
});
|
|
263
|
+
closeDb();
|
|
245
264
|
});
|
|
246
265
|
process.on("exit", () => unadvertise(ctx));
|
|
247
|
-
|
|
266
|
+
server = await listenWithPortFallback(app, ctx.config.server.port, {
|
|
248
267
|
host: ctx.config.server.host,
|
|
249
268
|
label: "server",
|
|
250
269
|
onFallback: ({ requestedPort, actualPort }) => {
|
|
@@ -254,7 +273,7 @@ export async function startServer(overrides = {}) {
|
|
|
254
273
|
ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
|
|
255
274
|
ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
|
|
256
275
|
console.log(`Image Gen running at ${ctx.serverUrl}`);
|
|
257
|
-
console.log(`Provider policy: OAuth, API-key Responses, and Grok Images providers. OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
|
|
276
|
+
console.log(`Provider policy: GPT OAuth, API-key Responses, and Grok Images providers. GPT OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
|
|
258
277
|
advertise(ctx);
|
|
259
278
|
try {
|
|
260
279
|
const s = ensureDefaultSession();
|
|
@@ -268,6 +287,18 @@ export async function startServer(overrides = {}) {
|
|
|
268
287
|
console.error("[server] Failed to start:", err?.message || err);
|
|
269
288
|
process.exit(1);
|
|
270
289
|
});
|
|
290
|
+
reapTimer = setInterval(() => {
|
|
291
|
+
reapTerminalJobs();
|
|
292
|
+
reapCardNewsJobs();
|
|
293
|
+
}, 60_000);
|
|
294
|
+
reapTimer.unref?.();
|
|
295
|
+
process.on("uncaughtException", (err) => {
|
|
296
|
+
console.error("[fatal] uncaughtException:", err);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
});
|
|
299
|
+
process.on("unhandledRejection", (reason) => {
|
|
300
|
+
console.error("[fatal] unhandledRejection:", reason);
|
|
301
|
+
});
|
|
271
302
|
return { app, server, oauthChild, ctx };
|
|
272
303
|
}
|
|
273
304
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|