seo-generator-server/lib/adversarial-generation/AdversarialCore.js
StillHammer 6335a16f99 feat(adversarial): Tournures originales + grammaire audacieuse + anecdotes - Qualité maximale
TOURNURES ORIGINALES ET INATTENDUES:
- Toutes stratégies: Instructions explicites "Construit phrases de façon SURPRENANTE"
- Reformulations avec angles TOTALEMENT INÉDITS
- Grammaire créative comme principe transversal

GRAMMAIRE AUDACIEUSE:
- Instructions "Ose structures atypiques, inversions, ellipses créatives"
- Poussé dans toutes stratégies (GPTZero, Originality, CopyLeaks, Winston, General)
- Structures non-standard encouragées fortement

ANECDOTES SYSTÉMATIQUES:
- Nouvelle instruction dans tous prompts: "Intègre mini-histoires, exemples vécus"
- Instructions spécifiques par élément (intro, texte, conclusion, FAQ)
- Transformation données factuelles en récits authentiques

RETRAIT MENTIONS "SUBTILE":
- Remplacé "subtil" par "MARQUÉ", "FORT", "NOTABLE"
- Focus qualité MAXIMALE et originalité REMARQUABLE
- Contenu qui se DÉMARQUE vraiment

ENRICHISSEMENTS PAR STRATÉGIE:
- GPTZeroStrategy: +4 règles (tournures, grammaire, anecdotes, qualité max)
- OriginalityStrategy: +2 règles (tournures, anecdotes)
- CopyLeaksStrategy: +2 règles (grammaire créative, anecdotes récits)
- WinstonStrategy: +2 règles (tournures imprévisibles, anecdotes)
- GeneralStrategy: +3 règles (tournures, grammaire, anecdotes)

ADVERSARIAL CORE PROMPTS:
- createRegenerationPrompt: +5 consignes (tournures, grammaire, anecdotes, originalité, qualité max)
- createEnhancementPrompt: +4 consignes (tournures, grammaire, anecdotes, qualité max)
- generateElementSpecificInstructions: Enrichi tous types (titres, intro, texte, FAQ, conclusion)
- getElementSpecificTip: Tips enrichis avec tournures/grammaire/anecdotes

IMPACT:
- Prompts passent de "amélioration légère" à "qualité maximale remarquable"
- Focus absolu sur originalité, authenticité, contenu mémorable
- Anti-détection via créativité authentique plutôt que manipulation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 20:56:23 +08:00

