fix: cover image diversity — 12 styles + topic-aware visual metaphors

COVER_STYLES: 4 → 12 стилей (amber-terrain, violet-gradient, monochrome-sharp,
  coral-horizon, neon-circuits, blueprint-tech, glass-morphism, retro-wave,
  zen-minimal, data-cosmos, editorial-ink, teal-architecture)
  Теперь стиль повторяется раз в 12 статей (было каждые 4)

buildCoverPrompt(): новая структура промта:
  - VISUAL CONCEPT: тематическая метафора из getVisualMetaphor()
  - STYLE/PALETTE/MOOD/COMPOSITION: стиль из ротации
  Промт явно ставит концепцию первой → картинка отражает тему статьи

getVisualMetaphor(): 10 тематических категорий (cybersec, AI, automation,
  data, deepfake, code, marketing, email, vector/rag, prompt engineering)
  + 10 универсальных метафор. Детерминированный выбор по хешу заголовка.
This commit is contained in:
Ник (Claude)
2026-06-12 22:55:25 +03:00
parent 7a70f79e61
commit 5a765d27e1
+185 -171
View File
@@ -22,50 +22,91 @@ if (!fs.existsSync(UPLOADS_DIR)) {
* один стиль при регенерации, но разные статьи выглядят по-разному. * один стиль при регенерации, но разные статьи выглядят по-разному.
*/ */
const COVER_STYLES = [ const COVER_STYLES = [
{
name: 'emerald-flow',
palette: 'emerald green (#10b981, #34d399) as primary, soft teal, warm off-white background (#fafaf9), subtle dark accents',
style: 'flat geometric shapes, smooth flowing curves, layered planes, vector-clean lines',
mood: 'clean, modern, calm, intellectual — Stripe Press / Linear / Anthropic brand aesthetic',
composition: 'balanced asymmetry, flowing diagonal forms, generous negative space',
},
{
name: 'midnight-blueprint',
palette: 'deep navy (#0f172a, #1e3a5f) as background, electric blue (#3b82f6, #60a5fa) as accent, crisp white details, subtle cyan highlights',
style: 'precise isometric geometry, technical grid lines, architectural cross-sections, blueprint-inspired forms',
mood: 'focused, technical, precise, trustworthy — like a well-engineered system',
composition: 'structured grid layout, strong geometric hierarchy, tight angular forms',
},
{ {
name: 'amber-terrain', name: 'amber-terrain',
palette: 'warm amber (#f59e0b, #fbbf24) as primary, burnt sienna (#c2410c), cream (#fef3c7) background, deep brown accents', palette: 'warm amber (#f59e0b, #fbbf24) primary, burnt sienna (#c2410c), cream (#fef3c7) background, deep brown accents',
style: 'organic flowing shapes, topographic contour lines, layered paper-cut planes, earthy textures', style: 'organic flowing shapes, topographic contour lines, layered paper-cut planes, earthy textures',
mood: 'warm, grounded, thoughtful, human — like physical craftsmanship', mood: 'warm, grounded, thoughtful, human',
composition: 'layered depth with overlapping organic planes, horizon-inspired layout', composition: 'layered depth with overlapping organic planes, horizon-inspired layout',
}, },
{ {
name: 'violet-gradient', name: 'violet-gradient',
palette: 'deep purple (#7c3aed, #6d28d9) transitioning to lavender (#c4b5fd), pale rose (#fce7f3) background, soft white highlights', palette: 'deep purple (#7c3aed, #6d28d9) to lavender (#c4b5fd), pale rose background, white highlights',
style: 'smooth gradient fields, soft abstract blobs, gentle radial forms, dreamy overlapping circles', style: 'smooth gradient fields, soft abstract blobs, gentle radial forms, dreamy overlapping circles',
mood: 'creative, forward-thinking, imaginative, innovative', mood: 'creative, forward-thinking, imaginative, innovative',
composition: 'radial soft composition, overlapping translucent shapes, dreamy depth', composition: 'radial soft composition, overlapping translucent shapes, dreamy depth',
}, },
{ {
name: 'monochrome-sharp', name: 'monochrome-sharp',
palette: 'near-black (#111827) and pure white (#ffffff) as primary, single bold vivid red (#ef4444) accent used sparingly on one element only', palette: 'near-black (#111827) and white primary, single vivid red (#ef4444) accent on one element only',
style: 'bold stark geometry, high contrast hard edges, strong typographic-inspired forms, Swiss design influence', style: 'bold stark geometry, high contrast hard edges, Swiss design influence, graphic poster style',
mood: 'bold, editorial, authoritative, direct — like a magazine cover', mood: 'bold, editorial, authoritative, direct',
composition: 'strong visual tension, deliberate asymmetry, dominant single focal element', composition: 'strong visual tension, deliberate asymmetry, dominant single focal element',
}, },
{ {
name: 'coral-horizon', name: 'coral-horizon',
palette: 'coral (#f97316, #fb923c) as primary, dusty rose (#fda4af), soft warm grey (#e5e7eb) background, muted sage green accents', palette: 'coral (#f97316, #fb923c) primary, dusty rose, warm grey background, sage green accents',
style: 'rounded soft shapes, gentle gradient washes, warm abstract landscape forms, approachable curves', style: 'rounded soft shapes, gentle gradient washes, warm abstract landscape forms, approachable curves',
mood: 'warm, accessible, optimistic, friendly — welcoming and human', mood: 'warm, accessible, optimistic, friendly',
composition: 'wide horizon-like bands, soft rounded forms, gentle layered gradients', composition: 'wide horizon-like bands, soft rounded forms, gentle layered gradients',
}, },
{
name: 'neon-circuits',
palette: 'deep black (#050505) background, electric cyan (#00f5ff) and magenta (#ff00e4) glowing lines, white highlights',
style: 'glowing circuit board traces, neural network node patterns, light trails, digital matrix aesthetic',
mood: 'futuristic, technical, high-energy, cutting-edge AI',
composition: 'diagonal flowing lines from corner to corner, perspective vanishing point, depth grid',
},
{
name: 'blueprint-tech',
palette: 'deep navy (#0a1628) background, bright white line art, cyan (#38bdf8) highlights, gold accents',
style: 'technical blueprint drawing style, precise geometric wireframes, schematic diagrams, engineering aesthetic',
mood: 'precise, methodical, professional, architectural',
composition: 'centered technical diagram, radiating lines, architectural cross-section feel',
},
{
name: 'glass-morphism',
palette: 'soft blue-grey background, frosted white panels, electric blue (#3b82f6) accents, subtle shadows',
style: 'frosted glass panels layered with blur, translucent overlapping shapes, light refraction, modern UI feel',
mood: 'modern, clean, sophisticated, premium tech',
composition: 'overlapping glass cards at angles, light source from top-left, subtle shadows',
},
{
name: 'retro-wave',
palette: 'deep purple-black sky (#12032e), hot pink (#ff2d78) and electric blue (#00b4ff) grid, orange (#ff6b35) sun',
style: 'synthwave 80s aesthetic, chrome style, sunset grid perspective, VHS vibe',
mood: 'nostalgic-futuristic, energetic, bold, iconic',
composition: 'infinite perspective grid to horizon, oversized sun, dramatic sky gradient',
},
{
name: 'zen-minimal',
palette: 'off-white (#fafaf9) background, single ink-black focal element, dusty sage subtle accent',
style: 'Japanese minimalism, single brushstroke gesture, generous negative space, ink wash',
mood: 'calm, contemplative, refined, timeless',
composition: 'single isolated element in lower-third, 80% empty space, asymmetric zen balance',
},
{
name: 'data-cosmos',
palette: 'deep space black background, galaxy purple to teal nebula, white star points',
style: 'data visualization aesthetic, floating 3D scatter plots, constellation connections, cosmic scale',
mood: 'vast, intellectual, data-driven, awe-inspiring',
composition: 'scattered nodes connected by thin lines, clusters, perspective depth, cosmic scale',
},
{
name: 'editorial-ink',
palette: 'cream (#fef9ef) background, deep charcoal ink, muted rust single highlight',
style: 'editorial illustration, hatching textures, woodblock print aesthetic, newspaper editorial',
mood: 'journalistic, authentic, handcrafted, trustworthy',
composition: 'dominant illustration element with organic borders, imperfect hand-drawn feel',
},
{
name: 'teal-architecture',
palette: 'white background, architectural teal (#0f766e, #0d9488) primary, charcoal structural lines',
style: 'architectural isometric illustration, precise 3D geometric forms, modern building aesthetic, clean vectors',
mood: 'structured, innovative, professional, spatial',
composition: 'isometric 3D perspective, modular stacked forms, clean technical beauty',
},
]; ];
/** /**
* Детерминированный хэш articleId → индекс стиля. * Детерминированный хэш articleId → индекс стиля.
*/ */
@@ -87,34 +128,27 @@ function pickStyleIndex(articleId) {
* Рубрика полностью задаёт визуальный язык — ограничения внутри неё. * Рубрика полностью задаёт визуальный язык — ограничения внутри неё.
*/ */
function buildCoverPrompt({ title, tags = [], articleId = 0, channelStyle = null, rubric = null }) { function buildCoverPrompt({ title, tags = [], articleId = 0, channelStyle = null, rubric = null }) {
const subject = title.replace(/[«»\\\":?!.]/g, '').slice(0, 100); const subject = title.replace(/[«»":?!.]/g, '').slice(0, 100);
const tagHint = tags.slice(0, 2).join(', '); const tagHint = tags.slice(0, 2).join(', ');
// Если рубрика выбрана — она задаёт весь визуальный язык
if (rubric?.prompt) { if (rubric?.prompt) {
return `${rubric.prompt} return `${rubric.prompt}\n\nArticle subject: "${subject}".${tagHint ? ` Theme: ${tagHint}.` : ''}\nWide 16:9 format. No text, no letters, no logos, no identifiable real human faces.`;
Article subject: "${subject}".${tagHint ? ` Theme: ${tagHint}.` : ''}
Wide 16:9 format. No text, no letters, no logos, no identifiable real human faces.`;
} }
let styleDesc, paletteDesc, moodDesc, compositionDesc; let styleDesc, paletteDesc, moodDesc, compositionDesc;
// Если задано несколько стилей через запятую — берём случайный из них
const rawStyle = channelStyle?.image_style || ''; const rawStyle = channelStyle?.image_style || '';
const styleList = rawStyle.split(',').map(s => s.trim()).filter(s => s && s !== 'auto'); const styleList = rawStyle.split(',').map(s => s.trim()).filter(s => s && s !== 'auto');
const csStyle = styleList.length > 0 const csStyle = styleList.length > 0 ? styleList[Math.floor(Math.random() * styleList.length)] : null;
? styleList[Math.floor(Math.random() * styleList.length)]
: null;
const STYLE_MAP = { const STYLE_MAP = {
'realistic-photo': { style: 'photorealistic, high-quality photography, natural lighting, sharp focus, realistic textures', mood: 'professional, realistic', comp: 'rule of thirds, natural depth of field' }, 'realistic-photo': { style: 'photorealistic, high-quality photography, natural lighting, sharp focus', mood: 'professional, realistic', comp: 'rule of thirds, natural depth of field' },
'flat-illustration': { style: 'flat vector illustration, clean geometric shapes, modern editorial style, smooth gradients, minimal', mood: 'clean, modern', comp: 'balanced, centered focal point' }, 'flat-illustration': { style: 'flat vector illustration, clean geometric shapes, modern editorial style, smooth gradients', mood: 'clean, modern', comp: 'balanced, centered focal point' },
'3d-render': { style: '3D render, soft studio lighting, isometric perspective, smooth surfaces, modern materials, Blender-quality', mood: 'polished, technical, premium', comp: 'three-quarter view, dramatic lighting' }, '3d-render': { style: '3D render, soft studio lighting, isometric perspective, smooth surfaces, modern materials', mood: 'polished, technical, premium', comp: 'three-quarter view, dramatic lighting' },
'cartoon': { style: 'cartoon illustration, bold outlines, vibrant colors, expressive shapes, comic book style', mood: 'playful, energetic', comp: 'dynamic, high contrast' }, 'cartoon': { style: 'cartoon illustration, bold outlines, vibrant colors, expressive shapes', mood: 'playful, energetic', comp: 'dynamic, high contrast' },
'minimal': { style: 'extremely minimalist, single focal element, generous negative space, monochrome or duotone', mood: 'calm, sophisticated, editorial', comp: 'single centered element, maximum negative space' }, 'minimal': { style: 'extremely minimalist, single focal element, generous negative space, duotone', mood: 'calm, sophisticated', comp: 'single centered element, maximum negative space' },
'abstract': { style: 'abstract artwork, layered geometric shapes, conceptual composition, mood and texture focused', mood: 'creative, sophisticated, tech-forward', comp: 'asymmetric layers, visual tension' }, 'abstract': { style: 'abstract artwork, layered geometric shapes, conceptual composition', mood: 'creative, tech-forward', comp: 'asymmetric layers, visual tension' },
'sketch': { style: 'hand-drawn sketch style, pencil and ink, loose confident lines, editorial illustration', mood: 'authentic, crafted, organic', comp: 'dynamic gestural lines, sketch book aesthetic' }, 'sketch': { style: 'hand-drawn sketch style, pencil and ink, loose confident lines', mood: 'authentic, organic', comp: 'dynamic gestural lines' },
'cyberpunk': { style: 'cyberpunk aesthetic, neon glowing lights, futuristic dark atmosphere, Blade Runner vibe', mood: 'bold, futuristic, dark', comp: 'dramatic angles, foreground/background depth' }, 'cyberpunk': { style: 'cyberpunk aesthetic, neon glowing lights, futuristic dark atmosphere', mood: 'bold, futuristic, dark', comp: 'dramatic angles, foreground/background depth' },
}; };
if (csStyle && csStyle !== 'auto' && STYLE_MAP[csStyle]) { if (csStyle && csStyle !== 'auto' && STYLE_MAP[csStyle]) {
@@ -129,152 +163,132 @@ Wide 16:9 format. No text, no letters, no logos, no identifiable real human face
if (channelStyle?.image_custom_colors) { if (channelStyle?.image_custom_colors) {
paletteDesc = `custom brand palette: ${channelStyle.image_custom_colors}`; paletteDesc = `custom brand palette: ${channelStyle.image_custom_colors}`;
} else if (channelStyle?.image_palette && channelStyle.image_palette !== 'auto') { } else if (channelStyle?.image_palette && channelStyle.image_palette !== 'auto') {
const PALETTES = { dark: 'dark palette, deep blues and blacks, bright highlights', light: 'light palette, soft whites, pastels', warm: 'warm palette, oranges, reds, gold', cool: 'cool palette, blues, teals, purples', mono: 'monochromatic, single hue with shades', vibrant: 'vibrant saturated colors, high energy' }; const PALETTES = { dark: 'dark palette, deep blues and blacks', light: 'light palette, soft whites, pastels', warm: 'warm palette, oranges, reds, gold', cool: 'cool palette, blues, teals, purples', mono: 'monochromatic', vibrant: 'vibrant saturated colors' };
paletteDesc = PALETTES[channelStyle.image_palette] || null; paletteDesc = PALETTES[channelStyle.image_palette] || null;
} }
return `Abstract editorial cover illustration for an article titled "${subject}". const visualMetaphor = getVisualMetaphor(title, tags);
Style: ${styleDesc}. return `Create a wide 16:9 editorial cover illustration.
${paletteDesc ? `Color palette: ${paletteDesc}.` : ''}
Mood: ${moodDesc}.
Composition: ${compositionDesc}
${tagHint ? `Theme cues (subtle, abstract): ${tagHint}.` : ''}
${channelStyle?.image_prompt_instructions ? `\nChannel visual guidelines: ${channelStyle.image_prompt_instructions}` : ''}
Strictly: no text, no letters, no logos, no identifiable real human faces.`; VISUAL CONCEPT: ${visualMetaphor}
STYLE: ${styleDesc}.
${paletteDesc ? `COLOR PALETTE: ${paletteDesc}.` : ''}
MOOD: ${moodDesc}.
COMPOSITION: ${compositionDesc}
${tagHint ? `THEME (abstract cues only): ${tagHint}.` : ''}
${channelStyle?.image_prompt_instructions ? `\nCHANNEL STYLE: ${channelStyle.image_prompt_instructions}` : ''}
RULE: absolutely no text, no letters, no words, no logos, no real human faces.`;
} }
/** function getVisualMetaphor(title, tags = []) {
* Генерирует картинку через /v1/responses + image_generation tool на GPT-5. const t = (title + ' ' + tags.join(' ')).toLowerCase();
* Это работает даже когда /v1/images/generations отдаёт unavailable.
*/
async function generateCoverViaResponses({ prompt }) {
const model = config.ai.imageModelViaResponses || 'gpt-5.2';
// GPT-5 иногда не вызывает инструмент сама — даём явную инструкцию
const wrappedInput = `Use the image_generation tool to create the following illustration. Do not write any text response, only call the tool.\n\n${prompt}`;
const started = Date.now(); const patterns = [
let res; { kw: ['взлом', 'хакер', 'атак', 'уязвим', 'безопасн', 'hack', 'secur', 'cyber', 'exploit', 'inject', 'фишинг'],
try { metaphors: [
res = await axios.post( 'A digital lock being opened by invisible force, streams of binary code dissolving into particles',
`${config.ai.baseUrl}/responses`, 'Abstract shield shattering into geometric fragments, neon cracks spreading through dark digital surface',
{ 'A labyrinth of glowing circuits with a single thread breaking through a barrier',
model, 'Mirror reflecting a distorted version of itself, duality and deception made abstract',
input: wrappedInput, ]
tools: [{ type: 'image_generation' }],
tool_choice: { type: 'image_generation' },
}, },
{ { kw: ['ии', ' ai ', 'нейро', 'llm', 'gpt', 'claude', 'модел', 'neural', 'machine learn', 'deep learn', 'искусственн'],
headers: { Authorization: `Bearer ${config.ai.apiKey}` }, metaphors: [
timeout: 300_000, // GPT-5 reasoning + image — медленно, до 5 минут 'Abstract neural network nodes pulsing with light, interconnected pathways branching into infinity',
'A geometric brain lattice dissolving at the edges into streams of luminous particles',
'Floating data cubes assembling themselves into emergent patterns, representing machine cognition',
'Concentric rings of light expanding outward from a central pulsing node',
]
},
{ kw: ['автомат', 'бот', 'automat', 'workflow', 'pipeline', 'скрипт', 'robot', 'n8n', 'make ', 'zapier'],
metaphors: [
'Elegant gears of light meshing perfectly, each tooth sparking as they turn in unison',
'Abstract conveyor belt transforming raw geometric shapes into refined crystalline forms',
'Overlapping circular flow arrows suggesting perpetual automation, each loop more refined',
]
},
{ kw: ['данн', 'аналит', 'data', 'analyt', 'метрик', 'статист', 'chart', 'график'],
metaphors: [
'Rivers of light converging from edges to a bright focal point, order emerging from streams',
'Translucent scatter plot in 3D space, clusters of meaning floating in abstract dimension',
'Cascading data streams transforming from chaotic noise into clean organized patterns',
]
},
{ kw: ['дипфейк', 'голос', 'deepfake', 'voice', 'fraud', 'fake', 'мошенн'],
metaphors: [
'A face dissolving at the edges into a mosaic of pixels, the real becoming uncertain',
'Abstract mask layered over another mask, shadow figures suggesting layered identity',
'Two mirrors facing each other creating infinite regress, reality and reflection indistinguishable',
]
},
{ kw: ['код', 'разработ', 'програм', 'code', 'develop', 'software', 'api', 'github', 'cursor', 'copilot'],
metaphors: [
'Layers of abstract code symbols transforming into crystalline architectural structures',
'A branching tree of glowing paths, each fork representing choices in logic flow',
'Floating geometric modules snapping together like elegant puzzle pieces in zero gravity',
]
},
{ kw: ['seo', 'маркетинг', 'контент', 'реклам', 'marketing', 'content', 'growth', 'продвиж'],
metaphors: [
'Abstract particles rising in an upward spiral, accelerating as they approach the light',
'Geometric shapes spontaneously organizing from scattered chaos into perfect aligned grid',
'A single beam cutting through layered fog, illuminating a clear path forward',
]
},
{ kw: ['email', 'рассылк', 'письм', 'newsletter', 'inbox'],
metaphors: [
'Paper planes of light streaming outward from a central pulsing source in all directions',
'Abstract envelopes unfolding into origami shapes that take flight as geometric birds',
'Digital waves radiating concentrically outward, each ring more refined and intentional',
]
},
{ kw: ['vector', 'embed', 'pinecone', 'база знаний', 'rag ', 'retriev', 'weaviate', 'pgvector'],
metaphors: [
'Multi-dimensional space: glowing constellation points clustering by invisible forces of meaning',
'Abstract geometric lattice stretching to infinity, nodes connected by silk-thin lines',
'A library of light — glowing geometric books organized in vast abstract infinite space',
]
},
{ kw: ['prompt', 'промпт', 'инжиниринг', 'instruct', 'few-shot', 'chain'],
metaphors: [
'Abstract command flowing as light through a series of transforming geometric gates',
'A sculptor chisel made of light, shaping formless material into precise form',
'Words becoming shapes becoming outcomes — a visual sequence of transformation',
]
},
];
for (const { kw, metaphors } of patterns) {
if (kw.some(k => t.includes(k))) {
// Детерминированный но разнообразный выбор по хешу заголовка
let seed = 0;
for (let i = 0; i < title.length; i++) seed = (seed * 31 + title.charCodeAt(i)) >>> 0;
return metaphors[seed % metaphors.length];
} }
);
} catch (err) {
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'image_via_responses',
model, imageCount: 1,
durationMs: Date.now() - started, succeeded: false,
errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500),
}).catch(() => {});
throw err;
} }
const output = res.data?.output || []; const generic = [
const imgCall = output.find(o => o.type === 'image_generation_call'); 'Abstract architectural forms assembling from scattered fragments into a coherent whole',
if (!imgCall) { 'Geometric light rays converging from multiple directions into a single illuminated focal point',
aiUsage.log({ 'Flowing liquid-like shapes morphing and transforming, suggesting evolution and emergence',
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl), 'A horizon where two contrasting worlds meet — ordered and organic — in perfect visual tension',
requestType: 'image_via_responses', model, imageCount: 1, 'Cascading layers of translucent geometric planes revealing depth and hidden complexity',
durationMs: Date.now() - started, succeeded: false, 'Crystalline growth pattern emerging organically from a single seed point into elaborate structure',
errorMessage: 'No image_generation_call in response output', 'Diagonal force lines cutting through deep negative space with directional energy and purpose',
}).catch(() => {}); 'Nested geometric forms expanding outward from center like a visual echo in still water',
throw new Error('No image_generation_call in response output'); 'Abstract topography of peaks and valleys formed by data, landscape of pure information',
} 'A vortex of geometric elements spiraling inward toward a luminous center of clarity',
if (!imgCall.result) { ];
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'image_via_responses', model, imageCount: 1,
durationMs: Date.now() - started, succeeded: false,
errorMessage: `image_generation_call without result, status=${imgCall.status}`,
}).catch(() => {});
throw new Error(`image_generation_call without result, status=${imgCall.status}`);
}
aiUsage.log({ let seed = 0;
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl), for (let i = 0; i < title.length; i++) seed = (seed * 31 + title.charCodeAt(i)) >>> 0;
requestType: 'image_via_responses', model, imageCount: 1, return generic[seed % generic.length];
requestId: res.data?.id,
durationMs: Date.now() - started, succeeded: true,
}).catch(() => {});
const bytes = Buffer.from(imgCall.result, 'base64');
return {
bytes,
format: imgCall.output_format || 'png',
size: imgCall.size,
revisedPrompt: imgCall.revised_prompt,
};
} }
/**
* Старый путь — на случай если шлюз внезапно починят и захотим вернуться.
*/
async function generateCoverViaImagesEndpoint({ prompt }) {
const model = config.ai.models?.image || 'gpt-image-1';
const started = Date.now();
let res;
try {
res = await axios.post(
`${config.ai.baseUrl}/images/generations`,
{ model, prompt, n: 1, size: '1536x1024' },
{
headers: { Authorization: `Bearer ${config.ai.apiKey}` },
timeout: 120_000,
}
);
} catch (err) {
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'image', model, imageCount: 1,
durationMs: Date.now() - started, succeeded: false,
errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500),
}).catch(() => {});
throw err;
}
const item = res.data?.data?.[0];
if (!item) {
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'image', model, imageCount: 1,
durationMs: Date.now() - started, succeeded: false,
errorMessage: 'Empty image response',
}).catch(() => {});
throw new Error('Empty image response');
}
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'image', model, imageCount: 1,
durationMs: Date.now() - started, succeeded: true,
}).catch(() => {});
if (item.b64_json) return { bytes: Buffer.from(item.b64_json, '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 image data');
}
/**
* Основной путь — /images/generations (aiguoguo199.com или любой OpenAI-совместимый).
* Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2.
*/
/**
* RouterAI /responses + gpt-5-image-mini.
* Единственный провайдер картинок. Цена: ~₽2.72/картинка.
* quality параметр routerai игнорирует — всегда high (4175 image tokens).
*/
async function generateCoverViaRouterAI({ prompt }) { async function generateCoverViaRouterAI({ prompt }) {
const base = config.ai.routeraiBaseUrl; const base = config.ai.routeraiBaseUrl;
const key = config.ai.routeraiApiKey; const key = config.ai.routeraiApiKey;