forked from admin/zeropost-engine
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:
+186
-172
@@ -22,50 +22,91 @@ if (!fs.existsSync(UPLOADS_DIR)) {
|
||||
* один стиль при регенерации, но разные статьи выглядят по-разному.
|
||||
*/
|
||||
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',
|
||||
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',
|
||||
mood: 'warm, grounded, thoughtful, human — like physical craftsmanship',
|
||||
mood: 'warm, grounded, thoughtful, human',
|
||||
composition: 'layered depth with overlapping organic planes, horizon-inspired layout',
|
||||
},
|
||||
{
|
||||
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',
|
||||
mood: 'creative, forward-thinking, imaginative, innovative',
|
||||
composition: 'radial soft composition, overlapping translucent shapes, dreamy depth',
|
||||
},
|
||||
{
|
||||
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',
|
||||
style: 'bold stark geometry, high contrast hard edges, strong typographic-inspired forms, Swiss design influence',
|
||||
mood: 'bold, editorial, authoritative, direct — like a magazine cover',
|
||||
palette: 'near-black (#111827) and white primary, single vivid red (#ef4444) accent on one element only',
|
||||
style: 'bold stark geometry, high contrast hard edges, Swiss design influence, graphic poster style',
|
||||
mood: 'bold, editorial, authoritative, direct',
|
||||
composition: 'strong visual tension, deliberate asymmetry, dominant single focal element',
|
||||
},
|
||||
{
|
||||
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',
|
||||
mood: 'warm, accessible, optimistic, friendly — welcoming and human',
|
||||
mood: 'warm, accessible, optimistic, friendly',
|
||||
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 → индекс стиля.
|
||||
*/
|
||||
@@ -87,34 +128,27 @@ function pickStyleIndex(articleId) {
|
||||
* Рубрика полностью задаёт визуальный язык — ограничения внутри неё.
|
||||
*/
|
||||
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(', ');
|
||||
|
||||
// Если рубрика выбрана — она задаёт весь визуальный язык
|
||||
if (rubric?.prompt) {
|
||||
return `${rubric.prompt}
|
||||
|
||||
Article subject: "${subject}".${tagHint ? ` Theme: ${tagHint}.` : ''}
|
||||
Wide 16:9 format. No text, no letters, no logos, no identifiable real human faces.`;
|
||||
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.`;
|
||||
}
|
||||
|
||||
let styleDesc, paletteDesc, moodDesc, compositionDesc;
|
||||
|
||||
// Если задано несколько стилей через запятую — берём случайный из них
|
||||
const rawStyle = channelStyle?.image_style || '';
|
||||
const styleList = rawStyle.split(',').map(s => s.trim()).filter(s => s && s !== 'auto');
|
||||
const csStyle = styleList.length > 0
|
||||
? styleList[Math.floor(Math.random() * styleList.length)]
|
||||
: null;
|
||||
const csStyle = styleList.length > 0 ? styleList[Math.floor(Math.random() * styleList.length)] : null;
|
||||
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' },
|
||||
'flat-illustration': { style: 'flat vector illustration, clean geometric shapes, modern editorial style, smooth gradients, minimal', 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' },
|
||||
'cartoon': { style: 'cartoon illustration, bold outlines, vibrant colors, expressive shapes, comic book style', 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' },
|
||||
'abstract': { style: 'abstract artwork, layered geometric shapes, conceptual composition, mood and texture focused', mood: 'creative, sophisticated, 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' },
|
||||
'cyberpunk': { style: 'cyberpunk aesthetic, neon glowing lights, futuristic dark atmosphere, Blade Runner vibe', mood: 'bold, futuristic, dark', comp: 'dramatic angles, foreground/background depth' },
|
||||
'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', mood: 'clean, modern', comp: 'balanced, centered focal point' },
|
||||
'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', mood: 'playful, energetic', comp: 'dynamic, high contrast' },
|
||||
'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: 'creative, tech-forward', comp: 'asymmetric layers, visual tension' },
|
||||
'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', mood: 'bold, futuristic, dark', comp: 'dramatic angles, foreground/background depth' },
|
||||
};
|
||||
|
||||
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) {
|
||||
paletteDesc = `custom brand palette: ${channelStyle.image_custom_colors}`;
|
||||
} 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;
|
||||
}
|
||||
|
||||
return `Abstract editorial cover illustration for an article titled "${subject}".
|
||||
const visualMetaphor = getVisualMetaphor(title, tags);
|
||||
|
||||
Style: ${styleDesc}.
|
||||
${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}` : ''}
|
||||
return `Create a wide 16:9 editorial cover illustration.
|
||||
|
||||
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.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует картинку через /v1/responses + image_generation tool на GPT-5.
|
||||
* Это работает даже когда /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}`;
|
||||
function getVisualMetaphor(title, tags = []) {
|
||||
const t = (title + ' ' + tags.join(' ')).toLowerCase();
|
||||
|
||||
const started = Date.now();
|
||||
let res;
|
||||
try {
|
||||
res = await axios.post(
|
||||
`${config.ai.baseUrl}/responses`,
|
||||
{
|
||||
model,
|
||||
input: wrappedInput,
|
||||
tools: [{ type: 'image_generation' }],
|
||||
tool_choice: { type: 'image_generation' },
|
||||
const patterns = [
|
||||
{ kw: ['взлом', 'хакер', 'атак', 'уязвим', 'безопасн', 'hack', 'secur', 'cyber', 'exploit', 'inject', 'фишинг'],
|
||||
metaphors: [
|
||||
'A digital lock being opened by invisible force, streams of binary code dissolving into particles',
|
||||
'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',
|
||||
'Mirror reflecting a distorted version of itself, duality and deception made abstract',
|
||||
]
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${config.ai.apiKey}` },
|
||||
timeout: 300_000, // GPT-5 reasoning + image — медленно, до 5 минут
|
||||
{ kw: ['ии', ' ai ', 'нейро', 'llm', 'gpt', 'claude', 'модел', 'neural', 'machine learn', 'deep learn', 'искусственн'],
|
||||
metaphors: [
|
||||
'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 imgCall = output.find(o => o.type === 'image_generation_call');
|
||||
if (!imgCall) {
|
||||
aiUsage.log({
|
||||
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
|
||||
requestType: 'image_via_responses', model, imageCount: 1,
|
||||
durationMs: Date.now() - started, succeeded: false,
|
||||
errorMessage: 'No image_generation_call in response output',
|
||||
}).catch(() => {});
|
||||
throw new Error('No image_generation_call in response output');
|
||||
}
|
||||
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}`);
|
||||
const generic = [
|
||||
'Abstract architectural forms assembling from scattered fragments into a coherent whole',
|
||||
'Geometric light rays converging from multiple directions into a single illuminated focal point',
|
||||
'Flowing liquid-like shapes morphing and transforming, suggesting evolution and emergence',
|
||||
'A horizon where two contrasting worlds meet — ordered and organic — in perfect visual tension',
|
||||
'Cascading layers of translucent geometric planes revealing depth and hidden complexity',
|
||||
'Crystalline growth pattern emerging organically from a single seed point into elaborate structure',
|
||||
'Diagonal force lines cutting through deep negative space with directional energy and purpose',
|
||||
'Nested geometric forms expanding outward from center like a visual echo in still water',
|
||||
'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',
|
||||
];
|
||||
|
||||
let seed = 0;
|
||||
for (let i = 0; i < title.length; i++) seed = (seed * 31 + title.charCodeAt(i)) >>> 0;
|
||||
return generic[seed % generic.length];
|
||||
}
|
||||
|
||||
aiUsage.log({
|
||||
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
|
||||
requestType: 'image_via_responses', model, imageCount: 1,
|
||||
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 }) {
|
||||
const base = config.ai.routeraiBaseUrl;
|
||||
const key = config.ai.routeraiApiKey;
|
||||
|
||||
Reference in New Issue
Block a user