forked from admin/zeropost-engine
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:
+52
-12
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const { query } = require('../config/db');
|
||||
const localGen = require('./localCoverGenerator');
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||
|
||||
@@ -159,11 +160,34 @@ async function generateCoverViaImagesEndpoint({ prompt }) {
|
||||
throw new Error('No image data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Резервный путь — Pollinations.AI (https://pollinations.ai).
|
||||
* 100% бесплатно, без API ключа, без регистрации.
|
||||
* GET запрос → JPEG обложка за ~1-2 секунды.
|
||||
* Используется только когда aiprimetech.io недоступен.
|
||||
*/
|
||||
async function generateCoverViaPollinations({ prompt }) {
|
||||
// Pollinations: простой GET по URL, сразу возвращает бинарный JPEG
|
||||
const encoded = encodeURIComponent(prompt.slice(0, 1000)); // лимит на длину URL
|
||||
const url = `https://image.pollinations.ai/prompt/${encoded}?width=1600&height=900&model=flux&nologo=true`;
|
||||
const res = await axios.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 90_000, // Pollinations иногда медленный при нагрузке
|
||||
headers: { 'User-Agent': 'ZeroPost/1.0 blog-cover-generator' },
|
||||
});
|
||||
if (!res.data || res.data.byteLength < 5000) {
|
||||
throw new Error(`Pollinations returned too small response: ${res.data?.byteLength} bytes`);
|
||||
}
|
||||
return {
|
||||
bytes: Buffer.from(res.data),
|
||||
format: 'jpg',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Главный путь генерации. Использует /v1/responses, при ошибке падает в legacy.
|
||||
*/
|
||||
async function generateCover({ articleId, title, tags = [] }) {
|
||||
// Передаём articleId в buildCoverPrompt для детерминированного выбора стиля
|
||||
const prompt = buildCoverPrompt({ title, tags, articleId });
|
||||
const styleIdx = pickStyleIndex(articleId);
|
||||
const styleName = COVER_STYLES[styleIdx].name;
|
||||
@@ -171,20 +195,36 @@ async function generateCover({ articleId, title, tags = [] }) {
|
||||
|
||||
let img;
|
||||
let usedPath = 'responses';
|
||||
|
||||
// Пробуем все внешние API, при любой ошибке — сразу local SVG
|
||||
try {
|
||||
img = await generateCoverViaResponses({ prompt });
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.error?.message || err.message;
|
||||
console.warn(`[Cover] /responses path failed: ${msg.slice(0, 200)}`);
|
||||
// Пробуем legacy
|
||||
try {
|
||||
img = await generateCoverViaImagesEndpoint({ prompt });
|
||||
usedPath = 'images-legacy';
|
||||
} catch (err2) {
|
||||
const msg2 = err2.response?.data?.error?.message || err2.message;
|
||||
console.warn(`[Cover] legacy path failed too: ${msg2.slice(0, 200)}`);
|
||||
throw new Error(`Both image paths failed: ${msg}`);
|
||||
img = await generateCoverViaResponses({ prompt });
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.error?.message || err.message;
|
||||
console.warn(`[Cover] /responses path failed: ${msg.slice(0, 200)}`);
|
||||
try {
|
||||
img = await generateCoverViaImagesEndpoint({ prompt });
|
||||
usedPath = 'images-legacy';
|
||||
} catch (err2) {
|
||||
const msg2 = err2.response?.data?.error?.message || err2.message;
|
||||
console.warn(`[Cover] legacy path failed too: ${msg2.slice(0, 200)}`);
|
||||
try {
|
||||
img = await generateCoverViaPollinations({ prompt });
|
||||
usedPath = 'pollinations';
|
||||
console.log(`[Cover] article=${articleId} using Pollinations.AI fallback`);
|
||||
} catch (err3) {
|
||||
console.warn(`[Cover] Pollinations fallback failed: ${err3.message.slice(0, 200)}`);
|
||||
throw new Error('all_external_failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (outerErr) {
|
||||
// Все внешние API упали — используем локальную SVG-генерацию
|
||||
console.log(`[Cover] article=${articleId} → local SVG generator (all external APIs unavailable)`);
|
||||
const localUrl = await localGen.generateLocalCover({ articleId, title, category: tags?.[0] || '' });
|
||||
await query('UPDATE articles SET cover_url=$1, updated_at=NOW() WHERE id=$2', [localUrl, articleId]);
|
||||
return localUrl;
|
||||
}
|
||||
|
||||
// Сохраняем оригинал
|
||||
|
||||
Reference in New Issue
Block a user