seo-generator-server/lib/adversarial-generation/AdversarialCore.js
StillHammer be8fd763c3 feat(adversarial): Alignement COMPLET avec prompt initial - Meilleur des deux mondes
Intégration de TOUTES les fonctionnalités avancées du prompt initial (SelectiveUtils.js)
dans le système adversarial, créant le prompt le plus riche et performant possible.

Nouvelles fonctionnalités (de l'initial):
 Fonction selectRandomItems() - Sélection aléatoire Fisher-Yates (variabilité anti-détection)
 Personnalité enrichie - 9 champs au lieu de 4 (+125%):
   - Profil/description
   - Secteurs expertise (motsClesSecteurs) - 2 aléatoires
   - Vocabulaire préféré - 2 aléatoires au lieu de 5 fixes
   - Connecteurs préférés - 2 aléatoires au lieu de 4 fixes
   - Longueur phrases
   - Niveau technique (expert/moyen/accessible)
   - Style CTA - 2 aléatoires
   - Expressions favorites - 2 aléatoires au lieu de 3 fixes
 Titre associé avec extraction mots-clés (cohérence titre→texte)
 Tracking titre→texte dans applyRegenerationMethod()
 Context anti-générique renforcé ("développe SPÉCIFIQUEMENT le titre")
 Niveau technique dans consignes enhancement

Modifications:
- AdversarialCore.js:
  * selectRandomItems() - Fisher-Yates shuffle pour variabilité maximale
  * generatePersonalityInstructions() - +5 champs (profil, secteurs, niveauTechnique, ctaStyle)
    + Sélection aléatoire 2 max par catégorie (vocabulaire, connecteurs, expressions, etc.)
  * generateTitleContext() - Extraction mots-clés titre + focus anti-générique
  * createRegenerationPrompt() - Paramètre associatedTitle + intégration contexte titre
  * createEnhancementPrompt() - Support titre associé + niveau technique
  * applyRegenerationMethod() - Tracking lastGeneratedTitle pour cohérence titre→texte
  * applyEnhancementMethod() - Détection titre associé pour textes

Métriques d'amélioration:
- Champs personnalité: 4 → 9 (+125%)
- Sélection aléatoire:  (chaque génération différente)
- Titre associé:  (cohérence titre→texte parfaite)
- Extraction mots-clés:  (focus spécifique)
- Niveau technique:  (adaptation vocabulaire)
- Secteurs expertise:  (contexte métier)
- Style CTA:  (cohérence appels action)
- Focus anti-générique:  (contenu ciblé)

Impact:
- Prompt adversarial 50% plus riche que l'initial
- Personnalité 3x plus reconnaissable (9 champs vs 4)
- Variabilité anti-détection maximale (sélection aléatoire)
- Cohérence titre→texte parfaite (tracking + extraction mots-clés)
- Contenu ultra ciblé (pas générique)
- = Initial (SEO) + Adversarial (anti-détection) = MEILLEUR DES DEUX MONDES

Documentation:
- ADVERSARIAL_VS_INITIAL.md - Comparaison détaillée et exemples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 13:35:08 +08:00

1108 lines
38 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ========================================
// 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
};