Module system code base

This commit is contained in:
Trouve Alexis 2025-09-04 21:24:45 +08:00
parent ad9e3e1374
commit 590f6a93a8
32 changed files with 12974 additions and 53 deletions

126
ARCHITECTURE_REFACTOR.md Normal file
View File

@ -0,0 +1,126 @@
# 🏗️ REFACTORISATION ARCHITECTURE - GÉNÉRATION PAR ÉTAPES
## 📁 **NOUVELLE STRUCTURE FICHIERS**
```
lib/
├── Main.js ← ORCHESTRATEUR PRINCIPAL
├── ContentGeneration.js ← ORCHESTRATEUR GÉNÉRATION (4 étapes)
├── generation/
│ ├── InitialGeneration.js ← ÉTAPE 1: Génération base (Claude)
│ ├── TechnicalEnhancement.js ← ÉTAPE 2: Amélioration technique (GPT-4)
│ ├── TransitionEnhancement.js ← ÉTAPE 3: Fluidité transitions (Gemini)
│ └── StyleEnhancement.js ← ÉTAPE 4: Style personnalité (Mistral)
└── [autres fichiers existants...]
```
## 🎯 **PRINCIPE D'ARCHITECTURE**
### **1 FICHIER = 1 ÉTAPE = 1 RESPONSABILITÉ**
- Chaque fichier a **UN SEUL OBJECTIF**
- Chaque fichier a **UN LLM PRINCIPAL**
- Chaque fichier **TESTABLE INDÉPENDAMMENT**
### **MAIN ENTRY POINT PAR FICHIER**
Chaque fichier expose une fonction principale claire :
- `InitialGeneration.js``generateInitialContent()`
- `TechnicalEnhancement.js``enhanceTechnicalTerms()`
- `TransitionEnhancement.js``enhanceTransitions()`
- `StyleEnhancement.js``applyPersonalityStyle()`
## 🔄 **FLUX D'ORCHESTRATION**
```
Main.js:handleFullWorkflow()
ContentGeneration.js:generateWithSelectiveEnhancement()
InitialGeneration.js:generateInitialContent() [Claude]
TechnicalEnhancement.js:enhanceTechnicalTerms() [GPT-4]
TransitionEnhancement.js:enhanceTransitions() [Gemini]
StyleEnhancement.js:applyPersonalityStyle() [Mistral]
RÉSULTAT FINAL
```
## 📋 **INTERFACES STANDARDISÉES**
### **INPUT/OUTPUT CHAQUE ÉTAPE**
```javascript
// INPUT standardisé
{
content: { "|tag1|": "contenu1", "|tag2|": "contenu2" },
csvData: { mc0, t0, personality, ... },
context: { step, totalSteps, metadata }
}
// OUTPUT standardisé
{
content: { "|tag1|": "contenu_amélioré1", "|tag2|": "contenu_amélioré2" },
stats: { processed: 15, enhanced: 8, duration: 2500 },
debug: { llmProvider: "claude", tokens: 1200 }
}
```
## ⚡ **AVANTAGES ARCHITECTURE**
### **DÉVELOPPEMENT**
- ✅ Debug par étape indépendante
- ✅ Tests unitaires par fichier
- ✅ Ajout/suppression d'étapes facile
- ✅ Code plus lisible et maintenable
### **PRODUCTION**
- ✅ Bypass d'étapes si problème
- ✅ Monitoring précis par étape
- ✅ Optimisation performance individuelle
- ✅ Rollback par étape possible
## 🔧 **MIGRATION PROGRESSIVE**
### **PHASE 1**: Créer nouvelle structure
- Créer dossier `generation/` et fichiers
- Garder ancien code fonctionnel
### **PHASE 2**: Migrer étape par étape
- InitialGeneration.js d'abord
- Puis TechnicalEnhancement.js
- Etc.
### **PHASE 3**: Nettoyer ancien code
- Supprimer SelectiveEnhancement.js
- Mettre à jour imports
## 🎨 **EXEMPLE STRUCTURE FICHIER**
```javascript
// generation/InitialGeneration.js
const { callLLM } = require('../LLMManager');
const { tracer } = require('../trace');
/**
* ÉTAPE 1: GÉNÉRATION INITIALE
* Responsabilité: Créer le contenu de base avec Claude
* Input: hierarchy, csvData
* Output: contenu généré initial
*/
async function generateInitialContent(hierarchy, csvData) {
return await tracer.run('InitialGeneration.generateInitialContent()', async () => {
// Logique génération initiale
// Claude uniquement
});
}
// Helper functions locales
function createBasePrompt() { /* ... */ }
function parseInitialResponse() { /* ... */ }
module.exports = {
generateInitialContent, // ← MAIN ENTRY POINT
createBasePrompt, // ← Helpers si besoin externe
parseInitialResponse
};
```

555
fdsm Normal file
View File

@ -0,0 +1,555 @@
diff --git a/.gitignore b/.gitignore
index cff1b08..7217f01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,8 +53,7 @@ test_*.js
*_debug.js
test-*.js

-# HTML généré (logs viewer)
-logs-viewer.html
+# HTML généré était ici mais maintenant on le garde dans tools/

# Unit test reports
TEST*.xml
diff --git a/code.js b/code.js
index a1dcd72..a2bdb22 100644
--- a/code.js
+++ b/code.js
@@ -1,6 +1,6 @@
/*
code.js — bundle concaténé
- Généré: 2025-09-03T04:21:57.159Z
+ Généré: 2025-09-04T01:10:08.540Z
Source: lib
Fichiers: 16
Ordre: topo
@@ -57,12 +57,13 @@ const fileDest = pino.destination({
});
tee.pipe(fileDest);

-// Custom levels for Pino to include TRACE and PROMPT
+// Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = {
trace: 5, // Below debug (10)
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
+ llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30,
error: 40,
fatal: 50
@@ -178,6 +179,9 @@ async function logSh(message, level = 'INFO') {
case 'prompt':
logger.prompt(traceData, message);
break;
+ case 'llm':
+ logger.llm(traceData, message);
+ break;
default:
logger.info(traceData, message);
}
@@ -1282,7 +1286,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, 'PROMPT');
- logSh(`===== FIN PROMPT ${llmProvider.toUpperCase()} (${personality?.nom || 'AUCUNE'}) =====\n`, 'PROMPT');
+ 
+ // 📤 LOG LLM REQUEST COMPLET
+ logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
+ logSh(prompt, 'LLM');

// Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality);
@@ -1293,8 +1300,12 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response);

+ // 📥 LOG LLM RESPONSE COMPLET
+ logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
+ logSh(content, 'LLM');
+ 
const duration = Date.now() - startTime;
- logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms: "${content.substring(0, 150)}${content.length > 150 ? '...' : ''}"`, 'INFO');
+ logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');

// Enregistrer les stats d'usage
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
@@ -1727,6 +1738,8 @@ module.exports = {
LLM_CONFIG
};

+
+
/*
┌────────────────────────────────────────────────────────────────────┐
│ File: lib/ElementExtraction.js │
@@ -2309,38 +2322,21 @@ CONTEXTE:
`;

missingElements.forEach((missing, index) => {
- prompt += `${index + 1}. [${missing.name}] `;
- 
- // INSTRUCTIONS SPÉCIFIQUES PAR TYPE
- if (missing.type.includes('titre_h1')) {
- prompt += `→ Titre H1 principal (8-10 mots) pour ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('titre_h2')) {
- prompt += `→ Titre H2 section (6-8 mots) lié à ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('titre_h3')) {
- prompt += `→ Sous-titre H3 (4-6 mots) spécialisé ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('texte') || missing.type.includes('txt')) {
- prompt += `→ Thème/sujet pour paragraphe 150 mots sur ${contextAnalysis.mainKeyword}\n`;
- } else if (missing.type.includes('faq_question')) {
- prompt += `→ Question client directe sur ${contextAnalysis.mainKeyword} (8-12 mots)\n`;
- } else if (missing.type.includes('faq_reponse')) {
- prompt += `→ Thème réponse experte ${contextAnalysis.mainKeyword} (2-4 mots)\n`;
- } else {
- prompt += `→ Expression/mot-clé pertinent ${contextAnalysis.mainKeyword}\n`;
- }
+ prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
});

prompt += `\nCONSIGNES:
-- Reste dans le thème ${contextAnalysis.mainKeyword}
-- Varie les angles et expressions
-- Évite répétitions avec mots-clés existants
-- Précis et pertinents
+- Thème: ${contextAnalysis.mainKeyword}
+- Mots-clés SEO naturels
+- Varie les termes
+- Évite répétitions

FORMAT:
[${missingElements[0].name}]
-Expression/mot-clé généré 1
+mot-clé

[${missingElements[1] ? missingElements[1].name : 'exemple'}]
-Expression/mot-clé généré 2
+mot-clé

etc...`;

@@ -2596,6 +2592,11 @@ const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace.js');
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');

+// Utilitaire pour les délais
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
/**
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
@@ -2804,8 +2805,8 @@ async function generateAllContentBase(hierarchy, csvData, aiProvider) {
}

/**
- * ÉTAPE 2 - Enhancement technique BATCH OPTIMISÉ avec IA configurable
- * OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+
+ * ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
+ * NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
*/
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
@@ -2872,96 +2873,96 @@ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
}

/**
- * NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
+ * Analyser un seul élément pour détecter les termes techniques
*/
-async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
- const contentEntries = Object.keys(baseContents);
- 
- const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
+async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
+ const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.

CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression

-CONTENUS À ANALYSER:
-
-${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
-CONTENU: "${baseContents[tag]}"`).join('\n\n')}
+CONTENU À ANALYSER:
+TAG: ${tag}
+CONTENU: "${content}"

CONSIGNES:
-- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
+- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
-- Focus: matériaux, procédés, normes, dimensions, technologies
-- Si aucun terme technique → "AUCUN"
+- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques

-EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
-EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
+EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
+EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance

-FORMAT RÉPONSE EXACT:
-[1] dibond, impression UV, 3mm OU AUCUN
-[2] aluminium, fraisage CNC OU AUCUN
-[3] AUCUN
-etc... (${contentEntries.length} lignes total)`;
+RÉPONSE REQUISE:
+- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
+- Si aucun terme technique: "NON"
+
+EXEMPLE:
+OUI - termes: aluminium composite, impression numérique, gravure laser`;

try {
- const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
- temperature: 0.3,
- maxTokens: 2000
- }, csvData.personality);
- 
- return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
- 
+ const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
+ 
+ if (response.toUpperCase().startsWith('OUI')) {
+ // Extraire les termes de la réponse
+ const termsMatch = response.match(/termes:\s*(.+)/i);
+ const terms = termsMatch ? termsMatch[1].trim() : '';
+ logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
+ return true;
+ } else {
+ logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG'); 
+ return false;
+ }
} catch (error) {
- logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
- throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
+ logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
+ return false; // En cas d'erreur, on skip l'enhancement
}
}

/**
- * NOUVELLE FONCTION : Enhancement batch TOUS les éléments
+ * Enhancer un seul élément techniquement
*/
-async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
- if (elementsNeedingEnhancement.length === 0) return {};
- 
- const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
+async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
+ const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.

-PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
-CONTEXTE: ${csvData.mc0} - Secteur: Signalétique/impression
-VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
-
-CONTENUS + TERMES À AMÉLIORER:
-
-${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
-CONTENU ACTUEL: "${item.content}"
-TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
+CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression

-CONSIGNES STRICTES:
-- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
-- GARDE la même longueur, structure et ton
-- Intègre naturellement les termes techniques listés
-- NE CHANGE PAS le fond du message ni le style personnel
-- Utilise un vocabulaire expert mais accessible
-- ÉVITE les répétitions excessives
-- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
-- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
+CONTENU À AMÉLIORER:
+TAG: ${tag}
+CONTENU: "${content}"

-FORMAT RÉPONSE:
-[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
-[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
-etc... (${elementsNeedingEnhancement.length} éléments total)`;
+OBJECTIFS:
+- Remplace les termes génériques par des termes techniques précis
+- Ajoute des spécifications techniques réalistes
+- Maintient le même style et longueur
+- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
+
+EXEMPLE DE TRANSFORMATION:
+"matériaux haute performance" → "dibond 3mm ou aluminium composite"
+"impression moderne" → "impression UV haute définition"
+"fixation solide" → "fixation par chevilles inox Ø6mm"
+
+CONTRAINTES:
+- GARDE la même structure
+- MÊME longueur approximative
+- Style cohérent avec l'original
+- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;

try {
- const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
- temperature: 0.4,
- maxTokens: 5000 // Plus large pour batch total
- }, csvData.personality);
- 
- return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
- 
+ const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
+ return enhancedContent.trim();
} catch (error) {
- logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
- throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
+ logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
+ return content; // En cas d'erreur, on retourne le contenu original
}
}

+// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
+
+/**
+ * NOUVELLE FONCTION : Enhancement batch TOUS les éléments
+ */
+// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
+
/**
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
*/
@@ -3015,7 +3016,8 @@ async function enhanceAllTransitions(baseContents, csvData, aiProvider) {

const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.

-PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
+CONTEXTE: Article SEO professionnel pour site web commercial
+PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web)
CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}

