seo-generator-server/tests/validators/AntiDetectionValidator.js

492 lines
16 KiB
JavaScript

// ========================================
// VALIDATEUR ANTI-DÉTECTION IA
// Détection des patterns typiques d'IA et évaluation de naturalité
// ========================================
// Import du LLMManager pour validation IA
let LLMManager;
try {
const llmModule = require('../../lib/LLMManager');
if (llmModule && typeof llmModule.callLLM === 'function') {
LLMManager = llmModule;
}
} catch (error) {
console.warn('LLMManager non disponible pour AntiDetectionValidator:', error.message);
}
/**
* Validateur spécialisé dans la détection d'empreintes IA
* Score de naturalité et détection de patterns suspects
*/
class AntiDetectionValidator {
// Base de données des empreintes IA suspectes
static AI_FINGERPRINTS = {
// Mots/expressions typiquement IA (niveau critique)
critical: {
english: [
'comprehensive', 'robust', 'seamless', 'innovative', 'cutting-edge',
'state-of-the-art', 'furthermore', 'moreover', 'in conclusion',
'it is important to note', 'it\'s worth noting', 'additionally'
],
french: [
'complet et exhaustif', 'robuste et fiable', 'innovant et révolutionnaire',
'de pointe', 'il est important de souligner', 'il convient de noter',
'en outre', 'par ailleurs', 'en définitive', 'en fin de compte'
]
},
// Patterns de structure suspects (niveau élevé)
high: {
patterns: [
/^(Premièrement|Deuxièmement|Troisièmement).+(Deuxièmement|Troisièmement|Enfin)/s,
/En conclusion.+(il est|nous pouvons|il convient)/i,
/(Il est important|Il faut souligner|Il convient de noter).+(que|de)/gi,
/^\d+\.\s.+\n\d+\.\s.+\n\d+\.\s/m // Listes numérotées systématiques
],
phrases: [
'permet de', 'contribue à', 'favorise le développement',
'optimise les performances', 'améliore significativement'
]
},
// Indicateurs moyens (niveau modéré)
moderate: {
overused: [
'cependant', 'néanmoins', 'toutefois', 'ainsi', 'donc',
'par conséquent', 'en effet', 'effectivement', 'naturellement'
],
formal: [
'il s\'avère que', 'force est de constater', 'il ressort que',
'on peut considérer', 'il est possible de'
]
}
};
// Patterns de transitions trop parfaites
static PERFECT_TRANSITIONS = [
/\.\s+(Cependant|Néanmoins|Toutefois|Par ailleurs),/g,
/\.\s+(En outre|De plus|Par ailleurs),/g,
/\.\s+(Ainsi|Donc|Par conséquent),/g,
/\.\s+(En effet|Effectivement|Naturellement),/g
];
// Structures de phrases trop régulières
static REGULAR_PATTERNS = [
/^[A-Z][^.]{50,80}\./gm, // Phrases de longueur trop uniforme
/^[A-Z][^.]+, [^.]+, [^.]+\./gm, // Structure triple répétitive
];
/**
* Validation anti-détection principale
*/
static async validateAntiDetection(content, options = {}) {
// Analyse statique (patterns)
const staticAnalysis = {
fingerprintScore: this.analyzeFingerprintScore(content),
variabilityScore: this.analyzeVariabilityScore(content),
naturalness: this.analyzeNaturalness(content),
humanErrors: this.detectHumanLikeErrors(content),
overallScore: 0,
details: {}
};
// Calcul score global pondéré avec vérification NaN
const fp = isNaN(staticAnalysis.fingerprintScore?.score) ? 50 : staticAnalysis.fingerprintScore.score;
const vs = isNaN(staticAnalysis.variabilityScore) ? 50 : staticAnalysis.variabilityScore;
const nat = isNaN(staticAnalysis.naturalness) ? 50 : staticAnalysis.naturalness;
const he = isNaN(staticAnalysis.humanErrors) ? 50 : staticAnalysis.humanErrors;
staticAnalysis.overallScore = Math.round(
fp * 0.3 + vs * 0.25 + nat * 0.25 + he * 0.2
);
// Classification du niveau de détection
staticAnalysis.detectionRisk = this.calculateDetectionRisk(staticAnalysis.overallScore);
// Validation IA complémentaire (si LLM disponible)
let aiAnalysis = null;
if (LLMManager && options.enableLLM !== false) {
try {
aiAnalysis = await this.validateWithLLM(content);
// Fusion des scores (moyenne pondérée)
const combinedScore = Math.round(staticAnalysis.overallScore * 0.7 + aiAnalysis.score * 0.3);
return {
...staticAnalysis,
overallScore: combinedScore,
aiAnalysis: aiAnalysis,
detectionRisk: this.calculateDetectionRisk(combinedScore)
};
} catch (error) {
console.warn('Erreur validation LLM anti-détection:', error.message);
}
}
return staticAnalysis;
}
/**
* Validation avec LLM pour analyse qualitative
*/
static async validateWithLLM(content) {
const prompt = `
MISSION: Analyse si ce texte a été généré par une IA ou écrit par un humain.
TEXTE À ANALYSER:
---
${content.substring(0, 1500)}${content.length > 1500 ? '...[TRONQUÉ]' : ''}
---
CRITÈRES D'ANALYSE:
1. EMPREINTES IA: Expressions typiques ("comprehensive", "robuste", "il convient de noter", transitions parfaites)
2. VARIABILITÉ: Longueur des phrases, structures variées, vocabulaire diversifié
3. NATURALITÉ: Hésitations, nuances, imperfections subtiles, ton spontané
4. FLUIDITÉ: Transitions naturelles vs mécaniques
RÉPONSE JSON STRICTE:
{
"score": 85,
"humanLikelihood": "high",
"aiFingerprints": ["expressions trouvées"],
"naturalElements": ["éléments humains détectés"],
"feedback": "Analyse détaillée en 2-3 phrases",
"confidence": 0.8
}
SCORE: 0-100 (0=clairement IA, 100=clairement humain)`;
const response = await LLMManager.callLLM('openai', prompt, {
temperature: 0.1,
max_tokens: 500
});
try {
// Parse JSON de la réponse
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)),
likelihood: parsed.humanLikelihood || 'medium',
fingerprints: parsed.aiFingerprints || [],
naturalElements: parsed.naturalElements || [],
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 anti-détection:', error.message);
return {
score: 50,
feedback: `Erreur parsing: ${error.message}`,
confidence: 0.1,
source: 'fallback'
};
}
}
/**
* Analyse des empreintes IA suspectes
*/
static analyzeFingerprintScore(content) {
let score = 100;
const details = {
critical: [],
high: [],
moderate: []
};
const lowerContent = content.toLowerCase();
// Vérification empreintes critiques
this.AI_FINGERPRINTS.critical.english.forEach(phrase => {
const count = (lowerContent.match(new RegExp(phrase.toLowerCase(), 'g')) || []).length;
if (count > 0) {
score -= count * 15; // -15 points par occurrence critique
details.critical.push({ phrase, count });
}
});
this.AI_FINGERPRINTS.critical.french.forEach(phrase => {
const count = (lowerContent.match(new RegExp(phrase.toLowerCase(), 'g')) || []).length;
if (count > 0) {
score -= count * 15;
details.critical.push({ phrase, count });
}
});
// Vérification patterns suspects
this.AI_FINGERPRINTS.high.patterns.forEach(pattern => {
const matches = content.match(pattern);
if (matches) {
score -= matches.length * 10; // -10 points par pattern
details.high.push({ pattern: pattern.toString(), matches: matches.length });
}
});
// Vérification phrases surutilisées
this.AI_FINGERPRINTS.moderate.overused.forEach(phrase => {
const count = (lowerContent.split(phrase.toLowerCase()).length - 1);
if (count > 2) { // Plus de 2 occurrences = suspect
score -= (count - 2) * 5;
details.moderate.push({ phrase, count });
}
});
return {
score: Math.max(0, score),
details: details
};
}
/**
* Analyse de la variabilité linguistique
*/
static analyzeVariabilityScore(content) {
const sentences = this.getSentences(content);
if (sentences.length < 3) return 50;
// Variabilité longueur des phrases
const lengths = sentences.map(s => s.trim().length);
const lengthVariation = this.calculateCoeffVariation(lengths);
// Variabilité structure (début de phrase)
const starters = sentences.map(s => s.trim().substring(0, 10).toLowerCase());
const uniqueStarters = new Set(starters).size;
const starterVariation = uniqueStarters / sentences.length;
// Variabilité ponctuation
const punctuationTypes = this.analyzePunctuationVariety(content);
// Score combiné
let variabilityScore = 0;
variabilityScore += Math.min(lengthVariation * 30, 30);
variabilityScore += starterVariation * 40;
variabilityScore += punctuationTypes * 10;
// Pénalité pour transitions trop parfaites
const perfectTransitions = this.countPerfectTransitions(content);
variabilityScore -= perfectTransitions * 5;
return Math.max(0, Math.min(100, Math.round(variabilityScore)));
}
/**
* Analyse de naturalité (imperfections humaines)
*/
static analyzeNaturalness(content) {
let naturalness = 50; // Score de base
// Présence d'hésitations ou nuances humaines
const hesitationMarkers = [
'peut-être', 'probablement', 'il semble que', 'on dirait que',
'j\'ai l\'impression', 'si je ne me trompe', 'en quelque sorte'
];
hesitationMarkers.forEach(marker => {
if (content.toLowerCase().includes(marker)) {
naturalness += 8;
}
});
// Présence d'expressions familières
const colloquialisms = [
'du coup', 'en fait', 'franchement', 'carrément',
'pas mal de', 'un tas de', 'histoire de'
];
colloquialisms.forEach(expr => {
if (content.toLowerCase().includes(expr)) {
naturalness += 5;
}
});
// Variation dans les connecteurs (évite la répétition)
const connectorVariety = this.analyzeConnectorVariety(content);
naturalness += connectorVariety;
// Présence de parenthèses ou commentaires entre tirets
const asides = (content.match(/\([^)]+\)/g) || []).length +
(content.match(/—[^—]+—/g) || []).length;
naturalness += Math.min(asides * 3, 15);
return Math.max(0, Math.min(100, Math.round(naturalness)));
}
/**
* Détection d'erreurs/imperfections humaines positives
*/
static detectHumanLikeErrors(content) {
let humanScore = 20; // Score de base faible
// Phrases incomplètes (positif pour naturalité)
const incompleteMarkers = content.match(/\.\.\./g) || [];
humanScore += Math.min(incompleteMarkers.length * 10, 30);
// Questions rhétoriques
const questions = (content.match(/\?/g) || []).length;
humanScore += Math.min(questions * 8, 25);
// Exclamations (modération)
const exclamations = (content.match(/!/g) || []).length;
if (exclamations > 0 && exclamations < 5) {
humanScore += exclamations * 5;
}
// Répétitions légères (positives si modérées)
const repetitionScore = this.analyzePositiveRepetition(content);
humanScore += repetitionScore;
// Longueurs de phrases irrégulières (bon signe)
const sentences = this.getSentences(content);
const lengthIrregularity = this.calculateLengthIrregularity(sentences);
humanScore += lengthIrregularity;
return Math.max(0, Math.min(100, Math.round(humanScore)));
}
/**
* Calcul du risque de détection
*/
static calculateDetectionRisk(overallScore) {
if (overallScore >= 85) return { level: 'LOW', text: 'Risque faible' };
if (overallScore >= 70) return { level: 'MODERATE', text: 'Risque modéré' };
if (overallScore >= 50) return { level: 'HIGH', text: 'Risque élevé' };
return { level: 'CRITICAL', text: 'Risque critique' };
}
/**
* Utilitaires de calcul
*/
static getSentences(content) {
return content.split(/[.!?]+/).filter(s => s.trim().length > 10);
}
static calculateCoeffVariation(numbers) {
if (numbers.length < 2) return 0;
const mean = numbers.reduce((a, b) => a + b) / numbers.length;
const variance = numbers.reduce((sum, n) => sum + Math.pow(n - mean, 2), 0) / numbers.length;
const stdDev = Math.sqrt(variance);
return mean > 0 ? stdDev / mean : 0;
}
static analyzePunctuationVariety(content) {
const punctTypes = ['.', ',', ';', ':', '!', '?', '...', '—'].filter(p =>
content.includes(p)
);
return punctTypes.length;
}
static countPerfectTransitions(content) {
let count = 0;
this.PERFECT_TRANSITIONS.forEach(pattern => {
const matches = content.match(pattern);
if (matches) count += matches.length;
});
return count;
}
static analyzeConnectorVariety(content) {
const connectors = [
'mais', 'cependant', 'toutefois', 'néanmoins', 'ainsi', 'donc',
'par conséquent', 'en effet', 'par ailleurs', 'en outre'
];
const used = connectors.filter(c => content.toLowerCase().includes(c));
return Math.min(used.length * 5, 25); // Max 25 points
}
static analyzePositiveRepetition(content) {
const words = content.toLowerCase().split(/\s+/);
const importantWords = words.filter(w => w.length > 5);
const repetitions = {};
importantWords.forEach(word => {
repetitions[word] = (repetitions[word] || 0) + 1;
});
// Répétitions modérées = humain (2-3 fois max)
let positiveReps = 0;
Object.values(repetitions).forEach(count => {
if (count === 2) positiveReps += 3;
if (count === 3) positiveReps += 2;
if (count > 3) positiveReps -= 2; // Pénalité excessive
});
return Math.max(0, Math.min(positiveReps, 15));
}
static calculateLengthIrregularity(sentences) {
if (sentences.length < 3) return 0;
const lengths = sentences.map(s => s.trim().length);
const coeffVar = this.calculateCoeffVariation(lengths);
// Irrégularité modérée = bon signe humain
if (coeffVar >= 0.2 && coeffVar <= 0.5) {
return 20;
} else if (coeffVar > 0.1) {
return 10;
}
return 0;
}
/**
* Rapport détaillé pour debugging
*/
static generateDetailedReport(content) {
const validation = this.validateAntiDetection(content);
return {
score: validation.overallScore,
risk: validation.detectionRisk,
breakdown: {
fingerprints: validation.fingerprintScore,
variability: validation.variabilityScore,
naturalness: validation.naturalness,
humanErrors: validation.humanErrors
},
recommendations: this.generateRecommendations(validation),
timestamp: new Date().toISOString()
};
}
/**
* Génération de recommandations d'amélioration
*/
static generateRecommendations(validation) {
const recommendations = [];
if (validation.fingerprintScore.score < 70) {
recommendations.push('Réduire les expressions typiquement IA identifiées');
}
if (validation.variabilityScore < 60) {
recommendations.push('Augmenter la variabilité des structures de phrases');
}
if (validation.naturalness < 60) {
recommendations.push('Ajouter des nuances et hésitations humaines');
}
if (validation.humanErrors < 40) {
recommendations.push('Introduire des imperfections subtiles pour plus de naturalité');
}
return recommendations;
}
}
module.exports = { AntiDetectionValidator };