ima2-gen 2.0.15 → 2.0.16
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 +30 -0
- package/bin/commands/skill.js +336 -17
- package/bin/ima2.js +22 -2
- package/bin/lib/doctor-checks.js +6 -4
- package/config.js +1 -0
- package/docs/CLI.md +25 -3
- package/docs/PROMPT_STUDIO.ko.md +29 -0
- package/docs/PROMPT_STUDIO.md +31 -0
- package/docs/migration/runtime-test-inventory.md +7 -1
- package/lib/agentImageVideoGen.js +8 -0
- package/lib/agentQueueStore.js +36 -10
- package/lib/agentQueueWorker.js +79 -7
- package/lib/agentRuntime.js +2 -0
- package/lib/assetLifecycle.js +40 -8
- package/lib/canvasVersionStore.js +86 -0
- package/lib/cardNewsGenerator.js +37 -3
- package/lib/cardNewsJobStore.js +10 -0
- package/lib/cardNewsManifestStore.js +6 -12
- package/lib/cardNewsPath.js +25 -0
- package/lib/db.js +2 -0
- package/lib/generatePipeline.js +474 -0
- package/lib/generationInputValidation.js +32 -0
- package/lib/grokImageCore.js +18 -1
- package/lib/historyIndex.js +11 -4
- package/lib/historyList.js +3 -0
- package/lib/imageMetadata.js +3 -0
- package/lib/inflight.js +30 -3
- package/lib/multimodePipeline.js +462 -0
- package/lib/nodeGeneration.js +440 -0
- package/lib/nodeValidation.js +23 -0
- package/lib/oauthProxy/generators.js +3 -246
- package/lib/oauthProxy/multimodeGenerators.js +251 -0
- package/lib/routeHelpers.js +9 -2
- package/package.json +2 -2
- package/routes/agent.js +4 -4
- package/routes/canvasVersions.js +21 -1
- package/routes/cardNews.js +2 -1
- package/routes/edit.js +25 -9
- package/routes/events.js +5 -1
- package/routes/generate.js +2 -481
- package/routes/keys.js +31 -39
- package/routes/multimode.js +2 -502
- package/routes/nodes.js +3 -448
- package/routes/video.js +12 -3
- package/routes/videoExtended.js +19 -4
- package/server.js +53 -1
- package/skills/ima2/SKILL.md +204 -0
- package/skills/ima2-front/SKILL.md +611 -0
- package/skills/ima2-front/references/a11y-patterns.md +134 -0
- package/skills/ima2-front/references/aesthetics.md +327 -0
- package/skills/ima2-front/references/anti-slop.md +241 -0
- package/skills/ima2-front/references/asset-requirements.md +431 -0
- package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
- package/skills/ima2-front/references/color-system.md +132 -0
- package/skills/ima2-front/references/consistency-locks.md +20 -0
- package/skills/ima2-front/references/crud-ui.md +53 -0
- package/skills/ima2-front/references/dropdown-layer.md +110 -0
- package/skills/ima2-front/references/i18n-global.md +104 -0
- package/skills/ima2-front/references/iterative-design.md +90 -0
- package/skills/ima2-front/references/korea-2026.md +174 -0
- package/skills/ima2-front/references/layout-discipline.md +161 -0
- package/skills/ima2-front/references/liquid-glass.md +181 -0
- package/skills/ima2-front/references/logo-sections.md +194 -0
- package/skills/ima2-front/references/mobile-ux.md +144 -0
- package/skills/ima2-front/references/performance-budget.md +106 -0
- package/skills/ima2-front/references/preflight-full.md +58 -0
- package/skills/ima2-front/references/product-density.md +53 -0
- package/skills/ima2-front/references/prototype-variants.md +54 -0
- package/skills/ima2-front/references/reference-capture.md +120 -0
- package/skills/ima2-front/references/responsive-viewport.md +139 -0
- package/skills/ima2-front/references/seo-baseline.md +123 -0
- package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
- package/skills/ima2-front/references/stacks/astro.md +204 -0
- package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
- package/skills/ima2-front/references/stacks/nextjs.md +219 -0
- package/skills/ima2-front/references/stacks/react.md +264 -0
- package/skills/ima2-front/references/stacks/svelte.md +235 -0
- package/skills/ima2-front/references/stacks/vanilla.md +226 -0
- package/skills/ima2-front/references/theme-switching.md +82 -0
- package/skills/ima2-front/references/top-bar.md +126 -0
- package/skills/ima2-front/references/typography-wrapping.md +270 -0
- package/skills/ima2-front/references/ux-writing-ko.md +62 -0
- package/skills/ima2-front/references/visual-verification.md +102 -0
- package/skills/ima2-uiux/SKILL.md +477 -0
- package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
- package/skills/ima2-uiux/references/color-system.md +80 -0
- package/skills/ima2-uiux/references/design-isms.md +194 -0
- package/skills/ima2-uiux/references/design-read-example.md +33 -0
- package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
- package/skills/ima2-uiux/references/favicon-logo.md +355 -0
- package/skills/ima2-uiux/references/form-patterns.md +118 -0
- package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
- package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
- package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
- package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
- package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
- package/skills/ima2-uiux/references/product-personalities.md +118 -0
- package/skills/ima2-uiux/references/responsive-nav.md +66 -0
- package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
- package/skills/ima2-uiux/references/ux-preflight.md +65 -0
- package/skills/ima2-uiux/references/ux-states.md +197 -0
- package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
- package/ui/dist/.vite/manifest.json +13 -13
- package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
- package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
- package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
- package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
- package/ui/dist/assets/index-0-_vgFGs.js +4 -0
- package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
- package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
- package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
- package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
- package/ui/dist/assets/index-2oG6in1i.css +0 -1
- package/ui/dist/assets/index-CoxMOXAU.js +0 -23
- package/ui/dist/assets/index-p8egnGHk.js +0 -4
package/routes/edit.js
CHANGED
|
@@ -11,7 +11,7 @@ import { editViaResponses } from "../lib/responsesImageAdapter.js";
|
|
|
11
11
|
import { editViaGrok } from "../lib/grokImageAdapter.js";
|
|
12
12
|
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
13
13
|
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
14
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled } from "../lib/inflight.js";
|
|
14
|
+
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, INFLIGHT_RETRY_AFTER_SECONDS } from "../lib/inflight.js";
|
|
15
15
|
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
16
16
|
import { logEvent, logError } from "../lib/logger.js";
|
|
17
17
|
import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
|
|
@@ -84,6 +84,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
84
84
|
let finishErrorCode;
|
|
85
85
|
let finishMeta = {};
|
|
86
86
|
let finishCanceled = false;
|
|
87
|
+
let jobOwned = false;
|
|
87
88
|
const cancelController = new AbortController();
|
|
88
89
|
try {
|
|
89
90
|
const { prompt, image: imageB64, mask: rawMask, quality: rawQuality = "medium", size = "1024x1024", moderation = "low", provider = "oauth", mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
|
|
@@ -108,7 +109,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
108
109
|
const activeProvider = providerOptions.provider;
|
|
109
110
|
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
110
111
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
111
|
-
startJob({
|
|
112
|
+
const started = startJob({
|
|
112
113
|
requestId,
|
|
113
114
|
kind: "classic",
|
|
114
115
|
prompt,
|
|
@@ -120,6 +121,20 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
120
121
|
size: effectiveSize,
|
|
121
122
|
},
|
|
122
123
|
});
|
|
124
|
+
if (started && isStartJobFailure(started)) {
|
|
125
|
+
const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
126
|
+
if (started.code === "TOO_MANY_JOBS") {
|
|
127
|
+
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
128
|
+
}
|
|
129
|
+
return res.status(status).json({
|
|
130
|
+
error: started.code === "TOO_MANY_JOBS"
|
|
131
|
+
? "Too many concurrent generation jobs"
|
|
132
|
+
: "Request ID already in use",
|
|
133
|
+
code: started.code,
|
|
134
|
+
requestId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
jobOwned = true;
|
|
123
138
|
registerJobAbortController(requestId, cancelController);
|
|
124
139
|
if (!prompt || !imageB64) {
|
|
125
140
|
finishStatus = "error";
|
|
@@ -335,13 +350,14 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
335
350
|
});
|
|
336
351
|
}
|
|
337
352
|
finally {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
353
|
+
if (jobOwned)
|
|
354
|
+
finishJob(requestId, {
|
|
355
|
+
canceled: finishCanceled,
|
|
356
|
+
status: finishStatus,
|
|
357
|
+
httpStatus: finishHttpStatus,
|
|
358
|
+
errorCode: finishErrorCode,
|
|
359
|
+
meta: finishMeta,
|
|
360
|
+
});
|
|
345
361
|
}
|
|
346
362
|
});
|
|
347
363
|
}
|
package/routes/events.js
CHANGED
|
@@ -14,6 +14,10 @@ function safeWrite(res, chunk) {
|
|
|
14
14
|
function formatSse(ev) {
|
|
15
15
|
return `id: ${ev.id}\nevent: ${ev.event}\ndata: ${JSON.stringify({ ...ev.data, jobId: ev.jobId })}\n\n`;
|
|
16
16
|
}
|
|
17
|
+
function parseLastEventIdHeader(value) {
|
|
18
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
19
|
+
return parseInt(raw?.split(",", 1)[0]?.trim() ?? "", 10);
|
|
20
|
+
}
|
|
17
21
|
export function registerEventsRoute(app, _ctx) {
|
|
18
22
|
app.get("/api/events", (req, res) => {
|
|
19
23
|
if (activeConnections >= MAX_SSE_LISTENERS) {
|
|
@@ -27,7 +31,7 @@ export function registerEventsRoute(app, _ctx) {
|
|
|
27
31
|
res.setHeader("X-Accel-Buffering", "no");
|
|
28
32
|
res.flushHeaders?.();
|
|
29
33
|
activeConnections++;
|
|
30
|
-
const headerLastId =
|
|
34
|
+
const headerLastId = parseLastEventIdHeader(req.headers["last-event-id"]);
|
|
31
35
|
const queryLastId = parseInt(String(req.query.lastEventId ?? ""), 10);
|
|
32
36
|
const lastId = !Number.isNaN(headerLastId) ? headerLastId : queryLastId;
|
|
33
37
|
if (!Number.isNaN(lastId)) {
|
package/routes/generate.js
CHANGED
|
@@ -1,485 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { randomBytes } from "crypto";
|
|
5
|
-
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
6
|
-
import { generateImageThumbnailFromBuffer } from "../lib/imageThumb.js";
|
|
7
|
-
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
8
|
-
import { appendGenerationRequestLog } from "../lib/generationRequestLog.js";
|
|
9
|
-
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
10
|
-
import { resolveProviderOptions } from "../lib/providerOptions.js";
|
|
11
|
-
import { generateViaResponses } from "../lib/responsesImageAdapter.js";
|
|
12
|
-
import { generateViaGrok, planGrokImage } from "../lib/grokImageAdapter.js";
|
|
13
|
-
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
14
|
-
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
15
|
-
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
16
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, setJobPhase, INFLIGHT_RETRY_AFTER_SECONDS, } from "../lib/inflight.js";
|
|
17
|
-
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
18
|
-
import { logEvent, logError } from "../lib/logger.js";
|
|
19
|
-
import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
|
|
20
|
-
import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
21
|
-
import { normalizeComposerInsertedPrompts, normalizeComposerPrompt, } from "../lib/composerSnapshot.js";
|
|
22
|
-
import { errInfo } from "../lib/errInfo.js";
|
|
1
|
+
import { runGeneratePipeline } from "../lib/generatePipeline.js";
|
|
23
2
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
24
|
-
import { STORYBOARD_PREFIX } from "../lib/storyboardPrefix.js";
|
|
25
|
-
import { validateModeration, imageFormatFromMime, upstreamErrorFields } from "../lib/routeHelpers.js";
|
|
26
|
-
import { publish } from "../lib/eventBus.js";
|
|
27
|
-
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
28
3
|
export function registerGenerateRoutes(app, ctxRaw) {
|
|
29
4
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
30
|
-
app.post("/api/generate",
|
|
31
|
-
const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
|
|
32
|
-
const asyncMode = req.body?.async === true;
|
|
33
|
-
let finishStatus = "completed";
|
|
34
|
-
let finishHttpStatus;
|
|
35
|
-
let finishErrorCode;
|
|
36
|
-
let finishMeta = {};
|
|
37
|
-
let finishCanceled = false;
|
|
38
|
-
const cancelController = new AbortController();
|
|
39
|
-
const fail = (status, payload) => {
|
|
40
|
-
finishStatus = "error";
|
|
41
|
-
finishHttpStatus = status;
|
|
42
|
-
finishErrorCode = typeof payload.code === "string" ? payload.code : finishErrorCode;
|
|
43
|
-
if (asyncMode && res.headersSent) {
|
|
44
|
-
publish(requestId, "error", { ...payload, status, requestId });
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
return res.status(status).json(payload);
|
|
48
|
-
};
|
|
49
|
-
const succeed = (payload) => {
|
|
50
|
-
if (asyncMode) {
|
|
51
|
-
publishJobEvent(requestId, "done", payload);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
res.json(payload);
|
|
55
|
-
};
|
|
56
|
-
try {
|
|
57
|
-
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
58
|
-
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
59
|
-
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", n = 1, references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
|
|
60
|
-
const storyboardActive = req.body?.storyboard === true;
|
|
61
|
-
const storyboardPrefix = storyboardActive ? STORYBOARD_PREFIX : "";
|
|
62
|
-
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
63
|
-
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
64
|
-
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
65
|
-
const providerOptions = resolveProviderOptions(ctx, {
|
|
66
|
-
provider,
|
|
67
|
-
rawModel,
|
|
68
|
-
rawReasoningEffort,
|
|
69
|
-
rawSize: size,
|
|
70
|
-
rawWebSearchEnabled,
|
|
71
|
-
});
|
|
72
|
-
if (providerOptions.error) {
|
|
73
|
-
return fail(providerOptions.status, { error: providerOptions.error, code: providerOptions.code });
|
|
74
|
-
}
|
|
75
|
-
const imageModel = providerOptions.model;
|
|
76
|
-
const reasoningEffort = providerOptions.reasoningEffort;
|
|
77
|
-
const effectiveSize = providerOptions.size;
|
|
78
|
-
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
79
|
-
const activeProvider = providerOptions.provider;
|
|
80
|
-
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
81
|
-
const generationPrompt = storyboardPrefix + prompt;
|
|
82
|
-
if (!prompt)
|
|
83
|
-
return fail(400, { error: "Prompt is required" });
|
|
84
|
-
const moderationCheck = validateModeration(ctx, moderation);
|
|
85
|
-
if (moderationCheck.error)
|
|
86
|
-
return fail(400, { error: moderationCheck.error });
|
|
87
|
-
const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits.maxGeneratedImages) || 1));
|
|
88
|
-
const count = Math.min(Math.max(parseInt(n) || 1, 1), maxGeneratedImages);
|
|
89
|
-
const referencePayload = summarizeReferencePayload(references);
|
|
90
|
-
const refCheckResult = validateAndNormalizeRefs(references);
|
|
91
|
-
if (refCheckResult.error) {
|
|
92
|
-
return fail(400, { error: refCheckResult.error, code: refCheckResult.code });
|
|
93
|
-
}
|
|
94
|
-
const refCheck = refCheckResult;
|
|
95
|
-
const incomingProviderUrl = typeof req.body?.providerUrl === "string" && req.body.providerUrl.startsWith("http")
|
|
96
|
-
? req.body.providerUrl
|
|
97
|
-
: null;
|
|
98
|
-
const grokRefs = incomingProviderUrl
|
|
99
|
-
? [{ b64: "", url: incomingProviderUrl, declaredMime: "image/png", detectedMime: "image/png" }, ...refCheck.refDetails]
|
|
100
|
-
: refCheck.refDetails;
|
|
101
|
-
const providerRefCount = activeProvider === "grok" || activeProvider === "grok-api"
|
|
102
|
-
? grokRefs.length
|
|
103
|
-
: refCheck.refs.length;
|
|
104
|
-
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && providerRefCount > 3) {
|
|
105
|
-
return fail(400, {
|
|
106
|
-
error: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images`,
|
|
107
|
-
code: activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY",
|
|
108
|
-
requestId,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const started = startJob({
|
|
112
|
-
requestId,
|
|
113
|
-
kind: "classic",
|
|
114
|
-
prompt,
|
|
115
|
-
meta: {
|
|
116
|
-
kind: "classic",
|
|
117
|
-
sessionId,
|
|
118
|
-
parentNodeId: null,
|
|
119
|
-
clientNodeId,
|
|
120
|
-
quality,
|
|
121
|
-
model: imageModel,
|
|
122
|
-
size: effectiveSize,
|
|
123
|
-
n: count,
|
|
124
|
-
refsCount: providerRefCount,
|
|
125
|
-
referenceBytes: referencePayload.referenceBytes,
|
|
126
|
-
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
127
|
-
composerPrompt,
|
|
128
|
-
composerInsertedPrompts,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
if (started && isStartJobFailure(started)) {
|
|
132
|
-
const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
133
|
-
if (started.code === "TOO_MANY_JOBS") {
|
|
134
|
-
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
135
|
-
}
|
|
136
|
-
return fail(status, {
|
|
137
|
-
error: started.code === "TOO_MANY_JOBS"
|
|
138
|
-
? "Too many concurrent generation jobs"
|
|
139
|
-
: "Request ID already in use",
|
|
140
|
-
code: started.code,
|
|
141
|
-
requestId,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
registerJobAbortController(requestId, cancelController);
|
|
145
|
-
if (asyncMode) {
|
|
146
|
-
res.status(202).json({ requestId, async: true });
|
|
147
|
-
}
|
|
148
|
-
setJobPhase(requestId, "streaming");
|
|
149
|
-
if (asyncMode)
|
|
150
|
-
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
151
|
-
const client = req.get("x-ima2-client") || "ui";
|
|
152
|
-
const referenceDiagnostics = refCheck.referenceDiagnostics || [];
|
|
153
|
-
const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
154
|
-
logEvent("generate", "request", {
|
|
155
|
-
requestId,
|
|
156
|
-
client,
|
|
157
|
-
provider: activeProvider,
|
|
158
|
-
quality,
|
|
159
|
-
model: imageModel,
|
|
160
|
-
size: effectiveSize,
|
|
161
|
-
moderation,
|
|
162
|
-
n: count,
|
|
163
|
-
refs: providerRefCount,
|
|
164
|
-
referenceBytes: referencePayload.referenceBytes,
|
|
165
|
-
referenceMismatchCount,
|
|
166
|
-
refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
|
|
167
|
-
refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
|
|
168
|
-
sessionId,
|
|
169
|
-
clientNodeId,
|
|
170
|
-
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
171
|
-
promptMode: normalizedPromptMode,
|
|
172
|
-
webSearchEnabled,
|
|
173
|
-
});
|
|
174
|
-
const startTime = Date.now();
|
|
175
|
-
const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
176
|
-
const effectiveFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : String(format);
|
|
177
|
-
const mime = mimeMap[effectiveFormat] || "image/png";
|
|
178
|
-
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
179
|
-
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
180
|
-
const sharedGrokPlan = activeProvider === "grok" || activeProvider === "grok-api"
|
|
181
|
-
? await planGrokImage(generationPrompt, ctx, {
|
|
182
|
-
model: quality === "high" ? "grok-imagine-image-quality" : imageModel,
|
|
183
|
-
size: effectiveSize,
|
|
184
|
-
signal: cancelController.signal,
|
|
185
|
-
requestId,
|
|
186
|
-
referenceCount: grokRefs.length,
|
|
187
|
-
references: grokRefs,
|
|
188
|
-
directApiKey: grokDirectApiKey,
|
|
189
|
-
})
|
|
190
|
-
: null;
|
|
191
|
-
const generateOne = async () => {
|
|
192
|
-
if (activeProvider === "gemini-api") {
|
|
193
|
-
const r = await generateViaGeminiApi(generationPrompt, requireRuntimeContext(ctx), {
|
|
194
|
-
model: imageModel,
|
|
195
|
-
size: effectiveSize,
|
|
196
|
-
signal: cancelController.signal,
|
|
197
|
-
requestId,
|
|
198
|
-
references: refCheck.refDetails,
|
|
199
|
-
});
|
|
200
|
-
throwIfJobCanceled(requestId);
|
|
201
|
-
return r;
|
|
202
|
-
}
|
|
203
|
-
if (activeProvider === "agy") {
|
|
204
|
-
const r = await generateViaAgy(generationPrompt, {
|
|
205
|
-
references: refCheck.refDetails,
|
|
206
|
-
signal: cancelController.signal,
|
|
207
|
-
requestId,
|
|
208
|
-
});
|
|
209
|
-
throwIfJobCanceled(requestId);
|
|
210
|
-
return r;
|
|
211
|
-
}
|
|
212
|
-
if (activeProvider === "grok" || activeProvider === "grok-api") {
|
|
213
|
-
const grokModel = quality === "high" ? "grok-imagine-image-quality" : imageModel;
|
|
214
|
-
const r = await generateViaGrok(generationPrompt, ctx, {
|
|
215
|
-
model: grokModel,
|
|
216
|
-
size: effectiveSize,
|
|
217
|
-
signal: cancelController.signal,
|
|
218
|
-
requestId,
|
|
219
|
-
plannedPrompt: sharedGrokPlan?.prompt,
|
|
220
|
-
webSearchCalls: sharedGrokPlan?.webSearchCalls,
|
|
221
|
-
references: grokRefs,
|
|
222
|
-
directApiKey: grokDirectApiKey,
|
|
223
|
-
});
|
|
224
|
-
throwIfJobCanceled(requestId);
|
|
225
|
-
return r;
|
|
226
|
-
}
|
|
227
|
-
const MAX_RETRIES = 1;
|
|
228
|
-
let lastErr;
|
|
229
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
230
|
-
try {
|
|
231
|
-
const r = await generateViaResponses(activeProvider, generationPrompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
|
|
232
|
-
model: imageModel,
|
|
233
|
-
reasoningEffort,
|
|
234
|
-
webSearchEnabled,
|
|
235
|
-
signal: cancelController.signal,
|
|
236
|
-
allowPromptOnlyOAuthFallback: activeProvider !== "api",
|
|
237
|
-
});
|
|
238
|
-
throwIfJobCanceled(requestId);
|
|
239
|
-
if (r.b64)
|
|
240
|
-
return r;
|
|
241
|
-
lastErr = new Error("Empty response (safety refusal)");
|
|
242
|
-
}
|
|
243
|
-
catch (e) {
|
|
244
|
-
lastErr = e;
|
|
245
|
-
if (isNonRetryableGenerationError(e))
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
if (attempt < MAX_RETRIES) {
|
|
249
|
-
const errCode = (lastErr && typeof lastErr === "object" && "code" in lastErr)
|
|
250
|
-
? lastErr.code
|
|
251
|
-
: undefined;
|
|
252
|
-
logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: errCode });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
throw normalizeGenerationFailure(lastErr, {
|
|
256
|
-
safetyMessage: "Content generation refused after retries",
|
|
257
|
-
});
|
|
258
|
-
};
|
|
259
|
-
const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
|
|
260
|
-
throwIfJobCanceled(requestId);
|
|
261
|
-
const images = [];
|
|
262
|
-
let totalUsage = null;
|
|
263
|
-
let totalWebSearchCalls = 0;
|
|
264
|
-
let firstRetryMeta = null;
|
|
265
|
-
for (const r of results) {
|
|
266
|
-
if (r.status === "fulfilled" && r.value.b64) {
|
|
267
|
-
throwIfJobCanceled(requestId);
|
|
268
|
-
const valueWithMime = r.value;
|
|
269
|
-
const resultMime = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api"
|
|
270
|
-
? (valueWithMime.mime || detectImageMimeFromB64(r.value.b64) || mime)
|
|
271
|
-
: mime;
|
|
272
|
-
const resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? imageFormatFromMime(resultMime) : effectiveFormat;
|
|
273
|
-
const retryValue = r.value;
|
|
274
|
-
if (!firstRetryMeta && retryValue.retryKind) {
|
|
275
|
-
firstRetryMeta = {
|
|
276
|
-
retryKind: retryValue.retryKind,
|
|
277
|
-
initialEventCount: retryValue.initialEventCount ?? null,
|
|
278
|
-
initialEventTypes: retryValue.initialEventTypes || null,
|
|
279
|
-
referencesDroppedOnRetry: retryValue.referencesDroppedOnRetry ?? null,
|
|
280
|
-
developerPromptDroppedOnRetry: retryValue.developerPromptDroppedOnRetry ?? null,
|
|
281
|
-
webSearchDroppedOnRetry: retryValue.webSearchDroppedOnRetry ?? null,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
285
|
-
const filename = `${Date.now()}_${rand}_${images.length}.${resultFormat}`;
|
|
286
|
-
const createdAt = Date.now();
|
|
287
|
-
const valueWithProviderUrl = r.value;
|
|
288
|
-
const providerUrl = typeof valueWithProviderUrl.providerUrl === "string"
|
|
289
|
-
? valueWithProviderUrl.providerUrl
|
|
290
|
-
: undefined;
|
|
291
|
-
const meta = {
|
|
292
|
-
kind: "classic",
|
|
293
|
-
requestId,
|
|
294
|
-
sessionId,
|
|
295
|
-
clientNodeId,
|
|
296
|
-
prompt,
|
|
297
|
-
userPrompt: prompt,
|
|
298
|
-
revisedPrompt: r.value.revisedPrompt || null,
|
|
299
|
-
promptMode: normalizedPromptMode,
|
|
300
|
-
composerPrompt,
|
|
301
|
-
composerInsertedPrompts,
|
|
302
|
-
quality,
|
|
303
|
-
size: effectiveSize,
|
|
304
|
-
format: resultFormat,
|
|
305
|
-
moderation,
|
|
306
|
-
model: activeProvider === "grok" ? (quality === "high" ? "grok-imagine-image-quality" : imageModel) : imageModel,
|
|
307
|
-
reasoningEffort,
|
|
308
|
-
provider: activeProvider,
|
|
309
|
-
createdAt,
|
|
310
|
-
...(providerUrl ? { providerUrl } : {}),
|
|
311
|
-
usage: r.value.usage || null,
|
|
312
|
-
webSearchCalls: r.value.webSearchCalls || 0,
|
|
313
|
-
webSearchEnabled,
|
|
314
|
-
refsCount: providerRefCount,
|
|
315
|
-
};
|
|
316
|
-
const rawBuffer = Buffer.from(r.value.b64, "base64");
|
|
317
|
-
const embedded = await embedImageMetadataBestEffort(rawBuffer, resultFormat, meta, {
|
|
318
|
-
version: ctx.packageVersion,
|
|
319
|
-
});
|
|
320
|
-
if (!embedded.embedded) {
|
|
321
|
-
logEvent("generate", "metadata_embed_skipped", {
|
|
322
|
-
requestId,
|
|
323
|
-
filename,
|
|
324
|
-
code: embedded.code,
|
|
325
|
-
warning: embedded.warning,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
329
|
-
await writeFile(filePath, embedded.buffer);
|
|
330
|
-
await safeWriteSidecar(filePath + ".json", meta);
|
|
331
|
-
generateImageThumbnailFromBuffer(embedded.buffer, filePath).catch(() => { });
|
|
332
|
-
invalidateHistoryIndex();
|
|
333
|
-
images.push({
|
|
334
|
-
image: `data:${resultMime};base64,${r.value.b64}`,
|
|
335
|
-
filename,
|
|
336
|
-
revisedPrompt: r.value.revisedPrompt || null,
|
|
337
|
-
...(providerUrl ? { providerUrl } : {}),
|
|
338
|
-
createdAt,
|
|
339
|
-
});
|
|
340
|
-
if (r.value.usage) {
|
|
341
|
-
const usageValue = r.value.usage;
|
|
342
|
-
if (!totalUsage)
|
|
343
|
-
totalUsage = { ...usageValue };
|
|
344
|
-
else {
|
|
345
|
-
const tu = totalUsage;
|
|
346
|
-
Object.keys(usageValue).forEach((k) => {
|
|
347
|
-
if (typeof usageValue[k] === "number")
|
|
348
|
-
tu[k] = (tu[k] || 0) + usageValue[k];
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (typeof r.value.webSearchCalls === "number") {
|
|
353
|
-
totalWebSearchCalls = activeProvider === "grok" || activeProvider === "grok-api"
|
|
354
|
-
? Math.max(totalWebSearchCalls, r.value.webSearchCalls)
|
|
355
|
-
: totalWebSearchCalls + r.value.webSearchCalls;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
else if (r.status === "rejected") {
|
|
359
|
-
logError("generate", "parallel_failed", r.reason, { requestId });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (images.length === 0) {
|
|
363
|
-
const firstErr = results.find((r) => r.status === "rejected")?.reason;
|
|
364
|
-
if (firstErr?.code) {
|
|
365
|
-
const status = firstErr.status || 500;
|
|
366
|
-
if (isGenerationCanceledError(firstErr)) {
|
|
367
|
-
finishCanceled = true;
|
|
368
|
-
return fail(firstErr.status, {
|
|
369
|
-
error: firstErr.message,
|
|
370
|
-
code: firstErr.code,
|
|
371
|
-
requestId,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return fail(status, {
|
|
375
|
-
error: firstErr.message,
|
|
376
|
-
code: firstErr.code,
|
|
377
|
-
...upstreamErrorFields(firstErr),
|
|
378
|
-
requestId,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
return fail(500, { error: "All generation attempts failed", code: "GENERATE_ALL_FAILED", requestId });
|
|
382
|
-
}
|
|
383
|
-
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
384
|
-
// Persist elapsed (computed after the generation loop) into each image's sidecar.
|
|
385
|
-
// forward-fix: only newly generated items get elapsed. The embedded XMP is written
|
|
386
|
-
// earlier in the loop (before elapsed exists), so history reload relies on this sidecar patch.
|
|
387
|
-
await Promise.all(images.map(async ({ filename }) => {
|
|
388
|
-
try {
|
|
389
|
-
const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
|
|
390
|
-
const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
|
|
391
|
-
sidecarMeta.elapsed = elapsed;
|
|
392
|
-
await atomicWriteJson(sidecarPath, sidecarMeta);
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
/* best-effort elapsed patch */
|
|
396
|
-
}
|
|
397
|
-
}));
|
|
398
|
-
const firstRevised = images[0]?.revisedPrompt || null;
|
|
399
|
-
const extra = {
|
|
400
|
-
usage: totalUsage,
|
|
401
|
-
provider: activeProvider,
|
|
402
|
-
reasoningEffort,
|
|
403
|
-
webSearchCalls: totalWebSearchCalls,
|
|
404
|
-
quality,
|
|
405
|
-
size: effectiveSize,
|
|
406
|
-
moderation,
|
|
407
|
-
model: imageModel,
|
|
408
|
-
warnings: qualityWarnings,
|
|
409
|
-
revisedPrompt: firstRevised,
|
|
410
|
-
promptMode: normalizedPromptMode,
|
|
411
|
-
webSearchEnabled,
|
|
412
|
-
...(firstRetryMeta || {}),
|
|
413
|
-
};
|
|
414
|
-
if (count === 1) {
|
|
415
|
-
finishHttpStatus = 200;
|
|
416
|
-
finishMeta = { filenames: [images[0].filename], imageCount: 1 };
|
|
417
|
-
logEvent("generate", "saved", {
|
|
418
|
-
requestId,
|
|
419
|
-
imageCount: 1,
|
|
420
|
-
elapsedMs: Date.now() - startTime,
|
|
421
|
-
filename: images[0].filename,
|
|
422
|
-
});
|
|
423
|
-
succeed({
|
|
424
|
-
image: images[0].image,
|
|
425
|
-
elapsed,
|
|
426
|
-
filename: images[0].filename,
|
|
427
|
-
requestId,
|
|
428
|
-
providerUrl: images[0].providerUrl ?? null,
|
|
429
|
-
createdAt: images[0].createdAt,
|
|
430
|
-
...extra,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
finishHttpStatus = 200;
|
|
435
|
-
finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
|
|
436
|
-
logEvent("generate", "saved", {
|
|
437
|
-
requestId,
|
|
438
|
-
imageCount: images.length,
|
|
439
|
-
elapsedMs: Date.now() - startTime,
|
|
440
|
-
});
|
|
441
|
-
succeed({ images, elapsed, count: images.length, requestId, ...extra });
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
catch (e) {
|
|
445
|
-
const err = errInfo(e);
|
|
446
|
-
const ext = (err.raw && typeof err.raw === "object" ? err.raw : {});
|
|
447
|
-
const fallbackCode = err.code || classifyUpstreamError(err.message);
|
|
448
|
-
if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
|
|
449
|
-
const canceled = makeGenerationCanceledError();
|
|
450
|
-
finishCanceled = true;
|
|
451
|
-
return fail(canceled.status, {
|
|
452
|
-
error: canceled.message,
|
|
453
|
-
code: canceled.code,
|
|
454
|
-
requestId,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
finishErrorCode = fallbackCode || "GENERATE_FAILED";
|
|
458
|
-
logError("generate", "error", err.raw, { requestId, code: finishErrorCode });
|
|
459
|
-
fail(err.status || 500, {
|
|
460
|
-
error: err.message,
|
|
461
|
-
code: finishErrorCode,
|
|
462
|
-
...upstreamErrorFields(ext),
|
|
463
|
-
requestId,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
finally {
|
|
467
|
-
finishJob(requestId, {
|
|
468
|
-
canceled: finishCanceled,
|
|
469
|
-
status: finishStatus,
|
|
470
|
-
httpStatus: finishHttpStatus,
|
|
471
|
-
errorCode: finishErrorCode,
|
|
472
|
-
meta: finishMeta,
|
|
473
|
-
});
|
|
474
|
-
appendGenerationRequestLog(ctx.config.storage.generationRequestLogFile, {
|
|
475
|
-
id: randomBytes(8).toString("hex"),
|
|
476
|
-
requestId,
|
|
477
|
-
createdAt: Date.now(),
|
|
478
|
-
prompt: typeof req.body?.prompt === "string" ? req.body.prompt : "",
|
|
479
|
-
requested: parseInt(req.body?.n) || 1,
|
|
480
|
-
succeeded: finishStatus === "completed" ? (finishMeta.imageCount ?? 1) : 0,
|
|
481
|
-
error: finishStatus === "error" ? (finishErrorCode ?? "unknown") : null,
|
|
482
|
-
}).catch(() => { });
|
|
483
|
-
}
|
|
484
|
-
});
|
|
5
|
+
app.post("/api/generate", (req, res) => runGeneratePipeline(req, res, ctx));
|
|
485
6
|
}
|