seo-generator-server/tests/validators/QualityMetrics.js
Trouve Alexis 870cfb0340 [200~add step-by-step versioning system with Google Sheets integration
- Add intermediate saves (v1.0-v1.4) to Generated_Articles_Versioned
  - Fix compiled_text pipeline (generatedTexts object structure)
  - Add /api/workflow-modulaire endpoint with version tracking
  - Create test-modulaire.html interface with real-time logs
  - Support parent-child linking via Parent_Article_ID
2025-09-06 16:38:20 +08:00

563 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ========================================
// 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 (<h1-6> 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 /<h[1-6]>/ 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 = /<h[1-6]>/.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 };