/* code.js — bundle concaténé Généré: 2025-09-04T15:08:51.662Z Source: lib Fichiers: 44 Ordre: topo */ /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ErrorReporting.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS // Description: Système de validation et rapport d'erreur // ======================================== const { google } = require('googleapis'); const nodemailer = require('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(); tee.pipe(prettyStream).pipe(process.stdout); // 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 function initWebSocketServer() { if (!wsServer) { wsServer = new WebSocket.Server({ port: process.env.LOG_WS_PORT || 8081 }); 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); }); }); logger.info(`Log WebSocket server started on port ${process.env.LOG_WS_PORT || 8081}`); } } // 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) { // 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(); } // 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 // 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

Informations Système

`; return html; } // 🔄 NODE.JS EXPORTS module.exports = { logSh, cleanLogSheet, validateWorkflowIntegrity, detectDuplicateTags, detectMissingCSVVariables, assessGenerationQuality, sendErrorReport, createHTMLReport }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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'); // ============= CONFIGURATION CENTRALISÉE ============= const LLM_CONFIG = { openai: { apiKey: process.env.OPENAI_API_KEY || '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', 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.CLAUDE_API_KEY || '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', 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, timeout: 300000, // 5 minutes retries: 6 }, gemini: { apiKey: process.env.GEMINI_API_KEY || 'AIzaSyAMzmIGbW5nJlBG5Qyr35sdjb3U2bIBtoE', 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 || 'sk-6e02bc9513884bb8b92b9920524e17b5', 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 || 'sk-zU9gyNkux2zcsj61cdKfztuP1Jozr6lFJ9viUJRPD8p8owhL', 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 || 'wESikMCIuixajSH8WHCiOV2z5sevgmVF', 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 } }; // ============= 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 '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 '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/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/generation/InitialGeneration.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // É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 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/generation/TechnicalEnhancement.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // É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 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/generation/TransitionEnhancement.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // É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 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/generation/StyleEnhancement.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // É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 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/post-processing/SentenceVariation.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // PATTERN BREAKING - TECHNIQUE 1: SENTENCE VARIATION // Responsabilité: Varier les longueurs de phrases pour casser l'uniformité // Anti-détection: Éviter patterns syntaxiques réguliers des LLMs // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * MAIN ENTRY POINT - VARIATION LONGUEUR PHRASES * @param {Object} input - { content: {}, config: {}, context: {} } * @returns {Object} - { content: {}, stats: {}, debug: {} } */ async function applySentenceVariation(input) { return await tracer.run('SentenceVariation.applySentenceVariation()', async () => { const { content, config = {}, context = {} } = input; const { intensity = 0.3, // Probabilité de modification (30%) splitThreshold = 100, // Chars pour split mergeThreshold = 30, // Chars pour merge preserveQuestions = true, // Préserver questions FAQ preserveTitles = true // Préserver titres } = config; await tracer.annotate({ technique: 'sentence_variation', intensity, elementsCount: Object.keys(content).length }); const startTime = Date.now(); logSh(`📐 TECHNIQUE 1/3: Variation longueur phrases (intensité: ${intensity})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'DEBUG'); try { const results = {}; let totalProcessed = 0; let totalModified = 0; let modificationsDetails = []; // Traiter chaque élément de contenu for (const [tag, text] of Object.entries(content)) { totalProcessed++; // Skip certains éléments selon config if (shouldSkipElement(tag, text, { preserveQuestions, preserveTitles })) { results[tag] = text; logSh(` ⏭️ [${tag}]: Préservé (${getSkipReason(tag, text)})`, 'DEBUG'); continue; } // Appliquer variation si éligible const variationResult = varyTextStructure(text, { intensity, splitThreshold, mergeThreshold, tag }); results[tag] = variationResult.text; if (variationResult.modified) { totalModified++; modificationsDetails.push({ tag, modifications: variationResult.modifications, originalLength: text.length, newLength: variationResult.text.length }); logSh(` ✏️ [${tag}]: ${variationResult.modifications.length} modifications`, 'DEBUG'); } else { logSh(` ➡️ [${tag}]: Aucune modification`, 'DEBUG'); } } const duration = Date.now() - startTime; const stats = { processed: totalProcessed, modified: totalModified, modificationRate: Math.round((totalModified / totalProcessed) * 100), duration, technique: 'sentence_variation' }; logSh(`✅ VARIATION PHRASES: ${stats.modified}/${stats.processed} éléments modifiés (${stats.modificationRate}%) en ${duration}ms`, 'INFO'); await tracer.event('Sentence variation terminée', stats); return { content: results, stats, debug: { technique: 'sentence_variation', config: { intensity, splitThreshold, mergeThreshold }, modifications: modificationsDetails } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ VARIATION PHRASES échouée après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`SentenceVariation failed: ${error.message}`); } }, input); } /** * Appliquer variation structure à un texte */ function varyTextStructure(text, config) { const { intensity, splitThreshold, mergeThreshold, tag } = config; if (text.length < 50) { return { text, modified: false, modifications: [] }; } // Séparer en phrases const sentences = splitIntoSentences(text); if (sentences.length < 2) { return { text, modified: false, modifications: [] }; } let modifiedSentences = [...sentences]; const modifications = []; // TECHNIQUE 1: SPLIT des phrases longues for (let i = 0; i < modifiedSentences.length; i++) { const sentence = modifiedSentences[i]; if (sentence.length > splitThreshold && Math.random() < intensity) { const splitResult = splitLongSentence(sentence); if (splitResult.success) { modifiedSentences.splice(i, 1, splitResult.part1, splitResult.part2); modifications.push({ type: 'split', original: sentence.substring(0, 50) + '...', result: `${splitResult.part1.substring(0, 25)}... | ${splitResult.part2.substring(0, 25)}...` }); i++; // Skip la phrase suivante (qui est notre part2) } } } // TECHNIQUE 2: MERGE des phrases courtes for (let i = 0; i < modifiedSentences.length - 1; i++) { const current = modifiedSentences[i]; const next = modifiedSentences[i + 1]; if (current.length < mergeThreshold && next.length < mergeThreshold && Math.random() < intensity) { const merged = mergeSentences(current, next); if (merged.success) { modifiedSentences.splice(i, 2, merged.result); modifications.push({ type: 'merge', original: `${current.substring(0, 20)}... + ${next.substring(0, 20)}...`, result: merged.result.substring(0, 50) + '...' }); } } } const finalText = modifiedSentences.join(' ').trim(); return { text: finalText, modified: modifications.length > 0, modifications }; } /** * Diviser texte en phrases */ function splitIntoSentences(text) { // Regex plus sophistiquée pour gérer les abréviations const sentences = text.split(/(? s.trim()) .filter(s => s.length > 5); return sentences; } /** * Diviser une phrase longue en deux */ function splitLongSentence(sentence) { // Points de rupture naturels const breakPoints = [ ', et ', ', mais ', ', car ', ', donc ', ', ainsi ', ', alors ', ', tandis que ', ', bien que ' ]; // Chercher le meilleur point de rupture proche du milieu const idealBreak = sentence.length / 2; let bestBreak = null; let bestDistance = Infinity; for (const breakPoint of breakPoints) { const index = sentence.indexOf(breakPoint, idealBreak - 50); if (index > 0 && index < sentence.length - 20) { const distance = Math.abs(index - idealBreak); if (distance < bestDistance) { bestDistance = distance; bestBreak = { index, breakPoint }; } } } if (bestBreak) { const part1 = sentence.substring(0, bestBreak.index + 1).trim(); const part2 = sentence.substring(bestBreak.index + bestBreak.breakPoint.length).trim(); // Assurer que part2 commence par une majuscule const capitalizedPart2 = part2.charAt(0).toUpperCase() + part2.slice(1); return { success: true, part1, part2: capitalizedPart2 }; } return { success: false }; } /** * Fusionner deux phrases courtes */ function mergeSentences(sentence1, sentence2) { // Connecteurs pour fusion naturelle const connectors = [ 'et', 'puis', 'aussi', 'également', 'de plus' ]; // Choisir connecteur aléatoire const connector = connectors[Math.floor(Math.random() * connectors.length)]; // Nettoyer les phrases let cleaned1 = sentence1.replace(/[.!?]+$/, '').trim(); let cleaned2 = sentence2.trim(); // Mettre sentence2 en minuscule sauf si nom propre if (!/^[A-Z][a-z]*\s+[A-Z]/.test(cleaned2)) { cleaned2 = cleaned2.charAt(0).toLowerCase() + cleaned2.slice(1); } const merged = `${cleaned1}, ${connector} ${cleaned2}`; return { success: merged.length < 200, // Éviter phrases trop longues result: merged }; } /** * Déterminer si un élément doit être skippé */ function shouldSkipElement(tag, text, config) { // Skip titres si demandé if (config.preserveTitles && (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2'))) { return true; } // Skip questions FAQ si demandé if (config.preserveQuestions && (tag.includes('Faq_q') || text.includes('?'))) { return true; } // Skip textes très courts if (text.length < 50) { return true; } return false; } /** * Obtenir raison du skip pour debug */ function getSkipReason(tag, text) { if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) return 'titre'; if (tag.includes('Faq_q') || text.includes('?')) return 'question'; if (text.length < 50) return 'trop court'; return 'autre'; } /** * Analyser les patterns de phrases d'un texte */ function analyzeSentencePatterns(text) { const sentences = splitIntoSentences(text); if (sentences.length < 2) { return { needsVariation: false, patterns: [] }; } const lengths = sentences.map(s => s.length); const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; // Calculer uniformité (variance faible = uniformité élevée) const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length; const uniformity = 1 / (1 + Math.sqrt(variance) / avgLength); // 0-1, 1 = très uniforme return { needsVariation: uniformity > 0.7, // Seuil d'uniformité problématique patterns: { avgLength: Math.round(avgLength), uniformity: Math.round(uniformity * 100), sentenceCount: sentences.length, variance: Math.round(variance) } }; } module.exports = { applySentenceVariation, // ← MAIN ENTRY POINT varyTextStructure, splitIntoSentences, splitLongSentence, mergeSentences, analyzeSentencePatterns }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/post-processing/LLMFingerprintRemoval.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // PATTERN BREAKING - TECHNIQUE 2: LLM FINGERPRINT REMOVAL // Responsabilité: Remplacer mots/expressions typiques des LLMs // Anti-détection: Éviter vocabulaire détectable par les analyseurs IA // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * DICTIONNAIRE ANTI-DÉTECTION * Mots/expressions LLM → Alternatives humaines naturelles */ const LLM_FINGERPRINTS = { // Mots techniques/corporate typiques IA 'optimal': ['idéal', 'parfait', 'adapté', 'approprié', 'convenable'], 'optimale': ['idéale', 'parfaite', 'adaptée', 'appropriée', 'convenable'], 'comprehensive': ['complet', 'détaillé', 'exhaustif', 'approfondi', 'global'], 'seamless': ['fluide', 'naturel', 'sans accroc', 'harmonieux', 'lisse'], 'robust': ['solide', 'fiable', 'résistant', 'costaud', 'stable'], 'robuste': ['solide', 'fiable', 'résistant', 'costaud', 'stable'], // Expressions trop formelles/IA 'il convient de noter': ['on remarque', 'il faut savoir', 'à noter', 'important'], 'il convient de': ['il faut', 'on doit', 'mieux vaut', 'il est bon de'], 'par conséquent': ['du coup', 'donc', 'résultat', 'ainsi'], 'néanmoins': ['cependant', 'mais', 'pourtant', 'malgré tout'], 'toutefois': ['cependant', 'mais', 'pourtant', 'quand même'], 'de surcroît': ['de plus', 'en plus', 'aussi', 'également'], // Superlatifs excessifs typiques IA 'extrêmement': ['très', 'super', 'vraiment', 'particulièrement'], 'particulièrement': ['très', 'vraiment', 'spécialement', 'surtout'], 'remarquablement': ['très', 'vraiment', 'sacrément', 'fichement'], 'exceptionnellement': ['très', 'vraiment', 'super', 'incroyablement'], // Mots de liaison trop mécaniques 'en définitive': ['au final', 'finalement', 'bref', 'en gros'], 'il s\'avère que': ['on voit que', 'il se trouve que', 'en fait'], 'force est de constater': ['on constate', 'on voit bien', 'c\'est clair'], // Expressions commerciales robotiques 'solution innovante': ['nouveauté', 'innovation', 'solution moderne', 'nouvelle approche'], 'approche holistique': ['approche globale', 'vision d\'ensemble', 'approche complète'], 'expérience utilisateur': ['confort d\'utilisation', 'facilité d\'usage', 'ergonomie'], 'retour sur investissement': ['rentabilité', 'bénéfices', 'profits'], // Adjectifs surutilisés par IA 'révolutionnaire': ['nouveau', 'moderne', 'innovant', 'original'], 'game-changer': ['nouveauté', 'innovation', 'changement', 'révolution'], 'cutting-edge': ['moderne', 'récent', 'nouveau', 'avancé'], 'state-of-the-art': ['moderne', 'récent', 'performant', 'haut de gamme'] }; /** * EXPRESSIONS CONTEXTUELLES SECTEUR SIGNALÉTIQUE * Adaptées au domaine métier pour plus de naturel */ const CONTEXTUAL_REPLACEMENTS = { 'solution': { 'signalétique': ['plaque', 'panneau', 'support', 'réalisation'], 'impression': ['tirage', 'print', 'production', 'fabrication'], 'default': ['option', 'possibilité', 'choix', 'alternative'] }, 'produit': { 'signalétique': ['plaque', 'panneau', 'enseigne', 'support'], 'default': ['article', 'réalisation', 'création'] }, 'service': { 'signalétique': ['prestation', 'réalisation', 'travail', 'création'], 'default': ['prestation', 'travail', 'aide'] } }; /** * MAIN ENTRY POINT - SUPPRESSION EMPREINTES LLM * @param {Object} input - { content: {}, config: {}, context: {} } * @returns {Object} - { content: {}, stats: {}, debug: {} } */ async function removeLLMFingerprints(input) { return await tracer.run('LLMFingerprintRemoval.removeLLMFingerprints()', async () => { const { content, config = {}, context = {} } = input; const { intensity = 1.0, // Probabilité de remplacement (100%) preserveKeywords = true, // Préserver mots-clés SEO contextualMode = true, // Mode contextuel métier csvData = null // Pour contexte métier } = config; await tracer.annotate({ technique: 'fingerprint_removal', intensity, elementsCount: Object.keys(content).length, contextualMode }); const startTime = Date.now(); logSh(`🔍 TECHNIQUE 2/3: Suppression empreintes LLM (intensité: ${intensity})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à nettoyer`, 'DEBUG'); try { const results = {}; let totalProcessed = 0; let totalReplacements = 0; let replacementDetails = []; // Préparer contexte métier const businessContext = extractBusinessContext(csvData); // Traiter chaque élément de contenu for (const [tag, text] of Object.entries(content)) { totalProcessed++; if (text.length < 20) { results[tag] = text; continue; } // Appliquer suppression des empreintes const cleaningResult = cleanTextFingerprints(text, { intensity, preserveKeywords, contextualMode, businessContext, tag }); results[tag] = cleaningResult.text; if (cleaningResult.replacements.length > 0) { totalReplacements += cleaningResult.replacements.length; replacementDetails.push({ tag, replacements: cleaningResult.replacements, fingerprintsFound: cleaningResult.fingerprintsDetected }); logSh(` 🧹 [${tag}]: ${cleaningResult.replacements.length} remplacements`, 'DEBUG'); } else { logSh(` ✅ [${tag}]: Aucune empreinte détectée`, 'DEBUG'); } } const duration = Date.now() - startTime; const stats = { processed: totalProcessed, totalReplacements, avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100, elementsWithFingerprints: replacementDetails.length, duration, technique: 'fingerprint_removal' }; logSh(`✅ NETTOYAGE EMPREINTES: ${stats.totalReplacements} remplacements sur ${stats.elementsWithFingerprints}/${stats.processed} éléments en ${duration}ms`, 'INFO'); await tracer.event('Fingerprint removal terminée', stats); return { content: results, stats, debug: { technique: 'fingerprint_removal', config: { intensity, preserveKeywords, contextualMode }, replacements: replacementDetails, businessContext } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ NETTOYAGE EMPREINTES échoué après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`LLMFingerprintRemoval failed: ${error.message}`); } }, input); } /** * Nettoyer les empreintes LLM d'un texte */ function cleanTextFingerprints(text, config) { const { intensity, preserveKeywords, contextualMode, businessContext, tag } = config; let cleanedText = text; const replacements = []; const fingerprintsDetected = []; // PHASE 1: Remplacements directs du dictionnaire for (const [fingerprint, alternatives] of Object.entries(LLM_FINGERPRINTS)) { const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi'); const matches = text.match(regex); if (matches) { fingerprintsDetected.push(fingerprint); // Appliquer remplacement selon intensité if (Math.random() <= intensity) { const alternative = selectBestAlternative(alternatives, businessContext, contextualMode); cleanedText = cleanedText.replace(regex, (match) => { // Préserver la casse originale return preserveCase(match, alternative); }); replacements.push({ type: 'direct', original: fingerprint, replacement: alternative, occurrences: matches.length }); } } } // PHASE 2: Remplacements contextuels if (contextualMode && businessContext) { const contextualReplacements = applyContextualReplacements(cleanedText, businessContext); cleanedText = contextualReplacements.text; replacements.push(...contextualReplacements.replacements); } // PHASE 3: Détection patterns récurrents const patternReplacements = replaceRecurringPatterns(cleanedText, intensity); cleanedText = patternReplacements.text; replacements.push(...patternReplacements.replacements); return { text: cleanedText, replacements, fingerprintsDetected }; } /** * Sélectionner la meilleure alternative selon le contexte */ function selectBestAlternative(alternatives, businessContext, contextualMode) { if (!contextualMode || !businessContext) { // Mode aléatoire simple return alternatives[Math.floor(Math.random() * alternatives.length)]; } // Mode contextuel : privilégier alternatives adaptées au métier const contextualAlternatives = alternatives.filter(alt => isContextuallyAppropriate(alt, businessContext) ); const finalAlternatives = contextualAlternatives.length > 0 ? contextualAlternatives : alternatives; return finalAlternatives[Math.floor(Math.random() * finalAlternatives.length)]; } /** * Vérifier si une alternative est contextuelle appropriée */ function isContextuallyAppropriate(alternative, businessContext) { const { sector, vocabulary } = businessContext; // Signalétique : privilégier vocabulaire technique/artisanal if (sector === 'signalétique') { const technicalWords = ['solide', 'fiable', 'costaud', 'résistant', 'adapté']; return technicalWords.includes(alternative); } return true; // Par défaut accepter } /** * Appliquer remplacements contextuels */ function applyContextualReplacements(text, businessContext) { let processedText = text; const replacements = []; for (const [word, contexts] of Object.entries(CONTEXTUAL_REPLACEMENTS)) { const regex = new RegExp(`\\b${word}\\b`, 'gi'); const matches = processedText.match(regex); if (matches) { const contextAlternatives = contexts[businessContext.sector] || contexts.default; const replacement = contextAlternatives[Math.floor(Math.random() * contextAlternatives.length)]; processedText = processedText.replace(regex, (match) => { return preserveCase(match, replacement); }); replacements.push({ type: 'contextual', original: word, replacement, occurrences: matches.length, context: businessContext.sector }); } } return { text: processedText, replacements }; } /** * Remplacer patterns récurrents */ function replaceRecurringPatterns(text, intensity) { let processedText = text; const replacements = []; // Pattern 1: "très + adjectif" → variantes const veryPattern = /\btrès\s+(\w+)/gi; const veryMatches = [...text.matchAll(veryPattern)]; if (veryMatches.length > 2 && Math.random() < intensity) { // Remplacer certains "très" par des alternatives const alternatives = ['super', 'vraiment', 'particulièrement', 'assez']; veryMatches.slice(1).forEach((match, index) => { if (Math.random() < 0.5) { const alternative = alternatives[Math.floor(Math.random() * alternatives.length)]; const fullMatch = match[0]; const adjective = match[1]; const replacement = `${alternative} ${adjective}`; processedText = processedText.replace(fullMatch, replacement); replacements.push({ type: 'pattern', pattern: '"très + adjectif"', original: fullMatch, replacement }); } }); } return { text: processedText, replacements }; } /** * Extraire contexte métier des données CSV */ function extractBusinessContext(csvData) { if (!csvData) { return { sector: 'general', vocabulary: [] }; } const mc0 = csvData.mc0?.toLowerCase() || ''; // Détection secteur let sector = 'general'; if (mc0.includes('plaque') || mc0.includes('panneau') || mc0.includes('enseigne')) { sector = 'signalétique'; } else if (mc0.includes('impression') || mc0.includes('print')) { sector = 'impression'; } // Extraction vocabulaire clé const vocabulary = [csvData.mc0, csvData.t0, csvData.tMinus1].filter(Boolean); return { sector, vocabulary }; } /** * Préserver la casse originale */ function preserveCase(original, replacement) { if (original === original.toUpperCase()) { return replacement.toUpperCase(); } else if (original[0] === original[0].toUpperCase()) { return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase(); } else { return replacement.toLowerCase(); } } /** * Échapper caractères regex */ function escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Analyser les empreintes LLM dans un texte */ function analyzeLLMFingerprints(text) { const detectedFingerprints = []; let totalMatches = 0; for (const fingerprint of Object.keys(LLM_FINGERPRINTS)) { const regex = new RegExp(`\\b${escapeRegex(fingerprint)}\\b`, 'gi'); const matches = text.match(regex); if (matches) { detectedFingerprints.push({ fingerprint, occurrences: matches.length, category: categorizefingerprint(fingerprint) }); totalMatches += matches.length; } } return { hasFingerprints: detectedFingerprints.length > 0, fingerprints: detectedFingerprints, totalMatches, riskLevel: calculateRiskLevel(detectedFingerprints, text.length) }; } /** * Catégoriser une empreinte LLM */ function categorizefingerprint(fingerprint) { const categories = { 'technical': ['optimal', 'comprehensive', 'robust', 'seamless'], 'formal': ['il convient de', 'néanmoins', 'par conséquent'], 'superlative': ['extrêmement', 'particulièrement', 'remarquablement'], 'commercial': ['solution innovante', 'game-changer', 'révolutionnaire'] }; for (const [category, words] of Object.entries(categories)) { if (words.some(word => fingerprint.includes(word))) { return category; } } return 'other'; } /** * Calculer niveau de risque de détection */ function calculateRiskLevel(fingerprints, textLength) { if (fingerprints.length === 0) return 'low'; const fingerprintDensity = fingerprints.reduce((sum, fp) => sum + fp.occurrences, 0) / (textLength / 100); if (fingerprintDensity > 3) return 'high'; if (fingerprintDensity > 1.5) return 'medium'; return 'low'; } module.exports = { removeLLMFingerprints, // ← MAIN ENTRY POINT cleanTextFingerprints, analyzeLLMFingerprints, LLM_FINGERPRINTS, CONTEXTUAL_REPLACEMENTS, extractBusinessContext }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/post-processing/TransitionHumanization.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // PATTERN BREAKING - TECHNIQUE 3: TRANSITION HUMANIZATION // Responsabilité: Remplacer connecteurs mécaniques par transitions naturelles // Anti-détection: Éviter patterns de liaison typiques des LLMs // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * DICTIONNAIRE CONNECTEURS HUMANISÉS * Connecteurs LLM → Alternatives naturelles par contexte */ const TRANSITION_REPLACEMENTS = { // Connecteurs trop formels → versions naturelles 'par ailleurs': { alternatives: ['d\'ailleurs', 'au fait', 'soit dit en passant', 'à propos', 'sinon'], weight: 0.8, contexts: ['casual', 'conversational'] }, 'en effet': { alternatives: ['effectivement', 'c\'est vrai', 'tout à fait', 'absolument', 'exactement'], weight: 0.9, contexts: ['confirmative', 'agreement'] }, 'de plus': { alternatives: ['aussi', 'également', 'qui plus est', 'en plus', 'et puis'], weight: 0.7, contexts: ['additive', 'continuation'] }, 'cependant': { alternatives: ['mais', 'pourtant', 'néanmoins', 'malgré tout', 'quand même'], weight: 0.6, contexts: ['contrast', 'opposition'] }, 'ainsi': { alternatives: ['donc', 'du coup', 'comme ça', 'par conséquent', 'résultat'], weight: 0.8, contexts: ['consequence', 'result'] }, 'donc': { alternatives: ['du coup', 'alors', 'par conséquent', 'ainsi', 'résultat'], weight: 0.5, contexts: ['consequence', 'logical'] }, // Connecteurs de séquence 'ensuite': { alternatives: ['puis', 'après', 'et puis', 'alors', 'du coup'], weight: 0.6, contexts: ['sequence', 'temporal'] }, 'puis': { alternatives: ['ensuite', 'après', 'et puis', 'alors'], weight: 0.4, contexts: ['sequence', 'temporal'] }, // Connecteurs d'emphase 'également': { alternatives: ['aussi', 'de même', 'pareillement', 'en plus'], weight: 0.6, contexts: ['similarity', 'addition'] }, 'aussi': { alternatives: ['également', 'de même', 'en plus', 'pareillement'], weight: 0.3, contexts: ['similarity', 'addition'] }, // Connecteurs de conclusion 'enfin': { alternatives: ['finalement', 'au final', 'pour finir', 'en dernier'], weight: 0.5, contexts: ['conclusion', 'final'] }, 'finalement': { alternatives: ['au final', 'en fin de compte', 'pour finir', 'enfin'], weight: 0.4, contexts: ['conclusion', 'final'] } }; /** * PATTERNS DE TRANSITION NATURELLE * Selon le style de personnalité */ const PERSONALITY_TRANSITIONS = { 'décontracté': { preferred: ['du coup', 'alors', 'bon', 'après', 'sinon'], avoided: ['par conséquent', 'néanmoins', 'toutefois'] }, 'technique': { preferred: ['donc', 'ainsi', 'par conséquent', 'résultat'], avoided: ['du coup', 'bon', 'franchement'] }, 'commercial': { preferred: ['aussi', 'de plus', 'également', 'qui plus est'], avoided: ['du coup', 'bon', 'franchement'] }, 'familier': { preferred: ['du coup', 'bon', 'alors', 'après', 'franchement'], avoided: ['par conséquent', 'néanmoins', 'de surcroît'] } }; /** * MAIN ENTRY POINT - HUMANISATION TRANSITIONS * @param {Object} input - { content: {}, config: {}, context: {} } * @returns {Object} - { content: {}, stats: {}, debug: {} } */ async function humanizeTransitions(input) { return await tracer.run('TransitionHumanization.humanizeTransitions()', async () => { const { content, config = {}, context = {} } = input; const { intensity = 0.6, // Probabilité de remplacement (60%) personalityStyle = null, // Style de personnalité pour guidage avoidRepetition = true, // Éviter répétitions excessives preserveFormal = false, // Préserver style formel csvData = null // Données pour personnalité } = config; await tracer.annotate({ technique: 'transition_humanization', intensity, personalityStyle: personalityStyle || csvData?.personality?.style, elementsCount: Object.keys(content).length }); const startTime = Date.now(); logSh(`🔗 TECHNIQUE 3/3: Humanisation transitions (intensité: ${intensity})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à humaniser`, 'DEBUG'); try { const results = {}; let totalProcessed = 0; let totalReplacements = 0; let humanizationDetails = []; // Extraire style de personnalité const effectivePersonalityStyle = personalityStyle || csvData?.personality?.style || 'neutral'; // Analyser patterns globaux pour éviter répétitions const globalPatterns = analyzeGlobalTransitionPatterns(content); // Traiter chaque élément de contenu for (const [tag, text] of Object.entries(content)) { totalProcessed++; if (text.length < 30) { results[tag] = text; continue; } // Appliquer humanisation des transitions const humanizationResult = humanizeTextTransitions(text, { intensity, personalityStyle: effectivePersonalityStyle, avoidRepetition, preserveFormal, globalPatterns, tag }); results[tag] = humanizationResult.text; if (humanizationResult.replacements.length > 0) { totalReplacements += humanizationResult.replacements.length; humanizationDetails.push({ tag, replacements: humanizationResult.replacements, transitionsDetected: humanizationResult.transitionsFound }); logSh(` 🔄 [${tag}]: ${humanizationResult.replacements.length} transitions humanisées`, 'DEBUG'); } else { logSh(` ➡️ [${tag}]: Transitions déjà naturelles`, 'DEBUG'); } } const duration = Date.now() - startTime; const stats = { processed: totalProcessed, totalReplacements, avgReplacementsPerElement: Math.round(totalReplacements / totalProcessed * 100) / 100, elementsWithTransitions: humanizationDetails.length, personalityStyle: effectivePersonalityStyle, duration, technique: 'transition_humanization' }; logSh(`✅ HUMANISATION TRANSITIONS: ${stats.totalReplacements} remplacements sur ${stats.elementsWithTransitions}/${stats.processed} éléments en ${duration}ms`, 'INFO'); await tracer.event('Transition humanization terminée', stats); return { content: results, stats, debug: { technique: 'transition_humanization', config: { intensity, personalityStyle: effectivePersonalityStyle, avoidRepetition }, humanizations: humanizationDetails, globalPatterns } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ HUMANISATION TRANSITIONS échouée après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`TransitionHumanization failed: ${error.message}`); } }, input); } /** * Humaniser les transitions d'un texte */ function humanizeTextTransitions(text, config) { const { intensity, personalityStyle, avoidRepetition, preserveFormal, globalPatterns, tag } = config; let humanizedText = text; const replacements = []; const transitionsFound = []; // Statistiques usage pour éviter répétitions const usageStats = {}; // Traiter chaque connecteur du dictionnaire for (const [transition, transitionData] of Object.entries(TRANSITION_REPLACEMENTS)) { const { alternatives, weight, contexts } = transitionData; // Rechercher occurrences (insensible à la casse, mais préserver limites mots) const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi'); const matches = [...text.matchAll(regex)]; if (matches.length > 0) { transitionsFound.push(transition); // Décider si on remplace selon intensité et poids const shouldReplace = Math.random() < (intensity * weight); if (shouldReplace && !preserveFormal) { // Sélectionner meilleure alternative const selectedAlternative = selectBestTransitionAlternative( alternatives, personalityStyle, usageStats, avoidRepetition ); // Appliquer remplacement en préservant la casse humanizedText = humanizedText.replace(regex, (match) => { return preserveCase(match, selectedAlternative); }); // Enregistrer usage usageStats[selectedAlternative] = (usageStats[selectedAlternative] || 0) + matches.length; replacements.push({ original: transition, replacement: selectedAlternative, occurrences: matches.length, contexts, personalityMatch: isPersonalityAppropriate(selectedAlternative, personalityStyle) }); } } } // Post-processing : éviter accumulations if (avoidRepetition) { const repetitionCleaned = reduceTransitionRepetition(humanizedText, usageStats); humanizedText = repetitionCleaned.text; replacements.push(...repetitionCleaned.additionalChanges); } return { text: humanizedText, replacements, transitionsFound }; } /** * Sélectionner meilleure alternative de transition */ function selectBestTransitionAlternative(alternatives, personalityStyle, usageStats, avoidRepetition) { // Filtrer selon personnalité const personalityFiltered = alternatives.filter(alt => isPersonalityAppropriate(alt, personalityStyle) ); const candidateList = personalityFiltered.length > 0 ? personalityFiltered : alternatives; if (!avoidRepetition) { return candidateList[Math.floor(Math.random() * candidateList.length)]; } // Éviter les alternatives déjà trop utilisées const lessUsedAlternatives = candidateList.filter(alt => (usageStats[alt] || 0) < 2 ); const finalList = lessUsedAlternatives.length > 0 ? lessUsedAlternatives : candidateList; return finalList[Math.floor(Math.random() * finalList.length)]; } /** * Vérifier si alternative appropriée pour personnalité */ function isPersonalityAppropriate(alternative, personalityStyle) { if (!personalityStyle || personalityStyle === 'neutral') return true; const styleMapping = { 'décontracté': PERSONALITY_TRANSITIONS.décontracté, 'technique': PERSONALITY_TRANSITIONS.technique, 'commercial': PERSONALITY_TRANSITIONS.commercial, 'familier': PERSONALITY_TRANSITIONS.familier }; const styleConfig = styleMapping[personalityStyle.toLowerCase()]; if (!styleConfig) return true; // Éviter les connecteurs inappropriés if (styleConfig.avoided.includes(alternative)) return false; // Privilégier les connecteurs préférés if (styleConfig.preferred.includes(alternative)) return true; return true; } /** * Réduire répétitions excessives de transitions */ function reduceTransitionRepetition(text, usageStats) { let processedText = text; const additionalChanges = []; // Identifier connecteurs surutilisés (>3 fois) const overusedTransitions = Object.entries(usageStats) .filter(([transition, count]) => count > 3) .map(([transition]) => transition); for (const overusedTransition of overusedTransitions) { // Remplacer quelques occurrences par des alternatives const regex = new RegExp(`\\b${escapeRegex(overusedTransition)}\\b`, 'g'); let replacements = 0; processedText = processedText.replace(regex, (match, offset) => { // Remplacer 1 occurrence sur 3 environ if (Math.random() < 0.33 && replacements < 2) { replacements++; const alternatives = findAlternativesFor(overusedTransition); const alternative = alternatives[Math.floor(Math.random() * alternatives.length)]; additionalChanges.push({ type: 'repetition_reduction', original: overusedTransition, replacement: alternative, reason: 'overuse' }); return preserveCase(match, alternative); } return match; }); } return { text: processedText, additionalChanges }; } /** * Trouver alternatives pour un connecteur donné */ function findAlternativesFor(transition) { // Chercher dans le dictionnaire for (const [key, data] of Object.entries(TRANSITION_REPLACEMENTS)) { if (data.alternatives.includes(transition)) { return data.alternatives.filter(alt => alt !== transition); } } // Alternatives génériques const genericAlternatives = { 'du coup': ['alors', 'donc', 'ainsi'], 'alors': ['du coup', 'donc', 'ensuite'], 'donc': ['du coup', 'alors', 'ainsi'], 'aussi': ['également', 'de plus', 'en plus'], 'mais': ['cependant', 'pourtant', 'néanmoins'] }; return genericAlternatives[transition] || ['donc', 'alors']; } /** * Analyser patterns globaux de transitions */ function analyzeGlobalTransitionPatterns(content) { const allText = Object.values(content).join(' '); const transitionCounts = {}; const repetitionPatterns = []; // Compter occurrences globales for (const transition of Object.keys(TRANSITION_REPLACEMENTS)) { const regex = new RegExp(`\\b${escapeRegex(transition)}\\b`, 'gi'); const matches = allText.match(regex); if (matches) { transitionCounts[transition] = matches.length; } } // Identifier patterns de répétition problématiques const sortedTransitions = Object.entries(transitionCounts) .sort(([,a], [,b]) => b - a) .slice(0, 5); // Top 5 plus utilisées sortedTransitions.forEach(([transition, count]) => { if (count > 5) { repetitionPatterns.push({ transition, count, severity: count > 10 ? 'high' : count > 7 ? 'medium' : 'low' }); } }); return { transitionCounts, repetitionPatterns, diversityScore: Object.keys(transitionCounts).length / Math.max(1, Object.values(transitionCounts).reduce((a,b) => a+b, 0)) }; } /** * Préserver la casse originale */ function preserveCase(original, replacement) { if (original === original.toUpperCase()) { return replacement.toUpperCase(); } else if (original[0] === original[0].toUpperCase()) { return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase(); } else { return replacement.toLowerCase(); } } /** * Échapper caractères regex */ function escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Analyser qualité des transitions d'un texte */ function analyzeTransitionQuality(text) { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 5); if (sentences.length < 2) { return { score: 100, issues: [], naturalness: 'high' }; } let mechanicalTransitions = 0; let totalTransitions = 0; const issues = []; // Analyser chaque transition sentences.forEach((sentence, index) => { if (index === 0) return; const trimmed = sentence.trim(); const startsWithTransition = Object.keys(TRANSITION_REPLACEMENTS).some(transition => trimmed.toLowerCase().startsWith(transition.toLowerCase()) ); if (startsWithTransition) { totalTransitions++; // Vérifier si transition mécanique const transition = Object.keys(TRANSITION_REPLACEMENTS).find(t => trimmed.toLowerCase().startsWith(t.toLowerCase()) ); if (transition && TRANSITION_REPLACEMENTS[transition].weight > 0.7) { mechanicalTransitions++; issues.push({ type: 'mechanical_transition', transition, suggestion: TRANSITION_REPLACEMENTS[transition].alternatives[0] }); } } }); const mechanicalRatio = totalTransitions > 0 ? mechanicalTransitions / totalTransitions : 0; const score = Math.max(0, 100 - (mechanicalRatio * 100)); let naturalness = 'high'; if (mechanicalRatio > 0.5) naturalness = 'low'; else if (mechanicalRatio > 0.25) naturalness = 'medium'; return { score: Math.round(score), issues, naturalness, mechanicalRatio }; } module.exports = { humanizeTransitions, // ← MAIN ENTRY POINT humanizeTextTransitions, analyzeTransitionQuality, analyzeGlobalTransitionPatterns, TRANSITION_REPLACEMENTS, PERSONALITY_TRANSITIONS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/post-processing/PatternBreaking.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ORCHESTRATEUR PATTERN BREAKING - NIVEAU 2 // Responsabilité: Coordonner les 3 techniques anti-détection // Objectif: -20% détection IA vs Niveau 1 // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); // Import des 3 techniques Pattern Breaking const { applySentenceVariation } = require('./SentenceVariation'); const { removeLLMFingerprints } = require('./LLMFingerprintRemoval'); const { humanizeTransitions } = require('./TransitionHumanization'); /** * MAIN ENTRY POINT - PATTERN BREAKING COMPLET * @param {Object} input - { content: {}, csvData: {}, options: {} } * @returns {Object} - { content: {}, stats: {}, debug: {} } */ async function applyPatternBreaking(input) { return await tracer.run('PatternBreaking.applyPatternBreaking()', async () => { const { content, csvData, options = {} } = input; const config = { // Configuration globale intensity: 0.6, // Intensité générale (60%) // Contrôle par technique sentenceVariation: true, // Activer variation phrases fingerprintRemoval: true, // Activer suppression empreintes transitionHumanization: true, // Activer humanisation transitions // Configuration spécifique par technique sentenceVariationConfig: { intensity: 0.3, splitThreshold: 100, mergeThreshold: 30, preserveQuestions: true, preserveTitles: true }, fingerprintRemovalConfig: { intensity: 1.0, preserveKeywords: true, contextualMode: true, csvData }, transitionHumanizationConfig: { intensity: 0.6, personalityStyle: csvData?.personality?.style, avoidRepetition: true, preserveFormal: false, csvData }, // Options avancées qualityPreservation: true, // Préserver qualité contenu seoIntegrity: true, // Maintenir intégrité SEO readabilityCheck: true, // Vérifier lisibilité ...options // Override avec options fournies }; await tracer.annotate({ level: 2, technique: 'pattern_breaking', elementsCount: Object.keys(content).length, personality: csvData?.personality?.nom, config: { sentenceVariation: config.sentenceVariation, fingerprintRemoval: config.fingerprintRemoval, transitionHumanization: config.transitionHumanization, intensity: config.intensity } }); const startTime = Date.now(); logSh(`🎯 NIVEAU 2: PATTERN BREAKING (3 techniques)`, 'INFO'); logSh(` 🎭 Personnalité: ${csvData?.personality?.nom} (${csvData?.personality?.style})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à traiter`, 'INFO'); logSh(` ⚙️ Techniques actives: ${[config.sentenceVariation && 'Variation', config.fingerprintRemoval && 'Empreintes', config.transitionHumanization && 'Transitions'].filter(Boolean).join(' + ')}`, 'INFO'); try { let currentContent = { ...content }; const pipelineStats = { techniques: [], totalDuration: 0, qualityMetrics: {} }; // Analyse initiale de qualité if (config.qualityPreservation) { pipelineStats.qualityMetrics.initial = analyzeContentQuality(currentContent); } // TECHNIQUE 1: VARIATION LONGUEUR PHRASES if (config.sentenceVariation) { const step1Result = await applySentenceVariation({ content: currentContent, config: config.sentenceVariationConfig, context: { step: 1, totalSteps: 3 } }); currentContent = step1Result.content; pipelineStats.techniques.push({ name: 'SentenceVariation', ...step1Result.stats, qualityImpact: calculateQualityImpact(content, step1Result.content) }); logSh(` ✅ 1/3: Variation phrases - ${step1Result.stats.modified}/${step1Result.stats.processed} éléments`, 'INFO'); } // TECHNIQUE 2: SUPPRESSION EMPREINTES LLM if (config.fingerprintRemoval) { const step2Result = await removeLLMFingerprints({ content: currentContent, config: config.fingerprintRemovalConfig, context: { step: 2, totalSteps: 3 } }); currentContent = step2Result.content; pipelineStats.techniques.push({ name: 'FingerprintRemoval', ...step2Result.stats, qualityImpact: calculateQualityImpact(content, step2Result.content) }); logSh(` ✅ 2/3: Suppression empreintes - ${step2Result.stats.totalReplacements} remplacements`, 'INFO'); } // TECHNIQUE 3: HUMANISATION TRANSITIONS if (config.transitionHumanization) { const step3Result = await humanizeTransitions({ content: currentContent, config: config.transitionHumanizationConfig, context: { step: 3, totalSteps: 3 } }); currentContent = step3Result.content; pipelineStats.techniques.push({ name: 'TransitionHumanization', ...step3Result.stats, qualityImpact: calculateQualityImpact(content, step3Result.content) }); logSh(` ✅ 3/3: Humanisation transitions - ${step3Result.stats.totalReplacements} améliorations`, 'INFO'); } // POST-PROCESSING: Vérifications qualité if (config.qualityPreservation || config.readabilityCheck) { const qualityCheck = performQualityChecks(content, currentContent, config); pipelineStats.qualityMetrics.final = qualityCheck; // Rollback si qualité trop dégradée if (qualityCheck.shouldRollback) { logSh(`⚠️ ROLLBACK: Qualité dégradée, retour contenu original`, 'WARNING'); currentContent = content; pipelineStats.rollback = true; } } // RÉSULTATS FINAUX const totalDuration = Date.now() - startTime; pipelineStats.totalDuration = totalDuration; const totalModifications = pipelineStats.techniques.reduce((sum, tech) => { return sum + (tech.modified || tech.totalReplacements || 0); }, 0); const stats = { level: 2, technique: 'pattern_breaking', processed: Object.keys(content).length, totalModifications, techniquesUsed: pipelineStats.techniques.length, duration: totalDuration, techniques: pipelineStats.techniques, qualityPreserved: !pipelineStats.rollback, rollback: pipelineStats.rollback || false }; logSh(`🎯 NIVEAU 2 TERMINÉ: ${totalModifications} modifications sur ${stats.processed} éléments (${totalDuration}ms)`, 'INFO'); // Log détaillé par technique pipelineStats.techniques.forEach(tech => { const modificationsCount = tech.modified || tech.totalReplacements || 0; logSh(` • ${tech.name}: ${modificationsCount} modifications (${tech.duration}ms)`, 'DEBUG'); }); await tracer.event('Pattern breaking terminé', stats); return { content: currentContent, stats, debug: { level: 2, technique: 'pattern_breaking', config, pipeline: pipelineStats, qualityMetrics: pipelineStats.qualityMetrics } }; } catch (error) { const totalDuration = Date.now() - startTime; logSh(`❌ NIVEAU 2 ÉCHOUÉ après ${totalDuration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); await tracer.event('Pattern breaking échoué', { error: error.message, duration: totalDuration, fallback: true }); return { content, stats: { level: 2, technique: 'pattern_breaking', processed: Object.keys(content).length, totalModifications: 0, duration: totalDuration, error: error.message, fallback: true }, debug: { error: error.message, fallback: true } }; } }, input); } /** * MODE DIAGNOSTIC - Test individuel des techniques */ async function diagnosticPatternBreaking(content, csvData) { logSh(`🔬 DIAGNOSTIC NIVEAU 2: Test individuel des techniques`, 'INFO'); const diagnostics = { techniques: [], errors: [], performance: {}, recommendations: [] }; const techniques = [ { name: 'SentenceVariation', func: applySentenceVariation }, { name: 'FingerprintRemoval', func: removeLLMFingerprints }, { name: 'TransitionHumanization', func: humanizeTransitions } ]; for (const technique of techniques) { try { const startTime = Date.now(); const result = await technique.func({ content, config: { csvData }, context: { diagnostic: true } }); diagnostics.techniques.push({ name: technique.name, success: true, duration: Date.now() - startTime, stats: result.stats, effectivenessScore: calculateEffectivenessScore(result.stats) }); } catch (error) { diagnostics.errors.push({ technique: technique.name, error: error.message }); diagnostics.techniques.push({ name: technique.name, success: false, error: error.message }); } } // Générer recommandations diagnostics.recommendations = generateRecommendations(diagnostics.techniques); const successfulTechniques = diagnostics.techniques.filter(t => t.success); diagnostics.performance.totalDuration = diagnostics.techniques.reduce((sum, t) => sum + (t.duration || 0), 0); diagnostics.performance.successRate = Math.round((successfulTechniques.length / techniques.length) * 100); logSh(`🔬 DIAGNOSTIC TERMINÉ: ${successfulTechniques.length}/${techniques.length} techniques opérationnelles`, 'INFO'); return diagnostics; } /** * Analyser qualité du contenu */ function analyzeContentQuality(content) { const allText = Object.values(content).join(' '); const wordCount = allText.split(/\s+/).length; const avgWordsPerElement = wordCount / Object.keys(content).length; // Métrique de lisibilité approximative (Flesch simplifié) const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 5); const avgWordsPerSentence = wordCount / Math.max(1, sentences.length); const readabilityScore = Math.max(0, 100 - (avgWordsPerSentence * 1.5)); return { wordCount, elementCount: Object.keys(content).length, avgWordsPerElement: Math.round(avgWordsPerElement), avgWordsPerSentence: Math.round(avgWordsPerSentence), readabilityScore: Math.round(readabilityScore), sentenceCount: sentences.length }; } /** * Calculer impact qualité entre avant/après */ function calculateQualityImpact(originalContent, modifiedContent) { const originalQuality = analyzeContentQuality(originalContent); const modifiedQuality = analyzeContentQuality(modifiedContent); const wordCountChange = ((modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount) * 100; const readabilityChange = modifiedQuality.readabilityScore - originalQuality.readabilityScore; return { wordCountChange: Math.round(wordCountChange * 100) / 100, readabilityChange: Math.round(readabilityChange), severe: Math.abs(wordCountChange) > 10 || Math.abs(readabilityChange) > 15 }; } /** * Effectuer vérifications qualité */ function performQualityChecks(originalContent, modifiedContent, config) { const originalQuality = analyzeContentQuality(originalContent); const modifiedQuality = analyzeContentQuality(modifiedContent); const qualityThresholds = { maxWordCountChange: 15, // % max changement nombre mots minReadabilityScore: 50, // Score lisibilité minimum maxReadabilityDrop: 20 // Baisse max lisibilité }; const issues = []; // Vérification nombre de mots const wordCountChange = Math.abs(modifiedQuality.wordCount - originalQuality.wordCount) / originalQuality.wordCount * 100; if (wordCountChange > qualityThresholds.maxWordCountChange) { issues.push({ type: 'word_count_change', severity: 'high', change: wordCountChange, threshold: qualityThresholds.maxWordCountChange }); } // Vérification lisibilité if (modifiedQuality.readabilityScore < qualityThresholds.minReadabilityScore) { issues.push({ type: 'low_readability', severity: 'medium', score: modifiedQuality.readabilityScore, threshold: qualityThresholds.minReadabilityScore }); } const readabilityDrop = originalQuality.readabilityScore - modifiedQuality.readabilityScore; if (readabilityDrop > qualityThresholds.maxReadabilityDrop) { issues.push({ type: 'readability_drop', severity: 'high', drop: readabilityDrop, threshold: qualityThresholds.maxReadabilityDrop }); } // Décision rollback const highSeverityIssues = issues.filter(issue => issue.severity === 'high'); const shouldRollback = highSeverityIssues.length > 0 && config.qualityPreservation; return { originalQuality, modifiedQuality, issues, shouldRollback, qualityScore: calculateOverallQualityScore(issues, modifiedQuality) }; } /** * Calculer score de qualité global */ function calculateOverallQualityScore(issues, quality) { let baseScore = 100; issues.forEach(issue => { const penalty = issue.severity === 'high' ? 30 : issue.severity === 'medium' ? 15 : 5; baseScore -= penalty; }); // Bonus pour bonne lisibilité if (quality.readabilityScore > 70) baseScore += 10; return Math.max(0, Math.min(100, baseScore)); } /** * Calculer score d'efficacité d'une technique */ function calculateEffectivenessScore(stats) { if (!stats) return 0; const modificationsCount = stats.modified || stats.totalReplacements || 0; const processedCount = stats.processed || 1; const modificationRate = (modificationsCount / processedCount) * 100; // Score basé sur taux de modification et durée const baseScore = Math.min(100, modificationRate * 2); // Max 50% modification = score 100 const durationPenalty = Math.max(0, (stats.duration - 1000) / 100); // Pénalité si > 1s return Math.max(0, Math.round(baseScore - durationPenalty)); } /** * Générer recommandations basées sur diagnostic */ function generateRecommendations(techniqueResults) { const recommendations = []; techniqueResults.forEach(tech => { if (!tech.success) { recommendations.push({ type: 'error', technique: tech.name, message: `${tech.name} a échoué: ${tech.error}`, action: 'Vérifier configuration et dépendances' }); return; } const effectiveness = tech.effectivenessScore || 0; if (effectiveness < 30) { recommendations.push({ type: 'low_effectiveness', technique: tech.name, message: `${tech.name} peu efficace (score: ${effectiveness})`, action: 'Augmenter intensité ou réviser configuration' }); } else if (effectiveness > 80) { recommendations.push({ type: 'high_effectiveness', technique: tech.name, message: `${tech.name} très efficace (score: ${effectiveness})`, action: 'Configuration optimale' }); } if (tech.duration > 3000) { recommendations.push({ type: 'performance', technique: tech.name, message: `${tech.name} lent (${tech.duration}ms)`, action: 'Considérer réduction intensité ou optimisation' }); } }); return recommendations; } module.exports = { applyPatternBreaking, // ← MAIN ENTRY POINT diagnosticPatternBreaking, // ← Mode diagnostic analyzeContentQuality, performQualityChecks, calculateQualityImpact, calculateEffectivenessScore }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ContentGeneration.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // 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 }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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(); // 1. ANALYSER l'ordre original des éléments const originalOrder = elements ? elements.map(el => el.originalTag) : Object.keys(generatedTexts); 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(); // Vérifier si la sheet existe, sinon la créer let articlesSheet = await getOrCreateSheet(sheets, 'Generated_Articles'); // ===== 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' }; // Préparer la ligne de données 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, '', '', '', '', JSON.stringify({ csvData: csvData, config: config, stats: metadata }) ]; // DEBUG: Vérifier le slug généré logSh(`💾 Sauvegarde avec slug: "${metadata.slug}" (colonne B)`, 'DEBUG'); // Ajouter la ligne aux données await sheets.spreadsheets.values.append({ spreadsheetId: SHEET_CONFIG.sheetId, range: 'Generated_Articles!A:Q', valueInputOption: 'USER_ENTERED', resource: { values: [row] } }); // Récupérer le numéro de ligne pour l'ID article const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_CONFIG.sheetId, range: 'Generated_Articles!A:A' }); 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) { logSh('🗄️ Création sheet Generated_Articles...', 'INFO'); try { // Créer la nouvelle sheet await sheets.spreadsheets.batchUpdate({ spreadsheetId: SHEET_CONFIG.sheetId, resource: { requests: [{ addSheet: { properties: { title: 'Generated_Articles' } } }] } }); // Headers const headers = [ 'Timestamp', 'Slug', 'MC0', 'T0', 'Personality', 'AntiDetection_Level', 'Compiled_Text', // ← COLONNE PRINCIPALE 'Text_Length', 'Word_Count', 'Elements_Count', 'LLM_Used', 'Validation_Status', 'GPTZero_Score', // Scores détecteurs (à remplir) 'Originality_Score', 'CopyLeaks_Score', 'Human_Quality_Score', 'Full_Metadata_JSON' // Backup complet ]; // Ajouter les headers await sheets.spreadsheets.values.update({ spreadsheetId: SHEET_CONFIG.sheetId, range: 'Generated_Articles!A1:Q1', 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, 'Generated_Articles'), 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 Generated_Articles 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') { await createArticlesStorageSheet(sheets); 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/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} - Résultat du workflow */ async function triggerAutonomousWorkflow(rowNumber) { try { logSh('🚀 TRIGGER AUTONOME DÉCLENCHÉ (Digital Ocean)', 'INFO'); // Anti-bouncing simulé await new Promise(resolve => setTimeout(resolve, 2000)); return await runAutonomousWorkflowFromTrigger(rowNumber); } catch (error) { logSh(`❌ Erreur trigger autonome DO: ${error.toString()}`, 'ERROR'); throw error; } } /** * ORCHESTRATEUR: Prépare les données et délègue à Main.js */ async function runAutonomousWorkflowFromTrigger(rowNumber) { const startTime = Date.now(); try { logSh(`🎬 ORCHESTRATION AUTONOME - LIGNE ${rowNumber}`, 'INFO'); // 1. LIRE DONNÉES CSV + XML FILENAME const csvData = await readCSVDataWithXMLFileName(rowNumber); logSh(`✅ CSV: ${csvData.mc0}, XML: ${csvData.xmlFileName}`, 'INFO'); // 2. RÉCUPÉRER XML DEPUIS DIGITAL OCEAN const xmlTemplate = await fetchXMLFromDigitalOceanSimple(csvData.xmlFileName); logSh(`✅ XML récupéré: ${xmlTemplate.length} caractères`, 'INFO'); // 3. 🎯 DÉLÉGUER LE WORKFLOW À MAIN.JS const workflowData = { rowNumber: rowNumber, xmlTemplate: Buffer.from(xmlTemplate).toString('base64'), // Encoder comme Make.com csvData: csvData, source: 'digital_ocean_autonomous' }; const result = await handleFullWorkflow(workflowData); const duration = Date.now() - startTime; logSh(`🏆 ORCHESTRATION TERMINÉE en ${Math.round(duration/1000)}s`, 'INFO'); // 4. MARQUER LIGNE COMME TRAITÉE await markRowAsProcessed(rowNumber, result); return result; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ERREUR ORCHESTRATION: ${error.toString()}`, 'ERROR'); await markRowAsError(rowNumber, error.toString()); throw error; } } // ============= INTERFACE DIGITAL OCEAN ============= async function fetchXMLFromDigitalOceanSimple(fileName) { const filePath = `wp-content/XML/${fileName}`; const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`; try { const response = await axios.get(fileUrl); // Sans auth return response.data; } catch (error) { throw new Error(`Fichier non accessible: ${error.message}`); } } /** * Récupérer XML depuis Digital Ocean Spaces avec authentification */ async function fetchXMLFromDigitalOcean(fileName) { if (!fileName) { throw new Error('Nom de fichier XML requis'); } const filePath = `wp-content/XML/${fileName}`; logSh(`🌊 Récupération XML: ${fileName} , ${filePath}`, 'DEBUG'); const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`; logSh(`🔗 URL complète: ${fileUrl}`, 'DEBUG'); const signature = generateAWSSignature(filePath); try { const response = await axios.get(fileUrl, { headers: signature.headers }); logSh(`📡 Response code: ${response.status}`, 'DEBUG'); logSh(`📄 Response: ${response.data.toString()}`, 'DEBUG'); if (response.status === 200) { return response.data; } else { throw new Error(`HTTP ${response.status}: ${response.data}`); } } catch (error) { logSh(`❌ Erreur DO complète: ${error.toString()}`, 'ERROR'); throw error; } } /** * Lire données CSV avec nom fichier XML (colonne J) */ async function readCSVDataWithXMLFileName(rowNumber) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { // Utiliser variables d'environnement serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { // Utiliser fichier JSON serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; if (!sheet) { throw new Error('Sheet "instructions" non trouvée'); } await sheet.loadCells(`A${rowNumber}:I${rowNumber}`); const slug = sheet.getCell(rowNumber - 1, 0).value; const t0 = sheet.getCell(rowNumber - 1, 1).value; const mc0 = sheet.getCell(rowNumber - 1, 2).value; const tMinus1 = sheet.getCell(rowNumber - 1, 3).value; const lMinus1 = sheet.getCell(rowNumber - 1, 4).value; const mcPlus1 = sheet.getCell(rowNumber - 1, 5).value; const tPlus1 = sheet.getCell(rowNumber - 1, 6).value; const lPlus1 = sheet.getCell(rowNumber - 1, 7).value; const xmlFileName = sheet.getCell(rowNumber - 1, 8).value; if (!xmlFileName || xmlFileName.toString().trim() === '') { throw new Error(`Nom fichier XML manquant colonne I, ligne ${rowNumber}`); } let cleanFileName = xmlFileName.toString().trim(); if (!cleanFileName.endsWith('.xml')) { cleanFileName += '.xml'; } // Récupérer personnalité (délègue au système existant BrainConfig.js) const personalities = await getPersonalities(); // Pas de paramètre, lit depuis JSON const selectedPersonality = await selectPersonalityWithAI(mc0, t0, personalities); return { rowNumber: rowNumber, slug: slug, t0: t0, mc0: mc0, tMinus1: tMinus1, lMinus1: lMinus1, mcPlus1: mcPlus1, tPlus1: tPlus1, lPlus1: lPlus1, xmlFileName: cleanFileName, personality: selectedPersonality }; } catch (error) { logSh(`❌ Erreur lecture CSV: ${error.toString()}`, 'ERROR'); throw error; } } // ============= STATUTS ET VALIDATION ============= /** * Vérifier si le workflow doit être déclenché * En Node.js, cette logique sera adaptée selon votre stratégie (webhook, polling, etc.) */ function shouldTriggerWorkflow(rowNumber, xmlFileName) { if (!rowNumber || rowNumber <= 1) { return false; } if (!xmlFileName || xmlFileName.toString().trim() === '') { logSh('⚠️ Pas de fichier XML (colonne J), workflow ignoré', 'WARNING'); return false; } return true; } async function markRowAsProcessed(rowNumber, result) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; // Vérifier et ajouter headers si nécessaire await sheet.loadCells('K1:N1'); if (!sheet.getCell(0, 10).value) { sheet.getCell(0, 10).value = 'Status'; sheet.getCell(0, 11).value = 'Processed_At'; sheet.getCell(0, 12).value = 'Article_ID'; sheet.getCell(0, 13).value = 'Source'; await sheet.saveUpdatedCells(); } // Marquer la ligne await sheet.loadCells(`K${rowNumber}:N${rowNumber}`); sheet.getCell(rowNumber - 1, 10).value = '✅ DO_SUCCESS'; sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString(); sheet.getCell(rowNumber - 1, 12).value = result.articleStorage?.articleId || ''; sheet.getCell(rowNumber - 1, 13).value = 'Digital Ocean'; await sheet.saveUpdatedCells(); logSh(`✅ Ligne ${rowNumber} marquée comme traitée`, 'INFO'); } catch (error) { logSh(`⚠️ Erreur marquage statut: ${error.toString()}`, 'WARNING'); } } async function markRowAsError(rowNumber, errorMessage) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; await sheet.loadCells(`K${rowNumber}:N${rowNumber}`); sheet.getCell(rowNumber - 1, 10).value = '❌ DO_ERROR'; sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString(); sheet.getCell(rowNumber - 1, 12).value = errorMessage.substring(0, 100); sheet.getCell(rowNumber - 1, 13).value = 'DO Error'; await sheet.saveUpdatedCells(); } catch (error) { logSh(`⚠️ Erreur marquage erreur: ${error.toString()}`, 'WARNING'); } } // ============= SIGNATURE AWS V4 ============= function generateAWSSignature(filePath) { const now = new Date(); const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStamp = now.toISOString().replace(/[-:]/g, '').slice(0, -5) + 'Z'; const headers = { 'Host': DO_CONFIG.endpoint.replace('https://', ''), 'X-Amz-Date': timeStamp, 'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD' }; const credentialScope = `${dateStamp}/${DO_CONFIG.region}/s3/aws4_request`; const canonicalHeaders = Object.keys(headers) .sort() .map(key => `${key.toLowerCase()}:${headers[key]}`) .join('\n'); const signedHeaders = Object.keys(headers) .map(key => key.toLowerCase()) .sort() .join(';'); const canonicalRequest = [ 'GET', `/${filePath}`, '', canonicalHeaders + '\n', signedHeaders, 'UNSIGNED-PAYLOAD' ].join('\n'); const stringToSign = [ 'AWS4-HMAC-SHA256', timeStamp, credentialScope, crypto.createHash('sha256').update(canonicalRequest).digest('hex') ].join('\n'); // Calculs HMAC étape par étape const kDate = crypto.createHmac('sha256', 'AWS4' + DO_CONFIG.secretAccessKey).update(dateStamp).digest(); const kRegion = crypto.createHmac('sha256', kDate).update(DO_CONFIG.region).digest(); const kService = crypto.createHmac('sha256', kRegion).update('s3').digest(); const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex'); headers['Authorization'] = `AWS4-HMAC-SHA256 Credential=${DO_CONFIG.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; return { headers: headers }; } // ============= SETUP ET TEST ============= /** * Configuration du trigger autonome - Remplacé par webhook ou polling en Node.js */ function setupAutonomousTrigger() { logSh('⚙️ Configuration trigger autonome Digital Ocean...', 'INFO'); // En Node.js, vous pourriez utiliser: // - Express.js avec webhooks // - Cron jobs avec node-cron // - Polling de la Google Sheet // - WebSocket connections logSh('✅ Configuration prête pour webhooks/polling Node.js', 'INFO'); logSh('🎯 Mode: Webhook/API → Digital Ocean → Main.js', 'INFO'); } async function testDigitalOceanConnection() { logSh('🧪 Test connexion Digital Ocean...', 'INFO'); try { const testFiles = ['template1.xml', 'plaque-rue.xml', 'test.xml']; for (const fileName of testFiles) { try { const content = await fetchXMLFromDigitalOceanSimple(fileName); logSh(`✅ Fichier '${fileName}' accessible (${content.length} chars)`, 'INFO'); return true; } catch (error) { logSh(`⚠️ '${fileName}' non accessible: ${error.toString()}`, 'DEBUG'); } } logSh('❌ Aucun fichier test accessible dans DO', 'ERROR'); return false; } catch (error) { logSh(`❌ Test DO échoué: ${error.toString()}`, 'ERROR'); return false; } } // ============= EXPORTS ============= module.exports = { triggerAutonomousWorkflow, runAutonomousWorkflowFromTrigger, fetchXMLFromDigitalOcean, fetchXMLFromDigitalOceanSimple, readCSVDataWithXMLFileName, markRowAsProcessed, markRowAsError, testDigitalOceanConnection, setupAutonomousTrigger, DO_CONFIG }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/Main.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: lib/main.js - CONVERTI POUR NODE.JS // RESPONSABILITÉ: COEUR DU WORKFLOW DE GÉNÉRATION // ======================================== // 🔧 CONFIGURATION ENVIRONNEMENT require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); // 🔄 IMPORTS NODE.JS (remplace les dépendances Apps Script) const { getBrainConfig } = require('./BrainConfig'); const { extractElements, buildSmartHierarchy } = require('./ElementExtraction'); const { generateMissingKeywords } = require('./MissingKeywords'); const { generateWithContext } = require('./ContentGeneration'); const { injectGeneratedContent, cleanStrongTags } = require('./ContentAssembly'); const { validateWorkflowIntegrity, logSh } = require('./ErrorReporting'); const { saveGeneratedArticleOrganic } = require('./ArticleStorage'); const { tracer } = require('./trace.js'); const { fetchXMLFromDigitalOcean } = require('./DigitalOceanWorkflow'); const { spawn } = require('child_process'); const path = require('path'); // Variable pour éviter de relancer Edge plusieurs fois let logViewerLaunched = false; /** * Lancer le log viewer dans Edge */ function launchLogViewer() { if (logViewerLaunched || process.env.NODE_ENV === 'test') return; try { const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html'); const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`; // Détecter l'environnement et adapter la commande const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP; const isWindows = process.platform === 'win32'; if (isWindows && !isWSL) { // Windows natif const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], { detached: true, stdio: 'ignore' }); edgeProcess.unref(); } else if (isWSL) { // WSL - utiliser cmd.exe via /mnt/c/Windows/System32/ const edgeProcess = spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'msedge', fileUrl], { detached: true, stdio: 'ignore' }); edgeProcess.unref(); } else { // Linux/Mac - essayer xdg-open ou open const command = process.platform === 'darwin' ? 'open' : 'xdg-open'; const browserProcess = spawn(command, [fileUrl], { detached: true, stdio: 'ignore' }); browserProcess.unref(); } logViewerLaunched = true; logSh('🌐 Log viewer lancé', 'INFO'); } catch (error) { logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING'); } } /** * COEUR DU WORKFLOW - Compatible Make.com ET Digital Ocean ET Node.js * @param {object} data - Données du workflow * @param {string} data.xmlTemplate - XML template (base64 encodé) * @param {object} data.csvData - Données CSV ou rowNumber * @param {string} data.source - 'make_com' | 'digital_ocean_autonomous' | 'node_server' */ async function handleFullWorkflow(data) { // Lancer le log viewer au début du workflow launchLogViewer(); return await tracer.run('Main.handleFullWorkflow()', async () => { await tracer.annotate({ source: data.source || 'node_server', mc0: data.csvData?.mc0 || data.rowNumber }); // 1. PRÉPARER LES DONNÉES CSV const csvData = await tracer.run('Main.prepareCSVData()', async () => { const result = await prepareCSVData(data); await tracer.event(`CSV préparé: ${result.mc0}`, { csvKeys: Object.keys(result) }); return result; }, { rowNumber: data.rowNumber, source: data.source }); // 2. DÉCODER LE XML TEMPLATE const xmlString = await tracer.run('Main.decodeXMLTemplate()', async () => { const result = decodeXMLTemplate(data.xmlTemplate); await tracer.event(`XML décodé: ${result.length} caractères`); return result; }, { templateLength: data.xmlTemplate?.length }); // 3. PREPROCESSING XML const processedXML = await tracer.run('Main.preprocessXML()', async () => { const result = preprocessXML(xmlString); await tracer.event('XML préprocessé'); global.currentXmlTemplate = result; return result; }, { originalLength: xmlString?.length }); // 4. EXTRAIRE ÉLÉMENTS const elements = await tracer.run('ElementExtraction.extractElements()', async () => { const result = await extractElements(processedXML, csvData); await tracer.event(`${result.length} éléments extraits`); return result; }, { xmlLength: processedXML?.length, mc0: csvData.mc0 }); // 5. GÉNÉRER MOTS-CLÉS MANQUANTS const finalElements = await tracer.run('MissingKeywords.generateMissingKeywords()', async () => { const updatedElements = await generateMissingKeywords(elements, csvData); const result = Object.keys(updatedElements).length > 0 ? updatedElements : elements; await tracer.event('Mots-clés manquants traités'); return result; }, { elementsCount: elements.length, mc0: csvData.mc0 }); // 6. CONSTRUIRE HIÉRARCHIE INTELLIGENTE const hierarchy = await tracer.run('ElementExtraction.buildSmartHierarchy()', async () => { const result = await buildSmartHierarchy(finalElements); await tracer.event(`Hiérarchie construite: ${Object.keys(result).length} sections`); return result; }, { finalElementsCount: finalElements.length }); // 7. 🎯 GÉNÉRATION AVEC SELECTIVE ENHANCEMENT (Phase 2) const generatedContent = await tracer.run('ContentGeneration.generateWithContext()', async () => { const result = await generateWithContext(hierarchy, csvData); await tracer.event(`Contenu généré: ${Object.keys(result).length} éléments`); return result; }, { elementsCount: Object.keys(hierarchy).length, personality: csvData.personality?.nom }); // 8. ASSEMBLER XML FINAL const finalXML = await tracer.run('ContentAssembly.injectGeneratedContent()', async () => { const result = injectGeneratedContent(processedXML, generatedContent, finalElements); await tracer.event('XML final assemblé'); return result; }, { contentPieces: Object.keys(generatedContent).length, elementsCount: finalElements.length }); // 9. VALIDATION INTÉGRITÉ const validationReport = await tracer.run('ErrorReporting.validateWorkflowIntegrity()', async () => { const result = validateWorkflowIntegrity(finalElements, generatedContent, finalXML, csvData); await tracer.event(`Validation: ${result.status}`); return result; }, { finalXMLLength: finalXML?.length, contentKeys: Object.keys(generatedContent).length }); // 10. SAUVEGARDE ARTICLE const articleStorage = await tracer.run('Main.saveArticle()', async () => { const result = await saveArticle(finalXML, generatedContent, finalElements, csvData, data.source); if (result) { await tracer.event(`Article sauvé: ID ${result.articleId}`); } return result; }, { source: data.source, mc0: csvData.mc0, elementsCount: finalElements.length }); // 11. RÉPONSE FINALE const response = await tracer.run('Main.buildWorkflowResponse()', async () => { const result = await buildWorkflowResponse(finalXML, generatedContent, finalElements, csvData, validationReport, articleStorage, data.source); await tracer.event(`Response keys: ${Object.keys(result).join(', ')}`); return result; }, { validationStatus: validationReport?.status, articleId: articleStorage?.articleId }); return response; }, { source: data.source || 'node_server', rowNumber: data.rowNumber, hasXMLTemplate: !!data.xmlTemplate }); } // ============= PRÉPARATION DONNÉES ============= /** * Préparer les données CSV selon la source - ASYNC pour Node.js * RÉCUPÈRE: Google Sheets (données CSV) + Digital Ocean (XML template) */ async function prepareCSVData(data) { if (data.csvData && data.csvData.mc0) { // Données déjà préparées (Digital Ocean ou direct) return data.csvData; } else if (data.rowNumber) { // 1. RÉCUPÉRER DONNÉES CSV depuis Google Sheet (OBLIGATOIRE) await logSh(`🧠 Récupération données CSV ligne ${data.rowNumber}...`, 'INFO'); const config = await getBrainConfig(data.rowNumber); if (!config.success) { await logSh('❌ ÉCHEC: Impossible de récupérer les données Google Sheets', 'ERROR'); throw new Error('FATAL: Google Sheets inaccessible - arrêt du workflow'); } // 2. VÉRIFIER XML FILENAME depuis Google Sheet (colonne I) const xmlFileName = config.data.xmlFileName; if (!xmlFileName || xmlFileName.trim() === '') { await logSh('❌ ÉCHEC: Nom fichier XML manquant (colonne I Google Sheets)', 'ERROR'); throw new Error('FATAL: XML filename manquant - arrêt du workflow'); } await logSh(`📋 CSV récupéré: ${config.data.mc0}`, 'INFO'); await logSh(`📄 XML filename: ${xmlFileName}`, 'INFO'); // 3. RÉCUPÉRER XML CONTENT depuis Digital Ocean avec AUTH (OBLIGATOIRE) await logSh(`🌊 Récupération XML template depuis Digital Ocean (avec signature AWS)...`, 'INFO'); let xmlContent; try { xmlContent = await fetchXMLFromDigitalOcean(xmlFileName); await logSh(`✅ XML récupéré: ${xmlContent.length} caractères`, 'INFO'); } catch (digitalOceanError) { await logSh(`❌ ÉCHEC: Digital Ocean inaccessible - ${digitalOceanError.message}`, 'ERROR'); throw new Error(`FATAL: Digital Ocean échec - arrêt du workflow: ${digitalOceanError.message}`); } // 4. ENCODER XML pour le workflow (comme Make.com) // Si on a récupéré un fichier XML, l'utiliser. Sinon utiliser le template par défaut déjà dans config.data.xmlTemplate if (xmlContent) { data.xmlTemplate = Buffer.from(xmlContent).toString('base64'); await logSh('🔄 XML depuis Digital Ocean encodé base64 pour le workflow', 'DEBUG'); } else if (config.data.xmlTemplate) { data.xmlTemplate = Buffer.from(config.data.xmlTemplate).toString('base64'); await logSh('🔄 XML template par défaut encodé base64 pour le workflow', 'DEBUG'); } return config.data; } else { throw new Error('FATAL: Données CSV invalides - rowNumber requis'); } } /** * Décoder le XML template - NODE.JS VERSION */ function decodeXMLTemplate(xmlTemplate) { if (!xmlTemplate) { throw new Error('Template XML manquant'); } // Si le template commence déjà par processed = cleanStrongTags(processed); // Autres nettoyages futurs... return processed; } // ============= SAUVEGARDE ============= /** * Sauvegarder l'article avec métadonnées source - ASYNC pour Node.js */ async function saveArticle(finalXML, generatedContent, finalElements, csvData, source) { await logSh('💾 Sauvegarde article...', 'INFO'); const articleData = { xmlContent: finalXML, generatedTexts: generatedContent, elementsGenerated: finalElements.length, originalElements: finalElements }; const storageConfig = { antiDetectionLevel: 'Selective_Enhancement', llmUsed: 'claude+openai+gemini+mistral', workflowVersion: '2.0-NodeJS', // 🔄 Mise à jour version source: source || 'node_server', // 🔄 Source par défaut enhancementTechniques: [ 'technical_terms_gpt4', 'transitions_gemini', 'personality_style_mistral' ] }; try { const articleStorage = await saveGeneratedArticleOrganic(articleData, csvData, storageConfig); await logSh(`✅ Article sauvé: ID ${articleStorage.articleId}`, 'INFO'); return articleStorage; } catch (storageError) { await logSh(`⚠️ Erreur sauvegarde: ${storageError.toString()}`, 'WARNING'); return null; // Non-bloquant } } // ============= RÉPONSE ============= /** * Construire la réponse finale du workflow - ASYNC pour logSh */ async function buildWorkflowResponse(finalXML, generatedContent, finalElements, csvData, validationReport, articleStorage, source) { const response = { success: true, source: source, xmlContent: finalXML, generatedTexts: generatedContent, elementsGenerated: finalElements.length, personality: csvData.personality?.nom || 'Unknown', csvData: { mc0: csvData.mc0, t0: csvData.t0, personality: csvData.personality?.nom }, timestamp: new Date().toISOString(), validationReport: validationReport, articleStorage: articleStorage, // NOUVELLES MÉTADONNÉES PHASE 2 antiDetectionLevel: 'Selective_Enhancement', llmsUsed: ['claude', 'openai', 'gemini', 'mistral'], enhancementApplied: true, workflowVersion: '2.0-NodeJS', // 🔄 Version mise à jour // STATS PERFORMANCE stats: { xmlLength: finalXML.length, contentPieces: Object.keys(generatedContent).length, wordCount: calculateTotalWordCount(generatedContent), validationStatus: validationReport.status } }; await logSh(`🔍 Response.stats: ${JSON.stringify(response.stats)}`, 'DEBUG'); return response; } // ============= HELPERS ============= /** * Calculer nombre total de mots - IDENTIQUE */ function calculateTotalWordCount(generatedContent) { let totalWords = 0; Object.values(generatedContent).forEach(content => { if (content && typeof content === 'string') { totalWords += content.trim().split(/\s+/).length; } }); return totalWords; } // ============= POINTS D'ENTRÉE SUPPLÉMENTAIRES ============= /** * Test du workflow principal - ASYNC pour Node.js */ async function testMainWorkflow() { try { const testData = { csvData: { mc0: 'plaque test nodejs', t0: 'Test workflow principal Node.js', personality: { nom: 'Marc', style: 'professionnel' }, tMinus1: 'parent test', mcPlus1: 'mot1,mot2,mot3,mot4', tPlus1: 'Titre1,Titre2,Titre3,Titre4' }, xmlTemplate: Buffer.from('|Test_Element{{T0}}|').toString('base64'), source: 'test_main_nodejs' }; const result = await handleFullWorkflow(testData); return result; } catch (error) { throw error; } finally { tracer.printSummary(); } } // 🔄 NODE.JS EXPORTS module.exports = { handleFullWorkflow, testMainWorkflow, prepareCSVData, decodeXMLTemplate, preprocessXML, saveArticle, buildWorkflowResponse, calculateTotalWordCount, launchLogViewer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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/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 { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - APPLICATION COUCHE ADVERSARIALE * Input: contenu existant + configuration adversariale * Output: contenu avec couche adversariale appliquée */ async function applyAdversarialLayer(existingContent, config = {}) { return await tracer.run('AdversarialCore.applyAdversarialLayer()', async () => { const { detectorTarget = 'general', intensity = 1.0, method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid' preserveStructure = true, csvData = null, context = {} } = config; await tracer.annotate({ adversarialLayer: true, detectorTarget, intensity, method, elementsCount: Object.keys(existingContent).length }); const startTime = Date.now(); logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO'); logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO'); try { // Initialiser stratégie détecteur const detectorManager = new DetectorStrategyManager(detectorTarget); const strategy = detectorManager.getStrategy(); // Appliquer méthode adversariale choisie let adversarialContent = {}; switch (method) { case 'regeneration': adversarialContent = await applyRegenerationMethod(existingContent, config, strategy); break; case 'enhancement': adversarialContent = await applyEnhancementMethod(existingContent, config, strategy); break; case 'hybrid': adversarialContent = await applyHybridMethod(existingContent, config, strategy); break; default: throw new Error(`Méthode adversariale inconnue: ${method}`); } const duration = Date.now() - startTime; const stats = { elementsProcessed: Object.keys(existingContent).length, elementsModified: countModifiedElements(existingContent, adversarialContent), detectorTarget, intensity, method, duration }; logSh(`✅ COUCHE ADVERSARIALE APPLIQUÉE: ${stats.elementsModified}/${stats.elementsProcessed} modifiés (${duration}ms)`, 'INFO'); await tracer.event('Couche adversariale appliquée', stats); return { content: adversarialContent, stats, original: existingContent, config }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ COUCHE ADVERSARIALE ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); return { content: existingContent, stats: { fallback: true, duration }, original: existingContent, config, error: error.message }; } }, { existingContent: Object.keys(existingContent), config }); } /** * MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux */ async function applyRegenerationMethod(existingContent, config, strategy) { logSh(`🔄 Méthode régénération adversariale`, 'DEBUG'); const results = {}; const contentEntries = Object.entries(existingContent); // Traiter en chunks pour éviter timeouts const chunks = chunkArray(contentEntries, 4); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy); const response = await callLLM('claude', regenerationPrompt, { temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité maxTokens: 2000 * chunk.length }, config.csvData?.personality); const chunkResults = parseRegenerationResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments régénérés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: garder contenu original pour ce chunk chunk.forEach(([tag, content]) => { results[tag] = content; }); } } return results; } /** * MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement */ async function applyEnhancementMethod(existingContent, config, strategy) { logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG'); const results = { ...existingContent }; // Base: contenu original const elementsToEnhance = selectElementsForEnhancement(existingContent, config); if (elementsToEnhance.length === 0) { logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG'); return results; } logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG'); const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy); try { const response = await callLLM('gpt4', enhancementPrompt, { temperature: 0.5 + (config.intensity * 0.3), maxTokens: 3000 }, config.csvData?.personality); const enhancedResults = parseEnhancementResponse(response, elementsToEnhance); // Appliquer améliorations Object.keys(enhancedResults).forEach(tag => { if (enhancedResults[tag] !== existingContent[tag]) { results[tag] = enhancedResults[tag]; } }); return results; } catch (error) { logSh(`❌ Enhancement échoué: ${error.message}`, 'ERROR'); return results; // Fallback: contenu original } } /** * MÉTHODE HYBRIDE - Combinaison régénération + enhancement */ async function applyHybridMethod(existingContent, config, strategy) { logSh(`⚡ Méthode hybride adversariale`, 'DEBUG'); // 1. Enhancement léger sur tout le contenu const enhancedContent = await applyEnhancementMethod(existingContent, { ...config, intensity: config.intensity * 0.6 // Intensité réduite pour enhancement }, strategy); // 2. Régénération ciblée sur éléments clés const keyElements = selectKeyElementsForRegeneration(enhancedContent, config); if (keyElements.length === 0) { return enhancedContent; } const keyElementsContent = {}; keyElements.forEach(tag => { keyElementsContent[tag] = enhancedContent[tag]; }); const regeneratedElements = await applyRegenerationMethod(keyElementsContent, { ...config, intensity: config.intensity * 1.2 // Intensité augmentée pour régénération }, strategy); // 3. Merger résultats const hybridContent = { ...enhancedContent }; Object.keys(regeneratedElements).forEach(tag => { hybridContent[tag] = regeneratedElements[tag]; }); return hybridContent; } // ============= HELPER FUNCTIONS ============= /** * Créer prompt de régénération adversariale */ function createRegenerationPrompt(chunk, config, strategy) { const { detectorTarget, intensity, csvData } = config; let prompt = `MISSION: Réécris ces contenus pour éviter détection par ${detectorTarget}. TECHNIQUE ANTI-${detectorTarget.toUpperCase()}: ${strategy.getInstructions(intensity).join('\n')} CONTENUS À RÉÉCRIRE: ${chunk.map(([tag, content], i) => `[${i + 1}] TAG: ${tag} ORIGINAL: "${content}"`).join('\n\n')} CONSIGNES: - GARDE exactement le même message et informations factuelles - CHANGE structure, vocabulaire, style pour éviter détection ${detectorTarget} - Intensité adversariale: ${intensity.toFixed(2)} ${csvData?.personality ? `- Style: ${csvData.personality.nom} (${csvData.personality.style})` : ''} IMPORTANT: Réponse DIRECTE par les contenus réécrits, pas d'explication. FORMAT: [1] Contenu réécrit anti-${detectorTarget} [2] Contenu réécrit anti-${detectorTarget} etc...`; return prompt; } /** * Créer prompt d'enhancement adversarial */ function createEnhancementPrompt(elementsToEnhance, config, strategy) { const { detectorTarget, intensity } = config; let prompt = `MISSION: Améliore subtilement ces contenus pour réduire détection ${detectorTarget}. AMÉLIORATIONS CIBLÉES: ${strategy.getEnhancementTips(intensity).join('\n')} ÉLÉMENTS À AMÉLIORER: ${elementsToEnhance.map((element, i) => `[${i + 1}] TAG: ${element.tag} CONTENU: "${element.content}" PROBLÈME: ${element.detectionRisk}`).join('\n\n')} CONSIGNES: - Modifications LÉGÈRES et naturelles - GARDE le fond du message intact - Focus sur réduction détection ${detectorTarget} - Intensité: ${intensity.toFixed(2)} FORMAT: [1] Contenu légèrement amélioré [2] Contenu légèrement amélioré etc...`; return prompt; } /** * Parser réponse régénération */ function parseRegenerationResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; const content = cleanAdversarialContent(match[2].trim()); if (index >= 0 && index < chunk.length) { parsedItems[index] = content; } } // Mapper aux vrais tags chunk.forEach(([tag, originalContent], index) => { if (parsedItems[index] && parsedItems[index].length > 10) { results[tag] = parsedItems[index]; } else { results[tag] = originalContent; // Fallback logSh(`⚠️ Fallback régénération pour [${tag}]`, 'WARNING'); } }); return results; } /** * Parser réponse enhancement */ function parseEnhancementResponse(response, elementsToEnhance) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < elementsToEnhance.length) { let enhancedContent = cleanAdversarialContent(match[2].trim()); const element = elementsToEnhance[index]; if (enhancedContent && enhancedContent.length > 10) { results[element.tag] = enhancedContent; } else { results[element.tag] = element.content; // Fallback } index++; } return results; } /** * Sélectionner éléments pour enhancement */ function selectElementsForEnhancement(existingContent, config) { const elements = []; Object.entries(existingContent).forEach(([tag, content]) => { const detectionRisk = assessDetectionRisk(content, config.detectorTarget); if (detectionRisk.score > 0.6) { // Risque élevé elements.push({ tag, content, detectionRisk: detectionRisk.reasons.join(', '), priority: detectionRisk.score }); } }); // Trier par priorité (risque élevé en premier) elements.sort((a, b) => b.priority - a.priority); return elements; } /** * Sélectionner éléments clés pour régénération (hybride) */ function selectKeyElementsForRegeneration(content, config) { const keyTags = []; Object.keys(content).forEach(tag => { // Éléments clés: titres, intro, premiers paragraphes if (tag.includes('Titre') || tag.includes('H1') || tag.includes('intro') || tag.includes('Introduction') || tag.includes('1')) { keyTags.push(tag); } }); return keyTags.slice(0, 3); // Maximum 3 éléments clés } /** * Évaluer risque de détection */ function assessDetectionRisk(content, detectorTarget) { let score = 0; const reasons = []; // Indicateurs génériques de contenu IA const aiWords = ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge']; const aiCount = aiWords.reduce((count, word) => { return count + (content.toLowerCase().includes(word) ? 1 : 0); }, 0); if (aiCount > 2) { score += 0.4; reasons.push('mots_typiques_ia'); } // Structure trop parfaite const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); if (sentences.length > 2) { const avgLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length; const variance = sentences.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / sentences.length; const uniformity = 1 - (Math.sqrt(variance) / avgLength); if (uniformity > 0.8) { score += 0.3; reasons.push('structure_uniforme'); } } // Spécifique selon détecteur if (detectorTarget === 'gptZero') { // GPTZero détecte la prévisibilité if (content.includes('par ailleurs') && content.includes('en effet')) { score += 0.3; reasons.push('connecteurs_prévisibles'); } } return { score: Math.min(1, score), reasons }; } /** * Nettoyer contenu adversarial généré */ function cleanAdversarialContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré)[:\s]*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } /** * Compter éléments modifiés */ function countModifiedElements(original, modified) { let count = 0; Object.keys(original).forEach(tag => { if (modified[tag] && modified[tag] !== original[tag]) { count++; } }); return count; } /** * Chunk array utility */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Sleep utility */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } module.exports = { applyAdversarialLayer, // ← MAIN ENTRY POINT MODULAIRE applyRegenerationMethod, applyEnhancementMethod, applyHybridMethod, assessDetectionRisk, selectElementsForEnhancement }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialPromptEngine.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ADVERSARIAL PROMPT ENGINE - NIVEAU 3 // Responsabilité: Générer instructions anti-détection pour prompts // Anti-détection: Prévention par modification des prompts LLM // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * INSTRUCTIONS ADVERSARIALES PAR DÉTECTEUR * Stratégies spécialisées pour contrer chaque détecteur IA */ const ADVERSARIAL_INSTRUCTIONS = { general: { title: 'CONTRAINTES ANTI-DÉTECTION GÉNÉRALES', rules: [ 'Évite ces mots typiques IA : "optimal", "comprehensive", "seamless", "robust", "leverage"', 'Varie longueurs phrases : mélange phrases courtes (5-10 mots) ET longues (20-30 mots)', 'Utilise expressions françaises familières et tournures idiomatiques', 'Ajoute imperfections naturelles : répétitions légères, hésitations, reformulations', 'Change angle ou perspective 1-2 fois dans le texte', 'Préfère synonymes moins évidents : "bon" plutôt qu\'"excellent", "solide" plutôt qu\'optimisé"', 'Intègre connecteurs variés et naturels selon le contexte' ], intensity: 1.0, weight: 0.8 }, gptZero: { title: 'SPÉCIAL ANTI-GPTZERO', rules: [ 'Surprends avec tournures inattendues et constructions atypiques', 'Évite phrases trop prévisibles ou patterns répétitifs', 'Utilise synonymes moins évidents et vocabulaire moins commun', 'Varie drastiquement la complexité syntaxique entre phrases', 'Intercale observations personnelles ou détours narratifs', 'Alterne registres de langue (soutenu → familier → technique)', 'Brise la logique linéaire avec des parenthèses, incises, apartés' ], intensity: 1.2, weight: 0.9, targetMetric: 'unpredictability' }, originality: { title: 'SPÉCIAL ANTI-ORIGINALITY', rules: [ 'Vocabulaire TRÈS varié : évite répétitions même de synonymes', 'Structures phrases délibérément irrégulières et asymétriques', 'Changements angles fréquents : technique → personnel → général', 'Créativité sémantique : métaphores, comparaisons inattendues', 'Évite formulations académiques ou trop structurées', 'Intègre références culturelles, expressions régionales', 'Subvertis les attentes : commence par la fin, questionne l\'évidence' ], intensity: 1.3, weight: 0.95, targetMetric: 'semantic_diversity' }, copyLeaks: { title: 'SPÉCIAL ANTI-COPYLEAKS', rules: [ 'Reformule idées communes avec angles totalement originaux', 'Évite phrases-types et formulations standard du secteur', 'Personnalise chaque assertion avec exemples spécifiques', 'Réinvente la façon de présenter informations basiques', 'Utilise analogies et métaphores plutôt que descriptions directes', 'Fragmente informations techniques en observations pratiques', 'Transforme données factuelles en récits ou témoignages' ], intensity: 1.1, weight: 0.85, targetMetric: 'originality_score' }, winston: { title: 'SPÉCIAL ANTI-WINSTON', rules: [ 'Évite cohérence stylistique trop parfaite entre paragraphes', 'Simule variation naturelle d\'humeur et d\'énergie rédactionnelle', 'Intègre "erreurs" humaines : répétitions, corrections, précisions', 'Varie niveau de détail : parfois précis, parfois elliptique', 'Alterne registres émotionnels : enthousiaste → neutre → critique', 'Inclus hésitations et nuances : "peut-être", "généralement", "souvent"', 'Personnalise avec opinions subjectives et préférences' ], intensity: 1.0, weight: 0.9, targetMetric: 'human_variation' } }; /** * INSTRUCTIONS PERSONNALISÉES PAR TYPE D'ÉLÉMENT */ const ELEMENT_SPECIFIC_INSTRUCTIONS = { titre_h1: { base: 'Crée un titre percutant mais naturel', adversarial: 'Évite formules marketing lisses, préfère authentique et direct' }, titre_h2: { base: 'Génère un sous-titre informatif', adversarial: 'Varie structure : question, affirmation, exclamation selon contexte' }, intro: { base: 'Rédige introduction engageante', adversarial: 'Commence par angle inattendu : anecdote, constat, question rhétorique' }, texte: { base: 'Développe paragraphe informatif', adversarial: 'Mélange informations factuelles et observations personnelles' }, faq_question: { base: 'Formule question client naturelle', adversarial: 'Utilise formulations vraiment utilisées par clients, pas académiques' }, faq_reponse: { base: 'Réponds de façon experte et rassurante', adversarial: 'Ajoute nuances, "ça dépend", précisions contextuelles comme humain' } }; /** * MAIN ENTRY POINT - GÉNÉRATEUR DE PROMPTS ADVERSARIAUX * @param {string} basePrompt - Prompt de base * @param {Object} config - Configuration adversariale * @returns {string} - Prompt enrichi d'instructions anti-détection */ function createAdversarialPrompt(basePrompt, config = {}) { return tracer.run('AdversarialPromptEngine.createAdversarialPrompt()', () => { const { detectorTarget = 'general', intensity = 1.0, elementType = 'generic', personality = null, contextualMode = true, csvData = null, debugMode = false } = config; tracer.annotate({ detectorTarget, intensity, elementType, personalityStyle: personality?.style }); try { // 1. Sélectionner stratégie détecteur const strategy = ADVERSARIAL_INSTRUCTIONS[detectorTarget] || ADVERSARIAL_INSTRUCTIONS.general; // 2. Adapter intensité const effectiveIntensity = intensity * (strategy.intensity || 1.0); const shouldApplyStrategy = Math.random() < (strategy.weight || 0.8); if (!shouldApplyStrategy && detectorTarget !== 'general') { // Fallback sur stratégie générale return createAdversarialPrompt(basePrompt, { ...config, detectorTarget: 'general' }); } // 3. Construire instructions adversariales const adversarialSection = buildAdversarialInstructions(strategy, { elementType, personality, effectiveIntensity, contextualMode, csvData }); // 4. Assembler prompt final const enhancedPrompt = assembleEnhancedPrompt(basePrompt, adversarialSection, { strategy, elementType, debugMode }); if (debugMode) { logSh(`🎯 Prompt adversarial généré: ${detectorTarget} (intensité: ${effectiveIntensity.toFixed(2)})`, 'DEBUG'); logSh(` Instructions: ${strategy.rules.length} règles appliquées`, 'DEBUG'); } tracer.event('Prompt adversarial créé', { detectorTarget, rulesCount: strategy.rules.length, promptLength: enhancedPrompt.length }); return enhancedPrompt; } catch (error) { logSh(`❌ Erreur génération prompt adversarial: ${error.message}`, 'ERROR'); // Fallback: retourner prompt original return basePrompt; } }, config); } /** * Construire section instructions adversariales */ function buildAdversarialInstructions(strategy, config) { const { elementType, personality, effectiveIntensity, contextualMode, csvData } = config; let instructions = `\n\n=== ${strategy.title} ===\n`; // Règles de base de la stratégie const activeRules = selectActiveRules(strategy.rules, effectiveIntensity); activeRules.forEach(rule => { instructions += `• ${rule}\n`; }); // Instructions spécifiques au type d'élément if (ELEMENT_SPECIFIC_INSTRUCTIONS[elementType]) { const elementInstructions = ELEMENT_SPECIFIC_INSTRUCTIONS[elementType]; instructions += `\nSPÉCIFIQUE ${elementType.toUpperCase()}:\n`; instructions += `• ${elementInstructions.adversarial}\n`; } // Adaptations personnalité if (personality && contextualMode) { const personalityAdaptations = generatePersonalityAdaptations(personality, strategy); if (personalityAdaptations) { instructions += `\nADAPTATION PERSONNALITÉ ${personality.nom.toUpperCase()}:\n`; instructions += personalityAdaptations; } } // Contexte métier si disponible if (csvData && contextualMode) { const contextualInstructions = generateContextualInstructions(csvData, strategy); if (contextualInstructions) { instructions += `\nCONTEXTE MÉTIER:\n`; instructions += contextualInstructions; } } instructions += `\nIMPORTANT: Ces contraintes doivent sembler naturelles, pas forcées.\n`; return instructions; } /** * Sélectionner règles actives selon intensité */ function selectActiveRules(allRules, intensity) { if (intensity >= 1.0) { return allRules; // Toutes les règles } // Sélection proportionnelle à l'intensité const ruleCount = Math.ceil(allRules.length * intensity); return allRules.slice(0, ruleCount); } /** * Générer adaptations personnalité */ function generatePersonalityAdaptations(personality, strategy) { if (!personality) return null; const adaptations = []; // Style de la personnalité if (personality.style) { adaptations.push(`• Respecte le style ${personality.style} de ${personality.nom} tout en appliquant les contraintes`); } // Vocabulaire préféré if (personality.vocabulairePref) { adaptations.push(`• Intègre vocabulaire naturel: ${personality.vocabulairePref}`); } // Connecteurs préférés if (personality.connecteursPref) { adaptations.push(`• Utilise connecteurs variés: ${personality.connecteursPref}`); } // Longueur phrases selon personnalité if (personality.longueurPhrases) { adaptations.push(`• Longueur phrases: ${personality.longueurPhrases} mais avec variation anti-détection`); } return adaptations.length > 0 ? adaptations.join('\n') + '\n' : null; } /** * Générer instructions contextuelles métier */ function generateContextualInstructions(csvData, strategy) { if (!csvData.mc0) return null; const instructions = []; // Contexte sujet instructions.push(`• Sujet: ${csvData.mc0} - utilise terminologie naturelle du domaine`); // Éviter jargon selon détecteur if (strategy.targetMetric === 'unpredictability') { instructions.push(`• Évite jargon technique trop prévisible, privilégie explications accessibles`); } else if (strategy.targetMetric === 'semantic_diversity') { instructions.push(`• Varie façons de nommer/décrire ${csvData.mc0} - synonymes créatifs`); } return instructions.join('\n') + '\n'; } /** * Assembler prompt final */ function assembleEnhancedPrompt(basePrompt, adversarialSection, config) { const { strategy, elementType, debugMode } = config; // Structure du prompt amélioré let enhancedPrompt = basePrompt; // Injecter instructions adversariales enhancedPrompt += adversarialSection; // Rappel final selon stratégie if (strategy.targetMetric) { enhancedPrompt += `\nOBJECTIF PRIORITAIRE: Maximiser ${strategy.targetMetric} tout en conservant qualité.\n`; } // Instructions de réponse enhancedPrompt += `\nRÉPONDS DIRECTEMENT par le contenu demandé, en appliquant naturellement ces contraintes.`; return enhancedPrompt; } /** * Analyser efficacité d'un prompt adversarial */ function analyzePromptEffectiveness(originalPrompt, adversarialPrompt, generatedContent) { const analysis = { promptEnhancement: { originalLength: originalPrompt.length, adversarialLength: adversarialPrompt.length, enhancementRatio: adversarialPrompt.length / originalPrompt.length, instructionsAdded: (adversarialPrompt.match(/•/g) || []).length }, contentMetrics: analyzeGeneratedContent(generatedContent), effectiveness: 0 }; // Score d'efficacité simple analysis.effectiveness = Math.min(100, (analysis.promptEnhancement.enhancementRatio - 1) * 50 + analysis.contentMetrics.diversityScore ); return analysis; } /** * Analyser contenu généré */ function analyzeGeneratedContent(content) { if (!content || typeof content !== 'string') { return { diversityScore: 0, wordCount: 0, sentenceVariation: 0 }; } const words = content.split(/\s+/).filter(w => w.length > 2); const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5); // Diversité vocabulaire const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))]; const diversityScore = uniqueWords.length / Math.max(1, words.length) * 100; // Variation longueurs phrases const sentenceLengths = sentences.map(s => s.split(/\s+/).length); const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / Math.max(1, sentenceLengths.length); const variance = sentenceLengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / Math.max(1, sentenceLengths.length); const sentenceVariation = Math.sqrt(variance) / Math.max(1, avgLength) * 100; return { diversityScore: Math.round(diversityScore), wordCount: words.length, sentenceCount: sentences.length, sentenceVariation: Math.round(sentenceVariation), avgSentenceLength: Math.round(avgLength) }; } /** * Obtenir liste des détecteurs supportés */ function getSupportedDetectors() { return Object.keys(ADVERSARIAL_INSTRUCTIONS).map(key => ({ id: key, name: ADVERSARIAL_INSTRUCTIONS[key].title, intensity: ADVERSARIAL_INSTRUCTIONS[key].intensity, weight: ADVERSARIAL_INSTRUCTIONS[key].weight, rulesCount: ADVERSARIAL_INSTRUCTIONS[key].rules.length, targetMetric: ADVERSARIAL_INSTRUCTIONS[key].targetMetric || 'general' })); } module.exports = { createAdversarialPrompt, // ← MAIN ENTRY POINT buildAdversarialInstructions, analyzePromptEffectiveness, analyzeGeneratedContent, getSupportedDetectors, ADVERSARIAL_INSTRUCTIONS, ELEMENT_SPECIFIC_INSTRUCTIONS }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialInitialGeneration.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE // Responsabilité: Créer le contenu de base avec Claude + anti-détection // LLM: Claude Sonnet (température 0.7) + Prompts adversariaux // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { createAdversarialPrompt } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - GÉNÉRATION INITIALE ADVERSARIALE * Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} } * Output: { content: {}, stats: {}, debug: {} } */ async function generateInitialContentAdversarial(input) { return await tracer.run('AdversarialInitialGeneration.generateInitialContentAdversarial()', async () => { const { hierarchy, csvData, context = {}, adversarialConfig = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true, contextualMode: adversarialConfig.contextualMode !== false, ...adversarialConfig }; // Initialiser manager détecteur const detectorManager = new DetectorStrategyManager(config.detectorTarget); await tracer.annotate({ step: '1/4', llmProvider: 'claude', elementsCount: Object.keys(hierarchy).length, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 ÉTAPE 1/4 ADVERSARIAL: Génération initiale (Claude + ${config.detectorTarget})`, 'INFO'); logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO'); try { // Collecter tous les éléments dans l'ordre XML const allElements = collectElementsInXMLOrder(hierarchy); // Séparer FAQ pairs et autres éléments const { faqPairs, otherElements } = separateElementTypes(allElements); // Générer en chunks pour éviter timeouts const results = {}; // 1. Générer éléments normaux avec prompts adversariaux if (otherElements.length > 0) { const normalResults = await generateNormalElementsAdversarial(otherElements, csvData, config, detectorManager); Object.assign(results, normalResults); } // 2. Générer paires FAQ adversariales si présentes if (faqPairs.length > 0) { const faqResults = await generateFAQPairsAdversarial(faqPairs, csvData, config, detectorManager); Object.assign(results, faqResults); } const duration = Date.now() - startTime; const stats = { processed: Object.keys(results).length, generated: Object.keys(results).length, faqPairs: faqPairs.length, duration }; logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO'); await tracer.event(`Génération initiale terminée`, stats); return { content: results, stats, debug: { llmProvider: 'claude', step: 1, elementsGenerated: Object.keys(results), adversarialConfig: config, detectorTarget: config.detectorTarget, intensity: config.intensity } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`InitialGeneration failed: ${error.message}`); } }, input); } /** * Générer éléments normaux avec prompts adversariaux en chunks */ async function generateNormalElementsAdversarial(elements, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Génération éléments normaux adversariaux: ${elements.length} éléments`, 'DEBUG'); const results = {}; const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { const basePrompt = createBatchPrompt(chunk, csvData); // Générer prompt adversarial const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity, elementType: getElementTypeFromChunk(chunk), personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const response = await callLLM('claude', adversarialPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const chunkResults = parseBatchResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments générés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); throw error; } } return results; } /** * Générer paires FAQ adversariales cohérentes */ async function generateFAQPairsAdversarial(faqPairs, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Génération paires FAQ adversariales: ${faqPairs.length} paires`, 'DEBUG'); const basePrompt = createFAQPairsPrompt(faqPairs, csvData); // Générer prompt adversarial spécialisé FAQ const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity * 1.1, // Intensité légèrement plus élevée pour FAQ elementType: 'faq_mixed', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const response = await callLLM('claude', adversarialPrompt, { temperature: 0.8, maxTokens: 3000 }, csvData.personality); return parseFAQResponse(response, faqPairs); } /** * Créer prompt batch pour éléments normaux */ function createBatchPrompt(elements, csvData) { const personality = csvData.personality; let prompt = `=== GÉNÉRATION CONTENU INITIAL === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Rédacteur: ${personality.nom} (${personality.style}) ÉLÉMENTS À GÉNÉRER: `; elements.forEach((elementInfo, index) => { const cleanTag = elementInfo.tag.replace(/\|/g, ''); prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`; }); prompt += ` STYLE ${personality.nom.toUpperCase()}: - Vocabulaire: ${personality.vocabulairePref} - Phrases: ${personality.longueurPhrases} - Niveau: ${personality.niveauTechnique} CONSIGNES: - Contenu SEO optimisé pour ${csvData.mc0} - Style ${personality.style} naturel - Pas de références techniques dans contenu - RÉPONSE DIRECTE par le contenu FORMAT: [${elements[0].tag.replace(/\|/g, '')}] Contenu généré... [${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}] Contenu généré...`; return prompt; } /** * Parser réponse batch */ function parseBatchResponse(response, elements) { const results = {}; const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); const content = cleanGeneratedContent(match[2].trim()); parsedItems[tag] = content; } // Mapper aux vrais tags elements.forEach(element => { const cleanTag = element.tag.replace(/\|/g, ''); if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) { results[element.tag] = parsedItems[cleanTag]; } else { results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`; logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING'); } }); return results; } /** * Créer prompt pour paires FAQ */ function createFAQPairsPrompt(faqPairs, csvData) { const personality = csvData.personality; let prompt = `=== GÉNÉRATION PAIRES FAQ === Sujet: ${csvData.mc0} Rédacteur: ${personality.nom} (${personality.style}) PAIRES À GÉNÉRER: `; faqPairs.forEach((pair, index) => { const qTag = pair.question.tag.replace(/\|/g, ''); const aTag = pair.answer.tag.replace(/\|/g, ''); prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`; }); prompt += ` CONSIGNES: - Questions naturelles de clients - Réponses expertes ${personality.style} - Couvrir: prix, livraison, personnalisation FORMAT: [${faqPairs[0].question.tag.replace(/\|/g, '')}] Question client naturelle ? [${faqPairs[0].answer.tag.replace(/\|/g, '')}] Réponse utile et rassurante.`; return prompt; } /** * Parser réponse FAQ */ function parseFAQResponse(response, faqPairs) { const results = {}; const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); const content = cleanGeneratedContent(match[2].trim()); parsedItems[tag] = content; } // Mapper aux paires FAQ faqPairs.forEach(pair => { const qCleanTag = pair.question.tag.replace(/\|/g, ''); const aCleanTag = pair.answer.tag.replace(/\|/g, ''); if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag]; if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag]; }); return results; } // ============= HELPER FUNCTIONS ============= function collectElementsInXMLOrder(hierarchy) { const allElements = []; Object.keys(hierarchy).forEach(path => { const section = hierarchy[path]; if (section.title) { allElements.push({ tag: section.title.originalElement.originalTag, element: section.title.originalElement, type: section.title.originalElement.type }); } if (section.text) { allElements.push({ tag: section.text.originalElement.originalTag, element: section.text.originalElement, type: section.text.originalElement.type }); } section.questions.forEach(q => { allElements.push({ tag: q.originalElement.originalTag, element: q.originalElement, type: q.originalElement.type }); }); }); return allElements; } function separateElementTypes(allElements) { const faqPairs = []; const otherElements = []; const faqQuestions = {}; const faqAnswers = {}; // Collecter FAQ questions et answers allElements.forEach(element => { if (element.type === 'faq_question') { const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqQuestions[faqNumber] = element; } else if (element.type === 'faq_reponse') { const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqAnswers[faqNumber] = element; } else { otherElements.push(element); } }); // Créer paires FAQ Object.keys(faqQuestions).forEach(number => { const question = faqQuestions[number]; const answer = faqAnswers[number]; if (question && answer) { faqPairs.push({ number, question, answer }); } else if (question) { otherElements.push(question); } else if (answer) { otherElements.push(answer); } }); return { faqPairs, otherElements }; } function getElementDescription(elementInfo) { switch (elementInfo.type) { case 'titre_h1': return 'Titre principal accrocheur'; case 'titre_h2': return 'Titre de section'; case 'titre_h3': return 'Sous-titre'; case 'intro': return 'Introduction engageante'; case 'texte': return 'Paragraphe informatif'; default: return 'Contenu pertinent'; } } function cleanGeneratedContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Helper: Déterminer type d'élément dominant dans un chunk */ function getElementTypeFromChunk(chunk) { if (!chunk || chunk.length === 0) return 'generic'; // Compter les types dans le chunk const typeCounts = {}; chunk.forEach(element => { const type = element.type || 'generic'; typeCounts[type] = (typeCounts[type] || 0) + 1; }); // Retourner type le plus fréquent return Object.keys(typeCounts).reduce((a, b) => typeCounts[a] > typeCounts[b] ? a : b ); } module.exports = { generateInitialContentAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL generateNormalElementsAdversarial, generateFAQPairsAdversarial, createBatchPrompt, parseBatchResponse, collectElementsInXMLOrder, separateElementTypes, getElementTypeFromChunk }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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/adversarial-generation/AdversarialStyleEnhancement.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ÉTAPE 4: ENHANCEMENT STYLE PERSONNALITÉ ADVERSARIAL // Responsabilité: Appliquer le style personnalité avec Mistral + anti-détection // LLM: Mistral (température 0.8) + Prompts adversariaux // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { createAdversarialPrompt } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - ENHANCEMENT STYLE * Input: { content: {}, csvData: {}, context: {} } * Output: { content: {}, stats: {}, debug: {} } */ async function applyPersonalityStyleAdversarial(input) { return await tracer.run('AdversarialStyleEnhancement.applyPersonalityStyleAdversarial()', async () => { const { content, csvData, context = {}, adversarialConfig = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true, contextualMode: adversarialConfig.contextualMode !== false, ...adversarialConfig }; // Initialiser manager détecteur const detectorManager = new DetectorStrategyManager(config.detectorTarget); await tracer.annotate({ step: '4/4', llmProvider: 'mistral', elementsCount: Object.keys(content).length, personality: csvData.personality?.nom, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 ÉTAPE 4/4 ADVERSARIAL: Enhancement style ${csvData.personality?.nom} (Mistral + ${config.detectorTarget})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à styliser`, 'INFO'); try { const personality = csvData.personality; if (!personality) { logSh(`⚠️ ÉTAPE 4/4: Aucune personnalité définie, style standard`, 'WARNING'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime }, debug: { llmProvider: 'mistral', step: 4, personalityApplied: 'none' } }; } // 1. Préparer éléments pour stylisation const styleElements = prepareElementsForStyling(content); // 2. Appliquer style en chunks avec prompts adversariaux const styledResults = await applyStyleInChunksAdversarial(styleElements, csvData, config, detectorManager); // 3. Merger résultats const finalContent = { ...content }; let actuallyStyled = 0; Object.keys(styledResults).forEach(tag => { if (styledResults[tag] !== content[tag]) { finalContent[tag] = styledResults[tag]; actuallyStyled++; } }); const duration = Date.now() - startTime; const stats = { processed: Object.keys(content).length, enhanced: actuallyStyled, personality: personality.nom, duration }; logSh(`✅ ÉTAPE 4/4 TERMINÉE: ${stats.enhanced} éléments stylisés ${personality.nom} (${duration}ms)`, 'INFO'); await tracer.event(`Enhancement style terminé`, stats); return { content: finalContent, stats, debug: { llmProvider: 'mistral', step: 4, personalityApplied: personality.nom, styleCharacteristics: { vocabulaire: personality.vocabulairePref, connecteurs: personality.connecteursPref, style: personality.style }, adversarialConfig: config, detectorTarget: config.detectorTarget, intensity: config.intensity } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ÉTAPE 4/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original si Mistral indisponible logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, duration }, debug: { llmProvider: 'mistral', step: 4, error: error.message, fallback: true } }; } }, input); } /** * Préparer éléments pour stylisation */ function prepareElementsForStyling(content) { const styleElements = []; Object.keys(content).forEach(tag => { const text = content[tag]; // Tous les éléments peuvent bénéficier d'adaptation personnalité // Même les courts (titres) peuvent être adaptés au style styleElements.push({ tag, content: text, priority: calculateStylePriority(text, tag) }); }); // Trier par priorité (titres d'abord, puis textes longs) styleElements.sort((a, b) => b.priority - a.priority); return styleElements; } /** * Calculer priorité de stylisation */ function calculateStylePriority(text, tag) { let priority = 1.0; // Titres = haute priorité (plus visible) if (tag.includes('Titre') || tag.includes('H1') || tag.includes('H2')) { priority += 0.5; } // Textes longs = priorité selon longueur if (text.length > 200) { priority += 0.3; } else if (text.length > 100) { priority += 0.2; } // Introduction = haute priorité if (tag.includes('intro') || tag.includes('Introduction')) { priority += 0.4; } return priority; } /** * Appliquer style en chunks avec prompts adversariaux */ async function applyStyleInChunksAdversarial(styleElements, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Stylisation adversarial: ${styleElements.length} éléments selon ${csvData.personality.nom}`, 'DEBUG'); const results = {}; const chunks = chunkArray(styleElements, 8); // Chunks de 8 pour Mistral for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const basePrompt = createStylePrompt(chunk, csvData); // Générer prompt adversarial pour stylisation const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity * 1.1, // Intensité plus élevée pour style (plus visible) elementType: 'style_enhancement', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const styledResponse = await callLLM('mistral', adversarialPrompt, { temperature: 0.8, maxTokens: 3000 }, csvData.personality); const chunkResults = parseStyleResponse(styledResponse, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} stylisés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: garder contenu original chunk.forEach(element => { results[element.tag] = element.content; }); } } return results; } /** * Créer prompt de stylisation */ function createStylePrompt(chunk, csvData) { const personality = csvData.personality; let prompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}. CONTEXTE: Article SEO e-commerce ${csvData.mc0} PERSONNALITÉ: ${personality.nom} DESCRIPTION: ${personality.description} STYLE: ${personality.style} adapté web professionnel VOCABULAIRE: ${personality.vocabulairePref} CONNECTEURS: ${personality.connecteursPref} NIVEAU TECHNIQUE: ${personality.niveauTechnique} LONGUEUR PHRASES: ${personality.longueurPhrases} CONTENUS À STYLISER: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} (Priorité: ${item.priority.toFixed(1)}) CONTENU: "${item.content}"`).join('\n\n')} OBJECTIFS STYLISATION ${personality.nom.toUpperCase()}: - Adapte le TON selon ${personality.style} - Vocabulaire: ${personality.vocabulairePref} - Connecteurs variés: ${personality.connecteursPref} - Phrases: ${personality.longueurPhrases} - Niveau: ${personality.niveauTechnique} CONSIGNES STRICTES: - GARDE le même contenu informatif et technique - Adapte SEULEMENT ton, expressions, vocabulaire selon ${personality.nom} - RESPECTE longueur approximative (±20%) - ÉVITE répétitions excessives - Style ${personality.nom} reconnaissable mais NATUREL web - PAS de messages d'excuse FORMAT RÉPONSE: [1] Contenu stylisé selon ${personality.nom} [2] Contenu stylisé selon ${personality.nom} etc...`; return prompt; } /** * Parser réponse stylisation */ function parseStyleResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < chunk.length) { let styledContent = match[2].trim(); const element = chunk[index]; // Nettoyer le contenu stylisé styledContent = cleanStyledContent(styledContent); if (styledContent && styledContent.length > 10) { results[element.tag] = styledContent; logSh(`✅ Styled [${element.tag}]: "${styledContent.substring(0, 100)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; logSh(`⚠️ Fallback [${element.tag}]: stylisation invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < chunk.length) { const element = chunk[index]; results[element.tag] = element.content; index++; } return results; } /** * Nettoyer contenu stylisé */ function cleanStyledContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?voici\s+/gi, ''); content = content.replace(/^pour\s+ce\s+contenu[,\s]*/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); // Réduire répétitions excessives mais garder le style personnalité content = content.replace(/(du coup[,\s]+){4,}/gi, 'du coup '); content = content.replace(/(bon[,\s]+){4,}/gi, 'bon '); content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement '); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } /** * Obtenir instructions de style dynamiques */ function getPersonalityStyleInstructions(personality) { if (!personality) return "Style professionnel standard"; return `STYLE ${personality.nom.toUpperCase()} (${personality.style}): - Description: ${personality.description} - Vocabulaire: ${personality.vocabulairePref || 'professionnel'} - Connecteurs: ${personality.connecteursPref || 'par ailleurs, en effet'} - Mots-clés: ${personality.motsClesSecteurs || 'technique, qualité'} - Phrases: ${personality.longueurPhrases || 'Moyennes'} - Niveau: ${personality.niveauTechnique || 'Accessible'} - CTA: ${personality.ctaStyle || 'Professionnel'}`; } // ============= HELPER FUNCTIONS ============= function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } module.exports = { applyPersonalityStyleAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL prepareElementsForStyling, calculateStylePriority, applyStyleInChunksAdversarial, createStylePrompt, parseStyleResponse, getPersonalityStyleInstructions }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialTechnicalEnhancem… │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL // Responsabilité: Améliorer la précision technique avec GPT-4 + anti-détection // LLM: GPT-4o-mini (température 0.4) + Prompts adversariaux // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { createAdversarialPrompt } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - ENHANCEMENT TECHNIQUE ADVERSARIAL * Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} } * Output: { content: {}, stats: {}, debug: {} } */ async function enhanceTechnicalTermsAdversarial(input) { return await tracer.run('AdversarialTechnicalEnhancement.enhanceTechnicalTermsAdversarial()', async () => { const { content, csvData, context = {}, adversarialConfig = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true, contextualMode: adversarialConfig.contextualMode !== false, ...adversarialConfig }; // Initialiser manager détecteur const detectorManager = new DetectorStrategyManager(config.detectorTarget); await tracer.annotate({ step: '2/4', llmProvider: 'gpt4', elementsCount: Object.keys(content).length, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 ÉTAPE 2/4 ADVERSARIAL: Enhancement technique (GPT-4 + ${config.detectorTarget})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO'); try { // 1. Analyser tous les éléments pour détecter termes techniques (adversarial) const technicalAnalysis = await analyzeTechnicalTermsAdversarial(content, csvData, config, detectorManager); // 2. Filter les éléments qui ont besoin d'enhancement const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement); logSh(` 📋 Analyse: ${elementsNeedingEnhancement.length}/${Object.keys(content).length} éléments nécessitent enhancement`, 'INFO'); if (elementsNeedingEnhancement.length === 0) { logSh(`✅ ÉTAPE 2/4: Aucun enhancement nécessaire`, 'INFO'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime }, debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: [] } }; } // 3. Améliorer les éléments sélectionnés avec prompts adversariaux const enhancedResults = await enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, config, detectorManager); // 4. Merger avec contenu original const finalContent = { ...content }; let actuallyEnhanced = 0; Object.keys(enhancedResults).forEach(tag => { if (enhancedResults[tag] !== content[tag]) { finalContent[tag] = enhancedResults[tag]; actuallyEnhanced++; } }); const duration = Date.now() - startTime; const stats = { processed: Object.keys(content).length, enhanced: actuallyEnhanced, candidate: elementsNeedingEnhancement.length, duration }; logSh(`✅ ÉTAPE 2/4 TERMINÉE: ${stats.enhanced} éléments améliorés (${duration}ms)`, 'INFO'); await tracer.event(`Enhancement technique terminé`, stats); return { content: finalContent, stats, debug: { llmProvider: 'gpt4', step: 2, enhancementsApplied: Object.keys(enhancedResults), technicalTermsFound: elementsNeedingEnhancement.map(e => e.technicalTerms), adversarialConfig: config, detectorTarget: config.detectorTarget, intensity: config.intensity } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ÉTAPE 2/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`TechnicalEnhancement failed: ${error.message}`); } }, input); } /** * Analyser tous les éléments pour détecter termes techniques (adversarial) */ async function analyzeTechnicalTermsAdversarial(content, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Analyse termes techniques adversarial batch`, 'DEBUG'); const contentEntries = Object.keys(content); const analysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENUS À ANALYSER: ${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} CONTENU: "${content[tag]}"`).join('\n\n')} CONSIGNES: - Identifie UNIQUEMENT les vrais termes techniques métier/industrie - Évite mots génériques (qualité, service, pratique, personnalisé) - Focus: matériaux, procédés, normes, dimensions, technologies - Si aucun terme technique → "AUCUN" EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne FORMAT RÉPONSE: [1] dibond, impression UV OU AUCUN [2] AUCUN [3] aluminium, fraisage CNC OU AUCUN etc...`; try { // Générer prompt adversarial pour analyse const adversarialAnalysisPrompt = createAdversarialPrompt(analysisPrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity * 0.8, // Intensité modérée pour analyse elementType: 'technical_analysis', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const analysisResponse = await callLLM('gpt4', adversarialAnalysisPrompt, { temperature: 0.3, maxTokens: 2000 }, csvData.personality); return parseAnalysisResponse(analysisResponse, content, contentEntries); } catch (error) { logSh(`❌ Analyse termes techniques échouée: ${error.message}`, 'ERROR'); throw error; } } /** * Améliorer les éléments sélectionnés avec prompts adversariaux */ async function enhanceSelectedElementsAdversarial(elementsNeedingEnhancement, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Enhancement adversarial ${elementsNeedingEnhancement.length} éléments`, 'DEBUG'); const enhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus. CONTEXTE: ${csvData.mc0} - Secteur signalétique/impression PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style}) CONTENUS À AMÉLIORER: ${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} CONTENU: "${item.content}" TERMES TECHNIQUES: ${item.technicalTerms.join(', ')}`).join('\n\n')} CONSIGNES: - GARDE même longueur, structure et ton ${csvData.personality?.style} - Intègre naturellement les termes techniques listés - NE CHANGE PAS le fond du message - Vocabulaire expert mais accessible - Termes secteur: dibond, aluminium, impression UV, fraisage, PMMA FORMAT RÉPONSE: [1] Contenu avec amélioration technique [2] Contenu avec amélioration technique etc...`; try { // Générer prompt adversarial pour enhancement const adversarialEnhancementPrompt = createAdversarialPrompt(enhancementPrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity, elementType: 'technical_enhancement', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const enhancedResponse = await callLLM('gpt4', adversarialEnhancementPrompt, { temperature: 0.4, maxTokens: 5000 }, csvData.personality); return parseEnhancementResponse(enhancedResponse, elementsNeedingEnhancement); } catch (error) { logSh(`❌ Enhancement éléments échoué: ${error.message}`, 'ERROR'); throw error; } } /** * Parser réponse analyse */ function parseAnalysisResponse(response, content, contentEntries) { const results = []; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; const termsText = match[2].trim(); parsedItems[index] = termsText; } contentEntries.forEach((tag, index) => { const termsText = parsedItems[index] || 'AUCUN'; const hasTerms = !termsText.toUpperCase().includes('AUCUN'); const technicalTerms = hasTerms ? termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) : []; results.push({ tag, content: content[tag], technicalTerms, needsEnhancement: hasTerms && technicalTerms.length > 0 }); logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'aucun terme technique'}`, 'DEBUG'); }); return results; } /** * Parser réponse enhancement */ function parseEnhancementResponse(response, elementsNeedingEnhancement) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) { let enhancedContent = match[2].trim(); const element = elementsNeedingEnhancement[index]; // Nettoyer le contenu généré enhancedContent = cleanEnhancedContent(enhancedContent); if (enhancedContent && enhancedContent.length > 10) { results[element.tag] = enhancedContent; logSh(`✅ Enhanced [${element.tag}]: "${enhancedContent.substring(0, 100)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; logSh(`⚠️ Fallback [${element.tag}]: contenu invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < elementsNeedingEnhancement.length) { const element = elementsNeedingEnhancement[index]; results[element.tag] = element.content; index++; } return results; } /** * Nettoyer contenu amélioré */ function cleanEnhancedContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } module.exports = { enhanceTechnicalTermsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL analyzeTechnicalTermsAdversarial, enhanceSelectedElementsAdversarial, parseAnalysisResponse, parseEnhancementResponse }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialTransitionEnhance… │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL // Responsabilité: Améliorer la fluidité avec Gemini + anti-détection // LLM: Gemini (température 0.6) + Prompts adversariaux // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { createAdversarialPrompt } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - ENHANCEMENT TRANSITIONS ADVERSARIAL * Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} } * Output: { content: {}, stats: {}, debug: {} } */ async function enhanceTransitionsAdversarial(input) { return await tracer.run('AdversarialTransitionEnhancement.enhanceTransitionsAdversarial()', async () => { const { content, csvData, context = {}, adversarialConfig = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true, contextualMode: adversarialConfig.contextualMode !== false, ...adversarialConfig }; // Initialiser manager détecteur const detectorManager = new DetectorStrategyManager(config.detectorTarget); await tracer.annotate({ step: '3/4', llmProvider: 'gemini', elementsCount: Object.keys(content).length, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 ÉTAPE 3/4 ADVERSARIAL: Enhancement transitions (Gemini + ${config.detectorTarget})`, 'INFO'); logSh(` 📊 ${Object.keys(content).length} éléments à analyser`, 'INFO'); try { // 1. Analyser quels éléments ont besoin d'amélioration transitions const elementsNeedingTransitions = analyzeTransitionNeeds(content); logSh(` 📋 Analyse: ${elementsNeedingTransitions.length}/${Object.keys(content).length} éléments nécessitent fluidité`, 'INFO'); if (elementsNeedingTransitions.length === 0) { logSh(`✅ ÉTAPE 3/4: Transitions déjà optimales`, 'INFO'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, duration: Date.now() - startTime }, debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: [] } }; } // 2. Améliorer en chunks avec prompts adversariaux pour Gemini const improvedResults = await improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, config, detectorManager); // 3. Merger avec contenu original const finalContent = { ...content }; let actuallyImproved = 0; Object.keys(improvedResults).forEach(tag => { if (improvedResults[tag] !== content[tag]) { finalContent[tag] = improvedResults[tag]; actuallyImproved++; } }); const duration = Date.now() - startTime; const stats = { processed: Object.keys(content).length, enhanced: actuallyImproved, candidate: elementsNeedingTransitions.length, duration }; logSh(`✅ ÉTAPE 3/4 TERMINÉE: ${stats.enhanced} éléments fluidifiés (${duration}ms)`, 'INFO'); await tracer.event(`Enhancement transitions terminé`, stats); return { content: finalContent, stats, debug: { llmProvider: 'gemini', step: 3, enhancementsApplied: Object.keys(improvedResults), transitionIssues: elementsNeedingTransitions.map(e => e.issues), adversarialConfig: config, detectorTarget: config.detectorTarget, intensity: config.intensity } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ÉTAPE 3/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); // Fallback: retourner contenu original si Gemini indisponible logSh(`🔄 Fallback: contenu original conservé`, 'WARNING'); return { content, stats: { processed: Object.keys(content).length, enhanced: 0, duration }, debug: { llmProvider: 'gemini', step: 3, error: error.message, fallback: true } }; } }, input); } /** * Analyser besoin d'amélioration transitions */ function analyzeTransitionNeeds(content) { const elementsNeedingTransitions = []; Object.keys(content).forEach(tag => { const text = content[tag]; // Filtrer les éléments longs (>150 chars) qui peuvent bénéficier d'améliorations if (text.length > 150) { const needsTransitions = evaluateTransitionQuality(text); if (needsTransitions.needsImprovement) { elementsNeedingTransitions.push({ tag, content: text, issues: needsTransitions.issues, score: needsTransitions.score }); logSh(` 🔍 [${tag}]: Score=${needsTransitions.score.toFixed(2)}, Issues: ${needsTransitions.issues.join(', ')}`, 'DEBUG'); } } else { logSh(` ⏭️ [${tag}]: Trop court (${text.length}c), ignoré`, 'DEBUG'); } }); // Trier par score (plus problématique en premier) elementsNeedingTransitions.sort((a, b) => a.score - b.score); return elementsNeedingTransitions; } /** * Évaluer qualité transitions d'un texte */ function evaluateTransitionQuality(text) { const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10); if (sentences.length < 2) { return { needsImprovement: false, score: 1.0, issues: [] }; } const issues = []; let score = 1.0; // Score parfait = 1.0, problématique = 0.0 // Analyse 1: Connecteurs répétitifs const repetitiveConnectors = analyzeRepetitiveConnectors(text); if (repetitiveConnectors > 0.3) { issues.push('connecteurs_répétitifs'); score -= 0.3; } // Analyse 2: Transitions abruptes const abruptTransitions = analyzeAbruptTransitions(sentences); if (abruptTransitions > 0.4) { issues.push('transitions_abruptes'); score -= 0.4; } // Analyse 3: Manque de variété dans longueurs const sentenceVariety = analyzeSentenceVariety(sentences); if (sentenceVariety < 0.3) { issues.push('phrases_uniformes'); score -= 0.2; } // Analyse 4: Trop formel ou trop familier const formalityIssues = analyzeFormalityBalance(text); if (formalityIssues > 0.5) { issues.push('formalité_déséquilibrée'); score -= 0.1; } return { needsImprovement: score < 0.6, score: Math.max(0, score), issues }; } /** * Améliorer transitions en chunks avec prompts adversariaux */ async function improveTransitionsInChunksAdversarial(elementsNeedingTransitions, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Amélioration transitions adversarial: ${elementsNeedingTransitions.length} éléments`, 'DEBUG'); const results = {}; const chunks = chunkArray(elementsNeedingTransitions, 6); // Chunks plus petits pour Gemini for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const basePrompt = createTransitionImprovementPrompt(chunk, csvData); // Générer prompt adversarial pour amélioration transitions const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity * 0.9, // Intensité légèrement réduite pour transitions elementType: 'transition_enhancement', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const improvedResponse = await callLLM('gemini', adversarialPrompt, { temperature: 0.6, maxTokens: 2500 }, csvData.personality); const chunkResults = parseTransitionResponse(improvedResponse, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: garder contenu original pour ce chunk chunk.forEach(element => { results[element.tag] = element.content; }); } } return results; } /** * Créer prompt amélioration transitions */ function createTransitionImprovementPrompt(chunk, csvData) { const personality = csvData.personality; let prompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus. CONTEXTE: Article SEO ${csvData.mc0} PERSONNALITÉ: ${personality?.nom} (${personality?.style} web professionnel) CONNECTEURS PRÉFÉRÉS: ${personality?.connecteursPref} CONTENUS À FLUIDIFIER: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} PROBLÈMES: ${item.issues.join(', ')} CONTENU: "${item.content}"`).join('\n\n')} OBJECTIFS: - Connecteurs plus naturels et variés: ${personality?.connecteursPref} - Transitions fluides entre idées - ÉVITE répétitions excessives ("du coup", "franchement", "par ailleurs") - Style ${personality?.style} mais professionnel web CONSIGNES STRICTES: - NE CHANGE PAS le fond du message - GARDE même structure et longueur - Améliore SEULEMENT la fluidité - RESPECTE le style ${personality?.nom} FORMAT RÉPONSE: [1] Contenu avec transitions améliorées [2] Contenu avec transitions améliorées etc...`; return prompt; } /** * Parser réponse amélioration transitions */ function parseTransitionResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < chunk.length) { let improvedContent = match[2].trim(); const element = chunk[index]; // Nettoyer le contenu amélioré improvedContent = cleanImprovedContent(improvedContent); if (improvedContent && improvedContent.length > 10) { results[element.tag] = improvedContent; logSh(`✅ Improved [${element.tag}]: "${improvedContent.substring(0, 100)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; logSh(`⚠️ Fallback [${element.tag}]: amélioration invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < chunk.length) { const element = chunk[index]; results[element.tag] = element.content; index++; } return results; } // ============= HELPER FUNCTIONS ============= function analyzeRepetitiveConnectors(content) { const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc']; let totalConnectors = 0; let repetitions = 0; connectors.forEach(connector => { const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []); totalConnectors += matches.length; if (matches.length > 1) repetitions += matches.length - 1; }); return totalConnectors > 0 ? repetitions / totalConnectors : 0; } function analyzeAbruptTransitions(sentences) { if (sentences.length < 2) return 0; let abruptCount = 0; for (let i = 1; i < sentences.length; i++) { const current = sentences[i].trim(); const hasConnector = hasTransitionWord(current); if (!hasConnector && current.length > 30) { abruptCount++; } } return abruptCount / (sentences.length - 1); } function analyzeSentenceVariety(sentences) { if (sentences.length < 2) return 1; const lengths = sentences.map(s => s.trim().length); const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); return Math.min(1, stdDev / avgLength); } function analyzeFormalityBalance(content) { const formalIndicators = ['il convient de', 'par conséquent', 'néanmoins', 'toutefois']; const casualIndicators = ['du coup', 'bon', 'franchement', 'nickel']; let formalCount = 0; let casualCount = 0; formalIndicators.forEach(indicator => { if (content.toLowerCase().includes(indicator)) formalCount++; }); casualIndicators.forEach(indicator => { if (content.toLowerCase().includes(indicator)) casualCount++; }); const total = formalCount + casualCount; if (total === 0) return 0; // Déséquilibre si trop d'un côté const balance = Math.abs(formalCount - casualCount) / total; return balance; } function hasTransitionWord(sentence) { const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi']; return connectors.some(connector => sentence.toLowerCase().includes(connector)); } function cleanImprovedContent(content) { if (!content) return content; content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?/, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } module.exports = { enhanceTransitionsAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL analyzeTransitionNeeds, evaluateTransitionQuality, improveTransitionsInChunksAdversarial, createTransitionImprovementPrompt, parseTransitionResponse }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/AdversarialUtils.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ADVERSARIAL UTILS - UTILITAIRES MODULAIRES // Responsabilité: Fonctions utilitaires partagées par tous les modules adversariaux // Architecture: Helper functions réutilisables et composables // ======================================== const { logSh } = require('../ErrorReporting'); /** * ANALYSEURS DE CONTENU */ /** * Analyser score de diversité lexicale */ function analyzeLexicalDiversity(content) { if (!content || typeof content !== 'string') return 0; const words = content.toLowerCase() .split(/\s+/) .filter(word => word.length > 2) .map(word => word.replace(/[^\w]/g, '')); if (words.length === 0) return 0; const uniqueWords = [...new Set(words)]; return (uniqueWords.length / words.length) * 100; } /** * Analyser variation des longueurs de phrases */ function analyzeSentenceVariation(content) { if (!content || typeof content !== 'string') return 0; const sentences = content.split(/[.!?]+/) .map(s => s.trim()) .filter(s => s.length > 5); if (sentences.length < 2) return 0; const lengths = sentences.map(s => s.split(/\s+/).length); const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); return Math.min(100, (stdDev / avgLength) * 100); } /** * Détecter mots typiques IA */ function detectAIFingerprints(content) { const aiFingerprints = { words: ['optimal', 'comprehensive', 'seamless', 'robust', 'leverage', 'cutting-edge', 'state-of-the-art', 'furthermore', 'moreover'], phrases: ['it is important to note', 'it should be noted', 'it is worth mentioning', 'in conclusion', 'to summarize'], connectors: ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'] }; const results = { words: 0, phrases: 0, connectors: 0, totalScore: 0 }; const lowerContent = content.toLowerCase(); // Compter mots IA aiFingerprints.words.forEach(word => { const matches = (lowerContent.match(new RegExp(`\\b${word}\\b`, 'g')) || []); results.words += matches.length; }); // Compter phrases typiques aiFingerprints.phrases.forEach(phrase => { if (lowerContent.includes(phrase)) { results.phrases += 1; } }); // Compter connecteurs répétitifs aiFingerprints.connectors.forEach(connector => { const matches = (lowerContent.match(new RegExp(`\\b${connector}\\b`, 'g')) || []); if (matches.length > 1) { results.connectors += matches.length - 1; // Pénalité répétition } }); // Score total (sur 100) const wordCount = content.split(/\s+/).length; results.totalScore = Math.min(100, (results.words * 5 + results.phrases * 10 + results.connectors * 3) / Math.max(wordCount, 1) * 100 ); return results; } /** * Analyser uniformité structurelle */ function analyzeStructuralUniformity(content) { const sentences = content.split(/[.!?]+/) .map(s => s.trim()) .filter(s => s.length > 5); if (sentences.length < 3) return 0; const structures = sentences.map(sentence => { const words = sentence.split(/\s+/); return { length: words.length, startsWithConnector: /^(par ailleurs|en effet|de plus|cependant|ainsi|donc|ensuite|puis)/i.test(sentence), hasComma: sentence.includes(','), hasSubordinate: /qui|que|dont|où|quand|comme|parce que|puisque|bien que/i.test(sentence) }; }); // Calculer uniformité const avgLength = structures.reduce((sum, s) => sum + s.length, 0) / structures.length; const lengthVariance = structures.reduce((sum, s) => sum + Math.pow(s.length - avgLength, 2), 0) / structures.length; const connectorRatio = structures.filter(s => s.startsWithConnector).length / structures.length; const commaRatio = structures.filter(s => s.hasComma).length / structures.length; // Plus c'est uniforme, plus le score est élevé (mauvais pour anti-détection) const uniformityScore = 100 - (Math.sqrt(lengthVariance) / avgLength * 100) - (Math.abs(0.3 - connectorRatio) * 50) - (Math.abs(0.5 - commaRatio) * 30); return Math.max(0, Math.min(100, uniformityScore)); } /** * COMPARATEURS DE CONTENU */ /** * Comparer deux contenus et calculer taux de modification */ function compareContentModification(original, modified) { if (!original || !modified) return 0; const originalWords = original.toLowerCase().split(/\s+/).filter(w => w.length > 2); const modifiedWords = modified.toLowerCase().split(/\s+/).filter(w => w.length > 2); // Calcul de distance Levenshtein approximative (par mots) let changes = 0; const maxLength = Math.max(originalWords.length, modifiedWords.length); for (let i = 0; i < maxLength; i++) { if (originalWords[i] !== modifiedWords[i]) { changes++; } } return (changes / maxLength) * 100; } /** * Évaluer amélioration adversariale */ function evaluateAdversarialImprovement(original, modified, detectorTarget = 'general') { const originalFingerprints = detectAIFingerprints(original); const modifiedFingerprints = detectAIFingerprints(modified); const originalDiversity = analyzeLexicalDiversity(original); const modifiedDiversity = analyzeLexicalDiversity(modified); const originalVariation = analyzeSentenceVariation(original); const modifiedVariation = analyzeSentenceVariation(modified); const fingerprintReduction = originalFingerprints.totalScore - modifiedFingerprints.totalScore; const diversityIncrease = modifiedDiversity - originalDiversity; const variationIncrease = modifiedVariation - originalVariation; const improvementScore = ( fingerprintReduction * 0.4 + diversityIncrease * 0.3 + variationIncrease * 0.3 ); return { fingerprintReduction, diversityIncrease, variationIncrease, improvementScore: Math.round(improvementScore * 100) / 100, modificationRate: compareContentModification(original, modified), recommendation: getImprovementRecommendation(improvementScore, detectorTarget) }; } /** * UTILITAIRES DE CONTENU */ /** * Nettoyer contenu adversarial généré */ function cleanAdversarialContent(content) { if (!content || typeof content !== 'string') return content; let cleaned = content; // Supprimer préfixes de génération cleaned = cleaned.replace(/^(voici\s+)?le\s+contenu\s+(réécrit|amélioré|modifié)[:\s]*/gi, ''); cleaned = cleaned.replace(/^(bon,?\s*)?(alors,?\s*)?(pour\s+)?(ce\s+contenu[,\s]*)?/gi, ''); // Nettoyer formatage cleaned = cleaned.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown cleaned = cleaned.replace(/\s{2,}/g, ' '); // Espaces multiples cleaned = cleaned.replace(/([.!?])\s*([.!?])/g, '$1 '); // Double ponctuation // Nettoyer début/fin cleaned = cleaned.trim(); cleaned = cleaned.replace(/^[,.\s]+/, ''); cleaned = cleaned.replace(/[,\s]+$/, ''); return cleaned; } /** * Valider qualité du contenu adversarial */ function validateAdversarialContent(content, originalContent, minLength = 10, maxModificationRate = 90) { const validation = { isValid: true, issues: [], suggestions: [] }; // Vérifier longueur minimale if (!content || content.length < minLength) { validation.isValid = false; validation.issues.push('Contenu trop court'); validation.suggestions.push('Augmenter la longueur du contenu généré'); } // Vérifier cohérence if (originalContent) { const modificationRate = compareContentModification(originalContent, content); if (modificationRate > maxModificationRate) { validation.issues.push('Modification trop importante'); validation.suggestions.push('Réduire l\'intensité adversariale pour préserver le sens'); } if (modificationRate < 5) { validation.issues.push('Modification insuffisante'); validation.suggestions.push('Augmenter l\'intensité adversariale'); } } // Vérifier empreintes IA résiduelles const fingerprints = detectAIFingerprints(content); if (fingerprints.totalScore > 15) { validation.issues.push('Empreintes IA encore présentes'); validation.suggestions.push('Appliquer post-processing anti-fingerprints'); } return validation; } /** * UTILITAIRES TECHNIQUES */ /** * Chunk array avec préservation des paires */ function chunkArraySmart(array, size, preservePairs = false) { if (!preservePairs) { return chunkArray(array, size); } const chunks = []; for (let i = 0; i < array.length; i += size) { let chunk = array.slice(i, i + size); // Si on coupe au milieu d'une paire (nombre impair), ajuster if (chunk.length % 2 !== 0 && i + size < array.length) { chunk = array.slice(i, i + size - 1); } chunks.push(chunk); } return chunks; } /** * Chunk array standard */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Sleep avec variation */ function sleep(ms, variation = 0.2) { const actualMs = ms + (Math.random() - 0.5) * ms * variation; return new Promise(resolve => setTimeout(resolve, Math.max(100, actualMs))); } /** * RECOMMANDATIONS */ /** * Obtenir recommandation d'amélioration */ function getImprovementRecommendation(score, detectorTarget) { const recommendations = { general: { good: "Bon niveau d'amélioration générale", medium: "Appliquer techniques de variation syntaxique", poor: "Nécessite post-processing intensif" }, gptZero: { good: "Imprévisibilité suffisante contre GPTZero", medium: "Ajouter plus de ruptures narratives", poor: "Intensifier variation syntaxique et lexicale" }, originality: { good: "Créativité suffisante contre Originality", medium: "Enrichir diversité sémantique", poor: "Réinventer présentation des informations" } }; const category = score > 10 ? 'good' : score > 5 ? 'medium' : 'poor'; return recommendations[detectorTarget]?.[category] || recommendations.general[category]; } /** * MÉTRIQUES ET STATS */ /** * Calculer score composite anti-détection */ function calculateAntiDetectionScore(content, detectorTarget = 'general') { const diversity = analyzeLexicalDiversity(content); const variation = analyzeSentenceVariation(content); const fingerprints = detectAIFingerprints(content); const uniformity = analyzeStructuralUniformity(content); const baseScore = (diversity * 0.3 + variation * 0.3 + (100 - fingerprints.totalScore) * 0.2 + (100 - uniformity) * 0.2); // Ajustements selon détecteur let adjustedScore = baseScore; switch (detectorTarget) { case 'gptZero': adjustedScore = baseScore * (variation / 100) * 1.2; // Favorise variation break; case 'originality': adjustedScore = baseScore * (diversity / 100) * 1.2; // Favorise diversité break; } return Math.min(100, Math.max(0, Math.round(adjustedScore))); } module.exports = { // Analyseurs analyzeLexicalDiversity, analyzeSentenceVariation, detectAIFingerprints, analyzeStructuralUniformity, // Comparateurs compareContentModification, evaluateAdversarialImprovement, // Utilitaires contenu cleanAdversarialContent, validateAdversarialContent, // Utilitaires techniques chunkArray, chunkArraySmart, sleep, // Métriques calculateAntiDetectionScore, getImprovementRecommendation }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/ContentGenerationAdversarial.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // ORCHESTRATEUR CONTENU ADVERSARIAL - NIVEAU 3 // Responsabilité: Pipeline complet de génération anti-détection // Architecture: 4 étapes adversariales séparées et modulaires // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); // Importation des 4 étapes adversariales const { generateInitialContentAdversarial } = require('./AdversarialInitialGeneration'); const { enhanceTechnicalTermsAdversarial } = require('./AdversarialTechnicalEnhancement'); const { enhanceTransitionsAdversarial } = require('./AdversarialTransitionEnhancement'); const { applyPersonalityStyleAdversarial } = require('./AdversarialStyleEnhancement'); // Importation du moteur adversarial const { createAdversarialPrompt, getSupportedDetectors, analyzePromptEffectiveness } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - PIPELINE ADVERSARIAL COMPLET * Input: { hierarchy, csvData, adversarialConfig, context } * Output: { content, stats, debug, adversarialMetrics } */ async function generateWithAdversarialContext(input) { return await tracer.run('ContentGenerationAdversarial.generateWithAdversarialContext()', async () => { const { hierarchy, csvData, adversarialConfig = {}, context = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy !== false, contextualMode: adversarialConfig.contextualMode !== false, enableAllSteps: adversarialConfig.enableAllSteps !== false, // Configuration par étape steps: { initial: adversarialConfig.steps?.initial !== false, technical: adversarialConfig.steps?.technical !== false, transitions: adversarialConfig.steps?.transitions !== false, style: adversarialConfig.steps?.style !== false }, ...adversarialConfig }; await tracer.annotate({ adversarialPipeline: true, detectorTarget: config.detectorTarget, intensity: config.intensity, enabledSteps: Object.keys(config.steps).filter(k => config.steps[k]), elementsCount: Object.keys(hierarchy).length, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 PIPELINE ADVERSARIAL NIVEAU 3: Anti-détection ${config.detectorTarget}`, 'INFO'); logSh(` 🎚️ Intensité: ${config.intensity.toFixed(2)} | Étapes: ${Object.keys(config.steps).filter(k => config.steps[k]).join(', ')}`, 'INFO'); // Initialiser manager détecteur global const detectorManager = new DetectorStrategyManager(config.detectorTarget); try { let currentContent = {}; let pipelineStats = { steps: {}, totalDuration: 0, elementsProcessed: 0, adversarialMetrics: { promptsGenerated: 0, detectorTarget: config.detectorTarget, averageIntensity: config.intensity, effectivenessScore: 0 } }; // ======================================== // ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE // ======================================== if (config.steps.initial) { logSh(`🎯 ÉTAPE 1/4: Génération initiale adversariale`, 'INFO'); const step1Result = await generateInitialContentAdversarial({ hierarchy, csvData, context, adversarialConfig: config }); currentContent = step1Result.content; pipelineStats.steps.initial = step1Result.stats; pipelineStats.adversarialMetrics.promptsGenerated += Object.keys(currentContent).length; logSh(`✅ ÉTAPE 1/4: ${step1Result.stats.generated} éléments générés (${step1Result.stats.duration}ms)`, 'INFO'); } else { logSh(`⏭️ ÉTAPE 1/4: Ignorée (configuration)`, 'INFO'); } // ======================================== // ÉTAPE 2: ENHANCEMENT TECHNIQUE ADVERSARIAL // ======================================== if (config.steps.technical && Object.keys(currentContent).length > 0) { logSh(`🎯 ÉTAPE 2/4: Enhancement technique adversarial`, 'INFO'); const step2Result = await enhanceTechnicalTermsAdversarial({ content: currentContent, csvData, context, adversarialConfig: config }); currentContent = step2Result.content; pipelineStats.steps.technical = step2Result.stats; pipelineStats.adversarialMetrics.promptsGenerated += step2Result.stats.enhanced; logSh(`✅ ÉTAPE 2/4: ${step2Result.stats.enhanced} éléments améliorés (${step2Result.stats.duration}ms)`, 'INFO'); } else { logSh(`⏭️ ÉTAPE 2/4: Ignorée (configuration ou pas de contenu)`, 'INFO'); } // ======================================== // ÉTAPE 3: ENHANCEMENT TRANSITIONS ADVERSARIAL // ======================================== if (config.steps.transitions && Object.keys(currentContent).length > 0) { logSh(`🎯 ÉTAPE 3/4: Enhancement transitions adversarial`, 'INFO'); const step3Result = await enhanceTransitionsAdversarial({ content: currentContent, csvData, context, adversarialConfig: config }); currentContent = step3Result.content; pipelineStats.steps.transitions = step3Result.stats; pipelineStats.adversarialMetrics.promptsGenerated += step3Result.stats.enhanced; logSh(`✅ ÉTAPE 3/4: ${step3Result.stats.enhanced} éléments fluidifiés (${step3Result.stats.duration}ms)`, 'INFO'); } else { logSh(`⏭️ ÉTAPE 3/4: Ignorée (configuration ou pas de contenu)`, 'INFO'); } // ======================================== // ÉTAPE 4: ENHANCEMENT STYLE ADVERSARIAL // ======================================== if (config.steps.style && Object.keys(currentContent).length > 0 && csvData.personality) { logSh(`🎯 ÉTAPE 4/4: Enhancement style adversarial`, 'INFO'); const step4Result = await applyPersonalityStyleAdversarial({ content: currentContent, csvData, context, adversarialConfig: config }); currentContent = step4Result.content; pipelineStats.steps.style = step4Result.stats; pipelineStats.adversarialMetrics.promptsGenerated += step4Result.stats.enhanced; logSh(`✅ ÉTAPE 4/4: ${step4Result.stats.enhanced} éléments stylisés (${step4Result.stats.duration}ms)`, 'INFO'); } else { logSh(`⏭️ ÉTAPE 4/4: Ignorée (configuration, pas de contenu ou pas de personnalité)`, 'INFO'); } // ======================================== // FINALISATION PIPELINE // ======================================== const totalDuration = Date.now() - startTime; pipelineStats.totalDuration = totalDuration; pipelineStats.elementsProcessed = Object.keys(currentContent).length; // Calculer score d'efficacité adversarial pipelineStats.adversarialMetrics.effectivenessScore = calculateAdversarialEffectiveness( pipelineStats, config, currentContent ); logSh(`🎯 PIPELINE ADVERSARIAL TERMINÉ: ${pipelineStats.elementsProcessed} éléments (${totalDuration}ms)`, 'INFO'); logSh(` 📊 Score efficacité: ${pipelineStats.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'INFO'); await tracer.event(`Pipeline adversarial terminé`, { ...pipelineStats, detectorTarget: config.detectorTarget, intensity: config.intensity }); return { content: currentContent, stats: pipelineStats, debug: { adversarialPipeline: true, detectorTarget: config.detectorTarget, intensity: config.intensity, stepsExecuted: Object.keys(config.steps).filter(k => config.steps[k]), detectorManager: detectorManager.getStrategyInfo() }, adversarialMetrics: pipelineStats.adversarialMetrics }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ PIPELINE ADVERSARIAL ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`AdversarialContentGeneration failed: ${error.message}`); } }, input); } /** * MODE SIMPLE ADVERSARIAL (équivalent à generateSimple mais adversarial) */ async function generateSimpleAdversarial(hierarchy, csvData, adversarialConfig = {}) { return await generateWithAdversarialContext({ hierarchy, csvData, adversarialConfig: { detectorTarget: 'general', intensity: 0.8, enableAllSteps: false, steps: { initial: true, technical: false, transitions: false, style: true }, ...adversarialConfig } }); } /** * MODE AVANCÉ ADVERSARIAL (configuration personnalisée) */ async function generateAdvancedAdversarial(hierarchy, csvData, options = {}) { const { detectorTarget = 'general', intensity = 1.0, technical = true, transitions = true, style = true, ...otherConfig } = options; return await generateWithAdversarialContext({ hierarchy, csvData, adversarialConfig: { detectorTarget, intensity, enableAdaptiveStrategy: true, contextualMode: true, steps: { initial: true, technical, transitions, style }, ...otherConfig } }); } /** * DIAGNOSTIC PIPELINE ADVERSARIAL */ async function diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) { logSh(`🔬 DIAGNOSTIC ADVERSARIAL: Testing ${detectorTargets.length} détecteurs`, 'INFO'); const results = {}; for (const target of detectorTargets) { try { logSh(` 🎯 Test détecteur: ${target}`, 'DEBUG'); const result = await generateWithAdversarialContext({ hierarchy, csvData, adversarialConfig: { detectorTarget: target, intensity: 1.0, enableAllSteps: true } }); results[target] = { success: true, content: result.content, stats: result.stats, effectivenessScore: result.adversarialMetrics.effectivenessScore }; logSh(` ✅ ${target}: Score ${result.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG'); } catch (error) { results[target] = { success: false, error: error.message, effectivenessScore: 0 }; logSh(` ❌ ${target}: Échec - ${error.message}`, 'ERROR'); } } return results; } // ============= HELPER FUNCTIONS ============= /** * Calculer efficacité adversariale */ function calculateAdversarialEffectiveness(pipelineStats, config, content) { let effectiveness = 0; // Base score selon intensité effectiveness += config.intensity * 30; // Bonus selon nombre d'étapes const stepsExecuted = Object.keys(config.steps).filter(k => config.steps[k]).length; effectiveness += stepsExecuted * 10; // Bonus selon prompts adversariaux générés const promptRatio = pipelineStats.adversarialMetrics.promptsGenerated / Math.max(1, pipelineStats.elementsProcessed); effectiveness += promptRatio * 20; // Analyse contenu si disponible if (Object.keys(content).length > 0) { const contentSample = Object.values(content).join(' ').substring(0, 1000); const diversityScore = analyzeDiversityScore(contentSample); effectiveness += diversityScore * 0.3; } return Math.min(100, Math.max(0, effectiveness)); } /** * Analyser score de diversité */ function analyzeDiversityScore(content) { if (!content || typeof content !== 'string') return 0; const words = content.split(/\s+/).filter(w => w.length > 2); if (words.length === 0) return 0; const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))]; const diversityRatio = uniqueWords.length / words.length; return diversityRatio * 100; } /** * Obtenir informations détecteurs supportés */ function getAdversarialDetectorInfo() { return getSupportedDetectors(); } /** * Comparer efficacité de différents détecteurs */ async function compareAdversarialStrategies(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality', 'winston']) { const results = await diagnosticAdversarialPipeline(hierarchy, csvData, detectorTargets); const comparison = { bestStrategy: null, bestScore: 0, strategies: [], averageScore: 0 }; let totalScore = 0; let successCount = 0; detectorTargets.forEach(target => { const result = results[target]; if (result.success) { const strategyInfo = { detector: target, effectivenessScore: result.effectivenessScore, duration: result.stats.totalDuration, elementsProcessed: result.stats.elementsProcessed }; comparison.strategies.push(strategyInfo); totalScore += result.effectivenessScore; successCount++; if (result.effectivenessScore > comparison.bestScore) { comparison.bestStrategy = target; comparison.bestScore = result.effectivenessScore; } } }); comparison.averageScore = successCount > 0 ? totalScore / successCount : 0; return comparison; } module.exports = { generateWithAdversarialContext, // ← MAIN ENTRY POINT generateSimpleAdversarial, generateAdvancedAdversarial, diagnosticAdversarialPipeline, compareAdversarialStrategies, getAdversarialDetectorInfo, calculateAdversarialEffectiveness }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/ComparisonFramework.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FRAMEWORK DE COMPARAISON ADVERSARIAL // Responsabilité: Comparer pipelines normales vs adversariales // Utilisation: A/B testing et validation efficacité anti-détection // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); // Pipelines à comparer const { generateWithContext } = require('../ContentGeneration'); // Pipeline normale const { generateWithAdversarialContext, compareAdversarialStrategies } = require('./ContentGenerationAdversarial'); // Pipeline adversariale /** * MAIN ENTRY POINT - COMPARAISON A/B PIPELINE * Compare pipeline normale vs adversariale sur même input */ async function compareNormalVsAdversarial(input, options = {}) { return await tracer.run('ComparisonFramework.compareNormalVsAdversarial()', async () => { const { hierarchy, csvData, adversarialConfig = {}, runBothPipelines = true, analyzeContent = true } = input; const { detectorTarget = 'general', intensity = 1.0, iterations = 1 } = options; await tracer.annotate({ comparisonType: 'normal_vs_adversarial', detectorTarget, intensity, iterations, elementsCount: Object.keys(hierarchy).length }); const startTime = Date.now(); logSh(`🆚 COMPARAISON A/B: Pipeline normale vs adversariale`, 'INFO'); logSh(` 🎯 Détecteur cible: ${detectorTarget} | Intensité: ${intensity} | Itérations: ${iterations}`, 'INFO'); const results = { normal: null, adversarial: null, comparison: null, iterations: [] }; try { for (let i = 0; i < iterations; i++) { logSh(`🔄 Itération ${i + 1}/${iterations}`, 'INFO'); const iterationResults = { iteration: i + 1, normal: null, adversarial: null, metrics: {} }; // ======================================== // PIPELINE NORMALE // ======================================== if (runBothPipelines) { logSh(` 📊 Génération pipeline normale...`, 'DEBUG'); const normalStartTime = Date.now(); try { const normalResult = await generateWithContext(hierarchy, csvData, { technical: true, transitions: true, style: true }); iterationResults.normal = { success: true, content: normalResult, duration: Date.now() - normalStartTime, elementsCount: Object.keys(normalResult).length }; logSh(` ✅ Pipeline normale: ${iterationResults.normal.elementsCount} éléments (${iterationResults.normal.duration}ms)`, 'DEBUG'); } catch (error) { iterationResults.normal = { success: false, error: error.message, duration: Date.now() - normalStartTime }; logSh(` ❌ Pipeline normale échouée: ${error.message}`, 'ERROR'); } } // ======================================== // PIPELINE ADVERSARIALE // ======================================== logSh(` 🎯 Génération pipeline adversariale...`, 'DEBUG'); const adversarialStartTime = Date.now(); try { const adversarialResult = await generateWithAdversarialContext({ hierarchy, csvData, adversarialConfig: { detectorTarget, intensity, enableAllSteps: true, ...adversarialConfig } }); iterationResults.adversarial = { success: true, content: adversarialResult.content, stats: adversarialResult.stats, adversarialMetrics: adversarialResult.adversarialMetrics, duration: Date.now() - adversarialStartTime, elementsCount: Object.keys(adversarialResult.content).length }; logSh(` ✅ Pipeline adversariale: ${iterationResults.adversarial.elementsCount} éléments (${iterationResults.adversarial.duration}ms)`, 'DEBUG'); logSh(` 📊 Score efficacité: ${adversarialResult.adversarialMetrics.effectivenessScore.toFixed(2)}%`, 'DEBUG'); } catch (error) { iterationResults.adversarial = { success: false, error: error.message, duration: Date.now() - adversarialStartTime }; logSh(` ❌ Pipeline adversariale échouée: ${error.message}`, 'ERROR'); } // ======================================== // ANALYSE COMPARATIVE ITÉRATION // ======================================== if (analyzeContent && iterationResults.normal?.success && iterationResults.adversarial?.success) { iterationResults.metrics = analyzeContentComparison( iterationResults.normal.content, iterationResults.adversarial.content ); logSh(` 📈 Diversité: Normal=${iterationResults.metrics.diversity.normal.toFixed(2)}% | Adversarial=${iterationResults.metrics.diversity.adversarial.toFixed(2)}%`, 'DEBUG'); } results.iterations.push(iterationResults); } // ======================================== // CONSOLIDATION RÉSULTATS // ======================================== const totalDuration = Date.now() - startTime; // Prendre les meilleurs résultats ou derniers si une seule itération const lastIteration = results.iterations[results.iterations.length - 1]; results.normal = lastIteration.normal; results.adversarial = lastIteration.adversarial; // Analyse comparative globale results.comparison = generateGlobalComparison(results.iterations, options); logSh(`🆚 COMPARAISON TERMINÉE: ${iterations} itérations (${totalDuration}ms)`, 'INFO'); if (results.comparison.winner) { logSh(`🏆 Gagnant: ${results.comparison.winner} (score: ${results.comparison.bestScore.toFixed(2)})`, 'INFO'); } await tracer.event('Comparaison A/B terminée', { iterations, winner: results.comparison.winner, totalDuration }); return results; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ COMPARAISON A/B ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`ComparisonFramework failed: ${error.message}`); } }, input); } /** * COMPARAISON MULTI-DÉTECTEURS */ async function compareMultiDetectors(hierarchy, csvData, detectorTargets = ['general', 'gptZero', 'originality']) { logSh(`🎯 COMPARAISON MULTI-DÉTECTEURS: ${detectorTargets.length} stratégies`, 'INFO'); const results = {}; const startTime = Date.now(); for (const detector of detectorTargets) { logSh(` 🔍 Test détecteur: ${detector}`, 'DEBUG'); try { const comparison = await compareNormalVsAdversarial({ hierarchy, csvData, adversarialConfig: { detectorTarget: detector } }, { detectorTarget: detector, intensity: 1.0, iterations: 1 }); results[detector] = { success: true, comparison, effectivenessGain: comparison.adversarial?.adversarialMetrics?.effectivenessScore || 0 }; logSh(` ✅ ${detector}: +${results[detector].effectivenessGain.toFixed(2)}% efficacité`, 'DEBUG'); } catch (error) { results[detector] = { success: false, error: error.message, effectivenessGain: 0 }; logSh(` ❌ ${detector}: Échec - ${error.message}`, 'ERROR'); } } // Analyse du meilleur détecteur const bestDetector = Object.keys(results).reduce((best, current) => { if (!results[best]?.success) return current; if (!results[current]?.success) return best; return results[current].effectivenessGain > results[best].effectivenessGain ? current : best; }); const totalDuration = Date.now() - startTime; logSh(`🎯 MULTI-DÉTECTEURS TERMINÉ: Meilleur=${bestDetector} (${totalDuration}ms)`, 'INFO'); return { results, bestDetector, bestScore: results[bestDetector]?.effectivenessGain || 0, totalDuration }; } /** * BENCHMARK PERFORMANCE */ async function benchmarkPerformance(hierarchy, csvData, configurations = []) { const defaultConfigs = [ { name: 'Normal', type: 'normal' }, { name: 'Simple Adversarial', type: 'adversarial', detectorTarget: 'general', intensity: 0.5 }, { name: 'Intense Adversarial', type: 'adversarial', detectorTarget: 'gptZero', intensity: 1.0 }, { name: 'Max Adversarial', type: 'adversarial', detectorTarget: 'originality', intensity: 1.5 } ]; const configs = configurations.length > 0 ? configurations : defaultConfigs; logSh(`⚡ BENCHMARK PERFORMANCE: ${configs.length} configurations`, 'INFO'); const results = []; for (const config of configs) { logSh(` 🔧 Test: ${config.name}`, 'DEBUG'); const startTime = Date.now(); try { let result; if (config.type === 'normal') { result = await generateWithContext(hierarchy, csvData); } else { const adversarialResult = await generateWithAdversarialContext({ hierarchy, csvData, adversarialConfig: { detectorTarget: config.detectorTarget || 'general', intensity: config.intensity || 1.0 } }); result = adversarialResult.content; } const duration = Date.now() - startTime; results.push({ name: config.name, type: config.type, success: true, duration, elementsCount: Object.keys(result).length, performance: Object.keys(result).length / (duration / 1000) // éléments par seconde }); logSh(` ✅ ${config.name}: ${Object.keys(result).length} éléments (${duration}ms)`, 'DEBUG'); } catch (error) { results.push({ name: config.name, type: config.type, success: false, error: error.message, duration: Date.now() - startTime }); logSh(` ❌ ${config.name}: Échec - ${error.message}`, 'ERROR'); } } // Analyser les résultats const successfulResults = results.filter(r => r.success); const fastest = successfulResults.reduce((best, current) => current.duration < best.duration ? current : best, successfulResults[0]); const mostEfficient = successfulResults.reduce((best, current) => current.performance > best.performance ? current : best, successfulResults[0]); logSh(`⚡ BENCHMARK TERMINÉ: Fastest=${fastest?.name} | Most efficient=${mostEfficient?.name}`, 'INFO'); return { results, fastest, mostEfficient, summary: { totalConfigs: configs.length, successful: successfulResults.length, failed: results.length - successfulResults.length } }; } // ============= HELPER FUNCTIONS ============= /** * Analyser différences de contenu entre normal et adversarial */ function analyzeContentComparison(normalContent, adversarialContent) { const metrics = { diversity: { normal: analyzeDiversityScore(Object.values(normalContent).join(' ')), adversarial: analyzeDiversityScore(Object.values(adversarialContent).join(' ')) }, length: { normal: Object.values(normalContent).join(' ').length, adversarial: Object.values(adversarialContent).join(' ').length }, elementsCount: { normal: Object.keys(normalContent).length, adversarial: Object.keys(adversarialContent).length }, differences: compareContentElements(normalContent, adversarialContent) }; return metrics; } /** * Score de diversité lexicale */ function analyzeDiversityScore(content) { if (!content || typeof content !== 'string') return 0; const words = content.split(/\s+/).filter(w => w.length > 2); if (words.length === 0) return 0; const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))]; return (uniqueWords.length / words.length) * 100; } /** * Comparer éléments de contenu */ function compareContentElements(normalContent, adversarialContent) { const differences = { modified: 0, identical: 0, totalElements: Math.max(Object.keys(normalContent).length, Object.keys(adversarialContent).length) }; const allTags = [...new Set([...Object.keys(normalContent), ...Object.keys(adversarialContent)])]; allTags.forEach(tag => { if (normalContent[tag] && adversarialContent[tag]) { if (normalContent[tag] === adversarialContent[tag]) { differences.identical++; } else { differences.modified++; } } }); differences.modificationRate = differences.totalElements > 0 ? (differences.modified / differences.totalElements) * 100 : 0; return differences; } /** * Générer analyse comparative globale */ function generateGlobalComparison(iterations, options) { const successfulIterations = iterations.filter(it => it.normal?.success && it.adversarial?.success); if (successfulIterations.length === 0) { return { winner: null, bestScore: 0, summary: 'Aucune itération réussie' }; } // Moyenner les métriques const avgMetrics = { diversity: { normal: 0, adversarial: 0 }, performance: { normal: 0, adversarial: 0 } }; successfulIterations.forEach(iteration => { if (iteration.metrics) { avgMetrics.diversity.normal += iteration.metrics.diversity.normal; avgMetrics.diversity.adversarial += iteration.metrics.diversity.adversarial; } avgMetrics.performance.normal += iteration.normal.elementsCount / (iteration.normal.duration / 1000); avgMetrics.performance.adversarial += iteration.adversarial.elementsCount / (iteration.adversarial.duration / 1000); }); const iterCount = successfulIterations.length; avgMetrics.diversity.normal /= iterCount; avgMetrics.diversity.adversarial /= iterCount; avgMetrics.performance.normal /= iterCount; avgMetrics.performance.adversarial /= iterCount; // Déterminer le gagnant const diversityGain = avgMetrics.diversity.adversarial - avgMetrics.diversity.normal; const performanceLoss = avgMetrics.performance.normal - avgMetrics.performance.adversarial; // Score composite (favorise diversité avec pénalité performance) const adversarialScore = diversityGain * 2 - (performanceLoss * 0.5); return { winner: adversarialScore > 5 ? 'adversarial' : 'normal', bestScore: Math.max(avgMetrics.diversity.normal, avgMetrics.diversity.adversarial), diversityGain, performanceLoss, avgMetrics, summary: `Diversité: +${diversityGain.toFixed(2)}%, Performance: ${performanceLoss > 0 ? '-' : '+'}${Math.abs(performanceLoss).toFixed(2)} elem/s` }; } module.exports = { compareNormalVsAdversarial, // ← MAIN ENTRY POINT compareMultiDetectors, benchmarkPerformance, analyzeContentComparison, analyzeDiversityScore }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/adversarial-generation/demo-modulaire.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // DÉMONSTRATION ARCHITECTURE MODULAIRE // Usage: node lib/adversarial-generation/demo-modulaire.js // Objectif: Valider l'intégration modulaire adversariale // ======================================== const { logSh } = require('../ErrorReporting'); // Import modules adversariaux modulaires const { applyAdversarialLayer } = require('./AdversarialCore'); const { applyPredefinedStack, applyAdaptiveLayers, getAvailableStacks } = require('./AdversarialLayers'); const { calculateAntiDetectionScore, evaluateAdversarialImprovement } = require('./AdversarialUtils'); /** * EXEMPLE D'UTILISATION MODULAIRE */ async function demoModularAdversarial() { console.log('\n🎯 === DÉMONSTRATION ADVERSARIAL MODULAIRE ===\n'); // Contenu d'exemple (simulé contenu généré normal) const exempleContenu = { '|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée', '|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu optimal pour votre entreprise. Cette solution comprehensive permet de créer une identité visuelle robuste et seamless.', '|Texte_1|': 'Il est important de noter que les matériaux utilisés sont cutting-edge. Par ailleurs, la qualité est optimal. En effet, nos solutions sont comprehensive et robust.', '|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?', '|FAQ_Reponse_1|': 'Nos matériaux sont optimal : dibond, aluminium, PMMA. Ces solutions comprehensive garantissent une qualité robust et seamless.' }; console.log('📊 CONTENU ORIGINAL:'); Object.entries(exempleContenu).forEach(([tag, content]) => { console.log(` ${tag}: "${content.substring(0, 60)}..."`); }); // Analyser contenu original const scoreOriginal = calculateAntiDetectionScore(Object.values(exempleContenu).join(' ')); console.log(`\n📈 Score anti-détection original: ${scoreOriginal}/100`); try { // ======================================== // TEST 1: COUCHE SIMPLE // ======================================== console.log('\n🔧 TEST 1: Application couche adversariale simple'); const result1 = await applyAdversarialLayer(exempleContenu, { detectorTarget: 'general', intensity: 0.8, method: 'enhancement' }); console.log(`✅ Résultat: ${result1.stats.elementsModified}/${result1.stats.elementsProcessed} éléments modifiés`); const scoreAmeliore = calculateAntiDetectionScore(Object.values(result1.content).join(' ')); console.log(`📈 Score anti-détection amélioré: ${scoreAmeliore}/100 (+${scoreAmeliore - scoreOriginal})`); // ======================================== // TEST 2: STACK PRÉDÉFINI // ======================================== console.log('\n📦 TEST 2: Application stack prédéfini'); // Lister stacks disponibles const stacks = getAvailableStacks(); console.log(' Stacks disponibles:'); stacks.forEach(stack => { console.log(` - ${stack.name}: ${stack.description} (${stack.layersCount} couches)`); }); const result2 = await applyPredefinedStack(exempleContenu, 'standardDefense', { csvData: { personality: { nom: 'Marc', style: 'technique' }, mc0: 'plaque personnalisée' } }); console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`); console.log(` 📊 Couches appliquées: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length}`); const scoreStack = calculateAntiDetectionScore(Object.values(result2.content).join(' ')); console.log(`📈 Score anti-détection stack: ${scoreStack}/100 (+${scoreStack - scoreOriginal})`); // ======================================== // TEST 3: COUCHES ADAPTATIVES // ======================================== console.log('\n🧠 TEST 3: Application couches adaptatives'); const result3 = await applyAdaptiveLayers(exempleContenu, { targetDetectors: ['gptZero', 'originality'], maxIntensity: 1.2 }); if (result3.stats.adaptive) { console.log(`✅ Adaptatif: ${result3.stats.layersApplied || result3.stats.totalModifications} modifications`); const scoreAdaptatif = calculateAntiDetectionScore(Object.values(result3.content).join(' ')); console.log(`📈 Score anti-détection adaptatif: ${scoreAdaptatif}/100 (+${scoreAdaptatif - scoreOriginal})`); } // ======================================== // COMPARAISON FINALE // ======================================== console.log('\n📊 COMPARAISON FINALE:'); const evaluation = evaluateAdversarialImprovement( Object.values(exempleContenu).join(' '), Object.values(result2.content).join(' '), 'general' ); console.log(` 🔹 Réduction empreintes IA: ${evaluation.fingerprintReduction.toFixed(2)}%`); console.log(` 🔹 Augmentation diversité: ${evaluation.diversityIncrease.toFixed(2)}%`); console.log(` 🔹 Amélioration variation: ${evaluation.variationIncrease.toFixed(2)}%`); console.log(` 🔹 Score amélioration global: ${evaluation.improvementScore}`); console.log(` 🔹 Taux modification: ${evaluation.modificationRate.toFixed(2)}%`); console.log(` 💡 Recommandation: ${evaluation.recommendation}`); // ======================================== // EXEMPLES DE CONTENU TRANSFORMÉ // ======================================== console.log('\n✨ EXEMPLES DE TRANSFORMATION:'); const exempleTransforme = result2.content['|Introduction_1|'] || result1.content['|Introduction_1|']; console.log('\n📝 AVANT:'); console.log(` "${exempleContenu['|Introduction_1|']}"`); console.log('\n📝 APRÈS:'); console.log(` "${exempleTransforme}"`); console.log('\n✅ === DÉMONSTRATION MODULAIRE TERMINÉE ===\n'); return { success: true, originalScore: scoreOriginal, improvedScore: Math.max(scoreAmeliore, scoreStack), improvement: evaluation.improvementScore }; } catch (error) { console.error('\n❌ ERREUR DÉMONSTRATION:', error.message); return { success: false, error: error.message }; } } /** * EXEMPLE D'INTÉGRATION AVEC PIPELINE NORMALE */ async function demoIntegrationPipeline() { console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n'); // Simuler résultat pipeline normale (Level 1) const contenuNormal = { '|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles', '|Intro_1|': 'Notre expertise en signalétique permet de créer des plaques sur mesure adaptées à vos besoins spécifiques.', '|Texte_1|': 'Les matériaux proposés incluent l\'aluminium, le dibond et le PMMA. Chaque solution présente des avantages particuliers selon l\'usage prévu.' }; console.log('💼 SCÉNARIO: Application adversarial post-pipeline normale'); try { // Exemple Level 6 - Post-processing adversarial console.log('\n🎯 Étape 1: Contenu généré par pipeline normale'); console.log(' ✅ Contenu de base: qualité préservée'); console.log('\n🎯 Étape 2: Application couche adversariale modulaire'); const resultAdversarial = await applyAdversarialLayer(contenuNormal, { detectorTarget: 'gptZero', intensity: 0.9, method: 'hybrid', preserveStructure: true }); console.log(` ✅ Couche adversariale: ${resultAdversarial.stats.elementsModified} éléments modifiés`); console.log('\n📊 RÉSULTAT FINAL:'); Object.entries(resultAdversarial.content).forEach(([tag, content]) => { console.log(` ${tag}:`); console.log(` AVANT: "${contenuNormal[tag]}"`); console.log(` APRÈS: "${content}"`); console.log(''); }); return { success: true, result: resultAdversarial }; } catch (error) { console.error('❌ ERREUR INTÉGRATION:', error.message); return { success: false, error: error.message }; } } // Exécuter démonstrations si fichier appelé directement if (require.main === module) { (async () => { await demoModularAdversarial(); await demoIntegrationPipeline(); })().catch(console.error); } module.exports = { demoModularAdversarial, demoIntegrationPipeline }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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`; } /** * STATISTIQUES ET RAPPORTS */ /** * Générer rapport amélioration */ function generateImprovementReport(originalContent, enhancedContent, layerType = 'general') { const report = { layerType, timestamp: new Date().toISOString(), summary: { elementsProcessed: 0, elementsImproved: 0, averageImprovement: 0, totalExecutionTime: 0 }, details: { byElement: [], qualityMetrics: {}, recommendations: [] } }; // Analyser chaque élément Object.keys(originalContent).forEach(tag => { const original = originalContent[tag]; const enhanced = enhancedContent[tag]; if (original && enhanced) { report.summary.elementsProcessed++; const improvement = compareContentImprovement(original, enhanced, layerType); if (improvement.improvementRate > 0) { report.summary.elementsImproved++; } report.summary.averageImprovement += improvement.improvementRate; report.details.byElement.push({ tag, improvementRate: improvement.improvementRate, lengthChange: improvement.details.lengthChange, contentPreserved: improvement.details.contentPreserved }); } }); // Calculer moyennes if (report.summary.elementsProcessed > 0) { report.summary.averageImprovement = report.summary.averageImprovement / report.summary.elementsProcessed; } // Métriques qualité globales const fullOriginal = Object.values(originalContent).join(' '); const fullEnhanced = Object.values(enhancedContent).join(' '); report.details.qualityMetrics = { technical: analyzeTechnicalQuality(fullEnhanced), transitions: analyzeTransitionFluidity(fullEnhanced), style: analyzeStyleConsistency(fullEnhanced) }; // Recommandations if (report.summary.averageImprovement < 10) { report.details.recommendations.push('Augmenter l\'intensité d\'amélioration'); } if (report.details.byElement.some(e => !e.contentPreserved)) { report.details.recommendations.push('Améliorer préservation du contenu original'); } return report; } module.exports = { // Analyseurs analyzeTechnicalQuality, analyzeTransitionFluidity, analyzeStyleConsistency, // Comparateurs compareContentImprovement, // Utilitaires contenu cleanGeneratedContent, validateSelectiveContent, // Utilitaires techniques chunkArray, sleep, measurePerformance, formatDuration, // Rapports generateImprovementReport }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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'); const results = {}; const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4 for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; try { logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config); const response = await callLLM(config.llmProvider, enhancementPrompt, { temperature: 0.4, // Précision technique maxTokens: 3000 }, csvData?.personality); const chunkResults = this.parseTechnicalResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); // Fallback: conserver contenu original chunk.forEach(element => { results[element.tag] = element.content; }); } } return results; } /** * AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT */ async enhanceAllElementsDirect(content, csvData, config) { const allElements = Object.entries(content).map(([tag, text]) => ({ tag, content: text, technicalTerms: [], improvements: ['amélioration_générale_technique'] })); return await this.enhanceTechnicalElements(allElements, csvData, config); } // ============= HELPER METHODS ============= /** * Analyser élément technique individuel */ analyzeTechnicalElement(text, contextualTerms, csvData) { let score = 0; const foundTerms = []; const missingTerms = []; const improvements = []; // 1. Détecter termes techniques présents contextualTerms.forEach(term => { if (text.toLowerCase().includes(term.toLowerCase())) { foundTerms.push(term); } else if (text.length > 100) { // Seulement pour textes longs missingTerms.push(term); } }); // 2. Évaluer manque de précision technique if (foundTerms.length === 0 && text.length > 80) { score += 0.4; improvements.push('ajout_termes_techniques'); } // 3. Détecter vocabulaire trop générique const genericWords = ['produit', 'solution', 'service', 'offre', 'article']; const genericCount = genericWords.filter(word => text.toLowerCase().includes(word) ).length; if (genericCount > 1) { score += 0.3; improvements.push('spécialisation_vocabulaire'); } // 4. Manque de données techniques (dimensions, etc.) if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) { score += 0.2; improvements.push('ajout_données_techniques'); } // 5. Contexte métier spécifique if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) { score += 0.1; improvements.push('intégration_contexte_métier'); } return { needsImprovement: score > 0.3, score, foundTerms, missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants improvements }; } /** * Obtenir termes techniques contextuels */ getContextualTechnicalTerms(mc0, targetTerms) { // Termes de base signalétique const baseTerms = [ 'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas', 'impression', 'gravure', 'découpe', 'fraisage', 'perçage', 'adhésif', 'fixation', 'visserie', 'support' ]; // Termes spécifiques selon contexte const contextualTerms = []; if (mc0) { const mc0Lower = mc0.toLowerCase(); if (mc0Lower.includes('plaque')) { contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation'); } if (mc0Lower.includes('signalétique')) { contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité'); } if (mc0Lower.includes('personnalisée')) { contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone'); } } // Ajouter termes ciblés si fournis if (targetTerms && Array.isArray(targetTerms)) { contextualTerms.push(...targetTerms); } return [...baseTerms, ...contextualTerms]; } /** * Créer prompt amélioration technique */ createTechnicalEnhancementPrompt(chunk, csvData, config) { const personality = csvData?.personality; let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus. CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique ${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''} INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif) ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} CONTENU: "${item.content}" AMÉLIORATIONS: ${item.improvements.join(', ')} ${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')} CONSIGNES TECHNIQUES: - GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''} - AJOUTE précision technique naturelle et vocabulaire spécialisé - INTÈGRE termes métier : matériaux, procédés, normes, dimensions - REMPLACE vocabulaire générique par termes techniques appropriés - ÉVITE jargon incompréhensible, reste accessible - PRESERVE longueur approximative (±15%) VOCABULAIRE TECHNIQUE RECOMMANDÉ: - Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé - Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC - Finitions: brossé, poli, texturé, laqué - Fixations: perçage, adhésif double face, vis inox, plots de fixation FORMAT RÉPONSE: [1] Contenu avec amélioration technique précise [2] Contenu avec amélioration technique précise etc... IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`; return prompt; } /** * Parser réponse technique */ parseTechnicalResponse(response, chunk) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < chunk.length) { let technicalContent = match[2].trim(); const element = chunk[index]; // Nettoyer contenu technique technicalContent = this.cleanTechnicalContent(technicalContent); if (technicalContent && technicalContent.length > 10) { results[element.tag] = technicalContent; logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG'); } else { results[element.tag] = element.content; // Fallback logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING'); } index++; } // Compléter les manquants while (index < chunk.length) { const element = chunk[index]; results[element.tag] = element.content; index++; } return results; } /** * Nettoyer contenu technique généré */ cleanTechnicalContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, ''); content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, ''); content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, ''); // Nettoyer formatage content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown content = content.replace(/\s{2,}/g, ' '); // Espaces multiples content = content.trim(); return content; } /** * Compter différences entre contenus */ countDifferences(original, enhanced) { let count = 0; Object.keys(original).forEach(tag => { if (enhanced[tag] && enhanced[tag] !== original[tag]) { count++; } }); return count; } } module.exports = { TechnicalLayer }; /* ┌────────────────────────────────────────────────────────────────────┐ │ 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/main_modulaire.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'); const { generateSimple } = require('./ContentGeneration'); const { injectGeneratedContent } = require('./ContentAssembly'); const { compileAndStoreArticle } = 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'); /** * WORKFLOW MODULAIRE PRINCIPAL */ async function handleModularWorkflow(config = {}) { return await tracer.run('MainModulaire.handleModularWorkflow()', async () => { const { rowNumber = 2, selectiveStack = 'standardEnhancement', // lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive adversarialMode = 'light', // none, light, standard, heavy, adaptive source = 'main_modulaire' } = config; await tracer.annotate({ modularWorkflow: true, rowNumber, selectiveStack, adversarialMode, source }); const startTime = Date.now(); logSh(`🚀 WORKFLOW MODULAIRE DÉMARRÉ`, 'INFO'); logSh(` 📊 Ligne: ${rowNumber} | Selective: ${selectiveStack} | Adversarial: ${adversarialMode}`, 'INFO'); try { // ======================================== // PHASE 1: PRÉPARATION DONNÉES // ======================================== logSh(`📋 PHASE 1: Préparation données`, 'INFO'); const csvData = await readInstructionsData(rowNumber); if (!csvData) { throw new Error(`Impossible de lire les données ligne ${rowNumber}`); } const personalities = await getPersonalities(); const selectedPersonality = await selectPersonalityWithAI( csvData.mc0, csvData.t0, personalities ); csvData.personality = selectedPersonality; logSh(` ✅ Données: ${csvData.mc0} | Personnalité: ${selectedPersonality.nom}`, 'DEBUG'); // ======================================== // PHASE 2: EXTRACTION ÉLÉMENTS // ======================================== logSh(`📝 PHASE 2: Extraction éléments XML`, 'INFO'); const elements = await 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'); // ======================================== // PHASE 6: SELECTIVE ENHANCEMENT MODULAIRE // ======================================== logSh(`🔧 PHASE 6: Selective Enhancement Modulaire (${selectiveStack})`, 'INFO'); let selectiveResult; switch (selectiveStack) { case 'adaptive': selectiveResult = await applyAdaptiveLayers(generatedContent, { maxIntensity: 1.1, analysisThreshold: 0.3, csvData }); break; case 'technical': case 'transitions': case 'style': selectiveResult = await applySelectiveLayer(generatedContent, { layerType: selectiveStack, llmProvider: 'auto', intensity: 1.0, csvData }); break; default: // Stack prédéfini selectiveResult = await applyPredefinedStack(generatedContent, selectiveStack, { csvData, analysisMode: true }); } const enhancedContent = selectiveResult.content; logSh(` ✅ Selective: ${selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0} améliorations`, 'INFO'); // ======================================== // PHASE 7: ADVERSARIAL ENHANCEMENT (OPTIONNEL) // ======================================== let finalContent = enhancedContent; let adversarialStats = null; if (adversarialMode !== 'none') { logSh(`🎯 PHASE 7: Adversarial Enhancement (${adversarialMode})`, 'INFO'); let adversarialResult; switch (adversarialMode) { case 'adaptive': // Utiliser adversarial adaptatif adversarialResult = await applyAdversarialLayer(enhancedContent, { detectorTarget: 'general', method: 'hybrid', intensity: 0.8, analysisMode: true }); break; case 'light': case 'standard': case 'heavy': // Utiliser stack adversarial prédéfini const stackMapping = { light: 'lightDefense', standard: 'standardDefense', heavy: 'heavyDefense' }; adversarialResult = await applyAdversarialStack(enhancedContent, stackMapping[adversarialMode], { csvData }); break; } if (adversarialResult && !adversarialResult.fallback) { finalContent = adversarialResult.content; adversarialStats = adversarialResult.stats; logSh(` ✅ Adversarial: ${adversarialStats.elementsModified || adversarialStats.totalModifications || 0} modifications`, 'INFO'); } else { logSh(` ⚠️ Adversarial fallback: contenu selective préservé`, 'WARNING'); } } // ======================================== // PHASE 8: ASSEMBLAGE ET STOCKAGE // ======================================== logSh(`🔗 PHASE 8: Assemblage et stockage`, 'INFO'); const assembledContent = await injectGeneratedContent(finalContent, csvData.xmlTemplate); const storageResult = await compileAndStoreArticle(assembledContent, { ...csvData, source: `${source}_${selectiveStack}${adversarialMode !== 'none' ? `_${adversarialMode}` : ''}` }); logSh(` ✅ Stocké: ${storageResult.compiledLength} caractères`, 'DEBUG'); // ======================================== // RÉSUMÉ FINAL // ======================================== const totalDuration = Date.now() - startTime; const finalStats = { rowNumber, selectiveStack, adversarialMode, totalDuration, elementsGenerated: Object.keys(generatedContent).length, selectiveEnhancements: selectiveResult.stats.elementsEnhanced || selectiveResult.stats.totalModifications || 0, adversarialModifications: adversarialStats?.elementsModified || adversarialStats?.totalModifications || 0, finalLength: storageResult.compiledLength, personality: selectedPersonality.nom, source }; logSh(`✅ WORKFLOW MODULAIRE TERMINÉ (${totalDuration}ms)`, 'INFO'); logSh(` 📊 ${finalStats.elementsGenerated} générés | ${finalStats.selectiveEnhancements} selective | ${finalStats.adversarialModifications} adversarial`, 'INFO'); logSh(` 🎭 Personnalité: ${finalStats.personality} | Taille finale: ${finalStats.finalLength} chars`, 'INFO'); await tracer.event('Workflow modulaire terminé', finalStats); return { success: true, stats: finalStats, content: finalContent, assembledContent, storageResult, selectiveResult, adversarialResult: adversarialStats ? { stats: adversarialStats } : null }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ WORKFLOW MODULAIRE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR'); logSh(`Stack trace: ${error.stack}`, 'ERROR'); await tracer.event('Workflow modulaire échoué', { error: error.message, duration, rowNumber, selectiveStack, adversarialMode }); throw error; } }, { config }); } /** * BENCHMARK COMPARATIF STACKS */ async function benchmarkStacks(rowNumber = 2) { console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n'); const stacks = getAvailableStacks(); const adversarialModes = ['none', 'light', 'standard']; const results = []; for (const stack of stacks.slice(0, 3)) { // Tester 3 stacks principaux for (const advMode of adversarialModes.slice(0, 2)) { // 2 modes adversarial console.log(`🧪 Test: ${stack.name} + adversarial ${advMode}`); try { const startTime = Date.now(); const result = await handleModularWorkflow({ rowNumber, selectiveStack: stack.name, adversarialMode: advMode, source: 'benchmark' }); const duration = Date.now() - startTime; results.push({ stack: stack.name, adversarial: advMode, duration, success: true, selectiveEnhancements: result.stats.selectiveEnhancements, adversarialModifications: result.stats.adversarialModifications, finalLength: result.stats.finalLength }); console.log(` ✅ ${duration}ms | ${result.stats.selectiveEnhancements} selective | ${result.stats.adversarialModifications} adversarial`); } catch (error) { results.push({ stack: stack.name, adversarial: advMode, success: false, error: error.message }); console.log(` ❌ Échoué: ${error.message}`); } } } // Résumé benchmark console.log('\n📊 RÉSUMÉ BENCHMARK:'); const successful = results.filter(r => r.success); if (successful.length > 0) { const avgDuration = successful.reduce((sum, r) => sum + r.duration, 0) / successful.length; const bestPerf = successful.reduce((best, r) => r.duration < best.duration ? r : best); const mostEnhancements = successful.reduce((best, r) => (r.selectiveEnhancements + r.adversarialModifications) > (best.selectiveEnhancements + best.adversarialModifications) ? r : best ); console.log(` ⚡ Durée moyenne: ${avgDuration.toFixed(0)}ms`); console.log(` 🏆 Meilleure perf: ${bestPerf.stack} + ${bestPerf.adversarial} (${bestPerf.duration}ms)`); console.log(` 🔥 Plus d'améliorations: ${mostEnhancements.stack} + ${mostEnhancements.adversarial} (${mostEnhancements.selectiveEnhancements + mostEnhancements.adversarialModifications})`); } return results; } /** * INTERFACE LIGNE DE COMMANDE */ async function main() { const args = process.argv.slice(2); const command = args[0] || 'workflow'; try { switch (command) { case 'workflow': const rowNumber = parseInt(args[1]) || 2; const selectiveStack = args[2] || 'standardEnhancement'; const adversarialMode = args[3] || 'light'; console.log(`\n🚀 Exécution workflow modulaire:`); console.log(` 📊 Ligne: ${rowNumber}`); console.log(` 🔧 Stack selective: ${selectiveStack}`); console.log(` 🎯 Mode adversarial: ${adversarialMode}`); const result = await handleModularWorkflow({ rowNumber, selectiveStack, adversarialMode }); console.log('\n✅ WORKFLOW MODULAIRE RÉUSSI'); console.log(`📈 Stats: ${JSON.stringify(result.stats, null, 2)}`); break; case 'benchmark': const benchRowNumber = parseInt(args[1]) || 2; console.log(`\n⚡ Benchmark stacks (ligne ${benchRowNumber})`); const benchResults = await benchmarkStacks(benchRowNumber); console.log('\n📊 Résultats complets:'); console.table(benchResults); break; case 'stacks': console.log('\n📦 STACKS SELECTIVE DISPONIBLES:'); const availableStacks = getAvailableStacks(); availableStacks.forEach(stack => { console.log(`\n 🔧 ${stack.name}:`); console.log(` 📝 ${stack.description}`); console.log(` 📊 ${stack.layersCount} couches`); console.log(` 🎯 Couches: ${stack.layers ? stack.layers.map(l => `${l.type}(${l.llm})`).join(' → ') : 'N/A'}`); }); console.log('\n🎯 MODES ADVERSARIAL DISPONIBLES:'); console.log(' - none: Pas d\'adversarial'); console.log(' - light: Défense légère'); console.log(' - standard: Défense standard'); console.log(' - heavy: Défense intensive'); console.log(' - adaptive: Adaptatif intelligent'); break; case 'help': default: console.log('\n🔧 === MAIN MODULAIRE - USAGE ==='); console.log('\nCommandes disponibles:'); console.log(' workflow [ligne] [stack] [adversarial] - Exécuter workflow complet'); console.log(' benchmark [ligne] - Benchmark stacks'); console.log(' stacks - Lister stacks disponibles'); console.log(' help - Afficher cette aide'); console.log('\nExemples:'); console.log(' node main_modulaire.js workflow 2 fullEnhancement standard'); console.log(' node main_modulaire.js workflow 3 adaptive light'); console.log(' node main_modulaire.js benchmark 2'); console.log(' node main_modulaire.js stacks'); break; } } catch (error) { console.error('\n❌ ERREUR MAIN MODULAIRE:', error.message); console.error(error.stack); process.exit(1); } } // Export pour usage programmatique module.exports = { handleModularWorkflow, benchmarkStacks }; // Exécution CLI si appelé directement if (require.main === module) { main().catch(error => { console.error('❌ ERREUR FATALE:', error.message); process.exit(1); }); } /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ManualTrigger.js │ └────────────────────────────────────────────────────────────────────┘ */ const { logSh } = require('./ErrorReporting'); // Using unified logSh from ErrorReporting /** * 🚀 TRIGGER MANUEL - Lit ligne 2 et lance le workflow * Exécute cette fonction depuis l'éditeur Apps Script */ function runWorkflowLigne(numeroLigne = 2) { cleanLogSheet(); // Nettoie les logs pour ce test try { logSh('🎬 >>> DÉMARRAGE WORKFLOW MANUEL <<<', 'INFO'); // 1. LIRE AUTOMATIQUEMENT LA LIGNE INDIQUÉ const csvData = readCSVDataFromRow(numeroLigne); logSh(`✅ Données lues - MC0: ${csvData.mc0}`, 'INFO'); logSh(`✅ Titre: ${csvData.t0}`, 'INFO'); logSh(`✅ Personnalité: ${csvData.personality.nom}`, 'INFO'); // 2. XML TEMPLATE SIMPLE POUR TEST (ou lit depuis Digital Ocean si configuré) const xmlTemplate = getXMLTemplateForTest(csvData); logSh(`✅ XML template: ${xmlTemplate.length} caractères`, 'INFO'); // 3. 🎯 LANCER LE WORKFLOW PRINCIPAL const workflowData = { csvData: csvData, xmlTemplate: Utilities.base64Encode(xmlTemplate), source: 'manuel_ligne2' }; const result = handleFullWorkflow(workflowData); logSh('🏆 === WORKFLOW MANUEL TERMINÉ ===', 'INFO'); // ← EXTRAIRE LES VRAIES DONNÉES let actualData; if (result && result.getContentText) { // C'est un ContentService, extraire le JSON actualData = JSON.parse(result.getContentText()); } else { actualData = result; } logSh(`Type result: ${typeof result}`, 'DEBUG'); logSh(`Result keys: ${Object.keys(result || {})}`, 'DEBUG'); logSh(`ActualData keys: ${Object.keys(actualData || {})}`, 'DEBUG'); logSh(`ActualData: ${JSON.stringify(actualData)}`, 'DEBUG'); if (actualData && actualData.stats) { logSh(`📊 Éléments générés: ${actualData.stats.contentPieces}`, 'INFO'); logSh(`📝 Nombre de mots: ${actualData.stats.wordCount}`, 'INFO'); } else { logSh('⚠️ Format résultat inattendu', 'WARNING'); logSh('ActualData: ' + JSON.stringify(actualData, null, 2), 'DEBUG'); // Using logSh instead of console.log } return actualData; } catch (error) { logSh(`❌ ERREUR WORKFLOW MANUEL: ${error.toString()}`, 'ERROR'); logSh(`Stack: ${error.stack}`, 'ERROR'); throw error; } } /** * HELPER - Lire CSV depuis une ligne spécifique */ function readCSVDataFromRow(rowNumber) { const sheetId = '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'; const spreadsheet = SpreadsheetApp.openById(sheetId); const articlesSheet = spreadsheet.getSheetByName('instructions'); // Lire la ligne complète (colonnes A à H) const range = articlesSheet.getRange(rowNumber, 1, 1, 9); const [slug, t0, mc0, tMinus1, lMinus1, mcPlus1, tPlus1, lPlus1, xmlFileName] = range.getValues()[0]; logSh(`📖 Lecture ligne ${rowNumber}: ${slug}`, 'DEBUG'); // Récupérer personnalités et sélectionner automatiquement const personalitiesSheet = spreadsheet.getSheetByName('Personnalites'); const personalities = getPersonalities(personalitiesSheet); const selectedPersonality = selectPersonalityWithAI(mc0, t0, personalities); return { rowNumber: rowNumber, slug: slug || 'test-slug', t0: t0 || 'Titre par défaut', mc0: mc0 || 'mot-clé test', tMinus1: tMinus1 || 'parent', lMinus1: lMinus1 || '/parent', mcPlus1: mcPlus1 || 'mot1,mot2,mot3,mot4', tPlus1: tPlus1 || 'Titre1,Titre2,Titre3,Titre4', lPlus1: lPlus1 || '/lien1,/lien2,/lien3,/lien4', personality: selectedPersonality, xmlFileName: xmlFileName ? xmlFileName.toString().trim() : null }; } /** * HELPER - XML Template simple pour test (ou depuis Digital Ocean) */ function getXMLTemplateForTest(csvData) { logSh("csvData.xmlFileName: " + csvData.xmlFileName, 'DEBUG'); // Using logSh instead of console.log if (csvData.xmlFileName) { logSh("Tentative Digital Ocean...", 'INFO'); // Using logSh instead of console.log try { return fetchXMLFromDigitalOceanSimple(csvData.xmlFileName); } catch (error) { // ← ENLÈVE LE CATCH SILENCIEUX logSh("Erreur DO: " + error.toString(), 'WARNING'); // Using logSh instead of console.log logSh(`❌ ERREUR DO DÉTAILLÉE: ${error.toString()}`, 'ERROR'); // Continue sans Digital Ocean } } logSh("❌ FATAL: Aucun template XML disponible", 'ERROR'); throw new Error("FATAL: Template XML indisponible (Digital Ocean inaccessible + pas de fallback) - arrêt du workflow"); } /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/selective-enhancement/demo-modulaire.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // DÉMONSTRATION ARCHITECTURE MODULAIRE SELECTIVE // Usage: node lib/selective-enhancement/demo-modulaire.js // Objectif: Valider l'intégration modulaire selective enhancement // ======================================== const { logSh } = require('../ErrorReporting'); // Import modules selective modulaires const { applySelectiveLayer } = require('./SelectiveCore'); const { applyPredefinedStack, applyAdaptiveLayers, getAvailableStacks } = require('./SelectiveLayers'); const { analyzeTechnicalQuality, analyzeTransitionFluidity, analyzeStyleConsistency, generateImprovementReport } = require('./SelectiveUtils'); /** * EXEMPLE D'UTILISATION MODULAIRE SELECTIVE */ async function demoModularSelective() { console.log('\n🔧 === DÉMONSTRATION SELECTIVE MODULAIRE ===\n'); // Contenu d'exemple avec problèmes de qualité const exempleContenu = { '|Titre_Principal_1|': 'Guide complet pour choisir votre plaque personnalisée', '|Introduction_1|': 'La personnalisation d\'une plaque signalétique représente un enjeu important pour votre entreprise. Cette solution permet de créer une identité visuelle.', '|Texte_1|': 'Il est important de noter que les matériaux utilisés sont de qualité. Par ailleurs, la qualité est bonne. En effet, nos solutions sont bonnes et robustes. Par ailleurs, cela fonctionne bien.', '|FAQ_Question_1|': 'Quels sont les matériaux disponibles ?', '|FAQ_Reponse_1|': 'Nos matériaux sont de qualité : ils conviennent parfaitement. Ces solutions garantissent une qualité et un rendu optimal.' }; console.log('📊 CONTENU ORIGINAL:'); Object.entries(exempleContenu).forEach(([tag, content]) => { console.log(` ${tag}: "${content}"`); }); // Analyser qualité originale const fullOriginal = Object.values(exempleContenu).join(' '); const qualiteOriginale = { technical: analyzeTechnicalQuality(fullOriginal, ['dibond', 'aluminium', 'pmma', 'impression']), transitions: analyzeTransitionFluidity(fullOriginal), style: analyzeStyleConsistency(fullOriginal) }; console.log(`\n📈 QUALITÉ ORIGINALE:`); console.log(` 🔧 Technique: ${qualiteOriginale.technical.score}/100`); console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score}/100`); console.log(` 🎨 Style: ${qualiteOriginale.style.score}/100`); try { // ======================================== // TEST 1: COUCHE TECHNIQUE SEULE // ======================================== console.log('\n🔧 TEST 1: Application couche technique'); const result1 = await applySelectiveLayer(exempleContenu, { layerType: 'technical', llmProvider: 'gpt4', intensity: 0.9, csvData: { personality: { nom: 'Marc', style: 'technique' }, mc0: 'plaque personnalisée' } }); console.log(`✅ Résultat: ${result1.stats.enhanced}/${result1.stats.processed} éléments améliorés`); console.log(` ⏱️ Durée: ${result1.stats.duration}ms`); // ======================================== // TEST 2: STACK PRÉDÉFINI // ======================================== console.log('\n📦 TEST 2: Application stack prédéfini'); // Lister stacks disponibles const stacks = getAvailableStacks(); console.log(' Stacks disponibles:'); stacks.forEach(stack => { console.log(` - ${stack.name}: ${stack.description}`); }); const result2 = await applyPredefinedStack(exempleContenu, 'standardEnhancement', { csvData: { personality: { nom: 'Sophie', style: 'professionnel', vocabulairePref: 'signalétique,personnalisation,qualité,expertise', niveauTechnique: 'standard' }, mc0: 'plaque personnalisée' } }); console.log(`✅ Stack standard: ${result2.stats.totalModifications} modifications totales`); console.log(` 📊 Couches: ${result2.stats.layers.filter(l => l.success).length}/${result2.stats.layers.length} réussies`); // ======================================== // TEST 3: COUCHES ADAPTATIVES // ======================================== console.log('\n🧠 TEST 3: Application couches adaptatives'); const result3 = await applyAdaptiveLayers(exempleContenu, { maxIntensity: 1.2, analysisThreshold: 0.3, csvData: { personality: { nom: 'Laurent', style: 'commercial', vocabulairePref: 'expertise,solution,performance,innovation', niveauTechnique: 'accessible' }, mc0: 'signalétique personnalisée' } }); if (result3.stats.adaptive) { console.log(`✅ Adaptatif: ${result3.stats.layersApplied} couches appliquées`); console.log(` 📊 Modifications: ${result3.stats.totalModifications}`); } // ======================================== // COMPARAISON QUALITÉ FINALE // ======================================== console.log('\n📊 ANALYSE QUALITÉ FINALE:'); const contenuFinal = result2.content; // Prendre résultat stack standard const fullEnhanced = Object.values(contenuFinal).join(' '); const qualiteFinale = { technical: analyzeTechnicalQuality(fullEnhanced, ['dibond', 'aluminium', 'pmma', 'impression']), transitions: analyzeTransitionFluidity(fullEnhanced), style: analyzeStyleConsistency(fullEnhanced, result2.csvData?.personality) }; console.log('\n📈 AMÉLIORATION QUALITÉ:'); console.log(` 🔧 Technique: ${qualiteOriginale.technical.score} → ${qualiteFinale.technical.score} (+${(qualiteFinale.technical.score - qualiteOriginale.technical.score).toFixed(1)})`); console.log(` 🔗 Transitions: ${qualiteOriginale.transitions.score} → ${qualiteFinale.transitions.score} (+${(qualiteFinale.transitions.score - qualiteOriginale.transitions.score).toFixed(1)})`); console.log(` 🎨 Style: ${qualiteOriginale.style.score} → ${qualiteFinale.style.score} (+${(qualiteFinale.style.score - qualiteOriginale.style.score).toFixed(1)})`); // Rapport détaillé const rapport = generateImprovementReport(exempleContenu, contenuFinal, 'selective'); console.log('\n📋 RAPPORT AMÉLIORATION:'); console.log(` 📈 Amélioration moyenne: ${rapport.summary.averageImprovement.toFixed(1)}%`); console.log(` ✅ Éléments améliorés: ${rapport.summary.elementsImproved}/${rapport.summary.elementsProcessed}`); if (rapport.details.recommendations.length > 0) { console.log(` 💡 Recommandations: ${rapport.details.recommendations.join(', ')}`); } // ======================================== // EXEMPLES DE TRANSFORMATION // ======================================== console.log('\n✨ EXEMPLES DE TRANSFORMATION:'); console.log('\n📝 INTRODUCTION:'); console.log('AVANT:', `"${exempleContenu['|Introduction_1|']}"`); console.log('APRÈS:', `"${contenuFinal['|Introduction_1|']}"`); console.log('\n📝 TEXTE PRINCIPAL:'); console.log('AVANT:', `"${exempleContenu['|Texte_1|']}"`); console.log('APRÈS:', `"${contenuFinal['|Texte_1|']}"`); console.log('\n✅ === DÉMONSTRATION SELECTIVE MODULAIRE TERMINÉE ===\n'); return { success: true, originalQuality: qualiteOriginale, finalQuality: qualiteFinale, improvementReport: rapport }; } catch (error) { console.error('\n❌ ERREUR DÉMONSTRATION:', error.message); console.error(error.stack); return { success: false, error: error.message }; } } /** * EXEMPLE D'INTÉGRATION AVEC PIPELINE EXISTANTE */ async function demoIntegrationExistante() { console.log('\n🔗 === DÉMONSTRATION INTÉGRATION PIPELINE ===\n'); // Simuler contenu venant de ContentGeneration.js (Level 1) const contenuExistant = { '|Titre_H1_1|': 'Solutions de plaques personnalisées professionnelles', '|Meta_Description_1|': 'Découvrez notre gamme complète de plaques personnalisées pour tous vos besoins de signalétique professionnelle.', '|Introduction_1|': 'Dans le domaine de la signalétique personnalisée, le choix des matériaux et des techniques de fabrication constitue un élément déterminant.', '|Texte_Avantages_1|': 'Les avantages de nos solutions incluent la durabilité, la résistance aux intempéries et la possibilité de personnalisation complète.' }; console.log('💼 SCÉNARIO: Application selective post-génération normale'); try { console.log('\n🎯 Étape 1: Contenu généré par pipeline Level 1'); console.log(' ✅ Contenu de base: qualité préservée'); console.log('\n🎯 Étape 2: Application selective enhancement modulaire'); // Test avec couche technique puis style let contenuEnhanced = contenuExistant; // Amélioration technique const resultTechnique = await applySelectiveLayer(contenuEnhanced, { layerType: 'technical', llmProvider: 'gpt4', intensity: 1.0, analysisMode: true, csvData: { personality: { nom: 'Marc', style: 'technique' }, mc0: 'plaque personnalisée' } }); contenuEnhanced = resultTechnique.content; console.log(` ✅ Couche technique: ${resultTechnique.stats.enhanced} éléments améliorés`); // Amélioration style const resultStyle = await applySelectiveLayer(contenuEnhanced, { layerType: 'style', llmProvider: 'mistral', intensity: 0.8, analysisMode: true, csvData: { personality: { nom: 'Sophie', style: 'professionnel moderne', vocabulairePref: 'innovation,expertise,personnalisation,qualité', niveauTechnique: 'accessible' } } }); contenuEnhanced = resultStyle.content; console.log(` ✅ Couche style: ${resultStyle.stats.enhanced} éléments stylisés`); console.log('\n📊 RÉSULTAT FINAL INTÉGRÉ:'); Object.entries(contenuEnhanced).forEach(([tag, content]) => { console.log(`\n ${tag}:`); console.log(` ORIGINAL: "${contenuExistant[tag]}"`); console.log(` ENHANCED: "${content}"`); }); return { success: true, techniqueResult: resultTechnique, styleResult: resultStyle, finalContent: contenuEnhanced }; } catch (error) { console.error('❌ ERREUR INTÉGRATION:', error.message); return { success: false, error: error.message }; } } /** * TEST PERFORMANCE ET BENCHMARKS */ async function benchmarkPerformance() { console.log('\n⚡ === BENCHMARK PERFORMANCE ===\n'); // Contenu de test de taille variable const contenuTest = {}; // Générer contenu test for (let i = 1; i <= 10; i++) { contenuTest[`|Element_${i}|`] = `Ceci est un contenu de test numéro ${i} pour valider les performances du système selective enhancement modulaire. ` + `Il est important de noter que ce contenu contient du vocabulaire générique et des répétitions. Par ailleurs, les transitions sont basiques. ` + `En effet, la qualité technique est faible et le style est générique. Par ailleurs, cela nécessite des améliorations.`.repeat(Math.floor(i/3) + 1); } console.log(`📊 Contenu test: ${Object.keys(contenuTest).length} éléments`); try { const benchmarks = []; // Test 1: Couche technique seule const start1 = Date.now(); const result1 = await applySelectiveLayer(contenuTest, { layerType: 'technical', intensity: 0.8 }); benchmarks.push({ test: 'Couche technique seule', duration: Date.now() - start1, enhanced: result1.stats.enhanced, processed: result1.stats.processed }); // Test 2: Stack complet const start2 = Date.now(); const result2 = await applyPredefinedStack(contenuTest, 'fullEnhancement'); benchmarks.push({ test: 'Stack complet (3 couches)', duration: Date.now() - start2, totalModifications: result2.stats.totalModifications, layers: result2.stats.layers.length }); // Test 3: Adaptatif const start3 = Date.now(); const result3 = await applyAdaptiveLayers(contenuTest, { maxIntensity: 1.0 }); benchmarks.push({ test: 'Couches adaptatives', duration: Date.now() - start3, layersApplied: result3.stats.layersApplied, totalModifications: result3.stats.totalModifications }); console.log('\n📈 RÉSULTATS BENCHMARK:'); benchmarks.forEach(bench => { console.log(`\n ${bench.test}:`); console.log(` ⏱️ Durée: ${bench.duration}ms`); if (bench.enhanced) console.log(` ✅ Améliorés: ${bench.enhanced}/${bench.processed}`); if (bench.totalModifications) console.log(` 🔄 Modifications: ${bench.totalModifications}`); if (bench.layers) console.log(` 📦 Couches: ${bench.layers}`); if (bench.layersApplied) console.log(` 🧠 Couches adaptées: ${bench.layersApplied}`); }); return { success: true, benchmarks }; } catch (error) { console.error('❌ ERREUR BENCHMARK:', error.message); return { success: false, error: error.message }; } } // Exécuter démonstrations si fichier appelé directement if (require.main === module) { (async () => { await demoModularSelective(); await demoIntegrationExistante(); await benchmarkPerformance(); })().catch(console.error); } module.exports = { demoModularSelective, demoIntegrationExistante, benchmarkPerformance }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/SelectiveEnhancement.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: SelectiveEnhancement.js - Node.js Version // Description: Enhancement par batch pour éviter timeouts // ======================================== const { callLLM } = require('./LLMManager'); const { logSh } = require('./ErrorReporting'); const { tracer } = require('./trace.js'); const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig'); // Utilitaire pour les délais function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement * 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection */ async function generateWithBatchEnhancement(hierarchy, csvData) { const totalElements = Object.keys(hierarchy).length; // NOUVEAU: Sélection de 4 personnalités complémentaires const personalities = await tracer.run('SelectiveEnhancement.selectMultiplePersonalities()', async () => { const allPersonalities = await getPersonalities(); const selectedPersonalities = await selectMultiplePersonalitiesWithAI(csvData.mc0, csvData.t0, allPersonalities); await tracer.event(`4 personnalités sélectionnées: ${selectedPersonalities.map(p => p.nom).join(', ')}`); return selectedPersonalities; }, { mc0: csvData.mc0, t0: csvData.t0 }); await tracer.annotate({ totalElements, personalities: personalities.map(p => `${p.nom}(${p.style})`).join(', '), mc0: csvData.mc0 }); // ÉTAPE 1 : Génération base avec IA configurée + Personnalité 1 const baseContents = await tracer.run('SelectiveEnhancement.generateAllContentBase()', async () => { const csvDataWithPersonality1 = { ...csvData, personality: personalities[0] }; const aiProvider1 = personalities[0].aiEtape1Base; const result = await generateAllContentBase(hierarchy, csvDataWithPersonality1, aiProvider1); await tracer.event(`${Object.keys(result).length} éléments générés avec ${personalities[0].nom} via ${aiProvider1.toUpperCase()}`); return result; }, { hierarchyElements: Object.keys(hierarchy).length, personality1: personalities[0].nom, llmProvider: personalities[0].aiEtape1Base, mc0: csvData.mc0 }); // ÉTAPE 2 : Enhancement technique avec IA configurée + Personnalité 2 const technicalEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTechnicalTerms()', async () => { const csvDataWithPersonality2 = { ...csvData, personality: personalities[1] }; const aiProvider2 = personalities[1].aiEtape2Technique; const result = await enhanceAllTechnicalTerms(baseContents, csvDataWithPersonality2, aiProvider2); const enhancedCount = Object.keys(result).filter(k => result[k] !== baseContents[k]).length; await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments techniques améliorés avec ${personalities[1].nom} via ${aiProvider2.toUpperCase()}`); return result; }, { baseElements: Object.keys(baseContents).length, personality2: personalities[1].nom, llmProvider: personalities[1].aiEtape2Technique, mc0: csvData.mc0 }); // ÉTAPE 3 : Enhancement transitions avec IA configurée + Personnalité 3 const transitionsEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTransitions()', async () => { const csvDataWithPersonality3 = { ...csvData, personality: personalities[2] }; const aiProvider3 = personalities[2].aiEtape3Transitions; const result = await enhanceAllTransitions(technicalEnhanced, csvDataWithPersonality3, aiProvider3); const enhancedCount = Object.keys(result).filter(k => result[k] !== technicalEnhanced[k]).length; await tracer.event(`${enhancedCount}/${Object.keys(result).length} transitions fluidifiées avec ${personalities[2].nom} via ${aiProvider3.toUpperCase()}`); return result; }, { technicalElements: Object.keys(technicalEnhanced).length, personality3: personalities[2].nom, llmProvider: personalities[2].aiEtape3Transitions }); // ÉTAPE 4 : Enhancement style avec IA configurée + Personnalité 4 const finalContents = await tracer.run('SelectiveEnhancement.enhanceAllPersonalityStyle()', async () => { const csvDataWithPersonality4 = { ...csvData, personality: personalities[3] }; const aiProvider4 = personalities[3].aiEtape4Style; const result = await enhanceAllPersonalityStyle(transitionsEnhanced, csvDataWithPersonality4, aiProvider4); const enhancedCount = Object.keys(result).filter(k => result[k] !== transitionsEnhanced[k]).length; const avgWords = Math.round(Object.values(result).reduce((acc, content) => acc + content.split(' ').length, 0) / Object.keys(result).length); await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments stylisés avec ${personalities[3].nom} via ${aiProvider4.toUpperCase()}`, { avgWordsPerElement: avgWords }); return result; }, { transitionElements: Object.keys(transitionsEnhanced).length, personality4: personalities[3].nom, llmProvider: personalities[3].aiEtape4Style }); // Log final du DNA Mixing réussi avec IA configurables const aiChain = personalities.map((p, i) => `${p.aiEtape1Base || p.aiEtape2Technique || p.aiEtape3Transitions || p.aiEtape4Style}`.toUpperCase()).join(' → '); logSh(`✅ DNA MIXING MULTI-PERSONNALITÉS TERMINÉ:`, 'INFO'); logSh(` 🎭 4 personnalités utilisées: ${personalities.map(p => p.nom).join(' → ')}`, 'INFO'); logSh(` 🤖 IA configurées: ${personalities[0].aiEtape1Base.toUpperCase()} → ${personalities[1].aiEtape2Technique.toUpperCase()} → ${personalities[2].aiEtape3Transitions.toUpperCase()} → ${personalities[3].aiEtape4Style.toUpperCase()}`, 'INFO'); logSh(` 📝 ${Object.keys(finalContents).length} éléments avec style hybride généré`, 'INFO'); return finalContents; } /** * ÉTAPE 1 - Génération base TOUS éléments avec IA configurable */ async function generateAllContentBase(hierarchy, csvData, aiProvider) { logSh('🔍 === DEBUG GÉNÉRATION BASE ===', 'DEBUG'); // Debug: logger la hiérarchie complète logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'DEBUG'); Object.keys(hierarchy).forEach((path, i) => { const section = hierarchy[path]; logSh(`🔍 Section ${i+1} [${path}]:`, 'DEBUG'); logSh(`🔍 - title: ${section.title ? section.title.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG'); logSh(`🔍 - text: ${section.text ? section.text.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG'); logSh(`🔍 - questions: ${section.questions?.length || 0}`, 'DEBUG'); }); const allElements = collectAllElements(hierarchy); logSh(`🔍 Éléments collectés: ${allElements.length}`, 'DEBUG'); // Debug: logger tous les éléments collectés allElements.forEach((element, i) => { logSh(`🔍 Élément ${i+1}: tag="${element.tag}", type="${element.type}"`, 'DEBUG'); }); // NOUVELLE LOGIQUE : SÉPARER PAIRES FAQ ET AUTRES ÉLÉMENTS const results = {}; logSh(`🔍 === GÉNÉRATION INTELLIGENTE DE ${allElements.length} ÉLÉMENTS ===`, 'DEBUG'); logSh(`🔍 Ordre respecté: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'DEBUG'); // 1. IDENTIFIER les paires FAQ const { faqPairs, otherElements } = separateFAQPairsAndOthers(allElements); logSh(`🔍 ${faqPairs.length} paires FAQ trouvées, ${otherElements.length} autres éléments`, 'INFO'); // 2. GÉNÉRER les autres éléments EN BATCH ORDONNÉ (titres d'abord, puis textes avec contexte) const groupedElements = groupElementsByType(otherElements); // ORDRE DE GÉNÉRATION : TITRES → TEXTES → INTRO → AUTRES const orderedTypes = ['titre', 'texte', 'intro']; for (const type of orderedTypes) { const elements = groupedElements[type]; if (!elements || elements.length === 0) continue; // DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS const chunks = chunkArray(elements, 4); logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO'); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { // Passer les résultats déjà générés pour contexte (titres → textes) const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results); const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const batchResults = parseBatchResponse(batchResponse, chunk); Object.assign(results, batchResults); logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO'); } catch (error) { logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`); } // Délai entre chunks pour éviter rate limiting if (chunkIndex < chunks.length - 1) { await sleep(1500); } } logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO'); } // TRAITER les types restants (autres que titre/texte/intro) for (const [type, elements] of Object.entries(groupedElements)) { if (orderedTypes.includes(type) || elements.length === 0) continue; // DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS const chunks = chunkArray(elements, 4); logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO'); 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 batchPrompt = createBatchBasePrompt(chunk, type, csvData, results); const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const batchResults = parseBatchResponse(batchResponse, chunk); Object.assign(results, batchResults); logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO'); } catch (error) { logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`); } // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO'); } // 3. GÉNÉRER les paires FAQ ensemble (RESTAURÉ depuis .gs) if (faqPairs.length > 0) { logSh(`🔍 === GÉNÉRATION PAIRES FAQ (${faqPairs.length} paires) ===`, 'INFO'); const faqResults = await generateFAQPairsRestored(faqPairs, csvData, aiProvider); Object.assign(results, faqResults); } logSh(`🔍 === RÉSULTATS FINAUX GÉNÉRATION BASE ===`, 'DEBUG'); logSh(`🔍 Total généré: ${Object.keys(results).length} éléments`, 'DEBUG'); Object.keys(results).forEach(tag => { logSh(`🔍 [${tag}]: "${results[tag]}"`, 'DEBUG'); }); return results; } /** * ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable * NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis */ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) { logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO'); logSh('Enhancement technique BATCH TOTAL...', 'DEBUG'); const allElements = Object.keys(baseContents); if (allElements.length === 0) { logSh('⚠️ Aucun élément à analyser techniquement', 'WARNING'); return baseContents; } const analysisStart = Date.now(); logSh(`📊 Analyse démarrée: ${allElements.length} éléments à examiner`, 'INFO'); try { // ÉTAPE 1 : Extraction batch TOUS les termes techniques (1 seul appel) logSh(`🔍 Analyse technique batch: ${allElements.length} éléments`, 'INFO'); const technicalAnalysis = await extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider); const analysisEnd = Date.now(); // ÉTAPE 2 : Enhancement batch TOUS les éléments qui en ont besoin (1 seul appel) const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement); logSh(`📋 Analyse terminée (${analysisEnd - analysisStart}ms):`, 'INFO'); logSh(` • ${elementsNeedingEnhancement.length}/${allElements.length} éléments nécessitent enhancement`, 'INFO'); if (elementsNeedingEnhancement.length === 0) { logSh('✅ Aucun élément ne nécessite enhancement technique - contenu déjà optimal', 'INFO'); return baseContents; } // Log détaillé des éléments à améliorer elementsNeedingEnhancement.forEach((item, i) => { logSh(` ${i+1}. [${item.tag}]: ${item.technicalTerms.join(', ')}`, 'DEBUG'); }); const enhancementStart = Date.now(); logSh(`🔧 Enhancement technique: ${elementsNeedingEnhancement.length}/${allElements.length} éléments`, 'INFO'); const enhancedContents = await enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider); const enhancementEnd = Date.now(); // ÉTAPE 3 : Merger résultats const results = { ...baseContents }; let actuallyEnhanced = 0; Object.keys(enhancedContents).forEach(tag => { if (enhancedContents[tag] !== baseContents[tag]) { results[tag] = enhancedContents[tag]; actuallyEnhanced++; } }); logSh(`⚡ Enhancement terminé (${enhancementEnd - enhancementStart}ms):`, 'INFO'); logSh(` • ${actuallyEnhanced} éléments réellement améliorés`, 'INFO'); logSh(` • Termes intégrés: dibond, impression UV, fraisage, etc.`, 'DEBUG'); logSh(`✅ Enhancement technique terminé avec succès`, 'INFO'); return results; } catch (error) { const analysisTotal = Date.now() - analysisStart; logSh(`❌ FATAL: Enhancement technique échoué après ${analysisTotal}ms`, 'ERROR'); logSh(`❌ Message: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Enhancement technique impossible - arrêt du workflow: ${error.message}`); } } /** * Analyser un seul élément pour détecter les termes techniques */ async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) { const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENU À ANALYSER: TAG: ${tag} CONTENU: "${content}" CONSIGNES: - Cherche UNIQUEMENT des vrais termes techniques métier/industrie - Évite mots génériques (qualité, service, pratique, personnalisé, etc.) - Focus: matériaux, procédés, normes, dimensions, technologies spécifiques EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance RÉPONSE REQUISE: - Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]" - Si aucun terme technique: "NON" EXEMPLE: OUI - termes: aluminium composite, impression numérique, gravure laser`; try { const response = await callLLM(aiProvider, prompt, { temperature: 0.3 }); if (response.toUpperCase().startsWith('OUI')) { // Extraire les termes de la réponse const termsMatch = response.match(/termes:\s*(.+)/i); const terms = termsMatch ? termsMatch[1].trim() : ''; logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG'); return true; } else { logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG'); return false; } } catch (error) { logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR'); return false; // En cas d'erreur, on skip l'enhancement } } /** * Enhancer un seul élément techniquement */ async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) { const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENU À AMÉLIORER: TAG: ${tag} CONTENU: "${content}" OBJECTIFS: - Remplace les termes génériques par des termes techniques précis - Ajoute des spécifications techniques réalistes - Maintient le même style et longueur - Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes EXEMPLE DE TRANSFORMATION: "matériaux haute performance" → "dibond 3mm ou aluminium composite" "impression moderne" → "impression UV haute définition" "fixation solide" → "fixation par chevilles inox Ø6mm" CONTRAINTES: - GARDE la même structure - MÊME longueur approximative - Style cohérent avec l'original - RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`; try { const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 }); return enhancedContent.trim(); } catch (error) { logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR'); return content; // En cas d'erreur, on retourne le contenu original } } // ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL /** * NOUVELLE FONCTION : Enhancement batch TOUS les éléments */ // FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel /** * ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable */ async function enhanceAllTransitions(baseContents, csvData, aiProvider) { logSh('🔗 === DÉBUT ENHANCEMENT TRANSITIONS ===', 'INFO'); logSh('Enhancement transitions batch...', 'DEBUG'); const transitionStart = Date.now(); const allElements = Object.keys(baseContents); logSh(`📊 Analyse transitions: ${allElements.length} éléments à examiner`, 'INFO'); // Sélectionner éléments longs qui bénéficient d'amélioration transitions const transitionElements = []; let analyzedCount = 0; Object.keys(baseContents).forEach(tag => { const content = baseContents[tag]; analyzedCount++; if (content.length > 150) { const needsTransitions = analyzeTransitionNeed(content); logSh(` [${tag}]: ${content.length}c, transitions=${needsTransitions ? '✅' : '❌'}`, 'DEBUG'); if (needsTransitions) { transitionElements.push({ tag: tag, content: content }); } } else { logSh(` [${tag}]: ${content.length}c - trop court, ignoré`, 'DEBUG'); } }); logSh(`📋 Analyse transitions terminée:`, 'INFO'); logSh(` • ${analyzedCount} éléments analysés`, 'INFO'); logSh(` • ${transitionElements.length} nécessitent amélioration`, 'INFO'); if (transitionElements.length === 0) { logSh('✅ Pas d\'éléments nécessitant enhancement transitions - fluidité déjà optimale', 'INFO'); return baseContents; } logSh(`${transitionElements.length} éléments à améliorer (transitions)`, 'INFO'); const chunks = chunkArray(transitionElements, 6); // Plus petit pour Gemini const results = { ...baseContents }; 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 batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus. CONTEXTE: Article SEO professionnel pour site web commercial PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web) CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref} CONTENUS: ${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag} "${item.content}"`).join('\n\n')} OBJECTIFS: - Connecteurs plus naturels et variés issus de: ${csvData.personality?.connecteursPref} - Transitions fluides entre idées - ÉVITE répétitions excessives ("franchement", "du coup", "vraiment", "par ailleurs") - Style cohérent ${csvData.personality?.style} CONTRAINTES STRICTES: - NE CHANGE PAS le fond du message - GARDE la même structure et longueur approximative - Améliore SEULEMENT la fluidité des transitions - RESPECTE le style ${csvData.personality?.nom} - RÉPONDS DIRECTEMENT PAR LE CONTENU AMÉLIORÉ, sans préfixe ni tag XML FORMAT DE RÉPONSE: [1] Contenu avec transitions améliorées selon ${csvData.personality?.nom} [2] Contenu avec transitions améliorées selon ${csvData.personality?.nom} etc...`; const improved = await callLLM(aiProvider, batchTransitionsPrompt, { temperature: 0.6, maxTokens: 2500 }, csvData.personality); const parsedImprovements = parseTransitionsBatchResponse(improved, chunk); Object.keys(parsedImprovements).forEach(tag => { results[tag] = parsedImprovements[tag]; }); } catch (error) { logSh(`❌ Erreur chunk transitions ${chunkIndex + 1}: ${error.message}`, 'ERROR'); } if (chunkIndex < chunks.length - 1) { await sleep(1500); } } return results; } /** * ÉTAPE 4 - Enhancement style personnalité BATCH avec IA configurable */ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) { const personality = csvData.personality; if (!personality) { logSh('Pas de personnalité, skip enhancement style', 'DEBUG'); return baseContents; } logSh(`Enhancement style ${personality.nom} batch...`, 'DEBUG'); // Tous les éléments bénéficient de l'adaptation personnalité const styleElements = Object.keys(baseContents).map(tag => ({ tag: tag, content: baseContents[tag] })); const chunks = chunkArray(styleElements, 8); const results = { ...baseContents }; 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 batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}. CONTEXTE: Finalisation article SEO pour site e-commerce professionnel PERSONNALITÉ: ${personality.nom} DESCRIPTION: ${personality.description} STYLE CIBLE: ${personality.style} adapté au 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} "${item.content}"`).join('\n\n')} CONSIGNES STRICTES: - GARDE le même contenu informatif et technique - Adapte SEULEMENT le ton, les expressions et le vocabulaire selon ${personality.nom} - RESPECTE la longueur approximative (même nombre de mots ±20%) - ÉVITE les répétitions excessives ("franchement", "du coup", "vraiment") - VARIE les expressions et connecteurs selon: ${personality.connecteursPref} - Style ${personality.nom} reconnaissable mais NATUREL - RÉPONDS DIRECTEMENT PAR LE CONTENU STYLISÉ, sans préfixe ni tag XML - PAS de messages d'excuse ou d'incapacité FORMAT DE RÉPONSE: [1] Contenu stylisé selon ${personality.nom} (${personality.style}) [2] Contenu stylisé selon ${personality.nom} (${personality.style}) etc...`; const styled = await callLLM(aiProvider, batchStylePrompt, { temperature: 0.8, maxTokens: 3000 }, personality); const parsedStyles = parseStyleBatchResponse(styled, chunk); Object.keys(parsedStyles).forEach(tag => { results[tag] = parsedStyles[tag]; }); } catch (error) { logSh(`❌ Erreur chunk style ${chunkIndex + 1}: ${error.message}`, 'ERROR'); } if (chunkIndex < chunks.length - 1) { await sleep(1500); } } return results; } // ============= HELPER FUNCTIONS ============= /** * Sleep function replacement for Utilities.sleep */ // FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12 /** * RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes */ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) { logSh(`🔍 === GÉNÉRATION PAIRES FAQ (logique .gs restaurée) ===`, 'INFO'); if (faqPairs.length === 0) return {}; const batchPrompt = createBatchFAQPairsPrompt(faqPairs, csvData); logSh(`🔍 Prompt FAQ paires (${batchPrompt.length} chars): "${batchPrompt.substring(0, 300)}..."`, 'DEBUG'); try { const batchResponse = await callLLM(aiProvider, batchPrompt, { temperature: 0.8, maxTokens: 3000 // Plus large pour les paires }, csvData.personality); logSh(`🔍 Réponse FAQ paires reçue: ${batchResponse.length} caractères`, 'DEBUG'); logSh(`🔍 Début réponse: "${batchResponse.substring(0, 200)}..."`, 'DEBUG'); return parseFAQPairsResponse(batchResponse, faqPairs); } catch (error) { logSh(`❌ FATAL: Erreur génération paires FAQ: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Génération paires FAQ échouée - arrêt du workflow: ${error.message}`); } } /** * RESTAURÉ DEPUIS .GS : Prompt pour paires FAQ cohérentes */ function createBatchFAQPairsPrompt(faqPairs, csvData) { const personality = csvData.personality; let prompt = `=== 1. CONTEXTE === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Section: FAQ pour article SEO commercial === 2. PERSONNALITÉ === Rédacteur: ${personality.nom} Style: ${personality.style} Ton: ${personality.description || 'professionnel'} === 3. RÈGLES GÉNÉRALES === - Questions naturelles de clients - Réponses expertes et rassurantes - Langage professionnel mais accessible - Textes rédigés humainement et de façon authentique - Couvrir: prix, livraison, personnalisation, installation, durabilité - IMPÉRATIF: Respecter strictement les contraintes XML === 4. PAIRES FAQ À GÉNÉRER === `; faqPairs.forEach((pair, index) => { const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle `; }); prompt += ` FORMAT DE RÉPONSE: PAIRE 1: [${faqPairs[0].question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] Question client directe et naturelle sur ${csvData.mc0} ? [${faqPairs[0].answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] Réponse utile et rassurante selon le style ${personality.style} de ${personality.nom}. `; if (faqPairs.length > 1) { prompt += `PAIRE 2: etc... `; } return prompt; } /** * RESTAURÉ DEPUIS .GS : Parser réponse paires FAQ */ function parseFAQPairsResponse(response, faqPairs) { const results = {}; logSh(`🔍 Parsing FAQ paires: "${response.substring(0, 300)}..."`, 'DEBUG'); // Parser avec regex [TAG] contenu const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); let content = match[2].trim().replace(/\n\s*\n/g, '\n').replace(/^\n+|\n+$/g, ''); // NOUVEAU: Appliquer le nettoyage XML pour FAQ aussi content = cleanXMLTagsFromContent(content); if (content && content.length > 0) { parsedItems[tag] = content; logSh(`🔍 Parsé [${tag}]: "${content.substring(0, 100)}..."`, 'DEBUG'); } } // Mapper aux vrais tags FAQ avec | let pairesCompletes = 0; faqPairs.forEach(pair => { const questionCleanTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const answerCleanTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const questionContent = parsedItems[questionCleanTag]; const answerContent = parsedItems[answerCleanTag]; if (questionContent && answerContent) { results[pair.question.tag] = questionContent; results[pair.answer.tag] = answerContent; pairesCompletes++; logSh(`✅ Paire FAQ ${pair.number} complète: Q="${questionContent}" R="${answerContent.substring(0, 50)}..."`, 'INFO'); } else { logSh(`⚠️ Paire FAQ ${pair.number} incomplète: Q=${!!questionContent} R=${!!answerContent}`, 'WARNING'); if (questionContent) results[pair.question.tag] = questionContent; if (answerContent) results[pair.answer.tag] = answerContent; } }); logSh(`📊 FAQ parsing: ${pairesCompletes}/${faqPairs.length} paires complètes`, 'INFO'); // FATAL si aucune paire complète (comme dans le .gs) if (pairesCompletes === 0 && faqPairs.length > 0) { logSh(`❌ FATAL: Aucune paire FAQ générée correctement`, 'ERROR'); throw new Error(`FATAL: Génération FAQ incomplète (0/${faqPairs.length} paires complètes) - arrêt du workflow`); } return results; } /** * RESTAURÉ DEPUIS .GS : Nettoyer instructions FAQ */ function cleanFAQInstructions(instructions, csvData) { if (!instructions) return ''; let cleanInstructions = instructions; // Remplacer variables cleanInstructions = cleanInstructions.replace(/\{\{T0\}\}/g, csvData.t0 || ''); cleanInstructions = cleanInstructions.replace(/\{\{MC0\}\}/g, csvData.mc0 || ''); cleanInstructions = cleanInstructions.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || ''); cleanInstructions = cleanInstructions.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || ''); // Variables multiples MC+1_X, T+1_X, L+1_X if (csvData.mcPlus1) { const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`; cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue); } } if (csvData.tPlus1) { const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`; cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue); } } // Nettoyer HTML cleanInstructions = cleanInstructions.replace(/<\/?[^>]+>/g, ''); cleanInstructions = cleanInstructions.replace(/\s+/g, ' ').trim(); return cleanInstructions; } /** * Collecter tous les éléments dans l'ordre XML original * CORRECTION: Suit l'ordre séquentiel XML au lieu de grouper par section */ function collectAllElements(hierarchy) { const allElements = []; const tagToElementMap = {}; // 1. Créer un mapping de tous les éléments disponibles Object.keys(hierarchy).forEach(path => { const section = hierarchy[path]; if (section.title) { tagToElementMap[section.title.originalElement.originalTag] = { tag: section.title.originalElement.originalTag, element: section.title.originalElement, type: 'titre' }; } if (section.text) { tagToElementMap[section.text.originalElement.originalTag] = { tag: section.text.originalElement.originalTag, element: section.text.originalElement, type: 'texte' }; } section.questions.forEach(q => { tagToElementMap[q.originalElement.originalTag] = { tag: q.originalElement.originalTag, element: q.originalElement, type: q.originalElement.type }; }); }); // 2. Récupérer l'ordre XML original depuis le template global logSh(`🔍 Global XML Template disponible: ${!!global.currentXmlTemplate}`, 'DEBUG'); if (global.currentXmlTemplate && global.currentXmlTemplate.length > 0) { logSh(`🔍 Template XML: ${global.currentXmlTemplate.substring(0, 200)}...`, 'DEBUG'); const regex = /\|([^|]+)\|/g; let match; // Parcourir le XML dans l'ordre d'apparition while ((match = regex.exec(global.currentXmlTemplate)) !== null) { const fullMatch = match[1]; // Extraire le nom du tag (sans variables) const nameMatch = fullMatch.match(/^([^{]+)/); const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0]; const pureTag = `|${tagName}|`; // Si cet élément existe dans notre mapping, l'ajouter dans l'ordre if (tagToElementMap[pureTag]) { allElements.push(tagToElementMap[pureTag]); logSh(`🔍 Ajouté dans l'ordre: ${pureTag}`, 'DEBUG'); delete tagToElementMap[pureTag]; // Éviter les doublons } else { logSh(`🔍 Tag XML non trouvé dans mapping: ${pureTag}`, 'DEBUG'); } } } // 3. Ajouter les éléments restants (sécurité) const remainingElements = Object.values(tagToElementMap); if (remainingElements.length > 0) { logSh(`🔍 Éléments restants ajoutés: ${remainingElements.map(el => el.tag).join(', ')}`, 'DEBUG'); remainingElements.forEach(element => { allElements.push(element); }); } logSh(`🔍 ORDRE FINAL: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'INFO'); return allElements; } /** * RESTAURÉ DEPUIS .GS : Séparer les paires FAQ des autres éléments */ function separateFAQPairsAndOthers(allElements) { const faqPairs = []; const otherElements = []; const faqQuestions = {}; const faqAnswers = {}; // 1. Collecter toutes les questions et réponses FAQ allElements.forEach(element => { if (element.type === 'faq_question') { // Extraire le numéro : |Faq_q_1| → 1 const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqQuestions[faqNumber] = element; logSh(`🔍 Question FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG'); } else if (element.type === 'faq_reponse') { // Extraire le numéro : |Faq_a_1| → 1 const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqAnswers[faqNumber] = element; logSh(`🔍 Réponse FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG'); } else { // Élément normal (titre, texte, intro, etc.) otherElements.push(element); } }); // 2. Créer les paires FAQ cohérentes Object.keys(faqQuestions).forEach(number => { const question = faqQuestions[number]; const answer = faqAnswers[number]; if (question && answer) { faqPairs.push({ number: number, question: question, answer: answer }); logSh(`✅ Paire FAQ ${number} créée: ${question.tag} + ${answer.tag}`, 'INFO'); } else if (question) { logSh(`⚠️ Question FAQ ${number} sans réponse correspondante`, 'WARNING'); otherElements.push(question); // Traiter comme élément individuel } else if (answer) { logSh(`⚠️ Réponse FAQ ${number} sans question correspondante`, 'WARNING'); otherElements.push(answer); // Traiter comme élément individuel } }); logSh(`🔍 Séparation terminée: ${faqPairs.length} paires FAQ, ${otherElements.length} autres éléments`, 'INFO'); return { faqPairs, otherElements }; } /** * Grouper éléments par type */ function groupElementsByType(elements) { const groups = {}; elements.forEach(element => { const type = element.type; if (!groups[type]) { groups[type] = []; } groups[type].push(element); }); return groups; } /** * Diviser array en chunks */ function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } /** * Trouver le titre associé à un élément texte */ function findAssociatedTitle(textElement, existingResults) { const textName = textElement.element.name || textElement.tag; // STRATÉGIE 1: Correspondance directe (Txt_H2_1 → Titre_H2_1) const directMatch = textName.replace(/Txt_/, 'Titre_').replace(/Text_/, 'Titre_'); const directTitle = existingResults[`|${directMatch}|`] || existingResults[directMatch]; if (directTitle) return directTitle; // STRATÉGIE 2: Même niveau hiérarchique (H2, H3) const levelMatch = textName.match(/(H\d)_(\d+)/); if (levelMatch) { const [, level, number] = levelMatch; const titleTag = `Titre_${level}_${number}`; const levelTitle = existingResults[`|${titleTag}|`] || existingResults[titleTag]; if (levelTitle) return levelTitle; } // STRATÉGIE 3: Proximité dans l'ordre (texte suivant un titre) const allTitles = Object.entries(existingResults) .filter(([tag]) => tag.includes('Titre')) .sort(([a], [b]) => a.localeCompare(b)); if (allTitles.length > 0) { // Retourner le premier titre disponible comme contexte général return allTitles[0][1]; } return null; } /** * Créer prompt batch de base */ function createBatchBasePrompt(elements, type, csvData, existingResults = {}) { const personality = csvData.personality; let prompt = `=== 1. CONTEXTE === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Type d'article: SEO professionnel pour site commercial === 2. PERSONNALITÉ === Rédacteur: ${personality.nom} Style: ${personality.style} Ton: ${personality.description || 'professionnel'} === 3. RÈGLES GÉNÉRALES === - Contenu SEO optimisé - Langage naturel et fluide - Éviter répétitions - Pas de références techniques dans le contenu - Textes rédigés humainement et de façon authentique - IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.) === 4. ÉLÉMENTS À GÉNÉRER === `; // AJOUTER CONTEXTE DES TITRES POUR LES TEXTES if (type === 'texte' && Object.keys(existingResults).length > 0) { const generatedTitles = Object.entries(existingResults) .filter(([tag]) => tag.includes('Titre')) .map(([tag, title]) => `• ${tag.replace(/\|/g, '')}: "${title}"`) .slice(0, 5); // Limiter à 5 titres pour éviter surcharge if (generatedTitles.length > 0) { prompt += ` Titres existants pour contexte: ${generatedTitles.join('\n')} `; } } elements.forEach((elementInfo, index) => { const cleanTag = elementInfo.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); prompt += `${index + 1}. [${cleanTag}] `; // INSTRUCTIONS PROPRES PAR ÉLÉMENT if (type === 'titre') { if (elementInfo.element.type === 'titre_h1') { prompt += `Titre principal accrocheur\n`; } else if (elementInfo.element.type === 'titre_h2') { prompt += `Titre de section engageant\n`; } else if (elementInfo.element.type === 'titre_h3') { prompt += `Sous-titre spécialisé\n`; } else { prompt += `Titre pertinent\n`; } } else if (type === 'texte') { prompt += `Paragraphe informatif\n`; // ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT const associatedTitle = findAssociatedTitle(elementInfo, existingResults); if (associatedTitle) { prompt += ` Contexte: "${associatedTitle}"\n`; } if (elementInfo.element.resolvedContent) { prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`; } } else if (type === 'intro') { prompt += `Introduction engageante\n`; } else { prompt += `Contenu pertinent\n`; } }); prompt += `\nSTYLE ${personality.nom.toUpperCase()} - ${personality.style}: - Vocabulaire: ${personality.vocabulairePref} - Connecteurs: ${personality.connecteursPref} - Phrases: ${personality.longueurPhrases} - Niveau technique: ${personality.niveauTechnique} CONSIGNES STRICTES POUR ARTICLE SEO: - CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels - STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel - INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top") - VOCABULAIRE: Mélange expertise technique + accessibilité client - SEO: Utilise naturellement "${csvData.mc0}" et termes associés - POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7" - EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024" - CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial - RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}: [${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] ${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H1_1")' : 'Contenu rédigé selon le style ' + personality.nom} [${elements[1] ? elements[1].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '') : 'element2'}] ${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H2_1")' : 'Contenu rédigé selon le style ' + personality.nom} etc...`; return prompt; } /** * Parser réponse batch générique avec nettoyage des tags XML */ function parseBatchResponse(response, elements) { const results = {}; // Parser avec regex [TAG] contenu const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); let content = match[2].trim(); // NOUVEAU: Nettoyer les tags XML qui peuvent apparaître dans le contenu content = cleanXMLTagsFromContent(content); parsedItems[tag] = content; } // Mapper aux vrais tags avec | elements.forEach(element => { const cleanTag = element.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) { results[element.tag] = parsedItems[cleanTag]; logSh(`✅ Parsé [${cleanTag}]: "${parsedItems[cleanTag].substring(0, 100)}..."`, 'DEBUG'); } else { // Fallback si parsing échoue ou contenu trop court results[element.tag] = `Contenu professionnel pour ${element.element.name}`; logSh(`⚠️ Fallback [${cleanTag}]: parsing échoué ou contenu invalide`, 'WARNING'); } }); return results; } /** * NOUVELLE FONCTION: Nettoyer les tags XML du contenu généré */ function cleanXMLTagsFromContent(content) { if (!content) return content; // Supprimer les tags XML avec ** content = content.replace(/\*\*[^*]+\*\*/g, ''); // Supprimer les préfixes de titres indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?(voici\s+le\s+topo\s+pour\s+)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/^(Bon,?\s*)?(donc,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); // Supprimer les messages d'excuse content = content.replace(/Oh là là,?\s*je\s*(suis\s*)?(\w+\s*)?désolée?\s*,?\s*mais\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, ''); content = content.replace(/Bon,?\s*passons\s*au\s*suivant.*?(?=\.|$)/gi, ''); content = content.replace(/je\s*ne\s*sais\s*pas\s*quoi\s*vous\s*dire.*?(?=\.|$)/gi, ''); content = content.replace(/encore\s*un\s*point\s*où\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, ''); // Réduire les répétitions excessives d'expressions familières content = content.replace(/(du coup[,\s]+){3,}/gi, 'du coup '); content = content.replace(/(bon[,\s]+){3,}/gi, 'bon '); content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement '); content = content.replace(/(alors[,\s]+){3,}/gi, 'alors '); content = content.replace(/(nickel[,\s]+){2,}/gi, 'nickel '); content = content.replace(/(tip-top[,\s]+){2,}/gi, 'tip-top '); content = content.replace(/(costaud[,\s]+){2,}/gi, 'costaud '); // Nettoyer espaces multiples et retours ligne content = content.replace(/\s{2,}/g, ' '); content = content.replace(/\n{2,}/g, '\n'); content = content.trim(); return content; } // ============= PARSING FUNCTIONS ============= // FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel // FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel // Placeholder pour les fonctions de parsing conservées qui suivent function parseTransitionsBatchResponse(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 content = match[2].trim(); // Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[chunk[index].tag] = content; } else { // Fallback si contenu invalide results[chunk[index].tag] = chunk[index].content; // Garder contenu original } index++; } return results; } function parseStyleBatchResponse(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 content = match[2].trim(); // Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[chunk[index].tag] = content; } else { // Fallback si contenu invalide results[chunk[index].tag] = chunk[index].content; // Garder contenu original } index++; } return results; } // ============= ANALYSIS FUNCTIONS ============= /** * Analyser besoin d'amélioration transitions */ function analyzeTransitionNeed(content) { const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10); // Critères multiples d'analyse const metrics = { repetitiveConnectors: analyzeRepetitiveConnectors(content), abruptTransitions: analyzeAbruptTransitions(sentences), sentenceVariety: analyzeSentenceVariety(sentences), formalityLevel: analyzeFormalityLevel(content), overallLength: content.length }; // Score de besoin (0-1) let needScore = 0; needScore += metrics.repetitiveConnectors * 0.3; needScore += metrics.abruptTransitions * 0.4; needScore += (1 - metrics.sentenceVariety) * 0.2; needScore += metrics.formalityLevel * 0.1; // Seuil ajustable selon longueur const threshold = metrics.overallLength > 300 ? 0.4 : 0.6; logSh(`🔍 Analyse transitions: score=${needScore.toFixed(2)}, seuil=${threshold}`, 'DEBUG'); return needScore > threshold; } 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 previous = sentences[i-1].trim(); const hasConnector = hasTransitionWord(current); const topicContinuity = calculateTopicContinuity(previous, current); // Transition abrupte = pas de connecteur + faible continuité thématique if (!hasConnector && topicContinuity < 0.3) { 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; // Calculer variance des longueurs const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length; const stdDev = Math.sqrt(variance); // Score de variété (0-1) - plus la variance est élevée, plus c'est varié return Math.min(1, stdDev / avgLength); } function analyzeFormalityLevel(content) { const formalIndicators = [ 'il convient de', 'par conséquent', 'néanmoins', 'toutefois', 'de surcroît', 'en définitive', 'il s\'avère que', 'force est de constater' ]; let formalCount = 0; formalIndicators.forEach(indicator => { if (content.toLowerCase().includes(indicator)) formalCount++; }); const sentences = content.split(/[.!?]+/).length; return sentences > 0 ? formalCount / sentences : 0; } function calculateTopicContinuity(sentence1, sentence2) { const stopWords = ['les', 'des', 'une', 'sont', 'avec', 'pour', 'dans', 'cette', 'vous', 'peut', 'tout']; const words1 = extractSignificantWords(sentence1, stopWords); const words2 = extractSignificantWords(sentence2, stopWords); if (words1.length === 0 || words2.length === 0) return 0; const commonWords = words1.filter(word => words2.includes(word)); const semanticSimilarity = commonWords.length / Math.min(words1.length, words2.length); const technicalWords = ['plaque', 'dibond', 'aluminium', 'impression', 'signalétique']; const commonTechnical = commonWords.filter(word => technicalWords.includes(word)); const technicalBonus = commonTechnical.length * 0.2; return Math.min(1, semanticSimilarity + technicalBonus); } function extractSignificantWords(sentence, stopWords) { return sentence.toLowerCase() .match(/\b[a-zàâäéèêëïîôùûüÿç]{4,}\b/g) // Mots 4+ lettres avec accents ?.filter(word => !stopWords.includes(word)) || []; } function hasTransitionWord(sentence) { const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi', 'toutefois', 'néanmoins', 'alors', 'enfin']; return connectors.some(connector => sentence.toLowerCase().includes(connector)); } /** * Instructions de style dynamiques */ function getPersonalityStyleInstructions(personality) { // CORRECTION: Utilisation des VRAIS champs Google Sheets au lieu du hardcodé if (!personality) return "Style professionnel standard"; const instructions = `STYLE ${personality.nom.toUpperCase()} (${personality.style}): - Description: ${personality.description} - Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel, qualité'} - Connecteurs préférés: ${personality.connecteursPref || 'par ailleurs, en effet'} - Mots-clés secteurs: ${personality.motsClesSecteurs || 'technique, qualité'} - Longueur phrases: ${personality.longueurPhrases || 'Moyennes (15-25 mots)'} - Niveau technique: ${personality.niveauTechnique || 'Accessible'} - Style CTA: ${personality.ctaStyle || 'Professionnel'} - Défauts simulés: ${personality.defautsSimules || 'Aucun'} - Erreurs typiques à éviter: ${personality.erreursTypiques || 'Répétitions, généralités'}`; return instructions; } /** * Créer prompt pour élément (fonction de base nécessaire) */ function createPromptForElement(element, csvData) { const personality = csvData.personality; const styleContext = `Rédige dans le style ${personality.style} de ${personality.nom} (${personality.description}).`; switch (element.type) { case 'titre_h1': return `${styleContext} MISSION: Crée un titre H1 accrocheur pour: ${csvData.mc0} Référence: ${csvData.t0} CONSIGNES: 10 mots maximum, direct et impactant, optimisé SEO. RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`; case 'titre_h2': return `${styleContext} MISSION: Crée un titre H2 optimisé SEO pour: ${csvData.mc0} CONSIGNES: Intègre naturellement le mot-clé, 8 mots maximum. RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`; case 'intro': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} Données contextuelles: - MC0: ${csvData.mc0} - T-1: ${csvData.tMinus1} - L-1: ${csvData.lMinus1} RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } return `${styleContext} MISSION: Rédige une introduction de 100 mots pour ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; case 'texte': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } return `${styleContext} MISSION: Rédige un paragraphe de 150 mots sur ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; case 'faq_question': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} CONTEXTE: ${csvData.mc0} - ${csvData.t0} STYLE: Question ${csvData.personality?.style} de ${csvData.personality?.nom} CONSIGNES: - Vraie question que se poserait un client intéressé par ${csvData.mc0} - Commence par "Comment", "Quel", "Pourquoi", "Où", "Quand" ou "Est-ce que" - Maximum 15 mots, pratique et concrète - Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'} RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`; } return `${styleContext} MISSION: Génère une vraie question FAQ client sur ${csvData.mc0}. CONSIGNES: - Question pratique et concrète qu'un client se poserait - Commence par "Comment", "Quel", "Pourquoi", "Combien", "Où" ou "Est-ce que" - Maximum 15 mots, style ${csvData.personality?.style} - Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'} RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`; case 'faq_reponse': if (element.instructions) { return `${styleContext} MISSION: ${element.instructions} CONTEXTE: ${csvData.mc0} - ${csvData.t0} STYLE: Réponse ${csvData.personality?.style} de ${csvData.personality?.nom} CONSIGNES: - Réponse utile et rassurante - 50-80 mots, ton ${csvData.personality?.style} - Vocabulaire: ${csvData.personality?.vocabulairePref} - Connecteurs: ${csvData.personality?.connecteursPref} RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`; } return `${styleContext} MISSION: Réponds à une question client sur ${csvData.mc0}. CONSIGNES: - Réponse utile, claire et rassurante - 50-80 mots, ton ${csvData.personality?.style} de ${csvData.personality?.nom} - Vocabulaire: ${csvData.personality?.vocabulairePref || 'professionnel'} - Connecteurs: ${csvData.personality?.connecteursPref || 'par ailleurs'} RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`; default: return `${styleContext} MISSION: Génère du contenu pertinent pour ${csvData.mc0}. RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`; } } /** * NOUVELLE FONCTION : Extraction batch TOUS les termes techniques */ async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) { const contentEntries = Object.keys(baseContents); const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques. CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTENUS À ANALYSER: ${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} CONTENU: "${baseContents[tag]}"`).join('\n\n')} CONSIGNES: - Identifie UNIQUEMENT les vrais termes techniques métier/industrie - Évite mots génériques (qualité, service, pratique, personnalisé, etc.) - Focus: matériaux, procédés, normes, dimensions, technologies - Si aucun terme technique → "AUCUN" EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique FORMAT RÉPONSE EXACT: [1] dibond, impression UV, 3mm OU AUCUN [2] aluminium, fraisage CNC OU AUCUN [3] AUCUN etc... (${contentEntries.length} lignes total)`; try { const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, { temperature: 0.3, maxTokens: 2000 }, csvData.personality); return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries); } catch (error) { logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`); } } /** * NOUVELLE FONCTION : Enhancement batch TOUS les éléments */ async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) { if (elementsNeedingEnhancement.length === 0) return {}; const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus. CONTEXTE: Article SEO pour site e-commerce de signalétique PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel) SUJET: ${csvData.mc0} - Secteur: Signalétique/impression VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref} CONTENUS + TERMES À AMÉLIORER: ${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} CONTENU ACTUEL: "${item.content}" TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} CONSIGNES STRICTES: - Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom} - GARDE la même longueur, structure et ton - Intègre naturellement les termes techniques listés - NE CHANGE PAS le fond du message ni le style personnel - Utilise un vocabulaire expert mais accessible - ÉVITE les répétitions excessives - RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique} - Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA FORMAT RÉPONSE: [1] Contenu avec amélioration technique selon ${csvData.personality?.nom} [2] Contenu avec amélioration technique selon ${csvData.personality?.nom} etc... (${elementsNeedingEnhancement.length} éléments total)`; try { const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, { temperature: 0.4, maxTokens: 5000 // Plus large pour batch total }, csvData.personality); return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement); } catch (error) { logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR'); throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`); } } /** * Parser réponse extraction termes */ function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) { const results = []; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; const parsedItems = {}; // Parser la réponse while ((match = regex.exec(response)) !== null) { const index = parseInt(match[1]) - 1; // Convertir en 0-indexé const termsText = match[2].trim(); parsedItems[index] = termsText; } // Mapper aux éléments contentEntries.forEach((tag, index) => { const termsText = parsedItems[index] || 'AUCUN'; const hasTerms = !termsText.toUpperCase().includes('AUCUN'); const technicalTerms = hasTerms ? termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) : []; results.push({ tag: tag, content: baseContents[tag], technicalTerms: technicalTerms, needsEnhancement: hasTerms && technicalTerms.length > 0 }); logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG'); }); const enhancementCount = results.filter(r => r.needsEnhancement).length; logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO'); return results; } /** * Parser réponse enhancement technique */ function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) { const results = {}; const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; let match; let index = 0; while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) { let content = match[2].trim(); const element = elementsNeedingEnhancement[index]; // NOUVEAU: Appliquer le nettoyage XML content = cleanXMLTagsFromContent(content); if (content && content.length > 10) { results[element.tag] = content; logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG'); } else { // Fallback si contenu invalide après nettoyage results[element.tag] = element.content; logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING'); } index++; } // Vérifier si on a bien tout parsé if (Object.keys(results).length < elementsNeedingEnhancement.length) { logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING'); // Compléter avec contenu original pour les manquants elementsNeedingEnhancement.forEach(element => { if (!results[element.tag]) { results[element.tag] = element.content; } }); } return results; } // ============= EXPORTS ============= module.exports = { generateWithBatchEnhancement, generateAllContentBase, enhanceAllTechnicalTerms, enhanceAllTransitions, enhanceAllPersonalityStyle, collectAllElements, groupElementsByType, chunkArray, createBatchBasePrompt, parseBatchResponse, cleanXMLTagsFromContent, analyzeTransitionNeed, getPersonalityStyleInstructions, createPromptForElement, sleep, separateFAQPairsAndOthers, generateFAQPairsRestored, createBatchFAQPairsPrompt, parseFAQPairsResponse, cleanFAQInstructions, extractAllTechnicalTermsBatch, enhanceAllElementsTechnicalBatch, parseAllTechnicalTermsResponse, parseTechnicalEnhancementBatchResponse }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/trace-wrap.js │ └────────────────────────────────────────────────────────────────────┘ */ // lib/trace-wrap.js const { tracer } = require('./trace.js'); const traced = (name, fn, attrs) => (...args) => tracer.run(name, () => fn(...args), attrs); module.exports = { traced }; /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/Utils.js │ └────────────────────────────────────────────────────────────────────┘ */ // ======================================== // FICHIER: utils.js - Conversion Node.js // Description: Utilitaires génériques pour le workflow // ======================================== // Import du système de logging (assumant que logSh est disponible globalement) // const { logSh } = require('./logging'); // À décommenter si logSh est dans un module séparé /** * Créer une réponse de succès standardisée * @param {Object} data - Données à retourner * @returns {Object} Réponse formatée pour Express/HTTP */ function createSuccessResponse(data) { return { success: true, data: data, timestamp: new Date().toISOString() }; } /** * Créer une réponse d'erreur standardisée * @param {string|Error} error - Message d'erreur ou objet Error * @returns {Object} Réponse d'erreur formatée */ function createErrorResponse(error) { const errorMessage = error instanceof Error ? error.message : error.toString(); return { success: false, error: errorMessage, timestamp: new Date().toISOString(), stack: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined }; } /** * Middleware Express pour envoyer des réponses standardisées * Usage: res.success(data) ou res.error(error) */ function responseMiddleware(req, res, next) { // Méthode pour réponse de succès res.success = (data, statusCode = 200) => { res.status(statusCode).json(createSuccessResponse(data)); }; // Méthode pour réponse d'erreur res.error = (error, statusCode = 500) => { res.status(statusCode).json(createErrorResponse(error)); }; next(); } /** * HELPER : Nettoyer les instructions FAQ * Remplace les variables et nettoie le HTML * @param {string} instructions - Instructions à nettoyer * @param {Object} csvData - Données CSV pour remplacement variables * @returns {string} Instructions nettoyées */ function cleanFAQInstructions(instructions, csvData) { if (!instructions || !csvData) { return instructions || ''; } let clean = instructions.toString(); try { // Remplacer variables simples clean = clean.replace(/\{\{MC0\}\}/g, csvData.mc0 || ''); clean = clean.replace(/\{\{T0\}\}/g, csvData.t0 || ''); // Variables multiples si nécessaire if (csvData.mcPlus1) { const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`; clean = clean.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue); } } // Variables T+1 et L+1 si disponibles if (csvData.tPlus1) { const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`; clean = clean.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue); } } if (csvData.lPlus1) { const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim()); for (let i = 1; i <= 6; i++) { const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`; clean = clean.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue); } } // Nettoyer HTML clean = clean.replace(/<\/?[^>]+>/g, ''); // Nettoyer espaces en trop clean = clean.replace(/\s+/g, ' ').trim(); } catch (error) { if (typeof logSh === 'function') { logSh(`⚠️ Erreur nettoyage instructions FAQ: ${error.toString()}`, 'WARNING'); } // Retourner au moins la version partiellement nettoyée } return clean; } /** * Utilitaire pour attendre un délai (remplace Utilities.sleep de Google Apps Script) * @param {number} ms - Millisecondes à attendre * @returns {Promise} Promise qui se résout après le délai */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Utilitaire pour encoder en base64 * @param {string} text - Texte à encoder * @returns {string} Texte encodé en base64 */ function base64Encode(text) { return Buffer.from(text, 'utf8').toString('base64'); } /** * Utilitaire pour décoder du base64 * @param {string} base64Text - Texte base64 à décoder * @returns {string} Texte décodé */ function base64Decode(base64Text) { return Buffer.from(base64Text, 'base64').toString('utf8'); } /** * Valider et nettoyer un slug/filename * @param {string} slug - Slug à nettoyer * @returns {string} Slug nettoyé */ function cleanSlug(slug) { if (!slug) return ''; return slug .toString() .toLowerCase() .replace(/[^a-z0-9\-_]/g, '-') // Remplacer caractères spéciaux par - .replace(/-+/g, '-') // Éviter doubles tirets .replace(/^-+|-+$/g, ''); // Enlever tirets début/fin } /** * Compter les mots dans un texte * @param {string} text - Texte à analyser * @returns {number} Nombre de mots */ function countWords(text) { if (!text || typeof text !== 'string') return 0; return text .trim() .replace(/\s+/g, ' ') // Normaliser espaces .split(' ') .filter(word => word.length > 0) .length; } /** * Formater une durée en millisecondes en format lisible * @param {number} ms - Durée en millisecondes * @returns {string} Durée formatée (ex: "2.3s" ou "450ms") */ function formatDuration(ms) { if (ms < 1000) { return `${ms}ms`; } else if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } else { const minutes = Math.floor(ms / 60000); const seconds = ((ms % 60000) / 1000).toFixed(1); return `${minutes}m ${seconds}s`; } } /** * Utilitaire pour retry automatique d'une fonction * @param {Function} fn - Fonction à exécuter avec retry * @param {number} maxRetries - Nombre maximum de tentatives * @param {number} delay - Délai entre tentatives (ms) * @returns {Promise} Résultat de la fonction ou erreur finale */ async function withRetry(fn, maxRetries = 3, delay = 1000) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (typeof logSh === 'function') { logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.toString()}`, 'WARNING'); } if (attempt < maxRetries) { await sleep(delay * attempt); // Exponential backoff } } } throw lastError; } /** * Validation basique d'email * @param {string} email - Email à valider * @returns {boolean} True si email valide */ function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Générer un ID unique simple * @returns {string} ID unique basé sur timestamp + random */ function generateId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Truncate un texte à une longueur donnée * @param {string} text - Texte à tronquer * @param {number} maxLength - Longueur maximale * @param {string} suffix - Suffixe à ajouter si tronqué (défaut: '...') * @returns {string} Texte tronqué */ function truncate(text, maxLength, suffix = '...') { if (!text || text.length <= maxLength) { return text; } return text.substring(0, maxLength - suffix.length) + suffix; } // ============= EXPORTS ============= module.exports = { createSuccessResponse, createErrorResponse, responseMiddleware, cleanFAQInstructions, sleep, base64Encode, base64Decode, cleanSlug, countWords, formatDuration, withRetry, isValidEmail, generateId, truncate };