CONTENUS:
@@ -3093,9 +3095,10 @@ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {

const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.

+CONTEXTE: Finalisation article SEO pour site e-commerce professionnel
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
-STYLE CIBLE: ${personality.style}
+STYLE CIBLE: ${personality.style} adapté au web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
@@ -3149,9 +3152,8 @@ etc...`;
/**
* Sleep function replacement for Utilities.sleep
*/
-function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
+
+// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12

/**
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
@@ -3187,32 +3189,33 @@ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
function createBatchFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;

- let prompt = `PERSONNALITÉ: ${personality.nom} | ${personality.description}
-STYLE: ${personality.style}
-VOCABULAIRE: ${personality.vocabulairePref}
-CONNECTEURS: ${personality.connecteursPref}
-NIVEAU TECHNIQUE: ${personality.niveauTechnique}
+ let prompt = `=== 1. CONTEXTE ===
+Entreprise: Autocollant.fr - signalétique personnalisée
+Sujet: ${csvData.mc0}
+Section: FAQ pour article SEO commercial

-GÉNÈRE ${faqPairs.length} PAIRES FAQ COHÉRENTES pour ${csvData.mc0}:
+=== 2. PERSONNALITÉ ===
+Rédacteur: ${personality.nom}
+Style: ${personality.style}
+Ton: ${personality.description || 'professionnel'}

-RÈGLES STRICTES:
-- QUESTIONS: Neutres, directes, langage client naturel (8-15 mots)
-- RÉPONSES: Style ${personality.style}, vocabulaire ${personality.vocabulairePref} (50-80 mots)
-- Sujets à couvrir: prix, livraison, personnalisation, installation, durabilité
-- ÉVITE répétitions excessives et expressions trop familières
-- Style ${personality.nom} reconnaissable mais PROFESSIONNEL
-- PAS de messages d'excuse ("je n'ai pas l'information")
-- RÉPONDS DIRECTEMENT par questions et réponses, sans préfixe
+=== 3. RÈGLES GÉNÉRALES ===
+- Questions naturelles de clients
+- Réponses expertes et rassurantes 
+- Langage professionnel mais accessible
+- Textes rédigés humainement et de façon authentique
+- Couvrir: prix, livraison, personnalisation, installation, durabilité
+- IMPÉRATIF: Respecter strictement les contraintes XML
+
+=== 4. PAIRES FAQ À GÉNÉRER ===

-PAIRES À GÉNÉRER:
`;

faqPairs.forEach((pair, index) => {
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');

- prompt += `${index + 1}. [${questionTag}] + [${answerTag}]
- Question client sur ${csvData.mc0} → Réponse ${personality.style}
+ prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
`;
});

@@ -3533,10 +3536,25 @@ function findAssociatedTitle(textElement, existingResults) {
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
const personality = csvData.personality;

- let prompt = `RÉDACTEUR: ${personality.nom} | Style: ${personality.style}
-SUJET: ${csvData.mc0}
-
-${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${elements.length} ${type.toUpperCase()}S PROFESSIONNELS`}:
+ let prompt = `=== 1. CONTEXTE ===
+Entreprise: Autocollant.fr - signalétique personnalisée
+Sujet: ${csvData.mc0}
+Type d'article: SEO professionnel pour site commercial
+
+=== 2. PERSONNALITÉ ===
+Rédacteur: ${personality.nom}
+Style: ${personality.style}
+Ton: ${personality.description || 'professionnel'}
+
+=== 3. RÈGLES GÉNÉRALES ===
+- Contenu SEO optimisé
+- Langage naturel et fluide
+- Éviter répétitions
+- Pas de références techniques dans le contenu
+- Textes rédigés humainement et de façon authentique
+- IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.)
+
+=== 4. ÉLÉMENTS À GÉNÉRER ===
`;

// AJOUTER CONTEXTE DES TITRES POUR LES TEXTES
@@ -3548,7 +3566,7 @@ ${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${el

if (generatedTitles.length > 0) {
prompt += `
-CONTEXTE - TITRES GÉNÉRÉS:
+Titres existants pour contexte:
${generatedTitles.join('\n')}

`;
@@ -3560,34 +3578,33 @@ ${generatedTitles.join('\n')}

prompt += `${index + 1}. [${cleanTag}] `;

- // INSTRUCTIONS SPÉCIFIQUES ET COURTES PAR TYPE
+ // INSTRUCTIONS PROPRES PAR ÉLÉMENT
if (type === 'titre') {
if (elementInfo.element.type === 'titre_h1') {
- prompt += `CRÉER UN TITRE H1 PRINCIPAL (8-12 mots) sur "${csvData.t0}" - NE PAS écrire "Titre_H1_1"\n`;
+ prompt += `Titre principal accrocheur\n`;
} else if (elementInfo.element.type === 'titre_h2') {
- prompt += `CRÉER UN TITRE H2 SECTION (6-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_H2_X"\n`;
+ prompt += `Titre de section engageant\n`;
} else if (elementInfo.element.type === 'titre_h3') {
- prompt += `CRÉER UN TITRE H3 SOUS-SECTION (4-8 mots) - NE PAS écrire "Titre_H3_X"\n`;
+ prompt += `Sous-titre spécialisé\n`;
} else {
- prompt += `CRÉER UN TITRE ACCROCHEUR (4-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_"\n`;
+ prompt += `Titre pertinent\n`;
}
} else if (type === 'texte') {
- const wordCount = elementInfo.element.name && elementInfo.element.name.includes('H2') ? '150' : '100';
- prompt += `Paragraphe ${wordCount} mots, style ${personality.style}\n`;
+ prompt += `Paragraphe informatif\n`;

// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
if (associatedTitle) {
- prompt += ` Développe le titre: "${associatedTitle}"\n`;
+ prompt += ` Contexte: "${associatedTitle}"\n`;
}

if (elementInfo.element.resolvedContent) {
- prompt += ` Thème: "${elementInfo.element.resolvedContent}"\n`;
+ prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
}
} else if (type === 'intro') {
- prompt += `Introduction 80-100 mots, ton accueillant\n`;
+ prompt += `Introduction engageante\n`;
} else {
- prompt += `Contenu pertinent pour ${csvData.mc0}\n`;
+ prompt += `Contenu pertinent\n`;
}
});

@@ -3597,15 +3614,16 @@ ${generatedTitles.join('\n')}
- Phrases: ${personality.longueurPhrases}
- Niveau technique: ${personality.niveauTechnique}

-CONSIGNES STRICTES:
-- RESPECTE le style ${personality.style} de ${personality.nom} mais RESTE PROFESSIONNEL
-- INTERDICTION ABSOLUE: "du coup", "bon", "alors", "franchement", "nickel", "tip-top", "costaud" en excès
-- VARIE les connecteurs: ${personality.connecteursPref}
-- POUR LES TITRES: SEULEMENT le titre réel, JAMAIS de référence "Titre_H1_1" ou "Titre_H2_7"
-- EXEMPLE TITRE: "Plaques personnalisées résistantes aux intempéries" PAS "Titre_H2_1" 
-- RÉPONDS DIRECTEMENT par le contenu demandé, SANS introduction ni nom de tag
-- PAS de message d'excuse du type "je n'ai pas l'information"
-- CONTENU cohérent et professionnel, évite la sur-familiarité
+CONSIGNES STRICTES POUR ARTICLE SEO:
+- CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
+- STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
+- INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top") 
+- VOCABULAIRE: Mélange expertise technique + accessibilité client
+- SEO: Utilise naturellement "${csvData.mc0}" et termes associés
+- POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
+- EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024" 
+- CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial
+- RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe

FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
@@ -3696,107 +3714,11 @@ function cleanXMLTagsFromContent(content) {

// ============= PARSING FUNCTIONS =============

-/**
- * Parser réponse extraction termes
- */
-function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
- const results = [];
- const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
- let match;
- const parsedItems = {};
- 
- // Parser la réponse
- while ((match = regex.exec(response)) !== null) {
- const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
- const termsText = match[2].trim();
- parsedItems[index] = termsText;
- }
- 
- // Mapper aux éléments
- contentEntries.forEach((tag, index) => {
- const termsText = parsedItems[index] || 'AUCUN';
- const hasTerms = !termsText.toUpperCase().includes('AUCUN');
- 
- const technicalTerms = hasTerms ? 
- termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) : 
- [];
- 
- results.push({
- tag: tag,
- content: baseContents[tag],
- technicalTerms: technicalTerms,
- needsEnhancement: hasTerms && technicalTerms.length > 0
- });
- 
- logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
- });
- 
- const enhancementCount = results.filter(r => r.needsEnhancement).length;
- logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
- 
- return results;
[31

View File

@ -1,23 +1,315 @@
// ========================================
// FICHIER: lib/content-generation.js - CONVERTI POUR NODE.JS
// Description: Génération de contenu avec batch enhancement
// ORCHESTRATEUR GÉNÉRATION - ARCHITECTURE REFACTORISÉE
// Responsabilité: Coordonner les 4 étapes de génération
// ========================================
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
const { generateWithBatchEnhancement } = require('./SelectiveEnhancement');
const { tracer } = require('./trace');
// ============= GÉNÉRATION PRINCIPALE - ADAPTÉE =============
// Import des 4 étapes séparées
const { generateInitialContent } = require('./generation/InitialGeneration');
const { enhanceTechnicalTerms } = require('./generation/TechnicalEnhancement');
const { enhanceTransitions } = require('./generation/TransitionEnhancement');
const { applyPersonalityStyle } = require('./generation/StyleEnhancement');
async function generateWithContext(hierarchy, csvData) {
logSh('=== GÉNÉRATION AVEC BATCH ENHANCEMENT ===', 'INFO');
// Import Pattern Breaking (Niveau 2)
const { applyPatternBreaking } = require('./post-processing/PatternBreaking');
// *** UTILISE LE SELECTIVE ENHANCEMENT ***
return await generateWithBatchEnhancement(hierarchy, csvData);
/**
* MAIN ENTRY POINT - GÉNÉRATION AVEC SELECTIVE ENHANCEMENT
* @param {Object} hierarchy - Hiérarchie des éléments extraits
* @param {Object} csvData - Données CSV avec personnalité
* @param {Object} options - Options de génération
* @returns {Object} - Contenu généré final
*/
async function generateWithContext(hierarchy, csvData, options = {}) {
return await tracer.run('ContentGeneration.generateWithContext()', async () => {
const startTime = Date.now();
const pipelineName = options.patternBreaking ? 'selective_enhancement_with_pattern_breaking' : 'selective_enhancement';
const totalSteps = options.patternBreaking ? 5 : 4;
await tracer.annotate({
pipeline: pipelineName,
elementsCount: Object.keys(hierarchy).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0,
options,
totalSteps
});
logSh(`🚀 DÉBUT PIPELINE ${options.patternBreaking ? 'NIVEAU 2' : 'NIVEAU 1'}`, 'INFO');
logSh(` 🎭 Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à traiter`, 'INFO');
logSh(` 🔧 Options: ${JSON.stringify(options)}`, 'DEBUG');
try {
let pipelineResults = {
content: {},
stats: { stages: [], totalDuration: 0 },
debug: { pipeline: 'selective_enhancement', stages: [] }
};
// ÉTAPE 1: GÉNÉRATION INITIALE (Claude)
const step1Result = await generateInitialContent({
hierarchy,
csvData,
context: { step: 1, totalSteps, options }
});
pipelineResults.content = step1Result.content;
pipelineResults.stats.stages.push({ stage: 1, name: 'InitialGeneration', ...step1Result.stats });
pipelineResults.debug.stages.push(step1Result.debug);
// ÉTAPE 2: ENHANCEMENT TECHNIQUE (GPT-4) - Optionnel
if (!options.skipTechnical) {
const step2Result = await enhanceTechnicalTerms({
content: pipelineResults.content,
csvData,
context: { step: 2, totalSteps, options }
});
pipelineResults.content = step2Result.content;
pipelineResults.stats.stages.push({ stage: 2, name: 'TechnicalEnhancement', ...step2Result.stats });
pipelineResults.debug.stages.push(step2Result.debug);
} else {
logSh(`⏭️ ÉTAPE 2/4 IGNORÉE: Enhancement technique désactivé`, 'INFO');
}
// ============= EXPORTS =============
// ÉTAPE 3: ENHANCEMENT TRANSITIONS (Gemini) - Optionnel
if (!options.skipTransitions) {
const step3Result = await enhanceTransitions({
content: pipelineResults.content,
csvData,
context: { step: 3, totalSteps, options }
});
pipelineResults.content = step3Result.content;
pipelineResults.stats.stages.push({ stage: 3, name: 'TransitionEnhancement', ...step3Result.stats });
pipelineResults.debug.stages.push(step3Result.debug);
} else {
logSh(`⏭️ ÉTAPE 3/4 IGNORÉE: Enhancement transitions désactivé`, 'INFO');
}
// ÉTAPE 4: ENHANCEMENT STYLE (Mistral) - Optionnel
if (!options.skipStyle) {
const step4Result = await applyPersonalityStyle({
content: pipelineResults.content,
csvData,
context: { step: 4, totalSteps, options }
});
pipelineResults.content = step4Result.content;
pipelineResults.stats.stages.push({ stage: 4, name: 'StyleEnhancement', ...step4Result.stats });
pipelineResults.debug.stages.push(step4Result.debug);
} else {
logSh(`⏭️ ÉTAPE 4/${totalSteps} IGNORÉE: Enhancement style désactivé`, 'INFO');
}
// ÉTAPE 5: PATTERN BREAKING (NIVEAU 2) - Optionnel
if (options.patternBreaking) {
const step5Result = await applyPatternBreaking({
content: pipelineResults.content,
csvData,
options: options.patternBreakingConfig || {}
});
pipelineResults.content = step5Result.content;
pipelineResults.stats.stages.push({ stage: 5, name: 'PatternBreaking', ...step5Result.stats });
pipelineResults.debug.stages.push(step5Result.debug);
} else if (totalSteps === 5) {
logSh(`⏭️ ÉTAPE 5/5 IGNORÉE: Pattern Breaking désactivé`, 'INFO');
}
// RÉSULTATS FINAUX
const totalDuration = Date.now() - startTime;
pipelineResults.stats.totalDuration = totalDuration;
const totalProcessed = pipelineResults.stats.stages.reduce((sum, stage) => sum + (stage.processed || 0), 0);
const totalEnhanced = pipelineResults.stats.stages.reduce((sum, stage) => sum + (stage.enhanced || 0), 0);
logSh(`✅ PIPELINE TERMINÉ: ${Object.keys(pipelineResults.content).length} éléments générés`, 'INFO');
logSh(` ⏱️ Durée totale: ${totalDuration}ms`, 'INFO');
logSh(` 📈 Enhancements: ${totalEnhanced} sur ${totalProcessed} éléments traités`, 'INFO');
// Log détaillé par étape
pipelineResults.stats.stages.forEach(stage => {
const enhancementRate = stage.processed > 0 ? Math.round((stage.enhanced / stage.processed) * 100) : 0;
logSh(` ${stage.stage}. ${stage.name}: ${stage.enhanced}/${stage.processed} (${enhancementRate}%) en ${stage.duration}ms`, 'DEBUG');
});
await tracer.event(`Pipeline ${pipelineName} terminé`, {
totalElements: Object.keys(pipelineResults.content).length,
totalEnhanced,
totalDuration,
stagesExecuted: pipelineResults.stats.stages.length
});
// Retourner uniquement le contenu pour compatibilité
return pipelineResults.content;
} catch (error) {
const totalDuration = Date.now() - startTime;
logSh(`❌ PIPELINE ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR');
logSh(`❌ Stack trace: ${error.stack}`, 'DEBUG');
await tracer.event(`Pipeline ${pipelineName} échoué`, {
error: error.message,
duration: totalDuration
});
throw new Error(`ContentGeneration pipeline failed: ${error.message}`);
}
}, { hierarchy, csvData, options });
}
/**
* GÉNÉRATION SIMPLE (ÉTAPE 1 UNIQUEMENT)
* Pour tests ou fallback rapide
*/
async function generateSimple(hierarchy, csvData) {
logSh(`🔥 GÉNÉRATION SIMPLE: Claude uniquement`, 'INFO');
const result = await generateInitialContent({
hierarchy,
csvData,
context: { step: 1, totalSteps: 1, simple: true }
});
return result.content;
}
/**
* GÉNÉRATION AVANCÉE AVEC CONTRÔLE GRANULAIRE
* Permet de choisir exactement quelles étapes exécuter
*/
async function generateAdvanced(hierarchy, csvData, stageConfig = {}) {
const {
initial = true,
technical = true,
transitions = true,
style = true,
patternBreaking = false, // ✨ NOUVEAU: Niveau 2
patternBreakingConfig = {} // ✨ NOUVEAU: Config Pattern Breaking
} = stageConfig;
const options = {
skipTechnical: !technical,
skipTransitions: !transitions,
skipStyle: !style,
patternBreaking, // ✨ NOUVEAU
patternBreakingConfig // ✨ NOUVEAU
};
const activeStages = [
initial && 'Initial',
technical && 'Technical',
transitions && 'Transitions',
style && 'Style',
patternBreaking && 'PatternBreaking' // ✨ NOUVEAU
].filter(Boolean);
logSh(`🎛️ GÉNÉRATION AVANCÉE: ${activeStages.join(' + ')}`, 'INFO');
return await generateWithContext(hierarchy, csvData, options);
}
/**
* GÉNÉRATION NIVEAU 2 (AVEC PATTERN BREAKING)
* Shortcut pour activer Pattern Breaking facilement
*/
async function generateWithPatternBreaking(hierarchy, csvData, patternConfig = {}) {
logSh(`🎯 GÉNÉRATION NIVEAU 2: Pattern Breaking activé`, 'INFO');
const options = {
patternBreaking: true,
patternBreakingConfig: {
intensity: 0.6,
sentenceVariation: true,
fingerprintRemoval: true,
transitionHumanization: true,
...patternConfig
}
};
return await generateWithContext(hierarchy, csvData, options);
}
/**
* DIAGNOSTIC PIPELINE
* Exécute chaque étape avec mesures détaillées
*/
async function diagnosticPipeline(hierarchy, csvData) {
logSh(`🔬 MODE DIAGNOSTIC: Analyse détaillée pipeline`, 'INFO');
const diagnostics = {
stages: [],
errors: [],
performance: {},
content: {}
};
let currentContent = {};
try {
// Test étape 1
const step1Start = Date.now();
const step1Result = await generateInitialContent({ hierarchy, csvData });
diagnostics.stages.push({
stage: 1,
name: 'InitialGeneration',
success: true,
duration: Date.now() - step1Start,
elementsGenerated: Object.keys(step1Result.content).length,
stats: step1Result.stats
});
currentContent = step1Result.content;
} catch (error) {
diagnostics.errors.push({ stage: 1, error: error.message });
diagnostics.stages.push({ stage: 1, name: 'InitialGeneration', success: false });
return diagnostics;
}
// Test étapes 2-4 individuellement
const stages = [
{ stage: 2, name: 'TechnicalEnhancement', func: enhanceTechnicalTerms },
{ stage: 3, name: 'TransitionEnhancement', func: enhanceTransitions },
{ stage: 4, name: 'StyleEnhancement', func: applyPersonalityStyle }
];
for (const stageInfo of stages) {
try {
const stageStart = Date.now();
const stageResult = await stageInfo.func({ content: currentContent, csvData });
diagnostics.stages.push({
...stageInfo,
success: true,
duration: Date.now() - stageStart,
stats: stageResult.stats
});
currentContent = stageResult.content;
} catch (error) {
diagnostics.errors.push({ stage: stageInfo.stage, error: error.message });
diagnostics.stages.push({ ...stageInfo, success: false });
}
}
diagnostics.content = currentContent;
diagnostics.performance.totalDuration = diagnostics.stages.reduce((sum, stage) => sum + (stage.duration || 0), 0);
logSh(`🔬 DIAGNOSTIC TERMINÉ: ${diagnostics.stages.filter(s => s.success).length}/4 étapes réussies`, 'INFO');
return diagnostics;
}
module.exports = {
generateWithContext
generateWithContext, // ← MAIN ENTRY POINT (compatible ancien code)
generateSimple, // ← Génération rapide
generateAdvanced, // ← Contrôle granulaire
generateWithPatternBreaking, // ← NOUVEAU: Niveau 2 shortcut
diagnosticPipeline // ← Tests et debug
};

View File

@ -0,0 +1,489 @@
// ========================================
// 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 { DetectorStrategyManager } = 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 = {}
} = config;
await tracer.annotate({
adversarialLayer: true,
detectorTarget,
intensity,
method,
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}`, 'INFO');
try {
// Initialiser stratégie détecteur
const detectorManager = new DetectorStrategyManager(detectorTarget);
const strategy = detectorManager.getStrategy();
// Appliquer méthode adversariale choisie
let adversarialContent = {};
switch (method) {
case 'regeneration':
adversarialContent = await applyRegenerationMethod(existingContent, config, strategy);
break;
case 'enhancement':
adversarialContent = await applyEnhancementMethod(existingContent, config, strategy);
break;
case 'hybrid':
adversarialContent = await applyHybridMethod(existingContent, config, 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,
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) {
logSh(`🔄 Méthode régénération adversariale`, 'DEBUG');
const results = {};
const contentEntries = Object.entries(existingContent);
// 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 {
const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy);
const response = await callLLM('claude', 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);
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) {
logSh(`🔧 Méthode enhancement adversarial`, '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');
const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy);
try {
const response = await callLLM('gpt4', 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) {
const { detectorTarget, intensity, csvData } = config;
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) => `[${i + 1}] TAG: ${tag}
ORIGINAL: "${content}"`).join('\n\n')}
CONSIGNES:
- GARDE exactement le même message et informations factuelles
- CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget}
- Intensité adversariale: ${intensity.toFixed(2)}
${csvData?.personality ? `- Style: ${csvData.personality.nom} (${csvData.personality.style})` : ''}
IMPORTANT: 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;
}
/**
* Créer prompt d'enhancement adversarial
*/
function createEnhancementPrompt(elementsToEnhance, config, strategy) {
const { detectorTarget, intensity } = config;
let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}.
AMÉLIORATIONS CIBLÉES:
${strategy.getEnhancementTips(intensity).join('\n')}
ÉLÉMENTS À AMÉLIORER:
${elementsToEnhance.map((element, i) => `[${i + 1}] TAG: ${element.tag}
CONTENU: "${element.content}"
PROBLÈME: ${element.detectionRisk}`).join('\n\n')}
CONSIGNES:
- Modifications LÉGÈRES et naturelles
- GARDE le fond du message intact
- Focus sur réduction détection ${detectorTarget}
- Intensité: ${intensity.toFixed(2)}
FORMAT:
[1] Contenu légèrement amélioré
[2] Contenu légèrement amélioré
etc...`;
return prompt;
}
/**
* 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 = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsToEnhance.length) {
let enhancedContent = cleanAdversarialContent(match[2].trim());
const element = elementsToEnhance[index];
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
} else {
results[element.tag] = element.content; // Fallback
}
index++;
}
return results;
}
/**
* Sélectionner éléments pour enhancement
*/
function selectElementsForEnhancement(existingContent, config) {
const elements = [];
Object.entries(existingContent).forEach(([tag, content]) => {
const detectionRisk = assessDetectionRisk(content, config.detectorTarget);
if (detectionRisk.score > 0.6) { // Risque élevé
elements.push({
tag,
content,
detectionRisk: detectionRisk.reasons.join(', '),
priority: detectionRisk.score
});
}
});
// Trier par priorité (risque élevé en premier)
elements.sort((a, b) => b.priority - a.priority);
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
*/
function assessDetectionRisk(content, detectorTarget) {
let score = 0;
const reasons = [];
// Indicateurs génériques de contenu IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge'];
const aiCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().includes(word) ? 1 : 0);
}, 0);
if (aiCount > 2) {
score += 0.4;
reasons.push('mots_typiques_ia');
}
// Structure trop parfaite
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length > 2) {
const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length;
const uniformity = 1 - (Math.sqrt(variance) / avgLength);
if (uniformity > 0.8) {
score += 0.3;
reasons.push('structure_uniforme');
}
}
// Spécifique selon détecteur
if (detectorTarget === 'gptZero') {
// GPTZero détecte la prévisibilité
if (content.includes('par ailleurs') && content.includes('en effet')) {
score += 0.3;
reasons.push('connecteurs_prévisibles');
}
}
return { score: Math.min(1, score), reasons };
}
/**
* 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
};

View File

@ -0,0 +1,448 @@
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// Responsabilité: Créer le contenu de base avec Claude + anti-détection
// LLM: Claude Sonnet (température 0.7) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - GÉNÉRATION INITIALE ADVERSARIALE
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function generateInitialContentAdversarial(input) {
return await tracer.run('AdversarialInitialGeneration.generateInitialContentAdversarial()', async () => {
const { hierarchy, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '1/4',
llmProvider: 'claude',
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 1/4 ADVERSARIAL: Génération initiale (Claude + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO');
try {
// Collecter tous les éléments dans l'ordre XML
const allElements = collectElementsInXMLOrder(hierarchy);
// Séparer FAQ pairs et autres éléments
const { faqPairs, otherElements } = separateElementTypes(allElements);
// Générer en chunks pour éviter timeouts
const results = {};
// 1. Générer éléments normaux avec prompts adversariaux
if (otherElements.length > 0) {
const normalResults = await generateNormalElementsAdversarial(otherElements, csvData, config, detectorManager);
Object.assign(results, normalResults);
}
// 2. Générer paires FAQ adversariales si présentes
if (faqPairs.length > 0) {
const faqResults = await generateFAQPairsAdversarial(faqPairs, csvData, config, detectorManager);
Object.assign(results, faqResults);
}
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(results).length,
generated: Object.keys(results).length,
faqPairs: faqPairs.length,
duration
};
logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO');
await tracer.event(`Génération initiale terminée`, stats);
return {
content: results,
stats,
debug: {
llmProvider: 'claude',
step: 1,
elementsGenerated: Object.keys(results),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`InitialGeneration failed: ${error.message}`);
}
}, input);
}
/**
* Générer éléments normaux avec prompts adversariaux en chunks
*/
async function generateNormalElementsAdversarial(elements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération éléments normaux adversariaux: ${elements.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const basePrompt = createBatchPrompt(chunk, csvData);
// Générer prompt adversarial
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: getElementTypeFromChunk(chunk),
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const chunkResults = parseBatchResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments 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');
throw error;
}
}
return results;
}
/**
* Générer paires FAQ adversariales cohérentes
*/
async function generateFAQPairsAdversarial(faqPairs, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Génération paires FAQ adversariales: ${faqPairs.length} paires`, 'DEBUG');
const basePrompt = createFAQPairsPrompt(faqPairs, csvData);
// Générer prompt adversarial spécialisé FAQ
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité légèrement plus élevée pour FAQ
elementType: 'faq_mixed',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const response = await callLLM('claude', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
return parseFAQResponse(response, faqPairs);
}
/**
* Créer prompt batch pour éléments normaux
*/
function createBatchPrompt(elements, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION CONTENU INITIAL ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
ÉLÉMENTS À GÉNÉRER:
`;
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`;
});
prompt += `
STYLE ${personality.nom.toUpperCase()}:
- Vocabulaire: ${personality.vocabulairePref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES:
- Contenu SEO optimisé pour ${csvData.mc0}
- Style ${personality.style} naturel
- Pas de références techniques dans contenu
- RÉPONSE DIRECTE par le contenu
FORMAT:
[${elements[0].tag.replace(/\|/g, '')}]
Contenu généré...
[${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}]
Contenu généré...`;
return prompt;
}
/**
* Parser réponse batch
*/
function parseBatchResponse(response, elements) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux vrais tags
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
} else {
results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`;
logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING');
}
});
return results;
}
/**
* Créer prompt pour paires FAQ
*/
function createFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION PAIRES FAQ ===
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
PAIRES À GÉNÉRER:
`;
faqPairs.forEach((pair, index) => {
const qTag = pair.question.tag.replace(/\|/g, '');
const aTag = pair.answer.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`;
});
prompt += `
CONSIGNES:
- Questions naturelles de clients
- Réponses expertes ${personality.style}
- Couvrir: prix, livraison, personnalisation
FORMAT:
[${faqPairs[0].question.tag.replace(/\|/g, '')}]
Question client naturelle ?
[${faqPairs[0].answer.tag.replace(/\|/g, '')}]
Réponse utile et rassurante.`;
return prompt;
}
/**
* Parser réponse FAQ
*/
function parseFAQResponse(response, faqPairs) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux paires FAQ
faqPairs.forEach(pair => {
const qCleanTag = pair.question.tag.replace(/\|/g, '');
const aCleanTag = pair.answer.tag.replace(/\|/g, '');
if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag];
if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag];
});
return results;
}
// ============= HELPER FUNCTIONS =============
function collectElementsInXMLOrder(hierarchy) {
const allElements = [];
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
allElements.push({
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: section.title.originalElement.type
});
}
if (section.text) {
allElements.push({
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: section.text.originalElement.type
});
}
section.questions.forEach(q => {
allElements.push({
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
});
});
});
return allElements;
}
function separateElementTypes(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// Collecter FAQ questions et answers
allElements.forEach(element => {
if (element.type === 'faq_question') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
} else if (element.type === 'faq_reponse') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
} else {
otherElements.push(element);
}
});
// Créer paires FAQ
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({ number, question, answer });
} else if (question) {
otherElements.push(question);
} else if (answer) {
otherElements.push(answer);
}
});
return { faqPairs, otherElements };
}
function getElementDescription(elementInfo) {
switch (elementInfo.type) {
case 'titre_h1': return 'Titre principal accrocheur';
case 'titre_h2': return 'Titre de section';
case 'titre_h3': return 'Sous-titre';
case 'intro': return 'Introduction engageante';
case 'texte': return 'Paragraphe informatif';
default: return 'Contenu pertinent';
}
}
function cleanGeneratedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Helper: Déterminer type d'élément dominant dans un chunk
*/
function getElementTypeFromChunk(chunk) {
if (!chunk || chunk.length === 0) return 'generic';
// Compter les types dans le chunk
const typeCounts = {};
chunk.forEach(element => {
const type = element.type || 'generic';
typeCounts[type] = (typeCounts[type] || 0) + 1;
});
// Retourner type le plus fréquent
return Object.keys(typeCounts).reduce((a, b) =>
typeCounts[a] > typeCounts[b] ? a : b
);
}
module.exports = {
generateInitialContentAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
generateNormalElementsAdversarial,
generateFAQPairsAdversarial,
createBatchPrompt,
parseBatchResponse,
collectElementsInXMLOrder,
separateElementTypes,
getElementTypeFromChunk
};

View File

@ -0,0 +1,413 @@
// ========================================
// ADVERSARIAL LAYERS - COUCHES MODULAIRES
// Responsabilité: Couches adversariales composables et réutilisables
// Architecture: Fonction pipeline |> layer1 |> layer2 |> layer3
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applyAdversarialLayer } = require('./AdversarialCore');
/**
* COUCHE ANTI-GPTZEERO - Spécialisée contre GPTZero
*/
async function applyAntiGPTZeroLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'gptZero',
intensity: options.intensity || 1.0,
method: options.method || 'regeneration',
...options
});
}
/**
* COUCHE ANTI-ORIGINALITY - Spécialisée contre Originality.ai
*/
async function applyAntiOriginalityLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'originality',
intensity: options.intensity || 1.1,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE ANTI-WINSTON - Spécialisée contre Winston AI
*/
async function applyAntiWinstonLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'winston',
intensity: options.intensity || 0.9,
method: options.method || 'enhancement',
...options
});
}
/**
* COUCHE GÉNÉRALE - Protection généraliste multi-détecteurs
*/
async function applyGeneralAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: 'general',
intensity: options.intensity || 0.8,
method: options.method || 'hybrid',
...options
});
}
/**
* COUCHE LÉGÈRE - Modifications subtiles pour préserver qualité
*/
async function applyLightAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'general',
intensity: 0.5,
method: 'enhancement',
preserveStructure: true,
...options
});
}
/**
* COUCHE INTENSIVE - Maximum anti-détection
*/
async function applyIntensiveAdversarialLayer(content, options = {}) {
return await applyAdversarialLayer(content, {
detectorTarget: options.detectorTarget || 'gptZero',
intensity: 1.5,
method: 'regeneration',
preserveStructure: false,
...options
});
}
/**
* PIPELINE COMPOSABLE - Application séquentielle de couches
*/
async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
return await tracer.run('AdversarialLayers.applyLayerPipeline()', async () => {
await tracer.annotate({
layersPipeline: true,
layersCount: layers.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES ADVERSARIALES: ${layers.length} couches`, 'INFO');
let currentContent = content;
const pipelineStats = {
layers: [],
totalDuration: 0,
totalModifications: 0,
success: true
};
try {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
const layerStartTime = Date.now();
logSh(` 🎯 Couche ${i + 1}/${layers.length}: ${layer.name || layer.type || 'anonyme'}`, 'DEBUG');
try {
const layerResult = await applyLayerByConfig(currentContent, layer, globalOptions);
currentContent = layerResult.content;
const layerStats = {
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
modificationsCount: layerResult.stats?.elementsModified || 0,
success: true
};
pipelineStats.layers.push(layerStats);
pipelineStats.totalModifications += layerStats.modificationsCount;
logSh(`${layerStats.name}: ${layerStats.modificationsCount} modifs (${layerStats.duration}ms)`, 'DEBUG');
} catch (error) {
logSh(` ❌ Couche ${i + 1} échouée: ${error.message}`, 'ERROR');
pipelineStats.layers.push({
name: layer.name || `layer_${i + 1}`,
type: layer.type,
duration: Date.now() - layerStartTime,
success: false,
error: error.message
});
// Continuer avec le contenu précédent si une couche échoue
if (!globalOptions.stopOnError) {
continue;
} else {
throw error;
}
}
}
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = pipelineStats.layers.every(layer => layer.success);
logSh(`🔄 PIPELINE TERMINÉ: ${pipelineStats.totalModifications} modifs totales (${pipelineStats.totalDuration}ms)`, 'INFO');
await tracer.event('Pipeline couches terminé', pipelineStats);
return {
content: currentContent,
stats: pipelineStats,
original: content
};
} catch (error) {
pipelineStats.totalDuration = Date.now() - startTime;
pipelineStats.success = false;
logSh(`❌ PIPELINE COUCHES ÉCHOUÉ après ${pipelineStats.totalDuration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { layers: layers.map(l => l.name || l.type), content: Object.keys(content) });
}
/**
* COUCHES PRÉDÉFINIES - Configurations courantes
*/
const PREDEFINED_LAYERS = {
// Stack défensif léger
lightDefense: [
{ type: 'general', name: 'General Light', intensity: 0.6, method: 'enhancement' },
{ type: 'anti-gptZero', name: 'GPTZero Light', intensity: 0.5, method: 'enhancement' }
],
// Stack défensif standard
standardDefense: [
{ type: 'general', name: 'General Standard', intensity: 0.8, method: 'hybrid' },
{ type: 'anti-gptZero', name: 'GPTZero Standard', intensity: 0.9, method: 'enhancement' },
{ type: 'anti-originality', name: 'Originality Standard', intensity: 0.8, method: 'enhancement' }
],
// Stack défensif intensif
heavyDefense: [
{ type: 'general', name: 'General Heavy', intensity: 1.0, method: 'regeneration' },
{ type: 'anti-gptZero', name: 'GPTZero Heavy', intensity: 1.2, method: 'regeneration' },
{ type: 'anti-originality', name: 'Originality Heavy', intensity: 1.1, method: 'hybrid' },
{ type: 'anti-winston', name: 'Winston Heavy', intensity: 1.0, method: 'enhancement' }
],
// Stack ciblé GPTZero
gptZeroFocused: [
{ type: 'anti-gptZero', name: 'GPTZero Primary', intensity: 1.3, method: 'regeneration' },
{ type: 'general', name: 'General Support', intensity: 0.7, method: 'enhancement' }
],
// Stack ciblé Originality
originalityFocused: [
{ type: 'anti-originality', name: 'Originality Primary', intensity: 1.4, method: 'hybrid' },
{ type: 'general', name: 'General Support', intensity: 0.8, method: 'enhancement' }
]
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, options = {}) {
const stack = PREDEFINED_LAYERS[stackName];
if (!stack) {
throw new Error(`Stack prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_LAYERS).join(', ')}`);
}
logSh(`📦 APPLICATION STACK PRÉDÉFINI: ${stackName}`, 'INFO');
return await applyLayerPipeline(content, stack, options);
}
/**
* COUCHES ADAPTATIVES - S'adaptent selon le contenu
*/
async function applyAdaptiveLayers(content, options = {}) {
const {
targetDetectors = ['gptZero', 'originality'],
maxIntensity = 1.0,
analysisMode = true
} = options;
logSh(`🧠 COUCHES ADAPTATIVES: Analyse + adaptation auto`, 'INFO');
// 1. Analyser le contenu pour détecter les risques
const contentAnalysis = analyzeContentRisks(content);
// 2. Construire pipeline adaptatif selon l'analyse
const adaptiveLayers = [];
// Niveau de base selon risque global
const baseIntensity = Math.min(maxIntensity, contentAnalysis.globalRisk * 1.2);
if (baseIntensity > 0.3) {
adaptiveLayers.push({
type: 'general',
name: 'Adaptive Base',
intensity: baseIntensity,
method: baseIntensity > 0.7 ? 'hybrid' : 'enhancement'
});
}
// Couches spécifiques selon détecteurs ciblés
targetDetectors.forEach(detector => {
const detectorRisk = contentAnalysis.detectorRisks[detector] || 0;
if (detectorRisk > 0.4) {
const intensity = Math.min(maxIntensity * 1.1, detectorRisk * 1.5);
adaptiveLayers.push({
type: `anti-${detector}`,
name: `Adaptive ${detector}`,
intensity,
method: intensity > 0.8 ? 'regeneration' : 'enhancement'
});
}
});
logSh(` 🎯 ${adaptiveLayers.length} couches adaptatives générées`, 'DEBUG');
if (adaptiveLayers.length === 0) {
logSh(` ✅ Contenu déjà optimal, aucune couche nécessaire`, 'INFO');
return { content, stats: { adaptive: true, layersApplied: 0 }, original: content };
}
return await applyLayerPipeline(content, adaptiveLayers, options);
}
// ============= HELPER FUNCTIONS =============
/**
* Appliquer couche selon configuration
*/
async function applyLayerByConfig(content, layerConfig, globalOptions = {}) {
const { type, intensity, method, ...layerOptions } = layerConfig;
const options = { ...globalOptions, ...layerOptions, intensity, method };
switch (type) {
case 'general':
return await applyGeneralAdversarialLayer(content, options);
case 'anti-gptZero':
return await applyAntiGPTZeroLayer(content, options);
case 'anti-originality':
return await applyAntiOriginalityLayer(content, options);
case 'anti-winston':
return await applyAntiWinstonLayer(content, options);
case 'light':
return await applyLightAdversarialLayer(content, options);
case 'intensive':
return await applyIntensiveAdversarialLayer(content, options);
default:
throw new Error(`Type de couche inconnu: ${type}`);
}
}
/**
* Analyser risques du contenu pour adaptation
*/
function analyzeContentRisks(content) {
const analysis = {
globalRisk: 0,
detectorRisks: {},
riskFactors: []
};
const allContent = Object.values(content).join(' ');
// Risques génériques
let riskScore = 0;
// 1. Mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'furthermore', 'moreover'];
const aiWordCount = aiWords.filter(word => allContent.toLowerCase().includes(word)).length;
if (aiWordCount > 2) {
riskScore += 0.3;
analysis.riskFactors.push(`mots_ia: ${aiWordCount}`);
}
// 2. Structure uniforme
const contentLengths = Object.values(content).map(c => c.length);
const avgLength = contentLengths.reduce((a, b) => a + b, 0) / contentLengths.length;
const variance = contentLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / contentLengths.length;
const uniformity = 1 - (Math.sqrt(variance) / Math.max(avgLength, 1));
if (uniformity > 0.8) {
riskScore += 0.2;
analysis.riskFactors.push(`uniformité: ${uniformity.toFixed(2)}`);
}
// 3. Connecteurs répétitifs
const repetitiveConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
const connectorCount = repetitiveConnectors.filter(conn =>
(allContent.match(new RegExp(conn, 'gi')) || []).length > 1
).length;
if (connectorCount > 2) {
riskScore += 0.2;
analysis.riskFactors.push(`connecteurs_répétitifs: ${connectorCount}`);
}
analysis.globalRisk = Math.min(1, riskScore);
// Risques spécifiques par détecteur
analysis.detectorRisks = {
gptZero: analysis.globalRisk + (uniformity > 0.7 ? 0.3 : 0),
originality: analysis.globalRisk + (aiWordCount > 3 ? 0.4 : 0),
winston: analysis.globalRisk + (connectorCount > 2 ? 0.2 : 0)
};
return analysis;
}
/**
* Obtenir informations sur les stacks disponibles
*/
function getAvailableStacks() {
return Object.keys(PREDEFINED_LAYERS).map(stackName => ({
name: stackName,
layersCount: PREDEFINED_LAYERS[stackName].length,
description: getStackDescription(stackName),
layers: PREDEFINED_LAYERS[stackName]
}));
}
/**
* Description des stacks prédéfinis
*/
function getStackDescription(stackName) {
const descriptions = {
lightDefense: 'Protection légère préservant la qualité',
standardDefense: 'Protection équilibrée multi-détecteurs',
heavyDefense: 'Protection maximale tous détecteurs',
gptZeroFocused: 'Optimisation spécifique anti-GPTZero',
originalityFocused: 'Optimisation spécifique anti-Originality.ai'
};
return descriptions[stackName] || 'Stack personnalisé';
}
module.exports = {
// Couches individuelles
applyAntiGPTZeroLayer,
applyAntiOriginalityLayer,
applyAntiWinstonLayer,
applyGeneralAdversarialLayer,
applyLightAdversarialLayer,
applyIntensiveAdversarialLayer,
// Pipeline et stacks
applyLayerPipeline, // ← MAIN ENTRY POINT PIPELINE
applyPredefinedStack, // ← MAIN ENTRY POINT STACKS
applyAdaptiveLayers, // ← MAIN ENTRY POINT ADAPTATIF
// Utilitaires
getAvailableStacks,
analyzeContentRisks,
PREDEFINED_LAYERS
};

View File

@ -0,0 +1,408 @@
// ========================================
// ADVERSARIAL PROMPT ENGINE - NIVEAU 3
// Responsabilité: Générer instructions anti-détection pour prompts
// Anti-détection: Prévention par modification des prompts LLM
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* INSTRUCTIONS ADVERSARIALES PAR DÉTECTEUR
* Stratégies spécialisées pour contrer chaque détecteur IA
*/
const ADVERSARIAL_INSTRUCTIONS = {
general: {
title: 'CONTRAINTES ANTI-DÉTECTION GÉNÉRALES',
rules: [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations',
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'"excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte'
],
intensity: 1.0,
weight: 0.8
},
gptZero: {
title: 'SPÉCIAL ANTI-GPTZERO',
rules: [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases',
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés'
],
intensity: 1.2,
weight: 0.9,
targetMetric: 'unpredictability'
},
originality: {
title: 'SPÉCIAL ANTI-ORIGINALITY',
rules: [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues',
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence'
],
intensity: 1.3,
weight: 0.95,
targetMetric: 'semantic_diversity'
},
copyLeaks: {
title: 'SPÉCIAL ANTI-COPYLEAKS',
rules: [
'Reformule idées communes avec angles totalement originaux',
'Évite phrases-types et formulations standard du secteur',
'Personnalise chaque assertion avec exemples spécifiques',
'Réinvente la façon de présenter informations basiques',
'Utilise analogies et métaphores plutôt que descriptions directes',
'Fragmente informations techniques en observations pratiques',
'Transforme données factuelles en récits ou témoignages'
],
intensity: 1.1,
weight: 0.85,
targetMetric: 'originality_score'
},
winston: {
title: 'SPÉCIAL ANTI-WINSTON',
rules: [
'Évite cohérence stylistique trop parfaite entre paragraphes',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle',
'Intègre "erreurs" humaines : répétitions, corrections, précisions',
'Varie niveau de détail : parfois précis, parfois elliptique',
'Alterne registres émotionnels : enthousiaste → neutre → critique',
'Inclus hésitations et nuances : "peut-être", "généralement", "souvent"',
'Personnalise avec opinions subjectives et préférences'
],
intensity: 1.0,
weight: 0.9,
targetMetric: 'human_variation'
}
};
/**
* INSTRUCTIONS PERSONNALISÉES PAR TYPE D'ÉLÉMENT
*/
const ELEMENT_SPECIFIC_INSTRUCTIONS = {
titre_h1: {
base: 'Crée un titre percutant mais naturel',
adversarial: 'Évite formules marketing lisses, préfère authentique et direct'
},
titre_h2: {
base: 'Génère un sous-titre informatif',
adversarial: 'Varie structure : question, affirmation, exclamation selon contexte'
},
intro: {
base: 'Rédige introduction engageante',
adversarial: 'Commence par angle inattendu : anecdote, constat, question rhétorique'
},
texte: {
base: 'Développe paragraphe informatif',
adversarial: 'Mélange informations factuelles et observations personnelles'
},
faq_question: {
base: 'Formule question client naturelle',
adversarial: 'Utilise formulations vraiment utilisées par clients, pas académiques'
},
faq_reponse: {
base: 'Réponds de façon experte et rassurante',
adversarial: 'Ajoute nuances, "ça dépend", précisions contextuelles comme humain'
}
};
/**
* MAIN ENTRY POINT - GÉNÉRATEUR DE PROMPTS ADVERSARIAUX
* @param {string} basePrompt - Prompt de base
* @param {Object} config - Configuration adversariale
* @returns {string} - Prompt enrichi d'instructions anti-détection
*/
function createAdversarialPrompt(basePrompt, config = {}) {
return tracer.run('AdversarialPromptEngine.createAdversarialPrompt()', () => {
const {
detectorTarget = 'general',
intensity = 1.0,
elementType = 'generic',
personality = null,
contextualMode = true,
csvData = null,
debugMode = false
} = config;
tracer.annotate({
detectorTarget,
intensity,
elementType,
personalityStyle: personality?.style
});
try {
// 1. Sélectionner stratégie détecteur
const strategy = ADVERSARIAL_INSTRUCTIONS[detectorTarget] || ADVERSARIAL_INSTRUCTIONS.general;
// 2. Adapter intensité
const effectiveIntensity = intensity * (strategy.intensity || 1.0);
const shouldApplyStrategy = Math.random() < (strategy.weight || 0.8);
if (!shouldApplyStrategy && detectorTarget !== 'general') {
// Fallback sur stratégie générale
return createAdversarialPrompt(basePrompt, { ...config, detectorTarget: 'general' });
}
// 3. Construire instructions adversariales
const adversarialSection = buildAdversarialInstructions(strategy, {
elementType,
personality,
effectiveIntensity,
contextualMode,
csvData
});
// 4. Assembler prompt final
const enhancedPrompt = assembleEnhancedPrompt(basePrompt, adversarialSection, {
strategy,
elementType,
debugMode
});
if (debugMode) {
logSh(`🎯 Prompt adversarial généré: ${detectorTarget} (intensité: ${effectiveIntensity.toFixed(2)})`, 'DEBUG');
logSh(` Instructions: ${strategy.rules.length} règles appliquées`, 'DEBUG');
}
tracer.event('Prompt adversarial créé', {
detectorTarget,
rulesCount: strategy.rules.length,
promptLength: enhancedPrompt.length
});
return enhancedPrompt;
} catch (error) {
logSh(`❌ Erreur génération prompt adversarial: ${error.message}`, 'ERROR');
// Fallback: retourner prompt original
return basePrompt;
}
}, config);
}
/**
* Construire section instructions adversariales
*/
function buildAdversarialInstructions(strategy, config) {
const { elementType, personality, effectiveIntensity, contextualMode, csvData } = config;
let instructions = `\n\n=== ${strategy.title} ===\n`;
// Règles de base de la stratégie
const activeRules = selectActiveRules(strategy.rules, effectiveIntensity);
activeRules.forEach(rule => {
instructions += `${rule}\n`;
});
// Instructions spécifiques au type d'élément
if (ELEMENT_SPECIFIC_INSTRUCTIONS[elementType]) {
const elementInstructions = ELEMENT_SPECIFIC_INSTRUCTIONS[elementType];
instructions += `\nSPÉCIFIQUE ${elementType.toUpperCase()}:\n`;
instructions += `${elementInstructions.adversarial}\n`;
}
// Adaptations personnalité
if (personality && contextualMode) {
const personalityAdaptations = generatePersonalityAdaptations(personality, strategy);
if (personalityAdaptations) {
instructions += `\nADAPTATION PERSONNALITÉ ${personality.nom.toUpperCase()}:\n`;
instructions += personalityAdaptations;
}
}
// Contexte métier si disponible
if (csvData && contextualMode) {
const contextualInstructions = generateContextualInstructions(csvData, strategy);
if (contextualInstructions) {
instructions += `\nCONTEXTE MÉTIER:\n`;
instructions += contextualInstructions;
}
}
instructions += `\nIMPORTANT: Ces contraintes doivent sembler naturelles, pas forcées.\n`;
return instructions;
}
/**
* Sélectionner règles actives selon intensité
*/
function selectActiveRules(allRules, intensity) {
if (intensity >= 1.0) {
return allRules; // Toutes les règles
}
// Sélection proportionnelle à l'intensité
const ruleCount = Math.ceil(allRules.length * intensity);
return allRules.slice(0, ruleCount);
}
/**
* Générer adaptations personnalité
*/
function generatePersonalityAdaptations(personality, strategy) {
if (!personality) return null;
const adaptations = [];
// Style de la personnalité
if (personality.style) {
adaptations.push(`• Respecte le style ${personality.style} de ${personality.nom} tout en appliquant les contraintes`);
}
// Vocabulaire préféré
if (personality.vocabulairePref) {
adaptations.push(`• Intègre vocabulaire naturel: ${personality.vocabulairePref}`);
}
// Connecteurs préférés
if (personality.connecteursPref) {
adaptations.push(`• Utilise connecteurs variés: ${personality.connecteursPref}`);
}
// Longueur phrases selon personnalité
if (personality.longueurPhrases) {
adaptations.push(`• Longueur phrases: ${personality.longueurPhrases} mais avec variation anti-détection`);
}
return adaptations.length > 0 ? adaptations.join('\n') + '\n' : null;
}
/**
* Générer instructions contextuelles métier
*/
function generateContextualInstructions(csvData, strategy) {
if (!csvData.mc0) return null;
const instructions = [];
// Contexte sujet
instructions.push(`• Sujet: ${csvData.mc0} - utilise terminologie naturelle du domaine`);
// Éviter jargon selon détecteur
if (strategy.targetMetric === 'unpredictability') {
instructions.push(`• Évite jargon technique trop prévisible, privilégie explications accessibles`);
} else if (strategy.targetMetric === 'semantic_diversity') {
instructions.push(`• Varie façons de nommer/décrire ${csvData.mc0} - synonymes créatifs`);
}
return instructions.join('\n') + '\n';
}
/**
* Assembler prompt final
*/
function assembleEnhancedPrompt(basePrompt, adversarialSection, config) {
const { strategy, elementType, debugMode } = config;
// Structure du prompt amélioré
let enhancedPrompt = basePrompt;
// Injecter instructions adversariales
enhancedPrompt += adversarialSection;
// Rappel final selon stratégie
if (strategy.targetMetric) {
enhancedPrompt += `\nOBJECTIF PRIORITAIRE: Maximiser ${strategy.targetMetric} tout en conservant qualité.\n`;
}
// Instructions de réponse
enhancedPrompt += `\nRÉPONDS DIRECTEMENT par le contenu demandé, en appliquant naturellement ces contraintes.`;
return enhancedPrompt;
}
/**
* Analyser efficacité d'un prompt adversarial
*/
function analyzePromptEffectiveness(originalPrompt, adversarialPrompt, generatedContent) {
const analysis = {
promptEnhancement: {
originalLength: originalPrompt.length,
adversarialLength: adversarialPrompt.length,
enhancementRatio: adversarialPrompt.length / originalPrompt.length,
instructionsAdded: (adversarialPrompt.match(/•/g) || []).length
},
contentMetrics: analyzeGeneratedContent(generatedContent),
effectiveness: 0
};
// Score d'efficacité simple
analysis.effectiveness = Math.min(100,
(analysis.promptEnhancement.enhancementRatio - 1) * 50 +
analysis.contentMetrics.diversityScore
);
return analysis;
}
/**
* Analyser contenu généré
*/
function analyzeGeneratedContent(content) {
if (!content || typeof content !== 'string') {
return { diversityScore: 0, wordCount: 0, sentenceVariation: 0 };
}
const words = content.split(/\s+/).filter(w => w.length > 2);
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
// Diversité vocabulaire
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityScore = uniqueWords.length / Math.max(1, words.length) * 100;
// Variation longueurs phrases
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / Math.max(1, sentenceLengths.length);
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / Math.max(1, sentenceLengths.length);
const sentenceVariation = Math.sqrt(variance) / Math.max(1, avgLength) * 100;
return {
diversityScore: Math.round(diversityScore),
wordCount: words.length,
sentenceCount: sentences.length,
sentenceVariation: Math.round(sentenceVariation),
avgSentenceLength: Math.round(avgLength)
};
}
/**
* Obtenir liste des détecteurs supportés
*/
function getSupportedDetectors() {
return Object.keys(ADVERSARIAL_INSTRUCTIONS).map(key => ({
id: key,
name: ADVERSARIAL_INSTRUCTIONS[key].title,
intensity: ADVERSARIAL_INSTRUCTIONS[key].intensity,
weight: ADVERSARIAL_INSTRUCTIONS[key].weight,
rulesCount: ADVERSARIAL_INSTRUCTIONS[key].rules.length,
targetMetric: ADVERSARIAL_INSTRUCTIONS[key].targetMetric || 'general'
}));
}
module.exports = {
createAdversarialPrompt, // ← MAIN ENTRY POINT
buildAdversarialInstructions,
analyzePromptEffectiveness,
analyzeGeneratedContent,
getSupportedDetectors,
ADVERSARIAL_INSTRUCTIONS,
ELEMENT_SPECIFIC_INSTRUCTIONS
};

View File

@ -0,0 +1,368 @@
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ ADVERSARIAL
// Responsabilité: Appliquer le style personnalité avec Mistral + anti-détection
// LLM: Mistral (température 0.8) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyleAdversarial(input) {
return await tracer.run('AdversarialStyleEnhancement.applyPersonalityStyleAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 4/4 ADVERSARIAL: Enhancement style ${csvData.personality?.nom} (Mistral + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks avec prompts adversariaux
const styledResults = await applyStyleInChunksAdversarial(styleElements, csvData, config, detectorManager);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
},
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks avec prompts adversariaux
*/
async function applyStyleInChunksAdversarial(styleElements, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Stylisation adversarial: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createStylePrompt(chunk, csvData);
// Générer prompt adversarial pour stylisation
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 1.1, // Intensité plus élevée pour style (plus visible)
elementType: 'style_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const styledResponse = await callLLM('mistral', adversarialPrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisé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
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyleAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunksAdversarial,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};

View File

@ -0,0 +1,316 @@
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// Responsabilité: Améliorer la précision technique avec GPT-4 + anti-détection
// LLM: GPT-4o-mini (température 0.4) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTechnicalTermsAdversarial(input) {
return await tracer.run('AdversarialTechnicalEnhancement.enhanceTechnicalTermsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '2/4',
llmProvider: 'gpt4',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 2/4 ADVERSARIAL: Enhancement technique (GPT-4 + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser tous les éléments pour détecter termes techniques (adversarial)
const technicalAnalysis = await analyzeTechnicalTermsAdversarial(content, csvData, config, detectorManager);
// 2. Filter les éléments qui ont besoin d'enhancement
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] }
};
}
// 3. Améliorer les éléments sélectionnés avec prompts adversariaux
const enhancedResults = await enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, config, detectorManager);
// 4. Merger avec contenu original
const finalContent = { ...content };
let actuallyEnhanced = 0;
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== content[tag]) {
finalContent[tag] = enhancedResults[tag];
actuallyEnhanced++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyEnhanced,
candidate: elementsNeedingEnhancement.length,
duration
};
logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement technique terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gpt4',
step: 2,
enhancementsApplied: Object.keys(enhancedResults),
technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TechnicalEnhancement failed: ${error.message}`);
}
}, input);
}
/**
* Analyser tous les éléments pour détecter termes techniques (adversarial)
*/
async function analyzeTechnicalTermsAdversarial(content, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Analyse termes techniques adversarial batch`, 'DEBUG');
const contentEntries = Object.keys(content);
const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${content[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne
FORMAT RÉPONSE:
[1] dibond, impression UV OU AUCUN
[2] AUCUN
[3] aluminium, fraisage CNC OU AUCUN
etc...`;
try {
// Générer prompt adversarial pour analyse
const adversarialAnalysisPrompt = createAdversarialPrompt(analysisPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.8, // Intensité modérée pour analyse
elementType: 'technical_analysis',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const analysisResponse = await callLLM('gpt4', adversarialAnalysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAnalysisResponse(analysisResponse, content, contentEntries);
} catch (error) {
logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Améliorer les éléments sélectionnés avec prompts adversariaux
*/
async function enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Enhancement adversarial ${elementsNeedingEnhancement.length} éléments`, 'DEBUG');
const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
CONTENUS À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES:
- GARDE même longueur, structure et ton ${csvData.personality?.style}
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message
- Vocabulaire expert mais accessible
- Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique
[2] Contenu avec amélioration technique
etc...`;
try {
// Générer prompt adversarial pour enhancement
const adversarialEnhancementPrompt = createAdversarialPrompt(enhancementPrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity,
elementType: 'technical_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const enhancedResponse = await callLLM('gpt4', adversarialEnhancementPrompt, {
temperature: 0.4,
maxTokens: 5000
}, csvData.personality);
return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Parser réponse analyse
*/
function parseAnalysisResponse(response, content, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag,
content: content[tag],
technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG');
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let enhancedContent = match[2].trim();
const element = elementsNeedingEnhancement[index];
// Nettoyer le contenu généré
enhancedContent = cleanEnhancedContent(enhancedContent);
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < elementsNeedingEnhancement.length) {
const element = elementsNeedingEnhancement[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu amélioré
*/
function cleanEnhancedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
module.exports = {
enhanceTechnicalTermsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTechnicalTermsAdversarial,
enhanceSelectedElementsAdversarial,
parseAnalysisResponse,
parseEnhancementResponse
};

View File

@ -0,0 +1,429 @@
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// Responsabilité: Améliorer la fluidité avec Gemini + anti-détection
// LLM: Gemini (température 0.6) + Prompts adversariaux
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { createAdversarialPrompt } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS ADVERSARIAL
* Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTransitionsAdversarial(input) {
return await tracer.run('AdversarialTransitionEnhancement.enhanceTransitionsAdversarial()', async () => {
const { content, csvData, context = {}, adversarialConfig = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true,
contextualMode: adversarialConfig.contextualMode !== false,
...adversarialConfig
};
// Initialiser manager détecteur
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
await tracer.annotate({
step: '3/4',
llmProvider: 'gemini',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 ÉTAPE 3/4 ADVERSARIAL: Enhancement transitions (Gemini + ${config.detectorTarget})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser quels éléments ont besoin d'amélioration transitions
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
if (elementsNeedingTransitions.length === 0) {
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
};
}
// 2. Améliorer en chunks avec prompts adversariaux pour Gemini
const improvedResults = await improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, config, detectorManager);
// 3. Merger avec contenu original
const finalContent = { ...content };
let actuallyImproved = 0;
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
finalContent[tag] = improvedResults[tag];
actuallyImproved++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyImproved,
candidate: elementsNeedingTransitions.length,
duration
};
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement transitions terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gemini',
step: 3,
enhancementsApplied: Object.keys(improvedResults),
transitionIssues: elementsNeedingTransitions.map(e => e.issues),
adversarialConfig: config,
detectorTarget: config.detectorTarget,
intensity: config.intensity
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Gemini indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeeds(content) {
const elementsNeedingTransitions = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
if (text.length > 150) {
const needsTransitions = evaluateTransitionQuality(text);
if (needsTransitions.needsImprovement) {
elementsNeedingTransitions.push({
tag,
content: text,
issues: needsTransitions.issues,
score: needsTransitions.score
});
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
}
} else {
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
}
});
// Trier par score (plus problématique en premier)
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
return elementsNeedingTransitions;
}
/**
* Évaluer qualité transitions d'un texte
*/
function evaluateTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 1.0, issues: [] };
}
const issues = [];
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
// Analyse 1: Connecteurs répétitifs
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
if (repetitiveConnectors > 0.3) {
issues.push('connecteurs_répétitifs');
score -= 0.3;
}
// Analyse 2: Transitions abruptes
const abruptTransitions = analyzeAbruptTransitions(sentences);
if (abruptTransitions > 0.4) {
issues.push('transitions_abruptes');
score -= 0.4;
}
// Analyse 3: Manque de variété dans longueurs
const sentenceVariety = analyzeSentenceVariety(sentences);
if (sentenceVariety < 0.3) {
issues.push('phrases_uniformes');
score -= 0.2;
}
// Analyse 4: Trop formel ou trop familier
const formalityIssues = analyzeFormalityBalance(text);
if (formalityIssues > 0.5) {
issues.push('formalité_déséquilibrée');
score -= 0.1;
}
return {
needsImprovement: score < 0.6,
score: Math.max(0, score),
issues
};
}
/**
* Améliorer transitions en chunks avec prompts adversariaux
*/
async function improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, adversarialConfig, detectorManager) {
logSh(`🎯 Amélioration transitions adversarial: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const basePrompt = createTransitionImprovementPrompt(chunk, csvData);
// Générer prompt adversarial pour amélioration transitions
const adversarialPrompt = createAdversarialPrompt(basePrompt, {
detectorTarget: adversarialConfig.detectorTarget,
intensity: adversarialConfig.intensity * 0.9, // Intensité légèrement réduite pour transitions
elementType: 'transition_enhancement',
personality: csvData.personality,
contextualMode: adversarialConfig.contextualMode,
csvData: csvData,
debugMode: false
});
const improvedResponse = await callLLM('gemini', adversarialPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} amélioré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(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt amélioration transitions
*/
function createTransitionImprovementPrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData.mc0}
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
- Style ${personality?.style} mais professionnel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE même structure et longueur
- Améliore SEULEMENT la fluidité
- RESPECTE le style ${personality?.nom}
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...`;
return prompt;
}
/**
* Parser réponse amélioration transitions
*/
function parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let improvedContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu amélioré
improvedContent = cleanImprovedContent(improvedContent);
if (improvedContent && improvedContent.length > 10) {
results[element.tag] = improvedContent;
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
// ============= HELPER FUNCTIONS =============
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const hasConnector = hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityBalance(content) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
const balance = Math.abs(formalCount - casualCount) / total;
return balance;
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
function cleanImprovedContent(content) {
if (!content) return content;
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
enhanceTransitionsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL
analyzeTransitionNeeds,
evaluateTransitionQuality,
improveTransitionsInChunksAdversarial,
createTransitionImprovementPrompt,
parseTransitionResponse
};

View File

@ -0,0 +1,391 @@
// ========================================
// ADVERSARIAL UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules adversariaux
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU
*/
/**
* Analyser score de diversité lexicale
*/
function analyzeLexicalDiversity(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.toLowerCase()
.split(/\s+/)
.filter(word => word.length > 2)
.map(word => word.replace(/[^\w]/g, ''));
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words)];
return (uniqueWords.length / words.length) * 100;
}
/**
* Analyser variation des longueurs de phrases
*/
function analyzeSentenceVariation(content) {
if (!content || typeof content !== 'string') return 0;
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) return 0;
const lengths = sentences.map(s => s.split(/\s+/).length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(100, (stdDev / avgLength) * 100);
}
/**
* Détecter mots typiques IA
*/
function detectAIFingerprints(content) {
const aiFingerprints = {
words: ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'state-of-the-art', 'furthermore', 'moreover'],
phrases: ['it is important to note', 'it should be noted', 'it is worth mentioning', 'in conclusion', 'to summarize'],
connectors: ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc']
};
const results = {
words: 0,
phrases: 0,
connectors: 0,
totalScore: 0
};
const lowerContent = content.toLowerCase();
// Compter mots IA
aiFingerprints.words.forEach(word => {
const matches = (lowerContent.match(new RegExp(`\\b${word}\\b`, 'g')) || []);
results.words += matches.length;
});
// Compter phrases typiques
aiFingerprints.phrases.forEach(phrase => {
if (lowerContent.includes(phrase)) {
results.phrases += 1;
}
});
// Compter connecteurs répétitifs
aiFingerprints.connectors.forEach(connector => {
const matches = (lowerContent.match(new RegExp(`\\b${connector}\\b`, 'g')) || []);
if (matches.length > 1) {
results.connectors += matches.length - 1; // Pénalité répétition
}
});
// Score total (sur 100)
const wordCount = content.split(/\s+/).length;
results.totalScore = Math.min(100,
(results.words * 5 + results.phrases * 10 + results.connectors * 3) / Math.max(wordCount, 1) * 100
);
return results;
}
/**
* Analyser uniformité structurelle
*/
function analyzeStructuralUniformity(content) {
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 3) return 0;
const structures = sentences.map(sentence => {
const words = sentence.split(/\s+/);
return {
length: words.length,
startsWithConnector: /^(par ailleurs|en effet|de plus|cependant|ainsi|donc|ensuite|puis)/i.test(sentence),
hasComma: sentence.includes(','),
hasSubordinate: /qui|que|dont|où|quand|comme|parce que|puisque|bien que/i.test(sentence)
};
});
// Calculer uniformité
const avgLength = structures.reduce((sum, s) => sum + s.length, 0) / structures.length;
const lengthVariance = structures.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / structures.length;
const connectorRatio = structures.filter(s => s.startsWithConnector).length / structures.length;
const commaRatio = structures.filter(s => s.hasComma).length / structures.length;
// Plus c'est uniforme, plus le score est élevé (mauvais pour anti-détection)
const uniformityScore = 100 - (Math.sqrt(lengthVariance) / avgLength * 100) -
(Math.abs(0.3 - connectorRatio) * 50) - (Math.abs(0.5 - commaRatio) * 30);
return Math.max(0, Math.min(100, uniformityScore));
}
/**
* COMPARATEURS DE CONTENU
*/
/**
* Comparer deux contenus et calculer taux de modification
*/
function compareContentModification(original, modified) {
if (!original || !modified) return 0;
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const modifiedWords = modified.toLowerCase().split(/\s+/).filter(w => w.length > 2);
// Calcul de distance Levenshtein approximative (par mots)
let changes = 0;
const maxLength = Math.max(originalWords.length, modifiedWords.length);
for (let i = 0; i < maxLength; i++) {
if (originalWords[i] !== modifiedWords[i]) {
changes++;
}
}
return (changes / maxLength) * 100;
}
/**
* Évaluer amélioration adversariale
*/
function evaluateAdversarialImprovement(original, modified, detectorTarget = 'general') {
const originalFingerprints = detectAIFingerprints(original);
const modifiedFingerprints = detectAIFingerprints(modified);
const originalDiversity = analyzeLexicalDiversity(original);
const modifiedDiversity = analyzeLexicalDiversity(modified);
const originalVariation = analyzeSentenceVariation(original);
const modifiedVariation = analyzeSentenceVariation(modified);
const fingerprintReduction = originalFingerprints.totalScore - modifiedFingerprints.totalScore;
const diversityIncrease = modifiedDiversity - originalDiversity;
const variationIncrease = modifiedVariation - originalVariation;
const improvementScore = (
fingerprintReduction * 0.4 +
diversityIncrease * 0.3 +
variationIncrease * 0.3
);
return {
fingerprintReduction,
diversityIncrease,
variationIncrease,
improvementScore: Math.round(improvementScore * 100) / 100,
modificationRate: compareContentModification(original, modified),
recommendation: getImprovementRecommendation(improvementScore, detectorTarget)
};
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu adversarial généré
*/
function cleanAdversarialContent(content) {
if (!content || typeof content !== 'string') return content;
let cleaned = content;
// Supprimer préfixes de génération
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré|modifié)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(pour\s+)?(ce\s+contenu[,\s]*)?/gi, '');
// Nettoyer formatage
cleaned = cleaned.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
// Nettoyer début/fin
cleaned = cleaned.trim();
cleaned = cleaned.replace(/^[,.\s]+/, '');
cleaned = cleaned.replace(/[,\s]+$/, '');
return cleaned;
}
/**
* Valider qualité du contenu adversarial
*/
function validateAdversarialContent(content, originalContent, minLength = 10, maxModificationRate = 90) {
const validation = {
isValid: true,
issues: [],
suggestions: []
};
// Vérifier longueur minimale
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
}
// Vérifier cohérence
if (originalContent) {
const modificationRate = compareContentModification(originalContent, content);
if (modificationRate > maxModificationRate) {
validation.issues.push('Modification trop importante');
validation.suggestions.push('Réduire l\'intensité adversariale pour préserver le sens');
}
if (modificationRate < 5) {
validation.issues.push('Modification insuffisante');
validation.suggestions.push('Augmenter l\'intensité adversariale');
}
}
// Vérifier empreintes IA résiduelles
const fingerprints = detectAIFingerprints(content);
if (fingerprints.totalScore > 15) {
validation.issues.push('Empreintes IA encore présentes');
validation.suggestions.push('Appliquer post-processing anti-fingerprints');
}
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec préservation des paires
*/
function chunkArraySmart(array, size, preservePairs = false) {
if (!preservePairs) {
return chunkArray(array, size);
}
const chunks = [];
for (let i = 0; i < array.length; i += size) {
let chunk = array.slice(i, i + size);
// Si on coupe au milieu d'une paire (nombre impair), ajuster
if (chunk.length % 2 !== 0 && i + size < array.length) {
chunk = array.slice(i, i + size - 1);
}
chunks.push(chunk);
}
return chunks;
}
/**
* Chunk array standard
*/
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 avec variation
*/
function sleep(ms, variation = 0.2) {
const actualMs = ms + (Math.random() - 0.5) * ms * variation;
return new Promise(resolve => setTimeout(resolve, Math.max(100, actualMs)));
}
/**
* RECOMMANDATIONS
*/
/**
* Obtenir recommandation d'amélioration
*/
function getImprovementRecommendation(score, detectorTarget) {
const recommendations = {
general: {
good: "Bon niveau d'amélioration générale",
medium: "Appliquer techniques de variation syntaxique",
poor: "Nécessite post-processing intensif"
},
gptZero: {
good: "Imprévisibilité suffisante contre GPTZero",
medium: "Ajouter plus de ruptures narratives",
poor: "Intensifier variation syntaxique et lexicale"
},
originality: {
good: "Créativité suffisante contre Originality",
medium: "Enrichir diversité sémantique",
poor: "Réinventer présentation des informations"
}
};
const category = score > 10 ? 'good' : score > 5 ? 'medium' : 'poor';
return recommendations[detectorTarget]?.[category] || recommendations.general[category];
}
/**
* MÉTRIQUES ET STATS
*/
/**
* Calculer score composite anti-détection
*/
function calculateAntiDetectionScore(content, detectorTarget = 'general') {
const diversity = analyzeLexicalDiversity(content);
const variation = analyzeSentenceVariation(content);
const fingerprints = detectAIFingerprints(content);
const uniformity = analyzeStructuralUniformity(content);
const baseScore = (diversity * 0.3 + variation * 0.3 + (100 - fingerprints.totalScore) * 0.2 + (100 - uniformity) * 0.2);
// Ajustements selon détecteur
let adjustedScore = baseScore;
switch (detectorTarget) {
case 'gptZero':
adjustedScore = baseScore * (variation / 100) * 1.2; // Favorise variation
break;
case 'originality':
adjustedScore = baseScore * (diversity / 100) * 1.2; // Favorise diversité
break;
}
return Math.min(100, Math.max(0, Math.round(adjustedScore)));
}
module.exports = {
// Analyseurs
analyzeLexicalDiversity,
analyzeSentenceVariation,
detectAIFingerprints,
analyzeStructuralUniformity,
// Comparateurs
compareContentModification,
evaluateAdversarialImprovement,
// Utilitaires contenu
cleanAdversarialContent,
validateAdversarialContent,
// Utilitaires techniques
chunkArray,
chunkArraySmart,
sleep,
// Métriques
calculateAntiDetectionScore,
getImprovementRecommendation
};

