// Генерация 8 новых поз Зеро — расширенный набор. // Запуск: cd /var/www/zeropost-engine && node scripts/generate-zero-poses-v2.js const fs = require('fs'); const path = require('path'); const axios = require('axios'); const ROOT = '/var/www/zeropost-engine'; process.chdir(ROOT); require('dotenv').config({ path: path.join(ROOT, '.env') }); const config = require(path.join(ROOT, 'src/config')); const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads'; let sharp = null; try { sharp = require('sharp'); } catch {} const CHARACTER_BASE = ` Character: a small, friendly mascot named Zero. Body: a soft rounded square shape (like a plump pixel or chubby tetris block). Color: bright emerald green (#10b981) with slight gradient to teal, soft and matte. Face: two simple round black dot eyes, a small mouth expressing the current emotion. Style: clean modern vector illustration, flat design with soft shading, no outlines. Background: warm off-white (#fafaf9) with subtle geometric shapes in light teal/emerald. Composition: 1:1 square format with comfortable padding, character is the main focus. Strictly: no text, no letters, no logos, no humans, no realistic robots, no circuits. `.trim(); const NEW_POSES = [ { name: 'swimming', desc: 'Zero is swimming or floating happily in stylized blue water waves. Small stick arms doing a swim stroke, surrounded by abstract blue and teal wave shapes. Big happy smile, eyes bright. Playful aquatic mood.', }, { name: 'thinking', desc: 'Zero sits quietly, looking thoughtfully into the distance. One small stick arm raised slightly touching chin area in a thinking gesture. Eyes are slightly narrowed and upward-looking. Calm, contemplative expression. A small thought-bubble or single floating dot above.', }, { name: 'coffee', desc: 'Zero holds an oversized steaming coffee mug (the mug is almost as big as Zero itself) with both stick arms. Cozy content smile, eyes half-closed in morning satisfaction. Steam rising in decorative curls. Warm amber and cream color accents from the mug.', }, { name: 'telescope', desc: 'Zero peers through a small stylized telescope (geometric, teal colored). One eye closed, one looking through the eyepiece. Expression of curiosity and discovery. Stars or geometric star shapes floating in background.', }, { name: 'rocket', desc: 'Zero sits on top of or rides a small stylized rocket (geometric, emerald and white colored). Both stick arms raised in excitement. Big grin, eyes wide with excitement. Abstract motion lines and geometric stars around. Launch/deploy energy.', }, { name: 'bug', desc: 'Zero stands next to a large stylized bug (geometric beetle shape, in a contrasting color like amber or red). Zero has a detective expression — slightly furrowed brow, focused eyes, one stick arm pointing at the bug. Bug-hunting / debugging moment.', }, { name: 'sleep', desc: 'Zero is sleeping peacefully. Eyes closed (shown as two curved lines). Small content smile. Floating ZZZ letters nearby (styled as geometric shapes, not text). A small pillow or star shape underneath. Soft, calm, night-time mood with dark blue accents.', }, { name: 'thumbsup', desc: 'Zero gives an enthusiastic thumbs up with one raised stick arm (the thumb is stylized as a geometric shape). Big confident smile, eyes bright and happy. Possibly a small star or sparkle nearby. Approval, recommendation, "great choice" energy.', }, ]; const ATTEMPTS = 8; const LOG = '/tmp/zero-poses-v2.log'; function log(msg) { const line = `[${new Date().toISOString().slice(11,19)}] ${msg}`; fs.appendFileSync(LOG, line + '\n'); console.log(line); } async function generateOne(prompt, attempt = 1) { const model = process.env.AI_MODEL_IMAGE_VIA_RESPONSES || 'gpt-5.5'; const wrapped = `Use the image_generation tool to create the following illustration. Do not write any text response, only call the tool.\n\n${prompt}`; try { const res = await axios.post( `${config.ai.baseUrl}/responses`, { model, input: wrapped, tools: [{ type: 'image_generation' }], tool_choice: { type: 'image_generation' } }, { headers: { Authorization: `Bearer ${config.ai.imageApiKey}` }, timeout: 300_000 } ); const output = res.data?.output || []; const imgCall = output.find(o => o.type === 'image_generation_call'); if (!imgCall || !imgCall.result) { if (attempt < ATTEMPTS) { log(` retry ${attempt+1}/${ATTEMPTS} (no image)`); return generateOne(prompt, attempt + 1); } throw new Error('No image after ' + ATTEMPTS + ' attempts'); } return Buffer.from(imgCall.result, 'base64'); } catch (err) { if (attempt < ATTEMPTS) { const msg = err.response?.data?.error?.message || err.message; log(` retry ${attempt+1}/${ATTEMPTS} (${msg.slice(0,50)})`); return generateOne(prompt, attempt + 1); } throw err; } } async function saveBytes(bytes, name) { const outPath = path.join(UPLOADS_DIR, `zero-${name}.webp`); if (sharp) { await sharp(bytes).resize(1024, 1024, { fit: 'cover' }).webp({ quality: 88 }).toFile(outPath); } else { fs.writeFileSync(outPath, bytes); } return outPath; } (async () => { fs.writeFileSync(LOG, ''); log(`=== generating ${NEW_POSES.length} new Zero poses ===`); const done = [], failed = []; for (const p of NEW_POSES) { const outPath = path.join(UPLOADS_DIR, `zero-${p.name}.webp`); if (fs.existsSync(outPath)) { log(`[${p.name}] already exists, skip`); done.push(p.name); continue; } log(`[${p.name}] starting...`); const t = Date.now(); try { const bytes = await generateOne(`${CHARACTER_BASE}\n\nPose: ${p.desc}`); const out = await saveBytes(bytes, p.name); const elapsed = ((Date.now() - t) / 1000).toFixed(0); log(`[${p.name}] ✅ saved (${elapsed}s) → ${out}`); done.push(p.name); } catch (err) { log(`[${p.name}] ❌ FAILED: ${err.message.slice(0, 100)}`); failed.push(p.name); } } log(`=== DONE: ${done.length} ok, ${failed.length} failed ===`); if (failed.length) log('Failed: ' + failed.join(', ')); process.exit(0); })();