feat: add visual style picker and contextual previews
This commit is contained in:
108
scripts/generate-style-previews.mjs
Normal file
108
scripts/generate-style-previews.mjs
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const envPath = path.join(root, '.env.local');
|
||||
const outputDir = path.join(root, 'public', 'style-previews');
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
|
||||
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)\s*$/);
|
||||
if (!match) continue;
|
||||
const [, key, rawValue] = match;
|
||||
if (process.env[key]) continue;
|
||||
process.env[key] = rawValue.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
const styles = [
|
||||
{
|
||||
id: 'none',
|
||||
label: 'neutral toy concept',
|
||||
prompt: 'neutral original toy mascot concept preview, small rounded collectible character, clean studio light, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'plush',
|
||||
label: 'plush toy',
|
||||
prompt: 'plush toy style preview, soft fuzzy fabric original rounded mascot, stitched details, warm studio light, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'mecha',
|
||||
label: 'mecha toy',
|
||||
prompt: 'mecha toy style preview, original rounded robot collectible, polished armor panels, tiny mechanical joints, clean dramatic light, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'kawaii',
|
||||
label: 'kawaii toy',
|
||||
prompt: 'kawaii toy style preview, original cute rounded mascot, soft pastel colors, friendly expression, clean studio background, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'blueprint',
|
||||
label: 'patent blueprint',
|
||||
prompt: 'patent blueprint style preview, original toy mascot shown as clean white technical line art on deep blue blueprint background, no readable text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'cyber',
|
||||
label: 'cyberpunk toy',
|
||||
prompt: 'cyberpunk toy style preview, original rounded collectible mascot, neon rim light, glossy dark materials, futuristic display glow, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
{
|
||||
id: 'minimal',
|
||||
label: 'minimal toy',
|
||||
prompt: 'minimal toy style preview, original rounded mascot, simple geometric silhouette, restrained colors, premium clean product render, no text, no logo, no watermark, square UI thumbnail',
|
||||
},
|
||||
];
|
||||
|
||||
async function imageToBuffer(payload, apiKey) {
|
||||
const first = payload?.data?.[0];
|
||||
if (!first) throw new Error('missing image data');
|
||||
if (first.b64_json) return Buffer.from(first.b64_json, 'base64');
|
||||
if (first.url) {
|
||||
const res = await fetch(first.url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
||||
if (!res.ok) throw new Error(`image url fetch ${res.status}: ${await res.text()}`);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
throw new Error('image payload has no b64_json or url');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadEnvFile(envPath);
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) throw new Error('OPENAI_API_KEY missing');
|
||||
|
||||
const apiBase = process.env.GPT_API_BASE || 'https://api.openai.com/v1';
|
||||
const model = process.env.GPT_IMAGE_MODEL || 'gpt-image-2';
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
for (const style of styles) {
|
||||
const out = path.join(outputDir, `${style.id}.png`);
|
||||
if (fs.existsSync(out) && !process.argv.includes('--force')) {
|
||||
console.log(`skip ${style.id}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`generate ${style.id}`);
|
||||
const res = await fetch(`${apiBase}/images/generations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
prompt: style.prompt,
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`GPT image ${style.id} ${res.status}: ${await res.text()}`);
|
||||
fs.writeFileSync(out, await imageToBuffer(await res.json(), apiKey));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user