feat(covers): концепт обложки по смыслу статьи (LLM)
Вместо общей метафоры по ключевому слову — haiku придумывает конкретную визуальную сцену под конкретную статью (generateVisualConcept). Это убирает 'обложки не по теме'. При ошибке LLM — откат на getVisualMetaphor.
This commit is contained in:
+30
-3
@@ -5,6 +5,7 @@ const config = require('../config');
|
|||||||
const { query } = require('../config/db');
|
const { query } = require('../config/db');
|
||||||
const localGen = require('./localCoverGenerator');
|
const localGen = require('./localCoverGenerator');
|
||||||
const aiUsage = require('./aiUsage');
|
const aiUsage = require('./aiUsage');
|
||||||
|
const ai = require('./ai');
|
||||||
|
|
||||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||||
|
|
||||||
@@ -122,12 +123,31 @@ function pickStyleIndex(articleId) {
|
|||||||
/**
|
/**
|
||||||
* Промпт для обложки — стиль выбирается по articleId, содержание по теме статьи.
|
* Промпт для обложки — стиль выбирается по articleId, содержание по теме статьи.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Концепт обложки от LLM — придумывает конкретную визуальную сцену ПО СМЫСЛУ статьи.
|
||||||
|
* Дешёвый haiku-вызов. Это устраняет «обложки не по теме»: вместо общей метафоры
|
||||||
|
* по ключевому слову модель читает заголовок и предлагает сцену именно под эту статью.
|
||||||
|
* При ошибке возвращает null → buildCoverPrompt откатится на getVisualMetaphor.
|
||||||
|
*/
|
||||||
|
async function generateVisualConcept({ title, tags = [], excerpt = '' }) {
|
||||||
|
try {
|
||||||
|
const sys = 'You are an art director for a tech/AI blog. Given an article, invent ONE concrete, literal visual scene for its cover that clearly fits THIS article\'s specific topic. One clear subject or visual metaphor, photographable or illustratable, with a setting and lighting. Avoid generic "robot at a desk" clichés. STRICTLY: no text, letters, numbers, logos, UI, or real human faces. Answer in English, 1-2 sentences, only the scene description.';
|
||||||
|
const user = `Title: "${title}"\nTags: ${(tags || []).join(', ') || 'none'}${excerpt ? `\nExcerpt: ${excerpt.slice(0, 400)}` : ''}\n\nCover scene:`;
|
||||||
|
const res = await ai.chat(config.ai.models.post || 'claude-haiku-4-5-20251001', sys, user, { maxTokens: 120, temperature: 0.9 });
|
||||||
|
const text = (res?.text || '').trim().replace(/^["']|["']$/g, '');
|
||||||
|
return text.length > 10 ? text : null;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Cover] visual concept LLM failed, fallback to metaphor:', (err.message || '').slice(0, 80));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Промпт для обложки.
|
* Промпт для обложки.
|
||||||
* Приоритет: rubric.prompt → channelStyle.image_style → COVER_STYLES rotation.
|
* Приоритет: rubric.prompt → channelStyle.image_style → COVER_STYLES rotation.
|
||||||
* Рубрика полностью задаёт визуальный язык — ограничения внутри неё.
|
* Рубрика полностью задаёт визуальный язык — ограничения внутри неё.
|
||||||
*/
|
*/
|
||||||
function buildCoverPrompt({ title, tags = [], articleId = 0, channelStyle = null, rubric = null }) {
|
function buildCoverPrompt({ title, tags = [], articleId = 0, channelStyle = null, rubric = null, concept = 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(', ');
|
||||||
|
|
||||||
@@ -196,7 +216,7 @@ function buildCoverPrompt({ title, tags = [], articleId = 0, channelStyle = null
|
|||||||
lighting: 'soft side window light', temp: 'warm brick reds and natural wood' },
|
lighting: 'soft side window light', temp: 'warm brick reds and natural wood' },
|
||||||
];
|
];
|
||||||
const scene = SCENES[articleId % SCENES.length];
|
const scene = SCENES[articleId % SCENES.length];
|
||||||
const visualConcept = getVisualMetaphor(title, tags, articleId);
|
const visualConcept = concept || getVisualMetaphor(title, tags, articleId);
|
||||||
|
|
||||||
return `Generate a 16:9 editorial cover photograph or illustration.
|
return `Generate a 16:9 editorial cover photograph or illustration.
|
||||||
|
|
||||||
@@ -557,7 +577,14 @@ async function generateCover({ articleId, title, tags = [], channelId = null })
|
|||||||
console.log(`[Cover] article=${articleId} channel=${channelId || 'none'} style=${styleName}`);
|
console.log(`[Cover] article=${articleId} channel=${channelId || 'none'} style=${styleName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = buildCoverPrompt({ title, tags, articleId, channelStyle, rubric: selectedRubric });
|
// Концепт обложки ПО СМЫСЛУ статьи (LLM). Только без жёсткой рубрики.
|
||||||
|
let concept = null;
|
||||||
|
if (!selectedRubric) {
|
||||||
|
concept = await generateVisualConcept({ title, tags });
|
||||||
|
if (concept) console.log(`[Cover] article=${articleId} concept="${concept.slice(0, 90)}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = buildCoverPrompt({ title, tags, articleId, channelStyle, rubric: selectedRubric, concept });
|
||||||
|
|
||||||
let img = null;
|
let img = null;
|
||||||
let usedPath = 'routerai';
|
let usedPath = 'routerai';
|
||||||
|
|||||||
Reference in New Issue
Block a user