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
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Маппинг постов на иллюстрации с персонажем Зеро.
|
||||
// 15 поз хранятся как /var/www/zeropost-uploads/zero-{name}.webp
|
||||
//
|
||||
// Логика выбора:
|
||||
// 1. Если в title/excerpt есть triggers — берём соответствующую эмоциональную/активную позу
|
||||
// 2. Иначе — берём позу по категории
|
||||
// 3. Если в локации файла нет — fallback на 'avatar'
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||
|
||||
// Эмоциональные/активные позы — выбираются по ключевым словам в title/excerpt.
|
||||
// Порядок важен: первое срабатывание побеждает.
|
||||
const EMOTIONAL_TRIGGERS = [
|
||||
// "Получилось / заработало / победа" → victory
|
||||
{ pose: 'victory', words: ['получилось', 'заработало', 'победа', 'отличный результат', 'удалось', 'успех'] },
|
||||
|
||||
// "Не работает / сломалось / провал" → facepalm
|
||||
{ pose: 'facepalm', words: ['не работает', 'сломал', 'ошибк', 'провал', 'факап', 'fail', 'баг', 'неудач', 'облажал'] },
|
||||
|
||||
// "Нашёл / открыл / классный" → eureka
|
||||
{ pose: 'eureka', words: ['нашёл', 'нашел', 'открыл', 'классн', 'крутая фича', 'интересн', 'wow', 'неожиданн'] },
|
||||
|
||||
// "Запутался / непонятно / разбираемся" → confused
|
||||
{ pose: 'confused', words: ['запутал', 'непонятно', 'разбира', 'разобрат', 'странн', 'не пойму', 'почему'] },
|
||||
|
||||
// "Устал / долго / ночь" → tired
|
||||
{ pose: 'tired', words: ['устал', 'долго', 'часами', 'ночь', 'утром понял', 'выгорел'] },
|
||||
|
||||
// "Изучаю / разбор / гайд / шпаргалка" → reading или present
|
||||
{ pose: 'reading', words: ['изуча', 'разбор', 'шпаргалк', 'гайд', 'мануал', 'документац'] },
|
||||
{ pose: 'present', words: ['как сделать', 'туториал', 'инструкц', 'объясн', 'показыва', 'учимся'] },
|
||||
|
||||
// "Расследую / разбираю / копаю" → magnifier
|
||||
{ pose: 'magnifier', words: ['расследова', 'разбираю', 'копа', 'докопат', 'под капот', 'как устроен'] },
|
||||
|
||||
// "Аналитика / метрики / графики" → chart
|
||||
{ pose: 'chart', words: ['метрик', 'аналитик', 'график', 'статистик', 'цифр', 'данные показ', 'результат за'] },
|
||||
// "Запуск / деплой" → rocket
|
||||
{ pose: 'rocket', words: ['деплой', 'запустил', 'релиз', 'в продакш', 'залил', 'выкатил', 'запуск проект'] },
|
||||
// "Баг / отладка" → bug
|
||||
{ pose: 'bug', words: ['баг', 'ошибк', 'дебаг', 'отлаживал', 'починил', 'не работало'] },
|
||||
// "Рекомендация / топ" → thumbsup
|
||||
{ pose: 'thumbsup', words: ['рекомендую', 'советую', 'топ-', 'лучший', 'отличный инструмент', 'понравилось'] },
|
||||
// "Плавание / спорт" → swimming
|
||||
{ pose: 'swimming', words: ['плавани', 'бассейн', 'плыть', 'тренировк', 'спортивн'] },
|
||||
// "Думаю / вопрос" → thinking
|
||||
{ pose: 'thinking', words: ['думаю', 'размышляю', 'не знаю точно', 'интересный вопрос', 'а что если'] },
|
||||
// "Исследование" → telescope
|
||||
{ pose: 'telescope', words: ['исследова', 'изучаю', 'смотрю внимательно', 'нашёл интересн', 'открытие'] },
|
||||
|
||||
// "Подумать / поразмышлять / медитация" → meditate
|
||||
{ pose: 'meditate', words: ['подумать', 'размышл', 'осмысл', 'мысли вслух', 'рефлекс'] },
|
||||
];
|
||||
|
||||
// Категорийные позы — fallback если эмоциональных триггеров не нашлось
|
||||
const CATEGORY_POSES = {
|
||||
'ai-tools': 'tools',
|
||||
'cybersec': 'lock',
|
||||
'automation': 'gears',
|
||||
'ai-dev': 'coding',
|
||||
};
|
||||
|
||||
const FALLBACK_POSE = 'avatar';
|
||||
|
||||
/**
|
||||
* Выбирает имя позы Зеро под пост.
|
||||
* @param {{ title?: string, excerpt?: string, category?: string }} ctx
|
||||
* @returns {{ pose: string, path: string|null, exists: boolean }}
|
||||
*/
|
||||
function pickPose({ title = '', excerpt = '', category = '' }) {
|
||||
const haystack = `${title} ${excerpt}`.toLowerCase();
|
||||
|
||||
// 1. Эмоциональные триггеры
|
||||
for (const t of EMOTIONAL_TRIGGERS) {
|
||||
for (const w of t.words) {
|
||||
if (haystack.includes(w)) {
|
||||
return resolve(t.pose, 'emotional');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. По категории
|
||||
const catPose = CATEGORY_POSES[category];
|
||||
if (catPose) return resolve(catPose, 'category');
|
||||
|
||||
// 3. Fallback
|
||||
return resolve(FALLBACK_POSE, 'fallback');
|
||||
}
|
||||
|
||||
function resolve(name, source) {
|
||||
const localPath = path.join(UPLOADS_DIR, `zero-${name}.webp`);
|
||||
const exists = fs.existsSync(localPath);
|
||||
// Если позы нет — пробуем avatar
|
||||
if (!exists && name !== FALLBACK_POSE) {
|
||||
const fbPath = path.join(UPLOADS_DIR, `zero-${FALLBACK_POSE}.webp`);
|
||||
if (fs.existsSync(fbPath)) {
|
||||
return { pose: FALLBACK_POSE, path: fbPath, exists: true, source: `${source}-fallback` };
|
||||
}
|
||||
return { pose: name, path: null, exists: false, source };
|
||||
}
|
||||
return { pose: name, path: exists ? localPath : null, exists, source };
|
||||
}
|
||||
|
||||
/**
|
||||
* Список доступных поз (для UI).
|
||||
*/
|
||||
function listAvailablePoses() {
|
||||
const out = [];
|
||||
for (const name of [
|
||||
'avatar', 'coding', 'tools', 'lock', 'gears',
|
||||
'eureka', 'confused', 'facepalm', 'victory', 'tired',
|
||||
'reading', 'magnifier', 'chart', 'meditate', 'present',
|
||||
'swimming', 'thinking', 'coffee', 'telescope', 'rocket', 'bug', 'sleep', 'thumbsup',
|
||||
]) {
|
||||
const p = path.join(UPLOADS_DIR, `zero-${name}.webp`);
|
||||
out.push({ name, exists: fs.existsSync(p), path: p, url: `/uploads/zero-${name}.webp` });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { pickPose, listAvailablePoses, CATEGORY_POSES, EMOTIONAL_TRIGGERS };
|
||||
Reference in New Issue
Block a user