import { Buffer } from 'node:buffer'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..'); const outputDir = path.join( repoRoot, 'public', 'branding', 'taonier-logo-capybara-jar-ref01-logo-refine-concepts', ); const referenceImagePath = path.join( repoRoot, 'public', 'branding', 'taonier-logo-peeking-head-jar-new-animals-concepts', 'taonier-peeking-head-jar-new-animals-01-capybara.png', ); const timeoutMsDefault = 180000; const args = new Map(); for (let index = 2; index < process.argv.length; index += 1) { const raw = process.argv[index]; if (!raw.startsWith('--')) { continue; } const next = process.argv[index + 1]; if (next && !next.startsWith('--')) { args.set(raw, next); index += 1; } else { args.set(raw, true); } } const logoBrief = { brand: '陶泥儿', source: '基于 peeking-head-jar-new-animals 批次 01 水豚头参考图继续收敛', logoType: 'symbol/icon-only mascot mark, no wordmark', product: 'AI UGC 轻休闲小游戏创作与传播平台,用户把脑洞、梗和灵感塑造成可分享的作品', keep: [ '陶罐容器为主形体', '水豚式半圆脑袋只露到眼睛位置', '两只纯黑点眼,无高光', '小圆耳朵与平静亲和感', '中心构图与 32px 可读性', ], explore: [ '不同罐子颜色', '不同动物头色彩浓度', '更扁平、更抽象、更商标化', '更强黑白轮廓', '减少插画感、渐变感和材质细节', ], avoid: [ '中文或英文字', '鼻子、嘴巴、腮红、表情高光', '罐子表情', '星星、闪光、手、陶艺工具', '甜点、面包、巧克力、糖果、布丁、餐具感', '完整动物身体、爪子、复杂场景', '贴纸感、儿童玩具感、写实陶瓷质感', ], }; const basePrompt = [ 'Use the uploaded reference image as the composition and pose reference. Keep the same core idea: a ceramic jar container with a capybara-like small animal peeking out only to eye level.', 'Create a refined icon-only brand logo symbol for the Chinese product named "陶泥儿"; do not render any Chinese, English, letters, numbers, wordmark, tagline, or text.', 'Preserve the reference structure: the jar is the main brand shape; the animal shows only small rounded ears, the upper half-dome head, and two matte black dot eyes above the jar rim.', 'Do not show nose, mouth, cheek blush, teeth, paws, body, lower face, or a full animal head. The eyes must be pure black dots with no highlights, no white sparkle, and no glossy pupils.', 'The jar itself must have no face, no expression, no eyes, no smile, and no decorative emoji marks.', 'Make the result more like a trademark and logo than an illustration: simplified silhouette, clean vector curves, broad flat color fields, fewer gradients, fewer soft shadows, no realistic ceramic rendering.', 'Keep a warm ceramic-container feeling through color and silhouette only. The jar should not look like a bowl of food, cup, dessert package, pudding cup, bread, bun, chocolate, candy, jelly, or pastry.', 'Style target: premium friendly mascot-symbol logo, all-age and women-friendly, calm but memorable, suitable for app icon, social avatar, and trademark review.', 'Composition: centered on a clean light background with generous safe area. Strong readable silhouette first. It must still read at 32px and in black and white.', 'Avoid star, spark, halo, magic wand, hand, pottery tool, UI, border, sticker outline, background scene, and watermark.', ]; const variants = [ { id: '01-flat-terracotta', title: '扁平陶橙', prompt: [ ...basePrompt, 'Variant focus: the most direct logo refinement. Use flat terracotta jar, warm caramel capybara head, minimal rim shadow, almost no gradients. Make the jar silhouette slightly more iconic and compact.', ], }, { id: '02-cream-cocoa', title: '奶白可可', prompt: [ ...basePrompt, 'Variant focus: cream ceramic jar with a cocoa-brown capybara head. Keep the palette soft but not edible; use graphic flat fills and a crisp rim shape to avoid dessert feeling.', ], }, { id: '03-sage-clay', title: '鼠尾草陶', prompt: [ ...basePrompt, 'Variant focus: muted sage green ceramic jar paired with a warm ochre capybara head. More mature and boutique. Keep the silhouette simple and logo-like, with only two or three main color regions.', ], }, { id: '04-outline-emblem', title: '线面徽记', prompt: [ ...basePrompt, 'Variant focus: bolder trademark mark with clean outline plus flat fills. Use a dark warm-brown contour line around the jar and animal, but keep it soft and modern, not sticker-like.', ], }, { id: '05-abstract-geometric', title: '抽象几何', prompt: [ ...basePrompt, 'Variant focus: higher abstraction. Reduce the capybara head to a clean half-dome with two round ears and two black dots; reduce the jar to a distinct pot silhouette with a single rim band. Very vector-ready.', ], }, { id: '06-monochrome-first', title: '黑白优先', prompt: [ ...basePrompt, 'Variant focus: black-and-white survival first. Design with strong positive and negative shapes; color is secondary. Use warm clay and dark umber, but the mark must remain clear if converted to pure black and white.', ], }, { id: '07-soft-gradient-logo', title: '轻渐变商标', prompt: [ ...basePrompt, 'Variant focus: allow only a very subtle premium gradient on broad shapes, like a polished app logo. Keep it much flatter than the reference and remove painterly shadows or texture.', ], }, { id: '08-bold-avatar', title: '头像强识别', prompt: [ ...basePrompt, 'Variant focus: compact social-avatar readability. Make the jar a fuller rounded vessel and enlarge the peeking capybara head slightly, while preserving the hidden half-head rhythm and black dot eyes.', ], }, ]; function readDotenv(fileName) { const filePath = path.join(repoRoot, fileName); if (!existsSync(filePath)) { return {}; } const values = {}; for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) { continue; } const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed); if (!match) { continue; } let value = match[2].trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } values[match[1]] = value; } return values; } function resolveEnv() { const loaded = { ...readDotenv('.env.example'), ...readDotenv('.env.local'), ...readDotenv('.env.secrets.local'), ...process.env, }; return { baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '') .trim() .replace(/\/+$/u, ''), apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), timeoutMs: Number.parseInt( String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || timeoutMsDefault), 10, ), }; } function buildVectorEngineImagesEditUrl(baseUrl) { return baseUrl.endsWith('/v1') ? `${baseUrl}/images/edits` : `${baseUrl}/v1/images/edits`; } function buildDryRunFields(variant) { return { model: 'gpt-image-2', prompt: variant.prompt.join('\n'), n: '1', size: '1024x1024', image: referenceImagePath, }; } function collectStringsByKey(value, targetKey, output) { if (Array.isArray(value)) { value.forEach((entry) => collectStringsByKey(entry, targetKey, output)); return; } if (!value || typeof value !== 'object') { return; } for (const [key, nested] of Object.entries(value)) { if (key === targetKey) { if (typeof nested === 'string' && nested.trim()) { output.push(nested.trim()); } if (Array.isArray(nested)) { nested.forEach((entry) => { if (typeof entry === 'string' && entry.trim()) { output.push(entry.trim()); } }); } } collectStringsByKey(nested, targetKey, output); } } function extractImageUrls(payload) { const urls = []; collectStringsByKey(payload, 'url', urls); collectStringsByKey(payload, 'image', urls); collectStringsByKey(payload, 'image_url', urls); return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url)); } function extractBase64Images(payload) { const values = []; collectStringsByKey(payload, 'b64_json', values); return values; } function inferExtensionFromContentType(contentType) { const normalized = contentType.split(';')[0]?.trim().toLowerCase(); if (normalized === 'image/png') { return 'png'; } if (normalized === 'image/webp') { return 'webp'; } if (normalized === 'image/gif') { return 'gif'; } return 'jpg'; } function inferExtensionFromBytes(bytes) { if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) { return 'png'; } if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) { return 'jpg'; } if ( bytes.subarray(0, 4).toString('ascii') === 'RIFF' && bytes.subarray(8, 12).toString('ascii') === 'WEBP' ) { return 'webp'; } return 'png'; } async function fetchJson(url, options, timeoutMs) { const abortController = new AbortController(); const timer = setTimeout(() => abortController.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: abortController.signal, }); const text = await response.text(); if (!response.ok) { throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`); } return JSON.parse(text); } catch (error) { if (error?.name === 'AbortError') { throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`); } throw error; } finally { clearTimeout(timer); } } async function downloadUrl(url, timeoutMs) { const abortController = new AbortController(); const timer = setTimeout(() => abortController.abort(), timeoutMs); try { const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) { throw new Error(`download ${response.status}`); } const bytes = Buffer.from(await response.arrayBuffer()); return { bytes, extension: inferExtensionFromContentType( response.headers.get('content-type') || 'image/jpeg', ), }; } catch (error) { if (error?.name === 'AbortError') { throw new Error(`Generated image download timed out after ${timeoutMs}ms`); } throw error; } finally { clearTimeout(timer); } } function createEditFormData(variant) { const form = new FormData(); const imageBytes = readFileSync(referenceImagePath); form.append('model', 'gpt-image-2'); form.append('prompt', variant.prompt.join('\n')); form.append('n', '1'); form.append('size', '1024x1024'); form.append( 'image', new Blob([imageBytes], { type: 'image/png' }), path.basename(referenceImagePath), ); return form; } async function generateOne(env, variant) { const payload = await fetchJson( buildVectorEngineImagesEditUrl(env.baseUrl), { method: 'POST', headers: { Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json', }, body: createEditFormData(variant), }, env.timeoutMs, ); const urls = extractImageUrls(payload); const b64Images = extractBase64Images(payload); let image; if (urls[0]) { image = await downloadUrl(urls[0], env.timeoutMs); } else if (b64Images[0]) { const bytes = Buffer.from(b64Images[0], 'base64'); image = { bytes, extension: inferExtensionFromBytes(bytes), }; } else { throw new Error(`VectorEngine returned no image for ${variant.id}`); } mkdirSync(outputDir, { recursive: true }); const outputPath = path.join( outputDir, `taonier-capybara-jar-ref01-logo-refine-${variant.id}.${image.extension}`, ); writeFileSync(outputPath, image.bytes); return outputPath; } function writeManifest(files) { const manifestPath = path.join( outputDir, 'taonier-logo-capybara-jar-ref01-logo-refine-manifest.json', ); writeFileSync( manifestPath, `${JSON.stringify( { model: 'gpt-image-2', endpoint: '/v1/images/edits', size: '1024x1024', referenceImage: path.relative(repoRoot, referenceImagePath), generatedAt: new Date().toISOString(), logoSkillSummary: { requiredReview: 'visual inspection, 32px readability, black-white viability', outputStatus: 'AI concept only; final logo needs vector cleanup', }, brief: logoBrief, variants: variants.map((variant) => { const file = files.find((item) => path.basename(item).includes(variant.id), ); return { id: variant.id, title: variant.title, file: file ? path.basename(file) : null, prompt: variant.prompt.join('\n'), }; }), }, null, 2, )}\n`, 'utf8', ); return manifestPath; } const dryRun = args.has('--dry-run') || !args.has('--live'); const limit = Number.parseInt(String(args.get('--limit') || variants.length), 10); const selectedVariants = variants.slice(0, limit); if (dryRun) { console.log( JSON.stringify( { mode: 'dry-run', outputDir, referenceImagePath, count: selectedVariants.length, brief: logoBrief, requests: selectedVariants.map((variant) => ({ id: variant.id, title: variant.title, fields: buildDryRunFields(variant), })), }, null, 2, ), ); process.exit(0); } if (!existsSync(referenceImagePath)) { console.error( JSON.stringify({ ok: false, error: 'Reference image does not exist', referenceImagePath, }), ); process.exit(1); } const env = resolveEnv(); if (!env.baseUrl || !env.apiKey) { console.error( JSON.stringify({ ok: false, error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY', hasBaseUrl: Boolean(env.baseUrl), hasApiKey: Boolean(env.apiKey), }), ); process.exit(1); } const generated = []; for (const variant of selectedVariants) { console.log(`Generating ${variant.id} ${variant.title}...`); generated.push(await generateOne(env, variant)); } const manifestPath = writeManifest(generated); console.log( JSON.stringify( { ok: true, count: generated.length, files: generated, manifest: manifestPath, }, null, 2, ), );