340 lines
10 KiB
JavaScript
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
|
|
}; |