Switch to VectorEngine gpt-image-2 and edits

Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
2026-05-22 03:06:41 +08:00
parent 321e1ea33a
commit ae014ac881
90 changed files with 7078 additions and 3389 deletions

View File

@@ -772,6 +772,12 @@ function buildVectorEngineImagesGenerationUrl(baseUrl) {
: `${baseUrl}/v1/images/generations`;
}
function buildVectorEngineImagesEditUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/edits`
: `${baseUrl}/v1/images/edits`;
}
function collectStringsByKey(value, targetKey, output) {
if (Array.isArray(value)) {
value.forEach((entry) => collectStringsByKey(entry, targetKey, output));
@@ -828,28 +834,41 @@ function inferExtensionFromBytes(bytes, preferredPath) {
return path.extname(preferredPath).replace(/^\./u, '') || 'png';
}
function toDataUrl(filePath) {
function mimeFromExtension(extension) {
if (extension === 'jpg' || extension === 'jpeg') {
return 'image/jpeg';
}
if (extension === 'webp') {
return 'image/webp';
}
return 'image/png';
}
function readReferenceImage(filePath) {
if (!existsSync(filePath)) {
return null;
}
const bytes = readFileSync(filePath);
const extension = inferExtensionFromBytes(bytes, filePath);
const mime = extension === 'jpg' ? 'image/jpeg' : `image/${extension}`;
return `data:${mime};base64,${bytes.toString('base64')}`;
return {
fileName: path.basename(filePath).replace(/"/gu, '_'),
mimeType: mimeFromExtension(extension),
bytes,
};
}
function pushReferenceImage(body, filePath) {
const reference = toDataUrl(filePath);
const reference = readReferenceImage(filePath);
if (!reference) {
return false;
}
body.image = [...(body.image || []), reference];
body.referenceImages = [...(body.referenceImages || []), reference];
return true;
}
function buildRequestBody(asset, size) {
const body = {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: asset.prompt,
n: 1,
size: size || asset.size,
@@ -1624,18 +1643,49 @@ async function generateAsset(asset, env, size, force) {
};
}
const requestBody = buildRequestBody(asset, size);
const { referenceImages = [], ...requestBody } = buildRequestBody(asset, size);
const hasReferenceImages = referenceImages.length > 0;
const requestOptions = hasReferenceImages
? (() => {
const formData = new FormData();
formData.set('model', requestBody.model);
formData.set('prompt', requestBody.prompt);
formData.set('n', String(requestBody.n));
formData.set('size', requestBody.size);
for (const referenceImage of referenceImages) {
formData.append(
'image',
new Blob([referenceImage.bytes], { type: referenceImage.mimeType }),
referenceImage.fileName,
);
}
return {
url: buildVectorEngineImagesEditUrl(env.baseUrl),
options: {
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
},
body: formData,
},
};
})()
: {
url: buildVectorEngineImagesGenerationUrl(env.baseUrl),
options: {
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
};
const payloadText = await fetchWithTimeout(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
requestOptions.url,
requestOptions.options,
env.timeoutMs,
);
@@ -1687,7 +1737,7 @@ async function generateAsset(asset, env, size, force) {
size: requestBody.size,
extension: actualExtension,
source: urls[0] ? 'url' : 'b64_json',
usedReferenceImage: Boolean(requestBody.image),
usedReferenceImage: hasReferenceImages,
};
}
@@ -1715,19 +1765,27 @@ function dryRun(selectedAssets, size) {
{
mode: 'dry-run',
assets: selectedAssets.map((asset) => {
const body = buildRequestBody(asset, size);
const { referenceImages = [], ...body } = buildRequestBody(asset, size);
return {
id: asset.id,
endpoint: referenceImages.length
? '/v1/images/edits'
: '/v1/images/generations',
outputPath: outputPathFor(asset),
sourceOutputPath: asset.transparent
? sourceOutputPathFor(asset)
: undefined,
transparent: asset.transparent,
localPostprocess: asset.localPostprocess,
body: {
...body,
image: body.image ? ['<local style reference image>'] : undefined,
},
body: referenceImages.length ? undefined : body,
form: referenceImages.length
? {
...body,
imageParts: referenceImages.map(
(referenceImage) => referenceImage.fileName,
),
}
: undefined,
};
}),
},