492 lines
16 KiB
JavaScript
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 }; |