// ======================================== // MÉTRIQUES DE QUALITÉ - VALIDATION OBJECTIVE // Calculs automatisés de qualité textuelle // ======================================== // Import du LLMManager pour validation IA complémentaire let LLMManager; try { const llmModule = require('../../lib/LLMManager'); if (llmModule && typeof llmModule.callLLM === 'function') { LLMManager = llmModule; } } catch (error) { console.warn('LLMManager non disponible pour QualityMetrics:', error.message); } /** * Système de métriques objectifs pour évaluer la qualité du contenu */ class QualityMetrics { /** * 📊 CALCUL COMPLET MÉTRIQUES - ORCHESTRATEUR PRINCIPAL * * CE QUI EST TESTÉ : * ✅ Calcul 5 métriques objectives automatiques (lisibilité, vocabulaire, structure, cohérence, SEO) * ✅ Score global pondéré équilibré 25% par métrique * ✅ Validation IA complémentaire optionnelle (si LLM disponible) * * ALGORITHMES EXÉCUTÉS : * - 5 calculs en parallèle des métriques objectives * - Pondération : (readability + vocabulary + structure + coherence) * 0.25 chacun * - Si LLM activé : fusion 80% objectif + 20% validation IA * - Retour structure unifiée avec breakdown détaillé */ static async calculateMetrics(content, options = {}) { // Métriques objectives (calculs mathématiques) const objectiveMetrics = { readability: this.calculateReadability(content), vocabulary: this.calculateVocabularyRichness(content), structure: this.calculateStructureMetrics(content), coherence: this.calculateCoherenceMetrics(content), seo: this.calculateSEOMetrics(content), overall: 0 }; // Score global pondéré objectif objectiveMetrics.overall = Math.round( objectiveMetrics.readability * 0.25 + objectiveMetrics.vocabulary * 0.25 + objectiveMetrics.structure * 0.25 + objectiveMetrics.coherence * 0.25 ); // Validation IA complémentaire (si LLM disponible) if (LLMManager && options.enableLLM !== false) { try { const aiValidation = await this.validateQualityWithLLM(content, objectiveMetrics); // Fusion des métriques (privilégier les calculs objectifs) const enhancedMetrics = { ...objectiveMetrics, aiInsights: aiValidation, // Score final: 80% objectif + 20% IA overall: Math.round(objectiveMetrics.overall * 0.8 + aiValidation.score * 0.2) }; return enhancedMetrics; } catch (error) { console.warn('Erreur validation LLM qualité:', error.message); } } return objectiveMetrics; } /** * 🤖 VALIDATION LLM COMPLÉMENTAIRE - ANALYSE QUALITATIVE IA * * CE QUI EST TESTÉ : * ✅ Appel OpenAI pour validation qualitative des métriques objectives * ✅ Évaluation fluidité, clarté, engagement, professionnalisme * ✅ Fusion intelligente scores objectifs + perception IA * * ALGORITHMES EXÉCUTÉS : * - Prompt structuré avec métriques objectives en contexte * - Demande JSON strict : score + breakdown qualitatif + feedback * - Parse robuste avec fallback sur échec JSON * - Clamp scores dans [0,100] et confidence dans [0,1] */ static async validateQualityWithLLM(content, objectiveMetrics) { const prompt = ` MISSION: Valide la qualité de ce contenu et confirme/ajuste les métriques objectives. MÉTRIQUES OBJECTIVES CALCULÉES: - Lisibilité: ${objectiveMetrics.readability}/100 - Vocabulaire: ${objectiveMetrics.vocabulary}/100 - Structure: ${objectiveMetrics.structure}/100 - Cohérence: ${objectiveMetrics.coherence}/100 - Score global: ${objectiveMetrics.overall}/100 CONTENU À ANALYSER: --- ${content.substring(0, 1000)}${content.length > 1000 ? '...[TRONQUÉ]' : ''} --- VALIDATION QUALITATIVE: 1. FLUIDITÉ: Le texte se lit-il naturellement? 2. CLARTÉ: Les idées sont-elles bien exprimées? 3. ENGAGEMENT: Le contenu est-il intéressant? 4. PROFESSIONNALISME: Niveau de qualité rédactionnelle? RÉPONSE JSON STRICTE: { "score": 75, "validation": "confirmed", "qualitativeAssessment": { "fluidity": 80, "clarity": 75, "engagement": 70, "professionalism": 85 }, "feedback": "Analyse qualitative en 2-3 phrases", "confidence": 0.9 } SCORE: 0-100 (qualité globale perçue par un lecteur)`; const response = await LLMManager.callLLM('openai', prompt, { temperature: 0.1, max_tokens: 300 }); try { let jsonStr = response.trim(); const jsonMatch = jsonStr.match(/\{[\s\S]*\}/); if (jsonMatch) { jsonStr = jsonMatch[0]; } const parsed = JSON.parse(jsonStr); return { score: Math.max(0, Math.min(100, parsed.score || 50)), validation: parsed.validation || 'partial', qualitative: parsed.qualitativeAssessment || {}, feedback: parsed.feedback || 'Analyse non disponible', confidence: Math.max(0, Math.min(1, parsed.confidence || 0.5)), source: 'llm' }; } catch (error) { console.warn('Erreur parsing réponse LLM qualité:', error.message); return { score: objectiveMetrics.overall, // Fallback sur score objectif feedback: `Erreur parsing: ${error.message}`, confidence: 0.1, source: 'fallback' }; } } /** * 📚 MÉTRIQUES LISIBILITÉ - ALGORITHME FLESCH-KINCAID FRANÇAIS * * CE QUI EST TESTÉ : * ✅ Comptage précis phrases, mots, syllabes avec regex françaises * ✅ Formule Flesch adaptée : 206.835 - (1.015 * mots/phrase) - (84.6 * syllabes/mot) * ✅ Normalisation [0,100] avec Math.max/Math.min * * ALGORITHMES EXÉCUTÉS : * - Comptage phrases : split(/[.!?]+/) + filter longueur>0 * - Comptage syllabes : regex voyelles françaises [aeiouyàâäéèêëïîôöùûü] * - Ajustements français : -1 si mot finit par 'e', corrections 'eau'/'oui' * - Score final arrondi Math.round() */ static calculateReadability(content) { const sentences = this.countSentences(content); const words = this.countWords(content); const syllables = this.estimateSyllables(content); if (sentences === 0 || words === 0) return 0; // Adaptation française de Flesch-Kincaid const avgWordsPerSentence = words / sentences; const avgSyllablesPerWord = syllables / words; // Formule adaptée pour le français let readabilityScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); // Normalisation 0-100 readabilityScore = Math.max(0, Math.min(100, readabilityScore)); return Math.round(readabilityScore); } /** * 📝 RICHESSE VOCABULAIRE - ALGORITHME DE DIVERSITÉ LEXICALE * * CE QUI EST TESTÉ : * ✅ Ratio mots uniques vs mots totaux (diversité lexicale) * ✅ Bonus longueur moyenne des mots (complexité vocabulaire) * ✅ Pénalité répétitions excessives (>3 occurrences) * * ALGORITHMES EXÉCUTÉS : * - Calcul uniqueness = Set(words.lowercase).size / words.length * - Bonus longueur = Math.min((avgWordLength - 3) * 10, 20) // max 20pts * - Pénalité répétition = (count-3)*2 pour chaque mot>3 occurrences * - Score final = (uniqueness * 80) + bonus - penalty, clamp [0,100] */ static calculateVocabularyRichness(content) { const words = this.getWords(content); const uniqueWords = new Set(words.map(w => w.toLowerCase())); if (words.length === 0) return 0; // Ratio mots uniques / mots totaux const uniquenessRatio = uniqueWords.size / words.length; // Bonus pour la longueur des mots const avgWordLength = words.reduce((sum, word) => sum + word.length, 0) / words.length; const lengthBonus = Math.min((avgWordLength - 3) * 10, 20); // Max 20 points bonus // Pénalité pour répétitions excessives const repetitionPenalty = this.calculateRepetitionPenalty(words); let richness = (uniquenessRatio * 80) + lengthBonus - repetitionPenalty; richness = Math.max(0, Math.min(100, richness)); return Math.round(richness); } /** * 🏢 MÉTRIQUES STRUCTURE - ANALYSE ARCHITECTURE TEXTUELLE * * CE QUI EST TESTÉ : * ✅ Présence et qualité des paragraphes (split \n\n) * ✅ Longueur optimale paragraphes [100-800 chars] * ✅ Détection listes/énumérations (bonus structure) * ✅ Variabilité longueur phrases (coefficient variation) * * ALGORITHMES EXÉCUTÉS : * - Score base = 100, décrément selon défauts * - -20pts si <2 paragraphes, -15pts si >800 chars/parag, -10pts si <100 * - +5pts si listes détectées : regex /[-•*]\s/ ou /\d+\.\s/ * - +15pts max selon variation = écart-type(longueurs) / moyenne */ static calculateStructureMetrics(content) { let score = 100; // Vérification paragraphes const paragraphs = content.split('\n\n').filter(p => p.trim().length > 0); if (paragraphs.length < 2) score -= 20; // Longueur des paragraphes const avgParagraphLength = paragraphs.reduce((sum, p) => sum + p.length, 0) / paragraphs.length; if (avgParagraphLength > 800) score -= 15; // Paragraphes trop longs if (avgParagraphLength < 100) score -= 10; // Paragraphes trop courts // Présence de listes ou énumérations const hasLists = /[-•*]\s/.test(content) || /\d+\.\s/.test(content); if (hasLists) score += 5; // Variété dans la longueur des phrases const sentences = this.getSentences(content); const sentenceLengthVariation = this.calculateVariation(sentences.map(s => s.length)); score += Math.min(sentenceLengthVariation * 10, 15); return Math.max(0, Math.min(100, Math.round(score))); } /** * 🔗 MÉTRIQUES COHÉRENCE - ANALYSE CONNECTEURS LOGIQUES * * CE QUI EST TESTÉ : * ✅ Détection 24 connecteurs français (donc, ainsi, cependant, etc.) * ✅ Calcul ratio connecteurs/phrases (optimum 0.2-0.4) * ✅ Scoring selon courbe : parfait à 0.2-0.4, dégradé en dehors * * ALGORITHMES EXÉCUTÉS : * - Liste connecteurs = ['donc', 'ainsi', 'par conséquent', ...] (24 total) * - Comptage = words.filter(w => connectors.includes(w.toLowerCase())) * - Ratio = connectorCount / sentences.length * - Score : si [0.2,0.4]=100pts, si <0.2=50+(ratio*250), si >0.4=100-((ratio-0.4)*100) */ static calculateCoherenceMetrics(content) { const words = this.getWords(content); const sentences = this.getSentences(content); if (sentences.length < 2) return 50; // Mots de liaison français const connectors = [ 'donc', 'ainsi', 'par conséquent', 'en effet', 'cependant', 'néanmoins', 'toutefois', 'd\'ailleurs', 'en outre', 'de plus', 'également', 'aussi', 'enfin', 'finalement', 'en conclusion', 'premièrement', 'deuxièmement', 'mais', 'or', 'car', 'parce que', 'puisque', 'comme', 'si', 'bien que' ]; // Comptage des connecteurs const connectorCount = words.filter(word => connectors.includes(word.toLowerCase()) ).length; const connectorRatio = connectorCount / sentences.length; // Score basé sur la densité de connecteurs (optimum autour de 0.2-0.4) let coherenceScore; if (connectorRatio >= 0.2 && connectorRatio <= 0.4) { coherenceScore = 100; } else if (connectorRatio < 0.2) { coherenceScore = 50 + (connectorRatio * 250); // Manque de connecteurs } else { coherenceScore = 100 - ((connectorRatio - 0.4) * 100); // Trop de connecteurs } return Math.max(0, Math.min(100, Math.round(coherenceScore))); } /** * 🔍 MÉTRIQUES SEO - ANALYSE OPTIMISATION MOTEURS RECHERCHE * * CE QUI EST TESTÉ : * ✅ Longueur contenu optimale [300-2000 mots] selon standards SEO * ✅ Présence titres HTML ou Markdown ( ou #) * ✅ Détection suroptimisation mots-clés (>5% = pénalité) * * ALGORITHMES EXÉCUTÉS : * - Score base = 100, pénalités selon défauts * - Longueur : -30pts si <300 mots, -15pts si <500, -10pts si >2000 * - Titres : -20pts si pas de regex // ni /^#{1,6}\s/ * - Densité mots-clés : -15pts si estimateKeywordDensity() > 0.05 */ static calculateSEOMetrics(content) { let score = 100; // Longueur du contenu const wordCount = this.countWords(content); if (wordCount < 300) score -= 30; else if (wordCount < 500) score -= 15; else if (wordCount > 2000) score -= 10; // Présence de titres (simulation) const hasTitles = //.test(content) || /^#{1,6}\s/.test(content); if (!hasTitles) score -= 20; // Densité des mots-clés (éviter la suroptimisation) const keywordDensity = this.estimateKeywordDensity(content); if (keywordDensity > 0.05) score -= 15; // Plus de 5% = suroptimisation return Math.max(0, Math.min(100, Math.round(score))); } /** * 🔢 COMPTAGE PHRASES - UTILITAIRE PARSING TEXTUEL * * CE QUI EST TESTÉ : * ✅ Split sur ponctuations finales [.!?] avec support multiples * ✅ Filtrage phrases vides (trim().length > 0) * ✅ Comptage précis pour calculs lisibilité * * ALGORITHME EXÉCUTÉ : * - Regex split : /[.!?]+/ pour gérer '...', '!!', '?!' etc. * - Filter : s => s.trim().length > 0 pour ignorer phrases vides * - Return count final pour ratios mots/phrases */ static countSentences(content) { return content.split(/[.!?]+/).filter(s => s.trim().length > 0).length; } static countWords(content) { return this.getWords(content).length; } static getWords(content) { return content .toLowerCase() .replace(/[^\w\s-]/g, ' ') .split(/\s+/) .filter(word => word.length > 0); } static getSentences(content) { return content.split(/[.!?]+/).filter(s => s.trim().length > 0); } /** * 🗣️ ESTIMATION SYLLABES - ALGORITHME PHONÉTIQUE FRANÇAIS * * CE QUI EST TESTÉ : * ✅ Comptage voyelles françaises avec accents complets * ✅ Ajustements linguistiques : -1 si finit par 'e', corrections diphtongues * ✅ Minimum 1 syllabe par mot (pas de mots à 0 syllabe) * * ALGORITHMES EXÉCUTÉS : * - Regex voyelles = /[aeiouyàâäéèêëïîôöùûü]/gi * - Correction 'e' final : if (word.endsWith('e') && syllables>1) syllables-- * - Correction diphtongues : if ('eau'||'oui') syllables-- * - Math.max(1, syllables) pour éviter 0 */ static estimateSyllables(content) { const words = this.getWords(content); let totalSyllables = 0; words.forEach(word => { // Approximation simple pour le français let syllables = word.match(/[aeiouyàâäéèêëïîôöùûü]/gi); syllables = syllables ? syllables.length : 1; // Ajustements pour le français if (word.endsWith('e') && syllables > 1) syllables--; if (word.includes('eau') || word.includes('oui')) syllables--; totalSyllables += Math.max(1, syllables); }); return totalSyllables; } /** * ⛔ CALCUL PÉNALITÉ RÉPÉTITIONS - DÉTECTEUR REDONDANCE * * CE QUI EST TESTÉ : * ✅ Comptage fréquence mots significatifs (longueur>3) * ✅ Pénalité progressive : 2pts par occurrence au-delà de 3 * ✅ Plafond max 30pts pour éviter pénalités excessives * * ALGORITHMES EXÉCUTÉS : * - Filter mots : word.length > 3 pour ignorer articles/prépositions * - Map fréquences : wordCount[lower] = (wordCount[lower] || 0) + 1 * - Pénalité = Σ((count-3)*2) pour chaque mot avec count>3 * - Return Math.min(penalty, 30) pour plafonner */ static calculateRepetitionPenalty(words) { const wordCount = {}; words.forEach(word => { const lower = word.toLowerCase(); if (lower.length > 3) { // Ignorer les mots trop courts wordCount[lower] = (wordCount[lower] || 0) + 1; } }); let penalty = 0; Object.values(wordCount).forEach(count => { if (count > 3) { penalty += (count - 3) * 2; // 2 points de pénalité par répétition excessive } }); return Math.min(penalty, 30); // Max 30 points de pénalité } /** * 📊 CALCUL VARIATION - COEFFICIENT DE VARIATION STATISTIQUE * * CE QUI EST TESTÉ : * ✅ Calcul écart-type des longueurs de phrases * ✅ Coefficient variation = σ/μ (normalisation par moyenne) * ✅ Mesure diversité structurelle du texte * * ALGORITHMES EXÉCUTÉS : * - Moyenne : avg = Σ(numbers) / numbers.length * - Variance : variance = Σ((n-avg)²) / length * - Écart-type : σ = Math.sqrt(variance) * - Coefficient : cv = σ/avg (variation relative) */ static calculateVariation(numbers) { if (numbers.length < 2) return 0; const avg = numbers.reduce((sum, n) => sum + n, 0) / numbers.length; const variance = numbers.reduce((sum, n) => sum + Math.pow(n - avg, 2), 0) / numbers.length; return Math.sqrt(variance) / avg; // Coefficient de variation } /** * 🎯 ESTIMATION DENSITÉ MOTS-CLÉS - DÉTECTEUR SUROPTIMISATION * * CE QUI EST TESTÉ : * ✅ Détection mot le plus répété (potentiel mot-clé) * ✅ Calcul ratio fréquence_max / total_mots * ✅ Seuil alerte >5% pour détecter suroptimisation SEO * * ALGORITHMES EXÉCUTÉS : * - Filter mots significatifs : word.length > 4 * - Comptage fréquences : wordFreq[word] = count * - Recherche maximum : Math.max(...Object.values(wordFreq)) * - Densité = maxFreq / totalWords (ratio [0,1]) */ static estimateKeywordDensity(content) { const words = this.getWords(content); const totalWords = words.length; if (totalWords === 0) return 0; // Recherche de répétitions potentielles de mots-clés const wordFreq = {}; words.forEach(word => { if (word.length > 4) { // Mots significatifs wordFreq[word] = (wordFreq[word] || 0) + 1; } }); // Trouver le mot le plus répété (potentiel mot-clé) const maxFreq = Math.max(...Object.values(wordFreq)); return maxFreq / totalWords; } /** * ⚡ ANALYSE RAPIDE DASHBOARD - PRESET PERFORMANCE OPTIMISÉ * * CE QUI EST TESTÉ : * ✅ Calculs basiques sans appel LLM (temps <1 seconde) * ✅ Stats essentielles : mots, phrases, lisibilité, ratios * ✅ Classification lisibilité textuelle (Très facile à Très difficile) * * ALGORITHMES EXÉCUTÉS : * - Comptages directs : countWords(), countSentences() * - Ratio : Math.round(words/sentences) pour moyenne * - Lisibilité : calculateReadability() + classification textuelle * - Structure optimisée pour affichage dashboard temps réel */ static quickAnalysis(content) { const words = this.countWords(content); const sentences = this.countSentences(content); const readability = this.calculateReadability(content); return { wordCount: words, sentenceCount: sentences, avgWordsPerSentence: sentences > 0 ? Math.round(words / sentences) : 0, readabilityScore: readability, readabilityLevel: this.getReadabilityLevel(readability) }; } /** * 📈 NIVEAU LISIBILITÉ TEXTUEL - CLASSIFICATEUR SEUILS * * CE QUI EST TESTÉ : * ✅ Conversion score numérique [0-100] en niveau lisible humain * ✅ 7 niveaux définis selon standards éducatifs français * ✅ Seuils optimisés pour compréhension intuitive * * ALGORITHME EXÉCUTÉ : * - Classification par seuils : ≥90='Très facile', ≥80='Facile', etc. * - Ordre décroissant : 90,80,70,60,50,30 puis 'Très difficile' * - Correspondance standards Flesch-Kincaid adaptés français */ static getReadabilityLevel(score) { if (score >= 90) return 'Très facile'; if (score >= 80) return 'Facile'; if (score >= 70) return 'Assez facile'; if (score >= 60) return 'Standard'; if (score >= 50) return 'Assez difficile'; if (score >= 30) return 'Difficile'; return 'Très difficile'; } } module.exports = { QualityMetrics };