ima2-gen 3.8.2 → 3.9.0
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/bin/commands/gen.js +10 -0
- package/docs/migration/runtime-test-inventory.md +5 -1
- package/lib/alphaDecode.js +18 -0
- package/lib/atlasCloudImageAdapter.js +6 -1
- package/lib/backgroundPresets.js +28 -1
- package/lib/generatePipeline.js +74 -5
- package/lib/imageBackgroundParam.js +108 -0
- package/lib/promptImport/gptImageHints.js +4 -1
- package/lib/responsesFallback.js +8 -2
- package/lib/responsesImageAdapter.js +10 -1
- package/package.json +2 -2
- package/routes/video.js +7 -0
- package/skills/ima2/SKILL.md +28 -4
- package/skills/ima2-front/references/asset-requirements.md +23 -8
- package/skills/ima2-uiux/SKILL.md +15 -4
- package/ui/dist/.vite/manifest.json +32 -32
- package/ui/dist/assets/{AgentWorkspace-bUd5NsQ_.js → AgentWorkspace-fcIXiWV6.js} +1 -1
- package/ui/dist/assets/AssetGenWorkspace-CqXyN9at.js +2 -0
- package/ui/dist/assets/{AssetsWorkspace-DO_wfFQB.js → AssetsWorkspace-xwHFVOb7.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-D3M0w8pg.js → CardNewsWorkspace-ltdjG40R.js} +1 -1
- package/ui/dist/assets/{GenerationRequestLogPanel-WvSV0yXP.js → GenerationRequestLogPanel-B68EUfUg.js} +1 -1
- package/ui/dist/assets/{HomeWorkspace-BGCBf9iK.js → HomeWorkspace-BBOG_aNm.js} +1 -1
- package/ui/dist/assets/KeyingPanel-Bv0n_bhk.js +1 -0
- package/ui/dist/assets/{NodeCanvas-DwbR2nol.js → NodeCanvas-VwdctvAW.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-KSH3ChjC.js → PromptBuilderPanel-BBMpF_3Q.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-CjHiCEo9.js → PromptImportDialog-D4QYbRfH.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-CWU-vKyM.js → PromptImportDiscoverySection-Bx7BeTFh.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-_zRnJGkJ.js → PromptImportFolderSection-Bwf60URx.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-64VN4nml.js → PromptLibraryPanel-mNG2wZkQ.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-DZMNEpuq.js → SettingsWorkspace-DA5tIa0Q.js} +1 -1
- package/ui/dist/assets/{SpriteRecipeWorkspace-Y6GqpVHs.js → SpriteRecipeWorkspace-Da9qTQX0.js} +1 -1
- package/ui/dist/assets/{index-CFixfKaC.js → index-Bv1Yrz8W.js} +3 -3
- package/ui/dist/assets/{index-waPOg2eb.css → index-C0nfanMr.css} +1 -1
- package/ui/dist/assets/{index-XXtoj9ME.js → index-CxEuSZMC.js} +16 -16
- package/ui/dist/assets/{pptxgen.es-hCJot2gG.js → pptxgen.es-BE4CSd5F.js} +1 -1
- package/ui/dist/assets/{useAgentDialogFocus-PWmMjerz.js → useAgentDialogFocus-R64NV-ZZ.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AssetGenWorkspace-8fK-K64E.js +0 -2
- package/ui/dist/assets/KeyingPanel-CEBLIhhu.js +0 -1
package/bin/commands/gen.js
CHANGED
|
@@ -14,6 +14,7 @@ import { out, die, dieWithError, color, err, fail, json } from "../lib/output.js
|
|
|
14
14
|
import { createCliRequestId, recoverGeneratedOutputs, formatRecoveryHint } from "../lib/recover-output.js";
|
|
15
15
|
import { deriveProviderIds } from "../../lib/providers/derive.js";
|
|
16
16
|
import { listProviders } from "../../lib/mcp/providerRegistry.js";
|
|
17
|
+
import { BACKGROUND_PRESETS } from "../../lib/backgroundPresets.js";
|
|
17
18
|
const VALID_MODES = new Set(["auto", "direct"]);
|
|
18
19
|
const VALID_MODERATION = new Set(["auto", "low"]);
|
|
19
20
|
const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
@@ -71,6 +72,9 @@ const HELP = `
|
|
|
71
72
|
'auto' was removed; choose a lane explicitly
|
|
72
73
|
--mode <auto|direct> Core lanes only. Default: auto
|
|
73
74
|
--moderation <auto|low> Core lanes only. Default: low
|
|
75
|
+
--bg <chroma-green|white|black|transparent>
|
|
76
|
+
Core lanes only. 'transparent' asks GPT Image 2
|
|
77
|
+
for a real alpha channel (saved as PNG)
|
|
74
78
|
--session <id> Core lanes only
|
|
75
79
|
--reasoning-effort <none|low|medium|high|xhigh|max>
|
|
76
80
|
Core lanes only
|
|
@@ -80,6 +84,7 @@ const HELP = `
|
|
|
80
84
|
ima2 defaults set image oauth/gpt-5.6-luna
|
|
81
85
|
ima2 gen "a shiba in space"
|
|
82
86
|
ima2 gen "poster" --model oauth/luna --mode direct
|
|
87
|
+
ima2 gen "fox logo mark" --bg transparent -o logo.png
|
|
83
88
|
ima2 gen "campaign still" --model runway/gen-4 --ref 1780000000000_abcd.png
|
|
84
89
|
`;
|
|
85
90
|
function failServer(jsonMode, error) {
|
|
@@ -216,6 +221,11 @@ function validateCoreFlags(args) {
|
|
|
216
221
|
die(2, "--mode must be one of: auto, direct");
|
|
217
222
|
if (!VALID_MODERATION.has(String(args.moderation)))
|
|
218
223
|
die(2, "--moderation must be one of: auto, low");
|
|
224
|
+
// Fail locally on a typo instead of spending a round trip to learn the
|
|
225
|
+
// server rejected it.
|
|
226
|
+
if (args.bg && !BACKGROUND_PRESETS.includes(String(args.bg))) {
|
|
227
|
+
die(2, `--bg must be one of: ${BACKGROUND_PRESETS.join(", ")}`);
|
|
228
|
+
}
|
|
219
229
|
const validReasoning = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
220
230
|
if (args["reasoning-effort"] && !validReasoning.has(String(args["reasoning-effort"])))
|
|
221
231
|
die(2, "--reasoning-effort must be one of: none, low, medium, high, xhigh, max");
|
|
@@ -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: 376 (runtime: 172, contract: 204)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-mode-auto-planner-contract.test.ts`
|
|
@@ -162,6 +162,9 @@ Total: 372 (runtime: 169, contract: 203)
|
|
|
162
162
|
- `tests/style-sheet.test.ts`
|
|
163
163
|
- `tests/terminal-jobs-restart.test.ts`
|
|
164
164
|
- `tests/thumb-backfill.test.ts`
|
|
165
|
+
- `tests/transparent-alpha-verification.test.ts`
|
|
166
|
+
- `tests/transparent-background-contract.test.ts`
|
|
167
|
+
- `tests/transparent-background-route.test.ts`
|
|
165
168
|
- `tests/video-chroma-key.test.ts`
|
|
166
169
|
- `tests/video-concat.test.ts`
|
|
167
170
|
- `tests/video-history-item.test.ts`
|
|
@@ -370,6 +373,7 @@ Total: 372 (runtime: 169, contract: 203)
|
|
|
370
373
|
- `tests/subprocess-deadline-contract.test.ts`
|
|
371
374
|
- `tests/toast-stack-contract.test.js`
|
|
372
375
|
- `tests/tools-cli-contract.test.ts`
|
|
376
|
+
- `tests/transparent-background-ui-contract.test.js`
|
|
373
377
|
- `tests/ui-error-code-contract.test.js`
|
|
374
378
|
- `tests/ui-glyph-policy.test.ts`
|
|
375
379
|
- `tests/video-continuity-ui-contract.test.js`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sharp-backed raw decoder for alpha verification.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module so lib/imageBackgroundParam.ts stays pure and
|
|
5
|
+
* synchronously testable: the verifier takes the decoder as a parameter, and
|
|
6
|
+
* tests can drive decode failures without stubbing sharp itself.
|
|
7
|
+
*/
|
|
8
|
+
import sharp from "sharp";
|
|
9
|
+
export async function decodeRawForAlpha(buffer) {
|
|
10
|
+
const image = sharp(buffer);
|
|
11
|
+
const meta = await image.metadata();
|
|
12
|
+
if (!meta.hasAlpha) {
|
|
13
|
+
// No alpha channel at all: skip the raw decode, the answer is already known.
|
|
14
|
+
return { data: Buffer.alloc(0), channels: 0, hasAlpha: false };
|
|
15
|
+
}
|
|
16
|
+
const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
17
|
+
return { data, channels: info.channels, hasAlpha: true };
|
|
18
|
+
}
|
|
@@ -166,7 +166,10 @@ export async function generateViaAtlasCloud(prompt, ctx, options = {}) {
|
|
|
166
166
|
prompt,
|
|
167
167
|
size: options.size || "1024x1024",
|
|
168
168
|
quality: options.quality || "medium",
|
|
169
|
-
|
|
169
|
+
// JPEG cannot carry an alpha channel, so a transparent request must never
|
|
170
|
+
// fall back to it. gpt-image-2 accepts png/webp with transparency.
|
|
171
|
+
output_format: options.outputFormat || (options.background === "transparent" ? "png" : "jpeg"),
|
|
172
|
+
...(options.background ? { background: options.background } : {}),
|
|
170
173
|
enable_base64_output: false,
|
|
171
174
|
enable_sync_mode: false,
|
|
172
175
|
};
|
|
@@ -177,6 +180,8 @@ export async function generateViaAtlasCloud(prompt, ctx, options = {}) {
|
|
|
177
180
|
model,
|
|
178
181
|
size: body.size,
|
|
179
182
|
refs: imageUrls.length,
|
|
183
|
+
background: options.background ?? null,
|
|
184
|
+
outputFormat: body.output_format,
|
|
180
185
|
});
|
|
181
186
|
const predictionId = await submitGeneration(apiKey, body, options.signal);
|
|
182
187
|
const output = await pollOutputUrl(apiKey, predictionId, options.signal);
|
package/lib/backgroundPresets.js
CHANGED
|
@@ -4,8 +4,31 @@
|
|
|
4
4
|
* The prompt suffix keeps generated backgrounds uniform enough for a
|
|
5
5
|
* deterministic color key. Prompt assembly is server-owned so the UI, CLI,
|
|
6
6
|
* and integrations share one contract (devlog/_plan/260715_asset_gen_mode/020).
|
|
7
|
+
*
|
|
8
|
+
* "transparent" is a different KIND of preset: the other three ask the model
|
|
9
|
+
* for a uniform matte that a later color-key pass removes, while transparent
|
|
10
|
+
* asks for a real alpha channel up front. GPT-Image-2 can do this now
|
|
11
|
+
* (devlog/_plan/260821_gpt_image2_transparent_background), so the transparent
|
|
12
|
+
* preset drives BOTH a prompt suffix and the image_generation `background`
|
|
13
|
+
* parameter. It is deliberately NOT color-keyable: there is no matte to key.
|
|
14
|
+
*/
|
|
15
|
+
export const BACKGROUND_PRESETS = ["chroma-green", "white", "black", "transparent"];
|
|
16
|
+
/**
|
|
17
|
+
* Presets that produce a solid matte for downstream color keying. Transparent
|
|
18
|
+
* is excluded because it already carries alpha; keying it would be a no-op at
|
|
19
|
+
* best and would eat anti-aliased edges at worst.
|
|
20
|
+
*/
|
|
21
|
+
export const COLOR_KEYABLE_PRESETS = ["chroma-green", "white", "black"];
|
|
22
|
+
export function isColorKeyablePreset(preset) {
|
|
23
|
+
return COLOR_KEYABLE_PRESETS.includes(preset);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Alpha-bearing presets cannot be encoded as JPEG. Callers that pick an output
|
|
27
|
+
* format must consult this before defaulting to a lossy opaque format.
|
|
7
28
|
*/
|
|
8
|
-
export
|
|
29
|
+
export function presetRequiresAlpha(preset) {
|
|
30
|
+
return preset === "transparent";
|
|
31
|
+
}
|
|
9
32
|
export function parseBackgroundPreset(raw) {
|
|
10
33
|
if (raw === undefined || raw === null || raw === "")
|
|
11
34
|
return { preset: null };
|
|
@@ -21,6 +44,7 @@ const SUFFIX_BY_PRESET = {
|
|
|
21
44
|
"chroma-green": "The entire background must be a completely uniform solid chroma key green, perfectly flat like a professional green screen, with even studio lighting and no shadows, gradients, or texture on the background. The subject must have absolutely no green color cast, no green rim lighting, no green reflections, and no green spill from the background.",
|
|
22
45
|
white: "The entire background must be a pure seamless white studio background, perfectly uniform, with even lighting and no shadows, gradients, or texture on the background.",
|
|
23
46
|
black: "The entire background must be a pure seamless black studio background, perfectly uniform, with even lighting and no gradients or texture on the background.",
|
|
47
|
+
transparent: "The background must be fully transparent with a real alpha channel: an isolated cutout of the subject with no backdrop, no backdrop color, no ground plane, no drop shadow, and no checkerboard pattern drawn into the image. Keep edges cleanly anti-aliased against transparency, and preserve genuine partial transparency in glass, smoke, hair, and other translucent areas.",
|
|
24
48
|
};
|
|
25
49
|
export function backgroundPromptSuffix(preset, kind) {
|
|
26
50
|
const base = SUFFIX_BY_PRESET[preset];
|
|
@@ -29,6 +53,9 @@ export function backgroundPromptSuffix(preset, kind) {
|
|
|
29
53
|
: base;
|
|
30
54
|
}
|
|
31
55
|
export function backgroundPlannerConstraint(preset) {
|
|
56
|
+
if (preset === "transparent") {
|
|
57
|
+
return "Hard constraint: the final prompt MUST explicitly require a fully transparent background with a real alpha channel — an isolated cutout with no backdrop, no ground plane, and no drop shadow. Never drop, weaken, or reinterpret this requirement, and never substitute a solid color background for transparency.";
|
|
58
|
+
}
|
|
32
59
|
const color = preset === "chroma-green" ? "chroma key green" : preset;
|
|
33
60
|
return `Hard constraint: the final prompt MUST explicitly require a completely uniform solid ${color} background with even lighting and no shadows, gradients, or texture on the background.${preset === "chroma-green" ? " The subject must have no green color cast, green rim lighting, green reflections, or green spill from the background." : ""} Never drop, weaken, or reinterpret this requirement.`;
|
|
34
61
|
}
|
package/lib/generatePipeline.js
CHANGED
|
@@ -27,6 +27,8 @@ import { errInfo } from "./errInfo.js";
|
|
|
27
27
|
import { requireRuntimeContext } from "./runtimeContext.js";
|
|
28
28
|
import { STORYBOARD_PREFIX } from "./storyboardPrefix.js";
|
|
29
29
|
import { parseBackgroundPreset, backgroundPromptSuffix, backgroundPlannerConstraint } from "./backgroundPresets.js";
|
|
30
|
+
import { resolveImageBackgroundParams, validateTransparentFormat, validateTransparentProvider, verifyBufferAlpha, makeTransparentResultError } from "./imageBackgroundParam.js";
|
|
31
|
+
import { decodeRawForAlpha } from "./alphaDecode.js";
|
|
30
32
|
import { validateModeration, imageFormatFromMime, upstreamErrorFields } from "./routeHelpers.js";
|
|
31
33
|
import { publish } from "./eventBus.js";
|
|
32
34
|
import { publishJobEvent } from "./ssePublish.js";
|
|
@@ -135,6 +137,15 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
135
137
|
return fail(400, { error: backgroundParse.error, code: backgroundParse.code });
|
|
136
138
|
}
|
|
137
139
|
const backgroundPreset = backgroundParse.preset;
|
|
140
|
+
// `format` is the canonical request field (default "png"). Validating
|
|
141
|
+
// req.body.outputFormat instead would let format:"jpeg" slip past and
|
|
142
|
+
// then get transcoded to JPEG on save, destroying the alpha channel.
|
|
143
|
+
const formatConflict = validateTransparentFormat(backgroundPreset, format);
|
|
144
|
+
if (formatConflict) {
|
|
145
|
+
return fail(400, { error: formatConflict.error, code: formatConflict.code });
|
|
146
|
+
}
|
|
147
|
+
// Atlas Cloud talks to the gpt-image-2 API directly and accepts the
|
|
148
|
+
// forced value; the OAuth proxy does not (see lib/imageBackgroundParam.ts).
|
|
138
149
|
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
139
150
|
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
140
151
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
@@ -153,6 +164,23 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
153
164
|
const effectiveSize = providerOptions.size;
|
|
154
165
|
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
155
166
|
const activeProvider = providerOptions.provider;
|
|
167
|
+
// Resolved AFTER provider resolution on purpose: the raw request `provider`
|
|
168
|
+
// defaults to "auto", so only `activeProvider` names the lane that will
|
|
169
|
+
// actually run. Atlas Cloud talks to the gpt-image-2 API directly and
|
|
170
|
+
// accepts a forced transparent background; the OAuth proxy rejects it
|
|
171
|
+
// (see lib/imageBackgroundParam.ts).
|
|
172
|
+
const backgroundParams = resolveImageBackgroundParams({
|
|
173
|
+
preset: backgroundPreset,
|
|
174
|
+
supportsForcedTransparent: activeProvider === "atlascloud",
|
|
175
|
+
requestedFormat: typeof format === "string" ? format : undefined,
|
|
176
|
+
});
|
|
177
|
+
// Grok/Gemini/Agy/MiniMax have no background parameter and their branches
|
|
178
|
+
// force JPEG, so a transparent request there would return an opaque image
|
|
179
|
+
// recorded as a cutout. Refuse instead of billing for a wrong result.
|
|
180
|
+
const providerConflict = validateTransparentProvider(backgroundPreset, activeProvider);
|
|
181
|
+
if (providerConflict) {
|
|
182
|
+
return fail(400, { error: providerConflict.error, code: providerConflict.code });
|
|
183
|
+
}
|
|
156
184
|
// --- Element injection (after provider resolution) ---
|
|
157
185
|
const rawElementIds = Array.isArray(req.body?.elementIds)
|
|
158
186
|
? req.body.elementIds.filter((id) => typeof id === "string" && id)
|
|
@@ -330,7 +358,13 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
330
358
|
});
|
|
331
359
|
const startTime = Date.now();
|
|
332
360
|
const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
333
|
-
const
|
|
361
|
+
const providerForcesJpeg = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" || activeProvider === "atlascloud" || activeProvider === "minimax";
|
|
362
|
+
// An alpha-bearing result must never be persisted through a lossy opaque
|
|
363
|
+
// format: embedImageMetadata re-encodes with sharp.toFormat(), so a JPEG
|
|
364
|
+
// here silently flattens the transparency we just asked for.
|
|
365
|
+
const effectiveFormat = backgroundParams
|
|
366
|
+
? (backgroundParams.outputFormat ?? "png")
|
|
367
|
+
: (providerForcesJpeg ? "jpeg" : String(format));
|
|
334
368
|
const mime = mimeMap[effectiveFormat] || "image/png";
|
|
335
369
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
336
370
|
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
@@ -376,6 +410,8 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
376
410
|
signal: cancelController.signal,
|
|
377
411
|
requestId,
|
|
378
412
|
references: refCheck.refDetails,
|
|
413
|
+
...(backgroundParams ? { background: backgroundParams.background } : {}),
|
|
414
|
+
...(backgroundParams?.outputFormat ? { outputFormat: backgroundParams.outputFormat } : {}),
|
|
379
415
|
});
|
|
380
416
|
throwIfJobCanceled(requestId);
|
|
381
417
|
return r;
|
|
@@ -416,6 +452,8 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
416
452
|
webSearchEnabled,
|
|
417
453
|
signal: cancelController.signal,
|
|
418
454
|
allowPromptOnlyOAuthFallback: activeProvider !== "api",
|
|
455
|
+
...(backgroundParams ? { background: backgroundParams.background } : {}),
|
|
456
|
+
...(backgroundParams?.outputFormat ? { outputFormat: backgroundParams.outputFormat } : {}),
|
|
419
457
|
});
|
|
420
458
|
throwIfJobCanceled(requestId);
|
|
421
459
|
if (r.b64)
|
|
@@ -440,6 +478,22 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
440
478
|
};
|
|
441
479
|
const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
|
|
442
480
|
throwIfJobCanceled(requestId);
|
|
481
|
+
// Alpha is verified for EVERY result before anything is written. Doing it
|
|
482
|
+
// inside the write loop would let an earlier image land on disk before a
|
|
483
|
+
// later opaque one failed the batch, so the error would claim "nothing was
|
|
484
|
+
// saved" while an orphan file existed (adversarial review 260821 round 4).
|
|
485
|
+
if (backgroundParams) {
|
|
486
|
+
for (const r of results) {
|
|
487
|
+
if (r.status !== "fulfilled" || !r.value.b64)
|
|
488
|
+
continue;
|
|
489
|
+
const verdict = await verifyBufferAlpha(Buffer.from(r.value.b64, "base64"), decodeRawForAlpha);
|
|
490
|
+
if (verdict.hasAlpha === false) {
|
|
491
|
+
const { reason } = verdict;
|
|
492
|
+
logEvent("generate", "transparent_result_opaque", { requestId, provider: activeProvider, reason });
|
|
493
|
+
throw makeTransparentResultError(activeProvider, reason);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
443
497
|
const images = [];
|
|
444
498
|
let totalUsage = null;
|
|
445
499
|
let totalWebSearchCalls = 0;
|
|
@@ -448,10 +502,25 @@ export async function runGeneratePipeline(req, res, ctx) {
|
|
|
448
502
|
if (r.status === "fulfilled" && r.value.b64) {
|
|
449
503
|
throwIfJobCanceled(requestId);
|
|
450
504
|
const valueWithMime = r.value;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
505
|
+
// When alpha was requested, trust the BYTES, never a provider-supplied
|
|
506
|
+
// Content-Type. Atlas reads its mime from the download response header
|
|
507
|
+
// (lib/atlasCloudImageAdapter.ts), and a transparent PNG mislabeled
|
|
508
|
+
// "image/jpeg" would otherwise be re-encoded to JPEG by
|
|
509
|
+
// embedImageMetadata's sharp.toFormat() and lose its alpha channel.
|
|
510
|
+
const providerReportsMime = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" || activeProvider === "atlascloud" || activeProvider === "minimax";
|
|
511
|
+
// Lazily decoded: only alpha requests always need the byte check, and
|
|
512
|
+
// the provider-mime path keeps its original short-circuit order.
|
|
513
|
+
const detectMime = () => detectImageMimeFromB64(r.value.b64);
|
|
514
|
+
const resultMime = backgroundParams
|
|
515
|
+
? (detectMime() || mime)
|
|
516
|
+
: providerReportsMime
|
|
517
|
+
? (valueWithMime.mime || detectMime() || mime)
|
|
518
|
+
: mime;
|
|
519
|
+
const resultFormat = backgroundParams
|
|
520
|
+
? imageFormatFromMime(resultMime)
|
|
521
|
+
: providerReportsMime
|
|
522
|
+
? imageFormatFromMime(resultMime)
|
|
523
|
+
: effectiveFormat;
|
|
455
524
|
const retryValue = r.value;
|
|
456
525
|
if (!firstRetryMeta && retryValue.retryKind) {
|
|
457
526
|
firstRetryMeta = {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const VALID_BACKGROUND_VALUES = ["auto", "opaque", "transparent"];
|
|
2
|
+
/** Formats that can carry an alpha channel. JPEG cannot. */
|
|
3
|
+
export const ALPHA_CAPABLE_FORMATS = ["png", "webp"];
|
|
4
|
+
export function isAlphaCapableFormat(value) {
|
|
5
|
+
return typeof value === "string" && ALPHA_CAPABLE_FORMATS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Image lanes that can actually return an alpha channel.
|
|
9
|
+
*
|
|
10
|
+
* Only the GPT image tool (OAuth/API) and the gpt-image-2 API surface expose a
|
|
11
|
+
* background parameter. Grok, Gemini, Agy, and MiniMax have no equivalent and
|
|
12
|
+
* their pipeline branches force JPEG, so a transparent request there would bill
|
|
13
|
+
* the user for an opaque image labeled as a cutout.
|
|
14
|
+
*/
|
|
15
|
+
export const ALPHA_CAPABLE_PROVIDERS = ["oauth", "api", "atlascloud"];
|
|
16
|
+
export function providerSupportsTransparent(provider) {
|
|
17
|
+
return typeof provider === "string" && ALPHA_CAPABLE_PROVIDERS.includes(provider);
|
|
18
|
+
}
|
|
19
|
+
/** Refuse a transparent request on a lane that cannot deliver alpha. */
|
|
20
|
+
export function validateTransparentProvider(preset, provider) {
|
|
21
|
+
if (preset !== "transparent")
|
|
22
|
+
return null;
|
|
23
|
+
if (providerSupportsTransparent(provider))
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
error: `transparent backgrounds are not supported on the "${String(provider)}" lane (no alpha channel); use ${ALPHA_CAPABLE_PROVIDERS.join(", ")}, or pick a solid background and key it`,
|
|
27
|
+
code: "TRANSPARENT_PROVIDER_UNSUPPORTED",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Fast pre-check: JPEG can never carry alpha, so skip the decode entirely. */
|
|
31
|
+
export function isJpegBuffer(buffer) {
|
|
32
|
+
return buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @param decode injected so tests can drive failure paths without stubbing sharp
|
|
36
|
+
*/
|
|
37
|
+
export async function verifyBufferAlpha(buffer, decode) {
|
|
38
|
+
if (isJpegBuffer(buffer))
|
|
39
|
+
return { hasAlpha: false, reason: "jpeg" };
|
|
40
|
+
let decoded;
|
|
41
|
+
try {
|
|
42
|
+
decoded = await decode(buffer);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Unreadable bytes are not evidence of transparency.
|
|
46
|
+
return { hasAlpha: false, reason: "undetectable" };
|
|
47
|
+
}
|
|
48
|
+
if (!decoded.hasAlpha)
|
|
49
|
+
return { hasAlpha: false, reason: "no-alpha-channel" };
|
|
50
|
+
const { data, channels } = decoded;
|
|
51
|
+
if (channels < 4 || data.length < channels)
|
|
52
|
+
return { hasAlpha: false, reason: "no-alpha-channel" };
|
|
53
|
+
// One non-opaque pixel is enough: partial alpha (glass, hair, anti-aliased
|
|
54
|
+
// edges) counts as transparency just as much as a fully cut-out background.
|
|
55
|
+
for (let i = channels - 1; i < data.length; i += channels) {
|
|
56
|
+
if (data[i] < 255)
|
|
57
|
+
return { hasAlpha: true };
|
|
58
|
+
}
|
|
59
|
+
return { hasAlpha: false, reason: "fully-opaque" };
|
|
60
|
+
}
|
|
61
|
+
/** Operational error for a transparency request that came back opaque. */
|
|
62
|
+
export function makeTransparentResultError(provider, reason) {
|
|
63
|
+
const detail = reason === "jpeg"
|
|
64
|
+
? "the provider returned JPEG, which cannot carry an alpha channel"
|
|
65
|
+
: reason === "no-alpha-channel"
|
|
66
|
+
? "the returned image has no alpha channel"
|
|
67
|
+
: reason === "fully-opaque"
|
|
68
|
+
? "the returned image has an alpha channel but every pixel is fully opaque"
|
|
69
|
+
: "the returned image could not be decoded to verify transparency";
|
|
70
|
+
const err = new Error(`transparent background requested but ${detail} (lane: ${String(provider)}). Nothing was saved; retry, or use a solid background and key it.`);
|
|
71
|
+
err.status = 502;
|
|
72
|
+
err.code = "TRANSPARENT_RESULT_OPAQUE";
|
|
73
|
+
err.isOperational = true;
|
|
74
|
+
return err;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the tool parameters for a preset. Returns `null` when the preset
|
|
78
|
+
* implies no explicit background handling, so existing callers keep their
|
|
79
|
+
* current payload byte-for-byte.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveImageBackgroundParams(input) {
|
|
82
|
+
if (input.preset !== "transparent")
|
|
83
|
+
return null;
|
|
84
|
+
const requested = input.requestedFormat;
|
|
85
|
+
// JPEG cannot hold alpha: silently honoring it would ship an opaque image
|
|
86
|
+
// while the UI claims transparency. Fall back to PNG instead.
|
|
87
|
+
const outputFormat = isAlphaCapableFormat(requested) ? requested : "png";
|
|
88
|
+
return {
|
|
89
|
+
background: input.supportsForcedTransparent ? "transparent" : "auto",
|
|
90
|
+
outputFormat,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reject an explicit alpha-incapable format paired with a transparent
|
|
95
|
+
* background instead of quietly producing an opaque image.
|
|
96
|
+
*/
|
|
97
|
+
export function validateTransparentFormat(preset, requestedFormat) {
|
|
98
|
+
if (preset !== "transparent")
|
|
99
|
+
return null;
|
|
100
|
+
if (requestedFormat === undefined || requestedFormat === null || requestedFormat === "")
|
|
101
|
+
return null;
|
|
102
|
+
if (isAlphaCapableFormat(requestedFormat))
|
|
103
|
+
return null;
|
|
104
|
+
return {
|
|
105
|
+
error: `a transparent background requires an alpha-capable output format (${ALPHA_CAPABLE_FORMATS.join(", ")}); received "${String(requestedFormat)}"`,
|
|
106
|
+
code: "TRANSPARENT_FORMAT_CONFLICT",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -39,7 +39,10 @@ export function extractGptImageHints(text) {
|
|
|
39
39
|
const qualityHints = matches(value, QUALITY_HINTS);
|
|
40
40
|
const warnings = [];
|
|
41
41
|
if (/\btransparent|alpha channel|no background|cutout\b/i.test(value)) {
|
|
42
|
-
|
|
42
|
+
// gpt-image-2 supports real alpha since 2026-08-21; the remaining hazard is
|
|
43
|
+
// asking for it in bare prompt text instead of the background parameter,
|
|
44
|
+
// which can bake a fake checkerboard into an opaque image.
|
|
45
|
+
warnings.push("transparent-needs-background-param");
|
|
43
46
|
}
|
|
44
47
|
if (/\bexact text|small text|dense text|legal copy\b/i.test(value)) {
|
|
45
48
|
warnings.push("text-rendering-sensitive");
|
package/lib/responsesFallback.js
CHANGED
|
@@ -3,7 +3,7 @@ import { imageToolChoice, tools } from "./responsesTools.js";
|
|
|
3
3
|
import { emptyResponseError } from "./responsesErrors.js";
|
|
4
4
|
import { GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, buildUserTextPrompt, } from "./oauthProxy.js";
|
|
5
5
|
const MAX_RETRIES = 2;
|
|
6
|
-
export async function retryPromptOnlyJsonImage({ postResponses, ctx, provider, prompt, mode, model, quality, size, moderation, requestId, signal, initial, referenceInputs = [], webSearchDroppedOnRetry, reasoningEffort, }) {
|
|
6
|
+
export async function retryPromptOnlyJsonImage({ postResponses, ctx, provider, prompt, mode, model, quality, size, moderation, requestId, signal, initial, referenceInputs = [], webSearchDroppedOnRetry, reasoningEffort, background, outputFormat, }) {
|
|
7
7
|
if (provider === "api")
|
|
8
8
|
return null;
|
|
9
9
|
const developerPrompt = webSearchDroppedOnRetry
|
|
@@ -62,7 +62,13 @@ export async function retryPromptOnlyJsonImage({ postResponses, ctx, provider, p
|
|
|
62
62
|
...(plan.developerPromptDroppedOnRetry ? [] : [{ role: "developer", content: developerPrompt }]),
|
|
63
63
|
{ role: "user", content: userContent },
|
|
64
64
|
],
|
|
65
|
-
tools: tools(false, {
|
|
65
|
+
tools: tools(false, {
|
|
66
|
+
quality,
|
|
67
|
+
size,
|
|
68
|
+
moderation,
|
|
69
|
+
...(background ? { background } : {}),
|
|
70
|
+
...(outputFormat ? { output_format: outputFormat } : {}),
|
|
71
|
+
}),
|
|
66
72
|
tool_choice: imageToolChoice(true),
|
|
67
73
|
reasoning: { effort: reasoningEffort || "low" },
|
|
68
74
|
// OAuth/Codex proxy returns empty output[] for non-stream image requests; SSE required.
|
|
@@ -211,7 +211,14 @@ export async function generateViaResponses(provider, prompt, quality, size, mode
|
|
|
211
211
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
212
212
|
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.6-luna";
|
|
213
213
|
const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
|
|
214
|
-
const requestTools = tools(webSearchEnabled, {
|
|
214
|
+
const requestTools = tools(webSearchEnabled, {
|
|
215
|
+
quality,
|
|
216
|
+
size,
|
|
217
|
+
moderation,
|
|
218
|
+
...(options.partialImages ? { partial_images: options.partialImages } : {}),
|
|
219
|
+
...(options.background ? { background: options.background } : {}),
|
|
220
|
+
...(options.outputFormat ? { output_format: options.outputFormat } : {}),
|
|
221
|
+
});
|
|
215
222
|
const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
|
|
216
223
|
const toolChoiceKind = imageToolChoiceKind(toolChoice);
|
|
217
224
|
const referenceInputs = references.map(normalizeRef);
|
|
@@ -258,6 +265,8 @@ export async function generateViaResponses(provider, prompt, quality, size, mode
|
|
|
258
265
|
referenceInputs,
|
|
259
266
|
webSearchDroppedOnRetry: webSearchEnabled,
|
|
260
267
|
...(options.reasoningEffort !== undefined ? { reasoningEffort: options.reasoningEffort } : {}),
|
|
268
|
+
...(options.background !== undefined ? { background: options.background } : {}),
|
|
269
|
+
...(options.outputFormat !== undefined ? { outputFormat: options.outputFormat } : {}),
|
|
261
270
|
});
|
|
262
271
|
if (fallback)
|
|
263
272
|
return fallback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
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": "3883482f57e132376dd7cd078a3f1e11791e94c0"
|
|
123
123
|
}
|
package/routes/video.js
CHANGED
|
@@ -143,6 +143,13 @@ export function registerVideoRoutes(app, ctxRaw) {
|
|
|
143
143
|
return fail(400, backgroundParse.code, backgroundParse.error);
|
|
144
144
|
}
|
|
145
145
|
const backgroundPreset = backgroundParse.preset;
|
|
146
|
+
// "transparent" is an image-only preset: it depends on the GPT image
|
|
147
|
+
// tool's alpha-capable output, and Grok video has no such parameter.
|
|
148
|
+
// Accepting it here would append a cutout suffix that the model cannot
|
|
149
|
+
// honor and hand back an opaque clip that claims transparency.
|
|
150
|
+
if (backgroundPreset === "transparent") {
|
|
151
|
+
return fail(400, "TRANSPARENT_VIDEO_UNSUPPORTED", "transparent backgrounds are image-only; video generation has no alpha channel — use chroma-green and key the clip instead");
|
|
152
|
+
}
|
|
146
153
|
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
147
154
|
const topic = typeof req.body?.topic === "string" ? req.body.topic.trim() : "";
|
|
148
155
|
if (provider !== "grok" && provider !== "grok-api")
|
package/skills/ima2/SKILL.md
CHANGED
|
@@ -263,8 +263,29 @@ discouraged.
|
|
|
263
263
|
|
|
264
264
|
### Cutout Assets and Background Strategy
|
|
265
265
|
|
|
266
|
-
GPT Image 2
|
|
267
|
-
|
|
266
|
+
GPT Image 2 CAN produce true transparent (alpha) backgrounds. Prefer
|
|
267
|
+
`--bg transparent` for cutout assets:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
ima2 gen "a minimal geometric fox head logo mark, flat vector style" \
|
|
271
|
+
--bg transparent --quality high --mode direct -o logo.png
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
This asks for a real alpha channel instead of a matte you have to key out
|
|
275
|
+
later. Verified on the live OAuth path 2026-08-21: 5/5 generations returned
|
|
276
|
+
RGBA PNGs with all four corners at alpha 0 and 42-56% fully transparent
|
|
277
|
+
pixels, including genuine PARTIAL alpha on glass and leaf veins. Saved as PNG;
|
|
278
|
+
JPEG is refused because it cannot carry alpha.
|
|
279
|
+
|
|
280
|
+
Mechanics worth knowing: ChatGPT-session models pin the image tool to the
|
|
281
|
+
`gpt-image-2-codex` variant, which rejects a FORCED `background: "transparent"`
|
|
282
|
+
with a 400. ima2 therefore sends `background: "auto"` and puts the cutout
|
|
283
|
+
intent in the prompt, which is what actually produces the alpha. You do not
|
|
284
|
+
need to hand-write that suffix — `--bg transparent` adds it.
|
|
285
|
+
|
|
286
|
+
**Use the solid-background-then-remove strategy only when you need a matte**
|
|
287
|
+
(chroma keying a video, compositing pipelines that expect green screen), or
|
|
288
|
+
when a specific generation refuses to isolate the subject cleanly:
|
|
268
289
|
|
|
269
290
|
**Generate on a pure solid background:**
|
|
270
291
|
- **Black** (`#000000`) for reflective/metallic/glass subjects
|
|
@@ -288,8 +309,11 @@ ima2 gen "3D chrome splash on PURE SOLID BLACK background hex #000000. \
|
|
|
288
309
|
- `ima2 edit asset.png --prompt "remove the background, keep only the subject"`
|
|
289
310
|
- Programmatic: `sharp` / ImageMagick / `rembg`
|
|
290
311
|
|
|
291
|
-
**Anti-pattern:**
|
|
292
|
-
|
|
312
|
+
**Anti-pattern:** hand-writing "transparent background" into a prompt WITHOUT
|
|
313
|
+
`--bg transparent`. Bare prompt wording sometimes yields a checkerboard
|
|
314
|
+
pattern painted into an opaque image; the flag sends the real API parameter and
|
|
315
|
+
the tuned suffix together. Always verify alpha rather than trusting the look of
|
|
316
|
+
a preview: `sharp(file).metadata()` should report `channels: 4, hasAlpha: true`.
|
|
293
317
|
|
|
294
318
|
### Korean Text in Images
|
|
295
319
|
|
|
@@ -322,10 +322,25 @@ When generating images with Korean text:
|
|
|
322
322
|
|
|
323
323
|
### Asset Background Strategy (FE-ASSET-BG-01, DEFAULT)
|
|
324
324
|
|
|
325
|
-
GPT Image 2
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
GPT Image 2 CAN produce true transparent (alpha) backgrounds. For cutout
|
|
326
|
+
assets, reach for `--bg transparent` FIRST:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
ima2 gen "3D render of a liquid chrome splash blob, organic starburst shape, \
|
|
330
|
+
mirror-polished surface with iridescent cyan and gold reflections." \
|
|
331
|
+
--bg transparent --model oauth/luna --quality high --mode direct -o chrome-splash.png
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The flag sends the real API background parameter plus a tuned cutout suffix,
|
|
335
|
+
and the asset is saved as PNG (JPEG is refused — it cannot carry alpha).
|
|
336
|
+
Verified 2026-08-21 on the live OAuth path: 5/5 generations returned RGBA with
|
|
337
|
+
corners at alpha 0 and 42-56% fully transparent pixels, preserving PARTIAL
|
|
338
|
+
alpha on glass and foliage. Always confirm with `sharp(file).metadata()`
|
|
339
|
+
(`channels: 4, hasAlpha: true`) rather than trusting a preview.
|
|
340
|
+
|
|
341
|
+
**Still use the solid-background-then-remove strategy when** you specifically
|
|
342
|
+
need a matte (chroma-key video pipelines, compositing tools that expect green
|
|
343
|
+
screen), or when one generation refuses to isolate its subject cleanly:
|
|
329
344
|
|
|
330
345
|
**Generation: pure solid background.**
|
|
331
346
|
|
|
@@ -393,10 +408,10 @@ ima2 gen "Flat illustration of a coffee cup with steam, centered. \
|
|
|
393
408
|
**`ima2 gen` fallback:** same solid-background prompting strategy applies.
|
|
394
409
|
No Canvas Mode available; use CSS blend modes or programmatic removal only.
|
|
395
410
|
|
|
396
|
-
**Anti-pattern:**
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
411
|
+
**Anti-pattern:** hand-writing "transparent background" or "PNG with alpha
|
|
412
|
+
channel" into a prompt WITHOUT `--bg transparent`. Bare wording can still bake
|
|
413
|
+
a fake checkerboard into an opaque image; the flag pairs the API parameter with
|
|
414
|
+
the tuned suffix. Verify the alpha channel instead of eyeballing the preview.
|
|
400
415
|
|
|
401
416
|
### Prompt Iteration
|
|
402
417
|
|
|
@@ -437,8 +437,17 @@ mockups is NOT a skip; it becomes generation input via `--ref`.
|
|
|
437
437
|
When concept exploration (UX-CONCEPT-GEN-01) or image-first ism discovery
|
|
438
438
|
(UX-IMAGE-FIRST-01) generates component/element mockups that need to float
|
|
439
439
|
over arbitrary backgrounds — icons, 3D objects, product shots, stickers,
|
|
440
|
-
UI chrome elements — use the cutout asset pipeline. GPT Image 2
|
|
441
|
-
|
|
440
|
+
UI chrome elements — use the cutout asset pipeline. GPT Image 2 CAN produce
|
|
441
|
+
real transparent backgrounds: pass `--bg transparent` and the asset comes back
|
|
442
|
+
as an RGBA PNG, no keying pass required.
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
ima2 gen "3D render of [subject], [material], [composition]" \
|
|
446
|
+
--bg transparent --quality high --mode direct -o concept-01.png
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Fall back to solid-bg-then-remove when you need a matte for a chroma-key
|
|
450
|
+
pipeline, or when a subject refuses to isolate cleanly.
|
|
442
451
|
|
|
443
452
|
**Cutout asset prompt template (concept pass):**
|
|
444
453
|
|
|
@@ -467,8 +476,10 @@ ima2 gen "[subject], centered. PURE SOLID background hex #[target]. \
|
|
|
467
476
|
- Wrap in `isolation: isolate` container to prevent bleed
|
|
468
477
|
- Programmatic: `sharp`, ImageMagick, `rembg`. Interactive: ima2 Canvas Mode.
|
|
469
478
|
|
|
470
|
-
**Anti-pattern:**
|
|
471
|
-
the prompt
|
|
479
|
+
**Anti-pattern:** hand-writing "transparent background" or "PNG with alpha"
|
|
480
|
+
into the prompt WITHOUT `--bg transparent` — bare wording can bake a fake
|
|
481
|
+
checkerboard into an opaque image. Verify alpha (`channels: 4`) rather than
|
|
482
|
+
trusting the preview.
|
|
472
483
|
|
|
473
484
|
Full pipeline reference: `ima2-front/references/asset-requirements.md`
|
|
474
485
|
§ Asset Background Strategy (FE-ASSET-BG-01).
|