#!/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); });