Merge remote-tracking branch 'origin/master' into codex/wooden-fish-template
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -101,7 +101,7 @@ const onlyIds = process.argv
|
||||
.filter(Boolean);
|
||||
const templates = rawTemplates.filter((template) => !onlyIds.length || onlyIds.includes(template.id));
|
||||
const dryRun = args.has('--dry-run') || !args.has('--live');
|
||||
const requests = templates.map((template) => ({ id: template.id, title: template.title, body: { model: 'gpt-image-2-all', prompt: template.prompt, n: 1, size: '1024x1024' } }));
|
||||
const requests = templates.map((template) => ({ id: template.id, title: template.title, body: { model: 'gpt-image-2', prompt: template.prompt, n: 1, size: '1024x1024' } }));
|
||||
if (dryRun) {
|
||||
console.log(JSON.stringify({ mode: 'dry-run', outDir, count: requests.length, requests }, null, 2));
|
||||
process.exit(0);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -215,7 +215,7 @@ async function downloadImage(url, timeoutMs) {
|
||||
|
||||
async function generateOne(env, template, outDir, size) {
|
||||
const requestBody = {
|
||||
model: 'gpt-image-2-all',
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size,
|
||||
@@ -278,7 +278,7 @@ if (dryRun) {
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
body: {
|
||||
model: 'gpt-image-2-all',
|
||||
model: 'gpt-image-2',
|
||||
prompt: buildPrompt(template),
|
||||
n: 1,
|
||||
size,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user