diff --git a/src/config/index.js b/src/config/index.js index 0c0e94e..fa44340 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -8,6 +8,8 @@ module.exports = { baseUrl: process.env.AI_BASE_URL || 'https://aiprimetech.io/v1', apiKey: process.env.AI_API_KEY, imageApiKey: process.env.AI_IMAGE_API_KEY || process.env.AI_API_KEY, + imageBaseUrl: process.env.AI_IMAGE_BASE_URL || process.env.AI_BASE_URL || 'https://aiprimetech.io/v1', + imageModel: process.env.AI_MODEL_IMAGE || 'gpt-image-1-mini', // Per-task model selection — tune cost vs quality here models: { post: process.env.AI_MODEL_POST || 'claude-haiku-4-5-20251001', diff --git a/src/services/covers.js b/src/services/covers.js index 52013d1..4fe2dab 100644 --- a/src/services/covers.js +++ b/src/services/covers.js @@ -160,6 +160,39 @@ async function generateCoverViaImagesEndpoint({ prompt }) { throw new Error('No image data'); } +/** + * Основной путь — /images/generations (aiguoguo199.com или любой OpenAI-совместимый). + * Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2. + */ +async function generateCoverViaImageGenerations({ prompt }) { + const model = config.ai.imageModel || 'gpt-image-1-mini'; + const baseUrl = config.ai.imageBaseUrl || config.ai.baseUrl; + const res = await axios.post( + `${baseUrl}/images/generations`, + { + model, + prompt: prompt.slice(0, 4000), + n: 1, + size: '1024x1024', + }, + { + headers: { Authorization: `Bearer ${config.ai.imageApiKey}` }, + timeout: 120_000, + } + ); + const item = res.data?.data?.[0]; + if (!item) throw new Error('No image data in response'); + // b64_json или url + const b64 = item.b64_json; + if (b64) return { bytes: Buffer.from(b64, 'base64'), format: 'png' }; + if (item.url) { + const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 }); + return { bytes: Buffer.from(r.data), format: 'png' }; + } + throw new Error('No b64_json or url in response'); + return { bytes: Buffer.from(b64, 'base64'), format: 'png' }; +} + /** * Резервный путь — Pollinations.AI (https://pollinations.ai). * 100% бесплатно, без API ключа, без регистрации. @@ -194,34 +227,31 @@ async function generateCover({ articleId, title, tags = [] }) { console.log(`[Cover] article=${articleId} style=${styleIdx}:${styleName}`); let img; - let usedPath = 'responses'; + let usedPath = 'images-generations'; - // Пробуем все внешние API, при любой ошибке — сразу local SVG + // Цепочка: 1) aiguoguo /images/generations → 2) aiprimetech /responses → 3) legacy → 4) local SVG try { try { - img = await generateCoverViaResponses({ prompt }); + img = await generateCoverViaImageGenerations({ prompt }); } catch (err) { - const msg = err.response?.data?.error?.message || err.message; - console.warn(`[Cover] /responses path failed: ${msg.slice(0, 200)}`); + console.warn(`[Cover] /images/generations failed: ${(err.response?.data?.error?.message || err.message).slice(0, 150)}`); try { - img = await generateCoverViaImagesEndpoint({ prompt }); - usedPath = 'images-legacy'; + img = await generateCoverViaResponses({ prompt }); + usedPath = 'responses'; } catch (err2) { - const msg2 = err2.response?.data?.error?.message || err2.message; - console.warn(`[Cover] legacy path failed too: ${msg2.slice(0, 200)}`); + console.warn(`[Cover] /responses failed: ${(err2.response?.data?.error?.message || err2.message).slice(0, 150)}`); try { - img = await generateCoverViaPollinations({ prompt }); - usedPath = 'pollinations'; - console.log(`[Cover] article=${articleId} using Pollinations.AI fallback`); + img = await generateCoverViaImagesEndpoint({ prompt }); + usedPath = 'images-legacy'; } catch (err3) { - console.warn(`[Cover] Pollinations fallback failed: ${err3.message.slice(0, 200)}`); + console.warn(`[Cover] legacy failed: ${(err3.response?.data?.error?.message || err3.message).slice(0, 150)}`); throw new Error('all_external_failed'); } } } } catch (outerErr) { - // Все внешние API упали — используем локальную SVG-генерацию - console.log(`[Cover] article=${articleId} → local SVG generator (all external APIs unavailable)`); + // Все внешние API упали — local SVG + console.log(`[Cover] article=${articleId} → local SVG (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;