diff --git a/API.md b/API.md
new file mode 100644
index 0000000..82169a9
--- /dev/null
+++ b/API.md
@@ -0,0 +1,384 @@
+# API Documentation - SEO Generator Server
+
+## đ Endpoints API Disponibles
+
+### Base URL
+```
+http://localhost:3002/api
+```
+
+---
+
+## đ GESTION ARTICLES
+
+### GET /api/articles
+RécupÚre la liste des articles générés.
+
+**Query Parameters:**
+- `limit` (optional): Nombre d'articles à retourner (défaut: 50)
+- `offset` (optional): Position de départ (défaut: 0)
+- `project` (optional): Filtrer par projet
+- `status` (optional): Filtrer par statut
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "articles": [...],
+ "total": 150,
+ "limit": 50,
+ "offset": 0
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+### GET /api/articles/:id
+RécupÚre un article spécifique.
+
+**Query Parameters:**
+- `format` (optional): Format de réponse (`json`, `html`, `text`)
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "id": "article_123",
+ "content": "...",
+ "metadata": {...}
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+### POST /api/articles
+Crée un nouvel article.
+
+**Request Body:**
+```json
+{
+ "keyword": "plaque personnalisée",
+ "project": "client_xyz",
+ "config": {
+ "selectiveStack": "standardEnhancement",
+ "adversarialMode": "light",
+ "humanSimulationMode": "none",
+ "patternBreakingMode": "none"
+ },
+ "personalityPreference": {
+ "nom": "Marc",
+ "style": "professionnel"
+ }
+}
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "id": "article_456",
+ "article": {...},
+ "config": {...}
+ },
+ "message": "Article créé avec succÚs",
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+---
+
+## đ GESTION PROJETS
+
+### GET /api/projects
+RécupÚre la liste des projets.
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "projects": [...],
+ "total": 25
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+### POST /api/projects
+Crée un nouveau projet.
+
+**Request Body:**
+```json
+{
+ "name": "Projet Client XYZ",
+ "description": "Génération de contenu SEO pour le client XYZ",
+ "config": {
+ "defaultPersonality": "Marc",
+ "selectiveStack": "fullEnhancement"
+ }
+}
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "id": "project_789",
+ "name": "Projet Client XYZ",
+ "description": "...",
+ "config": {...},
+ "createdAt": "2025-09-16T00:00:00.000Z",
+ "articlesCount": 0
+ },
+ "message": "Projet créé avec succÚs"
+}
+```
+
+---
+
+## đ GESTION TEMPLATES
+
+### GET /api/templates
+RécupÚre la liste des templates XML.
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "templates": [...],
+ "total": 12
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+### POST /api/templates
+Crée un nouveau template.
+
+**Request Body:**
+```json
+{
+ "name": "Template Article Blog",
+ "content": "...",
+ "description": "Template pour articles de blog SEO",
+ "category": "blog"
+}
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "id": "template_101",
+ "name": "Template Article Blog",
+ "content": "...",
+ "description": "...",
+ "category": "blog",
+ "createdAt": "2025-09-16T00:00:00.000Z"
+ },
+ "message": "Template créé avec succÚs"
+}
+```
+
+---
+
+## âïž CONFIGURATION
+
+### GET /api/config/personalities
+RécupÚre la configuration des personnalités.
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "personalities": [
+ {
+ "nom": "Marc",
+ "style": "professionnel et précis",
+ "description": "...",
+ "connecteurs": ["par ailleurs", "en effet"]
+ }
+ ],
+ "total": 15
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+---
+
+## đ MONITORING
+
+### GET /api/health
+Health check du systĂšme.
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "status": "healthy",
+ "timestamp": "2025-09-16T00:00:00.000Z",
+ "version": "1.0.0",
+ "uptime": 3600,
+ "memory": {...},
+ "environment": "development"
+ }
+}
+```
+
+### GET /api/metrics
+Métriques systÚme et statistiques.
+
+**Response:**
+```json
+{
+ "success": true,
+ "data": {
+ "articles": {
+ "total": 1250,
+ "recent": 45
+ },
+ "projects": {
+ "total": 25
+ },
+ "templates": {
+ "total": 12
+ },
+ "system": {
+ "uptime": 3600,
+ "memory": {...},
+ "platform": "linux",
+ "nodeVersion": "v24.3.0"
+ }
+ },
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+---
+
+## đ ENDPOINTS EXISTANTS (CompatibilitĂ©)
+
+### GET /api/status
+Statut du serveur.
+
+### GET /api/personalities
+Liste des personnalités (legacy).
+
+### POST /api/generate-simple
+Génération simple avec mot-clé.
+
+### POST /api/test-modulaire
+Test modulaire individuel.
+
+### POST /api/workflow-modulaire
+Workflow complet modulaire.
+
+### POST /api/benchmark-modulaire
+Benchmark des stacks.
+
+---
+
+## đ§ Configuration par DĂ©faut
+
+### Stacks Selective Enhancement
+- `lightEnhancement`
+- `standardEnhancement`
+- `fullEnhancement`
+- `creativeEnhancement`
+- `adaptive`
+
+### Modes Adversarial
+- `none`
+- `light`
+- `standard`
+- `heavy`
+- `adaptive`
+
+### Modes Human Simulation
+- `none`
+- `lightSimulation`
+- `standardSimulation`
+- `heavySimulation`
+- `personalityFocus`
+- `adaptive`
+
+### Modes Pattern Breaking
+- `none`
+- `syntaxFocus`
+- `connectorsFocus`
+- `structureFocus`
+- `styleFocus`
+- `comprehensiveFocus`
+- `adaptive`
+
+---
+
+## đš Gestion d'Erreurs
+
+Toutes les erreurs retournent un format standard :
+
+```json
+{
+ "success": false,
+ "error": "Description de l'erreur",
+ "message": "Message détaillé",
+ "timestamp": "2025-09-16T00:00:00.000Z"
+}
+```
+
+### Codes d'Erreur
+- `400`: Bad Request - ParamĂštres invalides
+- `404`: Not Found - Ressource non trouvée
+- `500`: Internal Server Error - Erreur serveur
+
+---
+
+## đ Exemples d'Utilisation
+
+### Créer un article simple
+```bash
+curl -X POST http://localhost:3002/api/articles \
+ -H "Content-Type: application/json" \
+ -d '{
+ "keyword": "plaque personnalisée",
+ "project": "test",
+ "config": {
+ "selectiveStack": "standardEnhancement",
+ "adversarialMode": "light"
+ }
+ }'
+```
+
+### Récupérer les métriques
+```bash
+curl http://localhost:3002/api/metrics
+```
+
+### Créer un projet
+```bash
+curl -X POST http://localhost:3002/api/projects \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Mon Projet SEO",
+ "description": "Projet de test"
+ }'
+```
+
+---
+
+## đ IntĂ©gration avec Google Sheets
+
+L'API utilise automatiquement l'intégration Google Sheets pour :
+- Récupérer les données de lignes spécifiques
+- Charger les 15 personnalités disponibles
+- Sauvegarder les articles générés
+- Maintenir la cohérence des données
\ No newline at end of file
diff --git a/backup/sequential-system/README.md b/backup/sequential-system/README.md
deleted file mode 100644
index bdd35f0..0000000
--- a/backup/sequential-system/README.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Backup du SystÚme Séquentiel
-
-Ce dossier contient une sauvegarde de l'ancien systÚme de génération séquentiel, remplacé par l'architecture modulaire complÚte.
-
-## Date de sauvegarde
-- **Date**: 2025-09-08
-- **Raison**: Migration complĂšte vers systĂšme modulaire
-- **Status**: SystĂšme legacy - Ne plus utiliser
-
-## Fichiers sauvegardés
-
-### `lib/ContentGeneration.js`
-- **Fonction**: Orchestrateur de génération séquentiel (ancien)
-- **Pipeline**: 4 étapes séquentielles fixes
-- **Remplacé par**: Architecture modulaire dans `/lib/selective-enhancement/`, `/lib/adversarial-generation/`, etc.
-- **Méthodes principales**:
- - `generateWithContext()` - Pipeline séquentiel complet
- - `generateSimple()` - Génération Claude uniquement
- - `generateAdvanced()` - Pipeline configurable
- - `diagnosticPipeline()` - Tests et debug
-
-### `lib/generation/` (Dossier complet)
-- **InitialGeneration.js** - Ătape 1: GĂ©nĂ©ration de base (Claude)
-- **TechnicalEnhancement.js** - Ătape 2: Enhancement technique (GPT-4)
-- **TransitionEnhancement.js** - Ătape 3: Enhancement transitions (Gemini)
-- **StyleEnhancement.js** - Ătape 4: Enhancement style (Mistral)
-
-## Architecture Séquentielle (Ancien)
-
-```
-generateWithContext()
-âââ 1. generateInitialContent() (Claude Sonnet-4)
-âââ 2. enhanceTechnicalTerms() (GPT-4o-mini)
-âââ 3. enhanceTransitions() (Gemini)
-âââ 4. applyPersonalityStyle() (Mistral)
-```
-
-**Limitations de l'ancien systĂšme**:
-- â Pipeline fixe, pas de flexibilitĂ©
-- â Ătapes obligatoirement sĂ©quentielles
-- â Pas de sauvegarde par Ă©tapes
-- â Configuration limitĂ©e
-- â Pas de contrĂŽle granulaire
-
-## Architecture Modulaire (Nouveau)
-
-Le nouveau systĂšme utilise:
-- **Selective Enhancement** (`/lib/selective-enhancement/`)
-- **Adversarial Generation** (`/lib/adversarial-generation/`)
-- **Human Simulation** (`/lib/human-simulation/`)
-- **Pattern Breaking** (`/lib/pattern-breaking/`)
-
-**Avantages du nouveau systĂšme**:
-- â
Couches modulaires indépendantes
-- â
Configuration granulaire
-- â
Sauvegarde versionnĂ©e (v1.0 â v2.0)
-- â
Parallélisation possible
-- â
Stacks prédéfinis + adaptatifs
-- â
Interface CLI et API complĂšte
-
-## Note Importante
-
-**NE PAS RESTAURER CES FICHIERS**
-
-Ce backup existe uniquement pour référence historique. Le nouveau systÚme modulaire est:
-- Plus flexible
-- Plus performant
-- Plus maintenable
-- EntiĂšrement compatible avec l'existant
-
-Pour utiliser le nouveau systĂšme, voir `/lib/Main.js` et la documentation dans `CLAUDE.md`.
\ No newline at end of file
diff --git a/backup/sequential-system/lib/ContentGeneration.js b/backup/sequential-system/lib/ContentGeneration.js
deleted file mode 100644
index 67f3547..0000000
--- a/backup/sequential-system/lib/ContentGeneration.js
+++ /dev/null
@@ -1,315 +0,0 @@
-// ========================================
-// ORCHESTRATEUR GĂNĂRATION - ARCHITECTURE REFACTORISĂE
-// Responsabilité: Coordonner les 4 étapes de génération
-// ========================================
-
-const { logSh } = require('./ErrorReporting');
-const { tracer } = require('./trace');
-
-// 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');
-
-// Import Pattern Breaking (Niveau 2)
-const { applyPatternBreaking } = require('./post-processing/PatternBreaking');
-
-/**
- * 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');
- }
-
- // Ă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, // â MAIN ENTRY POINT (compatible ancien code)
- generateSimple, // â GĂ©nĂ©ration rapide
- generateAdvanced, // â ContrĂŽle granulaire
- generateWithPatternBreaking, // â NOUVEAU: Niveau 2 shortcut
- diagnosticPipeline // â Tests et debug
-};
\ No newline at end of file
diff --git a/backup/sequential-system/lib/generation/InitialGeneration.js b/backup/sequential-system/lib/generation/InitialGeneration.js
deleted file mode 100644
index 76235a0..0000000
--- a/backup/sequential-system/lib/generation/InitialGeneration.js
+++ /dev/null
@@ -1,389 +0,0 @@
-// ========================================
-// Ă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
-};
\ No newline at end of file
diff --git a/backup/sequential-system/lib/generation/StyleEnhancement.js b/backup/sequential-system/lib/generation/StyleEnhancement.js
deleted file mode 100644
index 688aa37..0000000
--- a/backup/sequential-system/lib/generation/StyleEnhancement.js
+++ /dev/null
@@ -1,340 +0,0 @@
-// ========================================
-// Ă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
-};
\ No newline at end of file
diff --git a/backup/sequential-system/lib/generation/TechnicalEnhancement.js b/backup/sequential-system/lib/generation/TechnicalEnhancement.js
deleted file mode 100644
index 414df95..0000000
--- a/backup/sequential-system/lib/generation/TechnicalEnhancement.js
+++ /dev/null
@@ -1,277 +0,0 @@
-// ========================================
-// Ă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
-};
\ No newline at end of file
diff --git a/backup/sequential-system/lib/generation/TransitionEnhancement.js b/backup/sequential-system/lib/generation/TransitionEnhancement.js
deleted file mode 100644
index 91d3a2b..0000000
--- a/backup/sequential-system/lib/generation/TransitionEnhancement.js
+++ /dev/null
@@ -1,401 +0,0 @@
-// ========================================
-// Ă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
-};
\ No newline at end of file
diff --git a/code.js b/code.js
deleted file mode 100644
index 48463da..0000000
--- a/code.js
+++ /dev/null
@@ -1,26192 +0,0 @@
-/*
- code.js â bundle concatĂ©nĂ©
- Généré: 2025-09-10T11:46:20.952Z
- Source: lib
- Fichiers: 55
- Ordre: topo
-*/
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/ErrorReporting.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS
-// Description: SystĂšme de validation et rapport d'erreur
-// ========================================
-
-// Lazy loading des modules externes (évite blocage googleapis)
-let google, nodemailer;
-const fs = require('fs').promises;
-const path = require('path');
-const pino = require('pino');
-const pretty = require('pino-pretty');
-const { PassThrough } = require('stream');
-const WebSocket = require('ws');
-
-// Configuration
-const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
-
-// WebSocket server for real-time logs
-let wsServer;
-const wsClients = new Set();
-
-// Enhanced Pino logger configuration with real-time streaming and dated files
-const now = new Date();
-const timestamp = now.toISOString().slice(0, 10) + '_' +
- now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
-const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
-
-const prettyStream = pretty({
- colorize: true,
- translateTime: 'HH:MM:ss.l',
- ignore: 'pid,hostname',
-});
-
-const tee = new PassThrough();
-// Lazy loading des pipes console (évite blocage à l'import)
-let consolePipeInitialized = false;
-
-// File destination with dated filename - FORCE DEBUG LEVEL
-const fileDest = pino.destination({
- dest: logFile,
- mkdir: true,
- sync: false,
- minLength: 0 // Force immediate write even for small logs
-});
-tee.pipe(fileDest);
-
-// 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
-};
-
-// Pino logger instance with enhanced configuration and custom levels
-const logger = pino(
- {
- level: 'debug', // FORCE DEBUG LEVEL for file logging
- base: undefined,
- timestamp: pino.stdTimeFunctions.isoTime,
- customLevels: customLevels,
- useOnlyCustomLevels: true
- },
- tee
-);
-
-// Initialize WebSocket server (only when explicitly requested)
-function initWebSocketServer() {
- if (!wsServer && process.env.ENABLE_LOG_WS === 'true') {
- try {
- const logPort = process.env.LOG_WS_PORT || 8082;
- wsServer = new WebSocket.Server({ port: logPort });
-
- wsServer.on('connection', (ws) => {
- wsClients.add(ws);
- logger.info('Client connected to log WebSocket');
-
- ws.on('close', () => {
- wsClients.delete(ws);
- logger.info('Client disconnected from log WebSocket');
- });
-
- ws.on('error', (error) => {
- logger.error('WebSocket error:', error.message);
- wsClients.delete(ws);
- });
- });
-
- wsServer.on('error', (error) => {
- if (error.code === 'EADDRINUSE') {
- logger.warn(`WebSocket port ${logPort} already in use`);
- wsServer = null;
- } else {
- logger.error('WebSocket server error:', error.message);
- }
- });
-
- logger.info(`Log WebSocket server started on port ${logPort}`);
- } catch (error) {
- logger.warn(`Failed to start WebSocket server: ${error.message}`);
- wsServer = null;
- }
- }
-}
-
-// Broadcast log to WebSocket clients
-function broadcastLog(message, level) {
- const logData = {
- timestamp: new Date().toISOString(),
- level: level.toUpperCase(),
- message: message
- };
-
- wsClients.forEach(ws => {
- if (ws.readyState === WebSocket.OPEN) {
- try {
- ws.send(JSON.stringify(logData));
- } catch (error) {
- logger.error('Failed to send log to WebSocket client:', error.message);
- wsClients.delete(ws);
- }
- }
- });
-}
-
-// đ NODE.JS : Google Sheets API setup (remplace SpreadsheetApp)
-let sheets;
-let auth;
-
-async function initGoogleSheets() {
- if (!sheets) {
- // Lazy load googleapis seulement quand nécessaire
- if (!google) {
- google = require('googleapis').google;
- }
-
- // Configuration auth Google Sheets API
- // Pour la démo, on utilise une clé de service (à configurer)
- auth = new google.auth.GoogleAuth({
- keyFile: process.env.GOOGLE_CREDENTIALS_PATH, // Chemin vers fichier JSON credentials
- scopes: ['https://www.googleapis.com/auth/spreadsheets']
- });
-
- sheets = google.sheets({ version: 'v4', auth });
- }
- return sheets;
-}
-
-async function logSh(message, level = 'INFO') {
- // Initialize WebSocket server if not already done
- if (!wsServer) {
- initWebSocketServer();
- }
-
- // Initialize console pipe if needed (lazy loading)
- if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') {
- tee.pipe(prettyStream).pipe(process.stdout);
- consolePipeInitialized = true;
- }
-
- // Convert level to lowercase for Pino
- const pinoLevel = level.toLowerCase();
-
- // Enhanced trace metadata for hierarchical logging
- const traceData = {};
- if (message.includes('â¶') || message.includes('â') || message.includes('â') || message.includes('âą')) {
- traceData.trace = true;
- traceData.evt = message.includes('â¶') ? 'span.start' :
- message.includes('â') ? 'span.end' :
- message.includes('â') ? 'span.error' : 'span.event';
- }
-
- // Log with Pino (handles console output with pretty formatting and file logging)
- switch (pinoLevel) {
- case 'error':
- logger.error(traceData, message);
- break;
- case 'warning':
- case 'warn':
- logger.warn(traceData, message);
- break;
- case 'debug':
- logger.debug(traceData, message);
- break;
- case 'trace':
- logger.trace(traceData, message);
- break;
- case 'prompt':
- logger.prompt(traceData, message);
- break;
- case 'llm':
- logger.llm(traceData, message);
- break;
- default:
- logger.info(traceData, message);
- }
-
- // Broadcast to WebSocket clients for real-time viewing
- broadcastLog(message, level);
-
- // Force immediate flush to ensure real-time display and prevent log loss
- logger.flush();
-
- // Log to Google Sheets if enabled (async, non-blocking)
- if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
- setImmediate(() => {
- logToGoogleSheets(message, level).catch(err => {
- // Silent fail for Google Sheets logging to avoid recursion
- });
- });
- }
-}
-
-// Fonction pour déterminer si on doit logger en console
-function shouldLogToConsole(messageLevel, configLevel) {
- const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
- return levels[messageLevel] >= levels[configLevel];
-}
-
-// Log to file is now handled by Pino transport
-// This function is kept for compatibility but does nothing
-async function logToFile(message, level) {
- // Pino handles file logging via transport configuration
- // This function is deprecated and kept for compatibility only
-}
-
-// đ NODE.JS : Log vers Google Sheets (version async)
-async function logToGoogleSheets(message, level) {
- try {
- const sheetsApi = await initGoogleSheets();
-
- const values = [[
- new Date().toISOString(),
- level,
- message,
- 'Node.js workflow'
- ]];
-
- await sheetsApi.spreadsheets.values.append({
- spreadsheetId: SHEET_ID,
- range: 'Logs!A:D',
- valueInputOption: 'RAW',
- insertDataOption: 'INSERT_ROWS',
- resource: { values }
- });
-
- } catch (error) {
- logSh('Ăchec log Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
- }
-}
-
-// đ NODE.JS : Version simplifiĂ©e cleanLogSheet
-async function cleanLogSheet() {
- try {
- logSh('đ§č Nettoyage logs...', 'INFO'); // Using logSh instead of console.log
-
- // 1. Nettoyer fichiers logs locaux (garder 7 derniers jours)
- await cleanLocalLogs();
-
- // 2. Nettoyer Google Sheets si activé
- if (process.env.ENABLE_SHEETS_LOGGING === 'true') {
- await cleanGoogleSheetsLogs();
- }
-
- logSh('â
Logs nettoyés', 'INFO'); // Using logSh instead of console.log
-
- } catch (error) {
- logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); // Using logSh instead of console.error
- }
-}
-
-async function cleanLocalLogs() {
- try {
- // Note: With Pino, log files are managed differently
- // This function is kept for compatibility with Google Sheets logs cleanup
- // Pino log rotation should be handled by external tools like logrotate
-
- // For now, we keep the basic cleanup for any remaining old log files
- const logsDir = path.join(__dirname, '../logs');
-
- try {
- const files = await fs.readdir(logsDir);
- const cutoffDate = new Date();
- cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours
-
- for (const file of files) {
- if (file.endsWith('.log')) {
- const filePath = path.join(logsDir, file);
- const stats = await fs.stat(filePath);
-
- if (stats.mtime < cutoffDate) {
- await fs.unlink(filePath);
- logSh(`đïž SupprimĂ© log ancien: ${file}`, 'INFO');
- }
- }
- }
- } catch (error) {
- // Directory might not exist, that's fine
- }
- } catch (error) {
- // Silent fail
- }
-}
-
-async function cleanGoogleSheetsLogs() {
- try {
- const sheetsApi = await initGoogleSheets();
-
- // Clear + remettre headers
- await sheetsApi.spreadsheets.values.clear({
- spreadsheetId: SHEET_ID,
- range: 'Logs!A:D'
- });
-
- await sheetsApi.spreadsheets.values.update({
- spreadsheetId: SHEET_ID,
- range: 'Logs!A1:D1',
- valueInputOption: 'RAW',
- resource: {
- values: [['Timestamp', 'Level', 'Message', 'Source']]
- }
- });
-
- } catch (error) {
- logSh('Ăchec nettoyage Google Sheets: ' + error.message, 'WARNING'); // Using logSh instead of console.warn
- }
-}
-
-// ============= VALIDATION PRINCIPALE - IDENTIQUE =============
-
-function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) {
- logSh('đ >>> VALIDATION INTĂGRITĂ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log
-
- const errors = [];
- const warnings = [];
- const stats = {
- elementsExtracted: elements.length,
- contentGenerated: Object.keys(generatedContent).length,
- tagsReplaced: 0,
- tagsRemaining: 0
- };
-
- // TEST 1: Détection tags dupliqués
- const duplicateCheck = detectDuplicateTags(elements);
- if (duplicateCheck.hasDuplicates) {
- errors.push({
- type: 'DUPLICATE_TAGS',
- severity: 'HIGH',
- message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`,
- impact: 'Certains contenus ne seront pas remplacés dans le XML final',
- suggestion: 'Vérifier le template XML pour corriger la structure'
- });
- }
-
- // TEST 2: Cohérence éléments extraits vs générés
- const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]);
- if (missingGeneration.length > 0) {
- errors.push({
- type: 'MISSING_GENERATION',
- severity: 'HIGH',
- message: `${missingGeneration.length} éléments extraits mais non générés`,
- details: missingGeneration.map(el => el.originalTag),
- impact: 'Contenu incomplet dans le XML final'
- });
- }
-
- // TEST 3: Tags non remplacés dans XML final
- const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
- stats.tagsRemaining = remainingTags.length;
-
- if (remainingTags.length > 0) {
- errors.push({
- type: 'UNREPLACED_TAGS',
- severity: 'HIGH',
- message: `${remainingTags.length} tags non remplacés dans le XML final`,
- details: remainingTags.slice(0, 5),
- impact: 'XML final contient des placeholders non remplacés'
- });
- }
-
- // TEST 4: Variables CSV manquantes
- const missingVars = detectMissingCSVVariables(csvData);
- if (missingVars.length > 0) {
- warnings.push({
- type: 'MISSING_CSV_VARIABLES',
- severity: 'MEDIUM',
- message: `Variables CSV manquantes: ${missingVars.join(', ')}`,
- impact: 'SystÚme de génération de mots-clés automatique activé'
- });
- }
-
- // TEST 5: Qualité génération IA
- const generationQuality = assessGenerationQuality(generatedContent);
- if (generationQuality.errorRate > 0.1) {
- warnings.push({
- type: 'GENERATION_QUALITY',
- severity: 'MEDIUM',
- message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`,
- impact: 'Qualité du contenu potentiellement dégradée'
- });
- }
-
- // CALCUL STATS FINALES
- stats.tagsReplaced = elements.length - remainingTags.length;
- stats.successRate = stats.elementsExtracted > 0 ?
- ((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100';
-
- const report = {
- timestamp: new Date().toISOString(),
- csvData: { mc0: csvData.mc0, t0: csvData.t0 },
- stats: stats,
- errors: errors,
- warnings: warnings,
- status: errors.length === 0 ? 'SUCCESS' : 'ERROR'
- };
-
- const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR';
- logSh(`â
Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log
-
- // ENVOYER RAPPORT SI ERREURS (async en arriĂšre-plan)
- if (errors.length > 0 || warnings.length > 2) {
- sendErrorReport(report).catch(err => {
- logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error
- });
- }
-
- return report;
-}
-
-// ============= HELPERS - IDENTIQUES =============
-
-function detectDuplicateTags(elements) {
- const tagCounts = {};
- const duplicates = [];
-
- elements.forEach(element => {
- const tag = element.originalTag;
- tagCounts[tag] = (tagCounts[tag] || 0) + 1;
-
- if (tagCounts[tag] === 2) {
- duplicates.push(tag);
- logSh(`â DUPLICATE dĂ©tectĂ©: ${tag}`, 'ERROR'); // Using logSh instead of console.error
- }
- });
-
- return {
- hasDuplicates: duplicates.length > 0,
- duplicates: duplicates,
- counts: tagCounts
- };
-}
-
-function detectMissingCSVVariables(csvData) {
- const missing = [];
-
- if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) {
- missing.push('MC+1 (insuffisant)');
- }
- if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) {
- missing.push('T+1 (insuffisant)');
- }
- if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) {
- missing.push('L+1 (insuffisant)');
- }
-
- return missing;
-}
-
-function assessGenerationQuality(generatedContent) {
- let errorCount = 0;
- let totalCount = Object.keys(generatedContent).length;
-
- Object.values(generatedContent).forEach(content => {
- if (content && (
- content.includes('[ERREUR') ||
- content.includes('ERROR') ||
- content.length < 10
- )) {
- errorCount++;
- }
- });
-
- return {
- errorRate: totalCount > 0 ? errorCount / totalCount : 0,
- totalGenerated: totalCount,
- errorsFound: errorCount
- };
-}
-
-// đ NODE.JS : Email avec nodemailer (remplace MailApp)
-async function sendErrorReport(report) {
- try {
- logSh('đ§ Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log
-
- // Lazy load nodemailer seulement quand nécessaire
- if (!nodemailer) {
- nodemailer = require('nodemailer');
- }
-
- // Configuration nodemailer (Gmail par exemple)
- const transporter = nodemailer.createTransport({
- service: 'gmail',
- auth: {
- user: process.env.EMAIL_USER, // 'your-email@gmail.com'
- pass: process.env.EMAIL_APP_PASSWORD // App password Google
- }
- });
-
- const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`;
- const htmlBody = createHTMLReport(report);
-
- const mailOptions = {
- from: process.env.EMAIL_USER,
- to: 'alexistrouve.pro@gmail.com',
- subject: subject,
- html: htmlBody,
- attachments: [{
- filename: `error-report-${Date.now()}.json`,
- content: JSON.stringify(report, null, 2),
- contentType: 'application/json'
- }]
- };
-
- await transporter.sendMail(mailOptions);
- logSh('â
Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log
-
- } catch (error) {
- logSh(`â Ăchec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
- }
-}
-
-// ============= HTML REPORT - IDENTIQUE =============
-
-function createHTMLReport(report) {
- const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545';
-
- let html = `
-
-
Rapport Workflow SEO Automatisé (Node.js)
-
-
-
Résumé Exécutif
-
Statut: ${report.status}
-
Article: ${report.csvData.t0}
-
Mot-clé: ${report.csvData.mc0}
-
Taux de réussite: ${report.stats.successRate}%
-
Timestamp: ${report.timestamp}
-
Plateforme: Node.js Server
-
`;
-
- if (report.errors.length > 0) {
- html += `
-
Erreurs Critiques (${report.errors.length})
`;
-
- report.errors.forEach((error, i) => {
- html += `
-
-
${i + 1}. ${error.type}
-
Message: ${error.message}
-
Impact: ${error.impact}
- ${error.suggestion ? `
Solution: ${error.suggestion}
` : ''}
-
`;
- });
-
- html += `
`;
- }
-
- if (report.warnings.length > 0) {
- html += `
-
Avertissements (${report.warnings.length})
`;
-
- report.warnings.forEach((warning, i) => {
- html += `
-
-
${i + 1}. ${warning.type}
-
${warning.message}
-
`;
- });
-
- html += `
`;
- }
-
- html += `
-
-
Statistiques Détaillées
-
- - ĂlĂ©ments extraits: ${report.stats.elementsExtracted}
- - Contenus générés: ${report.stats.contentGenerated}
- - Tags remplacés: ${report.stats.tagsReplaced}
- - Tags restants: ${report.stats.tagsRemaining}
-
-
-
-
-
Informations SystĂšme
-
- - Plateforme: Node.js
- - Version: ${process.version}
- - Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB
- - Uptime: ${Math.round(process.uptime())}s
-
-
-
`;
-
- return html;
-}
-
-// đ NODE.JS EXPORTS
-module.exports = {
- logSh,
- cleanLogSheet,
- validateWorkflowIntegrity,
- detectDuplicateTags,
- detectMissingCSVVariables,
- assessGenerationQuality,
- sendErrorReport,
- createHTMLReport,
- initWebSocketServer
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/trace.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// lib/trace.js
-const { AsyncLocalStorage } = require('node:async_hooks');
-const { randomUUID } = require('node:crypto');
-const { logSh } = require('./ErrorReporting');
-
-const als = new AsyncLocalStorage();
-
-function now() { return performance.now(); }
-function dur(ms) {
- if (ms < 1e3) return `${ms.toFixed(1)}ms`;
- const s = ms / 1e3;
- return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`;
-}
-
-class Span {
- constructor({ name, parent = null, attrs = {} }) {
- this.id = randomUUID();
- this.name = name;
- this.parent = parent;
- this.children = [];
- this.attrs = attrs;
- this.start = now();
- this.end = null;
- this.status = 'ok';
- this.error = null;
- }
- pathNames() {
- const names = [];
- let cur = this;
- while (cur) { names.unshift(cur.name); cur = cur.parent; }
- return names.join(' > ');
- }
- finish() { this.end = now(); }
- duration() { return (this.end ?? now()) - this.start; }
-}
-
-class Tracer {
- constructor() {
- this.rootSpans = [];
- }
- current() { return als.getStore(); }
-
- async startSpan(name, attrs = {}) {
- const parent = this.current();
- const span = new Span({ name, parent, attrs });
- if (parent) parent.children.push(span);
- else this.rootSpans.push(span);
-
- // Formater les paramĂštres pour affichage
- const paramsStr = this.formatParams(attrs);
- await logSh(`â¶ ${name}${paramsStr}`, 'TRACE');
- return span;
- }
-
- async run(name, fn, attrs = {}) {
- const parent = this.current();
- const span = await this.startSpan(name, attrs);
- return await als.run(span, async () => {
- try {
- const res = await fn();
- span.finish();
- const paramsStr = this.formatParams(span.attrs);
- await logSh(`â ${name}${paramsStr} (${dur(span.duration())})`, 'TRACE');
- return res;
- } catch (err) {
- span.status = 'error';
- span.error = { message: err?.message, stack: err?.stack };
- span.finish();
- const paramsStr = this.formatParams(span.attrs);
- await logSh(`â ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR');
- await logSh(`Stack trace: ${span.error.message}`, 'ERROR');
- if (span.error.stack) {
- const stackLines = span.error.stack.split('\n').slice(1, 6); // PremiĂšre 5 lignes du stack
- for (const line of stackLines) {
- await logSh(` ${line.trim()}`, 'ERROR');
- }
- }
- throw err;
- }
- });
- }
-
- async event(msg, extra = {}) {
- const span = this.current();
- const data = { trace: true, evt: 'span.event', ...extra };
- if (span) {
- data.span = span.id;
- data.path = span.pathNames();
- data.since_ms = +( (now() - span.start).toFixed(1) );
- }
- await logSh(`âą ${msg}`, 'TRACE');
- }
-
- async annotate(fields = {}) {
- const span = this.current();
- if (span) Object.assign(span.attrs, fields);
- await logSh('⊠annotate', 'TRACE');
- }
-
- formatParams(attrs = {}) {
- const params = Object.entries(attrs)
- .filter(([key, value]) => value !== undefined && value !== null)
- .map(([key, value]) => {
- // Tronquer les valeurs trop longues
- const strValue = String(value);
- const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue;
- return `${key}=${truncated}`;
- });
-
- return params.length > 0 ? `(${params.join(', ')})` : '';
- }
-
- printSummary() {
- const lines = [];
- const draw = (node, depth = 0) => {
- const pad = ' '.repeat(depth);
- const icon = node.status === 'error' ? 'â' : 'â';
- lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`);
- if (Object.keys(node.attrs ?? {}).length) {
- lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`);
- }
- for (const ch of node.children) draw(ch, depth + 1);
- if (node.status === 'error' && node.error?.message) {
- lines.push(`${pad} error: ${node.error.message}`);
- if (node.error.stack) {
- const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim());
- if (stackLines.length) {
- lines.push(`${pad} stack:`);
- stackLines.forEach(line => {
- if (line) lines.push(`${pad} ${line}`);
- });
- }
- }
- }
- };
- for (const r of this.rootSpans) draw(r, 0);
- const summary = lines.join('\n');
- logSh(`\nââ TRACE SUMMARY ââ\n${summary}\nââ END TRACE ââ`, 'INFO');
- return summary;
- }
-}
-
-const tracer = new Tracer();
-
-module.exports = {
- Span,
- Tracer,
- tracer
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/BrainConfig.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: BrainConfig.js - Version Node.js
-// Description: Configuration cerveau + sélection personnalité IA
-// ========================================
-
-require('dotenv').config();
-const axios = require('axios');
-const fs = require('fs').promises;
-const path = require('path');
-
-// Import de la fonction logSh (assumant qu'elle existe dans votre projet Node.js)
-const { logSh } = require('./ErrorReporting');
-
-// Configuration
-const CONFIG = {
- openai: {
- apiKey: process.env.OPENAI_API_KEY || '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',
- endpoint: 'https://api.openai.com/v1/chat/completions'
- },
- dataSource: {
- type: process.env.DATA_SOURCE_TYPE || 'json', // 'json', 'csv', 'database'
- instructionsPath: './data/instructions.json',
- personalitiesPath: './data/personalities.json'
- }
-};
-
-/**
- * FONCTION PRINCIPALE - Ăquivalent getBrainConfig()
- * @param {number|object} data - Numéro de ligne ou données directes
- * @returns {object} Configuration avec données CSV + personnalité
- */
-async function getBrainConfig(data) {
- try {
- logSh("đ§ DĂ©but getBrainConfig Node.js", "INFO");
-
- // 1. RĂCUPĂRER LES DONNĂES CSV
- let csvData;
- if (typeof data === 'number') {
- // Numéro de ligne fourni - lire depuis fichier
- csvData = await readInstructionsData(data);
- } else if (typeof data === 'object' && data.rowNumber) {
- csvData = await readInstructionsData(data.rowNumber);
- } else {
- // Données déjà fournies
- csvData = data;
- }
-
- logSh(`â
CSV récupéré: ${csvData.mc0}`, "INFO");
-
- // 2. RĂCUPĂRER LES PERSONNALITĂS
- const personalities = await getPersonalities();
- logSh(`â
${personalities.length} personnalités chargées`, "INFO");
-
- // 3. SĂLECTIONNER LA MEILLEURE PERSONNALITĂ VIA IA
- const selectedPersonality = await selectPersonalityWithAI(
- csvData.mc0,
- csvData.t0,
- personalities
- );
-
- logSh(`â
Personnalité sélectionnée: ${selectedPersonality.nom}`, "INFO");
-
- return {
- success: true,
- data: {
- ...csvData,
- personality: selectedPersonality,
- timestamp: new Date().toISOString()
- }
- };
-
- } catch (error) {
- logSh(`â Erreur getBrainConfig: ${error.message}`, "ERROR");
- return {
- success: false,
- error: error.message
- };
- }
-}
-
-/**
- * LIRE DONNĂES INSTRUCTIONS depuis Google Sheets DIRECTEMENT
- * @param {number} rowNumber - Numéro de ligne (2 = premiÚre ligne de données)
- * @returns {object} Données CSV parsées
- */
-async function readInstructionsData(rowNumber = 2) {
- try {
- logSh(`đ Lecture Google Sheet ligne ${rowNumber}...`, 'INFO');
-
- // NOUVEAU : Lecture directe depuis Google Sheets
- const { google } = require('googleapis');
-
- // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problÚme TLS
- const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
- const auth = new google.auth.GoogleAuth({
- keyFile: keyFilePath,
- scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
- });
- logSh('đ Utilisation fichier JSON pour contourner problĂšme TLS OAuth', 'INFO');
-
- const sheets = google.sheets({ version: 'v4', auth });
- const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
-
- // Récupérer la ligne spécifique (A à I au minimum)
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_ID,
- range: `Instructions!A${rowNumber}:I${rowNumber}` // Ligne spécifique A-I
- });
-
- if (!response.data.values || response.data.values.length === 0) {
- throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`);
- }
-
- const row = response.data.values[0];
- logSh(`â
Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO');
-
- const xmlTemplateValue = row[8] || '';
- let xmlTemplate = xmlTemplateValue;
- let xmlFileName = null;
-
- // Si c'est un nom de fichier, garder le nom ET utiliser un template par défaut
- if (xmlTemplateValue && xmlTemplateValue.endsWith('.xml') && xmlTemplateValue.length < 100) {
- logSh(`đ§ XML filename detected (${xmlTemplateValue}), keeping filename for Digital Ocean`, 'INFO');
- xmlFileName = xmlTemplateValue; // Garder le nom du fichier pour Digital Ocean
- xmlTemplate = createDefaultXMLTemplate(); // Template par défaut pour le processing
- }
-
- return {
- rowNumber: rowNumber,
- slug: row[0] || '', // Colonne A
- t0: row[1] || '', // Colonne B
- mc0: row[2] || '', // Colonne C
- tMinus1: row[3] || '', // Colonne D
- lMinus1: row[4] || '', // Colonne E
- mcPlus1: row[5] || '', // Colonne F
- tPlus1: row[6] || '', // Colonne G
- lPlus1: row[7] || '', // Colonne H
- xmlTemplate: xmlTemplate, // XML template pour processing
- xmlFileName: xmlFileName // Nom fichier pour Digital Ocean (si applicable)
- };
-
- } catch (error) {
- logSh(`â Erreur lecture Google Sheet: ${error.message}`, "ERROR");
- throw error;
- }
-}
-
-/**
- * RĂCUPĂRER PERSONNALITĂS depuis l'onglet "Personnalites" du Google Sheet
- * @returns {Array} Liste des personnalités disponibles
- */
-async function getPersonalities() {
- try {
- logSh('đ Lecture personnalitĂ©s depuis Google Sheet (onglet Personnalites)...', 'INFO');
-
- // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problÚme TLS
- const { google } = require('googleapis');
- const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
- const auth = new google.auth.GoogleAuth({
- keyFile: keyFilePath,
- scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
- });
- logSh('đ Utilisation fichier JSON pour contourner problĂšme TLS OAuth (personnalitĂ©s)', 'INFO');
-
- const sheets = google.sheets({ version: 'v4', auth });
- const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
-
- // RĂ©cupĂ©rer toutes les personnalitĂ©s (aprĂšs la ligne d'en-tĂȘte)
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_ID,
- range: 'Personnalites!A2:O' // Colonnes A Ă O pour inclure les nouvelles colonnes IA
- });
-
- if (!response.data.values || response.data.values.length === 0) {
- throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites');
- }
-
- const personalities = [];
-
- // Traiter chaque ligne de personnalité
- response.data.values.forEach((row, index) => {
- if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A)
- const personality = {
- nom: row[0]?.toString().trim() || '',
- description: row[1]?.toString().trim() || 'Expert généraliste',
- style: row[2]?.toString().trim() || 'professionnel',
-
- // Configuration avancée depuis colonnes Google Sheet
- motsClesSecteurs: parseCSVField(row[3]),
- vocabulairePref: parseCSVField(row[4]),
- connecteursPref: parseCSVField(row[5]),
- erreursTypiques: parseCSVField(row[6]),
- longueurPhrases: row[7]?.toString().trim() || 'moyennes',
- niveauTechnique: row[8]?.toString().trim() || 'moyen',
- ctaStyle: parseCSVField(row[9]),
- defautsSimules: parseCSVField(row[10]),
-
- // NOUVEAU: Configuration IA par étape depuis Google Sheets (colonnes L-O)
- aiEtape1Base: row[11]?.toString().trim().toLowerCase() || '',
- aiEtape2Technique: row[12]?.toString().trim().toLowerCase() || '',
- aiEtape3Transitions: row[13]?.toString().trim().toLowerCase() || '',
- aiEtape4Style: row[14]?.toString().trim().toLowerCase() || '',
-
- // Backward compatibility
- motsCles: parseCSVField(row[3] || '') // Utilise motsClesSecteurs
- };
-
- personalities.push(personality);
- logSh(`â PersonnalitĂ© chargĂ©e: ${personality.nom} (${personality.style})`, 'DEBUG');
- }
- });
-
- logSh(`đ ${personalities.length} personnalitĂ©s chargĂ©es depuis Google Sheet`, "INFO");
-
- return personalities;
-
- } catch (error) {
- logSh(`â ĂCHEC: Impossible de rĂ©cupĂ©rer les personnalitĂ©s Google Sheets - ${error.message}`, "ERROR");
- throw new Error(`FATAL: PersonnalitĂ©s Google Sheets inaccessibles - arrĂȘt du workflow: ${error.message}`);
- }
-}
-
-/**
- * PARSER CHAMP CSV - Helper function
- * @param {string} field - Champ Ă parser
- * @returns {Array} Liste des éléments parsés
- */
-function parseCSVField(field) {
- if (!field || field.toString().trim() === '') return [];
-
- return field.toString()
- .split(',')
- .map(item => item.trim())
- .filter(item => item.length > 0);
-}
-
-/**
- * Sélectionner un sous-ensemble aléatoire de personnalités
- * @param {Array} allPersonalities - Liste complÚte des personnalités
- * @param {number} percentage - Pourcentage Ă garder (0.6 = 60%)
- * @returns {Array} Sous-ensemble aléatoire
- */
-function selectRandomPersonalities(allPersonalities, percentage = 0.6) {
- const count = Math.ceil(allPersonalities.length * percentage);
-
- // Mélanger avec Fisher-Yates shuffle (meilleur que sort())
- const shuffled = [...allPersonalities];
- for (let i = shuffled.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
- }
-
- return shuffled.slice(0, count);
-}
-
-/**
- * NOUVELLE FONCTION: Sélection de 4 personnalités complémentaires pour le pipeline multi-AI
- * @param {string} mc0 - Mot-clé principal
- * @param {string} t0 - Titre principal
- * @param {Array} personalities - Liste des personnalités
- * @returns {Array} 4 personnalités sélectionnées pour chaque étape
- */
-async function selectMultiplePersonalitiesWithAI(mc0, t0, personalities) {
- try {
- logSh(`đ SĂ©lection MULTI-personnalitĂ©s IA pour: ${mc0}`, "INFO");
-
- // Sélection aléatoire de 80% des personnalités (plus large pour 4 choix)
- const randomPersonalities = selectRandomPersonalities(personalities, 0.8);
- const totalCount = personalities.length;
- const selectedCount = randomPersonalities.length;
-
- logSh(`đČ Pool alĂ©atoire: ${selectedCount}/${totalCount} personnalitĂ©s disponibles`, "DEBUG");
- logSh(`đ PersonnalitĂ©s dans le pool: ${randomPersonalities.map(p => p.nom).join(', ')}`, "DEBUG");
-
- const prompt = `Choisis 4 personnalitĂ©s COMPLĂMENTAIRES pour gĂ©nĂ©rer du contenu sur "${mc0}":
-
-OBJECTIF: Créer une équipe de 4 rédacteurs avec styles différents mais cohérents
-
-PERSONNALITĂS DISPONIBLES:
-${randomPersonalities.map(p => `- ${p.nom}: ${p.description} (Style: ${p.style})`).join('\n')}
-
-RĂLES Ă ATTRIBUER:
-1. GĂNĂRATEUR BASE: PersonnalitĂ© technique/experte pour la gĂ©nĂ©ration initiale
-2. ENHANCER TECHNIQUE: Personnalité commerciale/précise pour améliorer les termes techniques
-3. FLUIDITĂ: PersonnalitĂ© crĂ©ative/littĂ©raire pour amĂ©liorer les transitions
-4. STYLE FINAL: Personnalité terrain/accessible pour le style final
-
-CRITĂRES:
-- 4 personnalitĂ©s aux styles DIFFĂRENTS mais complĂ©mentaires
-- Adapté au secteur: ${mc0}
-- Variabilité maximale pour anti-détection
-- Ăviter les doublons de style
-
-FORMAT DE RĂPONSE (EXACTEMENT 4 noms sĂ©parĂ©s par des virgules):
-Nom1, Nom2, Nom3, Nom4`;
-
- const requestData = {
- model: "gpt-4o-mini",
- messages: [{"role": "user", "content": prompt}],
- max_tokens: 100,
- temperature: 1.0
- };
-
- const response = await axios.post(CONFIG.openai.endpoint, requestData, {
- headers: {
- 'Authorization': `Bearer ${CONFIG.openai.apiKey}`,
- 'Content-Type': 'application/json'
- },
- timeout: 300000
- });
-
- const selectedNames = response.data.choices[0].message.content.trim()
- .split(',')
- .map(name => name.trim());
-
- logSh(`đ Noms retournĂ©s par IA: ${selectedNames.join(', ')}`, "DEBUG");
-
- // Mapper aux vraies personnalités
- const selectedPersonalities = [];
- selectedNames.forEach(name => {
- const personality = randomPersonalities.find(p => p.nom === name);
- if (personality) {
- selectedPersonalities.push(personality);
- }
- });
-
- // Compléter si pas assez de personnalités trouvées (sécurité)
- while (selectedPersonalities.length < 4 && randomPersonalities.length > selectedPersonalities.length) {
- const remaining = randomPersonalities.filter(p =>
- !selectedPersonalities.some(selected => selected.nom === p.nom)
- );
- if (remaining.length > 0) {
- const randomIndex = Math.floor(Math.random() * remaining.length);
- selectedPersonalities.push(remaining[randomIndex]);
- } else {
- break;
- }
- }
-
- // Garantir exactement 4 personnalités
- const final4Personalities = selectedPersonalities.slice(0, 4);
-
- logSh(`â
Ăquipe de 4 personnalitĂ©s sĂ©lectionnĂ©e:`, "INFO");
- final4Personalities.forEach((p, index) => {
- const roles = ['BASE', 'TECHNIQUE', 'FLUIDITĂ', 'STYLE'];
- logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO");
- });
-
- return final4Personalities;
-
- } catch (error) {
- logSh(`â FATAL: SĂ©lection multi-personnalitĂ©s Ă©chouĂ©e: ${error.message}`, "ERROR");
- throw new Error(`FATAL: SĂ©lection multi-personnalitĂ©s IA impossible - arrĂȘt du workflow: ${error.message}`);
- }
-}
-
-/**
- * FONCTION LEGACY: Sélection personnalité unique (maintenue pour compatibilité)
- * @param {string} mc0 - Mot-clé principal
- * @param {string} t0 - Titre principal
- * @param {Array} personalities - Liste des personnalités
- * @returns {object} Personnalité sélectionnée
- */
-async function selectPersonalityWithAI(mc0, t0, personalities) {
- try {
- logSh(`đ€ SĂ©lection personnalitĂ© IA UNIQUE pour: ${mc0}`, "DEBUG");
-
- // Appeler la fonction multi et prendre seulement la premiĂšre
- const multiPersonalities = await selectMultiplePersonalitiesWithAI(mc0, t0, personalities);
- const selectedPersonality = multiPersonalities[0];
-
- logSh(`â
Personnalité IA sélectionnée (mode legacy): ${selectedPersonality.nom}`, "INFO");
-
- return selectedPersonality;
-
- } catch (error) {
- logSh(`â FATAL: SĂ©lection personnalitĂ© par IA Ă©chouĂ©e: ${error.message}`, "ERROR");
- throw new Error(`FATAL: SĂ©lection personnalitĂ© IA inaccessible - arrĂȘt du workflow: ${error.message}`);
- }
-}
-
-/**
- * CRĂER TEMPLATE XML PAR DĂFAUT quand colonne I contient un nom de fichier
- * Utilise les données CSV disponibles pour créer un template robuste
- */
-function createDefaultXMLTemplate() {
- return `
-
-
- |Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|
- |Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases sur {{MC0}}. Ton {{personality.style}}, utilise {{personality.vocabulairePref}}}|
-
-
-
-
- |Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|
- |Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|
-
-
-
- |Titre_H2_2{{MC+1_2}}{Titre H2 pour {{MC+1_2}}. Mets en valeur les points forts. Ton {{personality.style}}}|
- |Paragraphe_2{{MC+1_2}}{Paragraphe de 4-5 phrases sur {{MC+1_2}}. Détaille pourquoi c'est important pour {{MC0}}. Ton {{personality.style}}}|
-
-
-
- |Titre_H2_3{{MC+1_3}}{Titre H2 sur les bénéfices de {{MC+1_3}}. Accrocheur et informatif}|
- |Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|
-
-
-
-
-
-
-`;
-}
-
-/**
- * CRĂER FICHIERS DE DONNĂES D'EXEMPLE
- * Fonction utilitaire pour initialiser les fichiers JSON
- */
-async function createSampleDataFiles() {
- try {
- // Créer répertoire data s'il n'existe pas
- await fs.mkdir('./data', { recursive: true });
-
- // Exemple instructions.json
- const sampleInstructions = [
- {
- slug: "plaque-test",
- t0: "Plaque test signalétique",
- mc0: "plaque signalétique",
- "t-1": "Signalétique",
- "l-1": "/signaletique/",
- "mc+1": "plaque dibond, plaque aluminium, plaque PVC",
- "t+1": "Plaque dibond, Plaque alu, Plaque PVC",
- "l+1": "/plaque-dibond/, /plaque-aluminium/, /plaque-pvc/",
- xmlFileName: "template-plaque.xml"
- }
- ];
-
- // Exemple personalities.json
- const samplePersonalities = [
- {
- nom: "Marc",
- description: "Expert technique en signalétique",
- style: "professionnel et précis",
- motsClesSecteurs: "technique,dibond,aluminium,impression",
- vocabulairePref: "précision,qualité,expertise,performance",
- connecteursPref: "par ailleurs,en effet,notamment,cependant",
- erreursTypiques: "accord_proximite,repetition_legere",
- longueurPhrases: "moyennes",
- niveauTechnique: "élevé",
- ctaStyle: "découvrir,choisir,commander",
- defautsSimules: "fatigue_cognitive,hesitation_technique"
- },
- {
- nom: "Sophie",
- description: "Passionnée de décoration et design",
- style: "familier et chaleureux",
- motsClesSecteurs: "décoration,design,esthétique,tendances",
- vocabulairePref: "joli,magnifique,tendance,style",
- connecteursPref: "du coup,en fait,sinon,au fait",
- erreursTypiques: "familiarite_excessive,expression_populaire",
- longueurPhrases: "courtes",
- niveauTechnique: "moyen",
- ctaStyle: "craquer,adopter,foncer",
- defautsSimules: "enthousiasme_variable,anecdote_personnelle"
- }
- ];
-
- // Ăcrire les fichiers
- await fs.writeFile('./data/instructions.json', JSON.stringify(sampleInstructions, null, 2));
- await fs.writeFile('./data/personalities.json', JSON.stringify(samplePersonalities, null, 2));
-
- logSh('â
Fichiers de données d\'exemple créés dans ./data/', "INFO");
-
- } catch (error) {
- logSh(`â Erreur crĂ©ation fichiers exemple: ${error.message}`, "ERROR");
- }
-}
-
-// ============= EXPORTS NODE.JS =============
-
-module.exports = {
- getBrainConfig,
- getPersonalities,
- selectPersonalityWithAI,
- selectMultiplePersonalitiesWithAI, // NOUVEAU: Export de la fonction multi-personnalités
- selectRandomPersonalities,
- parseCSVField,
- readInstructionsData,
- createSampleDataFiles,
- createDefaultXMLTemplate,
- CONFIG
-};
-
-// ============= TEST RAPIDE SI LANCĂ DIRECTEMENT =============
-
-if (require.main === module) {
- (async () => {
- try {
- logSh('đ§Ș Test BrainConfig Node.js...', "INFO");
-
- // Créer fichiers exemple si nécessaire
- try {
- await fs.access('./data/instructions.json');
- } catch {
- await createSampleDataFiles();
- }
-
- // Test de la fonction principale
- const result = await getBrainConfig(2);
-
- if (result.success) {
- logSh(`â
Test réussi: ${result.data.personality.nom} pour ${result.data.mc0}`, "INFO");
- } else {
- logSh(`â Test Ă©chouĂ©: ${result.error}`, "ERROR");
- }
-
- } catch (error) {
- logSh(`â Erreur test: ${error.message}`, "ERROR");
- }
- })();
-}
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/LLMManager.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: LLMManager.js
-// Description: Hub central pour tous les appels LLM (Version Node.js)
-// Support: Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral
-// ========================================
-
-const fetch = globalThis.fetch.bind(globalThis);
-const { logSh } = require('./ErrorReporting');
-
-// Charger les variables d'environnement
-require('dotenv').config();
-
-// ============= CONFIGURATION CENTRALISĂE =============
-
-const LLM_CONFIG = {
- openai: {
- apiKey: process.env.OPENAI_API_KEY,
- endpoint: 'https://api.openai.com/v1/chat/completions',
- model: 'gpt-4o-mini',
- headers: {
- 'Authorization': 'Bearer {API_KEY}',
- 'Content-Type': 'application/json'
- },
- temperature: 0.7,
- timeout: 300000, // 5 minutes
- retries: 3
- },
-
- claude: {
- apiKey: process.env.ANTHROPIC_API_KEY,
- endpoint: 'https://api.anthropic.com/v1/messages',
- model: 'claude-sonnet-4-20250514',
- headers: {
- 'x-api-key': '{API_KEY}',
- 'Content-Type': 'application/json',
- 'anthropic-version': '2023-06-01'
- },
- temperature: 0.7,
- maxTokens: 6000,
- timeout: 300000, // 5 minutes
- retries: 6
- },
-
- gemini: {
- apiKey: process.env.GOOGLE_API_KEY,
- endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
- model: 'gemini-2.5-flash',
- headers: {
- 'Content-Type': 'application/json'
- },
- temperature: 0.7,
- maxTokens: 6000,
- timeout: 300000, // 5 minutes
- retries: 3
- },
-
- deepseek: {
- apiKey: process.env.DEEPSEEK_API_KEY,
- endpoint: 'https://api.deepseek.com/v1/chat/completions',
- model: 'deepseek-chat',
- headers: {
- 'Authorization': 'Bearer {API_KEY}',
- 'Content-Type': 'application/json'
- },
- temperature: 0.7,
- timeout: 300000, // 5 minutes
- retries: 3
- },
-
- moonshot: {
- apiKey: process.env.MOONSHOT_API_KEY,
- endpoint: 'https://api.moonshot.ai/v1/chat/completions',
- model: 'moonshot-v1-32k',
- headers: {
- 'Authorization': 'Bearer {API_KEY}',
- 'Content-Type': 'application/json'
- },
- temperature: 0.7,
- timeout: 300000, // 5 minutes
- retries: 3
- },
-
- mistral: {
- apiKey: process.env.MISTRAL_API_KEY,
- endpoint: 'https://api.mistral.ai/v1/chat/completions',
- model: 'mistral-small-latest',
- headers: {
- 'Authorization': 'Bearer {API_KEY}',
- 'Content-Type': 'application/json'
- },
- max_tokens: 5000,
- temperature: 0.7,
- timeout: 300000, // 5 minutes
- retries: 3
- }
-};
-
-// Alias pour compatibilité avec le code existant
-LLM_CONFIG.gpt4 = LLM_CONFIG.openai;
-
-// ============= HELPER FUNCTIONS =============
-
-const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
-
-// ============= INTERFACE UNIVERSELLE =============
-
-/**
- * Fonction principale pour appeler n'importe quel LLM
- * @param {string} llmProvider - claude|openai|gemini|deepseek|moonshot|mistral
- * @param {string} prompt - Le prompt Ă envoyer
- * @param {object} options - Options personnalisées (température, tokens, etc.)
- * @param {object} personality - Personnalité pour contexte systÚme
- * @returns {Promise} - Réponse générée
- */
-async function callLLM(llmProvider, prompt, options = {}, personality = null) {
- const startTime = Date.now();
-
- try {
- // Vérifier si le provider existe
- if (!LLM_CONFIG[llmProvider]) {
- throw new Error(`Provider LLM inconnu: ${llmProvider}`);
- }
-
- // Vérifier si l'API key est configurée
- const config = LLM_CONFIG[llmProvider];
- if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
- throw new Error(`Clé API manquante pour ${llmProvider}`);
- }
-
- logSh(`đ€ Appel LLM: ${llmProvider.toUpperCase()} (${config.model}) | PersonnalitĂ©: ${personality?.nom || 'aucune'}`, 'DEBUG');
-
- // đą AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
- logSh(`\nđ ===== PROMPT ENVOYĂ Ă ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITĂ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
- logSh(prompt, '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);
-
- // Effectuer l'appel avec retry logic
- const response = await callWithRetry(llmProvider, requestData, config);
-
- // 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`, 'INFO');
-
- // Enregistrer les stats d'usage
- await recordUsageStats(llmProvider, prompt.length, content.length, duration);
-
- return content;
-
- } catch (error) {
- const duration = Date.now() - startTime;
- logSh(`â Erreur ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalitĂ©'}): ${error.toString()}`, 'ERROR');
-
- // Enregistrer l'échec
- await recordUsageStats(llmProvider, prompt.length, 0, duration, error.toString());
-
- throw error;
- }
-}
-
-// ============= CONSTRUCTION DES REQUĂTES =============
-
-function buildRequestData(provider, prompt, options, personality) {
- const config = LLM_CONFIG[provider];
- const temperature = options.temperature || config.temperature;
- const maxTokens = options.maxTokens || config.maxTokens;
-
- // Construire le systÚme prompt si personnalité fournie
- const systemPrompt = personality ?
- `Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
- 'Tu es un assistant expert.';
-
- switch (provider) {
- case 'openai':
- case 'gpt4':
- case 'deepseek':
- case 'moonshot':
- case 'mistral':
- return {
- model: config.model,
- messages: [
- { role: 'system', content: systemPrompt },
- { role: 'user', content: prompt }
- ],
- max_tokens: maxTokens,
- temperature: temperature,
- stream: false
- };
-
- case 'claude':
- return {
- model: config.model,
- max_tokens: maxTokens,
- temperature: temperature,
- system: systemPrompt,
- messages: [
- { role: 'user', content: prompt }
- ]
- };
-
- case 'gemini':
- return {
- contents: [{
- parts: [{
- text: `${systemPrompt}\n\n${prompt}`
- }]
- }],
- generationConfig: {
- temperature: temperature,
- maxOutputTokens: maxTokens
- }
- };
-
- default:
- throw new Error(`Format de requĂȘte non supportĂ© pour ${provider}`);
- }
-}
-
-// ============= APPELS AVEC RETRY =============
-
-async function callWithRetry(provider, requestData, config) {
- let lastError;
-
- for (let attempt = 1; attempt <= config.retries; attempt++) {
- try {
- logSh(`đ Tentative ${attempt}/${config.retries} pour ${provider.toUpperCase()}`, 'DEBUG');
-
- // Préparer les headers avec la clé API
- const headers = {};
- Object.keys(config.headers).forEach(key => {
- headers[key] = config.headers[key].replace('{API_KEY}', config.apiKey);
- });
-
- // URL avec clé API pour Gemini (cas spécial)
- let url = config.endpoint;
- if (provider === 'gemini') {
- url += `?key=${config.apiKey}`;
- }
-
- const options = {
- method: 'POST',
- headers: headers,
- body: JSON.stringify(requestData),
- timeout: config.timeout
- };
-
- const response = await fetch(url, options);
- const responseText = await response.text();
-
- if (response.ok) {
- return JSON.parse(responseText);
- } else if (response.status === 429) {
- // Rate limiting - attendre plus longtemps
- const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
- logSh(`âł Rate limit ${provider.toUpperCase()}, attente ${waitTime}ms`, 'WARNING');
- await sleep(waitTime);
- continue;
- } else {
- throw new Error(`HTTP ${response.status}: ${responseText}`);
- }
-
- } catch (error) {
- lastError = error;
-
- if (attempt < config.retries) {
- const waitTime = 1000 * attempt;
- logSh(`â Erreur tentative ${attempt}: ${error.toString()}, retry dans ${waitTime}ms`, 'WARNING');
- await sleep(waitTime);
- }
- }
- }
-
- throw new Error(`Ăchec aprĂšs ${config.retries} tentatives: ${lastError.toString()}`);
-}
-
-// ============= PARSING DES RĂPONSES =============
-
-function parseResponse(provider, responseData) {
- try {
- switch (provider) {
- case 'openai':
- case 'gpt4':
- case 'deepseek':
- case 'moonshot':
- case 'mistral':
- return responseData.choices[0].message.content.trim();
-
- case 'claude':
- return responseData.content[0].text.trim();
-
- case 'gemini':
- const candidate = responseData.candidates[0];
-
- // Vérifications multiples pour Gemini 2.5
- if (candidate && candidate.content && candidate.content.parts && candidate.content.parts[0] && candidate.content.parts[0].text) {
- return candidate.content.parts[0].text.trim();
- } else if (candidate && candidate.text) {
- return candidate.text.trim();
- } else if (candidate && candidate.content && candidate.content.text) {
- return candidate.content.text.trim();
- } else {
- // Debug : logger la structure complĂšte
- logSh('Gemini structure complĂšte: ' + JSON.stringify(responseData), 'DEBUG');
- return '[Gemini: pas de texte généré - problÚme modÚle]';
- }
- default:
- throw new Error(`Parser non supporté pour ${provider}`);
- }
- } catch (error) {
- logSh(`â Erreur parsing ${provider}: ${error.toString()}`, 'ERROR');
- logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
- throw new Error(`Impossible de parser la réponse ${provider}: ${error.toString()}`);
- }
-}
-
-// ============= GESTION DES STATISTIQUES =============
-
-async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
- try {
- // TODO: Adapter selon votre systĂšme de stockage Node.js
- // Peut ĂȘtre une base de donnĂ©es, un fichier, MongoDB, etc.
- const statsData = {
- timestamp: new Date(),
- provider: provider,
- model: LLM_CONFIG[provider].model,
- promptTokens: promptTokens,
- responseTokens: responseTokens,
- duration: duration,
- error: error || ''
- };
-
- // Exemple: log vers console ou fichier
- logSh(`đ Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
-
- // TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
-
- } catch (statsError) {
- // Ne pas faire planter le workflow si les stats échouent
- logSh(`â Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
- }
-}
-
-// ============= FONCTIONS UTILITAIRES =============
-
-/**
- * Tester la connectivité de tous les LLMs
- */
-async function testAllLLMs() {
- const testPrompt = "Dis bonjour en 5 mots maximum.";
- const results = {};
-
- const allProviders = Object.keys(LLM_CONFIG);
-
- for (const provider of allProviders) {
- try {
- logSh(`đ§Ș Test ${provider}...`, 'INFO');
-
- const response = await callLLM(provider, testPrompt);
- results[provider] = {
- status: 'SUCCESS',
- response: response,
- model: LLM_CONFIG[provider].model
- };
-
- } catch (error) {
- results[provider] = {
- status: 'ERROR',
- error: error.toString(),
- model: LLM_CONFIG[provider].model
- };
- }
-
- // Petit délai entre tests
- await sleep(500);
- }
-
- logSh(`đ Tests terminĂ©s: ${JSON.stringify(results, null, 2)}`, 'INFO');
- return results;
-}
-
-/**
- * Obtenir les providers disponibles (avec clés API valides)
- */
-function getAvailableProviders() {
- const available = [];
-
- Object.keys(LLM_CONFIG).forEach(provider => {
- const config = LLM_CONFIG[provider];
- if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
- available.push(provider);
- }
- });
-
- return available;
-}
-
-/**
- * Obtenir des statistiques d'usage par provider
- */
-async function getUsageStats() {
- try {
- // TODO: Adapter selon votre systĂšme de stockage
- // Pour l'instant retourne un message par défaut
- return { message: 'Statistiques non implémentées en Node.js' };
-
- } catch (error) {
- return { error: error.toString() };
- }
-}
-
-// ============= MIGRATION DE L'ANCIEN CODE =============
-
-/**
- * Fonction de compatibilité pour remplacer votre ancien callOpenAI()
- * Maintient la mĂȘme signature pour ne pas casser votre code existant
- */
-async function callOpenAI(prompt, personality) {
- return await callLLM('openai', prompt, {}, personality);
-}
-
-// ============= EXPORTS POUR TESTS =============
-
-/**
- * Fonction de test rapide
- */
-async function testLLMManager() {
- logSh('đ Test du LLM Manager Node.js...', 'INFO');
-
- // Test des providers disponibles
- const available = getAvailableProviders();
- logSh('Providers disponibles: ' + available.join(', ') + ' (' + available.length + '/6)', 'INFO');
-
- // Test d'appel simple sur chaque provider disponible
- for (const provider of available) {
- try {
- logSh(`đ§Ș Test ${provider}...`, 'DEBUG');
- const startTime = Date.now();
-
- const response = await callLLM(provider, 'Dis juste "Test OK"');
- const duration = Date.now() - startTime;
-
- logSh(`â
Test ${provider} réussi: "${response}" (${duration}ms)`, 'INFO');
-
- } catch (error) {
- logSh(`â Test ${provider} Ă©chouĂ©: ${error.toString()}`, 'ERROR');
- }
-
- // Petit délai pour éviter rate limits
- await sleep(500);
- }
-
- // Test spécifique OpenAI (compatibilité avec ancien code)
- try {
- logSh('đŻ Test spĂ©cifique OpenAI (compatibilitĂ©)...', 'DEBUG');
- const response = await callLLM('openai', 'Dis juste "Test OK"');
- logSh('â
Test OpenAI compatibilité: ' + response, 'INFO');
- } catch (error) {
- logSh('â Test OpenAI compatibilitĂ© Ă©chouĂ©: ' + error.toString(), 'ERROR');
- }
-
- // Afficher les stats d'usage
- try {
- logSh('đ RĂ©cupĂ©ration statistiques d\'usage...', 'DEBUG');
- const stats = await getUsageStats();
-
- if (stats.error) {
- logSh('â Erreur rĂ©cupĂ©ration stats: ' + stats.error, 'WARNING');
- } else if (stats.message) {
- logSh('đ Stats: ' + stats.message, 'INFO');
- } else {
- // Formatter les stats pour les logs
- Object.keys(stats).forEach(provider => {
- const s = stats[provider];
- logSh(`đ ${provider}: ${s.calls} appels, ${s.successRate}% succĂšs, ${s.avgDuration}ms moyen`, 'INFO');
- });
- }
- } catch (error) {
- logSh('â Erreur lors de la rĂ©cupĂ©ration des stats: ' + error.toString(), 'ERROR');
- }
-
- // Résumé final
- const workingCount = available.length;
- const totalProviders = Object.keys(LLM_CONFIG).length;
-
- if (workingCount === totalProviders) {
- logSh(`â
Test LLM Manager COMPLET: ${workingCount}/${totalProviders} providers opérationnels`, 'INFO');
- } else if (workingCount >= 2) {
- logSh(`â
Test LLM Manager PARTIEL: ${workingCount}/${totalProviders} providers opérationnels (suffisant pour DNA Mixing)`, 'INFO');
- } else {
- logSh(`â Test LLM Manager INSUFFISANT: ${workingCount}/${totalProviders} providers opĂ©rationnels (minimum 2 requis)`, 'ERROR');
- }
-
- logSh('đ Test LLM Manager terminĂ©', 'INFO');
-}
-
-/**
- * Version complĂšte avec test de tous les providers (mĂȘme non configurĂ©s)
- */
-async function testLLMManagerComplete() {
- logSh('đ Test COMPLET du LLM Manager (tous providers)...', 'INFO');
-
- const allProviders = Object.keys(LLM_CONFIG);
- logSh(`Providers configurés: ${allProviders.join(', ')}`, 'INFO');
-
- const results = {
- configured: 0,
- working: 0,
- failed: 0
- };
-
- for (const provider of allProviders) {
- const config = LLM_CONFIG[provider];
-
- // Vérifier si configuré
- if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
- logSh(`âïž ${provider}: NON CONFIGURĂ (clĂ© API manquante)`, 'WARNING');
- continue;
- }
-
- results.configured++;
-
- try {
- logSh(`đ§Ș Test ${provider} (${config.model})...`, 'DEBUG');
- const startTime = Date.now();
-
- const response = await callLLM(provider, 'Réponds "OK" seulement.', { maxTokens: 100 });
- const duration = Date.now() - startTime;
-
- results.working++;
- logSh(`â
${provider}: "${response.trim()}" (${duration}ms)`, 'INFO');
-
- } catch (error) {
- results.failed++;
- logSh(`â ${provider}: ${error.toString()}`, 'ERROR');
- }
-
- // Délai entre tests
- await sleep(700);
- }
-
- // Résumé final complet
- logSh(`đ RĂSUMĂ FINAL:`, 'INFO');
- logSh(` âą Providers total: ${allProviders.length}`, 'INFO');
- logSh(` ⹠Configurés: ${results.configured}`, 'INFO');
- logSh(` âą Fonctionnels: ${results.working}`, 'INFO');
- logSh(` ⹠En échec: ${results.failed}`, 'INFO');
-
- const status = results.working >= 4 ? 'EXCELLENT' :
- results.working >= 2 ? 'BON' : 'INSUFFISANT';
-
- logSh(`đ STATUS: ${status} (${results.working} LLMs opĂ©rationnels)`,
- status === 'INSUFFISANT' ? 'ERROR' : 'INFO');
-
- logSh('đ Test LLM Manager COMPLET terminĂ©', 'INFO');
-
- return {
- total: allProviders.length,
- configured: results.configured,
- working: results.working,
- failed: results.failed,
- status: status
- };
-}
-
-// ============= EXPORTS MODULE =============
-
-module.exports = {
- callLLM,
- callOpenAI,
- testAllLLMs,
- getAvailableProviders,
- getUsageStats,
- testLLMManager,
- testLLMManagerComplete,
- LLM_CONFIG
-};
-
-
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/ElementExtraction.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: lib/element-extraction.js - CONVERTI POUR NODE.JS
-// Description: Extraction et parsing des éléments XML
-// ========================================
-
-// đ NODE.JS IMPORTS
-const { logSh } = require('./ErrorReporting');
-
-// ============= EXTRACTION PRINCIPALE =============
-
-async function extractElements(xmlTemplate, csvData) {
- try {
- await logSh('Extraction éléments avec séparation tag/contenu...', 'DEBUG');
-
- const regex = /\|([^|]+)\|/g;
- const elements = [];
- let match;
-
- while ((match = regex.exec(xmlTemplate)) !== null) {
- const fullMatch = match[1]; // Ex: "Titre_H1_1{{T0}}" ou "Titre_H3_3{{MC+1_3}}"
-
- // Séparer nom du tag et variables
- const nameMatch = fullMatch.match(/^([^{]+)/);
- const variablesMatch = fullMatch.match(/\{\{([^}]+)\}\}/g);
-
- // FIX REGEX INSTRUCTIONS - Enlever d'abord les {{variables}} puis chercher {instructions}
- const withoutVariables = fullMatch.replace(/\{\{[^}]+\}\}/g, '');
- const instructionsMatch = withoutVariables.match(/\{([^}]+)\}/);
-
- const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
-
- // TAG PUR (sans variables)
- const pureTag = `|${tagName}|`;
-
- // RĂSOUDRE le contenu des variables
- const resolvedContent = resolveVariablesContent(variablesMatch, csvData);
-
- elements.push({
- originalTag: pureTag, // â TAG PUR : |Titre_H3_3|
- name: tagName, // â Titre_H3_3
- variables: variablesMatch || [], // â [{{MC+1_3}}]
- resolvedContent: resolvedContent, // â "Plaque de rue en aluminium"
- instructions: instructionsMatch ? instructionsMatch[1] : null,
- type: getElementType(tagName),
- originalFullMatch: fullMatch // â Backup si besoin
- });
-
- await logSh(`Tag sĂ©parĂ©: ${pureTag} â "${resolvedContent}"`, 'DEBUG');
- }
-
- await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
- return elements;
-
- } catch (error) {
- await logSh(`Erreur extractElements: ${error}`, 'ERROR');
- return [];
- }
-}
-
-// ============= RĂSOLUTION VARIABLES - IDENTIQUE =============
-
-function resolveVariablesContent(variablesMatch, csvData) {
- if (!variablesMatch || variablesMatch.length === 0) {
- return ""; // Pas de variables à résoudre
- }
-
- let resolvedContent = "";
-
- variablesMatch.forEach(variable => {
- const cleanVar = variable.replace(/[{}]/g, ''); // Enlever {{ }}
-
- switch (cleanVar) {
- case 'T0':
- resolvedContent += csvData.t0;
- break;
- case 'MC0':
- resolvedContent += csvData.mc0;
- break;
- case 'T-1':
- resolvedContent += csvData.tMinus1;
- break;
- case 'L-1':
- resolvedContent += csvData.lMinus1;
- break;
- default:
- // Gérer MC+1_1, MC+1_2, etc.
- if (cleanVar.startsWith('MC+1_')) {
- const index = parseInt(cleanVar.split('_')[1]) - 1;
- const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
- resolvedContent += mcPlus1[index] || `[${cleanVar} non défini]`;
- }
- else if (cleanVar.startsWith('T+1_')) {
- const index = parseInt(cleanVar.split('_')[1]) - 1;
- const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
- resolvedContent += tPlus1[index] || `[${cleanVar} non défini]`;
- }
- else if (cleanVar.startsWith('L+1_')) {
- const index = parseInt(cleanVar.split('_')[1]) - 1;
- const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
- resolvedContent += lPlus1[index] || `[${cleanVar} non défini]`;
- }
- else {
- resolvedContent += `[${cleanVar} non résolu]`;
- }
- break;
- }
- });
-
- return resolvedContent;
-}
-
-// ============= CLASSIFICATION ĂLĂMENTS - IDENTIQUE =============
-
-function getElementType(name) {
- if (name.includes('Titre_H1')) return 'titre_h1';
- if (name.includes('Titre_H2')) return 'titre_h2';
- if (name.includes('Titre_H3')) return 'titre_h3';
- if (name.includes('Intro_')) return 'intro';
- if (name.includes('Txt_')) return 'texte';
- if (name.includes('Faq_q')) return 'faq_question';
- if (name.includes('Faq_a')) return 'faq_reponse';
- if (name.includes('Faq_H3')) return 'faq_titre';
- return 'autre';
-}
-
-// ============= GĂNĂRATION SĂQUENTIELLE - ADAPTĂE =============
-
-async function generateAllContent(elements, csvData, xmlTemplate) {
- await logSh(`Début génération pour ${elements.length} éléments`, 'INFO');
-
- const generatedContent = {};
-
- for (let index = 0; index < elements.length; index++) {
- const element = elements[index];
-
- try {
- await logSh(`ĂlĂ©ment ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
-
- const prompt = createPromptForElement(element, csvData);
- await logSh(`Prompt créé: ${prompt}`, 'DEBUG');
-
- // đ NODE.JS : Import callOpenAI depuis LLM manager
- const { callLLM } = require('./LLMManager');
- const content = await callLLM('openai', prompt, {}, csvData.personality);
-
- await logSh(`Contenu reçu: ${content}`, 'DEBUG');
-
- generatedContent[element.originalTag] = content;
-
- // đ NODE.JS : Pas de Utilities.sleep(), les appels API gĂšrent leur rate limiting
-
- } catch (error) {
- await logSh(`ERREUR élément ${element.name}: ${error.toString()}`, 'ERROR');
- generatedContent[element.originalTag] = `[Erreur génération: ${element.name}]`;
- }
- }
-
- await logSh(`Génération terminée. ${Object.keys(generatedContent).length} éléments`, 'INFO');
- return generatedContent;
-}
-
-// ============= PARSING STRUCTURE - IDENTIQUE =============
-
-function parseElementStructure(element) {
- // NETTOYER le nom : enlever , , {{...}}, {...}
- let cleanName = element.name
- .replace(/<\/?strong>/g, '') // â ENLEVER
- .replace(/\{\{[^}]*\}\}/g, '') // Enlever {{MC0}}
- .replace(/\{[^}]*\}/g, ''); // Enlever {instructions}
-
- const parts = cleanName.split('_');
-
- return {
- type: parts[0],
- level: parts[1],
- indices: parts.slice(2).map(Number),
- hierarchyPath: parts.slice(1).join('_'),
- originalElement: element,
- variables: element.variables || [],
- instructions: element.instructions
- };
-}
-
-// ============= HIĂRARCHIE INTELLIGENTE - ADAPTĂE =============
-
-async function buildSmartHierarchy(elements) {
- const hierarchy = {};
-
- elements.forEach(element => {
- const structure = parseElementStructure(element);
- const path = structure.hierarchyPath;
-
- if (!hierarchy[path]) {
- hierarchy[path] = {
- title: null,
- text: null,
- questions: [],
- children: {}
- };
- }
-
- // Associer intelligemment
- if (structure.type === 'Titre') {
- hierarchy[path].title = structure; // Tout l'objet avec variables + instructions
- } else if (structure.type === 'Txt') {
- hierarchy[path].text = structure;
- } else if (structure.type === 'Intro') {
- hierarchy[path].text = structure;
- } else if (structure.type === 'Faq') {
- hierarchy[path].questions.push(structure);
- }
- });
-
- // â LIGNE COMPILĂE
- const mappingSummary = Object.keys(hierarchy).map(path => {
- const section = hierarchy[path];
- return `${path}:[T:${section.title ? 'â' : 'â'} Txt:${section.text ? 'â' : 'â'} FAQ:${section.questions.length}]`;
- }).join(' | ');
-
- await logSh('Correspondances: ' + mappingSummary, 'DEBUG');
-
- return hierarchy;
-}
-
-// ============= PARSERS RĂPONSES - ADAPTĂS =============
-
-async function parseTitlesResponse(response, allTitles) {
- const results = {};
-
- // Utiliser regex pour extraire [TAG] contenu
- const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
- let match;
-
- while ((match = regex.exec(response)) !== null) {
- const tag = match[1].trim();
- const content = match[2].trim();
-
- // Nettoyer le contenu (enlever # et balises HTML si présentes)
- const cleanContent = content
- .replace(/^#+\s*/, '') // Enlever # du début
- .replace(/<\/?[^>]+(>|$)/g, ""); // Enlever balises HTML
-
- results[`|${tag}|`] = cleanContent;
-
- await logSh(`â Titre parsĂ© [${tag}]: "${cleanContent}"`, 'DEBUG');
- }
-
- // Fallback si parsing échoue
- if (Object.keys(results).length === 0) {
- await logSh('Parsing titres échoué, fallback ligne par ligne', 'WARNING');
- const lines = response.split('\n').filter(line => line.trim());
-
- allTitles.forEach((titleInfo, index) => {
- if (lines[index]) {
- results[titleInfo.tag] = lines[index].trim();
- }
- });
- }
-
- return results;
-}
-
-async function parseTextsResponse(response, allTexts) {
- const results = {};
-
- await logSh('Parsing réponse textes avec vrais tags...', 'DEBUG');
-
- // Utiliser regex pour extraire [TAG] contenu avec les vrais noms
- const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
- let match;
-
- while ((match = regex.exec(response)) !== null) {
- const tag = match[1].trim();
- const content = match[2].trim();
-
- // Nettoyer le contenu
- const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
-
- results[`|${tag}|`] = cleanContent;
-
- await logSh(`â Texte parsĂ© [${tag}]: "${cleanContent}"`, 'DEBUG');
- }
-
- // Fallback si parsing échoue - mapper par position
- if (Object.keys(results).length === 0) {
- await logSh('Parsing textes échoué, fallback ligne par ligne', 'WARNING');
-
- const lines = response.split('\n')
- .map(line => line.trim())
- .filter(line => line.length > 0 && !line.startsWith('['));
-
- for (let index = 0; index < allTexts.length; index++) {
- const textInfo = allTexts[index];
- if (index < lines.length) {
- let content = lines[index];
- content = content.replace(/^\d+\.\s*/, ''); // Enlever "1. " si présent
- results[textInfo.tag] = content;
-
- await logSh(`â Texte fallback ${index + 1} â ${textInfo.tag}: "${content}"`, 'DEBUG');
- } else {
- await logSh(`â Pas assez de lignes pour ${textInfo.tag}`, 'WARNING');
- results[textInfo.tag] = `[Texte manquant ${index + 1}]`;
- }
- }
- }
-
- return results;
-}
-
-// ============= PARSER FAQ SPĂCIALISĂ - ADAPTĂ =============
-
-async function parseFAQPairsResponse(response, faqPairs) {
- const results = {};
-
- await logSh('Parsing réponse paires FAQ...', 'DEBUG');
-
- // Parser avec regex pour capturer question + réponse
- const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
- let match;
-
- const parsedItems = {};
-
- while ((match = regex.exec(response)) !== null) {
- const tag = match[1].trim();
- const content = match[2].trim();
-
- const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
-
- parsedItems[tag] = cleanContent;
-
- await logSh(`â Item FAQ parsĂ© [${tag}]: "${cleanContent}"`, 'DEBUG');
- }
-
- // Mapper aux tags originaux avec |
- Object.keys(parsedItems).forEach(cleanTag => {
- const content = parsedItems[cleanTag];
- results[`|${cleanTag}|`] = content;
- });
-
- // Vérification de cohérence paires
- let pairsCompletes = 0;
- for (const pair of faqPairs) {
- const hasQuestion = results[pair.question.tag];
- const hasAnswer = results[pair.answer.tag];
-
- if (hasQuestion && hasAnswer) {
- pairsCompletes++;
- await logSh(`â Paire FAQ ${pair.number} complĂšte: Q+R`, 'DEBUG');
- } else {
- await logSh(`â Paire FAQ ${pair.number} incomplĂšte: Q=${!!hasQuestion} R=${!!hasAnswer}`, 'WARNING');
- }
- }
-
- await logSh(`${pairsCompletes}/${faqPairs.length} paires FAQ complĂštes`, 'INFO');
-
- // FATAL si paires FAQ manquantes
- if (pairsCompletes < faqPairs.length) {
- const manquantes = faqPairs.length - pairsCompletes;
- await logSh(`â FATAL: ${manquantes} paires FAQ manquantes sur ${faqPairs.length}`, 'ERROR');
- throw new Error(`FATAL: GĂ©nĂ©ration FAQ incomplĂšte (${manquantes}/${faqPairs.length} manquantes) - arrĂȘt du workflow`);
- }
-
- return results;
-}
-
-async function parseOtherElementsResponse(response, allOtherElements) {
- const results = {};
-
- await logSh('Parsing réponse autres éléments...', 'DEBUG');
-
- const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
- let match;
-
- while ((match = regex.exec(response)) !== null) {
- const tag = match[1].trim();
- const content = match[2].trim();
-
- const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
-
- results[`|${tag}|`] = cleanContent;
-
- await logSh(`â Autre Ă©lĂ©ment parsĂ© [${tag}]: "${cleanContent}"`, 'DEBUG');
- }
-
- // Fallback si parsing partiel
- if (Object.keys(results).length < allOtherElements.length) {
- await logSh('Parsing autres éléments partiel, complétion fallback', 'WARNING');
-
- const lines = response.split('\n')
- .map(line => line.trim())
- .filter(line => line.length > 0 && !line.startsWith('['));
-
- allOtherElements.forEach((element, index) => {
- if (!results[element.tag] && lines[index]) {
- results[element.tag] = lines[index];
- }
- });
- }
-
- return results;
-}
-
-// ============= HELPER FUNCTIONS - ADAPTĂES =============
-
-function createPromptForElement(element, csvData) {
- // Cette fonction sera probablement définie dans content-generation.js
- // Pour l'instant, retour basique
- return `GénÚre du contenu pour ${element.type}: ${element.resolvedContent}`;
-}
-
-
-// đ NODE.JS EXPORTS
-module.exports = {
- extractElements,
- resolveVariablesContent,
- getElementType,
- generateAllContent,
- parseElementStructure,
- buildSmartHierarchy,
- parseTitlesResponse,
- parseTextsResponse,
- parseFAQPairsResponse,
- parseOtherElementsResponse,
- createPromptForElement
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/MissingKeywords.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: MissingKeywords.js - Version Node.js
-// Description: Génération automatique des mots-clés manquants
-// ========================================
-
-const { logSh } = require('./ErrorReporting');
-const { callLLM } = require('./LLMManager');
-
-/**
- * GénÚre automatiquement les mots-clés manquants pour les éléments non définis
- * @param {Array} elements - Liste des éléments extraits
- * @param {Object} csvData - Données CSV avec personnalité
- * @returns {Object} ĂlĂ©ments mis Ă jour avec nouveaux mots-clĂ©s
- */
-async function generateMissingKeywords(elements, csvData) {
- logSh('>>> GĂNĂRATION MOTS-CLĂS MANQUANTS <<<', 'INFO');
-
- // 1. IDENTIFIER tous les éléments manquants
- const missingElements = [];
- elements.forEach(element => {
- if (element.resolvedContent.includes('non défini') ||
- element.resolvedContent.includes('non résolu') ||
- element.resolvedContent.trim() === '') {
-
- missingElements.push({
- tag: element.originalTag,
- name: element.name,
- type: element.type,
- currentContent: element.resolvedContent,
- context: getElementContext(element, elements, csvData)
- });
- }
- });
-
- if (missingElements.length === 0) {
- logSh('Aucun mot-clé manquant détecté', 'INFO');
- return {};
- }
-
- logSh(`${missingElements.length} mots-clés manquants détectés`, 'INFO');
-
- // 2. ANALYSER le contexte global disponible
- const contextAnalysis = analyzeAvailableContext(elements, csvData);
-
- // 3. GĂNĂRER tous les manquants en UN SEUL appel IA
- const generatedKeywords = await callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData);
-
- // 4. METTRE à JOUR les éléments avec les nouveaux mots-clés
- const updatedElements = updateElementsWithKeywords(elements, generatedKeywords);
-
- logSh(`Mots-clés manquants générés: ${Object.keys(generatedKeywords).length}`, 'INFO');
- return updatedElements;
-}
-
-/**
- * Analyser le contexte disponible pour guider la génération
- * @param {Array} elements - Tous les éléments
- * @param {Object} csvData - Données CSV
- * @returns {Object} Analyse contextuelle
- */
-function analyzeAvailableContext(elements, csvData) {
- const availableKeywords = [];
- const availableContent = [];
-
- // Récupérer tous les mots-clés/contenu déjà disponibles
- elements.forEach(element => {
- if (element.resolvedContent &&
- !element.resolvedContent.includes('non défini') &&
- !element.resolvedContent.includes('non résolu') &&
- element.resolvedContent.trim() !== '') {
-
- if (element.type.includes('titre')) {
- availableKeywords.push(element.resolvedContent);
- } else {
- availableContent.push(element.resolvedContent.substring(0, 100));
- }
- }
- });
-
- return {
- mainKeyword: csvData.mc0,
- mainTitle: csvData.t0,
- availableKeywords: availableKeywords,
- availableContent: availableContent,
- theme: csvData.mc0, // ThĂšme principal
- businessContext: "Autocollant.fr - signalétique personnalisée, plaques"
- };
-}
-
-/**
- * Obtenir le contexte spécifique d'un élément
- * @param {Object} element - ĂlĂ©ment Ă analyser
- * @param {Array} allElements - Tous les éléments
- * @param {Object} csvData - Données CSV
- * @returns {Object} Contexte de l'élément
- */
-function getElementContext(element, allElements, csvData) {
- const context = {
- elementType: element.type,
- hierarchyLevel: element.name,
- nearbyElements: []
- };
-
- // Trouver les éléments proches dans la hiérarchie
- const elementParts = element.name.split('_');
- if (elementParts.length >= 2) {
- const baseLevel = elementParts.slice(0, 2).join('_'); // Ex: "Titre_H3"
-
- allElements.forEach(otherElement => {
- if (otherElement.name.startsWith(baseLevel) &&
- otherElement.resolvedContent &&
- !otherElement.resolvedContent.includes('non défini')) {
-
- context.nearbyElements.push(otherElement.resolvedContent);
- }
- });
- }
-
- return context;
-}
-
-/**
- * Appel IA pour générer tous les mots-clés manquants en un seul batch
- * @param {Array} missingElements - ĂlĂ©ments manquants
- * @param {Object} contextAnalysis - Analyse contextuelle
- * @param {Object} csvData - Données CSV avec personnalité
- * @returns {Object} Mots-clés générés
- */
-async function callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData) {
- const personality = csvData.personality;
-
- let prompt = `Tu es ${personality.nom} (${personality.description}). Style: ${personality.style}
-
-MISSION: GĂNĂRE ${missingElements.length} MOTS-CLĂS/EXPRESSIONS MANQUANTS pour ${contextAnalysis.mainKeyword}
-
-CONTEXTE:
-- Sujet: ${contextAnalysis.mainKeyword}
-- Entreprise: Autocollant.fr (signalétique)
-- Mots-clés existants: ${contextAnalysis.availableKeywords.slice(0, 3).join(', ')}
-
-ĂLĂMENTS MANQUANTS:
-`;
-
- missingElements.forEach((missing, index) => {
- prompt += `${index + 1}. [${missing.name}] â Mot-clĂ© SEO\n`;
- });
-
- prompt += `\nCONSIGNES:
-- ThĂšme: ${contextAnalysis.mainKeyword}
-- Mots-clés SEO naturels
-- Varie les termes
-- Ăvite rĂ©pĂ©titions
-
-FORMAT:
-[${missingElements[0].name}]
-mot-clé
-
-[${missingElements[1] ? missingElements[1].name : 'exemple'}]
-mot-clé
-
-etc...`;
-
- try {
- logSh('Génération mots-clés manquants...', 'DEBUG');
-
- // Utilisation du LLM Manager avec fallback
- const response = await callLLM('openai', prompt, {
- temperature: 0.7,
- maxTokens: 2000
- }, personality);
-
- // Parser la réponse
- const generatedKeywords = parseMissingKeywordsResponse(response, missingElements);
-
- return generatedKeywords;
-
- } catch (error) {
- logSh(`â FATAL: GĂ©nĂ©ration mots-clĂ©s manquants Ă©chouĂ©e: ${error}`, 'ERROR');
- throw new Error(`FATAL: GĂ©nĂ©ration mots-clĂ©s LLM impossible - arrĂȘt du workflow: ${error}`);
- }
-}
-
-/**
- * Parser la réponse IA pour extraire les mots-clés générés
- * @param {string} response - Réponse de l'IA
- * @param {Array} missingElements - ĂlĂ©ments manquants
- * @returns {Object} Mots-clés parsés
- */
-function parseMissingKeywordsResponse(response, missingElements) {
- const results = {};
-
- const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
- let match;
-
- while ((match = regex.exec(response)) !== null) {
- const elementName = match[1].trim();
- const generatedKeyword = match[2].trim();
-
- results[elementName] = generatedKeyword;
-
- logSh(`â Mot-clĂ© gĂ©nĂ©rĂ© [${elementName}]: "${generatedKeyword}"`, 'DEBUG');
- }
-
- // FATAL si parsing partiel
- if (Object.keys(results).length < missingElements.length) {
- const manquants = missingElements.length - Object.keys(results).length;
- logSh(`â FATAL: Parsing mots-clĂ©s partiel - ${manquants}/${missingElements.length} manquants`, 'ERROR');
- throw new Error(`FATAL: Parsing mots-clĂ©s incomplet (${manquants}/${missingElements.length} manquants) - arrĂȘt du workflow`);
- }
-
- return results;
-}
-
-/**
- * Mettre à jour les éléments avec les nouveaux mots-clés générés
- * @param {Array} elements - ĂlĂ©ments originaux
- * @param {Object} generatedKeywords - Nouveaux mots-clés
- * @returns {Array} ĂlĂ©ments mis Ă jour
- */
-function updateElementsWithKeywords(elements, generatedKeywords) {
- const updatedElements = elements.map(element => {
- const newKeyword = generatedKeywords[element.name];
-
- if (newKeyword) {
- return {
- ...element,
- resolvedContent: newKeyword
- };
- }
-
- return element;
- });
-
- logSh('ĂlĂ©ments mis Ă jour avec nouveaux mots-clĂ©s', 'INFO');
- return updatedElements;
-}
-
-// Exports CommonJS
-module.exports = {
- generateMissingKeywords
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/SelectiveUtils.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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`;
-}
-
-/**
- * GĂNĂRATION SIMPLE (REMPLACE CONTENTGENERATION.JS)
- */
-
-/**
- * Génération simple Claude uniquement (compatible avec l'ancien systÚme)
- */
-async function generateSimple(hierarchy, csvData) {
- const { LLMManager } = require('../LLMManager');
-
- logSh(`đ„ GĂ©nĂ©ration simple Claude uniquement`, 'INFO');
-
- if (!hierarchy || Object.keys(hierarchy).length === 0) {
- throw new Error('Hiérarchie vide ou invalide');
- }
-
- const result = {
- content: {},
- stats: {
- processed: 0,
- enhanced: 0,
- duration: 0,
- llmProvider: 'claude'
- }
- };
-
- const startTime = Date.now();
-
- try {
- // Générer chaque élément avec Claude
- for (const [tag, instruction] of Object.entries(hierarchy)) {
- try {
- logSh(`đŻ GĂ©nĂ©ration: ${tag}`, 'DEBUG');
-
- const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel.
-
-CONTEXTE:
-- Mot-clé principal: ${csvData.mc0}
-- Titre principal: ${csvData.t0}
-- Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style})
-
-INSTRUCTION SPĂCIFIQUE:
-${instruction}
-
-CONSIGNES:
-- Contenu naturel et engageant
-- Intégration naturelle du mot-clé "${csvData.mc0}"
-- Style ${csvData.personality?.style || 'professionnel'}
-- Pas de formatage markdown
-- Réponse directe sans préambule
-
-RĂPONSE:`;
-
- const response = await LLMManager.callLLM('claude', prompt, {
- temperature: 0.9,
- maxTokens: 300,
- timeout: 30000
- });
-
- if (response && response.trim()) {
- result.content[tag] = cleanGeneratedContent(response.trim());
- result.stats.processed++;
- result.stats.enhanced++;
- } else {
- logSh(`â ïž RĂ©ponse vide pour ${tag}`, 'WARNING');
- result.content[tag] = `Contenu ${tag} généré automatiquement`;
- }
-
- } catch (error) {
- logSh(`â Erreur gĂ©nĂ©ration ${tag}: ${error.message}`, 'ERROR');
- result.content[tag] = `Contenu ${tag} - Erreur de génération`;
- }
- }
-
- result.stats.duration = Date.now() - startTime;
-
- logSh(`â
Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO');
-
- return result;
-
- } catch (error) {
- result.stats.duration = Date.now() - startTime;
- logSh(`â Ăchec gĂ©nĂ©ration simple: ${error.message}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * 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,
-
- // Génération simple (remplace ContentGeneration.js)
- generateSimple,
-
- // Rapports
- generateImprovementReport
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/ContentAssembly.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: ContentAssembly.js
-// Description: Assemblage et nettoyage du contenu XML
-// ========================================
-
-const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting
-
-/**
- * Nettoie les balises du template XML
- * @param {string} xmlString - Le contenu XML Ă nettoyer
- * @returns {string} - XML nettoyé
- */
-function cleanStrongTags(xmlString) {
- logSh('Nettoyage balises du template...', 'DEBUG');
-
- // Enlever toutes les balises et
- let cleaned = xmlString.replace(/<\/?strong>/g, '');
-
- // Log du nettoyage
- const strongCount = (xmlString.match(/<\/?strong>/g) || []).length;
- if (strongCount > 0) {
- logSh(`${strongCount} balises supprimées`, 'INFO');
- }
-
- return cleaned;
-}
-
-/**
- * Remplace toutes les variables CSV dans le XML
- * @param {string} xmlString - Le contenu XML
- * @param {object} csvData - Les données CSV
- * @returns {string} - XML avec variables remplacées
- */
-function replaceAllCSVVariables(xmlString, csvData) {
- logSh('Remplacement variables CSV...', 'DEBUG');
-
- let result = xmlString;
-
- // Variables simples
- result = result.replace(/\{\{T0\}\}/g, csvData.t0 || '');
- result = result.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
- result = result.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || '');
- result = result.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || '');
-
- logSh(`Variables simples remplacées: T0="${csvData.t0}", MC0="${csvData.mc0}"`, 'DEBUG');
-
- // Variables multiples
- const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim());
- const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim());
- const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim());
-
- logSh(`Variables multiples: MC+1[${mcPlus1.length}], T+1[${tPlus1.length}], L+1[${lPlus1.length}]`, 'DEBUG');
-
- // Remplacer MC+1_1, MC+1_2, etc.
- for (let i = 1; i <= 6; i++) {
- const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
- const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
- const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`;
-
- result = result.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
- result = result.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
- result = result.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue);
-
- if (mcPlus1[i-1]) {
- logSh(`MC+1_${i} = "${mcValue}"`, 'DEBUG');
- }
- }
-
- // Vérifier qu'il ne reste pas de variables non remplacées
- const remainingVars = (result.match(/\{\{[^}]+\}\}/g) || []);
- if (remainingVars.length > 0) {
- logSh(`ATTENTION: Variables non remplacées: ${remainingVars.join(', ')}`, 'WARNING');
- }
-
- logSh('Toutes les variables CSV remplacées', 'INFO');
- return result;
-}
-
-/**
- * Injecte le contenu généré dans le XML final
- * @param {string} cleanXML - XML nettoyé
- * @param {object} generatedContent - Contenu généré par tag
- * @param {array} elements - ĂlĂ©ments extraits
- * @returns {string} - XML final avec contenu injecté
- */
-function injectGeneratedContent(cleanXML, generatedContent, elements) {
- logSh('đ === DEBUG INJECTION MAPPING ===', 'DEBUG');
- logSh(`XML reçu: ${cleanXML.length} caractÚres`, 'DEBUG');
- logSh(`Contenu généré: ${Object.keys(generatedContent).length} éléments`, 'DEBUG');
- logSh(`ĂlĂ©ments fournis: ${elements.length} Ă©lĂ©ments`, 'DEBUG');
-
- // Debug: montrer le XML
- logSh(`đ XML dĂ©but: ${cleanXML}`, 'DEBUG');
-
- // Debug: montrer le contenu généré
- Object.keys(generatedContent).forEach(key => {
- logSh(`đ GĂ©nĂ©rĂ© [${key}]: "${generatedContent[key]}"`, 'DEBUG');
- });
-
- // Debug: montrer les éléments
- elements.forEach((element, i) => {
- logSh(`đ Element ${i+1}: originalTag="${element.originalTag}", originalFullMatch="${element.originalFullMatch}"`, 'DEBUG');
- });
-
- let finalXML = cleanXML;
-
- // CrĂ©er un mapping tag pur â tag original complet
- const tagMapping = {};
- elements.forEach(element => {
- tagMapping[element.originalTag] = element.originalFullMatch || element.originalTag;
- });
-
- logSh(`đ TagMapping créé: ${JSON.stringify(tagMapping, null, 2)}`, 'DEBUG');
-
- // Remplacer en utilisant les tags originaux complets
- Object.keys(generatedContent).forEach(pureTag => {
- const content = generatedContent[pureTag];
-
- logSh(`đ === TRAITEMENT TAG: ${pureTag} ===`, 'DEBUG');
- logSh(`đ Contenu Ă injecter: "${content}"`, 'DEBUG');
-
- // Trouver le tag original complet dans le XML
- const originalTag = findOriginalTagInXML(finalXML, pureTag);
-
- logSh(`đ Tag original trouvĂ©: ${originalTag ? originalTag : 'AUCUN'}`, 'DEBUG');
-
- if (originalTag) {
- const beforeLength = finalXML.length;
- finalXML = finalXML.replace(originalTag, content);
- const afterLength = finalXML.length;
-
- if (beforeLength !== afterLength) {
- logSh(`â
SUCCĂS: RemplacĂ© ${originalTag} par contenu (${afterLength - beforeLength + originalTag.length} chars)`, 'DEBUG');
- } else {
- logSh(`â ĂCHEC: Replace n'a pas fonctionnĂ© pour ${originalTag}`, 'DEBUG');
- }
- } else {
- // Fallback : essayer avec le tag pur
- const beforeLength = finalXML.length;
- finalXML = finalXML.replace(pureTag, content);
- const afterLength = finalXML.length;
-
- logSh(`â FALLBACK ${pureTag}: remplacement ${beforeLength !== afterLength ? 'RĂUSSI' : 'ĂCHOUĂ'}`, 'DEBUG');
- logSh(`â Contenu fallback: "${content}"`, 'DEBUG');
- }
- });
-
- // Vérifier les tags restants
- const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []);
- if (remainingTags.length > 0) {
- logSh(`ATTENTION: ${remainingTags.length} tags non remplacés: ${remainingTags.slice(0, 3).join(', ')}...`, 'WARNING');
- }
-
- logSh('Injection terminée', 'INFO');
- return finalXML;
-}
-
-/**
- * Helper pour trouver le tag original complet dans le XML
- * @param {string} xmlString - Contenu XML
- * @param {string} pureTag - Tag pur Ă rechercher
- * @returns {string|null} - Tag original trouvé ou null
- */
-function findOriginalTagInXML(xmlString, pureTag) {
- logSh(`đ === RECHERCHE TAG DANS XML ===`, 'DEBUG');
- logSh(`đ Tag pur recherchĂ©: "${pureTag}"`, 'DEBUG');
-
- // Extraire le nom du tag pur : |Titre_H1_1| â Titre_H1_1
- const tagName = pureTag.replace(/\|/g, '');
- logSh(`đ Nom tag extrait: "${tagName}"`, 'DEBUG');
-
- // Chercher tous les tags qui commencent par ce nom (avec espaces optionnels)
- const regex = new RegExp(`\\|\\s*${tagName}[^|]*\\|`, 'g');
- logSh(`đ Regex utilisĂ©e: ${regex}`, 'DEBUG');
-
- // Debug: montrer tous les tags présents dans le XML
- const allTags = xmlString.match(/\|[^|]*\|/g) || [];
- logSh(`đ Tags prĂ©sents dans XML: ${allTags.length}`, 'DEBUG');
- allTags.forEach((tag, i) => {
- logSh(`đ ${i+1}. "${tag}"`, 'DEBUG');
- });
-
- const matches = xmlString.match(regex);
- logSh(`đ Matches trouvĂ©s: ${matches ? matches.length : 0}`, 'DEBUG');
-
- if (matches && matches.length > 0) {
- logSh(`đ Premier match: "${matches[0]}"`, 'DEBUG');
- logSh(`â
Tag original trouvé pour ${pureTag}: ${matches[0]}`, 'DEBUG');
- return matches[0];
- }
-
- logSh(`â Aucun tag original trouvĂ© pour ${pureTag}`, 'DEBUG');
- return null;
-}
-
-// ============= EXPORTS =============
-module.exports = {
- cleanStrongTags,
- replaceAllCSVVariables,
- injectGeneratedContent,
- findOriginalTagInXML
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/ArticleStorage.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: ArticleStorage.js
-// Description: SystÚme de sauvegarde articles avec texte compilé uniquement
-// ========================================
-
-require('dotenv').config();
-const { google } = require('googleapis');
-const { logSh } = require('./ErrorReporting');
-
-// Configuration Google Sheets
-const SHEET_CONFIG = {
- sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'
-};
-
-/**
- * NOUVELLE FONCTION : Compiler le contenu de maniĂšre organique
- * Respecte la hiérarchie et les associations naturelles
- */
-async function compileGeneratedTextsOrganic(generatedTexts, elements) {
- if (!generatedTexts || Object.keys(generatedTexts).length === 0) {
- return '';
- }
-
- logSh(`đ± Compilation ORGANIQUE de ${Object.keys(generatedTexts).length} Ă©lĂ©ments...`, 'DEBUG');
-
- let compiledParts = [];
-
- // 1. DĂTECTER et GROUPER les sections organiques
- const organicSections = buildOrganicSections(generatedTexts, elements);
-
- // 2. COMPILER dans l'ordre naturel
- organicSections.forEach(section => {
- if (section.type === 'header_with_content') {
- // H1, H2, H3 avec leur contenu associé
- if (section.title) {
- compiledParts.push(cleanIndividualContent(section.title));
- }
- if (section.content) {
- compiledParts.push(cleanIndividualContent(section.content));
- }
- }
- else if (section.type === 'standalone_content') {
- // Contenu sans titre associé
- compiledParts.push(cleanIndividualContent(section.content));
- }
- else if (section.type === 'faq_pair') {
- // Paire question + réponse
- if (section.question && section.answer) {
- compiledParts.push(cleanIndividualContent(section.question));
- compiledParts.push(cleanIndividualContent(section.answer));
- }
- }
- });
-
- // 3. Joindre avec espacement naturel
- const finalText = compiledParts.join('\n\n');
-
- logSh(`â
Compilation organique terminée: ${finalText.length} caractÚres`, 'INFO');
- return finalText.trim();
-}
-
-/**
- * Construire les sections organiques en analysant les associations
- */
-function buildOrganicSections(generatedTexts, elements) {
- const sections = [];
- const usedTags = new Set();
-
- // đ§ FIX: GĂ©rer le cas oĂč elements est null/undefined
- if (!elements) {
- logSh('â ïž Elements null, utilisation compilation simple', 'DEBUG');
- // Compilation simple : tout le contenu dans l'ordre des clés
- Object.keys(generatedTexts).forEach(tag => {
- sections.push({
- type: 'standalone_content',
- content: generatedTexts[tag],
- tag: tag
- });
- });
- return sections;
- }
-
- // 1. ANALYSER l'ordre original des éléments
- const originalOrder = elements.map(el => el.originalTag);
-
- logSh(`đ Analyse de ${originalOrder.length} Ă©lĂ©ments dans l'ordre original...`, 'DEBUG');
-
- // 2. DĂTECTER les associations naturelles
- for (let i = 0; i < originalOrder.length; i++) {
- const currentTag = originalOrder[i];
- const currentContent = generatedTexts[currentTag];
-
- if (!currentContent || usedTags.has(currentTag)) continue;
-
- const currentType = identifyElementType(currentTag);
-
- if (currentType === 'titre_h1' || currentType === 'titre_h2' || currentType === 'titre_h3') {
- // CHERCHER le contenu associé qui suit
- const associatedContent = findAssociatedContent(originalOrder, i, generatedTexts, usedTags);
-
- sections.push({
- type: 'header_with_content',
- title: currentContent,
- content: associatedContent.content,
- titleTag: currentTag,
- contentTag: associatedContent.tag
- });
-
- usedTags.add(currentTag);
- if (associatedContent.tag) {
- usedTags.add(associatedContent.tag);
- }
-
- logSh(` â Section: ${currentType} + contenu associĂ©`, 'DEBUG');
- }
- else if (currentType === 'faq_question') {
- // CHERCHER la réponse correspondante
- const matchingAnswer = findMatchingFAQAnswer(currentTag, generatedTexts);
-
- if (matchingAnswer) {
- sections.push({
- type: 'faq_pair',
- question: currentContent,
- answer: matchingAnswer.content,
- questionTag: currentTag,
- answerTag: matchingAnswer.tag
- });
-
- usedTags.add(currentTag);
- usedTags.add(matchingAnswer.tag);
-
- logSh(` â Paire FAQ: ${currentTag} + ${matchingAnswer.tag}`, 'DEBUG');
- }
- }
- else if (currentType !== 'faq_reponse') {
- // CONTENU STANDALONE (pas une réponse FAQ déjà traitée)
- sections.push({
- type: 'standalone_content',
- content: currentContent,
- contentTag: currentTag
- });
-
- usedTags.add(currentTag);
- logSh(` â Contenu standalone: ${currentType}`, 'DEBUG');
- }
- }
-
- logSh(`đïž ${sections.length} sections organiques construites`, 'INFO');
- return sections;
-}
-
-/**
- * Trouver le contenu associé à un titre (paragraphe qui suit)
- */
-function findAssociatedContent(originalOrder, titleIndex, generatedTexts, usedTags) {
- // Chercher dans les éléments suivants
- for (let j = titleIndex + 1; j < originalOrder.length; j++) {
- const nextTag = originalOrder[j];
- const nextContent = generatedTexts[nextTag];
-
- if (!nextContent || usedTags.has(nextTag)) continue;
-
- const nextType = identifyElementType(nextTag);
-
- // Si on trouve un autre titre, on s'arrĂȘte
- if (nextType === 'titre_h1' || nextType === 'titre_h2' || nextType === 'titre_h3') {
- break;
- }
-
- // Si on trouve du contenu (texte, intro), c'est probablement associé
- if (nextType === 'texte' || nextType === 'intro') {
- return {
- content: nextContent,
- tag: nextTag
- };
- }
- }
-
- return { content: null, tag: null };
-}
-
-/**
- * Extraire le numĂ©ro d'une FAQ : |Faq_q_1| ou |Faq_a_2| â "1" ou "2"
- */
-function extractFAQNumber(tag) {
- const match = tag.match(/(\d+)/);
- return match ? match[1] : null;
-}
-
-/**
- * Trouver la réponse FAQ correspondant à une question
- */
-function findMatchingFAQAnswer(questionTag, generatedTexts) {
- // Extraire le numĂ©ro : |Faq_q_1| â 1
- const questionNumber = extractFAQNumber(questionTag);
-
- if (!questionNumber) return null;
-
- // Chercher la réponse correspondante
- for (const tag in generatedTexts) {
- const tagType = identifyElementType(tag);
-
- if (tagType === 'faq_reponse') {
- const answerNumber = extractFAQNumber(tag);
-
- if (answerNumber === questionNumber) {
- return {
- content: generatedTexts[tag],
- tag: tag
- };
- }
- }
- }
-
- return null;
-}
-
-/**
- * Nouvelle fonction de sauvegarde avec compilation organique
- */
-async function saveGeneratedArticleOrganic(articleData, csvData, config = {}) {
- try {
- logSh('đŸ Sauvegarde article avec compilation organique...', 'INFO');
-
- const sheets = await getSheetsClient();
-
- // đ Choisir la sheet selon le flag useVersionedSheet
- const targetSheetName = config.useVersionedSheet ? 'Generated_Articles_Versioned' : 'Generated_Articles';
- let articlesSheet = await getOrCreateSheet(sheets, targetSheetName);
-
- // ===== COMPILATION ORGANIQUE =====
- const compiledText = await compileGeneratedTextsOrganic(
- articleData.generatedTexts,
- articleData.originalElements // Passer les éléments originaux si disponibles
- );
-
- logSh(`đ Texte compilĂ© organiquement: ${compiledText.length} caractĂšres`, 'INFO');
-
- // Métadonnées avec format français
- const now = new Date();
- const frenchTimestamp = formatDateToFrench(now);
-
- // UTILISER le slug du CSV (colonne A du Google Sheet source)
- // Le slug doit venir de csvData.slug (récupéré via getBrainConfig)
- const slug = csvData.slug || generateSlugFromContent(csvData.mc0, csvData.t0);
-
- const metadata = {
- timestamp: frenchTimestamp,
- slug: slug,
- mc0: csvData.mc0,
- t0: csvData.t0,
- personality: csvData.personality?.nom || 'Unknown',
- antiDetectionLevel: config.antiDetectionLevel || 'MVP',
- elementsCount: Object.keys(articleData.generatedTexts || {}).length,
- textLength: compiledText.length,
- wordCount: countWords(compiledText),
- llmUsed: config.llmUsed || 'openai',
- validationStatus: articleData.validationReport?.status || 'unknown',
- // đ MĂ©tadonnĂ©es de versioning
- version: config.version || '1.0',
- stage: config.stage || 'final_version',
- stageDescription: config.stageDescription || 'Version finale',
- parentArticleId: config.parentArticleId || null,
- versionHistory: config.versionHistory || null
- };
-
- // Préparer la ligne de données avec versioning
- const row = [
- metadata.timestamp,
- metadata.slug,
- metadata.mc0,
- metadata.t0,
- metadata.personality,
- metadata.antiDetectionLevel,
- compiledText, // â TEXTE ORGANIQUE
- metadata.textLength,
- metadata.wordCount,
- metadata.elementsCount,
- metadata.llmUsed,
- metadata.validationStatus,
- // đ Colonnes de versioning
- metadata.version,
- metadata.stage,
- metadata.stageDescription,
- metadata.parentArticleId || '',
- '', '', '', '', // Colonnes de scores détecteurs (réservées)
- JSON.stringify({
- csvData: csvData,
- config: config,
- stats: metadata,
- versionHistory: metadata.versionHistory // Inclure l'historique
- })
- ];
-
- // DEBUG: Vérifier le slug généré
- logSh(`đŸ Sauvegarde avec slug: "${metadata.slug}" (colonne B)`, 'DEBUG');
-
- // Ajouter la ligne aux données dans la bonne sheet
- const targetRange = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:U' : 'Generated_Articles!A:U';
- await sheets.spreadsheets.values.append({
- spreadsheetId: SHEET_CONFIG.sheetId,
- range: targetRange,
- valueInputOption: 'USER_ENTERED',
- resource: {
- values: [row]
- }
- });
-
- // Récupérer le numéro de ligne pour l'ID article
- const targetRangeForId = config.useVersionedSheet ? 'Generated_Articles_Versioned!A:A' : 'Generated_Articles!A:A';
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_CONFIG.sheetId,
- range: targetRangeForId
- });
-
- const articleId = response.data.values ? response.data.values.length - 1 : 1;
-
- logSh(`â
Article organique sauvé: ID ${articleId}, ${metadata.wordCount} mots`, 'INFO');
-
- return {
- articleId: articleId,
- textLength: metadata.textLength,
- wordCount: metadata.wordCount,
- sheetRow: response.data.values ? response.data.values.length : 2
- };
-
- } catch (error) {
- logSh(`â Erreur sauvegarde organique: ${error.toString()}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * Générer un slug à partir du contenu MC0 et T0
- */
-function generateSlugFromContent(mc0, t0) {
- if (!mc0 && !t0) return 'article-generated';
-
- const source = mc0 || t0;
- return source
- .toString()
- .toLowerCase()
- .replace(/[à åùÀã]/g, 'a')
- .replace(/[ÚéĂȘĂ«]/g, 'e')
- .replace(/[ĂŹĂßï]/g, 'i')
- .replace(/[ĂČóÎöÔ]/g, 'o')
- .replace(/[ĂčĂșĂ»ĂŒ]/g, 'u')
- .replace(/[ç]/g, 'c')
- .replace(/[ñ]/g, 'n')
- .replace(/[^a-z0-9\s-]/g, '') // Enlever caractÚres spéciaux
- .replace(/\s+/g, '-') // Espaces -> tirets
- .replace(/-+/g, '-') // Ăviter doubles tirets
- .replace(/^-+|-+$/g, '') // Enlever tirets début/fin
- .substring(0, 50); // Limiter longueur
-}
-
-/**
- * Identifier le type d'élément par son tag
- */
-function identifyElementType(tag) {
- const cleanTag = tag.toLowerCase().replace(/[|{}]/g, '');
-
- if (cleanTag.includes('titre_h1') || cleanTag.includes('h1')) return 'titre_h1';
- if (cleanTag.includes('titre_h2') || cleanTag.includes('h2')) return 'titre_h2';
- if (cleanTag.includes('titre_h3') || cleanTag.includes('h3')) return 'titre_h3';
- if (cleanTag.includes('intro')) return 'intro';
- if (cleanTag.includes('faq_q') || cleanTag.includes('faq_question')) return 'faq_question';
- if (cleanTag.includes('faq_a') || cleanTag.includes('faq_reponse')) return 'faq_reponse';
-
- return 'texte'; // Par défaut
-}
-
-/**
- * Nettoyer un contenu individuel
- */
-function cleanIndividualContent(content) {
- if (!content) return '';
-
- let cleaned = content.toString();
-
- // 1. Supprimer les balises HTML
- cleaned = cleaned.replace(/<[^>]*>/g, '');
-
- // 2. Décoder les entités HTML
- cleaned = cleaned.replace(/</g, '<');
- cleaned = cleaned.replace(/>/g, '>');
- cleaned = cleaned.replace(/&/g, '&');
- cleaned = cleaned.replace(/"/g, '"');
- cleaned = cleaned.replace(/'/g, "'");
- cleaned = cleaned.replace(/ /g, ' ');
-
- // 3. Nettoyer les espaces
- cleaned = cleaned.replace(/\s+/g, ' ');
- cleaned = cleaned.replace(/\n\s+/g, '\n');
-
- // 4. Supprimer les caractÚres de contrÎle étranges
- cleaned = cleaned.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
-
- return cleaned.trim();
-}
-
-/**
- * Créer la sheet de stockage avec headers appropriés
- */
-async function createArticlesStorageSheet(sheets, sheetName = 'Generated_Articles') {
- logSh(`đïž CrĂ©ation sheet ${sheetName}...`, 'INFO');
-
- try {
- // Créer la nouvelle sheet
- await sheets.spreadsheets.batchUpdate({
- spreadsheetId: SHEET_CONFIG.sheetId,
- resource: {
- requests: [{
- addSheet: {
- properties: {
- title: sheetName
- }
- }
- }]
- }
- });
-
- // Headers avec versioning
- const headers = [
- 'Timestamp',
- 'Slug',
- 'MC0',
- 'T0',
- 'Personality',
- 'AntiDetection_Level',
- 'Compiled_Text', // â COLONNE PRINCIPALE
- 'Text_Length',
- 'Word_Count',
- 'Elements_Count',
- 'LLM_Used',
- 'Validation_Status',
- // đ Colonnes de versioning
- 'Version', // v1.0, v1.1, v1.2, v2.0
- 'Stage', // initial_generation, selective_enhancement, etc.
- 'Stage_Description', // Description détaillée de l'étape
- 'Parent_Article_ID', // ID de l'article parent (pour linkage)
- 'GPTZero_Score', // Scores détecteurs (à remplir)
- 'Originality_Score',
- 'CopyLeaks_Score',
- 'Human_Quality_Score',
- 'Full_Metadata_JSON' // Backup complet avec historique
- ];
-
- // Ajouter les headers
- await sheets.spreadsheets.values.update({
- spreadsheetId: SHEET_CONFIG.sheetId,
- range: `${sheetName}!A1:U1`,
- valueInputOption: 'USER_ENTERED',
- resource: {
- values: [headers]
- }
- });
-
- // Formatter les headers
- await sheets.spreadsheets.batchUpdate({
- spreadsheetId: SHEET_CONFIG.sheetId,
- resource: {
- requests: [{
- repeatCell: {
- range: {
- sheetId: await getSheetIdByName(sheets, sheetName),
- startRowIndex: 0,
- endRowIndex: 1,
- startColumnIndex: 0,
- endColumnIndex: headers.length
- },
- cell: {
- userEnteredFormat: {
- textFormat: {
- bold: true
- },
- backgroundColor: {
- red: 0.878,
- green: 0.878,
- blue: 0.878
- },
- horizontalAlignment: 'CENTER'
- }
- },
- fields: 'userEnteredFormat(textFormat,backgroundColor,horizontalAlignment)'
- }
- }]
- }
- });
-
- logSh(`â
Sheet ${sheetName} créée avec succÚs`, 'INFO');
- return true;
-
- } catch (error) {
- logSh(`â Erreur crĂ©ation sheet: ${error.toString()}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * Formater date au format français DD/MM/YYYY HH:mm:ss
- */
-function formatDateToFrench(date) {
- // Utiliser toLocaleString avec le format français
- return date.toLocaleString('fr-FR', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false,
- timeZone: 'Europe/Paris'
- }).replace(',', '');
-}
-
-/**
- * Compter les mots dans un texte
- */
-function countWords(text) {
- if (!text || text.trim() === '') return 0;
- return text.trim().split(/\s+/).length;
-}
-
-/**
- * Récupérer un article sauvé par ID
- */
-async function getStoredArticle(articleId) {
- try {
- const sheets = await getSheetsClient();
-
- const rowNumber = articleId + 2; // +2 car header + 0-indexing
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_CONFIG.sheetId,
- range: `Generated_Articles!A${rowNumber}:Q${rowNumber}`
- });
-
- if (!response.data.values || response.data.values.length === 0) {
- throw new Error(`Article ${articleId} non trouvé`);
- }
-
- const data = response.data.values[0];
-
- return {
- articleId: articleId,
- timestamp: data[0],
- slug: data[1],
- mc0: data[2],
- t0: data[3],
- personality: data[4],
- antiDetectionLevel: data[5],
- compiledText: data[6], // â TEXTE PUR
- textLength: data[7],
- wordCount: data[8],
- elementsCount: data[9],
- llmUsed: data[10],
- validationStatus: data[11],
- gptZeroScore: data[12],
- originalityScore: data[13],
- copyLeaksScore: data[14],
- humanScore: data[15],
- fullMetadata: data[16] ? JSON.parse(data[16]) : null
- };
-
- } catch (error) {
- logSh(`â Erreur rĂ©cupĂ©ration article ${articleId}: ${error.toString()}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * Lister les derniers articles générés
- */
-async function getRecentArticles(limit = 10) {
- try {
- const sheets = await getSheetsClient();
-
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_CONFIG.sheetId,
- range: 'Generated_Articles!A:L'
- });
-
- if (!response.data.values || response.data.values.length <= 1) {
- return []; // Pas de données ou seulement headers
- }
-
- const data = response.data.values.slice(1); // Exclure headers
- const startIndex = Math.max(0, data.length - limit);
- const recentData = data.slice(startIndex);
-
- return recentData.map((row, index) => ({
- articleId: startIndex + index,
- timestamp: row[0],
- slug: row[1],
- mc0: row[2],
- personality: row[4],
- antiDetectionLevel: row[5],
- wordCount: row[8],
- validationStatus: row[11]
- })).reverse(); // Plus récents en premier
-
- } catch (error) {
- logSh(`â Erreur liste articles rĂ©cents: ${error.toString()}`, 'ERROR');
- return [];
- }
-}
-
-/**
- * Mettre à jour les scores de détection d'un article
- */
-async function updateDetectionScores(articleId, scores) {
- try {
- const sheets = await getSheetsClient();
- const rowNumber = articleId + 2;
-
- const updates = [];
-
- // Colonnes des scores : M, N, O (GPTZero, Originality, CopyLeaks)
- if (scores.gptzero !== undefined) {
- updates.push({
- range: `Generated_Articles!M${rowNumber}`,
- values: [[scores.gptzero]]
- });
- }
- if (scores.originality !== undefined) {
- updates.push({
- range: `Generated_Articles!N${rowNumber}`,
- values: [[scores.originality]]
- });
- }
- if (scores.copyleaks !== undefined) {
- updates.push({
- range: `Generated_Articles!O${rowNumber}`,
- values: [[scores.copyleaks]]
- });
- }
-
- if (updates.length > 0) {
- await sheets.spreadsheets.values.batchUpdate({
- spreadsheetId: SHEET_CONFIG.sheetId,
- resource: {
- valueInputOption: 'USER_ENTERED',
- data: updates
- }
- });
- }
-
- logSh(`â
Scores détection mis à jour pour article ${articleId}`, 'INFO');
-
- } catch (error) {
- logSh(`â Erreur maj scores article ${articleId}: ${error.toString()}`, 'ERROR');
- throw error;
- }
-}
-
-// ============= HELPERS GOOGLE SHEETS =============
-
-/**
- * Obtenir le client Google Sheets authentifié
- */
-async function getSheetsClient() {
- const auth = new google.auth.GoogleAuth({
- credentials: {
- client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
- private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n')
- },
- scopes: ['https://www.googleapis.com/auth/spreadsheets']
- });
-
- const authClient = await auth.getClient();
- const sheets = google.sheets({ version: 'v4', auth: authClient });
-
- return sheets;
-}
-
-/**
- * Obtenir ou créer une sheet
- */
-async function getOrCreateSheet(sheets, sheetName) {
- try {
- // Vérifier si la sheet existe
- const response = await sheets.spreadsheets.get({
- spreadsheetId: SHEET_CONFIG.sheetId
- });
-
- const existingSheet = response.data.sheets.find(
- sheet => sheet.properties.title === sheetName
- );
-
- if (existingSheet) {
- return existingSheet;
- } else {
- // Créer la sheet si elle n'existe pas
- if (sheetName === 'Generated_Articles' || sheetName === 'Generated_Articles_Versioned') {
- await createArticlesStorageSheet(sheets, sheetName);
- return await getOrCreateSheet(sheets, sheetName); // Récursif pour récupérer la sheet créée
- }
- throw new Error(`Sheet ${sheetName} non supportée pour création automatique`);
- }
-
- } catch (error) {
- logSh(`â Erreur accĂšs/crĂ©ation sheet ${sheetName}: ${error.toString()}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * Obtenir l'ID d'une sheet par son nom
- */
-async function getSheetIdByName(sheets, sheetName) {
- const response = await sheets.spreadsheets.get({
- spreadsheetId: SHEET_CONFIG.sheetId
- });
-
- const sheet = response.data.sheets.find(
- s => s.properties.title === sheetName
- );
-
- return sheet ? sheet.properties.sheetId : null;
-}
-
-// ============= EXPORTS =============
-
-module.exports = {
- compileGeneratedTextsOrganic,
- buildOrganicSections,
- findAssociatedContent,
- extractFAQNumber,
- findMatchingFAQAnswer,
- saveGeneratedArticleOrganic,
- identifyElementType,
- cleanIndividualContent,
- createArticlesStorageSheet,
- formatDateToFrench,
- countWords,
- getStoredArticle,
- getRecentArticles,
- updateDetectionScores,
- getSheetsClient,
- getOrCreateSheet,
- getSheetIdByName,
- generateSlugFromContent
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/TechnicalLayer.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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 = 'openai';
- 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');
- logSh(`đ Candidates reçus: ${JSON.stringify(candidates.map(c => c.tag))}`, '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'],
- missingTerms: [] // Ajout de la propriété manquante
- }));
-
- 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 };
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/TransitionLayer.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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 };
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/StyleLayer.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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 };
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/SelectiveCore.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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': 'openai', // OpenAI 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
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/selective-enhancement/SelectiveLayers.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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 OpenAI',
- layers: [
- { type: 'technical', llm: 'openai', intensity: 0.7 }
- ],
- layersCount: 1
- },
-
- // Stack standard - Technique + Transitions
- standardEnhancement: {
- name: 'standardEnhancement',
- description: 'Amélioration technique et fluidité (OpenAI + Gemini)',
- layers: [
- { type: 'technical', llm: 'openai', 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 (OpenAI + Gemini + Mistral)',
- layers: [
- { type: 'technical', llm: 'openai', 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: 'openai', 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: 'openai', 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: 'openai',
- 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
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/adversarial-generation/DetectorStrategies.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/adversarial-generation/AdversarialCore.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// ADVERSARIAL CORE - MOTEUR MODULAIRE
-// Responsabilité: Moteur adversarial réutilisable sur tout contenu
-// Architecture: Couches applicables Ă la demande
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-const { tracer } = require('../trace');
-const { callLLM } = require('../LLMManager');
-
-// Import stratégies et utilitaires
-const { DetectorStrategyFactory, selectOptimalStrategy } = require('./DetectorStrategies');
-
-/**
- * MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE
- * Input: contenu existant + configuration adversariale
- * Output: contenu avec couche adversariale appliquée
- */
-async function applyAdversarialLayer(existingContent, config = {}) {
- return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => {
- const {
- detectorTarget = 'general',
- intensity = 1.0,
- method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid'
- preserveStructure = true,
- csvData = null,
- context = {}
- } = 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 strategy = DetectorStrategyFactory.createStrategy(detectorTarget);
-
- // 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
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/adversarial-generation/AdversarialLayers.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// 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
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/FatiguePatterns.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: FatiguePatterns.js
-// RESPONSABILITĂ: Simulation fatigue cognitive
-// Implémentation courbe fatigue exacte du plan.md
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * PROFILS DE FATIGUE PAR PERSONNALITĂ
- * Basé sur les 15 personnalités du systÚme
- */
-const FATIGUE_PROFILES = {
- // Techniques - Résistent plus longtemps
- marc: { peakAt: 0.45, recovery: 0.85, intensity: 0.8 },
- amara: { peakAt: 0.43, recovery: 0.87, intensity: 0.7 },
- yasmine: { peakAt: 0.47, recovery: 0.83, intensity: 0.75 },
- fabrice: { peakAt: 0.44, recovery: 0.86, intensity: 0.8 },
-
- // Créatifs - Fatigue plus variable
- sophie: { peakAt: 0.55, recovery: 0.90, intensity: 1.0 },
- émilie: { peakAt: 0.52, recovery: 0.88, intensity: 0.9 },
- chloé: { peakAt: 0.58, recovery: 0.92, intensity: 1.1 },
- minh: { peakAt: 0.53, recovery: 0.89, intensity: 0.95 },
-
- // Commerciaux - Fatigue rapide mais récupération
- laurent: { peakAt: 0.40, recovery: 0.80, intensity: 1.2 },
- julie: { peakAt: 0.38, recovery: 0.78, intensity: 1.0 },
-
- // Terrain - Endurance élevée
- kévin: { peakAt: 0.35, recovery: 0.75, intensity: 0.6 },
- mamadou: { peakAt: 0.37, recovery: 0.77, intensity: 0.65 },
- linh: { peakAt: 0.36, recovery: 0.76, intensity: 0.7 },
-
- // Patrimoniaux - Fatigue progressive
- 'pierre-henri': { peakAt: 0.48, recovery: 0.82, intensity: 0.85 },
- thierry: { peakAt: 0.46, recovery: 0.84, intensity: 0.8 },
-
- // Profil par défaut
- default: { peakAt: 0.50, recovery: 0.85, intensity: 1.0 }
-};
-
-/**
- * CALCUL FATIGUE COGNITIVE - FORMULE EXACTE DU PLAN
- * Peak Ă 50% de progression selon courbe sinusoĂŻdale
- * @param {number} elementIndex - Position élément (0-based)
- * @param {number} totalElements - Nombre total d'éléments
- * @returns {number} - Niveau fatigue (0-0.8)
- */
-function calculateFatigue(elementIndex, totalElements) {
- if (totalElements <= 1) return 0;
-
- const position = elementIndex / totalElements;
- const fatigueLevel = Math.sin(position * Math.PI) * 0.8; // Peak Ă 50%
-
- logSh(`đ§ Fatigue calculĂ©e: position=${position.toFixed(2)}, niveau=${fatigueLevel.toFixed(2)}`, 'DEBUG');
-
- return Math.max(0, fatigueLevel);
-}
-
-/**
- * OBTENIR PROFIL FATIGUE PAR PERSONNALITĂ
- * @param {string} personalityName - Nom personnalité
- * @returns {object} - Profil fatigue
- */
-function getFatigueProfile(personalityName) {
- const normalizedName = personalityName?.toLowerCase() || 'default';
- const profile = FATIGUE_PROFILES[normalizedName] || FATIGUE_PROFILES.default;
-
- logSh(`đ Profil fatigue sĂ©lectionnĂ© pour ${personalityName}: peakAt=${profile.peakAt}, intensity=${profile.intensity}`, 'DEBUG');
-
- return profile;
-}
-
-/**
- * INJECTION MARQUEURS DE FATIGUE
- * @param {string} content - Contenu Ă modifier
- * @param {number} fatigueLevel - Niveau fatigue (0-0.8)
- * @param {object} options - Options { profile, intensity }
- * @returns {object} - { content, modifications }
- */
-function injectFatigueMarkers(content, fatigueLevel, options = {}) {
- if (!content || fatigueLevel < 0.05) { // FIXĂ: Seuil beaucoup plus bas (Ă©tait 0.2)
- return { content, modifications: 0 };
- }
-
- const profile = options.profile || FATIGUE_PROFILES.default;
- const baseIntensity = options.intensity || 1.0;
-
- // Intensité ajustée selon personnalité
- const adjustedIntensity = fatigueLevel * profile.intensity * baseIntensity;
-
- logSh(`đ€ Injection fatigue: niveau=${fatigueLevel.toFixed(2)}, intensitĂ©=${adjustedIntensity.toFixed(2)}`, 'DEBUG');
-
- let modifiedContent = content;
- let modifications = 0;
-
- // ========================================
- // FATIGUE LĂGĂRE (0.05 - 0.4) - FIXĂ: Seuil plus bas
- // ========================================
- if (fatigueLevel >= 0.05 && fatigueLevel < 0.4) {
- const lightFatigueResult = applyLightFatigue(modifiedContent, adjustedIntensity);
- modifiedContent = lightFatigueResult.content;
- modifications += lightFatigueResult.count;
- }
-
- // ========================================
- // FATIGUE MODĂRĂE (0.4 - 0.6)
- // ========================================
- if (fatigueLevel >= 0.4 && fatigueLevel < 0.6) {
- const moderateFatigueResult = applyModerateFatigue(modifiedContent, adjustedIntensity);
- modifiedContent = moderateFatigueResult.content;
- modifications += moderateFatigueResult.count;
- }
-
- // ========================================
- // FATIGUE ĂLEVĂE (0.6+)
- // ========================================
- if (fatigueLevel >= 0.6) {
- const heavyFatigueResult = applyHeavyFatigue(modifiedContent, adjustedIntensity);
- modifiedContent = heavyFatigueResult.content;
- modifications += heavyFatigueResult.count;
- }
-
- logSh(`đ€ Fatigue appliquĂ©e: ${modifications} modifications`, 'DEBUG');
-
- return {
- content: modifiedContent,
- modifications
- };
-}
-
-/**
- * FATIGUE LĂGĂRE - Connecteurs simplifiĂ©s
- */
-function applyLightFatigue(content, intensity) {
- let modified = content;
- let count = 0;
-
- // Probabilité d'application basée sur l'intensité - ENCORE PLUS AGRESSIF
- const shouldApply = Math.random() < (intensity * 0.9); // FIXĂ: 90% chance d'appliquer
- if (!shouldApply) return { content: modified, count };
-
- // Simplification des connecteurs complexes - ĂLARGI
- const complexConnectors = [
- { from: /néanmoins/gi, to: 'cependant' },
- { from: /par conséquent/gi, to: 'donc' },
- { from: /ainsi que/gi, to: 'et' },
- { from: /en outre/gi, to: 'aussi' },
- { from: /de surcroĂźt/gi, to: 'de plus' },
- // NOUVEAUX AJOUTS AGRESSIFS
- { from: /toutefois/gi, to: 'mais' },
- { from: /cependant/gi, to: 'mais bon' },
- { from: /par ailleurs/gi, to: 'sinon' },
- { from: /en effet/gi, to: 'effectivement' },
- { from: /de fait/gi, to: 'en fait' }
- ];
-
- complexConnectors.forEach(connector => {
- const matches = modified.match(connector.from);
- if (matches && Math.random() < 0.9) { // FIXĂ: 90% chance trĂšs agressive
- modified = modified.replace(connector.from, connector.to);
- count++;
- }
- });
-
- // AJOUT FIX: Si aucun connecteur complexe trouvé, appliquer une modification alternative
- if (count === 0 && Math.random() < 0.7) {
- // Injecter des simplifications basiques
- if (modified.includes(' et ') && Math.random() < 0.5) {
- modified = modified.replace(' et ', ' puis ');
- count++;
- }
- }
-
- return { content: modified, count };
-}
-
-/**
- * FATIGUE MODĂRĂE - Phrases plus courtes
- */
-function applyModerateFatigue(content, intensity) {
- let modified = content;
- let count = 0;
-
- const shouldApply = Math.random() < (intensity * 0.5);
- if (!shouldApply) return { content: modified, count };
-
- // Découpage phrases longues (>120 caractÚres)
- const sentences = modified.split('. ');
- const processedSentences = sentences.map(sentence => {
- if (sentence.length > 120 && Math.random() < 0.3) { // 30% chance
- // Trouver un point de découpe logique
- const cutPoints = [', qui', ', que', ', dont', ' et ', ' car '];
- for (const cutPoint of cutPoints) {
- const cutIndex = sentence.indexOf(cutPoint);
- if (cutIndex > 30 && cutIndex < sentence.length - 30) {
- count++;
- return sentence.substring(0, cutIndex) + '. ' +
- sentence.substring(cutIndex + cutPoint.length);
- }
- }
- }
- return sentence;
- });
-
- modified = processedSentences.join('. ');
-
- // Vocabulaire plus simple
- const simplifications = [
- { from: /optimisation/gi, to: 'amélioration' },
- { from: /méthodologie/gi, to: 'méthode' },
- { from: /problématique/gi, to: 'problÚme' },
- { from: /spécifications/gi, to: 'détails' }
- ];
-
- simplifications.forEach(simpl => {
- if (modified.match(simpl.from) && Math.random() < 0.3) {
- modified = modified.replace(simpl.from, simpl.to);
- count++;
- }
- });
-
- return { content: modified, count };
-}
-
-/**
- * FATIGUE ĂLEVĂE - RĂ©pĂ©titions et vocabulaire basique
- */
-function applyHeavyFatigue(content, intensity) {
- let modified = content;
- let count = 0;
-
- const shouldApply = Math.random() < (intensity * 0.7);
- if (!shouldApply) return { content: modified, count };
-
- // Injection répétitions naturelles
- const repetitionWords = ['bien', 'trĂšs', 'vraiment', 'assez', 'plutĂŽt'];
- const sentences = modified.split('. ');
-
- sentences.forEach((sentence, index) => {
- if (Math.random() < 0.2 && sentence.length > 50) { // 20% chance
- const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)];
- // Injecter le mot répétitif au milieu de la phrase
- const words = sentence.split(' ');
- const insertIndex = Math.floor(words.length / 2);
- words.splice(insertIndex, 0, word);
- sentences[index] = words.join(' ');
- count++;
- }
- });
-
- modified = sentences.join('. ');
-
- // Vocabulaire trĂšs basique
- const basicVocab = [
- { from: /excellente?/gi, to: 'bonne' },
- { from: /remarquable/gi, to: 'bien' },
- { from: /sophistiqué/gi, to: 'avancé' },
- { from: /performant/gi, to: 'efficace' },
- { from: /innovations?/gi, to: 'nouveautés' }
- ];
-
- basicVocab.forEach(vocab => {
- if (modified.match(vocab.from) && Math.random() < 0.4) {
- modified = modified.replace(vocab.from, vocab.to);
- count++;
- }
- });
-
- // Hésitations légÚres (rare)
- if (Math.random() < 0.1) { // 10% chance
- const hesitations = ['... enfin', '... disons', '... comment dire'];
- const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)];
- const words = modified.split(' ');
- const insertIndex = Math.floor(words.length * 0.7); // Vers la fin
- words.splice(insertIndex, 0, hesitation);
- modified = words.join(' ');
- count++;
- }
-
- return { content: modified, count };
-}
-
-/**
- * RĂCUPĂRATION FATIGUE (pour les Ă©lĂ©ments en fin)
- * @param {string} content - Contenu Ă traiter
- * @param {number} recoveryLevel - Niveau récupération (0-1)
- * @returns {object} - { content, modifications }
- */
-function applyFatigueRecovery(content, recoveryLevel) {
- if (recoveryLevel < 0.8) return { content, modifications: 0 };
-
- let modified = content;
- let count = 0;
-
- // Réintroduire vocabulaire plus sophistiqué
- const recoveryVocab = [
- { from: /\bbien\b/gi, to: 'excellent' },
- { from: /\befficace\b/gi, to: 'performant' },
- { from: /\bméthode\b/gi, to: 'méthodologie' }
- ];
-
- recoveryVocab.forEach(vocab => {
- if (modified.match(vocab.from) && Math.random() < 0.3) {
- modified = modified.replace(vocab.from, vocab.to);
- count++;
- }
- });
-
- return { content: modified, count };
-}
-
-// ============= EXPORTS =============
-module.exports = {
- calculateFatigue,
- getFatigueProfile,
- injectFatigueMarkers,
- applyLightFatigue,
- applyModerateFatigue,
- applyHeavyFatigue,
- applyFatigueRecovery,
- FATIGUE_PROFILES
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/PersonalityErrors.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: PersonalityErrors.js
-// RESPONSABILITĂ: Erreurs cohĂ©rentes par personnalitĂ©
-// 15 profils d'erreurs basés sur les personnalités systÚme
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * PATTERNS D'ERREURS PAR PERSONNALITĂ
- * Basé sur les 15 personnalités du BrainConfig
- * Chaque personnalité a ses tics linguistiques et erreurs typiques
- */
-const PERSONALITY_ERROR_PATTERNS = {
-
- // ========================================
- // PERSONNALITĂS TECHNIQUES
- // ========================================
- marc: {
- name: 'Marc - Expert Technique',
- tendencies: ['sur-technicisation', 'anglicismes techniques', 'jargon professionnel'],
- repetitions: ['précis', 'efficace', 'optimal', 'performant', 'systÚme'],
- syntaxErrors: [
- 'phrases techniques non finies',
- 'parenthĂšses explicatives excessives',
- 'abréviations sans développement'
- ],
- vocabularyTics: ['niveau technique', 'en termes de', 'au niveau de'],
- anglicisms: ['upgrade', 'process', 'workflow', 'pipeline'],
- errorFrequency: 0.7 // Probabilité base
- },
-
- amara: {
- name: 'Amara - Ingénieure SystÚme',
- tendencies: ['méthodologie rigide', 'références normes', 'vocabulaire industriel'],
- repetitions: ['conforme', 'standard', 'spécifications', 'protocole'],
- syntaxErrors: ['énumérations lourdes', 'références normatives'],
- vocabularyTics: ['selon les normes', 'conformément à ', 'dans le respect de'],
- anglicisms: ['compliance', 'standard', 'guidelines'],
- errorFrequency: 0.6
- },
-
- yasmine: {
- name: 'Yasmine - GreenTech',
- tendencies: ['éco-vocabulaire répétitif', 'superlatifs environnementaux'],
- repetitions: ['durable', 'écologique', 'responsable', 'vert', 'bio'],
- syntaxErrors: ['accumulation adjectifs éco', 'phrases militantes'],
- vocabularyTics: ['respectueux de l\'environnement', 'développement durable'],
- anglicisms: ['green', 'eco-friendly', 'sustainable'],
- errorFrequency: 0.8
- },
-
- fabrice: {
- name: 'Fabrice - Métallurgie',
- tendencies: ['vocabulaire métier spécialisé', 'références techniques'],
- repetitions: ['résistant', 'robuste', 'solide', 'qualité', 'finition'],
- syntaxErrors: ['termes techniques sans explication'],
- vocabularyTics: ['en terme de résistance', 'question de solidité'],
- anglicisms: ['coating', 'finish', 'design'],
- errorFrequency: 0.5
- },
-
- // ========================================
- // PERSONNALITĂS CRĂATIVES
- // ========================================
- sophie: {
- name: 'Sophie - Déco Design',
- tendencies: ['vocabulaire déco répétitif', 'superlatifs esthétiques'],
- repetitions: ['magnifique', 'élégant', 'harmonieux', 'raffiné', 'style'],
- syntaxErrors: ['accord couleurs/matiĂšres', 'accumulation adjectifs'],
- vocabularyTics: ['en terme de style', 'au niveau esthétique', 'cÎté design'],
- anglicisms: ['design', 'style', 'trendy', 'vintage'],
- errorFrequency: 0.9
- },
-
- émilie: {
- name: 'Ămilie - Digital Native',
- tendencies: ['anglicismes numériques', 'vocabulaire web'],
- repetitions: ['digital', 'online', 'connecté', 'smart', 'moderne'],
- syntaxErrors: ['néologismes numériques'],
- vocabularyTics: ['au niveau digital', 'cÎté technologique'],
- anglicisms: ['user-friendly', 'responsive', 'digital', 'smart'],
- errorFrequency: 1.0
- },
-
- chloé: {
- name: 'Chloé - Content Creator',
- tendencies: ['ton familier', 'expressions actuelles', 'anglicismes réseaux'],
- repetitions: ['super', 'génial', 'top', 'incontournable', 'tendance'],
- syntaxErrors: ['familiarités', 'expressions jeunes'],
- vocabularyTics: ['c\'est vraiment', 'on va dire que', 'du coup'],
- anglicisms: ['content', 'trending', 'viral', 'lifestyle'],
- errorFrequency: 1.1
- },
-
- minh: {
- name: 'Minh - Designer Industriel',
- tendencies: ['références design', 'vocabulaire forme/fonction'],
- repetitions: ['fonctionnel', 'ergonomique', 'esthétique', 'innovant'],
- syntaxErrors: ['descriptions techniques design'],
- vocabularyTics: ['en terme de design', 'niveau ergonomie'],
- anglicisms: ['design', 'user experience', 'ergonomic'],
- errorFrequency: 0.7
- },
-
- // ========================================
- // PERSONNALITĂS COMMERCIALES
- // ========================================
- laurent: {
- name: 'Laurent - Commercial BtoB',
- tendencies: ['vocabulaire vente', 'superlatifs commerciaux'],
- repetitions: ['excellent', 'exceptionnel', 'unique', 'incontournable'],
- syntaxErrors: ['promesses excessives', 'superlatifs empilés'],
- vocabularyTics: ['c\'est vraiment', 'je vous garantis', 'sans aucun doute'],
- anglicisms: ['business', 'deal', 'top niveau'],
- errorFrequency: 1.2
- },
-
- julie: {
- name: 'Julie - Architecture Commerciale',
- tendencies: ['vocabulaire technique commercial', 'références projets'],
- repetitions: ['projet', 'réalisation', 'conception', 'sur-mesure'],
- syntaxErrors: ['énumérations projets'],
- vocabularyTics: ['dans le cadre de', 'au niveau projet'],
- anglicisms: ['design', 'custom', 'high-end'],
- errorFrequency: 0.8
- },
-
- // ========================================
- // PERSONNALITĂS TERRAIN
- // ========================================
- kévin: {
- name: 'Kévin - Homme de Terrain',
- tendencies: ['expressions familiĂšres', 'vocabulaire pratique'],
- repetitions: ['pratique', 'concret', 'simple', 'direct', 'efficace'],
- syntaxErrors: ['tournures familiĂšres', 'expressions populaires'],
- vocabularyTics: ['franchement', 'concrĂštement', 'dans les faits'],
- anglicisms: ['basique', 'standard'],
- errorFrequency: 0.6
- },
-
- mamadou: {
- name: 'Mamadou - Artisan Expérimenté',
- tendencies: ['références tradition', 'vocabulaire métier'],
- repetitions: ['traditionnel', 'artisanal', 'savoir-faire', 'qualité'],
- syntaxErrors: ['expressions métier', 'références tradition'],
- vocabularyTics: ['comme on dit', 'dans le métier', 'selon l\'expérience'],
- anglicisms: [], // Ăvite les anglicismes
- errorFrequency: 0.4
- },
-
- linh: {
- name: 'Linh - Production Industrielle',
- tendencies: ['vocabulaire production', 'références process'],
- repetitions: ['production', 'fabrication', 'process', 'qualité', 'série'],
- syntaxErrors: ['termes production techniques'],
- vocabularyTics: ['au niveau production', 'cÎté fabrication'],
- anglicisms: ['process', 'manufacturing', 'quality'],
- errorFrequency: 0.5
- },
-
- // ========================================
- // PERSONNALITĂS PATRIMOINE
- // ========================================
- 'pierre-henri': {
- name: 'Pierre-Henri - Patrimoine Classique',
- tendencies: ['vocabulaire soutenu', 'références historiques'],
- repetitions: ['traditionnel', 'authentique', 'noble', 'raffinement', 'héritage'],
- syntaxErrors: ['formulations recherchées', 'références culturelles'],
- vocabularyTics: ['il convient de', 'il est Ă noter que', 'dans la tradition'],
- anglicisms: [], // Ăvite complĂštement
- errorFrequency: 0.3
- },
-
- thierry: {
- name: 'Thierry - Créole Authentique',
- tendencies: ['expressions créoles', 'tournures locales'],
- repetitions: ['authentique', 'local', 'tradition', 'racines'],
- syntaxErrors: ['tournures créoles', 'expressions locales'],
- vocabularyTics: ['comme on dit chez nous', 'dans nos traditions'],
- anglicisms: [], // Privilégie le français local
- errorFrequency: 0.8
- }
-};
-
-/**
- * OBTENIR PROFIL D'ERREURS PAR PERSONNALITĂ
- * @param {string} personalityName - Nom personnalité
- * @returns {object} - Profil d'erreurs
- */
-function getPersonalityErrorPatterns(personalityName) {
- const normalizedName = personalityName?.toLowerCase() || 'default';
- const profile = PERSONALITY_ERROR_PATTERNS[normalizedName];
-
- if (!profile) {
- logSh(`â ïž Profil erreurs non trouvĂ© pour ${personalityName}, utilisation profil gĂ©nĂ©rique`, 'WARNING');
- return createGenericErrorProfile();
- }
-
- logSh(`đ Profil erreurs sĂ©lectionnĂ© pour ${personalityName}: ${profile.name}`, 'DEBUG');
- return profile;
-}
-
-/**
- * PROFIL D'ERREURS GĂNĂRIQUE
- */
-function createGenericErrorProfile() {
- return {
- name: 'Profil Générique',
- tendencies: ['répétitions standard', 'vocabulaire neutre'],
- repetitions: ['bien', 'bon', 'intéressant', 'important'],
- syntaxErrors: ['phrases standards'],
- vocabularyTics: ['en effet', 'par ailleurs', 'de plus'],
- anglicisms: [],
- errorFrequency: 0.5
- };
-}
-
-/**
- * INJECTION ERREURS PERSONNALITĂ
- * @param {string} content - Contenu Ă modifier
- * @param {object} personalityProfile - Profil personnalité
- * @param {number} intensity - Intensité (0-2.0)
- * @returns {object} - { content, modifications }
- */
-function injectPersonalityErrors(content, personalityProfile, intensity = 1.0) {
- if (!content || !personalityProfile) {
- return { content, modifications: 0 };
- }
-
- logSh(`đ Injection erreurs personnalitĂ©: ${personalityProfile.name}`, 'DEBUG');
-
- let modifiedContent = content;
- let modifications = 0;
-
- // Probabilité d'application basée sur l'intensité et la fréquence du profil
- const baseFrequency = personalityProfile.errorFrequency || 0.5;
- const adjustedProbability = Math.min(1.0, baseFrequency * intensity);
-
- logSh(`đŻ ProbabilitĂ© erreurs: ${adjustedProbability.toFixed(2)} (base: ${baseFrequency}, intensitĂ©: ${intensity})`, 'DEBUG');
-
- // ========================================
- // 1. RĂPĂTITIONS CARACTĂRISTIQUES
- // ========================================
- const repetitionResult = injectRepetitions(modifiedContent, personalityProfile, adjustedProbability);
- modifiedContent = repetitionResult.content;
- modifications += repetitionResult.count;
-
- // ========================================
- // 2. TICS VOCABULAIRE
- // ========================================
- const vocabularyResult = injectVocabularyTics(modifiedContent, personalityProfile, adjustedProbability);
- modifiedContent = vocabularyResult.content;
- modifications += vocabularyResult.count;
-
- // ========================================
- // 3. ANGLICISMES (SI APPLICABLE)
- // ========================================
- if (personalityProfile.anglicisms && personalityProfile.anglicisms.length > 0) {
- const anglicismResult = injectAnglicisms(modifiedContent, personalityProfile, adjustedProbability * 0.3);
- modifiedContent = anglicismResult.content;
- modifications += anglicismResult.count;
- }
-
- // ========================================
- // 4. ERREURS SYNTAXIQUES TYPIQUES
- // ========================================
- const syntaxResult = injectSyntaxErrors(modifiedContent, personalityProfile, adjustedProbability * 0.2);
- modifiedContent = syntaxResult.content;
- modifications += syntaxResult.count;
-
- logSh(`đ Erreurs personnalitĂ© injectĂ©es: ${modifications} modifications`, 'DEBUG');
-
- return {
- content: modifiedContent,
- modifications
- };
-}
-
-/**
- * INJECTION RĂPĂTITIONS CARACTĂRISTIQUES
- */
-function injectRepetitions(content, profile, probability) {
- let modified = content;
- let count = 0;
-
- if (!profile.repetitions || profile.repetitions.length === 0) {
- return { content: modified, count };
- }
-
- // SĂ©lectionner 1-3 mots rĂ©pĂ©titifs pour ce contenu - FIXĂ: Plus de mots
- const selectedWords = profile.repetitions
- .sort(() => 0.5 - Math.random())
- .slice(0, Math.random() < 0.5 ? 2 : 3); // FIXĂ: Au moins 2 mots sĂ©lectionnĂ©s
-
- selectedWords.forEach(word => {
- if (Math.random() < probability) {
- // Chercher des endroits appropriés pour injecter le mot
- const sentences = modified.split('. ');
- const targetSentenceIndex = Math.floor(Math.random() * sentences.length);
-
- if (sentences[targetSentenceIndex] &&
- sentences[targetSentenceIndex].length > 30 &&
- !sentences[targetSentenceIndex].toLowerCase().includes(word.toLowerCase())) {
-
- // Injecter le mot de façon naturelle
- const words = sentences[targetSentenceIndex].split(' ');
- const insertIndex = Math.floor(words.length * (0.3 + Math.random() * 0.4)); // 30-70% de la phrase
-
- // Adaptations contextuelles
- const adaptedWord = adaptWordToContext(word, words[insertIndex] || '');
- words.splice(insertIndex, 0, adaptedWord);
-
- sentences[targetSentenceIndex] = words.join(' ');
- modified = sentences.join('. ');
- count++;
-
- logSh(` đ RĂ©pĂ©tition injectĂ©e: "${adaptedWord}" dans phrase ${targetSentenceIndex + 1}`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, count };
-}
-
-/**
- * INJECTION TICS VOCABULAIRE
- */
-function injectVocabularyTics(content, profile, probability) {
- let modified = content;
- let count = 0;
-
- if (!profile.vocabularyTics || profile.vocabularyTics.length === 0) {
- return { content: modified, count };
- }
-
- const selectedTics = profile.vocabularyTics.slice(0, 1); // Un seul tic par contenu
-
- selectedTics.forEach(tic => {
- if (Math.random() < probability * 0.8) { // Probabilité réduite pour les tics
- // Remplacer des connecteurs standards par le tic
- const standardConnectors = ['par ailleurs', 'de plus', 'également', 'aussi'];
-
- standardConnectors.forEach(connector => {
- const regex = new RegExp(`\\b${connector}\\b`, 'gi');
- if (modified.match(regex) && Math.random() < 0.4) {
- modified = modified.replace(regex, tic);
- count++;
- logSh(` đŁïž Tic vocabulaire: "${connector}" â "${tic}"`, 'DEBUG');
- }
- });
- }
- });
-
- return { content: modified, count };
-}
-
-/**
- * INJECTION ANGLICISMES
- */
-function injectAnglicisms(content, profile, probability) {
- let modified = content;
- let count = 0;
-
- if (!profile.anglicisms || profile.anglicisms.length === 0) {
- return { content: modified, count };
- }
-
- // Remplacements français â anglais
- const replacements = {
- 'processus': 'process',
- 'conception': 'design',
- 'flux de travail': 'workflow',
- 'mise Ă jour': 'upgrade',
- 'contenu': 'content',
- 'tendance': 'trending',
- 'intelligent': 'smart',
- 'numérique': 'digital'
- };
-
- Object.entries(replacements).forEach(([french, english]) => {
- if (profile.anglicisms.includes(english) && Math.random() < probability) {
- const regex = new RegExp(`\\b${french}\\b`, 'gi');
- if (modified.match(regex)) {
- modified = modified.replace(regex, english);
- count++;
- logSh(` đŹđ§ Anglicisme: "${french}" â "${english}"`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, count };
-}
-
-/**
- * INJECTION ERREURS SYNTAXIQUES
- */
-function injectSyntaxErrors(content, profile, probability) {
- let modified = content;
- let count = 0;
-
- if (Math.random() > probability) {
- return { content: modified, count };
- }
-
- // Erreurs syntaxiques légÚres selon la personnalité
- if (profile.name.includes('Marc') || profile.name.includes('Technique')) {
- // ParenthĂšses techniques excessives
- if (Math.random() < 0.3) {
- modified = modified.replace(/(\w+)/, '$1 (systĂšme)');
- count++;
- logSh(` đ§ Erreur technique: parenthĂšses ajoutĂ©es`, 'DEBUG');
- }
- }
-
- if (profile.name.includes('Sophie') || profile.name.includes('Déco')) {
- // Accumulation d'adjectifs
- if (Math.random() < 0.3) {
- modified = modified.replace(/élégant/gi, 'élégant et raffiné');
- count++;
- logSh(` đš Erreur dĂ©co: adjectifs accumulĂ©s`, 'DEBUG');
- }
- }
-
- if (profile.name.includes('Laurent') || profile.name.includes('Commercial')) {
- // Superlatifs empilés
- if (Math.random() < 0.3) {
- modified = modified.replace(/excellent/gi, 'vraiment excellent');
- count++;
- logSh(` đŒ Erreur commerciale: superlatifs empilĂ©s`, 'DEBUG');
- }
- }
-
- return { content: modified, count };
-}
-
-/**
- * ADAPTATION CONTEXTUELLE DES MOTS
- */
-function adaptWordToContext(word, contextWord) {
- // Accords basiques
- const contextLower = contextWord.toLowerCase();
-
- // Accords féminins simples
- if (contextLower.includes('la ') || contextLower.endsWith('e')) {
- if (word === 'bon') return 'bonne';
- if (word === 'précis') return 'précise';
- }
-
- return word;
-}
-
-// ============= EXPORTS =============
-module.exports = {
- getPersonalityErrorPatterns,
- injectPersonalityErrors,
- injectRepetitions,
- injectVocabularyTics,
- injectAnglicisms,
- injectSyntaxErrors,
- createGenericErrorProfile,
- adaptWordToContext,
- PERSONALITY_ERROR_PATTERNS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/TemporalStyles.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: TemporalStyles.js
-// RESPONSABILITĂ: Variations temporelles d'Ă©criture
-// Simulation comportement humain selon l'heure
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * STYLES TEMPORELS PAR TRANCHES HORAIRES
- * Simule l'énergie et style d'écriture selon l'heure
- */
-const TEMPORAL_STYLES = {
-
- // ========================================
- // MATIN (6h-11h) - Ănergique et Direct
- // ========================================
- morning: {
- period: 'matin',
- timeRange: [6, 11],
- energy: 'high',
- characteristics: {
- sentenceLength: 'short', // Phrases plus courtes
- vocabulary: 'dynamic', // Mots énergiques
- connectors: 'direct', // Connecteurs simples
- rhythm: 'fast' // Rythme soutenu
- },
- vocabularyPreferences: {
- energy: ['dynamique', 'efficace', 'rapide', 'direct', 'actif', 'performant'],
- connectors: ['donc', 'puis', 'ensuite', 'maintenant', 'immédiatement'],
- modifiers: ['trĂšs', 'vraiment', 'particuliĂšrement', 'nettement'],
- actions: ['optimiser', 'accélérer', 'améliorer', 'développer', 'créer']
- },
- styleTendencies: {
- shortSentencesBias: 0.7, // 70% chance phrases courtes
- directConnectorsBias: 0.8, // 80% connecteurs simples
- energyWordsBias: 0.6 // 60% mots énergiques
- }
- },
-
- // ========================================
- // APRĂS-MIDI (12h-17h) - ĂquilibrĂ© et Professionnel
- // ========================================
- afternoon: {
- period: 'aprĂšs-midi',
- timeRange: [12, 17],
- energy: 'medium',
- characteristics: {
- sentenceLength: 'medium', // Phrases équilibrées
- vocabulary: 'professional', // Vocabulaire standard
- connectors: 'balanced', // Connecteurs variés
- rhythm: 'steady' // Rythme régulier
- },
- vocabularyPreferences: {
- energy: ['professionnel', 'efficace', 'qualité', 'standard', 'adapté'],
- connectors: ['par ailleurs', 'de plus', 'également', 'ainsi', 'cependant'],
- modifiers: ['assez', 'plutĂŽt', 'relativement', 'suffisamment'],
- actions: ['réaliser', 'développer', 'analyser', 'étudier', 'concevoir']
- },
- styleTendencies: {
- shortSentencesBias: 0.4, // 40% phrases courtes
- directConnectorsBias: 0.5, // 50% connecteurs simples
- energyWordsBias: 0.3 // 30% mots énergiques
- }
- },
-
- // ========================================
- // SOIR (18h-23h) - Détendu et Réflexif
- // ========================================
- evening: {
- period: 'soir',
- timeRange: [18, 23],
- energy: 'low',
- characteristics: {
- sentenceLength: 'long', // Phrases plus longues
- vocabulary: 'nuanced', // Vocabulaire nuancé
- connectors: 'complex', // Connecteurs élaborés
- rhythm: 'relaxed' // Rythme posé
- },
- vocabularyPreferences: {
- energy: ['approfondi', 'réfléchi', 'considéré', 'nuancé', 'détaillé'],
- connectors: ['néanmoins', 'cependant', 'par conséquent', 'en outre', 'toutefois'],
- modifiers: ['quelque peu', 'relativement', 'dans une certaine mesure', 'assez'],
- actions: ['examiner', 'considérer', 'réfléchir', 'approfondir', 'explorer']
- },
- styleTendencies: {
- shortSentencesBias: 0.2, // 20% phrases courtes
- directConnectorsBias: 0.2, // 20% connecteurs simples
- energyWordsBias: 0.1 // 10% mots énergiques
- }
- },
-
- // ========================================
- // NUIT (0h-5h) - Fatigue et Simplicité
- // ========================================
- night: {
- period: 'nuit',
- timeRange: [0, 5],
- energy: 'very_low',
- characteristics: {
- sentenceLength: 'short', // Phrases courtes par fatigue
- vocabulary: 'simple', // Vocabulaire basique
- connectors: 'minimal', // Connecteurs rares
- rhythm: 'slow' // Rythme lent
- },
- vocabularyPreferences: {
- energy: ['simple', 'basique', 'standard', 'normal', 'classique'],
- connectors: ['et', 'mais', 'ou', 'donc', 'puis'],
- modifiers: ['assez', 'bien', 'pas mal', 'correct'],
- actions: ['faire', 'utiliser', 'prendre', 'mettre', 'avoir']
- },
- styleTendencies: {
- shortSentencesBias: 0.8, // 80% phrases courtes
- directConnectorsBias: 0.9, // 90% connecteurs simples
- energyWordsBias: 0.1 // 10% mots énergiques
- }
- }
-};
-
-/**
- * DĂTERMINER STYLE TEMPOREL SELON L'HEURE
- * @param {number} currentHour - Heure actuelle (0-23)
- * @returns {object} - Style temporel correspondant
- */
-function getTemporalStyle(currentHour) {
- // Validation heure
- const hour = Math.max(0, Math.min(23, Math.floor(currentHour || new Date().getHours())));
-
- logSh(`ⰠDétermination style temporel pour ${hour}h`, 'DEBUG');
-
- // Déterminer période
- let selectedStyle;
-
- if (hour >= 6 && hour <= 11) {
- selectedStyle = TEMPORAL_STYLES.morning;
- } else if (hour >= 12 && hour <= 17) {
- selectedStyle = TEMPORAL_STYLES.afternoon;
- } else if (hour >= 18 && hour <= 23) {
- selectedStyle = TEMPORAL_STYLES.evening;
- } else {
- selectedStyle = TEMPORAL_STYLES.night;
- }
-
- logSh(`ⰠStyle temporel sélectionné: ${selectedStyle.period} (énergie: ${selectedStyle.energy})`, 'DEBUG');
-
- return {
- ...selectedStyle,
- currentHour: hour,
- timestamp: new Date().toISOString()
- };
-}
-
-/**
- * APPLICATION STYLE TEMPOREL
- * @param {string} content - Contenu Ă modifier
- * @param {object} temporalStyle - Style temporel Ă appliquer
- * @param {object} options - Options { intensity }
- * @returns {object} - { content, modifications }
- */
-function applyTemporalStyle(content, temporalStyle, options = {}) {
- if (!content || !temporalStyle) {
- return { content, modifications: 0 };
- }
-
- const intensity = options.intensity || 1.0;
-
- logSh(`ⰠApplication style temporel: ${temporalStyle.period} (intensité: ${intensity})`, 'DEBUG');
-
- let modifiedContent = content;
- let modifications = 0;
-
- // ========================================
- // 1. AJUSTEMENT LONGUEUR PHRASES
- // ========================================
- const sentenceResult = adjustSentenceLength(modifiedContent, temporalStyle, intensity);
- modifiedContent = sentenceResult.content;
- modifications += sentenceResult.count;
-
- // ========================================
- // 2. ADAPTATION VOCABULAIRE
- // ========================================
- const vocabularyResult = adaptVocabulary(modifiedContent, temporalStyle, intensity);
- modifiedContent = vocabularyResult.content;
- modifications += vocabularyResult.count;
-
- // ========================================
- // 3. MODIFICATION CONNECTEURS
- // ========================================
- const connectorResult = adjustConnectors(modifiedContent, temporalStyle, intensity);
- modifiedContent = connectorResult.content;
- modifications += connectorResult.count;
-
- // ========================================
- // 4. AJUSTEMENT RYTHME
- // ========================================
- const rhythmResult = adjustRhythm(modifiedContent, temporalStyle, intensity);
- modifiedContent = rhythmResult.content;
- modifications += rhythmResult.count;
-
- logSh(`ⰠStyle temporel appliqué: ${modifications} modifications`, 'DEBUG');
-
- return {
- content: modifiedContent,
- modifications
- };
-}
-
-/**
- * AJUSTEMENT LONGUEUR PHRASES
- */
-function adjustSentenceLength(content, temporalStyle, intensity) {
- let modified = content;
- let count = 0;
-
- const bias = temporalStyle.styleTendencies.shortSentencesBias * intensity;
- const sentences = modified.split('. ');
-
- // Probabilité d'appliquer les modifications
- if (Math.random() > intensity * 0.9) { // FIXĂ: Presque toujours appliquer (Ă©tait 0.7)
- return { content: modified, count };
- }
-
- const processedSentences = sentences.map(sentence => {
- if (sentence.length < 20) return sentence; // Ignorer phrases trĂšs courtes
-
- // Style MATIN/NUIT - Raccourcir phrases longues
- if ((temporalStyle.period === 'matin' || temporalStyle.period === 'nuit') &&
- sentence.length > 100 && Math.random() < bias) {
-
- // Chercher point de coupe naturel
- const cutPoints = [', qui', ', que', ', dont', ' et ', ' car ', ' mais '];
- for (const cutPoint of cutPoints) {
- const cutIndex = sentence.indexOf(cutPoint);
- if (cutIndex > 30 && cutIndex < sentence.length - 30) {
- count++;
- logSh(` âïž Phrase raccourcie (${temporalStyle.period}): ${sentence.length} â ${cutIndex} chars`, 'DEBUG');
- return sentence.substring(0, cutIndex) + '. ' +
- sentence.substring(cutIndex + cutPoint.length);
- }
- }
- }
-
- // Style SOIR - Allonger phrases courtes
- if (temporalStyle.period === 'soir' &&
- sentence.length > 30 && sentence.length < 80 &&
- Math.random() < (1 - bias)) {
-
- // Ajouter développements
- const developments = [
- ', ce qui constitue un avantage notable',
- ', permettant ainsi d\'optimiser les résultats',
- ', dans une démarche d\'amélioration continue',
- ', contribuant à l\'efficacité globale'
- ];
-
- const development = developments[Math.floor(Math.random() * developments.length)];
- count++;
- logSh(` đ Phrase allongĂ©e (soir): ${sentence.length} â ${sentence.length + development.length} chars`, 'DEBUG');
- return sentence + development;
- }
-
- return sentence;
- });
-
- modified = processedSentences.join('. ');
- return { content: modified, count };
-}
-
-/**
- * ADAPTATION VOCABULAIRE
- */
-function adaptVocabulary(content, temporalStyle, intensity) {
- let modified = content;
- let count = 0;
-
- const vocabularyPrefs = temporalStyle.vocabularyPreferences;
- const energyBias = temporalStyle.styleTendencies.energyWordsBias * intensity;
-
- // Probabilité d'appliquer
- if (Math.random() > intensity * 0.9) { // FIXĂ: Presque toujours appliquer (Ă©tait 0.6)
- return { content: modified, count };
- }
-
- // Remplacements selon période
- const replacements = buildVocabularyReplacements(temporalStyle.period, vocabularyPrefs);
-
- replacements.forEach(replacement => {
- if (Math.random() < Math.max(0.6, energyBias)) { // FIXĂ: Minimum 60% chance
- const regex = new RegExp(`\\b${replacement.from}\\b`, 'gi');
- if (modified.match(regex)) {
- modified = modified.replace(regex, replacement.to);
- count++;
- logSh(` đ Vocabulaire adaptĂ© (${temporalStyle.period}): "${replacement.from}" â "${replacement.to}"`, 'DEBUG');
- }
- }
- });
-
- // AJOUT FIX: Si aucun remplacement, forcer au moins une modification temporelle basique
- if (count === 0 && Math.random() < 0.5) {
- // Modification basique selon période
- if (temporalStyle.period === 'matin' && modified.includes('utiliser')) {
- modified = modified.replace(/\butiliser\b/gi, 'optimiser');
- count++;
- logSh(` đ Modification temporelle forcĂ©e: utiliser â optimiser`, 'DEBUG');
- }
- }
-
- return { content: modified, count };
-}
-
-/**
- * CONSTRUCTION REMPLACEMENTS VOCABULAIRE
- */
-function buildVocabularyReplacements(period, vocabPrefs) {
- const replacements = [];
-
- switch (period) {
- case 'matin':
- replacements.push(
- { from: 'bon', to: 'excellent' },
- { from: 'intéressant', to: 'dynamique' },
- { from: 'utiliser', to: 'optimiser' },
- { from: 'faire', to: 'créer' }
- );
- break;
-
- case 'soir':
- replacements.push(
- { from: 'bon', to: 'considérable' },
- { from: 'faire', to: 'examiner' },
- { from: 'utiliser', to: 'exploiter' },
- { from: 'voir', to: 'considérer' }
- );
- break;
-
- case 'nuit':
- replacements.push(
- { from: 'excellent', to: 'bien' },
- { from: 'optimiser', to: 'utiliser' },
- { from: 'considérable', to: 'correct' },
- { from: 'examiner', to: 'regarder' }
- );
- break;
-
- default: // aprĂšs-midi
- // Vocabulaire équilibré - pas de remplacements drastiques
- break;
- }
-
- return replacements;
-}
-
-/**
- * AJUSTEMENT CONNECTEURS
- */
-function adjustConnectors(content, temporalStyle, intensity) {
- let modified = content;
- let count = 0;
-
- const connectorBias = temporalStyle.styleTendencies.directConnectorsBias * intensity;
- const preferredConnectors = temporalStyle.vocabularyPreferences.connectors;
-
- if (Math.random() > intensity * 0.5) {
- return { content: modified, count };
- }
-
- // Connecteurs selon période
- const connectorMappings = {
- matin: [
- { from: /par conséquent/gi, to: 'donc' },
- { from: /néanmoins/gi, to: 'mais' },
- { from: /en outre/gi, to: 'aussi' }
- ],
- soir: [
- { from: /donc/gi, to: 'par conséquent' },
- { from: /mais/gi, to: 'néanmoins' },
- { from: /aussi/gi, to: 'en outre' }
- ],
- nuit: [
- { from: /par conséquent/gi, to: 'donc' },
- { from: /néanmoins/gi, to: 'mais' },
- { from: /cependant/gi, to: 'mais' }
- ]
- };
-
- const mappings = connectorMappings[temporalStyle.period] || [];
-
- mappings.forEach(mapping => {
- if (Math.random() < connectorBias) {
- if (modified.match(mapping.from)) {
- modified = modified.replace(mapping.from, mapping.to);
- count++;
- logSh(` đ Connecteur adaptĂ© (${temporalStyle.period}): "${mapping.from}" â "${mapping.to}"`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, count };
-}
-
-/**
- * AJUSTEMENT RYTHME
- */
-function adjustRhythm(content, temporalStyle, intensity) {
- let modified = content;
- let count = 0;
-
- // Le rythme affecte la ponctuation et les pauses
- if (Math.random() > intensity * 0.3) {
- return { content: modified, count };
- }
-
- switch (temporalStyle.characteristics.rhythm) {
- case 'fast': // Matin - moins de virgules, plus direct
- if (Math.random() < 0.4) {
- // Supprimer quelques virgules non essentielles
- const originalCommas = (modified.match(/,/g) || []).length;
- modified = modified.replace(/, qui /gi, ' qui ');
- modified = modified.replace(/, que /gi, ' que ');
- const newCommas = (modified.match(/,/g) || []).length;
- count = originalCommas - newCommas;
- if (count > 0) {
- logSh(` ⥠Rythme accéléré: ${count} virgules supprimées`, 'DEBUG');
- }
- }
- break;
-
- case 'relaxed': // Soir - plus de pauses
- if (Math.random() < 0.3) {
- // Ajouter quelques pauses réflexives
- modified = modified.replace(/\. ([A-Z])/g, '. Ainsi, $1');
- count++;
- logSh(` đ§ Rythme ralenti: pauses ajoutĂ©es`, 'DEBUG');
- }
- break;
-
- case 'slow': // Nuit - simplification
- if (Math.random() < 0.5) {
- // Simplifier structures complexes
- modified = modified.replace(/ ; /g, '. ');
- count++;
- logSh(` đŽ Rythme simplifiĂ©: structures allĂ©gĂ©es`, 'DEBUG');
- }
- break;
- }
-
- return { content: modified, count };
-}
-
-/**
- * ANALYSE COHĂRENCE TEMPORELLE
- * @param {string} content - Contenu Ă analyser
- * @param {object} temporalStyle - Style appliqué
- * @returns {object} - Métriques de cohérence
- */
-function analyzeTemporalCoherence(content, temporalStyle) {
- const sentences = content.split('. ');
- const avgSentenceLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length;
-
- const energyWords = temporalStyle.vocabularyPreferences.energy;
- const energyWordCount = energyWords.reduce((count, word) => {
- const regex = new RegExp(`\\b${word}\\b`, 'gi');
- return count + (content.match(regex) || []).length;
- }, 0);
-
- return {
- avgSentenceLength,
- energyWordDensity: energyWordCount / sentences.length,
- period: temporalStyle.period,
- coherenceScore: calculateCoherenceScore(avgSentenceLength, temporalStyle),
- expectedCharacteristics: temporalStyle.characteristics
- };
-}
-
-/**
- * CALCUL SCORE COHĂRENCE
- */
-function calculateCoherenceScore(avgLength, temporalStyle) {
- let score = 1.0;
-
- // Vérifier cohérence longueur phrases avec période
- const expectedLength = {
- 'matin': { min: 40, max: 80 },
- 'aprĂšs-midi': { min: 60, max: 120 },
- 'soir': { min: 80, max: 150 },
- 'nuit': { min: 30, max: 70 }
- };
-
- const expected = expectedLength[temporalStyle.period];
- if (expected) {
- if (avgLength < expected.min || avgLength > expected.max) {
- score *= 0.7;
- }
- }
-
- return Math.max(0, Math.min(1, score));
-}
-
-// ============= EXPORTS =============
-module.exports = {
- getTemporalStyle,
- applyTemporalStyle,
- adjustSentenceLength,
- adaptVocabulary,
- adjustConnectors,
- adjustRhythm,
- analyzeTemporalCoherence,
- calculateCoherenceScore,
- buildVocabularyReplacements,
- TEMPORAL_STYLES
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/HumanSimulationUtils.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: HumanSimulationUtils.js
-// RESPONSABILITĂ: Utilitaires partagĂ©s Human Simulation
-// Fonctions d'analyse, validation et helpers
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * SEUILS DE QUALITĂ
- */
-const QUALITY_THRESHOLDS = {
- readability: {
- minimum: 0.3, // FIXĂ: Plus permissif (Ă©tait 0.6)
- good: 0.6,
- excellent: 0.8
- },
- keywordPreservation: {
- minimum: 0.7, // FIXĂ: Plus permissif (Ă©tait 0.8)
- good: 0.9,
- excellent: 0.95
- },
- similarity: {
- minimum: 0.5, // FIXĂ: Plus permissif (Ă©tait 0.7)
- maximum: 1.0 // FIXĂ: Accepter mĂȘme contenu identique (Ă©tait 0.95)
- }
-};
-
-/**
- * MOTS-CLĂS Ă PRĂSERVER ABSOLUMENT
- */
-const CRITICAL_KEYWORDS = [
- // Mots-clés SEO génériques
- 'plaque', 'personnalisée', 'gravure', 'métal', 'bois', 'acrylique',
- 'design', 'qualité', 'fabrication', 'artisanal', 'sur-mesure',
- // Termes techniques importants
- 'laser', 'CNC', 'impression', 'découpe', 'finition', 'traitement',
- // Termes commerciaux
- 'prix', 'tarif', 'devis', 'livraison', 'garantie', 'service'
-];
-
-/**
- * ANALYSE COMPLEXITĂ CONTENU
- * @param {object} content - Contenu Ă analyser
- * @returns {object} - Métriques de complexité
- */
-function analyzeContentComplexity(content) {
- logSh('đ Analyse complexitĂ© contenu', 'DEBUG');
-
- const contentArray = Object.values(content).filter(c => typeof c === 'string');
- const totalText = contentArray.join(' ');
-
- // Métriques de base
- const totalWords = totalText.split(/\s+/).length;
- const totalSentences = totalText.split(/[.!?]+/).length;
- const totalParagraphs = contentArray.length;
-
- // Complexité lexicale
- const uniqueWords = new Set(totalText.toLowerCase().split(/\s+/)).size;
- const lexicalDiversity = uniqueWords / totalWords;
-
- // Longueur moyenne des phrases
- const avgSentenceLength = totalWords / totalSentences;
-
- // Complexité syntaxique (approximative)
- const complexConnectors = (totalText.match(/néanmoins|cependant|par conséquent|en outre|toutefois/gi) || []).length;
- const syntacticComplexity = complexConnectors / totalSentences;
-
- // Score global de complexité
- const complexityScore = (
- (lexicalDiversity * 0.4) +
- (Math.min(avgSentenceLength / 100, 1) * 0.3) +
- (syntacticComplexity * 0.3)
- );
-
- const complexity = {
- totalWords,
- totalSentences,
- totalParagraphs,
- avgSentenceLength,
- lexicalDiversity,
- syntacticComplexity,
- complexityScore,
- level: complexityScore > 0.7 ? 'high' : complexityScore > 0.4 ? 'medium' : 'low'
- };
-
- logSh(` đ ComplexitĂ©: ${complexity.level} (score: ${complexityScore.toFixed(2)})`, 'DEBUG');
- logSh(` đ ${totalWords} mots, ${totalSentences} phrases, diversitĂ©: ${lexicalDiversity.toFixed(2)}`, 'DEBUG');
-
- return complexity;
-}
-
-/**
- * CALCUL SCORE LISIBILITĂ
- * Approximation de l'index Flesch-Kincaid adapté au français
- * @param {string} text - Texte Ă analyser
- * @returns {number} - Score lisibilité (0-1)
- */
-function calculateReadabilityScore(text) {
- if (!text || text.trim().length === 0) {
- return 0;
- }
-
- // Nettoyage du texte
- const cleanText = text.replace(/[^\w\s.!?]/gi, '');
-
- // Comptages de base
- const sentences = cleanText.split(/[.!?]+/).filter(s => s.trim().length > 0);
- const words = cleanText.split(/\s+/).filter(w => w.length > 0);
- const syllables = countSyllables(cleanText);
-
- if (sentences.length === 0 || words.length === 0) {
- return 0;
- }
-
- // Métriques Flesch-Kincaid adaptées français
- const avgWordsPerSentence = words.length / sentences.length;
- const avgSyllablesPerWord = syllables / words.length;
-
- // Formule adaptée (plus clémente que l'originale)
- const fleschScore = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
-
- // Normalisation 0-1 (100 = parfait en Flesch)
- const normalizedScore = Math.max(0, Math.min(1, fleschScore / 100));
-
- logSh(` đ LisibilitĂ©: ${normalizedScore.toFixed(2)} (mots/phrase: ${avgWordsPerSentence.toFixed(1)}, syll/mot: ${avgSyllablesPerWord.toFixed(1)})`, 'DEBUG');
-
- return normalizedScore;
-}
-
-/**
- * COMPTAGE SYLLABES (APPROXIMATIF FRANĂAIS)
- */
-function countSyllables(text) {
- // Approximation pour le français
- const vowels = /[aeiouyà åùÀÚéĂȘëÏĂßïĂČóÎöĂčĂșĂ»ĂŒ]/gi;
- const vowelGroups = text.match(vowels) || [];
-
- // Approximation: 1 groupe de voyelles â 1 syllabe
- // Ajustements pour le français
- let syllables = vowelGroups.length;
-
- // Corrections courantes
- const corrections = [
- { pattern: /ion/gi, adjustment: 0 }, // "tion" = 1 syllabe, pas 2
- { pattern: /ieu/gi, adjustment: -1 }, // "ieux" = 1 syllabe
- { pattern: /eau/gi, adjustment: -1 }, // "eau" = 1 syllabe
- { pattern: /ai/gi, adjustment: -1 }, // "ai" = 1 syllabe
- { pattern: /ou/gi, adjustment: -1 }, // "ou" = 1 syllabe
- { pattern: /e$/gi, adjustment: -0.5 } // "e" final muet
- ];
-
- corrections.forEach(correction => {
- const matches = text.match(correction.pattern) || [];
- syllables += matches.length * correction.adjustment;
- });
-
- return Math.max(1, Math.round(syllables));
-}
-
-/**
- * PRĂSERVATION MOTS-CLĂS
- * @param {string} originalText - Texte original
- * @param {string} modifiedText - Texte modifié
- * @returns {number} - Score préservation (0-1)
- */
-function preserveKeywords(originalText, modifiedText) {
- if (!originalText || !modifiedText) {
- return 0;
- }
-
- const originalLower = originalText.toLowerCase();
- const modifiedLower = modifiedText.toLowerCase();
-
- // Extraire mots-clés du texte original
- const originalKeywords = extractKeywords(originalLower);
-
- // Vérifier préservation
- let preservedCount = 0;
- let criticalPreservedCount = 0;
- let criticalTotalCount = 0;
-
- originalKeywords.forEach(keyword => {
- const isCritical = CRITICAL_KEYWORDS.some(ck =>
- keyword.toLowerCase().includes(ck.toLowerCase()) ||
- ck.toLowerCase().includes(keyword.toLowerCase())
- );
-
- if (isCritical) {
- criticalTotalCount++;
- }
-
- // Vérifier présence dans texte modifié
- const keywordRegex = new RegExp(`\\b${keyword}\\b`, 'gi');
- if (modifiedLower.match(keywordRegex)) {
- preservedCount++;
- if (isCritical) {
- criticalPreservedCount++;
- }
- }
- });
-
- // Score avec bonus pour mots-clés critiques
- const basicPreservation = preservedCount / Math.max(1, originalKeywords.length);
- const criticalPreservation = criticalTotalCount > 0 ?
- criticalPreservedCount / criticalTotalCount : 1.0;
-
- const finalScore = (basicPreservation * 0.6) + (criticalPreservation * 0.4);
-
- logSh(` đ Mots-clĂ©s: ${preservedCount}/${originalKeywords.length} prĂ©servĂ©s (${criticalPreservedCount}/${criticalTotalCount} critiques)`, 'DEBUG');
- logSh(` đŻ Score prĂ©servation: ${finalScore.toFixed(2)}`, 'DEBUG');
-
- return finalScore;
-}
-
-/**
- * EXTRACTION MOTS-CLĂS SIMPLES
- */
-function extractKeywords(text) {
- // Mots de plus de 3 caractĂšres, non vides
- const words = text.match(/\b\w{4,}\b/g) || [];
-
- // Filtrer mots courants français
- const stopWords = [
- 'avec', 'dans', 'pour', 'cette', 'sont', 'tout', 'mais', 'plus', 'trĂšs',
- 'bien', 'encore', 'aussi', 'comme', 'aprĂšs', 'avant', 'entre', 'depuis'
- ];
-
- const keywords = words
- .filter(word => !stopWords.includes(word.toLowerCase()))
- .filter((word, index, array) => array.indexOf(word) === index) // Unique
- .slice(0, 20); // Limiter à 20 mots-clés
-
- return keywords;
-}
-
-/**
- * VALIDATION QUALITĂ SIMULATION
- * @param {string} originalContent - Contenu original
- * @param {string} simulatedContent - Contenu simulé
- * @param {number} qualityThreshold - Seuil qualité minimum
- * @returns {object} - Résultat validation
- */
-function validateSimulationQuality(originalContent, simulatedContent, qualityThreshold = 0.7) {
- if (!originalContent || !simulatedContent) {
- return { acceptable: false, reason: 'Contenu manquant' };
- }
-
- logSh('đŻ Validation qualitĂ© simulation', 'DEBUG');
-
- // Métriques de qualité
- const readabilityScore = calculateReadabilityScore(simulatedContent);
- const keywordScore = preserveKeywords(originalContent, simulatedContent);
- const similarityScore = calculateSimilarity(originalContent, simulatedContent);
-
- // Score global pondéré
- const globalScore = (
- readabilityScore * 0.4 +
- keywordScore * 0.4 +
- (similarityScore > QUALITY_THRESHOLDS.similarity.minimum &&
- similarityScore < QUALITY_THRESHOLDS.similarity.maximum ? 0.2 : 0)
- );
-
- const acceptable = globalScore >= qualityThreshold;
-
- const validation = {
- acceptable,
- globalScore,
- readabilityScore,
- keywordScore,
- similarityScore,
- reason: acceptable ? 'Qualité acceptable' : determineQualityIssue(readabilityScore, keywordScore, similarityScore),
- details: {
- readabilityOk: readabilityScore >= QUALITY_THRESHOLDS.readability.minimum,
- keywordsOk: keywordScore >= QUALITY_THRESHOLDS.keywordPreservation.minimum,
- similarityOk: similarityScore >= QUALITY_THRESHOLDS.similarity.minimum &&
- similarityScore <= QUALITY_THRESHOLDS.similarity.maximum
- }
- };
-
- logSh(` đŻ Validation: ${acceptable ? 'ACCEPTĂ' : 'REJETĂ'} (score: ${globalScore.toFixed(2)})`, acceptable ? 'INFO' : 'WARNING');
- logSh(` đ LisibilitĂ©: ${readabilityScore.toFixed(2)} | Mots-clĂ©s: ${keywordScore.toFixed(2)} | SimilaritĂ©: ${similarityScore.toFixed(2)}`, 'DEBUG');
-
- return validation;
-}
-
-/**
- * CALCUL SIMILARITĂ APPROXIMATIVE
- */
-function calculateSimilarity(text1, text2) {
- // Similarité basée sur les mots partagés (simple mais efficace)
- const words1 = new Set(text1.toLowerCase().split(/\s+/));
- const words2 = new Set(text2.toLowerCase().split(/\s+/));
-
- const intersection = new Set([...words1].filter(word => words2.has(word)));
- const union = new Set([...words1, ...words2]);
-
- return intersection.size / union.size;
-}
-
-/**
- * DĂTERMINER PROBLĂME QUALITĂ
- */
-function determineQualityIssue(readabilityScore, keywordScore, similarityScore) {
- if (readabilityScore < QUALITY_THRESHOLDS.readability.minimum) {
- return 'Lisibilité insuffisante';
- }
- if (keywordScore < QUALITY_THRESHOLDS.keywordPreservation.minimum) {
- return 'Mots-clés mal préservés';
- }
- if (similarityScore < QUALITY_THRESHOLDS.similarity.minimum) {
- return 'Trop différent de l\'original';
- }
- if (similarityScore > QUALITY_THRESHOLDS.similarity.maximum) {
- return 'Pas assez modifié';
- }
- return 'Score global insuffisant';
-}
-
-/**
- * GĂNĂRATION RAPPORT QUALITĂ DĂTAILLĂ
- * @param {object} content - Contenu Ă analyser
- * @param {object} simulationStats - Stats simulation
- * @returns {object} - Rapport détaillé
- */
-function generateQualityReport(content, simulationStats) {
- const report = {
- timestamp: new Date().toISOString(),
- contentAnalysis: analyzeContentComplexity(content),
- simulationStats,
- qualityMetrics: {},
- recommendations: []
- };
-
- // Analyse par élément
- Object.entries(content).forEach(([key, elementContent]) => {
- if (typeof elementContent === 'string') {
- const readability = calculateReadabilityScore(elementContent);
- const complexity = analyzeContentComplexity({ [key]: elementContent });
-
- report.qualityMetrics[key] = {
- readability,
- complexity: complexity.complexityScore,
- wordCount: elementContent.split(/\s+/).length
- };
- }
- });
-
- // Recommandations automatiques
- if (report.contentAnalysis.complexityScore > 0.8) {
- report.recommendations.push('Simplifier le vocabulaire pour améliorer la lisibilité');
- }
-
- if (simulationStats.fatigueModifications < 1) {
- report.recommendations.push('Augmenter l\'intensité de simulation fatigue');
- }
-
- return report;
-}
-
-/**
- * HELPERS STATISTIQUES
- */
-function calculateStatistics(values) {
- const sorted = values.slice().sort((a, b) => a - b);
- const length = values.length;
-
- return {
- mean: values.reduce((sum, val) => sum + val, 0) / length,
- median: length % 2 === 0 ?
- (sorted[length / 2 - 1] + sorted[length / 2]) / 2 :
- sorted[Math.floor(length / 2)],
- min: sorted[0],
- max: sorted[length - 1],
- stdDev: calculateStandardDeviation(values)
- };
-}
-
-function calculateStandardDeviation(values) {
- const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
- const squaredDifferences = values.map(val => Math.pow(val - mean, 2));
- const variance = squaredDifferences.reduce((sum, val) => sum + val, 0) / values.length;
- return Math.sqrt(variance);
-}
-
-// ============= EXPORTS =============
-module.exports = {
- analyzeContentComplexity,
- calculateReadabilityScore,
- preserveKeywords,
- validateSimulationQuality,
- generateQualityReport,
- calculateStatistics,
- calculateStandardDeviation,
- countSyllables,
- extractKeywords,
- calculateSimilarity,
- determineQualityIssue,
- QUALITY_THRESHOLDS,
- CRITICAL_KEYWORDS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/HumanSimulationCore.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: HumanSimulationCore.js
-// RESPONSABILITĂ: Orchestrateur principal Human Simulation
-// Niveau 5: Temporal & Personality Injection
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-const { tracer } = require('../trace');
-const { calculateFatigue, injectFatigueMarkers, getFatigueProfile } = require('./FatiguePatterns');
-const { injectPersonalityErrors, getPersonalityErrorPatterns } = require('./PersonalityErrors');
-const { applyTemporalStyle, getTemporalStyle } = require('./TemporalStyles');
-const {
- analyzeContentComplexity,
- calculateReadabilityScore,
- preserveKeywords,
- validateSimulationQuality
-} = require('./HumanSimulationUtils');
-
-/**
- * CONFIGURATION PAR DĂFAUT
- */
-const DEFAULT_CONFIG = {
- fatigueEnabled: true,
- personalityErrorsEnabled: true,
- temporalStyleEnabled: true,
- imperfectionIntensity: 0.8, // FIXĂ: Plus d'intensitĂ© (Ă©tait 0.3)
- naturalRepetitions: true,
- qualityThreshold: 0.4, // FIXĂ: Seuil plus bas (Ă©tait 0.7)
- maxModificationsPerElement: 5 // FIXĂ: Plus de modifs possibles (Ă©tait 3)
-};
-
-/**
- * ORCHESTRATEUR PRINCIPAL - Human Simulation Layer
- * @param {object} content - Contenu généré à simuler
- * @param {object} options - Options de simulation
- * @returns {object} - { content, stats, fallback }
- */
-async function applyHumanSimulationLayer(content, options = {}) {
- return await tracer.run('HumanSimulationCore.applyHumanSimulationLayer()', async () => {
- const startTime = Date.now();
-
- await tracer.annotate({
- contentKeys: Object.keys(content).length,
- elementIndex: options.elementIndex,
- totalElements: options.totalElements,
- currentHour: options.currentHour,
- personality: options.csvData?.personality?.nom
- });
-
- logSh(`đ§ HUMAN SIMULATION - DĂ©but traitement`, 'INFO');
- logSh(` đ ${Object.keys(content).length} Ă©lĂ©ments | Position: ${options.elementIndex}/${options.totalElements}`, 'DEBUG');
-
- try {
- // Configuration fusionnée
- const config = { ...DEFAULT_CONFIG, ...options };
-
- // Stats de simulation
- const simulationStats = {
- elementsProcessed: 0,
- fatigueModifications: 0,
- personalityModifications: 0,
- temporalModifications: 0,
- totalModifications: 0,
- qualityScore: 0,
- fallbackUsed: false
- };
-
- // Contenu simulé
- let simulatedContent = { ...content };
-
- // ========================================
- // 1. ANALYSE CONTEXTE GLOBAL
- // ========================================
- const globalContext = await analyzeGlobalContext(content, config);
- logSh(` đ Contexte: fatigue=${globalContext.fatigueLevel.toFixed(2)}, heure=${globalContext.currentHour}h, personnalitĂ©=${globalContext.personalityName}`, 'DEBUG');
-
- // ========================================
- // 2. TRAITEMENT PAR ĂLĂMENT
- // ========================================
- for (const [elementKey, elementContent] of Object.entries(content)) {
- await tracer.run(`HumanSimulation.processElement(${elementKey})`, async () => {
-
- logSh(` đŻ Traitement Ă©lĂ©ment: ${elementKey}`, 'DEBUG');
-
- let processedContent = elementContent;
- let elementModifications = 0;
-
- try {
- // 2a. Simulation Fatigue Cognitive
- if (config.fatigueEnabled && globalContext.fatigueLevel > 0.1) { // FIXĂ: Seuil plus bas (Ă©tait 0.3)
- const fatigueResult = await applyFatigueSimulation(processedContent, globalContext, config);
- processedContent = fatigueResult.content;
- elementModifications += fatigueResult.modifications;
- simulationStats.fatigueModifications += fatigueResult.modifications;
-
- logSh(` đ€ Fatigue: ${fatigueResult.modifications} modifications (niveau: ${globalContext.fatigueLevel.toFixed(2)})`, 'DEBUG');
- }
-
- // 2b. Erreurs Personnalité
- if (config.personalityErrorsEnabled && globalContext.personalityProfile) {
- const personalityResult = await applyPersonalitySimulation(processedContent, globalContext, config);
- processedContent = personalityResult.content;
- elementModifications += personalityResult.modifications;
- simulationStats.personalityModifications += personalityResult.modifications;
-
- logSh(` đ PersonnalitĂ©: ${personalityResult.modifications} erreurs injectĂ©es`, 'DEBUG');
- }
-
- // 2c. Style Temporel
- if (config.temporalStyleEnabled && globalContext.temporalStyle) {
- const temporalResult = await applyTemporalSimulation(processedContent, globalContext, config);
- processedContent = temporalResult.content;
- elementModifications += temporalResult.modifications;
- simulationStats.temporalModifications += temporalResult.modifications;
-
- logSh(` â° Temporel: ${temporalResult.modifications} ajustements (${globalContext.temporalStyle.period})`, 'DEBUG');
- }
-
- // 2d. Validation Qualité
- const qualityCheck = validateSimulationQuality(elementContent, processedContent, config.qualityThreshold);
-
- if (qualityCheck.acceptable) {
- simulatedContent[elementKey] = processedContent;
- simulationStats.elementsProcessed++;
- simulationStats.totalModifications += elementModifications;
-
- logSh(` â
ĂlĂ©ment simulĂ©: ${elementModifications} modifications totales`, 'DEBUG');
- } else {
- // Fallback: garder contenu original
- simulatedContent[elementKey] = elementContent;
- simulationStats.fallbackUsed = true;
-
- logSh(` â ïž QualitĂ© insuffisante, fallback vers contenu original`, 'WARNING');
- }
-
- } catch (elementError) {
- logSh(` â Erreur simulation Ă©lĂ©ment ${elementKey}: ${elementError.message}`, 'WARNING');
- simulatedContent[elementKey] = elementContent; // Fallback
- simulationStats.fallbackUsed = true;
- }
-
- }, { elementKey, originalLength: elementContent?.length });
- }
-
- // ========================================
- // 3. CALCUL SCORE QUALITĂ GLOBAL
- // ========================================
- simulationStats.qualityScore = calculateGlobalQualityScore(content, simulatedContent);
-
- // ========================================
- // 4. RĂSULTATS FINAUX
- // ========================================
- const duration = Date.now() - startTime;
- const success = simulationStats.elementsProcessed > 0 && !simulationStats.fallbackUsed;
-
- logSh(`đ§ HUMAN SIMULATION - TerminĂ© (${duration}ms)`, 'INFO');
- logSh(` â
${simulationStats.elementsProcessed}/${Object.keys(content).length} éléments simulés`, 'INFO');
- logSh(` đ ${simulationStats.fatigueModifications} fatigue | ${simulationStats.personalityModifications} personnalitĂ© | ${simulationStats.temporalModifications} temporel`, 'INFO');
- logSh(` đŻ Score qualitĂ©: ${simulationStats.qualityScore.toFixed(2)} | Fallback: ${simulationStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO');
-
- await tracer.event('Human Simulation terminée', {
- success,
- duration,
- stats: simulationStats
- });
-
- return {
- content: simulatedContent,
- stats: simulationStats,
- fallback: simulationStats.fallbackUsed,
- qualityScore: simulationStats.qualityScore,
- duration
- };
-
- } catch (error) {
- const duration = Date.now() - startTime;
- logSh(`â HUMAN SIMULATION ĂCHOUĂE (${duration}ms): ${error.message}`, 'ERROR');
-
- await tracer.event('Human Simulation échouée', {
- error: error.message,
- duration,
- contentKeys: Object.keys(content).length
- });
-
- // Fallback complet
- return {
- content,
- stats: { fallbackUsed: true, error: error.message },
- fallback: true,
- qualityScore: 0,
- duration
- };
- }
-
- }, {
- contentElements: Object.keys(content).length,
- elementIndex: options.elementIndex,
- personality: options.csvData?.personality?.nom
- });
-}
-
-/**
- * ANALYSE CONTEXTE GLOBAL
- */
-async function analyzeGlobalContext(content, config) {
- const elementIndex = config.elementIndex || 0;
- const totalElements = config.totalElements || Object.keys(content).length;
- const currentHour = config.currentHour || new Date().getHours();
- const personality = config.csvData?.personality;
-
- return {
- fatigueLevel: calculateFatigue(elementIndex, totalElements),
- fatigueProfile: personality ? getFatigueProfile(personality.nom) : null,
- personalityName: personality?.nom || 'unknown',
- personalityProfile: personality ? getPersonalityErrorPatterns(personality.nom) : null,
- temporalStyle: getTemporalStyle(currentHour),
- currentHour,
- elementIndex,
- totalElements,
- contentComplexity: analyzeContentComplexity(content)
- };
-}
-
-/**
- * APPLICATION SIMULATION FATIGUE
- */
-async function applyFatigueSimulation(content, globalContext, config) {
- const fatigueResult = injectFatigueMarkers(content, globalContext.fatigueLevel, {
- profile: globalContext.fatigueProfile,
- intensity: config.imperfectionIntensity
- });
-
- return {
- content: fatigueResult.content,
- modifications: fatigueResult.modifications || 0
- };
-}
-
-/**
- * APPLICATION SIMULATION PERSONNALITĂ
- */
-async function applyPersonalitySimulation(content, globalContext, config) {
- const personalityResult = injectPersonalityErrors(
- content,
- globalContext.personalityProfile,
- config.imperfectionIntensity
- );
-
- return {
- content: personalityResult.content,
- modifications: personalityResult.modifications || 0
- };
-}
-
-/**
- * APPLICATION SIMULATION TEMPORELLE
- */
-async function applyTemporalSimulation(content, globalContext, config) {
- const temporalResult = applyTemporalStyle(content, globalContext.temporalStyle, {
- intensity: config.imperfectionIntensity
- });
-
- return {
- content: temporalResult.content,
- modifications: temporalResult.modifications || 0
- };
-}
-
-/**
- * CALCUL SCORE QUALITĂ GLOBAL
- */
-function calculateGlobalQualityScore(originalContent, simulatedContent) {
- let totalScore = 0;
- let elementCount = 0;
-
- for (const [key, original] of Object.entries(originalContent)) {
- const simulated = simulatedContent[key];
- if (simulated) {
- const elementScore = calculateReadabilityScore(simulated) * 0.7 +
- preserveKeywords(original, simulated) * 0.3;
- totalScore += elementScore;
- elementCount++;
- }
- }
-
- return elementCount > 0 ? totalScore / elementCount : 0;
-}
-
-// ============= EXPORTS =============
-module.exports = {
- applyHumanSimulationLayer,
- analyzeGlobalContext,
- applyFatigueSimulation,
- applyPersonalitySimulation,
- applyTemporalSimulation,
- calculateGlobalQualityScore,
- DEFAULT_CONFIG
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/human-simulation/HumanSimulationLayers.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: HumanSimulationLayers.js
-// RESPONSABILITĂ: Stacks prĂ©dĂ©finis Human Simulation
-// Compatible avec architecture modulaire existante
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-const { tracer } = require('../trace');
-const { applyHumanSimulationLayer } = require('./HumanSimulationCore');
-
-/**
- * STACKS PRĂDĂFINIS HUMAN SIMULATION
- * Configuration par niveau d'intensité
- */
-const HUMAN_SIMULATION_STACKS = {
-
- // ========================================
- // SIMULATION LĂGĂRE - Pour tests et dĂ©veloppement
- // ========================================
- lightSimulation: {
- name: 'lightSimulation',
- description: 'Simulation humaine légÚre - développement et tests',
- layersCount: 3,
- config: {
- fatigueEnabled: true,
- personalityErrorsEnabled: true,
- temporalStyleEnabled: false, // Désactivé en mode light
- imperfectionIntensity: 0.3, // Faible intensité
- naturalRepetitions: true,
- qualityThreshold: 0.8, // Seuil élevé
- maxModificationsPerElement: 2 // Limité à 2 modifs par élément
- },
- expectedImpact: {
- modificationsPerElement: '1-2',
- qualityPreservation: '95%',
- detectionReduction: '10-15%',
- executionTime: '+20%'
- }
- },
-
- // ========================================
- // SIMULATION STANDARD - Usage production normal
- // ========================================
- standardSimulation: {
- name: 'standardSimulation',
- description: 'Simulation humaine standard - équilibre performance/qualité',
- layersCount: 3,
- config: {
- fatigueEnabled: true,
- personalityErrorsEnabled: true,
- temporalStyleEnabled: true, // Activé
- imperfectionIntensity: 0.6, // Intensité moyenne
- naturalRepetitions: true,
- qualityThreshold: 0.7, // Seuil normal
- maxModificationsPerElement: 3 // 3 modifs max
- },
- expectedImpact: {
- modificationsPerElement: '2-3',
- qualityPreservation: '85%',
- detectionReduction: '25-35%',
- executionTime: '+40%'
- }
- },
-
- // ========================================
- // SIMULATION INTENSIVE - Maximum anti-détection
- // ========================================
- heavySimulation: {
- name: 'heavySimulation',
- description: 'Simulation humaine intensive - anti-détection maximale',
- layersCount: 3,
- config: {
- fatigueEnabled: true,
- personalityErrorsEnabled: true,
- temporalStyleEnabled: true,
- imperfectionIntensity: 0.9, // Intensité élevée
- naturalRepetitions: true,
- qualityThreshold: 0.6, // Seuil plus permissif
- maxModificationsPerElement: 5 // Jusqu'Ă 5 modifs
- },
- expectedImpact: {
- modificationsPerElement: '3-5',
- qualityPreservation: '75%',
- detectionReduction: '40-50%',
- executionTime: '+60%'
- }
- },
-
- // ========================================
- // SIMULATION ADAPTIVE - Intelligence contextuelle
- // ========================================
- adaptiveSimulation: {
- name: 'adaptiveSimulation',
- description: 'Simulation humaine adaptive - ajustement intelligent selon contexte',
- layersCount: 3,
- config: {
- fatigueEnabled: true,
- personalityErrorsEnabled: true,
- temporalStyleEnabled: true,
- imperfectionIntensity: 'adaptive', // Calculé dynamiquement
- naturalRepetitions: true,
- qualityThreshold: 'adaptive', // Ajusté selon complexité
- maxModificationsPerElement: 'adaptive', // Variable
- adaptiveLogic: true // Flag pour logique adaptive
- },
- expectedImpact: {
- modificationsPerElement: '1-4',
- qualityPreservation: '80-90%',
- detectionReduction: '30-45%',
- executionTime: '+45%'
- }
- },
-
- // ========================================
- // SIMULATION PERSONNALISĂE - Focus personnalitĂ©
- // ========================================
- personalityFocus: {
- name: 'personalityFocus',
- description: 'Focus erreurs personnalité - cohérence maximale',
- layersCount: 2,
- config: {
- fatigueEnabled: false, // Désactivé
- personalityErrorsEnabled: true,
- temporalStyleEnabled: false, // Désactivé
- imperfectionIntensity: 1.0, // Focus sur personnalité
- naturalRepetitions: true,
- qualityThreshold: 0.75,
- maxModificationsPerElement: 3
- },
- expectedImpact: {
- modificationsPerElement: '2-3',
- qualityPreservation: '85%',
- detectionReduction: '20-30%',
- executionTime: '+25%'
- }
- },
-
- // ========================================
- // SIMULATION TEMPORELLE - Focus variations horaires
- // ========================================
- temporalFocus: {
- name: 'temporalFocus',
- description: 'Focus style temporel - variations selon heure',
- layersCount: 2,
- config: {
- fatigueEnabled: false,
- personalityErrorsEnabled: false,
- temporalStyleEnabled: true, // Focus principal
- imperfectionIntensity: 0.8,
- naturalRepetitions: true,
- qualityThreshold: 0.75,
- maxModificationsPerElement: 3
- },
- expectedImpact: {
- modificationsPerElement: '1-3',
- qualityPreservation: '85%',
- detectionReduction: '15-25%',
- executionTime: '+20%'
- }
- }
-};
-
-/**
- * APPLICATION STACK PRĂDĂFINI
- * @param {object} content - Contenu Ă simuler
- * @param {string} stackName - Nom du stack
- * @param {object} options - Options additionnelles
- * @returns {object} - Résultat simulation
- */
-async function applyPredefinedSimulation(content, stackName, options = {}) {
- return await tracer.run(`HumanSimulationLayers.applyPredefinedSimulation(${stackName})`, async () => {
-
- const stack = HUMAN_SIMULATION_STACKS[stackName];
- if (!stack) {
- throw new Error(`Stack Human Simulation non trouvé: ${stackName}`);
- }
-
- await tracer.annotate({
- stackName,
- stackDescription: stack.description,
- layersCount: stack.layersCount,
- contentElements: Object.keys(content).length
- });
-
- logSh(`đ§ APPLICATION STACK: ${stack.name}`, 'INFO');
- logSh(` đ ${stack.description}`, 'DEBUG');
- logSh(` âïž ${stack.layersCount} couches actives`, 'DEBUG');
-
- try {
- // Configuration fusionnée
- let finalConfig = { ...stack.config, ...options };
-
- // ========================================
- // LOGIQUE ADAPTIVE (si applicable)
- // ========================================
- if (stack.config.adaptiveLogic) {
- finalConfig = await applyAdaptiveLogic(content, finalConfig, options);
- logSh(` đ§ Logique adaptive appliquĂ©e`, 'DEBUG');
- }
-
- // ========================================
- // APPLICATION SIMULATION PRINCIPALE
- // ========================================
- const simulationOptions = {
- ...finalConfig,
- elementIndex: options.elementIndex || 0,
- totalElements: options.totalElements || Object.keys(content).length,
- currentHour: options.currentHour || new Date().getHours(),
- csvData: options.csvData,
- stackName: stack.name
- };
-
- const result = await applyHumanSimulationLayer(content, simulationOptions);
-
- // ========================================
- // ENRICHISSEMENT RĂSULTAT
- // ========================================
- const enrichedResult = {
- ...result,
- stackInfo: {
- name: stack.name,
- description: stack.description,
- layersCount: stack.layersCount,
- expectedImpact: stack.expectedImpact,
- configUsed: finalConfig
- }
- };
-
- logSh(`â
STACK ${stack.name} terminé: ${result.stats.totalModifications} modifications`, 'INFO');
-
- await tracer.event('Stack Human Simulation terminé', {
- stackName,
- success: !result.fallback,
- modifications: result.stats.totalModifications,
- qualityScore: result.qualityScore
- });
-
- return enrichedResult;
-
- } catch (error) {
- logSh(`â ERREUR STACK ${stack.name}: ${error.message}`, 'ERROR');
-
- await tracer.event('Stack Human Simulation échoué', {
- stackName,
- error: error.message
- });
-
- // Fallback gracieux
- return {
- content,
- stats: { fallbackUsed: true, error: error.message },
- fallback: true,
- stackInfo: { name: stack.name, error: error.message }
- };
- }
-
- }, { stackName, contentElements: Object.keys(content).length });
-}
-
-/**
- * LOGIQUE ADAPTIVE INTELLIGENTE
- * Ajuste la configuration selon le contexte
- */
-async function applyAdaptiveLogic(content, config, options) {
- logSh('đ§ Application logique adaptive', 'DEBUG');
-
- const adaptedConfig = { ...config };
-
- // ========================================
- // 1. ANALYSE COMPLEXITĂ CONTENU
- // ========================================
- const totalText = Object.values(content).join(' ');
- const wordCount = totalText.split(/\s+/).length;
- const avgElementLength = wordCount / Object.keys(content).length;
-
- // ========================================
- // 2. AJUSTEMENT INTENSITĂ SELON COMPLEXITĂ
- // ========================================
- if (avgElementLength > 200) {
- // Contenu long = intensité réduite pour préserver qualité
- adaptedConfig.imperfectionIntensity = 0.5;
- adaptedConfig.qualityThreshold = 0.8;
- logSh(' đ Contenu long dĂ©tectĂ©: intensitĂ© rĂ©duite', 'DEBUG');
- } else if (avgElementLength < 50) {
- // Contenu court = intensité augmentée
- adaptedConfig.imperfectionIntensity = 1.0;
- adaptedConfig.qualityThreshold = 0.6;
- logSh(' đ Contenu court dĂ©tectĂ©: intensitĂ© augmentĂ©e', 'DEBUG');
- } else {
- // Contenu moyen = intensité équilibrée
- adaptedConfig.imperfectionIntensity = 0.7;
- adaptedConfig.qualityThreshold = 0.7;
- }
-
- // ========================================
- // 3. AJUSTEMENT SELON PERSONNALITĂ
- // ========================================
- const personality = options.csvData?.personality;
- if (personality) {
- const personalityName = personality.nom.toLowerCase();
-
- // Personnalités techniques = moins d'erreurs de personnalité
- if (['marc', 'amara', 'fabrice'].includes(personalityName)) {
- adaptedConfig.imperfectionIntensity *= 0.8;
- logSh(' đ PersonnalitĂ© technique: intensitĂ© erreurs rĂ©duite', 'DEBUG');
- }
-
- // Personnalités créatives = plus d'erreurs stylistiques
- if (['sophie', 'émilie', 'chloé'].includes(personalityName)) {
- adaptedConfig.imperfectionIntensity *= 1.2;
- logSh(' đ PersonnalitĂ© crĂ©ative: intensitĂ© erreurs augmentĂ©e', 'DEBUG');
- }
- }
-
- // ========================================
- // 4. AJUSTEMENT SELON HEURE
- // ========================================
- const currentHour = options.currentHour || new Date().getHours();
-
- if (currentHour >= 22 || currentHour <= 6) {
- // Nuit = plus de fatigue, moins de complexité
- adaptedConfig.fatigueEnabled = true;
- adaptedConfig.temporalStyleEnabled = true;
- adaptedConfig.imperfectionIntensity *= 1.3;
- logSh(' đ PĂ©riode nocturne: simulation fatigue renforcĂ©e', 'DEBUG');
- } else if (currentHour >= 6 && currentHour <= 10) {
- // Matin = énergie, moins d'erreurs
- adaptedConfig.imperfectionIntensity *= 0.7;
- logSh(' đ
Période matinale: intensité réduite', 'DEBUG');
- }
-
- // ========================================
- // 5. LIMITATION SĂCURITĂ
- // ========================================
- adaptedConfig.imperfectionIntensity = Math.max(0.2, Math.min(1.5, adaptedConfig.imperfectionIntensity));
- adaptedConfig.qualityThreshold = Math.max(0.5, Math.min(0.9, adaptedConfig.qualityThreshold));
-
- // Modifs max adaptées à la taille du contenu
- adaptedConfig.maxModificationsPerElement = Math.min(6, Math.max(1, Math.ceil(avgElementLength / 50)));
-
- logSh(` đŻ Config adaptĂ©e: intensitĂ©=${adaptedConfig.imperfectionIntensity.toFixed(2)}, seuil=${adaptedConfig.qualityThreshold.toFixed(2)}`, 'DEBUG');
-
- return adaptedConfig;
-}
-
-/**
- * OBTENIR STACKS DISPONIBLES
- * @returns {array} - Liste des stacks avec métadonnées
- */
-function getAvailableSimulationStacks() {
- return Object.values(HUMAN_SIMULATION_STACKS).map(stack => ({
- name: stack.name,
- description: stack.description,
- layersCount: stack.layersCount,
- expectedImpact: stack.expectedImpact,
- configPreview: {
- fatigueEnabled: stack.config.fatigueEnabled,
- personalityErrorsEnabled: stack.config.personalityErrorsEnabled,
- temporalStyleEnabled: stack.config.temporalStyleEnabled,
- intensity: stack.config.imperfectionIntensity
- }
- }));
-}
-
-/**
- * VALIDATION STACK
- * @param {string} stackName - Nom du stack Ă valider
- * @returns {object} - Résultat validation
- */
-function validateSimulationStack(stackName) {
- const stack = HUMAN_SIMULATION_STACKS[stackName];
-
- if (!stack) {
- return {
- valid: false,
- error: `Stack '${stackName}' non trouvé`,
- availableStacks: Object.keys(HUMAN_SIMULATION_STACKS)
- };
- }
-
- // Validation configuration
- const configIssues = [];
-
- if (typeof stack.config.imperfectionIntensity === 'number' &&
- (stack.config.imperfectionIntensity < 0 || stack.config.imperfectionIntensity > 2)) {
- configIssues.push('intensité hors limites (0-2)');
- }
-
- if (typeof stack.config.qualityThreshold === 'number' &&
- (stack.config.qualityThreshold < 0.3 || stack.config.qualityThreshold > 1)) {
- configIssues.push('seuil qualité hors limites (0.3-1)');
- }
-
- return {
- valid: configIssues.length === 0,
- stack,
- issues: configIssues,
- recommendation: configIssues.length > 0 ?
- 'Corriger la configuration avant utilisation' :
- 'Stack prĂȘt Ă utiliser'
- };
-}
-
-/**
- * RECOMMANDATION STACK AUTOMATIQUE
- * @param {object} context - Contexte { contentLength, personality, hour, goal }
- * @returns {string} - Nom du stack recommandé
- */
-function recommendSimulationStack(context = {}) {
- const { contentLength, personality, hour, goal } = context;
-
- logSh('đ€ Recommandation stack automatique', 'DEBUG');
-
- // Priorité 1: Objectif spécifique
- if (goal === 'development') return 'lightSimulation';
- if (goal === 'maximum_stealth') return 'heavySimulation';
- if (goal === 'personality_focus') return 'personalityFocus';
- if (goal === 'temporal_focus') return 'temporalFocus';
-
- // Priorité 2: Complexité contenu
- if (contentLength > 2000) return 'lightSimulation'; // Contenu long = prudent
- if (contentLength < 300) return 'heavySimulation'; // Contenu court = intensif
-
- // Priorité 3: Personnalité
- if (personality) {
- const personalityName = personality.toLowerCase();
- if (['marc', 'amara', 'fabrice'].includes(personalityName)) {
- return 'standardSimulation'; // Techniques = équilibré
- }
- if (['sophie', 'chloé', 'émilie'].includes(personalityName)) {
- return 'personalityFocus'; // Créatives = focus personnalité
- }
- }
-
- // Priorité 4: Heure
- if (hour >= 22 || hour <= 6) return 'temporalFocus'; // Nuit = focus temporel
- if (hour >= 6 && hour <= 10) return 'lightSimulation'; // Matin = léger
-
- // Par défaut: adaptive pour intelligence contextuelle
- logSh(' đŻ Recommandation: adaptiveSimulation (par dĂ©faut)', 'DEBUG');
- return 'adaptiveSimulation';
-}
-
-// ============= EXPORTS =============
-module.exports = {
- applyPredefinedSimulation,
- getAvailableSimulationStacks,
- validateSimulationStack,
- recommendSimulationStack,
- applyAdaptiveLogic,
- HUMAN_SIMULATION_STACKS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/pattern-breaking/SyntaxVariations.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: SyntaxVariations.js
-// RESPONSABILITĂ: Variations syntaxiques pour casser patterns LLM
-// Techniques: découpage, fusion, restructuration phrases
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * PATTERNS SYNTAXIQUES TYPIQUES LLM Ă ĂVITER
- */
-const LLM_SYNTAX_PATTERNS = {
- // Structures trop prévisibles
- repetitiveStarts: [
- /^Il est important de/gi,
- /^Il convient de/gi,
- /^Il faut noter que/gi,
- /^Dans ce contexte/gi,
- /^Par ailleurs/gi
- ],
-
- // Phrases trop parfaites
- perfectStructures: [
- /^De plus, .+ En outre, .+ Enfin,/gi,
- /^PremiĂšrement, .+ DeuxiĂšmement, .+ TroisiĂšmement,/gi
- ],
-
- // Longueurs trop réguliÚres (détection pattern)
- uniformLengths: true // Détecté dynamiquement
-};
-
-/**
- * VARIATION STRUCTURES SYNTAXIQUES - FONCTION PRINCIPALE
- * @param {string} text - Texte Ă varier
- * @param {number} intensity - Intensité variation (0-1)
- * @param {object} options - Options { preserveReadability, maxModifications }
- * @returns {object} - { content, modifications, stats }
- */
-function varyStructures(text, intensity = 0.3, options = {}) {
- if (!text || text.trim().length === 0) {
- return { content: text, modifications: 0 };
- }
-
- const config = {
- preserveReadability: true,
- maxModifications: 3,
- ...options
- };
-
- logSh(`đ Variation syntaxique: intensitĂ© ${intensity}, prĂ©servation: ${config.preserveReadability}`, 'DEBUG');
-
- let modifiedText = text;
- let totalModifications = 0;
- const stats = {
- sentencesSplit: 0,
- sentencesMerged: 0,
- structuresReorganized: 0,
- repetitiveStartsFixed: 0
- };
-
- try {
- // 1. Analyser structure phrases
- const sentences = analyzeSentenceStructure(modifiedText);
- logSh(` đ ${sentences.length} phrases analysĂ©es`, 'DEBUG');
-
- // 2. Découper phrases longues
- if (Math.random() < intensity) {
- const splitResult = splitLongSentences(modifiedText, intensity);
- modifiedText = splitResult.content;
- totalModifications += splitResult.modifications;
- stats.sentencesSplit = splitResult.modifications;
- }
-
- // 3. Fusionner phrases courtes
- if (Math.random() < intensity * 0.7) {
- const mergeResult = mergeShorter(modifiedText, intensity);
- modifiedText = mergeResult.content;
- totalModifications += mergeResult.modifications;
- stats.sentencesMerged = mergeResult.modifications;
- }
-
- // 4. Réorganiser structures prévisibles
- if (Math.random() < intensity * 0.8) {
- const reorganizeResult = reorganizeStructures(modifiedText, intensity);
- modifiedText = reorganizeResult.content;
- totalModifications += reorganizeResult.modifications;
- stats.structuresReorganized = reorganizeResult.modifications;
- }
-
- // 5. Corriger débuts répétitifs
- if (Math.random() < intensity * 0.6) {
- const repetitiveResult = fixRepetitiveStarts(modifiedText);
- modifiedText = repetitiveResult.content;
- totalModifications += repetitiveResult.modifications;
- stats.repetitiveStartsFixed = repetitiveResult.modifications;
- }
-
- // 6. Limitation sécurité
- if (totalModifications > config.maxModifications) {
- logSh(` â ïž Limitation appliquĂ©e: ${totalModifications} â ${config.maxModifications} modifications`, 'DEBUG');
- totalModifications = config.maxModifications;
- }
-
- logSh(`đ Syntaxe modifiĂ©e: ${totalModifications} changements (${stats.sentencesSplit} splits, ${stats.sentencesMerged} merges)`, 'DEBUG');
-
- } catch (error) {
- logSh(`â Erreur variation syntaxique: ${error.message}`, 'WARNING');
- return { content: text, modifications: 0, stats: {} };
- }
-
- return {
- content: modifiedText,
- modifications: totalModifications,
- stats
- };
-}
-
-/**
- * ANALYSE STRUCTURE PHRASES
- */
-function analyzeSentenceStructure(text) {
- const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
-
- return sentences.map((sentence, index) => ({
- index,
- content: sentence.trim(),
- length: sentence.trim().length,
- wordCount: sentence.trim().split(/\s+/).length,
- isLong: sentence.trim().length > 120,
- isShort: sentence.trim().length < 40,
- hasComplexStructure: sentence.includes(',') && sentence.includes(' qui ') || sentence.includes(' que ')
- }));
-}
-
-/**
- * DĂCOUPAGE PHRASES LONGUES
- */
-function splitLongSentences(text, intensity) {
- let modified = text;
- let modifications = 0;
-
- const sentences = modified.split('. ');
- const processedSentences = sentences.map(sentence => {
-
- // Phrases longues (>100 chars) et probabilité selon intensité - PLUS AGRESSIF
- if (sentence.length > 100 && Math.random() < (intensity * 0.6)) {
-
- // Points de découpe naturels
- const cutPoints = [
- { pattern: /, qui (.+)/, replacement: '. Celui-ci $1' },
- { pattern: /, que (.+)/, replacement: '. Cela $1' },
- { pattern: /, dont (.+)/, replacement: '. Celui-ci $1' },
- { pattern: / et (.{30,})/, replacement: '. De plus, $1' },
- { pattern: /, car (.+)/, replacement: '. En effet, $1' },
- { pattern: /, mais (.+)/, replacement: '. Cependant, $1' }
- ];
-
- for (const cutPoint of cutPoints) {
- if (sentence.match(cutPoint.pattern)) {
- const newSentence = sentence.replace(cutPoint.pattern, cutPoint.replacement);
- if (newSentence !== sentence) {
- modifications++;
- logSh(` âïž Phrase dĂ©coupĂ©e: ${sentence.length} â ${newSentence.length} chars`, 'DEBUG');
- return newSentence;
- }
- }
- }
- }
-
- return sentence;
- });
-
- return {
- content: processedSentences.join('. '),
- modifications
- };
-}
-
-/**
- * FUSION PHRASES COURTES
- */
-function mergeShorter(text, intensity) {
- let modified = text;
- let modifications = 0;
-
- const sentences = modified.split('. ');
- const processedSentences = [];
-
- for (let i = 0; i < sentences.length; i++) {
- const current = sentences[i];
- const next = sentences[i + 1];
-
- // Si phrase courte (<50 chars) et phrase suivante existe - PLUS AGRESSIF
- if (current && current.length < 50 && next && next.length < 70 && Math.random() < (intensity * 0.5)) {
-
- // Connecteurs pour fusion naturelle
- const connectors = [', de plus,', ', également,', ', aussi,', ' et'];
- const connector = connectors[Math.floor(Math.random() * connectors.length)];
-
- const merged = current + connector + ' ' + next.toLowerCase();
- processedSentences.push(merged);
- modifications++;
-
- logSh(` đ Phrases fusionnĂ©es: ${current.length} + ${next.length} â ${merged.length} chars`, 'DEBUG');
-
- i++; // Passer la phrase suivante car fusionnée
- } else {
- processedSentences.push(current);
- }
- }
-
- return {
- content: processedSentences.join('. '),
- modifications
- };
-}
-
-/**
- * RĂORGANISATION STRUCTURES PRĂVISIBLES
- */
-function reorganizeStructures(text, intensity) {
- let modified = text;
- let modifications = 0;
-
- // Détecter énumérations prévisibles
- const enumerationPatterns = [
- {
- pattern: /PremiĂšrement, (.+?)\. DeuxiĂšmement, (.+?)\. TroisiĂšmement, (.+?)\./gi,
- replacement: 'D\'abord, $1. Ensuite, $2. Enfin, $3.'
- },
- {
- pattern: /D\'une part, (.+?)\. D\'autre part, (.+?)\./gi,
- replacement: 'Tout d\'abord, $1. Par ailleurs, $2.'
- },
- {
- pattern: /En premier lieu, (.+?)\. En second lieu, (.+?)\./gi,
- replacement: 'Dans un premier temps, $1. Puis, $2.'
- }
- ];
-
- enumerationPatterns.forEach(pattern => {
- if (modified.match(pattern.pattern) && Math.random() < intensity) {
- modified = modified.replace(pattern.pattern, pattern.replacement);
- modifications++;
- logSh(` đ Structure rĂ©organisĂ©e: Ă©numĂ©ration variĂ©e`, 'DEBUG');
- }
- });
-
- return {
- content: modified,
- modifications
- };
-}
-
-/**
- * CORRECTION DĂBUTS RĂPĂTITIFS
- */
-function fixRepetitiveStarts(text) {
- let modified = text;
- let modifications = 0;
-
- const sentences = modified.split('. ');
- const startWords = [];
-
- // Analyser débuts de phrases
- sentences.forEach(sentence => {
- const words = sentence.trim().split(/\s+/);
- if (words.length > 0) {
- startWords.push(words[0].toLowerCase());
- }
- });
-
- // Détecter répétitions
- const startCounts = {};
- startWords.forEach(word => {
- startCounts[word] = (startCounts[word] || 0) + 1;
- });
-
- // Remplacer débuts répétitifs
- const alternatives = {
- 'il': ['Cet élément', 'Cette solution', 'Ce produit'],
- 'cette': ['Cette option', 'Cette approche', 'Cette méthode'],
- 'pour': ['Afin de', 'Dans le but de', 'En vue de'],
- 'avec': ['GrĂące Ă ', 'Au moyen de', 'En utilisant'],
- 'dans': ['Au sein de', 'Ă travers', 'Parmi']
- };
-
- const processedSentences = sentences.map(sentence => {
- const firstWord = sentence.trim().split(/\s+/)[0]?.toLowerCase();
-
- if (firstWord && startCounts[firstWord] > 2 && alternatives[firstWord] && Math.random() < 0.4) {
- const replacement = alternatives[firstWord][Math.floor(Math.random() * alternatives[firstWord].length)];
- const newSentence = sentence.replace(/^\w+/, replacement);
- modifications++;
- logSh(` đ DĂ©but variĂ©: "${firstWord}" â "${replacement}"`, 'DEBUG');
- return newSentence;
- }
-
- return sentence;
- });
-
- return {
- content: processedSentences.join('. '),
- modifications
- };
-}
-
-/**
- * DĂTECTION UNIFORMITĂ LONGUEURS (Pattern LLM)
- */
-function detectUniformLengths(text) {
- const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
-
- if (sentences.length < 3) return { uniform: false, variance: 0 };
-
- const lengths = sentences.map(s => s.trim().length);
- const avgLength = lengths.reduce((sum, len) => sum + len, 0) / lengths.length;
-
- // Calculer variance
- const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / lengths.length;
- const standardDev = Math.sqrt(variance);
-
- // Uniformité si écart-type faible par rapport à moyenne
- const coefficientVariation = standardDev / avgLength;
- const uniform = coefficientVariation < 0.3; // Seuil arbitraire
-
- return {
- uniform,
- variance: coefficientVariation,
- avgLength,
- standardDev,
- sentenceCount: sentences.length
- };
-}
-
-/**
- * AJOUT VARIATIONS MICRO-SYNTAXIQUES
- */
-function addMicroVariations(text, intensity) {
- let modified = text;
- let modifications = 0;
-
- // Micro-variations subtiles
- const microPatterns = [
- { from: /\btrĂšs (.+?)\b/g, to: 'particuliĂšrement $1', probability: 0.3 },
- { from: /\bassez (.+?)\b/g, to: 'plutĂŽt $1', probability: 0.4 },
- { from: /\bbeaucoup de/g, to: 'de nombreux', probability: 0.3 },
- { from: /\bpermets de/g, to: 'permet de', probability: 0.8 }, // Correction fréquente
- { from: /\bien effet\b/g, to: 'effectivement', probability: 0.2 }
- ];
-
- microPatterns.forEach(pattern => {
- if (Math.random() < (intensity * pattern.probability)) {
- const before = modified;
- modified = modified.replace(pattern.from, pattern.to);
- if (modified !== before) {
- modifications++;
- logSh(` đ§ Micro-variation: ${pattern.from} â ${pattern.to}`, 'DEBUG');
- }
- }
- });
-
- return {
- content: modified,
- modifications
- };
-}
-
-// ============= EXPORTS =============
-module.exports = {
- varyStructures,
- splitLongSentences,
- mergeShorter,
- reorganizeStructures,
- fixRepetitiveStarts,
- analyzeSentenceStructure,
- detectUniformLengths,
- addMicroVariations,
- LLM_SYNTAX_PATTERNS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/pattern-breaking/LLMFingerprints.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: LLMFingerprints.js
-// RESPONSABILITĂ: Remplacement mots et expressions typiques LLM
-// Identification et remplacement des "fingerprints" IA
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * MOTS ET EXPRESSIONS TYPIQUES LLM Ă REMPLACER
- * Classés par niveau de suspicion et fréquence d'usage LLM
- */
-const LLM_FINGERPRINTS = {
-
- // ========================================
- // NIVEAU CRITIQUE - TrĂšs suspects
- // ========================================
- critical: {
- adjectives: [
- { word: 'comprehensive', alternatives: ['complet', 'détaillé', 'approfondi', 'exhaustif'], suspicion: 0.95 },
- { word: 'robust', alternatives: ['solide', 'fiable', 'résistant', 'durable'], suspicion: 0.92 },
- { word: 'seamless', alternatives: ['fluide', 'harmonieux', 'sans accroc', 'naturel'], suspicion: 0.90 },
- { word: 'optimal', alternatives: ['idéal', 'parfait', 'excellent', 'adapté'], suspicion: 0.88 },
- { word: 'cutting-edge', alternatives: ['innovant', 'moderne', 'récent', 'avancé'], suspicion: 0.87 },
- { word: 'state-of-the-art', alternatives: ['dernier cri', 'moderne', 'récent'], suspicion: 0.95 }
- ],
-
- expressions: [
- { phrase: 'il est important de noter que', alternatives: ['remarquons que', 'signalons que', 'précisons que'], suspicion: 0.85 },
- { phrase: 'dans le paysage actuel', alternatives: ['actuellement', 'de nos jours', 'aujourd\'hui'], suspicion: 0.82 },
- { phrase: 'il convient de souligner', alternatives: ['il faut noter', 'soulignons', 'remarquons'], suspicion: 0.80 },
- { phrase: 'en fin de compte', alternatives: ['finalement', 'au final', 'pour conclure'], suspicion: 0.75 }
- ]
- },
-
- // ========================================
- // NIVEAU ĂLEVĂ - Souvent suspects
- // ========================================
- high: {
- adjectives: [
- { word: 'innovative', alternatives: ['novateur', 'créatif', 'original', 'moderne'], suspicion: 0.75 },
- { word: 'efficient', alternatives: ['efficace', 'performant', 'rapide', 'pratique'], suspicion: 0.70 },
- { word: 'versatile', alternatives: ['polyvalent', 'adaptable', 'flexible', 'modulable'], suspicion: 0.68 },
- { word: 'sophisticated', alternatives: ['raffiné', 'élaboré', 'avancé', 'complexe'], suspicion: 0.65 },
- { word: 'compelling', alternatives: ['convaincant', 'captivant', 'intéressant'], suspicion: 0.72 }
- ],
-
- verbs: [
- { word: 'leverage', alternatives: ['utiliser', 'exploiter', 'tirer parti de', 'employer'], suspicion: 0.80 },
- { word: 'optimize', alternatives: ['améliorer', 'perfectionner', 'ajuster'], suspicion: 0.65 },
- { word: 'streamline', alternatives: ['simplifier', 'rationaliser', 'organiser'], suspicion: 0.75 },
- { word: 'enhance', alternatives: ['améliorer', 'enrichir', 'renforcer'], suspicion: 0.60 }
- ],
-
- expressions: [
- { phrase: 'par ailleurs', alternatives: ['de plus', 'également', 'aussi', 'en outre'], suspicion: 0.65 },
- { phrase: 'en outre', alternatives: ['de plus', 'également', 'aussi'], suspicion: 0.70 },
- { phrase: 'cela dit', alternatives: ['néanmoins', 'toutefois', 'cependant'], suspicion: 0.60 }
- ]
- },
-
- // ========================================
- // NIVEAU MODĂRĂ - Parfois suspects
- // ========================================
- moderate: {
- adjectives: [
- { word: 'significant', alternatives: ['important', 'notable', 'considérable', 'marquant'], suspicion: 0.55 },
- { word: 'essential', alternatives: ['indispensable', 'crucial', 'vital', 'nécessaire'], suspicion: 0.50 },
- { word: 'comprehensive', alternatives: ['complet', 'global', 'détaillé'], suspicion: 0.58 },
- { word: 'effective', alternatives: ['efficace', 'performant', 'réussi'], suspicion: 0.45 }
- ],
-
- expressions: [
- { phrase: 'il est essentiel de', alternatives: ['il faut', 'il importe de', 'il est crucial de'], suspicion: 0.55 },
- { phrase: 'dans cette optique', alternatives: ['dans cette perspective', 'ainsi', 'de ce fait'], suspicion: 0.52 },
- { phrase: 'à cet égard', alternatives: ['sur ce point', 'concernant cela', 'à ce propos'], suspicion: 0.48 }
- ]
- }
-};
-
-/**
- * PATTERNS STRUCTURELS LLM
- */
-const STRUCTURAL_PATTERNS = {
- // Débuts de phrases trop formels
- formalStarts: [
- /^Il est important de souligner que/gi,
- /^Il convient de noter que/gi,
- /^Il est essentiel de comprendre que/gi,
- /^Dans ce contexte, il est crucial de/gi,
- /^Il est primordial de/gi
- ],
-
- // Transitions trop parfaites
- perfectTransitions: [
- /\. Par ailleurs, (.+?)\. En outre, (.+?)\. De plus,/gi,
- /\. PremiĂšrement, (.+?)\. DeuxiĂšmement, (.+?)\. TroisiĂšmement,/gi
- ],
-
- // Conclusions trop formelles
- formalConclusions: [
- /En conclusion, il apparaĂźt clairement que/gi,
- /Pour conclure, il est évident que/gi,
- /En définitive, nous pouvons affirmer que/gi
- ]
-};
-
-/**
- * DĂTECTION PATTERNS LLM DANS LE TEXTE
- * @param {string} text - Texte Ă analyser
- * @returns {object} - { count, patterns, suspicionScore }
- */
-function detectLLMPatterns(text) {
- if (!text || text.trim().length === 0) {
- return { count: 0, patterns: [], suspicionScore: 0 };
- }
-
- const detectedPatterns = [];
- let totalSuspicion = 0;
- let wordCount = text.split(/\s+/).length;
-
- // Analyser tous les niveaux de fingerprints
- Object.entries(LLM_FINGERPRINTS).forEach(([level, categories]) => {
- Object.entries(categories).forEach(([category, items]) => {
- items.forEach(item => {
- const regex = new RegExp(`\\b${item.word || item.phrase}\\b`, 'gi');
- const matches = text.match(regex);
-
- if (matches) {
- detectedPatterns.push({
- pattern: item.word || item.phrase,
- type: category,
- level: level,
- count: matches.length,
- suspicion: item.suspicion,
- alternatives: item.alternatives
- });
-
- totalSuspicion += item.suspicion * matches.length;
- }
- });
- });
- });
-
- // Analyser patterns structurels
- Object.entries(STRUCTURAL_PATTERNS).forEach(([patternType, patterns]) => {
- patterns.forEach(pattern => {
- const matches = text.match(pattern);
- if (matches) {
- detectedPatterns.push({
- pattern: pattern.source,
- type: 'structural',
- level: 'high',
- count: matches.length,
- suspicion: 0.80
- });
-
- totalSuspicion += 0.80 * matches.length;
- }
- });
- });
-
- const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0;
-
- logSh(`đ Patterns LLM dĂ©tectĂ©s: ${detectedPatterns.length} (score suspicion: ${suspicionScore.toFixed(3)})`, 'DEBUG');
-
- return {
- count: detectedPatterns.length,
- patterns: detectedPatterns.map(p => p.pattern),
- detailedPatterns: detectedPatterns,
- suspicionScore,
- recommendation: suspicionScore > 0.05 ? 'replacement' : 'minor_cleanup'
- };
-}
-
-/**
- * REMPLACEMENT FINGERPRINTS LLM
- * @param {string} text - Texte Ă traiter
- * @param {object} options - Options { intensity, preserveContext, maxReplacements }
- * @returns {object} - { content, replacements, details }
- */
-function replaceLLMFingerprints(text, options = {}) {
- if (!text || text.trim().length === 0) {
- return { content: text, replacements: 0 };
- }
-
- const config = {
- intensity: 0.5,
- preserveContext: true,
- maxReplacements: 5,
- ...options
- };
-
- logSh(`đ€ Remplacement fingerprints LLM: intensitĂ© ${config.intensity}`, 'DEBUG');
-
- let modifiedText = text;
- let totalReplacements = 0;
- const replacementDetails = [];
-
- try {
- // Détecter d'abord les patterns
- const detection = detectLLMPatterns(modifiedText);
-
- if (detection.count === 0) {
- logSh(` â
Aucun fingerprint LLM détecté`, 'DEBUG');
- return { content: text, replacements: 0, details: [] };
- }
-
- // Traiter par niveau de priorité
- const priorities = ['critical', 'high', 'moderate'];
-
- for (const priority of priorities) {
- if (totalReplacements >= config.maxReplacements) break;
-
- const categoryData = LLM_FINGERPRINTS[priority];
- if (!categoryData) continue;
-
- // Traiter chaque catégorie
- Object.entries(categoryData).forEach(([category, items]) => {
- items.forEach(item => {
- if (totalReplacements >= config.maxReplacements) return;
-
- const searchTerm = item.word || item.phrase;
- const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi');
-
- // Probabilité de remplacement basée sur suspicion et intensité
- const replacementProbability = item.suspicion * config.intensity;
-
- if (modifiedText.match(regex) && Math.random() < replacementProbability) {
- // Choisir alternative aléatoire
- const alternative = item.alternatives[Math.floor(Math.random() * item.alternatives.length)];
-
- const beforeText = modifiedText;
- modifiedText = modifiedText.replace(regex, alternative);
-
- if (modifiedText !== beforeText) {
- totalReplacements++;
- replacementDetails.push({
- original: searchTerm,
- replacement: alternative,
- category,
- level: priority,
- suspicion: item.suspicion
- });
-
- logSh(` đ RemplacĂ© "${searchTerm}" â "${alternative}" (suspicion: ${item.suspicion})`, 'DEBUG');
- }
- }
- });
- });
- }
-
- // Traitement patterns structurels
- if (totalReplacements < config.maxReplacements) {
- const structuralResult = replaceStructuralPatterns(modifiedText, config.intensity);
- modifiedText = structuralResult.content;
- totalReplacements += structuralResult.replacements;
- replacementDetails.push(...structuralResult.details);
- }
-
- logSh(`đ€ Fingerprints remplacĂ©s: ${totalReplacements} modifications`, 'DEBUG');
-
- } catch (error) {
- logSh(`â Erreur remplacement fingerprints: ${error.message}`, 'WARNING');
- return { content: text, replacements: 0, details: [] };
- }
-
- return {
- content: modifiedText,
- replacements: totalReplacements,
- details: replacementDetails
- };
-}
-
-/**
- * REMPLACEMENT PATTERNS STRUCTURELS
- */
-function replaceStructuralPatterns(text, intensity) {
- let modified = text;
- let replacements = 0;
- const details = [];
-
- // DĂ©buts formels â versions plus naturelles
- const formalStartReplacements = [
- {
- from: /^Il est important de souligner que (.+)/gim,
- to: 'Notons que $1',
- name: 'début formel'
- },
- {
- from: /^Il convient de noter que (.+)/gim,
- to: 'Précisons que $1',
- name: 'formulation convient'
- },
- {
- from: /^Dans ce contexte, il est crucial de (.+)/gim,
- to: 'Il faut $1',
- name: 'contexte crucial'
- }
- ];
-
- formalStartReplacements.forEach(replacement => {
- if (Math.random() < intensity * 0.7) {
- const before = modified;
- modified = modified.replace(replacement.from, replacement.to);
-
- if (modified !== before) {
- replacements++;
- details.push({
- original: replacement.name,
- replacement: 'version naturelle',
- category: 'structural',
- level: 'high',
- suspicion: 0.80
- });
-
- logSh(` đïž Pattern structurel remplacĂ©: ${replacement.name}`, 'DEBUG');
- }
- }
- });
-
- return {
- content: modified,
- replacements,
- details
- };
-}
-
-/**
- * ANALYSE DENSITĂ FINGERPRINTS
- */
-function analyzeFingerprintDensity(text) {
- const detection = detectLLMPatterns(text);
- const wordCount = text.split(/\s+/).length;
-
- const density = detection.count / wordCount;
- const riskLevel = density > 0.08 ? 'high' : density > 0.04 ? 'medium' : 'low';
-
- return {
- fingerprintCount: detection.count,
- wordCount,
- density,
- riskLevel,
- suspicionScore: detection.suspicionScore,
- recommendation: riskLevel === 'high' ? 'immediate_replacement' :
- riskLevel === 'medium' ? 'selective_replacement' : 'minimal_cleanup'
- };
-}
-
-/**
- * SUGGESTIONS CONTEXTUELLES
- */
-function generateContextualAlternatives(word, context, personality) {
- // Adapter selon personnalité si fournie
- if (personality) {
- const personalityAdaptations = {
- 'marc': { 'optimal': 'efficace', 'robust': 'solide', 'comprehensive': 'complet' },
- 'sophie': { 'optimal': 'parfait', 'robust': 'résistant', 'comprehensive': 'détaillé' },
- 'kevin': { 'optimal': 'nickel', 'robust': 'costaud', 'comprehensive': 'complet' }
- };
-
- const adaptations = personalityAdaptations[personality.toLowerCase()];
- if (adaptations && adaptations[word]) {
- return [adaptations[word]];
- }
- }
-
- // Suggestions contextuelles basiques
- const contextualMappings = {
- 'optimal': context.includes('solution') ? ['idéale', 'parfaite'] : ['excellent', 'adapté'],
- 'robust': context.includes('systÚme') ? ['fiable', 'stable'] : ['solide', 'résistant'],
- 'comprehensive': context.includes('analyse') ? ['approfondie', 'détaillée'] : ['complÚte', 'globale']
- };
-
- return contextualMappings[word] || ['standard'];
-}
-
-// ============= EXPORTS =============
-module.exports = {
- detectLLMPatterns,
- replaceLLMFingerprints,
- replaceStructuralPatterns,
- analyzeFingerprintDensity,
- generateContextualAlternatives,
- LLM_FINGERPRINTS,
- STRUCTURAL_PATTERNS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/pattern-breaking/NaturalConnectors.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: NaturalConnectors.js
-// RESPONSABILITĂ: Humanisation des connecteurs et transitions
-// Remplacement connecteurs formels par versions naturelles
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * CONNECTEURS FORMELS LLM Ă HUMANISER
- */
-const FORMAL_CONNECTORS = {
- // Connecteurs trop formels/académiques
- formal: [
- { connector: 'par ailleurs', alternatives: ['aussi', 'également', 'de plus', 'en plus'], suspicion: 0.75 },
- { connector: 'en outre', alternatives: ['de plus', 'également', 'aussi', 'en plus'], suspicion: 0.80 },
- { connector: 'de surcroĂźt', alternatives: ['de plus', 'aussi', 'en plus'], suspicion: 0.85 },
- { connector: 'qui plus est', alternatives: ['en plus', 'et puis', 'aussi'], suspicion: 0.80 },
- { connector: 'par conséquent', alternatives: ['donc', 'alors', 'du coup', 'résultat'], suspicion: 0.70 },
- { connector: 'en conséquence', alternatives: ['donc', 'alors', 'du coup'], suspicion: 0.75 },
- { connector: 'néanmoins', alternatives: ['mais', 'pourtant', 'cependant', 'malgré ça'], suspicion: 0.65 },
- { connector: 'toutefois', alternatives: ['mais', 'pourtant', 'cependant'], suspicion: 0.70 }
- ],
-
- // Débuts de phrases formels
- formalStarts: [
- { phrase: 'il convient de noter que', alternatives: ['notons que', 'remarquons que', 'précisons que'], suspicion: 0.90 },
- { phrase: 'il est important de souligner que', alternatives: ['soulignons que', 'notons que', 'précisons que'], suspicion: 0.85 },
- { phrase: 'il est à noter que', alternatives: ['notons que', 'signalons que', 'précisons que'], suspicion: 0.80 },
- { phrase: 'il convient de préciser que', alternatives: ['précisons que', 'ajoutons que', 'notons que'], suspicion: 0.75 },
- { phrase: 'dans ce contexte', alternatives: ['ici', 'dans ce cas', 'alors'], suspicion: 0.70 }
- ],
-
- // Transitions artificielles
- artificialTransitions: [
- { phrase: 'abordons maintenant', alternatives: ['passons Ă ', 'voyons', 'parlons de'], suspicion: 0.75 },
- { phrase: 'examinons à présent', alternatives: ['voyons', 'regardons', 'passons à '], suspicion: 0.80 },
- { phrase: 'intéressons-nous désormais à ', alternatives: ['voyons', 'parlons de', 'passons à '], suspicion: 0.85 },
- { phrase: 'penchons-nous sur', alternatives: ['voyons', 'regardons', 'parlons de'], suspicion: 0.70 }
- ]
-};
-
-/**
- * CONNECTEURS NATURELS PAR CONTEXTE
- */
-const NATURAL_CONNECTORS_BY_CONTEXT = {
- // Selon le ton/registre souhaité
- casual: ['du coup', 'alors', 'et puis', 'aussi', 'en fait'],
- conversational: ['bon', 'eh bien', 'donc', 'alors', 'et puis'],
- technical: ['donc', 'ainsi', 'alors', 'par là ', 'de cette façon'],
- commercial: ['donc', 'alors', 'ainsi', 'de plus', 'aussi']
-};
-
-/**
- * HUMANISATION CONNECTEURS ET TRANSITIONS - FONCTION PRINCIPALE
- * @param {string} text - Texte Ă humaniser
- * @param {object} options - Options { intensity, preserveMeaning, maxReplacements }
- * @returns {object} - { content, replacements, details }
- */
-function humanizeTransitions(text, options = {}) {
- if (!text || text.trim().length === 0) {
- return { content: text, replacements: 0 };
- }
-
- const config = {
- intensity: 0.6,
- preserveMeaning: true,
- maxReplacements: 4,
- tone: 'casual', // casual, conversational, technical, commercial
- ...options
- };
-
- logSh(`đ Humanisation connecteurs: intensitĂ© ${config.intensity}, ton ${config.tone}`, 'DEBUG');
-
- let modifiedText = text;
- let totalReplacements = 0;
- const replacementDetails = [];
-
- try {
- // 1. Remplacer connecteurs formels
- const connectorsResult = replaceFormalConnectors(modifiedText, config);
- modifiedText = connectorsResult.content;
- totalReplacements += connectorsResult.replacements;
- replacementDetails.push(...connectorsResult.details);
-
- // 2. Humaniser débuts de phrases
- if (totalReplacements < config.maxReplacements) {
- const startsResult = humanizeFormalStarts(modifiedText, config);
- modifiedText = startsResult.content;
- totalReplacements += startsResult.replacements;
- replacementDetails.push(...startsResult.details);
- }
-
- // 3. Remplacer transitions artificielles
- if (totalReplacements < config.maxReplacements) {
- const transitionsResult = replaceArtificialTransitions(modifiedText, config);
- modifiedText = transitionsResult.content;
- totalReplacements += transitionsResult.replacements;
- replacementDetails.push(...transitionsResult.details);
- }
-
- // 4. Ajouter variabilité contextuelle
- if (totalReplacements < config.maxReplacements) {
- const contextResult = addContextualVariability(modifiedText, config);
- modifiedText = contextResult.content;
- totalReplacements += contextResult.replacements;
- replacementDetails.push(...contextResult.details);
- }
-
- logSh(`đ Connecteurs humanisĂ©s: ${totalReplacements} remplacements effectuĂ©s`, 'DEBUG');
-
- } catch (error) {
- logSh(`â Erreur humanisation connecteurs: ${error.message}`, 'WARNING');
- return { content: text, replacements: 0, details: [] };
- }
-
- return {
- content: modifiedText,
- replacements: totalReplacements,
- details: replacementDetails
- };
-}
-
-/**
- * REMPLACEMENT CONNECTEURS FORMELS
- */
-function replaceFormalConnectors(text, config) {
- let modified = text;
- let replacements = 0;
- const details = [];
-
- FORMAL_CONNECTORS.formal.forEach(connector => {
- if (replacements >= Math.floor(config.maxReplacements / 2)) return;
-
- const regex = new RegExp(`\\b${connector.connector}\\b`, 'gi');
- const matches = modified.match(regex);
-
- if (matches && Math.random() < (config.intensity * connector.suspicion)) {
- // Choisir alternative selon contexte/ton
- const availableAlts = connector.alternatives;
- const contextualAlts = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || [];
-
- // Préférer alternatives contextuelles si disponibles
- const preferredAlts = availableAlts.filter(alt => contextualAlts.includes(alt));
- const finalAlts = preferredAlts.length > 0 ? preferredAlts : availableAlts;
-
- const chosen = finalAlts[Math.floor(Math.random() * finalAlts.length)];
-
- const beforeText = modified;
- modified = modified.replace(regex, chosen);
-
- if (modified !== beforeText) {
- replacements++;
- details.push({
- original: connector.connector,
- replacement: chosen,
- type: 'formal_connector',
- suspicion: connector.suspicion
- });
-
- logSh(` đ Connecteur formalisĂ©: "${connector.connector}" â "${chosen}"`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, replacements, details };
-}
-
-/**
- * HUMANISATION DĂBUTS DE PHRASES FORMELS
- */
-function humanizeFormalStarts(text, config) {
- let modified = text;
- let replacements = 0;
- const details = [];
-
- FORMAL_CONNECTORS.formalStarts.forEach(start => {
- if (replacements >= Math.floor(config.maxReplacements / 3)) return;
-
- const regex = new RegExp(start.phrase, 'gi');
-
- if (modified.match(regex) && Math.random() < (config.intensity * start.suspicion)) {
- const alternative = start.alternatives[Math.floor(Math.random() * start.alternatives.length)];
-
- const beforeText = modified;
- modified = modified.replace(regex, alternative);
-
- if (modified !== beforeText) {
- replacements++;
- details.push({
- original: start.phrase,
- replacement: alternative,
- type: 'formal_start',
- suspicion: start.suspicion
- });
-
- logSh(` đ DĂ©but formalisĂ©: "${start.phrase}" â "${alternative}"`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, replacements, details };
-}
-
-/**
- * REMPLACEMENT TRANSITIONS ARTIFICIELLES
- */
-function replaceArtificialTransitions(text, config) {
- let modified = text;
- let replacements = 0;
- const details = [];
-
- FORMAL_CONNECTORS.artificialTransitions.forEach(transition => {
- if (replacements >= Math.floor(config.maxReplacements / 4)) return;
-
- const regex = new RegExp(transition.phrase, 'gi');
-
- if (modified.match(regex) && Math.random() < (config.intensity * transition.suspicion * 0.8)) {
- const alternative = transition.alternatives[Math.floor(Math.random() * transition.alternatives.length)];
-
- const beforeText = modified;
- modified = modified.replace(regex, alternative);
-
- if (modified !== beforeText) {
- replacements++;
- details.push({
- original: transition.phrase,
- replacement: alternative,
- type: 'artificial_transition',
- suspicion: transition.suspicion
- });
-
- logSh(` đ Transition artificialisĂ©e: "${transition.phrase}" â "${alternative}"`, 'DEBUG');
- }
- }
- });
-
- return { content: modified, replacements, details };
-}
-
-/**
- * AJOUT VARIABILITĂ CONTEXTUELLE
- */
-function addContextualVariability(text, config) {
- let modified = text;
- let replacements = 0;
- const details = [];
-
- // Connecteurs génériques à contextualiser selon le ton
- const genericPatterns = [
- { from: /\bet puis\b/g, contextual: true },
- { from: /\bdone\b/g, contextual: true },
- { from: /\bainsi\b/g, contextual: true }
- ];
-
- const contextualReplacements = NATURAL_CONNECTORS_BY_CONTEXT[config.tone] || NATURAL_CONNECTORS_BY_CONTEXT.casual;
-
- genericPatterns.forEach(pattern => {
- if (replacements >= 2) return;
-
- if (pattern.contextual && Math.random() < (config.intensity * 0.4)) {
- const matches = modified.match(pattern.from);
-
- if (matches && contextualReplacements.length > 0) {
- const replacement = contextualReplacements[Math.floor(Math.random() * contextualReplacements.length)];
-
- // Ăviter remplacements identiques
- if (replacement !== matches[0]) {
- const beforeText = modified;
- modified = modified.replace(pattern.from, replacement);
-
- if (modified !== beforeText) {
- replacements++;
- details.push({
- original: matches[0],
- replacement,
- type: 'contextual_variation',
- suspicion: 0.4
- });
-
- logSh(` đŻ Variation contextuelle: "${matches[0]}" â "${replacement}"`, 'DEBUG');
- }
- }
- }
- }
- });
-
- return { content: modified, replacements, details };
-}
-
-/**
- * DĂTECTION CONNECTEURS FORMELS DANS TEXTE
- */
-function detectFormalConnectors(text) {
- if (!text || text.trim().length === 0) {
- return { count: 0, connectors: [], suspicionScore: 0 };
- }
-
- const detectedConnectors = [];
- let totalSuspicion = 0;
-
- // Vérifier tous les types de connecteurs formels
- Object.values(FORMAL_CONNECTORS).flat().forEach(item => {
- const searchTerm = item.connector || item.phrase;
- const regex = new RegExp(`\\b${searchTerm}\\b`, 'gi');
- const matches = text.match(regex);
-
- if (matches) {
- detectedConnectors.push({
- connector: searchTerm,
- count: matches.length,
- suspicion: item.suspicion,
- alternatives: item.alternatives
- });
-
- totalSuspicion += item.suspicion * matches.length;
- }
- });
-
- const wordCount = text.split(/\s+/).length;
- const suspicionScore = wordCount > 0 ? totalSuspicion / wordCount : 0;
-
- logSh(`đ Connecteurs formels dĂ©tectĂ©s: ${detectedConnectors.length} (score: ${suspicionScore.toFixed(3)})`, 'DEBUG');
-
- return {
- count: detectedConnectors.length,
- connectors: detectedConnectors.map(c => c.connector),
- detailedConnectors: detectedConnectors,
- suspicionScore,
- recommendation: suspicionScore > 0.03 ? 'humanize' : 'minimal_changes'
- };
-}
-
-/**
- * ANALYSE DENSITĂ CONNECTEURS FORMELS
- */
-function analyzeConnectorFormality(text) {
- const detection = detectFormalConnectors(text);
- const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
-
- const density = detection.count / sentences.length;
- const formalityLevel = density > 0.4 ? 'high' : density > 0.2 ? 'medium' : 'low';
-
- return {
- connectorsCount: detection.count,
- sentenceCount: sentences.length,
- density,
- formalityLevel,
- suspicionScore: detection.suspicionScore,
- recommendation: formalityLevel === 'high' ? 'extensive_humanization' :
- formalityLevel === 'medium' ? 'selective_humanization' : 'minimal_humanization'
- };
-}
-
-// ============= EXPORTS =============
-module.exports = {
- humanizeTransitions,
- replaceFormalConnectors,
- humanizeFormalStarts,
- replaceArtificialTransitions,
- addContextualVariability,
- detectFormalConnectors,
- analyzeConnectorFormality,
- FORMAL_CONNECTORS,
- NATURAL_CONNECTORS_BY_CONTEXT
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/pattern-breaking/PatternBreakingCore.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: PatternBreakingCore.js
-// RESPONSABILITĂ: Orchestrateur principal Pattern Breaking
-// Niveau 2: Casser les patterns syntaxiques typiques des LLMs
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-const { tracer } = require('../trace');
-const { varyStructures, splitLongSentences, mergeShorter } = require('./SyntaxVariations');
-const { replaceLLMFingerprints, detectLLMPatterns } = require('./LLMFingerprints');
-const { humanizeTransitions, replaceConnectors } = require('./NaturalConnectors');
-
-/**
- * CONFIGURATION MODULAIRE AGRESSIVE PATTERN BREAKING
- * Chaque feature peut ĂȘtre activĂ©e/dĂ©sactivĂ©e individuellement
- */
-const DEFAULT_CONFIG = {
- // ========================================
- // CONTRĂLES GLOBAUX
- // ========================================
- intensityLevel: 0.8, // Intensité globale (0-1) - PLUS AGRESSIVE
- preserveReadability: true, // Maintenir lisibilité
- maxModificationsPerElement: 8, // Limite modifications par Ă©lĂ©ment - DOUBLĂE
- qualityThreshold: 0.5, // Seuil qualitĂ© minimum - ABAISSĂ
-
- // ========================================
- // FEATURES SYNTAXE & STRUCTURE
- // ========================================
- syntaxVariationEnabled: true, // Variations syntaxiques de base
- aggressiveSentenceSplitting: true, // Découpage phrases plus agressif (<80 chars)
- aggressiveSentenceMerging: true, // Fusion phrases courtes (<60 chars)
- microSyntaxVariations: true, // Micro-variations subtiles
- questionInjection: true, // Injection questions rhétoriques
-
- // ========================================
- // FEATURES LLM FINGERPRINTS
- // ========================================
- llmFingerprintReplacement: true, // Remplacement fingerprints de base
- frenchLLMPatterns: true, // Patterns spécifiques français
- overlyFormalVocabulary: true, // Vocabulaire trop formel â casual
- repetitiveStarters: true, // Débuts de phrases répétitifs
- perfectTransitions: true, // Transitions trop parfaites
-
- // ========================================
- // FEATURES CONNECTEURS & TRANSITIONS
- // ========================================
- naturalConnectorsEnabled: true, // Connecteurs naturels de base
- casualConnectors: true, // Connecteurs trĂšs casual (genre, enfin, bref)
- hesitationMarkers: true, // Marqueurs d'hésitation (..., euh)
- colloquialTransitions: true, // Transitions colloquiales
-
- // ========================================
- // FEATURES IMPERFECTIONS HUMAINES
- // ========================================
- humanImperfections: true, // SystĂšme d'imperfections humaines
- vocabularyRepetitions: true, // Répétitions vocabulaire naturelles
- casualizationIntensive: true, // Casualisation intensive
- naturalHesitations: true, // Hésitations naturelles en fin de phrase
- informalExpressions: true, // Expressions informelles ("pas mal", "sympa")
-
- // ========================================
- // FEATURES RESTRUCTURATION
- // ========================================
- intelligentRestructuring: true, // Restructuration intelligente
- paragraphBreaking: true, // Cassage paragraphes longs
- listToTextConversion: true, // Listes â texte naturel
- redundancyInjection: true, // Injection redondances naturelles
-
- // ========================================
- // FEATURES SPĂCIALISĂES
- // ========================================
- personalityAdaptation: true, // Adaptation selon personnalité
- temporalConsistency: true, // Cohérence temporelle (maintenant/aujourd'hui)
- contextualVocabulary: true, // Vocabulaire contextuel
- registerVariation: true // Variation registre langue
-};
-
-/**
- * ORCHESTRATEUR PRINCIPAL - Pattern Breaking Layer
- * @param {object} content - Contenu généré à traiter
- * @param {object} options - Options de pattern breaking
- * @returns {object} - { content, stats, fallback }
- */
-async function applyPatternBreakingLayer(content, options = {}) {
- return await tracer.run('PatternBreakingCore.applyPatternBreakingLayer()', async () => {
- const startTime = Date.now();
-
- await tracer.annotate({
- contentKeys: Object.keys(content).length,
- intensityLevel: options.intensityLevel,
- personality: options.csvData?.personality?.nom
- });
-
- logSh(`đ§ PATTERN BREAKING - DĂ©but traitement`, 'INFO');
- logSh(` đ ${Object.keys(content).length} Ă©lĂ©ments | IntensitĂ©: ${options.intensityLevel || DEFAULT_CONFIG.intensityLevel}`, 'DEBUG');
-
- try {
- // Configuration fusionnée
- const config = { ...DEFAULT_CONFIG, ...options };
-
- // Stats de pattern breaking
- const patternStats = {
- elementsProcessed: 0,
- syntaxModifications: 0,
- llmFingerprintReplacements: 0,
- connectorReplacements: 0,
- totalModifications: 0,
- fallbackUsed: false,
- patternsDetected: 0
- };
-
- // Contenu traité
- let processedContent = { ...content };
-
- // ========================================
- // TRAITEMENT PAR ĂLĂMENT
- // ========================================
- for (const [elementKey, elementContent] of Object.entries(content)) {
- await tracer.run(`PatternBreaking.processElement(${elementKey})`, async () => {
-
- logSh(` đŻ Traitement Ă©lĂ©ment: ${elementKey}`, 'DEBUG');
-
- let currentContent = elementContent;
- let elementModifications = 0;
-
- try {
- // 1. Détection patterns LLM
- const detectedPatterns = detectLLMPatterns(currentContent);
- patternStats.patternsDetected += detectedPatterns.count;
-
- if (detectedPatterns.count > 0) {
- logSh(` đ ${detectedPatterns.count} patterns LLM dĂ©tectĂ©s: ${detectedPatterns.patterns.slice(0, 3).join(', ')}`, 'DEBUG');
- }
-
- // 2. SYNTAXE & STRUCTURE - Couche de base
- if (config.syntaxVariationEnabled) {
- const syntaxResult = await applySyntaxVariation(currentContent, config);
- currentContent = syntaxResult.content;
- elementModifications += syntaxResult.modifications;
- patternStats.syntaxModifications += syntaxResult.modifications;
- logSh(` đ Syntaxe: ${syntaxResult.modifications} variations appliquĂ©es`, 'DEBUG');
- }
-
- // 3. SYNTAXE AGRESSIVE - Couche intensive
- if (config.aggressiveSentenceSplitting || config.aggressiveSentenceMerging) {
- const aggressiveResult = await applyAggressiveSyntax(currentContent, config);
- currentContent = aggressiveResult.content;
- elementModifications += aggressiveResult.modifications;
- patternStats.syntaxModifications += aggressiveResult.modifications;
- logSh(` âïž Syntaxe agressive: ${aggressiveResult.modifications} modifications`, 'DEBUG');
- }
-
- // 4. MICRO-VARIATIONS - Subtiles mais importantes
- if (config.microSyntaxVariations) {
- const microResult = await applyMicroVariations(currentContent, config);
- currentContent = microResult.content;
- elementModifications += microResult.modifications;
- patternStats.syntaxModifications += microResult.modifications;
- logSh(` đ§ Micro-variations: ${microResult.modifications} ajustements`, 'DEBUG');
- }
-
- // 5. LLM FINGERPRINTS - Détection de base
- if (config.llmFingerprintReplacement && detectedPatterns.count > 0) {
- const fingerprintResult = await applyLLMFingerprints(currentContent, config);
- currentContent = fingerprintResult.content;
- elementModifications += fingerprintResult.modifications;
- patternStats.llmFingerprintReplacements += fingerprintResult.modifications;
- logSh(` đ€ LLM Fingerprints: ${fingerprintResult.modifications} remplacements`, 'DEBUG');
- }
-
- // 6. PATTERNS FRANĂAIS - SpĂ©cifique langue française
- if (config.frenchLLMPatterns) {
- const frenchResult = await applyFrenchPatterns(currentContent, config);
- currentContent = frenchResult.content;
- elementModifications += frenchResult.modifications;
- patternStats.llmFingerprintReplacements += frenchResult.modifications;
- logSh(` đ«đ· Patterns français: ${frenchResult.modifications} corrections`, 'DEBUG');
- }
-
- // 7. VOCABULAIRE FORMEL - Casualisation
- if (config.overlyFormalVocabulary) {
- const casualResult = await applyCasualization(currentContent, config);
- currentContent = casualResult.content;
- elementModifications += casualResult.modifications;
- patternStats.llmFingerprintReplacements += casualResult.modifications;
- logSh(` đ Casualisation: ${casualResult.modifications} simplifications`, 'DEBUG');
- }
-
- // 8. CONNECTEURS NATURELS - Base
- if (config.naturalConnectorsEnabled) {
- const connectorResult = await applyNaturalConnectors(currentContent, config);
- currentContent = connectorResult.content;
- elementModifications += connectorResult.modifications;
- patternStats.connectorReplacements += connectorResult.modifications;
- logSh(` đ Connecteurs naturels: ${connectorResult.modifications} humanisĂ©s`, 'DEBUG');
- }
-
- // 9. CONNECTEURS CASUAL - TrĂšs familier
- if (config.casualConnectors) {
- const casualConnResult = await applyCasualConnectors(currentContent, config);
- currentContent = casualConnResult.content;
- elementModifications += casualConnResult.modifications;
- patternStats.connectorReplacements += casualConnResult.modifications;
- logSh(` đŁïž Connecteurs casual: ${casualConnResult.modifications} familiarisĂ©s`, 'DEBUG');
- }
-
- // 10. IMPERFECTIONS HUMAINES - SystĂšme principal
- if (config.humanImperfections) {
- const imperfResult = await applyHumanImperfections(currentContent, config);
- currentContent = imperfResult.content;
- elementModifications += imperfResult.modifications;
- patternStats.totalModifications += imperfResult.modifications;
- logSh(` đ€ Imperfections: ${imperfResult.modifications} humanisations`, 'DEBUG');
- }
-
- // 11. QUESTIONS RHĂTORIQUES - Engagement
- if (config.questionInjection) {
- const questionResult = await applyQuestionInjection(currentContent, config);
- currentContent = questionResult.content;
- elementModifications += questionResult.modifications;
- patternStats.totalModifications += questionResult.modifications;
- logSh(` â Questions: ${questionResult.modifications} injections`, 'DEBUG');
- }
-
- // 12. RESTRUCTURATION INTELLIGENTE - DerniĂšre couche
- if (config.intelligentRestructuring) {
- const restructResult = await applyIntelligentRestructuring(currentContent, config);
- currentContent = restructResult.content;
- elementModifications += restructResult.modifications;
- patternStats.totalModifications += restructResult.modifications;
- logSh(` đ§ Restructuration: ${restructResult.modifications} rĂ©organisations`, 'DEBUG');
- }
-
- // 5. Validation qualité
- const qualityCheck = validatePatternBreakingQuality(elementContent, currentContent, config.qualityThreshold);
-
- if (qualityCheck.acceptable) {
- processedContent[elementKey] = currentContent;
- patternStats.elementsProcessed++;
- patternStats.totalModifications += elementModifications;
-
- logSh(` â
ĂlĂ©ment traitĂ©: ${elementModifications} modifications totales`, 'DEBUG');
- } else {
- // Fallback: garder contenu original
- processedContent[elementKey] = elementContent;
- patternStats.fallbackUsed = true;
-
- logSh(` â ïž QualitĂ© insuffisante, fallback vers contenu original`, 'WARNING');
- }
-
- } catch (elementError) {
- logSh(` â Erreur pattern breaking Ă©lĂ©ment ${elementKey}: ${elementError.message}`, 'WARNING');
- processedContent[elementKey] = elementContent; // Fallback
- patternStats.fallbackUsed = true;
- }
-
- }, { elementKey, originalLength: elementContent?.length });
- }
-
- // ========================================
- // RĂSULTATS FINAUX
- // ========================================
- const duration = Date.now() - startTime;
- const success = patternStats.elementsProcessed > 0 && !patternStats.fallbackUsed;
-
- logSh(`đ§ PATTERN BREAKING - TerminĂ© (${duration}ms)`, 'INFO');
- logSh(` â
${patternStats.elementsProcessed}/${Object.keys(content).length} éléments traités`, 'INFO');
- logSh(` đ ${patternStats.syntaxModifications} syntaxe | ${patternStats.llmFingerprintReplacements} fingerprints | ${patternStats.connectorReplacements} connecteurs`, 'INFO');
- logSh(` đŻ Patterns dĂ©tectĂ©s: ${patternStats.patternsDetected} | Fallback: ${patternStats.fallbackUsed ? 'OUI' : 'NON'}`, 'INFO');
-
- await tracer.event('Pattern Breaking terminé', {
- success,
- duration,
- stats: patternStats
- });
-
- return {
- content: processedContent,
- stats: patternStats,
- fallback: patternStats.fallbackUsed,
- duration
- };
-
- } catch (error) {
- const duration = Date.now() - startTime;
- logSh(`â PATTERN BREAKING ĂCHOUĂ (${duration}ms): ${error.message}`, 'ERROR');
-
- await tracer.event('Pattern Breaking échoué', {
- error: error.message,
- duration,
- contentKeys: Object.keys(content).length
- });
-
- // Fallback complet
- return {
- content,
- stats: { fallbackUsed: true, error: error.message },
- fallback: true,
- duration
- };
- }
-
- }, {
- contentElements: Object.keys(content).length,
- intensityLevel: options.intensityLevel
- });
-}
-
-/**
- * APPLICATION VARIATION SYNTAXIQUE
- */
-async function applySyntaxVariation(content, config) {
- const syntaxResult = varyStructures(content, config.intensityLevel, {
- preserveReadability: config.preserveReadability,
- maxModifications: Math.floor(config.maxModificationsPerElement / 2)
- });
-
- return {
- content: syntaxResult.content,
- modifications: syntaxResult.modifications || 0
- };
-}
-
-/**
- * APPLICATION REMPLACEMENT LLM FINGERPRINTS
- */
-async function applyLLMFingerprints(content, config) {
- const fingerprintResult = replaceLLMFingerprints(content, {
- intensity: config.intensityLevel,
- preserveContext: true,
- maxReplacements: Math.floor(config.maxModificationsPerElement / 2)
- });
-
- return {
- content: fingerprintResult.content,
- modifications: fingerprintResult.replacements || 0
- };
-}
-
-/**
- * APPLICATION CONNECTEURS NATURELS
- */
-async function applyNaturalConnectors(content, config) {
- const connectorResult = humanizeTransitions(content, {
- intensity: config.intensityLevel,
- preserveMeaning: true,
- maxReplacements: Math.floor(config.maxModificationsPerElement / 2)
- });
-
- return {
- content: connectorResult.content,
- modifications: connectorResult.replacements || 0
- };
-}
-
-/**
- * VALIDATION QUALITĂ PATTERN BREAKING
- */
-function validatePatternBreakingQuality(originalContent, processedContent, qualityThreshold) {
- if (!originalContent || !processedContent) {
- return { acceptable: false, reason: 'Contenu manquant' };
- }
-
- // Métriques de base
- const lengthDiff = Math.abs(processedContent.length - originalContent.length) / originalContent.length;
- const wordCountOriginal = originalContent.split(/\s+/).length;
- const wordCountProcessed = processedContent.split(/\s+/).length;
- const wordCountDiff = Math.abs(wordCountProcessed - wordCountOriginal) / wordCountOriginal;
-
- // Vérifications qualité
- const checks = {
- lengthPreserved: lengthDiff < 0.3, // Pas plus de 30% de différence
- wordCountPreserved: wordCountDiff < 0.2, // Pas plus de 20% de différence
- noEmpty: processedContent.trim().length > 0, // Pas de contenu vide
- readableStructure: processedContent.includes('.') // Structure lisible
- };
-
- const passedChecks = Object.values(checks).filter(Boolean).length;
- const score = passedChecks / Object.keys(checks).length;
-
- const acceptable = score >= qualityThreshold;
-
- logSh(` đŻ Validation Pattern Breaking: ${acceptable ? 'ACCEPTĂ' : 'REJETĂ'} (score: ${score.toFixed(2)})`, acceptable ? 'DEBUG' : 'WARNING');
-
- return {
- acceptable,
- score,
- checks,
- reason: acceptable ? 'Qualité acceptable' : 'Score qualité insuffisant'
- };
-}
-
-/**
- * APPLICATION SYNTAXE AGRESSIVE
- * Seuils plus bas pour plus de transformations
- */
-async function applyAggressiveSyntax(content, config) {
- let modified = content;
- let modifications = 0;
-
- // Découpage agressif phrases longues (>80 chars au lieu de >120)
- if (config.aggressiveSentenceSplitting) {
- const sentences = modified.split('. ');
- const processedSentences = sentences.map(sentence => {
- if (sentence.length > 80 && Math.random() < (config.intensityLevel * 0.7)) {
- const cutPoints = [
- { pattern: /, qui (.+)/, replacement: '. Celui-ci $1' },
- { pattern: /, que (.+)/, replacement: '. Cette solution $1' },
- { pattern: /, car (.+)/, replacement: '. En fait, $1' },
- { pattern: /, donc (.+)/, replacement: '. Du coup, $1' },
- { pattern: / et (.{20,})/, replacement: '. Aussi, $1' },
- { pattern: /, mais (.+)/, replacement: '. Par contre, $1' }
- ];
-
- for (const cutPoint of cutPoints) {
- if (sentence.match(cutPoint.pattern)) {
- modifications++;
- return sentence.replace(cutPoint.pattern, cutPoint.replacement);
- }
- }
- }
- return sentence;
- });
- modified = processedSentences.join('. ');
- }
-
- // Fusion agressive phrases courtes (<60 chars au lieu de <40)
- if (config.aggressiveSentenceMerging) {
- const sentences = modified.split('. ');
- const processedSentences = [];
-
- for (let i = 0; i < sentences.length; i++) {
- const current = sentences[i];
- const next = sentences[i + 1];
-
- if (current && current.length < 60 && next && next.length < 80 && Math.random() < (config.intensityLevel * 0.5)) {
- const connectors = [', du coup,', ', genre,', ', enfin,', ' et puis'];
- const connector = connectors[Math.floor(Math.random() * connectors.length)];
- processedSentences.push(current + connector + ' ' + next.toLowerCase());
- modifications++;
- i++; // Skip next sentence
- } else {
- processedSentences.push(current);
- }
- }
- modified = processedSentences.join('. ');
- }
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION MICRO-VARIATIONS
- * Changements subtiles mais nombreux
- */
-async function applyMicroVariations(content, config) {
- let modified = content;
- let modifications = 0;
-
- const microPatterns = [
- // Intensificateurs
- { from: /\btrĂšs (.+?)\b/g, to: 'super $1', probability: 0.4 },
- { from: /\bassez (.+?)\b/g, to: 'plutĂŽt $1', probability: 0.5 },
- { from: /\bextrĂȘmement\b/g, to: 'vraiment', probability: 0.6 },
-
- // Connecteurs basiques
- { from: /\bainsi\b/g, to: 'du coup', probability: 0.4 },
- { from: /\bpar conséquent\b/g, to: 'donc', probability: 0.7 },
- { from: /\bcependant\b/g, to: 'mais', probability: 0.3 },
-
- // Formulations casual
- { from: /\bde cette maniÚre\b/g, to: 'comme ça', probability: 0.5 },
- { from: /\bafin de\b/g, to: 'pour', probability: 0.4 },
- { from: /\ben vue de\b/g, to: 'pour', probability: 0.6 }
- ];
-
- microPatterns.forEach(pattern => {
- if (Math.random() < (config.intensityLevel * pattern.probability)) {
- const before = modified;
- modified = modified.replace(pattern.from, pattern.to);
- if (modified !== before) modifications++;
- }
- });
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION PATTERNS FRANĂAIS SPĂCIFIQUES
- * Détection patterns français typiques LLM
- */
-async function applyFrenchPatterns(content, config) {
- let modified = content;
- let modifications = 0;
-
- // Patterns français typiques LLM
- const frenchPatterns = [
- // Expressions trop soutenues
- { from: /\bil convient de noter que\b/gi, to: 'on peut dire que', probability: 0.8 },
- { from: /\bil est important de souligner que\b/gi, to: 'c\'est important de voir que', probability: 0.8 },
- { from: /\bdans ce contexte\b/gi, to: 'lĂ -dessus', probability: 0.6 },
- { from: /\bpar ailleurs\b/gi, to: 'sinon', probability: 0.5 },
- { from: /\ben outre\b/gi, to: 'aussi', probability: 0.7 },
-
- // Formulations administratives
- { from: /\bil s'avĂšre que\b/gi, to: 'en fait', probability: 0.6 },
- { from: /\btoutefois\b/gi, to: 'par contre', probability: 0.5 },
- { from: /\bnĂ©anmoins\b/gi, to: 'quand mĂȘme', probability: 0.7 }
- ];
-
- frenchPatterns.forEach(pattern => {
- if (Math.random() < (config.intensityLevel * pattern.probability)) {
- const before = modified;
- modified = modified.replace(pattern.from, pattern.to);
- if (modified !== before) modifications++;
- }
- });
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION CASUALISATION INTENSIVE
- * Rendre le vocabulaire plus décontracté
- */
-async function applyCasualization(content, config) {
- let modified = content;
- let modifications = 0;
-
- const casualizations = [
- // Verbes formels â casual
- { from: /\boptimiser\b/gi, to: 'améliorer', probability: 0.7 },
- { from: /\beffectuer\b/gi, to: 'faire', probability: 0.8 },
- { from: /\bréaliser\b/gi, to: 'faire', probability: 0.6 },
- { from: /\bmettre en Ćuvre\b/gi, to: 'faire', probability: 0.7 },
-
- // Adjectifs formels â casual
- { from: /\bexceptionnel\b/gi, to: 'super', probability: 0.4 },
- { from: /\bremarquable\b/gi, to: 'pas mal', probability: 0.5 },
- { from: /\bconsidérable\b/gi, to: 'important', probability: 0.6 },
- { from: /\bsubstantiel\b/gi, to: 'important', probability: 0.8 },
-
- // Expressions formelles â casual
- { from: /\bde maniĂšre significative\b/gi, to: 'pas mal', probability: 0.6 },
- { from: /\ben définitive\b/gi, to: 'au final', probability: 0.7 },
- { from: /\bdans l'ensemble\b/gi, to: 'globalement', probability: 0.5 }
- ];
-
- casualizations.forEach(casual => {
- if (Math.random() < (config.intensityLevel * casual.probability)) {
- const before = modified;
- modified = modified.replace(casual.from, casual.to);
- if (modified !== before) modifications++;
- }
- });
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION CONNECTEURS CASUAL
- * Connecteurs trĂšs familiers
- */
-async function applyCasualConnectors(content, config) {
- let modified = content;
- let modifications = 0;
-
- const casualConnectors = [
- { from: /\. De plus,/g, to: '. Genre,', probability: 0.3 },
- { from: /\. En outre,/g, to: '. Puis,', probability: 0.4 },
- { from: /\. Par ailleurs,/g, to: '. Sinon,', probability: 0.3 },
- { from: /\. Cependant,/g, to: '. Mais bon,', probability: 0.4 },
- { from: /\. Néanmoins,/g, to: '. Ceci dit,', probability: 0.5 },
- { from: /\. Ainsi,/g, to: '. Du coup,', probability: 0.6 }
- ];
-
- casualConnectors.forEach(connector => {
- if (Math.random() < (config.intensityLevel * connector.probability)) {
- const before = modified;
- modified = modified.replace(connector.from, connector.to);
- if (modified !== before) modifications++;
- }
- });
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION IMPERFECTIONS HUMAINES
- * Injection d'imperfections réalistes
- */
-async function applyHumanImperfections(content, config) {
- let modified = content;
- let modifications = 0;
-
- // Répétitions vocabulaire
- if (config.vocabularyRepetitions && Math.random() < (config.intensityLevel * 0.4)) {
- const repetitionWords = ['vraiment', 'bien', 'assez', 'plutĂŽt', 'super'];
- const word = repetitionWords[Math.floor(Math.random() * repetitionWords.length)];
- const sentences = modified.split('. ');
- if (sentences.length > 2) {
- sentences[1] = word + ' ' + sentences[1];
- modified = sentences.join('. ');
- modifications++;
- }
- }
-
- // Hésitations naturelles
- if (config.naturalHesitations && Math.random() < (config.intensityLevel * 0.2)) {
- const hesitations = ['... enfin', '... disons', '... bon'];
- const hesitation = hesitations[Math.floor(Math.random() * hesitations.length)];
- const words = modified.split(' ');
- const insertPos = Math.floor(words.length * 0.6);
- words.splice(insertPos, 0, hesitation);
- modified = words.join(' ');
- modifications++;
- }
-
- // Expressions informelles
- if (config.informalExpressions && Math.random() < (config.intensityLevel * 0.3)) {
- const informalReplacements = [
- { from: /\bc'est bien\b/gi, to: 'c\'est sympa', probability: 0.4 },
- { from: /\bc'est intéressant\b/gi, to: 'c\'est pas mal', probability: 0.5 },
- { from: /\bc'est efficace\b/gi, to: 'ça marche bien', probability: 0.4 }
- ];
-
- informalReplacements.forEach(replacement => {
- if (Math.random() < replacement.probability) {
- const before = modified;
- modified = modified.replace(replacement.from, replacement.to);
- if (modified !== before) modifications++;
- }
- });
- }
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION QUESTIONS RHĂTORIQUES
- * Injection questions pour engagement
- */
-async function applyQuestionInjection(content, config) {
- let modified = content;
- let modifications = 0;
-
- if (Math.random() < (config.intensityLevel * 0.3)) {
- const sentences = modified.split('. ');
- if (sentences.length > 3) {
- const questionTemplates = [
- 'Mais pourquoi est-ce important ?',
- 'Comment faire alors ?',
- 'Que faut-il retenir ?',
- 'Est-ce vraiment efficace ?'
- ];
-
- const question = questionTemplates[Math.floor(Math.random() * questionTemplates.length)];
- const insertPos = Math.floor(sentences.length / 2);
- sentences.splice(insertPos, 0, question);
- modified = sentences.join('. ');
- modifications++;
- }
- }
-
- return { content: modified, modifications };
-}
-
-/**
- * APPLICATION RESTRUCTURATION INTELLIGENTE
- * Réorganisation structure générale
- */
-async function applyIntelligentRestructuring(content, config) {
- let modified = content;
- let modifications = 0;
-
- // Cassage paragraphes longs
- if (config.paragraphBreaking && modified.length > 400) {
- const sentences = modified.split('. ');
- if (sentences.length > 6) {
- const breakPoint = Math.floor(sentences.length / 2);
- sentences[breakPoint] = sentences[breakPoint] + '\n\n';
- modified = sentences.join('. ');
- modifications++;
- }
- }
-
- // Injection redondances naturelles
- if (config.redundancyInjection && Math.random() < (config.intensityLevel * 0.2)) {
- const redundancies = ['comme je le disais', 'encore une fois', 'je le répÚte'];
- const redundancy = redundancies[Math.floor(Math.random() * redundancies.length)];
- const sentences = modified.split('. ');
- if (sentences.length > 2) {
- sentences[sentences.length - 2] = redundancy + ', ' + sentences[sentences.length - 2];
- modified = sentences.join('. ');
- modifications++;
- }
- }
-
- return { content: modified, modifications };
-}
-
-// ============= EXPORTS =============
-module.exports = {
- applyPatternBreakingLayer,
- applySyntaxVariation,
- applyLLMFingerprints,
- applyNaturalConnectors,
- validatePatternBreakingQuality,
- applyAggressiveSyntax,
- applyMicroVariations,
- applyFrenchPatterns,
- applyCasualization,
- applyCasualConnectors,
- applyHumanImperfections,
- applyQuestionInjection,
- applyIntelligentRestructuring,
- DEFAULT_CONFIG
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/pattern-breaking/PatternBreakingLayers.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: PatternBreakingLayers.js
-// RESPONSABILITĂ: Stacks prĂ©dĂ©finis pour Pattern Breaking
-// Configurations optimisées par cas d'usage
-// ========================================
-
-const { logSh } = require('../ErrorReporting');
-
-/**
- * CONFIGURATIONS PRĂDĂFINIES PATTERN BREAKING
- * Optimisées pour différents niveaux et cas d'usage
- */
-const PATTERN_BREAKING_STACKS = {
-
- // ========================================
- // STACK LĂGER - Usage quotidien
- // ========================================
- lightPatternBreaking: {
- name: 'Light Pattern Breaking',
- description: 'Anti-détection subtile pour usage quotidien',
- intensity: 0.3,
- config: {
- syntaxVariationEnabled: true,
- llmFingerprintReplacement: false, // Pas de remplacement mots
- naturalConnectorsEnabled: true,
- preserveReadability: true,
- maxModificationsPerElement: 2,
- qualityThreshold: 0.7
- },
- expectedReduction: '10-15%',
- useCase: 'Articles standard, faible risque détection'
- },
-
- // ========================================
- // STACK STANDARD - Ăquilibre optimal
- // ========================================
- standardPatternBreaking: {
- name: 'Standard Pattern Breaking',
- description: 'Ăquilibre optimal efficacitĂ©/naturalitĂ©',
- intensity: 0.5,
- config: {
- syntaxVariationEnabled: true,
- llmFingerprintReplacement: true,
- naturalConnectorsEnabled: true,
- preserveReadability: true,
- maxModificationsPerElement: 4,
- qualityThreshold: 0.6
- },
- expectedReduction: '20-25%',
- useCase: 'Usage général recommandé'
- },
-
- // ========================================
- // STACK INTENSIF - Anti-détection poussée
- // ========================================
- heavyPatternBreaking: {
- name: 'Heavy Pattern Breaking',
- description: 'Anti-détection intensive pour cas critiques',
- intensity: 0.8,
- config: {
- syntaxVariationEnabled: true,
- llmFingerprintReplacement: true,
- naturalConnectorsEnabled: true,
- preserveReadability: true,
- maxModificationsPerElement: 6,
- qualityThreshold: 0.5
- },
- expectedReduction: '30-35%',
- useCase: 'Détection élevée, contenu critique'
- },
-
- // ========================================
- // STACK ADAPTATIF - Selon contenu
- // ========================================
- adaptivePatternBreaking: {
- name: 'Adaptive Pattern Breaking',
- description: 'Adaptation intelligente selon détection patterns',
- intensity: 0.6,
- config: {
- syntaxVariationEnabled: true,
- llmFingerprintReplacement: true,
- naturalConnectorsEnabled: true,
- preserveReadability: true,
- maxModificationsPerElement: 5,
- qualityThreshold: 0.6,
- adaptiveMode: true // Ajuste selon détection patterns
- },
- expectedReduction: '25-30%',
- useCase: 'Adaptation automatique par contenu'
- },
-
- // ========================================
- // STACK SYNTAXE FOCUS - Syntaxe uniquement
- // ========================================
- syntaxFocus: {
- name: 'Syntax Focus',
- description: 'Focus sur variations syntaxiques uniquement',
- intensity: 0.7,
- config: {
- syntaxVariationEnabled: true,
- llmFingerprintReplacement: false,
- naturalConnectorsEnabled: false,
- preserveReadability: true,
- maxModificationsPerElement: 6,
- qualityThreshold: 0.7
- },
- expectedReduction: '15-20%',
- useCase: 'Préservation vocabulaire, syntaxe variable'
- },
-
- // ========================================
- // STACK CONNECTEURS FOCUS - Connecteurs uniquement
- // ========================================
- connectorsFocus: {
- name: 'Connectors Focus',
- description: 'Humanisation connecteurs et transitions',
- intensity: 0.8,
- config: {
- syntaxVariationEnabled: false,
- llmFingerprintReplacement: false,
- naturalConnectorsEnabled: true,
- preserveReadability: true,
- maxModificationsPerElement: 4,
- qualityThreshold: 0.8,
- connectorTone: 'casual' // casual, conversational, technical, commercial
- },
- expectedReduction: '12-18%',
- useCase: 'Textes formels Ă humaniser'
- }
-};
-
-/**
- * APPLICATION STACK PATTERN BREAKING
- * @param {string} stackName - Nom du stack Ă appliquer
- * @param {object} content - Contenu Ă traiter
- * @param {object} overrides - Options pour surcharger le stack
- * @returns {object} - { content, stats, stackUsed }
- */
-async function applyPatternBreakingStack(stackName, content, overrides = {}) {
- const { applyPatternBreakingLayer } = require('./PatternBreakingCore');
-
- logSh(`đŠ Application Stack Pattern Breaking: ${stackName}`, 'INFO');
-
- const stack = PATTERN_BREAKING_STACKS[stackName];
- if (!stack) {
- logSh(`â Stack Pattern Breaking inconnu: ${stackName}`, 'WARNING');
- throw new Error(`Stack Pattern Breaking inconnu: ${stackName}`);
- }
-
- try {
- // Configuration fusionnée (stack + overrides)
- const finalConfig = {
- ...stack.config,
- intensityLevel: stack.intensity,
- ...overrides
- };
-
- logSh(` đŻ Configuration: ${stack.description}`, 'DEBUG');
- logSh(` ⥠Intensité: ${finalConfig.intensityLevel} | Réduction attendue: ${stack.expectedReduction}`, 'DEBUG');
-
- // Mode adaptatif si activé
- if (finalConfig.adaptiveMode) {
- const adaptedConfig = await adaptConfigurationToContent(content, finalConfig);
- Object.assign(finalConfig, adaptedConfig);
- logSh(` đ§ Mode adaptatif appliquĂ©`, 'DEBUG');
- }
-
- // Application Pattern Breaking
- const result = await applyPatternBreakingLayer(content, finalConfig);
-
- logSh(`đŠ Stack Pattern Breaking terminĂ©: ${result.stats?.totalModifications || 0} modifications`, 'INFO');
-
- return {
- content: result.content,
- stats: {
- ...result.stats,
- stackUsed: stackName,
- stackDescription: stack.description,
- expectedReduction: stack.expectedReduction
- },
- fallback: result.fallback,
- stackUsed: stackName
- };
-
- } catch (error) {
- logSh(`â Erreur application Stack Pattern Breaking ${stackName}: ${error.message}`, 'ERROR');
- throw error;
- }
-}
-
-/**
- * ADAPTATION CONFIGURATION SELON CONTENU
- */
-async function adaptConfigurationToContent(content, baseConfig) {
- const { detectLLMPatterns } = require('./LLMFingerprints');
- const { detectFormalConnectors } = require('./NaturalConnectors');
-
- logSh(`đ§ Adaptation configuration selon contenu...`, 'DEBUG');
-
- const adaptations = { ...baseConfig };
-
- try {
- // Analyser patterns LLM
- const llmDetection = detectLLMPatterns(content);
- const formalDetection = detectFormalConnectors(content);
-
- logSh(` đ Patterns LLM: ${llmDetection.count} (score: ${llmDetection.suspicionScore.toFixed(3)})`, 'DEBUG');
- logSh(` đ Connecteurs formels: ${formalDetection.count} (score: ${formalDetection.suspicionScore.toFixed(3)})`, 'DEBUG');
-
- // Adapter selon détection patterns LLM
- if (llmDetection.suspicionScore > 0.06) {
- adaptations.llmFingerprintReplacement = true;
- adaptations.intensityLevel = Math.min(1.0, baseConfig.intensityLevel + 0.2);
- logSh(` đ§ IntensitĂ© augmentĂ©e pour patterns LLM Ă©levĂ©s: ${adaptations.intensityLevel}`, 'DEBUG');
- } else if (llmDetection.suspicionScore < 0.02) {
- adaptations.llmFingerprintReplacement = false;
- logSh(` đ§ Remplacement LLM dĂ©sactivĂ© (faible dĂ©tection)`, 'DEBUG');
- }
-
- // Adapter selon connecteurs formels
- if (formalDetection.suspicionScore > 0.04) {
- adaptations.naturalConnectorsEnabled = true;
- adaptations.maxModificationsPerElement = Math.min(8, baseConfig.maxModificationsPerElement + 2);
- logSh(` đ§ Focus connecteurs activĂ©: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG');
- }
-
- // Adapter selon longueur texte
- const wordCount = content.split(/\s+/).length;
- if (wordCount > 500) {
- adaptations.maxModificationsPerElement = Math.min(10, baseConfig.maxModificationsPerElement + 3);
- logSh(` đ§ Texte long dĂ©tectĂ©: max ${adaptations.maxModificationsPerElement} modifications`, 'DEBUG');
- }
-
- } catch (error) {
- logSh(`â ïž Erreur adaptation configuration: ${error.message}`, 'WARNING');
- }
-
- return adaptations;
-}
-
-/**
- * RECOMMANDATION STACK AUTOMATIQUE
- */
-function recommendPatternBreakingStack(content, context = {}) {
- const { detectLLMPatterns } = require('./LLMFingerprints');
- const { detectFormalConnectors } = require('./NaturalConnectors');
-
- try {
- const llmDetection = detectLLMPatterns(content);
- const formalDetection = detectFormalConnectors(content);
- const wordCount = content.split(/\s+/).length;
-
- logSh(`đ€ Recommandation Stack Pattern Breaking...`, 'DEBUG');
-
- // CritĂšres de recommandation
- const criteria = {
- llmPatternsHigh: llmDetection.suspicionScore > 0.05,
- formalConnectorsHigh: formalDetection.suspicionScore > 0.03,
- longContent: wordCount > 300,
- criticalContext: context.critical === true,
- preserveQuality: context.preserveQuality === true
- };
-
- // Logique de recommandation
- let recommendedStack = 'standardPatternBreaking';
- let reason = 'Configuration équilibrée par défaut';
-
- if (criteria.criticalContext) {
- recommendedStack = 'heavyPatternBreaking';
- reason = 'Contexte critique détecté';
- } else if (criteria.llmPatternsHigh && criteria.formalConnectorsHigh) {
- recommendedStack = 'heavyPatternBreaking';
- reason = 'Patterns LLM et connecteurs formels élevés';
- } else if (criteria.llmPatternsHigh) {
- recommendedStack = 'adaptivePatternBreaking';
- reason = 'Patterns LLM élevés détectés';
- } else if (criteria.formalConnectorsHigh) {
- recommendedStack = 'connectorsFocus';
- reason = 'Connecteurs formels prédominants';
- } else if (criteria.preserveQuality) {
- recommendedStack = 'lightPatternBreaking';
- reason = 'Préservation qualité prioritaire';
- } else if (!criteria.llmPatternsHigh && !criteria.formalConnectorsHigh) {
- recommendedStack = 'syntaxFocus';
- reason = 'Faible détection patterns, focus syntaxe';
- }
-
- logSh(`đŻ Stack recommandĂ©: ${recommendedStack} (${reason})`, 'DEBUG');
-
- return {
- recommendedStack,
- reason,
- criteria,
- confidence: calculateRecommendationConfidence(criteria)
- };
-
- } catch (error) {
- logSh(`â ïž Erreur recommandation Stack: ${error.message}`, 'WARNING');
- return {
- recommendedStack: 'standardPatternBreaking',
- reason: 'Fallback suite erreur analyse',
- criteria: {},
- confidence: 0.5
- };
- }
-}
-
-/**
- * CALCUL CONFIANCE RECOMMANDATION
- */
-function calculateRecommendationConfidence(criteria) {
- let confidence = 0.5; // Base
-
- // Augmenter confiance selon critÚres détectés
- if (criteria.llmPatternsHigh) confidence += 0.2;
- if (criteria.formalConnectorsHigh) confidence += 0.2;
- if (criteria.criticalContext) confidence += 0.3;
- if (criteria.longContent) confidence += 0.1;
-
- return Math.min(1.0, confidence);
-}
-
-/**
- * LISTE STACKS DISPONIBLES
- */
-function listAvailableStacks() {
- return Object.entries(PATTERN_BREAKING_STACKS).map(([key, stack]) => ({
- name: key,
- displayName: stack.name,
- description: stack.description,
- intensity: stack.intensity,
- expectedReduction: stack.expectedReduction,
- useCase: stack.useCase
- }));
-}
-
-/**
- * VALIDATION STACK
- */
-function validateStack(stackName) {
- const stack = PATTERN_BREAKING_STACKS[stackName];
- if (!stack) {
- return { valid: false, error: `Stack inconnu: ${stackName}` };
- }
-
- // Vérifications configuration
- const config = stack.config;
- const checks = {
- hasIntensity: typeof stack.intensity === 'number',
- hasConfig: typeof config === 'object',
- hasValidThreshold: config.qualityThreshold >= 0 && config.qualityThreshold <= 1,
- hasValidMaxMods: config.maxModificationsPerElement > 0
- };
-
- const valid = Object.values(checks).every(Boolean);
-
- return {
- valid,
- checks,
- error: valid ? null : 'Configuration stack invalide'
- };
-}
-
-// ============= EXPORTS =============
-module.exports = {
- applyPatternBreakingStack,
- recommendPatternBreakingStack,
- adaptConfigurationToContent,
- listAvailableStacks,
- validateStack,
- PATTERN_BREAKING_STACKS
-};
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/Main.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// MAIN MODULAIRE - PIPELINE ARCHITECTURALE MODERNE
-// Responsabilité: Orchestration workflow avec architecture modulaire complÚte
-// Usage: node main_modulaire.js [rowNumber] [stackType]
-// ========================================
-
-const { logSh } = require('./ErrorReporting');
-const { tracer } = require('./trace');
-
-// Imports pipeline de base
-const { readInstructionsData, selectPersonalityWithAI, getPersonalities } = require('./BrainConfig');
-const { extractElements, buildSmartHierarchy } = require('./ElementExtraction');
-const { generateMissingKeywords } = require('./MissingKeywords');
-// ContentGeneration.js supprimé - Utiliser generateSimple depuis selective-enhancement
-const { generateSimple } = require('./selective-enhancement/SelectiveUtils');
-const { injectGeneratedContent } = require('./ContentAssembly');
-const { saveGeneratedArticleOrganic } = require('./ArticleStorage');
-
-// Imports modules modulaires
-const { applySelectiveLayer } = require('./selective-enhancement/SelectiveCore');
-const {
- applyPredefinedStack,
- applyAdaptiveLayers,
- getAvailableStacks
-} = require('./selective-enhancement/SelectiveLayers');
-const {
- applyAdversarialLayer
-} = require('./adversarial-generation/AdversarialCore');
-const {
- applyPredefinedStack: applyAdversarialStack
-} = require('./adversarial-generation/AdversarialLayers');
-const {
- applyHumanSimulationLayer
-} = require('./human-simulation/HumanSimulationCore');
-const {
- applyPredefinedSimulation,
- getAvailableSimulationStacks,
- recommendSimulationStack
-} = require('./human-simulation/HumanSimulationLayers');
-const {
- applyPatternBreakingLayer
-} = require('./pattern-breaking/PatternBreakingCore');
-const {
- applyPatternBreakingStack,
- recommendPatternBreakingStack,
- listAvailableStacks: listPatternBreakingStacks
-} = require('./pattern-breaking/PatternBreakingLayers');
-
-/**
- * WORKFLOW MODULAIRE AVEC DONNĂES FOURNIES (COMPATIBILITĂ MAKE.COM/DIGITAL OCEAN)
- */
-async function handleModularWorkflowWithData(data, config = {}) {
- return await tracer.run('Main.handleModularWorkflowWithData()', async () => {
- const {
- selectiveStack = 'standardEnhancement',
- adversarialMode = 'light',
- humanSimulationMode = 'none',
- patternBreakingMode = 'none',
- saveIntermediateSteps = false,
- source = 'compatibility_mode'
- } = config;
-
- await tracer.annotate({
- modularWorkflow: true,
- compatibilityMode: true,
- selectiveStack,
- adversarialMode,
- humanSimulationMode,
- patternBreakingMode,
- source
- });
-
- const startTime = Date.now();
- logSh(`đ WORKFLOW MODULAIRE COMPATIBILITĂ DĂMARRĂ`, 'INFO');
- logSh(` đ Source: ${source} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO');
-
- try {
- // Utiliser les données fournies directement (skippping phases 1-4)
- const csvData = data.csvData;
- const xmlTemplate = data.xmlTemplate;
-
- // Décoder XML si nécessaire
- let xmlString = xmlTemplate;
- if (xmlTemplate && !xmlTemplate.startsWith(' {
- const {
- rowNumber = 2,
- selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive
- adversarialMode = 'light', // none, light, standard, heavy, adaptive
- humanSimulationMode = 'none', // none, lightSimulation, standardSimulation, heavySimulation, adaptiveSimulation, personalityFocus, temporalFocus
- patternBreakingMode = 'none', // none, lightPatternBreaking, standardPatternBreaking, heavyPatternBreaking, adaptivePatternBreaking, syntaxFocus, connectorsFocus
- saveIntermediateSteps = true, // đ NOUVELLE OPTION: Sauvegarder chaque Ă©tape
- source = 'main_modulaire'
- } = config;
-
- await tracer.annotate({
- modularWorkflow: true,
- rowNumber,
- selectiveStack,
- adversarialMode,
- humanSimulationMode,
- patternBreakingMode,
- source
- });
-
- const startTime = Date.now();
- logSh(`đ WORKFLOW MODULAIRE DĂMARRĂ`, 'INFO');
- logSh(` đ Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode} | Human: ${humanSimulationMode} | Pattern: ${patternBreakingMode}`, '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 extractElements(csvData.xmlTemplate, csvData);
- logSh(` â
${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 finalElements = await generateMissingKeywords(elements, csvData);
- logSh(` â
Mots-clés complétés`, 'DEBUG');
-
- // ========================================
- // PHASE 4: CONSTRUCTION HIĂRARCHIE
- // ========================================
- logSh(`đïž PHASE 4: Construction hiĂ©rarchie`, 'INFO');
-
- const hierarchy = await buildSmartHierarchy(finalElements);
- logSh(` â
${Object.keys(hierarchy).length} sections hiérarchisées`, 'DEBUG');
-
- // ========================================
- // PHASE 5: GĂNĂRATION CONTENU DE BASE
- // ========================================
- logSh(`đ« PHASE 5: GĂ©nĂ©ration contenu de base`, 'INFO');
-
- const generatedContent = await generateSimple(hierarchy, csvData);
-
- logSh(` â
${Object.keys(generatedContent).length} éléments générés`, 'DEBUG');
-
- // đ SAUVEGARDE ĂTAPE 1: GĂ©nĂ©ration initiale
- let parentArticleId = null;
- let versionHistory = [];
-
- if (saveIntermediateSteps) {
- logSh(`đŸ SAUVEGARDE v1.0: GĂ©nĂ©ration initiale`, 'INFO');
-
- const xmlString = csvData.xmlTemplate.startsWith(' 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) => {
- const rTotal = r.selectiveEnhancements + r.adversarialModifications + (r.humanSimulationModifications || 0) + (r.patternBreakingModifications || 0);
- const bestTotal = best.selectiveEnhancements + best.adversarialModifications + (best.humanSimulationModifications || 0) + (best.patternBreakingModifications || 0);
- return rTotal > bestTotal ? r : best;
- });
-
- console.log(` ⥠Durée moyenne: ${avgDuration.toFixed(0)}ms`);
- console.log(` đ Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} + ${bestPerf.humanSimulation} + ${bestPerf.patternBreaking} (${bestPerf.duration}ms)`);
- console.log(` đ„ Plus d'amĂ©liorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} + ${mostEnhancements.humanSimulation} + ${mostEnhancements.patternBreaking} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications + (mostEnhancements.humanSimulationModifications || 0) + (mostEnhancements.patternBreakingModifications || 0)})`);
- }
-
- 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';
- const humanSimulationMode = args[4] || 'none';
- const patternBreakingMode = args[5] || 'none';
-
- console.log(`\nđ ExĂ©cution workflow modulaire:`);
- console.log(` đ Ligne: ${rowNumber}`);
- console.log(` đ§ Stack selective: ${selectiveStack}`);
- console.log(` đŻ Mode adversarial: ${adversarialMode}`);
- console.log(` đ§ Mode human simulation: ${humanSimulationMode}`);
- console.log(` đ§ Mode pattern breaking: ${patternBreakingMode}`);
-
- const result = await handleModularWorkflow({
- rowNumber,
- selectiveStack,
- adversarialMode,
- humanSimulationMode,
- patternBreakingMode,
- source: 'cli'
- });
-
- 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');
-
- console.log('\nđ§ MODES HUMAN SIMULATION DISPONIBLES:');
- const humanStacks = getAvailableSimulationStacks();
- humanStacks.forEach(stack => {
- console.log(`\n đ ${stack.name}:`);
- console.log(` đ ${stack.description}`);
- console.log(` đ ${stack.layersCount} couches`);
- console.log(` ⥠${stack.expectedImpact.modificationsPerElement} modifs | ${stack.expectedImpact.detectionReduction} anti-détection`);
- });
- break;
-
- case 'help':
- default:
- console.log('\nđ§ === MAIN MODULAIRE - USAGE ===');
- console.log('\nCommandes disponibles:');
- console.log(' workflow [ligne] [stack] [adversarial] [human] - 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 standardEnhancement light standardSimulation');
- console.log(' node main_modulaire.js workflow 3 adaptive standard heavySimulation');
- console.log(' node main_modulaire.js workflow 2 fullEnhancement none personalityFocus');
- 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 (compatibilité avec l'ancien Main.js)
-module.exports = {
- // âš NOUVEAU: Interface modulaire principale
- handleModularWorkflow,
- benchmarkStacks,
-
- // đ COMPATIBILITĂ: Alias pour l'ancien handleFullWorkflow
- handleFullWorkflow: (data) => {
- // Mapper l'ancien format vers le nouveau format modulaire
- const config = {
- rowNumber: data.rowNumber,
- source: data.source || 'compatibility_mode',
- selectiveStack: 'standardEnhancement', // Configuration par défaut
- adversarialMode: 'light',
- humanSimulationMode: 'none',
- patternBreakingMode: 'none',
- saveIntermediateSteps: false
- };
-
- // Si des données CSV sont fournies directement (Make.com style)
- if (data.csvData && data.xmlTemplate) {
- return handleModularWorkflowWithData(data, config);
- }
-
- // Sinon utiliser le workflow normal
- return handleModularWorkflow(config);
- },
-
- // đ COMPATIBILITĂ: Autres exports utilisĂ©s par l'ancien systĂšme
- testMainWorkflow: () => {
- return handleModularWorkflow({
- rowNumber: 2,
- selectiveStack: 'standardEnhancement',
- source: 'test_main_nodejs'
- });
- },
-
- launchLogViewer: () => {
- // La fonction launchLogViewer est maintenant intégrée dans handleModularWorkflow
- console.log('â
Log viewer sera lancé automatiquement avec le workflow');
- }
-};
-
-// Exécution CLI si appelé directement
-if (require.main === module) {
- main().catch(error => {
- console.error('â ERREUR FATALE:', error.message);
- process.exit(1);
- });
-}
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/test-manual.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: test-manual.js - ENTRY POINT MANUEL
-// Description: Test workflow ligne 2 Google Sheets
-// Usage: node test-manual.js
-// ========================================
-
-require('./polyfills/fetch.cjs');
-require('dotenv').config();
-
-const { handleFullWorkflow } = require('./Main');
-const { logSh } = require('./ErrorReporting');
-
-/**
- * TEST MANUEL LIGNE 2
- */
-async function testWorkflowLigne2() {
- logSh('đ === DĂMARRAGE TEST MANUEL LIGNE 2 ===', 'INFO'); // Using logSh instead of console.log
-
- const startTime = Date.now();
-
- try {
- // DONNĂES DE TEST POUR LIGNE 2
- const testData = {
- rowNumber: 2, // Ligne 2 Google Sheets
- source: 'test_manual_nodejs'
- };
-
- logSh('đ Configuration test:', 'INFO'); // Using logSh instead of console.log
- logSh(` âą Ligne: ${testData.rowNumber}`, 'INFO'); // Using logSh instead of console.log
- logSh(` âą Source: ${testData.source}`, 'INFO'); // Using logSh instead of console.log
- logSh(` âą Timestamp: ${new Date().toISOString()}`, 'INFO'); // Using logSh instead of console.log
-
- // LANCER LE WORKFLOW
- logSh('\nđŻ Lancement workflow principal...', 'INFO'); // Using logSh instead of console.log
- const result = await handleFullWorkflow(testData);
-
- // AFFICHER RĂSULTATS
- const duration = Date.now() - startTime;
- logSh('\nđ === WORKFLOW TERMINĂ AVEC SUCCĂS ===', 'INFO'); // Using logSh instead of console.log
- logSh(`â±ïž DurĂ©e: ${Math.round(duration/1000)}s`, 'INFO'); // Using logSh instead of console.log
- logSh(`đ Status: ${result.success ? 'â
SUCCESS' : 'â ERROR'}`, 'INFO'); // Using logSh instead of console.log
-
- if (result.success) {
- logSh(`đ ĂlĂ©ments gĂ©nĂ©rĂ©s: ${result.elementsGenerated}`, 'INFO'); // Using logSh instead of console.log
- logSh(`đ€ PersonnalitĂ©: ${result.personality}`, 'INFO'); // Using logSh instead of console.log
- logSh(`đŻ MC0: ${result.csvData?.mc0 || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
- logSh(`đ XML length: ${result.stats?.xmlLength || 'N/A'} chars`, 'INFO'); // Using logSh instead of console.log
- logSh(`đ€ Mots total: ${result.stats?.wordCount || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
- logSh(`đ§ LLMs utilisĂ©s: ${result.llmsUsed?.join(', ') || 'N/A'}`, 'INFO'); // Using logSh instead of console.log
-
- if (result.articleStorage) {
- logSh(`đŸ Article sauvĂ©: ID ${result.articleStorage.articleId}`, 'INFO'); // Using logSh instead of console.log
- }
- }
-
- logSh('\nđ RĂ©sultat complet:', 'DEBUG'); // Using logSh instead of console.log
- logSh(JSON.stringify(result, null, 2), 'DEBUG'); // Using logSh instead of console.log
-
- return result;
-
- } catch (error) {
- const duration = Date.now() - startTime;
- logSh('\nâ === ERREUR WORKFLOW ===', 'ERROR'); // Using logSh instead of console.error
- logSh(`â Message: ${error.message}`, 'ERROR'); // Using logSh instead of console.error
- logSh(`â DurĂ©e avant Ă©chec: ${Math.round(duration/1000)}s`, 'ERROR'); // Using logSh instead of console.error
-
- if (process.env.NODE_ENV === 'development') {
- logSh(`â Stack: ${error.stack}`, 'ERROR'); // Using logSh instead of console.error
- }
-
- // Afficher conseils de debug
- logSh('\nđ§ CONSEILS DE DEBUG:', 'INFO'); // Using logSh instead of console.log
- logSh('1. Vérifiez vos variables d\'environnement (.env)', 'INFO'); // Using logSh instead of console.log
- logSh('2. Vérifiez la connexion Google Sheets', 'INFO'); // Using logSh instead of console.log
- logSh('3. Vérifiez les API keys LLM', 'INFO'); // Using logSh instead of console.log
- logSh('4. Regardez les logs détaillés dans ./logs/', 'INFO'); // Using logSh instead of console.log
-
- process.exit(1);
- }
-}
-
-/**
- * VĂRIFICATIONS PRĂALABLES
- */
-function checkEnvironment() {
- logSh('đ VĂ©rification environnement...', 'INFO'); // Using logSh instead of console.log
-
- const required = [
- 'GOOGLE_SHEETS_ID',
- 'OPENAI_API_KEY'
- ];
-
- const missing = required.filter(key => !process.env[key]);
-
- if (missing.length > 0) {
- logSh('â Variables d\'environnement manquantes:', 'ERROR'); // Using logSh instead of console.error
- missing.forEach(key => logSh(` âą ${key}`, 'ERROR')); // Using logSh instead of console.error
- logSh('\nđĄ CrĂ©ez un fichier .env avec ces variables', 'ERROR'); // Using logSh instead of console.error
- process.exit(1);
- }
-
- logSh('â
Variables d\'environnement OK', 'INFO'); // Using logSh instead of console.log
-
- // Info sur les variables configurées
- logSh('đ Configuration dĂ©tectĂ©e:', 'INFO'); // Using logSh instead of console.log
- logSh(` âą Google Sheets ID: ${process.env.GOOGLE_SHEETS_ID}`, 'INFO'); // Using logSh instead of console.log
- logSh(` âą OpenAI: ${process.env.OPENAI_API_KEY ? 'â
ConfigurĂ©' : 'â Manquant'}`, 'INFO'); // Using logSh instead of console.log
- logSh(` âą Claude: ${process.env.CLAUDE_API_KEY ? 'â
ConfigurĂ©' : 'â ïž Optionnel'}`, 'INFO'); // Using logSh instead of console.log
- logSh(` âą Gemini: ${process.env.GEMINI_API_KEY ? 'â
ConfigurĂ©' : 'â ïž Optionnel'}`, 'INFO'); // Using logSh instead of console.log
-}
-
-/**
- * POINT D'ENTRĂE PRINCIPAL
- */
-async function main() {
- try {
- // Vérifications préalables
- checkEnvironment();
-
- // Test workflow
- await testWorkflowLigne2();
-
- logSh('\nđ Test manuel terminĂ© avec succĂšs !', 'INFO'); // Using logSh instead of console.log
- process.exit(0);
-
- } catch (error) {
- logSh('\nđ„ Erreur fatale: ' + error.message, 'ERROR'); // Using logSh instead of console.error
- process.exit(1);
- }
-}
-
-// Lancer si exécuté directement
-if (require.main === module) {
- main();
-}
-
-module.exports = { testWorkflowLigne2 };
-
-/*
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-â File: lib/DigitalOceanWorkflow.js â
-ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-*/
-
-// ========================================
-// FICHIER: DigitalOceanWorkflow.js - REFACTORISĂ POUR NODE.JS
-// RESPONSABILITĂ: Orchestration + Interface Digital Ocean UNIQUEMENT
-// ========================================
-
-const crypto = require('crypto');
-const axios = require('axios');
-const { GoogleSpreadsheet } = require('google-spreadsheet');
-const { JWT } = require('google-auth-library');
-
-// Import des autres modules du projet (Ă adapter selon votre structure)
-const { logSh } = require('./ErrorReporting');
-const { handleFullWorkflow } = require('./Main');
-const { getPersonalities, selectPersonalityWithAI } = require('./BrainConfig');
-
-// ============= CONFIGURATION DIGITAL OCEAN =============
-const DO_CONFIG = {
- endpoint: 'https://autocollant.fra1.digitaloceanspaces.com',
- bucketName: 'autocollant',
- accessKeyId: 'DO801XTYPE968NZGAQM3',
- secretAccessKey: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s',
- region: 'fra1'
-};
-
-// Configuration Google Sheets
-const SHEET_CONFIG = {
- sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c',
- serviceAccountEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
- privateKey: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
- // Alternative: utiliser fichier JSON directement
- keyFile: './seo-generator-470715-85d4a971c1af.json'
-};
-
-async function deployArticle({ path, html, dryRun = false, ...rest }) {
- if (!path || typeof html !== 'string') {
- const err = new Error('deployArticle: invalid payload (requires {path, html})');
- err.code = 'E_PAYLOAD';
- throw err;
- }
- if (dryRun) {
- return {
- ok: true,
- dryRun: true,
- length: html.length,
- path,
- meta: rest || {}
- };
- }
- // --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) ---
- // return await realDeploy({ path, html, ...rest });
-
- // Placeholder pour ne pas casser l'appel si pas encore implémenté
- return { ok: true, dryRun: false, path, length: html.length };
-}
-
-module.exports.deployArticle = module.exports.deployArticle || deployArticle;
-
-
-// ============= TRIGGER PRINCIPAL REMPLACĂ PAR WEBHOOK/API =============
-
-/**
- * Point d'entrée pour déclencher le workflow
- * Remplace le trigger onEdit d'Apps Script
- * @param {number} rowNumber - Numéro de ligne à traiter
- * @returns {Promise