449 lines
15 KiB
JavaScript
449 lines
15 KiB
JavaScript
// ========================================
|
|
// PATTERN BREAKING - TECHNIQUE 2: LLM FINGERPRINT REMOVAL
|
|
// Responsabilité: Remplacer mots/expressions typiques des LLMs
|
|
// Anti-détection: Éviter vocabulaire détectable par les analyseurs IA
|
|
// ========================================
|
|
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
|
|
/**
|
|
* DICTIONNAIRE ANTI-DÉTECTION
|
|
* Mots/expressions LLM → Alternatives humaines naturelles
|
|
*/
|
|
const LLM_FINGERPRINTS = {
|
|
// Mots techniques/corporate typiques IA
|
|
'optimal': ['idéal', 'parfait', 'adapté', 'approprié', 'convenable'],
|
|
'optimale': ['idéale', 'parfaite', 'adaptée', 'appropriée', 'convenable'],
|
|
'comprehensive': ['complet', 'détaillé', 'exhaustif', 'approfondi', 'global'],
|
|
'seamless': ['fluide', 'naturel', 'sans accroc', 'harmonieux', 'lisse'],
|
|
'robust': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
|
|
'robuste': ['solide', 'fiable', 'résistant', 'costaud', 'stable'],
|
|
|
|
// Expressions trop formelles/IA
|
|
'il convient de noter': ['on remarque', 'il faut savoir', 'à noter', 'important'],
|
|
'il convient de': ['il faut', 'on doit', 'mieux vaut', 'il est bon de'],
|
|
'par conséquent': ['du coup', 'donc', 'résultat', 'ainsi'],
|
|
'néanmoins': ['cependant', 'mais', 'pourtant', 'malgré tout'],
|
|
'toutefois': ['cependant', 'mais', 'pourtant', 'quand même'],
|
|
'de surcroît': ['de plus', 'en plus', 'aussi', 'également'],
|
|
|
|
// Superlatifs excessifs typiques IA
|
|
'extrêmement': ['très', 'super', 'vraiment', 'particulièrement'],
|
|
'particulièrement': ['très', 'vraiment', 'spécialement', 'surtout'],
|
|
'remarquablement': ['très', 'vraiment', 'sacrément', 'fichement'],
|
|
'exceptionnellement': ['très', 'vraiment', 'super', 'incroyablement'],
|
|
|
|
// Mots de liaison trop mécaniques
|
|
'en définitive': ['au final', 'finalement', 'bref', 'en gros'],
|
|
'il s\'avère que': ['on voit que', 'il se trouve que', 'en fait'],
|
|
'force est de constater': ['on constate', 'on voit bien', 'c\'est clair'],
|
|
|
|
// Expressions commerciales robotiques
|
|
'solution innovante': ['nouveauté', 'innovation', 'solution moderne', 'nouvelle approche'],
|
|
'approche holistique': ['approche globale', 'vision d\'ensemble', 'approche complète'],
|
|
'expérience utilisateur': ['confort d\'utilisation', 'facilité d\'usage', 'ergonomie'],
|
|
'retour sur investissement': ['rentabilité', 'bénéfices', 'profits'],
|
|
|
|
// Adjectifs surutilisés par IA
|
|
'révolutionnaire': ['nouveau', 'moderne', 'innovant', 'original'],
|
|
'game-changer': ['nouveauté', 'innovation', 'changement', 'révolution'],
|
|
'cutting-edge': ['moderne', 'récent', 'nouveau', 'avancé'],
|
|
'state-of-the-art': ['moderne', 'récent', 'performant', 'haut de gamme']
|
|
};
|
|
|
|
/**
|
|
* EXPRESSIONS CONTEXTUELLES SECTEUR SIGNALÉTIQUE
|
|
* Adaptées au domaine métier pour plus de naturel
|
|
*/
|
|
const CONTEXTUAL_REPLACEMENTS = {
|
|
'solution': {
|
|
'signalétique': ['plaque', 'panneau', 'support', 'réalisation'],
|
|
'impression': ['tirage', 'print', 'production', 'fabrication'],
|
|
'default': ['option', 'possibilité', 'choix', 'alternative']
|
|
},
|
|
'produit': {
|
|
'signalétique': ['plaque', 'panneau', 'enseigne', 'support'],
|
|
'default': ['article', 'réalisation', 'création']
|
|
},
|
|
'service': {
|
|
'signalétique': ['prestation', 'réalisation', 'travail', 'création'],
|
|
'default': ['prestation', 'travail', 'aide']
|
|
}
|
|
};
|
|
|
|
/**
|
|
* MAIN ENTRY POINT - SUPPRESSION EMPREINTES LLM
|
|
* @param {Object} input - { content: {}, config: {}, context: {} }
|
|
* @returns {Object} - { content: {}, stats: {}, debug: {} }
|
|
*/
|
|
async function removeLLMFingerprints(input) {
|
|
return await tracer.run('LLMFingerprintRemoval.removeLLMFingerprints()', async () => {
|
|
const { content, config = {}, context = {} } = input;
|
|
|
|
const {
|
|
intensity = 1.0, // Probabilité de remplacement (100%)
|
|
preserveKeywords = true, // Préserver mots-clés SEO
|
|
contextualMode = true, // Mode contextuel métier
|
|
csvData = null // Pour contexte métier
|
|
} = config;
|
|
|
|
await tracer.annotate({
|
|
technique: 'fingerprint_removal',
|
|
intensity,
|
|
elementsCount: Object.keys(content).length,
|
|
contextualMode
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
logSh(`🔍 TECHNIQUE 2/3: Suppression empreintes LLM (intensité: ${intensity})`, 'INFO');
|
|
logSh(` 📊 ${Object.keys(content).length} éléments à nettoyer`, 'DEBUG');
|
|
|
|
try {
|
|
const results = {};
|
|
let totalProcessed = 0;
|
|
let totalReplacements = 0;
|
|
let replacementDetails = [];
|
|
|
|
// Préparer contexte métier
|
|
const businessContext = extractBusinessContext(csvData);
|
|
|
|
// Traiter chaque élément de contenu
|
|
for (const [tag, text] of Object.entries(content)) {
|
|
totalProcessed++;
|
|
|
|
if (text.length < 20) {
|
|
results[tag] = text;
|
|
continue;
|
|
}
|
|
|
|
// Appliquer suppression des empreintes
|
|
const cleaningResult = cleanTextFingerprints(text, {
|
|
intensity,
|
|
preserveKeywords,
|
|
contextualMode,
|
|
businessContext,
|
|
tag
|
|
});
|
|
|
|
results[tag] = cleaningResult.text;
|
|
|
|
if (cleaningResult.replacements.length > 0) {
|
|
totalReplacements += cleaningResult.replacements.length;
|
|
replacementDetails.push({
|
|
tag,
|
|
replacements: cleaningResult.replacements,
|
|
fingerprintsFound: cleaningResult.fingerprintsDetected
|
|
});
|
|
|
|
logSh(` 🧹 [${tag}]: ${cleaningResult.replacements.length} remplacements`, 'DEBUG');
|
|
} else {
|
|
logSh(` ✅ [${tag}]: Aucune empreinte détectée`, 'DEBUG');
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const stats = {
|
|
processed: totalProcessed,
|
|
totalReplacements,
|
|
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
|
|
elementsWithFingerprints: replacementDetails.length,
|
|
duration,
|
|
technique: 'fingerprint_removal'
|
|
};
|
|
|
|
logSh(`✅ NETTOYAGE EMPREINTES: ${stats.totalReplacements} remplacements sur ${stats.elementsWithFingerprints}/${stats.processed} éléments en ${duration}ms`, 'INFO');
|
|
|
|
await tracer.event('Fingerprint removal terminée', stats);
|
|
|
|
return {
|
|
content: results,
|
|
stats,
|
|
debug: {
|
|
technique: 'fingerprint_removal',
|
|
config: { intensity, preserveKeywords, contextualMode },
|
|
replacements: replacementDetails,
|
|
businessContext
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ NETTOYAGE EMPREINTES échoué après ${duration}ms: ${error.message}`, 'ERROR');
|
|
throw new Error(`LLMFingerprintRemoval failed: ${error.message}`);
|
|
}
|
|
}, input);
|
|
}
|
|
|
|
/**
|
|
* Nettoyer les empreintes LLM d'un texte
|
|
*/
|
|
function cleanTextFingerprints(text, config) {
|
|
const { intensity, preserveKeywords, contextualMode, businessContext, tag } = config;
|
|
|
|
let cleanedText = text;
|
|
const replacements = [];
|
|
const fingerprintsDetected = [];
|
|
|
|
// PHASE 1: Remplacements directs du dictionnaire
|
|
for (const [fingerprint, alternatives] of Object.entries(LLM_FINGERPRINTS)) {
|
|
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
|
|
const matches = text.match(regex);
|
|
|
|
if (matches) {
|
|
fingerprintsDetected.push(fingerprint);
|
|
|
|
// Appliquer remplacement selon intensité
|
|
if (Math.random() <= intensity) {
|
|
const alternative = selectBestAlternative(alternatives, businessContext, contextualMode);
|
|
|
|
cleanedText = cleanedText.replace(regex, (match) => {
|
|
// Préserver la casse originale
|
|
return preserveCase(match, alternative);
|
|
});
|
|
|
|
replacements.push({
|
|
type: 'direct',
|
|
original: fingerprint,
|
|
replacement: alternative,
|
|
occurrences: matches.length
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// PHASE 2: Remplacements contextuels
|
|
if (contextualMode && businessContext) {
|
|
const contextualReplacements = applyContextualReplacements(cleanedText, businessContext);
|
|
cleanedText = contextualReplacements.text;
|
|
replacements.push(...contextualReplacements.replacements);
|
|
}
|
|
|
|
// PHASE 3: Détection patterns récurrents
|
|
const patternReplacements = replaceRecurringPatterns(cleanedText, intensity);
|
|
cleanedText = patternReplacements.text;
|
|
replacements.push(...patternReplacements.replacements);
|
|
|
|
return {
|
|
text: cleanedText,
|
|
replacements,
|
|
fingerprintsDetected
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sélectionner la meilleure alternative selon le contexte
|
|
*/
|
|
function selectBestAlternative(alternatives, businessContext, contextualMode) {
|
|
if (!contextualMode || !businessContext) {
|
|
// Mode aléatoire simple
|
|
return alternatives[Math.floor(Math.random() * alternatives.length)];
|
|
}
|
|
|
|
// Mode contextuel : privilégier alternatives adaptées au métier
|
|
const contextualAlternatives = alternatives.filter(alt =>
|
|
isContextuallyAppropriate(alt, businessContext)
|
|
);
|
|
|
|
const finalAlternatives = contextualAlternatives.length > 0 ? contextualAlternatives : alternatives;
|
|
return finalAlternatives[Math.floor(Math.random() * finalAlternatives.length)];
|
|
}
|
|
|
|
/**
|
|
* Vérifier si une alternative est contextuelle appropriée
|
|
*/
|
|
function isContextuallyAppropriate(alternative, businessContext) {
|
|
const { sector, vocabulary } = businessContext;
|
|
|
|
// Signalétique : privilégier vocabulaire technique/artisanal
|
|
if (sector === 'signalétique') {
|
|
const technicalWords = ['solide', 'fiable', 'costaud', 'résistant', 'adapté'];
|
|
return technicalWords.includes(alternative);
|
|
}
|
|
|
|
return true; // Par défaut accepter
|
|
}
|
|
|
|
/**
|
|
* Appliquer remplacements contextuels
|
|
*/
|
|
function applyContextualReplacements(text, businessContext) {
|
|
let processedText = text;
|
|
const replacements = [];
|
|
|
|
for (const [word, contexts] of Object.entries(CONTEXTUAL_REPLACEMENTS)) {
|
|
const regex = new RegExp(`\\b${word}\\b`, 'gi');
|
|
const matches = processedText.match(regex);
|
|
|
|
if (matches) {
|
|
const contextAlternatives = contexts[businessContext.sector] || contexts.default;
|
|
const replacement = contextAlternatives[Math.floor(Math.random() * contextAlternatives.length)];
|
|
|
|
processedText = processedText.replace(regex, (match) => {
|
|
return preserveCase(match, replacement);
|
|
});
|
|
|
|
replacements.push({
|
|
type: 'contextual',
|
|
original: word,
|
|
replacement,
|
|
occurrences: matches.length,
|
|
context: businessContext.sector
|
|
});
|
|
}
|
|
}
|
|
|
|
return { text: processedText, replacements };
|
|
}
|
|
|
|
/**
|
|
* Remplacer patterns récurrents
|
|
*/
|
|
function replaceRecurringPatterns(text, intensity) {
|
|
let processedText = text;
|
|
const replacements = [];
|
|
|
|
// Pattern 1: "très + adjectif" → variantes
|
|
const veryPattern = /\btrès\s+(\w+)/gi;
|
|
const veryMatches = [...text.matchAll(veryPattern)];
|
|
|
|
if (veryMatches.length > 2 && Math.random() < intensity) {
|
|
// Remplacer certains "très" par des alternatives
|
|
const alternatives = ['super', 'vraiment', 'particulièrement', 'assez'];
|
|
|
|
veryMatches.slice(1).forEach((match, index) => {
|
|
if (Math.random() < 0.5) {
|
|
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
|
|
const fullMatch = match[0];
|
|
const adjective = match[1];
|
|
const replacement = `${alternative} ${adjective}`;
|
|
|
|
processedText = processedText.replace(fullMatch, replacement);
|
|
|
|
replacements.push({
|
|
type: 'pattern',
|
|
pattern: '"très + adjectif"',
|
|
original: fullMatch,
|
|
replacement
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return { text: processedText, replacements };
|
|
}
|
|
|
|
/**
|
|
* Extraire contexte métier des données CSV
|
|
*/
|
|
function extractBusinessContext(csvData) {
|
|
if (!csvData) {
|
|
return { sector: 'general', vocabulary: [] };
|
|
}
|
|
|
|
const mc0 = csvData.mc0?.toLowerCase() || '';
|
|
|
|
// Détection secteur
|
|
let sector = 'general';
|
|
if (mc0.includes('plaque') || mc0.includes('panneau') || mc0.includes('enseigne')) {
|
|
sector = 'signalétique';
|
|
} else if (mc0.includes('impression') || mc0.includes('print')) {
|
|
sector = 'impression';
|
|
}
|
|
|
|
// Extraction vocabulaire clé
|
|
const vocabulary = [csvData.mc0, csvData.t0, csvData.tMinus1].filter(Boolean);
|
|
|
|
return { sector, vocabulary };
|
|
}
|
|
|
|
/**
|
|
* Préserver la casse originale
|
|
*/
|
|
function preserveCase(original, replacement) {
|
|
if (original === original.toUpperCase()) {
|
|
return replacement.toUpperCase();
|
|
} else if (original[0] === original[0].toUpperCase()) {
|
|
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
|
|
} else {
|
|
return replacement.toLowerCase();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Échapper caractères regex
|
|
*/
|
|
function escapeRegex(text) {
|
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
/**
|
|
* Analyser les empreintes LLM dans un texte
|
|
*/
|
|
function analyzeLLMFingerprints(text) {
|
|
const detectedFingerprints = [];
|
|
let totalMatches = 0;
|
|
|
|
for (const fingerprint of Object.keys(LLM_FINGERPRINTS)) {
|
|
const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi');
|
|
const matches = text.match(regex);
|
|
|
|
if (matches) {
|
|
detectedFingerprints.push({
|
|
fingerprint,
|
|
occurrences: matches.length,
|
|
category: categorizefingerprint(fingerprint)
|
|
});
|
|
totalMatches += matches.length;
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasFingerprints: detectedFingerprints.length > 0,
|
|
fingerprints: detectedFingerprints,
|
|
totalMatches,
|
|
riskLevel: calculateRiskLevel(detectedFingerprints, text.length)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Catégoriser une empreinte LLM
|
|
*/
|
|
function categorizefingerprint(fingerprint) {
|
|
const categories = {
|
|
'technical': ['optimal', 'comprehensive', 'robust', 'seamless'],
|
|
'formal': ['il convient de', 'néanmoins', 'par conséquent'],
|
|
'superlative': ['extrêmement', 'particulièrement', 'remarquablement'],
|
|
'commercial': ['solution innovante', 'game-changer', 'révolutionnaire']
|
|
};
|
|
|
|
for (const [category, words] of Object.entries(categories)) {
|
|
if (words.some(word => fingerprint.includes(word))) {
|
|
return category;
|
|
}
|
|
}
|
|
|
|
return 'other';
|
|
}
|
|
|
|
/**
|
|
* Calculer niveau de risque de détection
|
|
*/
|
|
function calculateRiskLevel(fingerprints, textLength) {
|
|
if (fingerprints.length === 0) return 'low';
|
|
|
|
const fingerprintDensity = fingerprints.reduce((sum, fp) => sum + fp.occurrences, 0) / (textLength / 100);
|
|
|
|
if (fingerprintDensity > 3) return 'high';
|
|
if (fingerprintDensity > 1.5) return 'medium';
|
|
return 'low';
|
|
}
|
|
|
|
module.exports = {
|
|
removeLLMFingerprints, // ← MAIN ENTRY POINT
|
|
cleanTextFingerprints,
|
|
analyzeLLMFingerprints,
|
|
LLM_FINGERPRINTS,
|
|
CONTEXTUAL_REPLACEMENTS,
|
|
extractBusinessContext
|
|
}; |