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

@@ -817,12 +817,18 @@ function resolveEnv() {
};
}
function buildUrl(baseUrl) {
function buildGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
}
function buildEditUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/edits`
: `${baseUrl}/v1/images/edits`;
}
function hasHeader(headers, targetName) {
return Object.keys(headers).some(
(name) => name.toLowerCase() === targetName.toLowerCase(),
@@ -954,7 +960,7 @@ function inferExtensionFromBytes(bytes) {
return 'png';
}
function imagePathToDataUrl(imagePath) {
function imagePathToReferenceImage(imagePath) {
if (!existsSync(imagePath)) {
throw new Error(`Reference image not found: ${imagePath}`);
}
@@ -967,7 +973,44 @@ function imagePathToDataUrl(imagePath) {
: extension === '.webp'
? 'image/webp'
: 'image/png';
return `data:${mimeType};base64,${bytes.toString('base64')}`;
return {
fieldName: 'image',
fileName: path.basename(imagePath).replace(/"/gu, '_'),
mimeType,
bytes,
};
}
function buildMultipartBody(fields, files) {
const boundary = `----genarrative-${Date.now().toString(16)}-${Math.random()
.toString(16)
.slice(2)}`;
const chunks = [];
const push = (value) => {
chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value));
};
for (const [name, value] of Object.entries(fields)) {
push(`--${boundary}\r\n`);
push(`Content-Disposition: form-data; name="${name}"\r\n\r\n`);
push(`${value}\r\n`);
}
for (const file of files) {
push(`--${boundary}\r\n`);
push(
`Content-Disposition: form-data; name="${file.fieldName}"; filename="${file.fileName}"\r\n`,
);
push(`Content-Type: ${file.mimeType}\r\n\r\n`);
push(file.bytes);
push('\r\n');
}
push(`--${boundary}--\r\n`);
return {
body: Buffer.concat(chunks),
contentType: `multipart/form-data; boundary=${boundary}`,
};
}
async function fetchJson(url, options, timeoutMs) {
@@ -1011,27 +1054,45 @@ async function downloadUrl(url, timeoutMs) {
async function generateConcept(env, concept) {
const requestBody = {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
};
if (concept.referenceImages?.length) {
requestBody.image = concept.referenceImages.map(imagePathToDataUrl);
}
const payload = await fetchJson(
buildUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
const referenceImages = (concept.referenceImages || []).map(
imagePathToReferenceImage,
);
const payload = referenceImages.length
? await (async () => {
const multipart = buildMultipartBody(requestBody, referenceImages);
return fetchJson(
buildEditUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': multipart.contentType,
},
body: multipart.body,
},
env.timeoutMs,
);
})()
: await fetchJson(
buildGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
@@ -1072,19 +1133,28 @@ if (dryRun) {
requests: selected.map((concept) => ({
id: concept.id,
title: concept.title,
body: {
model: 'gpt-image-2-all',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
...(concept.referenceImages?.length
? {
image: concept.referenceImages.map((imagePath) =>
path.relative(repoRoot, imagePath),
),
}
: {}),
},
endpoint: concept.referenceImages?.length
? '/v1/images/edits'
: '/v1/images/generations',
body: concept.referenceImages?.length
? undefined
: {
model: 'gpt-image-2',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
},
form: concept.referenceImages?.length
? {
model: 'gpt-image-2',
prompt: concept.prompt,
n: 1,
size: '1024x1024',
imageParts: concept.referenceImages.map((imagePath) =>
path.relative(repoRoot, imagePath),
),
}
: undefined,
})),
},
null,