1129 lines
40 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}
- TOURNURES ORIGINALES: Construit phrases de façon SURPRENANTE et INATTENDUE
- GRAMMAIRE AUDACIEUSE: Ose structures atypiques, inversions créatives, ellipses
- Utilise expressions françaises AUTHENTIQUES et tournures idiomatiques naturelles
- Varie longueurs phrases RADICALEMENT : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)
- Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations
- ANECDOTES: Intègre mini-histoires, exemples vécus, observations personnelles pour illustrer
- Ne génère JAMAIS contenu générique, sois spécifique, informatif, et ORIGINAL
- QUALITÉ MAXIMALE: Produis contenu remarquable, mémorable, authentique
- Intensité adversariale: ${intensity.toFixed(2)}
${generatePersonalityInstructions(personality, intensity)}
${generateTitleContext(associatedTitle)}
${generateElementSpecificInstructions(chunk)}
IMPORTANT:
- Ces contraintes doivent sembler NATURELLES, AUTHENTIQUES, jamais forcées
- Priorité ABSOLUE à la QUALITÉ et l'ORIGINALITÉ du contenu
- Chaque phrase doit être REMARQUABLE et MÉMORABLE
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 ABSOLUMENT formules marketing lisses, préfère authentique et PERCUTANT`;
instructions += `\n TOURNURES ORIGINALES: Varie structure radicalement (question, affirmation, fragment créatif)`;
instructions += `\n GRAMMAIRE AUDACIEUSE: Ose inversions, ellipses, constructions atypiques`;
}
if (elementTypes.has('intro')) {
instructions += `\n• INTRO: Commence par angle TOTALEMENT INATTENDU : anecdote vivante, constat surprenant, question engageante`;
instructions += `\n Évite intro-types, crée SURPRISE forte puis retour naturel au sujet`;
instructions += `\n ANECDOTE: Commence si possible par mini-histoire, exemple vécu authentique`;
}
if (elementTypes.has('texte')) {
instructions += `\n• TEXTES: Mélange informations factuelles et observations personnelles AUTHENTIQUES`;
instructions += `\n Intègre apartés FRÉQUENTS : "(j'ai testé, c'est bluffant)", "(crois-moi)", questions rhétoriques`;
instructions += `\n ANECDOTES: Ajoute mini-histoires, exemples vécus pour illustrer chaque point important`;
instructions += `\n QUALITÉ MAXIMALE: Contenu remarquable, mémorable, qui se DÉMARQUE`;
}
if (elementTypes.has('faq_question')) {
instructions += `\n• QUESTIONS FAQ: Formulations VRAIMENT utilisées par clients, jamais académiques`;
instructions += `\n TOURNURES NATURELLES: Langage oral, expressions familières authentiques`;
}
if (elementTypes.has('faq_reponse')) {
instructions += `\n• RÉPONSES FAQ: Ajoute nuances FRÉQUENTES, "ça dépend", précisions contextuelles authentiques`;
instructions += `\n ANECDOTES: Illustre avec exemples concrets, mini-histoires vécues`;
}
if (elementTypes.has('conclusion')) {
instructions += `\n• CONCLUSION: Personnalise FORTEMENT avec avis subjectif marqué ou ouverture INATTENDUE`;
instructions += `\n ANECDOTE FINALE: Termine si possible par exemple vécu, observation personnelle mémorable`;
}
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:
- TOURNURES ORIGINALES: Reformule avec constructions SURPRENANTES et créatives
- GRAMMAIRE AUDACIEUSE: Ose structures atypiques, inversions, ellipses créatives
- Remplace mots typiques IA par synonymes NATURELS, expressions INVENTIVES
- Varie longueurs phrases et structures syntaxiques RADICALEMENT
- Utilise expressions idiomatiques françaises AUTHENTIQUES et tournures familières
- Ajoute nuances humaines FRÉQUENTES : "peut-être", "généralement", "souvent"
- Intègre connecteurs VARIÉS et naturels selon contexte
- ANECDOTES: Ajoute mini-histoires, exemples vécus, observations personnelles
- Ne génère JAMAIS contenu générique, sois spécifique, informatif et ORIGINAL
- QUALITÉ MAXIMALE: Contenu remarquable, mémorable, authentique
${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 EFFICACES et MARQUÉES pour anti-détection maximale
- GARDE le fond du message intact (informations factuelles identiques)
- Focus sur réduction détection ${detectorTarget} avec NATURALITÉ et AUTHENTICITÉ
- Priorité ABSOLUE : QUALITÉ et ORIGINALITÉ du contenu
${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, AUTHENTIQUES, jamais forcées
- Priorité ABSOLUE : QUALITÉ MAXIMALE et ORIGINALITÉ remarquable`;
return prompt;
}
/**
* Obtenir conseil spécifique pour type d'élément (enhancement)
*/
function getElementSpecificTip(elementType) {
const tips = {
'titre_h1': 'TIP: TOURNURES ORIGINALES, évite formules marketing, grammaire AUDACIEUSE (inversion, ellipse)',
'titre_h2': 'TIP: Varie structure RADICALEMENT (question percutante/affirmation créative/fragment surprenant)',
'intro': 'TIP: Commence par angle TOTALEMENT INATTENDU, ajoute ANECDOTE vivante si possible',
'texte': 'TIP: Ajoute observations personnelles AUTHENTIQUES, ANECDOTES détaillées, apartés fréquents',
'faq_question': 'TIP: Formulation VRAIMENT client (oral, familier), jamais académique',
'faq_reponse': 'TIP: Ajoute nuances FRÉQUENTES "ça dépend", ANECDOTES concrètes, précisions authentiques',
'conclusion': 'TIP: Personnalise FORTEMENT avec avis subjectif MARQUÉ, ANECDOTE finale mémorable'
};
return tips[elementType] || 'TIP: Rends plus naturel, humain, ORIGINAL avec ANECDOTES';
}
/**
* 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
};