Files
postcast-engine/scripts/generate-zero-poses-v2.js
Nik (Claude) a370b8f7d8 feat: Зеро-персонаж, auto-publish, auto-series, channel-stats, fallback covers
- Персонаж Зеро: 23 позы (zeroCharacter.js), скрипты генерации
- Auto-publish статей в TG: multipart upload, кнопки, режим alternating Zero/cover
- Fallback цепочка обложек: aiprimetech gpt-5.5 → Pollinations → local SVG (6 палитр)
- Auto-series: Claude haiku определяет серию для каждой статьи автоматически
- Channel stats: подписчики, история, delta 24h/7d
- Photo-search: Yandex API, профили доменов, Redis лимиты
- Scheduled posts runner: backfill, preview, queue, cancel
- promptBuilder: author_persona Зеро, голос от первого лица
- Fixes: dollar-placeholder bugs в PATCH channels/autogen, listArticles фильтры
- AI model: gpt-5.5 для image generation
2026-06-07 14:03:56 +03:00

141 lines
6.2 KiB
JavaScript

// Генерация 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);
})();