410 lines
13 KiB
JavaScript
410 lines
13 KiB
JavaScript
// ========================================
|
|
// 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 des métriques de qualité
|
|
*/
|
|
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 des métriques
|
|
*/
|
|
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 de lisibilité (basées sur Flesch-Kincaid adapté au français)
|
|
*/
|
|
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 du vocabulaire
|
|
*/
|
|
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 de structure
|
|
*/
|
|
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 de cohérence (approximation basée sur mots de liaison)
|
|
*/
|
|
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 basiques
|
|
*/
|
|
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)));
|
|
}
|
|
|
|
/**
|
|
* Utilitaires de comptage
|
|
*/
|
|
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 du nombre de syllabes (approximation française)
|
|
*/
|
|
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 de la pénalité pour répétitions
|
|
*/
|
|
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 de variation (écart-type simplifié)
|
|
*/
|
|
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 basique de la densité des mots-clés
|
|
*/
|
|
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 pour dashboard
|
|
*/
|
|
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 de lisibilité textuel
|
|
*/
|
|
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 }; |