// ======================================== // ADVERSARIAL CORE - MOTEUR MODULAIRE // Responsabilité: Moteur adversarial réutilisable sur tout contenu // Architecture: Couches applicables à la demande // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { callLLM } = require('../LLMManager'); // Import stratégies et utilitaires const { DetectorStrategyFactory, selectOptimalStrategy } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE * Input: contenu existant + configuration adversariale * Output: contenu avec couche adversariale appliquée */ async function applyAdversarialLayer(existingContent, config = {}) { return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => { const { detectorTarget = 'general', intensity = 1.0, method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid' preserveStructure = true, csvData = null, context = {}, llmProvider = 'gemini-pro' // ✅ AJOUTÉ: Extraction llmProvider avec fallback } = config; await tracer.annotate({ adversarialLayer: true, detectorTarget, intensity, method, llmProvider, elementsCount: Object.keys(existingContent).length }); const startTime = Date.now(); logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO'); logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity} | LLM: ${llmProvider}`, 'INFO'); try { // Initialiser stratégie détecteur const strategy = DetectorStrategyFactory.createStrategy(detectorTarget); // Appliquer méthode adversariale choisie avec LLM spécifié let adversarialContent = {}; const methodConfig = { ...config, llmProvider }; // ✅ Assurer propagation llmProvider switch (method) { case 'regeneration': adversarialContent = await applyRegenerationMethod(existingContent, methodConfig, strategy); break; case 'enhancement': adversarialContent = await applyEnhancementMethod(existingContent, methodConfig, strategy); break; case 'hybrid': adversarialContent = await applyHybridMethod(existingContent, methodConfig, strategy); break; default: throw new Error(`Méthode adversariale inconnue: ${method}`); } const duration = Date.now() - startTime; const stats = { elementsProcessed: Object.keys(existingContent).length, elementsModified: countModifiedElements(existingContent, adversarialContent), detectorTarget, intensity, method, duration }; logSh(`✅ COUCHE ADVERSARIALE APPLIQUÉE: ${stats.elementsModified}/${stats.elementsProcessed} modifiés (${duration}ms)`, 'INFO'); await tracer.event('Couche adversariale appliquée', stats); return { content: adversarialContent, stats, modifications: stats.elementsModified, // ✅ AJOUTÉ: Mapping pour PipelineExecutor original: existingContent, config }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ COUCHE ADVERSARIALE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); return { content: existingContent, stats: { fallback: true, duration }, original: existingContent, config, error: error.message }; } }, { existingContent: Object.keys(existingContent), config }); } /** * MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux */ async function applyRegenerationMethod(existingContent, config, strategy) { const llmToUse = config.llmProvider || 'gemini-pro'; logSh(`🔄 Méthode régénération adversariale (LLM: ${llmToUse})`, 'DEBUG'); const results = {}; const contentEntries = Object.entries(existingContent); // 🔥 NOUVEAU: Tracker le dernier titre généré pour l'associer au texte suivant let lastGeneratedTitle = null; // Traiter en chunks pour éviter timeouts const chunks = chunkArray(contentEntries, 4); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { // 🔥 NOUVEAU: Détecter si le chunk contient un texte et qu'on a un titre associé let titleToUse = null; const hasTextElement = chunk.some(([tag]) => { const tagLower = tag.toLowerCase(); return tagLower.startsWith('txt_') || tagLower.startsWith('intro_') || tagLower.includes('_text'); }); if (hasTextElement && lastGeneratedTitle) { titleToUse = lastGeneratedTitle; logSh(` 🎯 Utilisation titre associé pour ce chunk: "${titleToUse}"`, 'DEBUG'); } const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy, titleToUse); const response = await callLLM(llmToUse, regenerationPrompt, { temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité maxTokens: 2000 * chunk.length }, config.csvData?.personality); const chunkResults = parseRegenerationResponse(response, chunk); Object.assign(results, chunkResults); // 🔥 NOUVEAU: Détecter et stocker les titres générés chunk.forEach(([tag]) => { const tagLower = tag.toLowerCase(); const isTitle = tagLower.includes('titre_h') || tagLower.endsWith('_title'); if (isTitle && chunkResults[tag]) { lastGeneratedTitle = chunkResults[tag]; logSh(` 📌 Titre stocké pour prochain texte: "${lastGeneratedTitle.substring(0, 50)}..."`, 'DEBUG'); } // 🔥 NOUVEAU: Réinitialiser après avoir traité un texte const isText = tagLower.startsWith('txt_') || tagLower.startsWith('intro_') || tagLower.includes('_text'); if (isText && titleToUse) { lastGeneratedTitle = null; logSh(` 🔄 Titre associé consommé, réinitialisé`, 'DEBUG'); } }); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments régénérés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: garder contenu original pour ce chunk chunk.forEach(([tag, content]) => { results[tag] = content; }); } } return results; } /** * MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement */ async function applyEnhancementMethod(existingContent, config, strategy) { const llmToUse = config.llmProvider || 'gemini-pro'; logSh(`🔧 Méthode enhancement adversarial (LLM: ${llmToUse})`, 'DEBUG'); const results = { ...existingContent }; // Base: contenu original const elementsToEnhance = selectElementsForEnhancement(existingContent, config); if (elementsToEnhance.length === 0) { logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG'); return results; } logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG'); // 🔥 NOUVEAU: Détecter si on a un titre dans le contenu pour l'utiliser avec les textes let associatedTitle = null; const contentEntries = Object.entries(existingContent); // Chercher le dernier titre généré avant les éléments à améliorer for (let i = 0; i < contentEntries.length; i++) { const [tag, content] = contentEntries[i]; const tagLower = tag.toLowerCase(); const isTitle = tagLower.includes('titre_h') || tagLower.endsWith('_title'); if (isTitle && content) { associatedTitle = content; logSh(` 📌 Titre trouvé pour contexte: "${associatedTitle.substring(0, 50)}..."`, 'DEBUG'); } // Si on trouve un élément à améliorer qui est un texte, on arrête la recherche const elementToEnhance = elementsToEnhance.find(el => el.tag === tag); if (elementToEnhance) { const isText = tagLower.startsWith('txt_') || tagLower.startsWith('intro_') || tagLower.includes('_text'); if (isText) { break; } } } const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy, associatedTitle); try { const response = await callLLM(llmToUse, enhancementPrompt, { temperature: 0.5 + (config.intensity * 0.3), maxTokens: 3000 }, config.csvData?.personality); const enhancedResults = parseEnhancementResponse(response, elementsToEnhance); // Appliquer améliorations Object.keys(enhancedResults).forEach(tag => { if (enhancedResults[tag] !== existingContent[tag]) { results[tag] = enhancedResults[tag]; } }); return results; } catch (error) { logSh(`❌ Enhancement échoué: ${error.message}`, 'ERROR'); return results; // Fallback: contenu original } } /** * MÉTHODE HYBRIDE - Combinaison régénération + enhancement */ async function applyHybridMethod(existingContent, config, strategy) { logSh(`⚡ Méthode hybride adversariale`, 'DEBUG'); // 1. Enhancement léger sur tout le contenu const enhancedContent = await applyEnhancementMethod(existingContent, { ...config, intensity: config.intensity * 0.6 // Intensité réduite pour enhancement }, strategy); // 2. Régénération ciblée sur éléments clés const keyElements = selectKeyElementsForRegeneration(enhancedContent, config); if (keyElements.length === 0) { return enhancedContent; } const keyElementsContent = {}; keyElements.forEach(tag => { keyElementsContent[tag] = enhancedContent[tag]; }); const regeneratedElements = await applyRegenerationMethod(keyElementsContent, { ...config, intensity: config.intensity * 1.2 // Intensité augmentée pour régénération }, strategy); // 3. Merger résultats const hybridContent = { ...enhancedContent }; Object.keys(regeneratedElements).forEach(tag => { hybridContent[tag] = regeneratedElements[tag]; }); return hybridContent; } // ============= HELPER FUNCTIONS ============= /** * Créer prompt de régénération adversariale */ function createRegenerationPrompt(chunk, config, strategy, associatedTitle = null) { const { detectorTarget, intensity, csvData } = config; const personality = csvData?.personality; let prompt = `MISSION: Réécris ces contenus pour éviter détection par ${detectorTarget}. TECHNIQUE ANTI-${detectorTarget.toUpperCase()}: ${strategy.getInstructions(intensity).join('\n')} CONTENUS À RÉÉCRIRE: ${chunk.map(([tag, content], i) => { const elementType = detectElementTypeFromTag(tag); return `[${i + 1}] TAG: ${tag} | TYPE: ${elementType} ORIGINAL: "${content}"`; }).join('\n\n')} CONSIGNES GÉNÉRALES: - GARDE exactement le même message et informations factuelles - CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget} - Utilise expressions françaises familières et tournures idiomatiques authentiques - Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots) - Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations - Ne génère pas de contenu générique, sois spécifique et informatif - Intensité adversariale: ${intensity.toFixed(2)} ${generatePersonalityInstructions(personality, intensity)} ${generateTitleContext(associatedTitle)} ${generateElementSpecificInstructions(chunk)} IMPORTANT: Ces contraintes doivent sembler naturelles, pas forcées. Réponse DIRECTE par les contenus réécrits, pas d'explication. FORMAT: [1] Contenu réécrit anti-${detectorTarget} [2] Contenu réécrit anti-${detectorTarget} etc...`; return prompt; } /** * Sélectionner aléatoirement max N éléments d'un array (Fisher-Yates shuffle) * Utilisé pour variabilité anti-détection dans personnalité */ function selectRandomItems(arr, max = 2) { if (!Array.isArray(arr) || arr.length === 0) return arr; if (arr.length <= max) return arr; // Fisher-Yates shuffle puis prendre les N premiers const shuffled = [...arr]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, max); } /** * Générer instructions personnalité enrichies (inspiré ancien système) */ function generatePersonalityInstructions(personality, intensity) { if (!personality) return ''; let instructions = `\nADAPTATION PERSONNALITÉ ${personality.nom.toUpperCase()}:`; // Profil et description if (personality.description) { instructions += `\n- Profil: ${personality.description}`; } instructions += `\n- Style: ${personality.style} de ${personality.nom} de façon authentique${intensity >= 1.0 ? ' et marquée' : ''}`; // Secteurs d'expertise (motsClesSecteurs) - MAX 2 aléatoires if (personality.motsClesSecteurs) { const secteursArray = Array.isArray(personality.motsClesSecteurs) ? personality.motsClesSecteurs : personality.motsClesSecteurs.split(',').map(s => s.trim()).filter(s => s); const secteursList = selectRandomItems(secteursArray, 2); if (secteursList.length > 0) { instructions += `\n- Secteurs d'expertise: ${secteursList.join(', ')}`; } } // Vocabulaire préféré - MAX 2 aléatoires (pas tous!) if (personality.vocabulairePref) { const vocabArray = Array.isArray(personality.vocabulairePref) ? personality.vocabulairePref : personality.vocabulairePref.split(',').map(v => v.trim()).filter(v => v); const vocabList = selectRandomItems(vocabArray, 2); if (vocabList.length > 0) { instructions += `\n- Vocabulaire préféré: ${vocabList.join(', ')}`; } } // Connecteurs préférés - MAX 2 aléatoires if (personality.connecteursPref) { const connArray = Array.isArray(personality.connecteursPref) ? personality.connecteursPref : personality.connecteursPref.split(',').map(c => c.trim()).filter(c => c); const connList = selectRandomItems(connArray, 2); if (connList.length > 0) { instructions += `\n- Connecteurs préférés: ${connList.join(', ')}`; } } // Longueur phrases selon personnalité if (personality.longueurPhrases) { instructions += `\n- Longueur phrases: ${personality.longueurPhrases} mais avec variation anti-détection`; } // Niveau technique explicite if (personality.niveauTechnique) { instructions += `\n- Niveau technique: ${personality.niveauTechnique}`; } // Style CTA - MAX 2 aléatoires if (personality.ctaStyle) { const ctaArray = Array.isArray(personality.ctaStyle) ? personality.ctaStyle : personality.ctaStyle.split(',').map(c => c.trim()).filter(c => c); const ctaList = selectRandomItems(ctaArray, 2); if (ctaList.length > 0) { instructions += `\n- Style CTA: ${ctaList.join(', ')}`; } } // Expressions favorites - MAX 2 aléatoires if (personality.expressionsFavorites) { const exprArray = Array.isArray(personality.expressionsFavorites) ? personality.expressionsFavorites : personality.expressionsFavorites.split(',').map(e => e.trim()).filter(e => e); const exprList = selectRandomItems(exprArray, 2); if (exprList.length > 0) { instructions += `\n- Expressions typiques: ${exprList.join(', ')}`; } } return instructions; } /** * Générer contexte du titre associé (pour cohérence titre→texte) */ function generateTitleContext(associatedTitle) { if (!associatedTitle) return ''; // Extraire mots-clés importants du titre (> 4 lettres, sans stop words) const stopWords = ['dans', 'avec', 'pour', 'sans', 'sous', 'vers', 'chez', 'sur', 'par', 'tous', 'toutes', 'cette', 'votre', 'notre']; const titleWords = associatedTitle .toLowerCase() .replace(/[.,;:!?'"]/g, '') .split(/\s+/) .filter(word => word.length > 4 && !stopWords.includes(word)); const keywordsHighlight = titleWords.length > 0 ? `Mots-clés à développer: ${titleWords.join(', ')}\n` : ''; return ` 🎯 TITRE À DÉVELOPPER: "${associatedTitle}" ${keywordsHighlight}⚠️ IMPORTANT: Ton contenu doit développer SPÉCIFIQUEMENT ce titre et ses concepts clés. Ne génère pas de contenu générique, concentre-toi sur les mots-clés identifiés ci-dessus. `; } /** * Générer instructions spécifiques par type d'élément (inspiré ancien système) */ function generateElementSpecificInstructions(chunk) { const elementTypes = new Set(chunk.map(([tag]) => detectElementTypeFromTag(tag))); if (elementTypes.size === 0) return ''; let instructions = '\n\nINSTRUCTIONS SPÉCIFIQUES PAR TYPE:'; if (elementTypes.has('titre_h1') || elementTypes.has('titre_h2')) { instructions += `\n• TITRES: Évite formules marketing lisses, préfère authentique et direct`; instructions += `\n Varie structure : question, affirmation, fragment percutant`; } if (elementTypes.has('intro')) { instructions += `\n• INTRO: Commence par angle inattendu : anecdote, constat, question rhétorique`; instructions += `\n Évite intro-types, crée surprise puis retour naturel au sujet`; } if (elementTypes.has('texte')) { instructions += `\n• TEXTES: Mélange informations factuelles et observations personnelles`; instructions += `\n Intègre apartés : "(j'ai testé, c'est bluffant)", questions rhétoriques`; } if (elementTypes.has('faq_question')) { instructions += `\n• QUESTIONS FAQ: Formulations vraiment utilisées par clients, pas académiques`; } if (elementTypes.has('faq_reponse')) { instructions += `\n• RÉPONSES FAQ: Ajoute nuances, "ça dépend", précisions contextuelles comme humain`; } if (elementTypes.has('conclusion')) { instructions += `\n• CONCLUSION: Personnalise avec avis subjectif ou ouverture inattendue`; } return instructions; } /** * Détecter type d'élément depuis le tag */ function detectElementTypeFromTag(tag) { const tagLower = tag.toLowerCase(); if (tagLower.includes('titre_h1') || tagLower === 'titre_h1') return 'titre_h1'; if (tagLower.includes('titre_h2') || tagLower.includes('h2')) return 'titre_h2'; if (tagLower.includes('intro')) return 'intro'; if (tagLower.includes('conclusion')) return 'conclusion'; if (tagLower.includes('faq_question') || tagLower.includes('question')) return 'faq_question'; if (tagLower.includes('faq_reponse') || tagLower.includes('reponse')) return 'faq_reponse'; return 'texte'; } /** * Créer prompt d'enhancement adversarial */ function createEnhancementPrompt(elementsToEnhance, config, strategy, associatedTitle = null) { const { detectorTarget, intensity, csvData } = config; const personality = csvData?.personality; // 🔥 NOUVEAU: Détecter si les éléments contiennent des textes (pour titre associé) const hasTextElements = elementsToEnhance.some(el => { const tagLower = el.tag.toLowerCase(); return tagLower.startsWith('txt_') || tagLower.startsWith('intro_') || tagLower.includes('_text'); }); let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}. AMÉLIORATIONS CIBLÉES ANTI-${detectorTarget.toUpperCase()}: ${strategy.getEnhancementTips(intensity).join('\n')} TECHNIQUES GÉNÉRALES: - Remplace mots typiques IA par synonymes plus naturels et moins évidents - Varie longueurs phrases et structures syntaxiques - Utilise expressions idiomatiques françaises et tournures familières - Ajoute nuances humaines : "peut-être", "généralement", "souvent" - Intègre connecteurs variés et naturels selon contexte - Ne génère pas de contenu générique, sois spécifique et informatif ${personality && personality.niveauTechnique ? `- Niveau technique: ${personality.niveauTechnique}` : ''} ${generatePersonalityInstructions(personality, intensity)} ${hasTextElements && associatedTitle ? generateTitleContext(associatedTitle) : ''} ÉLÉMENTS À AMÉLIORER: ${elementsToEnhance.map((element, i) => { const elementType = detectElementTypeFromTag(element.tag); return `[${i + 1}] TAG: ${element.tag} | TYPE: ${elementType} CONTENU: "${element.content}" PROBLÈME DÉTECTÉ: ${element.detectionRisk} ${getElementSpecificTip(elementType)}`; }).join('\n\n')} CONSIGNES: - Modifications LÉGÈRES mais EFFICACES pour anti-détection - GARDE le fond du message intact (informations factuelles identiques) - Focus sur réduction détection ${detectorTarget} avec naturalité ${hasTextElements && associatedTitle ? `- 🎯 FOCUS: Développe spécifiquement les concepts du titre associé` : ''} - Intensité: ${intensity.toFixed(2)} FORMAT DE RÉPONSE OBLIGATOIRE (UN PAR LIGNE): [1] Contenu légèrement amélioré pour élément 1 [2] Contenu légèrement amélioré pour élément 2 [3] Contenu légèrement amélioré pour élément 3 etc... IMPORTANT: - Réponds UNIQUEMENT avec les contenus améliorés - GARDE le numéro [N] devant chaque contenu - PAS d'explications, PAS de commentaires - RESPECTE STRICTEMENT le format [N] Contenu - Ces améliorations doivent sembler naturelles, pas forcées`; return prompt; } /** * Obtenir conseil spécifique pour type d'élément (enhancement) */ function getElementSpecificTip(elementType) { const tips = { 'titre_h1': 'TIP: Évite formules marketing, préfère authentique et percutant', 'titre_h2': 'TIP: Varie structure (question/affirmation/fragment)', 'intro': 'TIP: Commence par angle inattendu si possible', 'texte': 'TIP: Ajoute observation personnelle ou aparté léger', 'faq_question': 'TIP: Formulation vraie client, pas académique', 'faq_reponse': 'TIP: Ajoute nuance "ça dépend" ou précision contextuelle', 'conclusion': 'TIP: Personnalise avec avis subjectif subtil' }; return tips[elementType] || 'TIP: Rends plus naturel et humain'; } /** * Parser réponse régénération */ function parseRegenerationResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; const content = cleanAdversarialContent(match[2].trim()); if (index >= 0 && index < chunk.length) { parsedItems[index] = content; } } // Mapper aux vrais tags chunk.forEach(([tag, originalContent], index) => { if (parsedItems[index] && parsedItems[index].length > 10) { results[tag] = parsedItems[index]; } else { results[tag] = originalContent; // Fallback logSh(`⚠️ Fallback régénération pour [${tag}]`, 'WARNING'); } }); return results; } /** * Parser réponse enhancement */ function parseEnhancementResponse(response, elementsToEnhance) { const results = {}; // Log réponse brute pour debug logSh(`📥 Réponse LLM (${response.length} chars): ${response.substring(0, 200)}...`, 'DEBUG'); const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; const parsedIndexes = new Set(); while ((match = regex.exec(response)) !== null) { const num = parseInt(match[1]); const index = num - 1; // [1] = index 0 if (index >= 0 && index < elementsToEnhance.length && !parsedIndexes.has(index)) { let enhancedContent = cleanAdversarialContent(match[2].trim()); const element = elementsToEnhance[index]; if (enhancedContent && enhancedContent.length > 10) { results[element.tag] = enhancedContent; parsedIndexes.add(index); logSh(` ✅ Parsé [${num}] ${element.tag}: ${enhancedContent.substring(0, 50)}...`, 'DEBUG'); } else { logSh(` ⚠️ [${num}] ${element.tag}: contenu trop court (${enhancedContent?.length || 0} chars)`, 'WARNING'); } } } // Vérifier si parsing a échoué if (Object.keys(results).length === 0 && elementsToEnhance.length > 0) { logSh(`❌ PARSING ÉCHOUÉ: Aucun élément parsé (format LLM invalide)`, 'ERROR'); logSh(` Réponse complète: ${response}`, 'ERROR'); // FALLBACK: Essayer parsing alternatif (sans numéros) logSh(` 🔄 Tentative parsing alternatif...`, 'WARNING'); // Diviser par double saut de ligne ou tirets const chunks = response.split(/\n\n+|---+/).map(c => c.trim()).filter(c => c.length > 10); chunks.forEach((chunk, idx) => { if (idx < elementsToEnhance.length) { const cleaned = cleanAdversarialContent(chunk); if (cleaned && cleaned.length > 10) { results[elementsToEnhance[idx].tag] = cleaned; logSh(` ✅ Fallback [${idx + 1}]: ${cleaned.substring(0, 50)}...`, 'DEBUG'); } } }); } logSh(`📦 Résultat parsing: ${Object.keys(results).length}/${elementsToEnhance.length} éléments extraits`, 'DEBUG'); return results; } /** * Sélectionner éléments pour enhancement */ function selectElementsForEnhancement(existingContent, config) { const elements = []; // ✅ Threshold basé sur intensity // intensity >= 1.0 → threshold = 0.3 (traiter risque moyen/élevé) // intensity < 1.0 → threshold = 0.4 (traiter uniquement risque élevé) const threshold = config.intensity >= 1.0 ? 0.3 : 0.4; logSh(`🎯 Sélection enhancement avec threshold=${(threshold * 100).toFixed(0)}% (intensity=${config.intensity})`, 'DEBUG'); Object.entries(existingContent).forEach(([tag, content]) => { const detectionRisk = assessDetectionRisk(content, config.detectorTarget); if (detectionRisk.score > threshold) { elements.push({ tag, content, detectionRisk: detectionRisk.reasons.join(', ') || 'prévention_générale', priority: detectionRisk.score }); logSh(` ✅ [${tag}] Sélectionné: score=${(detectionRisk.score * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%`, 'INFO'); } else { // Log éléments ignorés pour debug logSh(` ⏭️ [${tag}] Ignoré: score=${(detectionRisk.score * 100).toFixed(0)}% ≤ ${(threshold * 100).toFixed(0)}%`, 'DEBUG'); } }); // Trier par priorité (risque élevé en premier) elements.sort((a, b) => b.priority - a.priority); logSh(` 📊 Sélection: ${elements.length}/${Object.keys(existingContent).length} éléments (threshold=${(threshold * 100).toFixed(0)}%)`, 'DEBUG'); return elements; } /** * Sélectionner éléments clés pour régénération (hybride) */ function selectKeyElementsForRegeneration(content, config) { const keyTags = []; Object.keys(content).forEach(tag => { // Éléments clés: titres, intro, premiers paragraphes if (tag.includes('Titre') || tag.includes('H1') || tag.includes('intro') || tag.includes('Introduction') || tag.includes('1')) { keyTags.push(tag); } }); return keyTags.slice(0, 3); // Maximum 3 éléments clés } /** * Évaluer risque de détection (approche statistique générique) * Basé sur des métriques linguistiques universelles sans mots hardcodés */ function assessDetectionRisk(content, detectorTarget) { const reasons = []; // Parsing de base const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); const words = content.split(/\s+/).filter(w => w.length > 0); // Validation & Mode texte court if (words.length < 5) { return { score: 0, reasons: ['texte_trop_court(<5_mots)'], metrics: {} }; } // ✅ MODE TEXTE COURT (1 phrase ou <10 mots) if (sentences.length < 2 || words.length < 10) { return assessShortTextRisk(content, words, detectorTarget); } // === CALCULER TOUTES LES MÉTRIQUES === const metrics = { lexicalDiversity: calculateLexicalDiversity(words), burstiness: calculateBurstiness(sentences), syntaxEntropy: calculateSyntaxEntropy(sentences), punctuationComplexity: calculatePunctuationComplexity(content), redundancy: calculateRedundancy(words), wordUniformity: calculateWordUniformity(words) }; // === SCORING ADAPTATIF PAR DÉTECTEUR === let score = 0; if (detectorTarget === 'gptZero') { // GPTZero privilégie : perplexité + burstiness score += metrics.lexicalDiversity.score * 0.30; score += metrics.burstiness.score * 0.25; score += metrics.syntaxEntropy.score * 0.15; score += metrics.punctuationComplexity.score * 0.10; score += metrics.redundancy.score * 0.10; score += metrics.wordUniformity.score * 0.10; if (metrics.lexicalDiversity.score > 0.3 && metrics.burstiness.score > 0.3) { score += 0.05; // Bonus si double flag reasons.push('gptzero_double_flag'); } } else if (detectorTarget === 'originality') { // Originality.ai privilégie : redondance + entropie syntaxique score += metrics.redundancy.score * 0.30; score += metrics.syntaxEntropy.score * 0.25; score += metrics.lexicalDiversity.score * 0.15; score += metrics.burstiness.score * 0.15; score += metrics.punctuationComplexity.score * 0.10; score += metrics.wordUniformity.score * 0.05; if (metrics.redundancy.score > 0.4) { score += 0.05; // Bonus haute redondance reasons.push('originality_redondance_élevée'); } } else { // Détecteur général : ponctuation = meilleur indicateur (40%) // Les LLMs modernes ont bon TTR et burstiness, mais ponctuation trop simple const weights = [0.10, 0.20, 0.10, 0.40, 0.15, 0.05]; const metricScores = Object.values(metrics).map(m => m.score); score = metricScores.reduce((sum, s, i) => sum + s * weights[i], 0); } // Collecter raisons Object.entries(metrics).forEach(([name, data]) => { if (data.score > 0.3) { // Seuil significatif reasons.push(data.reason); } }); return { score: Math.min(1, score), reasons: reasons.length > 0 ? reasons : ['analyse_générale'], metrics // Retourner pour debug }; } // ============= HELPER FUNCTIONS - MÉTRIQUES STATISTIQUES ============= /** * 1️⃣ Diversité lexicale (Type-Token Ratio) */ function calculateLexicalDiversity(words) { const cleanWords = words.map(w => w.toLowerCase().replace(/[^\w]/g, '')).filter(w => w.length > 0); const uniqueWords = new Set(cleanWords); const ttr = uniqueWords.size / cleanWords.length; // TTR < 0.5 = vocabulaire répétitif (IA) let score = 0; if (ttr < 0.5) { score = (0.5 - ttr) / 0.5; // Normaliser 0.5→0 = 0, 0→0.5 = 1 } return { score, value: ttr, reason: `low_lexical_diversity(TTR=${ttr.toFixed(2)})` }; } /** * 2️⃣ Burstiness (Variation longueur phrases) */ function calculateBurstiness(sentences) { const lengths = sentences.map(s => s.length); const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length; const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avg, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); const cv = stdDev / avg; // Coefficient de variation // ✅ FIX: Seuil abaissé de 0.35 à 0.25 (LLMs modernes plus uniformes) // CV < 0.25 = phrases très uniformes (IA moderne) let score = 0; if (cv < 0.25) { score = (0.25 - cv) / 0.25; // Normaliser 0.25→0 = 0, 0→0.25 = 1 } return { score, value: cv, reason: `low_burstiness(CV=${cv.toFixed(2)})` }; } /** * 3️⃣ Entropie syntaxique (Débuts de phrases répétés) */ function calculateSyntaxEntropy(sentences) { const starts = sentences.map(s => { const words = s.trim().split(/\s+/); return words.slice(0, 2).join(' ').toLowerCase(); }); const freq = {}; starts.forEach(start => { freq[start] = (freq[start] || 0) + 1; }); const maxFreq = Math.max(...Object.values(freq)); const entropy = maxFreq / sentences.length; // Entropie > 0.5 = >50% phrases commencent pareil (monotone) let score = 0; if (entropy > 0.5) { score = (entropy - 0.5) / 0.5; // Normaliser 0.5→1 = 0→1 } return { score, value: entropy, reason: `high_syntax_entropy(${(entropy * 100).toFixed(0)}%)` }; } /** * 4️⃣ Complexité ponctuation */ function calculatePunctuationComplexity(content) { const simplePunct = (content.match(/[.,]/g) || []).length; const complexPunct = (content.match(/[;:!?()—…]/g) || []).length; const total = simplePunct + complexPunct; if (total === 0) { return { score: 0, value: 0, reason: 'no_punctuation' }; } const ratio = complexPunct / total; // Ratio < 0.1 = ponctuation trop simple (IA) let score = 0; if (ratio < 0.1) { score = (0.1 - ratio) / 0.1; // Normaliser 0.1→0 = 0, 0→0.1 = 1 } return { score, value: ratio, reason: `low_punctuation_complexity(${(ratio * 100).toFixed(0)}%)` }; } /** * 5️⃣ Redondance structurelle (Bigrammes répétés) */ function calculateRedundancy(words) { const bigrams = []; for (let i = 0; i < words.length - 1; i++) { const bigram = `${words[i]} ${words[i + 1]}`.toLowerCase(); bigrams.push(bigram); } const freq = {}; bigrams.forEach(bg => { freq[bg] = (freq[bg] || 0) + 1; }); const repeatedCount = Object.values(freq).filter(count => count > 1).length; const redundancy = repeatedCount / bigrams.length; // Redondance > 0.2 = 20%+ bigrammes répétés (IA) let score = 0; if (redundancy > 0.2) { score = Math.min(1, (redundancy - 0.2) / 0.3); // Normaliser 0.2→0.5 = 0→1 } return { score, value: redundancy, reason: `high_redundancy(${(redundancy * 100).toFixed(0)}%)` }; } /** * 6️⃣ Uniformité longueur mots */ function calculateWordUniformity(words) { const lengths = words.map(w => w.replace(/[^\w]/g, '').length).filter(l => l > 0); if (lengths.length === 0) { return { score: 0, value: 0, reason: 'no_words' }; } const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length; const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avg, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); // StdDev < 2.5 ET moyenne 4-8 lettres = mots uniformes (IA) let score = 0; if (stdDev < 2.5 && avg >= 4 && avg <= 8) { score = (2.5 - stdDev) / 2.5; // Normaliser 2.5→0 = 0, 0→2.5 = 1 } return { score, value: stdDev, reason: `uniform_word_length(σ=${stdDev.toFixed(1)}, avg=${avg.toFixed(1)})` }; } /** * ✅ MODE SPÉCIAL: Évaluation textes courts (1 phrase ou <10 mots) * Utilise métriques adaptées aux textes courts */ function assessShortTextRisk(content, words, detectorTarget) { let score = 0; const reasons = []; // === MÉTRIQUE 1: Complexité ponctuation (poids 50%) === const simplePunct = (content.match(/[.,]/g) || []).length; const complexPunct = (content.match(/[;:!?()—…]/g) || []).length; const total = simplePunct + complexPunct; let punctScore = 0; if (total > 0) { const ratio = complexPunct / total; if (ratio < 0.1) { punctScore = (0.1 - ratio) / 0.1; reasons.push(`low_punctuation(${(ratio * 100).toFixed(0)}%)`); } } else { // Aucune ponctuation = suspect punctScore = 0.3; reasons.push('no_punctuation'); } score += punctScore * 0.50; // === MÉTRIQUE 2: Longueur moyenne mots (poids 30%) === const lengths = words.map(w => w.replace(/[^\w]/g, '').length).filter(l => l > 0); if (lengths.length > 0) { const avg = lengths.reduce((a, b) => a + b) / lengths.length; // Mots trop longs = formel/IA (avg > 7 lettres) if (avg > 7) { const wordLengthScore = (avg - 7) / 5; // Normaliser 7→12 = 0→1 score += Math.min(1, wordLengthScore) * 0.30; reasons.push(`long_words(avg=${avg.toFixed(1)})`); } } // === MÉTRIQUE 3: Ton formel (poids 20%) === const lowerContent = content.toLowerCase(); // Mots formels suspects const formalWords = ['optimal', 'idéal', 'efficace', 'robuste', 'innovant', 'essentiel', 'crucial']; const formalCount = formalWords.reduce((c, w) => c + (lowerContent.includes(w) ? 1 : 0), 0); // Mots casual const casualWords = ['super', 'top', 'cool', 'bref', 'truc', 'machin', 'genre']; const casualCount = casualWords.reduce((c, w) => c + (lowerContent.includes(w) ? 1 : 0), 0); if (formalCount > 0 && casualCount === 0 && words.length > 5) { score += 0.20; reasons.push(`formal_tone(${formalCount}_mots)`); } return { score: Math.min(1, score), reasons: reasons.length > 0 ? reasons : ['short_text_ok'], metrics: { textLength: words.length, punctuationRatio: total > 0 ? complexPunct / total : 0, avgWordLength: lengths.length > 0 ? lengths.reduce((a, b) => a + b) / lengths.length : 0 } }; } /** * Nettoyer contenu adversarial généré */ function cleanAdversarialContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré)[:\s]*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } /** * Compter éléments modifiés */ function countModifiedElements(original, modified) { let count = 0; Object.keys(original).forEach(tag => { if (modified[tag] && modified[tag] !== original[tag]) { count++; } }); return count; } /** * Chunk array utility */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Sleep utility */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } module.exports = { applyAdversarialLayer, // ← MAIN ENTRY POINT MODULAIRE applyRegenerationMethod, applyEnhancementMethod, applyHybridMethod, assessDetectionRisk, selectElementsForEnhancement };