View File

@ -0,0 +1,466 @@
// ========================================
// FRAMEWORK DE COMPARAISON ADVERSARIAL
// Responsabilité: Comparer pipelines normales vs adversariales
// Utilisation: A/B testing et validation efficacité anti-détection
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Pipelines à comparer
const { generateWithContext } = require('../ContentGeneration'); // Pipeline normale
const { generateWithAdversarialContext, compareAdversarialStrategies } = require('./ContentGenerationAdversarial'); // Pipeline adversariale
/**
* MAIN ENTRY POINT - COMPARAISON A/B PIPELINE
* Compare pipeline normale vs adversariale sur même input
*/
async function compareNormalVsAdversarial(input, options = {}) {
return await tracer.run('ComparisonFramework.compareNormalVsAdversarial()', async () => {
const {
hierarchy,
csvData,
adversarialConfig = {},
runBothPipelines = true,
analyzeContent = true
} = input;
const {
detectorTarget = 'general',
intensity = 1.0,
iterations = 1
} = options;
await tracer.annotate({
comparisonType: 'normal_vs_adversarial',
detectorTarget,
intensity,
iterations,
elementsCount: Object.keys(hierarchy).length
});
const startTime = Date.now();
logSh(`🆚 COMPARAISON A/B: Pipeline normale vs adversariale`, 'INFO');
logSh(` 🎯 Détecteur cible: ${detectorTarget} | Intensité: ${intensity} | Itérations: ${iterations}`, 'INFO');
const results = {
normal: null,
adversarial: null,
comparison: null,
iterations: []
};
try {
for (let i = 0; i < iterations; i++) {
logSh(`🔄 Itération ${i + 1}/${iterations}`, 'INFO');
const iterationResults = {
iteration: i + 1,
normal: null,
adversarial: null,
metrics: {}
};
// ========================================
// PIPELINE NORMALE
// ========================================
if (runBothPipelines) {
logSh(` 📊 Génération pipeline normale...`, 'DEBUG');
const normalStartTime = Date.now();
try {
const normalResult = await generateWithContext(hierarchy, csvData, {
technical: true,
transitions: true,
style: true
});
iterationResults.normal = {
success: true,
content: normalResult,
duration: Date.now() - normalStartTime,
elementsCount: Object.keys(normalResult).length
};
logSh(` ✅ Pipeline normale: ${iterationResults.normal.elementsCount} éléments (${iterationResults.normal.duration}ms)`, 'DEBUG');
} catch (error) {
iterationResults.normal = {
success: false,
error: error.message,
duration: Date.now() - normalStartTime
};
logSh(` ❌ Pipeline normale échouée: ${error.message}`, 'ERROR');
}
}
// ========================================
// PIPELINE ADVERSARIALE
// ========================================
logSh(` 🎯 Génération pipeline adversariale...`, 'DEBUG');
const adversarialStartTime = Date.now();
try {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAllSteps: true,
...adversarialConfig
}
});
iterationResults.adversarial = {
success: true,
content: adversarialResult.content,
stats: adversarialResult.stats,
adversarialMetrics: adversarialResult.adversarialMetrics,
duration: Date.now() - adversarialStartTime,
elementsCount: Object.keys(adversarialResult.content).length
};
logSh(` ✅ Pipeline adversariale: ${iterationResults.adversarial.elementsCount} éléments (${iterationResults.adversarial.duration}ms)`, 'DEBUG');
logSh(` 📊 Score efficacité: ${adversarialResult.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
iterationResults.adversarial = {
success: false,
error: error.message,
duration: Date.now() - adversarialStartTime
};
logSh(` ❌ Pipeline adversariale échouée: ${error.message}`, 'ERROR');
}
// ========================================
// ANALYSE COMPARATIVE ITÉRATION
// ========================================
if (analyzeContent && iterationResults.normal?.success && iterationResults.adversarial?.success) {
iterationResults.metrics = analyzeContentComparison(
iterationResults.normal.content,
iterationResults.adversarial.content
);
logSh(` 📈 Diversité: Normal=${iterationResults.metrics.diversity.normal.toFixed(2)}% | Adversarial=${iterationResults.metrics.diversity.adversarial.toFixed(2)}%`, 'DEBUG');
}
results.iterations.push(iterationResults);
}
// ========================================
// CONSOLIDATION RÉSULTATS
// ========================================
const totalDuration = Date.now() - startTime;
// Prendre les meilleurs résultats ou derniers si une seule itération
const lastIteration = results.iterations[results.iterations.length - 1];
results.normal = lastIteration.normal;
results.adversarial = lastIteration.adversarial;
// Analyse comparative globale
results.comparison = generateGlobalComparison(results.iterations, options);
logSh(`🆚 COMPARAISON TERMINÉE: ${iterations} itérations (${totalDuration}ms)`, 'INFO');
if (results.comparison.winner) {
logSh(`🏆 Gagnant: ${results.comparison.winner} (score: ${results.comparison.bestScore.toFixed(2)})`, 'INFO');
}
await tracer.event('Comparaison A/B terminée', {
iterations,
winner: results.comparison.winner,
totalDuration
});
return results;
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COMPARAISON A/B ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`ComparisonFramework failed: ${error.message}`);
}
}, input);
}
/**
* COMPARAISON MULTI-DÉTECTEURS
*/
async function compareMultiDetectors(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🎯 COMPARAISON MULTI-DÉTECTEURS: ${detectorTargets.length} stratégies`, 'INFO');
const results = {};
const startTime = Date.now();
for (const detector of detectorTargets) {
logSh(` 🔍 Test détecteur: ${detector}`, 'DEBUG');
try {
const comparison = await compareNormalVsAdversarial({
hierarchy,
csvData,
adversarialConfig: { detectorTarget: detector }
}, {
detectorTarget: detector,
intensity: 1.0,
iterations: 1
});
results[detector] = {
success: true,
comparison,
effectivenessGain: comparison.adversarial?.adversarialMetrics?.effectivenessScore || 0
};
logSh(`${detector}: +${results[detector].effectivenessGain.toFixed(2)}% efficacité`, 'DEBUG');
} catch (error) {
results[detector] = {
success: false,
error: error.message,
effectivenessGain: 0
};
logSh(`${detector}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyse du meilleur détecteur
const bestDetector = Object.keys(results).reduce((best, current) => {
if (!results[best]?.success) return current;
if (!results[current]?.success) return best;
return results[current].effectivenessGain > results[best].effectivenessGain ? current : best;
});
const totalDuration = Date.now() - startTime;
logSh(`🎯 MULTI-DÉTECTEURS TERMINÉ: Meilleur=${bestDetector} (${totalDuration}ms)`, 'INFO');
return {
results,
bestDetector,
bestScore: results[bestDetector]?.effectivenessGain || 0,
totalDuration
};
}
/**
* BENCHMARK PERFORMANCE
*/
async function benchmarkPerformance(hierarchy, csvData, configurations = []) {
const defaultConfigs = [
{ name: 'Normal', type: 'normal' },
{ name: 'Simple Adversarial', type: 'adversarial', detectorTarget: 'general', intensity: 0.5 },
{ name: 'Intense Adversarial', type: 'adversarial', detectorTarget: 'gptZero', intensity: 1.0 },
{ name: 'Max Adversarial', type: 'adversarial', detectorTarget: 'originality', intensity: 1.5 }
];
const configs = configurations.length > 0 ? configurations : defaultConfigs;
logSh(`⚡ BENCHMARK PERFORMANCE: ${configs.length} configurations`, 'INFO');
const results = [];
for (const config of configs) {
logSh(` 🔧 Test: ${config.name}`, 'DEBUG');
const startTime = Date.now();
try {
let result;
if (config.type === 'normal') {
result = await generateWithContext(hierarchy, csvData);
} else {
const adversarialResult = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: config.detectorTarget || 'general',
intensity: config.intensity || 1.0
}
});
result = adversarialResult.content;
}
const duration = Date.now() - startTime;
results.push({
name: config.name,
type: config.type,
success: true,
duration,
elementsCount: Object.keys(result).length,
performance: Object.keys(result).length / (duration / 1000) // éléments par seconde
});
logSh(`${config.name}: ${Object.keys(result).length} éléments (${duration}ms)`, 'DEBUG');
} catch (error) {
results.push({
name: config.name,
type: config.type,
success: false,
error: error.message,
duration: Date.now() - startTime
});
logSh(`${config.name}: Échec - ${error.message}`, 'ERROR');
}
}
// Analyser les résultats
const successfulResults = results.filter(r => r.success);
const fastest = successfulResults.reduce((best, current) =>
current.duration < best.duration ? current : best, successfulResults[0]);
const mostEfficient = successfulResults.reduce((best, current) =>
current.performance > best.performance ? current : best, successfulResults[0]);
logSh(`⚡ BENCHMARK TERMINÉ: Fastest=${fastest?.name} | Most efficient=${mostEfficient?.name}`, 'INFO');
return {
results,
fastest,
mostEfficient,
summary: {
totalConfigs: configs.length,
successful: successfulResults.length,
failed: results.length - successfulResults.length
}
};
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser différences de contenu entre normal et adversarial
*/
function analyzeContentComparison(normalContent, adversarialContent) {
const metrics = {
diversity: {
normal: analyzeDiversityScore(Object.values(normalContent).join(' ')),
adversarial: analyzeDiversityScore(Object.values(adversarialContent).join(' '))
},
length: {
normal: Object.values(normalContent).join(' ').length,
adversarial: Object.values(adversarialContent).join(' ').length
},
elementsCount: {
normal: Object.keys(normalContent).length,
adversarial: Object.keys(adversarialContent).length
},
differences: compareContentElements(normalContent, adversarialContent)
};
return metrics;
}
/**
* Score de diversité lexicale
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
return (uniqueWords.length / words.length) * 100;
}
/**
* Comparer éléments de contenu
*/
function compareContentElements(normalContent, adversarialContent) {
const differences = {
modified: 0,
identical: 0,
totalElements: Math.max(Object.keys(normalContent).length, Object.keys(adversarialContent).length)
};
const allTags = [...new Set([...Object.keys(normalContent), ...Object.keys(adversarialContent)])];
allTags.forEach(tag => {
if (normalContent[tag] && adversarialContent[tag]) {
if (normalContent[tag] === adversarialContent[tag]) {
differences.identical++;
} else {
differences.modified++;
}
}
});
differences.modificationRate = differences.totalElements > 0 ?
(differences.modified / differences.totalElements) * 100 : 0;
return differences;
}
/**
* Générer analyse comparative globale
*/
function generateGlobalComparison(iterations, options) {
const successfulIterations = iterations.filter(it =>
it.normal?.success && it.adversarial?.success);
if (successfulIterations.length === 0) {
return {
winner: null,
bestScore: 0,
summary: 'Aucune itération réussie'
};
}
// Moyenner les métriques
const avgMetrics = {
diversity: {
normal: 0,
adversarial: 0
},
performance: {
normal: 0,
adversarial: 0
}
};
successfulIterations.forEach(iteration => {
if (iteration.metrics) {
avgMetrics.diversity.normal += iteration.metrics.diversity.normal;
avgMetrics.diversity.adversarial += iteration.metrics.diversity.adversarial;
}
avgMetrics.performance.normal += iteration.normal.elementsCount / (iteration.normal.duration / 1000);
avgMetrics.performance.adversarial += iteration.adversarial.elementsCount / (iteration.adversarial.duration / 1000);
});
const iterCount = successfulIterations.length;
avgMetrics.diversity.normal /= iterCount;
avgMetrics.diversity.adversarial /= iterCount;
avgMetrics.performance.normal /= iterCount;
avgMetrics.performance.adversarial /= iterCount;
// Déterminer le gagnant
const diversityGain = avgMetrics.diversity.adversarial - avgMetrics.diversity.normal;
const performanceLoss = avgMetrics.performance.normal - avgMetrics.performance.adversarial;
// Score composite (favorise diversité avec pénalité performance)
const adversarialScore = diversityGain * 2 - (performanceLoss * 0.5);
return {
winner: adversarialScore > 5 ? 'adversarial' : 'normal',
bestScore: Math.max(avgMetrics.diversity.normal, avgMetrics.diversity.adversarial),
diversityGain,
performanceLoss,
avgMetrics,
summary: `Diversité: +${diversityGain.toFixed(2)}%, Performance: ${performanceLoss > 0 ? '-' : '+'}${Math.abs(performanceLoss).toFixed(2)} elem/s`
};
}
module.exports = {
compareNormalVsAdversarial, // ← MAIN ENTRY POINT
compareMultiDetectors,
benchmarkPerformance,
analyzeContentComparison,
analyzeDiversityScore
};

View File

@ -0,0 +1,408 @@
// ========================================
// ORCHESTRATEUR CONTENU ADVERSARIAL - NIVEAU 3
// Responsabilité: Pipeline complet de génération anti-détection
// Architecture: 4 étapes adversariales séparées et modulaires
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Importation des 4 étapes adversariales
const { generateInitialContentAdversarial } = require('./AdversarialInitialGeneration');
const { enhanceTechnicalTermsAdversarial } = require('./AdversarialTechnicalEnhancement');
const { enhanceTransitionsAdversarial } = require('./AdversarialTransitionEnhancement');
const { applyPersonalityStyleAdversarial } = require('./AdversarialStyleEnhancement');
// Importation du moteur adversarial
const { createAdversarialPrompt, getSupportedDetectors, analyzePromptEffectiveness } = require('./AdversarialPromptEngine');
const { DetectorStrategyManager } = require('./DetectorStrategies');
/**
* MAIN ENTRY POINT - PIPELINE ADVERSARIAL COMPLET
* Input: { hierarchy, csvData, adversarialConfig, context }
* Output: { content, stats, debug, adversarialMetrics }
*/
async function generateWithAdversarialContext(input) {
return await tracer.run('ContentGenerationAdversarial.generateWithAdversarialContext()', async () => {
const { hierarchy, csvData, adversarialConfig = {}, context = {} } = input;
// Configuration adversariale par défaut
const config = {
detectorTarget: adversarialConfig.detectorTarget || 'general',
intensity: adversarialConfig.intensity || 1.0,
enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy !== false,
contextualMode: adversarialConfig.contextualMode !== false,
enableAllSteps: adversarialConfig.enableAllSteps !== false,
// Configuration par étape
steps: {
initial: adversarialConfig.steps?.initial !== false,
technical: adversarialConfig.steps?.technical !== false,
transitions: adversarialConfig.steps?.transitions !== false,
style: adversarialConfig.steps?.style !== false
},
...adversarialConfig
};
await tracer.annotate({
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
enabledSteps: Object.keys(config.steps).filter(k => config.steps[k]),
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎯 PIPELINE ADVERSARIAL NIVEAU 3: Anti-détection ${config.detectorTarget}`, 'INFO');
logSh(` 🎚️ Intensité: ${config.intensity.toFixed(2)} | Étapes: ${Object.keys(config.steps).filter(k => config.steps[k]).join(', ')}`, 'INFO');
// Initialiser manager détecteur global
const detectorManager = new DetectorStrategyManager(config.detectorTarget);
try {
let currentContent = {};
let pipelineStats = {
steps: {},
totalDuration: 0,
elementsProcessed: 0,
adversarialMetrics: {
promptsGenerated: 0,
detectorTarget: config.detectorTarget,
averageIntensity: config.intensity,
effectivenessScore: 0
}
};
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE
// ========================================
if (config.steps.initial) {
logSh(`🎯 ÉTAPE 1/4: Génération initiale adversariale`, 'INFO');
const step1Result = await generateInitialContentAdversarial({
hierarchy,
csvData,
context,
adversarialConfig: config
});
currentContent = step1Result.content;
pipelineStats.steps.initial = step1Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += Object.keys(currentContent).length;
logSh(`✅ ÉTAPE 1/4: ${step1Result.stats.generated} éléments générés (${step1Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 1/4: Ignorée (configuration)`, 'INFO');
}
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL
// ========================================
if (config.steps.technical && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 2/4: Enhancement technique adversarial`, 'INFO');
const step2Result = await enhanceTechnicalTermsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step2Result.content;
pipelineStats.steps.technical = step2Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step2Result.stats.enhanced;
logSh(`✅ ÉTAPE 2/4: ${step2Result.stats.enhanced} éléments améliorés (${step2Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 2/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL
// ========================================
if (config.steps.transitions && Object.keys(currentContent).length > 0) {
logSh(`🎯 ÉTAPE 3/4: Enhancement transitions adversarial`, 'INFO');
const step3Result = await enhanceTransitionsAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step3Result.content;
pipelineStats.steps.transitions = step3Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step3Result.stats.enhanced;
logSh(`✅ ÉTAPE 3/4: ${step3Result.stats.enhanced} éléments fluidifiés (${step3Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 3/4: Ignorée (configuration ou pas de contenu)`, 'INFO');
}
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE ADVERSARIAL
// ========================================
if (config.steps.style && Object.keys(currentContent).length > 0 && csvData.personality) {
logSh(`🎯 ÉTAPE 4/4: Enhancement style adversarial`, 'INFO');
const step4Result = await applyPersonalityStyleAdversarial({
content: currentContent,
csvData,
context,
adversarialConfig: config
});
currentContent = step4Result.content;
pipelineStats.steps.style = step4Result.stats;
pipelineStats.adversarialMetrics.promptsGenerated += step4Result.stats.enhanced;
logSh(`✅ ÉTAPE 4/4: ${step4Result.stats.enhanced} éléments stylisés (${step4Result.stats.duration}ms)`, 'INFO');
} else {
logSh(`⏭️ ÉTAPE 4/4: Ignorée (configuration, pas de contenu ou pas de personnalité)`, 'INFO');
}
// ========================================
// FINALISATION PIPELINE
// ========================================
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
pipelineStats.elementsProcessed = Object.keys(currentContent).length;
// Calculer score d'efficacité adversarial
pipelineStats.adversarialMetrics.effectivenessScore = calculateAdversarialEffectiveness(
pipelineStats,
config,
currentContent
);
logSh(`🎯 PIPELINE ADVERSARIAL TERMINÉ: ${pipelineStats.elementsProcessed} éléments (${totalDuration}ms)`, 'INFO');
logSh(` 📊 Score efficacité: ${pipelineStats.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'INFO');
await tracer.event(`Pipeline adversarial terminé`, {
...pipelineStats,
detectorTarget: config.detectorTarget,
intensity: config.intensity
});
return {
content: currentContent,
stats: pipelineStats,
debug: {
adversarialPipeline: true,
detectorTarget: config.detectorTarget,
intensity: config.intensity,
stepsExecuted: Object.keys(config.steps).filter(k => config.steps[k]),
detectorManager: detectorManager.getStrategyInfo()
},
adversarialMetrics: pipelineStats.adversarialMetrics
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE ADVERSARIAL ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`AdversarialContentGeneration failed: ${error.message}`);
}
}, input);
}
/**
* MODE SIMPLE ADVERSARIAL (équivalent à generateSimple mais adversarial)
*/
async function generateSimpleAdversarial(hierarchy, csvData, adversarialConfig = {}) {
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: 'general',
intensity: 0.8,
enableAllSteps: false,
steps: {
initial: true,
technical: false,
transitions: false,
style: true
},
...adversarialConfig
}
});
}
/**
* MODE AVANCÉ ADVERSARIAL (configuration personnalisée)
*/
async function generateAdvancedAdversarial(hierarchy, csvData, options = {}) {
const {
detectorTarget = 'general',
intensity = 1.0,
technical = true,
transitions = true,
style = true,
...otherConfig
} = options;
return await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget,
intensity,
enableAdaptiveStrategy: true,
contextualMode: true,
steps: {
initial: true,
technical,
transitions,
style
},
...otherConfig
}
});
}
/**
* DIAGNOSTIC PIPELINE ADVERSARIAL
*/
async function diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) {
logSh(`🔬 DIAGNOSTIC ADVERSARIAL: Testing ${detectorTargets.length} détecteurs`, 'INFO');
const results = {};
for (const target of detectorTargets) {
try {
logSh(` 🎯 Test détecteur: ${target}`, 'DEBUG');
const result = await generateWithAdversarialContext({
hierarchy,
csvData,
adversarialConfig: {
detectorTarget: target,
intensity: 1.0,
enableAllSteps: true
}
});
results[target] = {
success: true,
content: result.content,
stats: result.stats,
effectivenessScore: result.adversarialMetrics.effectivenessScore
};
logSh(`${target}: Score ${result.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG');
} catch (error) {
results[target] = {
success: false,
error: error.message,
effectivenessScore: 0
};
logSh(`${target}: Échec - ${error.message}`, 'ERROR');
}
}
return results;
}
// ============= HELPER FUNCTIONS =============
/**
* Calculer efficacité adversariale
*/
function calculateAdversarialEffectiveness(pipelineStats, config, content) {
let effectiveness = 0;
// Base score selon intensité
effectiveness += config.intensity * 30;
// Bonus selon nombre d'étapes
const stepsExecuted = Object.keys(config.steps).filter(k => config.steps[k]).length;
effectiveness += stepsExecuted * 10;
// Bonus selon prompts adversariaux générés
const promptRatio = pipelineStats.adversarialMetrics.promptsGenerated / Math.max(1, pipelineStats.elementsProcessed);
effectiveness += promptRatio * 20;
// Analyse contenu si disponible
if (Object.keys(content).length > 0) {
const contentSample = Object.values(content).join(' ').substring(0, 1000);
const diversityScore = analyzeDiversityScore(contentSample);
effectiveness += diversityScore * 0.3;
}
return Math.min(100, Math.max(0, effectiveness));
}
/**
* Analyser score de diversité
*/
function analyzeDiversityScore(content) {
if (!content || typeof content !== 'string') return 0;
const words = content.split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return 0;
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
const diversityRatio = uniqueWords.length / words.length;
return diversityRatio * 100;
}
/**
* Obtenir informations détecteurs supportés
*/
function getAdversarialDetectorInfo() {
return getSupportedDetectors();
}
/**
* Comparer efficacité de différents détecteurs
*/
async function compareAdversarialStrategies(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality', 'winston']) {
const results = await diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets);
const comparison = {
bestStrategy: null,
bestScore: 0,
strategies: [],
averageScore: 0
};
let totalScore = 0;
let successCount = 0;
detectorTargets.forEach(target => {
const result = results[target];
if (result.success) {
const strategyInfo = {
detector: target,
effectivenessScore: result.effectivenessScore,
duration: result.stats.totalDuration,
elementsProcessed: result.stats.elementsProcessed
};
comparison.strategies.push(strategyInfo);
totalScore += result.effectivenessScore;
successCount++;
if (result.effectivenessScore > comparison.bestScore) {
comparison.bestStrategy = target;
comparison.bestScore = result.effectivenessScore;
}
}
});
comparison.averageScore = successCount > 0 ? totalScore / successCount : 0;
return comparison;
}
module.exports = {
generateWithAdversarialContext, // ← MAIN ENTRY POINT
generateSimpleAdversarial,
generateAdvancedAdversarial,
diagnosticAdversarialPipeline,
compareAdversarialStrategies,
getAdversarialDetectorInfo,
calculateAdversarialEffectiveness
};

View File

@ -0,0 +1,574 @@
// ========================================
// DETECTOR STRATEGIES - NIVEAU 3
// Responsabilité: Stratégies spécialisées par détecteur IA
// Anti-détection: Techniques ciblées contre chaque analyseur
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* STRATÉGIES DÉTECTEUR PAR DÉTECTEUR
* Chaque classe implémente une approche spécialisée
*/
class BaseDetectorStrategy {
constructor(name) {
this.name = name;
this.effectiveness = 0.8;
this.targetMetrics = [];
}
/**
* Générer instructions spécifiques pour ce détecteur
*/
generateInstructions(elementType, personality, csvData) {
throw new Error('generateInstructions must be implemented by subclass');
}
/**
* Obtenir instructions anti-détection (NOUVEAU pour modularité)
*/
getInstructions(intensity = 1.0) {
throw new Error('getInstructions must be implemented by subclass');
}
/**
* Obtenir conseils d'amélioration (NOUVEAU pour modularité)
*/
getEnhancementTips(intensity = 1.0) {
throw new Error('getEnhancementTips must be implemented by subclass');
}
/**
* Analyser efficacité contre ce détecteur
*/
analyzeEffectiveness(content) {
return {
detector: this.name,
effectiveness: this.effectiveness,
metrics: this.analyzeContent(content)
};
}
/**
* Analyser contenu selon métriques de ce détecteur
*/
analyzeContent(content) {
return {
wordCount: content.split(/\s+/).length,
sentenceCount: content.split(/[.!?]+/).length
};
}
}
/**
* STRATÉGIE ANTI-GPTZERO
* Focus: Imprévisibilité et variation syntaxique
*/
class GPTZeroStrategy extends BaseDetectorStrategy {
constructor() {
super('GPTZero');
this.effectiveness = 0.9;
this.targetMetrics = ['perplexity', 'burstiness', 'unpredictability'];
this.techniques = {
syntaxVariation: {
name: 'Variation syntaxique drastique',
rules: [
'Alterne phrases simples (5-8 mots) et complexes (25-35 mots)',
'Utilise structures atypiques : inversion, anacoluthe, ellipse',
'Intercale incises, parenthèses, tirets pour briser linéarité',
'Varie types : déclarative → interrogative → exclamative'
]
},
lexicalUnpredictability: {
name: 'Imprévisibilité lexicale',
rules: [
'Synonymes rares ou détournés : "performant" → "qui tient la route"',
'Néologismes justifiés : "sur-mesurable" pour "très mesurable"',
'Registres mélangés : soutenu puis familier dans même paragraphe',
'Évite collocations attendues : "haute qualité" → "qualité qui dépote"'
]
},
narrativeDisruption: {
name: 'Disruption narrative',
rules: [
'Apartés personnels : "(j\'ai testé, c\'est bluffant)"',
'Questions rhétoriques inattendues au milieu des faits',
'Changements de perspective soudains : technique → humain → commercial',
'Digressions courtes puis retour au sujet'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-GPTZERO - MAXIMUM IMPRÉVISIBILITÉ:`);
// Techniques syntaxiques
instructions.push(`\nSYNTAXE VARIABLE:`);
this.techniques.syntaxVariation.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Imprévisibilité lexicale
instructions.push(`\nLEXIQUE IMPRÉVISIBLE:`);
this.techniques.lexicalUnpredictability.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Adaptation selon type élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
instructions.push(`\nTITRES ANTI-GPTZERO:`);
instructions.push(`• Évite formules attendues : "Guide complet" → "Le vrai topo sur"`);
instructions.push(`• Structure atypique : question, exclamation, fragment`);
} else if (elementType === 'texte') {
instructions.push(`\nTEXTES ANTI-GPTZERO:`);
this.techniques.narrativeDisruption.rules.forEach(rule => {
instructions.push(`${rule}`);
});
}
// Adaptation personnalité
if (personality) {
instructions.push(`\nAVEC STYLE ${personality.nom.toUpperCase()}:`);
instructions.push(`• Pousse ${personality.style} vers extrêmes : plus marqué, moins lisse`);
instructions.push(`• Accentue tics de langage : répétitions, expressions favorites`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Surprends avec tournures inattendues et constructions atypiques',
'Évite phrases trop prévisibles ou patterns répétitifs',
'Utilise synonymes moins évidents et vocabulaire moins commun',
'Varie drastiquement la complexité syntaxique entre phrases'
];
const intensiveRules = [
'Intercale observations personnelles ou détours narratifs',
'Alterne registres de langue (soutenu → familier → technique)',
'Brise la logique linéaire avec des parenthèses, incises, apartés',
'Créé ruptures narratives et changements de perspective soudains'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace connecteurs prévisibles par variations inattendues',
'Ajoute incises courtes pour briser la linéarité',
'Varie longueurs phrases dans même paragraphe',
'Utilise synonymes moins courants mais naturels',
...(intensity > 0.8 ? [
'Insère questions rhétoriques ponctuelles',
'Ajoute nuances et hésitations authentiques'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse perplexité approximative
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const sentenceLengths = sentences.map(s => s.split(/\s+/).length);
// Variance longueur (proxy pour burstiness)
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceLengths.length;
const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / sentenceLengths.length;
const burstiness = Math.sqrt(variance) / avgLength;
// Diversité lexicale (proxy pour imprévisibilité)
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const uniqueWords = [...new Set(words)];
const lexicalDiversity = uniqueWords.length / words.length;
return {
...baseMetrics,
burstiness: Math.round(burstiness * 100) / 100,
lexicalDiversity: Math.round(lexicalDiversity * 100) / 100,
avgSentenceLength: Math.round(avgLength),
gptZeroRiskLevel: this.calculateGPTZeroRisk(burstiness, lexicalDiversity)
};
}
calculateGPTZeroRisk(burstiness, lexicalDiversity) {
// Heuristique : GPTZero détecte uniformité faible + diversité faible
const uniformityScore = Math.min(burstiness, 1) * 100;
const diversityScore = lexicalDiversity * 100;
const combinedScore = (uniformityScore + diversityScore) / 2;
if (combinedScore > 70) return 'low';
if (combinedScore > 40) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE ANTI-ORIGINALITY
* Focus: Diversité sémantique et originalité
*/
class OriginalityStrategy extends BaseDetectorStrategy {
constructor() {
super('Originality');
this.effectiveness = 0.85;
this.targetMetrics = ['semantic_diversity', 'originality_score', 'vocabulary_range'];
this.techniques = {
semanticCreativity: {
name: 'Créativité sémantique',
rules: [
'Métaphores inattendues : "cette plaque, c\'est le passeport de votre façade"',
'Comparaisons originales : évite clichés, invente analogies',
'Reformulations créatives : "résistant aux intempéries" → "qui brave les saisons"',
'Néologismes justifiés et expressifs'
]
},
perspectiveShifting: {
name: 'Changements de perspective',
rules: [
'Angles multiples sur même info : technique → esthétique → pratique',
'Points de vue variés : fabricant, utilisateur, installateur, voisin',
'Temporalités mélangées : présent, futur proche, retour d\'expérience',
'Niveaux d\'abstraction : détail précis puis vue d\'ensemble'
]
},
linguisticInventiveness: {
name: 'Inventivité linguistique',
rules: [
'Jeux de mots subtils et expressions détournées',
'Régionalismes et références culturelles précises',
'Vocabulaire technique humanisé avec créativité',
'Rythmes et sonorités travaillés : allitérations, assonances'
]
}
};
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-ORIGINALITY - MAXIMUM CRÉATIVITÉ SÉMANTIQUE:`);
// Créativité sémantique
instructions.push(`\nCRÉATIVITÉ SÉMANTIQUE:`);
this.techniques.semanticCreativity.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Changements de perspective
instructions.push(`\nPERSPECTIVES MULTIPLES:`);
this.techniques.perspectiveShifting.rules.forEach(rule => {
instructions.push(`${rule}`);
});
// Spécialisation par élément
if (elementType === 'intro') {
instructions.push(`\nINTROS ANTI-ORIGINALITY:`);
instructions.push(`• Commence par angle totalement inattendu pour le sujet`);
instructions.push(`• Évite intro-types, réinvente présentation du sujet`);
instructions.push(`• Crée surprise puis retour naturel au cœur du sujet`);
} else if (elementType.includes('faq')) {
instructions.push(`\nFAQ ANTI-ORIGINALITY:`);
instructions.push(`• Questions vraiment originales, pas standard secteur`);
instructions.push(`• Réponses avec angles créatifs et exemples inédits`);
}
// Contexte métier créatif
if (csvData && csvData.mc0) {
instructions.push(`\nCRÉATIVITÉ CONTEXTUELLE ${csvData.mc0.toUpperCase()}:`);
instructions.push(`• Réinvente façon de parler de ${csvData.mc0}`);
instructions.push(`• Évite vocabulaire convenu du secteur, invente expressions`);
instructions.push(`• Trouve analogies originales spécifiques à ${csvData.mc0}`);
}
// Inventivité linguistique
instructions.push(`\nINVENTIVITÉ LINGUISTIQUE:`);
this.techniques.linguisticInventiveness.rules.forEach(rule => {
instructions.push(`${rule}`);
});
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Vocabulaire TRÈS varié : évite répétitions même de synonymes',
'Structures phrases délibérément irrégulières et asymétriques',
'Changements angles fréquents : technique → personnel → général',
'Créativité sémantique : métaphores, comparaisons inattendues'
];
const intensiveRules = [
'Évite formulations académiques ou trop structurées',
'Intègre références culturelles, expressions régionales',
'Subvertis les attentes : commence par la fin, questionne l\'évidence',
'Réinvente façon de présenter informations basiques'
];
return intensity >= 1.0 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Trouve synonymes créatifs et expressions détournées',
'Ajoute métaphores subtiles et comparaisons originales',
'Varie angles d\'approche dans même contenu',
'Utilise vocabulaire technique humanisé',
...(intensity > 0.8 ? [
'Insère références culturelles ou régionalismes',
'Crée néologismes justifiés et expressifs'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Analyse diversité sémantique
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 3);
const uniqueWords = [...new Set(words)];
const semanticDiversity = uniqueWords.length / words.length;
// Détection créativité (heuristique)
const creativityIndicators = [
'comme', 'tel', 'sorte de', 'façon de', 'manière de', // métaphores
'(', ')', '"', // originalité structure
'?', '!', // variation tonale
];
const creativityCount = creativityIndicators.reduce((count, indicator) => {
return count + (content.match(new RegExp(indicator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
}, 0);
const creativityScore = Math.min(100, (creativityCount / words.length) * 1000);
return {
...baseMetrics,
semanticDiversity: Math.round(semanticDiversity * 100) / 100,
creativityScore: Math.round(creativityScore),
uniqueWordRatio: Math.round((uniqueWords.length / words.length) * 100),
originalityRiskLevel: this.calculateOriginalityRisk(semanticDiversity, creativityScore)
};
}
calculateOriginalityRisk(semanticDiversity, creativityScore) {
const diversityScore = semanticDiversity * 100;
const combinedScore = (diversityScore + creativityScore) / 2;
if (combinedScore > 60) return 'low';
if (combinedScore > 35) return 'medium';
return 'high';
}
}
/**
* STRATÉGIE GÉNÉRALE
* Équilibre entre toutes les techniques
*/
class GeneralStrategy extends BaseDetectorStrategy {
constructor() {
super('General');
this.effectiveness = 0.75;
this.targetMetrics = ['overall_naturalness', 'human_likeness'];
}
generateInstructions(elementType, personality, csvData) {
const instructions = [];
instructions.push(`ANTI-DÉTECTION GÉNÉRALE - ÉQUILIBRE OPTIMAL:`);
// Instructions générales équilibrées
instructions.push(`\nNATUREL ET VARIÉ:`);
instructions.push(`• Évite mots typiques IA : "optimal", "comprehensive", "seamless", "robust"`);
instructions.push(`• Mélange phrases courtes (5-10 mots) et longues (15-25 mots) naturellement`);
instructions.push(`• Utilise expressions idiomatiques françaises authentiques`);
instructions.push(`• Intègre imperfections humaines : répétitions légères, hésitations`);
instructions.push(`• Change angle ou ton 1-2 fois selon longueur du contenu`);
// Adaptation personnalité équilibrée
if (personality) {
instructions.push(`\nSTYLE ${personality.nom} NATUREL:`);
instructions.push(`• Applique style ${personality.style} de façon authentique, pas forcée`);
instructions.push(`• Respecte vocabulaire naturel : ${personality.vocabulairePref || 'varié'}`);
instructions.push(`• Utilise connecteurs variés : ${personality.connecteursPref || 'naturels'}`);
}
return instructions.join('\n');
}
/**
* Instructions modulaires pour AdversarialCore
*/
getInstructions(intensity = 1.0) {
const baseRules = [
'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"',
'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)',
'Utilise expressions françaises familières et tournures idiomatiques',
'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations'
];
const intensiveRules = [
'Change angle ou perspective 1-2 fois dans le texte',
'Préfère synonymes moins évidents : "bon" plutôt qu\'excellent", "solide" plutôt qu\'optimisé"',
'Intègre connecteurs variés et naturels selon le contexte',
'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle'
];
return intensity >= 0.8 ? [...baseRules, ...intensiveRules] : baseRules;
}
/**
* Conseils d'amélioration pour enhancement method
*/
getEnhancementTips(intensity = 1.0) {
return [
'Remplace mots typiques IA par synonymes plus naturels',
'Ajoute nuances et hésitations : "peut-être", "généralement", "souvent"',
'Varie connecteurs pour éviter répétitions mécaniques',
'Personnalise avec observations subjectives légères',
...(intensity > 0.7 ? [
'Intègre "erreurs" humaines : corrections, précisions',
'Simule changement léger de ton ou d\'énergie'
] : [])
];
}
analyzeContent(content) {
const baseMetrics = super.analyzeContent(content);
// Métrique naturalité générale
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = baseMetrics.wordCount / baseMetrics.sentenceCount;
// Détection mots typiques IA
const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage'];
const aiWordCount = aiWords.reduce((count, word) => {
return count + (content.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g')) || []).length;
}, 0);
const aiWordDensity = aiWordCount / baseMetrics.wordCount * 100;
const naturalness = Math.max(0, 100 - (aiWordDensity * 10) - Math.abs(avgWordsPerSentence - 15));
return {
...baseMetrics,
avgWordsPerSentence: Math.round(avgWordsPerSentence),
aiWordCount,
aiWordDensity: Math.round(aiWordDensity * 100) / 100,
naturalnessScore: Math.round(naturalness),
generalRiskLevel: naturalness > 70 ? 'low' : naturalness > 40 ? 'medium' : 'high'
};
}
}
/**
* FACTORY POUR CRÉER STRATÉGIES
*/
class DetectorStrategyFactory {
static strategies = {
'general': GeneralStrategy,
'gptZero': GPTZeroStrategy,
'originality': OriginalityStrategy
};
static createStrategy(detectorName) {
const StrategyClass = this.strategies[detectorName];
if (!StrategyClass) {
logSh(`⚠️ Stratégie inconnue: ${detectorName}, fallback vers général`, 'WARNING');
return new GeneralStrategy();
}
return new StrategyClass();
}
static getSupportedDetectors() {
return Object.keys(this.strategies).map(name => {
const strategy = this.createStrategy(name);
return {
name,
displayName: strategy.name,
effectiveness: strategy.effectiveness,
targetMetrics: strategy.targetMetrics
};
});
}
static analyzeContentAgainstAllDetectors(content) {
const results = {};
Object.keys(this.strategies).forEach(detectorName => {
const strategy = this.createStrategy(detectorName);
results[detectorName] = strategy.analyzeEffectiveness(content);
});
return results;
}
}
/**
* FONCTION UTILITAIRE - SÉLECTION STRATÉGIE OPTIMALE
*/
function selectOptimalStrategy(elementType, personality, previousResults = {}) {
// Logique de sélection intelligente
// Si résultats précédents disponibles, adapter
if (previousResults.gptZero && previousResults.gptZero.effectiveness < 0.6) {
return 'gptZero'; // Renforcer anti-GPTZero
}
if (previousResults.originality && previousResults.originality.effectiveness < 0.6) {
return 'originality'; // Renforcer anti-Originality
}
// Sélection par type d'élément
if (elementType === 'titre_h1' || elementType === 'titre_h2') {
return 'gptZero'; // Titres bénéficient imprévisibilité
}
if (elementType === 'intro' || elementType === 'texte') {
return 'originality'; // Corps bénéficie créativité sémantique
}
if (elementType.includes('faq')) {
return 'general'; // FAQ équilibre naturalité
}
// Par personnalité
if (personality) {
if (personality.style === 'créatif' || personality.style === 'original') {
return 'originality';
}
if (personality.style === 'technique' || personality.style === 'expert') {
return 'gptZero';
}
}
return 'general'; // Fallback
}
module.exports = {
DetectorStrategyFactory,
GPTZeroStrategy,
OriginalityStrategy,
GeneralStrategy,
selectOptimalStrategy,
BaseDetectorStrategy
};

View File

@ -0,0 +1,202 @@
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE
// Usage: node lib/adversarial-generation/demo-modulaire.js
// Objectif: Valider l'intégration modulaire adversariale
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules adversariaux modulaires
const { applyAdversarialLayer } = require('./AdversarialCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./AdversarialLayers');
const { calculateAntiDetectionScore, evaluateAdversarialImprovement } = require('./AdversarialUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE
*/
async function demoModularAdversarial() {
console.log('\n🎯 === DÉMONSTRATION ADVERSARIAL MODULAIRE ===\n');
// Contenu d'exemple (simulé contenu généré normal)
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu optimal pour votre entreprise. Cette solution comprehensive permet de créer une identité visuelle robuste et seamless.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont cutting-edge. Par ailleurs, la qualité est optimal. En effet, nos solutions sont comprehensive et robust.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont optimal : dibond, aluminium, PMMA. Ces solutions comprehensive garantissent une qualité robust et seamless.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content.substring(0, 60)}..."`);
});
// Analyser contenu original
const scoreOriginal = calculateAntiDetectionScore(Object.values(exempleContenu).join(' '));
console.log(`\n📈 Score anti-détection original: ${scoreOriginal}/100`);
try {
// ========================================
// TEST 1: COUCHE SIMPLE
// ========================================
console.log('\n🔧 TEST 1: Application couche adversariale simple');
const result1 = await applyAdversarialLayer(exempleContenu, {
detectorTarget: 'general',
intensity: 0.8,
method: 'enhancement'
});
console.log(`✅ Résultat: ${result1.stats.elementsModified}/${result1.stats.elementsProcessed} éléments modifiés`);
const scoreAmeliore = calculateAntiDetectionScore(Object.values(result1.content).join(' '));
console.log(`📈 Score anti-détection amélioré: ${scoreAmeliore}/100 (+${scoreAmeliore - scoreOriginal})`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description} (${stack.layersCount} couches)`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardDefense', {
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches appliquées: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length}`);
const scoreStack = calculateAntiDetectionScore(Object.values(result2.content).join(' '));
console.log(`📈 Score anti-détection stack: ${scoreStack}/100 (+${scoreStack - scoreOriginal})`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
targetDetectors: ['gptZero', 'originality'],
maxIntensity: 1.2
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied || result3.stats.totalModifications} modifications`);
const scoreAdaptatif = calculateAntiDetectionScore(Object.values(result3.content).join(' '));
console.log(`📈 Score anti-détection adaptatif: ${scoreAdaptatif}/100 (+${scoreAdaptatif - scoreOriginal})`);
}
// ========================================
// COMPARAISON FINALE
// ========================================
console.log('\n📊 COMPARAISON FINALE:');
const evaluation = evaluateAdversarialImprovement(
Object.values(exempleContenu).join(' '),
Object.values(result2.content).join(' '),
'general'
);
console.log(` 🔹 Réduction empreintes IA: ${evaluation.fingerprintReduction.toFixed(2)}%`);
console.log(` 🔹 Augmentation diversité: ${evaluation.diversityIncrease.toFixed(2)}%`);
console.log(` 🔹 Amélioration variation: ${evaluation.variationIncrease.toFixed(2)}%`);
console.log(` 🔹 Score amélioration global: ${evaluation.improvementScore}`);
console.log(` 🔹 Taux modification: ${evaluation.modificationRate.toFixed(2)}%`);
console.log(` 💡 Recommandation: ${evaluation.recommendation}`);
// ========================================
// EXEMPLES DE CONTENU TRANSFORMÉ
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
const exempleTransforme = result2.content['|Introduction_1|'] || result1.content['|Introduction_1|'];
console.log('\n📝 AVANT:');
console.log(` "${exempleContenu['|Introduction_1|']}"`);
console.log('\n📝 APRÈS:');
console.log(` "${exempleTransforme}"`);
console.log('\n✅ === DÉMONSTRATION MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalScore: scoreOriginal,
improvedScore: Math.max(scoreAmeliore, scoreStack),
improvement: evaluation.improvementScore
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE NORMALE
*/
async function demoIntegrationPipeline() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler résultat pipeline normale (Level 1)
const contenuNormal = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Intro_1|': 'Notre expertise en signalétique permet de créer des plaques sur mesure adaptées à vos besoins spécifiques.',
'|Texte_1|': 'Les matériaux proposés incluent l\'aluminium, le dibond et le PMMA. Chaque solution présente des avantages particuliers selon l\'usage prévu.'
};
console.log('💼 SCÉNARIO: Application adversarial post-pipeline normale');
try {
// Exemple Level 6 - Post-processing adversarial
console.log('\n🎯 Étape 1: Contenu généré par pipeline normale');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application couche adversariale modulaire');
const resultAdversarial = await applyAdversarialLayer(contenuNormal, {
detectorTarget: 'gptZero',
intensity: 0.9,
method: 'hybrid',
preserveStructure: true
});
console.log(` ✅ Couche adversariale: ${resultAdversarial.stats.elementsModified} éléments modifiés`);
console.log('\n📊 RÉSULTAT FINAL:');
Object.entries(resultAdversarial.content).forEach(([tag, content]) => {
console.log(` ${tag}:`);
console.log(` AVANT: "${contenuNormal[tag]}"`);
console.log(` APRÈS: "${content}"`);
console.log('');
});
return { success: true, result: resultAdversarial };
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularAdversarial();
await demoIntegrationPipeline();
})().catch(console.error);
}
module.exports = {
demoModularAdversarial,
demoIntegrationPipeline
};

View File

@ -0,0 +1,389 @@
// ========================================
// ÉTAPE 1: GÉNÉRATION INITIALE
// Responsabilité: Créer le contenu de base avec Claude uniquement
// LLM: Claude Sonnet (température 0.7)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - GÉNÉRATION INITIALE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function generateInitialContent(input) {
return await tracer.run('InitialGeneration.generateInitialContent()', async () => {
const { hierarchy, csvData, context = {} } = input;
await tracer.annotate({
step: '1/4',
llmProvider: 'claude',
elementsCount: Object.keys(hierarchy).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🚀 ÉTAPE 1/4: Génération initiale (Claude)`, 'INFO');
logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO');
try {
// Collecter tous les éléments dans l'ordre XML
const allElements = collectElementsInXMLOrder(hierarchy);
// Séparer FAQ pairs et autres éléments
const { faqPairs, otherElements } = separateElementTypes(allElements);
// Générer en chunks pour éviter timeouts
const results = {};
// 1. Générer éléments normaux (titres, textes, intro)
if (otherElements.length > 0) {
const normalResults = await generateNormalElements(otherElements, csvData);
Object.assign(results, normalResults);
}
// 2. Générer paires FAQ si présentes
if (faqPairs.length > 0) {
const faqResults = await generateFAQPairs(faqPairs, csvData);
Object.assign(results, faqResults);
}
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(results).length,
generated: Object.keys(results).length,
faqPairs: faqPairs.length,
duration
};
logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO');
await tracer.event(`Génération initiale terminée`, stats);
return {
content: results,
stats,
debug: {
llmProvider: 'claude',
step: 1,
elementsGenerated: Object.keys(results)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`InitialGeneration failed: ${error.message}`);
}
}, input);
}
/**
* Générer éléments normaux (titres, textes, intro) en chunks
*/
async function generateNormalElements(elements, csvData) {
logSh(`📝 Génération éléments normaux: ${elements.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const prompt = createBatchPrompt(chunk, csvData);
const response = await callLLM('claude', prompt, {
temperature: 0.7,
maxTokens: 2000 * chunk.length
}, csvData.personality);
const chunkResults = parseBatchResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments 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');
throw error;
}
}
return results;
}
/**
* Générer paires FAQ cohérentes
*/
async function generateFAQPairs(faqPairs, csvData) {
logSh(`❓ Génération paires FAQ: ${faqPairs.length} paires`, 'DEBUG');
const prompt = createFAQPairsPrompt(faqPairs, csvData);
const response = await callLLM('claude', prompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
return parseFAQResponse(response, faqPairs);
}
/**
* Créer prompt batch pour éléments normaux
*/
function createBatchPrompt(elements, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION CONTENU INITIAL ===
Entreprise: Autocollant.fr - signalétique personnalisée
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
ÉLÉMENTS À GÉNÉRER:
`;
elements.forEach((elementInfo, index) => {
const cleanTag = elementInfo.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`;
});
prompt += `
STYLE ${personality.nom.toUpperCase()}:
- Vocabulaire: ${personality.vocabulairePref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES:
- Contenu SEO optimisé pour ${csvData.mc0}
- Style ${personality.style} naturel
- Pas de références techniques dans contenu
- RÉPONSE DIRECTE par le contenu
FORMAT:
[${elements[0].tag.replace(/\|/g, '')}]
Contenu généré...
[${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}]
Contenu généré...`;
return prompt;
}
/**
* Parser réponse batch
*/
function parseBatchResponse(response, elements) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux vrais tags
elements.forEach(element => {
const cleanTag = element.tag.replace(/\|/g, '');
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
results[element.tag] = parsedItems[cleanTag];
} else {
results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`;
logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING');
}
});
return results;
}
/**
* Créer prompt pour paires FAQ
*/
function createFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality;
let prompt = `=== GÉNÉRATION PAIRES FAQ ===
Sujet: ${csvData.mc0}
Rédacteur: ${personality.nom} (${personality.style})
PAIRES À GÉNÉRER:
`;
faqPairs.forEach((pair, index) => {
const qTag = pair.question.tag.replace(/\|/g, '');
const aTag = pair.answer.tag.replace(/\|/g, '');
prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`;
});
prompt += `
CONSIGNES:
- Questions naturelles de clients
- Réponses expertes ${personality.style}
- Couvrir: prix, livraison, personnalisation
FORMAT:
[${faqPairs[0].question.tag.replace(/\|/g, '')}]
Question client naturelle ?
[${faqPairs[0].answer.tag.replace(/\|/g, '')}]
Réponse utile et rassurante.`;
return prompt;
}
/**
* Parser réponse FAQ
*/
function parseFAQResponse(response, faqPairs) {
const results = {};
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = cleanGeneratedContent(match[2].trim());
parsedItems[tag] = content;
}
// Mapper aux paires FAQ
faqPairs.forEach(pair => {
const qCleanTag = pair.question.tag.replace(/\|/g, '');
const aCleanTag = pair.answer.tag.replace(/\|/g, '');
if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag];
if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag];
});
return results;
}
// ============= HELPER FUNCTIONS =============
function collectElementsInXMLOrder(hierarchy) {
const allElements = [];
Object.keys(hierarchy).forEach(path => {
const section = hierarchy[path];
if (section.title) {
allElements.push({
tag: section.title.originalElement.originalTag,
element: section.title.originalElement,
type: section.title.originalElement.type
});
}
if (section.text) {
allElements.push({
tag: section.text.originalElement.originalTag,
element: section.text.originalElement,
type: section.text.originalElement.type
});
}
section.questions.forEach(q => {
allElements.push({
tag: q.originalElement.originalTag,
element: q.originalElement,
type: q.originalElement.type
});
});
});
return allElements;
}
function separateElementTypes(allElements) {
const faqPairs = [];
const otherElements = [];
const faqQuestions = {};
const faqAnswers = {};
// Collecter FAQ questions et answers
allElements.forEach(element => {
if (element.type === 'faq_question') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqQuestions[faqNumber] = element;
} else if (element.type === 'faq_reponse') {
const numberMatch = element.tag.match(/(\d+)/);
const faqNumber = numberMatch ? numberMatch[1] : '1';
faqAnswers[faqNumber] = element;
} else {
otherElements.push(element);
}
});
// Créer paires FAQ
Object.keys(faqQuestions).forEach(number => {
const question = faqQuestions[number];
const answer = faqAnswers[number];
if (question && answer) {
faqPairs.push({ number, question, answer });
} else if (question) {
otherElements.push(question);
} else if (answer) {
otherElements.push(answer);
}
});
return { faqPairs, otherElements };
}
function getElementDescription(elementInfo) {
switch (elementInfo.type) {
case 'titre_h1': return 'Titre principal accrocheur';
case 'titre_h2': return 'Titre de section';
case 'titre_h3': return 'Sous-titre';
case 'intro': return 'Introduction engageante';
case 'texte': return 'Paragraphe informatif';
default: return 'Contenu pertinent';
}
}
function cleanGeneratedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
generateInitialContent, // ← MAIN ENTRY POINT
generateNormalElements,
generateFAQPairs,
createBatchPrompt,
parseBatchResponse,
collectElementsInXMLOrder,
separateElementTypes
};

View File

@ -0,0 +1,340 @@
// ========================================
// ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ
// Responsabilité: Appliquer le style personnalité avec Mistral
// LLM: Mistral (température 0.8)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT STYLE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function applyPersonalityStyle(input) {
return await tracer.run('StyleEnhancement.applyPersonalityStyle()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '4/4',
llmProvider: 'mistral',
elementsCount: Object.keys(content).length,
personality: csvData.personality?.nom,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🎭 ÉTAPE 4/4: Enhancement style ${csvData.personality?.nom} (Mistral)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO');
try {
const personality = csvData.personality;
if (!personality) {
logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' }
};
}
// 1. Préparer éléments pour stylisation
const styleElements = prepareElementsForStyling(content);
// 2. Appliquer style en chunks
const styledResults = await applyStyleInChunks(styleElements, csvData);
// 3. Merger résultats
const finalContent = { ...content };
let actuallyStyled = 0;
Object.keys(styledResults).forEach(tag => {
if (styledResults[tag] !== content[tag]) {
finalContent[tag] = styledResults[tag];
actuallyStyled++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyStyled,
personality: personality.nom,
duration
};
logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement style terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'mistral',
step: 4,
personalityApplied: personality.nom,
styleCharacteristics: {
vocabulaire: personality.vocabulairePref,
connecteurs: personality.connecteursPref,
style: personality.style
}
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Mistral indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Préparer éléments pour stylisation
*/
function prepareElementsForStyling(content) {
const styleElements = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Tous les éléments peuvent bénéficier d'adaptation personnalité
// Même les courts (titres) peuvent être adaptés au style
styleElements.push({
tag,
content: text,
priority: calculateStylePriority(text, tag)
});
});
// Trier par priorité (titres d'abord, puis textes longs)
styleElements.sort((a, b) => b.priority - a.priority);
return styleElements;
}
/**
* Calculer priorité de stylisation
*/
function calculateStylePriority(text, tag) {
let priority = 1.0;
// Titres = haute priorité (plus visible)
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) {
priority += 0.5;
}
// Textes longs = priorité selon longueur
if (text.length > 200) {
priority += 0.3;
} else if (text.length > 100) {
priority += 0.2;
}
// Introduction = haute priorité
if (tag.includes('intro') || tag.includes('Introduction')) {
priority += 0.4;
}
return priority;
}
/**
* Appliquer style en chunks
*/
async function applyStyleInChunks(styleElements, csvData) {
logSh(`🎨 Stylisation: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG');
const results = {};
const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const stylePrompt = createStylePrompt(chunk, csvData);
const styledResponse = await callLLM('mistral', stylePrompt, {
temperature: 0.8,
maxTokens: 3000
}, csvData.personality);
const chunkResults = parseStyleResponse(styledResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisé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
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt de stylisation
*/
function createStylePrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
CONTEXTE: Article SEO e-commerce ${csvData.mc0}
PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description}
STYLE: ${personality.style} adapté web professionnel
VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
LONGUEUR PHRASES: ${personality.longueurPhrases}
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)})
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}:
- Adapte le TON selon ${personality.style}
- Vocabulaire: ${personality.vocabulairePref}
- Connecteurs variés: ${personality.connecteursPref}
- Phrases: ${personality.longueurPhrases}
- Niveau: ${personality.niveauTechnique}
CONSIGNES STRICTES:
- GARDE le même contenu informatif et technique
- Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom}
- RESPECTE longueur approximative (±20%)
- ÉVITE répétitions excessives
- Style ${personality.nom} reconnaissable mais NATUREL web
- PAS de messages d'excuse
FORMAT RÉPONSE:
[1] Contenu stylisé selon ${personality.nom}
[2] Contenu stylisé selon ${personality.nom}
etc...`;
return prompt;
}
/**
* Parser réponse stylisation
*/
function parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu stylisé
styledContent = cleanStyledContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu stylisé
*/
function cleanStyledContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, '');
content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
// Réduire répétitions excessives mais garder le style personnalité
content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup ');
content = content.replace(/(bon[,\s]+){4,}/gi, 'bon ');
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
/**
* Obtenir instructions de style dynamiques
*/
function getPersonalityStyleInstructions(personality) {
if (!personality) return "Style professionnel standard";
return `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
- Description: ${personality.description}
- Vocabulaire: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'}
- Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'}
- Phrases: ${personality.longueurPhrases || 'Moyennes'}
- Niveau: ${personality.niveauTechnique || 'Accessible'}
- CTA: ${personality.ctaStyle || 'Professionnel'}`;
}
// ============= HELPER FUNCTIONS =============
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
applyPersonalityStyle, // ← MAIN ENTRY POINT
prepareElementsForStyling,
calculateStylePriority,
applyStyleInChunks,
createStylePrompt,
parseStyleResponse,
getPersonalityStyleInstructions
};

View File

@ -0,0 +1,277 @@
// ========================================
// ÉTAPE 2: ENHANCEMENT TECHNIQUE
// Responsabilité: Améliorer la précision technique avec GPT-4
// LLM: GPT-4o-mini (température 0.4)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTechnicalTerms(input) {
return await tracer.run('TechnicalEnhancement.enhanceTechnicalTerms()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '2/4',
llmProvider: 'gpt4',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🔧 ÉTAPE 2/4: Enhancement technique (GPT-4)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser tous les éléments pour détecter termes techniques
const technicalAnalysis = await analyzeTechnicalTerms(content, csvData);
// 2. Filter les éléments qui ont besoin d'enhancement
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO');
if (elementsNeedingEnhancement.length === 0) {
logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] }
};
}
// 3. Améliorer les éléments sélectionnés
const enhancedResults = await enhanceSelectedElements(elementsNeedingEnhancement, csvData);
// 4. Merger avec contenu original
const finalContent = { ...content };
let actuallyEnhanced = 0;
Object.keys(enhancedResults).forEach(tag => {
if (enhancedResults[tag] !== content[tag]) {
finalContent[tag] = enhancedResults[tag];
actuallyEnhanced++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyEnhanced,
candidate: elementsNeedingEnhancement.length,
duration
};
logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement technique terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gpt4',
step: 2,
enhancementsApplied: Object.keys(enhancedResults),
technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TechnicalEnhancement failed: ${error.message}`);
}
}, input);
}
/**
* Analyser tous les éléments pour détecter termes techniques
*/
async function analyzeTechnicalTerms(content, csvData) {
logSh(`🔍 Analyse termes techniques batch`, 'DEBUG');
const contentEntries = Object.keys(content);
const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER:
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
CONTENU: "${content[tag]}"`).join('\n\n')}
CONSIGNES:
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
- Évite mots génériques (qualité, service, pratique, personnalisé)
- Focus: matériaux, procédés, normes, dimensions, technologies
- Si aucun terme technique "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne
FORMAT RÉPONSE:
[1] dibond, impression UV OU AUCUN
[2] AUCUN
[3] aluminium, fraisage CNC OU AUCUN
etc...`;
try {
const analysisResponse = await callLLM('gpt4', analysisPrompt, {
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAnalysisResponse(analysisResponse, content, contentEntries);
} catch (error) {
logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Améliorer les éléments sélectionnés
*/
async function enhanceSelectedElements(elementsNeedingEnhancement, csvData) {
logSh(`🛠️ Enhancement ${elementsNeedingEnhancement.length} éléments`, 'DEBUG');
const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
CONTENUS À AMÉLIORER:
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')}
CONSIGNES:
- GARDE même longueur, structure et ton ${csvData.personality?.style}
- Intègre naturellement les termes techniques listés
- NE CHANGE PAS le fond du message
- Vocabulaire expert mais accessible
- Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique
[2] Contenu avec amélioration technique
etc...`;
try {
const enhancedResponse = await callLLM('gpt4', enhancementPrompt, {
temperature: 0.4,
maxTokens: 5000
}, csvData.personality);
return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement);
} catch (error) {
logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Parser réponse analyse
*/
function parseAnalysisResponse(response, content, contentEntries) {
const results = [];
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const index = parseInt(match[1]) - 1;
const termsText = match[2].trim();
parsedItems[index] = termsText;
}
contentEntries.forEach((tag, index) => {
const termsText = parsedItems[index] || 'AUCUN';
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
const technicalTerms = hasTerms ?
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
[];
results.push({
tag,
content: content[tag],
technicalTerms,
needsEnhancement: hasTerms && technicalTerms.length > 0
});
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG');
});
return results;
}
/**
* Parser réponse enhancement
*/
function parseEnhancementResponse(response, elementsNeedingEnhancement) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
let enhancedContent = match[2].trim();
const element = elementsNeedingEnhancement[index];
// Nettoyer le contenu généré
enhancedContent = cleanEnhancedContent(enhancedContent);
if (enhancedContent && enhancedContent.length > 10) {
results[element.tag] = enhancedContent;
logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < elementsNeedingEnhancement.length) {
const element = elementsNeedingEnhancement[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu amélioré
*/
function cleanEnhancedContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
content = content.replace(/\*\*[^*]+\*\*/g, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
module.exports = {
enhanceTechnicalTerms, // ← MAIN ENTRY POINT
analyzeTechnicalTerms,
enhanceSelectedElements,
parseAnalysisResponse,
parseEnhancementResponse
};

View File

@ -0,0 +1,401 @@
// ========================================
// ÉTAPE 3: ENHANCEMENT TRANSITIONS
// Responsabilité: Améliorer la fluidité avec Gemini
// LLM: Gemini (température 0.6)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS
* Input: { content: {}, csvData: {}, context: {} }
* Output: { content: {}, stats: {}, debug: {} }
*/
async function enhanceTransitions(input) {
return await tracer.run('TransitionEnhancement.enhanceTransitions()', async () => {
const { content, csvData, context = {} } = input;
await tracer.annotate({
step: '3/4',
llmProvider: 'gemini',
elementsCount: Object.keys(content).length,
mc0: csvData.mc0
});
const startTime = Date.now();
logSh(`🔗 ÉTAPE 3/4: Enhancement transitions (Gemini)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO');
try {
// 1. Analyser quels éléments ont besoin d'amélioration transitions
const elementsNeedingTransitions = analyzeTransitionNeeds(content);
logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO');
if (elementsNeedingTransitions.length === 0) {
logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime },
debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] }
};
}
// 2. Améliorer en chunks pour Gemini
const improvedResults = await improveTransitionsInChunks(elementsNeedingTransitions, csvData);
// 3. Merger avec contenu original
const finalContent = { ...content };
let actuallyImproved = 0;
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
finalContent[tag] = improvedResults[tag];
actuallyImproved++;
}
});
const duration = Date.now() - startTime;
const stats = {
processed: Object.keys(content).length,
enhanced: actuallyImproved,
candidate: elementsNeedingTransitions.length,
duration
};
logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO');
await tracer.event(`Enhancement transitions terminé`, stats);
return {
content: finalContent,
stats,
debug: {
llmProvider: 'gemini',
step: 3,
enhancementsApplied: Object.keys(improvedResults),
transitionIssues: elementsNeedingTransitions.map(e => e.issues)
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original si Gemini indisponible
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
return {
content,
stats: { processed: Object.keys(content).length, enhanced: 0, duration },
debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true }
};
}
}, input);
}
/**
* Analyser besoin d'amélioration transitions
*/
function analyzeTransitionNeeds(content) {
const elementsNeedingTransitions = [];
Object.keys(content).forEach(tag => {
const text = content[tag];
// Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations
if (text.length > 150) {
const needsTransitions = evaluateTransitionQuality(text);
if (needsTransitions.needsImprovement) {
elementsNeedingTransitions.push({
tag,
content: text,
issues: needsTransitions.issues,
score: needsTransitions.score
});
logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG');
}
} else {
logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG');
}
});
// Trier par score (plus problématique en premier)
elementsNeedingTransitions.sort((a, b) => a.score - b.score);
return elementsNeedingTransitions;
}
/**
* Évaluer qualité transitions d'un texte
*/
function evaluateTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 1.0, issues: [] };
}
const issues = [];
let score = 1.0; // Score parfait = 1.0, problématique = 0.0
// Analyse 1: Connecteurs répétitifs
const repetitiveConnectors = analyzeRepetitiveConnectors(text);
if (repetitiveConnectors > 0.3) {
issues.push('connecteurs_répétitifs');
score -= 0.3;
}
// Analyse 2: Transitions abruptes
const abruptTransitions = analyzeAbruptTransitions(sentences);
if (abruptTransitions > 0.4) {
issues.push('transitions_abruptes');
score -= 0.4;
}
// Analyse 3: Manque de variété dans longueurs
const sentenceVariety = analyzeSentenceVariety(sentences);
if (sentenceVariety < 0.3) {
issues.push('phrases_uniformes');
score -= 0.2;
}
// Analyse 4: Trop formel ou trop familier
const formalityIssues = analyzeFormalityBalance(text);
if (formalityIssues > 0.5) {
issues.push('formalité_déséquilibrée');
score -= 0.1;
}
return {
needsImprovement: score < 0.6,
score: Math.max(0, score),
issues
};
}
/**
* Améliorer transitions en chunks
*/
async function improveTransitionsInChunks(elementsNeedingTransitions, csvData) {
logSh(`🔄 Amélioration transitions: ${elementsNeedingTransitions.length} éléments`, 'DEBUG');
const results = {};
const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const improvementPrompt = createTransitionImprovementPrompt(chunk, csvData);
const improvedResponse = await callLLM('gemini', improvementPrompt, {
temperature: 0.6,
maxTokens: 2500
}, csvData.personality);
const chunkResults = parseTransitionResponse(improvedResponse, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} amélioré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(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* Créer prompt amélioration transitions
*/
function createTransitionImprovementPrompt(chunk, csvData) {
const personality = csvData.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData.mc0}
PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel)
CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref}
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS:
- Connecteurs plus naturels et variés: ${personality?.connecteursPref}
- Transitions fluides entre idées
- ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs")
- Style ${personality?.style} mais professionnel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message
- GARDE même structure et longueur
- Améliore SEULEMENT la fluidité
- RESPECTE le style ${personality?.nom}
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...`;
return prompt;
}
/**
* Parser réponse amélioration transitions
*/
function parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let improvedContent = match[2].trim();
const element = chunk[index];
// Nettoyer le contenu amélioré
improvedContent = cleanImprovedContent(improvedContent);
if (improvedContent && improvedContent.length > 10) {
results[element.tag] = improvedContent;
logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content;
logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
// ============= HELPER FUNCTIONS =============
function analyzeRepetitiveConnectors(content) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
function analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim();
const hasConnector = hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
function analyzeSentenceVariety(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
function analyzeFormalityBalance(content) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (content.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
const balance = Math.abs(formalCount - casualCount) / total;
return balance;
}
function hasTransitionWord(sentence) {
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi'];
return connectors.some(connector => sentence.toLowerCase().includes(connector));
}
function cleanImprovedContent(content) {
if (!content) return content;
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, '');
content = content.replace(/\s{2,}/g, ' ');
content = content.trim();
return content;
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
enhanceTransitions, // ← MAIN ENTRY POINT
analyzeTransitionNeeds,
evaluateTransitionQuality,
improveTransitionsInChunks,
createTransitionImprovementPrompt,
parseTransitionResponse
};

422
lib/main_modulaire.js Normal file
View File

@ -0,0 +1,422 @@
// ========================================
// MAIN MODULAIRE - PIPELINE ARCHITECTURALE MODERNE
// Responsabilité: Orchestration workflow avec architecture modulaire complète
// Usage: node main_modulaire.js [rowNumber] [stackType]
// ========================================
const { logSh } = require('./lib/ErrorReporting');
const { tracer } = require('./lib/trace');
// Imports pipeline de base
const { readInstructionsData, selectPersonalityWithAI, getPersonalities } = require('./lib/BrainConfig');
const { extractElementsFromXML } = require('./lib/ElementExtraction');
const { generateMissingKeywords } = require('./lib/MissingKeywords');
const { generateDirectElements } = require('./lib/generation/DirectGeneration');
const { injectContentIntoTemplate } = require('./lib/ContentAssembly');
const { compileAndStoreArticle } = require('./lib/ArticleStorage');
// Imports modules modulaires
const { applySelectiveLayer } = require('./lib/selective-enhancement/SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./lib/selective-enhancement/SelectiveLayers');
const {
applyAdversarialLayer
} = require('./lib/adversarial-generation/AdversarialCore');
const {
applyPredefinedStack: applyAdversarialStack
} = require('./lib/adversarial-generation/AdversarialLayers');
/**
* WORKFLOW MODULAIRE PRINCIPAL
*/
async function handleModularWorkflow(config = {}) {
return await tracer.run('MainModulaire.handleModularWorkflow()', async () => {
const {
rowNumber = 2,
selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive
adversarialMode = 'light', // none, light, standard, heavy, adaptive
source = 'main_modulaire'
} = config;
await tracer.annotate({
modularWorkflow: true,
rowNumber,
selectiveStack,
adversarialMode,
source
});
const startTime = Date.now();
logSh(`🚀 WORKFLOW MODULAIRE DÉMARRÉ`, 'INFO');
logSh(` 📊 Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO');
try {
// ========================================
// PHASE 1: PRÉPARATION DONNÉES
// ========================================
logSh(`📋 PHASE 1: Préparation données`, 'INFO');
const csvData = await readInstructionsData(rowNumber);
if (!csvData) {
throw new Error(`Impossible de lire les données ligne ${rowNumber}`);
}
const personalities = await getPersonalities();
const selectedPersonality = await selectPersonalityWithAI(
csvData.mc0,
csvData.t0,
personalities
);
csvData.personality = selectedPersonality;
logSh(` ✅ Données: ${csvData.mc0} | Personnalité: ${selectedPersonality.nom}`, 'DEBUG');
// ========================================
// PHASE 2: EXTRACTION ÉLÉMENTS
// ========================================
logSh(`📝 PHASE 2: Extraction éléments XML`, 'INFO');
const elements = await extractElementsFromXML(csvData.xmlTemplate);
logSh(`${Object.keys(elements).length} éléments extraits`, 'DEBUG');
// ========================================
// PHASE 3: GÉNÉRATION MOTS-CLÉS MANQUANTS
// ========================================
logSh(`🔍 PHASE 3: Génération mots-clés manquants`, 'INFO');
const enhancedCsvData = await generateMissingKeywords(csvData);
logSh(` ✅ Mots-clés complétés`, 'DEBUG');
// ========================================
// PHASE 4: GÉNÉRATION CONTENU DE BASE
// ========================================
logSh(`💫 PHASE 4: Génération contenu de base`, 'INFO');
const generatedContent = await generateDirectElements(elements, enhancedCsvData, {
source: 'main_modulaire',
usePersonality: true
});
logSh(`${Object.keys(generatedContent).length} éléments générés`, 'DEBUG');
// ========================================
// PHASE 5: SELECTIVE ENHANCEMENT MODULAIRE
// ========================================
logSh(`🔧 PHASE 5: Selective Enhancement Modulaire (${selectiveStack})`, 'INFO');
let selectiveResult;
switch (selectiveStack) {
case 'adaptive':
selectiveResult = await applyAdaptiveLayers(generatedContent, {
maxIntensity: 1.1,
analysisThreshold: 0.3,
csvData: enhancedCsvData
});
break;
case 'technical':
case 'transitions':
case 'style':
selectiveResult = await applySelectiveLayer(generatedContent, {
layerType: selectiveStack,
llmProvider: 'auto',
intensity: 1.0,
csvData: enhancedCsvData
});
break;
default:
// Stack prédéfini
selectiveResult = await applyPredefinedStack(generatedContent, selectiveStack, {
csvData: enhancedCsvData,
analysisMode: true
});
}
const enhancedContent = selectiveResult.content;
logSh(` ✅ Selective: ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} améliorations`, 'INFO');
// ========================================
// PHASE 6: ADVERSARIAL ENHANCEMENT (OPTIONNEL)
// ========================================
let finalContent = enhancedContent;
let adversarialStats = null;
if (adversarialMode !== 'none') {
logSh(`🎯 PHASE 6: Adversarial Enhancement (${adversarialMode})`, 'INFO');
let adversarialResult;
switch (adversarialMode) {
case 'adaptive':
// Utiliser adversarial adaptatif
adversarialResult = await applyAdversarialLayer(enhancedContent, {
detectorTarget: 'general',
method: 'hybrid',
intensity: 0.8,
analysisMode: true
});
break;
case 'light':
case 'standard':
case 'heavy':
// Utiliser stack adversarial prédéfini
const stackMapping = {
light: 'lightDefense',
standard: 'standardDefense',
heavy: 'heavyDefense'
};
adversarialResult = await applyAdversarialStack(enhancedContent, stackMapping[adversarialMode], {
csvData: enhancedCsvData
});
break;
}
if (adversarialResult && !adversarialResult.fallback) {
finalContent = adversarialResult.content;
adversarialStats = adversarialResult.stats;
logSh(` ✅ Adversarial: ${adversarialStats.elementsModified || adversarialStats.totalModifications || 0} modifications`, 'INFO');
} else {
logSh(` ⚠️ Adversarial fallback: contenu selective préservé`, 'WARNING');
}
}
// ========================================
// PHASE 7: ASSEMBLAGE ET STOCKAGE
// ========================================
logSh(`🔗 PHASE 7: Assemblage et stockage`, 'INFO');
const assembledContent = await injectContentIntoTemplate(finalContent, enhancedCsvData.xmlTemplate);
const storageResult = await compileAndStoreArticle(assembledContent, {
...enhancedCsvData,
source: `${source}_${selectiveStack}${adversarialMode !== 'none' ? `_${adversarialMode}` : ''}`
});
logSh(` ✅ Stocké: ${storageResult.compiledLength} caractères`, 'DEBUG');
// ========================================
// RÉSUMÉ FINAL
// ========================================
const totalDuration = Date.now() - startTime;
const finalStats = {
rowNumber,
selectiveStack,
adversarialMode,
totalDuration,
elementsGenerated: Object.keys(generatedContent).length,
selectiveEnhancements: selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0,
adversarialModifications: adversarialStats?.elementsModified || adversarialStats?.totalModifications || 0,
finalLength: storageResult.compiledLength,
personality: selectedPersonality.nom,
source
};
logSh(`✅ WORKFLOW MODULAIRE TERMINÉ (${totalDuration}ms)`, 'INFO');
logSh(` 📊 ${finalStats.elementsGenerated} générés | ${finalStats.selectiveEnhancements} selective | ${finalStats.adversarialModifications} adversarial`, 'INFO');
logSh(` 🎭 Personnalité: ${finalStats.personality} | Taille finale: ${finalStats.finalLength} chars`, 'INFO');
await tracer.event('Workflow modulaire terminé', finalStats);
return {
success: true,
stats: finalStats,
content: finalContent,
assembledContent,
storageResult,
selectiveResult,
adversarialResult: adversarialStats ? { stats: adversarialStats } : null
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ WORKFLOW MODULAIRE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
logSh(`Stack trace: ${error.stack}`, 'ERROR');
await tracer.event('Workflow modulaire échoué', {
error: error.message,
duration,
rowNumber,
selectiveStack,
adversarialMode
});
throw error;
}
}, { config });
}
/**
* BENCHMARK COMPARATIF STACKS
*/
async function benchmarkStacks(rowNumber = 2) {
console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n');
const stacks = getAvailableStacks();
const adversarialModes = ['none', 'light', 'standard'];
const results = [];
for (const stack of stacks.slice(0, 3)) { // Tester 3 stacks principaux
for (const advMode of adversarialModes.slice(0, 2)) { // 2 modes adversarial
console.log(`🧪 Test: ${stack.name} + adversarial ${advMode}`);
try {
const startTime = Date.now();
const result = await handleModularWorkflow({
rowNumber,
selectiveStack: stack.name,
adversarialMode: advMode,
source: 'benchmark'
});
const duration = Date.now() - startTime;
results.push({
stack: stack.name,
adversarial: advMode,
duration,
success: true,
selectiveEnhancements: result.stats.selectiveEnhancements,
adversarialModifications: result.stats.adversarialModifications,
finalLength: result.stats.finalLength
});
console.log(`${duration}ms | ${result.stats.selectiveEnhancements} selective | ${result.stats.adversarialModifications} adversarial`);
} catch (error) {
results.push({
stack: stack.name,
adversarial: advMode,
success: false,
error: error.message
});
console.log(` ❌ Échoué: ${error.message}`);
}
}
}
// Résumé benchmark
console.log('\n📊 RÉSUMÉ BENCHMARK:');
const successful = results.filter(r => r.success);
if (successful.length > 0) {
const avgDuration = successful.reduce((sum, r) => sum + r.duration, 0) / successful.length;
const bestPerf = successful.reduce((best, r) => r.duration < best.duration ? r : best);
const mostEnhancements = successful.reduce((best, r) =>
(r.selectiveEnhancements + r.adversarialModifications) > (best.selectiveEnhancements + best.adversarialModifications) ? r : best
);
console.log(` ⚡ Durée moyenne: ${avgDuration.toFixed(0)}ms`);
console.log(` 🏆 Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} (${bestPerf.duration}ms)`);
console.log(` 🔥 Plus d'améliorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications})`);
}
return results;
}
/**
* INTERFACE LIGNE DE COMMANDE
*/
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'workflow';
try {
switch (command) {
case 'workflow':
const rowNumber = parseInt(args[1]) || 2;
const selectiveStack = args[2] || 'standardEnhancement';
const adversarialMode = args[3] || 'light';
console.log(`\n🚀 Exécution workflow modulaire:`);
console.log(` 📊 Ligne: ${rowNumber}`);
console.log(` 🔧 Stack selective: ${selectiveStack}`);
console.log(` 🎯 Mode adversarial: ${adversarialMode}`);
const result = await handleModularWorkflow({
rowNumber,
selectiveStack,
adversarialMode
});
console.log('\n✅ WORKFLOW MODULAIRE RÉUSSI');
console.log(`📈 Stats: ${JSON.stringify(result.stats, null, 2)}`);
break;
case 'benchmark':
const benchRowNumber = parseInt(args[1]) || 2;
console.log(`\n⚡ Benchmark stacks (ligne ${benchRowNumber})`);
const benchResults = await benchmarkStacks(benchRowNumber);
console.log('\n📊 Résultats complets:');
console.table(benchResults);
break;
case 'stacks':
console.log('\n📦 STACKS SELECTIVE DISPONIBLES:');
const availableStacks = getAvailableStacks();
availableStacks.forEach(stack => {
console.log(`\n 🔧 ${stack.name}:`);
console.log(` 📝 ${stack.description}`);
console.log(` 📊 ${stack.layersCount} couches`);
console.log(` 🎯 Couches: ${stack.layers ? stack.layers.map(l => `${l.type}(${l.llm})`).join(' → ') : 'N/A'}`);
});
console.log('\n🎯 MODES ADVERSARIAL DISPONIBLES:');
console.log(' - none: Pas d\'adversarial');
console.log(' - light: Défense légère');
console.log(' - standard: Défense standard');
console.log(' - heavy: Défense intensive');
console.log(' - adaptive: Adaptatif intelligent');
break;
case 'help':
default:
console.log('\n🔧 === MAIN MODULAIRE - USAGE ===');
console.log('\nCommandes disponibles:');
console.log(' workflow [ligne] [stack] [adversarial] - Exécuter workflow complet');
console.log(' benchmark [ligne] - Benchmark stacks');
console.log(' stacks - Lister stacks disponibles');
console.log(' help - Afficher cette aide');
console.log('\nExemples:');
console.log(' node main_modulaire.js workflow 2 fullEnhancement standard');
console.log(' node main_modulaire.js workflow 3 adaptive light');
console.log(' node main_modulaire.js benchmark 2');
console.log(' node main_modulaire.js stacks');
break;
}
} catch (error) {
console.error('\n❌ ERREUR MAIN MODULAIRE:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Export pour usage programmatique
module.exports = {
handleModularWorkflow,
benchmarkStacks
};
// Exécution CLI si appelé directement
if (require.main === module) {
main().catch(error => {
console.error('❌ ERREUR FATALE:', error.message);
process.exit(1);
});
}

View File

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

View File

@ -0,0 +1,485 @@
// ========================================
// ORCHESTRATEUR PATTERN BREAKING - NIVEAU 2
// Responsabilité: Coordonner les 3 techniques anti-détection
// Objectif: -20% détection IA vs Niveau 1
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
// Import des 3 techniques Pattern Breaking
const { applySentenceVariation } = require('./SentenceVariation');
const { removeLLMFingerprints } = require('./LLMFingerprintRemoval');
const { humanizeTransitions } = require('./TransitionHumanization');
/**
* MAIN ENTRY POINT - PATTERN BREAKING COMPLET
* @param {Object} input - { content: {}, csvData: {}, options: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applyPatternBreaking(input) {
return await tracer.run('PatternBreaking.applyPatternBreaking()', async () => {
const { content, csvData, options = {} } = input;
const config = {
// Configuration globale
intensity = 0.6, // Intensité générale (60%)
// Contrôle par technique
sentenceVariation: true, // Activer variation phrases
fingerprintRemoval: true, // Activer suppression empreintes
transitionHumanization: true, // Activer humanisation transitions
// Configuration spécifique par technique
sentenceVariationConfig: {
intensity: 0.3,
splitThreshold: 100,
mergeThreshold: 30,
preserveQuestions: true,
preserveTitles: true
},
fingerprintRemovalConfig: {
intensity: 1.0,
preserveKeywords: true,
contextualMode: true,
csvData
},
transitionHumanizationConfig: {
intensity: 0.6,
personalityStyle: csvData?.personality?.style,
avoidRepetition: true,
preserveFormal: false,
csvData
},
// Options avancées
qualityPreservation: true, // Préserver qualité contenu
seoIntegrity: true, // Maintenir intégrité SEO
readabilityCheck: true, // Vérifier lisibilité
...options // Override avec options fournies
};
await tracer.annotate({
level: 2,
technique: 'pattern_breaking',
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom,
config: {
sentenceVariation: config.sentenceVariation,
fingerprintRemoval: config.fingerprintRemoval,
transitionHumanization: config.transitionHumanization,
intensity: config.intensity
}
});
const startTime = Date.now();
logSh(`🎯 NIVEAU 2: PATTERN BREAKING (3 techniques)`, 'INFO');
logSh(` 🎭 Personnalité: ${csvData?.personality?.nom} (${csvData?.personality?.style})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à traiter`, 'INFO');
logSh(` ⚙️ Techniques actives: ${[config.sentenceVariation && 'Variation', config.fingerprintRemoval && 'Empreintes', config.transitionHumanization && 'Transitions'].filter(Boolean).join(' + ')}`, 'INFO');
try {
let currentContent = { ...content };
const pipelineStats = {
techniques: [],
totalDuration: 0,
qualityMetrics: {}
};
// Analyse initiale de qualité
if (config.qualityPreservation) {
pipelineStats.qualityMetrics.initial = analyzeContentQuality(currentContent);
}
// TECHNIQUE 1: VARIATION LONGUEUR PHRASES
if (config.sentenceVariation) {
const step1Result = await applySentenceVariation({
content: currentContent,
config: config.sentenceVariationConfig,
context: { step: 1, totalSteps: 3 }
});
currentContent = step1Result.content;
pipelineStats.techniques.push({
name: 'SentenceVariation',
...step1Result.stats,
qualityImpact: calculateQualityImpact(content, step1Result.content)
});
logSh(` ✅ 1/3: Variation phrases - ${step1Result.stats.modified}/${step1Result.stats.processed} éléments`, 'INFO');
}
// TECHNIQUE 2: SUPPRESSION EMPREINTES LLM
if (config.fingerprintRemoval) {
const step2Result = await removeLLMFingerprints({
content: currentContent,
config: config.fingerprintRemovalConfig,
context: { step: 2, totalSteps: 3 }
});
currentContent = step2Result.content;
pipelineStats.techniques.push({
name: 'FingerprintRemoval',
...step2Result.stats,
qualityImpact: calculateQualityImpact(content, step2Result.content)
});
logSh(` ✅ 2/3: Suppression empreintes - ${step2Result.stats.totalReplacements} remplacements`, 'INFO');
}
// TECHNIQUE 3: HUMANISATION TRANSITIONS
if (config.transitionHumanization) {
const step3Result = await humanizeTransitions({
content: currentContent,
config: config.transitionHumanizationConfig,
context: { step: 3, totalSteps: 3 }
});
currentContent = step3Result.content;
pipelineStats.techniques.push({
name: 'TransitionHumanization',
...step3Result.stats,
qualityImpact: calculateQualityImpact(content, step3Result.content)
});
logSh(` ✅ 3/3: Humanisation transitions - ${step3Result.stats.totalReplacements} améliorations`, 'INFO');
}
// POST-PROCESSING: Vérifications qualité
if (config.qualityPreservation || config.readabilityCheck) {
const qualityCheck = performQualityChecks(content, currentContent, config);
pipelineStats.qualityMetrics.final = qualityCheck;
// Rollback si qualité trop dégradée
if (qualityCheck.shouldRollback) {
logSh(`⚠️ ROLLBACK: Qualité dégradée, retour contenu original`, 'WARNING');
currentContent = content;
pipelineStats.rollback = true;
}
}
// RÉSULTATS FINAUX
const totalDuration = Date.now() - startTime;
pipelineStats.totalDuration = totalDuration;
const totalModifications = pipelineStats.techniques.reduce((sum, tech) => {
return sum + (tech.modified || tech.totalReplacements || 0);
}, 0);
const stats = {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications,
techniquesUsed: pipelineStats.techniques.length,
duration: totalDuration,
techniques: pipelineStats.techniques,
qualityPreserved: !pipelineStats.rollback,
rollback: pipelineStats.rollback || false
};
logSh(`🎯 NIVEAU 2 TERMINÉ: ${totalModifications} modifications sur ${stats.processed} éléments (${totalDuration}ms)`, 'INFO');
// Log détaillé par technique
pipelineStats.techniques.forEach(tech => {
const modificationsCount = tech.modified || tech.totalReplacements || 0;
logSh(`${tech.name}: ${modificationsCount} modifications (${tech.duration}ms)`, 'DEBUG');
});
await tracer.event('Pattern breaking terminé', stats);
return {
content: currentContent,
stats,
debug: {
level: 2,
technique: 'pattern_breaking',
config,
pipeline: pipelineStats,
qualityMetrics: pipelineStats.qualityMetrics
}
};
} catch (error) {
const totalDuration = Date.now() - startTime;
logSh(`❌ NIVEAU 2 ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR');
// Fallback: retourner contenu original
logSh(`🔄 Fallback: contenu original conservé`, 'WARNING');
await tracer.event('Pattern breaking échoué', {
error: error.message,
duration: totalDuration,
fallback: true
});
return {
content,
stats: {
level: 2,
technique: 'pattern_breaking',
processed: Object.keys(content).length,
totalModifications: 0,
duration: totalDuration,
error: error.message,
fallback: true
},
debug: { error: error.message, fallback: true }
};
}
}, input);
}
/**
* MODE DIAGNOSTIC - Test individuel des techniques
*/
async function diagnosticPatternBreaking(content, csvData) {
logSh(`🔬 DIAGNOSTIC NIVEAU 2: Test individuel des techniques`, 'INFO');
const diagnostics = {
techniques: [],
errors: [],
performance: {},
recommendations: []
};
const techniques = [
{ name: 'SentenceVariation', func: applySentenceVariation },
{ name: 'FingerprintRemoval', func: removeLLMFingerprints },
{ name: 'TransitionHumanization', func: humanizeTransitions }
];
for (const technique of techniques) {
try {
const startTime = Date.now();
const result = await technique.func({
content,
config: { csvData },
context: { diagnostic: true }
});
diagnostics.techniques.push({
name: technique.name,
success: true,
duration: Date.now() - startTime,
stats: result.stats,
effectivenessScore: calculateEffectivenessScore(result.stats)
});
} catch (error) {
diagnostics.errors.push({
technique: technique.name,
error: error.message
});
diagnostics.techniques.push({
name: technique.name,
success: false,
error: error.message
});
}
}
// Générer recommandations
diagnostics.recommendations = generateRecommendations(diagnostics.techniques);
const successfulTechniques = diagnostics.techniques.filter(t => t.success);
diagnostics.performance.totalDuration = diagnostics.techniques.reduce((sum, t) => sum + (t.duration || 0), 0);
diagnostics.performance.successRate = Math.round((successfulTechniques.length / techniques.length) * 100);
logSh(`🔬 DIAGNOSTIC TERMINÉ: ${successfulTechniques.length}/${techniques.length} techniques opérationnelles`, 'INFO');
return diagnostics;
}
/**
* Analyser qualité du contenu
*/
function analyzeContentQuality(content) {
const allText = Object.values(content).join(' ');
const wordCount = allText.split(/\s+/).length;
const avgWordsPerElement = wordCount / Object.keys(content).length;
// Métrique de lisibilité approximative (Flesch simplifié)
const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 5);
const avgWordsPerSentence = wordCount / Math.max(1, sentences.length);
const readabilityScore = Math.max(0, 100 - (avgWordsPerSentence * 1.5));
return {
wordCount,
elementCount: Object.keys(content).length,
avgWordsPerElement: Math.round(avgWordsPerElement),
avgWordsPerSentence: Math.round(avgWordsPerSentence),
readabilityScore: Math.round(readabilityScore),
sentenceCount: sentences.length
};
}
/**
* Calculer impact qualité entre avant/après
*/
function calculateQualityImpact(originalContent, modifiedContent) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const wordCountChange = ((modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount) * 100;
const readabilityChange = modifiedQuality.readabilityScore - originalQuality.readabilityScore;
return {
wordCountChange: Math.round(wordCountChange * 100) / 100,
readabilityChange: Math.round(readabilityChange),
severe: Math.abs(wordCountChange) > 10 || Math.abs(readabilityChange) > 15
};
}
/**
* Effectuer vérifications qualité
*/
function performQualityChecks(originalContent, modifiedContent, config) {
const originalQuality = analyzeContentQuality(originalContent);
const modifiedQuality = analyzeContentQuality(modifiedContent);
const qualityThresholds = {
maxWordCountChange: 15, // % max changement nombre mots
minReadabilityScore: 50, // Score lisibilité minimum
maxReadabilityDrop: 20 // Baisse max lisibilité
};
const issues = [];
// Vérification nombre de mots
const wordCountChange = Math.abs(modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount * 100;
if (wordCountChange > qualityThresholds.maxWordCountChange) {
issues.push({
type: 'word_count_change',
severity: 'high',
change: wordCountChange,
threshold: qualityThresholds.maxWordCountChange
});
}
// Vérification lisibilité
if (modifiedQuality.readabilityScore < qualityThresholds.minReadabilityScore) {
issues.push({
type: 'low_readability',
severity: 'medium',
score: modifiedQuality.readabilityScore,
threshold: qualityThresholds.minReadabilityScore
});
}
const readabilityDrop = originalQuality.readabilityScore - modifiedQuality.readabilityScore;
if (readabilityDrop > qualityThresholds.maxReadabilityDrop) {
issues.push({
type: 'readability_drop',
severity: 'high',
drop: readabilityDrop,
threshold: qualityThresholds.maxReadabilityDrop
});
}
// Décision rollback
const highSeverityIssues = issues.filter(issue => issue.severity === 'high');
const shouldRollback = highSeverityIssues.length > 0 && config.qualityPreservation;
return {
originalQuality,
modifiedQuality,
issues,
shouldRollback,
qualityScore: calculateOverallQualityScore(issues, modifiedQuality)
};
}
/**
* Calculer score de qualité global
*/
function calculateOverallQualityScore(issues, quality) {
let baseScore = 100;
issues.forEach(issue => {
const penalty = issue.severity === 'high' ? 30 : issue.severity === 'medium' ? 15 : 5;
baseScore -= penalty;
});
// Bonus pour bonne lisibilité
if (quality.readabilityScore > 70) baseScore += 10;
return Math.max(0, Math.min(100, baseScore));
}
/**
* Calculer score d'efficacité d'une technique
*/
function calculateEffectivenessScore(stats) {
if (!stats) return 0;
const modificationsCount = stats.modified || stats.totalReplacements || 0;
const processedCount = stats.processed || 1;
const modificationRate = (modificationsCount / processedCount) * 100;
// Score basé sur taux de modification et durée
const baseScore = Math.min(100, modificationRate * 2); // Max 50% modification = score 100
const durationPenalty = Math.max(0, (stats.duration - 1000) / 100); // Pénalité si > 1s
return Math.max(0, Math.round(baseScore - durationPenalty));
}
/**
* Générer recommandations basées sur diagnostic
*/
function generateRecommendations(techniqueResults) {
const recommendations = [];
techniqueResults.forEach(tech => {
if (!tech.success) {
recommendations.push({
type: 'error',
technique: tech.name,
message: `${tech.name} a échoué: ${tech.error}`,
action: 'Vérifier configuration et dépendances'
});
return;
}
const effectiveness = tech.effectivenessScore || 0;
if (effectiveness < 30) {
recommendations.push({
type: 'low_effectiveness',
technique: tech.name,
message: `${tech.name} peu efficace (score: ${effectiveness})`,
action: 'Augmenter intensité ou réviser configuration'
});
} else if (effectiveness > 80) {
recommendations.push({
type: 'high_effectiveness',
technique: tech.name,
message: `${tech.name} très efficace (score: ${effectiveness})`,
action: 'Configuration optimale'
});
}
if (tech.duration > 3000) {
recommendations.push({
type: 'performance',
technique: tech.name,
message: `${tech.name} lent (${tech.duration}ms)`,
action: 'Considérer réduction intensité ou optimisation'
});
}
});
return recommendations;
}
module.exports = {
applyPatternBreaking, // ← MAIN ENTRY POINT
diagnosticPatternBreaking, // ← Mode diagnostic
analyzeContentQuality,
performQualityChecks,
calculateQualityImpact,
calculateEffectivenessScore
};

View File

@ -0,0 +1,336 @@
// ========================================
// PATTERN BREAKING - TECHNIQUE 1: SENTENCE VARIATION
// Responsabilité: Varier les longueurs de phrases pour casser l'uniformité
// Anti-détection: Éviter patterns syntaxiques réguliers des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - VARIATION LONGUEUR PHRASES
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function applySentenceVariation(input) {
return await tracer.run('SentenceVariation.applySentenceVariation()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.3, // Probabilité de modification (30%)
splitThreshold = 100, // Chars pour split
mergeThreshold = 30, // Chars pour merge
preserveQuestions = true, // Préserver questions FAQ
preserveTitles = true // Préserver titres
} = config;
await tracer.annotate({
technique: 'sentence_variation',
intensity,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📐 TECHNIQUE 1/3: Variation longueur phrases (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalModified = 0;
let modificationsDetails = [];
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
// Skip certains éléments selon config
if (shouldSkipElement(tag, text, { preserveQuestions, preserveTitles })) {
results[tag] = text;
logSh(` ⏭️ [${tag}]: Préservé (${getSkipReason(tag, text)})`, 'DEBUG');
continue;
}
// Appliquer variation si éligible
const variationResult = varyTextStructure(text, {
intensity,
splitThreshold,
mergeThreshold,
tag
});
results[tag] = variationResult.text;
if (variationResult.modified) {
totalModified++;
modificationsDetails.push({
tag,
modifications: variationResult.modifications,
originalLength: text.length,
newLength: variationResult.text.length
});
logSh(` ✏️ [${tag}]: ${variationResult.modifications.length} modifications`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Aucune modification`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
modified: totalModified,
modificationRate: Math.round((totalModified / totalProcessed) * 100),
duration,
technique: 'sentence_variation'
};
logSh(`✅ VARIATION PHRASES: ${stats.modified}/${stats.processed} éléments modifiés (${stats.modificationRate}%) en ${duration}ms`, 'INFO');
await tracer.event('Sentence variation terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'sentence_variation',
config: { intensity, splitThreshold, mergeThreshold },
modifications: modificationsDetails
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ VARIATION PHRASES échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`SentenceVariation failed: ${error.message}`);
}
}, input);
}
/**
* Appliquer variation structure à un texte
*/
function varyTextStructure(text, config) {
const { intensity, splitThreshold, mergeThreshold, tag } = config;
if (text.length < 50) {
return { text, modified: false, modifications: [] };
}
// Séparer en phrases
const sentences = splitIntoSentences(text);
if (sentences.length < 2) {
return { text, modified: false, modifications: [] };
}
let modifiedSentences = [...sentences];
const modifications = [];
// TECHNIQUE 1: SPLIT des phrases longues
for (let i = 0; i < modifiedSentences.length; i++) {
const sentence = modifiedSentences[i];
if (sentence.length > splitThreshold && Math.random() < intensity) {
const splitResult = splitLongSentence(sentence);
if (splitResult.success) {
modifiedSentences.splice(i, 1, splitResult.part1, splitResult.part2);
modifications.push({
type: 'split',
original: sentence.substring(0, 50) + '...',
result: `${splitResult.part1.substring(0, 25)}... | ${splitResult.part2.substring(0, 25)}...`
});
i++; // Skip la phrase suivante (qui est notre part2)
}
}
}
// TECHNIQUE 2: MERGE des phrases courtes
for (let i = 0; i < modifiedSentences.length - 1; i++) {
const current = modifiedSentences[i];
const next = modifiedSentences[i + 1];
if (current.length < mergeThreshold && next.length < mergeThreshold && Math.random() < intensity) {
const merged = mergeSentences(current, next);
if (merged.success) {
modifiedSentences.splice(i, 2, merged.result);
modifications.push({
type: 'merge',
original: `${current.substring(0, 20)}... + ${next.substring(0, 20)}...`,
result: merged.result.substring(0, 50) + '...'
});
}
}
}
const finalText = modifiedSentences.join(' ').trim();
return {
text: finalText,
modified: modifications.length > 0,
modifications
};
}
/**
* Diviser texte en phrases
*/
function splitIntoSentences(text) {
// Regex plus sophistiquée pour gérer les abréviations
const sentences = text.split(/(?<![A-Z][a-z]\.)\s*[.!?]+\s+/)
.map(s => s.trim())
.filter(s => s.length > 5);
return sentences;
}
/**
* Diviser une phrase longue en deux
*/
function splitLongSentence(sentence) {
// Points de rupture naturels
const breakPoints = [
', et ',
', mais ',
', car ',
', donc ',
', ainsi ',
', alors ',
', tandis que ',
', bien que '
];
// Chercher le meilleur point de rupture proche du milieu
const idealBreak = sentence.length / 2;
let bestBreak = null;
let bestDistance = Infinity;
for (const breakPoint of breakPoints) {
const index = sentence.indexOf(breakPoint, idealBreak - 50);
if (index > 0 && index < sentence.length - 20) {
const distance = Math.abs(index - idealBreak);
if (distance < bestDistance) {
bestDistance = distance;
bestBreak = { index, breakPoint };
}
}
}
if (bestBreak) {
const part1 = sentence.substring(0, bestBreak.index + 1).trim();
const part2 = sentence.substring(bestBreak.index + bestBreak.breakPoint.length).trim();
// Assurer que part2 commence par une majuscule
const capitalizedPart2 = part2.charAt(0).toUpperCase() + part2.slice(1);
return {
success: true,
part1,
part2: capitalizedPart2
};
}
return { success: false };
}
/**
* Fusionner deux phrases courtes
*/
function mergeSentences(sentence1, sentence2) {
// Connecteurs pour fusion naturelle
const connectors = [
'et',
'puis',
'aussi',
'également',
'de plus'
];
// Choisir connecteur aléatoire
const connector = connectors[Math.floor(Math.random() * connectors.length)];
// Nettoyer les phrases
let cleaned1 = sentence1.replace(/[.!?]+$/, '').trim();
let cleaned2 = sentence2.trim();
// Mettre sentence2 en minuscule sauf si nom propre
if (!/^[A-Z][a-z]*\s+[A-Z]/.test(cleaned2)) {
cleaned2 = cleaned2.charAt(0).toLowerCase() + cleaned2.slice(1);
}
const merged = `${cleaned1}, ${connector} ${cleaned2}`;
return {
success: merged.length < 200, // Éviter phrases trop longues
result: merged
};
}
/**
* Déterminer si un élément doit être skippé
*/
function shouldSkipElement(tag, text, config) {
// Skip titres si demandé
if (config.preserveTitles && (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2'))) {
return true;
}
// Skip questions FAQ si demandé
if (config.preserveQuestions && (tag.includes('Faq_q') || text.includes('?'))) {
return true;
}
// Skip textes très courts
if (text.length < 50) {
return true;
}
return false;
}
/**
* Obtenir raison du skip pour debug
*/
function getSkipReason(tag, text) {
if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) return 'titre';
if (tag.includes('Faq_q') || text.includes('?')) return 'question';
if (text.length < 50) return 'trop court';
return 'autre';
}
/**
* Analyser les patterns de phrases d'un texte
*/
function analyzeSentencePatterns(text) {
const sentences = splitIntoSentences(text);
if (sentences.length < 2) {
return { needsVariation: false, patterns: [] };
}
const lengths = sentences.map(s => s.length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
// Calculer uniformité (variance faible = uniformité élevée)
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const uniformity = 1 / (1 + Math.sqrt(variance) / avgLength); // 0-1, 1 = très uniforme
return {
needsVariation: uniformity > 0.7, // Seuil d'uniformité problématique
patterns: {
avgLength: Math.round(avgLength),
uniformity: Math.round(uniformity * 100),
sentenceCount: sentences.length,
variance: Math.round(variance)
}
};
}
module.exports = {
applySentenceVariation, // ← MAIN ENTRY POINT
varyTextStructure,
splitIntoSentences,
splitLongSentence,
mergeSentences,
analyzeSentencePatterns
};

View File

@ -0,0 +1,526 @@
// ========================================
// PATTERN BREAKING - TECHNIQUE 3: TRANSITION HUMANIZATION
// Responsabilité: Remplacer connecteurs mécaniques par transitions naturelles
// Anti-détection: Éviter patterns de liaison typiques des LLMs
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* DICTIONNAIRE CONNECTEURS HUMANISÉS
* Connecteurs LLM Alternatives naturelles par contexte
*/
const TRANSITION_REPLACEMENTS = {
// Connecteurs trop formels → versions naturelles
'par ailleurs': {
alternatives: ['d\'ailleurs', 'au fait', 'soit dit en passant', 'à propos', 'sinon'],
weight: 0.8,
contexts: ['casual', 'conversational']
},
'en effet': {
alternatives: ['effectivement', 'c\'est vrai', 'tout à fait', 'absolument', 'exactement'],
weight: 0.9,
contexts: ['confirmative', 'agreement']
},
'de plus': {
alternatives: ['aussi', 'également', 'qui plus est', 'en plus', 'et puis'],
weight: 0.7,
contexts: ['additive', 'continuation']
},
'cependant': {
alternatives: ['mais', 'pourtant', 'néanmoins', 'malgré tout', 'quand même'],
weight: 0.6,
contexts: ['contrast', 'opposition']
},
'ainsi': {
alternatives: ['donc', 'du coup', 'comme ça', 'par conséquent', 'résultat'],
weight: 0.8,
contexts: ['consequence', 'result']
},
'donc': {
alternatives: ['du coup', 'alors', 'par conséquent', 'ainsi', 'résultat'],
weight: 0.5,
contexts: ['consequence', 'logical']
},
// Connecteurs de séquence
'ensuite': {
alternatives: ['puis', 'après', 'et puis', 'alors', 'du coup'],
weight: 0.6,
contexts: ['sequence', 'temporal']
},
'puis': {
alternatives: ['ensuite', 'après', 'et puis', 'alors'],
weight: 0.4,
contexts: ['sequence', 'temporal']
},
// Connecteurs d'emphase
'également': {
alternatives: ['aussi', 'de même', 'pareillement', 'en plus'],
weight: 0.6,
contexts: ['similarity', 'addition']
},
'aussi': {
alternatives: ['également', 'de même', 'en plus', 'pareillement'],
weight: 0.3,
contexts: ['similarity', 'addition']
},
// Connecteurs de conclusion
'enfin': {
alternatives: ['finalement', 'au final', 'pour finir', 'en dernier'],
weight: 0.5,
contexts: ['conclusion', 'final']
},
'finalement': {
alternatives: ['au final', 'en fin de compte', 'pour finir', 'enfin'],
weight: 0.4,
contexts: ['conclusion', 'final']
}
};
/**
* PATTERNS DE TRANSITION NATURELLE
* Selon le style de personnalité
*/
const PERSONALITY_TRANSITIONS = {
'décontracté': {
preferred: ['du coup', 'alors', 'bon', 'après', 'sinon'],
avoided: ['par conséquent', 'néanmoins', 'toutefois']
},
'technique': {
preferred: ['donc', 'ainsi', 'par conséquent', 'résultat'],
avoided: ['du coup', 'bon', 'franchement']
},
'commercial': {
preferred: ['aussi', 'de plus', 'également', 'qui plus est'],
avoided: ['du coup', 'bon', 'franchement']
},
'familier': {
preferred: ['du coup', 'bon', 'alors', 'après', 'franchement'],
avoided: ['par conséquent', 'néanmoins', 'de surcroît']
}
};
/**
* MAIN ENTRY POINT - HUMANISATION TRANSITIONS
* @param {Object} input - { content: {}, config: {}, context: {} }
* @returns {Object} - { content: {}, stats: {}, debug: {} }
*/
async function humanizeTransitions(input) {
return await tracer.run('TransitionHumanization.humanizeTransitions()', async () => {
const { content, config = {}, context = {} } = input;
const {
intensity = 0.6, // Probabilité de remplacement (60%)
personalityStyle = null, // Style de personnalité pour guidage
avoidRepetition = true, // Éviter répétitions excessives
preserveFormal = false, // Préserver style formel
csvData = null // Données pour personnalité
} = config;
await tracer.annotate({
technique: 'transition_humanization',
intensity,
personalityStyle: personalityStyle || csvData?.personality?.style,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔗 TECHNIQUE 3/3: Humanisation transitions (intensité: ${intensity})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments à humaniser`, 'DEBUG');
try {
const results = {};
let totalProcessed = 0;
let totalReplacements = 0;
let humanizationDetails = [];
// Extraire style de personnalité
const effectivePersonalityStyle = personalityStyle || csvData?.personality?.style || 'neutral';
// Analyser patterns globaux pour éviter répétitions
const globalPatterns = analyzeGlobalTransitionPatterns(content);
// Traiter chaque élément de contenu
for (const [tag, text] of Object.entries(content)) {
totalProcessed++;
if (text.length < 30) {
results[tag] = text;
continue;
}
// Appliquer humanisation des transitions
const humanizationResult = humanizeTextTransitions(text, {
intensity,
personalityStyle: effectivePersonalityStyle,
avoidRepetition,
preserveFormal,
globalPatterns,
tag
});
results[tag] = humanizationResult.text;
if (humanizationResult.replacements.length > 0) {
totalReplacements += humanizationResult.replacements.length;
humanizationDetails.push({
tag,
replacements: humanizationResult.replacements,
transitionsDetected: humanizationResult.transitionsFound
});
logSh(` 🔄 [${tag}]: ${humanizationResult.replacements.length} transitions humanisées`, 'DEBUG');
} else {
logSh(` ➡️ [${tag}]: Transitions déjà naturelles`, 'DEBUG');
}
}
const duration = Date.now() - startTime;
const stats = {
processed: totalProcessed,
totalReplacements,
avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100,
elementsWithTransitions: humanizationDetails.length,
personalityStyle: effectivePersonalityStyle,
duration,
technique: 'transition_humanization'
};
logSh(`✅ HUMANISATION TRANSITIONS: ${stats.totalReplacements} remplacements sur ${stats.elementsWithTransitions}/${stats.processed} éléments en ${duration}ms`, 'INFO');
await tracer.event('Transition humanization terminée', stats);
return {
content: results,
stats,
debug: {
technique: 'transition_humanization',
config: { intensity, personalityStyle: effectivePersonalityStyle, avoidRepetition },
humanizations: humanizationDetails,
globalPatterns
}
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ HUMANISATION TRANSITIONS échouée après ${duration}ms: ${error.message}`, 'ERROR');
throw new Error(`TransitionHumanization failed: ${error.message}`);
}
}, input);
}
/**
* Humaniser les transitions d'un texte
*/
function humanizeTextTransitions(text, config) {
const { intensity, personalityStyle, avoidRepetition, preserveFormal, globalPatterns, tag } = config;
let humanizedText = text;
const replacements = [];
const transitionsFound = [];
// Statistiques usage pour éviter répétitions
const usageStats = {};
// Traiter chaque connecteur du dictionnaire
for (const [transition, transitionData] of Object.entries(TRANSITION_REPLACEMENTS)) {
const { alternatives, weight, contexts } = transitionData;
// Rechercher occurrences (insensible à la casse, mais préserver limites mots)
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = [...text.matchAll(regex)];
if (matches.length > 0) {
transitionsFound.push(transition);
// Décider si on remplace selon intensité et poids
const shouldReplace = Math.random() < (intensity * weight);
if (shouldReplace && !preserveFormal) {
// Sélectionner meilleure alternative
const selectedAlternative = selectBestTransitionAlternative(
alternatives,
personalityStyle,
usageStats,
avoidRepetition
);
// Appliquer remplacement en préservant la casse
humanizedText = humanizedText.replace(regex, (match) => {
return preserveCase(match, selectedAlternative);
});
// Enregistrer usage
usageStats[selectedAlternative] = (usageStats[selectedAlternative] || 0) + matches.length;
replacements.push({
original: transition,
replacement: selectedAlternative,
occurrences: matches.length,
contexts,
personalityMatch: isPersonalityAppropriate(selectedAlternative, personalityStyle)
});
}
}
}
// Post-processing : éviter accumulations
if (avoidRepetition) {
const repetitionCleaned = reduceTransitionRepetition(humanizedText, usageStats);
humanizedText = repetitionCleaned.text;
replacements.push(...repetitionCleaned.additionalChanges);
}
return {
text: humanizedText,
replacements,
transitionsFound
};
}
/**
* Sélectionner meilleure alternative de transition
*/
function selectBestTransitionAlternative(alternatives, personalityStyle, usageStats, avoidRepetition) {
// Filtrer selon personnalité
const personalityFiltered = alternatives.filter(alt =>
isPersonalityAppropriate(alt, personalityStyle)
);
const candidateList = personalityFiltered.length > 0 ? personalityFiltered : alternatives;
if (!avoidRepetition) {
return candidateList[Math.floor(Math.random() * candidateList.length)];
}
// Éviter les alternatives déjà trop utilisées
const lessUsedAlternatives = candidateList.filter(alt =>
(usageStats[alt] || 0) < 2
);
const finalList = lessUsedAlternatives.length > 0 ? lessUsedAlternatives : candidateList;
return finalList[Math.floor(Math.random() * finalList.length)];
}
/**
* Vérifier si alternative appropriée pour personnalité
*/
function isPersonalityAppropriate(alternative, personalityStyle) {
if (!personalityStyle || personalityStyle === 'neutral') return true;
const styleMapping = {
'décontracté': PERSONALITY_TRANSITIONS.décontracté,
'technique': PERSONALITY_TRANSITIONS.technique,
'commercial': PERSONALITY_TRANSITIONS.commercial,
'familier': PERSONALITY_TRANSITIONS.familier
};
const styleConfig = styleMapping[personalityStyle.toLowerCase()];
if (!styleConfig) return true;
// Éviter les connecteurs inappropriés
if (styleConfig.avoided.includes(alternative)) return false;
// Privilégier les connecteurs préférés
if (styleConfig.preferred.includes(alternative)) return true;
return true;
}
/**
* Réduire répétitions excessives de transitions
*/
function reduceTransitionRepetition(text, usageStats) {
let processedText = text;
const additionalChanges = [];
// Identifier connecteurs surutilisés (>3 fois)
const overusedTransitions = Object.entries(usageStats)
.filter(([transition, count]) => count > 3)
.map(([transition]) => transition);
for (const overusedTransition of overusedTransitions) {
// Remplacer quelques occurrences par des alternatives
const regex = new RegExp(`\\b${escapeRegex(overusedTransition)}\\b`, 'g');
let replacements = 0;
processedText = processedText.replace(regex, (match, offset) => {
// Remplacer 1 occurrence sur 3 environ
if (Math.random() < 0.33 && replacements < 2) {
replacements++;
const alternatives = findAlternativesFor(overusedTransition);
const alternative = alternatives[Math.floor(Math.random() * alternatives.length)];
additionalChanges.push({
type: 'repetition_reduction',
original: overusedTransition,
replacement: alternative,
reason: 'overuse'
});
return preserveCase(match, alternative);
}
return match;
});
}
return { text: processedText, additionalChanges };
}
/**
* Trouver alternatives pour un connecteur donné
*/
function findAlternativesFor(transition) {
// Chercher dans le dictionnaire
for (const [key, data] of Object.entries(TRANSITION_REPLACEMENTS)) {
if (data.alternatives.includes(transition)) {
return data.alternatives.filter(alt => alt !== transition);
}
}
// Alternatives génériques
const genericAlternatives = {
'du coup': ['alors', 'donc', 'ainsi'],
'alors': ['du coup', 'donc', 'ensuite'],
'donc': ['du coup', 'alors', 'ainsi'],
'aussi': ['également', 'de plus', 'en plus'],
'mais': ['cependant', 'pourtant', 'néanmoins']
};
return genericAlternatives[transition] || ['donc', 'alors'];
}
/**
* Analyser patterns globaux de transitions
*/
function analyzeGlobalTransitionPatterns(content) {
const allText = Object.values(content).join(' ');
const transitionCounts = {};
const repetitionPatterns = [];
// Compter occurrences globales
for (const transition of Object.keys(TRANSITION_REPLACEMENTS)) {
const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi');
const matches = allText.match(regex);
if (matches) {
transitionCounts[transition] = matches.length;
}
}
// Identifier patterns de répétition problématiques
const sortedTransitions = Object.entries(transitionCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5); // Top 5 plus utilisées
sortedTransitions.forEach(([transition, count]) => {
if (count > 5) {
repetitionPatterns.push({
transition,
count,
severity: count > 10 ? 'high' : count > 7 ? 'medium' : 'low'
});
}
});
return {
transitionCounts,
repetitionPatterns,
diversityScore: Object.keys(transitionCounts).length / Math.max(1, Object.values(transitionCounts).reduce((a,b) => a+b, 0))
};
}
/**
* Préserver la casse originale
*/
function preserveCase(original, replacement) {
if (original === original.toUpperCase()) {
return replacement.toUpperCase();
} else if (original[0] === original[0].toUpperCase()) {
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
} else {
return replacement.toLowerCase();
}
}
/**
* Échapper caractères regex
*/
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Analyser qualité des transitions d'un texte
*/
function analyzeTransitionQuality(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 5);
if (sentences.length < 2) {
return { score: 100, issues: [], naturalness: 'high' };
}
let mechanicalTransitions = 0;
let totalTransitions = 0;
const issues = [];
// Analyser chaque transition
sentences.forEach((sentence, index) => {
if (index === 0) return;
const trimmed = sentence.trim();
const startsWithTransition = Object.keys(TRANSITION_REPLACEMENTS).some(transition =>
trimmed.toLowerCase().startsWith(transition.toLowerCase())
);
if (startsWithTransition) {
totalTransitions++;
// Vérifier si transition mécanique
const transition = Object.keys(TRANSITION_REPLACEMENTS).find(t =>
trimmed.toLowerCase().startsWith(t.toLowerCase())
);
if (transition && TRANSITION_REPLACEMENTS[transition].weight > 0.7) {
mechanicalTransitions++;
issues.push({
type: 'mechanical_transition',
transition,
suggestion: TRANSITION_REPLACEMENTS[transition].alternatives[0]
});
}
}
});
const mechanicalRatio = totalTransitions > 0 ? mechanicalTransitions / totalTransitions : 0;
const score = Math.max(0, 100 - (mechanicalRatio * 100));
let naturalness = 'high';
if (mechanicalRatio > 0.5) naturalness = 'low';
else if (mechanicalRatio > 0.25) naturalness = 'medium';
return { score: Math.round(score), issues, naturalness, mechanicalRatio };
}
module.exports = {
humanizeTransitions, // ← MAIN ENTRY POINT
humanizeTextTransitions,
analyzeTransitionQuality,
analyzeGlobalTransitionPatterns,
TRANSITION_REPLACEMENTS,
PERSONALITY_TRANSITIONS
};

View File

@ -0,0 +1,422 @@
// ========================================
// SELECTIVE CORE - MOTEUR MODULAIRE
// Responsabilité: Moteur selective enhancement réutilisable sur tout contenu
// Architecture: Couches applicables à la demande
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
/**
* MAIN ENTRY POINT - APPLICATION COUCHE SELECTIVE ENHANCEMENT
* Input: contenu existant + configuration selective
* Output: contenu avec couche selective appliquée
*/
async function applySelectiveLayer(existingContent, config = {}) {
return await tracer.run('SelectiveCore.applySelectiveLayer()', async () => {
const {
layerType = 'technical', // 'technical' | 'transitions' | 'style' | 'all'
llmProvider = 'auto', // 'claude' | 'gpt4' | 'gemini' | 'mistral' | 'auto'
analysisMode = true, // Analyser avant d'appliquer
preserveStructure = true,
csvData = null,
context = {}
} = config;
await tracer.annotate({
selectiveLayer: true,
layerType,
llmProvider,
analysisMode,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🔧 APPLICATION COUCHE SELECTIVE: ${layerType} (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Mode: ${analysisMode ? 'analysé' : 'direct'}`, 'INFO');
try {
let enhancedContent = {};
let layerStats = {};
// Sélection automatique du LLM si 'auto'
const selectedLLM = selectOptimalLLM(layerType, llmProvider);
// Application selon type de couche
switch (layerType) {
case 'technical':
const technicalResult = await applyTechnicalEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = technicalResult.content;
layerStats = technicalResult.stats;
break;
case 'transitions':
const transitionResult = await applyTransitionEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = transitionResult.content;
layerStats = transitionResult.stats;
break;
case 'style':
const styleResult = await applyStyleEnhancement(existingContent, { ...config, llmProvider: selectedLLM });
enhancedContent = styleResult.content;
layerStats = styleResult.stats;
break;
case 'all':
const allResult = await applyAllSelectiveLayers(existingContent, config);
enhancedContent = allResult.content;
layerStats = allResult.stats;
break;
default:
throw new Error(`Type de couche selective inconnue: ${layerType}`);
}
const duration = Date.now() - startTime;
const stats = {
layerType,
llmProvider: selectedLLM,
elementsProcessed: Object.keys(existingContent).length,
elementsEnhanced: countEnhancedElements(existingContent, enhancedContent),
duration,
...layerStats
};
logSh(`✅ COUCHE SELECTIVE APPLIQUÉE: ${stats.elementsEnhanced}/${stats.elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Couche selective appliquée', stats);
return {
content: enhancedContent,
stats,
original: existingContent,
config: { ...config, llmProvider: selectedLLM }
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHE SELECTIVE É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 });
}
/**
* APPLICATION TECHNIQUE MODULAIRE
*/
async function applyTechnicalEnhancement(content, config = {}) {
const { TechnicalLayer } = require('./TechnicalLayer');
const layer = new TechnicalLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TRANSITIONS MODULAIRE
*/
async function applyTransitionEnhancement(content, config = {}) {
const { TransitionLayer } = require('./TransitionLayer');
const layer = new TransitionLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION STYLE MODULAIRE
*/
async function applyStyleEnhancement(content, config = {}) {
const { StyleLayer } = require('./StyleLayer');
const layer = new StyleLayer();
return await layer.apply(content, config);
}
/**
* APPLICATION TOUTES COUCHES SÉQUENTIELLES
*/
async function applyAllSelectiveLayers(content, config = {}) {
logSh(`🔄 Application séquentielle toutes couches selective`, 'DEBUG');
let currentContent = content;
const allStats = {
steps: [],
totalDuration: 0,
totalEnhancements: 0
};
const steps = [
{ name: 'technical', llm: 'gpt4' },
{ name: 'transitions', llm: 'gemini' },
{ name: 'style', llm: 'mistral' }
];
for (const step of steps) {
try {
logSh(` 🔧 Étape: ${step.name} (${step.llm})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
layerType: step.name,
llmProvider: step.llm
});
currentContent = stepResult.content;
allStats.steps.push({
name: step.name,
llm: step.llm,
...stepResult.stats
});
allStats.totalDuration += stepResult.stats.duration;
allStats.totalEnhancements += stepResult.stats.elementsEnhanced;
} catch (error) {
logSh(` ❌ Étape ${step.name} échouée: ${error.message}`, 'ERROR');
allStats.steps.push({
name: step.name,
llm: step.llm,
error: error.message,
duration: 0,
elementsEnhanced: 0
});
}
}
return {
content: currentContent,
stats: allStats
};
}
/**
* ANALYSE BESOIN D'ENHANCEMENT
*/
async function analyzeEnhancementNeeds(content, config = {}) {
logSh(`🔍 Analyse besoins selective enhancement`, 'DEBUG');
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] },
recommendation: 'none'
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (termes techniques manquants)
const technicalNeed = assessTechnicalNeed(text, config.csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score, reason: technicalNeed.reason });
}
// Analyse transitions (fluidité)
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.4) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score, reason: transitionNeed.reason });
}
// Analyse style (personnalité)
const styleNeed = assessStyleNeed(text, config.csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score, reason: styleNeed.reason });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
// Recommandation
const scores = [
{ type: 'technical', score: analysis.technical.score },
{ type: 'transitions', score: analysis.transitions.score },
{ type: 'style', score: analysis.style.score }
].sort((a, b) => b.score - a.score);
if (scores[0].score > 0.6) {
analysis.recommendation = scores[0].type;
} else if (scores[0].score > 0.4) {
analysis.recommendation = 'light_' + scores[0].type;
}
logSh(` 📊 Analyse: Tech=${analysis.technical.score.toFixed(2)} | Trans=${analysis.transitions.score.toFixed(2)} | Style=${analysis.style.score.toFixed(2)}`, 'DEBUG');
logSh(` 💡 Recommandation: ${analysis.recommendation}`, 'DEBUG');
return analysis;
}
// ============= HELPER FUNCTIONS =============
/**
* Sélectionner LLM optimal selon type de couche
*/
function selectOptimalLLM(layerType, llmProvider) {
if (llmProvider !== 'auto') return llmProvider;
const optimalMapping = {
'technical': 'gpt4', // GPT-4 excellent pour précision technique
'transitions': 'gemini', // Gemini bon pour fluidité
'style': 'mistral', // Mistral excellent pour style personnalité
'all': 'claude' // Claude polyvalent pour tout
};
return optimalMapping[layerType] || 'claude';
}
/**
* Compter éléments améliorés
*/
function countEnhancedElements(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
/**
* Évaluer besoin technique
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
let reason = [];
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure', 'découpe'];
const contentLower = content.toLowerCase();
const foundTerms = technicalTerms.filter(term => contentLower.includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
reason.push('manque_termes_techniques');
}
}
// Vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'qualité', 'offre'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) {
score += 0.3;
reason.push('vocabulaire_générique');
}
// Manque de précision dimensionnelle/technique
if (content.length > 50 && !(/\d+\s*(mm|cm|m|%|°)/.test(content))) {
score += 0.2;
reason.push('manque_précision_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin transitions
*/
function assessTransitionNeed(content) {
let score = 0;
let reason = [];
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0, reason: '' };
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant'];
let repetitiveConnectors = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitiveConnectors++;
});
if (repetitiveConnectors > 1) {
score += 0.4;
reason.push('connecteurs_répétitifs');
}
// Transitions abruptes (phrases sans connecteurs logiques)
let abruptTransitions = 0;
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].trim().toLowerCase();
const hasConnector = connectors.some(conn => sentence.startsWith(conn)) ||
/^(puis|ensuite|également|aussi|donc|ainsi)/.test(sentence);
if (!hasConnector && sentence.length > 30) {
abruptTransitions++;
}
}
if (abruptTransitions / sentences.length > 0.6) {
score += 0.3;
reason.push('transitions_abruptes');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
/**
* Évaluer besoin style
*/
function assessStyleNeed(content, personality) {
let score = 0;
let reason = [];
if (!personality) {
score += 0.2;
reason.push('pas_personnalité');
return { score, reason: reason.join(',') };
}
// Style générique (pas de personnalité visible)
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const contentLower = content.toLowerCase();
const personalityFound = personalityWords.some(word =>
word.trim() && contentLower.includes(word.trim())
);
if (!personalityFound && content.length > 50) {
score += 0.4;
reason.push('style_générique');
}
// Niveau technique inadapté
if (personality.niveauTechnique === 'accessible' && /\b(optimisation|implémentation|méthodologie)\b/i.test(content)) {
score += 0.3;
reason.push('trop_technique');
}
return { score: Math.min(1, score), reason: reason.join(',') };
}
module.exports = {
applySelectiveLayer, // ← MAIN ENTRY POINT MODULAIRE
applyTechnicalEnhancement,
applyTransitionEnhancement,
applyStyleEnhancement,
applyAllSelectiveLayers,
analyzeEnhancementNeeds,
selectOptimalLLM
};

View File

@ -0,0 +1,553 @@
// ========================================
// SELECTIVE LAYERS - COUCHES COMPOSABLES
// Responsabilité: Stacks prédéfinis et couches adaptatives pour selective enhancement
// Architecture: Composable layers avec orchestration intelligente
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applySelectiveLayer } = require('./SelectiveCore');
/**
* STACKS PRÉDÉFINIS SELECTIVE ENHANCEMENT
*/
const PREDEFINED_STACKS = {
// Stack léger - Amélioration technique uniquement
lightEnhancement: {
name: 'lightEnhancement',
description: 'Amélioration technique légère avec GPT-4',
layers: [
{ type: 'technical', llm: 'gpt4', intensity: 0.7 }
],
layersCount: 1
},
// Stack standard - Technique + Transitions
standardEnhancement: {
name: 'standardEnhancement',
description: 'Amélioration technique et fluidité (GPT-4 + Gemini)',
layers: [
{ type: 'technical', llm: 'gpt4', intensity: 0.9 },
{ type: 'transitions', llm: 'gemini', intensity: 0.8 }
],
layersCount: 2
},
// Stack complet - Toutes couches séquentielles
fullEnhancement: {
name: 'fullEnhancement',
description: 'Enhancement complet multi-LLM (GPT-4 + Gemini + Mistral)',
layers: [
{ type: 'technical', llm: 'gpt4', intensity: 1.0 },
{ type: 'transitions', llm: 'gemini', intensity: 0.9 },
{ type: 'style', llm: 'mistral', intensity: 0.8 }
],
layersCount: 3
},
// Stack personnalité - Style prioritaire
personalityFocus: {
name: 'personalityFocus',
description: 'Focus personnalité et style avec Mistral + technique légère',
layers: [
{ type: 'style', llm: 'mistral', intensity: 1.2 },
{ type: 'technical', llm: 'gpt4', intensity: 0.6 }
],
layersCount: 2
},
// Stack fluidité - Transitions prioritaires
fluidityFocus: {
name: 'fluidityFocus',
description: 'Focus fluidité avec Gemini + enhancements légers',
layers: [
{ type: 'transitions', llm: 'gemini', intensity: 1.1 },
{ type: 'technical', llm: 'gpt4', intensity: 0.7 },
{ type: 'style', llm: 'mistral', intensity: 0.6 }
],
layersCount: 3
}
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, config = {}) {
return await tracer.run('SelectiveLayers.applyPredefinedStack()', async () => {
const stack = PREDEFINED_STACKS[stackName];
if (!stack) {
throw new Error(`Stack selective prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_STACKS).join(', ')}`);
}
await tracer.annotate({
selectivePredefinedStack: true,
stackName,
layersCount: stack.layersCount,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📦 APPLICATION STACK SELECTIVE: ${stack.name} (${stack.layersCount} couches)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Description: ${stack.description}`, 'INFO');
try {
let currentContent = content;
const stackStats = {
stackName,
layers: [],
totalModifications: 0,
totalDuration: 0,
success: true
};
// Appliquer chaque couche séquentiellement
for (let i = 0; i < stack.layers.length; i++) {
const layer = stack.layers[i];
try {
logSh(` 🔧 Couche ${i + 1}/${stack.layersCount}: ${layer.type} (${layer.llm})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
stackStats.totalModifications += layerResult.stats.elementsEnhanced;
stackStats.totalDuration += layerResult.stats.duration;
logSh(` ✅ Couche ${layer.type}: ${layerResult.stats.elementsEnhanced} améliorations`, 'DEBUG');
} catch (layerError) {
logSh(` ❌ Couche ${layer.type} échouée: ${layerError.message}`, 'ERROR');
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
error: layerError.message,
duration: 0,
success: false
});
// Continuer avec les autres couches
}
}
const duration = Date.now() - startTime;
const successfulLayers = stackStats.layers.filter(l => l.success).length;
logSh(`✅ STACK SELECTIVE ${stackName}: ${successfulLayers}/${stack.layersCount} couches | ${stackStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Stack selective appliqué', { ...stackStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...stackStats, totalDuration: duration },
original: content,
stackApplied: stackName
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STACK SELECTIVE ${stackName} ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { stackName, error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), stackName, config });
}
/**
* APPLIQUER COUCHES ADAPTATIVES
*/
async function applyAdaptiveLayers(content, config = {}) {
return await tracer.run('SelectiveLayers.applyAdaptiveLayers()', async () => {
const {
maxIntensity = 1.0,
analysisThreshold = 0.4,
csvData = null
} = config;
await tracer.annotate({
selectiveAdaptiveLayers: true,
maxIntensity,
analysisThreshold,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🧠 APPLICATION COUCHES ADAPTATIVES SELECTIVE`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Seuil: ${analysisThreshold}`, 'INFO');
try {
// 1. Analyser besoins de chaque type de couche
const needsAnalysis = await analyzeSelectiveNeeds(content, csvData);
logSh(` 📋 Analyse besoins: Tech=${needsAnalysis.technical.score.toFixed(2)} | Trans=${needsAnalysis.transitions.score.toFixed(2)} | Style=${needsAnalysis.style.score.toFixed(2)}`, 'DEBUG');
// 2. Déterminer couches à appliquer selon scores
const layersToApply = [];
if (needsAnalysis.technical.needed && needsAnalysis.technical.score > analysisThreshold) {
layersToApply.push({
type: 'technical',
llm: 'gpt4',
intensity: Math.min(maxIntensity, needsAnalysis.technical.score * 1.2),
priority: 1
});
}
if (needsAnalysis.transitions.needed && needsAnalysis.transitions.score > analysisThreshold) {
layersToApply.push({
type: 'transitions',
llm: 'gemini',
intensity: Math.min(maxIntensity, needsAnalysis.transitions.score * 1.1),
priority: 2
});
}
if (needsAnalysis.style.needed && needsAnalysis.style.score > analysisThreshold) {
layersToApply.push({
type: 'style',
llm: 'mistral',
intensity: Math.min(maxIntensity, needsAnalysis.style.score),
priority: 3
});
}
if (layersToApply.length === 0) {
logSh(`✅ COUCHES ADAPTATIVES: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
adaptive: true,
layersApplied: 0,
analysisOnly: true,
duration: Date.now() - startTime
}
};
}
// 3. Appliquer couches par ordre de priorité
layersToApply.sort((a, b) => a.priority - b.priority);
logSh(` 🎯 Couches sélectionnées: ${layersToApply.map(l => `${l.type}(${l.intensity.toFixed(1)})`).join(' → ')}`, 'INFO');
let currentContent = content;
const adaptiveStats = {
layersAnalyzed: 3,
layersApplied: layersToApply.length,
layers: [],
totalModifications: 0,
adaptive: true
};
for (const layer of layersToApply) {
try {
logSh(` 🔧 Couche adaptative: ${layer.type} (intensité: ${layer.intensity.toFixed(1)})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
adaptiveStats.layers.push({
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
adaptiveStats.totalModifications += layerResult.stats.elementsEnhanced;
} catch (layerError) {
logSh(` ❌ Couche adaptative ${layer.type} échouée: ${layerError.message}`, 'ERROR');
adaptiveStats.layers.push({
type: layer.type,
error: layerError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulLayers = adaptiveStats.layers.filter(l => l.success).length;
logSh(`✅ COUCHES ADAPTATIVES: ${successfulLayers}/${layersToApply.length} appliquées | ${adaptiveStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Couches adaptatives appliquées', { ...adaptiveStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...adaptiveStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHES ADAPTATIVES ÉCHOUÉES après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { adaptive: true, error: error.message, duration },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), config });
}
/**
* PIPELINE COUCHES PERSONNALISÉ
*/
async function applyLayerPipeline(content, layerSequence, config = {}) {
return await tracer.run('SelectiveLayers.applyLayerPipeline()', async () => {
if (!Array.isArray(layerSequence) || layerSequence.length === 0) {
throw new Error('Séquence de couches invalide ou vide');
}
await tracer.annotate({
selectiveLayerPipeline: true,
pipelineLength: layerSequence.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES SELECTIVE PERSONNALISÉ: ${layerSequence.length} étapes`, 'INFO');
try {
let currentContent = content;
const pipelineStats = {
pipelineLength: layerSequence.length,
steps: [],
totalModifications: 0,
success: true
};
for (let i = 0; i < layerSequence.length; i++) {
const step = layerSequence[i];
try {
logSh(` 📍 Étape ${i + 1}/${layerSequence.length}: ${step.type} (${step.llm || 'auto'})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
...step
});
currentContent = stepResult.content;
pipelineStats.steps.push({
order: i + 1,
...step,
elementsEnhanced: stepResult.stats.elementsEnhanced,
duration: stepResult.stats.duration,
success: !stepResult.stats.fallback
});
pipelineStats.totalModifications += stepResult.stats.elementsEnhanced;
} catch (stepError) {
logSh(` ❌ Étape ${i + 1} échouée: ${stepError.message}`, 'ERROR');
pipelineStats.steps.push({
order: i + 1,
...step,
error: stepError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulSteps = pipelineStats.steps.filter(s => s.success).length;
logSh(`✅ PIPELINE SELECTIVE: ${successfulSteps}/${layerSequence.length} étapes | ${pipelineStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Pipeline selective appliqué', { ...pipelineStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...pipelineStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE SELECTIVE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), layerSequence, config });
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser besoins selective enhancement
*/
async function analyzeSelectiveNeeds(content, csvData) {
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] }
};
// Analyser chaque élément pour tous types de besoins
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (import depuis SelectiveCore logic)
const technicalNeed = assessTechnicalNeed(text, csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score });
}
// Analyse transitions
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.3) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score });
}
// Analyse style
const styleNeed = assessStyleNeed(text, csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
return analysis;
}
/**
* Évaluer besoin technique (simplifié de SelectiveCore)
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure'];
const foundTerms = technicalTerms.filter(term => content.toLowerCase().includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
}
}
// Vocabulaire générique
const genericWords = ['produit', 'solution', 'service', 'qualité'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) score += 0.3;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin transitions (simplifié)
*/
function assessTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0 };
let score = 0;
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus'];
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitions++;
});
if (repetitions > 1) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin style (simplifié)
*/
function assessStyleNeed(content, personality) {
let score = 0;
if (!personality) {
score += 0.2;
return { score };
}
// Style générique
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const personalityFound = personalityWords.some(word =>
word.trim() && content.toLowerCase().includes(word.trim())
);
if (!personalityFound && content.length > 50) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Obtenir stacks disponibles
*/
function getAvailableStacks() {
return Object.values(PREDEFINED_STACKS);
}
module.exports = {
// Main functions
applyPredefinedStack,
applyAdaptiveLayers,
applyLayerPipeline,
// Utils
getAvailableStacks,
analyzeSelectiveNeeds,
// Constants
PREDEFINED_STACKS
};

View File

@ -0,0 +1,579 @@
// ========================================
// SELECTIVE UTILS - UTILITAIRES MODULAIRES
// Responsabilité: Fonctions utilitaires partagées par tous les modules selective
// Architecture: Helper functions réutilisables et composables
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* ANALYSEURS DE CONTENU SELECTIVE
*/
/**
* Analyser qualité technique d'un contenu
*/
function analyzeTechnicalQuality(content, contextualTerms = []) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
technicalTermsFound: 0,
technicalTermsExpected: contextualTerms.length,
genericWordsCount: 0,
hasSpecifications: false,
hasDimensions: false,
contextIntegration: 0
}
};
const lowerContent = content.toLowerCase();
// 1. Compter termes techniques présents
contextualTerms.forEach(term => {
if (lowerContent.includes(term.toLowerCase())) {
analysis.details.technicalTermsFound++;
}
});
// 2. Détecter mots génériques
const genericWords = ['produit', 'solution', 'service', 'offre', 'article', 'élément'];
analysis.details.genericWordsCount = genericWords.filter(word =>
lowerContent.includes(word)
).length;
// 3. Vérifier spécifications techniques
analysis.details.hasSpecifications = /\b(norme|iso|din|ce)\b/i.test(content);
// 4. Vérifier dimensions/données techniques
analysis.details.hasDimensions = /\d+\s*(mm|cm|m|%|°|kg|g)\b/i.test(content);
// 5. Calculer score global (0-100)
const termRatio = contextualTerms.length > 0 ?
(analysis.details.technicalTermsFound / contextualTerms.length) * 40 : 20;
const genericPenalty = Math.min(20, analysis.details.genericWordsCount * 5);
const specificationBonus = analysis.details.hasSpecifications ? 15 : 0;
const dimensionBonus = analysis.details.hasDimensions ? 15 : 0;
const lengthBonus = content.length > 100 ? 10 : 0;
analysis.score = Math.max(0, Math.min(100,
termRatio + specificationBonus + dimensionBonus + lengthBonus - genericPenalty
));
return analysis;
}
/**
* Analyser fluidité des transitions
*/
function analyzeTransitionFluidity(content) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const sentences = content.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 5);
if (sentences.length < 2) {
return { score: 100, details: { reason: 'Contenu trop court pour analyse transitions' } };
}
const analysis = {
score: 0,
details: {
sentencesCount: sentences.length,
connectorsFound: 0,
repetitiveConnectors: 0,
abruptTransitions: 0,
averageSentenceLength: 0,
lengthVariation: 0
}
};
// 1. Analyser connecteurs
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite'];
const connectorCounts = {};
commonConnectors.forEach(connector => {
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
connectorCounts[connector] = matches.length;
analysis.details.connectorsFound += matches.length;
if (matches.length > 1) analysis.details.repetitiveConnectors++;
});
// 2. Détecter transitions abruptes
for (let i = 1; i < sentences.length; i++) {
const sentence = sentences[i].toLowerCase().trim();
const hasConnector = commonConnectors.some(connector =>
sentence.startsWith(connector) || sentence.includes(` ${connector} `)
);
if (!hasConnector && sentence.length > 20) {
analysis.details.abruptTransitions++;
}
}
// 3. Analyser variation de longueur
const lengths = sentences.map(s => s.split(/\s+/).length);
analysis.details.averageSentenceLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) =>
acc + Math.pow(len - analysis.details.averageSentenceLength, 2), 0
) / lengths.length;
analysis.details.lengthVariation = Math.sqrt(variance);
// 4. Calculer score fluidité (0-100)
const connectorScore = Math.min(30, (analysis.details.connectorsFound / sentences.length) * 100);
const repetitionPenalty = Math.min(20, analysis.details.repetitiveConnectors * 5);
const abruptPenalty = Math.min(30, (analysis.details.abruptTransitions / sentences.length) * 50);
const variationScore = Math.min(20, analysis.details.lengthVariation * 2);
analysis.score = Math.max(0, Math.min(100,
connectorScore + variationScore - repetitionPenalty - abruptPenalty + 50
));
return analysis;
}
/**
* Analyser cohérence de style
*/
function analyzeStyleConsistency(content, expectedPersonality = null) {
if (!content || typeof content !== 'string') return { score: 0, details: {} };
const analysis = {
score: 0,
details: {
personalityAlignment: 0,
toneConsistency: 0,
vocabularyLevel: 'standard',
formalityScore: 0,
personalityWordsFound: 0
}
};
// 1. Analyser alignement personnalité
if (expectedPersonality && expectedPersonality.vocabulairePref) {
const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(',');
const contentLower = content.toLowerCase();
personalityWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
analysis.details.personalityWordsFound++;
}
});
analysis.details.personalityAlignment = personalityWords.length > 0 ?
(analysis.details.personalityWordsFound / personalityWords.length) * 100 : 0;
}
// 2. Analyser niveau vocabulaire
const technicalWords = content.match(/\b\w{8,}\b/g) || [];
const totalWords = content.split(/\s+/).length;
const techRatio = technicalWords.length / totalWords;
if (techRatio > 0.15) analysis.details.vocabularyLevel = 'expert';
else if (techRatio < 0.05) analysis.details.vocabularyLevel = 'accessible';
else analysis.details.vocabularyLevel = 'standard';
// 3. Analyser formalité
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois'];
const casualIndicators = ['du coup', 'sympa', 'cool', 'nickel'];
let formalCount = formalIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
let casualCount = casualIndicators.filter(indicator =>
content.toLowerCase().includes(indicator)
).length;
analysis.details.formalityScore = formalCount - casualCount; // Positif = formel, négatif = casual
// 4. Calculer score cohérence (0-100)
let baseScore = 50;
if (expectedPersonality) {
baseScore += analysis.details.personalityAlignment * 0.3;
// Ajustements selon niveau technique attendu
const expectedLevel = expectedPersonality.niveauTechnique || 'standard';
if (expectedLevel === analysis.details.vocabularyLevel) {
baseScore += 20;
} else {
baseScore -= 10;
}
}
// Bonus cohérence tonale
const sentences = content.split(/[.!?]+/).filter(s => s.length > 10);
if (sentences.length > 1) {
baseScore += Math.min(20, analysis.details.lengthVariation || 10);
}
analysis.score = Math.max(0, Math.min(100, baseScore));
return analysis;
}
/**
* COMPARATEURS ET MÉTRIQUES
*/
/**
* Comparer deux contenus et calculer taux amélioration
*/
function compareContentImprovement(original, enhanced, analysisType = 'general') {
if (!original || !enhanced) return { improvementRate: 0, details: {} };
const comparison = {
improvementRate: 0,
details: {
lengthChange: ((enhanced.length - original.length) / original.length) * 100,
wordCountChange: 0,
structuralChanges: 0,
contentPreserved: true
}
};
// 1. Analyser changements structurels
const originalSentences = original.split(/[.!?]+/).length;
const enhancedSentences = enhanced.split(/[.!?]+/).length;
comparison.details.structuralChanges = Math.abs(enhancedSentences - originalSentences);
// 2. Analyser changements de mots
const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2);
const enhancedWords = enhanced.toLowerCase().split(/\s+/).filter(w => w.length > 2);
comparison.details.wordCountChange = enhancedWords.length - originalWords.length;
// 3. Vérifier préservation du contenu principal
const originalKeyWords = originalWords.filter(w => w.length > 4);
const preservedWords = originalKeyWords.filter(w => enhanced.toLowerCase().includes(w));
comparison.details.contentPreserved = (preservedWords.length / originalKeyWords.length) > 0.7;
// 4. Calculer taux amélioration selon type d'analyse
switch (analysisType) {
case 'technical':
const originalTech = analyzeTechnicalQuality(original);
const enhancedTech = analyzeTechnicalQuality(enhanced);
comparison.improvementRate = enhancedTech.score - originalTech.score;
break;
case 'transitions':
const originalFluid = analyzeTransitionFluidity(original);
const enhancedFluid = analyzeTransitionFluidity(enhanced);
comparison.improvementRate = enhancedFluid.score - originalFluid.score;
break;
case 'style':
const originalStyle = analyzeStyleConsistency(original);
const enhancedStyle = analyzeStyleConsistency(enhanced);
comparison.improvementRate = enhancedStyle.score - originalStyle.score;
break;
default:
// Amélioration générale (moyenne pondérée)
comparison.improvementRate = Math.min(50, Math.abs(comparison.details.lengthChange) * 0.1 +
(comparison.details.contentPreserved ? 20 : -20) +
Math.min(15, Math.abs(comparison.details.wordCountChange)));
}
return comparison;
}
/**
* UTILITAIRES DE CONTENU
*/
/**
* Nettoyer contenu généré par LLM
*/
function cleanGeneratedContent(content, cleaningLevel = 'standard') {
if (!content || typeof content !== 'string') return content;
let cleaned = content.trim();
// Nettoyage de base
cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(amélioré|modifié|réécrit)[:\s]*/gi, '');
cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(voici\s+)?/gi, '');
cleaned = cleaned.replace(/^(avec\s+les?\s+)?améliorations?\s*[:\s]*/gi, '');
// Nettoyage formatage
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1'); // Gras markdown → texte normal
cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples
cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation
if (cleaningLevel === 'intensive') {
// Nettoyage intensif
cleaned = cleaned.replace(/^\s*[-*+]\s*/gm, ''); // Puces en début de ligne
cleaned = cleaned.replace(/^(pour\s+)?(ce\s+)?(contenu\s*)?[,:]?\s*/gi, '');
cleaned = cleaned.replace(/\([^)]*\)/g, ''); // Parenthèses et contenu
}
// Nettoyage final
cleaned = cleaned.replace(/^[,.\s]+/, ''); // Début
cleaned = cleaned.replace(/[,\s]+$/, ''); // Fin
cleaned = cleaned.trim();
return cleaned;
}
/**
* Valider contenu selective
*/
function validateSelectiveContent(content, originalContent, criteria = {}) {
const validation = {
isValid: true,
score: 0,
issues: [],
suggestions: []
};
const {
minLength = 20,
maxLengthChange = 50, // % de changement maximum
preserveContent = true,
checkTechnicalTerms = true
} = criteria;
// 1. Vérifier longueur
if (!content || content.length < minLength) {
validation.isValid = false;
validation.issues.push('Contenu trop court');
validation.suggestions.push('Augmenter la longueur du contenu généré');
} else {
validation.score += 25;
}
// 2. Vérifier changements de longueur
if (originalContent) {
const lengthChange = Math.abs((content.length - originalContent.length) / originalContent.length) * 100;
if (lengthChange > maxLengthChange) {
validation.issues.push('Changement de longueur excessif');
validation.suggestions.push('Réduire l\'intensité d\'amélioration');
} else {
validation.score += 25;
}
// 3. Vérifier préservation du contenu
if (preserveContent) {
const preservation = compareContentImprovement(originalContent, content);
if (!preservation.details.contentPreserved) {
validation.isValid = false;
validation.issues.push('Contenu original non préservé');
validation.suggestions.push('Améliorer conservation du sens original');
} else {
validation.score += 25;
}
}
}
// 4. Vérifications spécifiques
if (checkTechnicalTerms) {
const technicalQuality = analyzeTechnicalQuality(content);
if (technicalQuality.score > 60) {
validation.score += 25;
} else if (technicalQuality.score < 30) {
validation.issues.push('Qualité technique insuffisante');
validation.suggestions.push('Ajouter plus de termes techniques spécialisés');
}
}
// Score final et validation
validation.score = Math.min(100, validation.score);
validation.isValid = validation.isValid && validation.score >= 60;
return validation;
}
/**
* UTILITAIRES TECHNIQUES
*/
/**
* Chunk array avec gestion intelligente
*/
function chunkArray(array, chunkSize, smartChunking = false) {
if (!Array.isArray(array)) return [];
if (array.length <= chunkSize) return [array];
const chunks = [];
if (smartChunking) {
// Chunking intelligent : éviter de séparer éléments liés
let currentChunk = [];
for (let i = 0; i < array.length; i++) {
currentChunk.push(array[i]);
// Conditions de fin de chunk intelligente
const isChunkFull = currentChunk.length >= chunkSize;
const isLastElement = i === array.length - 1;
const nextElementRelated = i < array.length - 1 &&
array[i].tag && array[i + 1].tag &&
array[i].tag.includes('FAQ') && array[i + 1].tag.includes('FAQ');
if ((isChunkFull && !nextElementRelated) || isLastElement) {
chunks.push([...currentChunk]);
currentChunk = [];
}
}
// Ajouter chunk restant si non vide
if (currentChunk.length > 0) {
if (chunks.length > 0 && chunks[chunks.length - 1].length + currentChunk.length <= chunkSize * 1.2) {
// Merger avec dernier chunk si pas trop gros
chunks[chunks.length - 1].push(...currentChunk);
} else {
chunks.push(currentChunk);
}
}
} else {
// Chunking standard
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
}
return chunks;
}
/**
* Sleep avec logging optionnel
*/
async function sleep(ms, logMessage = null) {
if (logMessage) {
logSh(`${logMessage} (${ms}ms)`, 'DEBUG');
}
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Mesurer performance d'opération
*/
function measurePerformance(operationName, startTime = Date.now()) {
const endTime = Date.now();
const duration = endTime - startTime;
const performance = {
operationName,
startTime,
endTime,
duration,
durationFormatted: formatDuration(duration)
};
return performance;
}
/**
* Formater durée en format lisible
*/
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}
/**
* STATISTIQUES ET RAPPORTS
*/
/**
* Générer rapport amélioration
*/
function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') {
const report = {
layerType,
timestamp: new Date().toISOString(),
summary: {
elementsProcessed: 0,
elementsImproved: 0,
averageImprovement: 0,
totalExecutionTime: 0
},
details: {
byElement: [],
qualityMetrics: {},
recommendations: []
}
};
// Analyser chaque élément
Object.keys(originalContent).forEach(tag => {
const original = originalContent[tag];
const enhanced = enhancedContent[tag];
if (original && enhanced) {
report.summary.elementsProcessed++;
const improvement = compareContentImprovement(original, enhanced, layerType);
if (improvement.improvementRate > 0) {
report.summary.elementsImproved++;
}
report.summary.averageImprovement += improvement.improvementRate;
report.details.byElement.push({
tag,
improvementRate: improvement.improvementRate,
lengthChange: improvement.details.lengthChange,
contentPreserved: improvement.details.contentPreserved
});
}
});
// Calculer moyennes
if (report.summary.elementsProcessed > 0) {
report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed;
}
// Métriques qualité globales
const fullOriginal = Object.values(originalContent).join(' ');
const fullEnhanced = Object.values(enhancedContent).join(' ');
report.details.qualityMetrics = {
technical: analyzeTechnicalQuality(fullEnhanced),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced)
};
// Recommandations
if (report.summary.averageImprovement < 10) {
report.details.recommendations.push('Augmenter l\'intensité d\'amélioration');
}
if (report.details.byElement.some(e => !e.contentPreserved)) {
report.details.recommendations.push('Améliorer préservation du contenu original');
}
return report;
}
module.exports = {
// Analyseurs
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
// Comparateurs
compareContentImprovement,
// Utilitaires contenu
cleanGeneratedContent,
validateSelectiveContent,
// Utilitaires techniques
chunkArray,
sleep,
measurePerformance,
formatDuration,
// Rapports
generateImprovementReport
};

View File

@ -0,0 +1,532 @@
// ========================================
// STYLE LAYER - COUCHE STYLE MODULAIRE
// Responsabilité: Adaptation personnalité modulaire réutilisable
// LLM: Mistral (excellence style et personnalité)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE STYLE MODULAIRE
*/
class StyleLayer {
constructor() {
this.name = 'StyleEnhancement';
this.defaultLLM = 'mistral';
this.priority = 3; // Priorité basse - appliqué en dernier
}
/**
* MAIN METHOD - Appliquer amélioration style
*/
async apply(content, config = {}) {
return await tracer.run('StyleLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetStyle = null // Style spécifique à appliquer
} = config;
await tracer.annotate({
styleLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
personality: csvData?.personality?.nom
});
const startTime = Date.now();
logSh(`🎨 STYLE LAYER: Amélioration personnalité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Style: ${csvData?.personality?.nom || 'standard'}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
// Vérifier présence personnalité
if (!csvData?.personality && !targetStyle) {
logSh(`⚠️ STYLE LAYER: Pas de personnalité définie, style générique appliqué`, 'WARNING');
}
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration style
const analysis = await this.analyzeStyleNeeds(content, csvData, targetStyle);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ STYLE LAYER: Style déjà cohérent`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceStyleElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments textuels
const textualElements = Object.entries(content)
.filter(([tag, text]) => text.length > 50 && !tag.includes('FAQ_Question'))
.map(([tag, text]) => ({ tag, content: text, styleIssues: ['adaptation_générale'] }));
if (textualElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceStyleElements(
textualElements,
csvData,
{ llmProvider, intensity, preserveStructure, targetStyle }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = textualElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity,
personalityApplied: csvData?.personality?.nom || targetStyle || 'générique'
};
logSh(`✅ STYLE LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} stylisés (${duration}ms)`, 'INFO');
await tracer.event('Style layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STYLE LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: style original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS STYLE
*/
async analyzeStyleNeeds(content, csvData, targetStyle = null) {
logSh(`🎨 Analyse besoins style`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
styleIssues: {
genericLanguage: 0,
personalityMismatch: 0,
inconsistentTone: 0,
missingVocabulary: 0
}
};
const personality = csvData?.personality;
const expectedStyle = targetStyle || personality;
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeStyleElement(text, expectedStyle, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
styleIssues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.styleIssues.hasOwnProperty(issue)) {
analysis.styleIssues[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global style: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🎭 Issues style: ${JSON.stringify(analysis.styleIssues)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS STYLE SÉLECTIONNÉS
*/
async enhanceStyleElements(candidates, csvData, config) {
logSh(`🎨 Amélioration ${candidates.length} éléments style`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 5); // Chunks optimisés pour Mistral
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk style ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createStyleEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.8, // Créativité élevée pour style
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseStyleResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk style ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1800);
}
} catch (error) {
logSh(` ❌ Chunk style ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément style individuel
*/
analyzeStyleElement(text, expectedStyle, csvData) {
let score = 0;
const issues = [];
const improvements = [];
// Si pas de style attendu, score faible
if (!expectedStyle) {
return { needsImprovement: false, score: 0.1, issues: ['pas_style_défini'], improvements: [] };
}
// 1. Analyser langage générique
const genericScore = this.analyzeGenericLanguage(text);
if (genericScore > 0.4) {
score += 0.3;
issues.push('genericLanguage');
improvements.push('personnaliser_vocabulaire');
}
// 2. Analyser adéquation personnalité
if (expectedStyle.vocabulairePref) {
const personalityScore = this.analyzePersonalityAlignment(text, expectedStyle);
if (personalityScore < 0.3) {
score += 0.4;
issues.push('personalityMismatch');
improvements.push('appliquer_style_personnalité');
}
}
// 3. Analyser cohérence de ton
const toneScore = this.analyzeToneConsistency(text, expectedStyle);
if (toneScore > 0.5) {
score += 0.2;
issues.push('inconsistentTone');
improvements.push('unifier_ton');
}
// 4. Analyser vocabulaire spécialisé
if (expectedStyle.niveauTechnique) {
const vocabScore = this.analyzeVocabularyLevel(text, expectedStyle);
if (vocabScore > 0.4) {
score += 0.1;
issues.push('missingVocabulary');
improvements.push('ajuster_niveau_vocabulaire');
}
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser langage générique
*/
analyzeGenericLanguage(text) {
const genericPhrases = [
'nos solutions', 'notre expertise', 'notre savoir-faire',
'nous vous proposons', 'nous mettons à votre disposition',
'qualité optimale', 'service de qualité', 'expertise reconnue'
];
let genericCount = 0;
genericPhrases.forEach(phrase => {
if (text.toLowerCase().includes(phrase)) genericCount++;
});
const wordCount = text.split(/\s+/).length;
return Math.min(1, (genericCount / Math.max(wordCount / 50, 1)));
}
/**
* Analyser alignement personnalité
*/
analyzePersonalityAlignment(text, personality) {
if (!personality.vocabulairePref) return 1;
const preferredWords = personality.vocabulairePref.toLowerCase().split(',');
const contentLower = text.toLowerCase();
let alignmentScore = 0;
preferredWords.forEach(word => {
if (word.trim() && contentLower.includes(word.trim())) {
alignmentScore++;
}
});
return Math.min(1, alignmentScore / Math.max(preferredWords.length, 1));
}
/**
* Analyser cohérence de ton
*/
analyzeToneConsistency(text, expectedStyle) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return 0;
const tones = sentences.map(sentence => this.detectSentenceTone(sentence));
const expectedTone = this.getExpectedTone(expectedStyle);
let inconsistencies = 0;
tones.forEach(tone => {
if (tone !== expectedTone && tone !== 'neutral') {
inconsistencies++;
}
});
return inconsistencies / tones.length;
}
/**
* Analyser niveau vocabulaire
*/
analyzeVocabularyLevel(text, expectedStyle) {
const technicalWords = text.match(/\b\w{8,}\b/g) || [];
const expectedLevel = expectedStyle.niveauTechnique || 'standard';
const techRatio = technicalWords.length / text.split(/\s+/).length;
switch (expectedLevel) {
case 'accessible':
return techRatio > 0.1 ? techRatio : 0; // Trop technique
case 'expert':
return techRatio < 0.05 ? 1 - techRatio : 0; // Pas assez technique
default:
return techRatio > 0.15 || techRatio < 0.02 ? Math.abs(0.08 - techRatio) : 0;
}
}
/**
* Détecter ton de phrase
*/
detectSentenceTone(sentence) {
const lowerSentence = sentence.toLowerCase();
if (/\b(excellent|remarquable|exceptionnel|parfait)\b/.test(lowerSentence)) return 'enthusiastic';
if (/\b(il convient|nous recommandons|il est conseillé)\b/.test(lowerSentence)) return 'formal';
if (/\b(sympa|cool|nickel|top)\b/.test(lowerSentence)) return 'casual';
if (/\b(technique|précision|spécification)\b/.test(lowerSentence)) return 'technical';
return 'neutral';
}
/**
* Obtenir ton attendu selon personnalité
*/
getExpectedTone(personality) {
if (!personality || !personality.style) return 'neutral';
const style = personality.style.toLowerCase();
if (style.includes('technique') || style.includes('expert')) return 'technical';
if (style.includes('commercial') || style.includes('vente')) return 'enthusiastic';
if (style.includes('décontracté') || style.includes('moderne')) return 'casual';
if (style.includes('professionnel') || style.includes('formel')) return 'formal';
return 'neutral';
}
/**
* Créer prompt amélioration style
*/
createStyleEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality || config.targetStyle;
let prompt = `MISSION: Adapte UNIQUEMENT le style et la personnalité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : 'STYLE: Professionnel standard'}
${personality?.description ? `DESCRIPTION: ${personality.description}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À STYLISER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.styleIssues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
PROFIL PERSONNALITÉ ${personality?.nom || 'Standard'}:
${personality ? `- Style: ${personality.style}
- Niveau: ${personality.niveauTechnique || 'standard'}
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel'}
- Connecteurs: ${personality.connecteursPref || 'variés'}
${personality.specificites ? `- Spécificités: ${personality.specificites}` : ''}` : '- Style professionnel web standard'}
OBJECTIFS STYLE:
- Appliquer personnalité ${personality?.nom || 'standard'} de façon naturelle
- Utiliser vocabulaire et expressions caractéristiques
- Maintenir cohérence de ton sur tout le contenu
- Adapter niveau technique selon profil (${personality?.niveauTechnique || 'standard'})
- Style web ${personality?.style || 'professionnel'} mais authentique
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations factuelles
- GARDE même structure et longueur approximative (±15%)
- Applique SEULEMENT style et personnalité sur la forme
- RESPECTE impérativement le niveau ${personality?.niveauTechnique || 'standard'}
- ÉVITE exagération qui rendrait artificiel
TECHNIQUES STYLE:
${personality?.vocabulairePref ? `- Intégrer naturellement: ${personality.vocabulairePref}` : '- Vocabulaire professionnel équilibré'}
- Adapter registre de langue selon ${personality?.style || 'professionnel'}
- Expressions et tournures caractéristiques personnalité
- Ton cohérent: ${this.getExpectedTone(personality)} mais naturel
- Connecteurs préférés: ${personality?.connecteursPref || 'variés et naturels'}
FORMAT RÉPONSE:
[1] Contenu avec style personnalisé
[2] Contenu avec style personnalisé
etc...
IMPORTANT: Réponse DIRECTE par les contenus stylisés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse style
*/
parseStyleResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let styledContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu stylisé
styledContent = this.cleanStyleContent(styledContent);
if (styledContent && styledContent.length > 10) {
results[element.tag] = styledContent;
logSh(`✅ Stylisé [${element.tag}]: "${styledContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback style [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu style généré
*/
cleanStyleContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(stylisé|adapté|personnalisé)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?style\s+[^:]*\s*[:.]?\s*/gi, '');
content = content.replace(/^(dans\s+le\s+style\s+de\s+)[^:]*[:.]?\s*/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { StyleLayer };

View File

@ -0,0 +1,441 @@
// ========================================
// TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE
// Responsabilité: Amélioration technique modulaire réutilisable
// LLM: GPT-4o-mini (précision technique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TECHNIQUE MODULAIRE
*/
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
this.defaultLLM = 'gpt4';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}
/**
* MAIN METHOD - Appliquer amélioration technique
*/
async apply(content, config = {}) {
return await tracer.run('TechnicalLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetTerms = null // Termes techniques ciblés
} = config;
await tracer.annotate({
technicalLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration technique
const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTechnicalElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments
enhancedContent = await this.enhanceAllElementsDirect(
content,
csvData,
{ llmProvider, intensity, preserveStructure }
);
elementsProcessed = Object.keys(content).length;
elementsEnhanced = this.countDifferences(content, enhancedContent);
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO');
await tracer.event('Technical layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
throw error;
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TECHNIQUES
*/
async analyzeTechnicalNeeds(content, csvData, targetTerms = null) {
logSh(`🔍 Analyse besoins techniques`, 'DEBUG');
const analysis = {
candidates: [],
technicalTermsFound: [],
missingTerms: [],
globalScore: 0
};
// Définir termes techniques selon contexte
const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms);
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
technicalTerms: elementAnalysis.foundTerms,
missingTerms: elementAnalysis.missingTerms,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
}
analysis.technicalTermsFound.push(...elementAnalysis.foundTerms);
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)];
logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS
*/
async enhanceTechnicalElements(candidates, csvData, config) {
logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.4, // Précision technique
maxTokens: 3000
}, csvData?.personality);
const chunkResults = this.parseTechnicalResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
/**
* AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT
*/
async enhanceAllElementsDirect(content, csvData, config) {
const allElements = Object.entries(content).map(([tag, text]) => ({
tag,
content: text,
technicalTerms: [],
improvements: ['amélioration_générale_technique']
}));
return await this.enhanceTechnicalElements(allElements, csvData, config);
}
// ============= HELPER METHODS =============
/**
* Analyser élément technique individuel
*/
analyzeTechnicalElement(text, contextualTerms, csvData) {
let score = 0;
const foundTerms = [];
const missingTerms = [];
const improvements = [];
// 1. Détecter termes techniques présents
contextualTerms.forEach(term => {
if (text.toLowerCase().includes(term.toLowerCase())) {
foundTerms.push(term);
} else if (text.length > 100) { // Seulement pour textes longs
missingTerms.push(term);
}
});
// 2. Évaluer manque de précision technique
if (foundTerms.length === 0 && text.length > 80) {
score += 0.4;
improvements.push('ajout_termes_techniques');
}
// 3. Détecter vocabulaire trop générique
const genericWords = ['produit', 'solution', 'service', 'offre', 'article'];
const genericCount = genericWords.filter(word =>
text.toLowerCase().includes(word)
).length;
if (genericCount > 1) {
score += 0.3;
improvements.push('spécialisation_vocabulaire');
}
// 4. Manque de données techniques (dimensions, etc.)
if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) {
score += 0.2;
improvements.push('ajout_données_techniques');
}
// 5. Contexte métier spécifique
if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) {
score += 0.1;
improvements.push('intégration_contexte_métier');
}
return {
needsImprovement: score > 0.3,
score,
foundTerms,
missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants
improvements
};
}
/**
* Obtenir termes techniques contextuels
*/
getContextualTechnicalTerms(mc0, targetTerms) {
// Termes de base signalétique
const baseTerms = [
'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas',
'impression', 'gravure', 'découpe', 'fraisage', 'perçage',
'adhésif', 'fixation', 'visserie', 'support'
];
// Termes spécifiques selon contexte
const contextualTerms = [];
if (mc0) {
const mc0Lower = mc0.toLowerCase();
if (mc0Lower.includes('plaque')) {
contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation');
}
if (mc0Lower.includes('signalétique')) {
contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité');
}
if (mc0Lower.includes('personnalisée')) {
contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone');
}
}
// Ajouter termes ciblés si fournis
if (targetTerms && Array.isArray(targetTerms)) {
contextualTerms.push(...targetTerms);
}
return [...baseTerms, ...contextualTerms];
}
/**
* Créer prompt amélioration technique
*/
createTechnicalEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
CONTENU: "${item.content}"
AMÉLIORATIONS: ${item.improvements.join(', ')}
${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')}
CONSIGNES TECHNIQUES:
- GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''}
- AJOUTE précision technique naturelle et vocabulaire spécialisé
- INTÈGRE termes métier : matériaux, procédés, normes, dimensions
- REMPLACE vocabulaire générique par termes techniques appropriés
- ÉVITE jargon incompréhensible, reste accessible
- PRESERVE longueur approximative (±15%)
VOCABULAIRE TECHNIQUE RECOMMANDÉ:
- Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé
- Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC
- Finitions: brossé, poli, texturé, laqué
- Fixations: perçage, adhésif double face, vis inox, plots de fixation
FORMAT RÉPONSE:
[1] Contenu avec amélioration technique précise
[2] Contenu avec amélioration technique précise
etc...
IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse technique
*/
parseTechnicalResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let technicalContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu technique
technicalContent = this.cleanTechnicalContent(technicalContent);
if (technicalContent && technicalContent.length > 10) {
results[element.tag] = technicalContent;
logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu technique généré
*/
cleanTechnicalContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
/**
* Compter différences entre contenus
*/
countDifferences(original, enhanced) {
let count = 0;
Object.keys(original).forEach(tag => {
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
count++;
}
});
return count;
}
}
module.exports = { TechnicalLayer };

View File

@ -0,0 +1,495 @@
// ========================================
// TRANSITION LAYER - COUCHE TRANSITIONS MODULAIRE
// Responsabilité: Amélioration fluidité modulaire réutilisable
// LLM: Gemini (fluidité linguistique optimale)
// ========================================
const { callLLM } = require('../LLMManager');
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { chunkArray, sleep } = require('./SelectiveUtils');
/**
* COUCHE TRANSITIONS MODULAIRE
*/
class TransitionLayer {
constructor() {
this.name = 'TransitionEnhancement';
this.defaultLLM = 'gemini';
this.priority = 2; // Priorité moyenne - appliqué après technique
}
/**
* MAIN METHOD - Appliquer amélioration transitions
*/
async apply(content, config = {}) {
return await tracer.run('TransitionLayer.apply()', async () => {
const {
llmProvider = this.defaultLLM,
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
analysisMode = true, // Analyser avant d'appliquer
csvData = null,
preserveStructure = true,
targetIssues = null // Issues spécifiques à corriger
} = config;
await tracer.annotate({
transitionLayer: true,
llmProvider,
intensity,
elementsCount: Object.keys(content).length,
mc0: csvData?.mc0
});
const startTime = Date.now();
logSh(`🔗 TRANSITION LAYER: Amélioration fluidité (${llmProvider})`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
try {
let enhancedContent = {};
let elementsProcessed = 0;
let elementsEnhanced = 0;
if (analysisMode) {
// 1. Analyser éléments nécessitant amélioration transitions
const analysis = await this.analyzeTransitionNeeds(content, csvData, targetIssues);
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
if (analysis.candidates.length === 0) {
logSh(`✅ TRANSITION LAYER: Fluidité déjà optimale`, 'INFO');
return {
content,
stats: {
processed: Object.keys(content).length,
enhanced: 0,
analysisSkipped: true,
duration: Date.now() - startTime
}
};
}
// 2. Améliorer les éléments sélectionnés
const improvedResults = await this.enhanceTransitionElements(
analysis.candidates,
csvData,
{ llmProvider, intensity, preserveStructure }
);
// 3. Merger avec contenu original
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = analysis.candidates.length;
} else {
// Mode direct : améliorer tous les éléments longs
const longElements = Object.entries(content)
.filter(([tag, text]) => text.length > 150)
.map(([tag, text]) => ({ tag, content: text, issues: ['amélioration_générale'] }));
if (longElements.length === 0) {
return { content, stats: { processed: 0, enhanced: 0, duration: Date.now() - startTime } };
}
const improvedResults = await this.enhanceTransitionElements(
longElements,
csvData,
{ llmProvider, intensity, preserveStructure }
);
enhancedContent = { ...content };
Object.keys(improvedResults).forEach(tag => {
if (improvedResults[tag] !== content[tag]) {
enhancedContent[tag] = improvedResults[tag];
elementsEnhanced++;
}
});
elementsProcessed = longElements.length;
}
const duration = Date.now() - startTime;
const stats = {
processed: elementsProcessed,
enhanced: elementsEnhanced,
total: Object.keys(content).length,
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
duration,
llmProvider,
intensity
};
logSh(`✅ TRANSITION LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} fluidifiés (${duration}ms)`, 'INFO');
await tracer.event('Transition layer appliquée', stats);
return { content: enhancedContent, stats };
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ TRANSITION LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
// Fallback gracieux : retourner contenu original
logSh(`🔄 Fallback: contenu original préservé`, 'WARNING');
return {
content,
stats: { fallback: true, duration },
error: error.message
};
}
}, { content: Object.keys(content), config });
}
/**
* ANALYSER BESOINS TRANSITIONS
*/
async analyzeTransitionNeeds(content, csvData, targetIssues = null) {
logSh(`🔍 Analyse besoins transitions`, 'DEBUG');
const analysis = {
candidates: [],
globalScore: 0,
issuesFound: {
repetitiveConnectors: 0,
abruptTransitions: 0,
uniformSentences: 0,
formalityImbalance: 0
}
};
// Analyser chaque élément
Object.entries(content).forEach(([tag, text]) => {
const elementAnalysis = this.analyzeTransitionElement(text, csvData);
if (elementAnalysis.needsImprovement) {
analysis.candidates.push({
tag,
content: text,
issues: elementAnalysis.issues,
score: elementAnalysis.score,
improvements: elementAnalysis.improvements
});
analysis.globalScore += elementAnalysis.score;
// Compter types d'issues
elementAnalysis.issues.forEach(issue => {
if (analysis.issuesFound.hasOwnProperty(issue)) {
analysis.issuesFound[issue]++;
}
});
}
});
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
logSh(` 📊 Score global transitions: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
logSh(` 🔍 Issues trouvées: ${JSON.stringify(analysis.issuesFound)}`, 'DEBUG');
return analysis;
}
/**
* AMÉLIORER ÉLÉMENTS TRANSITIONS SÉLECTIONNÉS
*/
async enhanceTransitionElements(candidates, csvData, config) {
logSh(`🔄 Amélioration ${candidates.length} éléments transitions`, 'DEBUG');
const results = {};
const chunks = chunkArray(candidates, 6); // Chunks plus petits pour Gemini
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
try {
logSh(` 📦 Chunk transitions ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
const enhancementPrompt = this.createTransitionEnhancementPrompt(chunk, csvData, config);
const response = await callLLM(config.llmProvider, enhancementPrompt, {
temperature: 0.6, // Créativité modérée pour fluidité
maxTokens: 2500
}, csvData?.personality);
const chunkResults = this.parseTransitionResponse(response, chunk);
Object.assign(results, chunkResults);
logSh(` ✅ Chunk transitions ${chunkIndex + 1}: ${Object.keys(chunkResults).length} fluidifiés`, 'DEBUG');
// Délai entre chunks
if (chunkIndex < chunks.length - 1) {
await sleep(1500);
}
} catch (error) {
logSh(` ❌ Chunk transitions ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
// Fallback: conserver contenu original
chunk.forEach(element => {
results[element.tag] = element.content;
});
}
}
return results;
}
// ============= HELPER METHODS =============
/**
* Analyser élément transition individuel
*/
analyzeTransitionElement(text, csvData) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) {
return { needsImprovement: false, score: 0, issues: [], improvements: [] };
}
let score = 0;
const issues = [];
const improvements = [];
// 1. Analyser connecteurs répétitifs
const repetitiveScore = this.analyzeRepetitiveConnectors(text);
if (repetitiveScore > 0.3) {
score += 0.3;
issues.push('repetitiveConnectors');
improvements.push('varier_connecteurs');
}
// 2. Analyser transitions abruptes
const abruptScore = this.analyzeAbruptTransitions(sentences);
if (abruptScore > 0.4) {
score += 0.4;
issues.push('abruptTransitions');
improvements.push('ajouter_transitions_fluides');
}
// 3. Analyser uniformité des phrases
const uniformityScore = this.analyzeSentenceUniformity(sentences);
if (uniformityScore < 0.3) {
score += 0.2;
issues.push('uniformSentences');
improvements.push('varier_longueurs_phrases');
}
// 4. Analyser équilibre formalité
const formalityScore = this.analyzeFormalityBalance(text);
if (formalityScore > 0.5) {
score += 0.1;
issues.push('formalityImbalance');
improvements.push('équilibrer_registre_langue');
}
return {
needsImprovement: score > 0.3,
score,
issues,
improvements
};
}
/**
* Analyser connecteurs répétitifs
*/
analyzeRepetitiveConnectors(text) {
const commonConnectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
let totalConnectors = 0;
let repetitions = 0;
commonConnectors.forEach(connector => {
const matches = (text.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
totalConnectors += matches.length;
if (matches.length > 1) repetitions += matches.length - 1;
});
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
}
/**
* Analyser transitions abruptes
*/
analyzeAbruptTransitions(sentences) {
if (sentences.length < 2) return 0;
let abruptCount = 0;
for (let i = 1; i < sentences.length; i++) {
const current = sentences[i].trim().toLowerCase();
const hasConnector = this.hasTransitionWord(current);
if (!hasConnector && current.length > 30) {
abruptCount++;
}
}
return abruptCount / (sentences.length - 1);
}
/**
* Analyser uniformité des phrases
*/
analyzeSentenceUniformity(sentences) {
if (sentences.length < 2) return 1;
const lengths = sentences.map(s => s.trim().length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
const stdDev = Math.sqrt(variance);
return Math.min(1, stdDev / avgLength);
}
/**
* Analyser équilibre formalité
*/
analyzeFormalityBalance(text) {
const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois', 'cependant'];
const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel', 'sympa'];
let formalCount = 0;
let casualCount = 0;
formalIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) formalCount++;
});
casualIndicators.forEach(indicator => {
if (text.toLowerCase().includes(indicator)) casualCount++;
});
const total = formalCount + casualCount;
if (total === 0) return 0;
// Déséquilibre si trop d'un côté
return Math.abs(formalCount - casualCount) / total;
}
/**
* Vérifier présence mots de transition
*/
hasTransitionWord(sentence) {
const transitionWords = [
'par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc',
'ensuite', 'puis', 'également', 'aussi', 'néanmoins', 'toutefois',
'd\'ailleurs', 'en outre', 'par contre', 'en revanche'
];
return transitionWords.some(word => sentence.includes(word));
}
/**
* Créer prompt amélioration transitions
*/
createTransitionEnhancementPrompt(chunk, csvData, config) {
const personality = csvData?.personality;
let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
CONTEXTE: Article SEO ${csvData?.mc0 || 'signalétique personnalisée'}
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style} web professionnel)` : ''}
${personality?.connecteursPref ? `CONNECTEURS PRÉFÉRÉS: ${personality.connecteursPref}` : ''}
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
CONTENUS À FLUIDIFIER:
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
PROBLÈMES: ${item.issues.join(', ')}
CONTENU: "${item.content}"`).join('\n\n')}
OBJECTIFS FLUIDITÉ:
- Connecteurs plus naturels et variés${personality?.connecteursPref ? `: ${personality.connecteursPref}` : ''}
- Transitions fluides entre idées et paragraphes
- Variation naturelle longueurs phrases
- ÉVITE répétitions excessives ("du coup", "par ailleurs", "en effet")
- Style ${personality?.style || 'professionnel'} mais naturel web
CONSIGNES STRICTES:
- NE CHANGE PAS le fond du message ni les informations
- GARDE même structure générale et longueur approximative (±20%)
- Améliore SEULEMENT la fluidité et les enchaînements
- RESPECTE le style ${personality?.nom || 'professionnel'}${personality?.style ? ` (${personality.style})` : ''}
- ÉVITE sur-correction qui rendrait artificiel
TECHNIQUES FLUIDITÉ:
- Varier connecteurs logiques sans répétition
- Alterner phrases courtes (8-12 mots) et moyennes (15-20 mots)
- Utiliser pronoms et reprises pour cohésion
- Ajouter transitions implicites par reformulation
- Équilibrer registre soutenu/accessible
FORMAT RÉPONSE:
[1] Contenu avec transitions améliorées
[2] Contenu avec transitions améliorées
etc...
IMPORTANT: Réponse DIRECTE par les contenus fluidifiés, pas d'explication.`;
return prompt;
}
/**
* Parser réponse transitions
*/
parseTransitionResponse(response, chunk) {
const results = {};
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
let match;
let index = 0;
while ((match = regex.exec(response)) && index < chunk.length) {
let fluidContent = match[2].trim();
const element = chunk[index];
// Nettoyer contenu fluidifié
fluidContent = this.cleanTransitionContent(fluidContent);
if (fluidContent && fluidContent.length > 10) {
results[element.tag] = fluidContent;
logSh(`✅ Fluidifié [${element.tag}]: "${fluidContent.substring(0, 60)}..."`, 'DEBUG');
} else {
results[element.tag] = element.content; // Fallback
logSh(`⚠️ Fallback transitions [${element.tag}]: amélioration invalide`, 'WARNING');
}
index++;
}
// Compléter les manquants
while (index < chunk.length) {
const element = chunk[index];
results[element.tag] = element.content;
index++;
}
return results;
}
/**
* Nettoyer contenu transitions généré
*/
cleanTransitionContent(content) {
if (!content) return content;
// Supprimer préfixes indésirables
content = content.replace(/^(voici\s+)?le\s+contenu\s+(fluidifié|amélioré)\s*[:.]?\s*/gi, '');
content = content.replace(/^(avec\s+)?transitions\s+améliorées\s*[:.]?\s*/gi, '');
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/, '');
// Nettoyer formatage
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
content = content.trim();
return content;
}
}
module.exports = { TransitionLayer };

