// Тест: генерируем 3 ракурса Зеро. Каждая картинка — до 5 попыток (GPT-5.2 капризничает с tool_choice). const fs = require('fs'); const path = require('path'); const axios = require('axios'); const FormData = require('form-data'); 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 settings = require(path.join(ROOT, 'src/services/settings')); const { query } = require(path.join(ROOT, 'src/config/db')); 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 a chubby tetris block), about the size of a small companion creature. Color: bright emerald green (#10b981) with a slight gradient to teal, soft and matte. Face: two simple round black dot eyes, a small understated mouth that expresses the current emotion. Personality: friendly, curious, enthusiastic, a bit nerdy. 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, like Notion/Linear/Anthropic editorial illustrations. 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 glowing nodes, no circuits. `.trim(); const POSES = [ { name: 'avatar', desc: `Front-facing portrait pose. Zero is looking directly at the viewer with curious wide-open eyes and a small confident smile. Sits centered in the frame. Used as channel avatar.`, }, { name: 'laptop', desc: `Zero sits at a tiny laptop (the laptop is also stylized geometric, in cream/emerald colors). Eyes focused on the screen, small thoughtful expression. Cozy setup with a small plant nearby. The mascot is enthusiastic about coding.`, }, { name: 'eureka', desc: `Zero has just discovered something exciting. One small stick arm raised in the air with a soft glowing dot above the head representing an idea. Eyes wide and sparkly, big enthusiastic smile. Confetti or stylized geometric sparkles around.`, }, ]; async function generateOne(prompt, attempt = 1) { const MAX_ATTEMPTS = 5; const model = process.env.AI_MODEL_IMAGE_VIA_RESPONSES || 'gpt-5.2'; 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 < MAX_ATTEMPTS) { console.log(`[zero] retry attempt=${attempt + 1} (no image_generation_call)`); return generateOne(prompt, attempt + 1); } throw new Error(`No image after ${MAX_ATTEMPTS} attempts`); } return Buffer.from(imgCall.result, 'base64'); } catch (err) { if (attempt < MAX_ATTEMPTS) { const msg = err.response?.data?.error?.message || err.message; console.log(`[zero] retry attempt=${attempt + 1} (${msg.slice(0, 60)})`); return generateOne(prompt, attempt + 1); } throw err; } } async function processBytes(bytes, name) { const ts = Date.now(); const webpName = `zero-${name}-${ts}.webp`; const webpPath = path.join(UPLOADS_DIR, webpName); if (sharp) { await sharp(bytes) .resize(1024, 1024, { fit: 'cover' }) .webp({ quality: 88 }) .toFile(webpPath); } else { fs.writeFileSync(webpPath, bytes); } return webpPath; } (async () => { try { console.log('[zero] generating 3 poses sequentially (with retries)...'); const results = []; for (const p of POSES) { console.log(`[zero] starting ${p.name}...`); const prompt = `${CHARACTER_BASE}\n\nPose: ${p.desc}`; const bytes = await generateOne(prompt); const localPath = await processBytes(bytes, p.name); console.log(`[zero] ${p.name} → ${localPath}`); results.push({ name: p.name, path: localPath }); } // Шлём все 3 в TG как media group с подписью const { rows: chs } = await query(`SELECT * FROM channels WHERE id=1`); const channel = chs[0]; const tgBase = await settings.get('TELEGRAM_API_BASE', 'https://api.telegram.org'); const form = new FormData(); form.append('chat_id', String(channel.tg_channel_id)); const media = results.map((r, i) => ({ type: 'photo', media: `attach://photo${i}`, caption: i === 0 ? `🧪 Тест Зеро — три позы:\n1. аватар 2. за ноутом 3. эврика` : undefined, })); form.append('media', JSON.stringify(media)); results.forEach((r, i) => { form.append(`photo${i}`, fs.createReadStream(r.path)); }); const res = await axios.post(`${tgBase}/bot${channel.bot_token}/sendMediaGroup`, form, { headers: form.getHeaders(), timeout: 60000, maxContentLength: Infinity, maxBodyLength: Infinity, }); const msgIds = (res.data?.result || []).map(m => m.message_id); console.log(`[zero] sent media group, message_ids=${msgIds.join(',')}`); console.log('[zero] DONE'); process.exit(0); } catch (err) { console.error('[zero] FAILED:', err.response?.data || err.message); process.exit(1); } })();