// ======================================== // STYLE LAYER - COUCHE STYLE MODULAIRE // Responsabilité: Adaptation personnalité modulaire réutilisable // LLM: Mistral (excellence style et personnalité) // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { chunkArray, sleep } = require('./SelectiveUtils'); /** * COUCHE STYLE MODULAIRE */ class StyleLayer { constructor() { this.name = 'StyleEnhancement'; this.defaultLLM = 'mistral'; this.priority = 3; // Priorité basse - appliqué en dernier } /** * MAIN METHOD - Appliquer amélioration style */ async apply(content, config = {}) { return await tracer.run('StyleLayer.apply()', async () => { const { llmProvider = this.defaultLLM, intensity = 1.0, // 0.0-2.0 intensité d'amélioration analysisMode = true, // Analyser avant d'appliquer csvData = null, preserveStructure = true, targetStyle = null // Style spécifique à appliquer } = config; await tracer.annotate({ styleLayer: true, llmProvider, intensity, elementsCount: Object.keys(content).length, personality: csvData?.personality?.nom }); const startTime = Date.now(); logSh(`🎨 STYLE LAYER: Amélioration personnalité (${llmProvider})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments | Style: ${csvData?.personality?.nom || 'standard'}`, 'INFO'); try { let enhancedContent = {}; let elementsProcessed = 0; let elementsEnhanced = 0; // Vérifier présence personnalité if (!csvData?.personality && !targetStyle) { logSh(`⚠️ STYLE LAYER: Pas de personnalité définie, style générique appliqué`, 'WARNING'); } if (analysisMode) { // 1. Analyser éléments nécessitant amélioration style const analysis = await this.analyzeStyleNeeds(content, csvData, targetStyle); logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG'); if (analysis.candidates.length === 0) { logSh(`✅ STYLE LAYER: Style déjà cohérent`, 'INFO'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, analysisSkipped: true, duration: Date.now() - startTime } }; } // 2. Améliorer les éléments sélectionnés const improvedResults = await this.enhanceStyleElements( analysis.candidates, csvData, { llmProvider, intensity, preserveStructure, targetStyle } ); // 3. Merger avec contenu original enhancedContent = { ...content }; Object.keys(improvedResults).forEach(tag => { if (improvedResults[tag] !== content[tag]) { enhancedContent[tag] = improvedResults[tag]; elementsEnhanced++; } }); elementsProcessed = analysis.candidates.length; } else { // Mode direct : améliorer tous les éléments textuels const textualElements = Object.entries(content) .filter(([tag, text]) => text.length > 50 && !tag.includes('FAQ_Question')) .map(([tag, text]) => ({ tag, content: text, styleIssues: ['adaptation_générale'] })); if (textualElements.length === 0) { return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } }; } const improvedResults = await this.enhanceStyleElements( textualElements, csvData, { llmProvider, intensity, preserveStructure, targetStyle } ); enhancedContent = { ...content }; Object.keys(improvedResults).forEach(tag => { if (improvedResults[tag] !== content[tag]) { enhancedContent[tag] = improvedResults[tag]; elementsEnhanced++; } }); elementsProcessed = textualElements.length; } const duration = Date.now() - startTime; const stats = { processed: elementsProcessed, enhanced: elementsEnhanced, total: Object.keys(content).length, enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100, duration, llmProvider, intensity, personalityApplied: csvData?.personality?.nom || targetStyle || 'générique' }; logSh(`✅ STYLE LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} stylisés (${duration}ms)`, 'INFO'); await tracer.event('Style layer appliquée', stats); return { content: enhancedContent, stats }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ STYLE LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback gracieux : retourner contenu original logSh(`🔄 Fallback: style original préservé`, 'WARNING'); return { content, stats: { fallback: true, duration }, error: error.message }; } }, { content: Object.keys(content), config }); } /** * ANALYSER BESOINS STYLE */ async analyzeStyleNeeds(content, csvData, targetStyle = null) { logSh(`🎨 Analyse besoins style`, 'DEBUG'); const analysis = { candidates: [], globalScore: 0, styleIssues: { genericLanguage: 0, personalityMismatch: 0, inconsistentTone: 0, missingVocabulary: 0 } }; const personality = csvData?.personality; const expectedStyle = targetStyle || personality; // Analyser chaque élément Object.entries(content).forEach(([tag, text]) => { const elementAnalysis = this.analyzeStyleElement(text, expectedStyle, csvData); if (elementAnalysis.needsImprovement) { analysis.candidates.push({ tag, content: text, styleIssues: elementAnalysis.issues, score: elementAnalysis.score, improvements: elementAnalysis.improvements }); analysis.globalScore += elementAnalysis.score; // Compter types d'issues elementAnalysis.issues.forEach(issue => { if (analysis.styleIssues.hasOwnProperty(issue)) { analysis.styleIssues[issue]++; } }); } }); analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1); logSh(` 📊 Score global style: ${analysis.globalScore.toFixed(2)}`, 'DEBUG'); logSh(` 🎭 Issues style: ${JSON.stringify(analysis.styleIssues)}`, 'DEBUG'); return analysis; } /** * AMÉLIORER ÉLÉMENTS STYLE SÉLECTIONNÉS */ async enhanceStyleElements(candidates, csvData, config) { logSh(`🎨 Amélioration ${candidates.length} éléments style`, 'DEBUG'); const results = {}; const chunks = chunkArray(candidates, 5); // Chunks optimisés pour Mistral for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk style ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const enhancementPrompt = this.createStyleEnhancementPrompt(chunk, csvData, config); const response = await callLLM(config.llmProvider, enhancementPrompt, { temperature: 0.8, // Créativité élevée pour style maxTokens: 3000 }, csvData?.personality); const chunkResults = this.parseStyleResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk style ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1800); } } catch (error) { logSh(` ❌ Chunk style ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: conserver contenu original chunk.forEach(element => { results[element.tag] = element.content; }); } } return results; } // ============= HELPER METHODS ============= /** * Analyser élément style individuel */ analyzeStyleElement(text, expectedStyle, csvData) { let score = 0; const issues = []; const improvements = []; // Si pas de style attendu, score faible if (!expectedStyle) { return { needsImprovement: false, score: 0.1, issues: ['pas_style_défini'], improvements: [] }; } // 1. Analyser langage générique const genericScore = this.analyzeGenericLanguage(text); if (genericScore > 0.4) { score += 0.3; issues.push('genericLanguage'); improvements.push('personnaliser_vocabulaire'); } // 2. Analyser adéquation personnalité if (expectedStyle.vocabulairePref) { const personalityScore = this.analyzePersonalityAlignment(text, expectedStyle); if (personalityScore < 0.3) { score += 0.4; issues.push('personalityMismatch'); improvements.push('appliquer_style_personnalité'); } } // 3. Analyser cohérence de ton const toneScore = this.analyzeToneConsistency(text, expectedStyle); if (toneScore > 0.5) { score += 0.2; issues.push('inconsistentTone'); improvements.push('unifier_ton'); } // 4. Analyser vocabulaire spécialisé if (expectedStyle.niveauTechnique) { const vocabScore = this.analyzeVocabularyLevel(text, expectedStyle); if (vocabScore > 0.4) { score += 0.1; issues.push('missingVocabulary'); improvements.push('ajuster_niveau_vocabulaire'); } } return { needsImprovement: score > 0.3, score, issues, improvements }; } /** * Analyser langage générique */ analyzeGenericLanguage(text) { const genericPhrases = [ 'nos solutions', 'notre expertise', 'notre savoir-faire', 'nous vous proposons', 'nous mettons à votre disposition', 'qualité optimale', 'service de qualité', 'expertise reconnue' ]; let genericCount = 0; genericPhrases.forEach(phrase => { if (text.toLowerCase().includes(phrase)) genericCount++; }); const wordCount = text.split(/\s+/).length; return Math.min(1, (genericCount / Math.max(wordCount / 50, 1))); } /** * Analyser alignement personnalité */ analyzePersonalityAlignment(text, personality) { if (!personality.vocabulairePref) return 1; const preferredWords = personality.vocabulairePref.toLowerCase().split(','); const contentLower = text.toLowerCase(); let alignmentScore = 0; preferredWords.forEach(word => { if (word.trim() && contentLower.includes(word.trim())) { alignmentScore++; } }); return Math.min(1, alignmentScore / Math.max(preferredWords.length, 1)); } /** * Analyser cohérence de ton */ analyzeToneConsistency(text, expectedStyle) { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10); if (sentences.length < 2) return 0; const tones = sentences.map(sentence => this.detectSentenceTone(sentence)); const expectedTone = this.getExpectedTone(expectedStyle); let inconsistencies = 0; tones.forEach(tone => { if (tone !== expectedTone && tone !== 'neutral') { inconsistencies++; } }); return inconsistencies / tones.length; } /** * Analyser niveau vocabulaire */ analyzeVocabularyLevel(text, expectedStyle) { const technicalWords = text.match(/\b\w{8,}\b/g) || []; const expectedLevel = expectedStyle.niveauTechnique || 'standard'; const techRatio = technicalWords.length / text.split(/\s+/).length; switch (expectedLevel) { case 'accessible': return techRatio > 0.1 ? techRatio : 0; // Trop technique case 'expert': return techRatio < 0.05 ? 1 - techRatio : 0; // Pas assez technique default: return techRatio > 0.15 || techRatio < 0.02 ? Math.abs(0.08 - techRatio) : 0; } } /** * Détecter ton de phrase */ detectSentenceTone(sentence) { const lowerSentence = sentence.toLowerCase(); if (/\b(excellent|remarquable|exceptionnel|parfait)\b/.test(lowerSentence)) return 'enthusiastic'; if (/\b(il convient|nous recommandons|il est conseillé)\b/.test(lowerSentence)) return 'formal'; if (/\b(sympa|cool|nickel|top)\b/.test(lowerSentence)) return 'casual'; if (/\b(technique|précision|spécification)\b/.test(lowerSentence)) return 'technical'; return 'neutral'; } /** * Obtenir ton attendu selon personnalité */ getExpectedTone(personality) { if (!personality || !personality.style) return 'neutral'; const style = personality.style.toLowerCase(); if (style.includes('technique') || style.includes('expert')) return 'technical'; if (style.includes('commercial') || style.includes('vente')) return 'enthusiastic'; if (style.includes('décontracté') || style.includes('moderne')) return 'casual'; if (style.includes('professionnel') || style.includes('formel')) return 'formal'; return 'neutral'; } /** * Créer prompt amélioration style */ createStyleEnhancementPrompt(chunk, csvData, config) { const personality = csvData?.personality || config.targetStyle; let prompt = `MISSION: Adapte UNIQUEMENT le style et la personnalité de ces contenus. CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'} ${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : 'STYLE: Professionnel standard'} ${personality?.description ? `DESCRIPTION: ${personality.description}` : ''} INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif) CONTENUS À STYLISER: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} PROBLÈMES: ${item.styleIssues.join(', ')} CONTENU: "${item.content}"`).join('\n\n')} PROFIL PERSONNALITÉ ${personality?.nom || 'Standard'}: ${personality ? `- Style: ${personality.style} - Niveau: ${personality.niveauTechnique || 'standard'} - Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel'} - Connecteurs: ${personality.connecteursPref || 'variés'} ${personality.specificites ? `- Spécificités: ${personality.specificites}` : ''}` : '- Style professionnel web standard'} OBJECTIFS STYLE: - Appliquer personnalité ${personality?.nom || 'standard'} de façon naturelle - Utiliser vocabulaire et expressions caractéristiques - Maintenir cohérence de ton sur tout le contenu - Adapter niveau technique selon profil (${personality?.niveauTechnique || 'standard'}) - Style web ${personality?.style || 'professionnel'} mais authentique CONSIGNES STRICTES: - NE CHANGE PAS le fond du message ni les informations factuelles - GARDE même structure et longueur approximative (±15%) - Applique SEULEMENT style et personnalité sur la forme - RESPECTE impérativement le niveau ${personality?.niveauTechnique || 'standard'} - ÉVITE exagération qui rendrait artificiel TECHNIQUES STYLE: ${personality?.vocabulairePref ? `- Intégrer naturellement: ${personality.vocabulairePref}` : '- Vocabulaire professionnel équilibré'} - Adapter registre de langue selon ${personality?.style || 'professionnel'} - Expressions et tournures caractéristiques personnalité - Ton cohérent: ${this.getExpectedTone(personality)} mais naturel - Connecteurs préférés: ${personality?.connecteursPref || 'variés et naturels'} FORMAT RÉPONSE: [1] Contenu avec style personnalisé [2] Contenu avec style personnalisé etc... IMPORTANT: Réponse DIRECTE par les contenus stylisés, pas d'explication.`; return prompt; } /** * Parser réponse style */ 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 contenu stylisé styledContent = this.cleanStyleContent(styledContent); if (styledContent && styledContent.length > 10) { results[element.tag] = styledContent; logSh(`✅ Stylisé [${element.tag}]: "${styledContent.substring(0, 60)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; // Fallback logSh(`⚠️ Fallback style [${element.tag}]: amélioration 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 style généré */ cleanStyleContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+(stylisé|adapté|personnalisé)\s*[:.]?\s*/gi, ''); content = content.replace(/^(avec\s+)?style\s+[^:]*\s*[:.]?\s*/gi, ''); content = content.replace(/^(dans\s+le\s+style\s+de\s+)[^:]*[:.]?\s*/gi, ''); // Nettoyer formatage content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown content = content.replace(/\s{2,}/g, ' '); // Espaces multiples content = content.trim(); return content; } } module.exports = { StyleLayer };