View File

@ -0,0 +1,349 @@
// ========================================
// DÉMONSTRATION ARCHITECTURE MODULAIRE SELECTIVE
// Usage: node lib/selective-enhancement/demo-modulaire.js
// Objectif: Valider l'intégration modulaire selective enhancement
// ========================================
const { logSh } = require('../ErrorReporting');
// Import modules selective modulaires
const { applySelectiveLayer } = require('./SelectiveCore');
const {
applyPredefinedStack,
applyAdaptiveLayers,
getAvailableStacks
} = require('./SelectiveLayers');
const {
analyzeTechnicalQuality,
analyzeTransitionFluidity,
analyzeStyleConsistency,
generateImprovementReport
} = require('./SelectiveUtils');
/**
* EXEMPLE D'UTILISATION MODULAIRE SELECTIVE
*/
async function demoModularSelective() {
console.log('\n🔧 === DÉMONSTRATION SELECTIVE MODULAIRE ===\n');
// Contenu d'exemple avec problèmes de qualité
const exempleContenu = {
'|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée',
'|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu important pour votre entreprise. Cette solution permet de créer une identité visuelle.',
'|Texte_1|': 'Il est important de noter que les matériaux utilisés sont de qualité. Par ailleurs, la qualité est bonne. En effet, nos solutions sont bonnes et robustes. Par ailleurs, cela fonctionne bien.',
'|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?',
'|FAQ_Reponse_1|': 'Nos matériaux sont de qualité : ils conviennent parfaitement. Ces solutions garantissent une qualité et un rendu optimal.'
};
console.log('📊 CONTENU ORIGINAL:');
Object.entries(exempleContenu).forEach(([tag, content]) => {
console.log(` ${tag}: "${content}"`);
});
// Analyser qualité originale
const fullOriginal = Object.values(exempleContenu).join(' ');
const qualiteOriginale = {
technical: analyzeTechnicalQuality(fullOriginal, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullOriginal),
style: analyzeStyleConsistency(fullOriginal)
};
console.log(`\n📈 QUALITÉ ORIGINALE:`);
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}/100`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}/100`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}/100`);
try {
// ========================================
// TEST 1: COUCHE TECHNIQUE SEULE
// ========================================
console.log('\n🔧 TEST 1: Application couche technique');
const result1 = await applySelectiveLayer(exempleContenu, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 0.9,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Résultat: ${result1.stats.enhanced}/${result1.stats.processed} éléments améliorés`);
console.log(` ⏱️ Durée: ${result1.stats.duration}ms`);
// ========================================
// TEST 2: STACK PRÉDÉFINI
// ========================================
console.log('\n📦 TEST 2: Application stack prédéfini');
// Lister stacks disponibles
const stacks = getAvailableStacks();
console.log(' Stacks disponibles:');
stacks.forEach(stack => {
console.log(` - ${stack.name}: ${stack.description}`);
});
const result2 = await applyPredefinedStack(exempleContenu, 'standardEnhancement', {
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel',
vocabulairePref: 'signalétique,personnalisation,qualité,expertise',
niveauTechnique: 'standard'
},
mc0: 'plaque personnalisée'
}
});
console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`);
console.log(` 📊 Couches: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length} réussies`);
// ========================================
// TEST 3: COUCHES ADAPTATIVES
// ========================================
console.log('\n🧠 TEST 3: Application couches adaptatives');
const result3 = await applyAdaptiveLayers(exempleContenu, {
maxIntensity: 1.2,
analysisThreshold: 0.3,
csvData: {
personality: {
nom: 'Laurent',
style: 'commercial',
vocabulairePref: 'expertise,solution,performance,innovation',
niveauTechnique: 'accessible'
},
mc0: 'signalétique personnalisée'
}
});
if (result3.stats.adaptive) {
console.log(`✅ Adaptatif: ${result3.stats.layersApplied} couches appliquées`);
console.log(` 📊 Modifications: ${result3.stats.totalModifications}`);
}
// ========================================
// COMPARAISON QUALITÉ FINALE
// ========================================
console.log('\n📊 ANALYSE QUALITÉ FINALE:');
const contenuFinal = result2.content; // Prendre résultat stack standard
const fullEnhanced = Object.values(contenuFinal).join(' ');
const qualiteFinale = {
technical: analyzeTechnicalQuality(fullEnhanced, ['dibond', 'aluminium', 'pmma', 'impression']),
transitions: analyzeTransitionFluidity(fullEnhanced),
style: analyzeStyleConsistency(fullEnhanced, result2.csvData?.personality)
};
console.log('\n📈 AMÉLIORATION QUALITÉ:');
console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}${qualiteFinale.technical.score} (+${(qualiteFinale.technical.score - qualiteOriginale.technical.score).toFixed(1)})`);
console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}${qualiteFinale.transitions.score} (+${(qualiteFinale.transitions.score - qualiteOriginale.transitions.score).toFixed(1)})`);
console.log(` 🎨 Style: ${qualiteOriginale.style.score}${qualiteFinale.style.score} (+${(qualiteFinale.style.score - qualiteOriginale.style.score).toFixed(1)})`);
// Rapport détaillé
const rapport = generateImprovementReport(exempleContenu, contenuFinal, 'selective');
console.log('\n📋 RAPPORT AMÉLIORATION:');
console.log(` 📈 Amélioration moyenne: ${rapport.summary.averageImprovement.toFixed(1)}%`);
console.log(` ✅ Éléments améliorés: ${rapport.summary.elementsImproved}/${rapport.summary.elementsProcessed}`);
if (rapport.details.recommendations.length > 0) {
console.log(` 💡 Recommandations: ${rapport.details.recommendations.join(', ')}`);
}
// ========================================
// EXEMPLES DE TRANSFORMATION
// ========================================
console.log('\n✨ EXEMPLES DE TRANSFORMATION:');
console.log('\n📝 INTRODUCTION:');
console.log('AVANT:', `"${exempleContenu['|Introduction_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Introduction_1|']}"`);
console.log('\n📝 TEXTE PRINCIPAL:');
console.log('AVANT:', `"${exempleContenu['|Texte_1|']}"`);
console.log('APRÈS:', `"${contenuFinal['|Texte_1|']}"`);
console.log('\n✅ === DÉMONSTRATION SELECTIVE MODULAIRE TERMINÉE ===\n');
return {
success: true,
originalQuality: qualiteOriginale,
finalQuality: qualiteFinale,
improvementReport: rapport
};
} catch (error) {
console.error('\n❌ ERREUR DÉMONSTRATION:', error.message);
console.error(error.stack);
return { success: false, error: error.message };
}
}
/**
* EXEMPLE D'INTÉGRATION AVEC PIPELINE EXISTANTE
*/
async function demoIntegrationExistante() {
console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n');
// Simuler contenu venant de ContentGeneration.js (Level 1)
const contenuExistant = {
'|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles',
'|Meta_Description_1|': 'Découvrez notre gamme complète de plaques personnalisées pour tous vos besoins de signalétique professionnelle.',
'|Introduction_1|': 'Dans le domaine de la signalétique personnalisée, le choix des matériaux et des techniques de fabrication constitue un élément déterminant.',
'|Texte_Avantages_1|': 'Les avantages de nos solutions incluent la durabilité, la résistance aux intempéries et la possibilité de personnalisation complète.'
};
console.log('💼 SCÉNARIO: Application selective post-génération normale');
try {
console.log('\n🎯 Étape 1: Contenu généré par pipeline Level 1');
console.log(' ✅ Contenu de base: qualité préservée');
console.log('\n🎯 Étape 2: Application selective enhancement modulaire');
// Test avec couche technique puis style
let contenuEnhanced = contenuExistant;
// Amélioration technique
const resultTechnique = await applySelectiveLayer(contenuEnhanced, {
layerType: 'technical',
llmProvider: 'gpt4',
intensity: 1.0,
analysisMode: true,
csvData: {
personality: { nom: 'Marc', style: 'technique' },
mc0: 'plaque personnalisée'
}
});
contenuEnhanced = resultTechnique.content;
console.log(` ✅ Couche technique: ${resultTechnique.stats.enhanced} éléments améliorés`);
// Amélioration style
const resultStyle = await applySelectiveLayer(contenuEnhanced, {
layerType: 'style',
llmProvider: 'mistral',
intensity: 0.8,
analysisMode: true,
csvData: {
personality: {
nom: 'Sophie',
style: 'professionnel moderne',
vocabulairePref: 'innovation,expertise,personnalisation,qualité',
niveauTechnique: 'accessible'
}
}
});
contenuEnhanced = resultStyle.content;
console.log(` ✅ Couche style: ${resultStyle.stats.enhanced} éléments stylisés`);
console.log('\n📊 RÉSULTAT FINAL INTÉGRÉ:');
Object.entries(contenuEnhanced).forEach(([tag, content]) => {
console.log(`\n ${tag}:`);
console.log(` ORIGINAL: "${contenuExistant[tag]}"`);
console.log(` ENHANCED: "${content}"`);
});
return {
success: true,
techniqueResult: resultTechnique,
styleResult: resultStyle,
finalContent: contenuEnhanced
};
} catch (error) {
console.error('❌ ERREUR INTÉGRATION:', error.message);
return { success: false, error: error.message };
}
}
/**
* TEST PERFORMANCE ET BENCHMARKS
*/
async function benchmarkPerformance() {
console.log('\n⚡ === BENCHMARK PERFORMANCE ===\n');
// Contenu de test de taille variable
const contenuTest = {};
// Générer contenu test
for (let i = 1; i <= 10; i++) {
contenuTest[`|Element_${i}|`] = `Ceci est un contenu de test numéro ${i} pour valider les performances du système selective enhancement modulaire. ` +
`Il est important de noter que ce contenu contient du vocabulaire générique et des répétitions. Par ailleurs, les transitions sont basiques. ` +
`En effet, la qualité technique est faible et le style est générique. Par ailleurs, cela nécessite des améliorations.`.repeat(Math.floor(i/3) + 1);
}
console.log(`📊 Contenu test: ${Object.keys(contenuTest).length} éléments`);
try {
const benchmarks = [];
// Test 1: Couche technique seule
const start1 = Date.now();
const result1 = await applySelectiveLayer(contenuTest, {
layerType: 'technical',
intensity: 0.8
});
benchmarks.push({
test: 'Couche technique seule',
duration: Date.now() - start1,
enhanced: result1.stats.enhanced,
processed: result1.stats.processed
});
// Test 2: Stack complet
const start2 = Date.now();
const result2 = await applyPredefinedStack(contenuTest, 'fullEnhancement');
benchmarks.push({
test: 'Stack complet (3 couches)',
duration: Date.now() - start2,
totalModifications: result2.stats.totalModifications,
layers: result2.stats.layers.length
});
// Test 3: Adaptatif
const start3 = Date.now();
const result3 = await applyAdaptiveLayers(contenuTest, { maxIntensity: 1.0 });
benchmarks.push({
test: 'Couches adaptatives',
duration: Date.now() - start3,
layersApplied: result3.stats.layersApplied,
totalModifications: result3.stats.totalModifications
});
console.log('\n📈 RÉSULTATS BENCHMARK:');
benchmarks.forEach(bench => {
console.log(`\n ${bench.test}:`);
console.log(` ⏱️ Durée: ${bench.duration}ms`);
if (bench.enhanced) console.log(` ✅ Améliorés: ${bench.enhanced}/${bench.processed}`);
if (bench.totalModifications) console.log(` 🔄 Modifications: ${bench.totalModifications}`);
if (bench.layers) console.log(` 📦 Couches: ${bench.layers}`);
if (bench.layersApplied) console.log(` 🧠 Couches adaptées: ${bench.layersApplied}`);
});
return { success: true, benchmarks };
} catch (error) {
console.error('❌ ERREUR BENCHMARK:', error.message);
return { success: false, error: error.message };
}
}
// Exécuter démonstrations si fichier appelé directement
if (require.main === module) {
(async () => {
await demoModularSelective();
await demoIntegrationExistante();
await benchmarkPerformance();
})().catch(console.error);
}
module.exports = {
demoModularSelective,
demoIntegrationExistante,
benchmarkPerformance
};

112
plan.md
View File

@ -37,49 +37,89 @@ Full Arsenal
Tests industriels
🎯 NIVEAU 1 : Selective Enhancement
Base solide - Faible risque
Objectif
Remplacer l'approche actuelle (1 LLM par élément) par 4 améliorations ciblées.
Implémentation
// AJOUTER dans ContentGeneration.gs
function generateWithSelectiveEnhancement(element, csvData) {
// 1. Base avec Claude (comme avant)
let content = callLLM('claude', createPrompt(element, csvData), {}, csvData.personality);
🎯 NIVEAU 1 : Selective Enhancement ✅ IMPLÉMENTÉ
Base solide - Faible risque - **REFACTORISÉ EN ARCHITECTURE MODULAIRE**
// 2. Améliorations ciblées
content = enhanceTechnicalTerms(content, csvData); // GPT-4
content = improveTransitions(content, csvData); // Gemini
content = applyPersonalityStyle(content, csvData); // Mistral
## ✅ **STATUT: ARCHITECTURE REFACTORISÉE TERMINÉE**
return content;
### 🏗️ **NOUVELLE ARCHITECTURE MODULAIRE**
```
lib/ContentGeneration.js ← Orchestrateur principal
lib/generation/
├── InitialGeneration.js ← ÉTAPE 1: Claude (génération base)
├── TechnicalEnhancement.js ← ÉTAPE 2: GPT-4 (termes techniques)
├── TransitionEnhancement.js ← ÉTAPE 3: Gemini (fluidité)
└── StyleEnhancement.js ← ÉTAPE 4: Mistral (personnalité)
```
### 🔥 **IMPLÉMENTATION RÉELLE**
```javascript
// ORCHESTRATEUR PRINCIPAL - lib/ContentGeneration.js
async function generateWithContext(hierarchy, csvData, options = {}) {
// ÉTAPE 1: Génération initiale (Claude)
const step1Result = await generateInitialContent({ hierarchy, csvData });
// ÉTAPE 2: Enhancement technique (GPT-4) - Optionnel
const step2Result = await enhanceTechnicalTerms({
content: step1Result.content, csvData
});
// ÉTAPE 3: Enhancement transitions (Gemini) - Optionnel
const step3Result = await enhanceTransitions({
content: step2Result.content, csvData
});
// ÉTAPE 4: Enhancement style (Mistral) - Optionnel
const finalResult = await applyPersonalityStyle({
content: step3Result.content, csvData
});
return finalResult.content;
}
```
// Fonctions helper simples
function enhanceTechnicalTerms(content, csvData) {
const technicalElements = extractTechnicalTerms(content);
if (technicalElements.length === 0) return content;
### 🎛️ **MODES D'UTILISATION**
```javascript
// Mode complet (4 étapes)
const result = await generateWithContext(hierarchy, csvData);
const enhanced = callLLM('gpt4',
`Améliore SEULEMENT la précision technique de ces éléments: ${technicalElements.join(', ')}. Contexte: ${content}`,
{ temperature: 0.6 }, csvData.personality
);
// Mode simple (Claude uniquement)
const result = await generateSimple(hierarchy, csvData);
return replaceTargetedElements(content, technicalElements, enhanced);
}
// Mode avancé (choisir étapes)
const result = await generateAdvanced(hierarchy, csvData, {
technical: true, // GPT-4 ON
transitions: false, // Gemini OFF
style: true // Mistral ON
});
Tests à effectuer
[ ] Générer 10 articles avec ancienne méthode
[ ] Générer 10 articles avec Selective Enhancement
[ ] Comparer sur GPTZero et Originality.ai
[ ] Vérifier temps de génération (doit rester < 5 min/article)
Critères de validation
✅ Réduction détection IA : -15% minimum
✅ Qualité préservée : Score humain ≥ ancien système
✅ Performance : < 20% augmentation temps génération
✅ Stabilité : 0 erreur sur 20 tests
Rollback plan
Si échec → Revenir à l'ancienne méthode avec 1 ligne de code.
// Mode diagnostic
const diagnostic = await diagnosticPipeline(hierarchy, csvData);
```
### ✅ **AVANTAGES OBTENUS**
- **Séparation claire** : 1 fichier = 1 étape = 1 LLM = 1 responsabilité
- **Debug facile** : Chaque étape testable indépendamment
- **Fallback robuste** : Skip automatique si étape échoue
- **Logs détaillés** : Tracing complet par étape
- **Performance** : Stats précises par enhancement
### 📋 **TESTS À EFFECTUER**
[ ] Tester nouvelle architecture avec generateSimple()
[ ] Tester pipeline complet avec generateWithContext()
[ ] Valider chaque étape individuellement
[ ] Comparer performance vs ancien système
[ ] Tests anti-détection sur GPTZero
### ✅ **CRITÈRES DE VALIDATION**
✅ Architecture modulaire implémentée
✅ 4 étapes séparées et autonomes
✅ Interface standardisée entre étapes
✅ Modes de fonctionnement multiples
✅ Compatibilité rétroactive maintenue
### 🚀 **PRÊT POUR NIVEAU 2**
Avec cette base solide, on peut maintenant implémenter les Pattern Breaking techniques.
🔧 NIVEAU 2 : Pattern Breaking Simple
Premières techniques adversariales