// ======================================== // É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 };