seogeneratorserver/lib/generation/StyleEnhancement.js
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

340 lines
10 KiB
JavaScript

// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ
// Responsabilité: Appliquer le style personnalité avec Mistral
// LLM: Mistral (température 0.8)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyle(input) {
return await tracer.run('StyleEnhancement.applyPersonalityStyle()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎭 ÉTAPE 4/4: Enhancement style ${csvData.personality?.nom} (Mistral)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks
const styledResults = await applyStyleInChunks(styleElements, csvData);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
}
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks
*/
async function applyStyleInChunks(styleElements, csvData) {
logSh(`🎨 Stylisation: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const stylePrompt = createStylePrompt(chunk, csvData);
const styledResponse = await callLLM('mistral', stylePrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: garder contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyle, // ← MAIN ENTRY POINT
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunks,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};