a370b8f7d8
- Персонаж Зеро: 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
141 lines
6.2 KiB
JavaScript
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);
|
|
})();
|