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 localGen = require('./localCoverGenerator');
|
||||
const aiUsage = require('./aiUsage');
|
||||
const ai = require('./ai');
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
||||
|
||||
@@ -122,12 +123,31 @@ function pickStyleIndex(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.
|
||||
* Рубрика полностью задаёт визуальный язык — ограничения внутри неё.
|
||||
*/
|
||||
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 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' },
|
||||
];
|
||||
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.
|
||||
|
||||
@@ -557,7 +577,14 @@ async function generateCover({ articleId, title, tags = [], channelId = null })
|
||||
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 usedPath = 'routerai';
|
||||
|
||||
Reference in New Issue
Block a user